--- 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` (源码核对:`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