# Phase 0 Research: 平台订单状态受控调整 **Date**: 2026-08-10 **Scope**: 平台订单状态、配送状态、线下支付确认、OMG 补单/退款、日志、并发和前端管理页面。 ## R1. 当前页面为何只能修改订单状态 **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 又会更新所有非空字段;为支付/配送增加下拉框会放大批量赋值和并发覆盖风险。 ## R2. 通用订单编辑接口已经不符合新状态机 **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`。静默忽略会让调用者误以为修改成功,也不利于发现旧客户端。 ## R3. 用状态上下文代替前端复制状态机 **Decision**: 新增 `GET /system/order/{id}/status-context`,返回当前四状态、OMG 处理态和允许动作。 **Rationale**: - 前端只负责呈现,后端是唯一规则来源; - 可以区分 `pos_order.payStatus` 与 OMG 流水的退款中/失败/已退状态; - 页面打开后保留旧快照,写入时用于并发校验; - 后端写接口仍会重新校验,能力字段不是授权依据。 ## R4. 无数据库版本字段时的并发控制 **Decision**: 以 `state`、`deliveryStatus`、`payStatus`、`afterSaleStatus` 四个旧值作为复合版本,执行单行条件更新。 **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=7`、`payStatus=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.content` 为 `VARCHAR(512)`,已有操作者类型、ID、名称和时间字段。`OrderLogHelper.log()` 异步写入,`logSync()` 可参与当前事务。 **Decision**: 人工状态操作使用 `logSync()`,原因限制 200 字。内容采用固定、可读格式: ```text 平台调整[配送状态]: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 退款记录。