21 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7994 | 团期配车受控重开窗口:分组列上线前的历史派车行现在可以被收编,不再让团期卡死 | admin | wx(GIT) | 修改接口 | deployed | verified | verified | mmg | 2044ec5281e0eb4a0fb3b1567a92e73916bd4204 | 2026-09-21 | gateway_status=verified 的判据:2026-09-21 经测试服网关 https://api.test.1814.love:9443 对团期 2099959465330556929 实跑了完整四步(重开窗口 → 重配 → 确认配车 → 确认需求),全部 200,三行历史派车行的分组由空写成 GC、团期车务就绪位由 0 置 1,逐步请求/响应已记入工单 #7994 AC-3。backend_status=deployed 有两条互相独立的判据:①行为自证——这四步在本次修复之前必然报 602013(工单背景节实测过两次),能走通本身就说明跑着的字节里含本次修复;②测试服上 /opt/hulalv/jars/hl-fleet-service-1.0.0-SNAPSHOT.jar 的 mtime 是 2026-09-21 10:18:22,晚于本次修复合入 dev-v3 的 09:47:35。🔴 顺带订正一条会误导人的登记值:另一条取证线记录的 fleet 部署点 51571c58a 与上述两条判据矛盾(51571c58a 的提交时刻是 04:25,且本次修复不是它的祖先)——那是 10:18 重滚之前的陈旧读数,别再据它判断「某修复还没上测试服」。⚠️ 未覆盖:团期状态推进(本次只解开死路,未推进 batch_status);计划刷新是同步还是异步未区分(两者终态相同)。 | 2026-09-21 | dev-v3 |
团期车务:受控重开窗口内,无分组的历史派车行可以被收编(工单 #7994)
存放目录: 二期(order-v3 标签工单)→
changelogs-v2/2026-09/服务: hl-fleet-service(8087) PR: #8085 Issue: #7994 日期: 2026-09-21 影响范围: 管理后台「团期配车页」——受控重开窗口内的重新配车操作,以及它报错时的提示文案
⚠️ 关键变化
- 请求体和响应体的字段一个都没变,前端不需要改接口对接代码。变的是两件事:①一类原先必定被拒的请求现在会成功;②被拒时的提示文案变长了,且新文案里带着操作指引。
- 🔴 唯一需要前端确认的一点:
602013的提示文案现在可能很长,必须完整展示给车务,不能截断、不能只显示前一行。 新增的那半句正是告诉车务「该怎么自救」的部分,截掉了这条路就没人找得到(详见「三、接口详情 → 错误响应」)。 - 这次改动没有放松任何权限或范围校验:窗口授权范围以外的分组、以外的日期,行为一字未变,仍然整批拒绝且零写入。
一、背景
「分组」这一列是 2026-09-17 才加到团期派车记录上的。在那之前排的车,记录上没有分组——不是脏数据,是那个事实当时就不存在。
车务给一个团期开「受控重开窗口」重新配车时,系统要检查每一行改动有没有越出窗口授权的范围。此前的规则是:没有分组的历史行一律判越界(说不出它属于哪个组,就不敢让窗口里的操作动它)。
这条规则本身没错,但它和另一条规则撞上了:团期进入「物资准备」阶段后,不开窗口就不许配车。两条一叠加,一个带历史派车行的团期就成了单向死路:
| 车务的走法 | 结果 |
|---|---|
| 开窗口后重新配车 | 拒绝:「本次配车改动越出重开窗口授权范围」(602013) |
| 不开窗口直接配车 | 拒绝:「团期当前状态不可配车: 物资准备中」(600010) |
| 跳过配车直接确认 | 拒绝:「整组未排车」(602008,历史行不算数) |
而开窗口这个动作本身会把团期的车务就绪位清零,于是团期再也过不了发团门禁——出不了团,也修不回去。测试环境实际撞上这个状态的团期有 1 个。
改后的判定口径:按「这一行会被怎么处理」分,而不是按「它属于哪个组」分
没有分组的历史行,在一次重新配车里只有两种下场,风险完全不对称:
| 下场 | 什么时候发生 | 改后 |
|---|---|---|
| 被收编——这一行被写上分组 | 车务在本次请求里带上了同一辆车、同一天,并给了分组 | ✅ 放行 |
| 被删除——这一行被撤掉、占用被释放 | 车务在本次请求里没写这辆车这一天 | ❌ 仍然拒绝(602013),一字未变 |
放行为什么是安全的:收编时写进去的那个分组,本身仍要经过窗口授权范围的检查。想把历史行收编进一个没被授权的分组,一样会被拒。⇒ 历史行永远不可能被写进未授权的分组,也永远不可能被窗口删掉。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 整团逐日配车提交(重新配车) | POST | /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure |
行为放宽 + 错误提示文案变化 | 请求体/响应体结构零变化 |
三、接口详情
1. 整团逐日配车提交 POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure
VO: GroupDispatchReconfigureReqVO → GroupDispatchReconfigureRespVO(字段无增删改)
使用场景
管理后台「团期配车页」,车务在受控重开窗口有效期内提交整团逐日的配车安排。本次改动只影响**团期带有「分组列上线前的历史派车行」**这一种情形;不带历史行的团期,行为逐字不变。
入参(本次无变化)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | path | Long | 是 | — | 团期主订单 ID |
| requirementId | body | Long | 是 | 须等于基线当前活跃需求 | 正式团级用车需求 ID,落后抛 602005 |
| requirementVersion | body | Integer | 是 | 须等于基线当前版本 | 需求版本,落后抛 602005 |
| clearAll | body | Boolean | 否 | 默认 false | 显式整团清零标志 |
| reconfigureWindowToken | body | String | 条件必填 | 团期已过资源准备阶段时必填 | 受控重开窗口令牌;缺失/不匹配/过期抛 602012 |
| survivorPolicy | body | String | 条件必填 | clearAll=true 且存在 active 共用关系时必填 | 幸存共用派单处置策略 |
| demands | body | Array | 条件必填 | clearAll=false 时必填 | 逐日配车需求列表 |
| demands[].tripDate | body | LocalDate | 是 | yyyy-MM-dd |
行程日期 |
| demands[].assignments | body | Array | 是 | 非空 | 当日排车项列表 |
| demands[].assignments[].groupId | body | String | 是 | 非空白;须在窗口授权范围内 | 乘车分组键(= 需求侧 group_code) |
| demands[].assignments[].vehicleId | body | Long | 是 | — | 派出车辆 ID |
| demands[].assignments[].driverId | body | Long | 否 | 可空 = 仅排车未排司机 | 派出司机 ID |
| demands[].assignments[].remark | body | String | 否 | — | 备注 |
🔴 收编一条历史行的做法就落在这张表里,没有任何新字段:把历史行所在的「tripDate + vehicleId」原样写进 demands,并给它一个 groupId。
出参(本次无变化,字段来源:GroupDispatchReconfigureRespVO 的 @ApiModelProperty 声明)
| 字段 | 类型 | 说明 |
|---|---|---|
| groupBatchId | Long | 团期主订单 ID |
| requirementId | Long | 正式团级用车需求 ID |
| requirementVersion | Integer | 正式团级用车需求版本 |
| planVersion | Long | 团期计划版本 |
| addedCount | Integer | 新增派车记录数 |
| removedCount | Integer | 软删派车记录数 |
| keptCount | Integer | 保留未变派车记录数 |
| updatedCount | Integer | 就地更新派车记录数——收编走的就是这一格 |
| aliveCount | Integer | 存活派车记录总数 |
| addedDispatchIds | List<Long> | 新增派车记录主键列表 |
| idempotentShortCircuit | Boolean | 本次是否被计划去重短路(幂等成功,非失败) |
| coverage | Object | 按乘车分组的覆盖明细,含 groups / missingGroupCodes / wholeBatchSatisfied |
| legacyGroupRowCount | Integer | 无分组键的历史派车行数(非错误,仅留痕)——这一格就是本次改动针对的那类行 |
| releasedShareGroupIds | List<Long> | 本次连带解除的共用关系 ID 清单 |
| keptSourceIds | List<Long> | 保留占用的 claim 来源 ID 清单 |
| releasedSourceIds | List<Long> | 占用已被真正释放的派单 ID 清单 |
| pendingReassignSourceIds | List<Long> | 待人工改派的派单 ID 清单(占用已释放、当前无车) |
请求示例
收编三行历史派车行(同一辆车、三个连续日期),给它们分组 GC:
{
"requirementId": 2099959465330556930,
"requirementVersion": 5,
"clearAll": false,
"reconfigureWindowToken": "<重开窗口返回的令牌>",
"demands": [
{
"tripDate": "2026-11-27",
"assignments": [
{ "groupId": "GC", "vehicleId": 2064995255698010113, "driverId": 2065272145289658370 }
]
},
{
"tripDate": "2026-11-28",
"assignments": [
{ "groupId": "GC", "vehicleId": 2064995255698010113, "driverId": 2065272145289658370 }
]
},
{
"tripDate": "2026-11-29",
"assignments": [
{ "groupId": "GC", "vehicleId": 2064995255698010113, "driverId": 2065272145289658370 }
]
}
]
}
响应示例
{
"code": 0,
"msg": "操作成功",
"data": {
"groupBatchId": 2099959465330556929,
"planVersion": 2,
"addedCount": 0,
"removedCount": 0,
"keptCount": 0,
"updatedCount": 3,
"aliveCount": 3,
"addedDispatchIds": [],
"idempotentShortCircuit": false,
"coverage": {
"missingGroupCodes": [],
"wholeBatchSatisfied": true
},
"legacyGroupRowCount": 0
}
}
⚠️ 上例中只有 updatedCount / addedCount / removedCount / wholeBatchSatisfied 四项是 2026-09-21 实测读数(见「八、测试环境已验证」);其余字段按其语义填的示意值,且为便于阅读省略了 coverage.groups 与几个空数组字段——不要拿它当字段全集,字段全集以上面的出参表为准。
📌 收编是就地更新,不是删掉重建:三行历史行被收编后 updatedCount=3 / addedCount=0 / removedCount=0,派车记录的 id 不变。前端若按派车记录 id 做过本地缓存或选中态,不会失效。
空数据 / 降级响应
- 团期一条派车行都没有:
aliveCount=0、coverage.wholeBatchSatisfied=false、coverage.missingGroupCodes列出缺的组码;这是正常响应不是错误。 - 重复提交同一份计划(超出 10 秒防重窗口):正常受理并返回
idempotentShortCircuit=true,addedCount/removedCount/updatedCount全 0——那是成功(计划未变、未落库),前端不要按错误提示。 - 10 秒内重复提交:被防重窗口拒绝,提示「团期配车重配处理中,请勿重复提交」,前端按「稍后重试」处理。
- 以上三种在本次改动中行为一字未变。
错误响应
602013 的 message 此前只有越界项清单。改后,当越界项里含「(无分组历史行)」时,末尾会追加一段操作指引:
{
"code": 602013,
"msg": "本次配车改动越出重开窗口授权范围: (无分组历史行):2026-11-27,(无分组历史行):2026-11-28; 其中 (无分组历史行) 是分组列上线前的历史派车行, 本次请求没有它因而会被删除; 窗口内不允许删除它, 请把该日期该车一并写进本次配车请求并给出正确的乘车分组, 即可将其收编",
"data": null
}
⇒ 🔴 请确认「团期配车页」的错误提示区能完整展示这段文字(多行换行展示即可,不要单行截断、不要因为 tooltip 放不下就省略)。这段话是车务自救的唯一入口。
📌 越界项里不含「(无分组历史行)」时(即普通的分组越界、日期越界),message 与改前逐字节相同,不会变长:
{
"code": 602013,
"msg": "本次配车改动越出重开窗口授权范围: GA:2026-11-27",
"data": null
}
错误码清单(本次不新增、不删除任何错误码):
| code | 说明 | 本次是否变化 |
|---|---|---|
| 602013 | 本次配车改动越出重开窗口授权范围 | 触发条件收窄;含历史行时文案追加指引 |
| 602012 | 重开窗口令牌缺失/不匹配/已过期 | 不变 |
| 602011 | 窗口内不允许整团清空 | 不变 |
| 602008 | 整组未排车 | 不变(但历史行被收编后不再误报) |
| 602005 | 需求身份落后 | 不变 |
| 602000 | 配车请求缺少乘车分组 | 不变 |
| 600010 | 团期当前状态不可配车 | 不变 |
| 600006 / 600007 | 车辆/司机当天已被占用 | 不变 |
业务边界
- 只有**同时满足「同一行程日 + 同一车辆」**的历史行才会被收编;只对上日期或只对上车辆都不算,仍按删除处理并拒绝。
- 收编时给的
groupId照样要过窗口授权范围校验:不在范围内仍返 602013,且零写入。 - 本次改动不触碰窗口授权范围本身的计算,也不触碰「窗口内不允许整团清空」(602011)。
- 一次请求里可以同时收编多行;三行历史行在一次请求里全部收编是实测过的形态。
- 收编不改变派车记录的主键,也不产生新的派车记录。
四、契约约束与正确调用方式
✅ 正确 / ❌ 错误 的应对方式
遇到「越出重开窗口授权范围」且提示里出现「(无分组历史行)」时:
| 做法 | 结果 | |
|---|---|---|
| ❌ | 去找管理员重开一个范围更大的窗口 | 没用。窗口里写什么分组都授权不了「没有分组」的行——这条路改前改后都走不通 |
| ✅ | 把提示里点名的那个日期、那辆车一并写进本次 demands,并给它一个正确的乘车分组 |
该行被就地收编,计入 updatedCount |
前端需要做什么
| 事项 | 是否需要改 |
|---|---|
| 请求体字段 | ❌ 不用改 |
| 响应体字段 | ❌ 不用改 |
| 错误码分支 | ❌ 不用改(没有新错误码) |
| 602013 提示文案的展示 | ✅ 确认能完整展示长文案,不截断 |
| 页面文案/引导 | 选做:若「团期配车页」有配车失败的帮助文案,可以补一句「提示里出现『无分组历史行』时,把它点名的日期和车辆一并加进本次配车即可」 |
前端已交付(mmg 2026-09-21, hl-admin 2044ec52):配车计划编辑器(GroupDispatchPlanEditor)提交失败时若 message 含「无分组历史行」,在编辑器内常驻 n-alert 完整展示该指引(自然换行不截断),再次提交自动清空;其余错误仍走拦截器 toast。请求/响应字段零变化,对接代码未动。editor spec 6 例全过(长文案常驻展示+普通失败不出常驻块),checkpoint 通过。 |
五、数据库行为
- 收编走的是就地 UPDATE:派车记录表
fleet_group_dispatch中被收编的行,group_id由 NULL 写成请求里给的分组码,version+1,主键dispatch_id不变,不产生新行、不软删旧行。 - 实测三行(2026-11-27/28/29,同车同司机):
group_idNULL →GC、statusASSIGNED → CONFIRMED(第 3 步确认配车所致)、version0 → 1。 - 被拒绝时(602013)零写入——这一点改前改后相同,本次未放松。
- 无新增表、无新增列、无 Flyway 迁移。
六、边界行为
| 情形 | 改前 | 改后 |
|---|---|---|
历史行所在的「日期 + 车辆」出现在本次 demands 里 |
602013 | ✅ 成功,该行分组被写成请求里给的值,计入 updatedCount |
历史行所在的「日期 + 车辆」没有出现在本次 demands 里 |
602013 | 602013(不变) |
| 收编时给的分组不在窗口授权范围内 | 602013 | 602013(不变) |
| 团期里没有任何无分组历史行 | 按窗口授权范围判 | 完全不变(越界集合与错误文案两侧都逐字节相同) |
| 普通的分组越界 / 日期越界 | 602013 | 602013,文案逐字节不变 |
六.6、修改前后对比
字段级对比
无任何字段变化——请求体、响应体、错误信封三处的字段名、类型、层级全部与改前一致。这也是本篇不要求前端改对接代码的原因。
行为级对比
| 维度 | 改前 | 改后 |
|---|---|---|
| 判定依据 | 这一行属于哪个组(没有组 ⇒ 一律越界) | 这一行会被怎么处理(被收编 ⇒ 放行;被删除 ⇒ 越界) |
| 带历史行的团期 | 单向死路,出不了团也修不回 | 车务可自行收编后继续走确认流程 |
| 602013 文案 | 只有越界项清单 | 含历史行时追加操作指引;不含时逐字节不变 |
六.7、影响评估
- 前端:只有一处——602013 长文案的展示。无字段改动、无错误码改动。
- 后端:仅 hl-fleet-service 一个服务,改动落在受控重开窗口的范围校验与错误文案两处。
- 数据:不需要任何存量数据订正。历史行被收编是车务在页面上的正常操作,不需要刷库。
- 回归面:不带无分组历史行的团期,越界判定与错误文案两侧结构性不可达本次改动(非空分组码走的仍是改前那条判断,一字未改),且该类的 10 条既有单测一字未动全绿。
七、不影响范围
显式声明没有被这次改动碰到的东西,帮前端/QA 缩小排查面:
- 整团清空(
clearAll=true)与survivorPolicy的处理:未碰。 - 窗口授权范围本身怎么算出来的:未碰。
- 602012(令牌)、602011(窗口内禁清空)、602005(需求身份落后)、600010(团期状态不可配车)、600006/600007(车/人被占)的触发条件:全部未碰。
- 共用关系(share-group)的建立、解除、收缩:未碰。
- 权限点
fleet:group-dispatch:write/fleet:group-dispatch:view:未碰。 - 防重窗口与幂等短路(
idempotentShortCircuit):未碰。 - 团期状态机的推进:未碰——本次只解开死路,
batch_status不因这次改动而变化。
八、测试环境已验证
2026-09-21 经测试服网关 https://api.test.1814.love:9443,对团期 2099959465330556929(三行 2026-11-27/28/29 的无分组历史派车行)走完整链路,四步全部 200:
| # | 动作 | 结果 |
|---|---|---|
| 1 | 重开受控窗口 | 200,拿到新窗口令牌 |
| 2 | 带窗口重新配车(请求里带上那辆车 + 三天 + 分组 GC) |
200,updatedCount=3 / addedCount=0 / removedCount=0,wholeBatchSatisfied=true |
| 3 | 确认配车 | 200,confirmedCount=3(改前此处报 602008「整组未排车」) |
| 4 | 确认团期车务需求 | 200,需求状态转 CONFIRMED |
三行派车记录的分组由空变为 GC,团期车务就绪位由 0 置 1,单向死路解除。
⚠️ 取证中踩到的一个坑(与本次改动无关,但会影响联调):以 ADMIN 角色调 /admin/fleet/** 全部端点返 403「无权限访问车务管理」,必须切到车务角色或超级管理员。这是既有的路径级角色门禁,不是本次引入的。
⚠️ 本次未覆盖:团期状态推进(本次只解开死路,batch_status 仍是物资准备中);「计划刷新」是同步完成还是极快的异步消费,两者终态相同,本次没有做间隔采样因而分不出来——这只影响时序假设,不影响上表任何一行结论。
九、相关历史 PR
| PR | 说明 |
|---|---|
| #8085 | 本次修复(工单 #7994) |
| 工单 #7442 | 分组列与受控重开窗口的来源——本次要修的死路正是这两条规则叠加出来的 |
十、相关文档
- 工单 #7994(含逐条验收取证与本次改动的结构性论证)
- 工单 #7442(分组列 + 受控重开窗口的原始需求)
- 同期同服务的相邻变更:
20_8051_解除共用关系不再跨服务日跨团误清派车行-修改接口-管理后台.md、20_8061_解除共用关系只清成员占用同槽非成员不动-修改接口-管理后台.md
关联 / 联系人
链接
- Issue: #7994
- PR: #8085
- 服务: hl-fleet-service(8087)
- 网关:
https://api.test.1814.love:9443
联系人
- 后端: wx
- 前端(管理后台): mmg