# 配送端（自营骑手）表 vs 第三方快递轨迹表：对比与数据库修改建议

本文目标：
- 整理“原先配送端（自营骑手/同城配送）”使用的核心数据表。
- 对比“现在第三方快递配送（韵达/圆通等）”的轨迹/运单数据表。
- 给出建议的数据库改造方案：如何在不破坏订单主表的前提下，引入第三方快递轨迹能力，并（可选）逐步弃用原先配送端表的依赖。

明确决策（当前结论）：
- 仅支持第三方快递轨迹（承运方运单 + 轨迹时间线）。
- 不做自营骑手端/同城配送任务流；`ml_delivery_drivers`/`ml_delivery_tasks` 视为历史遗留表，不再作为新能力的依赖与写入目标。

> 说明：本对比只讨论“第三方快递轨迹/运单”与“自营骑手任务”两套模型如何在数据库层共存/迁移；二者是不同业务域，不建议强行复用同一张表。

---

## 1. 原先配送端（自营骑手）核心表（现状）

数据来源：主库 DDL（`mall_sql/schemas/complete_mall_database.sql`）与旧配送端文档（`pages/mall/delivery/doc/old/*`）。

### 1.1 `ml_delivery_drivers`（配送员信息表）
用途：记录配送员（骑手）档案与实时工作状态；用于配送端登录后的“是否可接单/是否在线”。

关键字段（节选）：
- `id`：配送员 ID
- `user_id`：关联 `ak_users.id`
- `real_name`、`id_card`、`driver_license`
- `vehicle_type`、`vehicle_number`
- `service_areas`（JSONB）
- `work_status`：1 在线 / 2 忙碌 / 3 离线
- `current_lat` / `current_lng`
- `rating_avg` / `rating_count` / `order_count`
- `status`：1 正常 / 2 暂停 / 3 离职

### 1.2 `ml_delivery_tasks`（配送任务表，配送端“状态真源”）
用途：描述“一个订单被分配给某个骑手并经历接单→取货→配送→送达”的任务流。

关键字段（节选）：
- 关联：
  - `order_id`（UUID，**唯一**，FK 到 `ml_orders.id`）
  - `driver_id`（FK 到 `ml_delivery_drivers.id`，可为空表示未接单）
- 地址与费用：
  - `pickup_address`（JSONB）取货地址
  - `delivery_address`（JSONB）配送地址
  - `distance`、`estimated_time`、`delivery_fee`
- 任务状态：`status`（int）
  - 1 待接单
  - 2 已接单
  - 3 取货中
  - 4 配送中
  - 5 已送达
  - 6 配送失败
- 时间戳：`assigned_at` / `picked_at` / `delivered_at`
- 其他：`delivery_code`（取货码）、`remark`、`failure_reason`

> 注意：旧页面/旧文档里常出现 `accepted_at`、`pickup_time`、`delivered_time` 等命名，和主库 DDL 的 `assigned_at/picked_at/delivered_at` 存在差异；如果你要继续使用自营配送链路，建议在代码层做统一字段映射或做一次字段对齐迁移。

### 1.3 与订单表的关系（旧模型的关键耦合点）
- `ml_delivery_tasks.order_id` 强依赖 `ml_orders.id`，并且 **1 个订单只能有 1 条配送任务**（`order_id UNIQUE`）。
- 旧实现里常尝试把 `ml_delivery_tasks.status` 同步到 `ml_orders.order_status`，容易造成口径不一致（旧文档也提到了这种冲突风险）。

---

## 2. 现在第三方快递配送（轨迹/运单）表（目标模型）

数据来源：`pages/mall/delivery/doc/需求文档/express_tracking_mock_platform.sql`。

> 这套表的定位不是“骑手任务流”，而是“第三方承运的运单 + 轨迹事件时间线”。

### 2.1 `platform_express_waybills`（平台侧：运单主表）
用途：一条运单（一个 `tracking_no`）的聚合信息；用于订单详情页展示“承运方/运单号/当前状态/最后同步时间”。

关键字段（节选）：
- 关联：`order_id`（可选）、`order_no`（可选）
- 运单：`carrier`、`tracking_no`、`source`
- 聚合状态：`current_status_code`、`current_status_text`
- 时间：`eta`、`last_synced_at`
- 约束：`UNIQUE (carrier, tracking_no)`

特点：
- 同一个订单允许对应多条运单（拆包裹/分批发货）——因为 `order_id` 不唯一。

### 2.2 `platform_express_tracking_events`（平台侧：轨迹事件表）
用途：时间线的主数据来源；用于前端展示、告警统计与排障。

关键字段（节选）：
- 关联：`waybill_id`（FK 到 `platform_express_waybills.id`）
- 事件：`event_id`（第三方可能缺失）、`event_time`、`event_code`、`event_text`
- 平台统一状态：`status_code`
- 节点：`node_name`、`location`、`description`
- 证据：`evidence_urls`（jsonb）
- 原始回文：`raw_payload`（jsonb，可用于审计/排障）
- 幂等：`dedupe_key` + `UNIQUE (waybill_id, dedupe_key)`

### 2.3 `platform_express_event_raw`（平台侧：原始接收表）
用途：记录 webhook/轮询的原始请求、验签与解析错误，便于排障与审计。

### 2.4 `mock_*`（Mock 承运方侧表）
这部分只用于联调/回归/故障注入，不建议进入生产主库的核心业务 schema（可以放在独立 schema 或测试库）。

---

## 3. 两套模型的核心差异（为什么不建议“复用一张表”）

| 维度 | 自营骑手（旧配送端） | 第三方快递（新模型） |
|---|---|---|
| 业务对象 | 任务（骑手接单、取货、送达） | 运单（承运方扫描轨迹） |
| 与订单关系 | 1 订单 = 1 任务（`order_id UNIQUE`） | 1 订单 = N 运单（拆包裹常见） |
| 状态来源 | 平台自己驱动状态机（按钮/操作） | 第三方事件驱动（webhook/轮询） |
| 数据颗粒度 | 少量节点（接单/取货/送达） | 高频节点（到站/发车/分拣/派送/签收…） |
| 关键字段 | `driver_id`、地址 JSON、配送费、距离 | `carrier`、`tracking_no`、事件时间线、幂等去重 |
| 风险点 | 并发抢单、任务状态与订单状态不同步 | 乱序/重复事件、验签、防重放、脱敏 |

结论：
- 旧表（`ml_delivery_*`）适合“自营骑手/同城配送”。
- 新表（`platform_express_*`）适合“第三方快递轨迹”。
- 两者可以共存，但不要强行把第三方轨迹塞进 `ml_delivery_tasks`。

---

## 4. 推荐的数据库修改方案（兼容、可渐进）

### 4.1 方案 1（推荐）：新增第三方快递表，不改旧配送表
适用：你们当前要做第三方快递展示，且已明确“不做自营骑手”。

做法：
1) 在主库新增（或通过 migration 引入）三张平台侧表：
   - `platform_express_waybills`
   - `platform_express_tracking_events`
   - `platform_express_event_raw`
2) 商家发货时写入/绑定：创建 `platform_express_waybills` 行（填 `order_id`、`order_no`、`carrier`、`tracking_no`）。
3) 第三方回调/轮询入库：写 `platform_express_tracking_events`，并更新 `platform_express_waybills.current_status_*` 与 `last_synced_at`。

建议的小幅增强（强烈建议做）：
- 给 `platform_express_waybills(order_id)` 增加索引（便于按订单查运单）。
- 让 `platform_express_waybills.order_id` 建外键到 `ml_orders(id)`（如果你们能保证订单一定存在）。

### 4.2 方案 2：逐步“弃用旧配送端表”的依赖（当你们全面第三方快递时）
适用：你们不再提供自营骑手配送，或者想把骑手端做成独立项目。

做法（建议按阶段，不要硬删表）：
- 阶段 A：停止业务写入 `ml_delivery_tasks`（页面/接口不再创建任务）。
- 阶段 B：订单详情页的物流展示只依赖 `platform_express_*`。
- 阶段 C：将 `ml_delivery_tasks` 标为 legacy（只读保留一段时间），最终再评估是否归档/删除。

为什么不建议立刻删：
- 历史数据、结算对账、纠纷复盘可能仍需要旧数据。

在“不做自营骑手”的前提下，最小要求：
- 新增第三方快递表后，业务代码只写入/读取 `platform_express_*`，不要再创建/更新 `ml_delivery_tasks`。
- 旧表保留只读（或仅用于历史查询/清理脚本），后续再评估归档策略。

### 4.3 如果两种配送方式要并存（同城骑手 + 快递）
建议用“履约类型”做分流，而不是混表：
- 同城/自营：继续用 `ml_delivery_tasks`（任务流）
- 快递：用 `platform_express_*`（运单轨迹）
- 订单详情页按订单的履约类型选择展示模块（或同时展示多个包裹）

---

## 5. 迁移/改库执行清单（建议写成 migration）

### 5.1 建表落库
- 把 `express_tracking_mock_platform.sql` 的 A 部分（platform）整理为一份 migration（放到 `mall_sql/migrations/`），避免：
  - 重复创建 `set_updated_at()`（项目若已有同名函数）
  - 把 `mock_*` 测试表混进生产 schema

### 5.2 对接 `ml_orders`
- 建议：
  - `platform_express_waybills.order_id REFERENCES public.ml_orders(id)`
  - `CREATE INDEX ... ON platform_express_waybills(order_id)`
- 不建议：
  - 把运单号/承运方直接塞进 `ml_orders`（会导致一单多包裹很难处理）

### 5.3 数据回填（可选）
如果你们历史上已经保存过“承运方/运单号”在别处（例如订单扩展表/发货日志），可做一次性回填：
- 按订单生成 waybill 行
- 再按运单触发一次轨迹拉取

### 5.4 权限与隐私
- 买家/商家/客服看到的轨迹必须“同源”，但展示要按角色脱敏；敏感字段（如手机号、原始回文）按文档约束控制。

---

## 6. 你接下来要怎么改数据库（最短路线）

如果你现在的目标是：商家可选承运方 + 录入运单号 + 用户能看轨迹时间线。

最短路线：
1) 在主库落 `platform_express_waybills` / `platform_express_tracking_events` / `platform_express_event_raw`
2) 发货绑定时写 `platform_express_waybills(order_id, order_no, carrier, tracking_no)`
3) 第三方事件入库写 `platform_express_tracking_events`
4) 订单详情页按 `order_id` 查 waybills，再查 events 渲染时间线

---

（如你确认要我进一步把“建议的小幅增强”直接写成 SQL migration 文件，我可以在 `mall_sql/migrations/` 里新增一份只包含 platform 表的 migration，并确保不引入 `mock_*` 表。）
