iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Software Development

事件驅動電力交易平台:Spring Boot 實戰系列 第 4

Day 4|事件驅動實作:Wallet Service 接收訂單事件並執行資產鎖定

  • 分享至 

  • xImage
  •  

在上一篇中,我介紹了如何在 Order Service 發送一筆 OrderCreateEvent 到 RabbitMQ。本篇要延續這條事件流,帶你看看我如何在 Wallet Service 中接收這筆事件,並進行餘額驗證與資產鎖定。這是事件驅動架構中的第二步:訊息消費與處理。

消費端設定:監聽事件佇列
在 Wallet Service 中,我定義了一個 Listener 類別來處理來自 RabbitMQ 的訂單建立事件。這個類別使用 @Component 讓 Spring 自動註冊為 Bean,也透過 @RabbitListener 註解來接收指定佇列的事件:

@Slf4j
@Component
public class CreateOrderListener {

    @Autowired
    private WalletRepository walletRepository;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RabbitListener(queues = ORDER_CREATE_QUEUE)
    public void onOrderCreate(OrderCreateEvent event) {
        if (!isWalletEnough(event)) {
            log.warn("訂單金額超過可用餘額: " + event.getUserId());
            throw new ReturnException("訂單金額超過可用餘額: " + event.getUserId());
        }

        if (!isWalletEnoughForSell(event)) {
            log.warn("訂單可用電量不足: " + event.getUserId());
            throw new ReturnException("訂單可用電量不足: " + event.getUserId());
        }

        // 驗證通過後,進行資產鎖定
        WalletEntity wallet = walletRepository.findByUserId(event.getUserId());

        if ("BUY".equals(event.getOrderType())) {
            int cost = event.getPrice() * event.getAmount();
            wallet.setAvailableCurrency(wallet.getAvailableCurrency() - cost);
            wallet.setLockedCurrency(wallet.getLockedCurrency() + cost);
        } else if ("SELL".equals(event.getOrderType())) {
            int quantity = event.getAmount();
            wallet.setAvailableAmount(wallet.getAvailableAmount() - quantity);
            wallet.setLockedAmount(wallet.getLockedAmount() + quantity);
        }

        walletRepository.save(wallet);
        log.info(" 資產鎖定完成,用戶: {}", event.getUserId());

        // 發送已鎖定資產的事件給 MatchEngine 等後續處理
        OrderCreatedEvent orderCreatedEvent = OrderCreatedEvent.builder()
                .orderId(event.getOrderId())
                .userId(event.getUserId())
                .price(event.getPrice())
                .quantity(event.getAmount())
                .type(event.getOrderType())
                .createdAt(event.getCreatedAt())
                .build();

        rabbitTemplate.convertAndSend(ORDER_EXCHANGE, ORDER_CREATED_KEY, orderCreatedEvent);
    }

    private boolean isWalletEnough(OrderCreateEvent event) {
        WalletEntity wallet = walletRepository.findByUserId(event.getUserId());
        if (wallet == null) {
            log.warn("找不到使用者錢包: " + event.getUserId());
            return false;
        }
        return !"BUY".equals(event.getOrderType()) ||
               event.getAmount() * event.getPrice() <= wallet.getAvailableCurrency();
    }

    private boolean isWalletEnoughForSell(OrderCreateEvent event) {
        WalletEntity wallet = walletRepository.findByUserId(event.getUserId());
        if (wallet == null) {
            log.warn("找不到使用者錢包: " + event.getUserId());
            return false;
        }
        return !"SELL".equals(event.getOrderType()) ||
               event.getAmount() <= wallet.getAvailableAmount();
    }
}

資產鎖定邏輯說明
當收到 OrderCreateEvent 並通過驗證後,我會立即從用戶的錢包中扣除對應的金額或電量,並將這些資產移至鎖定欄位,以避免後續重複下單或餘額不足的問題。
例如:

  • 買單:扣除 availableCurrency、增加 lockedCurrency
  • 賣單:扣除 availableAmount、增加 lockedAmount
    這種「預先凍結資產」的做法是事件驅動設計中非常常見的防禦機制,也讓後續媒合或撤單時可以正確還原狀態。

功能流程總結

  • 訂單驗證:檢查用戶是否有足夠貨幣或電量
  • 資產鎖定:扣除可用資產,增加鎖定資產欄位
  • 發送後續事件:發送 OrderCreatedEvent 給 MatchEngine
  • 技術實作:使用 @RabbitListener, @Component, @Autowired 完成事件接收與處理

下一步預告
下一篇我將介紹如何透過 Spring Cloud Contract 撰寫測試,模擬事件的進入與驗證 Wallet Service 的反應是否正確,實現事件驅動架構中「契約優先」的測試策略,讓微服務之間的溝通更安全、協作更穩固。


上一篇
Day 3|事件驅動實作:從 Order Service 發送 RabbitMQ 訂單事件
下一篇
Day 5|為什麼選擇事件驅動 Wallet 核定,而不是 API 呼叫?以及我如何追蹤訂單狀態
系列文
事件驅動電力交易平台:Spring Boot 實戰9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言