修改文件

This commit is contained in:
not-like-juvenile
2026-02-04 18:04:44 +08:00
parent 3a0936c12e
commit 76f183af2a

View File

@@ -1,458 +0,0 @@
# 骑手类型与订单超时重分配方案
本文档汇总了将骑手分为“职业骑手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` -> accepted2清空 `reserved_*`
5. 若超时未 ACKWorker 检测到过期,清理 `reserved_*`,增加 `reserve_attempts`,选下一个候选重试;超过最大尝试次数后降级为公开抢单或人工处理。
## 5. 原子 SQL 示例
保留server side try-reserve保证原子性
```sql
-- 尝试把任务保留给 candidate_driver_id30s 保留
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/服务端示例代码生成到仓库中?