294 行
15 KiB
Markdown
294 行
15 KiB
Markdown
---
|
||
schema: "hl-changelog/v2"
|
||
ticket: "frontend"
|
||
title: "派单 Step2「下一步·发送给司机」置灰:后端实测判定可用可选无冲突,日格「未通过候选校验」为前端侧误判"
|
||
consumer: "admin"
|
||
change_type: "前端缺陷"
|
||
author: "wx(GIT)"
|
||
backend_status: "not_required"
|
||
gateway_status: "not_required"
|
||
frontend_status: "pending"
|
||
frontend_owner: "mmg"
|
||
frontend_ref: ""
|
||
target_release: ""
|
||
verified_at: ""
|
||
status_note: "对应后端工单 #5849。根因已定位并在测试服部署包核实:日格「已完成」要求一枚客户端候选证据戳 _candidateEvidence,而从服务端快照重建的既有派车日格该戳恒为 null,唯一逃生口 preservedFinalized 又被 #5851「换版即作废定稿」关闭,导致换版后每个日格都必须重新走一遍选车弹窗才能推进。后端侧已实测排除(8/18 返回 available/selectable 均为 true、conflicts 为空)。后端若需为前端补推进态字段,另发接口类 changelog。"
|
||
updated_at: "2026-08-11"
|
||
base: "dev-v3"
|
||
---
|
||
|
||
# 派单 Step2「下一步」置灰 —— 后端侧已排除(#5849 前端部分)
|
||
|
||
> **页面**:车务管理 → 派单看板 → 订单派车弹窗 → Step2「排车」
|
||
> **后端**:本条**无后端改动**。下面全部是测试服实测结论,用于把排查范围收敛到前端。
|
||
|
||
---
|
||
|
||
## 一、现象
|
||
|
||
订单 **26-3698**(08/17→08/19,成人 10):
|
||
|
||
- 车辆槽位 1 已选车 `蒙A-E5555 丰田埃尔法`、司机 `阿木古愣`,**三天全部「已规划用车」、全部勾了「用车」**,日历价 ¥2500、本次派车价 ¥2500 都已填
|
||
- **8月18日** 那一行挂橙色提示:「2026-08-18 车辆槽位1 尚未通过当前日期、车辆和司机的候选校验」
|
||
- 顶部「排车方案 1 个槽位 · **1/3 个日格已完成**」
|
||
- 底部「已完成 **0/1** 个最终方案槽位」
|
||
- 「下一步 · 发送给司机(1 辆)」**置灰不可点**
|
||
|
||
同一现象在 26-5067 上表现为 8月21日 报同样的提示、「已完成 0/1」。
|
||
|
||
---
|
||
|
||
## 二、✅ 已确认事实:后端判定这三天全部可用、可选、无冲突
|
||
|
||
### 2.1 真实 API 实测(测试服网关,车务 admin token,2026-08-11)
|
||
|
||
对**报错的那一天** 8/18 单独查候选:
|
||
|
||
```
|
||
POST /admin/fleet/assignments/candidates
|
||
Authorization: Bearer <admin token>
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"orderId": 2086270138171994114,
|
||
"requirementId": 2087111726523617282,
|
||
"fleetItemIndex": 0,
|
||
"startDate": "2026-08-18",
|
||
"endDate": "2026-08-18",
|
||
"headcount": 10,
|
||
"excludeAssignmentId": 2086270138708844545,
|
||
"vehicleKeyword": "E5555"
|
||
}
|
||
```
|
||
|
||
响应中该车(节选,**原样**):
|
||
|
||
```json
|
||
{
|
||
"plate": "蒙A-E5555",
|
||
"vehicleTypeKey": "mpv",
|
||
"vehicleTypeName": "商务车",
|
||
"seats": 7,
|
||
"seatsEnough": true,
|
||
"requirementMatched": true,
|
||
"vehicleStatus": "busy",
|
||
"serviceStatus": "ACTIVE",
|
||
"selected": false,
|
||
"available": true,
|
||
"selectable": true,
|
||
"availabilityReasonCode": "AVAILABLE",
|
||
"availabilityReasonMessage": "所选服务日期内可用",
|
||
"availabilityWindows": [{ "startDate": "2026-08-18", "endDate": "2026-08-18" }],
|
||
"conflicts": [],
|
||
"primaryDriverId": "2065272145633591298",
|
||
"primaryDriverName": "阿木古愣",
|
||
"vehicleFeePriceComplete": true,
|
||
"missingVehicleFeeDates": []
|
||
}
|
||
```
|
||
|
||
**后端对 8/18 的判定是:可用 + 可选 + 无冲突 + 需求匹配 + 车费价格完整。**
|
||
|
||
### 2.2 三种 `excludeAssignmentId` 传法结果一致
|
||
|
||
我们曾怀疑「逐日行传了组锚点 id,导致该日行没被排除、车被自己占用判成冲突」。**该猜测已被证伪**:
|
||
|
||
| `excludeAssignmentId` 传法 | 8/18 结果 |
|
||
|---|---|
|
||
| 组锚点 id(`2086270138708844545`) | `available: true` / `selectable: true` |
|
||
| 该日行真实 id(`345501044920422400`) | `available: true` / `selectable: true` |
|
||
| 完全不传 | `available: true` / `selectable: true` |
|
||
|
||
### 2.3 数据层核实:不存在任何真实资源冲突
|
||
|
||
槽位 0 的三行逐日切片**完全同质**,唯一差异是日期与「接机/参与」勾选:
|
||
|
||
| 服务日 | 状态 | 车 | 司机 | 协议价 | 日历价 | 接机/参与 |
|
||
|---|---|---|---|---|---|---|
|
||
| 2026-08-17 | holding | 蒙A-E5555 | 阿木古愣 | 2500 | 2500 | **勾选** |
|
||
| 2026-08-18 | holding | 蒙A-E5555 | 阿木古愣 | 2500 | 2500 | 未勾 |
|
||
| 2026-08-19 | holding | 蒙A-E5555 | 阿木古愣 | 2500 | 2500 | 未勾 |
|
||
|
||
跨订单全库核查:**该车与该司机在 08/17~08/19 除本单外零占用**。
|
||
|
||
---
|
||
|
||
## 三、✅ 根因已定位:日格要求一枚「候选证据」客户端戳,而服务端加载的既有派车行这枚戳恒为 `null`
|
||
|
||
**已在测试服部署包 `/var/www/hl-admin/assets/` 核实**(不是本地陈旧源码):`daily-vehicle-plan-CQiA9Iru.js` 与 `AssignModal-BWuKA9uB.js` 中逻辑与源码一致。
|
||
|
||
### 3.1 判定链路
|
||
|
||
`src/views/fleet/board/utils/daily-vehicle-plan.js`
|
||
|
||
```js
|
||
// 日格「已完成」判定(约 :465)的最后一关
|
||
return preservedFinalized || dailyVehiclePlanCandidateEvidenceMatches(normalized)
|
||
|
||
// 逐日方案校验(约 :570-577)报出那句提示的地方
|
||
if (requireCandidateEvidence && !preservedFinalized
|
||
&& !dailyVehiclePlanCandidateEvidenceMatches(cell)) {
|
||
return `${label}尚未通过当前日期、车辆和司机的候选校验`
|
||
}
|
||
|
||
// 判定式本身(约 :81-90)
|
||
export function dailyVehiclePlanCandidateEvidenceMatches(cell = {}) {
|
||
const evidence = normalizeCandidateEvidence(cell._candidateEvidence)
|
||
return Boolean(
|
||
evidence &&
|
||
evidence.serviceDate === String(cell.serviceDate || '') &&
|
||
evidence.vehicleId === stringId(cell.vehicleId) &&
|
||
evidence.driverId === stringId(cell.driverId) &&
|
||
evidence.confirmCrossResident === (cell.confirmCrossResident === true)
|
||
)
|
||
}
|
||
```
|
||
|
||
### 3.2 关键:`_candidateEvidence` 只有「本次会话手动走过选车弹窗」才会有
|
||
|
||
| 来源 | `_candidateEvidence` |
|
||
|---|---|
|
||
| 用户在选车弹窗里当场选车/选司机 | 写入(`AssignModal.vue` 约 :2122 / :2477) |
|
||
| **从服务端快照重建日格**(打开弹窗回显既有派车) | **恒 `null`**(`utils/canonical-assignment-snapshot.js` 约 :134,部署包同构) |
|
||
| 新建槽位的空白格 | `null`(`AssignModal.vue` 约 :3560) |
|
||
|
||
→ **凡是从后端读回来的既有派车日格,天生就"没通过候选校验"**,哪怕车、司机、价格全都在,哪怕后端明确回 `selectable: true`。
|
||
|
||
### 3.3 唯一的逃生口已被 #5851 关上
|
||
|
||
`preservedFinalized` = `isUnchangedFinalizedPreservedCell(cell)`,要求 **`(readOnly || locked) && planFinalized === true && _preservedPlanSignature 未变`**。
|
||
|
||
而 #5851「换版即作废定稿」的既定行为是:**定制师改需求/人数/天数/出发日期后,派车行的 `dispatch_plan_finalized` 置 0**(这是有意为之,让车务重新过一遍)。于是 `planFinalized === false` → 逃生口关闭 → **换版后每一个日格都必须由车务重新点开选车弹窗、重新选一遍车和司机,才能推进下一步**。
|
||
|
||
这正是 wx 说的那句话的反面:「定制师的需求变了,但车务可以不改派车」。
|
||
|
||
### 3.4 为什么偏偏是「1/3」、且只有一天挂橙标
|
||
|
||
两条机制叠加,**不是「中间那天特殊」**:
|
||
|
||
1. **只有 1 格能拿到证据**:弹窗初始化时自动激活「第一个未完成的用车格」(`AssignModal.vue` 约 :3536-3541,`dailyPlan` 按日期升序),即 **8/17**;picker 对该单日拉候选并**只回写这一格**的证据(约 :2418-2481)。8/18、8/19 从未被激活,永远拿不到证据。
|
||
2. **两天都不合格,但只显示第一条**:`validateDailyVehiclePlan` 返回**第一条**错误;能定位到具体格时 `dailyPlanGlobalValidationError` 返回空串(约 :1820-1836),改由矩阵按 `serviceDate` + 槽位号渲染**单行**橙标(`DailyVehiclePlanMatrix.vue` 约 :296-301)。所以只有 8/18 挂标,8/19 同样不合格。另一单表现为 8/21 是同理。
|
||
|
||
→ **「1/3 个日格已完成」**(只有自动激活的那格有证据)→ 槽位算未完成 → **「已完成 0/1 个最终方案槽位」** → 「下一步」置灰。
|
||
|
||
> 排除项:「接机/参与」勾选**不是**原因——`pickupParticipant` 只参与「某服务日要求接机则至少一辆车参与接机」这条独立校验,`isDailyVehiclePlanCellComplete` 通篇不读该字段。
|
||
|
||
### 3.5 按钮与两个读数的完整链路(供定位)
|
||
|
||
```js
|
||
// AssignModalFooter.vue 约 :107
|
||
:disabled="!selectionReady || submitting"
|
||
|
||
// AssignModal.vue 约 :1837-1846
|
||
selectionReady = batchMode
|
||
? selectedSlots.length > 0 && !selectionBlockedReason && !dailyPlanValidationError
|
||
: ...
|
||
|
||
// 「N/M 个日格已完成」 约 :1526
|
||
dailyPlan.filter(isDailyVehiclePlanCellComplete).length
|
||
|
||
// 「已完成 N/M 个最终方案槽位」 约 :2416
|
||
slot.selectionReady = cells.every(isDailyVehiclePlanCellComplete)
|
||
```
|
||
|
||
三者全部本地计算,**Step2 →Step3 全程零 HTTP**(`useAssignFlow.js` 约 :685-701 的 `onStep2Next` 只做 `step.value = 3`)。
|
||
|
||
---
|
||
|
||
## 🚑 立即可用的绕过(不需要发版)
|
||
|
||
抽屉里用「**统一选择车辆 / 司机**」→「**应用到槽位全部日期**」。该路径会给整槽**所有可编辑用车日格批量写入证据**(`AssignModal.vue` 约 :2122-2127,成功提示「已应用到当前槽位 N 个用车日格」),一次打满 3/3,「下一步」即可点。
|
||
|
||
---
|
||
|
||
## 四、建议改法
|
||
|
||
**核心认识:客户端「候选证据」是对后端已有保证的重复设卡。** 提交时后端在锁内会重新校验资源可用性、档期冲突、车辆维保/司机休假、日期窗,真有问题会返回明确错误码;前端不需要、也不应该要求车务为「本来就已经派好、且没改动过」的日格重新走一遍选车弹窗。
|
||
|
||
### 最小改动:`utils/daily-vehicle-plan.js` 两行(**必须同改**)
|
||
|
||
| # | 位置 | 现在 | 改为 | 解决 |
|
||
|---|---|---|---|---|
|
||
| 1 | 约 :484 | `requireCandidateEvidence = true` | `false` | 按钮置灰 + 橙标 |
|
||
| 2 | 约 :465 | `return preservedFinalized \|\| dailyVehiclePlanCandidateEvidenceMatches(normalized)` | `return true` | 「1/3」与「0/1」计数 |
|
||
|
||
只改 1 不改 2 → 按钮能点但计数仍显未完成;只改 2 不改 1 → 计数对了但按钮仍被 `dailyPlanValidationError` 卡住。
|
||
|
||
### 可选增强(与 #5810 / #5824「只提示不门禁」同构)
|
||
|
||
新增一个独立 computed 调 `validateDailyVehiclePlan(..., { requireCandidateEvidence: true })`,把结果渲染成**灰色软提示**,**不接入 `selectionReady`**。这样"这一天你还没重新验过"的信息仍然可见,但不再卡住流程。
|
||
|
||
### 另一个更彻底的方向(可与上面二选一)
|
||
|
||
从服务端快照重建日格时,用服务端数据补齐 `_candidateEvidence`(`serviceDate` / `vehicleId` / `driverId` / `confirmCrossResident` 直接取该行的值)——把「后端已存在这条派车行」本身当作证据。这样未被用户改动的既有日格天然通过,改动过的仍需走弹窗。
|
||
|
||
### 回归
|
||
|
||
需同步调整 `__tests__/assign-modal-title.spec.js`(约 1726 / 1744 行等)与 `utils/__tests__/daily-vehicle-plan.spec.js` 的期望值。
|
||
|
||
---
|
||
|
||
## 五、目标行为(wx 2026-08-11 拍板)
|
||
|
||
> 只要**槽位里有司机、有车辆,且日期是对的**,就可以进行下一步。**定制师的需求变了,但车务可以不改派车**;和需求数对不上也可以。
|
||
|
||
与 #5810「换版槽位人工制」、#5824「车型/数量不符只提示不做门禁」一脉相承。
|
||
|
||
### 硬约束 vs 软约束
|
||
|
||
| 类别 | 项 | 处理 |
|
||
|---|---|---|
|
||
| **硬**(仍须拦) | 同车/同司机同日期被**别单**占用(真实资源冲突) | 阻断 |
|
||
| **硬** | 派车日期越出当前行程窗 | 阻断(后端 605062) |
|
||
| **硬** | 车辆维保/停用(605037)、司机休假/未激活(605038) | 阻断 |
|
||
| **软**(应降级为提示) | 车型不匹配 | 只提示 |
|
||
| **软** | 座位数不足 | 只提示 |
|
||
| **软** | 槽位数与需求数量不符 | 只提示 |
|
||
| **软** | 日格「未完成」(在已有车 + 有司机 + 日期正确的前提下) | 只提示 |
|
||
|
||
> 提醒:**车型不匹配不会影响 `selectable`**。后端 `selectable = available && conflicts 全部非阻断`,`requirementMatched` / `seatsEnough` 均不参与该计算。请不要把车型不匹配纳入按钮启用条件。
|
||
|
||
---
|
||
|
||
## 六、后端侧:本条 **0 改动**,放开前端后不会再被拒
|
||
|
||
已逐条核实(供前端放心放开):
|
||
|
||
| 守卫 | 结论 |
|
||
|---|---|
|
||
| Step2 → Step3 | **无任何后端调用** |
|
||
| `batchCreate` 契约 | 明文「可少派、多派…**车型/数量/人数与需求不符不拦截**」;`strictSeats` 三处硬编码 `false` |
|
||
| 605062 日期越窗 | 判定式只看日期集合归属,**不含** vehicleType / count / seats / headcount |
|
||
| `hasInvalidDispatchPlanGeneration` | 本单三行 `generation=NULL` / `finalized=0` → 返回 false,不拦 |
|
||
| `isRequirementFullyAssignedRows` | 只 return false 从不抛,用于快照发布与订单车控 DONE 判定,**非提交门禁** |
|
||
| `assertAtomicGroupReady` | Step4 确认执行的门禁,与 Step2 无关 |
|
||
| 「每个保留槽位必须覆盖全部服务日」(100001) | 要求每槽×每日提交一条 item,`used=false` 空格合法;前端本就按满矩阵提交,天然满足 |
|
||
|
||
---
|
||
|
||
## 七、⚠️ 顺带发现的另一个前端缺陷(独立问题,本次修完仍需处理)
|
||
|
||
测试服 `hl-fleet-service.log` 17:40:39~17:41:20 抓到 **8 条**同款 WARN:
|
||
|
||
```
|
||
候选查询排除派单与请求槽位上下文不一致,已忽略排除继续查询:
|
||
reqFleetItemIndex=2 excludedFleetItemIndex=0
|
||
excludedAssignmentId=2086270138708844545 excludedStatus=holding
|
||
```
|
||
|
||
**新增槽位(index=2)时,前端仍把 slot0 的 `assignmentId` 当 `excludeAssignmentId` 传**。后端判定上下文不一致后**静默放弃排除**(不报错,如实展示占用),于是本单自己的占用被算成阻断冲突 → 该车/该司机在新槽位下 `selectable=false`。实测印证:`fleetItemIndex=1` 或 `2` 时三天全部 `selectable=false`,冲突体正是本单自己。
|
||
|
||
**影响**:新增的槽位**永远拿不到候选证据**,是本次问题的同源二次伤害。
|
||
|
||
**前端改法**:新增槽位时不要沿用其它槽位的 `assignmentId`,该槽无既有派车行就**不传** `excludeAssignmentId`。
|
||
|
||
> 后端侧会考虑加固(候选入参支持按槽位排除,而不是靠反查单行推断槽位身份),届时另发接口类 changelog。
|
||
|
||
---
|
||
|
||
## 八、关联
|
||
|
||
- 后端工单 #5849
|
||
- `selectable` 字段口径:`11_5850_候选接口显式返回selectable-修改接口-管理后台.md`
|
||
- 换版后车型陈旧快照(会让候选列表误标「需求不匹配」):#5871,修复后另发
|