--- 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 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 与现场完全对上 26-3698 三个日格里,8/17 有证据(wx 在本次会话里动过这一天,触发了候选查询并写戳),8/18、8/19 是回显数据、证据为 `null` → **「1/3 个日格已完成」**,槽位因此算未完成 → **「已完成 0/1 个最终方案槽位」** → 「下一步」置灰。 > 补充:先前怀疑的「接机/参与勾选」不是原因——`pickupParticipant` 只参与「某服务日要求接机则至少一辆车参与接机」这条独立校验,不进日格完成度判定。 --- ## 四、建议改法 **核心认识:客户端「候选证据」是对后端已有保证的重复设卡。** 提交时后端在锁内会重新校验资源可用性、档期冲突、车辆维保/司机休假、日期窗,真有问题会返回明确错误码;前端不需要、也不应该要求车务为「本来就已经派好、且没改动过」的日格重新走一遍选车弹窗。 按优先级: 1. **(必做)从服务端快照重建日格时,用服务端数据补齐 `_candidateEvidence`**——即把「后端已存在这条派车行」本身当作证据(`serviceDate` / `vehicleId` / `driverId` / `confirmCrossResident` 直接取该行的值)。这样未被用户改动的既有日格天然通过,改动过的仍需走弹窗。 2. **(按 wx 口径)放宽门禁本身**:日格判定与「下一步」启用条件收敛为 **有车 + 有司机 + 服务日期在当前行程窗内**;车型不匹配、座位不足、槽位数与需求数量不符一律降级为提示(见下方分类表)。 3. 保留「用户手动改过车/司机后需重新校验」的行为是合理的,但**即使证据缺失也不应该硬置灰**——改为提示 + 允许提交,由后端做最终裁决。 --- ## 五、目标行为(wx 2026-08-11 拍板) > 只要**槽位里有司机、有车辆,且日期是对的**,就可以进行下一步。**定制师的需求变了,但车务可以不改派车**;和需求数对不上也可以。 与 #5810「换版槽位人工制」、#5824「车型/数量不符只提示不做门禁」一脉相承。 ### 硬约束 vs 软约束 | 类别 | 项 | 处理 | |---|---|---| | **硬**(仍须拦) | 同车/同司机同日期被**别单**占用(真实资源冲突) | 阻断 | | **硬** | 派车日期越出当前行程窗 | 阻断(后端 605062) | | **硬** | 车辆维保/停用(605037)、司机休假/未激活(605038) | 阻断 | | **软**(应降级为提示) | 车型不匹配 | 只提示 | | **软** | 座位数不足 | 只提示 | | **软** | 槽位数与需求数量不符 | 只提示 | | **软** | 日格「未完成」(在已有车 + 有司机 + 日期正确的前提下) | 只提示 | > 提醒:**车型不匹配不会影响 `selectable`**。后端 `selectable = available && conflicts 全部非阻断`,`requirementMatched` / `seatsEnough` 均不参与该计算。请不要把车型不匹配纳入按钮启用条件。 --- ## 六、后端侧的后续(不在本条,另发) - 前端放开门禁后,后端提交链路上是否还有会拒的守卫,正在核;需要后端改的部分由 **#5849** 处理,涉及契约变化会另发接口类 changelog。 - 若确认需要后端为前端补一个「本槽位是否可推进 + 不可推进原因」的字段,会在那份 changelog 里给出字段名与口径,届时前端可从「自己推导」改为「读后端结论」,避免两边口径再次劈叉。 --- ## 七、关联 - 后端工单 #5849 - `selectable` 字段口径:`11_5850_候选接口显式返回selectable-修改接口-管理后台.md` - 换版后车型陈旧快照(会让候选列表误标「需求不匹配」):#5871,修复后另发