Date: 2026-08-10 Scope: 平台订单状态、配送状态、线下支付确认、OMG 补单/退款、日志、并发和前端管理页面。
Finding: foodie-admin-vue/src/views/system/order/index.vue 的修改弹窗只有 form.state。页面虽然能筛选和展示 payStatus、deliveryStatus、afterSaleStatus,但没有对应编辑控件。提交时调用 updateOrder(this.form),把从详情接口取得的完整订单对象发给 PUT /system/order。
Decision: 不在现有完整表单上直接增加两个下拉框。改为状态上下文 DTO 和专用动作 API。
Rationale: 完整对象提交会把金额、用户、门店、商品等字段一并带回,后端 Mapper 又会更新所有非空字段;为支付/配送增加下拉框会放大批量赋值和并发覆盖风险。
Finding: PosOrderController.edit() 仍判断旧 state=12(送达)、state=7/11(退款)和 kefuState→state=5,而当前订单状态只允许 0~4。PosOrderMapper.xml 的通用更新同时允许写 state、delivery_status、pay_status、after_sale_status。
Decision: 删除旧状态分支;通用 PUT /system/order 遇到任何状态字段或 sdTime 时直接拒绝。状态只能走新接口。
Alternative rejected: 在 Controller 里静默把状态字段设为 null。静默忽略会让调用者误以为修改成功,也不利于发现旧客户端。
Decision: 新增 GET /system/order/{id}/status-context,返回当前四状态、OMG 处理态和允许动作。
Rationale:
pos_order.payStatus 与 OMG 流水的退款中/失败/已退状态;Decision: 以 state、deliveryStatus、payStatus、afterSaleStatus 四个旧值作为复合版本,执行单行条件更新。
Rationale: 这些字段覆盖本功能会与之竞争的支付回调、骑手、售后和管理员写入;无需增加 version 字段,符合无 DDL 约束。
Implementation note: deliveryStatus 可空,使用 MyBatis-Plus isNull/eq 构建条件,禁止字符串拼 SQL。更新行数为 0 时重新读取订单并返回“状态已变化,请刷新后重试”。
Alternative rejected:
Finding: 商家完成、骑手送达、用户确认、自动完成和平台旧编辑分别直接写订单。当前骑手送达只写配送状态和时间,未写 state=3;006 契约明确要求同步完成。
Decision: 新增 OrderLifecycleService 处理:
Boundary: 本期只把骑手送达和新增管理端动作接入统一服务;其他完成入口保持行为不变,除非测试证明会破坏新不变量。现有账单按订单检查继续作为幂等兜底。
Finding: OrderService.setSanghuBilling() 和 setQishouBilling() 在写账单前按用户、类型、订单号检查是否已存在。商家完成和定时自动完成会生成账单,骑手送达当前不会。
Decision: 只有 CAS 首次把订单带入 state=3 的调用才执行完成副作用;外送生成商家和骑手账单,自取/堂食至少生成商家账单。状态、账单和同步日志放在同一事务。
Caveat: 现有账单幂等依赖应用层 count,没有数据库唯一约束。本期不改表,CAS 决定唯一完成者,账单 count 作为第二层保护。
Finding: 当前有效约定中 payType="1" 表示到付/现金,payType="7" 表示 OMG;代码库仍保留其他历史在线支付类型,但 CLAUDE.md 明确不得重新启用。
Decision: “线下支付”只认 payType="1",不采用 payType != "7"。确认收款只允许未支付且未取消的线下订单;确认退款只允许已支付、未完成、无售后的线下订单。
Rationale: 将历史在线类型当成线下会制造无法对账的人工已支付/已退款状态。
Finding: OmgPayController.reconcileByQuery() 已实现:读取最新支付流水、调用 QueryTradeInfo、校验交易号/金额/日期、通过 markSuccessIfUnpaid CAS 核销,并与回调共用 applyPaidResult()。
Decision: 管理端补单调用这一公开核心,来源标记为 admin;不新增“管理员直接设已支付”逻辑,也不新建支付表。
Additional guard: 管理端调用前检查订单快照、payType=7、payStatus=0 和非取消态;调用后重新读取订单作为最终响应。
Finding: 当前 refundOrder() 用 AjaxResult 同时表达成功、人工退款、失败和结果未知,不利于管理端根据结果刷新能力;退款成功只更新 pos_order.payStatus=2。
Decision: 增加内部 OmgRefundOutcome,至少区分:
REFUNDED:网关成功,支付流水已退;MANUAL_PENDING:支付方式不支持 API;UNKNOWN:请求超时/结果未知,流水保持退款中;FAILED:明确失败,流水恢复已支付;IDEMPOTENT:此前已经完成或正在处理。外部 /pay/omg/refund 仍转换成兼容的 AjaxResult;管理端使用明确结果展示提示。
Decision:
1→4(退款中)CAS;4→3,再通过生命周期服务同步 payStatus=2, afterSaleStatus=3, state=4;4→1;Rationale: 数据库事务无法覆盖外部网关,长事务还会扩大锁等待;支付流水是不可丢失的资金事实。
Finding: ATM/CVS/BarcodeATM 当前每次调用都会向 pos_order_omg_refund 插入 action=null, rtn_code=null 的人工记录。
Decision: 写入前查询该支付流水的退款记录;已有未决人工记录时直接返回 MANUAL_PENDING,不重复插入。订单 payStatus 保持 1。
Constraint: 现有表没有独立待办状态,本期通过支付流水和最新退款记录计算上下文,不新增字段。
人工退款在 OMG 后台完成后,管理员通过专用二次确认接口收口:必须存在同一支付流水的未决人工记录,先复用支付流水 1→4→3 CAS,再插入 action=NULL, rtnCode=1 的人工完成记录并同步订单三状态。action 保持 NULL 是因为现有列为 CHAR(1) 且只表示 OMG DoAction 的 C/R/E/N,不能伪造网关动作。
Finding: pos_order_log.content 为 VARCHAR(512),已有操作者类型、ID、名称和时间字段。OrderLogHelper.log() 异步写入,logSync() 可参与当前事务。
Decision: 人工状态操作使用 logSync(),原因限制 200 字。内容采用固定、可读格式:
平台调整[配送状态]:2→3;订单状态:2→3;支付状态:1→1;售后状态:0→0;原因:骑手端漏操作
支付补单/退款保留系统资金日志,同时再写一条管理员发起/结果日志,确保责任主体完整。
Decision:
@PreAuthorize("@ss.hasPermi('system:order:edit')");@Valid 和白名单 DTO;状态范围、原因长度、目标互斥/必填在服务端二次校验;@RepeatSubmit,但幂等最终依赖 CAS;Decision: 修改弹窗显示三部分:订单状态、配送状态、支付状态与动作。支付状态为只读;所有状态名称和操作文案使用 dingdanorder 四语言 key。退款使用 Element UI 二次确认,提交期间禁用按钮;任一成功或冲突后重新读取服务端。
Alternative rejected: 前端直接维护转换矩阵。它容易与骑手/支付规则漂移,也无法安全判断 OMG 流水退款中或人工待办。
Decision: 不新增表、字段、索引,不修改 updatesql/sql.md;全程保留当前 test 分支,不执行 Spec Kit 的建分支脚本。
本阶段没有阻塞实现的未决问题。已完成订单退款和结算冲正明确排除;历史在线支付不得按线下处理;人工退款待办及完成确认复用现有 OMG 退款记录。