research.md 9.4 KB

Phase 0 Research: 平台订单状态受控调整

Date: 2026-08-10 Scope: 平台订单状态、配送状态、线下支付确认、OMG 补单/退款、日志、并发和前端管理页面。

R1. 当前页面为何只能修改订单状态

Finding: foodie-admin-vue/src/views/system/order/index.vue 的修改弹窗只有 form.state。页面虽然能筛选和展示 payStatusdeliveryStatusafterSaleStatus,但没有对应编辑控件。提交时调用 updateOrder(this.form),把从详情接口取得的完整订单对象发给 PUT /system/order

Decision: 不在现有完整表单上直接增加两个下拉框。改为状态上下文 DTO 和专用动作 API。

Rationale: 完整对象提交会把金额、用户、门店、商品等字段一并带回,后端 Mapper 又会更新所有非空字段;为支付/配送增加下拉框会放大批量赋值和并发覆盖风险。

R2. 通用订单编辑接口已经不符合新状态机

Finding: PosOrderController.edit() 仍判断旧 state=12(送达)、state=7/11(退款)和 kefuState→state=5,而当前订单状态只允许 0~4。PosOrderMapper.xml 的通用更新同时允许写 statedelivery_statuspay_statusafter_sale_status

Decision: 删除旧状态分支;通用 PUT /system/order 遇到任何状态字段或 sdTime 时直接拒绝。状态只能走新接口。

Alternative rejected: 在 Controller 里静默把状态字段设为 null。静默忽略会让调用者误以为修改成功,也不利于发现旧客户端。

R3. 用状态上下文代替前端复制状态机

Decision: 新增 GET /system/order/{id}/status-context,返回当前四状态、OMG 处理态和允许动作。

Rationale:

  • 前端只负责呈现,后端是唯一规则来源;
  • 可以区分 pos_order.payStatus 与 OMG 流水的退款中/失败/已退状态;
  • 页面打开后保留旧快照,写入时用于并发校验;
  • 后端写接口仍会重新校验,能力字段不是授权依据。

R4. 无数据库版本字段时的并发控制

Decision: 以 statedeliveryStatuspayStatusafterSaleStatus 四个旧值作为复合版本,执行单行条件更新。

Rationale: 这些字段覆盖本功能会与之竞争的支付回调、骑手、售后和管理员写入;无需增加 version 字段,符合无 DDL 约束。

Implementation note: deliveryStatus 可空,使用 MyBatis-Plus isNull/eq 构建条件,禁止字符串拼 SQL。更新行数为 0 时重新读取订单并返回“状态已变化,请刷新后重试”。

Alternative rejected:

  • 只加 Redis 锁:不能覆盖支付回调和滚动发布期间不同锁键的写入;
  • 只在更新前查询:查询与更新之间仍有竞争窗口;
  • 新增版本列:本期没有必要的模式变更成本。

R5. 状态转换必须收敛到服务层

Finding: 商家完成、骑手送达、用户确认、自动完成和平台旧编辑分别直接写订单。当前骑手送达只写配送状态和时间,未写 state=3;006 契约明确要求同步完成。

Decision: 新增 OrderLifecycleService 处理:

  • 管理员订单/配送状态调整;
  • 骑手送达的统一完成;
  • 线下收款与退款;
  • OMG 退款后的订单状态收口;
  • CAS、账单、积分和同步订单日志。

Boundary: 本期只把骑手送达和新增管理端动作接入统一服务;其他完成入口保持行为不变,除非测试证明会破坏新不变量。现有账单按订单检查继续作为幂等兜底。

R6. 完成态与账单

Finding: OrderService.setSanghuBilling()setQishouBilling() 在写账单前按用户、类型、订单号检查是否已存在。商家完成和定时自动完成会生成账单,骑手送达当前不会。

Decision: 只有 CAS 首次把订单带入 state=3 的调用才执行完成副作用;外送生成商家和骑手账单,自取/堂食至少生成商家账单。状态、账单和同步日志放在同一事务。

Caveat: 现有账单幂等依赖应用层 count,没有数据库唯一约束。本期不改表,CAS 决定唯一完成者,账单 count 作为第二层保护。

R7. 线下支付边界

Finding: 当前有效约定中 payType="1" 表示到付/现金,payType="7" 表示 OMG;代码库仍保留其他历史在线支付类型,但 CLAUDE.md 明确不得重新启用。

Decision: “线下支付”只认 payType="1",不采用 payType != "7"。确认收款只允许未支付且未取消的线下订单;确认退款只允许已支付、未完成、无售后的线下订单。

Rationale: 将历史在线类型当成线下会制造无法对账的人工已支付/已退款状态。

R8. OMG 补单复用现有事实链路

Finding: OmgPayController.reconcileByQuery() 已实现:读取最新支付流水、调用 QueryTradeInfo、校验交易号/金额/日期、通过 markSuccessIfUnpaid CAS 核销,并与回调共用 applyPaidResult()

Decision: 管理端补单调用这一公开核心,来源标记为 admin;不新增“管理员直接设已支付”逻辑,也不新建支付表。

Additional guard: 管理端调用前检查订单快照、payType=7payStatus=0 和非取消态;调用后重新读取订单作为最终响应。

R9. OMG 退款结果需要显式类型

Finding: 当前 refundOrder()AjaxResult 同时表达成功、人工退款、失败和结果未知,不利于管理端根据结果刷新能力;退款成功只更新 pos_order.payStatus=2

Decision: 增加内部 OmgRefundOutcome,至少区分:

  • REFUNDED:网关成功,支付流水已退;
  • MANUAL_PENDING:支付方式不支持 API;
  • UNKNOWN:请求超时/结果未知,流水保持退款中;
  • FAILED:明确失败,流水恢复已支付;
  • IDEMPOTENT:此前已经完成或正在处理。

外部 /pay/omg/refund 仍转换成兼容的 AjaxResult;管理端使用明确结果展示提示。

R10. OMG 退款事务边界

Decision:

  1. 校验订单、支付流水和金额;
  2. 支付流水 1→4(退款中)CAS;
  3. 在数据库事务外调用 OMG;
  4. 成功后流水 4→3,再通过生命周期服务同步 payStatus=2, afterSaleStatus=3, state=4
  5. 明确失败时流水 4→1
  6. 超时/未知时保持 4,禁止重复发起,等待人工对账;
  7. 网关成功但订单同步失败时记录高优先级日志,后续只补偿本地状态。

Rationale: 数据库事务无法覆盖外部网关,长事务还会扩大锁等待;支付流水是不可丢失的资金事实。

R11. 人工退款待办的幂等

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,不能伪造网关动作。

R12. 审计记录

Finding: pos_order_log.contentVARCHAR(512),已有操作者类型、ID、名称和时间字段。OrderLogHelper.log() 异步写入,logSync() 可参与当前事务。

Decision: 人工状态操作使用 logSync(),原因限制 200 字。内容采用固定、可读格式:

平台调整[配送状态]:2→3;订单状态:2→3;支付状态:1→1;售后状态:0→0;原因:骑手端漏操作

支付补单/退款保留系统资金日志,同时再写一条管理员发起/结果日志,确保责任主体完整。

R13. 权限和输入安全

Decision:

  • 所有上下文和动作接口使用 @PreAuthorize("@ss.hasPermi('system:order:edit')")
  • 写接口使用 @Valid 和白名单 DTO;状态范围、原因长度、目标互斥/必填在服务端二次校验;
  • 支付动作使用 @RepeatSubmit,但幂等最终依赖 CAS;
  • 不向管理端返回 HashKey、HashIV、原始回调或其他支付秘密;
  • 错误消息只说明业务冲突,不回传堆栈或网关敏感报文。

R14. 前端策略

Decision: 修改弹窗显示三部分:订单状态、配送状态、支付状态与动作。支付状态为只读;所有状态名称和操作文案使用 dingdanorder 四语言 key。退款使用 Element UI 二次确认,提交期间禁用按钮;任一成功或冲突后重新读取服务端。

Alternative rejected: 前端直接维护转换矩阵。它容易与骑手/支付规则漂移,也无法安全判断 OMG 流水退款中或人工待办。

R15. 数据库与分支

Decision: 不新增表、字段、索引,不修改 updatesql/sql.md;全程保留当前 test 分支,不执行 Spec Kit 的建分支脚本。

Resolved Questions

本阶段没有阻塞实现的未决问题。已完成订单退款和结算冲正明确排除;历史在线支付不得按线下处理;人工退款待办及完成确认复用现有 OMG 退款记录。