Appearance
概述
租赁订单"申请→审核"二段式取消:用户已支付的租赁订单取消时不再直接关闭,而是进入"取消审核中"状态,由后台/门店客服在 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/cancel(StoreOrderController::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/:id、POST /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_deposittype=1 且 status∈(1,2))时拒绝进入,提示先走既有押金退款入口。 - 状态抢占:持订单级锁 → 条件更新 2→3 → 同步写
cancel_apply_reject_fulfill流水(含拒绝原因与操作人,卡单计时以此为据)→ 派发LeaseRejectFulfillJob→ 立即返回"已转入履约退款流程"。 - 链路(
StoreOrderServices::runRejectFulfillChain(),逐步幂等可重入):- 转自提:快递单事务内生成
verify_code+ 置shipping_type=2+ 写change_pickup流水(对齐后台发货"无需配送"StoreOrderDeliveryServices::changeToPickup,免物流公司/单号校验,不经delivery());check3 单同事务经CommerceRentServices::modify($id, 4)同步支付宝配送方式(SELFPICK); - 核销收货:自提单整单核销一步完成发货+收货(0→2),check3 的发货/签收由核销事件
Writeoff监听器内建send/receive同步支付宝;拒绝后商家已真实快递发货的竞态在途单,兜底走强制收货; - 完结:
completeOrder/endOrder取消退款专用分支($forCancelRefund=true)——未支付分期不调paySync向支付宝申报,本地直接置取消代扣(status=3),芝麻解冻/租赁宝解约/check3fulfillmentFinish照常; - 退款:自编排遍历全部"已支付且未退"分期逐期
withholdRefund(不复用afterLeaseCancel——其仅服务is_del=1订单);在途守卫(已写退款请求号 / check3 退款中流水)跳过本轮由退款查询任务复核,防重复退款;单期失败不阻断其他期,汇总抛错走重试; - 收尾:条件更新复位 3→0;不额外通知用户(与原取消路线一致,用户感知为订单完结 + 退款原路到账),退款合计金额写链路日志供追溯。
- 转自提:快递单事务内生成
- 失败治理:步骤失败
Log::error+ 原子节流告警(复用sendCancelApplyAlert,RedisSET NX EX1 小时)+ 延迟重试(60s/600s,最多 3 次);耗尽保持状态 3 并告警;卡单超 2 小时由autoCancelApplyAudit扫描告警(以cancel_apply_reject_fulfill流水计时)。 - 人工处置(列表"履约退款处理中"标识 + 重试/终止入口,链路卡在任何履约进度均可处置):
- 重试:原拒绝入口
handle_type=2对状态 3 订单仅重派任务,链路幂等跳过已完成步骤(也充当人工补救后的确认完成并复位); - 终止:
POST /order/cancel_apply_fulfill_abort/:id→abortFulfillRefund()回退 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企微通知,RedisSET 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_apply的url字段;通道未配置/关闭时boolSwitch安全跳过,不影响主流程。 - 路线 B 完成后不额外通知用户(2026-08-14 决策,与原取消路线一致);链路固有的发货/核销/租赁结束等既有通知照常触发。
- 订阅消息未实现,仅保留扩展点。
旁路拦截与并发安全
审核中(is_user_del == 2)订单在以下入口统一拦截,提示先处理取消申请:后台/门店直接取消、发货 delivery()、拆单发货 splitDelivery()、核销 writeoffOrder()、强制收货 take_delivery、用户支付、租赁审核(后两者为既有拦截)。
履约退款处理中(is_user_del == 3)订单在以下入口统一拦截:用户取消/撤销/再次申请/支付(api 7 处支付入口)、商家同意/拒绝并恢复、后台与门店直接取消(cancelOrder)、自动取消;发货/核销/强制收货放行(链路内部调用与人工补救通道)。
并发安全两道防线(同意/拒绝/撤销/自动取消/路线 B 进出共用):
- 订单级互斥锁
lock_order_cancel_audit_{oid}(RedisSET NX EX),未取到锁报"操作进行中,请稍后重试"; - 条件更新原子抢占,状态翻转一律
where is_user_del = <源状态>;cancelLeaseOrder终态更新同样带源状态条件。
任何外部副作用(关单/退款)只发生在抢占成功之后,杜绝"商家看到拒绝成功、订单实际已被同意取消并退款"的错乱。
数据与部署
- 种子 SQL(
db_sql/zyl.sql,幂等INSERT ... WHERE NOT EXISTS):eb_system_notification× 2(order_cancel_apply、order_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.php、app/services/system/timer/SystemTimerServices.php、route/api.php、route/admin.php/route/store.php(cancel_apply_fulfill_abort/:id) - 前端:
view/admin/src/pages/order/leaseOrderList/components/tableList.vue、view/store/src/pages/order/leaseOrderList/components/tableList.vue、view/admin/src/pages/order/leaseOrderList/js/orderMix.js、view/store/src/pages/order/leaseOrderList/js/orderMix.js(拒绝弹窗路线单选 + 状态 3 重试/终止)、view/admin/src/api/leaseOrder.js、view/store/src/api/leaseOrder.js(abortFulfillRefund)、view/uniapp/pages/goods/lease_order_details/index.vue、view/uniapp/pages/goods/components/cancelOrder/index.vue、view/uniapp/api/order.js
后续规划
- 路线 B 测试环境端到端回归待执行:构造真实已支付待发货租赁单走通全链路(含并发竞态、副作用落地验证),见 tasks.md 回归清单;
- 财务确认(发货分账开关、财务结算/返佣影响与冲正方案)与支付宝合规确认("即发即收即完结即退"模式口径)关闭后方可发布上线。