From 043a7798204b9ffea9563cfb5be4c4815d80a20d Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 21 Sep 2026 11:07:36 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#7994=20=E5=8F=97=E6=8E=A7?= =?UTF-8?q?=E9=87=8D=E5=BC=80=E7=AA=97=E5=8F=A3=E5=86=85=E6=97=A0=E5=88=86?= =?UTF-8?q?=E7=BB=84=E5=8E=86=E5=8F=B2=E6=B4=BE=E8=BD=A6=E8=A1=8C=E5=8F=AF?= =?UTF-8?q?=E8=A2=AB=E6=94=B6=E7=BC=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 团期配车重新配车接口:分组列上线前的历史派车行,此前无论被改还是被删 都判越界(602013),叠加「物资准备期不开窗口不许配车」后形成单向死路。 改后按动作分两路——被同键收编则放行,被删除仍判越界。 请求体/响应体零变化;602013 在含无分组历史行时追加自救提示, 前端需确认该文案能完整展示不截断。 Co-Authored-By: Claude Opus 5 (1M context) --- ...£内无分组历史派车行可被收编-修改接口-管理后台.md | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 changelogs-v2/2026-09/21_7994_受控重开窗口内无分组历史派车行可被收编-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/21_7994_受控重开窗口内无分组历史派车行可被收编-修改接口-管理后台.md b/changelogs-v2/2026-09/21_7994_受控重开窗口内无分组历史派车行可被收编-修改接口-管理后台.md new file mode 100644 index 00000000..54fa812b --- /dev/null +++ b/changelogs-v2/2026-09/21_7994_受控重开窗口内无分组历史派车行可被收编-修改接口-管理后台.md @@ -0,0 +1,376 @@ +--- +schema: "hl-changelog/v2" +ticket: "7994" +title: "团期配车受控重开窗口:分组列上线前的历史派车行现在可以被收编,不再让团期卡死" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-21" +status_note: "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);计划刷新是同步还是异步未区分(两者终态相同)。" +updated_at: "2026-09-21" +base: "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`: + +```json +{ + "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 } + ] + } + ] +} +``` + +#### 响应示例 + +```json +{ + "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` 此前只有越界项清单。改后,**当越界项里含「(无分组历史行)」时**,末尾会追加一段操作指引: + +```json +{ + "code": 602013, + "msg": "本次配车改动越出重开窗口授权范围: (无分组历史行):2026-11-27,(无分组历史行):2026-11-28; 其中 (无分组历史行) 是分组列上线前的历史派车行, 本次请求没有它因而会被删除; 窗口内不允许删除它, 请把该日期该车一并写进本次配车请求并给出正确的乘车分组, 即可将其收编", + "data": null +} +``` + +⇒ 🔴 **请确认「团期配车页」的错误提示区能完整展示这段文字**(多行换行展示即可,不要单行截断、不要因为 tooltip 放不下就省略)。这段话是车务自救的唯一入口。 + +📌 越界项里**不含**「(无分组历史行)」时(即普通的分组越界、日期越界),`message` 与改前**逐字节相同**,不会变长: + +```json +{ + "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 提示文案的展示** | ✅ **确认能完整展示长文案,不截断** | +| 页面文案/引导 | 选做:若「团期配车页」有配车失败的帮助文案,可以补一句「提示里出现『无分组历史行』时,把它点名的日期和车辆一并加进本次配车即可」 | + +--- + +## 五、数据库行为 + +- 收编走的是**就地 UPDATE**:派车记录表 `fleet_group_dispatch` 中被收编的行,`group_id` 由 NULL 写成请求里给的分组码,`version` +1,**主键 `dispatch_id` 不变**,不产生新行、不软删旧行。 +- 实测三行(2026-11-27/28/29,同车同司机):`group_id` NULL → `GC`、`status` ASSIGNED → CONFIRMED(第 3 步确认配车所致)、`version` 0 → 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