Skip to content

概述

租赁订单"申请→审核"二段式取消:用户已支付的租赁订单取消时不再直接关闭,而是进入"取消审核中"状态,由后台/门店客服在 2 小时内处理(同意/拒绝),超时未处理则系统自动取消。该功能最早建成后因投诉停用(入口注释于 commit 17736fbb3),2026-08 重启并补齐了原缺陷:已支付订单可申请、超时自动取消兜底、客服/用户通知、并发安全与旁路封堵。

商家拒绝时在两条路线中单选(弹窗 RadioGroup,默认"拒绝并恢复"):

  • 拒绝并恢复:订单恢复原状,用户可再次申请;

  • 拒绝并完结退款:异步链路自动完成"转自提→核销收货→完结订单→退全部已付租金",订单终态为"交易完成"而非"已取消",用户拿到关闭+退款、商家保住履约/发货统计口径。代码已实施(2026-08-14),上线前须先关闭财务/支付宝合规两项前置确认(见提案头部)。

  • OpenSpec 变更:openspec/changes/archive/2026-08-11-lease-cancel-apply-relaunch/(基础闭环,已归档)、openspec/changes/lease-cancel-reject-fulfill-refund/(拒绝并完结退款路线,代码已实施)

  • 主规格:openspec/specs/lease-cancel-apply/spec.md

  • 适用范围:租赁订单(is_lease=1)未发货(status<1);商城/积分等非租赁订单不受影响

状态机

订单字段 eb_store_order.is_user_del:0 正常 / 1 已取消 / 2 取消审核中 / 3 履约退款处理中。

text
用户点取消(租赁单, status<1)
   ├─ 未支付 ──────────────▶ 直接取消(原直退链路,不进审核)
   └─ 已支付 ── 申请 ──▶ ②取消审核中
                            ├─ 商家同意 ──▶ ①已取消(cancelLeaseOrder 全链路,含已支付退款)
                            ├─ 商家拒绝(拒绝并恢复)──▶ ⓪恢复正常 + 站内信通知用户(可再次申请)
                            ├─ 商家拒绝(拒绝并完结退款)──▶ ③履约退款处理中 ── 链路完成 ──▶ ⓪正常,status=6 交易完成
                            │                                   ▲    └─ 失败重试耗尽:保持③ + 告警,人工重试/终止
                            │                                   └─ 人工终止(3→2,回到取消审核中)
                            ├─ 用户撤销 ──▶ ⓪恢复正常(静默,可再次申请)
                            └─ 2h 超时 ──▶ 定时任务自动同意 ──▶ ①已取消

支付状态分流的设计意图:审核通过后会自动代扣首期租金,未支付单多为代扣失败的低资质用户,审核挽留无价值且拦截支付体验差,故未支付直接取消;已支付单才是发货率挽留的主战场,进入二段式。

用户端(小程序)

  • 申请取消POST /api/order/cancelStoreOrderController::cancel 内分流)。已支付待发货租赁单返回"取消申请已提交,2小时内未处理将自动取消";申请同时异步关闭在途支付单,防止审核期间钱到账。
  • 申请中展示:详情页 _status._type == 10 区块显示"申请取消中"、文案"订单已申请取消,等待商家确认;若2小时内未处理,将自动取消"、申请时间与原因(取最新一条 cancel_apply 状态流水)。
  • 完结退款处理中展示:详情页 _status._type == 11 区块显示"取消申请处理中:订单将完结并退款,请留意退款到账";无撤销按钮,取消按钮与取消弹窗对 _type==11 互斥隐藏。
  • 撤销POST /api/order/revoke_cancel,详情页"撤销取消申请"按钮(仅 _type==10 显示),撤销后订单恢复并可再次申请;静默处理不通知客服。
  • 取消弹窗(components/cancelOrder)申请成功后返回详情页就地刷新,用户立即看到申请中状态。

后台处理(admin / store)

  • 入口:租赁订单列表"同意取消 / 拒绝取消"按钮,仅 is_user_del == 2 显示;普通"取消订单"按钮仅 is_user_del == 0 显示(两端互斥已核对)。
  • 接口:POST /order/cancel_apply_agree/:idPOST /order/cancel_apply_reject/:id(admin 与 store 路由同名,共用 app/common/controller/Lease.php)。
  • 同意:复用 cancelLeaseOrder 既有取消全链路(已支付订单同步退首期租金 + 异步退其余期 + 关单/解冻/解约),回取用户取消原因写入流水。
  • 拒绝:弹窗单选处理方式(handle_type,默认"拒绝并恢复"):
    • handle_type=1 拒绝并恢复:仅恢复订单(is_user_del 2→0),不产生任何关单副作用,同时给用户发"取消申请未通过"站内信;
    • handle_type=2 拒绝并完结退款:见下节"拒绝并完结退款(路线 B)"。
  • 审核中订单在列表显示"取消审核中"标识;状态 tab 统计口径不变(待审核订单不落任何状态 tab)。

拒绝并完结退款(路线 B)

对已支付待发货(status==0)的取消申请单,商家可选择该路线:订单从"取消审核中"转入"履约退款处理中"(is_user_del 2→3),异步链路把订单走完结流程并退还全部已付租金。设计/规格详见 openspec/changes/lease-cancel-reject-fulfill-refund/(proposal.md / design.md / tasks.md)。

  • 前置拦截is_lease==1 && paid==1 && status==0;存在已支付且未退押金(eb_store_order_deposit type=1 且 status∈(1,2))时拒绝进入,提示先走既有押金退款入口。
  • 状态抢占:持订单级锁 → 条件更新 2→3 → 同步写 cancel_apply_reject_fulfill 流水(含拒绝原因与操作人,卡单计时以此为据)→ 派发 LeaseRejectFulfillJob → 立即返回"已转入履约退款流程"。
  • 链路StoreOrderServices::runRejectFulfillChain(),逐步幂等可重入):
    1. 转自提:快递单事务内生成 verify_code + 置 shipping_type=2 + 写 change_pickup 流水(对齐后台发货"无需配送" StoreOrderDeliveryServices::changeToPickup,免物流公司/单号校验,不经 delivery());check3 单同事务经 CommerceRentServices::modify($id, 4) 同步支付宝配送方式(SELFPICK);
    2. 核销收货:自提单整单核销一步完成发货+收货(0→2),check3 的发货/签收由核销事件 Writeoff 监听器内建 send/receive 同步支付宝;拒绝后商家已真实快递发货的竞态在途单,兜底走强制收货;
    3. 完结completeOrder/endOrder 取消退款专用分支($forCancelRefund=true)——未支付分期不调 paySync 向支付宝申报,本地直接置取消代扣(status=3),芝麻解冻/租赁宝解约/check3 fulfillmentFinish 照常;
    4. 退款:自编排遍历全部"已支付且未退"分期逐期 withholdRefund不复用 afterLeaseCancel——其仅服务 is_del=1 订单);在途守卫(已写退款请求号 / check3 退款中流水)跳过本轮由退款查询任务复核,防重复退款;单期失败不阻断其他期,汇总抛错走重试;
    5. 收尾:条件更新复位 3→0;不额外通知用户(与原取消路线一致,用户感知为订单完结 + 退款原路到账),退款合计金额写链路日志供追溯。
  • 失败治理:步骤失败 Log::error + 原子节流告警(复用 sendCancelApplyAlert,Redis SET NX EX 1 小时)+ 延迟重试(60s/600s,最多 3 次);耗尽保持状态 3 并告警;卡单超 2 小时由 autoCancelApplyAudit 扫描告警(以 cancel_apply_reject_fulfill 流水计时)。
  • 人工处置(列表"履约退款处理中"标识 + 重试/终止入口,链路卡在任何履约进度均可处置):
    • 重试:原拒绝入口 handle_type=2 对状态 3 订单仅重派任务,链路幂等跳过已完成步骤(也充当人工补救后的确认完成并复位);
    • 终止POST /order/cancel_apply_fulfill_abort/:idabortFulfillRefund() 回退 3→2 回到取消审核中(事务内写 cancel_apply_fulfill_aborted 流水);已完成的步骤不回滚(已发货/已完结/已退款维持),由商家后续人工处理。
  • 状态 3 围栏:同意/拒绝并恢复/用户撤销/再次申请/后台与门店直接取消/自动取消/api 各支付入口一律拒绝;仅允许重试、终止与人工发货补救。
  • 上线前置(未关闭不得发布):发货分账开关状态与财务结算/返佣影响及冲正方案(财务确认);支付宝对"即发即收即完结即退"履约模式的规则口径(业务确认)。

超时自动取消

  • 定时任务 lease_cancel_apply_auto(后台"系统设置→定时任务"可见可配,每分钟执行),入口 SystemTimer::implement_timer()StoreOrderServices::autoCancelApplyAudit()
  • 判定:以最新一条 cancel_apply 状态流水的 change_time 为申请时间(流水与状态翻转同事务写入,杜绝异步丢失),now >= 申请时间 + 7200 秒(常量 CANCEL_APPLY_AUTO_SECONDS)即自动执行同意取消。
  • 兜底策略:
    • 订单已发货(status>=1):跳过 + 告警人工处理,绝不自动取消;
    • 申请流水缺失:告警人工处理,不静默跳过;
    • 单笔执行失败(押金未退/退款失败等):订单保持待审核下轮重试 + 告警;
    • 履约退款处理中(is_user_del=3)卡单超 2 小时:仅告警人工处置(路线 B 扩展扫描);
    • 告警通道 system_dev_exception 企微通知,Redis SET NX EX 原子节流(同订单同类型 1 小时 1 条)。

消息通知

mark受众通道触发点
order_cancel_apply客服(type=2)kefu 站内信(门店单送门店店员,平台单送平台客服)+ 企微平台群用户申请取消
order_cancel_apply_reject用户(type=1)站内信商家拒绝(拒绝并恢复)
system_dev_exception(既有)运维/客服企微自动取消失败、发货跳过、流水缺失、异步退款失败、路线 B 链路失败/卡单
  • 通知 payload 为扁平结构:uid/store_id/shipping_type/order_id/paid_status/cancel_reason/apply_time/deadline_time,模板 {key} 占位符直接替换。
  • 企微机器人 URL 在后台"消息通知"配置中补填 order_cancel_applyurl 字段;通道未配置/关闭时 boolSwitch 安全跳过,不影响主流程。
  • 路线 B 完成后不额外通知用户(2026-08-14 决策,与原取消路线一致);链路固有的发货/核销/租赁结束等既有通知照常触发。
  • 订阅消息未实现,仅保留扩展点。

旁路拦截与并发安全

审核中(is_user_del == 2)订单在以下入口统一拦截,提示先处理取消申请:后台/门店直接取消、发货 delivery()、拆单发货 splitDelivery()、核销 writeoffOrder()、强制收货 take_delivery、用户支付、租赁审核(后两者为既有拦截)。

履约退款处理中(is_user_del == 3)订单在以下入口统一拦截:用户取消/撤销/再次申请/支付(api 7 处支付入口)、商家同意/拒绝并恢复、后台与门店直接取消(cancelOrder)、自动取消;发货/核销/强制收货放行(链路内部调用与人工补救通道)。

并发安全两道防线(同意/拒绝/撤销/自动取消/路线 B 进出共用):

  1. 订单级互斥锁 lock_order_cancel_audit_{oid}(Redis SET NX EX),未取到锁报"操作进行中,请稍后重试";
  2. 条件更新原子抢占,状态翻转一律 where is_user_del = <源状态>cancelLeaseOrder 终态更新同样带源状态条件。

任何外部副作用(关单/退款)只发生在抢占成功之后,杜绝"商家看到拒绝成功、订单实际已被同意取消并退款"的错乱。

数据与部署

  • 种子 SQL(db_sql/zyl.sql,幂等 INSERT ... WHERE NOT EXISTS):eb_system_notification × 2(order_cancel_applyorder_cancel_apply_reject)、eb_system_timer × 1(lease_cancel_apply_auto)。路线 B 无 DB 变更。
  • 部署后必须 php think clear:cache(定时任务列表走 Redis 哈希缓存,不重建新任务不生效),并重启 Swoole(路线 B 为纯代码变更,重启即生效)。
  • 回滚:重新注释 app/controller/api/v1/order/StoreOrder.php 取消入口的二段式分支即恢复全部直退;定时任务 is_open=0 关闭自动取消;通知配置删除或关闭通道即停发;种子按精确 mark 删除。路线 B 停用:后端忽略 handle_type(或前端移除路线单选);存量状态 3 订单经人工重试/终止消化,禁止直接改库;专用完结分支默认参数关闭,正常完结路径不受影响。

涉及文件

  • 后端:app/controller/api/v1/order/StoreOrder.php(入口分流 + 撤销接口 + 状态 3 支付拦截)、app/services/order/StoreOrderServices.php(applyCancel/rejectCancelApply/cancelApplyAgree/revokeCancelApply/autoCancelApplyAudit/sendCancelApplyAlert/cancelOrder 旁路封堵/cancelLeaseOrder 条件更新/_msg 文案;路线 B:enterRejectFulfill/runRejectFulfillChain/abortFulfillRefund/completeOrder+endOrder 专用分支)、app/jobs/order/LeaseRejectFulfillJob.php(路线 B 异步链路)、app/services/message/NoticeService.php(orderCancelApply/orderCancelApplyReject)、app/services/order/StoreOrderDeliveryServices.php(delivery/splitDelivery 拦截)、app/services/order/store/WriteOffOrderServices.php(核销拦截)、app/common/controller/Order.php(take_delivery 拦截)、app/common/controller/Lease.php(handle_type 透传 + 终止入口)、app/dao/order/StoreOrderStatusDao.php(getLatestRejectFulfillBatch)、app/jobs/order/OrderWithholdJob.php(退款失败告警)、app/listener/system/timer/SystemTimer.phpapp/services/system/timer/SystemTimerServices.phproute/api.phproute/admin.php/route/store.phpcancel_apply_fulfill_abort/:id
  • 前端:view/admin/src/pages/order/leaseOrderList/components/tableList.vueview/store/src/pages/order/leaseOrderList/components/tableList.vueview/admin/src/pages/order/leaseOrderList/js/orderMix.jsview/store/src/pages/order/leaseOrderList/js/orderMix.js(拒绝弹窗路线单选 + 状态 3 重试/终止)、view/admin/src/api/leaseOrder.jsview/store/src/api/leaseOrder.jsabortFulfillRefund)、view/uniapp/pages/goods/lease_order_details/index.vueview/uniapp/pages/goods/components/cancelOrder/index.vueview/uniapp/api/order.js

后续规划

  • 路线 B 测试环境端到端回归待执行:构造真实已支付待发货租赁单走通全链路(含并发竞态、副作用落地验证),见 tasks.md 回归清单;
  • 财务确认(发货分账开关、财务结算/返佣影响与冲正方案)与支付宝合规确认("即发即收即完结即退"模式口径)关闭后方可发布上线。

承信租多门店租赁商城系统官方文档