qmj 1 месяц назад
Родитель
Сommit
349ebfda6c
6 измененных файлов с 710 добавлено и 61 удалено
  1. 9 0
      CLAUDE.md
  2. 684 0
      specs/006-orderstate/spec.md
  3. 0 20
      sql/table_qrcode.sql
  4. 0 2
      sql/user_wallet_add_store_id.sql
  5. 0 39
      update-sql/appealMenu.sql
  6. 17 0
      updatesql/sql.md

+ 9 - 0
CLAUDE.md

@@ -44,6 +44,15 @@ Auto-generated from all feature plans. Last updated: 2026-04-29
 
 所有展示给越南用户的货币/金额必须使用**整数格式**(无小数位),因为越南盾不使用分。Java 计算使用 `.setScale(0, RoundingMode.HALF_UP)`,JS 格式化使用 `Math.floor()` 或 `parseInt()`。
 
+## 数据库变更管理
+
+所有数据库变更(ALTER TABLE、数据迁移等)**不直接执行**,必须写入 `updatesql/sql.md` 文件。由开发者统一到数据库手动执行。SQL 语句需标注日期和用途注释,例如:
+
+```sql
+-- 2026-05-15 订单状态四字段分离
+ALTER TABLE pos_order ADD COLUMN delivery_status BIGINT DEFAULT NULL COMMENT '配送状态';
+```
+
 ## 全栈字段添加清单
 
 添加新字段到实体时,必须按顺序更新以下所有层级:

+ 684 - 0
specs/006-orderstate/spec.md

@@ -0,0 +1,684 @@
+# 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=当前骑手 |
+
+### 前端展示给用户的文字
+
+| type | state | deliveryStatus | payStatus | afterSaleStatus | 展示给用户 |
+|------|-------|----------------|-----------|-----------------|-----------|
+| 0(外送) | 0 | - | 0 | 0 | 待付款 |
+| 0(外送) | 0 | - | 1 | 0 | 待商家确认 |
+| 0(外送) | 1 | - | 1 | 0 | 商家备餐中 |
+| 0(外送) | 2 | 0 | 1 | 0 | 待骑手配送 |
+| 0(外送) | 2 | 1 | 1 | 0 | 骑手已接单 |
+| 0(外送) | 2 | 2 | 1 | 0 | 配送中 |
+| 0(外送) | 2 | 3 | 1 | 0 | 已送达 |
+| 0(外送) | 3 | - | 1 | 0 | 已完成 |
+| 0(外送) | * | * | * | 1 | 退款申请中 |
+| 0(外送) | * | * | * | 2 | 退款中 |
+| 0(外送) | 4 | - | 2 | 3 | 已退款 |
+| 0(外送) | 4 | - | 0 | 0 | 已取消 |
+| 0(外送) | 4 | - | 1 | 0 | 已取消 |
+| 0(外送) | * | * | * | 4 | 退款已拒绝 |
+| 0(外送) | * | * | * | 5 | 客服介入中 |
+| 0(外送) | * | * | * | 6 | 售后完成 |
+| 1(自取) | 0 | - | 0 | 0 | 待商家确认 |
+| 1(自取) | 0 | - | 1 | 0 | 待商家确认 |
+| 1(自取) | 1 | - | 1 | 0 | 商家备餐中 |
+| 1(自取) | 2 | - | 1 | 0 | 待取餐(请到店取餐) |
+| 1(自取) | 3 | - | 1 | 0 | 已完成 |
+| 1(自取) | * | - | * | 1 | 退款申请中 |
+| 1(自取) | 4 | - | 0 | 0 | 已取消 |
+| 1(自取) | 4 | - | 1 | 0 | 已取消 |
+| 1(自取) | * | - | * | 2 | 退款中 |
+| 1(自取) | * | - | * | 4 | 退款已拒绝 |
+| 1(自取) | * | - | * | 5 | 客服介入中 |
+| 1(自取) | * | - | * | 6 | 售后完成 |
+| 2(堂食) | 0 | - | 0 | 0 | 待商家确认 |
+| 2(堂食) | 0 | - | 1 | 0 | 待商家确认 |
+| 2(堂食) | 1 | - | 1 | 0 | 商家备餐中 |
+| 2(堂食) | 2 | - | 1 | 0 | 备餐完成 |
+| 2(堂食) | 3 | - | 1 | 0 | 已完成 |
+| 2(堂食) | * | - | * | 1 | 退款申请中 |
+| 2(堂食) | 4 | - | 0 | 0 | 已取消 |
+| 2(堂食) | 4 | - | 1 | 0 | 已取消 |
+| 2(堂食) | * | - | * | 2 | 退款中 |
+| 2(堂食) | * | - | * | 4 | 退款已拒绝 |
+| 2(堂食) | * | - | * | 5 | 客服介入中 |
+| 2(堂食) | * | - | * | 6 | 售后完成 |
+
+## 需要修改的代码
+
+### 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 小时自动完成 |
+
+- **用户确认**:可提供可选的"确认取餐"按钮,用户点了也设 `state=3`,但不点也不影响
+- **定时任务兜底**:商家出餐(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` → 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 中扫码确认,但非必须。
+- **堂食**:商家出餐后自动倒计时完成。无需用户额外操作。
+
+核心原则:**商家/骑手操作是主触发器,用户确认是可选的,自动完成是兜底。** 用户不确认不会导致订单永远"挂着"。
+
+## 参考资源
+
+- [美团外卖开放平台 — 订单状态](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)

+ 0 - 20
sql/table_qrcode.sql

@@ -1,20 +0,0 @@
--- 餐桌码表
-CREATE TABLE `table_qrcode` (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
-  `table_no` varchar(32) NOT NULL COMMENT '桌号',
-  `type` tinyint NOT NULL DEFAULT 0 COMMENT '类型:0=摊位码, 1=公用码',
-  `store_id` bigint DEFAULT NULL COMMENT '关联摊位ID',
-  `night_market_id` bigint NOT NULL COMMENT '夜市ID',
-  `qr_code` varchar(255) DEFAULT NULL COMMENT '二维码内容',
-  `status` tinyint NOT NULL DEFAULT 0 COMMENT '启用状态:0=启用, 1=停用',
-  `use_status` tinyint NOT NULL DEFAULT 0 COMMENT '使用状态:0=空闲, 1=使用中',
-  `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
-  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
-  `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
-  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
-  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
-  PRIMARY KEY (`id`),
-  KEY `idx_store_id` (`store_id`),
-  KEY `idx_night_market_id` (`night_market_id`),
-  KEY `idx_type` (`type`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='餐桌码表';

+ 0 - 2
sql/user_wallet_add_store_id.sql

@@ -1,2 +0,0 @@
-ALTER TABLE user_wallet ADD COLUMN store_id BIGINT DEFAULT NULL COMMENT '关联摊位id,非摊位钱包为NULL';
-ALTER TABLE user_wallet MODIFY COLUMN user_id BIGINT DEFAULT NULL COMMENT '用户id,摊位钱包为NULL';

+ 0 - 39
update-sql/appealMenu.sql

@@ -1,39 +0,0 @@
--- 建表 SQL
-CREATE TABLE `pos_appeal` (
-  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
-  `dd_id` bigint DEFAULT NULL COMMENT '订单号',
-  `user_id` bigint DEFAULT NULL COMMENT '用户ID',
-  `user_type` varchar(1) DEFAULT NULL COMMENT '用户类型',
-  `nick_name` varchar(30) DEFAULT NULL COMMENT '昵称',
-  `content` varchar(4000) DEFAULT NULL COMMENT '内容',
-  `imgs` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '上传图片',
-  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户留言表';
-
--- 菜单 SQL
-insert into sys_menu (menu_name, parent_id, order_num, path, component,query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('用户留言信息', '2006', '1', 'appeal', 'system/appeal/index','{\"type\":0}', 1, 0, 'C', '0', '0', 'system:appeal:list', '#', 'admin', sysdate(), '', null, '用户留言信息菜单');
-insert into sys_menu (menu_name, parent_id, order_num, path, component,query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('商家留言信息', '2006', '2', 'appeal', 'system/appeal/index','{\"type\":1}', 1, 0, 'C', '0', '0', 'system:appeal:list', '#', 'admin', sysdate(), '', null, '商家留言信息菜单');
-insert into sys_menu (menu_name, parent_id, order_num, path, component,query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('骑手留言信息', '2006', '3', 'appeal', 'system/appeal/index','{\"type\":2}', 1, 0, 'C', '0', '0', 'system:appeal:list', '#', 'admin', sysdate(), '', null, '骑手留言信息菜单');
-
--- 按钮父菜单ID
-SELECT @parentId := LAST_INSERT_ID();
-
--- 按钮 SQL
-insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('留言信息查询', @parentId, '1',  '#', '', 1, 0, 'F', '0', '0', 'system:appeal:query',        '#', 'admin', sysdate(), '', null, '');
-
-insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('留言信息新增', @parentId, '2',  '#', '', 1, 0, 'F', '0', '0', 'system:appeal:add',          '#', 'admin', sysdate(), '', null, '');
-
-insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('留言信息修改', @parentId, '3',  '#', '', 1, 0, 'F', '0', '0', 'system:appeal:edit',         '#', 'admin', sysdate(), '', null, '');
-
-insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('留言信息删除', @parentId, '4',  '#', '', 1, 0, 'F', '0', '0', 'system:appeal:remove',       '#', 'admin', sysdate(), '', null, '');
-
-insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
-values('留言信息导出', @parentId, '5',  '#', '', 1, 0, 'F', '0', '0', 'system:appeal:export',       '#', 'admin', sysdate(), '', null, '');

+ 17 - 0
updatesql/sql.md

@@ -0,0 +1,17 @@
+# 数据库变更记录
+
+## 2026-05-15 订单状态四字段分离(006-orderstate)
+
+```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;
+```