资讯详情

GoWind Shop 拆解:订单超时关单——一个事务里的四联动

📅 2026/10/25 1:56:54 | 华诺云谱 👁 阅读
GoWind Shop 拆解:订单超时关单——一个事务里的四联动
GoWind Shop 拆解订单超时关单——一个事务里的四联动本文回答一个问题用户下单后 30 分钟没付款系统要收回什么、按什么顺序收、怎么防重防并发超时关单这个电商标配功能值得写 450 行吗GoWind Shop 的未支付订单 30 分钟自动过期ExpireOrderByTimeout约 150 行核心逻辑是全仓库把事务边界设计体现得最完整的一条链路释放库存、状态推进、孤儿支付清理、优惠券返还四件事在一个数据库事务里完成外层有延迟任务的投递去重与幂等闸门。拆它之前先看它的对照面——下单事务因为超时收回的每一样东西都是下单时锁定的。一、对照面下单事务锁定了什么Create是一个 ent 事务里的八步460 余行与超时相关的有四步行锁防超卖遍历购物车明细每张 SKU 卡ForUpdate行锁 → 校验stock_qty qty→ 写 OrderItem →锁内扣库存。下单减模式非支付减超卖防护就是这个行锁加事务回滚SKU 快照OrderItem 落sku_id/quantity/currency/unit_price快照 JSON——商品后续改价不影响已下单据优惠券事务内核销redeemCouponInTx两张 ForUpdate 行锁用户持券行 券模板行内完成状态/币种/领取窗口/全局核销上限/每人限用锁内 Count防领→用→退→再领绕限五重校验computeDiscount算抵扣券翻 USED 并回写redeemed_order_id金额五件套total_amount折后应付支付与退款取此值/original_amount折前审计/discount_amount/shipping_fee/tax_amount全部独立成列——对账的每一分钱都有独立来源。订单状态被强制置PENDING_PAYMENT忽略客户端传入提交事务后才投递超时任务——这个顺序保证任务处理时数据必然已提交与 ERP 篇的 defer LIFO 是同一个道理的另一种写法。所以超时关单要收回的清单就是这四步锁定的资源库存占用、USED 状态的券、PENDING_PAYMENT 状态本身以及一个悬空的支付窗口。二、投递侧延迟任务的防重与去重超时任务走 asynq 延迟投递投递侧有两道防重taskId:task.CreateOrderTimeoutTaskID(orderId)// order_timeout:{orderId}asynq.TaskID(taskId),// 同订单重复投递会被 asynq 按 TaskID 去重asynq.Unique(OrderPendingTTL),// TTL 内同 payload 只投一次TaskID是全局幂等键——手动重发、重试、多入口触发同一家订单的超时任务asynq 按 ID 去重只留一份。Unique(TTL)按 payload 再兜一层。这两道是投递侧防重同一个超时事件不会变成两个任务。但它们防不住任务执行到一半进程崩溃后重投——那是执行侧幂等的职责下一节。一个值得记录的工程决策为什么用延迟任务而不是定时扫描扫出所有到期未付订单延迟任务的语义是下单时刻就预约好了关单时刻到期即触发、无扫描周期延迟代价是 Redis 里躺着大量延时任务。GoWind 系两个项目恰好各选了一边ERP 的发布兜底用的就是每分钟扫描自愈网——任务量可控且时刻精确的选延迟任务任务量大且容忍分钟级延迟的选扫描两条路线没有对错但必须知道自己选的是哪条、失败模式是什么。三、执行侧一个事务里的四联动ExpireOrderByTimeout的第一步是幂等闸门只处理PENDING_PAYMENT状态的订单——任务重投、订单已被人工取消、或并发已被推进都在这道闸门处静默返回。之后的四步在一个 ent 事务内释放库存AddStockQty回补下单时锁定的量下单减模式的归还路径状态推进批量 Update 带status IN (PENDING_PAYMENT)前置条件——0 行受影响即并发已被推进静默跳过与执行侧幂等闸门呼应双保险孤儿支付清理查该订单的支付流水把已 SUCCEEDED 的翻 FAILED。这是四步里最反直觉的一步——支付成功了订单却超时了用户付款瞬间网络抖动、回调延迟这种悬空状态必须主动消除否则用户付了钱却看到订单已取消且永远等不到发货优惠券返还restoreCouponInTx按下单时回写的redeemed_order_id反查券行翻回 UNUSED、模板redeemed_count--——券的核销与返还是精确的镜像操作。第三步的合法性有一个专门的状态机口子支付流水的常规迁移只允许PENDING→SUCCEEDED/FAILED、SUCCEEDED→REFUNDEDSUCCEEDED→FAILED这条支付成功翻失败是 SystemViewer 系统上下文专属的特权路径运行时判断 viewer 来源注释明说是孤儿清理专用——为系统补偿开的口子必须收窄到特定的调用身份否则状态机的承诺会被旁路稀释。退款链路还有一个对称的补偿纪律值得记录退款成功后的联动订单翻 REFUNDED、回补库存若失败会把退款记录补偿回滚到 PENDING重试——注释坦承这是有意绕过退款状态机的特权操作。补偿动作污染状态机是分布式一致性的常见暗坑这里的处理是承认它、注释它、收窄它。四、状态机expected_status 前置条件的强制化贯穿下单、支付、关单、发货、退款全链路的并发纪律是任何状态变更必须携带expected_status前置条件。订单状态机本身很小PENDING_PAYMENT→PAID/CANCELLEDPAID→FULFILLED/CLOSEDFULFILLED→CLOSED终态吸收但所有变更都走条件更新 前置状态校验违反迁移表 → 拒绝前置状态不符并发已被推进→ 0 行受影响调用方按已被处理静默跳过。管理端 BFF 甚至为每个操作补齐了 core 侧期望的前置集合发货带[PAID]、关闭带[PAID, FULFILLED]——BFF 与 core 的状态机衔接靠显式的期望值传递而不是靠 core 隐式宽容。这套纪律让四联动里的每一步条件更新都有统一的失败语义0 行 时机已过静默退出。五、结语超时关单的复杂度清单其实很短投递去重TaskID Unique、执行幂等状态闸门 前置条件、四联动同事务库存/状态/支付/券一起收、特权口子收窄SystemViewer。四样东西没有一样新鲜新鲜的是它们被组织成了一个没有缝隙的闭环——每一层都假设上一层会失效且失效后的行为是定义过的。三条可迁移的经验超时收回的清单 下单时锁定的资源清单——写下单事务时就要回答这些占用怎么归还四联动不是超时功能的实现细节是下单设计的义务延迟任务与定时扫描是两条各有失败模式的路线——选了延迟任务就要处理任务丢失的兜底语义本项目的答案TaskID 去重 状态闸门让重投无害选了扫描就要处理扫描周期的精度与重复触发补偿性的状态回退要开显式的特权口子——用调用身份收窄 注释声明别让状态机的常规迁移表默默包含系统专用的边。仓库下单与超时在backend/app/core/service/internal/service/order_service.go任务定义backend/pkg/task/order_timeout.goGitHubgithub.com/tx7do/go-wind-shopGiteegitee.com/tx7do/go-wind-shop
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑