hl-api-changelog/changelogs-v2/2026-08/11_frontend_派单Step2下一步置灰与日格未完成误判-前端缺陷-管理后台.md
API Changelog Bot 45ea603be0
所有检测均成功
changelog-filename-gate / validate (push) Successful in 2s
docs(changelog): #5849 前端补完——两行最小改动、立即绕过办法、新增槽位 excludeAssignmentId 误传
2026-08-11 18:37:55 +08:00

15 KiB

schema, ticket, title, consumer, change_type, author, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer change_type author backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 frontend 派单 Step2「下一步·发送给司机」置灰后端实测判定可用可选无冲突,日格「未通过候选校验」为前端侧误判 admin 前端缺陷 wx(GIT) not_required not_required pending mmg 对应后端工单 #5849。根因已定位并在测试服部署包核实日格「已完成」要求一枚客户端候选证据戳 _candidateEvidence,而从服务端快照重建的既有派车日格该戳恒为 null,唯一逃生口 preservedFinalized 又被 #5851「换版即作废定稿」关闭,导致换版后每个日格都必须重新走一遍选车弹窗才能推进。后端侧已实测排除8/18 返回 available/selectable 均为 true、conflicts 为空)。后端若需为前端补推进态字段,另发接口类 changelog。 2026-08-11 dev-v3

派单 Step2「下一步」置灰 —— 后端侧已排除(#5849 前端部分)

页面:车务管理 → 派单看板 → 订单派车弹窗 → Step2「排车」 后端:本条无后端改动。下面全部是测试服实测结论,用于把排查范围收敛到前端。


一、现象

订单 26-369808/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"
}

响应中该车(节选,原样

{
  "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 结果
组锚点 id2086270138708844545 available: true / selectable: true
该日行真实 id345501044920422400 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.jsAssignModal-BWuKA9uB.js 中逻辑与源码一致。

3.1 判定链路

src/views/fleet/board/utils/daily-vehicle-plan.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
从服务端快照重建日格(打开弹窗回显既有派车) nullutils/canonical-assignment-snapshot.js 约 :134,部署包同构
新建槽位的空白格 nullAssignModal.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 按钮与两个读数的完整链路(供定位)

// 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 全程零 HTTPuseAssignFlow.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。这样"这一天你还没重新验过"的信息仍然可见,但不再卡住流程。

另一个更彻底的方向(可与上面二选一)

从服务端快照重建日格时,用服务端数据补齐 _candidateEvidenceserviceDate / 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 的 assignmentIdexcludeAssignmentId。后端判定上下文不一致后静默放弃排除(不报错,如实展示占用),于是本单自己的占用被算成阻断冲突 → 该车/该司机在新槽位下 selectable=false。实测印证:fleetItemIndex=12 时三天全部 selectable=false,冲突体正是本单自己。

影响:新增的槽位永远拿不到候选证据,是本次问题的同源二次伤害。

前端改法:新增槽位时不要沿用其它槽位的 assignmentId,该槽无既有派车行就不传 excludeAssignmentId

后端侧会考虑加固(候选入参支持按槽位排除,而不是靠反查单行推断槽位身份),届时另发接口类 changelog。


八、关联

  • 后端工单 #5849
  • selectable 字段口径:11_5850_候选接口显式返回selectable-修改接口-管理后台.md
  • 换版后车型陈旧快照(会让候选列表误标「需求不匹配」):#5871,修复后另发