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] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=8C=B9=E9=85=8D=E6=9C=BA?= =?UTF-8?q?=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' } -