444 行
30 KiB
Markdown
444 行
30 KiB
Markdown
---
|
||
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: "not_required"
|
||
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 并推送本文件;发布前不许为了过校验改这个状态位。 前端实证翻 not_required(mmg 2026-09-20):share-groups/costBearer/COST_BEARER/MEMBER_ADDED 全仓零命中,共用关系确认历史 UI 未建(#7444 挂起域),前端无 action 映射表可补;配车接入时映射必须含 COST_BEARER_CHANGED、同 operateTime 两条历史不去重、不假设每次确认历史 +1 条,已记入前端待办口径。"
|
||
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
|