# 006 - 订单状态流转设计（美团方案）

## 背景

系统新增自取订单（type=1）和堂食订单（type=2）后，订单不再需要骑手参与。现有 `state` 字段将支付状态、订单状态、配送状态、退款状态全部混在一起，导致自取/堂食订单无法直接复用。

### 旧状态定义（待废弃）

| 旧 state | 含义 | 类别 |
|----------|------|------|
| 0 | 未支付 | 支付 |
| 1 | 已付款 | 支付 |
| 2 | 已接单 | 订单 |
| 3 | 骑手接单 | 配送 |
| 4 | 配送中 | 配送 |
| 5 | 已完成 | 订单 |
| 6 | 申请退款 | 退款 |
| 7 | 同意退款 | 退款 |
| 8 | 拒绝退款 | 退款 |
| 9 | 客服接入 | 退款 |
| 10 | 作废 | 订单 |
| 11 | 售后完成 | 退款 |
| 12 | 送达 | 配送 |
| 13 | 退款处理中 | 退款 |

## 选定方案：全面重新设计，四字段分离

参考美团做法，将原来混在 `state` 中的四种关注点拆成四个独立字段：

- **state**：订单生命周期（所有类型共用）
- **deliveryStatus**：配送追踪（仅外送订单使用）
- **payStatus**：支付状态（所有类型共用）
- **afterSaleStatus**：售后/退款状态（独立于订单状态，参考美团做法）

同时废弃 `diningStatus` 字段，出餐动作由 `state` 状态变更表示。

## 新字段定义

### state（订单状态）— 重新编号

| 新 state | 含义 | 谁触发 | 说明 |
|----------|------|--------|------|
| 0 | 待处理 | 系统创建 | 订单已创建，等待商家接单 |
| 1 | 已接单 | 商家 | 商家已接单，备餐中 |
| 2 | 已出餐 | 商家 | 商家出餐完成 |
| 3 | 已完成 | 系统 | 外送：骑手送达；自取：用户确认取餐；堂食：备餐完成 |
| 4 | 已取消 | 系统/商家 | 作废/取消（全额退款、商家取消、系统超时取消） |

state 只有 5 个值，只跟踪订单的核心生命周期。

### deliveryStatus（配送状态）— 仅 type=0 外送订单

| deliveryStatus | 含义 | 谁触发 |
|----------------|------|--------|
| 0 | 待接单 | 系统（商家出餐后设为0，等待骑手） |
| 1 | 骑手已接单 | 骑手 |
| 2 | 配送中 | 骑手（取餐出发） |
| 3 | 已送达 | 骑手 |

自取（type=1）和堂食（type=2）订单 `deliveryStatus` 为 NULL。

### payStatus（支付状态）— 新增字段

| payStatus | 含义 | 说明 |
|-----------|------|------|
| 0 | 未支付 | 订单刚创建，等待付款 |
| 1 | 已支付 | 在线支付成功，或到付外送订单创建时直接设为1 |
| 2 | 已退款 | 订单退款完成 |

- **外送到付**（type=0, payType=1）：创建时直接设为 `payStatus=1`（不需要等在线支付）
- **自取/堂食现金**（type=1或2）：创建时 `payStatus=0`，商家收款并完成订单时同步设为 `1`
- **在线支付**（vnpay/zalopay/银行卡）：创建时 `payStatus=0`，支付回调成功后改为 `1`
- **退款完成**：`afterSaleStatus=3`（已退款）时，同步把 `payStatus` 改为 `2`

> **设计决策**：自取/堂食现金订单 payStatus 在下单时为 0，商家收款+完成时才设为 1。原因：商家需要明确知道"现金是否已收到"，收钱和完成是同一个动作。外送订单则不同，到付意味着骑手送货时收钱，所以创建时就确认支付方式。

### afterSaleStatus（售后状态）— 新增字段，独立于订单状态

参考美团做法：退款申请本身不改变订单状态，只有全额退款成功才将订单设为已取消。

| afterSaleStatus | 含义 | 谁触发 | 说明 |
|-----------------|------|--------|------|
| 0 | 无售后 | - | 订单正常，无退款申请 |
| 1 | 申请中 | 用户 | 用户发起退款申请，等待商家处理 |
| 2 | 退款中 | 系统/商家 | 商家同意退款，退款正在处理 |
| 3 | 已退款 | 系统 | 退款完成，全额退款时同步设 state=4 |
| 4 | 退款拒绝 | 商家 | 商家拒绝退款，订单继续正常流转 |
| 5 | 客服介入 | 客服 | 客服正在处理争议 |
| 6 | 售后完成 | 系统 | 售后流程结束 |

### 废弃字段

- **diningStatus**：不再使用。"出餐"动作由 `state` 从 1 变为 2 表示。

## 各类型状态流转

```
外送-在线支付 (type=0):
  state:          0 → 1 → 2 → 3
  deliveryStatus:          0 → 1 → 2 → 3
  payStatus:      0 → 1（支付回调）
  afterSaleStatus: 0（无售后时始终为0）
  骑手参与: 是（state 和 deliveryStatus 独立推进）
  完成触发: 骑手点"已送达"

外送-到付 (type=0, payType=1):
  state:          0 → 1 → 2 → 3
  deliveryStatus:          0 → 1 → 2 → 3
  payStatus:      1（创建时即确认，骑手送货时收现金）
  afterSaleStatus: 0
  骑手参与: 是
  完成触发: 骑手点"已送达"

自取-现金 (type=1):
  state:     0 → 1 → 2 → 3
  payStatus: 0 ──────────→ 1（商家收款+完成，同一操作）
  afterSaleStatus: 0
  骑手参与: 否（用户到店自取）
  deliveryStatus: 无
  完成触发: 商家点"完成"（收款+出餐）

堂食-现金 (type=2):
  state:     0 → 1 → 2 → 3
  payStatus: 0 ──────────→ 1（商家收款+完成，同一操作）
  afterSaleStatus: 0
  骑手参与: 否
  deliveryStatus: 无
  完成触发: 商家点"完成"（收款+出餐）
```

## 售后/退款流转（所有类型，独立状态机）

```
afterSaleStatus: 0(无售后) → 1(申请中) → 2(退款中) → 3(已退款)
                                         → 4(退款拒绝) → 5(客服介入) → 6(售后完成)
```

### 售后与订单状态的联动规则（参考美团）

| 售后动作 | afterSaleStatus | state | payStatus | deliveryStatus | 骑手行为 |
|---------|-----------------|-------|-----------|----------------|---------|
| 用户申请退款 | → 1(申请中) | **不变** | 不变 | 不变 | **继续配送** |
| 商家同意退款（全额） | → 2(退款中) | **→ 4(已取消)** | → 2(已退款) | 废弃 | **停止配送** |
| 商家同意退款（部分） | → 3(已退款) | **不变** | 视金额调整 | 不变 | **继续配送** |
| 商家拒绝退款 | → 4(退款拒绝) | **不变** | 不变 | 不变 | **继续配送** |
| 客服介入 | → 5(客服介入) | 不变 | 不变 | 不变 | 视客服裁决 |
| 用户撤销退款 | → 0(无售后) | 不变 | 不变 | 不变 | **继续配送** |

核心原则：退款申请本身不影响订单状态和配送，只有全额退款成功才取消订单。

### state=4（已取消）的触发场景

| 场景 | state | afterSaleStatus | 说明 |
|------|-------|-----------------|------|
| 商家主动取消 | → 4 | 0 | 未付款或刚接单时商家取消 |
| 系统超时自动取消 | → 4 | 0 | 商家超时未接单 |
| 全额退款成功 | → 4 | 3(已退款) | 退款完成后订单取消 |
| 用户未支付取消 | → 4 | 0 | 订单超时未支付 |

## 旧值 → 新值 数据迁移映射

| 旧 state | 旧含义 | 新 state | 新 payStatus | 新 deliveryStatus | 新 afterSaleStatus |
|----------|--------|----------|-------------|------------------|-------------------|
| 0 | 未支付 | 0 | 0 | NULL | 0 |
| 1 | 已付款 | 0 | 1 | NULL | 0 |
| 2 | 已接单 | 1 | 1 | NULL | 0 |
| 3 | 骑手接单 | 1 | 1 | 1 | 0 |
| 4 | 配送中 | 1 | 1 | 2 | 0 |
| 5 | 已完成 | 3 | 1 | 3（外送） | 0 |
| 6 | 申请退款 | 1 | 1 | NULL | 1 |
| 7 | 同意退款 | 4 | 2 | NULL | 3 |
| 8 | 拒绝退款 | 1 | 1 | NULL | 4 |
| 9 | 客服接入 | 1 | 1 | NULL | 5 |
| 10 | 作废 | 4 | 0 | NULL | 0 |
| 11 | 售后完成 | 4 | 2 | NULL | 6 |
| 12 | 送达 | 2 | 1 | 3 | 0 |
| 13 | 退款处理中 | 1 | 1 | NULL | 2 |

> 注：旧退款状态（6,8,9）迁移时新 state 推算为 1（已接单），因为退款前订单大概率在已接单阶段。历史数据够用即可。

## 前端展示逻辑

### 商家端订单列表 Tab

| 商家端 Tab | 筛选条件 |
|-----------|---------|
| 待受理 | state=0 AND (payStatus=1 OR type IN (1,2)) |
| 待出餐 | state=1 |
| 已出餐 | state=2 + afterSaleStatus=0 |
| 已完成 | state=3 + afterSaleStatus=0 |
| 已取消 | state=4 + afterSaleStatus=0 |
| 退款/售后 | afterSaleStatus > 0 |

> **设计决策**：待受理条件区分了外送和自取/堂食。外送订单必须 payStatus=1（已付款/到付确认）才出现在待受理；自取/堂食订单下单即可见（因为现金是线下收取的，不需要等在线支付）。

### 用户端订单列表 Tab

| 用户端 Tab | 筛选条件 |
|-----------|---------|
| 待付款 | payStatus=0 AND type=0（仅外送在线支付订单） |
| 进行中 | state IN (0,1,2) AND afterSaleStatus=0 AND (payStatus=1 OR type IN (1,2)) |
| 已完成 | state=3 + afterSaleStatus=0 |
| 已取消 | state=4 + afterSaleStatus=0 |
| 退款/售后 | afterSaleStatus > 0 |

> **设计决策**：待付款仅显示外送未支付订单。自取/堂食现金订单的 payStatus=0 但不显示在"待付款"（用户无需在线付款），直接进入"进行中"Tab。用户端 Tab 名从"待收货"改为"进行中"，因为自取/堂食不存在"收货"概念。

### 骑手端订单列表 Tab

| 骑手端 Tab | 筛选条件 |
|-----------|---------|
| 新任务 | type=0 + deliveryStatus=0 + state=2 + afterSaleStatus=0 |
| 待取货 | deliveryStatus=1 + qsId=当前骑手 + afterSaleStatus=0 |
| 配送中 | deliveryStatus=2 + qsId=当前骑手 + afterSaleStatus=0 |
| 已完成 | state=3 + afterSaleStatus=0 + qsId=当前骑手 |
| 已取消 | state=4 + qsId=当前骑手 + afterSaleStatus=0 |
| 退款/售后 | afterSaleStatus > 0 + qsId=当前骑手 |

### 前端展示给用户的文字（对齐美团精简方案）

用户只关心关键节点，多个内部状态合并为一个展示文字。前端根据字段组合判断，`afterSaleStatus` 优先。

**外送 (type=0) 用户看到的文字**

| 原来的文字 | 美团方案 | 对应字段条件 |
|-----------|---------|-------------|
| 待付款 | 待支付 | payStatus=0 |
| 待商家确认 + 商家备餐中 | 商家已接单 | state=0/1, payStatus=1, afterSaleStatus=0 |
| 待骑手配送 + 骑手已接单 + 配送中 | 配送中 | state=2, deliveryStatus=0/1/2 |
| 已送达 | 已送达 | state=2, deliveryStatus=3 |
| 已完成 | 已完成 | state=3 |
| 已取消 | 已取消 | state=4, afterSaleStatus=0 |
| 退款申请中/退款中/已退款 | 退款中/已退款 | afterSaleStatus=1/2/3 |

**自取 (type=1) 用户看到的文字**

| 美团方案 | 条件 |
|---------|------|
| 待支付 | payStatus=0 |
| 商家已接单 | state=0/1 |
| 待取餐 | state=2 |
| 已完成 | state=3 |
| 已取消 | state=4 |

**堂食 (type=2) 用户看到的文字**

| 美团方案 | 条件 |
|---------|------|
| 待支付 | payStatus=0 |
| 商家已接单 | state=0/1 |
| 备餐完成 | state=2 |
| 已完成 | state=3 |
| 已取消 | state=4 |

## 需要修改的代码

### 1. 数据库变更

```sql
-- 新增 delivery_status 字段
ALTER TABLE pos_order ADD COLUMN delivery_status BIGINT DEFAULT NULL COMMENT '配送状态：0待接单,1骑手已接单,2配送中,3已送达';

-- 新增 pay_status 字段
ALTER TABLE pos_order ADD COLUMN pay_status BIGINT DEFAULT 0 COMMENT '支付状态：0未支付,1已支付,2已退款';

-- 新增 after_sale_status 字段
ALTER TABLE pos_order ADD COLUMN after_sale_status BIGINT DEFAULT 0 COMMENT '售后状态：0无售后,1申请中,2退款中,3已退款,4退款拒绝,5客服介入,6售后完成';

-- 废弃 dining_status 字段
-- ALTER TABLE pos_order DROP COLUMN dining_status;  -- 视情况是否删除
```

### 2. 数据迁移

**不需要数据迁移**。系统未正式使用，历史订单数据由开发者手动清空。新字段（delivery_status, pay_status, after_sale_status）通过 ALTER TABLE 添加后，所有新订单直接使用新值。

> **注意**：如果将来需要迁移历史数据，不能用逐步 UPDATE 的方式（会有值冲突 bug），必须用单条 CASE WHEN 语句原子更新。

### 3. PosOrder 实体类

- 新增字段：`deliveryStatus`（Long）、`payStatus`（Long）、`afterSaleStatus`（Long）
- **废弃但保留的字段**：`diningStatus`、`isAccepted`、`kefuState`、`kefuContent`、`kefuRepeat`、`repeatDdId` — 新逻辑不再使用，保留在实体中避免序列化问题
  - `diningStatus` 由 `state` 值变更替代（state=2 即出餐）
  - `isAccepted` 由 `state=1`（已接单）替代
  - `kefuState` 等由 `afterSaleStatus` 替代（客服介入 = afterSaleStatus=5）

### 4. MyBatis Mapper（PosOrderMapper.xml）

- resultMap 新增 `delivery_status`、`pay_status`、`after_sale_status` 映射
- insert/update 新增对应字段

### 5. PosOrderShOprateController（商家操作）

- **接单**：`state` 从 0 改为 1
- **出餐**：`state` 从 1 改为 2
  - 外送订单：额外设置 `deliveryStatus=0`（等待骑手接单）
  - 自取/堂食订单：只改 `state=2`
- **完成（自取/堂食）**：`state` 从 2 改为 3，同时 `payStatus` 从 0 改为 1（收现金+完成，一个操作）
  - 仅限 type=1（自取）或 type=2（堂食）订单

### 6. PosOrderQsOprateController（骑手操作）

- `acceptOrder`：校验 type=0 且 afterSaleStatus=0，设置 `deliveryStatus=1`
- `pickupOrder`：校验 type=0 且 afterSaleStatus=0，设置 `deliveryStatus=2`
- `deliverOrder`：校验 type=0，设置 `deliveryStatus=3`，同时设置 `state=3`
- 所有骑手操作需检查 afterSaleStatus，有活跃退款申请时允许操作但需提示

### 7. 订单完成机制

参考美团做法：商家/骑手操作是主触发器，用户确认是可选的，自动完成是兜底。

| 类型 | 完成触发者 | 操作 | 自动完成兜底 |
|------|-----------|------|-------------|
| 外送 | 骑手 | 骑手点"已送达" → state=3, deliveryStatus=3 | 送达后 N 小时自动完成 |
| 自取 | 商家 | 商家点"完成" → state=3, payStatus=1 | 出餐后 N 小时自动完成 |
| 堂食 | 商家 | 商家点"完成" → state=3, payStatus=1 | 出餐后 N 小时自动完成 |

- **用户确认取餐**：可选的"确认取餐"按钮，仅限自取订单(type=1)且已支付(payStatus=1)的用户可操作，用户点了设 `state=3`。现金自取订单(payStatus=0)的用户无法自己确认取餐，只能等商家通过"完成"操作同时设置 state=3 和 payStatus=1。这样避免出现 state=3(已完成) 但 payStatus=0(未支付) 的矛盾状态。
- **定时任务兜底**：商家出餐（state=2）后超时未完成，系统自动设为 `state=3`

### 8. 订单创建（UserOrderController）

- 创建订单时设置 `state=0`、`afterSaleStatus=0`、`deliveryStatus=NULL`（所有类型），根据类型和支付方式设置 `payStatus`
  - 外送到付（type=0, payType=1）：`payStatus=1`
  - 自取/堂食现金（type=1或2）：`payStatus=0`（商家收款时设为1）
  - 在线支付：`payStatus=0`

### 9. 支付回调

- 支付成功后：`payStatus` 从 0 改为 1

### 10. 售后/退款操作（OrderAppealController 等）

> **本次不实现完整退款流程**。系统尚未接入在线支付，暂无退款场景。afterSaleStatus 字段预留，但只实现以下两个操作：
> - 用户确认取餐（自取已支付订单）：`state` → 3，仅限 type=1 且 payStatus=1 且 state=2 的订单
> - 用户取消订单（未接单前）：`state` → 4，`afterSaleStatus` 保持 0
> - 商家取消订单：`state` → 4，`afterSaleStatus` 保持 0
>
> 以下完整退款流程留待接入在线支付后实现：

- 用户申请退款：`afterSaleStatus` → 1，`state` 不变
- 商家同意退款（全额）：`afterSaleStatus` → 2 → 3，`state` → 4，`payStatus` → 2
- 商家同意退款（部分）：`afterSaleStatus` → 3，`state` 不变
- 商家拒绝退款：`afterSaleStatus` → 4，`state` 不变
- 客服介入：`afterSaleStatus` → 5
- 售后完成：`afterSaleStatus` → 6
- 用户撤销退款：`afterSaleStatus` → 0

### 11. 旧订单列表查询方法（废弃）

以下旧方法在数据迁移后自然废弃，不再修改。新方法见第 15 节。

- `PosOrderController.getstoreorderlist()` — 商家端订单列表（废弃，由 PosOrderShOprateController.orderList 替代）
- `PosOrderController.getorderlist()` — 用户端订单列表（废弃，由 UserOrderController.orderList 替代）
- `PosOrderController.getqsorderlist()` — 骑手端订单列表（废弃，由 PosOrderQsOprateController.orderList 替代）
- `PosOrderController.getqishouorderlist()` — 骑手抢单列表（废弃，由 PosOrderQsOprateController.orderList 的 newTask Tab 替代）
- `ShindexController` — 商家首页（废弃）
- `TableQrcodeController` — 堂食二维码订单（废弃）
- `PosOrderMapper` — 旧 SQL 中的 state 条件不再维护

### 12. 定时任务（TestTask.java）

- ~~旧自动完成逻辑（state=12→5）不再需要~~：骑手"已送达"操作直接设 `state=3 + deliveryStatus=3`，不存在需要从"送达"自动转为"已完成"的中间状态
- **新增自动完成兜底**：查 `state=2 AND afterSaleStatus=0` 且超时未完成，自动设 `state=3`（外送同时设 `deliveryStatus=3`，自取/堂食同时设 `payStatus=1`）
- 退款处理逻辑：原来查 `state=13` 改为查 `afterSaleStatus=2`（本次不实现）
- 作废逻辑：原来设 `state=10` 改为设 `state=4`

### 13. 骑手约束逻辑

- 防止骑手同时接多单：原来查 `state IN (3,4)` 改为查 `deliveryStatus IN (1,2)`
- `RiderPositionMapper` 排除有活跃订单的骑手：条件同步更新
- 有退款申请的订单（afterSaleStatus>0）仍允许骑手操作，直到全额退款成功

### 14. 推送通知

- 推送消息中的 state 值需对应新值
- 根据 `type + state + deliveryStatus + afterSaleStatus` 组合发送不同内容
- 退款相关推送应基于 afterSaleStatus 而非 state

## 15. 新订单列表接口设计

### 设计原则

- **旧方法不动**：PosOrderController 中旧的 getstoreorderlist/getorderlist/getqsorderlist/getqishouorderlist 等方法保留代码不删，数据迁移后自然废弃
- **新方法写在三个 Controller 中**：用户端写 UserOrderController，骑手端写 PosOrderQsOprateController，商家端写 PosOrderShOprateController
- **使用 LambdaQueryWrapper**：三个端统一用 LambdaQueryWrapper 拼查询条件
- **Tab 参数**：前端传 `tab` 字符串参数（如 `pending`、`active`、`completed`），后端映射到四字段查询条件
- **状态展示文字由前端计算**：后端只返回 state/deliveryStatus/payStatus/afterSaleStatus/type 字段，前端根据这些字段组合自行决定显示文案
- **分页返回 total**：新接口统一返回 `{ records, total, page, size }` 格式

### 前置条件

以下变更必须在列表接口开发前完成：
1. PosOrder 实体添加 `deliveryStatus`、`payStatus`、`afterSaleStatus` 字段
2. PosOrderMapper.xml resultMap 添加三个新字段映射，insert/update 语句同步更新
3. 数据库执行 ALTER TABLE 添加三个新列（第1节 SQL 脚本）
4. 历史订单数据手动清空（第2节：不需要迁移脚本）
5. PosOrder 已有 `longitude`（经度）和 `latitude`（纬度）字段，骑手距离排序可直接使用

### 15.1 用户端订单列表

**位置**：`UserOrderController.java`（路径前缀 `/system/userOrder`）
**接口**：`GET /system/userOrder/orderList`

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| token | Header | 是 | 用户身份 |
| page | int | 是 | 页码，从1开始 |
| size | int | 是 | 每页条数 |
| tab | String | 是 | Tab 标识 |
| type | String | 否 | 订单类型筛选：0 外送 / 1 自取 / 2 堂食，不传则查所有类型 |

**Tab 映射**：

| tab 值 | 查询条件 | 说明 |
|--------|---------|------|
| `unpaid` | `payStatus=0 AND type=0` | 待付款（仅外送在线支付订单） |
| `active` | `state IN (0,1,2) AND afterSaleStatus=0 AND (payStatus=1 OR type IN (1,2))` | 进行中 |
| `completed` | `state=3 AND afterSaleStatus=0` | 已完成 |
| `cancelled` | `state=4 AND afterSaleStatus=0` | 已取消 |
| `refund` | `afterSaleStatus > 0` | 退款/售后 |

**返回格式**：复用旧接口 `getorderlist()` 的返回格式。PosOrder 全字段自动序列化（含 `deliveryStatus`、`payStatus`、`afterSaleStatus` 新字段），额外附加以下关联数据：

| 附加字段 | 来源 | 说明 |
|---------|------|------|
| `ddId` | PosOrder.ddId | 转为字符串 |
| `shanghu` | InfoUser(shId) | 商家用户信息 |
| `store` | PosStore(mdId) | 门店信息 |
| `shaddress` | InfoAddress(shdzId) 或 JSON解析 | 收货地址 |
| `user` | InfoUser(userId) | 用户信息 |
| `food` | PosOrder.food | JSON 解析为商品列表 |
| `parentRemarks` | OrderParent | 父订单备注 |

骑手信息只返回 PosOrder 自带的 `qsId` 和 `qsImg`，不做额外 InfoUser 查询。

**返回示例**：

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1,
        "ddId": "202605150001",
        "type": 0,
        "state": 1,
        "deliveryStatus": null,
        "payStatus": 1,
        "afterSaleStatus": 0,
        "qsId": null,
        "qsImg": null,
        "mdId": 100,
        "amount": 50000,
        "cretim": "2026-05-15 12:00:00",
        "shanghu": { "userId": 50, "nickName": "商家A" },
        "store": { "id": 100, "mdName": "测试门店" },
        "shaddress": { "id": 1, "address": "..." },
        "user": { "userId": 1, "nickName": "用户A" },
        "food": [{ "foodName": "菜品1", "num": 2 }],
        "parentRemarks": ""
      }
    ],
    "total": 100,
    "page": 1,
    "size": 10
  }
}
```

### 15.2 商家端订单列表

**位置**：`PosOrderShOprateController.java`（路径前缀 `/system/orderShOprate`）
**接口**：`GET /system/orderShOprate/orderList`

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| token | Header | 是 | 商家身份 |
| page | int | 是 | 页码，从1开始 |
| size | int | 是 | 每页条数 |
| tab | String | 是 | Tab 标识 |
| mdId | String | 是 | 门店ID |
| type | String | 否 | 订单类型筛选：0 外送 / 1 自取 / 2 堂食，不传则显示全部类型 |

**Tab 映射**：

| tab 值 | 查询条件 | 说明 |
|--------|---------|------|
| `pending` | `state=0 AND (payStatus=1 OR type IN (1,2)) AND mdId=?` | 待受理 |
| `preparing` | `state=1 AND mdId=?` | 待出餐 |
| `ready` | `state=2 AND afterSaleStatus=0 AND mdId=?` | 已出餐 |
| `completed` | `state=3 AND afterSaleStatus=0 AND mdId=?` | 已完成 |
| `cancelled` | `state=4 AND afterSaleStatus=0 AND mdId=?` | 已取消 |
| `refund` | `afterSaleStatus > 0 AND mdId=?` | 退款/售后 |

**返回格式**：PosOrder 全字段序列化 + 新字段自动包含。返回 `{ records, total, page, size }` 格式。

### 15.3 骑手端订单列表

**位置**：`PosOrderQsOprateController.java`（路径前缀 `/system/orderQsOprate`）
**接口**：`GET /system/orderQsOprate/orderList`

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| token | Header | 是 | 骑手身份（JwtUtil.getusid(token) 即为 qsId） |
| page | int | 是 | 页码，从1开始 |
| size | int | 是 | 每页条数 |
| tab | String | 是 | Tab 标识 |
| longitude | BigDecimal | 条件必填 | 骑手当前经度（newTask Tab 必填） |
| latitude | BigDecimal | 条件必填 | 骑手当前纬度（newTask Tab 必填） |

**Tab 映射**：

| tab 值 | 查询条件 | 排序 |
|--------|---------|------|
| `newTask` | `type=0 AND deliveryStatus=0 AND state=2 AND afterSaleStatus=0` | 距离 ASC（使用 PosOrder.longitude/latitude 计算到骑手的距离） |
| `toPickup` | `deliveryStatus=1 AND qsId=当前骑手ID AND afterSaleStatus=0` | 时间 ASC |
| `delivering` | `deliveryStatus=2 AND qsId=当前骑手ID AND afterSaleStatus=0` | 时间 ASC |
| `completed` | `state=3 AND afterSaleStatus=0 AND qsId=当前骑手ID` | 时间 DESC |
| `cancelled` | `state=4 AND qsId=当前骑手ID AND afterSaleStatus=0` | 时间 DESC |
| `refund` | `afterSaleStatus > 0 AND qsId=当前骑手ID` | 时间 DESC |

**距离排序实现**：

```java
wrapper.last("ORDER BY ST_Distance_Sphere(point(longitude, latitude), " +
             "point(" + lng + ", " + lat + ")) ASC");
```

**返回格式**：PosOrder 全字段序列化 + 新字段自动包含。返回 `{ records, total, page, size }` 格式。

### 15.4 设计决策记录

以下问题在需求讨论中确认，记录备查：

| # | 问题 | 决策 | 原因 |
|---|------|------|------|
| 1 | 旧方法如何处理 | 保留代码不删，数据迁移后自然废弃 | 系统未正式使用，旧方法不再维护 |
| 2 | Tab 参数格式 | 用有意义的字符串（pending/active 等） | 参考美团做法，语义清晰，与旧接口 z01 模式完全独立 |
| 3 | 用户端返回格式 | 复用旧接口格式 + 新字段自动包含 | 前端切换成本低，新字段通过 PosOrder 序列化自动包含 |
| 4 | 骑手信息 | 只返回 qsId + qsImg | 和旧接口一致，不做额外 InfoUser 查询 |
| 5 | 已取消订单归属 | 三个端都新增 `cancelled` Tab | 参考美团做法，已取消订单（state=4, afterSaleStatus=0）需要独立 Tab 承接，否则无任何 Tab 显示 |
| 6 | 商家端 Tab 数量 | 从 4 个扩充到 6 个（加 completed + cancelled） | 参考美团商家端有"已完成"和"已取消"分类 |
| 7 | 骑手距离排序 | 用 LambdaQueryWrapper.last() 追加原始 SQL | PosOrder 已有 longitude/latitude 字段，无需 JOIN PosStore |
| 8 | 状态展示文字 | 前端根据 type+state+deliveryStatus+afterSaleStatus 组合自行计算 | 后端只返回数据，不返回 statusText |
| 9 | 分页格式 | 返回 { records, total, page, size } | 前端需要 total 做分页控件 |
| 10 | 类型筛选 | 用户端和商家端都支持可选的 type 参数 | 商家可能需要区分外送/自取/堂食订单 |
| 11 | 数据迁移策略 | 四步骤一次性执行（加字段+填充+迁移state） | 系统未正式使用，无需分步兼容 |
| 12 | 自取/堂食现金 payStatus | 创建时 payStatus=0，商家收款+完成时设为1 | 商家需要明确知道现金是否已收到，收钱和完成是同一动作 |
| 13 | 待受理 Tab 条件 | state=0 AND (payStatus=1 OR type IN (1,2)) | 自取/堂食下单即可见，外送需先付款 |
| 14 | 用户端"待付款"Tab | payStatus=0 AND type=0 | 自取/堂食现金无需在线付款，不显示在待付款 |
| 15 | 订单完成机制 | 商家/骑手操作为主触发，用户确认为可选，定时任务兜底 | 参考美团：不依赖用户点确认 |
| 16 | 数据迁移 | 不需要，历史数据手动清空 | 系统未正式使用 |
| 17 | 退款流程 | 本次不实现，afterSaleStatus 字段预留 | 尚未接入在线支付，无退款场景 |
| 18 | 旧字段处理 | diningStatus/isAccepted/kefuState 等保留但废弃 | 新逻辑不再使用，避免序列化问题 |
| 19 | 商家操作步骤 | 接单（state 0→1）和出餐（state 1→2）分两步 | 参考美团商家端流程 |

## 16. 平台管理端（foodie-admin-vue）调整

### 16.1 订单列表页（src/views/system/order/index.vue）

**位置**：`E:\QtwCode\foodie\foodie-admin-vue\src\views\system\order\index.vue`
**后端接口**：`/system/order/list`（保留现有接口，后端需支持新的筛选参数）

#### 筛选条件变更

原来用单个下拉框筛选旧 state（0-12），改为四个独立下拉框：

| 筛选字段 | 下拉选项 | 默认 |
|---------|---------|------|
| state | 全部 / 待处理(0) / 已接单(1) / 已出餐(2) / 已完成(3) / 已取消(4) | 全部 |
| payStatus | 全部 / 未支付(0) / 已支付(1) / 已退款(2) | 全部 |
| deliveryStatus | 全部 / 待接单(0) / 骑手已接单(1) / 配送中(2) / 已送达(3) / 不适用(NULL) | 全部 |
| afterSaleStatus | 全部 / 无售后(0) / 申请中(1) / 退款中(2) / 已退款(3) / 退款拒绝(4) / 客服介入(5) / 售后完成(6) | 全部 |

保留现有的订单号、用户ID、订单类型、创建时间筛选条件不变。

#### 状态列变更

原来单个 `el-tag` 显示旧 state，改为两个 tag 组合显示——主状态 + 副状态：

```
| 订单状态列 |
|-----------|
| [待处理] [已支付]          ← state=0, payStatus=1 |
| [已接单]                  ← state=1 |
| [已出餐] [配送中]          ← state=2, deliveryStatus=2 |
| [已出餐] [待骑手]          ← state=2, deliveryStatus=0 |
| [已完成]                  ← state=3 |
| [已取消]                  ← state=4, afterSaleStatus=0 |
| [已取消] [已退款]          ← state=4, afterSaleStatus=3 |
| [已接单] [退款申请中]      ← state=1, afterSaleStatus=1 |
```

规则：
- **主 tag**：根据 state 显示（待处理/已接单/已出餐/已完成/已取消）
- **副 tag**：根据订单类型和附加状态显示
  - 外送(type=0)：显示 deliveryStatus（待骑手/骑手已接单/配送中/已送达）
  - 所有类型：显示 payStatus（仅未支付/已退款时显示，已支付不显示避免冗余）
  - 所有类型：显示 afterSaleStatus（仅 >0 时显示）
- 已取消的订单主 tag 用红色(`type="danger"`)

#### 修改订单弹窗

原来用 `sys_order_type` 字典的 radio 选择旧 state 值。改为：
- 只能修改 `state` 字段（0-4 的下拉选择）
- 平台管理员通常不应该直接改状态，如确需保留此功能，下拉选项为：待处理(0) / 已接单(1) / 已出餐(2) / 已完成(3) / 已取消(4)

### 16.2 订单详情弹窗

#### 进度条变更

原来7步进度条（未支付→已付款→已接单→取餐中→配送中→已完成→异常），改为4步：

```
待处理(0) → 已接单(1) → 已出餐(2) → 已完成(3)
```

- 已取消(state=4)的订单不显示进度条，改为显示红色 `已取消` 标签
- `:active` 绑定 `orxiangq.state`（0-3 直接对应步骤索引）

#### 新增状态信息区域

在进度条下方，增加三个状态信息行（仅在对应值非默认时显示）：

| 显示条件 | 内容 |
|---------|------|
| payStatus != 1 | 支付状态：未支付(payStatus=0) / 已退款(payStatus=2) |
| type == 0 且 deliveryStatus != null | 配送状态：待接单(0) / 骑手已接单(1) / 配送中(2) / 已送达(3) |
| afterSaleStatus > 0 | 售后状态：申请中(1) / 退款中(2) / 已退款(3) / 退款拒绝(4) / 客服介入(5) / 售后完成(6) |

### 16.3 后端接口调整

现有 `/system/order/list` 接口需支持新的筛选参数：
- 新增 `state`、`payStatus`、`deliveryStatus`、`afterSaleStatus` 查询参数
- 多个筛选条件同时传入时为 AND 关系
- `deliveryStatus` 需支持 NULL 筛选（自取/堂食订单）
- 旧 `sys_order_type` 字典可废弃

### 16.4 字典数据

新增以下字典或在前端硬编码（推荐前端硬编码，因为值固定不变）：

| 字段 | 选项 |
|------|------|
| state | [{value:0, label:'待处理'}, {value:1, label:'已接单'}, {value:2, label:'已出餐'}, {value:3, label:'已完成'}, {value:4, label:'已取消'}] |
| payStatus | [{value:0, label:'未支付'}, {value:1, label:'已支付'}, {value:2, label:'已退款'}] |
| deliveryStatus | [{value:0, label:'待接单'}, {value:1, label:'骑手已接单'}, {value:2, label:'配送中'}, {value:3, label:'已送达'}] |
| afterSaleStatus | [{value:0, label:'无售后'}, {value:1, label:'申请中'}, {value:2, label:'退款中'}, {value:3, label:'已退款'}, {value:4, label:'退款拒绝'}, {value:5, label:'客服介入'}, {value:6, label:'售后完成'}] |

推荐前端硬编码而非字典表，因为这些值由代码逻辑定义，不会由运营人员修改。

## 美团订单完成机制参考

美团各类型订单的完成机制：

- **外送**：骑手点"已送达"后启动自动完成倒计时（通常24小时），到期自动标记已完成。用户可提前手动确认收货，但不点确认不影响完成。
- **自取**：商家点"出餐完成"后启动倒计时，到期自动完成。用户到店取餐可在 App 中扫码确认，但非必须。
- **堂食**：商家出餐后自动倒计时完成。无需用户额外操作。

核心原则：**商家/骑手操作是主触发器，用户确认是可选的，自动完成是兜底。** 用户不确认不会导致订单永远"挂着"。

## 2026-08-24 增量：自取配送费与出餐推送

### 自取配送费

- `type=1` 的自取订单配送费必须由后端固定为 `0`，不得信任或保存客户端传入的非零 `freight`。
- 该规则覆盖当前仍有效的全部下单入口：用户聚合下单、商家端下单及 `/system/order/addorder`。
- `type=0` 外送和 `type=2` 堂食订单保持现有配送费处理，不在本次调整中改变计价方式。
- 本项目不新增包装费字段或包装费计算规则。

### 自取出餐推送

- 商家将 `type=1` 自取订单从 `state=1` 推进到 `state=2` 时，必须通过当前用户推送通道发送专属通知。
- 专属文案含义为“您的餐品已备好，请凭取餐码 {0} 到店领取”，其中 `{0}` 使用订单当前 `pickUpNum`，不改变现有按门店、按天递增的取餐码生成方式。
- 专属文案必须覆盖简体中文、繁体中文、英文、越南语以及默认资源文件。
- 外送和堂食继续使用现有通用出餐通知，不改变既有推送行为。
- 本次只使用项目当前 App 推送及 `push_message` 入库通道，不新增短信通知，也不新增取餐码核销接口。

### 验收场景

1. 自取订单无论客户端传入 `freight=null`、`0` 或非零值，最终保存的 `freight` 均为 `0`。
2. 外送和堂食订单仍保存各自原有配送费值。
3. 自取订单出餐时，用户收到包含当前取餐码的专属到店领取通知。
4. 外送和堂食订单出餐时，仍收到原有通用出餐通知。

## 2026-08-28 增量：外送订单由骑手先接单

本节覆盖本规格中原有“商家出餐后开放给骑手”的外送流程；自取和堂食流程不变。

### 外送状态流转

1. 外送订单创建时写入 `state=0`、`deliveryStatus=0`、`afterSaleStatus=0`。
2. 到付订单创建后立即开放给骑手；在线支付订单必须在支付成功、`payStatus=1` 后才开放。
3. 骑手仅可对 `state=0`、`deliveryStatus=0`、`payStatus=1` 且无售后的订单接单，接单后写入 `deliveryStatus=1` 和 `qsId`，`state` 保持为 0。
4. 外送订单只有在 `deliveryStatus=1` 且 `qsId` 有值时，商家才可接单（`state=0→1`）或出餐（`state=1→2`）。商家出餐不得把配送状态重置为 0。
5. 骑手取餐仍要求 `state=2`、`deliveryStatus=1`，完成后进入 `deliveryStatus=2`；送达后进入 `deliveryStatus=3`、`state=3`。

### 推送规则

- 订单开放时通知附近可接单骑手，不提前通知商家备餐。
- 骑手接单后通知用户，并通知商家“骑手已接单，可以确认并备餐”。
- 商家出餐后通知用户及已分配骑手。
- 已分配骑手的订单被用户或商家取消时，通知除取消人外的相关用户、商家和骑手；取消订单不得继续占用骑手的可接单资格。

### 验收场景

1. 未支付在线外送订单不出现在骑手新任务列表，支付成功后才出现并触发骑手推送。
2. 到付外送订单创建后立即出现在骑手新任务列表并触发骑手推送。
3. 未分配骑手时，商家接单和出餐均被拒绝；骑手接单后商家可按顺序接单、出餐。
4. 商家出餐后 `deliveryStatus` 仍为 1，已接单骑手可继续取餐和送达。
5. 订单取消后，骑手可继续接取其他订单，并能在取消订单列表查看原订单。

## 参考资源

- [美团外卖开放平台 — 订单状态](https://developer.waimai.meituan.com/home/doc/market/100)
- [美团外卖开放平台 — 订单退款与取消](https://developer.waimai.meituan.com/home/questionDetail/6295)
- [美团外卖开放平台 — 订单确认退款请求](https://developer.waimai.meituan.com/home/docDetail/122)
- [美团外卖订单中心演进](https://tech.meituan.com/2016/09/09/mt-waimai-order-evolution.html)
