Skip to content

11 - 支付服务与外部集成 ​

支付服务模拟了与外部支付网关的异步交互,展示了 Watermill 在集成外部系统时的最佳实践。

服务架构 ​

支付服务无需数据库——它只做事件监听 + 处理 + 结果发布。通过 Watermill 将处理结果以事件形式发布回 Kafka,不维护本地状态。

在 ecommerce/cmd/payment-service/main.go:57 注册单个事件处理器:

go
router.AddNoPublisherHandler(
    "inventory_reserved",
    events.TopicInventoryReserved,
    sub,
    eventHandler.HandleInventoryReserved,
)

事件处理:从预留到支付 ​

ecommerce/internal/payment/biz/event_handler.go:23-35 展示了标准的事件消费模式:

go
func (h *PaymentEventHandler) HandleInventoryReserved(msg *message.Message) error {
    var event eventsv1.InventoryReserved
    if err := proto.Unmarshal(msg.Payload, &event); err != nil {
        return err
    }
    h.log.Info("库存已预留,发起支付", zap.String("order_id", event.OrderId))
    h.uc.ProcessPayment(context.Background(), &eventsv1.PaymentRequested{
        OrderId: event.OrderId,
        Amount:  100.0,
    })
    return nil
}

注意:HandleInventoryReserved 返回 nil 而真正进入支付处理的结果通过事件发布异步通知,这是一种常见的模式——消费方快速 ACK,处理结果异步返回。

支付处理与模拟成功率 ​

ecommerce/internal/payment/biz/usecase.go:24-44 包含核心处理逻辑:

go
func (uc *PaymentUseCase) ProcessPayment(ctx context.Context, event *eventsv1.PaymentRequested) {
    paymentID := watermill.NewUUID()

    if rand.Intn(100) < 90 {  // 90% 成功率
        completed := &eventsv1.PaymentCompleted{
            OrderId: event.OrderId, PaymentId: paymentID,
        }
        msg := message.NewMessage(watermill.NewUUID(), events.MustMarshal(completed))
        uc.pub.Publish(events.TopicPaymentCompleted, msg)
        uc.log.Info("支付成功", zap.String("order_id", event.OrderId))
    } else {
        failed := &eventsv1.PaymentFailed{
            OrderId: event.OrderId, Reason: "余额不足",
        }
        msg := message.NewMessage(watermill.NewUUID(), events.MustMarshal(failed))
        uc.pub.Publish(events.TopicPaymentFailed, msg)
        uc.log.Warn("支付失败", zap.String("order_id", event.OrderId))
    }
}

这段代码的精妙设计:

  • 异步处理:ProcessPayment 在 Handler goroutine 中同步执行(非后台 goroutine),但 Handler 返回 nil 表示消费成功,不阻塞后续消息
  • 随机失败:模拟真实场景的不确定性,10% 概率触发 Saga 补偿流程
  • 事件发布模式:无论成功或失败,结果都通过事件发布,而非 return error 返回给 Router

真实支付对接建议 ​

将 rand.Intn 替换为真实支付网关调用时的要点:

考虑项建议
幂等性支付接口传入 paymentID 作为幂等键,防止重复扣款
超时结合 Timeout 中间件设置合理的超时时间(如 10s)
回调支付网关异步回调时,将回调结果转换为 Watermill 事件发布
对账定时任务对比支付网关账单和本地事件日志
事务支付成功后的数据更新 + 事件发布使用 Outbox 模式保证一致性

超时处理 ​

当前简化版本未处理支付超时场景。生产环境可通过以下方式增强:

  1. Timeout 中间件:限制 Handler 执行时长,超时返回 error
  2. 定时检查:启动后台 goroutine 定时扫描超时未完成的支付,自动标记失败
  3. 支付网关回调 + Kafka 延迟消息:结合外部回调 + 内部延迟检查做双重保障

在 ecommerce/cmd/payment-service/main.go:55 中,Retry 中间件已配置为支付失败时自动重试,Cover 瞬时网络故障场景:

go
router.AddMiddleware(middleware.Retry{
    MaxRetries: 3, InitialInterval: time.Second, Logger: watermillLogger,
}.Middleware)