功能标识:037-order-no-rider-auto-cancel
创建日期:2026-10-14
状态:已提交;数据库部署与联调待执行
输入:需求「外卖订单如果订单生效后 30 分钟没有骑手接单,订单自动取消,退款给用户」。specs/036-multi-store-checkout/spec.md:140-142,181 已明确将「最迟接单/无人接单自动取消」甩为本独立后续事项,不复用废弃的 TestTask。
| 口径 | 结论 |
|---|---|
| 生效事实与计时起点 | 新增 effective_at DATETIME NULL,仅在订单首次具备履约资格时写入;在线支付以服务端确认成功、到付以服务端确认订单可履约、转账以确认收款为准。pay_status=1 仍须参与资格校验,但不能单独充当生效时间;is_display / display_time 表示骑手开放状态和实际开放时间,不是生效事实。 |
| 未支付单 | 不处理(payStatus=0 一律不动) |
| 到付单(payType=1) | 创建时选择到付,或未支付外送单从在线方式合法切换到到付后,订单立即生效;到时自动取消但不退款(本就没收过钱)。 |
| 转账单(payType=6,已确认收款) | 纳入取消;退款走线下人工(027 的「转账单不做超时自动取消」仅限未确认收款期间,spec.md:80,121;FR-006 确认收款后回归现有状态机) |
| 范围排除 | 堂食/自取(delivery_status 为 NULL 天然不命中)、商家自配送(self_delivery=1 不进骑手池)、闪送(独立表独立状态机)、未支付单 |
| 阈值 | 数据字典配置(新 dict_type,value=30 分钟,DictUtils 读取,缺省 30) |
| 预约单取消时点 | max(effective_at, 预约时段开始时间, 实际骑手开放时间 display_time) + 阈值,且必须仍可接单;提前付款不能在预约开始前触发取消。 |
| OMG 已付单退款 | 本轮实现正式环境自动退款;测试环境退款失败属预期,保留可查询的退款意图/未完成状态,不标记已退款,不调用正式网关进行测试。 |
effective_at,但不能从该时刻直接计 30 分钟并在预约开始前取消。delryTime 是 yyyy-MM-dd HH:mm-HH:mm 时间段字符串,取开始时间并与 effective_at、display_time 求最晚时间。若 15:00 生效、16:00 预约开始且正常开放,最早 16:30 判断;若 16:10 才开放,最早 16:40 判断。即时单从 effective_at 计时。N(已授权)、E 后 N(要关账且全额退款)或 R(已关账)。测试环境不调用正式退款接口,意图保持未完成并明确原因;不能把测试失败写成退款成功。超时或不确定网关结果不能盲重试同一动作,须查询事实再继续或进入人工核查。支付尝试保存创建时的 gateway_environment,支付查询沿用该环境;退款要求当前配置及尝试快照都为正式环境,避免将测试交易发送到正式退款接口。正式环境每个 MerchantID 还需配置对应 CreditCheckCode。参考官方文档:https://developers.omg.com.tw/payment/aio/api/05_refund.html 及 https://developers.omg.com.tw/payment/aio/api/04_order_query.html 。pos_order 加 effective_at DATETIME NULL。该字段只在首次合法生效时写一次,不因支付重试、重复回调或状态重放覆盖;渠道实际支付时间仍由支付渠道事实表记录。现有独立订单按以下服务端事实写入:
UserOrderController.java:301 创建时服务端确认订单可履约;到付尚未实际收款,不能把该时间称为支付时间。LinePayFactService.applyPaidFactInternal 调用 PosOrderMapper.xml:378 的 markLinePaid 成功时,随订单支付事实原子写入。OmgPaymentNotifyService.applyVerifiedFacts 调用 OmgPaymentAttemptMapper.xml:136 的 markOrderPaid 成功时,随订单支付事实原子写入;:205 只是修改用于推送的对象,不是数据库写点。PosOrderShOprateController.confirmTransferPayment:668 或平台 OrderLifecycleService.confirmOfflinePayment:286 确认收款时写入。其他在用的线下确认入口实施前再按外送资格核对;自取、堂食不在本任务范围。TestTask.java:145、NewebpayPayController.java:323切换支付方式:现有 POST /pay/switch 仅允许 payStatus=0 的独立订单,但错误地限制外送单不能切换为到付;本功能应允许未支付外送单从 LINE Pay 等在线方式切换到可用的到付(现金 payType=4 仍按原订单类型规则)。在线方式之间切换只改变 pay_type,effective_at 保持 NULL,待新渠道可信付款事实使订单首次生效时写入。切换到到付则须在同一事务内校验订单与目标方式可用、原渠道没有已确认实收;有未决尝试先查询现有支付事实,无法确认时暂缓切换。确认未实收后原子更新 pay_type=1、pay_status=1、effective_at=NOW(),仅在提交后开放接单。已生效订单禁止切换,重复付款或支付重试不得清空、覆盖生效时间。实现时将 PayMethodSwitchService 当前的整对象 updateById(order) 改为仅更新必要列的条件更新,条件至少包括 pay_status=0 AND effective_at IS NULL 及可切换状态,防止与支付回调并发时写回旧值。旧渠道若仍有迟到实收,只记渠道收款事实并创建幂等退款意图,不重开订单或再次改写 effective_at。新多店批次逐子单改方式及到付确认仍按 036 的父批次生效规则处理,不套用本独立订单入口。
不回填历史:effective_at IS NULL 的存量单不参与本功能,从上线起干净生效(避免任务首跑批量取消历史滞留单;旧滞留单人工处理)。
多店批次接入(2026-10-16 已确认、尚待实现):本轮在 036 FR-042/SC-030、T023 中接入现有阈值;全部有效成员就绪后以父批次统一生效、首次实际开放和预约开始三者最晚值计时,单子单先付款不启动倒计时,部分取消不续时。未承接配送组超时原子取消剩余有效成员,与接组互斥、逐交易退真实实收;无组的独立配送成员沿独立方式但不早于批次统一生效计时。现有 037 独立订单实现和验证不代表这段扩展已完成,普通单规则保持回归覆盖。
type=0 AND state=0 AND delivery_status=0 AND pay_status=1
AND after_sale_status=0 AND qs_id IS NULL AND IFNULL(self_delivery,0)=0
AND effective_at IS NOT NULL AND is_display=1
计时:即时单 effective_at + 阈值分钟 ≤ now
预约单 max(effective_at, 预约时段开始时间, display_time) + 阈值分钟 ≤ now
state=0 即「骑手池」状态——商家接单反而要求骑手已接单(PosOrderShOprateController.acceptOrder:337 requireRiderAssigned),不存在「商家已接单但无骑手」的中间态。本条件与骑手接单 CAS(OrderLifecycleService.acceptDeliveryByRiderLocked:171-181:state=0 + deliveryStatus=0 + payStatus=1 + afterSaleStatus=0 + qsId IS NULL)完全对偶。
UPDATE pos_order SET state=4 WHERE id=? AND 全套池条件——与骑手抢单天然防竞态,影响行数=0 则跳过该单(本轮已被抢/已变化)。linePayRefundService.createRefundIntent(paymentId, "AUTO_NO_RIDER")——幂等退款意图落 pos_order_line_refund,实际网关退款由现有 LinePayReconcileTask 异步执行(复用 USER_CANCEL/STORE_CANCEL 的既有模式)。orderLogHelper.logSync(operatorType=0 系统,文案「超时无骑手自动取消」,与取消事务一致)。merchantNotificationRouter.sendStoreNotification)。骑手无接收人(qsId 为空)。注意 notifyOrderCancelled(DeliveryOrderNotificationService.java:104-132)仅 type=0 且 qsId!=null 才推,不能直接复用,需用户侧推送变体。推送文案新增后端 i18n key(沿用 no.message.push.* 模式,四语言)。@Component("posOrderNoRiderTimeoutTask") 公开方法 + sys_job 注册(cron 每分钟),样板照抄 FlashDeliveryAutoCompleteTask(其类注释明确「由若依任务调度调 Bean 公开方法,避免应用内 @Scheduled 重复执行」)。SQL(sys_job INSERT)登记 updatesql/sql.md 由开发者手动执行。逐单 CAS 幂等,双跑安全。
数据字典:新 dict_type(如 pos_order_auto_cancel,字典项「无骑手自动取消分钟数」=30),任务每次执行时 DictUtils 读取,读取失败/未配置缺省 30。INSERT 登记 updatesql/sql.md。
OrderLifecycleService.java:169-181(只更新 deliveryStatus=1 + qsId,state 不变)PosOrderQsOprateController.orderTabWrapper newTask 分支 :447-451UserOrderController.cancelOrder:746-790LinePayRefundService.createRefundIntent:178(幂等);LinePayCancellationCompensationService(source=CANCEL_SCAN 补偿「已取消已付无退款意图」的单——注意:本功能上线后该补偿扫描可能把 AUTO_NO_RIDER 取消的 OMG/到付单也补出 LINE 意图吗?不会,补偿只针对有 PAID LINE payment 的单,实现时验证 source 语义即可)UserOrderController.handleIsDisplay:477-491 即时单下单即写、预约单临近才写;远期预约翻显示的原定时任务已随 TestTask 废弃。骑手新任务列表在 PosOrderQsOprateController.orderTabWrapper:445 要求 is_display=true;实施预约自动取消前须补持久化开放与重启补扫。pos_order_line_payment.pay_time / pos_order_omg_attempt.payment_date,不能替代到付和多店批次的生效时间。specs/006-orderstate);payStatus 0未支付/1已支付/2已退款;afterSaleStatus 0无售后…ruoyi-admin/.../flashdelivery/task/FlashDeliveryAutoCompleteTask.java;sys_job 登记样例 updatesql/sql.md:1346-1368updatesql/sql.md 的 2026-10-15 节手动执行 DDL、字典和调度任务;历史订单不回填 effective_at。omgpay.environment=PRODUCTION 和各 MerchantID 的 CreditCheckCode 后,再用真实交易验收查询及退款。