
## 摘要订单二开里最容易被低估的不是“如何把单建出来”而是“单没付成之后怎么收回来”。很多系统一旦只处理了订单取消但没同步回收库存、优惠券、积分、状态记录和缓存就会留下大量脏数据库存看着少了券也被占了订单状态却还停在待支付客服和用户两边都解释不清。CRMEB Pro 的订单收尾不是一个按钮而是一整套链路创建订单后会注册未支付提醒和超时取消队列用户手动取消时会走积分和优惠券回退、库存回退和订单删除退款时还要根据支付方式、订单类型和明细快照把售后、库存、佣金和资金再处理一遍。真正稳的做法不是“取消订单”而是“取消订单时把相关资源一并回收”。本文基于 CRMEB Pro 当前项目真实实现拆开未支付关单、手动取消、自动取消、退款回收和状态记录看看一笔订单在收尾阶段到底经历了什么。本文涉及的真实目录textapp/controller/api/v1/order/StoreOrder.phpapp/services/order/StoreOrderServices.phpapp/services/order/StoreOrderRefundServices.phpapp/services/order/StoreOrderStatusServices.phpapp/services/order/StoreOrderCartInfoServices.phpapp/jobs/order/UnpaidOrderJob.phpapp/jobs/order/UnpaidOrderCancelJob.phpapp/listener/order/Create.phpapp/listener/order/Pay.phpapp/listener/order/Refund.phpapp/jobs/order/OrderStatusJob.php## 一、订单一创建就已经埋好了“自动关单”的钩子CRMEB Pro 在订单创建事件里不只是写入订单还会顺手把后续处理都安排好。入口在textapp/listener/order/Create.php创建成功后会做几件事phpOrderCreateAfterJob::dispatchDo(updateUser, [$orderInfo, $group, $userInfo]);OrderCreateAfterJob::dispatchDo(delCart, [$group]);OrderCreateAfterJob::dispatchDo(delOrderCache, [$uid, $orderInfo[unique]], 120);OrderStatusJob::dispatch([$orderId, create, [change_message 订单生成, ...]]);最关键的是最后这段延迟任务php// 未支付10分钟后发送短信UnpaidOrderJob::dispatchSece(600, sendNotice, [$orderId]);// 未支付根据系统设置事件取消订单$secs $storeOrderServices-getOrderCancelTime($type);UnpaidOrderJob::dispatchSece((int)($secs * 3600), cancelOrder, [$orderId]);这意味着订单创建时系统已经默认把“提醒付款”和“超时关单”排进日程了。二开时如果你新增一种订单类型或者给某些活动单独配置关单时长就要从这条入口往下接不然只会出现“订单生成了但没人管它什么时候结束”。## 二、未支付订单的处理不是简单删单未支付关单的入口在textapp/jobs/order/UnpaidOrderJob.php它做两件事phpsendNotice($id)cancelOrder($orderId)sendNotice() 只是提醒用户付款真正收尾的是 cancelOrder()phpif ($orderInfo-paid) {return true;}if ($orderInfo-is_del) {return true;}if ($orderInfo-pay_type offline) {return true;}$services-cancelOrder((int)$orderId, 0, 订单未支付已超过系统预设时间);这里能看出一个很实在的边界text线下支付订单不走同一套自动取消逻辑。因为线下单往往需要人工确认不能拿普通在线支付单的超时策略直接套。二开时如果你给某些渠道单独加支付方式也要同步确认它是否应该进入自动关单队列。## 三、用户手动取消订单真正要回退的是一串资源取消订单的核心方法在phpapp/services/order/StoreOrderServices.php方法签名是phppublic function cancelOrder(int $id, int $uid 0, string $mark 用户取消订单)它不是直接把订单状态改掉而是先在事务里做三个动作php// 回退积分和优惠卷$res $refundServices-integralAndCouponBack($order);// 回退库存和销量$res $res $refundServices-regressionStock($order);// 修改订单状态$res $res $this-dao-update($order[id], [is_del 1, mark $mark]);这三步是绑定在一起的原因很直接text1. 不回退优惠券用户券池会少一张。2. 不回退库存商品会被白白占住。3. 不写订单状态前台、后台、统计都看不懂这单现在是什么状态。所以取消订单不是“删一条记录”而是“把下单时占用的资源恢复掉”。这就是为什么这类接口必须走事务。## 四、库存回退不是随便加回去而是按订单商品快照回收退款和取消都可能涉及库存回退但回退的依据不是商品当前信息而是订单商品快照。订单商品快照在textapp/services/order/StoreOrderCartInfoServices.php取消售后或者拒绝退款时也会把 refund_num 和订单商品快照重新整理php$cartInfos $storeOrderCartInfoServices-getColumn([[oid, , $oid], [cart_id, in, $cart_ids]], cart_id,refund_num, cart_id);然后按快照里的数量回写phpif ($cart[cart_num] $cart_refund_num) {$refund_num 0;} else {$refund_num bcsub((string)$cart_refund_num, (string)$cart[cart_num], 0);}$storeOrderCartInfoServices-update([oid $oid, cart_id $cart[id]], [refund_num $refund_num]);这说明一个核心原则text库存回退和售后处理必须基于订单快照而不是当前商品主数据。因为商品主数据可能已经变了活动可能已经结束SKU 甚至可能被删掉。真正能证明那一单买了什么、退了什么的只有订单快照。## 五、自动取消和手动取消走的是同一条回收思路系统除了单个订单的延迟任务还提供批量扫描未支付订单的入口phppublic function runOrderUnpaidCancel(int $page 0, int $limit 0)它会根据系统配置判断不同订单类型的超时时间php$secsArr $this-getOrderCancelTime();if (($order[add_time] bcmul($secs, 3600, 0)) time()) {$this-cancelOrder((int)$order[id], 0, 订单未支付已超过系统预设时间);}配置来源在textorder_cancel_timeorder_activity_timeorder_bargain_timeorder_seckill_timeorder_pink_timerebate_points_orders_time所以你如果想给某种订单单独缩短关单时间不需要重写整个取消逻辑只要把对应配置和类型映射弄清楚就行。真正的收尾动作仍然复用 cancelOrder()。## 六、退款回收比取消更复杂因为它还要管钱、券、库存和佣金退款相关逻辑在textapp/services/order/StoreOrderRefundServices.php它比取消订单更重因为它可能涉及text退款金额余额退款原路退款积分回退优惠券回退拼团状态回退佣金回退库存回退比如同意退款时核心动作是php// 回退积分和优惠卷if (!$this-integralAndCouponBack($order)) {throw new ValidateException(回退积分和优惠卷失败);}// 退拼团if ($order[pid] 0 $order[type] 3) {$pinkServices-setRefundPink($order);}// 退佣金if (!$userBrokerageServices-orderRefundBrokerageBack($order)) {throw new ValidateException(回退佣金失败);}// 回退库存if ($order[status] 0) {$this-regressionStock($order, 1, $refundOrder[order_id]);}这说明退款不是一个单独支付动作而是订单生命周期的逆操作。只要你在下单时占用了资源退款时就要尽可能把资源按原路径归还。## 七、订单状态记录也是一条独立链路别省订单状态变更不是只改主表字段系统还会写状态流水。状态记录服务在textapp/services/order/StoreOrderStatusServices.php它会把状态、操作人、变更类型、时间都存下来php$statusData [oid $orderId,change_time time(),change_type $changeType,change_message $data[change_message] ?? ,change_manager_type $changeManagerType,change_manager_id $changeManagerId];这个状态流水特别重要因为很多问题不是“订单没变”而是“到底谁在什么时候把它变成这个状态的”说不清。二开时如果你加了新的自动关单策略、外部同步策略或者人工审核策略也要同步打状态记录。## 八、二开时最容易漏的三个点如果你要改订单收尾链路优先检查这三处text1. 有没有把未支付超时任务挂上去。2. 取消订单时有没有回退积分、优惠券和库存。3. 退款时有没有同步处理快照、佣金和状态流水。再往深一点看最好再补一层text4. 自动取消和手动取消是否共用同一套回收逻辑。5. 订单类型不同超时时间是否不同。6. 退款后历史订单详情是否还能按快照正确展示。## 注意事项1. 订单取消、退款、库存、优惠券、积分都属于高风险业务改动前先说明影响范围。2. 不要只改主表状态忘了回退关联资源。3. 自动关单和手动取消最好复用同一套回收逻辑避免出现两套规则。4. 退款时要优先按订单快照处理不要直接读商品当前数据。5. 状态流水不要省它是后面排查问题最有用的证据链。## 标签建议#CRMEBPro #订单取消 #超时关单 #退款回收 #库存回退 #优惠券回退 #积分回退 #源码解析 #订单状态 #二开实战