DDD-033:案例:支付系统集成

📅 2026/6/25 14:20:32
DDD-033:案例:支付系统集成
DDD-033:案例:支付系统集成本章导读支付系统是电商平台的核心基础设施,涉及支付渠道对接、支付状态管理、退款处理等复杂业务。本章通过支付系统案例,展示支付聚合设计、防腐层(ACL)实现、第三方支付对接、分布式事务处理等实战内容。学习目标掌握支付聚合的设计与实现学会使用防腐层隔离第三方系统理解支付事务补偿机制前置知识DDD 聚合设计基础防腐层模式分布式事务基础阅读时长约 55-65 分钟【案例背景】支付系统一、业务需求分析1.1 核心业务场景支付系统业务场景: ┌────────────────────────────────────────────────────────────────┐ │ 支付系统 │ ├────────────────────────────────────────────────────────────────┤ │ │ │ 1. 支付渠道 │ │ ───────────── │ │ - 支付宝 │ │ - 微信支付 │ │ - 银行卡支付 │ │ - 余额支付 │ │ │ │ 2. 支付流程 │ │ ───────────── │ │ - 创建支付单 │ │ - 调用第三方支付 │ │ - 接收支付回调 │ │ - 支付状态同步 │ │ │ │ 3. 退款处理 │ │ ───────────── │ │ - 申请退款 │ │ - 退款审核 │ │ - 调用第三方退款 │ │ - 退款状态同步 │ │ │ │ 4. 对账 │ │ ───────────── │ │ - 每日对账 │ │ - 差异处理 │ │ │ └────────────────────────────────────────────────────────────────┘1.2 核心业务规则规则编号规则描述R1同一订单不能重复支付R2支付金额必须等于订单金额R3只有支付成功的订单才能退款R4退款金额不能超过已支付金额R5支付回调必须验签二、领域建模2.1 聚合设计支付上下文聚合设计: ┌────────────────────────────────────────────────────────────────┐ │ Payment Context │ ├────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Payment 聚合 │ │ │ │ │ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ │ │ Payment │───│ Refund │ │ │ │ │ │ 聚合根 │ │ 实体 │ │ │ │ │ └────────────────┘ └────────────────┘ │ │ │ │ │ │ │ │ 职责: │ │ │ │ - 管理支付生命周期 │ │ │ │ - 支付状态机 │ │ │ │ - 退款管理 │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 外部依赖(通过防腐层隔离): │ │ - AlipayGateway:支付宝网关 │ │ - WechatPayGateway:微信支付网关 │ │ - BankGateway:银行网关 │ │ │ └────────────────────────────────────────────────────────────────┘2.2 Payment 聚合详细设计┌────────────────────────────────────────────────────────────────┐ │ Payment │ │ (Aggregate Root) │ ├────────────────────────────────────────────────────────────────┤ │ - id: PaymentId │ │ - orderId: OrderId // 关联订单 │ │ - amount: Money // 支付金额 │ │ - status: PaymentStatus // 支付状态 │ │ - channel: PaymentChannel // 支付渠道 │ │ - channelTransactionId: String // 第三方交易号 │ │ - paidAt: Instant // 支付时间 │ │ - expiredAt: Instant // 过期时间 │ │ - refunds: ListRefund // 退款记录 │ ├────────────────────────────────────────────────────────────────┤ │ + create(orderId, amount, channel): Payment │ │ + pay(): void │ │ + markPaid(transactionId, paidAt): void │ │ + markFailed(reason): void │ │ + cancel(reason): void │ │ + refund(amount, reason): Refund │ │ + canRefund(): boolean │ │ + getRefundableAmount(): Money │ └────────────────────────────────────────────────────────────────┘ │ │ 包含 ▼ ┌────────────────────────────────────────────────────────────────┐ │ Refund │ │ (Entity) │ ├────────────────────────────────────────────────────────────────┤ │ - id: RefundId │ │ - amount: Money // 退款金额 │ │ - status: RefundStatus // 退款状态 │ │ - reason: String // 退款原因 │ │ - channelRefundId: String // 第三方退款号 │ │ - refundedAt: Instant // 退款时间 │ ├────────────────────────────────────────────────────────────────┤ │ + markSuccess(channelRefundId, refundedAt): void │ │ + markFailed(reason): void │ └────────────────────────────────────────────────────────────────┘三、代码实现3.1 Payment 聚合根// ✅ 支付聚合根publicclassPaymentextendsAggregateRootPaymentId{privateOrderIdorderId;privateMoneyamount;privatePaymentStatusstatus;privatePaymentChannelchannel;// 第三方交易信息privateStringchannelTransactionId;privateStringchannelNotifyData;// 时间信息privateInstantcreatedAt;privateInstantpaidAt;privateInstantexpiredAt;privateInstantcancelledAt;// 退款记录privateListRefundrefunds;// ========== 工厂方法 ==========publicstaticPaymentcreate(OrderIdorderId,Moneyamount,PaymentChannelchannel,DurationexpireDuration){Paymentpayment=newPayment();payment.id=PaymentId.generate();payment.orderId=orderId;payment.amount=amount;payment.channel=channel;payment.status=PaymentStatus.CREATED;payment.createdAt=Instant.now();payment.expiredAt=Instant.now().plus(expireDuration);payment.refunds=newArrayList();returnpayment;}// ========== 业务方法 ==========/** * 标记支付中 */publicvoidmarkPaying(){if(status!=PaymentStatus.CREATED){thrownewInvalidPaymentStateException(String.format("只有待支付的订单才能发起支付,当前状态:%s",status));}if(isExpired()){thrownewPaymentExpiredException("支付单已过期");}this.status=PaymentStatus.PAYING;}/** * 标记支付成功 */publicvoidmarkPaid(StringchannelTransactionId,StringnotifyData,InstantpaidAt){if(status!=PaymentStatus.PAYINGstatus!=PaymentStatus.CREATED){thrownewInvalidPaymentStateException(String.format("支付状态不正确,当前状态:%s",status));}this.status=PaymentStatus.PAID;this.channelTransactionId=channelTransactionId;this.channelNotifyData=notifyData;this.paidAt=paidAt;registerEvent(newPaymentPaidEvent(this.id,this.orderId,this.amount,this.channel,channelTransactionId,paidAt));}/** * 标记支付失败 */publicvoidmarkFailed(Stringreason){if(status!=PaymentStatus.PAYINGstatus!=PaymentStatus.CREATED){thrownewInvalidPaymentStateException("状态不正确");}this.status=PaymentStatus.FAILED;registerEvent(newPaymentFailedEvent(this.id,this.orderId,reason));}/** * 取消支付 */publicvoidcancel(Stringreason){if(status!=PaymentStatus.CREATED){thrownewInvalidPaymentStateException(String.format("只有待支付的订单才能取消,当前状态:%s",status));}this.status=PaymentStatus.CANCELLED;this.cancelledAt=Instant.now();registerEvent(newPaymentCancelledEvent(this.id,this.orderId,reason));}/** * 申请退款 */publicRefundrefund(MoneyrefundAmount,Stringreason){if(status!=PaymentStatus.PAID){thrownewInvalidPaymentStateException("只有已支付的订单才能退款");}MoneytotalRefunded=getTotalRefundedAmount();MoneyrefundableAmount=amount.subtract(totalRefunded);if(refundAmount.isGreaterThan(refundableAmount)){thrownewRefundAmountExceededException(String.format("退款金额超过可退金额,可退:%s,申请:%s",refundableAmount,refundAmount));}Refundrefund=Refund.create(this.id,refundAmount,reason);refunds.add(refund);// 如果是全额退款,更新支付状态if(refundAmount.add(totalRefunded).equals(amount)){this.status=PaymentStatus.REFUNDED;}else{this.status=PaymentStatus.PARTIALLY_REFUNDED;}registerEvent(newPaymentRefundCreatedEvent(this.id,this.orderId,refund.getId(),refundAmount));returnrefund;}/** * 标记退款成功 */publicvoidmarkRefundSuccess(StringrefundId,StringchannelRefundId,InstantrefundedAt){Refundrefund=findRefund(refundId);refund.markSuccess(channelRefundId,refundedAt);registerEvent(newPaymentRefundedEvent(this.id,this.orderId,refund.getId(),refund.getAmount(),channelRefundId));}/** * 标记退款失败 */publicvoidmarkRefundFailed(StringrefundId,Stringreason){Refundrefund=findRefund(refundId);refund.markFailed(reason)