# API Contract: 平台订单状态受控调整

**Base path**: `/system/order`
**Authentication**: RuoYi 管理后台 Bearer token
**Permission**: `system:order:edit`
**Content-Type**: `application/json`

## 1. Common Conventions

### 1.1 Response envelope

沿用 `AjaxResult`：

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {}
}
```

业务校验、状态冲突或网关失败沿用项目约定返回非 200 的业务 `code` 与安全错误消息。前端不得只根据 HTTP 状态推断业务成功。

### 1.2 Snapshot fields

所有写操作必须携带弹窗打开时的四状态快照：

```json
{
  "expectedState": 2,
  "expectedDeliveryStatus": 2,
  "expectedPayStatus": 1,
  "expectedAfterSaleStatus": 0,
  "reason": "骑手端漏操作"
}
```

`reason` trim 后 1～200 字。`expectedDeliveryStatus` 可为 `null`，其他快照字段必填。

### 1.3 Conflict behavior

如果订单在上下文读取后被回调、骑手、定时任务或另一管理员更新：

```json
{
  "code": 500,
  "msg": "订单状态已变化，请刷新后重试"
}
```

冲突请求不得产生订单、账单、积分、日志或推送副作用。

## 2. Get Status Context

### `GET /system/order/{id}/status-context`

读取修改弹窗所需的最小数据和服务端能力。

**Response**:

```json
{
  "code": 200,
  "data": {
    "id": 123,
    "ddId": "202608100001",
    "type": 0,
    "payType": "7",
    "state": 2,
    "deliveryStatus": 2,
    "payStatus": 1,
    "afterSaleStatus": 0,
    "sdTime": null,
    "riderAssigned": true,
    "allowedOrderStates": [2],
    "allowedDeliveryStatuses": [2, 3],
    "canConfirmOfflinePayment": false,
    "canConfirmOfflineRefund": false,
    "canReconcileOmg": false,
    "canRefundOmg": true,
    "canConfirmManualOmgRefund": false,
    "omgPaymentStatus": 1,
    "manualRefundPending": false,
    "refundUnknown": false
  }
}
```

规则：

- `allowed*` 至少包含当前值，终态只有当前值；
- 自取/堂食 `allowedDeliveryStatuses=[]`；
- OMG 无流水时 `omgPaymentStatus=null` 且所有 OMG 写能力为 false；
- 能力字段只控制显示，不能作为后端授权依据。

## 3. Update Order or Delivery Status

### `PUT /system/order/{id}/status`

**Annotations**: `@PreAuthorize(system:order:edit)`、`@RepeatSubmit`

**Request**:

```json
{
  "expectedState": 2,
  "expectedDeliveryStatus": 2,
  "expectedPayStatus": 1,
  "expectedAfterSaleStatus": 0,
  "targetState": null,
  "targetDeliveryStatus": 3,
  "reason": "骑手已实际送达，客户端漏操作"
}
```

至少一个目标字段非空且与当前值不同。未提交的目标字段保持不变。

**Success response**: 返回最新状态上下文。

```json
{
  "code": 200,
  "msg": "修改成功",
  "data": {
    "id": 123,
    "state": 3,
    "deliveryStatus": 3,
    "payStatus": 1,
    "afterSaleStatus": 0,
    "sdTime": "2026-08-10 11:30:00"
  }
}
```

### Validation

- `targetState` 只能是 0～4；`targetDeliveryStatus` 只能是 0～3；
- 只能执行 [data-model.md](../data-model.md) 中的下一步转换；
- 非外送不得提交配送目标；
- 配送到 1/2/3 要求已有骑手；
- 完成要求已支付且无售后；
- 已支付取消返回“请使用退款操作”；
- 送达自动同步完成和送达时间；客户端不能提交 `sdTime`。

## 4. Confirm Offline Payment

### `POST /system/order/{id}/offline-payment/confirm`

**Request**: Common snapshot fields only.

```json
{
  "expectedState": 2,
  "expectedDeliveryStatus": null,
  "expectedPayStatus": 0,
  "expectedAfterSaleStatus": 0,
  "reason": "门店确认已收到现金"
}
```

**Allowed**:

- `payType="1"`；
- `payStatus=0`；
- `state` 不是 3/4；
- `afterSaleStatus=0`。

**Result**: 只把 `payStatus` 改为 1，不创建 OMG/历史在线支付流水，不自动完成订单。

## 5. Confirm Offline Refund

### `POST /system/order/{id}/offline-refund/confirm`

此操作代表管理员已经在线下实际退回款项，前端必须先显示不可逆二次确认。

**Request**:

```json
{
  "expectedState": 1,
  "expectedDeliveryStatus": null,
  "expectedPayStatus": 1,
  "expectedAfterSaleStatus": 0,
  "reason": "门店无法履约，已现场退还现金"
}
```

**Allowed**:

- `payType="1"`；
- `payStatus=1`；
- `state!=3`；
- `afterSaleStatus=0`。

**Atomic result**:

```json
{
  "state": 4,
  "payStatus": 2,
  "afterSaleStatus": 3
}
```

订单使用积分时调用现有幂等积分返还逻辑。已完成订单返回“已完成订单需结算冲正，本期不支持退款”。

## 6. Reconcile OMG Payment

### `POST /system/order/{id}/omg-payment/reconcile`

**Request**: Common snapshot fields.

**Allowed**:

- `payType="7"`；
- `payStatus=0`；
- `state!=4`；
- 存在最新 OMG 支付流水且流水可查询。

**Behavior**:

1. 复用 `reconcileByQuery(ddId, "admin")`；
2. 校验 OMG 返回交易状态、交易号、金额和日期；
3. 只有真实已支付时通过流水 CAS 和订单 CAS 核销；
4. 未支付、失败、未知或字段不符时不得写 `order.payStatus=1`；
5. 回调和补单并发只执行一次支付副作用。

**Paid response**:

```json
{
  "code": 200,
  "msg": "OMG 支付核验成功",
  "data": {
    "payStatus": 1,
    "reconciled": true
  }
}
```

**Still unpaid response**:

```json
{
  "code": 200,
  "msg": "OMG 尚未确认支付",
  "data": {
    "payStatus": 0,
    "reconciled": false
  }
}
```

金额/订单信息不一致返回错误并写系统核对日志，不向前端返回原始网关报文。

## 7. Refund OMG Payment

### `POST /system/order/{id}/omg-refund`

前端必须显示不可逆二次确认。

**Request**: Common snapshot fields.

**Allowed**:

- `payType="7"`；
- `payStatus=1`；
- `state!=3`；
- `afterSaleStatus=0`；
- 最新 OMG 流水为已支付，金额和订单一致。

### Outcomes

#### Automatic refund succeeded

```json
{
  "code": 200,
  "msg": "退款成功",
  "data": {
    "outcome": "REFUNDED",
    "state": 4,
    "payStatus": 2,
    "afterSaleStatus": 3
  }
}
```

#### Manual refund required

```json
{
  "code": 200,
  "msg": "该支付方式需在 OMG 后台人工退款，订单暂保持已支付",
  "data": {
    "outcome": "MANUAL_PENDING",
    "payStatus": 1,
    "manualRefundPending": true
  }
}
```

重复调用不重复新增人工待办。

#### Result unknown

```json
{
  "code": 500,
  "msg": "OMG 退款结果待确认，请勿重复发起",
  "data": {
    "outcome": "UNKNOWN",
    "payStatus": 1,
    "refundUnknown": true
  }
}
```

OMG 流水保持退款中，后续请求不得再次调用网关。

#### Explicit failure

```json
{
  "code": 500,
  "msg": "OMG 退款失败，请核对后重试",
  "data": {
    "outcome": "FAILED",
    "payStatus": 1
  }
}
```

错误消息不包含 HashKey、HashIV、完整响应或堆栈。

## 8. Confirm Manual OMG Refund

### `POST /system/order/{id}/omg-refund/manual-confirm`

此接口只用于支付方式不支持 OMG 退款 API、平台管理员已经在 OMG 后台核实退款完成的场景。前端必须进行不可逆二次确认。

**Request**: Common snapshot fields；`reason` 必须说明人工退款核实依据。

**Allowed**:

- `payType="7"`、订单 `payStatus=1`、`state!=3`、`afterSaleStatus=0`；
- 最新 OMG 流水属于不支持自动退款的支付方式；
- 同一支付流水存在 `action=NULL, rtnCode=NULL` 的未决人工待办；
- 不存在 `action=NULL, rtnCode=1` 的人工完成记录。

**Behavior**:

1. 管理员二次确认；
2. OMG 支付流水复用现有 CAS 执行 `1→4→3`，不调用网关；
3. 插入 `action=NULL, rtnCode=1` 的人工退款完成记录，`rtnMsg` 标明管理员确认；
4. 同步 `state=4, payStatus=2, afterSaleStatus=3`；
5. 写管理员操作日志，包含管理员、前后状态和原因。

**Response**:

```json
{
  "code": 200,
  "msg": "已确认 OMG 人工退款完成",
  "data": {
    "outcome": "REFUNDED",
    "state": 4,
    "payStatus": 2,
    "afterSaleStatus": 3,
    "manualRefundPending": false
  }
}
```

无待办、已完成订单、流水状态变化或重复确认均不得产生新的退款记录或业务副作用。

## 9. Legacy Endpoint Hardening

### `PUT /system/order`

继续保留历史非状态编辑能力，但请求含以下任意字段时拒绝：

- `state`
- `deliveryStatus`
- `payStatus`
- `afterSaleStatus`
- `sdTime`

**Response**:

```json
{
  "code": 500,
  "msg": "订单状态请使用状态专用操作"
}
```

旧 `state=7/12`、`kefuState→state=5` 等逻辑必须删除。

## 10. Forbidden Payload Fields

新状态接口 DTO 不声明并不得使用以下字段：

```text
amount, userId, shId, mdId, food, freight, points,
payType, type, qsId, sdTime, payStatus, afterSaleStatus
```

其中 `expectedPayStatus`、`expectedAfterSaleStatus` 仅作为旧快照，不是目标值。服务端必须从数据库读取订单类型、支付类型、金额、骑手和业务标识。

## 11. Audit Requirements

每次实际改变数据的管理员操作写一条 `operatorType=1` 的同步订单日志，包含：

- 管理员 ID/名称；
- 动作类型；
- 四状态前后值；
- 管理员原因；
- 服务端时间。

相同目标的幂等请求不重复写成功日志。OMG 补单/退款还保留现有 `operatorType=0` 系统资金日志。
