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 模式保证一致性 |
超时处理
当前简化版本未处理支付超时场景。生产环境可通过以下方式增强:
- Timeout 中间件:限制 Handler 执行时长,超时返回 error
- 定时检查:启动后台 goroutine 定时扫描超时未完成的支付,自动标记失败
- 支付网关回调 + Kafka 延迟消息:结合外部回调 + 内部延迟检查做双重保障
在 ecommerce/cmd/payment-service/main.go:55 中,Retry 中间件已配置为支付失败时自动重试,Cover 瞬时网络故障场景:
go
router.AddMiddleware(middleware.Retry{
MaxRetries: 3, InitialInterval: time.Second, Logger: watermillLogger,
}.Middleware)