From c803a77c8fc16e7b18100a46a24bc4947c86843d Mon Sep 17 00:00:00 2001
From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com>
Date: Tue, 3 Feb 2026 12:01:10 +0800
Subject: [PATCH 01/26] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2?=
=?UTF-8?q?=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages.json | 7 +
pages/mall/delivery/all.uvue | 360 ++++++++++++++++++
.../{doc => db}/db-data-generation.md | 0
pages/mall/delivery/doc/all.md | 51 +++
pages/mall/delivery/doc/earnings.md | 3 +
pages/mall/delivery/doc/index.md | 10 +
pages/mall/delivery/doc/order-detail.md | 42 +-
pages/mall/delivery/doc/order-history.md | 75 ++++
pages/mall/delivery/index.uvue | 172 +++++++--
pages/mall/delivery/order-detail.uvue | 178 +++++++--
pages/mall/delivery/order-history.uvue | 182 +++++----
pages/mall/delivery/profile.uvue | 5 +
12 files changed, 904 insertions(+), 181 deletions(-)
create mode 100644 pages/mall/delivery/all.uvue
rename pages/mall/delivery/{doc => db}/db-data-generation.md (100%)
create mode 100644 pages/mall/delivery/doc/all.md
diff --git a/pages.json b/pages.json
index 6261795f..813e91fe 100644
--- a/pages.json
+++ b/pages.json
@@ -268,6 +268,13 @@
"navigationStyle": "custom"
}
},
+ {
+ "path": "all",
+ "style": {
+ "navigationBarTitleText": "待接取任务",
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "earnings",
"style": {
diff --git a/pages/mall/delivery/all.uvue b/pages/mall/delivery/all.uvue
new file mode 100644
index 00000000..bbdb9b58
--- /dev/null
+++ b/pages/mall/delivery/all.uvue
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
+
+
+
+ 附近暂时没有待接订单
+
+
+
+
+ ¥{{ order.delivery_fee }}
+
+
+
+
+ 📍
+ {{ order.pickup_address.detail || order.pickup_address.area }}
+
+ ↓
+
+ 🏠
+ {{ order.delivery_address.detail || order.delivery_address.area }}
+
+
+
+
+ 距离: {{ order.distance }}km
+ 预计: {{ order.estimated_time }}分钟
+ 下单: {{ order.created_at }}
+
+
+
+
+
+
+
+
+
+ 已加载全部订单
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/doc/db-data-generation.md b/pages/mall/delivery/db/db-data-generation.md
similarity index 100%
rename from pages/mall/delivery/doc/db-data-generation.md
rename to pages/mall/delivery/db/db-data-generation.md
diff --git a/pages/mall/delivery/doc/all.md b/pages/mall/delivery/doc/all.md
new file mode 100644
index 00000000..fcf50ed0
--- /dev/null
+++ b/pages/mall/delivery/doc/all.md
@@ -0,0 +1,51 @@
+
+## 1. 功能概述
+`all.uvue` 是为配送端设计的全量订单抓取页面。当首页待接订单超过 5 个时,用户可通过此页面查看并抢单。
+
+## 2. 核心设计说明
+根据最新 UI 指标,该页面采用了**现代垂直流布局**:
+- **中心费用展示**:订单金额在卡片顶部居中加粗显示,强化利益点。
+- **垂直路径流**:采用垂直排布的取货(📍)与送达(🏠)地址,中间以箭头连接,更符合手机屏阅读习惯。
+- **全宽操作按钮**:底部采用 100% 宽度的按钮,提高抢单操作的触达率。
+- **实时空态处理**:当订单被他人抢先接走时,列表会自动更新。
+
+## 3. 技术实现要点
+
+### 3.1 数据安全与并发控制
+在执行 `acceptOrder`(抢单)时,通过数据库约束确保操作的原子性:
+```uts
+// 增加 driver_id 为空的前提条件,防止已被他人接单
+const res = await supa.from('ml_delivery_tasks')
+ .update({
+ driver_id: driverId,
+ status: 2,
+ accepted_at: new Date().toISOString()
+ })
+ .eq('id', taskId)
+ .is('driver_id', 'null') // 关键:确保单子还没被接
+ .execute();
+```
+
+### 3.2 数据结构转换 (`_transformTask`)
+为了兼容数据库存储的 JSON 字符串格式与 UI 组件所需的 Object 格式,页面内置了转换逻辑:
+- 自动解析 `pickup_address` 和 `delivery_address` JSON 字符串。
+- 格式化 `delivery_fee` 为保留两位小数。
+- 映射状态码到对应的 UI 标签。
+
+### 3.3 路由与交互
+- **路由路径**:`pages/mall/delivery/all`
+- **导航栏**:配置了 `navigationBarTitleText: "待接订单"`,并开启了原生的回退功能。
+- **动态跳转**:支持从详情页返回后自动刷新列表(通过 `onShow` 触发)。
+
+## 4. 样式规范
+- 布局:Flexbox (Column)
+- 配色:
+ - 取货点:Pink (`#ff4d94`)
+ - 送达点:Blue (`#2196F3`)
+ - 主按钮:Green (`#4CAF50`)
+- 间距:标准 30rpx 外边距,卡片内 40rpx 内边距。
+
+## 5. 开发历史
+- **2026-02-03**: 页面初版创建。
+- **2026-02-03**: 完成从水平卡片到垂直流卡片的 UI 重构。
+- **2026-02-03**: 接入 Supabase 实现秒级抢单逻辑与状态同步。
diff --git a/pages/mall/delivery/doc/earnings.md b/pages/mall/delivery/doc/earnings.md
index c8b8a825..147f2b3b 100644
--- a/pages/mall/delivery/doc/earnings.md
+++ b/pages/mall/delivery/doc/earnings.md
@@ -58,6 +58,8 @@ const result = Array.from(orderMap.values())
## 数据源更新:包含已接订单(Accepted)
- 需求说明:为了与“历史订单”页面保持一致,收入统计应包含配送员已接取(accepted/assigned)的订单,不仅限于已完成订单。也就是说,只要 `ml_delivery_tasks` 中 `driver_id` = 当前司机且 `status >= 2`(已接取或进行中),其对应的订单都应计入收入统计范围。
+- 需求说明(已采纳):收入统计以 `ml_delivery_tasks` 为数据源之一:只要任务记录显示 `driver_id` = 当前司机且 `status >= 2`(已接取或进行中),其对应订单即应计入统计范围。注意:部分 `ml_delivery_tasks` 可能没有可匹配的 `ml_orders`(`order_id` 为空或在 `ml_orders` 中找不到),页面/后端应对缺失 `order_no` 做回退显示并记录供核查。
+
- 推荐 Supabase 查询示例(后端或前端按需实现):
```
@@ -78,6 +80,7 @@ LIMIT :size OFFSET :offset;
```js
const taskRes = await supa.from('ml_delivery_tasks').select('order_id').eq('driver_id', uid).gte('status', 2).execute()
const orderIds = taskRes.data.map(r => r.order_id)
+// 对于 order_id 为空或找不到的情况,server 端应记录这些 id 以便排查
const ordersRes = await supa.from('ml_orders').select('*,ml_delivery_tips(*)').in('id', orderIds).order('created_at',{ascending:false}).limit(size).execute()
```
diff --git a/pages/mall/delivery/doc/index.md b/pages/mall/delivery/doc/index.md
index dbea39f3..b3ef5702 100644
--- a/pages/mall/delivery/doc/index.md
+++ b/pages/mall/delivery/doc/index.md
@@ -46,6 +46,16 @@ await supa.from('ml_delivery_tasks')
- `toggleWorkStatus()`:切换 `isOnline` 并调用 `startWork()` / `stopWork()`。上线时会刷新可接订单列表。
- 接单/开始取货/确认取货/开始配送/确认送达等均通过对 `ml_delivery_tasks` 的 `update` 操作变更 `status`,并在成功后更新本地 `currentTask`。
+## UI 行为变动(已生效)
+- 当页面检测到存在 `currentTask`(来自 `ml_delivery_tasks`)时,页面不会再弹出“附近可接订单”列表。即:配送端以 `ml_delivery_tasks` 为状态真源,主页面的订单展示不再依赖或回退展示 `ml_orders.order_status`,以避免两表不同步导致的显示冲突。
+- 为了减少页面闪烁与重复刷新,`enableAutoRefresh` 在默认实现中已可被关闭(`false`),且 `loadAvailableOrders()` 在检测到 `currentTask` 时会跳过可接订单的渲染。
+
+## 会话与加载保护
+- `supaReady` 的会话恢复在某些环境中会较慢,页面中已对其使用 `Promise.race(..., 1500)` 超时包装:超时后页面会打警告并继续渲染以避免长时间阻塞用户界面。依赖用户 id 的查询在超时情况下可能为空,请参照 `order-history.md` 的“已实现的防护”部分进行排查。
+
+## 前端同步尝试(临时)
+- 在接单/确认送达流程中前端会尝试向 `ml_orders` 发送更新(将 `order_status` 同步到任务的状态)并记录返回结果用于诊断。但该同步并不保证在所有权限或网络错误下成功,因此更稳妥的方案仍是后端触发器同步或可信服务端接口。
+
## 注意事项
- 高并发接单场景需后端保证原子性(乐观锁或 DB 事务)以防止竞单冲突。
- `loadAvailableOrders()` 最好按司机服务区域与距离筛选,并使用分页/实时推送代替频繁轮询。
diff --git a/pages/mall/delivery/doc/order-detail.md b/pages/mall/delivery/doc/order-detail.md
index 31408d88..6b683279 100644
--- a/pages/mall/delivery/doc/order-detail.md
+++ b/pages/mall/delivery/doc/order-detail.md
@@ -22,30 +22,26 @@
- 解析 `options.id` 与 `options.status`,调用 `loadOrderDetail(id)`。
- `loadOrderDetail(id)`
- - 判断 ID 类型(UUID / 数字 / 非数字)以决定查询字段(`id`、`cid`、`order_no`)。
- - 并行查询 `ml_orders`, `ml_order_items`, `ml_shops`, `ml_delivery_tasks` 并合并到页面状态。
+ - **加载保护**:对 `supaReady` 采用 1.5s 超时策略包装,防止会话刷新阻塞页面加载。
+ - **智能 ID 回查**:优先从 `ml_orders` (UUID/cid/order_no) 查找。若未找到,则尝试从未分配任务表 `ml_delivery_tasks` 中根据 ID 查找,再反查关联订单。
+ - **降级机制(Fallback)**:当 `ml_orders` 行缺失时,自动回退到从 `ml_delivery_tasks` 提取地址、手机号、配送费及距离等基础信息进行展示,并在 UI 上显示回退加载提示。
+ - **清理加载状态**:在 `finally` 块中统一切除加载动画 (`uni.hideLoading`),防止界面挂起。
-- `acceptOrder()` / `rejectOrder(reason)`
- - accept: 尝试对 `ml_delivery_tasks` 执行 `update driver_id` 操作并设置 `status=2`(处理中),需要后端并发保护。
- - reject: 增加拒单原因到 `ml_delivery_tasks` 或 `order_notes` 并回滚本地 UI 状态。
+## 交互与样式优化(2026-02-03 更新)
+- **联系人信息解析**:地址栏(取货/送货)现在仅在“联系人姓名”存在时显示分隔点 `·`。若无姓名,则仅显示手机号,避免显示为 `. 手机号`。
+- **联系方式布局优化**:为了防止手机号在不同屏幕宽度下被图标遮挡,联系人区域采用**垂直居中布局**(图标在上,姓名电话在下),显著拉高了边框高度 (`min-height: 180rpx`) 并增加了垂直间距。
+- **文本显示优化**:
+ - 商品区域的“**订单号**”增加了加粗显示 (`bold`),提升核对便利性。
+ - 展示给配送员的联系手机号调大了字号并加粗,确保清晰可见。
+ - 修复了在 Uni-app x 下由于类型推断导致的手机号无法通过点语法访问的问题(改用索引访问)。
-- `confirmPickup()` / `confirmDelivery()`
- - 根据 `task.id` 更新相应时间戳字段(`picked_at`/`delivered_at`)并设置状态(例如 `status=3/4`)。
-
-## 示例:按 id 类型查询(伪代码)
-```
-let q = supa.from('ml_orders').select('*')
-if (isUUID(id)) q = q.eq('id', id)
-else if (isNumeric(id)) q = q.eq('cid', id)
-else q = q.eq('order_no', id)
-const { data: order } = await q.limit(1).execute()
+## 示例:地址兼容解析逻辑
+```typescript
+// 兼容 JSON 字符串及对象格式的地址字段
+let shipping = {}
+if (typeof raw == 'string') {
+ try { shipping = JSON.parse(raw) } catch (e) { shipping = { detail: raw } }
+} else { shipping = raw || {} }
+// 访问方式:(address as UTSJSONObject)['phone']
```
-## 事务与并发注意
-- 接单场景应使用后端原子性检查(数据库事务或行级乐观锁)以避免多司机同时接单。
-- 前端接单流程:先尝试 update(带 where driver_id IS NULL),若返回 0 row affected 则提示已被接单。
-
-## 错误处理与回退
-- 捕获所有 supa 调用错误并将友好错误展示给用户(例如:'网络错误,请稍后重试')。
-- 对可能缺失的字段(地址为字符串或对象)使用 `_transformAddress()` 做兼容处理。
-
diff --git a/pages/mall/delivery/doc/order-history.md b/pages/mall/delivery/doc/order-history.md
index b46ea431..9095fd79 100644
--- a/pages/mall/delivery/doc/order-history.md
+++ b/pages/mall/delivery/doc/order-history.md
@@ -1,3 +1,78 @@
+# 历史订单 页面说明(order-history.uvue)
+
+## 概要
+`order-history.uvue` 用于配送员查看历史订单与近期任务。页面会展示:
+- 以 `ml_delivery_tasks` 为配送端“状态真源”的任务记录(只要 `driver_id = 当前司机` 且 `status >= 2`,均会包含在统计/列表中);
+- 页面会批量回填对应 `ml_orders.order_no` 以补全显示(若 `order_no` 缺失会显示回退文本),避免直接以 `ml_orders.order_status` 作为展示依据而导致与配送端不一致。
+
+页面关键点:
+- 首次加载时通过 `loadOrderHistory()` 拉取数据;页面每次显示时会检查本地存储 `completed_order_for_history`,并把刚完成订单插入列表表头。
+- 使用 Supabase 客户端 `supa` 读取 `ml_delivery_tasks` 与 `ml_orders` 表,并通过 `getCurrentUser()` / `getCurrentUserId()` 获取当前用户/司机 id。
+
+## 行为细节
+- 当前实现优先以 `ml_delivery_tasks`(status >= 2)作为数据源,页面会:
+ - 查询 `ml_delivery_tasks` 中与 `driver_id` 相关的任务,按时间排序并映射为页面项;
+ - 对获取到的 `order_id` 列表做一次批量查询 `ml_orders` 以回填 `order_no` 和订单详情;
+ - 对没有匹配到 `ml_orders` 的 `order_id`,页面会用短 id 回退显示并在控制台打印缺失 id 列表,便于后台核查数据不一致的原因。
+- 为避免重复展示,页面在将“当前任务对应订单”插入顶部时,会先检查 `orderList` 是否已有相同 `id`。
+
+## 依赖 & 相关文件
+- 页面文件:`pages/mall/delivery/order-history.uvue`(当前)
+- Supabase 实例:`components/supadb/aksupainstance.uts`(导出 `supa` 与 `supaReady`)
+- 用户/会话工具:`utils/store.uts`(`getCurrentUser()`、`getCurrentUserId()`)
+- 相关文档:
+ - `pages/mall/delivery/doc/earnings.md`(收入聚合与 DB 建议)
+ - `pages/mall/delivery/doc/test-user-1_at_123.com.md`(测试用户与 SQL 示例)
+
+## 已实现的防护与诊断信息
+- `supaReady` 在会话恢复时可能会进行网络刷新(refreshSession),该步骤可能较慢。为避免页面长时间阻塞,页面中对 `supaReady` 使用了 `Promise.race` 的 1.5s 超时包装:如果超时会打印警告并继续执行(某些依赖用户 id 的查询可能因此为空)。
+- 如果 `getCurrentUserId()` 返回空,页面会尝试从 `supa.getSession()` 获取 auth id 并在 `ak_users` 表中查找对应的 `ak_users.id` 作为回退,这能修复 `driver_id` 在数据库中为 `ak_users.id` 的常见映射问题。
+
+## 常见不一致现象说明
+- 我们观察到的常见情况:`ml_delivery_tasks` 中的任务显示为“已完成”(配送端),但对应 `ml_orders.order_status` 仍为“已取消”或其它状态,导致不同页面显示冲突。原因通常为:
+ - `ml_delivery_tasks.order_id` 为空或格式不一致(UUID vs string);
+ - `ml_orders` 没有相应行(数据尚未同步或被删除);
+ - RLS/权限导致前端不能读取或更新 `ml_orders`。
+
+建议排查 SQL(例):
+```
+SELECT t.id AS task_id, t.order_id, t.status AS task_status, o.order_status
+FROM public.ml_delivery_tasks t
+LEFT JOIN public.ml_orders o ON o.id = t.order_id
+WHERE t.status >= 2
+ORDER BY t.created_at DESC
+LIMIT 200;
+```
+
+如需我为你生成触发器或前端重试队列示例,我可以继续实现。
+
+## 常见问题与排查步骤
+1. 问题:页面没有显示当前已接订单(即使首页显示有当前任务)。
+ - 检查控制台日志:页面会打印 `loadOrderHistory: currentUserId=`、`loadOrderHistory: session id fallback=`、`loadOrderHistory: delivery_tasks dtRes=`、`loadOrderHistory: ordersRes=`。把这些日志逐项核对:
+ - `currentUserId` 应为 `ak_users.id`(或系统实际使用的 driver id)。
+ - `dtRes`(delivery_tasks 查询)应包含对象数组,且数组项含 `order_id`。
+ - `ordersRes` 应包含对应的 `ml_orders` 行。
+ - 若 `dtRes` 为空且 `session id fallback` 有值,说明 `ak_users` 表中可能没有把 auth id 映射到 `ak_users.id`,需要把 `ak_users.auth_id` 填入或同步。
+ - 若 `ordersRes` 为空,但 `dtRes` 非空,请检查 `ml_orders.id` 与 `ml_delivery_tasks.order_id` 的数据类型(例如 UUID vs string)以及 RLS 策略。
+
+2. 问题:页面加载慢或时不时刷新。
+ - 原因:多个页面在 `onShow`/`onLoad` 时发起多次 supa 查询,且 `supaReady` 恢复会话有时较慢,导致累积延迟。已在 `index.uvue` 增加了防抖与 `enableAutoRefresh` 开关来禁止自动刷新。
+ - 排查:查看控制台是否有 `supaReady timeout/failed` 警告(若有,则说明会话恢复慢或失败)。
+
+## 性能与安全建议
+- 若数据量大,请在后端做分页与聚合(只返回必要字段);避免一次性查询大量 `ml_orders` 字段。参见 `earnings.md` 中的后端接口建议。
+- 长期建议:修改 `components/supadb/aksupainstance.uts` 中会话恢复逻辑,让刷新在后台异步进行或提供可配置的超时策略,避免阻塞页面加载。
+
+## 测试步骤(快速)
+1. 使用测试用户(参见 `test-user-1_at_123.com.md`)创建一个 `ml_delivery_tasks` 记录,`driver_id` 对应当前司机,且 `status >= 2`。
+2. 在首页确认当前任务显示;然后打开“历史订单”页面,观察顶部是否显示该订单。若未显示,贴上控制台中 `loadOrderHistory:` 的相关日志给开发者排查。
+
+## 变更历史
+- 2026-02-02:添加回退 mapping(session -> ak_users.id)、supaReady 超时保护的说明、调试日志建议及性能建议。
+
+---
+
+如需我把文档翻译为英文或生成 README 风格的一页说明,我可以继续补充。
# order-history.uvue — 历史订单
## 概要
diff --git a/pages/mall/delivery/index.uvue b/pages/mall/delivery/index.uvue
index cd916250..de149c9b 100644
--- a/pages/mall/delivery/index.uvue
+++ b/pages/mall/delivery/index.uvue
@@ -97,8 +97,6 @@
附近订单
@@ -107,34 +105,41 @@
请保持在线状态,有新订单会自动推送
-
-
-
-
-
- 📍
- {{ order.pickup_address.area }}
+
+
+
- →
-
- 🏠
- {{ order.delivery_address.area }}
+
+
+
+ 📍
+ {{ order.pickup_address.area || order.pickup_address.detail }}
+
+ →
+
+ 🏠
+ {{ order.delivery_address.area || order.delivery_address.detail }}
+
+
+
+
+ 距离: {{ order.distance }}km
+ 预计: {{ order.estimated_time }}分钟
+ 下单: {{ formatTime(order.created_at) }}
+
+
+
+
+
-
-
- 距离: {{ order.distance }}km
- 预计: {{ order.estimated_time }}分钟
- 下单: {{ formatTime(order.created_at) }}
-
-
-
-
-
-
+
+
+
+
@@ -308,7 +313,10 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
}
}
if (res && (res.data instanceof Array) && res.data.length > 0) {
- this.driverInfo = Object.assign(this.driverInfo, res.data[0])
+ const data = res.data[0] as DeliveryDriverType
+ this.driverInfo = Object.assign(this.driverInfo, data)
+ // 同步工作状态到本地变量
+ this.isOnline = (this.driverInfo.work_status === 1)
}
} catch (e) {
console.error('loadDriverInfo error', e)
@@ -434,6 +442,7 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
return {
id: task.id,
+ order_id: task.order_id || task.orderId || task.orderId || '',
order_no: task.order_no || task.orderNo || task.trade_no || '',
status: Number(task.status) || 1,
pickup_address: parseAddress(task.pickup_address),
@@ -465,8 +474,34 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
// 切换工作状态
toggleWorkStatus(event: UniSwitchChangeEvent) {
- this.isOnline = event.detail.value
+ const targetStatus = event.detail.value
+ // 检查是否有当前任务,不允许离线
+ if (!targetStatus && this.currentTask != null) {
+ // 1. 先同步 UI 状态为 false (由于用户已经拨动了开关)
+ this.isOnline = false
+
+ // 2. 弹出警告
+ uni.showModal({
+ title: '无法下线',
+ content: '您当前有正在进行的任务,请完成后再下线。',
+ showCancel: false,
+ success: (_) => {
+ // 3. 用户点击确定后或立即强制回弹开关为 true
+ this.$nextTick(() => {
+ this.isOnline = true
+ })
+ }
+ })
+
+ // 4. 冗余保障:如果 Modal 没及时回弹,延时强制重置
+ setTimeout(() => {
+ this.isOnline = true
+ }, 300)
+ return
+ }
+
+ this.isOnline = targetStatus
if (this.isOnline) {
this.startWork()
} else {
@@ -475,8 +510,15 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
},
// 开始工作
- startWork() {
- // TODO: 调用API开始工作,上传位置
+ async startWork() {
+ const driverId = this.driverInfo.id
+ if (driverId != '') {
+ try {
+ await supa.from('ml_delivery_drivers').update({ work_status: 1 } as any).eq('id', driverId).execute()
+ } catch (e) {
+ console.error('startWork update failed', e)
+ }
+ }
this.loadAvailableOrders()
uni.showToast({
title: '已上线接单',
@@ -485,8 +527,15 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
},
// 停止工作
- stopWork() {
- // TODO: 调用API停止工作
+ async stopWork() {
+ const driverId = this.driverInfo.id
+ if (driverId != '') {
+ try {
+ await supa.from('ml_delivery_drivers').update({ work_status: 0 } as any).eq('id', driverId).execute()
+ } catch (e) {
+ console.error('stopWork update failed', e)
+ }
+ }
this.availableOrders = []
uni.showToast({
title: '已下线休息',
@@ -631,6 +680,29 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
if (res && !res.error) {
const completedOrder = { ...this.currentTask }
uni.setStorageSync('completed_order_for_history', completedOrder)
+ // 同步更新 ml_orders 的状态,确保两个表状态一致
+ try {
+ // 尝试使用 currentTask.order_id(由 _transformTask 提供)
+ const orderId = (this.currentTask as any).order_id || ''
+ if (orderId) {
+ const upRes: any = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', orderId).execute()
+ console.log('confirmDelivery: ml_orders update res=', upRes)
+ if (!upRes || upRes.error) console.warn('confirmDelivery: ml_orders update failed', upRes)
+ } else {
+ // 如无 order_id,回退读取任务行以查找 order_id
+ const tRes: any = await supa.from('ml_delivery_tasks').select('order_id').eq('id', this.currentTask.id).limit(1).execute()
+ if (tRes && Array.isArray(tRes.data) && tRes.data.length > 0) {
+ const oid = tRes.data[0].order_id
+ if (oid) {
+ const upRes2: any = await supa.from('ml_orders').update({ order_status: 5 }).eq('id', oid).execute()
+ console.log('confirmDelivery: ml_orders update (fallback) res=', upRes2)
+ if (!upRes2 || upRes2.error) console.warn('confirmDelivery: ml_orders update (fallback) failed', upRes2)
+ }
+ }
+ }
+ } catch (syncErr) {
+ console.warn('confirmDelivery: failed to sync ml_orders status', syncErr)
+ }
uni.showToast({ title: '配送完成', icon: 'success' })
this.currentTask = null
this.loadAvailableOrders()
@@ -684,6 +756,21 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
const res = await supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('id', orderId).execute()
if (res && !res.error) {
uni.showToast({ title: '订单已接受', icon: 'success' })
+ // 同步更新 ml_orders 状态为已接取(2)
+ try {
+ // orderId 这里是 ml_delivery_tasks.id(task id),需要先获取 order_id
+ const tRes: any = await supa.from('ml_delivery_tasks').select('order_id').eq('id', orderId).limit(1).execute()
+ if (tRes && Array.isArray(tRes.data) && tRes.data.length > 0) {
+ const oid = tRes.data[0].order_id
+ if (oid) {
+ const upRes: any = await supa.from('ml_orders').update({ order_status: 2 }).eq('id', oid).execute()
+ console.log('acceptOrder: ml_orders update res=', upRes)
+ if (!upRes || upRes.error) console.warn('acceptOrder: ml_orders update failed', upRes)
+ }
+ }
+ } catch (syncErr) {
+ console.warn('acceptOrder: failed to sync ml_orders status', syncErr)
+ }
await this.loadCurrentTask()
await this.loadAvailableOrders()
}
@@ -1174,6 +1261,25 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
border: 1rpx solid #ddd;
}
+/* 加载更多订单入口样式 */
+.view-all-footer {
+ background-color: #ffffff;
+ padding: 24rpx;
+ border-radius: 12rpx;
+ margin: 10rpx 0 30rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1rpx dashed #4CAF50;
+ box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.1);
+}
+
+.view-all-text {
+ font-size: 26rpx;
+ color: #4CAF50;
+ font-weight: bold;
+}
+
/* 历史记录快捷入口 */
.quick-actions-section {
background-color: #fff;
diff --git a/pages/mall/delivery/order-detail.uvue b/pages/mall/delivery/order-detail.uvue
index d58fb0e4..d516ccd1 100644
--- a/pages/mall/delivery/order-detail.uvue
+++ b/pages/mall/delivery/order-detail.uvue
@@ -1,5 +1,6 @@
+
+
+
@@ -249,30 +252,78 @@ export default {
async loadOrderDetail(orderId: string) {
const originalStatus = this.order.status
try {
- await supaReady
- console.log('loadOrderDetail called', { orderId })
+ // 使用 1.5s 超时策略包装 supaReady,防止 session 刷新卡死页面
+ const readyPromise = supaReady
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('supaReady timeout')), 1500))
+ try {
+ await Promise.race([readyPromise, timeoutPromise])
+ } catch (e) {
+ console.warn('loadOrderDetail: supaReady timeout, proceeding with current session', e)
+ }
- // 获取订单主表:按 id(UUID) / cid(数字) / order_no 三种可能匹配
+ console.log('loadOrderDetail: start loading', { orderId, originalStatus })
+ uni.showLoading({ title: '加载中...' })
+
+ // 配送端详情页逻辑:
+ // 1. 先尝试直接查 ml_orders (传入的是真实订单ID时)
+ // 2. 如果没查到,尝试从 ml_delivery_tasks 查找 (传入的是任务ID时)
+ // 3. 如果从任务表查到了,再拿其关联的 order_id 回查 ml_orders
+
+ let targetOrderId = orderId
let orderRes: any = null
+ let taskData: any = null
+
const isUuid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(orderId)
const isNumber = /^\d+$/.test(orderId)
- console.log('loadOrderDetail: supa session=', supa.getSession && supa.getSession())
+
console.log('loadOrderDetail: detect id format', { isUuid, isNumber })
+
+ // 步骤 A: 查 ml_orders
if (isUuid) {
- console.log('loadOrderDetail: querying by id')
orderRes = await supa.from('ml_orders').select('*').eq('id', orderId).limit(1).execute()
} else if (isNumber) {
- console.log('loadOrderDetail: querying by cid')
orderRes = await supa.from('ml_orders').select('*').eq('cid', Number(orderId)).limit(1).execute()
} else {
- console.log('loadOrderDetail: querying by order_no')
orderRes = await supa.from('ml_orders').select('*').eq('order_no', orderId).limit(1).execute()
}
- console.log('loadOrderDetail: orderRes=', orderRes)
+
+ // 步骤 B: 如果订单没查到,尝试从任务表查
+ if (!(orderRes && Array.isArray(orderRes.data) && orderRes.data.length > 0)) {
+ console.log('loadOrderDetail: order not found directly, checking ml_delivery_tasks')
+ const taskRes: any = await supa.from('ml_delivery_tasks').select('*').eq('id', orderId).limit(1).execute()
+ if (taskRes && Array.isArray(taskRes.data) && taskRes.data.length > 0) {
+ taskData = taskRes.data[0]
+ console.log('loadOrderDetail: found task record', taskData)
+ if (taskData.order_id) {
+ targetOrderId = taskData.order_id
+ console.log('loadOrderDetail: found linked order_id in task', targetOrderId)
+ orderRes = await supa.from('ml_orders').select('*').eq('id', targetOrderId).limit(1).execute()
+ }
+ }
+ }
+
+ console.log('loadOrderDetail: final orderRes data length=', orderRes?.data?.length)
+
if (orderRes && Array.isArray(orderRes.data) && orderRes.data.length > 0) {
const row = orderRes.data[0]
-
- const shipping = row.shipping_address || {}
+ console.log('loadOrderDetail: discovered order details', row)
+
+ // 如果没有预先获取 taskData,在这里查一次
+ if (!taskData) {
+ const dtQuery: any = await supa.from('ml_delivery_tasks').select('*').eq('order_id', row.id).limit(1).execute()
+ if (dtQuery && Array.isArray(dtQuery.data) && dtQuery.data.length > 0) taskData = dtQuery.data[0]
+ }
+ let shipping: UTSJSONObject = {} as UTSJSONObject
+ try {
+ const rawShipping = row.shipping_address
+ if (typeof rawShipping == 'string' && (rawShipping as string).startsWith('{')) {
+ shipping = JSON.parse(rawShipping as string) as UTSJSONObject
+ } else if (rawShipping != null && typeof rawShipping == 'object') {
+ shipping = rawShipping as UTSJSONObject
+ }
+ } catch (e) {
+ console.warn('loadOrderDetail: parse shipping_address failed', e)
+ }
this.order = Object.assign(this.order, {
id: row.id,
@@ -287,9 +338,9 @@ export default {
payment_method: row.payment_method || 0,
payment_status: row.payment_status || 0,
delivery_address: {
- name: shipping.name || shipping.recipient || '',
- phone: shipping.phone || shipping.mobile || '',
- detail: shipping.detail || shipping.address || JSON.stringify(shipping)
+ name: shipping['name'] || shipping['recipient'] || '',
+ phone: shipping['phone'] || shipping['mobile'] || '',
+ detail: shipping['detail'] || shipping['address'] || JSON.stringify(shipping)
},
created_at: row.created_at || ''
})
@@ -341,19 +392,63 @@ export default {
}
// deliveryInfo 从 ml_delivery_tasks 中读取(如果存在)
- const dtRes: any = await supa.from('ml_delivery_tasks').select('*').eq('order_id', realOrderId).limit(1).execute()
- console.log('loadOrderDetail: dtRes=', dtRes)
- if (dtRes && Array.isArray(dtRes.data) && dtRes.data.length > 0) {
- const dt = dtRes.data[0]
- this.deliveryInfo.distance = Number(dt.distance) || this.deliveryInfo.distance
- this.deliveryInfo.estimated_time = Number(dt.estimated_time) || this.deliveryInfo.estimated_time
- this.deliveryInfo.courier_id = dt.driver_id || ''
- this.deliveryInfo.pickup_time = dt.pickup_time || ''
- this.deliveryInfo.delivery_time = dt.delivered_time || ''
+ if (taskData) {
+ this.deliveryInfo.distance = Number(taskData.distance) || this.deliveryInfo.distance
+ this.deliveryInfo.estimated_time = Number(taskData.estimated_time) || this.deliveryInfo.estimated_time
+ this.deliveryInfo.courier_id = taskData.driver_id || ''
+ this.deliveryInfo.pickup_time = taskData.pickup_time || ''
+ this.deliveryInfo.delivery_time = taskData.delivered_time || taskData.delivered_at || ''
}
+ } else if (taskData) {
+ // ⚠️ 数据不一致兜底:订单主表查不到,但任务表查到了
+ console.warn('loadOrderDetail: order missing but task found. Using fallback.')
+
+ const parseF = (v: any) => {
+ if (!v) return { detail: '', name: '', phone: '' }
+ let o: any = v
+ if (typeof v === 'string') {
+ try { o = JSON.parse(v) } catch (e) { o = { detail: v } }
+ }
+ return {
+ detail: o.detail || o.address || o.full_address || '',
+ name: o.name || o.contact_name || o.recipient_name || '',
+ phone: o.phone || o.mobile || o.contact_phone || ''
+ }
+ }
+
+ const pickup = parseF(taskData.pickup_address)
+ const delivery = parseF(taskData.delivery_address)
+ const pContact = parseF(taskData.pickup_contact)
+ const dContact = parseF(taskData.delivery_contact)
+
+ this.order = Object.assign(this.order, {
+ id: taskData.order_id || taskData.id,
+ order_no: taskData.order_no || taskData.id.substring(0, 8),
+ status: Number(taskData.status) || originalStatus,
+ delivery_address: {
+ name: dContact.name || delivery.name,
+ phone: dContact.phone || delivery.phone,
+ detail: delivery.detail
+ }
+ })
+
+ this.merchant.contact_name = pContact.name || pickup.name
+ this.merchant.contact_phone = pContact.phone || pickup.phone
+ this.pickupAddress = pickup.detail
+
+ this.deliveryInfo.distance = Number(taskData.distance) || 0
+ this.deliveryInfo.estimated_time = Number(taskData.estimated_time) || 0
+
+ uni.showToast({ title: '已回退从任务记录显示', icon: 'none' })
+ } else {
+ console.warn('loadOrderDetail: no order found for id', orderId)
+ uni.showToast({ title: '未找到订单或任务信息', icon: 'none' })
}
} catch (e) {
console.error('loadOrderDetail db error', e)
+ uni.showToast({ title: '加载订单失败', icon: 'none' })
+ } finally {
+ uni.hideLoading()
}
},
@@ -530,7 +625,15 @@ export default {
.delivery-order-detail {
background-color: #f5f5f5;
min-height: 100vh;
- padding-bottom: 160rpx;
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 0; /* padding moved to .page-content to avoid double spacing */
+}
+
+.page-content {
+ flex: 1;
+ overflow: auto;
+ padding-bottom: 300rpx; /* 留出底部操作区域高度,确保联系商家等内容可滚动到最下 */
}
/* ... 其余样式保持原样 ... */
@@ -738,7 +841,8 @@ export default {
.order-no {
font-size: 24rpx;
- color: #666;
+ color: #333;
+ font-weight: bold;
}
.product-item {
@@ -865,30 +969,38 @@ export default {
.contact-item {
flex: 1;
display: flex;
+ flex-direction: column; /* 改为垂直排列避免空间不足 */
align-items: center;
- padding: 25rpx;
+ justify-content: center;
+ padding: 20rpx;
background-color: #f8f9fa;
border-radius: 10rpx;
+ min-height: 120rpx;
}
.contact-icon {
- font-size: 32rpx;
- margin-right: 15rpx;
+ font-size: 36rpx;
+ margin-bottom: 10rpx;
}
.contact-info {
flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
}
.contact-name {
- font-size: 26rpx;
- color: #333;
+ font-size: 24rpx;
+ color: #666;
margin-bottom: 5rpx;
}
.contact-phone {
- font-size: 24rpx;
+ font-size: 26rpx;
color: #007aff;
+ font-weight: bold;
}
.bottom-actions {
diff --git a/pages/mall/delivery/order-history.uvue b/pages/mall/delivery/order-history.uvue
index 137a0879..8064b2bc 100644
--- a/pages/mall/delivery/order-history.uvue
+++ b/pages/mall/delivery/order-history.uvue
@@ -53,7 +53,7 @@
-
+
@@ -93,11 +93,15 @@ export default {
checkForNewCompletedOrder() {
const completedOrderFromStorage = uni.getStorageSync('completed_order_for_history')
if (completedOrderFromStorage) {
- // 如果有,将其添加到订单列表的开头
- // 检查是否已经存在于列表中,避免重复添加
- const exists = this.orderList.some(order => order.id === completedOrderFromStorage.id)
- if (!exists) {
- this.orderList.unshift(completedOrderFromStorage)
+ // 仅在本地存储条目标记为已完成(status >= 4)时才合并到历史
+ const storedStatus = Number(completedOrderFromStorage && (completedOrderFromStorage.status ?? 0))
+ if (storedStatus >= 4) {
+ const exists = this.orderList.some(order => order.id === completedOrderFromStorage.id)
+ if (!exists) {
+ this.orderList.unshift(completedOrderFromStorage)
+ }
+ } else {
+ console.warn('checkForNewCompletedOrder: ignoring stored completed_order_for_history with non-completed status', completedOrderFromStorage)
}
// 清除本地存储,防止下次进入页面时重复添加
uni.removeStorageSync('completed_order_for_history')
@@ -107,122 +111,114 @@ export default {
// 加载历史订单(从数据库读取)
async loadOrderHistory() {
try {
- const ready = await Promise.race([supaReady, new Promise(resolve => setTimeout(() => resolve(false), 1500))])
- if (!ready) console.warn('supaReady timeout/failed in loadOrderHistory - proceeding')
+ await supaReady
} catch (e) {
console.warn('supaReady failed', e)
}
- // Ensure profile is loaded so getCurrentUserId returns ak_users.id when available
- try {
- await getCurrentUser()
- } catch (e) {
- console.warn('getCurrentUser failed in loadOrderHistory', e)
- }
-
const uid = getCurrentUserId()
console.log('loadOrderHistory: currentUserId=', uid)
- // 首先查 ml_delivery_tasks 中分配给当前用户的任务(仅包含已接/分配的 status >= 2)
- let dtRes: any = { data: [] }
+ // 首先尝试解析出 ml_delivery_drivers 的 id(driver_id),避免直接使用 auth user id 导致不匹配
+ let driverId: string | null = null
try {
- let queryUid = uid
- // 如果 uid 为空,尝试从 supa session 获取 auth id 并映射到 ak_users.id
- if (!queryUid || queryUid === '') {
- try {
- const sess = supa.getSession && supa.getSession()
- const sessId = sess && sess.user && sess.user.getString && sess.user.getString('id')
- console.log('loadOrderHistory: session id fallback=', sessId)
- if (sessId) {
- const akRes = await supa.from('ak_users').select('id').eq('auth_id', sessId).limit(1).execute()
- if (akRes && Array.isArray(akRes.data) && akRes.data.length > 0) {
- queryUid = (akRes.data[0] as any).id
- console.log('loadOrderHistory: mapped ak_users.id=', queryUid)
+ if (uid && uid !== '') {
+ // 尝试直接按 user_id 查找 driver
+ const drvRes: any = await supa.from('ml_delivery_drivers').select('id').eq('user_id', uid).limit(1).execute()
+ if (drvRes && Array.isArray(drvRes.data) && drvRes.data.length > 0) {
+ driverId = drvRes.data[0].id
+ } else {
+ // 回退:尝试 ak_users 表根据 auth_id 查出 ak_users.id,再查 ml_delivery_drivers
+ const akRes: any = await supa.from('ak_users').select('id').eq('auth_id', uid).limit(1).execute()
+ if (akRes && Array.isArray(akRes.data) && akRes.data.length > 0) {
+ const akId = akRes.data[0].id
+ const drvRes2: any = await supa.from('ml_delivery_drivers').select('id').eq('user_id', akId).limit(1).execute()
+ if (drvRes2 && Array.isArray(drvRes2.data) && drvRes2.data.length > 0) {
+ driverId = drvRes2.data[0].id
}
}
- } catch (mapErr) {
- console.warn('loadOrderHistory: ak_users mapping failed', mapErr)
}
}
+ } catch (err) {
+ console.error('loadOrderHistory: driver lookup failed', err)
+ }
- if (queryUid && queryUid !== '') {
- dtRes = await supa.from('ml_delivery_tasks')
- .select('order_id,status')
- .eq('driver_id', queryUid)
+ // 直接以 ml_delivery_tasks 作为数据源(配送端真源)
+ let tasksRes: any = { data: [] }
+ try {
+ if (driverId) {
+ tasksRes = await supa.from('ml_delivery_tasks')
+ .select('*')
+ .eq('driver_id', driverId)
.gte('status', 2)
.order('created_at', { ascending: false })
.limit(200)
.execute()
} else {
- dtRes = { data: [] }
+ tasksRes = { data: [] }
}
- console.log('loadOrderHistory: delivery_tasks dtRes=', dtRes)
} catch (err) {
- console.error('loadOrderHistory: delivery_tasks query failed', err)
- dtRes = { data: [] }
+ console.error('loadOrderHistory: ml_delivery_tasks query failed', err)
+ tasksRes = { data: [] }
}
- const orderIds = (dtRes && Array.isArray(dtRes.data)) ? dtRes.data.map((r: any) => r.order_id) : []
+ console.log('loadOrderHistory: tasksRes=', tasksRes)
- // 如果没有通过 delivery_tasks 找到订单,改为直接读取最近完成/已取货订单(兼容测试环境)
- let ordersRes
+ // 如果任务包含 order_id,则从 ml_orders 查询 order_no 用于显示
+ const orderIdsFromTasks = (tasksRes && Array.isArray(tasksRes.data)) ? tasksRes.data.map((r: any) => r.order_id).filter(Boolean) : []
+ let ordersRes: any = { data: [] }
try {
- if (orderIds.length > 0) {
- ordersRes = await supa.from('ml_orders').select('*').in('id', orderIds).order('created_at', { ascending: false }).limit(200).execute()
- } else {
- ordersRes = await supa.from('ml_orders').select('*').in('order_status', [4,5]).order('created_at', { ascending: false }).limit(200).execute()
+ if (orderIdsFromTasks.length > 0) {
+ ordersRes = await supa.from('ml_orders').select('id,order_no').in('id', orderIdsFromTasks).execute()
}
} catch (err) {
- console.error('loadOrderHistory: ml_orders query failed', err)
+ console.warn('loadOrderHistory: ml_orders lookup failed', err)
ordersRes = { data: [] }
}
-
console.log('loadOrderHistory: ordersRes=', ordersRes)
- const mapOrder = (r: any) => ({
- id: r.id,
- order_no: r.order_no || String(r.cid || ''),
- status: r.order_status ?? r.status ?? 0,
- pickup_address: r.pickup_address || r.shipping_address || { detail: '', area: '' },
- delivery_address: r.delivery_address || r.shipping_address || { detail: '', area: '' },
- pickup_contact: r.pickup_contact || r.shipping_contact || { name: '', phone: '' },
- delivery_contact: r.delivery_contact || r.shipping_contact || { name: '', phone: '' },
- delivery_fee: r.delivery_fee || r.delivery_fees || 0,
- distance: r.distance || 0,
- estimated_time: r.estimated_time || 0,
- created_at: r.created_at
+ const orderNoMap: Record = {}
+ if (ordersRes && Array.isArray(ordersRes.data)) {
+ ordersRes.data.forEach((o: any) => { if (o && o.id) orderNoMap[o.id] = o.order_no })
+ }
+
+ const parseAddress = (a: any) => {
+ if (!a) return { detail: '', area: '' }
+ let obj = a
+ if (typeof a === 'string') {
+ try { obj = JSON.parse(a) } catch (e) { obj = { detail: a } }
+ }
+ const detail = obj.detail || obj.address || obj.full_address || obj.address_detail || obj.name || ''
+ const area = (obj.city || obj.district || obj.area || '')
+ return { detail, area }
+ }
+
+ const parseContact = (c: any) => {
+ if (!c) return { name: '', phone: '' }
+ let obj = c
+ if (typeof c === 'string') {
+ try { obj = JSON.parse(c) } catch (e) { obj = { name: c } }
+ }
+ return { name: obj.name || obj.contact_name || obj.receiver_name || '', phone: obj.phone || obj.mobile || obj.contact_phone || '' }
+ }
+
+ const mapTaskToOrder = (t: any) => ({
+ id: t.id,
+ // 优先使用任务自身的 order_no 字段(若存在),否则使用从 ml_orders 查询到的 order_no
+ order_no: t.order_no || t.orderNo || t.trade_no || orderNoMap[t.order_id] || '',
+ status: Number(t.status) || 0,
+ pickup_address: parseAddress(t.pickup_address),
+ delivery_address: parseAddress(t.delivery_address),
+ pickup_contact: parseContact(t.pickup_contact),
+ delivery_contact: parseContact(t.delivery_contact),
+ delivery_fee: Number(t.delivery_fee) || 0,
+ distance: Number(t.distance) || 0,
+ estimated_time: Number(t.estimated_time) || 0,
+ created_at: t.created_at,
+ order_id: t.order_id || ''
})
- this.orderList = (ordersRes && ordersRes.data) ? ordersRes.data.map(mapOrder) : []
-
- // 额外:把当前分配给本司机但尚未完成的任务对应订单也展示在列表顶部(便于查看当前任务)
- try {
- if (uid && uid !== '') {
- // 仅获取已经被接取/分配给本司机的任务(status >= 2),包括进行中和已取货但未完成的
- const taskRes: any = await supa.from('ml_delivery_tasks')
- .select('order_id,status')
- .eq('driver_id', uid)
- .gte('status', 2)
- .order('created_at', { ascending: false })
- .limit(50)
- .execute()
- if (taskRes && Array.isArray(taskRes.data) && taskRes.data.length > 0) {
- const taskOrderIds = taskRes.data.map((t: any) => t.order_id).filter((id: any) => id)
- if (taskOrderIds.length > 0) {
- const curOrdersRes: any = await supa.from('ml_orders').select('*').in('id', taskOrderIds).execute()
- const curMapped = (curOrdersRes && Array.isArray(curOrdersRes.data)) ? curOrdersRes.data.map(mapOrder) : []
- // 把当前任务对应订单插入到列表最前面(避免重复)
- curMapped.forEach((o: any) => {
- if (!this.orderList.some((ex: any) => ex.id === o.id)) {
- this.orderList.unshift(o)
- }
- })
- }
- }
- }
- } catch (err) {
- console.error('loadOrderHistory: fetch current tasks failed', err)
- }
+ this.orderList = (tasksRes && Array.isArray(tasksRes.data)) ? tasksRes.data.map(mapTaskToOrder) : []
// 检查是否有新完成的订单(在加载初始数据后)
this.checkForNewCompletedOrder()
@@ -253,9 +249,11 @@ export default {
},
// 查看订单详情
- viewOrderDetail(orderId: string, status: number) {
+ viewOrderDetail(order: any) {
+ // 优先传递 ml_orders.id,但如果订单表对应行确实缺失,详情页逻辑现在支持用 Task ID 回退显示
+ const targetId = order.order_id || order.id
uni.navigateTo({
- url: `/pages/mall/delivery/order-detail?id=${orderId}&status=${status}`
+ url: `/pages/mall/delivery/order-detail?id=${targetId}&status=${order.status}`
})
},
diff --git a/pages/mall/delivery/profile.uvue b/pages/mall/delivery/profile.uvue
index b35cba79..1592bcab 100644
--- a/pages/mall/delivery/profile.uvue
+++ b/pages/mall/delivery/profile.uvue
@@ -209,6 +209,11 @@ function backToIndex() {
uni.navigateBack({ url: '/pages/mall/delivery/index' })
}
+/* 兼容模板中的 goBack 绑定,保持与其它页面一致 */
+function goBack() {
+ uni.navigateBack()
+}
+
/* ----------------- 数据 & 状态 ----------------- */
const driverInfo = ref({ id: '', real_name: '配送员', avatar_url: '', rating: 4.9, total_orders: 0, work_status: 1 })
const workStatus = ref(1)
From 09c33be3944cd0928c4d4300b5091c480e1e6ce4 Mon Sep 17 00:00:00 2001
From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com>
Date: Tue, 3 Feb 2026 17:22:55 +0800
Subject: [PATCH 02/26] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=8C=B9=E9=85=8D?=
=?UTF-8?q?=E6=9C=BA=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages/mall/delivery/STATE_1-5_LOGIC.md | 81 ++++
pages/mall/delivery/doc/DELIVERY_LOGIC.md | 97 ++++
.../doc/rider-types-and-assignment.md | 458 ++++++++++++++++++
pages/mall/delivery/index.uvue | 2 +-
pages/mall/delivery/order-history.uvue | 2 +-
5 files changed, 638 insertions(+), 2 deletions(-)
create mode 100644 pages/mall/delivery/STATE_1-5_LOGIC.md
create mode 100644 pages/mall/delivery/doc/DELIVERY_LOGIC.md
create mode 100644 pages/mall/delivery/doc/rider-types-and-assignment.md
diff --git a/pages/mall/delivery/STATE_1-5_LOGIC.md b/pages/mall/delivery/STATE_1-5_LOGIC.md
new file mode 100644
index 00000000..69b5015c
--- /dev/null
+++ b/pages/mall/delivery/STATE_1-5_LOGIC.md
@@ -0,0 +1,81 @@
+# 状态 1–5 行为与代码位置说明
+
+本文档针对 `ml_delivery_tasks.status` 中 1–5 的状态,逐项列出在 delivery 页面中涉及的展示、查询、更新位置、按钮/动作与已发现的不一致点与建议。
+
+## 状态 1 — 待接取
+- 映射文本/样式
+ - `index.uvue`: `getTaskStatusClass` -> `task-pending`(见 index.uvue getTaskStatusClass)
+ - `index.uvue`: `getTaskStatusText` case 1 -> '待接取'(index.uvue#L571)
+ - `order-history.uvue`: 同样映射(order-history.uvue#L230, #L242)
+- 查询/展示
+ - `loadAvailableOrders` 查询:`supa.from('ml_delivery_tasks').is('driver_id','null').eq('status', 1)`(index.uvue loadAvailableOrders)
+ - 在可接订单列表与当前任务区显示“接受任务/接受订单”按钮(index.uvue#L81、order card accept button)
+- 更新/操作
+ - 接单时更新为 `status = 2`:`supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('id', ...)`(index.uvue acceptTask / acceptOrder,index.uvue#L602,#L756)
+- 建议/注意
+ - 接单使用条件更新或后端 RPC 避免抢单竞态(目前无 WHERE driver_id IS NULL 条件)。
+
+## 状态 2 — 已接取
+- 映射文本/样式
+ - `index.uvue` 文本 '已接取'(index.uvue#L572),样式 `task-accepted`(index.uvue#L560)
+ - `order-history.uvue` 相应映射(order-history.uvue#L243)
+- 查询/展示
+ - 当前任务区显示“开始取货”按钮(index.uvue#L82)
+ - `order-detail.uvue` 若 `order.status === 2` 显示“前往取货/正在取货”(order-detail.uvue#L169-L170)
+- 更新/操作
+ - `startPickup()` 会将 task 状态更新为 3(index.uvue startPickup)
+ - 接单后代码尝试同步 `ml_orders.order_status = 2`(index.uvue#L766)
+- 建议/注意
+ - 同步 ml_orders 的操作应做失败回退处理或在服务端做原子事务。
+
+## 状态 3 — 取货中
+- 映射文本/样式
+ - `index.uvue` 文本 '取货中'(index.uvue#L573),样式 `task-picking`(index.uvue#L561)
+ - `order-history.uvue` 同步映射(order-history.uvue#L244)
+- 查询/展示
+ - 当前任务区显示“确认取货”按钮(index.uvue#L83)
+ - `order-detail.uvue` 在 route 区显示“确认取货”按钮(order-detail.uvue#L55,#L173)
+- 更新/操作
+ - `confirmPickup()` 将 status -> 4 并写入 `pickup_time`,同时尝试把 `ml_orders.order_status = 4`(index.uvue & order-detail)
+- 建议/注意
+ - 确认取货应当原子更新任务与订单状态,或由后端封装接口完成。
+
+## 状态 4 — 已取货
+- 映射文本/样式
+ - `index.uvue` 文本 '已取货'(index.uvue#L574),样式 `task-picked`(index.uvue#L562)
+ - `order-history.uvue` 文本 '已取货'(order-history.uvue#L245)
+- 查询/展示
+ - 当前任务区显示“开始配送”按钮(index.uvue#L84)
+ - `order-detail.uvue` 显示“确认送达”按钮(order-detail.uvue#L68,#L176)
+- 更新/操作
+ - `startDelivery()` 将 status -> 5(index.uvue startDelivery)
+- 建议/注意
+ - 明确 4 与 5 的边界并在统计/历史中统一口径(是否将 5 视作已完成)。
+
+## 状态 5 — 配送中
+- 映射文本/样式
+ - `index.uvue` 文本 '配送中'(index.uvue#L575),样式 `task-delivering`(index.uvue#L563)
+ - `order-history.uvue` 在样式处标为 `status-delivered`(order-history.uvue#L234)且文本处 `case 5: return '已完成'`(order-history.uvue#L246)——存在不一致。
+- 查询/展示
+ - 当前任务区显示“确认送达”入口(index.uvue#L85 -> showConfirmDeliveryDialog)
+ - `order-detail.uvue` 在状态 >=5 时进度条显示为已送达(getStatusDesc)
+- 更新/操作
+ - `confirmDelivery()` 将 task.status -> 6 并写入 `delivered_time`,再尝试同步 `ml_orders.order_status = 5`(index.uvue#L679-691)
+- 建议/注意
+ - 目前统计(如 todayStats)将 `status >= 5` 视作“已完成”计数,而 UI 有时把 5 显示为“配送中”,建议统一规则(推荐:把 6 视作真正完成,5 为配送中)。
+
+## 跨页不一致与改进建议(摘要)
+- 不一致点:`status = 5` 在不同页面既被视作“配送中”,又被视作“已完成”;`confirmDelivery` 设置 task=6,但统计使用 `>=5` 判断完成,导致计数或展示差异。
+- 建议:
+ 1. 明确“完成”阈值(推荐:status === 6 为已完成,status === 5 为配送中),并在所有统计、过滤、UI 映射处统一实现。
+ 2. 将状态映射抽到共享工具文件 `utils/deliveryStatus.uts` 并替换硬编码。
+ 3. 接单、确认取货、确认送达等关键操作应使用后端事务或条件更新避免竞态与不一致。
+
+---
+
+文档已生成于 `pages/mall/delivery/STATE_1-5_LOGIC.md`。要我现在:
+- A) 将 `getTaskStatusText` / `getOrderStatusText` 抽到 `utils/deliveryStatus.uts` 并替换页面中的映射,或
+- B) 实现接单的条件更新补丁(修改 `acceptTask` / `acceptOrder`),或
+- C) 仅把“5 是否计作完成”的规范改写回 `DELIVERY_LOGIC.md` 并提交。
+
+请选择一项继续。
\ No newline at end of file
diff --git a/pages/mall/delivery/doc/DELIVERY_LOGIC.md b/pages/mall/delivery/doc/DELIVERY_LOGIC.md
new file mode 100644
index 00000000..510f802d
--- /dev/null
+++ b/pages/mall/delivery/doc/DELIVERY_LOGIC.md
@@ -0,0 +1,97 @@
+# 配送端逻辑说明(概要与建议)
+
+**概览**
+- 本文档总结 `pages/mall/delivery` 目录下配送端主要页面的工作流与实现细节,定位到关键数据库表与状态机,列出已识别的并发/同步风险,并给出可执行的改进建议。
+- 主要参考页面:
+ - [pages/mall/delivery/index.uvue](pages/mall/delivery/index.uvue)
+ - [pages/mall/delivery/order-history.uvue](pages/mall/delivery/order-history.uvue)
+ - [pages/mall/delivery/profile.uvue](pages/mall/delivery/profile.uvue)
+ - [pages/mall/delivery/order-detail.uvue](pages/mall/delivery/order-detail.uvue)
+
+**主要数据表(后端)**
+- `ml_delivery_drivers`:配送员信息与 `work_status`。
+- `ml_delivery_tasks`:配送任务(配送端主要“真源”),包含 `driver_id, order_id, status, pickup_time, delivered_time` 等。
+- `ml_orders`、`ml_order_items`:电商订单主表及明细(用于展示订单详情、订单号、商品列表等)。
+- 其它辅助表:`ml_shops`, `ak_users`(用于 auth->ak id 回退查找)等。
+
+**状态机(关键数值映射 — 汇总)
+- ml_delivery_tasks.status(代码中使用):
+ - 1 = 待接取(pending)
+ - 2 = 已接取(accepted)
+ - 3 = 取货中(picking)
+ - 4 = 已取货(picked)
+ - 5 = 配送中(delivering / startDelivery 将 status 置 5)
+ - 6 = 已送达 / 完成(confirmDelivery 将 status 置 6)
+- ml_orders.order_status(电商端状态,代码中做同步尝试):
+ - 2 = 已接取
+ - 4 = 已取货
+ - 5 = 已送达
+ - 7 = 已拒绝
+- 注意:两张表的状态值并不一一对应,代码中多处采用“尝试同步”的方式更新 `ml_orders.order_status`,但没有统一映射抽象,可能造成短时不一致。
+
+**页面职责与交互要点**
+- `index.uvue`(配送端首页)
+ - 加载司机信息、今日统计、当前任务(查询 `ml_delivery_tasks`,`lt status < 5`)、可接订单(`driver_id IS NULL AND status = 1`)。
+ - 提供在线/离线切换(更新 `ml_delivery_drivers.work_status`),并禁止在存在当前任务时下线。
+ - 任务操作:接受(status->2)、开始取货(3)、确认取货(4)、开始配送(5)、确认送达(6)。每步都更新 `ml_delivery_tasks`,并尝试同步 `ml_orders`。
+ - 使用 `supaReady` 包装并设 1.5s 超时降级,避免阻塞 UI。
+
+- `order-history.uvue`(历史订单)
+ - 以 `ml_delivery_tasks` 为源,查询 driver 相关任务(`gte('status', 2)`),并映射为历史条目供展示。
+ - 使用本地存储键 `completed_order_for_history`:确认送达时(index.confirmDelivery)会把完成的任务写入该键,历史页 onShow 时合并(仅当 status >= 4),之后删除该 key。
+
+- `profile.uvue`(个人中心)
+ - 展示司机统计(今日、最近任务、任务计数),通过 `ml_delivery_tasks` 聚合计算 pending/ongoing/completed 数量。
+
+- `order-detail.uvue`(订单/任务详情)
+ - 支持以订单 ID 或任务 ID 打开:优先查 `ml_orders`,若未找到则回退查 `ml_delivery_tasks` 并用任务数据回填页面。
+ - 对关键操作(接受/确认取货/确认送达/拒绝)会同时更新 `ml_delivery_tasks`(按 `order_id`)并尝试更新 `ml_orders.order_status`。
+
+**已识别风险与可改进点**
+- 状态不一致风险:
+ - `ml_delivery_tasks` 与 `ml_orders` 使用不同数值含义,且同步是“尝试式”的,短时内出现不一致或页面显示不同步的可能性高。
+- 抢单竞态(race condition):
+ - 可接订单查询基于 `driver_id IS NULL AND status = 1`,多个司机并发接受可能造成重复接单。
+ - 目前接单操作没有使用条件更新(例如 `UPDATE ... WHERE id = ? AND driver_id IS NULL`),因此依赖后续 reload 作为补救,非原子性操作。
+- 本地存储同步(`completed_order_for_history`):
+ - 以 localStorage 作为页面间同步的手段,适用于单设备单会话,但在多设备或短时间多页面切换(或 crash)场景下不可靠。
+- UI 与状态覆盖差异:
+ - 某些页面/映射未覆盖 `status = 6`(例如历史页 getOrderStatusText 没有 `6` 的友好文本),会显示“未知状态”。
+- supaReady 超时策略:
+ - 使用 1.5s 超时可以避免页面长时间等待,但在 session 刷新尚未完成的情况下可能读到空或旧数据;需要权衡 UX 与数据正确性。
+
+**短期可执行修复(优先级排序)**
+1. 接单使用条件更新(高优先)
+ - 在接单接口/前端更新时改为条件写入:
+ - SQL/SDK 示例:UPDATE ml_delivery_tasks SET driver_id = :driverId, status = 2 WHERE id = :taskId AND driver_id IS NULL
+ - 若返回表示 0 行更新,则提示“已被其他人接单”,并刷新列表。
+ - 目的:避免重复接单的竞态。
+
+2. 统一状态映射抽象(中优先)
+ - 在前端添加一个状态映射工具函数(例如 mapTaskStatusToOrderStatus(status)),并在每处状态同步时使用该映射。集中维护会减少散落的 magic number。
+
+3. 增强表间同步可靠性(中优先)
+ - 将关键同步(例如确认送达)封装为后端事务或 RPC:后端在事务中同时更新 `ml_delivery_tasks` 与 `ml_orders`,并返回原子成功/失败。前端只负责触发该 API。
+
+4. 替换 localStorage 同步(低优先)
+ - 使用队列/事件或服务端记录(例如在确认送达时写入 `ml_delivery_tasks`,历史页直接查询,不依赖 localStorage 写入),或者使用 WebSocket / 推送通知触发历史刷新。
+
+5. 补全 UI 映射(低优先)
+ - 为 `status = 6` 等补全友好文本和样式,避免“未知状态”展示。
+
+**建议的具体代码调整点(快速指引)**
+- 在 `index.uvue` 与 `order-detail.uvue` 的接单逻辑中替换无条件 update 为条件更新。例如:
+ - 当前(伪代码):
+ - supa.from('ml_delivery_tasks').update({ driver_id: driverId, status: 2 }).eq('id', orderId)
+ - 建议(伪代码):
+ - supa.rpc 或 SQL: UPDATE ml_delivery_tasks SET driver_id = $1, status = 2 WHERE id = $2 AND driver_id IS NULL RETURNING *
+ - 如果返回行数 === 0 则表示接单失败(已被抢)
+- 在多个页面里抽象出 `statusMap` 函数并复用:放在 `utils/deliveryStatus.uts` 或相应工具文件。
+
+**操作建议(下一步)**
+- 我可以为你:
+ 1. 在代码中实现“条件接单”补丁(修改 `index.uvue` 与 `order-detail.uvue` 的 `acceptOrder/acceptTask` 函数),并提交一个 patch;或者
+ 2. 生成一个状态映射工具文件并替换当前页面中的硬编码映射;或者
+ 3. 把本文档整理为项目 wiki 条目并打开 PR。
+
+请选择一个你希望我继续执行的项(例如“帮我做 1:实现条件接单补丁”),我会把对应步骤加入 TODO 并实现。
\ No newline at end of file
diff --git a/pages/mall/delivery/doc/rider-types-and-assignment.md b/pages/mall/delivery/doc/rider-types-and-assignment.md
new file mode 100644
index 00000000..9763fe00
--- /dev/null
+++ b/pages/mall/delivery/doc/rider-types-and-assignment.md
@@ -0,0 +1,458 @@
+# 骑手类型与订单超时重分配方案
+
+本文档汇总了将骑手分为“职业骑手(professional)”与“自由骑手(freelance)”的业务建议,并给出订单保留(reserve)→ 等待 ACK → 超时回退 → 自动重分配的可落地实现方案(DB 变更、服务端 API、客户端交互、守护进程示例和监控要点)。
+
+---
+
+## 1. 目标与背景
+
+- 支持两类骑手:职业骑手(签约、班次制、优先派单)与自由骑手(抢单为主、灵活)。
+- 解决问题:骑手被派单后长时间不响应导致订单延迟,需要一个可靠的超时回退与重分配机制,避免用户体验下降和运营人工介入成本过高。
+
+## 2. 业务差异化要点(简要)
+
+- 职业骑手:优先派单、需证照与审批、支持班次/周期结算、可配激励/罚款。
+- 自由骑手:公开/批量推送、按单结算、容错高、优先使用抢单策略。
+
+根据业务选型决定是否立即分离逻辑或用 `rider_type` 属性渐进演进。
+
+## 3. 必要的 DB 变更(Postgres / Supabase)
+
+示例 SQL(请在迁移环境先测试备份):
+
+```sql
+-- 骑手类型
+ALTER TABLE ml_delivery_drivers
+ ADD COLUMN rider_type varchar(20) DEFAULT 'freelance' NOT NULL; -- 'professional' | 'freelance'
+
+-- 订单保留与重试字段
+ALTER TABLE ml_delivery_tasks
+ ADD COLUMN reserved_driver_id uuid NULL,
+ ADD COLUMN reserved_until timestamptz NULL,
+ ADD COLUMN reserve_attempts int DEFAULT 0 NOT NULL,
+ ADD COLUMN notify_count int DEFAULT 0 NOT NULL;
+
+-- 可选:记录最后推送时间与候选池快照
+ALTER TABLE ml_delivery_tasks
+ ADD COLUMN last_push_at timestamptz NULL;
+```
+
+兼容策略:对既有数据默认 `rider_type='freelance'`,新字段 nullable/有默认值以避免回滚风险。
+
+## 4. 派单与保留流程(高层)
+
+1. 服务器为一个待分配任务计算候选骑手列表(基于:在线状态、距离、接单率、当前任务数、`rider_type` 权重等)。
+2. 按候选顺序尝试“原子保留”(reserve)给单个骑手:设置 `reserved_driver_id` 与 `reserved_until`(例如 30s)。
+3. 向被保留骑手发送高优先级推送 + deep link,客户端打开页面后必须调用 ACK 接口(accept-reservation)。
+4. 若骑手在 `reserved_until` 前 ACK:服务端将 `driver_id` 写入并把任务 `status` -> accepted(2),清空 `reserved_*`。
+5. 若超时未 ACK:Worker 检测到过期,清理 `reserved_*`,增加 `reserve_attempts`,选下一个候选重试;超过最大尝试次数后降级为公开抢单或人工处理。
+
+## 5. 原子 SQL 示例
+
+保留(server side try-reserve,保证原子性):
+
+```sql
+-- 尝试把任务保留给 candidate_driver_id,30s 保留
+UPDATE ml_delivery_tasks
+SET reserved_driver_id = $candidate_id,
+ reserved_until = NOW() + INTERVAL '30 seconds',
+ notify_count = notify_count + 1,
+ last_push_at = NOW()
+WHERE id = $task_id AND status = 1 AND reserved_driver_id IS NULL
+RETURNING *;
+```
+
+骑手接受(ACK,原子确认并完成分配):
+
+```sql
+UPDATE ml_delivery_tasks
+SET driver_id = $driver_id,
+ status = 2,
+ reserved_driver_id = NULL,
+ reserved_until = NULL
+WHERE id = $task_id AND reserved_driver_id = $driver_id AND status = 1
+RETURNING *;
+```
+
+若返回行数为 1 则成功;否则拒绝/失败(可能被超时回退或被其他骑手抢先)。
+
+## 6. 服务端 API 建议(示例)
+
+- POST /delivery/tasks/:id/reserve (内部/调度用)
+ - 输入:taskId, candidateDriverId
+ - 行为:执行原子保留 SQL,若成功返回 200 + 保留信息并触发推送;若失败返回 409。
+
+- POST /delivery/tasks/:id/accept-reservation (客户端调用)
+ - 输入:taskId
+ - 行为:验证 `reserved_driver_id == currentDriver`,执行原子接受 SQL;成功返回 200;失败返回 409(并建议刷新)。
+
+- POST /delivery/tasks/:id/reject-reservation (可选)
+ - 客户端主动拒绝通知服务端,服务端将清理或增加 `reserve_attempts` 并继续下一候选。
+
+- GET /delivery/candidates?taskId=... (仅供调试/运营查看)
+
+API 需要鉴权并防止滥用。
+
+## 7. 客户端行为(接单侧)
+
+- 收到保留推送后:打开 deep link,展示明显的保留倒计时(reserved_until),并提供“接受/拒绝”按钮。
+- 点击“接受”时调用 `/accept-reservation`,显示 loading,直到服务器确认;若失败显示错误并刷新状态。
+- 若客户端收到普通抢单(无 reserved),按现有抢单逻辑调用 `acceptOrder`。
+- 对于职业骑手 UI:可以展示“预留订单(请在 X 秒内确认)”的特殊样式。
+
+## 8. Worker / 守护进程示例(伪代码)
+
+Node.js 风格伪码(使用 supabase-js 或直接 PG):
+
+```js
+// 每 5s 扫描过期保留
+async function sweepExpiredReserves() {
+ const now = new Date().toISOString()
+ const res = await supa.from('ml_delivery_tasks')
+ .select('*')
+ .lte('reserved_until', now)
+ .is('reserved_driver_id', 'not.null')
+ .eq('status', 1)
+ .limit(100)
+ .execute()
+
+ for (const task of res.data) {
+ // 原子性回退:只有在仍然过期且 reserved_driver_id 不变时更新
+ await supa.from('ml_delivery_tasks').update({ reserved_driver_id: null, reserved_until: null, reserve_attempts: (task.reserve_attempts||0) + 1 }).eq('id', task.id).eq('reserved_driver_id', task.reserved_driver_id).execute()
+
+ // 根据 reserve_attempts 判断是否继续重试或降级
+ if ((task.reserve_attempts || 0) + 1 >= MAX_ATTEMPTS) {
+ // 放回公开抢单或标记人工介入
+ await supa.from('ml_delivery_tasks').update({ /* e.g. public = true */ }).eq('id', task.id).execute()
+ } else {
+ // 触发下一个候选 reserve 流程(可调用分发服务或 enqueue job)
+ await tryReserveNextCandidate(task.id)
+ }
+ }
+}
+```
+
+注意:生产需对并发做保护,使用事务或 Postgres 行锁(SELECT ... FOR UPDATE)来保证单任务在任何时刻只有一个 worker 在处理。
+
+## 9. 配置参数建议
+
+- `reserve_duration_seconds`: 20 ~ 40(默认 30)
+- `max_reserve_attempts`: 3
+- `candidate_batch_size`: 3(每轮推送给多少候选)
+- `candidate_selector_weights`: { distance: 0.4, acceptance_rate: 0.25, current_load: 0.2, rating: 0.1, rider_type_bonus: 0.05 }
+
+这些能在运行后通过运营配置调整。
+
+## 10. 监控与指标
+
+- 平均保留响应时间(保留到 ACK)
+- 保留过期率(被保留后未响应 %)
+- 平均重试次数
+- 最终人工介入率
+- 按骑手类型拆分的接单成功率与完成率
+
+## 11. 兼容性与迁移建议
+
+- 先做 DB 字段兼容性迁移,默认不影响现有逻辑。
+- 逐步启用保留分配:先在小范围(某城市或部分订单)开启 A/B 测试。
+- 运营控制台需支持开关与参数调整。
+
+## 12. 后续落地任务(优先级)
+
+1. 数据库迁移脚本 + 备份。
+2. 服务端 reserve/accept API(含推送触发逻辑)。
+3. 客户端 deep-link + accept-reservation 实现(倒计时 UI)。
+4. Worker/定时器实现并部署到后台任务(或云函数)。
+5. 监控面板与报警阈值设置。
+
+---
+
+如需,我可以继续:
+
+- 生成 `ml_delivery_tasks` 的具体 migration SQL 文件;
+- 在 `pages/mall/delivery` 下修改 `index.uvue` / `order-detail.uvue` 的接单流程代码样例以支持 accept-reservation;
+- 编写一个可部署的 Node.js Worker 示例(含 supabase-js 调用);
+
+请选择接下来要我实现的具体项。
+
+---
+
+## 13. 超时强制指派(强制把无人接单的订单指派给目标骑手)
+
+### 场景与目标
+
+- 场景:订单创建后在设定时限内(例如 5 分钟)始终无人接单或无人响应,需要系统自动将订单强制指派给某位配送员,减少等待并降低人工干预成本。
+- 目标:实现安全、可审计、可回滚的“强制指派”流程,优先保证原子性并记录审计日志,必要时降级为人工处理。
+
+### 推荐 DB 变更(migration SQL)
+
+```sql
+-- 强制指派字段
+ALTER TABLE ml_delivery_tasks
+ ADD COLUMN forced_assign boolean DEFAULT false NOT NULL,
+ ADD COLUMN forced_at timestamptz NULL,
+ ADD COLUMN forced_to uuid NULL,
+ ADD COLUMN forced_reason text NULL,
+ ADD COLUMN forced_by text NULL; -- 'system' | operator id
+
+-- 审计表(如不存在)
+CREATE TABLE IF NOT EXISTS ml_task_audit (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ task_id uuid NOT NULL,
+ action varchar(64) NOT NULL,
+ actor varchar(128) NOT NULL,
+ target_driver uuid NULL,
+ meta jsonb NULL,
+ created_at timestamptz DEFAULT now()
+);
+```
+
+兼容性:新增字段有默认值或 nullable,老数据无需回滚。
+
+### 强制指派的原子 SQL
+
+```sql
+-- 仅当任务仍处于待接状态时执行强制指派
+UPDATE ml_delivery_tasks
+SET driver_id = $driverId,
+ status = 2,
+ forced_assign = true,
+ forced_at = NOW(),
+ forced_to = $driverId,
+ forced_reason = $reason,
+ forced_by = 'system'
+WHERE id = $taskId AND status = 1
+RETURNING *;
+```
+
+执行成功返回一行则指派生效;若返回 0 行说明任务已被其它流程处理,应读取当前状态并按需回滚或记录冲突。
+
+### 目标骑手选择策略(示例)
+
+- 优先条件:在线(work_status=1)、当前任务数低、接单率高、评分高、车辆类型匹配、距离近(如可用定位)。
+- 简单 SQL 示例(无地理数据):
+
+```sql
+SELECT id FROM ml_delivery_drivers
+WHERE work_status = 1
+ AND current_task_count < 3
+ORDER BY acceptance_rate DESC, current_task_count ASC
+LIMIT 1;
+```
+
+对有位置数据的系统,应使用近邻查询优先选择最近且空闲的骑手。
+
+### 优先级规则(按你的要求:配送条件 → 距离 → 综合评分)
+
+实现原则:先严格筛除不满足“配送条件”的骑手(必须具备所需车辆类型、服务区域、资质等);在满足条件的候选集中,优先选择与取货点/送达点最近的骑手;当距离接近或候选数多时,再按综合评分排序(接单率、历史评分、当前负载等)。
+
+流程示例:
+
+1. 过滤(必须满足配送条件)
+ - 例如:`vehicle_type` 与订单要求匹配、`service_areas` 包含目标区域、`auth_status` 通过。
+2. 距离优先(近者优先)
+ - 若有经纬度,可以用近邻查询返回按距离排序的候选集(例如 10 个最近骑手)。
+3. 综合评分(用于同距离或候选排序)
+ - 建议评分函数(可调权重):
+
+$$
+score = w_d \times (1 - \frac{distance}{maxDistance}) + w_a \times acceptanceRate + w_r \times rating - w_l \times normalizedLoad
+$$
+
+ - 示例权重:`w_d=0.5`(距离)、`w_a=0.25`(接单率)、`w_r=0.2`(评分)、`w_l=0.05`(负载惩罚)。
+ - `normalizedLoad` 可定义为 `current_task_count / max_allowed_tasks`,范围 [0,1]。
+
+4. 按 score 降序选择候选并进行 `reserve` 或直接指派(依据调度策略)。
+
+示例伪 SQL(含地理与评分,Postgres + PostGIS 假设):
+
+```sql
+SELECT id, ST_Distance(location, ST_SetSRID(ST_MakePoint($lng, $lat), 4326)) AS dist,
+ (0.5 * (1 - LEAST(ST_Distance(location, ST_SetSRID(ST_MakePoint($lng, $lat),4326)) / $max_dist, 1))
+ + 0.25 * acceptance_rate
+ + 0.2 * rating
+ - 0.05 * (current_task_count::float / GREATEST(max_allowed_tasks,1))) AS score
+FROM ml_delivery_drivers
+WHERE work_status = 1
+ AND vehicle_type = $required_vehicle_type
+ AND service_areas @> ARRAY[$area]
+ORDER BY score DESC
+LIMIT 10;
+```
+
+注意事项:
+- `service_areas` 的存储与查询方式会影响性能(可考虑使用单独的覆盖表或索引)。
+- 地理查询需配置 PostGIS 或使用第三方距离服务;若无地理支持,可近似用行政区/街道距离或预计算网格。
+- 权重应可配置并在小范围内 A/B 测试调整为最优值。
+
+
+### 后台 Worker(伪代码)
+
+```js
+// 定时扫描超时未被接单的任务并强制指派
+async function sweepAndForceAssign() {
+ const TIMEOUT_MIN = 5;
+ const cutoff = new Date(Date.now() - TIMEOUT_MIN * 60 * 1000).toISOString();
+ const { data: tasks } = await supa.from('ml_delivery_tasks')
+ .select('*')
+ .lte('created_at', cutoff)
+ .eq('status', 1)
+ .limit(50)
+ .execute();
+
+ for (const t of tasks || []) {
+ // 选择候选骑手(示例)
+ const { data: cand } = await supa.from('ml_delivery_drivers')
+ .select('*')
+ .eq('work_status', 1)
+ .lt('current_task_count', 3)
+ .order('acceptance_rate', { ascending: false })
+ .limit(1)
+ .execute();
+
+ if (!cand || cand.length === 0) {
+ // 无可用骑手,标记人工处理或继续等待
+ await supa.from('ml_task_audit').insert({ task_id: t.id, action: 'force_assign_no_candidate', actor: 'system', meta: { reason: 'no_candidate' } }).execute();
+ continue;
+ }
+
+ const driver = cand[0];
+ // 原子更新强制指派
+ const res = await supa.from('ml_delivery_tasks').update({
+ driver_id: driver.id,
+ status: 2,
+ forced_assign: true,
+ forced_at: new Date().toISOString(),
+ forced_to: driver.id,
+ forced_reason: 'timeout_auto',
+ forced_by: 'system'
+ }).eq('id', t.id).eq('status', 1).execute();
+
+ if (res && !res.error && Array.isArray(res.data) && res.data.length > 0) {
+ await supa.from('ml_task_audit').insert({ task_id: t.id, action: 'force_assign', actor: 'system', target_driver: driver.id, meta: { reason: 'timeout_auto' } }).execute();
+ await sendPush(driver.user_id, `系统已指派订单 ${t.order_no} 给您,请尽快处理`);
+ } else {
+ // 处理并发失败或冲突
+ await supa.from('ml_task_audit').insert({ task_id: t.id, action: 'force_assign_failed', actor: 'system', meta: { error: res && res.error } }).execute();
+ }
+ }
+}
+```
+
+建议把关键更新封装为 Postgres 函数(RPC)以保证事务性与条件检查在 DB 内完成,减少竞态。
+
+### 审计与回滚
+
+- 每次强制指派都应写入 `ml_task_audit`,记录时间、操作人(system 或 operator id)、目标骑手与原因。
+- 若被指派骑手无法接单(例如拒绝或技术问题),应提供退回/重分配流程,并在 audit 中记录 reject 原因,必要时通知运营人工介入。
+
+### 运营与告警策略
+
+- 若 Worker 连续多次找不到骑手或指派失败,需将任务标记为“人工处理”并通知运营。
+- 设置告警阈值(例如:订单创建后 10 分钟仍未派出),并在控制台列出待人工处理订单。
+
+### 推荐落地步骤(优先级)
+
+1. 添加 migration(`forced_*` 字段)并部署(含备份)。
+2. 实现并测试 DB 原子更新(建议 RPC / Postgres 函数)。
+3. 编写并部署 Worker,先在小范围测试(例如个别城市)。
+4. 在客户端与管理端展示 `forced_assign` 状态与审计记录(便于追踪)。
+5. 根据运行数据调整选择策略与告警阈值。
+
+---
+
+我已把强制指派方案追加到文档中。需要我同时生成 migration SQL 文件或创建 Worker 示例代码文件吗?
+
+---
+
+## 14. 商家加价(Bump/Bounty)与系统激励机制
+
+### 概述
+
+支持商家为单笔订单支付额外费用以提高该订单的被分配/被抢到概率(称为 `bump` 或 `bounty`);同时系统可以对接受强制指派或在高优先级任务上表现良好的骑手发放奖励(一次性奖金或积分),以激励及时响应与履约。
+
+这些功能能显著降低用户等待时间并提高派单成功率,但需做好计费、结算与反作弊设计。
+
+### 推荐 DB 字段(示例迁移)
+
+```sql
+-- 在订单表加入商家加价字段
+ALTER TABLE ml_orders
+ ADD COLUMN bounty_amount numeric(10,2) DEFAULT 0 NOT NULL,
+ ADD COLUMN bounty_paid boolean DEFAULT false NOT NULL; -- 表示商家是否已为该笔加价完成支付
+
+-- 在配送任务表加入强制奖励字段
+ALTER TABLE ml_delivery_tasks
+ ADD COLUMN force_reward numeric(10,2) DEFAULT 0 NOT NULL, -- 系统为强制/特殊任务提供的奖励
+ ADD COLUMN bounty_source boolean DEFAULT false NOT NULL; -- 表示此任务来自商家加价
+
+-- 奖励/积分流水表(简单表)
+CREATE TABLE IF NOT EXISTS ml_driver_rewards (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ driver_id uuid NOT NULL,
+ task_id uuid NULL,
+ amount numeric(10,2) NULL,
+ points int NULL,
+ reason varchar(128) NOT NULL,
+ created_at timestamptz DEFAULT now()
+);
+```
+
+兼容逻辑:`bounty_amount=0` 表示无加价,`bounty_paid=false` 表示尚未扣款/未确认(下单后商家可在一定时间内完成支付)。
+
+### 商家加价流程(建议实现)
+
+1. 商家在下单或订单创建后,通过商家端页面选择“加价抢单”并填写 `bounty_amount`,系统计算费用并要求商家预付(或平台代扣)。
+2. 支付成功后设置 `ml_orders.bounty_paid = true` 并创建/更新对应 `ml_delivery_tasks`,将 `bounty_amount` 传递给调度模块作为 `bounty_weight`。
+3. 调度模块在计算候选时,将 `bounty` 作为优先级加权(例如将 `score` 加上 `bounty_amount` 的标准化值或直接提升到优先公开池)。
+4. 被分配或抢单的骑手在完成配送并被确认后,系统在结算周期或即时将 `bounty_amount` 的一部分/全部发放给履约骑手,并写入 `ml_driver_rewards`。
+
+注意:若商家加价但未被任何骑手接受或订单取消,应按平台政策退款并写退款记录。
+
+### 系统激励(接受强制指派、准时送达等)
+
+- 场景示例:
+ - 系统强制指派(`forced_assign = true`):若骑手接受并按时完成,可获得 `force_reward` 奖金。
+ - 高峰期临时任务:平台补贴奖励以提高接单率。
+ - 准时率或用户好评达到阈值:按月/周期发放积分或奖金。
+
+实现要点:
+- 在 `ml_delivery_tasks` 上记录 `force_reward`、`bounty_source` 与奖励发放状态(如 `reward_paid`)。
+- 在任务完成并确认后(例如订单状态变为已送达并经核验),执行奖励发放逻辑:插入 `ml_driver_rewards`,更新司机可提现余额或积分。
+- 奖励发放应在结算账目中保留审计链路,例如 `ml_financial_records` 或 `ml_payouts`。
+
+### 调度层的加价权重与计价模型(示例)
+
+- 最简单:把 `bounty_amount` 加入 `score`,比如 `score += normalize(bounty_amount)`。
+- 更保守:将加价放入优先池(paid-priority pool),先在该池内做分配,池内遵循原有的配送条件→距离→评分顺序。
+- 建议:将 `bounty` 设置为可配置的权重/阈值,并在 A/B 测试中观察对派单成功率与退款率的影响。
+
+### 计费、结算与会计注意
+
+- 若商家预付,平台应在完成时将 bounty 转入商家支出记录与司机收入(或待结算)。若平台代垫,应有相应的对账流程。
+- 退款场景:超时无人接单、订单取消或被判定不合规时,应触发退款并更新 `bounty_paid` 与退款记录表。
+
+### 前端/UX 改动(商家端与骑手端)
+
+- 商家端:在下单/订单详情页加入“加价抢单”入口、金额输入、支付流程提示与退改策略说明。
+- 骑手端:在任务卡片上显示 `bounty_amount` / `force_reward`;对高赏金任务给予明显视觉标识与优先提醒。
+- 管理端:展示加价订单、加价/奖励流水、退款记录与可手动干预入口。
+
+### 反作弊与风控
+
+- 防止商家刷单或通过加价包裹恶意收益:限制同一商家/收货人短时间内频繁加价,设置最小/最大加价额度,或要求人工审核异常加价。
+- 骑手端防止滥用:例如禁止重复接受强制指派后拒绝以获取奖励;对异常行为纳入扣分/违规记录。
+
+### 示例:把 `bounty` 加入评分(伪代码)
+
+```js
+// normalize bounty into [0,1] by expected_max_bounty
+const bountyNorm = Math.min(order.bounty_amount / expectedMaxBounty, 1)
+const score = 0.45 * distanceScore + 0.25 * acceptanceRate + 0.2 * rating - 0.05 * load + 0.05 * bountyNorm
+```
+
+权重与 normalize 函数应由运营可配置并在小范围内 AB 测试。
+
+---
+
+文档已更新:包含商家加价与系统激励机制的设计、DB 建议、计费与反作弊要点。是否需要我把 `migration SQL` 文件和一个简单的 Worker/服务端示例代码生成到仓库中?
\ No newline at end of file
diff --git a/pages/mall/delivery/index.uvue b/pages/mall/delivery/index.uvue
index de149c9b..901570cf 100644
--- a/pages/mall/delivery/index.uvue
+++ b/pages/mall/delivery/index.uvue
@@ -815,7 +815,7 @@ import { getCurrentUserId, getCurrentUser } from '@/utils/store.uts'
}
-
diff --git a/pages/mall/delivery/test/consumer-logistics-detail.uvue b/pages/mall/delivery/test/consumer-logistics-detail.uvue
new file mode 100644
index 00000000..f528ec3a
--- /dev/null
+++ b/pages/mall/delivery/test/consumer-logistics-detail.uvue
@@ -0,0 +1,494 @@
+
+
+
+
+
+
+
+
+
+
+ {{ order.carrier }}快递
+ 运单号: {{ order.tracking_no }}
+
+
+
+
+
+
+
+ 物流追踪
+
+
+
+
+
+
+
+
+
+
+
+
+ 📍
+ {{ event.location }}
+
+
+
+
+
+
+
+
+
+ 暂无物流动态
+
+
+
+
+
+
+ 订单信息
+
+ 订单编号
+ {{ order.order_no }}
+
+
+ 下单时间
+ {{ order.created_at }}
+
+
+ 实付金额
+ ¥{{ order.amount || '0.00' }}
+
+
+ 收货地址
+ {{ order.address }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/test/consumer-order-list.uvue b/pages/mall/delivery/test/consumer-order-list.uvue
new file mode 100644
index 00000000..5f81d2eb
--- /dev/null
+++ b/pages/mall/delivery/test/consumer-order-list.uvue
@@ -0,0 +1,225 @@
+
+
+
+
+
+ 全部
+ 进行中
+ 已签收
+
+
+
+
+
+
+
+ 📦
+
+ {{ getLatestMsg(order.order_no) }}
+ 订单号: {{ order.order_no }}
+
+ >
+
+
+
+
+
+
+ 📂
+ 暂无相关包裹信息
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/test/index.uvue b/pages/mall/delivery/test/index.uvue
new file mode 100644
index 00000000..e6f0e124
--- /dev/null
+++ b/pages/mall/delivery/test/index.uvue
@@ -0,0 +1,122 @@
+
+
+
+
+
+ 商家后台 (Merchant)
+
+ 订单列表与发货
+ 查看待发货订单、绑定运单号
+
+
+ 订单详情与物流轨迹
+ 物流时间线展示、签收凭证预览
+
+
+
+
+ 平台后台 (Platform/Ops)
+
+ 运单/轨迹查询 (排障)
+ 按运单号查询、查看 Raw Payload、Mock 控制
+
+
+ Webhook 接收日志
+ 核对回调、验签状态、入库结果
+
+
+ 对接配置中心
+ 承运商管理、Mock 服务开关、密钥配置
+
+
+
+
+ 消费者端 (Consumer/APP)
+
+ 我的快递 (物流列表)
+ 查看多个包裹状态、实时动态摘要
+
+
+ 用户物流详情页
+ 单包裹高保真轨迹、联系派送员
+
+
+
+
+ 模拟与开发工具 (Tools)
+
+ 第三方 API 推送模拟
+ 手动构造 Webhook Payload 发送给系统
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/test/merchant-order-detail.uvue b/pages/mall/delivery/test/merchant-order-detail.uvue
new file mode 100644
index 00000000..18b1a548
--- /dev/null
+++ b/pages/mall/delivery/test/merchant-order-detail.uvue
@@ -0,0 +1,269 @@
+
+
+
+
+ {{ getStatusText(order.status) }}
+ {{ order.carrier }}: {{ order.tracking_no }}
+ 待商家发货
+
+
+
+
+
+
+ 收货人
+ {{ order.receiver_name }}
+
+
+ 手机号
+ {{ order.receiver_masked_phone }}
+
+
+ 地址
+ {{ order.address }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ seg }}
+
+ {{ event.event_time }}
+
+
+
+
+
+
+
+
+
+ 暂无物流动态
+
+
+
+
+
+
+
+
+
+ 订单号
+ {{ order.order_no }}
+
+
+ 下单时间
+ {{ order.created_at }}
+
+
+ 支付金额
+ ¥{{ order.amount }}
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/test/merchant-order-list.uvue b/pages/mall/delivery/test/merchant-order-list.uvue
new file mode 100644
index 00000000..932fe6f5
--- /dev/null
+++ b/pages/mall/delivery/test/merchant-order-list.uvue
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+ 全部
+ 待发货
+ 已发货
+
+
+
+
+
+
+
+
+ 下单时间:
+ {{ order.created_at }}
+
+
+ 收件人:
+ {{ order.receiver_name }} {{ order.receiver_masked_phone }}
+
+
+ 运单号:
+ {{ order.tracking_no }} ({{ order.carrier }})
+
+
+
+
+
+
+
+ 暂无订单
+
+
+
+
+
+
+
+
+
+ 选择承运商
+
+ {{ currentCarrier || '请选择' }}
+
+
+
+ 运单号
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/test/mock-service.uts b/pages/mall/delivery/test/mock-service.uts
new file mode 100644
index 00000000..63dfd37d
--- /dev/null
+++ b/pages/mall/delivery/test/mock-service.uts
@@ -0,0 +1,496 @@
+/**
+ * 配送模块模拟数据服务 (Mock Service)
+ * 依据: pages/mall/delivery/doc/需求文档/
+ * 涵盖: 商家端订单、平台端轨迹、Webhook 日志与场景模拟
+ */
+
+export interface MockOrder {
+ order_no: string
+ status: string
+ created_at: string
+ receiver_name: string
+ receiver_masked_phone: string
+ address: string
+ amount: string
+ carrier: string
+ tracking_no: string
+ last_synced_at?: string
+}
+
+export interface MockTrackingEvent {
+ event_id: string
+ event_time: string
+ event_code: string
+ event_text: string
+ status_code: string
+ node_name?: string
+ location?: string
+ evidence_urls: string[]
+ raw_payload?: string
+}
+
+export interface MockWebhookLog {
+ time: string
+ carrier: string
+ tracking_no: string
+ event_code: string
+ success: boolean
+ result_text: string
+ payload: UTSJSONObject
+}
+
+export interface MockCarrierOption {
+ code: string
+ name: string
+}
+
+class MockService {
+ private orders: MockOrder[] = [
+ {
+ order_no: 'ORD20260205001',
+ status: 'PENDING',
+ created_at: '2026-02-05 10:00',
+ receiver_name: '张三',
+ receiver_masked_phone: '138****8000',
+ address: '北京市朝阳区某某街道100号',
+ amount: '299.00',
+ tracking_no: '',
+ carrier: ''
+ },
+ {
+ order_no: 'ORD20260205002',
+ status: 'DELIVERED',
+ created_at: '2026-02-05 09:30',
+ receiver_name: '李四',
+ receiver_masked_phone: '139****1234',
+ address: '上海市浦东新区某某路200号',
+ amount: '158.50',
+ tracking_no: 'YD987654321',
+ carrier: '韵达',
+ last_synced_at: '2026-02-05 14:35'
+ },
+ {
+ order_no: 'ORD20260205003',
+ status: 'SHIPPED',
+ created_at: '2026-02-04 15:00',
+ receiver_name: '王五',
+ receiver_masked_phone: '137****5566',
+ address: '广州市天河区某某大厦15楼',
+ amount: '88.00',
+ tracking_no: 'ZT123456789',
+ carrier: '中通',
+ last_synced_at: '2026-02-05 10:00'
+ },
+ {
+ order_no: 'ORD20260205004',
+ status: 'OUT_FOR_DELIVERY',
+ created_at: '2026-02-05 08:00',
+ receiver_name: '赵六',
+ receiver_masked_phone: '135****0011',
+ address: '杭州市西湖区某某创意园',
+ amount: '450.00',
+ tracking_no: 'SF666888999',
+ carrier: '顺丰',
+ last_synced_at: '2026-02-06 09:00'
+ },
+ {
+ order_no: 'ORD20260205005',
+ status: 'EXCEPTION',
+ created_at: '2026-02-03 12:00',
+ receiver_name: '孙七',
+ receiver_masked_phone: '136****9988',
+ address: '成都市武侯区某某软件园',
+ amount: '120.00',
+ tracking_no: 'YT555444333',
+ carrier: '圆通',
+ last_synced_at: '2026-02-04 18:00'
+ }
+ ]
+
+ // 全局 Mock 配置项,支持页面间同步
+ public isTestMode: boolean = true
+ public autoPush: boolean = true
+ public mockUrl: string = 'http://192.168.1.100:3000/mock/v1'
+
+ // 持久化存储轨迹:按订单号存储
+ private trackingHistory: Map = new Map()
+ private webhookLogs: MockWebhookLog[] = []
+
+ constructor() {
+ this.initDefaultHistory()
+ this.initDefaultLogs()
+ }
+
+ private initDefaultLogs() {
+ this.webhookLogs = [
+ {
+ time: '14:35:22', carrier: '韵达', tracking_no: 'YD987654321',
+ event_code: 'DELIVERED', success: true, result_text: '成功入库',
+ payload: {
+ trackingNo: 'YD987654321',
+ status: 'DELIVERED',
+ msg: '您的快件已由本人签收'
+ } as UTSJSONObject
+ }
+ ]
+ }
+
+ private initDefaultHistory() {
+ const tip = "【物流问题无需找商家或平台,请致电(95338)(专属热线:400-811-1111)更快解决】"
+
+ // 已签收订单轨迹
+ this.trackingHistory.set('ORD20260205002', [
+ {
+ event_id: 'e210',
+ event_time: '2026-02-05 18:30',
+ event_code: 'DELIVERED',
+ event_text: '您的快件已由本人签收。感谢使用韵达快递,期待再次为您服务!',
+ status_code: 'DELIVERED',
+ evidence_urls: ['https://img-shop.gmugmu.com/mock/pod_sample.png']
+ },
+ {
+ event_id: 'e209',
+ event_time: '2026-02-05 15:25',
+ event_code: 'OUT_FOR_DELIVERY',
+ event_text: '派送员张师傅(13800138000)正在派件(事先呼我,勿找商家)',
+ status_code: 'OUT_FOR_DELIVERY',
+ evidence_urls: []
+ },
+ {
+ event_id: 'e208',
+ event_time: '2026-02-05 15:24',
+ event_code: 'ARRIVED_HUB',
+ event_text: '【朝阳分部】已收入',
+ status_code: 'ARRIVED_HUB',
+ evidence_urls: []
+ },
+ {
+ event_id: 'e207',
+ event_time: '2026-02-05 15:23',
+ event_code: 'ARRIVED',
+ event_text: '您的快件已经到达【朝阳区】' + tip,
+ status_code: 'IN_TRANSIT',
+ evidence_urls: []
+ },
+ {
+ event_id: 'e200',
+ event_time: '2026-02-05 09:46',
+ event_code: 'SHIPPED',
+ event_text: '包裹正在等待揽收',
+ status_code: 'SHIPPED',
+ evidence_urls: []
+ }
+ ])
+
+ // 运输中订单轨迹
+ this.trackingHistory.set('ORD20260205003', [
+ {
+ event_id: 'e305',
+ event_time: '2026-02-05 10:00',
+ event_code: 'TRANSIT',
+ event_text: '快件离开【广州分拣中心】,已发往【天河分部】',
+ status_code: 'IN_TRANSIT',
+ evidence_urls: []
+ },
+ {
+ event_id: 'e301',
+ event_time: '2026-02-04 15:30',
+ event_code: 'PICKED',
+ event_text: '包裹已揽收',
+ status_code: 'ARRIVED_HUB',
+ evidence_urls: []
+ }
+ ])
+
+ // 派送中订单轨迹
+ this.trackingHistory.set('ORD20260205004', [
+ {
+ event_id: 'e405',
+ event_time: '2026-02-06 09:00',
+ event_code: 'OUT_FOR_DELIVERY',
+ event_text: '派送员王师傅(13700137000)正在派件',
+ status_code: 'OUT_FOR_DELIVERY',
+ evidence_urls: []
+ },
+ {
+ event_id: 'e401',
+ event_time: '2026-02-05 18:00',
+ event_code: 'ARRIVED',
+ event_text: '快件到达【杭州西湖分拨中心】',
+ status_code: 'IN_TRANSIT',
+ evidence_urls: []
+ }
+ ])
+
+ // 异常订单轨迹
+ this.trackingHistory.set('ORD20260205005', [
+ {
+ event_id: 'e505',
+ event_time: '2026-02-04 18:00',
+ event_code: 'EXCEPTION',
+ event_text: '【包裹异常】由于天气原因,快件将在分拨中心稍作停留',
+ status_code: 'EXCEPTION',
+ evidence_urls: []
+ },
+ {
+ event_id: 'e501',
+ event_time: '2026-02-03 14:00',
+ event_code: 'PICKED',
+ event_text: '包裹已揽收',
+ status_code: 'ARRIVED_HUB',
+ evidence_urls: []
+ }
+ ])
+ }
+
+ getAvailableCarriers(): MockCarrierOption[] {
+ return [
+ { code: 'YUNDA', name: '韵达' },
+ { code: 'YTO', name: '圆通' },
+ { code: 'ZTO', name: '中通' },
+ { code: 'STO', name: '申通' }
+ ]
+ }
+
+ getMockOrders(): MockOrder[] {
+ return this.orders
+ }
+
+ bindShipment(orderNo: string, carrier: string, trackingNo: string): MockOrder | null {
+ const order = this.orders.find(o => o.order_no === orderNo)
+ if (!order) return null
+
+ order.carrier = carrier
+ order.tracking_no = trackingNo
+ order.status = 'SHIPPED'
+ order.last_synced_at = this.formatDate(new Date())
+
+ // 初始化轨迹
+ this.trackingHistory.set(orderNo, [
+ {
+ event_id: 'init_' + Date.now(),
+ event_time: order.last_synced_at,
+ event_code: 'CREATED',
+ event_text: '商家已发货,等待快递公司揽收',
+ status_code: '已发货',
+ evidence_urls: []
+ }
+ ])
+ return order
+ }
+
+ getMockTracking(id: string): MockTrackingEvent[] {
+ const order = this.orders.find(o => o.order_no === id || o.tracking_no === id)
+ if (order != null && this.trackingHistory.has(order.order_no)) {
+ return this.trackingHistory.get(order.order_no)!
+ }
+ return []
+ }
+
+ /**
+ * 生成符合消费者端高保真展示的物流轨迹
+ */
+ generateFullProcess(id: string) {
+ const order = this.orders.find(o => o.order_no === id || o.tracking_no === id)
+ if (!order) return
+
+ const now = new Date()
+ const getPastTime = (days: number, hours: number): string => {
+ const d = new Date(now.getTime() - (days * 24 + hours) * 3600 * 1000)
+ const YY = d.getFullYear()
+ const M = (d.getMonth() + 1).toString().padStart(2, '0')
+ const DD = d.getDate().toString().padStart(2, '0')
+ const h = d.getHours().toString().padStart(2, '0')
+ const m = d.getMinutes().toString().padStart(2, '0')
+ return `${YY}-${M}-${DD} ${h}:${m}`
+ }
+
+ const tip = "【物流问题无需找商家或平台,请致电(95338)(专属热线:400-811-1111)更快解决】"
+
+ const fullProcess: MockTrackingEvent[] = [
+ {
+ event_id: 'f7',
+ event_time: getPastTime(0, 1),
+ event_code: 'OUT_FOR_DELIVERY',
+ event_text: '派送员张师傅(13800138000)正在派件(事先呼我,勿找商家)',
+ status_code: 'OUT_FOR_DELIVERY',
+ evidence_urls: []
+ },
+ {
+ event_id: 'f6',
+ event_time: getPastTime(0, 4),
+ event_code: 'TRANSIT',
+ event_text: '【朝阳分部】已收入',
+ status_code: 'ARRIVED_HUB',
+ evidence_urls: []
+ },
+ {
+ event_id: 'f5',
+ event_time: getPastTime(0, 12),
+ event_code: 'ARRIVED',
+ event_text: '您的快件已经到达【北京朝阳区】' + tip,
+ status_code: 'IN_TRANSIT',
+ evidence_urls: []
+ },
+ {
+ event_id: 'f4',
+ event_time: getPastTime(1, 2),
+ event_code: 'DEPARTED',
+ event_text: '您的快件离开【顺义转运中心】,已发往【北京朝阳区】',
+ status_code: 'IN_TRANSIT',
+ evidence_urls: []
+ },
+ {
+ event_id: 'f3',
+ event_time: getPastTime(1, 10),
+ event_code: 'ARRIVED_HUB',
+ event_text: '您的快件已经到达【顺义转运中心】' + tip,
+ status_code: 'ARRIVED_HUB',
+ evidence_urls: []
+ },
+ {
+ event_id: 'f2',
+ event_time: getPastTime(1, 20),
+ event_code: 'PICKED',
+ event_text: '您的快件在【北京海淀区】已揽收,揽收人:李师傅(13911112222)' + tip,
+ status_code: 'ARRIVED_HUB',
+ evidence_urls: []
+ },
+ {
+ event_id: 'f1',
+ event_time: getPastTime(2, 1),
+ event_code: 'SHIPPED',
+ event_text: '包裹正在等待揽收',
+ status_code: 'SHIPPED',
+ evidence_urls: []
+ }
+ ]
+
+ this.trackingHistory.set(order.order_no, fullProcess)
+ order.status = 'OUT_FOR_DELIVERY'
+ order.last_synced_at = this.formatDate(now)
+ }
+
+ runScenario(waybillNo: string, scenario: string) {
+ // 兼容逻辑:优先按运单号搜,搜不到按订单号搜
+ let order = this.orders.find(o => o.tracking_no === waybillNo)
+ if (!order) {
+ order = this.orders.find(o => o.order_no === waybillNo)
+ }
+ if (!order) return
+
+ if (scenario === 'full') {
+ this.generateFullProcess(order.order_no)
+ } else if (scenario === 'exception') {
+ const history = this.getMockTracking(order.order_no)
+ history.unshift({
+ event_id: 'ex_' + Date.now(),
+ event_time: this.formatDate(new Date()),
+ event_code: 'EXCEPTION',
+ event_text: '【包裹异常】由于天气原因,快件将在分拨中心稍作停留',
+ status_code: 'EXCEPTION',
+ evidence_urls: []
+ })
+ order.status = 'EXCEPTION'
+ } else {
+ // 默认:模拟一个新的在途节点
+ let history = this.getMockTracking(order.order_no)
+
+ // 如果该订单还没轨迹(比如刚发货),先初始化数组
+ if (!this.trackingHistory.has(order.order_no)) {
+ this.trackingHistory.set(order.order_no, [])
+ history = this.trackingHistory.get(order.order_no)!
+ }
+
+ const now = new Date()
+ history.unshift({
+ event_id: 'st_' + Date.now(),
+ event_time: this.formatDate(now),
+ event_code: 'TRANS_UPDATE',
+ event_text: '快件已到达新的中转场进行分拣,准备发往目的地',
+ status_code: 'IN_TRANSIT',
+ evidence_urls: []
+ })
+ order.last_synced_at = this.formatDate(now)
+ }
+ }
+
+ /**
+ * 模拟从云端同步最新轨迹(使刷新按钮生效)
+ */
+ async syncFromCloud(orderNo: string): Promise {
+ const order = this.orders.find(o => o.order_no === orderNo)
+ if (!order || order.status === 'DELIVERED') return false
+
+ // 模拟网络延迟
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ this.runScenario(order.tracking_no, 'step')
+ resolve(true)
+ }, 800)
+ })
+ }
+
+ getMockWebhookLogs(): MockWebhookLog[] {
+ return this.webhookLogs
+ }
+
+ /**
+ * 核心功能:模拟第三方回调接口
+ * 模拟外部物流平台向本系统发送轨迹更新 API
+ */
+ pushWebhookData(payload: UTSJSONObject): boolean {
+ const tracking_no = payload['tracking_no'] as string
+ const status_code = payload['status_code'] as string
+ const event_text = payload['event_text'] as string
+ const carrier = payload['carrier'] as string || '顺丰'
+
+ // 1. 记录原始日志
+ const now = new Date()
+ const log: MockWebhookLog = {
+ time: this.formatDate(now).split(' ')[1],
+ carrier: carrier,
+ tracking_no: tracking_no,
+ event_code: status_code,
+ success: true,
+ result_text: '接收成功',
+ payload: payload
+ }
+ this.webhookLogs.unshift(log)
+
+ // 2. 更新系统内部轨迹
+ const order = this.orders.find(o => o.tracking_no === tracking_no)
+ if (order != null) {
+ if (!this.trackingHistory.has(order.order_no)) {
+ this.trackingHistory.set(order.order_no, [])
+ }
+ const history = this.trackingHistory.get(order.order_no)!
+ history.unshift({
+ event_id: 'wb_' + Date.now(),
+ event_time: this.formatDate(now),
+ event_code: status_code,
+ event_text: event_text,
+ status_code: status_code,
+ evidence_urls: []
+ })
+ order.status = status_code
+ order.last_synced_at = this.formatDate(now)
+ return true
+ }
+
+ log.success = false
+ log.result_text = '未找到对应的运单号'
+ return false
+ }
+
+ private formatDate(date: Date): string {
+ const Y = date.getFullYear()
+ const M = (date.getMonth() + 1).toString().padStart(2, '0')
+ const D = date.getDate().toString().padStart(2, '0')
+ const h = date.getHours().toString().padStart(2, '0')
+ const m = date.getMinutes().toString().padStart(2, '0')
+ return `${Y}-${M}-${D} ${h}:${m}`
+ }
+}
+
+export const mockService = new MockService()
diff --git a/pages/mall/delivery/test/platform-config-center.uvue b/pages/mall/delivery/test/platform-config-center.uvue
new file mode 100644
index 00000000..20da4cfb
--- /dev/null
+++ b/pages/mall/delivery/test/platform-config-center.uvue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+ {{ isTestMode ? '测试 (MOCK 模式开启)' : '生产 (已连接真实承运商)' }}
+
+
+
+
+
+ MOCK 服务配置
+
+ Server 地址
+
+
+
+ Webhook 接收密钥 (Secret)
+
+
+
+ 自动推送场景
+
+
+
+
+
+
+
+
+
+
+
+ {{ carrier.name }} ({{ carrier.code }})
+ 回调: {{ carrier.callback_status ? '在线' : '离线' }} | 轮询: {{ carrier.polling ? '开' : '关' }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/test/platform-tracking-query.uvue b/pages/mall/delivery/test/platform-tracking-query.uvue
new file mode 100644
index 00000000..a2d32949
--- /dev/null
+++ b/pages/mall/delivery/test/platform-tracking-query.uvue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 基本信息
+
+
+ 运单号:
+ {{ waybillInfo.tracking_no }}
+
+
+ 承运商:
+ {{ waybillInfo.carrier }}
+
+
+ 订单号:
+ {{ waybillInfo.order_no }}
+
+
+ 当前状态:
+ {{ waybillInfo.status }}
+
+
+
+
+
+
+ 轨迹列表 (内网入库)
+
+
+ {{ formatTime(event.event_time) }}
+
+
+ {{ event.event_text }}
+
+ CODE: {{ event.event_code }}
+ MAP: {{ event.status_code }}
+
+
+ {{ JSON.stringify(event.raw_payload, null, 2) }}
+
+ {{ showRaw[index] ? '折叠原始报文' : '查看原始报文' }}
+
+
+
+
+
+
+ Mock 控制台 (测试辅助)
+
+
+
+
+
+
+
+
+
+ 未找到相关运单记录
+
+
+
+
+
+
+
diff --git a/pages/mall/delivery/test/platform-webhook-logs.uvue b/pages/mall/delivery/test/platform-webhook-logs.uvue
new file mode 100644
index 00000000..f20e3f4b
--- /dev/null
+++ b/pages/mall/delivery/test/platform-webhook-logs.uvue
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+ 99.8%
+ 成功率
+
+
+ 24
+ 验签失败
+
+
+ 1.2s
+ 平均延迟
+
+
+
+
+
+
+
+
+
+ 单号:
+ {{ log.tracking_no }}
+
+
+ 事件:
+ {{ log.event_code }}
+
+
+ 状态:
+ {{ log.result_text }}
+
+
+
+
+ 查看 Payload
+ 尝试重新入库
+
+
+
+
+
+
+
+
+
+
+ {{ currentPayload }}
+
+
+
+
+
+
+
+
+
+
From a5e7afacecda3f1b40ec9d68f515977214dd040c Mon Sep 17 00:00:00 2001
From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com>
Date: Fri, 6 Feb 2026 16:56:24 +0800
Subject: [PATCH 08/26] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=BA=93=E8=A1=A8=E6=A0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages/mall/delivery/doc/需求文档/README.md | 83 ++++++
.../express_tracking_platform_upgrade.sql | 8 +-
.../seed_platform_express_test_data.sql | 236 ++++++++++++++++++
.../需求文档/simulate_third_party_to_db.sql | 191 ++++++++++++++
pages/mall/delivery/test/api-simulator.uvue | 80 +++---
.../test/consumer-logistics-detail.uvue | 3 +-
.../delivery/test/consumer-order-list.uvue | 3 +-
.../delivery/test/merchant-order-detail.uvue | 9 +-
.../delivery/test/merchant-order-list.uvue | 15 +-
pages/mall/delivery/test/mock-service.uts | 34 ++-
.../test/platform-tracking-query.uvue | 28 ++-
.../delivery/test/platform-webhook-logs.uvue | 14 +-
12 files changed, 645 insertions(+), 59 deletions(-)
create mode 100644 pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
create mode 100644 pages/mall/delivery/doc/需求文档/simulate_third_party_to_db.sql
diff --git a/pages/mall/delivery/doc/需求文档/README.md b/pages/mall/delivery/doc/需求文档/README.md
index 04242a59..01fb1629 100644
--- a/pages/mall/delivery/doc/需求文档/README.md
+++ b/pages/mall/delivery/doc/需求文档/README.md
@@ -65,4 +65,87 @@
- 联调/设计参考(不要用于生产直接落库):[express_tracking_mock_platform.sql](express_tracking_mock_platform.sql)
- 包含平台侧表 + `mock_*` 测试表(故障注入/回放用),仅用于联调与文档说明。
+测试/预发环境(不使用 mock_* 表也能联调):
+- 仍然执行 [express_tracking_platform_upgrade.sql](express_tracking_platform_upgrade.sql) 创建“生产同款”三表。
+- 再执行 [seed_platform_express_test_data.sql](seed_platform_express_test_data.sql) 向 `platform_express_*` 写入少量 TEST_ 前缀示例数据,用于页面联调与排障演示(可随时清理)。
+
+### Ubuntu 上的 Supabase(测试/预发)怎么执行
+
+目标:在 Supabase 上只使用“生产同款”三表进行联调,不创建/不依赖任何 `mock_*` 表。
+
+执行顺序(必须按顺序):
+1) 执行建表脚本:[express_tracking_platform_upgrade.sql](express_tracking_platform_upgrade.sql)
+2) 执行种子数据脚本:[seed_platform_express_test_data.sql](seed_platform_express_test_data.sql)
+
+方式 A:Supabase Dashboard(推荐)
+- 打开 Supabase 项目 -> SQL Editor
+- 分别粘贴并执行以上两个 SQL 文件内容(先建表、后种子)
+- 建议使用具备 DDL 权限的角色执行(通常 SQL Editor 以高权限执行)
+
+方式 B:Ubuntu 通过 psql 执行(适合自动化/脚本化)
+1) 安装客户端:`sudo apt-get update && sudo apt-get install -y postgresql-client`
+2) 从 Supabase 项目设置里复制连接串(Database -> Connection string),导出环境变量(示例):
+ `export DATABASE_URL='postgresql://USER:PASSWORD@HOST:6543/postgres?sslmode=require'`
+3) 在仓库根目录执行:
+ - `psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql`
+ - `psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql`
+
+清理测试数据:
+- 种子脚本底部自带清理 SQL(按 `tracking_no LIKE 'TEST_%'` 删除),需要时复制执行即可。
+
+### 如何“伪造第三方推送到数据库”(无需后端)
+
+如果你暂时没有 webhook 接收服务,但希望测试环境表现得像“第三方已经推送了轨迹”,可以直接写数据库:
+- 使用脚本 [simulate_third_party_to_db.sql](simulate_third_party_to_db.sql)
+ - 会同时写入:
+ - `platform_express_event_raw`(原始请求留痕/验签/排障)
+ - `platform_express_tracking_events`(时间线事件)
+ - `platform_express_waybills`(运单摘要)
+- 用法:在 Supabase SQL Editor 打开脚本,修改顶部【参数区】后执行整段。
+- 建议:运单号使用 `TEST_` 前缀,便于按脚本底部清理 SQL 一键删除。
+
+### 与数据库其他表是否“相通”(关联与校验)
+
+这套三表与平台业务表的关键“相通点”是:
+- `public.platform_express_waybills.order_id` 外键引用 `public.ml_orders(id)`(订单主表)。
+
+因此:
+- 如果你的 Supabase(测试/预发)数据库里已经部署了完整商城库(包含 `public.ml_orders`),那么这三表可以直接和订单表联查/联动。
+- 如果你的 Supabase 只是一个“独立的轨迹联调库”,没有 `public.ml_orders`,那么执行建表脚本时会因为外键依赖缺失而失败;此时建议先导入/执行商城主库迁移(让 `ml_orders` 存在),或临时改为仅使用 `order_no` 关联(不建外键),待接入完整主库后再补外键。
+
+在 Supabase SQL Editor 里可执行以下校验:
+
+1) 检查订单表是否存在:
+```sql
+select to_regclass('public.ml_orders') as ml_orders;
+```
+
+2) 检查外键是否创建成功(应该能看到 `platform_express_waybills_order_id_fkey` 或类似名称):
+```sql
+select
+ conname as fk_name,
+ pg_get_constraintdef(c.oid) as fk_def
+from pg_constraint c
+join pg_class t on t.oid = c.conrelid
+join pg_namespace n on n.oid = t.relnamespace
+where c.contype = 'f'
+ and n.nspname = 'public'
+ and t.relname = 'platform_express_waybills';
+```
+
+3) 联查示例(有真实订单时):
+```sql
+select
+ o.id as order_id,
+ o.order_no,
+ w.carrier,
+ w.tracking_no,
+ w.current_status_code,
+ w.last_synced_at
+from public.ml_orders o
+left join public.platform_express_waybills w on w.order_id = o.id
+order by o.created_at desc
+limit 20;
+```
+
建议下一步:在平台侧实现一个可切换的“Mock 数据源”开关(仅测试环境),并在 QA 用例中覆盖重复/乱序/验签失败等注入场景。
diff --git a/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql b/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql
index 3d8073a1..03966177 100644
--- a/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql
+++ b/pages/mall/delivery/doc/需求文档/express_tracking_platform_upgrade.sql
@@ -16,20 +16,20 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
-- updated_at 维护函数:若主库已存在则不重复创建
-DO $$
+DO $do$
BEGIN
IF to_regprocedure('public.update_updated_at_column()') IS NULL THEN
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER
LANGUAGE plpgsql
- AS $$
+ AS $func$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
- $$;
+ $func$;
END IF;
-END $$;
+END $do$;
-- =====================================================================
-- A. 平台侧(platform):统一轨迹模型入库与查询
diff --git a/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql b/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
new file mode 100644
index 00000000..3672cebc
--- /dev/null
+++ b/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
@@ -0,0 +1,236 @@
+-- =====================================================================================
+-- 测试环境种子数据(仅写入 platform_express_* 三表)
+--
+-- 目的:在测试/预发环境使用“生产同款表结构”联调页面与接口,避免创建 mock_* 表。
+-- 适用:已执行 express_tracking_platform_upgrade.sql 后。
+--
+-- 注意:
+-- - 本文件会插入 tracking_no 以 TEST_ 前缀开头的示例数据。
+-- - 如需清理,可执行文末的清理 SQL。
+-- =====================================================================================
+
+BEGIN;
+
+-- 1) 创建/更新示例运单(若已存在则跳过)
+INSERT INTO public.platform_express_waybills (
+ order_id,
+ order_no,
+ carrier,
+ tracking_no,
+ source,
+ current_status_code,
+ current_status_text,
+ eta,
+ last_synced_at
+)
+VALUES
+ (NULL, 'ORD_TEST_20260206001', 'YUNDA', 'TEST_YD_20260206_0001', 'mock', 'IN_TRANSIT', '运输中', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206002', 'YTO', 'TEST_YT_20260206_0002', 'mock', 'OUT_FOR_DELIVERY', '派送中', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206003', 'ZTO', 'TEST_ZT_20260206_0003', 'mock', 'DELIVERED', '已签收', NULL, NOW())
+ON CONFLICT (carrier, tracking_no) DO NOTHING;
+
+
+-- 2) 插入轨迹事件(幂等:按 (waybill_id, dedupe_key) 去重)
+WITH w AS (
+ SELECT id, carrier, tracking_no
+ FROM public.platform_express_waybills
+ WHERE tracking_no IN ('TEST_YD_20260206_0001', 'TEST_YT_20260206_0002', 'TEST_ZT_20260206_0003')
+)
+INSERT INTO public.platform_express_tracking_events (
+ waybill_id,
+ carrier,
+ tracking_no,
+ event_id,
+ event_time,
+ event_code,
+ event_text,
+ status_code,
+ node_name,
+ location,
+ description,
+ evidence_urls,
+ raw_payload,
+ received_at,
+ source,
+ dedupe_key
+)
+SELECT * FROM (
+ -- 运单 1:运输中
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0001') AS waybill_id,
+ 'YUNDA' AS carrier,
+ 'TEST_YD_20260206_0001' AS tracking_no,
+ 'test_e_1001' AS event_id,
+ NOW() - INTERVAL '12 hours' AS event_time,
+ 'PICKED' AS event_code,
+ '包裹已揽收' AS event_text,
+ 'ARRIVED_HUB' AS status_code,
+ '上海浦东集散中心' AS node_name,
+ '上海市 浦东新区' AS location,
+ NULL AS description,
+ '[]'::jsonb AS evidence_urls,
+ NULL::jsonb AS raw_payload,
+ NOW() AS received_at,
+ 'poll' AS source,
+ 'test_e_1001' AS dedupe_key
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0001'),
+ 'YUNDA',
+ 'TEST_YD_20260206_0001',
+ 'test_e_1002',
+ NOW() - INTERVAL '6 hours',
+ 'TRANSIT',
+ '快件离开【上海浦东集散中心】,已发往【杭州转运中心】',
+ 'IN_TRANSIT',
+ '上海浦东集散中心',
+ '上海市 浦东新区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_1002'
+
+ -- 运单 2:派送中
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0002'),
+ 'YTO',
+ 'TEST_YT_20260206_0002',
+ 'test_e_2001',
+ NOW() - INTERVAL '8 hours',
+ 'ARRIVAL',
+ '快件已到达【广州天河网点】',
+ 'IN_TRANSIT',
+ '广州天河网点',
+ '广州市 天河区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_2001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0002'),
+ 'YTO',
+ 'TEST_YT_20260206_0002',
+ 'test_e_2002',
+ NOW() - INTERVAL '1 hours',
+ 'OUT_FOR_DELIVERY',
+ '派送员正在派件(测试数据)',
+ 'OUT_FOR_DELIVERY',
+ '广州天河网点',
+ '广州市 天河区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_2002'
+
+ -- 运单 3:已签收
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0003'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0003',
+ 'test_e_3001',
+ NOW() - INTERVAL '2 days',
+ 'PICKED',
+ '包裹已揽收',
+ 'ARRIVED_HUB',
+ '北京朝阳网点',
+ '北京市 朝阳区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_3001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0003'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0003',
+ 'test_e_3002',
+ NOW() - INTERVAL '1 days',
+ 'TRANSIT',
+ '快件运输中',
+ 'IN_TRANSIT',
+ '北京朝阳网点',
+ '北京市 朝阳区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_3002'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0003'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0003',
+ 'test_e_3003',
+ NOW() - INTERVAL '12 hours',
+ 'DELIVERED',
+ '您的快件已签收(测试数据)',
+ 'DELIVERED',
+ '北京朝阳网点',
+ '北京市 朝阳区',
+ NULL,
+ '["https://img-shop.gmugmu.com/mock/pod_sample.png"]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_3003'
+) AS rows_to_insert
+WHERE rows_to_insert.waybill_id IS NOT NULL
+ON CONFLICT (waybill_id, dedupe_key) DO NOTHING;
+
+
+-- 3) 可选:插入原始接收留痕(用于演示验签/排障界面)
+INSERT INTO public.platform_express_event_raw (
+ received_at,
+ source,
+ client_id,
+ carrier,
+ tracking_no,
+ signature_valid,
+ signature,
+ ts_header,
+ request_id,
+ remote_ip,
+ headers,
+ body,
+ parse_error,
+ dedupe_key
+)
+VALUES (
+ NOW(),
+ 'webhook',
+ 'test_client',
+ 'YTO',
+ 'TEST_YT_20260206_0002',
+ TRUE,
+ 'test-signature',
+ EXTRACT(EPOCH FROM NOW())::text,
+ 'req_test_0001',
+ '127.0.0.1',
+ '{"content-type":"application/json"}'::jsonb,
+ '{"tracking_no":"TEST_YT_20260206_0002","status_code":"OUT_FOR_DELIVERY","event_text":"派送员正在派件(测试数据)"}'::jsonb,
+ NULL,
+ 'raw_test_0001'
+);
+
+COMMIT;
+
+-- =====================================================================================
+-- 清理(需要时手工执行)
+-- =====================================================================================
+-- BEGIN;
+-- DELETE FROM public.platform_express_event_raw WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_tracking_events WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_waybills WHERE tracking_no LIKE 'TEST_%';
+-- COMMIT;
diff --git a/pages/mall/delivery/doc/需求文档/simulate_third_party_to_db.sql b/pages/mall/delivery/doc/需求文档/simulate_third_party_to_db.sql
new file mode 100644
index 00000000..7ef01733
--- /dev/null
+++ b/pages/mall/delivery/doc/需求文档/simulate_third_party_to_db.sql
@@ -0,0 +1,191 @@
+-- =====================================================================================
+-- 模拟第三方“推送到平台”的数据(直接写入数据库)
+--
+-- 适用场景:
+-- - 你暂时没有后端 webhook 接收服务,但希望在测试/预发环境快速伪造第三方推送效果。
+-- - 通过写入:
+-- 1) platform_express_event_raw (原始请求留痕/验签结果/排障信息)
+-- 2) platform_express_tracking_events(解析后的轨迹事件,用于时间线展示)
+-- 3) platform_express_waybills(运单摘要)
+--
+-- 注意:
+-- - 这是“绕过后端解析与验签”的直写方案,仅用于测试/演示。
+-- - 生产环境不建议这样做。
+-- - 执行前请确保已跑过 express_tracking_platform_upgrade.sql 创建三表。
+--
+-- 使用方式:
+-- - Supabase SQL Editor:把下面的【参数区】内容改成你要的值,然后整段执行。
+-- =====================================================================================
+
+BEGIN;
+
+-- =========================
+-- 参数区(手工修改这里)
+-- =========================
+-- 承运商编码:建议与平台枚举一致(YTO/YUNDA/ZTO/STO...)
+-- 运单号:建议用 TEST_ 前缀,便于清理
+-- 事件码/状态码:按你的平台约定(IN_TRANSIT/OUT_FOR_DELIVERY/DELIVERED/EXCEPTION...)
+
+DO $$
+DECLARE
+ v_order_no TEXT := 'ORD_TEST_20260206099';
+ v_carrier TEXT := 'YTO';
+ v_tracking_no TEXT := 'TEST_YT_20260206_0099';
+
+ v_event_id TEXT := NULL; -- 可为空
+ v_event_time TIMESTAMPTZ := NOW();
+ v_event_code TEXT := 'OUT_FOR_DELIVERY';
+ v_event_text TEXT := '派送员正在派件(伪造推送,直写数据库)';
+ v_status_code TEXT := 'OUT_FOR_DELIVERY';
+ v_node_name TEXT := '广州天河网点';
+ v_location TEXT := '广州市 天河区';
+
+ v_source TEXT := 'webhook'; -- webhook/poll/manual
+ v_client_id TEXT := 'test_client';
+ v_signature_valid BOOLEAN := TRUE;
+ v_signature TEXT := 'fake-signature';
+ v_remote_ip INET := '127.0.0.1';
+
+ v_waybill_id UUID;
+ v_dedupe_key TEXT;
+BEGIN
+ -- 1) Upsert 运单主表(保证 waybill 存在)
+ INSERT INTO public.platform_express_waybills (
+ order_id,
+ order_no,
+ carrier,
+ tracking_no,
+ source,
+ current_status_code,
+ current_status_text,
+ last_synced_at
+ )
+ VALUES (
+ NULL,
+ v_order_no,
+ v_carrier,
+ v_tracking_no,
+ 'mock',
+ v_status_code,
+ CASE
+ WHEN v_status_code = 'PENDING' THEN '待发货'
+ WHEN v_status_code = 'SHIPPED' THEN '已发货'
+ WHEN v_status_code = 'IN_TRANSIT' THEN '运输中'
+ WHEN v_status_code = 'ARRIVED_HUB' THEN '到达网点'
+ WHEN v_status_code = 'OUT_FOR_DELIVERY' THEN '派送中'
+ WHEN v_status_code = 'DELIVERED' THEN '已签收'
+ WHEN v_status_code = 'EXCEPTION' THEN '异常'
+ ELSE v_status_code
+ END,
+ NOW()
+ )
+ ON CONFLICT (carrier, tracking_no)
+ DO UPDATE SET
+ order_no = COALESCE(EXCLUDED.order_no, public.platform_express_waybills.order_no),
+ current_status_code = EXCLUDED.current_status_code,
+ current_status_text = EXCLUDED.current_status_text,
+ last_synced_at = EXCLUDED.last_synced_at
+ RETURNING id INTO v_waybill_id;
+
+ -- 2) 计算幂等键(优先 event_id;否则用运单+事件码+事件时间)
+ v_dedupe_key := COALESCE(
+ v_event_id,
+ v_tracking_no || '|' || v_event_code || '|' || to_char(v_event_time, 'YYYY-MM-DD"T"HH24:MI:SSOF')
+ );
+
+ -- 3) 写入原始接收留痕(模拟第三方请求)
+ INSERT INTO public.platform_express_event_raw (
+ received_at,
+ source,
+ client_id,
+ carrier,
+ tracking_no,
+ signature_valid,
+ signature,
+ ts_header,
+ request_id,
+ remote_ip,
+ headers,
+ body,
+ parse_error,
+ dedupe_key
+ )
+ VALUES (
+ NOW(),
+ v_source,
+ v_client_id,
+ v_carrier,
+ v_tracking_no,
+ v_signature_valid,
+ v_signature,
+ EXTRACT(EPOCH FROM NOW())::text,
+ 'req_fake_' || replace(v_dedupe_key, '|', '_'),
+ v_remote_ip,
+ '{"content-type":"application/json"}'::jsonb,
+ jsonb_build_object(
+ 'order_no', v_order_no,
+ 'carrier', v_carrier,
+ 'tracking_no', v_tracking_no,
+ 'event_id', v_event_id,
+ 'event_time', v_event_time,
+ 'event_code', v_event_code,
+ 'event_text', v_event_text,
+ 'status_code', v_status_code,
+ 'node_name', v_node_name,
+ 'location', v_location
+ ),
+ NULL,
+ 'raw|' || v_dedupe_key
+ );
+
+ -- 4) 写入解析后的轨迹事件(用于时间线展示)
+ INSERT INTO public.platform_express_tracking_events (
+ waybill_id,
+ carrier,
+ tracking_no,
+ event_id,
+ event_time,
+ event_code,
+ event_text,
+ status_code,
+ node_name,
+ location,
+ description,
+ evidence_urls,
+ raw_payload,
+ received_at,
+ source,
+ dedupe_key
+ )
+ VALUES (
+ v_waybill_id,
+ v_carrier,
+ v_tracking_no,
+ v_event_id,
+ v_event_time,
+ v_event_code,
+ v_event_text,
+ v_status_code,
+ v_node_name,
+ v_location,
+ NULL,
+ '[]'::jsonb,
+ NULL,
+ NOW(),
+ v_source,
+ v_dedupe_key
+ )
+ ON CONFLICT (waybill_id, dedupe_key) DO NOTHING;
+
+END $$;
+
+COMMIT;
+
+-- =====================================================================================
+-- 清理示例(需要时手工执行)
+-- =====================================================================================
+-- BEGIN;
+-- DELETE FROM public.platform_express_event_raw WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_tracking_events WHERE tracking_no LIKE 'TEST_%';
+-- DELETE FROM public.platform_express_waybills WHERE tracking_no LIKE 'TEST_%';
+-- COMMIT;
diff --git a/pages/mall/delivery/test/api-simulator.uvue b/pages/mall/delivery/test/api-simulator.uvue
index 4a18616f..cf3ef8fe 100644
--- a/pages/mall/delivery/test/api-simulator.uvue
+++ b/pages/mall/delivery/test/api-simulator.uvue
@@ -18,24 +18,24 @@
- 2. 构造回调数据 (JSON Payload)
+ 2. 构造圆通协议数据 (YTO Protocol)
- 运单号:
-
+ 物流单号 (mailNo):
+
- 快递公司:
-
+ 订单号 (txLogisticId):
+
- 物流状态:
+ 事件状态 (infoContent):
{{ currentStatusLabel }}
- 轨迹描述文字:
-
+ 轨迹描述 (remark):
+
@@ -64,61 +64,77 @@
const selectedOrderIndex = ref(-1)
const form = reactive({
- tracking_no: '',
- carrier: '',
- status_code: 'IN_TRANSIT',
- event_text: '快件已到达【XX转运中心】,准备发往下一站'
+ mailNo: '',
+ txLogisticId: '',
+ infoContent: 'SEND',
+ remark: '快件已到达【XX分拨中心】,准备发往下一站',
+ acceptTime: '',
+ carrier: '圆通速递'
})
const statusOptions = [
- { label: '在途中 (IN_TRANSIT)', value: 'IN_TRANSIT' },
- { label: '派送中 (OUT_FOR_DELIVERY)', value: 'OUT_FOR_DELIVERY' },
- { label: '已签收 (DELIVERED)', value: 'DELIVERED' },
- { label: '异常 (EXCEPTION)', value: 'EXCEPTION' }
+ { label: '揽收 (GOT)', value: 'GOT' },
+ { label: '运输中 (SEND)', value: 'SEND' },
+ { label: '派送中 (SENT)', value: 'SENT' },
+ { label: '已签收 (SIGNED)', value: 'SIGNED' },
+ { label: '异常 (FAILED)', value: 'FAILED' }
]
- const currentStatusLabel = computed(() => {
- const opt = statusOptions.find(o => o.value === form.status_code)
- return opt ? opt.label : '请选择'
+ const currentStatusLabel = computed((): string => {
+ const opt = statusOptions.find((o: UTSJSONObject): boolean => o['value'] === form.infoContent)
+ return (opt != null) ? opt['label'] as string : '请选择'
})
- const jsonString = computed(() => {
+ const jsonString = computed((): string => {
return JSON.stringify(form, null, 2)
})
function selectOrder(index: number) {
selectedOrderIndex.value = index
const order = shippedOrders.value[index]
- form.tracking_no = order.tracking_no
- form.carrier = order.carrier
+ form.mailNo = order.tracking_no
+ form.txLogisticId = order.order_no
+ form.carrier = order.carrier + '速递'
// 根据订单当前状态智能预设
if (order.status === 'SHIPPED') {
- form.status_code = 'IN_TRANSIT'
- form.event_text = '快件已揽收,正发往城市中心'
+ form.infoContent = 'SEND'
+ form.remark = '快件已到达北京分拨中心'
} else if (order.status === 'IN_TRANSIT') {
- form.status_code = 'OUT_FOR_DELIVERY'
- form.event_text = '派送员王师傅(13700008888)正在派件'
+ form.infoContent = 'SENT'
+ form.remark = '派送员王师傅(13700008888)正在派件'
}
}
function onStatusChange(e: UniPickerChangeEvent) {
const idx = e.detail.value as number
- form.status_code = statusOptions[idx].value as string
+ form.infoContent = statusOptions[idx].value
}
function sendWebhook() {
- if (!form.tracking_no) {
+ if (!form.mailNo) {
uni.showToast({ title: '请先填写运单号', icon: 'none' })
return
}
+ // 获取当前时间戳作为圆通要求的 acceptTime
+ const now = new Date()
+ const Y = now.getFullYear()
+ const M = (now.getMonth() + 1).toString().padStart(2, '0')
+ const D = now.getDate().toString().padStart(2, '0')
+ const h = now.getHours().toString().padStart(2, '0')
+ const m = now.getMinutes().toString().padStart(2, '0')
+ const s = now.getSeconds().toString().padStart(2, '0')
+ form.acceptTime = `${Y}-${M}-${D} ${h}:${m}:${s}`
+
// 执行模拟推送 (转换为普通对象以兼容 UTS)
const payload = {
- tracking_no: form.tracking_no,
- carrier: form.carrier,
- status_code: form.status_code,
- event_text: form.event_text
+ mailNo: form.mailNo,
+ txLogisticId: form.txLogisticId,
+ infoContent: form.infoContent,
+ remark: form.remark,
+ acceptTime: form.acceptTime,
+ carrier: form.carrier
} as UTSJSONObject
const success = mockService.pushWebhookData(payload)
diff --git a/pages/mall/delivery/test/consumer-logistics-detail.uvue b/pages/mall/delivery/test/consumer-logistics-detail.uvue
index f528ec3a..75181d33 100644
--- a/pages/mall/delivery/test/consumer-logistics-detail.uvue
+++ b/pages/mall/delivery/test/consumer-logistics-detail.uvue
@@ -135,7 +135,8 @@
},
getStatusText(status: string): string {
const maps = {
- 'SHIPPED': '运输中',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中',
'PENDING': '待揽收',
diff --git a/pages/mall/delivery/test/consumer-order-list.uvue b/pages/mall/delivery/test/consumer-order-list.uvue
index 5f81d2eb..d8117b92 100644
--- a/pages/mall/delivery/test/consumer-order-list.uvue
+++ b/pages/mall/delivery/test/consumer-order-list.uvue
@@ -71,7 +71,8 @@
},
getStatusText(status: string): string {
const maps = {
- 'SHIPPED': '运输中',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中',
'PENDING': '待揽收',
diff --git a/pages/mall/delivery/test/merchant-order-detail.uvue b/pages/mall/delivery/test/merchant-order-detail.uvue
index 18b1a548..0ad839f8 100644
--- a/pages/mall/delivery/test/merchant-order-detail.uvue
+++ b/pages/mall/delivery/test/merchant-order-detail.uvue
@@ -113,7 +113,14 @@
this.statusHistory = mockService.getMockTracking(this.orderNo)
},
getStatusText(status: string) : string {
- const maps = { 'PENDING': '待发货', 'SHIPPED': '待签收', 'DELIVERED': '已签收', 'EXCEPTION': '异常' }
+ const maps = {
+ 'PENDING': '待发货',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
+ 'DELIVERED': '已签收',
+ 'OUT_FOR_DELIVERY': '派送中',
+ 'EXCEPTION': '异常'
+ }
return (maps[status] != null) ? maps[status] : status
},
async refreshLogistics() {
diff --git a/pages/mall/delivery/test/merchant-order-list.uvue b/pages/mall/delivery/test/merchant-order-list.uvue
index 932fe6f5..4f5eebe9 100644
--- a/pages/mall/delivery/test/merchant-order-list.uvue
+++ b/pages/mall/delivery/test/merchant-order-list.uvue
@@ -54,7 +54,7 @@
选择承运商
-
+
{{ currentCarrier || '请选择' }}
@@ -83,8 +83,14 @@
orders: [] as MockOrder[],
showShipModal: false,
selectedOrder: null as MockOrder | null,
- carriers: ['YUNDA', 'YTO', 'ZTO', 'STO', 'SF'],
- currentCarrier: '',
+ carriers: [
+ { label: '韵达快递', value: '韵达' },
+ { label: '圆通速递', value: '圆通' },
+ { label: '中通快递', value: '中通' },
+ { label: '申通快递', value: '申通' },
+ { label: '顺丰速运', value: '顺丰' }
+ ],
+ currentCarrier: '韵达',
trackingNo: ''
}
},
@@ -117,6 +123,7 @@
const maps = {
'PENDING': '待发货',
'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
'DELIVERED': '已签收',
'OUT_FOR_DELIVERY': '派送中',
'EXCEPTION': '包裹异常'
@@ -131,7 +138,7 @@
},
onCarrierChange(e: any) {
const index = e.detail.value as number
- this.currentCarrier = this.carriers[index]
+ this.currentCarrier = this.carriers[index].value
},
async confirmShip() {
if (!this.trackingNo) {
diff --git a/pages/mall/delivery/test/mock-service.uts b/pages/mall/delivery/test/mock-service.uts
index 63dfd37d..36e8f04e 100644
--- a/pages/mall/delivery/test/mock-service.uts
+++ b/pages/mall/delivery/test/mock-service.uts
@@ -436,14 +436,24 @@ class MockService {
}
/**
- * 核心功能:模拟第三方回调接口
- * 模拟外部物流平台向本系统发送轨迹更新 API
+ * 核心功能:模拟第三方回调接口 (适配圆通推送协议)
+ * 对应字段: txLogisticId(订单号), mailNo(票号), infoContent(状态), remark(描述)
*/
pushWebhookData(payload: UTSJSONObject): boolean {
- const tracking_no = payload['tracking_no'] as string
- const status_code = payload['status_code'] as string
- const event_text = payload['event_text'] as string
- const carrier = payload['carrier'] as string || '顺丰'
+ // 兼容圆通协议字段
+ const tracking_no = (payload['mailNo'] != null) ? payload['mailNo'] as string : (payload['tracking_no'] as string)
+ const yto_status = (payload['infoContent'] != null) ? payload['infoContent'] as string : (payload['status_code'] as string)
+ const event_text = (payload['remark'] != null) ? payload['remark'] as string : (payload['event_text'] as string)
+ const order_id = payload['txLogisticId'] as string || ''
+ const carrier = payload['carrier'] as string || '圆通速递'
+
+ // 状态映射:圆通状态 -> 本系统状态
+ let status_code = 'IN_TRANSIT'
+ if (yto_status === 'GOT' || yto_status === 'SEND') status_code = 'IN_TRANSIT'
+ else if (yto_status === 'SENT') status_code = 'OUT_FOR_DELIVERY'
+ else if (yto_status === 'SIGNED') status_code = 'DELIVERED'
+ else if (yto_status === 'FAILED') status_code = 'EXCEPTION'
+ else status_code = yto_status
// 1. 记录原始日志
const now = new Date()
@@ -451,7 +461,7 @@ class MockService {
time: this.formatDate(now).split(' ')[1],
carrier: carrier,
tracking_no: tracking_no,
- event_code: status_code,
+ event_code: yto_status, // 保留圆通原始代码
success: true,
result_text: '接收成功',
payload: payload
@@ -459,16 +469,16 @@ class MockService {
this.webhookLogs.unshift(log)
// 2. 更新系统内部轨迹
- const order = this.orders.find(o => o.tracking_no === tracking_no)
+ const order = this.orders.find(o => o.tracking_no === tracking_no || o.order_no === order_id)
if (order != null) {
if (!this.trackingHistory.has(order.order_no)) {
this.trackingHistory.set(order.order_no, [])
}
const history = this.trackingHistory.get(order.order_no)!
history.unshift({
- event_id: 'wb_' + Date.now(),
- event_time: this.formatDate(now),
- event_code: status_code,
+ event_id: 'yto_' + Date.now(),
+ event_time: payload['acceptTime'] as string || this.formatDate(now),
+ event_code: yto_status,
event_text: event_text,
status_code: status_code,
evidence_urls: []
@@ -479,7 +489,7 @@ class MockService {
}
log.success = false
- log.result_text = '未找到对应的运单号'
+ log.result_text = '未找到对应的运单或订单号'
return false
}
diff --git a/pages/mall/delivery/test/platform-tracking-query.uvue b/pages/mall/delivery/test/platform-tracking-query.uvue
index a2d32949..b09928bf 100644
--- a/pages/mall/delivery/test/platform-tracking-query.uvue
+++ b/pages/mall/delivery/test/platform-tracking-query.uvue
@@ -29,7 +29,7 @@
当前状态:
- {{ waybillInfo.status }}
+ {{ getStatusText(waybillInfo.status) }}
@@ -44,8 +44,8 @@
{{ event.event_text }}
- CODE: {{ event.event_code }}
- MAP: {{ event.status_code }}
+ 代码: {{ event.event_code }}
+ 映射状态: {{ getStatusMapping(event.status_code) }}
{{ JSON.stringify(event.raw_payload, null, 2) }}
@@ -116,6 +116,28 @@
const current = this.showRaw[index]
this.showRaw[index] = !current
},
+ getStatusText(status: string) : string {
+ const maps = {
+ 'PENDING': '待发货',
+ 'SHIPPED': '已发货',
+ 'IN_TRANSIT': '运输中',
+ 'DELIVERED': '已签收',
+ 'OUT_FOR_DELIVERY': '派送中',
+ 'EXCEPTION': '异常'
+ }
+ return (maps[status] != null) ? maps[status] : status
+ },
+ getStatusMapping(code: string) : string {
+ const maps = {
+ 'SHIPPED': '已揽收',
+ 'IN_TRANSIT': '运输中',
+ 'ARRIVED_HUB': '到达分拨中心',
+ 'OUT_FOR_DELIVERY': '正在派送',
+ 'DELIVERED': '已签收',
+ 'EXCEPTION': '包裹异常'
+ }
+ return (maps[code] != null) ? maps[code] : code
+ },
runScenario(type: string) {
if (this.waybillInfo == null) return
uni.showModal({
diff --git a/pages/mall/delivery/test/platform-webhook-logs.uvue b/pages/mall/delivery/test/platform-webhook-logs.uvue
index f20e3f4b..77db79ce 100644
--- a/pages/mall/delivery/test/platform-webhook-logs.uvue
+++ b/pages/mall/delivery/test/platform-webhook-logs.uvue
@@ -35,7 +35,7 @@
事件:
- {{ log.event_code }}
+ {{ getEventText(log.event_code) }}
状态:
@@ -84,6 +84,18 @@
showPayload.value = true
}
+ function getEventText(code: string) : string {
+ const maps = {
+ 'SHIPPED': '发货揽收',
+ 'IN_TRANSIT': '运输中',
+ 'ARRIVED_HUB': '中转入库',
+ 'OUT_FOR_DELIVERY': '派件中',
+ 'DELIVERED': '确认签收',
+ 'EXCEPTION': '异常上报'
+ }
+ return (maps[code] != null) ? maps[code] : code
+ }
+
function reProcess(log: MockWebhookLog) {
uni.showLoading({ title: '重送中...' })
setTimeout(() => {
From 3ea0f008b578cbd0df3a1e5a8ce0261c429a0168 Mon Sep 17 00:00:00 2001
From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com>
Date: Mon, 9 Feb 2026 08:54:26 +0800
Subject: [PATCH 09/26] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=BA=93=EF=BC=8C=E6=A8=A1=E6=8B=9F=E7=AC=AC=E4=B8=89=E6=96=B9?=
=?UTF-8?q?=E6=8E=A5=E5=85=A5=E4=BF=A1=E6=81=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages/mall/delivery/doc/需求文档/README.md | 68 ++
.../seed_platform_express_test_data.sql | 245 +++++-
pages/mall/delivery/doc/需求文档/接口规范.md | 4 +
.../delivery/doc/需求文档/配送模块需求文档.md | 40 +-
pages/mall/delivery/test/api-simulator.uvue | 51 +-
.../test/consumer-logistics-detail.uvue | 38 +-
.../delivery/test/consumer-order-list.uvue | 42 +-
pages/mall/delivery/test/index.uvue | 15 +-
.../delivery/test/merchant-order-detail.uvue | 27 +-
.../delivery/test/merchant-order-list.uvue | 35 +-
pages/mall/delivery/test/mock-service.uts | 729 ++++++++----------
.../delivery/test/platform-config-center.uvue | 13 +
.../test/platform-tracking-query.uvue | 54 +-
.../delivery/test/platform-webhook-logs.uvue | 23 +-
14 files changed, 882 insertions(+), 502 deletions(-)
diff --git a/pages/mall/delivery/doc/需求文档/README.md b/pages/mall/delivery/doc/需求文档/README.md
index 01fb1629..4bcbba03 100644
--- a/pages/mall/delivery/doc/需求文档/README.md
+++ b/pages/mall/delivery/doc/需求文档/README.md
@@ -68,6 +68,7 @@
测试/预发环境(不使用 mock_* 表也能联调):
- 仍然执行 [express_tracking_platform_upgrade.sql](express_tracking_platform_upgrade.sql) 创建“生产同款”三表。
- 再执行 [seed_platform_express_test_data.sql](seed_platform_express_test_data.sql) 向 `platform_express_*` 写入少量 TEST_ 前缀示例数据,用于页面联调与排障演示(可随时清理)。
+ - 说明:当前脚本会插入 9 条 `TEST_` 运单(含 1 条 `PENDING/未发货`),并为每条运单插入 1~3 条不等的轨迹事件(幂等,可重复执行)。
### Ubuntu 上的 Supabase(测试/预发)怎么执行
@@ -93,6 +94,73 @@
清理测试数据:
- 种子脚本底部自带清理 SQL(按 `tracking_no LIKE 'TEST_%'` 删除),需要时复制执行即可。
+### 执行成功后的校验 SQL(建议)
+
+在 Supabase SQL Editor 执行以下查询,确认数据是否写入成功、事件是否按时间线可查:
+
+1) 查看全部 TEST_ 运单(应看到 9 条):
+```sql
+select
+ carrier,
+ tracking_no,
+ current_status_code,
+ current_status_text,
+ last_event_time,
+ last_synced_at,
+ created_at
+from public.platform_express_waybills
+where tracking_no like 'TEST_%'
+order by created_at desc;
+```
+
+2) 按运单统计事件数量(确认每单都有事件):
+```sql
+select
+ w.carrier,
+ w.tracking_no,
+ count(e.id) as event_count,
+ min(e.event_time) as first_event_time,
+ max(e.event_time) as last_event_time
+from public.platform_express_waybills w
+left join public.platform_express_tracking_events e
+ on e.waybill_id = w.id
+where w.tracking_no like 'TEST_%'
+group by w.carrier, w.tracking_no
+order by last_event_time desc nulls last;
+```
+
+3) 查看单个运单时间线(按 event_time 升序):
+```sql
+select
+ event_time,
+ event_code,
+ event_text,
+ status_code,
+ node_name,
+ node_location,
+ source,
+ dedupe_key
+from public.platform_express_tracking_events
+where tracking_no = 'TEST_YT_20260206_0002'
+order by event_time asc;
+```
+
+4) 查看原始接收留痕(可选,用于排障/审计):
+```sql
+select
+ received_at,
+ source,
+ carrier,
+ tracking_no,
+ signature_valid,
+ request_id,
+ dedupe_key
+from public.platform_express_event_raw
+where tracking_no like 'TEST_%'
+order by received_at desc
+limit 50;
+```
+
### 如何“伪造第三方推送到数据库”(无需后端)
如果你暂时没有 webhook 接收服务,但希望测试环境表现得像“第三方已经推送了轨迹”,可以直接写数据库:
diff --git a/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql b/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
index 3672cebc..8f064d5b 100644
--- a/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
+++ b/pages/mall/delivery/doc/需求文档/seed_platform_express_test_data.sql
@@ -26,7 +26,13 @@ INSERT INTO public.platform_express_waybills (
VALUES
(NULL, 'ORD_TEST_20260206001', 'YUNDA', 'TEST_YD_20260206_0001', 'mock', 'IN_TRANSIT', '运输中', NULL, NOW()),
(NULL, 'ORD_TEST_20260206002', 'YTO', 'TEST_YT_20260206_0002', 'mock', 'OUT_FOR_DELIVERY', '派送中', NULL, NOW()),
- (NULL, 'ORD_TEST_20260206003', 'ZTO', 'TEST_ZT_20260206_0003', 'mock', 'DELIVERED', '已签收', NULL, NOW())
+ (NULL, 'ORD_TEST_20260206003', 'ZTO', 'TEST_ZT_20260206_0003', 'mock', 'DELIVERED', '已签收', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206004', 'STO', 'TEST_STO_20260206_0004', 'mock', 'EXCEPTION', '异常', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206005', 'SF', 'TEST_SF_20260206_0005', 'mock', 'ARRIVED_HUB', '到达网点', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206006', 'YUNDA', 'TEST_YD_20260206_0006', 'mock', 'SHIPPED', '已发货', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206007', 'YTO', 'TEST_YT_20260206_0007', 'mock', 'IN_TRANSIT', '运输中', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206008', 'ZTO', 'TEST_ZT_20260206_0008', 'mock', 'OUT_FOR_DELIVERY', '派送中', NULL, NOW()),
+ (NULL, 'ORD_TEST_20260206009', 'YUNDA', 'TEST_YD_20260206_0009', 'mock', 'PENDING', '未发货', NULL, NOW())
ON CONFLICT (carrier, tracking_no) DO NOTHING;
@@ -34,7 +40,17 @@ ON CONFLICT (carrier, tracking_no) DO NOTHING;
WITH w AS (
SELECT id, carrier, tracking_no
FROM public.platform_express_waybills
- WHERE tracking_no IN ('TEST_YD_20260206_0001', 'TEST_YT_20260206_0002', 'TEST_ZT_20260206_0003')
+ WHERE tracking_no IN (
+ 'TEST_YD_20260206_0001',
+ 'TEST_YT_20260206_0002',
+ 'TEST_ZT_20260206_0003',
+ 'TEST_STO_20260206_0004',
+ 'TEST_SF_20260206_0005',
+ 'TEST_YD_20260206_0006',
+ 'TEST_YT_20260206_0007',
+ 'TEST_ZT_20260206_0008',
+ 'TEST_YD_20260206_0009'
+ )
)
INSERT INTO public.platform_express_tracking_events (
waybill_id,
@@ -185,6 +201,198 @@ SELECT * FROM (
NOW(),
'webhook',
'test_e_3003'
+
+ -- 运单 4:异常
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_STO_20260206_0004'),
+ 'STO',
+ 'TEST_STO_20260206_0004',
+ 'test_e_4001',
+ NOW() - INTERVAL '20 hours',
+ 'PICKED',
+ '包裹已揽收',
+ 'ARRIVED_HUB',
+ '深圳南山网点',
+ '深圳市 南山区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_4001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_STO_20260206_0004'),
+ 'STO',
+ 'TEST_STO_20260206_0004',
+ 'test_e_4002',
+ NOW() - INTERVAL '2 hours',
+ 'EXCEPTION',
+ '【包裹异常】收件地址不详,等待处理(测试数据)',
+ 'EXCEPTION',
+ '深圳南山网点',
+ '深圳市 南山区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_4002'
+
+ -- 运单 5:到达网点
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_SF_20260206_0005'),
+ 'SF',
+ 'TEST_SF_20260206_0005',
+ 'test_e_5001',
+ NOW() - INTERVAL '10 hours',
+ 'PICKED',
+ '包裹已揽收',
+ 'ARRIVED_HUB',
+ '南京江宁集散中心',
+ '南京市 江宁区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_5001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_SF_20260206_0005'),
+ 'SF',
+ 'TEST_SF_20260206_0005',
+ 'test_e_5002',
+ NOW() - INTERVAL '30 minutes',
+ 'ARRIVAL',
+ '快件已到达【南京江宁网点】,等待派送(测试数据)',
+ 'ARRIVED_HUB',
+ '南京江宁网点',
+ '南京市 江宁区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_5002'
+
+ -- 运单 6:已发货(待揽收)
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0006'),
+ 'YUNDA',
+ 'TEST_YD_20260206_0006',
+ 'test_e_6001',
+ NOW() - INTERVAL '5 hours',
+ 'SHIPPED',
+ '商家已发货,等待快递公司揽收(测试数据)',
+ 'SHIPPED',
+ NULL,
+ '杭州市 余杭区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'manual',
+ 'test_e_6001'
+
+ -- 运单 7:运输中
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0007'),
+ 'YTO',
+ 'TEST_YT_20260206_0007',
+ 'test_e_7001',
+ NOW() - INTERVAL '18 hours',
+ 'PICKED',
+ '包裹已揽收',
+ 'ARRIVED_HUB',
+ '武汉江夏网点',
+ '武汉市 江夏区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_7001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YT_20260206_0007'),
+ 'YTO',
+ 'TEST_YT_20260206_0007',
+ 'test_e_7002',
+ NOW() - INTERVAL '7 hours',
+ 'TRANSIT',
+ '快件离开【武汉江夏网点】,已发往【长沙转运中心】(测试数据)',
+ 'IN_TRANSIT',
+ '武汉江夏网点',
+ '武汉市 江夏区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_7002'
+
+ -- 运单 8:派送中
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0008'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0008',
+ 'test_e_8001',
+ NOW() - INTERVAL '9 hours',
+ 'ARRIVAL',
+ '快件已到达【西安高新网点】(测试数据)',
+ 'IN_TRANSIT',
+ '西安高新网点',
+ '西安市 雁塔区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'poll',
+ 'test_e_8001'
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_ZT_20260206_0008'),
+ 'ZTO',
+ 'TEST_ZT_20260206_0008',
+ 'test_e_8002',
+ NOW() - INTERVAL '20 minutes',
+ 'OUT_FOR_DELIVERY',
+ '派送员正在派件(测试数据)',
+ 'OUT_FOR_DELIVERY',
+ '西安高新网点',
+ '西安市 雁塔区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'webhook',
+ 'test_e_8002'
+
+ -- 运单 9:未发货(占位事件,便于前端展示)
+ UNION ALL
+ SELECT
+ (SELECT id FROM w WHERE tracking_no = 'TEST_YD_20260206_0009'),
+ 'YUNDA',
+ 'TEST_YD_20260206_0009',
+ 'test_e_9001',
+ NOW() - INTERVAL '3 hours',
+ 'PENDING',
+ '商家待发货(测试数据)',
+ 'PENDING',
+ NULL,
+ '杭州市 余杭区',
+ NULL,
+ '[]'::jsonb,
+ NULL::jsonb,
+ NOW(),
+ 'manual',
+ 'test_e_9001'
) AS rows_to_insert
WHERE rows_to_insert.waybill_id IS NOT NULL
ON CONFLICT (waybill_id, dedupe_key) DO NOTHING;
@@ -224,6 +432,39 @@ VALUES (
'raw_test_0001'
);
+INSERT INTO public.platform_express_event_raw (
+ received_at,
+ source,
+ client_id,
+ carrier,
+ tracking_no,
+ signature_valid,
+ signature,
+ ts_header,
+ request_id,
+ remote_ip,
+ headers,
+ body,
+ parse_error,
+ dedupe_key
+)
+VALUES (
+ NOW(),
+ 'poll',
+ 'test_client',
+ 'STO',
+ 'TEST_STO_20260206_0004',
+ NULL,
+ NULL,
+ EXTRACT(EPOCH FROM NOW())::text,
+ 'req_test_0002',
+ '127.0.0.1',
+ '{"content-type":"application/json"}'::jsonb,
+ '{"tracking_no":"TEST_STO_20260206_0004","status_code":"EXCEPTION","event_text":"【包裹异常】收件地址不详,等待处理(测试数据)"}'::jsonb,
+ NULL,
+ 'raw_test_0002'
+);
+
COMMIT;
-- =====================================================================================
diff --git a/pages/mall/delivery/doc/需求文档/接口规范.md b/pages/mall/delivery/doc/需求文档/接口规范.md
index 773fe154..562df9e7 100644
--- a/pages/mall/delivery/doc/需求文档/接口规范.md
+++ b/pages/mall/delivery/doc/需求文档/接口规范.md
@@ -49,6 +49,10 @@
- 平台接收 Mock Server 的 Webhook 推送(推荐)。
- 平台亦可主动调用 Mock Server 查询轨迹(模拟轮询补偿)。
+范围说明(避免误解):
+- 若要实现“电子面单/在线下单自动获取运单号”,需要对接第三方(快递公司直连或聚合平台)提供的下单/面单接口。
+- 该类“下单/面单”接口不属于本 Mock Server 接口规范范围;本文只覆盖轨迹事件的推送/查询与相关验签、幂等与字段口径。
+
## 四、接口定义(示例)
1) Mock Server -> 平台:事件推送(Webhook)
diff --git a/pages/mall/delivery/doc/需求文档/配送模块需求文档.md b/pages/mall/delivery/doc/需求文档/配送模块需求文档.md
index c3a7fe59..a2dace9a 100644
--- a/pages/mall/delivery/doc/需求文档/配送模块需求文档.md
+++ b/pages/mall/delivery/doc/需求文档/配送模块需求文档.md
@@ -59,8 +59,8 @@
与 Mock 的关系:
- Mock 用于第三方未联通阶段的替代数据源与故障注入;生产环境默认关闭。
-### 方案 B:商家自选配送(商家自找承运方 / 平台只做订单)
-适用:平台招商、品类/区域差异大、平台希望给商家“选择承运方”的灵活性。
+### 方案 B:商家自选配送(商家自选承运商并发货 / 平台负责运单绑定与轨迹展示)
+适用:平台招商、品类/区域差异大、平台希望给商家“选择承运方”的灵活性;平台不承担“实际配送执行”,但需要提供统一的运单绑定入口与轨迹展示底座。
重要说明(商家只是平台商户的常见情况):
- 默认不要求商家自建系统或自行对接第三方 API。
@@ -75,6 +75,30 @@
- 在平台选择承运方并完成发货动作:提供/回填 `tracking_no` 与必要的订单关联信息(如 `order_no`)。
- 若商家坚持使用“商家与第三方的独立合同/账号”,可向平台提交第三方对接所需材料,由平台统一配置与代接入(不要求商家自建回调服务)。
+可选增强:商家在平台内“自助对接第三方”(无需商家开发)
+- 定位:可选增强能力,非一期必做。
+- 目标:让商家在平台后台完成第三方账号授权/配置,从而在平台内完成下单打单、获取运单号、查询/订阅轨迹;平台仍统一入库与展示。
+- 常见两种形态(可二选一,也可并存):
+ 1) 聚合平台授权开通(推荐优先):平台统一对接一家聚合服务商;商家在平台内完成开通/授权后即可使用多家快递能力。
+ 2) 商家自带第三方账号(BYO Account):商家已与某快递/聚合平台签约;在平台后台录入密钥或走 OAuth 授权,平台代为调用第三方 API。
+- 商家后台最小页面/流程建议:
+ - 【物流渠道管理】:选择渠道类型(聚合/直连)、开启/停用
+ - 【授权与密钥】:录入/更新 `appKey/appSecret/token` 或 OAuth 绑定;展示“最后一次连通性检测”结果
+ - 【回调配置提示】:展示平台 Webhook 地址与白名单要求(若第三方需要配置回调)
+ - 【测试与排障】:一键“连通性测试/拉取一条轨迹/模拟下单”,失败给出可读错误
+- 安全要求(必须):
+ - 第三方密钥加密存储、最小权限、变更审计;仅商家管理员可配置。
+ - 不在前端暴露密钥;平台服务端代调用第三方接口。
+ - 轨迹/运单数据仍写入平台统一表结构与事件模型,避免不同渠道把差异带到前端。
+
+是否需要做(决策建议):
+- 暂不需要(建议先不做)的场景:一期目标仅是“商家回填运单号 + 平台展示第三方轨迹关键节点”,且商家规模不大/对接资源有限。
+- 建议需要(做了收益明显)的场景:商家量大且运单号回填错误率高、客服投诉“查不到物流/不更新”多;或明确要上“电子面单/平台一键下单”。
+- 推荐分期:
+ - Phase 1:回填运单号 + 轨迹接入与统一展示底座(本项目当前优先级)。
+ - Phase 2:先接入 1 家聚合平台做统一下单/面单/轨迹(降低多家直连成本)。
+ - Phase 3:再开放 BYO Account(商家自带账号)自助配置(安全与运维成本最高)。
+
优点:平台轻、商家灵活。
风险/成本:体验碎片化风险大;如果不强制回传规范,平台客服/前端会被迫处理多样差异,长期维护成本更高。
@@ -85,6 +109,18 @@
- `tracking_no`:运单号生成与回传方式(商家生成/第三方返回/平台生成)。
- 订单关联信息:`order_no`(或平台侧可解析的业务单号),用于把轨迹绑定到订单详情页。
+运单号(`tracking_no`)获取方式(两种常见落地,二选一或并存):
+1) 回填运单号(最小模式,推荐先上线):
+ - 商家在线下/快递官方系统/聚合平台完成下单与交接,获得运单号。
+ - 商家在平台后台“发货”时选择承运商并回填 `tracking_no`。
+ - 平台不需要调用第三方“下单/面单”接口,只需后续接入轨迹(Webhook/轮询)用于展示。
+2) 电子面单 / 在线下单(增强模式,体验更好):
+ - 平台(或平台集成的服务商)需要对接第三方提供的“下单/面单”接口,向第三方提交发货所需信息(收件人/地址/重量/件数等),并获得:运单号 `tracking_no` + 面单文件/面单号等。
+ - 第三方可以是:
+ - 直连快递公司接口(每家快递一套协议);或
+ - 快递聚合平台接口(一套协议覆盖多家快递)。
+ - 兜底策略建议:若第三方下单失败/超时,允许商家改为“手工回填运单号”完成发货闭环。
+
轨迹数据接入方式(平台统一接入,推荐第一种):
1) 第三方 -> 平台 Webhook 推送(推荐):
- 平台与第三方完成订阅/回调配置;第三方直接回调平台 Webhook。
diff --git a/pages/mall/delivery/test/api-simulator.uvue b/pages/mall/delivery/test/api-simulator.uvue
index cf3ef8fe..bd392901 100644
--- a/pages/mall/delivery/test/api-simulator.uvue
+++ b/pages/mall/delivery/test/api-simulator.uvue
@@ -1,6 +1,7 @@
@@ -18,7 +19,7 @@
- 2. 构造圆通协议数据 (YTO Protocol)
+ 2. 构造协议数据 (YTO Protocol)
物流单号 (mailNo):
@@ -56,11 +57,26 @@
-
diff --git a/pages/mall/delivery/test/consumer-order-list.uvue b/pages/mall/delivery/test/consumer-order-list.uvue
index 76dd57ae..4caa8904 100644
--- a/pages/mall/delivery/test/consumer-order-list.uvue
+++ b/pages/mall/delivery/test/consumer-order-list.uvue
@@ -24,7 +24,7 @@
📦
- {{ order.current_status_text || '包裹准备中' }}
+ {{ getDisplayMessage(order.current_status_text || '', order.status) }}
订单号: {{ order.order_no }}
>
@@ -77,15 +77,10 @@
this.orders = [...data]
},
getStatusText(status: string): string {
- const maps = {
- 'SHIPPED': '已发货',
- 'IN_TRANSIT': '运输中',
- 'DELIVERED': '已签收',
- 'OUT_FOR_DELIVERY': '派送中',
- 'PENDING': '待揽收',
- 'EXCEPTION': '包裹异常'
- }
- return (maps[status] != null) ? maps[status] : '处理中'
+ return mockService.getStatusText(status)
+ },
+ getDisplayMessage(text: string, status: string): string {
+ return mockService.getDisplayMessage(text, status)
},
goDetail(orderNo: string) {
uni.navigateTo({
diff --git a/pages/mall/delivery/test/merchant-order-detail.uvue b/pages/mall/delivery/test/merchant-order-detail.uvue
index 3ce0168e..bf0888f9 100644
--- a/pages/mall/delivery/test/merchant-order-detail.uvue
+++ b/pages/mall/delivery/test/merchant-order-detail.uvue
@@ -39,7 +39,7 @@
- {{ seg }}
+ {{ seg }}
{{ event.event_time }}
@@ -121,15 +121,7 @@
this.statusHistory = await mockService.getMockTracking(this.orderNo)
},
getStatusText(status: string) : string {
- const maps = {
- 'PENDING': '待发货',
- 'SHIPPED': '已发货',
- 'IN_TRANSIT': '运输中',
- 'DELIVERED': '已签收',
- 'OUT_FOR_DELIVERY': '派送中',
- 'EXCEPTION': '异常'
- }
- return (maps[status] != null) ? maps[status] : status
+ return mockService.getStatusText(status)
},
async refreshLogistics() {
if (this.order.status === 'DELIVERED') {
@@ -152,8 +144,13 @@
urls: urls
})
},
+ getProcessedText(text: string, status: string): string[] {
+ const displayMsg = mockService.getDisplayMessage(text, status)
+ return this.filterPhone(displayMsg)
+ },
filterPhone(text: string): string[] {
- // 商家端:不展示手机号
+ // 依照需求文档 1. 敏感信息展示差异:商家端不展示手机号
+ // 直接移除所有手机号
return [text.replace(/(1[3-9]\d{9})/g, '')]
}
}
diff --git a/pages/mall/delivery/test/merchant-order-list.uvue b/pages/mall/delivery/test/merchant-order-list.uvue
index 44cf55af..4660271c 100644
--- a/pages/mall/delivery/test/merchant-order-list.uvue
+++ b/pages/mall/delivery/test/merchant-order-list.uvue
@@ -28,14 +28,14 @@
收件人:
{{ order.receiver_name }} {{ order.receiver_masked_phone }}
-
+
运单号:
- {{ order.tracking_no }} ({{ order.carrier }})
+ {{ order.tracking_no ? order.tracking_no + ' (' + order.carrier + ')' : '暂无运单号' }}
@@ -49,7 +49,10 @@
@@ -61,7 +64,12 @@
运单号
-
+
+
+
+ 📷
+
+