# Data Model: 平台订单状态受控调整

**Date**: 2026-08-10
**Database change**: None

## 1. Existing Persistent Entities

### 1.1 `PosOrder` / `pos_order`

本功能只使用现有字段，不新增列。

| Java 字段 | 数据库列 | 类型 | 本功能含义 |
|---|---|---|---|
| `id` | `id` | BIGINT | 内部订单主键，管理端接口路径参数 |
| `ddId` | `dd_id` | VARCHAR | 业务订单号，日志和 OMG 流水关联键 |
| `type` | `type` | BIGINT | 0 外送、1 自取、2 堂食 |
| `state` | `state` | BIGINT | 0 待处理、1 已接单、2 已出餐、3 已完成、4 已取消 |
| `deliveryStatus` | `delivery_status` | BIGINT NULL | 外送：0 待接单、1 骑手已接单、2 配送中、3 已送达 |
| `payStatus` | `pay_status` | BIGINT | 0 未支付、1 已支付、2 已退款 |
| `afterSaleStatus` | `after_sale_status` | BIGINT | 0 无售后、1～6 为售后流程状态 |
| `payType` | `pay_type` | VARCHAR | 1 线下/到付，7 OMG；历史类型不在本功能启用 |
| `collectPayment` | `collect_payment` | VARCHAR | 到付标识，保留给既有账单逻辑 |
| `qsId` | `qs_id` | BIGINT NULL | 骑手 ID；本功能不提供指派能力 |
| `sdTime` | `sd_time` | DATETIME NULL | 实际送达时间 |
| `points` | `points` | INT NULL | 退款/取消时需要返还的积分 |

### 1.2 `PosOrderLog` / `pos_order_log`

| 字段 | 用途 |
|---|---|
| `ddId` | 关联业务订单号 |
| `operatorType` | 管理员固定为 1；系统资金日志为 0 |
| `operatorId` | 当前后台管理员 ID |
| `operatorName` | 当前后台管理员名称 |
| `content` | 状态前后值、动作和原因，最大 512 字 |
| `logTime` | 服务端写入时间 |

人工操作使用同步日志，保证状态事务回滚时不会残留“成功”日志。

### 1.3 `PosOrderOmgPayment` / `pos_order_omg_payment`

订单层 `payStatus` 只有 0/1/2；OMG 流水有更细的内部状态：

| `pay_status` | 含义 | 管理端行为 |
|---|---|---|
| 0 | 未支付 | 允许查询补单 |
| 1 | 已支付 | 未完成订单允许退款 |
| 2 | 支付失败 | 不得人工改为已支付；需要重新发起支付 |
| 3 | 已退款 | 订单层应补偿为 `payStatus=2` |
| 4 | 退款中/结果未知 | 禁止重复退款，提示等待对账 |

### 1.4 `PosOrderOmgRefund` / `pos_order_omg_refund`

| 字段组合 | 含义 |
|---|---|
| `action=R, rtnCode=1` | OMG 自动退款成功 |
| `action=R, rtnCode!=1` | OMG 明确退款失败 |
| `action=R, rtnCode=NULL` | 请求处理中或结果未知，结合 `rtnMsg` 判断 |
| `action=NULL, rtnCode=NULL` | 支付方式不支持退款 API，等待人工处理 |
| `action=NULL, rtnCode=1` | 管理员确认已在 OMG 后台完成人工退款 |

同一支付流水只保留一个未决人工待办；已存在时重复请求返回原状态。

## 2. API-only Models

### 2.1 `AdminOrderActionRequest`

所有支付动作与状态动作共享的旧快照和审计字段。

| 字段 | Java 类型 | 校验 |
|---|---|---|
| `expectedState` | `Long` | 必填，0～4 |
| `expectedDeliveryStatus` | `Long` | 可空；非空时 0～3 |
| `expectedPayStatus` | `Long` | 必填，0～2 |
| `expectedAfterSaleStatus` | `Long` | 必填，0～6 |
| `reason` | `String` | 必填，trim 后 1～200 字 |

### 2.2 `AdminOrderStatusUpdateRequest`

继承/组合 `AdminOrderActionRequest`，增加：

| 字段 | Java 类型 | 校验 |
|---|---|---|
| `targetState` | `Long` | 可空，非空时 0～4 |
| `targetDeliveryStatus` | `Long` | 可空，非空时 0～3 |

规则：至少一个目标值与快照不同；未提交的目标保持当前值。外送送达可以只提交 `targetDeliveryStatus=3`，服务端自动同步 `state=3`。

### 2.3 `AdminOrderStatusContext`

| 字段 | 类型 | 说明 |
|---|---|---|
| `id`, `ddId` | Long/String | 订单标识 |
| `type`, `payType` | Long/String | 类型判定依据 |
| `state`, `deliveryStatus`, `payStatus`, `afterSaleStatus` | Long | 当前快照 |
| `sdTime` | Date | 当前送达时间 |
| `riderAssigned` | Boolean | `qsId != null` |
| `allowedOrderStates` | List<Long> | 包含当前值及可选下一值 |
| `allowedDeliveryStatuses` | List<Long> | 非外送为空 |
| `canConfirmOfflinePayment` | Boolean | 可确认线下收款 |
| `canConfirmOfflineRefund` | Boolean | 可确认线下退款 |
| `canReconcileOmg` | Boolean | 可查询 OMG 补单 |
| `canRefundOmg` | Boolean | 可发起 OMG 退款 |
| `canConfirmManualOmgRefund` | Boolean | 已有待办且可确认人工退款完成 |
| `omgPaymentStatus` | Integer NULL | 最新 OMG 流水状态 0～4 |
| `manualRefundPending` | Boolean | 存在未决人工退款记录 |
| `refundUnknown` | Boolean | OMG 流水为退款中/结果未知 |

`allowed*` 和 `can*` 是 UI 提示，不替代写接口校验。

### 2.4 `OmgRefundOutcome`

| 状态 | 说明 | 订单层处理 |
|---|---|---|
| `REFUNDED` | OMG 明确退款成功 | 同步为已退款/已取消/售后已退款 |
| `MANUAL_PENDING` | 无自动退款 API | 保持已支付，显示待人工处理和确认入口 |
| `UNKNOWN` | 请求结果未知 | 保持已支付，OMG 流水保持退款中 |
| `FAILED` | 网关明确失败 | 保持已支付，OMG 流水恢复已支付 |
| `IDEMPOTENT` | 已退款或正在处理 | 返回当前最终状态，无重复副作用 |

## 3. State Invariants

以下不变量必须在每个写入口成立：

1. `type IN (1,2) → deliveryStatus IS NULL`。
2. `deliveryStatus=3 → type=0 AND state=3 AND sdTime IS NOT NULL`。
3. `state=3 → payStatus=1 AND afterSaleStatus=0`（本期不允许完成后退款）。
4. `payStatus=2 → state=4 AND afterSaleStatus=3`（本功能处理的全额退款）。
5. `payType=7 AND payStatus=1` 必须存在 OMG 已支付流水。
6. `payType=7 AND payStatus=2` 必须存在 OMG 自动退款成功记录或管理员明确确认人工退款完成；人工待办不提前置 2。
7. `state IN (3,4)`、`payStatus=2` 为本功能终态，普通状态接口不能重新打开。
8. `afterSaleStatus>0` 时，普通订单/配送状态接口不得变更数据。

## 4. Order State Transitions

### 4.1 Normal order state

| Current | Target | Additional conditions | Result |
|---|---|---|---|
| 0 | 1 | OMG 订单必须已支付；无售后 | `state=1` |
| 1 | 2 | 无售后 | `state=2`；外送若配送为空则初始化为 0 |
| 2 | 3 | 已支付；外送已送达，自取/堂食无配送状态 | `state=3` + 完成副作用 |
| 0 | 4 | 未支付、无售后 | `state=4` |
| 1 | 4 | 未支付、无售后 | `state=4` |

其他跳转全部拒绝。已支付订单取消必须使用退款动作。

### 4.2 Delivery state

| Current | Target | Conditions | Result |
|---|---|---|---|
| NULL | 0 | 外送，`state=2` | 等待骑手接单 |
| 0 | 1 | 外送，已有 `qsId`，`state=2` | 骑手已接单 |
| 1 | 2 | 外送，已有 `qsId`，`state=2` | 配送中 |
| 2 | 3 | 外送，已有 `qsId`，已支付，无售后 | 配送已送达 + 订单完成 + 送达时间 + 账单 |

配送状态不得跳级或回退。相同值视为无变化。

## 5. Payment State Transitions

### 5.1 Offline `payType="1"`

```text
0 --confirm-payment--> 1
1 --confirm-refund--> 2
```

- 收款不自动完成订单；完成仍要满足订单/配送规则。
- 退款原子同步 `state=4` 和 `afterSaleStatus=3`。

### 5.2 OMG `payType="7"`

```text
order.payStatus 0 --callback/query + ledger CAS--> 1
order.payStatus 1 --real refund success--> 2
```

管理员请求体不能携带目标 `payStatus`。OMG 流水状态 4 期间订单仍显示已支付，并额外显示“退款结果待核对”。

对于无退款 API 的支付方式：创建未决人工记录时订单和支付流水保持已支付；管理员完成外部人工退款并二次确认后，支付流水通过既有 `1→4→3` CAS，新增 `action=NULL, rtnCode=1` 的完成记录，再同步订单三状态。

## 6. Composite Snapshot CAS

逻辑条件：

```sql
UPDATE pos_order
SET ...
WHERE id = :id
  AND state = :expectedState
  AND pay_status = :expectedPayStatus
  AND after_sale_status = :expectedAfterSaleStatus
  AND delivery_status <=> :expectedDeliveryStatus;
```

实现使用 MyBatis-Plus 条件构造器参数化生成；上面的 MySQL `<=>` 仅用于说明 NULL-safe 语义，不要求新增 XML SQL。

CAS 失败时：

- 不写订单日志；
- 不生成账单、不返积分、不推送；
- 返回业务冲突并要求重新加载 `status-context`。

## 7. Audit Content

日志内容保持可读且不超过 512 字：

```text
平台调整[订单/配送]：state 2→3，delivery 2→3，pay 1→1，afterSale 0→0；原因：骑手端漏操作
平台确认线下收款：pay 0→1；原因：门店已收现金
平台发起OMG补单：pay 0→1；原因：用户提供付款凭证
平台OMG退款成功：state 1→4，pay 1→2，afterSale 0→3；原因：商家无法履约
```

不得记录 OMG HashKey/HashIV、token、完整回调原文或其他秘密。

## 8. Database Migration

无数据库迁移，不修改 `updatesql/sql.md`。如测试发现现有生产表 `pos_order_log.content` 不是 512 字，应暂停实现并单独提出迁移评审，不在本功能中自动执行 DDL。
