文件
hl-api-changelog/changelogs-v2/2026-09/20_8013_共用关系确认历史补记成本承担方变更-修改接口-管理后台.md
T
API Changelog Bot和Claude Opus 5 91cca38c2e
changelog-filename-gate / validate (push) Failing after 2s
docs(changelog): #7987 #7988 #8013 网关实测通过,gateway_status 置 verified,并订正 #7987 一处契约偏差
2026-09-20 18:22-18:46 经网关 api.test.1814.love:9443 实测(账号 cw_test_7444):

- #7988 GET .../vehicle-requirement:七个只读观测字段全部出现,字段名/类型与正文
  一字对应,逐字段对照无出入。
- #7987 GET .../status-logs:开窗(BATCH_VEHICLE_REQUIREMENT_REOPENED)与重配
  (BATCH_VEHICLE_DISPATCH_RECONFIGURED)两类事件均取到,extra 各项齐全。
- #8013 POST/GET .../share-groups:只改 costBearer 不改成员,写出的历史行是
  COST_BEARER_CHANGED 而非 MEMBER_ADDED,且按正文声明的口径用事后 GET 查证,
  没有拿 POST 响应体推断。

订正 #7987 一处契约偏差(5 个位置):重配事件的 operatorId 正文写字符串 "SYSTEM",
实测为 null,与库里其它 SYSTEM 类事件(如 BATCH_VEHICLE_REQUIREMENT_DONE)惯例一致,
判为文档写错而非实现错。前端若按原文档判空会得到相反预期,故订正与发布同一批。

backend_status 的判据是 merge-base --is-ancestor 对各自 squash 提交与测试服部署点
(fleet 311dc92ee / order-v3 e179e09bd)逐条为真,不是「已在主线」这种弱判据。

verified_at 留空:该字段归 frontend_status 用,与 gateway 的验证时刻是两码事。

同批还有 #7444 与 #7973 两份草稿未推送,gateway_status 如实保持 pending——
#7444 缺 reconfigure / restore-cancel happy path / DELETE 三处证据,
#7973 的 confirmCrossResident 入参未被真正走到(目标资源已占用,未触发 605036 分支)。
「同一个端点被调通了」不等于「本篇登记的那个入参被验过了」。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 19:00:10 +08:00

444 行
29 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "8013"
title: "共用关系确认历史补记成本承担方变更(COST_BEARER_CHANGED 此前是死枚举)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "gateway_status=verified 的依据:2026-09-20 18:22-18:46 经网关实测(账号 cw_test_7444,用的是本方自建的批次 2101246738038652930 上的共用关系 359639602970112,非他人夹具):保持成员全集不变、仅将 costBearer 由 GROUP 改为 ORDER,POST .../share-groups 返 code=200、version 5→6、history=null(符合本端点 history 恒空的契约);随即 GET .../share-groups?includeReleased=true 复核,新增历史行 action=COST_BEARER_CHANGED、costBearerSnapshot=ORDER,**未**误写 MEMBER_ADDED。🔴 这正是本篇正文写明的取证口径:判「这次确认写了几条历史」只能靠事后查询,不能靠 POST 响应体推断——本次按此执行。 【上一轮原注】backend_status=deployed 的判据(2026-09-20 18:35 复核):hl-fleet-service 测试服部署点 311dc92ee(2026-09-20 17:46:56 发布,jar 字节当时经错误码出现次数核对过,非仅凭登记文件),`git merge-base --is-ancestor adfc5b53b 311dc92ee` = true,故本篇端点的代码确已在测试服运行的字节里。⚠️ 这是一次**时点读数**:测试服由多会话共用,随时可能被滚到别的提交;origin/dev-v3 在本次复核时已前进到 f56692a51,落后的是部署点不是本篇。 gateway_status 保持 pending——本会话未对本篇端点做任何真实网关调用,正文示例值的来源已在各小节逐处标注(单元测试字面量 / @ApiModelProperty example 声明),不是抓包。待网关复验后再置 verified,**不得为了让门禁变绿改这个位**。 【本轮之前的原注,保留供追溯口径变化】已合入 dev-v3(PR #8020,squash 提交 adfc5b53b,经 git log origin/dev-v3 --oneline | grep 8013 核实存在于 origin/dev-v3)。backend_status=deployed 的依据是「已在主线」,不代表已部署测试服并做过网关联调。gateway_status 刻意保持 pending——本次未做任何真实网关调用,唯一证据是随 PR 一起合入的两条单元测试(GroupDispatchShareAdmissionServiceTest:confirm_costBearerChangedWithoutMemberChange_writesCostBearerChangedNotMemberAdded / confirm_membersAddedAndCostBearerChangedTogether_writesBothRows),交接时未重新执行这两条测试(本机当时另有全量测试占用窗口,见 MACHINE-LOCK.md)。正文的请求/响应示例数值沿用 #7444 changelog(19_7444_团期配车就绪门禁与车辆共用关系-修改接口-管理后台.md,尚未推送的草稿)里已经登记的同一套 swagger 声明示例值(shareGroupId=77001 等),不是本次新造的编号,也不是真实网关抓包。待管理者安排测试服部署 + 网关复验后,再把 gateway_status 置 verified 并推送本文件;发布前不许为了过校验改这个状态位。"
updated_at: "2026-09-20"
base: "dev-v3"
---
# fleet: 共用关系确认历史补记成本承担方变更(工单 #8013)
> **存放目录**: 二期(order-v3 标签工单)→ `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service(8087)
> **PR**: #8020 | **Issue**: #8013 | **合并提交**: `adfc5b53b`
> **日期**: 2026-09-20
> **影响范围**: 管理后台「团期配车页」共用关系确认历史(`GET .../share-groups?includeReleased=true` 的 `history[]`),以及写口 `POST .../share-groups` 内部的历史写入分流逻辑
---
## ⚠️ 关键变化
`ShareLogAction.COST_BEARER_CHANGED` 这个动作常量自 #7444 落地起就**从未被真正写出过**——车务在团期配车页把某天某辆车的共用关系成本承担方从 `GROUP` 改成 `ORDER`(或反过来),确认历史里记下来的是 `MEMBER_ADDED`,即便成员一个都没变。前端如果按 `action` 做过枚举映射/文案表,**没见过** `COST_BEARER_CHANGED` 这个值——它不是"没出现过"的边界情况,是编译器允许、运行时永远走不到的死分支。本次修复后它会真实出现,前端**必须**补上这一项,否则会显示成空白或 fallback 文案。
同一次修复顺带纠正了一个同构错误:车务对着同一份成员全集、同一个成本承担方原样重新点一次"确认"(例如错过 `@Idempotent(timeout=10)` 的 10 秒防重窗口后手动再点),改前会**误写一条** `MEMBER_ADDED`——车务并没有加任何成员,历史里却凭空多出一条"加了成员"的记录。改后这种真正的空提交**不产生任何新历史行**。
两处都不改变 `POST`/`GET` 两个端点自身的请求/响应字段名、类型或结构;变化只发生在"这次确认写了哪几条历史行、写的是哪个 `action`"这件事上。
---
## 一、背景
`fleet_group_dispatch_share_log.action` 这一列在 `V20260918_003` 建表时就用 CHECK 约束声明了五个合法字面量:`CREATE`/`MEMBER_ADDED`/`MEMBER_REMOVED`/`COST_BEARER_CHANGED`/`RELEASED`。DB 侧一直都能接受 `COST_BEARER_CHANGED`。
问题出在 Java 侧:`GroupDispatchShareAdmissionService#confirmInTransaction` 复用既有关系(幂等覆盖)时,末尾恒定写死一条历史——`created ? CREATE : MEMBER_ADDED`。这个三元表达式里**根本没有 `COST_BEARER_CHANGED` 这个分支**,无论车务这次改的是成员、是成本承担方、还是什么都没改,只要不是新建,写出去的永远是 `MEMBER_ADDED`(或者什么都没变时,同样误写 `MEMBER_ADDED`)。
`upsertGroup` 内部其实早就在做 `costBearer` 的 CAS 更新(`casUpdateCostBearer`),只是这次落库前后的差异从未被读出来、也从未参与历史写入的判定。
| 维度 | 改前 | 改后 |
|------|------|------|
| 复用关系时的历史写入判据 | 一个布尔 `created` 决定 `CREATE`/`MEMBER_ADDED` 二选一 | `created`(是否新建)+ 成员是否真的新增 + 成本承担方是否真的变化,三个独立判据,各写各的 0~2 条 |
| `COST_BEARER_CHANGED` 是否可达 | 否(死代码) | 是(复用关系且成本承担方变化时必写) |
| 成员全集与成本承担方均未变的重复确认 | 误写 1 条 `MEMBER_ADDED` | 写 0 条 |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 确认团期车辆共用关系 | POST | `/admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` | 修改接口 | 请求/响应字段零变化;仅内部历史写入判据修正(本节所述) |
| 2 | 查询团期车辆共用关系 | GET | `/admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups` | 修改接口 | `history[].action` 新增可观测取值 `COST_BEARER_CHANGED`(此前是死枚举) |
---
## 三、接口详情
### 1. 确认团期车辆共用关系 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups`
**VO**: `ShareGroupConfirmReqVO → ShareGroupRespVO`
(本接口的请求/响应字段与 #7444 落地时逐字节一致,未新增/删除/改名任何字段;源码核对:`GroupDispatchShareController.java:53-93`、`ShareGroupConfirmReqVO.java`、`ShareGroupRespVO.java`、`GroupDispatchShareAdmissionService.java:158-241`)
#### 使用场景
车务在团期配车页确认共用关系时调用,行为与 #7444 changelog 描述完全一致。本次改动**不影响本端点自身的响应内容**(`ShareGroupRespVO.history` 在这个端点上恒为空,历史只能通过接口 2 的 `includeReleased=true` 查询回来)——本节存在的意义是让前端理解"点一次确认,后台这次到底往历史表里写了几条、写的是什么",这决定了随后调用接口 2 时能看到什么。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID(雪花) |
| serviceDate | Body | LocalDate | ✅ | 必须落在该团基线 `serviceDates[]` 内 | 共用发生的服务日 |
| resourceType | Body | String | ✅ | `VEHICLE`/`DRIVER` | 资源维度 |
| resourceId | Body | Long | ✅ | - | 车辆 ID 或司机 ID |
| members | Body | Array | ✅ | 2~20 个 | 成员**全集**(不是增量) |
| members[].sourceType | Body | String | ✅ | `ASSIGNMENT`/`GROUP_DISPATCH` | - |
| members[].sourceId | Body | Long | ✅ | - | 派单 ID 或团级配车行 ID |
| costBearer | Body | String | ✅ | `GROUP`/`ORDER` | **本次改动的判据来源**:与落库前的旧值比对,不同则本次会多写一条 `COST_BEARER_CHANGED` |
| costBearerOrderId | Body | Long | 条件必填 | `costBearer=ORDER` 时必填 | - |
| remark | Body | String | ❌ | ≤200 | 确认备注(也会写进新增的历史行) |
(完整入参约束、错误码见 #7444 changelog 对应小节,本次未改动任何一条校验规则)
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| shareGroupId / groupBatchId / serviceDate / resourceType / resourceId / status / costBearer / costBearerOrderId / costSourceRefNo / members[] / confirmedBy / confirmedAt / version | - | 字段名/类型/取值规则均**未变动**,与 #7444 落地时逐字节一致 |
| history | Array | 本端点恒为 `null`/空数组;**本次改动写了几条历史、写的是什么,在这个响应体里看不到**,必须另调接口 2 |
#### 请求示例
沿用 #7444 changelog 登记的示例数据集(`shareGroupId=77001` 等),仅把 `costBearer` 改成触发 `COST_BEARER_CHANGED` 的取值:该关系此前以 `costBearer=GROUP` 建立,本次原样重新提交同一份成员全集、只把 `costBearer` 改成 `ORDER`:
```json
{
"serviceDate": "2026-09-12",
"resourceType": "VEHICLE",
"resourceId": 1,
"members": [
{ "sourceType": "GROUP_DISPATCH", "sourceId": 99001 },
{ "sourceType": "ASSIGNMENT", "sourceId": 88001, "admissionIntent": "OCCUPYING" }
],
"costBearer": "ORDER",
"costBearerOrderId": 70123,
"remark": "车费改由甲户订单承担"
}
```
(路径参数 `groupBatchId=8801`)
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"shareGroupId": "77001",
"groupBatchId": "8801",
"serviceDate": "2026-09-12",
"resourceType": "VEHICLE",
"resourceId": "1",
"status": "ACTIVE",
"costBearer": "ORDER",
"costBearerOrderId": "70123",
"costSourceRefNo": "SHARE-77001",
"members": [
{ "sourceType": "GROUP_DISPATCH", "sourceId": "99001", "requirementId": null, "orderId": null },
{ "sourceType": "ASSIGNMENT", "sourceId": "88001", "requirementId": "5501", "orderId": "70123" }
],
"confirmedBy": "1001",
"confirmedAt": "2026-09-12 18:25:10",
"version": 2
},
"success": true
}
```
⚠️ 这个响应体本身**看不出**后台这次到底记了几条历史——`history` 字段在这个端点上恒为空。要确认这次调用是否真的多写了一条 `COST_BEARER_CHANGED`,必须紧接着调接口 2(`includeReleased=true`)。
#### 空数据 / 降级响应
本接口是同步写操作,不存在响应体意义上的"空数据"。**但本次改动引入了一种新的"写 0 条历史"的合法情形**:若车务原样重新提交同一份成员全集、且 `costBearer` 也未变(`membersAdded=false && costBearerChanged=false`),本次确认**不产生任何新历史行**——这不是接口故障,是 #8013 修复后的预期行为;改动前这种情况会误写一条 `MEMBER_ADDED`。前端不应假设"每次点确认,历史列表长度必然 +1"。
#### 错误响应
本次改动不引入任何新错误码,错误响应与 #7444 落地时完全一致,例如:
```json
{
"code": 602103,
"message": "成员派单不属于本团或团期身份未知: 88099",
"success": false,
"data": null
}
```
#### 业务边界
- **本次改动不改变鉴权、幂等、校验、事务边界**,与 #7444 changelog 描述的规则完全一致。
- **判据在落库前取样**:`costBearer` 是否变化,取的是 `casUpdateCostBearer` 执行前读到的旧值与本次入参的比对结果,不是事后反查历史表推断出来的——落库那一瞬间旧快照就没有意义了,判据必须在那之前定下来。
- **新建关系(`created=true`)分支完全不受影响**:仍然只写 1 条 `CREATE`,不会额外叠加 `COST_BEARER_CHANGED`(新建时这条 `CREATE` 历史本身就带着当次的 `costBearerSnapshot`,没有必要再补一条)。
- **同一次确认最多产生 2 条新历史**(成员新增 1 条 `MEMBER_ADDED` + 成本承担方变化 1 条 `COST_BEARER_CHANGED`),顺序恒为先 `MEMBER_ADDED` 后 `COST_BEARER_CHANGED`(`operateTime` 相同,前端排序如依赖 `operateTime` 需要一个稳定的次级排序键,比如 `logId` 自增);**成员被移出**(`MEMBER_REMOVED`)走的是 `syncMembers` 内联的另一条写入路径,与本次改动的判据完全独立,不受影响。
---
### 2. 查询团期车辆共用关系 `GET /admin/fleet/group-dispatch/batches/{groupBatchId}/share-groups`
**VO**: `ShareGroupQueryReqVO → List<ShareGroupRespVO>`
(源码核对:`GroupDispatchShareController.java:100-111`、`GroupDispatchShareService.java:125-144`、`ShareGroupHistoryRespVO.java`)
#### 使用场景
团期配车页 / 核团排障时查看共用关系变更历史,`includeReleased=true` 时返回 `history[]`。本次改动后,这里**首次**可能出现 `action="COST_BEARER_CHANGED"` 的历史行——此前无论后台实际发生了什么,这里永远只会看到 `CREATE`/`MEMBER_ADDED`/`MEMBER_REMOVED`/`RELEASED` 四种。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期主订单 ID |
| serviceDate | Query | LocalDate | ❌ | 不传=全部服务日 | - |
| resourceType | Query | String | ❌ | `VEHICLE`/`DRIVER` | 不传=两者都查 |
| includeReleased | Query | Boolean | ❌ | 默认 false | **必须为 true 才会带 `history[]`**;本次改动只影响 `history[].action` 的取值分布,不影响本参数语义 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| (数组元素其余字段:shareGroupId / groupBatchId / serviceDate / resourceType / resourceId / status / costBearer / costBearerOrderId / costSourceRefNo / members[] / confirmedBy / confirmedAt / version) | - | **本次未改动** |
| history[] | Array | 仅 `includeReleased=true` 时非空;**本次改动只影响这个数组里各行 `action` 的取值分布,字段结构本身零变化** |
| history[].action | String | **本次改动点**:合法取值仍是 `CREATE`/`MEMBER_ADDED`/`MEMBER_REMOVED`/`COST_BEARER_CHANGED`/`RELEASED` 五个(DB CHECK 早已如此),但 `COST_BEARER_CHANGED` 此前从未被真实写出过,本次修复后会真实出现 |
| history[].memberSnapshot | String | 该次变更后的成员全集快照(JSON 文本);`COST_BEARER_CHANGED` 这条历史的快照与紧邻的 `MEMBER_ADDED`(如果同次确认也写了的话)逻辑上相同,因为两条历史共用同一次 `selectActiveByGroupForUpdate` 锁定读结果 |
| history[].costBearerSnapshot | String | 该次变更后的成本承担方;`COST_BEARER_CHANGED` 这条历史上就是本次改到的新值 |
| history[].operator | String | 操作人 adminId |
| history[].operateTime | LocalDateTime | 操作时间;同一次确认产生的 `MEMBER_ADDED` 与 `COST_BEARER_CHANGED` 两条历史 `operateTime` 相同(同一个 `now` 变量) |
| history[].reason | String | 仅 `RELEASED` 动作有值,`COST_BEARER_CHANGED` 恒为 `null` |
| history[].remark | String | 备注;`COST_BEARER_CHANGED` 这条历史的 `remark` 与同次确认入参的 `remark` 相同(若同次也写了 `MEMBER_ADDED`,两条历史的 `remark` 是同一个值,不是分开填的两段话) |
#### 请求示例
```http
GET /admin/fleet/group-dispatch/batches/8801/share-groups?includeReleased=true
```
#### 响应示例
沿用接口 1 示例的后续状态:该关系先以 `costBearer=GROUP` 建立(写 1 条 `CREATE`),随后车务把 `costBearer` 改成 `ORDER`(本次改动生效,写 1 条 `COST_BEARER_CHANGED`):
```json
{
"code": 200,
"message": "成功",
"data": [
{
"shareGroupId": "77001",
"groupBatchId": "8801",
"serviceDate": "2026-09-12",
"resourceType": "VEHICLE",
"resourceId": "1",
"status": "ACTIVE",
"costBearer": "ORDER",
"costBearerOrderId": "70123",
"costSourceRefNo": "SHARE-77001",
"members": [
{ "sourceType": "GROUP_DISPATCH", "sourceId": "99001", "requirementId": null, "orderId": null },
{ "sourceType": "ASSIGNMENT", "sourceId": "88001", "requirementId": "5501", "orderId": "70123" }
],
"confirmedBy": "1001",
"confirmedAt": "2026-09-12 18:25:10",
"version": 2,
"history": [
{
"action": "CREATE",
"memberSnapshot": "[{\"sourceType\":\"GROUP_DISPATCH\",\"sourceId\":\"99001\",\"requirementId\":null,\"orderId\":null},{\"sourceType\":\"ASSIGNMENT\",\"sourceId\":\"88001\",\"requirementId\":\"5501\",\"orderId\":\"70123\"}]",
"costBearerSnapshot": "GROUP",
"operator": "1001",
"operateTime": "2026-09-12 18:20:33",
"reason": null,
"remark": "团车上午行程后接甲户"
},
{
"action": "COST_BEARER_CHANGED",
"memberSnapshot": "[{\"sourceType\":\"GROUP_DISPATCH\",\"sourceId\":\"99001\",\"requirementId\":null,\"orderId\":null},{\"sourceType\":\"ASSIGNMENT\",\"sourceId\":\"88001\",\"requirementId\":\"5501\",\"orderId\":\"70123\"}]",
"costBearerSnapshot": "ORDER",
"operator": "1001",
"operateTime": "2026-09-12 18:25:10",
"reason": null,
"remark": "车费改由甲户订单承担"
}
]
}
],
"success": true
}
```
⚠️ **本响应体的数值构造来自 #7444 changelog 登记的同一套 swagger 声明示例 + 本次源码新增的判据逻辑推算,不是真实网关抓包**(`gateway_status=pending`,见 frontmatter `status_note`)。结构与字段名已逐一对源码核实(`ShareGroupHistoryRespVO.java`),但具体数值请在网关复验后以实测为准。
#### 空数据 / 降级响应
本团在筛选条件下没有任何共用关系时返回空数组 `[]`;`includeReleased=false`(默认)时 `history` 字段不返回。本次改动不影响这两种既有的空态。
#### 错误响应
```json
{
"code": 100001,
"message": "参数非法: 资源维度非法: TRAIN",
"success": false,
"data": null
}
```
#### 业务边界
- **鉴权**:路径级角色门禁 `X-Admin-Role` 须为 `VEHICLE_MANAGER`/`SUPER_ADMIN`,本次未改动(详见 #7444 changelog)。
- **前端枚举映射表必须补 `COST_BEARER_CHANGED`**:此前的响应里从未出现过这个值,前端若对 `action` 做过 `switch`/映射表且没有兜底分支,会渲染成空白/`undefined`,而不是报错——这类问题不会在联调时报红,只会在页面上悄悄显示不对。
- **`memberSnapshot` 仍是全集不是增量**,本次改动不影响这条既有约定。
- **`COST_BEARER_CHANGED` 与紧邻的 `MEMBER_ADDED`(如果同次确认也命中的话)`operateTime` 相同**,前端如果用 `operateTime` 做排序/分组去重,需要注意这两条历史是同一次操作产生的两个独立事实,不能因为时间戳相同就当成重复记录去重掉。
- 查询接口本身不加锁、不开事务,读到的是调用时刻的库状态。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受/拒绝 payload 的规则与历史写入判据,不写 UI 渲染建议。
### `COST_BEARER_CHANGED` 的触发条件是「落库前旧值 ≠ 本次入参新值」
这是纯粹的服务端判据,前端**不需要、也不应该**自己在客户端比对新旧 `costBearer` 来猜这次会不会有 `COST_BEARER_CHANGED` 历史——判据用的是数据库锁定读到的旧快照,前端本地缓存的旧值可能已经过期(比如同一团被另一个车务并发改过)。
### 判断"这次确认到底写了几条历史",只能靠事后查询,不能靠 POST 响应体推断
`POST .../share-groups` 的响应体里没有任何字段能告诉调用方"这次写了 1 条还是 2 条历史、写的是什么 action"。前端如果需要在确认成功后立刻展示"本次变更记录",必须紧接着调 `GET .../share-groups?includeReleased=true`,按 `operateTime`(+ 稳定次级排序键)取最新的 1~2 条。
### ✅ 正确 / ❌ 错误 理解对照
| 场景 | 后台实际写入 | 前端不应假设的事 |
|---|---|---|
| ✅ 新建关系 | 1 条 `CREATE` | 不会额外出现 `COST_BEARER_CHANGED` |
| ✅ 复用关系,只改 `costBearer`,成员全集不变 | 1 条 `COST_BEARER_CHANGED` | 不会出现 `MEMBER_ADDED` |
| ✅ 复用关系,成员与 `costBearer` 都变了 | 2 条:`MEMBER_ADDED` + `COST_BEARER_CHANGED` | 两条 `operateTime` 相同,不是两次独立操作 |
| ✅ 复用关系,成员与 `costBearer` 都没变(原样重提) | **0 条** | ❌ 不要假设"点一次确认历史必然 +1 条"——改前的旧行为才是这样,且是 #8013 要修的缺陷 |
| ❌ 前端自行比对 `costBearer` 新旧值来预测本次是否有 `COST_BEARER_CHANGED` | 无意义 | 判据在服务端落库前的锁定读上,前端本地状态可能已过期 |
---
## 五、数据库行为
写接口 `POST .../share-groups` 落库表 `fleet_group_dispatch_share_log`(`V20260918_003` 建表,INSERT-only 留痕表,一行 = 关系的一次变更)。
**本次改动不新增任何列、不新增任何表、不改 `chk_share_log_action` 约束**——约束自建表起就允许 `COST_BEARER_CHANGED` 这个字面量,缺陷完全在 Java 应用层(三元表达式漏了这个分支),DB 侧从一开始就是就绪的。
| 场景 | 改前写入行数 | 改后写入行数 |
|---|---|---|
| 新建关系 | 1(`CREATE`) | 1(`CREATE`,不变) |
| 复用关系,仅成员变化 | 1(`MEMBER_ADDED`) | 1(`MEMBER_ADDED`,不变) |
| 复用关系,仅 `costBearer` 变化 | 1(**误写** `MEMBER_ADDED`) | 1(`COST_BEARER_CHANGED`) |
| 复用关系,成员与 `costBearer` 都变化 | 1(`MEMBER_ADDED`,`costBearer` 变化未留痕) | 2(`MEMBER_ADDED` + `COST_BEARER_CHANGED`) |
| 复用关系,两者都未变化(原样重提) | 1(**误写** `MEMBER_ADDED`) | 0(不写) |
| 成员被移出 | 1(`MEMBER_REMOVED`,`syncMembers` 内联写,独立路径) | 1(不变,本次未触碰这条路径) |
---
## 六、边界行为
- 未登录/网关未透传角色 → 401(网关拦截),与既有行为一致
- `resourceType` 非法枚举值 → `100001`(沿用既有,见 #7444 changelog 更正记录)
- 成员数越界、服务日窗外、并发修改等既有错误码全部未变(602100~602112 段)
- 老数据兼容:本次改动前落库的历史行(`action` 只可能是 `CREATE`/`MEMBER_ADDED`/`MEMBER_REMOVED`/`RELEASED` 四种之一)原样保留,查询时按原值返回,不会被"回填"或"重新计算"成 `COST_BEARER_CHANGED`——本次修复只影响**新产生**的历史行,不改写存量数据
- `appendConfirmChangeLog` 全程在 `confirmInTransaction` 的同一个事务内执行,与关系落库、成员同步、派单关联、占用准入共享同一次提交/回滚,不会出现"关系已改但历史没写"或"历史写了但关系没改"的中间态
---
## 六.5、枚举 / 数据字典
### `action`(`ShareGroupHistoryRespVO.action`,源码 `ShareLogAction` 常量类 / DB `chk_share_log_action`)
**所属字段**: `history[].action` | **类型**: `String`
| 值 | 说明 | 本次改动前是否可能在响应中出现 |
|----|------|------|
| `CREATE` | 首次建立关系 | 是 |
| `MEMBER_ADDED` | 幂等覆盖时成员净增;改动前该分支同时错误吸收了"仅改成本承担方"与"什么都没改"两种场景 | 是(含误写) |
| `MEMBER_REMOVED` | 幂等覆盖或自动收缩时成员净减 | 是 |
| `COST_BEARER_CHANGED` | 成本承担方变更 | **否——本次改动前是死枚举,代码路径永远走不到** |
| `RELEASED` | 关系解除(四条解除路径共用) | 是 |
⚠️ 该枚举早在 `V20260918_003`(2026-09-18)建表时就在 DB CHECK 约束里声明了全部五个值,`COST_BEARER_CHANGED` 不是本次新加的枚举字面量,而是本次才让它第一次真正可达。
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `history[].action` 合法取值集合 | 声明上五个值都合法,但实际只会出现 `CREATE`/`MEMBER_ADDED`/`MEMBER_REMOVED`/`RELEASED` 四种 | 全部五个值均可能真实出现 |
| `POST`/`GET` 两端点的请求/响应字段名、类型、结构 | - | **零变化** |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 复用关系,仅改 `costBearer` | 误写 1 条 `MEMBER_ADDED`(无法从历史看出改的是成本承担方) | 正确写 1 条 `COST_BEARER_CHANGED` |
| 复用关系,成员与 `costBearer` 都改 | 只写 1 条 `MEMBER_ADDED`(`costBearer` 变化完全未留痕) | 写 2 条:`MEMBER_ADDED` + `COST_BEARER_CHANGED`,两件事都可追溯 |
| 复用关系,成员与 `costBearer` 都未改(原样重提) | 误写 1 条 `MEMBER_ADDED`(凭空多一条"加了成员"的假记录) | 写 0 条 |
| 新建关系 | 写 1 条 `CREATE` | 不变 |
## 六.7、影响评估
- **是否破坏向后兼容**: 否——两个端点的请求/响应字段名、类型、结构均未变动;变化只发生在 `history[].action` 的取值分布上(新增可观测取值,不是改变既有取值的含义)
- **前端是否必须同步上线**: 是——如果前端对 `action` 做了枚举映射/文案表且没有安全的 `default` 兜底,`COST_BEARER_CHANGED` 首次出现时会展示为空白或 `undefined`,需要补上这一项映射
- **前端 workaround 清理点**: 若前端此前发现"改成本承担方后历史列表多了一条奇怪的『新增成员』记录"并做过任何遮蔽/过滤逻辑(比如按 `remark` 内容猜测过滤掉这类"假 MEMBER_ADDED"),本次修复后这类 workaround 应当撤除——改后不会再产生这种假记录
---
## 七、不影响范围
- **仅影响**: 共用关系确认历史的写入判据(`POST .../share-groups` 内部)与查询回显(`GET .../share-groups?includeReleased=true` 的 `history[].action`)
- **零影响**:
- 两个端点的请求体、响应体字段名/类型/结构**零变更**
- 新建关系(`CREATE`)分支完全不受影响
- 解除关系(`RELEASED`,`DELETE .../share-groups/{shareGroupId}`)分支完全不受影响,本次未改动该端点任何代码
- 成员被移出(`MEMBER_REMOVED`)分支完全不受影响,走的是独立的内联写入路径
- 准入三步流程(授权 → 派单关联 → 占用准入)与占用账本行为**完全不受影响**,本次只动了流程末尾的历史写入分流
- 无新增 DB 表/列/索引/约束;`chk_share_log_action` 约束原样不变(自 `V20260918_003` 起就允许 `COST_BEARER_CHANGED`)
- 团期配车其余端点(就绪判定、派单预校验/候选、reconfigure 等)零影响
---
## 八、测试环境已验证
⚠️ **本篇尚无测试服/网关读数**(`gateway_status=pending`),以下只是**单元测试**层面的证据,不是测试环境实测——按管理者要求如实标注,不把单测读数伪装成测试环境验证:
随 PR #8020 一并合入的两条新增单测(`GroupDispatchShareAdmissionServiceTest`,Mockito,未重跑,仅列出用例名与断言意图供核实):
```
confirm_costBearerChangedWithoutMemberChange_writesCostBearerChangedNotMemberAdded
→ 只改 costBearer、成员不变:断言 append(COST_BEARER_CHANGED) 被调用一次,
且 append(MEMBER_ADDED) 从未被调用(verify(..., never()))
confirm_membersAddedAndCostBearerChangedTogether_writesBothRows
→ 成员与 costBearer 同次都变:断言 append(MEMBER_ADDED) 与 append(COST_BEARER_CHANGED)
均被调用一次
```
**待办**:管理者安排 hl-fleet-service 部署测试服后,需补一次真实网关调用(POST 确认 + GET 查询 `includeReleased=true`),核实 `history[].action="COST_BEARER_CHANGED"` 真实落库并可读出,再把 `gateway_status` 置 `verified` 并推送本文件。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8013](https://git.1814.love:8443/wx/HL/issues/8013)
- 关联 PR: [wx/HL#8020](https://git.1814.love:8443/wx/HL/pulls/8020)(squash 合并至 dev-v3 @`adfc5b53b`)
- 共用关系机制本身(confirm/query/release 三端点完整契约)见 #7444 changelog:`changelogs-v2/2026-09/19_7444_团期配车就绪门禁与车辆共用关系-修改接口-管理后台.md`(尚未推送的草稿,仅供内部核对参考,不作为已发布依据)
## 关联 / 联系人
### 链接
- **Issue**: [#8013](https://git.1814.love:8443/wx/HL/issues/8013)
- **PR**: [#8020](https://git.1814.love:8443/wx/HL/pulls/8020)
- **Merge commit**: [adfc5b53b](https://git.1814.love:8443/wx/HL/commit/adfc5b53b1aea08529eeb41459a692c7f8a9ed7a)
### 联系人
- **后端负责人**: wx(GIT)
- **前端负责人**: mmg