# 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 退款记录。
