From 3da4d008821145adc9fb96235d8512b2c7be1d28 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Tue, 11 Aug 2026 18:50:52 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#5849=20=E6=A0=B9=E5=9B=A0?= =?UTF-8?q?=E6=9B=B4=E6=AD=A3=E4=B8=8E=E8=AF=A6=E8=A7=A3=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=B7=B2=E5=AE=9A=E7=A8=BF=E8=B1=81=E5=85=8D=E7=BB=91=E6=AD=BB?= =?UTF-8?q?=20readOnly=20=E6=B0=B8=E4=B8=8D=E7=94=9F=E6=95=88=EF=BC=9B?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E5=BC=8F=E9=80=90=E8=A1=8C=E4=BB=A3=E5=85=A5?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E6=95=B0=E6=8D=AE=EF=BC=9B=E7=BB=95=E8=BF=87?= =?UTF-8?q?=E6=89=8B=E6=AE=B5=E5=AE=9E=E6=B5=8B=E6=97=A0=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...一步置灰与日格未完成误判-前端缺陷-管理后台.md | 302 +++++++++--------- 1 file changed, 148 insertions(+), 154 deletions(-) diff --git a/changelogs-v2/2026-08/11_frontend_派单Step2下一步置灰与日格未完成误判-前端缺陷-管理后台.md b/changelogs-v2/2026-08/11_frontend_派单Step2下一步置灰与日格未完成误判-前端缺陷-管理后台.md index 14bf099..5614ff9 100644 --- a/changelogs-v2/2026-08/11_frontend_派单Step2下一步置灰与日格未完成误判-前端缺陷-管理后台.md +++ b/changelogs-v2/2026-08/11_frontend_派单Step2下一步置灰与日格未完成误判-前端缺陷-管理后台.md @@ -1,7 +1,7 @@ --- schema: "hl-changelog/v2" ticket: "frontend" -title: "派单 Step2「下一步·发送给司机」置灰:后端实测判定可用可选无冲突,日格「未通过候选校验」为前端侧误判" +title: "派单 Step2「下一步·发送给司机」置灰 / 日格恒显 1/3:完成度只认前端会话内的候选证据戳,已定稿豁免因绑死 readOnly 而永不生效" consumer: "admin" change_type: "前端缺陷" author: "wx(GIT)" @@ -12,168 +12,148 @@ frontend_owner: "mmg" frontend_ref: "" target_release: "" verified_at: "" -status_note: "对应后端工单 #5849。根因已定位并在测试服部署包核实:日格「已完成」要求一枚客户端候选证据戳 _candidateEvidence,而从服务端快照重建的既有派车日格该戳恒为 null,唯一逃生口 preservedFinalized 又被 #5851「换版即作废定稿」关闭,导致换版后每个日格都必须重新走一遍选车弹窗才能推进。后端侧已实测排除(8/18 返回 available/selectable 均为 true、conflicts 为空)。后端若需为前端补推进态字段,另发接口类 changelog。" +status_note: "对应后端工单 #5849。后端已用真实 API + DB 双向排除(车与司机、单日与整段、三种 excludeAssignmentId 传法,全部 available=true / selectable=true / conflicts=[])。根因在前端 isDailyVehiclePlanCellComplete:完成度最终只取决于会话内存字段 _candidateEvidence,而服务端回显的日格该字段恒 null;唯一豁免 preservedFinalized 额外要求 readOnly||locked,后端下发的是 readOnly:false + planFinalized:true,故豁免分支永不进入。结论已用测试服部署包核实,非本地陈旧源码。注:先前版本称『豁免被 #5851 换版作废定稿关闭』有误,接口实测 planFinalized 为 true,与 #5851 无关,已更正。" updated_at: "2026-08-11" base: "dev-v3" --- -# 派单 Step2「下一步」置灰 —— 后端侧已排除(#5849 前端部分) +# 派单 Step2「下一步」置灰 / 日格恒显 1/3(#5849 前端部分) > **页面**:车务管理 → 派单看板 → 订单派车弹窗 → Step2「排车」 -> **后端**:本条**无后端改动**。下面全部是测试服实测结论,用于把排查范围收敛到前端。 +> **后端**:本条**零改动**。下面所有结论来自测试服真实 API、真实 DB 与**线上部署包**(`/var/www/hl-admin/assets/`,mtime 2026-08-11 18:06,比本地 hl-ui HEAD 新)三向核实。 --- ## 一、现象 -订单 **26-3698**(08/17→08/19,成人 10): +订单 **26-3698**(08/17→08/19,成人 10),车辆槽位 1: -- 车辆槽位 1 已选车 `蒙A-E5555 丰田埃尔法`、司机 `阿木古愣`,**三天全部「已规划用车」、全部勾了「用车」**,日历价 ¥2500、本次派车价 ¥2500 都已填 -- **8月18日** 那一行挂橙色提示:「2026-08-18 车辆槽位1 尚未通过当前日期、车辆和司机的候选校验」 -- 顶部「排车方案 1 个槽位 · **1/3 个日格已完成**」 +- 三天**全部「已规划用车」、全部勾「用车」**,车 `蒙A-E5555 丰田埃尔法`、司机 `阿木古愣`、日历价与派车价均 ¥2500 全填 +- 8月18日 挂橙标:「2026-08-18 车辆槽位1 尚未通过当前日期、车辆和司机的候选校验」 +- 顶部「1 个槽位 · **1/3 个日格已完成**」 - 底部「已完成 **0/1** 个最终方案槽位」 -- 「下一步 · 发送给司机(1 辆)」**置灰不可点** +- 「下一步 · 发送给司机(1 辆)」**置灰** -同一现象在 26-5067 上表现为 8月21日 报同样的提示、「已完成 0/1」。 +用户原话:**「3 日的行程都有数据,为啥完成 1/3」**。 --- -## 二、✅ 已确认事实:后端判定这三天全部可用、可选、无冲突 +## 二、后端已彻底排除 -### 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" -} -``` - -响应中该车(节选,**原样**): +`GET /admin/fleet/board/orders/2086270138171994114` 返回的三个日格**逐字段对称**,只有 `assignmentId` / `serviceDate` / `pickup*` 不同: ```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": [] + "serviceDate": "2026-08-18", + "fleetItemIndex": 0, + "assignmentSlotId": "344659955019812864", + "assignmentId": "345501044920422400", + "assignmentGroupId": "2086270138708844545", + "planFinalized": true, + "planState": "USED", + "used": true, + "readOnly": false, + "readOnlyReason": null, + "pickupParticipant": false, + "pickupRequired": false, + "vehicleId": "2064998142394183681", + "vehiclePlate": "蒙A-E5555", + "vehicleModel": "丰田埃尔法", + "driverId": "2065272145633591298", + "driverName": "阿木古愣", + "calendarPrice": "2500.00", + "assignmentPrice": "2500.00", + "priceSource": "CALENDAR", + "priceAdjustmentReason": null, + "assignmentStatus": "holding" } ``` -**后端对 8/18 的判定是:可用 + 可选 + 无冲突 + 需求匹配 + 车费价格完整。** +`POST /admin/fleet/assignments/candidates` 全组合实测——**车与司机、单日与整段(8/17→8/19)、三种 `excludeAssignmentId` 传法(组锚点 / 该日行真实 id / 不传)一律**: -### 2.2 三种 `excludeAssignmentId` 传法结果一致 +``` +车 available=true selectable=true availabilityReasonCode=AVAILABLE conflicts=[] +司机 available=true selectable=true availabilityReasonCode=AVAILABLE conflicts=[] +``` -我们曾怀疑「逐日行传了组锚点 id,导致该日行没被排除、车被自己占用判成冲突」。**该猜测已被证伪**: +DB 侧:跨订单全库核查该车该司机 08/17~08/19 **零外单占用**;车 `service_status=ACTIVE` 未维保;司机 `season=active` 未休假、且是该车 primary driver(不触发跨常驻确认);价格日历 08-15~08-22 每天 2500 AVAILABLE 不缺价。 -| `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 除本单外零占用**。 +**Step2 → Step3 全程零 HTTP**:`useAssignFlow.js` 约 :685-701 的 `onStep2Next` 只做 `step.value = 3`。 --- -## 三、✅ 根因已定位:日格要求一枚「候选证据」客户端戳,而服务端加载的既有派车行这枚戳恒为 `null` +## 三、根因 -**已在测试服部署包 `/var/www/hl-admin/assets/` 核实**(不是本地陈旧源码):`daily-vehicle-plan-CQiA9Iru.js` 与 `AssignModal-BWuKA9uB.js` 中逻辑与源码一致。 +### 3.1 判定式逐行代入真实数据 -### 3.1 判定链路 +`utils/daily-vehicle-plan.js` 约 :443-466(部署包 `daily-vehicle-plan-CQiA9Iru.js` 逐字一致): -`src/views/fleet/board/utils/daily-vehicle-plan.js` +| # | 代码 | 代入真实 cell | 结果 | +|---|---|---|---| +| 1 | `if (planState === UNPLANNED && !used) return false` | `USED` / `used=true` | 不早退 | +| 2 | `const preservedFinalized = (readOnly \| locked) ? isUnchangedFinalizedPreservedCell(cell) : false` | `readOnly=false`,`locked` 后端不下发 | **`false`(三元走 else,`isUnchangedFinalizedPreservedCell` 根本没被调用)** | +| 3 | `if ((readOnly \| locked) && !preservedFinalized) return false` | 条件为 false | 跳过 | +| 4 | `if (!used) return true` | `used=true` | 继续 | +| 5 | `if (!vehicleId \|\| !driverId) return false` | 两者都有 | 继续 | +| 6 | `if (!isValidAssignmentPrice(assignmentPrice)) return false` | `"2500.00"` 合法 | 继续 | +| 7 | `if (preservedFinalized) return true` | `false` | **不提前通过** | +| 8 | `if (needsPriceReason && !reason) return false` | `2500.00 === 2500.00` 不算改价 | 继续 | +| 9 | `if (requiresCrossResidentConfirmation && !confirmCrossResident) return false` | 后端不下发该字段 → `undefined` | 继续 | +| 10 | `return preservedFinalized \|\| dailyVehiclePlanCandidateEvidenceMatches(cell)` | `false \|\| evidence?` | **← 唯一决定因素** | + +**这三格的完成与否,100% 只取决于 `_candidateEvidence`。** + +### 3.2 `_candidateEvidence` 是纯前端会话内存字段,服务端回显的格子恒 null + +后端 `DailyVehiclePlanVO` 没有这个字段(见上面响应),前端 `createDailyVehiclePlan` 也从不合成、只做透传(约 :184-186)。 + +| 来源 | `_candidateEvidence` | +|---|---| +| 用户当场在选车抽屉里选车/选司机 | 写入(`AssignModal.vue` 约 :2122-2127 批量、:2477 单格) | +| **从服务端快照重建日格** | **硬写 `null`**(`utils/canonical-assignment-snapshot.js` 约 :134) | +| 新建槽位空白格 | `null`(`AssignModal.vue` 约 :3560) | + +### 3.3 🔴 唯一豁免 `preservedFinalized` 因为绑死 `readOnly` 而永不生效 ```js -// 日格「已完成」判定(约 :465)的最后一关 -return preservedFinalized || dailyVehiclePlanCandidateEvidenceMatches(normalized) - -// 逐日方案校验(约 :570-577)报出那句提示的地方 -if (requireCandidateEvidence && !preservedFinalized - && !dailyVehiclePlanCandidateEvidenceMatches(cell)) { - return `${label}尚未通过当前日期、车辆和司机的候选校验` +// :66-70 +export function markPreservedFinalizedCell(cell) { + if (!(cell.readOnly || cell.locked) || cell.planFinalized !== true) return cell // ← 直接原样返回 + if (cell._preservedPlanSignature) return cell + return { ...cell, _preservedPlanSignature: preservedPlanSignature(cell) } } -// 判定式本身(约 :81-90) -export function dailyVehiclePlanCandidateEvidenceMatches(cell = {}) { - const evidence = normalizeCandidateEvidence(cell._candidateEvidence) +// :72-79 +function isUnchangedFinalizedPreservedCell(cell = {}) { return Boolean( - evidence && - evidence.serviceDate === String(cell.serviceDate || '') && - evidence.vehicleId === stringId(cell.vehicleId) && - evidence.driverId === stringId(cell.driverId) && - evidence.confirmCrossResident === (cell.confirmCrossResident === true) + (cell.readOnly || cell.locked) && // ← 又要求一次 + cell.planFinalized === true && + cell._preservedPlanSignature && + cell._preservedPlanSignature === preservedPlanSignature(cell) ) } ``` -### 3.2 关键:`_candidateEvidence` 只有「本次会话手动走过选车弹窗」才会有 +后端对这三格下发的是 **`planFinalized: true` + `readOnly: false`**,即「**已定稿、但仍可编辑**」。这个组合三道都过不去: -| 来源 | `_candidateEvidence` | -|---|---| -| 用户在选车弹窗里当场选车/选司机 | 写入(`AssignModal.vue` 约 :2122 / :2477) | -| **从服务端快照重建日格**(打开弹窗回显既有派车) | **恒 `null`**(`utils/canonical-assignment-snapshot.js` 约 :134,部署包同构) | -| 新建槽位的空白格 | `null`(`AssignModal.vue` 约 :3560) | +1. `markPreservedFinalizedCell` 第一行就 `return cell`,**`_preservedPlanSignature` 从来没被写上**; +2. 即便写上了,`isUnchangedFinalizedPreservedCell` 仍要求 `readOnly || locked`; +3. 而且 3.1 第 2 步的三元决定了这个函数**压根不会被调用**。 -→ **凡是从后端读回来的既有派车日格,天生就"没通过候选校验"**,哪怕车、司机、价格全都在,哪怕后端明确回 `selectable: true`。 +**→ 对「已定稿但可编辑」的日格,豁免通道彻底是死的**,只能回落到候选证据。 -### 3.3 唯一的逃生口已被 #5851 关上 - -`preservedFinalized` = `isUnchangedFinalizedPreservedCell(cell)`,要求 **`(readOnly || locked) && planFinalized === true && _preservedPlanSignature 未变`**。 - -而 #5851「换版即作废定稿」的既定行为是:**定制师改需求/人数/天数/出发日期后,派车行的 `dispatch_plan_finalized` 置 0**(这是有意为之,让车务重新过一遍)。于是 `planFinalized === false` → 逃生口关闭 → **换版后每一个日格都必须由车务重新点开选车弹窗、重新选一遍车和司机,才能推进下一步**。 - -这正是 wx 说的那句话的反面:「定制师的需求变了,但车务可以不改派车」。 +> **更正一条早先的说法**:先前判断「#5851 换版即作废定稿把 `planFinalized` 置成 false 从而关闭豁免」——**不成立**。接口实测 `planFinalized` 为 `true`。豁免关闭的原因是上面的 `readOnly` 绑定,与 #5851 无关。 ### 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. **只有 1 格能拿到证据**:弹窗初始化自动激活「第一个未完成的用车格」(`AssignModal.vue` 约 :3536-3541;`dailyPlan` 按日期升序,`daily-vehicle-plan.js` 约 :201-205)→ 激活 **8/17**,抽屉对该单日拉候选并**只回写这一格**证据(约 :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 个最终方案槽位」** → 「下一步」置灰。 +**排除项**:「接机/参与」勾选**不是**原因——`isDailyVehiclePlanCellComplete` 通篇不读 `pickupParticipant`。 -> 排除项:「接机/参与」勾选**不是**原因——`pickupParticipant` 只参与「某服务日要求接机则至少一辆车参与接机」这条独立校验,`isDailyVehiclePlanCellComplete` 通篇不读该字段。 - -### 3.5 按钮与两个读数的完整链路(供定位) +### 3.5 按钮与两个读数的完整链路 ```js // AssignModalFooter.vue 约 :107 @@ -189,42 +169,58 @@ dailyPlan.filter(isDailyVehiclePlanCellComplete).length // 「已完成 N/M 个最终方案槽位」 约 :2416 slot.selectionReady = cells.every(isDailyVehiclePlanCellComplete) + +// dailyPlanValidationError 约 :1808 → validateDailyVehiclePlan(...) +// 其中约 :571-577 抛出那句橙标: +if (requireCandidateEvidence && !preservedFinalized + && !dailyVehiclePlanCandidateEvidenceMatches(cell)) { + return `${label}尚未通过当前日期、车辆和司机的候选校验` +} ``` -三者全部本地计算,**Step2 →Step3 全程零 HTTP**(`useAssignFlow.js` 约 :685-701 的 `onStep2Next` 只做 `step.value = 3`)。 +### 3.6 ⚠️「统一选择车辆/司机 → 应用到槽位全部日期」**不能**当绕过手段(wx 实测无效) ---- +该路径确实会给整槽用车格批量写证据(约 :2102-2128),但它自己还有三道前置门: -## 🚑 立即可用的绕过(不需要发版) +1. `applySlotResource` 要求 `selVehicle && selDriver` 都已在抽屉内选中,否则 `message.warning('请先选择车辆和司机')` 直接返回(约 :2045-2050); +2. `applySlotResourceSelection` 开头要求 `candidateSelectionReady`,重查一次仍不 ready 就 `return`(约 :2064-2080); +3. `targetCells` 只取 `cell.used && !cell.readOnly && !cell.locked`(约 :2087-2092),空集直接返回。 -抽屉里用「**统一选择车辆 / 司机**」→「**应用到槽位全部日期**」。该路径会给整槽**所有可编辑用车日格批量写入证据**(`AssignModal.vue` 约 :2122-2127,成功提示「已应用到当前槽位 N 个用车日格」),一次打满 3/3,「下一步」即可点。 +**不要把它写进用户手册当 workaround**,必须从判定逻辑本身修。 --- ## 四、建议改法 -**核心认识:客户端「候选证据」是对后端已有保证的重复设卡。** 提交时后端在锁内会重新校验资源可用性、档期冲突、车辆维保/司机休假、日期窗,真有问题会返回明确错误码;前端不需要、也不应该要求车务为「本来就已经派好、且没改动过」的日格重新走一遍选车弹窗。 +**核心认识:`_candidateEvidence` 是对后端既有保证的重复设卡。** 提交时后端会在锁内重新校验资源可用性、档期冲突、车辆维保、司机休假、日期窗,真有问题会返回明确错误码(605001/605003/605037/605038/605062)。前端不该要求车务为「本来就派好、且没改过」的日格重新点一遍选车抽屉。 -### 最小改动:`utils/daily-vehicle-plan.js` 两行(**必须同改**) +### 方案 A(最快,2 行,直接满足 wx 口径) -| # | 位置 | 现在 | 改为 | 解决 | -|---|---|---|---|---| -| 1 | 约 :484 | `requireCandidateEvidence = true` | `false` | 按钮置灰 + 橙标 | -| 2 | 约 :465 | `return preservedFinalized \|\| dailyVehiclePlanCandidateEvidenceMatches(normalized)` | `return true` | 「1/3」与「0/1」计数 | +| # | 位置 | 现在 | 改为 | +|---|---|---|---| +| 1 | `daily-vehicle-plan.js` 约 :484 | `requireCandidateEvidence = true` | `false` | +| 2 | `daily-vehicle-plan.js` 约 :465 | `return preservedFinalized \|\| dailyVehiclePlanCandidateEvidenceMatches(normalized)` | `return true` | -只改 1 不改 2 → 按钮能点但计数仍显未完成;只改 2 不改 1 → 计数对了但按钮仍被 `dailyPlanValidationError` 卡住。 +**必须同改**:只改 1 → 按钮能点但计数仍显 1/3;只改 2 → 计数对了但按钮仍被 `dailyPlanValidationError` 卡住。 + +### 方案 B(更保守,保留「改动过要重验」的语义) + +1. **修好豁免通道**:把 `markPreservedFinalizedCell` 与 `isUnchangedFinalizedPreservedCell` 的 `readOnly || locked` 前置去掉,只要 `planFinalized === true` 且签名未变即视为「已定稿未改动」;同时把 `isDailyVehiclePlanCellComplete` 第 2 步的三元改为**无条件**调用 `isUnchangedFinalizedPreservedCell(normalized)`。 + → 后端已定稿、用户没动过的格子天然完成;用户一改车/司机/价格,签名变化自动失效,仍需重验。 +2. **从服务端快照重建日格时补齐 `_candidateEvidence`**(`serviceDate` / `vehicleId` / `driverId` / `confirmCrossResident` 直接取该行的值)——把「后端已存在这条派车行」本身当作证据。 +3. 即使证据缺失也**不要硬置灰**,降级为灰色软提示,由后端做最终裁决。 + +### 建议 + +**先上方案 A 解阻塞**(wx 现在卡着),再按方案 B 把语义修正确。两者不冲突。 ### 可选增强(与 #5810 / #5824「只提示不门禁」同构) -新增一个独立 computed 调 `validateDailyVehiclePlan(..., { requireCandidateEvidence: true })`,把结果渲染成**灰色软提示**,**不接入 `selectionReady`**。这样"这一天你还没重新验过"的信息仍然可见,但不再卡住流程。 - -### 另一个更彻底的方向(可与上面二选一) - -从服务端快照重建日格时,用服务端数据补齐 `_candidateEvidence`(`serviceDate` / `vehicleId` / `driverId` / `confirmCrossResident` 直接取该行的值)——把「后端已存在这条派车行」本身当作证据。这样未被用户改动的既有日格天然通过,改动过的仍需走弹窗。 +新增独立 computed 调 `validateDailyVehiclePlan(..., { requireCandidateEvidence: true })`,渲染成**灰色软提示**、**不接入 `selectionReady`**——保留「这天还没重新验过」的信息,但不卡流程。 ### 回归 -需同步调整 `__tests__/assign-modal-title.spec.js`(约 1726 / 1744 行等)与 `utils/__tests__/daily-vehicle-plan.spec.js` 的期望值。 +同步调整 `__tests__/assign-modal-title.spec.js`(约 1726 / 1744 / 1988 / 2069 / 2208 / 2236 行)与 `utils/__tests__/daily-vehicle-plan.spec.js` 的期望值。 --- @@ -234,39 +230,37 @@ slot.selectionReady = cells.every(isDailyVehiclePlanCellComplete) 与 #5810「换版槽位人工制」、#5824「车型/数量不符只提示不做门禁」一脉相承。 -### 硬约束 vs 软约束 +| 类别 | 项 | 判定方 | 现状 | 应然 | +|---|---|---|---|---| +| **硬** | 同车/同司机同日期被**别单**占用 | 后端 605001/605003 + 锁内重校验 | 硬 | 硬 ✅ | +| **硬** | 派车日期越出当前需求窗 | 后端 605062(只看日期集合,不含车型/数量/座位/人数) | 硬 | 硬 ✅ | +| **硬** | 车辆维保/停用 | 后端 605037 | 硬 | 硬 ✅ | +| **硬** | 司机休假/待激活 | 后端 605038 | 硬 | 硬 ✅ | +| **硬** | 同日重复车/司机 | 前后端都有 | 硬 | 硬 ✅ | +| **硬** | 用车格缺车/缺司机 | 前后端都有 | 硬 | 硬 ✅ | +| 软 | 车型不匹配 | 后端已降级 | 软 | 软 ✅ | +| 软 | 座位不足 / 槽位数 vs 需求数 | 后端不拦 | 软 | 软 ✅ | +| **软** | **日格「候选证据」完成度** | **前端独有** | **硬** | ❌ **降级为提示** | -| 类别 | 项 | 处理 | -|---|---|---| -| **硬**(仍须拦) | 同车/同司机同日期被**别单**占用(真实资源冲突) | 阻断 | -| **硬** | 派车日期越出当前行程窗 | 阻断(后端 605062) | -| **硬** | 车辆维保/停用(605037)、司机休假/未激活(605038) | 阻断 | -| **软**(应降级为提示) | 车型不匹配 | 只提示 | -| **软** | 座位数不足 | 只提示 | -| **软** | 槽位数与需求数量不符 | 只提示 | -| **软** | 日格「未完成」(在已有车 + 有司机 + 日期正确的前提下) | 只提示 | - -> 提醒:**车型不匹配不会影响 `selectable`**。后端 `selectable = available && conflicts 全部非阻断`,`requirementMatched` / `seatsEnough` 均不参与该计算。请不要把车型不匹配纳入按钮启用条件。 +> 提醒:**车型不匹配不影响 `selectable`**。后端 `selectable = available && conflicts 全部非阻断`,`requirementMatched` / `seatsEnough` 均不参与。请勿把车型不匹配纳入按钮启用条件。 --- -## 六、后端侧:本条 **0 改动**,放开前端后不会再被拒 - -已逐条核实(供前端放心放开): +## 六、后端侧:本条 0 改动,放开后不会再被拒 | 守卫 | 结论 | |---|---| | Step2 → Step3 | **无任何后端调用** | -| `batchCreate` 契约 | 明文「可少派、多派…**车型/数量/人数与需求不符不拦截**」;`strictSeats` 三处硬编码 `false` | -| 605062 日期越窗 | 判定式只看日期集合归属,**不含** vehicleType / count / seats / headcount | -| `hasInvalidDispatchPlanGeneration` | 本单三行 `generation=NULL` / `finalized=0` → 返回 false,不拦 | +| `batchCreate` 契约 | 明文「可少派、多派…车型/数量/人数与需求不符不拦截」;`strictSeats` 三处硬编码 `false` | +| 605062 日期越窗 | 判定式只看日期集合归属 | +| `hasInvalidDispatchPlanGeneration` | 本单 `generation=NULL` / `finalized=0` → false,不拦 | | `isRequirementFullyAssignedRows` | 只 return false 从不抛,用于快照发布与订单车控 DONE 判定,**非提交门禁** | -| `assertAtomicGroupReady` | Step4 确认执行的门禁,与 Step2 无关 | -| 「每个保留槽位必须覆盖全部服务日」(100001) | 要求每槽×每日提交一条 item,`used=false` 空格合法;前端本就按满矩阵提交,天然满足 | +| `assertAtomicGroupReady` | Step4 确认执行门禁,与 Step2 无关 | +| 「每个保留槽位必须覆盖全部服务日」(100001) | 要求每槽×每日一条 item,`used=false` 空格合法;前端本就按满矩阵提交,天然满足 | --- -## 七、⚠️ 顺带发现的另一个前端缺陷(独立问题,本次修完仍需处理) +## 七、⚠️ 同源的第二个前端缺陷(独立问题,本次修完仍需处理) 测试服 `hl-fleet-service.log` 17:40:39~17:41:20 抓到 **8 条**同款 WARN: @@ -276,13 +270,13 @@ slot.selectionReady = cells.every(isDailyVehiclePlanCellComplete) excludedAssignmentId=2086270138708844545 excludedStatus=holding ``` -**新增槽位(index=2)时,前端仍把 slot0 的 `assignmentId` 当 `excludeAssignmentId` 传**。后端判定上下文不一致后**静默放弃排除**(不报错,如实展示占用),于是本单自己的占用被算成阻断冲突 → 该车/该司机在新槽位下 `selectable=false`。实测印证:`fleetItemIndex=1` 或 `2` 时三天全部 `selectable=false`,冲突体正是本单自己。 +**新增槽位(index=2)时,前端仍把 slot0 的 `assignmentId` 当 `excludeAssignmentId` 传。** 后端判定槽位上下文不一致后**静默放弃排除**(不报错、如实展示占用),于是本单自己的占用被算成阻断冲突 → 该车/司机在新槽位下 `selectable=false`。实测:`fleetItemIndex=1` 或 `2` 时三天全部 `selectable=false`,冲突体正是本单自己。 -**影响**:新增的槽位**永远拿不到候选证据**,是本次问题的同源二次伤害。 +**影响**:新增槽位**永远拿不到候选证据**——与本条是同一个死结的两端。 -**前端改法**:新增槽位时不要沿用其它槽位的 `assignmentId`,该槽无既有派车行就**不传** `excludeAssignmentId`。 +**前端改法**:新增槽位时不要沿用其它槽位的 `assignmentId`;该槽没有既有派车行就**不要传** `excludeAssignmentId`。 -> 后端侧会考虑加固(候选入参支持按槽位排除,而不是靠反查单行推断槽位身份),届时另发接口类 changelog。 +> 后端侧会加固(候选入参支持按槽位排除,不再靠反查单行推断槽位身份),届时另发接口类 changelog。 --- @@ -290,4 +284,4 @@ slot.selectionReady = cells.every(isDailyVehiclePlanCellComplete) - 后端工单 #5849 - `selectable` 字段口径:`11_5850_候选接口显式返回selectable-修改接口-管理后台.md` -- 换版后车型陈旧快照(会让候选列表误标「需求不匹配」):#5871,修复后另发 +- 换版后车型陈旧快照(候选列表误标「需求不匹配」):#5871,修复后另发