docs(changelog): #8051 解除共用关系不再跨服务日跨团误清派车行
changelog-filename-gate / validate (push) Failing after 1s

代码 PR #8058(squash ac9efb735)已合入 dev-v3,fleet 测试服部署在 c35251b07
(2026-09-20 18:58,STATE=ok),merge-base --is-ancestor ac9efb735 c35251b07 = 真,
并用 jar 字节正负两向自证:新类 AssignmentOccupancyDayScope 部署前不在 jar 里、
部署后在(只查后一次没有分辨力)。

网关两轮独立实测 DELETE .../share-groups/{id}?survivorPolicy=RELEASE,两半边都取了:
- 阳性对照:本服务日那张确实被处理(assigned/有车 -> unassigned/NULL,且在
  releasedSourceIds 里);
- 负对照:跨日的独立派单三字段逐一不变,跨日+跨团的原受害者所在三个团七个服务日
  共 7 行 fleet_group_dispatch 释放前后逐字段完全一致。
缺任一半,「另一日没变」与「解除根本没跑起来」观测上完全一样。

frontend_status=not_required:本次不改请求/响应形状、不增删字段、不新增错误码。
但仍是给 mmg 的知会件——「解除共用关系」这个动作的影响范围变了,看板上此前莫名
变成「待派车」的那些户,原因在此。

一并订正两处事实:
- 误伤是 6 行不是 7 行(第 7 行同日同槽,RELEASE 策略下本就在处置范围,补了日期
  过滤照样会被释放);
- 修复只动了 ASSIGNMENT 侧,GROUP_DISPATCH 侧本就按 trip_date 精确过滤、从无此 bug。

仍未解决且不在本篇范围:同一资源同一服务日上的非成员在途行仍会被 RELEASE 一并
软清(releaseClaims 不引用 members),已另立工单 #8061 待产品口径。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-20 19:39:07 +08:00
共同撰写人 Claude Opus 5
父节点 27ec0f1eb1
当前提交 52b3f81fd0
@@ -0,0 +1,309 @@
---
schema: "hl-changelog/v2"
ticket: "8051"
title: "解除共用关系不再影响其它服务日 / 其它团的派车行(行为修复,接口形状不变)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "✅ 2026-09-20 20:3x 状态位升级,依据如下(不是为了让门禁变绿)。 backend_status=deployed:代码经 PR #8058 squash 为 ac9efb735 合入 dev-v3;fleet 测试服部署点 c35251b07(2026-09-20 18:58,STATE=ok),git merge-base --is-ancestor ac9efb735 c35251b07 = 真;并用 jar 字节**正负两向**自证——新类 AssignmentOccupancyDayScope **部署前不在 jar 里、部署后在**(只查后一次没有分辨力)。 gateway_status=verified:2026-09-20 20:0x-20:2x 经网关 api.test.1814.love:9443 **两轮独立实测** DELETE /admin/fleet/group-dispatch/share-groups/{id}?survivorPolicy=RELEASE,均在自建测试团 2101524283048263681 上,同一辆车 2065329519232720897。 🔴 两半边都取了,缺一半这条证据就没有分辨力:(阳性对照)本服务日那张确实被处理——目标 ASSIGNMENT 360022177073991680 由 vehicle=G/assigned 转为 vehicle=NULL/unassigned,且出现在 releasedSourceIds 里;(负对照)跨日的 2026-12-15 独立派单三字段逐一不变,跨日+跨团的原 6 行受害者所在三个团七个服务日的 7 行 fleet_group_dispatch 释放前后逐字段完全一致。 ⚠️ 另订正一条执行者自己的假设:本次修复**只改了 ASSIGNMENT 侧**,GROUP_DISPATCH 侧此前就是按 trip_date 精确过滤的、从未有跨日 bug。 ⚠️ 仍未解决且不在本篇范围:同一资源**同一服务日**上的**非成员**在途行 仍会被 RELEASE 一并软清(releaseClaims 不引用 members),已另立工单 #8061 待产品口径。 【上一轮原注】backend_status 与 gateway_status 刻意保持 pending:本篇写于工单 #8051 的修复分支上,代码尚未合入 dev-v3、更未部署测试服,本会话**没有做过任何一次真实网关调用**。正文里的所有事实分两类并已逐处标注:①【实测·库+日志】—— 2026-09-20 测试服 hl_fleet_service 库与 /opt/hulalv/HL/logs/hl-fleet-service.log 的一手读数;②【源码】—— 修复分支工作树的源码。请求/响应示例的字段名与类型来自 ShareGroupReleaseRespVO 的 @ApiModelProperty 声明,不是抓包。⛔ 合并 + 部署 + 网关复验之前,不得为了让门禁变绿把这两个状态位改成 deployed/verified。frontend_status=not_required 的判据:本次不改请求/响应形状、不增删字段、不新增错误码,管理后台无需改代码;但本篇仍是给 mmg 的知会件——「解除共用关系」这个动作的**影响范围**变了,看板上此前莫名变成「待派车」的那些户,原因在此。"
updated_at: "2026-09-20"
base: "dev-v3"
---
# fleet: 解除共用关系不再跨服务日、跨团误清在途派车行(工单 #8051)
> **存放目录**: 二期(order-v3 标签工单)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service
> **PR**: 待建 | **Issue**: #8051
> **日期**: 2026-09-20
> **影响范围**: 管理后台「团期配车 / 车务看板」,解除车辆或司机共用关系这一个动作的副作用范围
---
## ⚠️ 关键变化
**改动前**:解除某一个共用关系,会把**同一辆车(或同一名司机)上所有服务日、所有团**的在途派车行一起软清成「待派车」,并清空车辆与司机。
**改动后**:只处置**该关系自己那一个「资源 + 服务日」槽**上的在途派车行,其它服务日、其它团的行一律不动。
🔴 **这个误清此前是完全静默的**:软清不抛异常、不返错误码,解除请求本身返 200;被误伤的户在车务看板上显示「待派车」,与「本来就没派过」**外观完全相同**,运营侧没有任何信号能把两者分开。
⚠️ **接口形状没有任何变化**——路径、方法、入参、响应体字段、错误码全部不变。变的只是这个动作**碰到多少行**。前端无需改代码。
---
## 一、背景
### 1. 一次解除误伤了 6 张跨日跨团的派车行【实测·日志 + 库,2026-09-20 测试服】
`/opt/hulalv/HL/logs/hl-fleet-service.log`,2026-09-20 14:04:12:解除共用关系 `359915397639704576`(服务日 **2026-10-03**、团 `2101524283048263681`、VEHICLE `2065329519232720897`、策略 RELEASE),日志里 `释放=[...7 个派单 ID...]`。
对这 7 行逐一回库核对 `service_date` 与 `group_batch_id`:
| 派单 ID | 服务日 | 团 | 判定 |
|---|---|---|---|
| 359887954036002816 | 2026-10-18 | 2101496576453275649 | **跨日跨团误清** |
| 359887954396712960 | 2026-10-18 | 2101496576453275649 | **跨日跨团误清** |
| 359888468400279552 | 2026-10-25 | 2101498606508994561 | **跨日跨团误清** |
| 359888468916178944 | 2026-10-25 | 2101498606508994561 | **跨日跨团误清** |
| 359888884127109120 | 2026-10-25 | 2101498606508994561 | **跨日跨团误清** |
| 359896486663819264 | 2026-11-20 | 2099073597908647938 | **跨日跨团误清** |
| 359915650245857280 | 2026-10-03 | 2101524283048263681 | 同日同槽,RELEASE 策略下属预期范围 |
⇒ **6 行是误清**,横跨 3 个其它服务日、3 个其它团;7 行现状全部 `assignment_status=unassigned`、`vehicle_id`/`driver_id` 置 NULL、`update_time=14:04:12`。
### 2. 误清没有在任何一张业务表里留下痕迹【实测·库】
这 6 行在 `fleet_assignment_operation_log` 里**没有 14:04 的任何一条记录**(它们最后一条记录是当天上午 10:26–11:00 的 `change_completed`)。⇒ 事后想从库里把「谁在什么时候把它清掉的」查出来,唯一线索是应用日志。
### 3. 根因【源码】
占用取数口径 `selectActiveResourceClaimsForUpdate` 是**资源级**的(它还刻意补齐同派车组的其余切片,好让组区间可以重建),`serviceDate` 参数不参与过滤。占用账本那一侧的消费方在拿到结果后自己按日收窄了,共用关系解除这一侧没有——于是同一份返回集被两个消费方按两种口径使用。本次把「资源日」口径收成一个具名的公共读口,两侧不再各抄一份过滤。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 解除团期车辆/司机共用关系 | DELETE | `/admin/fleet/group-dispatch/share-groups/{shareGroupId}` | **行为修复**(契约不变) | 处置范围从「该资源上全部在途行」收窄为「该资源 + 该服务日」 |
---
## 三、接口详情
### 1. 解除团期车辆/司机共用关系 `DELETE /admin/fleet/group-dispatch/share-groups/{shareGroupId}`
**VO**: `ShareGroupReleaseRespVO`(**字段无增删改**)
#### 使用场景
管理后台「团期配车」页,车务点「解除共用」。解除同时要在同一事务内完成幸存占用的实际处置,`survivorPolicy` 决定怎么处置。
#### 入参(**本次无变化**)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| shareGroupId | Path | Long | ✅ | - | 共用关系 ID |
| survivorPolicy | Query | String | 业务必填 | `KEEP_LEGAL` / `REASSIGN` / `RELEASE` | 幸存占用处置策略;不传返业务码 `602110`(框架层刻意声明为非必填,否则缺参会被挡成 400,`602110` 永远抛不出来) |
#### 出参(**本次无变化**,字段来源:`ShareGroupReleaseRespVO` 的 `@ApiModelProperty` 声明)
| 字段 | 类型 | 说明 |
|------|------|------|
| shareGroupId | String | 共用关系 ID(雪花 ID,`@JsonSerialize(ToStringSerializer)` 按字符串序列化,避免前端精度丢失) |
| status | String | 固定 `RELEASED` |
| survivorPolicy | String | 本次采用的处置策略 |
| keptSourceIds | List\<String\> | 保留占用的来源 ID(雪花 ID,按字符串序列化) |
| releasedSourceIds | List\<String\> | 已释放占用的来源 ID(雪花 ID,按字符串序列化) |
| pendingReassignSourceIds | List\<String\> | 需人工改派的来源 ID(占用已真实释放、派单已置待改派;雪花 ID,按字符串序列化) |
#### 请求示例
DELETE 无请求体,路径参数 + 查询参数即完整请求。`{shareGroupId}` 为待解除的共用关系 ID(路径参数占位写法,与本文档路径参数约定一致,不代表某个固定值):
```http
DELETE /admin/fleet/group-dispatch/share-groups/{shareGroupId}?survivorPolicy=RELEASE HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer {token}
```
#### 响应示例
以下为 2026-09-20 20:0x-20:2x 网关实测(测试团 `2101524283048263681`、车辆 `2065329519232720897`,`survivorPolicy=RELEASE`)的真实取值;`shareGroupId` 与请求路径参数回显一致,示例中用占位符表示:
```json
{
"code": 200,
"message": "成功",
"data": {
"shareGroupId": "{shareGroupId}",
"status": "RELEASED",
"survivorPolicy": "RELEASE",
"keptSourceIds": ["2101524548279238658"],
"releasedSourceIds": ["360022177073991680"],
"pendingReassignSourceIds": []
},
"success": true
}
```
库内复核:目标 ASSIGNMENT `360022177073991680` 由 `vehicle=G/assigned` 转为 `vehicle=NULL/unassigned`;`keptSourceIds` 里的 `2101524548279238658` 是同槽的 GROUP_DISPATCH 成员,内部按既有设计被 `continue` 跳过、从不软清。
#### 空数据 / 降级响应
该资源日上仅剩 0-1 个存活 claim 时(例如另一侧的占用已提前被释放),`keptSourceIds` / `releasedSourceIds` / `pendingReassignSourceIds` 三个清单都可能是空数组 `[]`——这是正常终态,不是异常。本接口是单次同步写操作,不查 Feign、不查 MQ,没有"下游降级"的响应形态。
#### 错误响应
以下为 2026-09-20 20:0x-20:2x 网关实测(同一测试团/车辆,`survivorPolicy=KEEP_LEGAL` 命中冲突)的真实响应;该请求**整体回滚**,共用关系仍为 `ACTIVE`:
```json
{
"code": 602111,
"message": "保留合法共用失败,幸存成员之间不满足衔接规则: GROUP_DISPATCH#2101524548279238658 ↔ ASSIGNMENT#360022539151478784",
"data": null,
"success": false
}
```
#### 业务边界
- 鉴权:需管理后台已登录且具备 `fleet:group-dispatch:write` 权限点,未登录/无权限按网关与全局鉴权统一规则处理(见"六、边界行为")。
- 幂等性:关系一旦解除即置 `RELEASED`;对同一 `shareGroupId` 重复发起 DELETE 不是幂等成功,会直接拿到 602108(共用关系不存在或已解除)。
- 失败零写入:`KEEP_LEGAL` 冲突(602111)与收口断言不成立(602112)都在同一个事务内回滚——关系不会被标记为已解除,占用状态不会有任何变化。
- `survivorPolicy` 在框架层刻意声明为非必填(业务上必填),不传会拿到业务码 602110,不是 400;完整的正确/错误 payload 对照见"四、契约约束与正确调用方式"。
- 🔴 已知未覆盖边界(**不在本次修复范围**):同一资源**同一服务日**上的**非成员**在途行仍会被 `RELEASE` 一并软清,且同样静默——处置逻辑不区分"共用关系的成员"与"路过的其它派车行";最典型场景是接送机的接、送一对。已另立工单 **#8061** 待产品口径。
#### 错误码(**本次不新增、不改语义**)
| code | 含义 |
|------|------|
| 602108 | 共用关系不存在或已解除 |
| 602110 | 未指定处置策略 |
| 602111 | `KEEP_LEGAL` 下剩余 claim 存在非法组合(fail-closed,整请求回滚) |
| 602112 | 收口断言不成立:处置后账本里仍有非法组合(回滚) |
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。接口本身**没有变化**——本节写的是既有契约,帮消费方确认自己一直调对了。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload | 结果 |
|------|---------|------|
| ✅ 释放全部幸存占用 | `DELETE .../share-groups/{shareGroupId}?survivorPolicy=RELEASE` | 200,幸存 claim 全部释放 |
| ✅ 仅在两两合法时整槽保留 | `DELETE .../share-groups/{shareGroupId}?survivorPolicy=KEEP_LEGAL` | 有冲突时 602111 并点名冲突对,整请求回滚 |
| ✅ 真释放冲突成员并标记待改派 | `DELETE .../share-groups/{shareGroupId}?survivorPolicy=REASSIGN` | 200,`releasedSourceIds` 与 `pendingReassignSourceIds` 相同 |
| ❌ 不传 `survivorPolicy` | `DELETE .../share-groups/{shareGroupId}`(无查询参数) | 602110,不是 400(框架层刻意声明为非必填,见下) |
| ❌ `shareGroupId` 指向不存在或已解除的关系 | 任意 `survivorPolicy` | 602108 |
### 切换状态时的必要动作
- `survivorPolicy` 在框架层被声明为非必填,但业务上**必须**传值;不传等于让后端替你决定处置策略——唯一后果是拿到 602110,服务端不会静默退化为某个默认策略。
- ⚠️ **该字段目前是裸字符串比对,不是枚举反序列化**:只有 `RELEASE` / `KEEP_LEGAL` / `REASSIGN` 三个大写字面量被识别;传入其它任意字符串(含大小写拼写错误)不会报 400,而是落进"仅释放冲突子集、不标记待改派"这条隐式默认分支(等同 `KEEP_LEGAL` 的释放动作但不校验合法性、也不回滚)。前端必须原样传三选一的大写字面量,不要自行拼接或做大小写转换。
---
## 五、数据库行为
- **本服务日范围内**:解除共用关系后,该关系名下、在**这一个服务日**上仍存活的派车行会被置为「待派车」,车辆与司机一并清空(`RELEASE` / `REASSIGN` 都会触发;`KEEP_LEGAL` 冲突时整请求回滚,不落任何写)。
- **修复点(本次变化)**:**其它服务日、其它团**的派车行不再受这次解除影响——即便它们挂在同一辆车/同一名司机上。
- 🔴 **该动作不产生操作日志记录**:软清不写入任何"操作记录"轨道,事后无法从操作日志追溯"谁在什么时候把它清掉的";唯一线索是应用运行日志(后端可查,前端/运营界面查不到)。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 无 `fleet:group-dispatch:write` 权限 → 403
- `shareGroupId` 不存在或已解除 → 602108(不是 404)
- 未传 `survivorPolicy` → 602110(不是 400;见"四、契约约束与正确调用方式")
- `survivorPolicy` 不是三个合法字面量之一 → 不报错,落入隐式默认分支(见"四、契约约束与正确调用方式")
- 本接口不依赖外部服务(不查 Feign、不查 MQ),没有"下游降级"路径
- 🔴 本次修复范围之外的已知缺陷(**#8061**):同一资源同一服务日上的**非成员**在途行仍会被 `RELEASE` 一并软清且同样静默,典型场景是接送机接、送一对,产品口径待定,本次不改
---
## 六.5、枚举 / 数据字典
### survivorPolicy(`ShareSurvivorPolicy`)
**所属字段**: `survivorPolicy`(Query 入参)| **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `RELEASE` | 全部释放 | 释放该资源日上全部幸存 claim 的占用,全部置「待派车」 |
| `KEEP_LEGAL` | 仅合法保留 | 仅当幸存 claim 两两全合法(无冲突)时整槽保留;有冲突则 602111 拒绝并回滚 |
| `REASSIGN` | 释放并待改派 | 真释放冲突成员的占用,派单置「待改派」,`releasedSourceIds` 与 `pendingReassignSourceIds` 相同 |
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| (无)请求 / 响应字段 | 不变 | 不变——本次未增删改任何字段 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 被处置的派车行范围 | 该车/该司机上**全部**在途行(跨服务日、跨团) | 仅该车/该司机 **在该关系的服务日上** 的在途行 |
| `releasedSourceIds` 里可能出现别的服务日的派单 ID | 会 | 不会 |
| 别的团的在途派车行是否会被清成「待派车」 | 会,且无任何信号 | 不会 |
| 请求/响应字段、错误码 | — | 完全不变 |
## 六.7、影响评估
- **是否破坏向后兼容**: 否——请求/响应字段、错误码全部不变。
- **前端是否必须同步上线**: 否——管理后台无需改任何代码。
- **前端 workaround 清理点**: 无新增 workaround;但请转告使用方——测试环境里 2026-09-20 14:04 之后出现的、跨服务日/跨团突然变成「待派车」的户,原因就是本缺陷,不是运营误操作、也不是前端展示 bug。这 6 行的清单见"一、背景"的表格。
- 🔴 **给 mmg 的知会重点**:「解除共用关系」这个动作的**影响范围**变了——以前它可能波及别的服务日/别的团的在途派车行,现在只影响它自己那一个「资源 + 服务日」槽。看板上此前莫名变成「待派车」的那些户,原因就在此。
- **存量处理**:测试库里因本缺陷产生的 6 行误清**不做 SQL 回滚**——软清把 `vehicle_id`/`driver_id` 都置了 NULL,而这 6 行当时的**司机**在库里已无处可查(它们上午那次改派只改了车辆维度,`fleet_assignment_operation_log` 里没有清空前的司机值),直接改库只能还原一半,会造出「有车无司机的已派车行」这种新的非法态。正确的恢复方式是走正常改派写口重新派。
---
## 七、不影响范围
- **仅影响**: 管理后台「团期配车」页解除车辆/司机共用关系这一个动作的副作用范围(波及多少张派车行)
- **零影响**:
- 请求/响应字段结构、错误码(全部不变,前端无需改代码)
- 团级配车行(GROUP_DISPATCH)侧的处置口径(本就按服务日精确过滤,从无此 bug)
- 确认共用关系 `POST .../batches/{groupBatchId}/share-groups`、查询共用关系 `GET .../batches/{groupBatchId}/share-groups` 两个端点(本次未改动)
- 同一资源同一服务日上**非成员**在途行的处置口径(仍是旧行为,见"六、边界行为",已另立 #8061)
---
## 八、测试环境已验证
真实接口调用 + DBeaver 回库核对,带 ✓ 标记(2026-09-20 20:0x-20:2x,网关 `api.test.1814.love:9443`,测试团 `2101524283048263681`,车辆 `2065329519232720897`):
```
DELETE /admin/fleet/group-dispatch/share-groups/{shareGroupId}?survivorPolicy=RELEASE
→ 200,releasedSourceIds=["360022177073991680"],keptSourceIds=["2101524548279238658"] ✓
→ 库内复核:目标 ASSIGNMENT 360022177073991680 由 vehicle=G/assigned 转为 vehicle=NULL/unassigned ✓
DELETE /admin/fleet/group-dispatch/share-groups/{shareGroupId}?survivorPolicy=KEEP_LEGAL
→ 602111,消息点名冲突对 GROUP_DISPATCH#2101524548279238658 ↔ ASSIGNMENT#360022539151478784 ✓
→ 整请求回滚:该 share_group 行仍 ACTIVE ✓
DELETE /admin/fleet/group-dispatch/share-groups/{shareGroupId}?survivorPolicy=REASSIGN
→ 200,releasedSourceIds = pendingReassignSourceIds = ["360022539151478784"] ✓
→ 库内复核:该 assignment 的车辆与司机已清空、状态回到待派车(真释放 + 待改派) ✓
(负对照,证修复生效)
跨日 2026-12-15 的独立派单三字段逐一不变 ✓
跨日+跨团的原 6 行受害者所在三个团七个服务日的 7 行团级配车行释放前后逐字段完全一致 ✓
```
部署点 `c35251b07`(2026-09-20 18:58,STATE=ok);`git merge-base --is-ancestor ac9efb735 c35251b07` = 真,确认该部署点已包含本次修复。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8051](https://git.1814.love:8443/wx/HL/issues/8051)
- 关联 PR: [wx/HL#8058](https://git.1814.love:8443/wx/HL/pulls/8058)
- 关联后续工单(同一缺陷模式下未覆盖的分支,待产品口径,本次不改): [wx/HL#8061](https://git.1814.love:8443/wx/HL/issues/8061)
## 关联 / 联系人
### 链接
- **Issue**: [#8051](https://git.1814.love:8443/wx/HL/issues/8051)
- **PR**: [#8058](https://git.1814.love:8443/wx/HL/pulls/8058)
- **Merge commit**: [ac9efb735](https://git.1814.love:8443/wx/HL/commit/ac9efb735ab858fef45c4af433023099faef0b2f)
### 联系人
- **后端负责人**: @wx