Repository navigation
feat(example): add the payment context and close unpaid orders in ddk-mall - #80
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
ddk-mallcould place an order and reserve stock, but the order then stayed inPENDING_PAYMENTforever: nothing could pay it, and nothing closed it if nobody did. This is step 4 of the reference application: a payment context and a job that closes unpaid orders. With it the whole business flow runs end to end.Changes
Payment context
A third bounded context,
com.example.mall.payment, with the same four layers as the other two. It never calls the order context. The amount and the payment deadline arrive in a neworder.awaiting-paymentmessage, so a payment is created without looking the order up.sequenceDiagram participant C as Customer participant O as Order participant I as Inventory participant P as Payment O-->>P: order.awaiting-payment (amount, deadline) P->>P: create payment (PENDING) C->>P: POST /payments/orders/{id}/pay P->>P: charge the channel, payment PAID P-->>O: payment.completed O->>O: PAID O-->>I: order.paid I->>I: deduct the reserved stock Note over O: customer cancels, or the job finds the deadline passed O-->>I: order.cancelled: release the reservation O-->>P: order.cancelled: close the payment, refund if money was takenPaymentChannelhaschargeandrefund;SimulatedPaymentChannelapproves anything up tomall.payment.simulated.decline-aboveand declines the rest. Both operations use the payment id as the merchant reference, the way real channels deduplicate, so a retry never charges twice.The order decides
A payment and a cancellation can cross: the customer pays while the order is being cancelled or closed. The order's status settles which one wins, and the other contexts follow it.
This is the same self-correcting approach
confirmStockalready uses for a reservation that arrives after a cancellation. That method now checks forCANCELLEDexplicitly; with the newPAIDstatus, "not open" no longer means "cancelled".Closing unpaid orders
mall.order.payment-timeout, 30 minutes by default), stored in a newexpires_atcolumn (migrationV4).CloseExpiredOrdersJoblives in the adapter layer, runs everymall.order.close-intervaland carries@SchedulerLock.ArchitectureTestnow also enforcesSCHEDULED_JOBS_MUST_BE_LOCKED.LocalJobLocksupplies a lock provider that always grants. It is single-instance only, like the in-process stock lock.Layout
背景
ddk-mall之前可以下单并预占库存,但订单会一直停在PENDING_PAYMENT:没有办法支付,也没有人在无人支付时把它关掉。这是参考应用的第 4 步:支付上下文,以及关闭未支付订单的定时任务。完成后整条业务链路可以从头走到尾。改动
支付上下文
第三个限界上下文
com.example.mall.payment,和另外两个一样是四层结构。它不调用订单上下文:应付金额和支付期限随新增的order.awaiting-payment消息带过来,建立支付单时不需要回头查订单。交互过程见上面的时序图。PaymentChannel有charge和refund两个操作;SimulatedPaymentChannel对不超过mall.payment.simulated.decline-above的金额都放行,超过的拒绝。两个操作都以支付单标识作为商户单号,和真实渠道的去重方式一致,重试不会多扣。订单说了算
付款和取消可能交叉:顾客付款的同时订单被取消或超时关闭。由订单的状态决定哪一个生效,另外两个上下文跟随它(逻辑见上面的伪代码)。订单已经取消时收到「支付已完成」,会再发一次「已取消」,支付上下文据此退款。
这和
confirmStock处理「取消之后才到的预占」是同一个办法。那个方法现在明确判断CANCELLED:新增PAID状态之后,「不在进行中」不再等于「已取消」。关闭未支付的订单
mall.order.payment-timeout,默认 30 分钟),存在新增的expires_at列里(迁移脚本V4)。CloseExpiredOrdersJob放在适配层,每隔mall.order.close-interval执行一次,带@SchedulerLock。ArchitectureTest新增检查SCHEDULED_JOBS_MUST_BE_LOCKED。LocalJobLock提供一个总是放行的锁实现。它只适合单实例,和进程内的库存锁一样。结构
包结构的变化见上面的 diff。