docs(changelog): #7987 #7988 #8013 网关实测通过,gateway_status 置 verified,并订正 #7987 一处契约偏差
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
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>
这个提交包含在:
@@ -0,0 +1,495 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7987"
|
||||
title: "团期时间线新增开窗与重配两类事件,事件类型枚举扩展"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: "hl-ui"
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "gateway_status=verified 的依据:2026-09-20 18:22-18:46 经网关 https://api.test.1814.love:9443 实测(账号 cw_test_7444)。GET /v3/admin/order/group-batch/{id}/status-logs 在两个批次上分别取到 BATCH_VEHICLE_REQUIREMENT_REOPENED(开窗,extra 含 windowMinutes/blockedStage/expiresAt/scopeGroupCodes/scopeDates 五项齐全)与 BATCH_VEHICLE_DISPATCH_RECONFIGURED(重配),eventType/eventTypeName/changeType=DATA/extra 各字段逐一对照一致。⚠️ 本次实测发现并已订正一处契约偏差:重配事件的 operatorId 原写字符串 SYSTEM,实测为 null(与库里其它 SYSTEM 类事件惯例一致),判为文档写错而非实现错,已在字段表/字段值/JSON 示例/注意事项共 5 处改完。前端若按原文档判空会得到相反预期,故这条订正与发布同一批。 【上一轮原注】backend_status=deployed 的判据(2026-09-20 18:35 复核):hl-order-service-v3 测试服部署点 e179e09bd(2026-09-20 17:55:02 发布),`git merge-base --is-ancestor ab92377c7 e179e09bd` = true,故本篇端点的代码确已在测试服运行的字节里。⚠️ 这是一次**时点读数**:测试服由多会话共用,随时可能被滚到别的提交;origin/dev-v3 在本次复核时已前进到 f56692a51,落后的是部署点不是本篇。 gateway_status 保持 pending——本会话未对本篇端点做任何真实网关调用,正文示例值的来源已在各小节逐处标注(单元测试字面量 / @ApiModelProperty example 声明),不是抓包。待网关复验后再置 verified,**不得为了让门禁变绿改这个位**。"
|
||||
updated_at: "2026-09-20"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期时间线新增开窗与重配两类事件
|
||||
|
||||
> **存放目录**: 二期(v3) → `changelogs-v2/{YYYY-MM}/`
|
||||
>
|
||||
> **服务**: hl-order-v3 (端口 8021)
|
||||
> **PR**: #8039
|
||||
> **Issue**: #7987
|
||||
> **日期**: 2026-09-20
|
||||
> **影响范围**: 管理后台 - 团期详情页 - 状态流水/时间线
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
后端在团期时间线(`group_batch_status_log` 表)新增两种事件记录:
|
||||
- **BATCH_VEHICLE_REQUIREMENT_REOPENED**(开窗事件)—— 管理员显式开启受控重配窗口时写入
|
||||
- **BATCH_VEHICLE_DISPATCH_RECONFIGURED**(重配事件)—— fleet 回调确认重配生效时写入
|
||||
|
||||
**前端必须新增这两个 event_type 值的渲染分支**,否则这两类事件在管理后台团期时间线上将无法正常显示其标签名(见下文「③ 前端渲染缺口」)。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
#7987 实施"受控重配窗口"机制:管理员可在团期进入待出发后仍显式开一个时间限制的窗口,允许车务在此期间重新排车。此前这两件事的事实只落在需求行的三个字段(`reconfigure_reason` / `reconfigure_opened_by` / `reconfigure_opened_at`),但运营打开团期详情页时看不到这些列,无法追溯窗口何时开过。
|
||||
|
||||
本次变更补上团期时间线的留痕,让管理员在团期详情页就能看到「谁在何时以什么原因开了重配窗口」与「窗口内换车是否生效」。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
既存接口(#7304 落地)返回值扩展:
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期状态流水查询 | GET | `/v3/admin/order/group-batch/{id}/status-logs` | 响应新增 event_type 值 | 新增两种事件类型 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期状态流水查询 `GET /v3/admin/order/group-batch/{id}/status-logs`
|
||||
|
||||
**VO**: `GroupBatchStatusLogItemVO` (既存响应 VO,新增 eventTypeName 映射值)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台团期详情页的状态流水/时间线面板,用于向运营展示该团期的历史状态变化与重要操作记录。本次变更使该时间线新增两类事件的可见性。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| id | Path | Long | ✅ | - | 团期主订单 ID |
|
||||
|
||||
#### 出参 `Result<List<GroupBatchStatusLogItemVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| logId | String | 流水记录 ID |
|
||||
| groupBatchId | Long | 团期主订单 ID |
|
||||
| changeType | String | 变更类型:STATUS(状态流转) / DATA(数据变更) |
|
||||
| eventType | String | 事件类型代码(如 BATCH_VEHICLE_REQUIREMENT_REOPENED) |
|
||||
| eventTypeName | String | 事件类型中文名(**新增两种**:团车受控重开·已开配车窗口 / 团车受控重配·车辆安排已变更) |
|
||||
| fromStatus | String | 状态前值代码(如 PENDING_DEPARTURE) |
|
||||
| fromStatusName | String | 状态前值中文名 |
|
||||
| toStatus | String | 状态后值代码 |
|
||||
| toStatusName | String | 状态后值中文名 |
|
||||
| content | String | 事件文本内容(前端直接渲染) |
|
||||
| reason | String | 事件原因/备注(可为 null) |
|
||||
| operatorType | String | 操作人类型:ADMIN / SYSTEM / MQ |
|
||||
| operatorId | String | 操作人 ID。⚠️ **2026-09-20 网关实测订正**:**`operatorType=SYSTEM` 时本字段是 `null`,不是字符串 `"SYSTEM"`**(与库里其它 SYSTEM 类事件如 `BATCH_VEHICLE_REQUIREMENT_DONE` 的惯例一致)。前端判空请按 `null` 处理 |
|
||||
| operatorName | String | 操作人名称 |
|
||||
| extra | Map<String, Object> | 结构化附加数据,字段随事件类型变化(见下文"新增事件详解") |
|
||||
| changedAt | String | 事件发生时刻(ISO 8601) |
|
||||
|
||||
#### 新增事件类型详解
|
||||
|
||||
**BATCH_VEHICLE_REQUIREMENT_REOPENED 事件**
|
||||
- `eventType`: `"BATCH_VEHICLE_REQUIREMENT_REOPENED"`
|
||||
- `eventTypeName`: `"团车受控重开·已开配车窗口"`
|
||||
- `changeType`: `"DATA"` (状态内数据变更,fromStatus = toStatus)
|
||||
- `operatorType`: `"ADMIN"`
|
||||
- `operatorId`: 开窗管理员 ID
|
||||
- `operatorName`: 开窗管理员真名
|
||||
- `content`: "开启受控重配窗口:允许车务在 X 分钟内重新安排本团车辆(授权 N 个乘车分组 / M 个行程日,2026-09-20 10:30:00 前有效)。此前团期已进入「待出发」,正常不可再配车。"
|
||||
- `reason`: 管理员必填的开窗原因(如"团期特殊变更")
|
||||
- `extra`: 结构化数据
|
||||
```json
|
||||
{
|
||||
"requirementId": "需求行主键(字符串)",
|
||||
"requirementVersion": 需求行版本号(整数),
|
||||
"blockedStage": "PENDING_DEPARTURE",
|
||||
"windowMinutes": 120,
|
||||
"expiresAt": "2026-09-20T12:30:00",
|
||||
"scopeGroupCodes": ["乘车分组编号数组"],
|
||||
"scopeDates": ["2026-09-21", "2026-09-22"]
|
||||
}
|
||||
```
|
||||
|
||||
**BATCH_VEHICLE_DISPATCH_RECONFIGURED 事件**
|
||||
- `eventType`: `"BATCH_VEHICLE_DISPATCH_RECONFIGURED"`
|
||||
- `eventTypeName`: `"团车受控重配·车辆安排已变更"`
|
||||
- `changeType`: `"DATA"` (状态内数据变更,fromStatus = toStatus)
|
||||
- `operatorType`: `"SYSTEM"`
|
||||
- `operatorId`: **`null`**(⚠️ **2026-09-20 网关实测订正**:原写 `"SYSTEM"`,实测为 `null`)
|
||||
- `operatorName`: `"系统"`
|
||||
- `content`: "本团车辆安排已被重新排定:车务在受控重配窗口内完成换车,新的配车计划(版本 20260920_v1)已生效,原计划作废。开窗授权人:张三;具体经办人见车务配车记录。"
|
||||
- `reason`: 开窗时管理员填写的原因(沿用窗口的 reason,非重配操作人的备注)
|
||||
- `extra`: 结构化数据
|
||||
```json
|
||||
{
|
||||
"requirementId": "需求行主键(字符串)",
|
||||
"requirementVersion": 需求行版本号(整数),
|
||||
"planVersion": "fleet 配车计划版本号(字符串)",
|
||||
"reconfigureOpenedBy": "开窗管理员 ID(与前一条事件的 operatorId 对应)",
|
||||
"reconfigureOpenedAt": "2026-09-20T10:15:30"
|
||||
}
|
||||
```
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
GET /v3/admin/order/group-batch/8000001/status-logs
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": [
|
||||
{
|
||||
"logId": "12345",
|
||||
"groupBatchId": "8000001",
|
||||
"changeType": "DATA",
|
||||
"eventType": "BATCH_VEHICLE_REQUIREMENT_REOPENED",
|
||||
"eventTypeName": "团车受控重开·已开配车窗口",
|
||||
"fromStatus": "PENDING_DEPARTURE",
|
||||
"fromStatusName": "待出发",
|
||||
"toStatus": "PENDING_DEPARTURE",
|
||||
"toStatusName": "待出发",
|
||||
"content": "开启受控重配窗口:允许车务在 120 分钟内重新安排本团车辆(授权 2 个乘车分组 / 3 个行程日,2026-09-20T12:30:00 前有效)。此前团期已进入「待出发」,正常不可再配车。",
|
||||
"reason": "临时增加3人,需调整座位分组与用车方案",
|
||||
"operatorType": "ADMIN",
|
||||
"operatorId": "1001",
|
||||
"operatorName": "李四",
|
||||
"extra": {
|
||||
"requirementId": "7000001",
|
||||
"requirementVersion": 2,
|
||||
"blockedStage": "PENDING_DEPARTURE",
|
||||
"windowMinutes": 120,
|
||||
"expiresAt": "2026-09-20T12:30:00",
|
||||
"scopeGroupCodes": ["GRP001", "GRP002"],
|
||||
"scopeDates": ["2026-09-21", "2026-09-22", "2026-09-23"]
|
||||
},
|
||||
"changedAt": "2026-09-20T10:30:00"
|
||||
},
|
||||
{
|
||||
"logId": "12346",
|
||||
"groupBatchId": "8000001",
|
||||
"changeType": "DATA",
|
||||
"eventType": "BATCH_VEHICLE_DISPATCH_RECONFIGURED",
|
||||
"eventTypeName": "团车受控重配·车辆安排已变更",
|
||||
"fromStatus": "PENDING_DEPARTURE",
|
||||
"fromStatusName": "待出发",
|
||||
"toStatus": "PENDING_DEPARTURE",
|
||||
"toStatusName": "待出发",
|
||||
"content": "本团车辆安排已被重新排定:车务在受控重配窗口内完成换车,新的配车计划(版本 20260920_v1)已生效,原计划作废。开窗授权人:李四;具体经办人见车务配车记录。",
|
||||
"reason": "临时增加3人,需调整座位分组与用车方案",
|
||||
"operatorType": "SYSTEM",
|
||||
"operatorId": null,
|
||||
"operatorName": "系统",
|
||||
"extra": {
|
||||
"requirementId": "7000001",
|
||||
"requirementVersion": 2,
|
||||
"planVersion": "20260920_v1",
|
||||
"reconfigureOpenedBy": "1001",
|
||||
"reconfigureOpenedAt": "2026-09-20T10:30:00"
|
||||
},
|
||||
"changedAt": "2026-09-20T11:45:00"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": [],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 接口返回的时间线按 `changedAt` 升序排列(从最早到最新)
|
||||
- 两个新事件都属于 `DATA` 类(状态内数据变更),不涉及团期主状态 `batch_status` 变化
|
||||
- 库里若有历史脏值或未知 event_type,后端 eventTypeLabel 返回 null,前端需兜底显示原始 code 值
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节说明后端接受请求、返回响应的规则。
|
||||
|
||||
### ✅ 正确 / ❌ 错误请求对照
|
||||
|
||||
| 场景 | 请求示例 | 结果 |
|
||||
|------|---------|------|
|
||||
| ✅ 查询既存团期的时间线 | `GET /v3/admin/order/group-batch/8000001/status-logs` | 200 + 时间线列表 |
|
||||
| ✅ 无流水时返回空列表 | `GET /v3/admin/order/group-batch/9999999/status-logs`(不存在团期) | 200 + `data: []` |
|
||||
| ❌ 团期 ID 非数字 | `GET /v3/admin/order/group-batch/abc/status-logs` | 400 |
|
||||
|
||||
### 字段约束与兼容性规则
|
||||
|
||||
- **eventType 兼容性**:库里可能存在本版本未定义的历史 event_type 值 → 后端 eventTypeLabel 返回 null → 前端应兜底显示原始 code 值,而不是报错
|
||||
- **extra 字段**:根据 eventType 不同而变化(见下文详解);前端需根据 eventType 判断应读取哪些 extra 字段,勿假设所有事件的 extra 结构相同
|
||||
- **operatorType 非 ADMIN 时**:⚠️ **2026-09-20 网关实测订正**:**`operatorId` 是 `null`、`operatorName` 是 `"系统"`——两个字段形态不同,不要当成同一回事**(原文把它们合并描述成「可能不是真实人名」,会让前端以为 `operatorId` 也有值) → 前端应按类型判断如何渲染,勿硬编码为"管理员"
|
||||
- **reason 字段**:可为 null(仅新增事件强制填写,既有历史事件可能无此字段) → 前端应做 null check
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举:新增事件类型
|
||||
|
||||
### eventType(GroupBatchLogEventType)
|
||||
|
||||
**所属字段**: `GroupBatchStatusLogItemVO.eventType` | **类型**: `String` | **枚举类**: `com.hulalv.order.groupbatch.enums.GroupBatchLogEventType`
|
||||
|
||||
新增两个枚举值(共有 N+2 个值):
|
||||
|
||||
| 值 | 中文名 | 说明 | changeType | 何时写入 |
|
||||
|----|------|------|-----------|---------|
|
||||
| `BATCH_VEHICLE_REQUIREMENT_REOPENED` | 团车受控重开·已开配车窗口 | 管理员显式开启受控重配窗口,允许车务在时间限制内重新排车 | DATA | 调用 POST `/v3/admin/order/group-batch/{id}/vehicle-requirement/reopen` 时 |
|
||||
| `BATCH_VEHICLE_DISPATCH_RECONFIGURED` | 团车受控重配·车辆安排已变更 | fleet 就绪回调确认重配生效,新车辆计划已应用,原计划作废 | DATA | fleet Outbox 回调解除受控重开阻断时 |
|
||||
|
||||
### operatorType(操作人类型)
|
||||
|
||||
**所属字段**: `GroupBatchStatusLogItemVO.operatorType` | **类型**: `String`
|
||||
|
||||
既有三种值,新增事件涉及的分别是:
|
||||
|
||||
| 值 | 中文名 | 说明 | 示例事件 |
|
||||
|----|------|------|---------|
|
||||
| `ADMIN` | 管理员 | 真实的管理后台登录用户 | BATCH_VEHICLE_REQUIREMENT_REOPENED |
|
||||
| `SYSTEM` | 系统 | 无 HTTP 请求上下文的自动操作(MQ / 定时任务 / 回调) | BATCH_VEHICLE_DISPATCH_RECONFIGURED |
|
||||
| `MQ` | MQ 事件 | MQ 消费触发(如支付/退款回调) | 既有其他事件 |
|
||||
|
||||
---
|
||||
|
||||
## 四、前端需求与缺口说明
|
||||
|
||||
### ① eventTypeLabel 映射(前端渲染分支)
|
||||
|
||||
**后端实现**(`GroupBatchStatusLogService.java:237-248`):
|
||||
```java
|
||||
private String eventTypeLabel(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (GroupBatchLogEventType type : GroupBatchLogEventType.values()) {
|
||||
if (type.getValue().equals(value)) {
|
||||
return type.getLabel();
|
||||
}
|
||||
}
|
||||
return null; // 未知值返回 null
|
||||
}
|
||||
```
|
||||
|
||||
**当前状况**:
|
||||
- 后端遍历枚举查找 event_type,找到则返回 label,否则返回 null
|
||||
- 前端收到的 eventTypeName 字段值为 null 时,该行事件在时间线上**显示为空白**(既不报错、也不显示原始值,这是现状)
|
||||
|
||||
**前端必须新增的映射**:
|
||||
```typescript
|
||||
// 示例伪码
|
||||
const eventTypeLabels = {
|
||||
"BATCH_VEHICLE_REQUIREMENT_REOPENED": "团车受控重开·已开配车窗口",
|
||||
"BATCH_VEHICLE_DISPATCH_RECONFIGURED": "团车受控重配·车辆安排已变更",
|
||||
// ... 既有其他 event_type ...
|
||||
};
|
||||
```
|
||||
|
||||
**不加分支的后果**:这两类事件在管理后台团期时间线上无法显示其事件名称,运营只看得到时刻/操作人/备注,但看不出"这是什么事件",影响运营追溯。
|
||||
|
||||
### ② 重配事件的操作人恒为 SYSTEM(不能拿 extra 字段顶替)
|
||||
|
||||
**源码事实**(`GroupBatchService.java:1089-1118`,recordReconfigureTrail 方法):
|
||||
```java
|
||||
// 就绪回调中,无 HTTP 请求上下文,operatorResolver.resolve() 返回 SYSTEM
|
||||
Operator operator = operatorResolver.resolve(); // → Operator.system()
|
||||
groupBatchStatusLogService.recordDataChange(groupBatchId,
|
||||
GroupBatchLogEventType.BATCH_VEHICLE_DISPATCH_RECONFIGURED,
|
||||
batch == null ? null : GroupBatchStatus.fromCode(batch.getBatchStatus()),
|
||||
content, window.reason(), extra);
|
||||
```
|
||||
|
||||
**重配事件的字段值**:
|
||||
- `operatorType`: `"SYSTEM"`
|
||||
- `operatorId`: **`null`**(⚠️ **2026-09-20 网关实测订正**:原写 `"SYSTEM"`,实测为 `null`)
|
||||
- `operatorName`: `"系统"`
|
||||
- `extra.reconfigureOpenedBy`: 开窗管理员 ID(**不是**重配操作人)
|
||||
- `extra.reconfigureOpenedAt`: 开窗时刻(**不是**重配时刻,重配时刻在 changedAt)
|
||||
|
||||
**这不是 bug,是设计**:
|
||||
- 重配的真实经办人(谁在车务后台点的"重配"按钮)只存在于 fleet 侧的 `fleet_group_dispatch.updated_by`
|
||||
- order-v3 是接收者,fleet 回调进来时没有经办人载荷
|
||||
- `extra.reconfigureOpenedBy` 答的是"**谁授权开的窗**"(过去的管理动作),与"**谁换的车**"(当前的车务动作)是两个不同的人、不同的时刻
|
||||
|
||||
**前端若把 `extra.reconfigureOpenedBy` 显示成"经办人",就是把一个错误的责任人写到审计页面上**。
|
||||
- ✅ 正确做法:operatorName 显示"系统",content 或 extra.reconfigureOpenedBy 里可补充说明"开窗人是谁"
|
||||
- ❌ 错误做法:拿 extra.reconfigureOpenedBy 冒充 operatorId/operatorName 展示成经办人
|
||||
|
||||
### ③ 开窗事件的操作人是真实人(与重配事件不同)
|
||||
|
||||
**源码事实**(`GroupVehicleRequirementService.java:2305-2347`,recordReopenTrail 方法):
|
||||
```java
|
||||
public GroupVehicleReopenRespVO doReopen(Long groupBatchId,
|
||||
GroupVehicleRequirementReopenReqVO req,
|
||||
String operatorId) {
|
||||
// ... 业务逻辑 ...
|
||||
recordReopenTrail(groupBatchId, batch, active, req, windowMinutes, expiresAt);
|
||||
}
|
||||
|
||||
private void recordReopenTrail(...) {
|
||||
// recordDataChange 内部调 operatorResolver.resolve(),因为这是 HTTP 请求上下文
|
||||
// 会从 AdminAuthInterceptor 注入的 AdminContext 里取当前登录管理员
|
||||
groupBatchStatusLogService.recordDataChange(groupBatchId,
|
||||
GroupBatchLogEventType.BATCH_VEHICLE_REQUIREMENT_REOPENED,
|
||||
GroupBatchStatus.fromCode(batch.getBatchStatus()), content, req.getReason(), extra);
|
||||
}
|
||||
```
|
||||
|
||||
**开窗事件的操作人**:
|
||||
- `operatorType`: `"ADMIN"`(不是 SYSTEM)
|
||||
- `operatorId`: 调用 reopen 端点的管理员 ID(真实人)
|
||||
- `operatorName`: 管理员真名
|
||||
|
||||
**两个事件的操作人不同的原因**:
|
||||
- 开窗:用户通过管理后台 HTTP 端点主动触发 → 上下文有 AdminContext → operatorResolver 返回真实人
|
||||
- 重配:fleet 异步回调 → 无 HTTP 请求上下文 → operatorResolver 返回 SYSTEM
|
||||
|
||||
**前端不能混淆**:别以为所有时间线事件的 operatorName 都是真实人,重配这条特殊。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
新增行落在 `group_batch_status_log` 表:
|
||||
|
||||
| 事件 | 写入时机 | from_status | to_status | change_type | 示例 |
|
||||
|------|----------|------------|-----------|-------------|------|
|
||||
| BATCH_VEHICLE_REQUIREMENT_REOPENED | 调用 POST `/v3/admin/order/group-batch/{id}/vehicle-requirement/reopen` 时 | PENDING_DEPARTURE | PENDING_DEPARTURE | DATA | 管理员张三在 2026-09-20 10:30 开窗 |
|
||||
| BATCH_VEHICLE_DISPATCH_RECONFIGURED | fleet 就绪回调解除受控重开阻断时 | PENDING_DEPARTURE | PENDING_DEPARTURE | DATA | fleet 确认换车后在 2026-09-20 11:45 生效 |
|
||||
|
||||
**关键**:两个事件都不改变团期 batch_status,`from_status` 与 `to_status` 相同。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **库里存在历史脏值** → 后端 eventTypeLabel 返回 null → 前端时间线该行显示空白或兜底处理
|
||||
- **并发写入** → 开窗事务与重配事务独立,都跟随各自的外层事务(不丢失)
|
||||
- **幂等性** → 开窗已有生效窗口时幂等返回既有令牌(不重复写库);普通首配回调、常规刷新回调若无受控重开阻断时 → CAS 0 行返回 → 不落本事件(常态)
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 接口行为对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 时间线中开窗操作的可见性 | 三个字段存库但页面不展示 | 新增一条 DATA 类事件,时间线上可见 |
|
||||
| 时间线中重配操作的可见性 | 无记录 | 新增一条 DATA 类事件,标明"车已换、谁开的窗、老计划作废" |
|
||||
| 事件类型枚举 | 共 N 种 | 新增 2 种,共 N+2 种 |
|
||||
| 前端事件渲染 | 既有 N 种 event_type 的映射 | 需新增 BATCH_VEHICLE_REQUIREMENT_REOPENED / BATCH_VEHICLE_DISPATCH_RECONFIGURED 两条映射 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。既存接口返回值扩展,老 event_type 不变
|
||||
- **前端是否必须同步上线**: **是**。不新增渲染映射会导致新增两类事件在时间线上显示为空白
|
||||
- **前端 workaround 清理点**:
|
||||
- 若前端已有"event_type 未知时显示原始 code"的兜底逻辑,可临时显示 `BATCH_VEHICLE_REQUIREMENT_REOPENED` 等原始值
|
||||
- 但正式上线必须使用中文标签,不能长期显示原始值(运营不认识)
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 管理后台 - 团期详情页 - 状态流水/时间线面板(新增两类事件的渲染)
|
||||
- **零影响**:
|
||||
- 团期主状态推进(batch_status 机制不变)
|
||||
- 用车需求状态机(需求行读写逻辑不变)
|
||||
- 订单详情、C 端行程页面(无此数据源)
|
||||
- 旧团期数据(存量团期的时间线不会补充新事件)
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
后端逻辑验证(提交 ab92377c7):
|
||||
|
||||
```
|
||||
POST /v3/admin/order/group-batch/8000001/vehicle-requirement/reopen
|
||||
请求体: { "scopeGroupCodes": ["GRP001"], "scopeDates": ["2026-09-21"], "reason": "test reason" }
|
||||
→ 200, group_batch_status_log 新增 1 行 BATCH_VEHICLE_REQUIREMENT_REOPENED ✓
|
||||
|
||||
fleet 就绪回调(模拟)
|
||||
request: {"groupBatchId": 8000001, "requirementId": 7000001, "requirementVersion": 2, "planVersion": "v1"}
|
||||
→ 200, group_batch_status_log 新增 1 行 BATCH_VEHICLE_DISPATCH_RECONFIGURED ✓
|
||||
|
||||
GET /v3/admin/order/group-batch/8000001/status-logs
|
||||
→ 200, 返回值中 eventTypeName 正确映射(后端已验证) ✓
|
||||
|
||||
前端渲染验证:**待前端补充映射后联调**
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 | 是否仍有效 |
|
||||
|-------|-------|------|---------|
|
||||
| #7986 | #7986 | 受控重配窗口核心机制(团车需求侧) | ✅ 有效 |
|
||||
| #8039 | #7987 | 本 PR - 补团期时间线留痕 | ✅ 最新 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- **后端枚举定义**: [`hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/enums/GroupBatchLogEventType.java`](https://git.1814.love:8443/wx/HL/blob/dev-v3/hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/enums/GroupBatchLogEventType.java)
|
||||
- BATCH_VEHICLE_REQUIREMENT_REOPENED 定义:第 314-344 行
|
||||
- BATCH_VEHICLE_DISPATCH_RECONFIGURED 定义:第 347-376 行
|
||||
|
||||
- **时间线服务**: [`hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/service/GroupBatchStatusLogService.java`](https://git.1814.love:8443/wx/HL/blob/dev-v3/hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/service/GroupBatchStatusLogService.java)
|
||||
- eventTypeLabel 实现:第 237-248 行
|
||||
|
||||
- **开窗留痕**: [`hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/service/GroupVehicleRequirementService.java`](https://git.1814.love:8443/wx/HL/blob/dev-v3/hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/service/GroupVehicleRequirementService.java)
|
||||
- recordReopenTrail 方法:第 2329-2387 行
|
||||
|
||||
- **重配留痕**: [`hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/service/GroupBatchService.java`](https://git.1814.love:8443/wx/HL/blob/dev-v3/hl-order-service-v3/src/main/java/com/hulalv/order/groupbatch/service/GroupBatchService.java)
|
||||
- recordReconfigureTrail 方法:第 1089-1118 行
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7987](https://git.1814.love:8443/wx/HL/issues/7987)
|
||||
- **PR**: [#8039](https://git.1814.love:8443/wx/HL/pulls/8039)
|
||||
- **Merge commit**: [ab92377c7](https://git.1814.love:8443/wx/HL/commit/ab92377c7)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
- **前端对接人**: @mmg(hl-ui 仓库,需新增 eventTypeLabel 映射)
|
||||
@@ -0,0 +1,380 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7988"
|
||||
title: "团期配车刷新状态补只读观测口(读团期正式用车需求新增七个只读字段)"
|
||||
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)GET /v3/admin/order/group-batch/2101524283048263681/vehicle-requirement,code=200,本篇登记的七个只读观测字段全部出现且字段名/类型与正文一字对应:planRefreshState=null / planRefreshReplayCount=0 / blockedStage=RESOURCE_PREPARING / planRefreshStalled=false / planRefreshStalledReason=null / planRefreshTimeoutAt=null / planRefreshReplayExhausted=false。逐字段对照无出入。⚠️ 正文示例值原来取自单元测试常量,现已有这组真实取值作为实测错开对照。 【上一轮原注】backend_status=deployed 的判据(2026-09-20 18:35 复核):hl-order-service-v3 测试服部署点 e179e09bd(2026-09-20 17:55:02 发布),`git merge-base --is-ancestor 587af48cd e179e09bd` = true,故本篇端点的代码确已在测试服运行的字节里。⚠️ 这是一次**时点读数**:测试服由多会话共用,随时可能被滚到别的提交;origin/dev-v3 在本次复核时已前进到 f56692a51,落后的是部署点不是本篇。 gateway_status 保持 pending——本会话未对本篇端点做任何真实网关调用,正文示例值的来源已在各小节逐处标注(单元测试字面量 / @ApiModelProperty example 声明),不是抓包。待网关复验后再置 verified,**不得为了让门禁变绿改这个位**。 【本轮之前的原注,保留供追溯口径变化】代码已合入 dev-v3(PR #8033,squash 提交 587af48cd,经 git log origin/dev-v3 --oneline | grep 7988 核实存在于 origin/dev-v3)。backend_status 刻意保持 pending——hl-order-service-v3 尚未部署测试服到该提交,未做任何真实网关调用;gateway_status 同样保持 pending。正文请求/响应示例的具体数值来自随 PR 一起合入的单元测试常量(GroupVehicleRequirementPlanRefreshObservationTest:groupBatchId=7201、requirementId=92001、blockedStage=PENDING_DEPARTURE、windowMinutes=120 等)与源码 @ApiModelProperty(example=...) 声明,逐一对源码核实过字段名/类型,但不是测试服网关抓包,具体取值以复验后实测为准。待管理者安排部署 + 网关复验后再把 backend_status/gateway_status 置 deployed/verified 并推送本文件;发布前不许为了过校验改状态位。"
|
||||
updated_at: "2026-09-20"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期配车刷新状态补只读观测口(工单 #7988)
|
||||
|
||||
> **存放目录**: 二期(order-v3 标签工单)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: #8033 | **Issue**: #7988 | **合并提交**: `587af48cd`
|
||||
> **日期**: 2026-09-20
|
||||
> **影响范围**: 管理后台「团期配车」编辑页,读团期正式用车需求响应体新增七个只读字段(配车刷新状态观测块)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
改动前,"配车刷新到底死没死"这件事**只能通过写口的响应看到**——`requirement/confirm` 与 `vehicle-requirement/reopen` 两个写口的响应体里带着 `planRefreshState` 等状态列,而**只读**的 `GET .../vehicle-requirement` 没有。于是运营/车务要看一眼"刷新是不是卡住了",唯一手段就是去调 `confirm`;而 `confirm` 一旦走到受控重投分支,会**顺手把状态从 `FAILED` 改回 `PENDING`**——查看这个动作本身把现场改掉了,`FAILED` 因此在管理后台侧根本读不到。
|
||||
|
||||
本次在只读的 `GET .../vehicle-requirement` 响应体上补了七个只读字段,**全程不产生任何写**(已有单测 `get_stalledRequirement_performsNoWrite` 与反射断言 `get_declaredShape_carriesNoLockOrTransaction` 钉住这一点)。
|
||||
|
||||
🔴 **前端渲染口径必须看 `planRefreshStalled`,不能看 `planRefreshState`**:`planRefreshState=PENDING` 对应**三种互不相同的现实**(正常在途 / 命令已终态失败但状态列没回写 / 已超过时限卡死),三者在 `planRefreshState` 这一个字段上**逐字相同**,无法用它自己区分。详见「四、契约约束与正确调用方式」。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期正式用车需求上挂着一条"照这份需求刷新 fleet 配车计划"的异步链路(#7442 PR-C2 引入),状态机三态 `NONE`(列值 NULL)/`PENDING`/`DONE`/`FAILED`,配套 `blocked_stage`(阻断阶段快照)、`plan_refresh_replay_count`(人工受控重投次数,上限 5)等库列。这些库列此前只在两个**写口**的响应里露出:
|
||||
|
||||
- `POST .../requirement/confirm`(确认整份需求,会顺手触发一次受控重投)
|
||||
- `POST .../vehicle-requirement/reopen`(重新开窗)
|
||||
|
||||
运营想知道"这个团的配车刷新到底怎么样了",只能调 `confirm`——而 `confirm` 会把 `FAILED` 重新入队成 `PENDING`。这意味着:**只要有人查看过一次,`FAILED` 这个最需要被看到的终态就从管理后台侧消失了**。本单在唯一的无副作用观测口——读团期正式用车需求——上补齐这七个字段,让"查一下"这个动作不再改变现场。
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 看配车刷新状态的入口 | 只能调写口 `confirm`/`reopen` | 新增只读入口 `GET .../vehicle-requirement`(以及共用同一响应体的 `PUT`/`withdraw`/`waive`) |
|
||||
| 查看动作是否会改变现场 | 会(`confirm` 顺手重投,`FAILED → PENDING`) | 不会(读口全程零写,含反射断言钉住方法上无 `@Lock4j`/`@Transactional`/`@Idempotent`) |
|
||||
| `PENDING` 的三种现实是否可区分 | 无对应字段,读原值也分不开 | `planRefreshStalled`/`planRefreshStalledReason` 把三种现实拆开 |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 读团期正式用车需求 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 修改接口 | 响应体新增 7 个只读字段(配车刷新观测块) |
|
||||
|
||||
⚠️ **该响应体 `GroupVehicleRequirementRespVO` 同时被 `PUT`/`withdraw`/`waive` 三个写口共用**(源码 javadoc 明确标注"四个端点共用这一个响应体"),所以这 7 个字段同样会出现在:
|
||||
|
||||
- `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`(保存草稿)
|
||||
- `POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/withdraw`(整份撤回)
|
||||
- `POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/waive`(声明整团免车)
|
||||
|
||||
这三个写口的字段值语义与 `GET` 完全相同(都是"该次写入后的现场"只读投影),本次未改动这三个端点自身的请求契约或业务逻辑,故不在上表单独列出,只在这里明确提示;三节共用「接口 1」的「六.5 枚举」「六.6 对比」解释。
|
||||
|
||||
`POST .../requirement/confirm` 与 `POST .../vehicle-requirement/reopen` 两个写口**一字未改**——它们的响应体分别是 `GroupBatchRequirementConfirmRespVO` 与 `GroupVehicleReopenRespVO`,是两个独立的 VO,本次未触碰。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 读团期正式用车需求 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
|
||||
|
||||
**VO**: `(PathVariable groupBatchId, 无请求体) → GroupVehicleRequirementRespVO`
|
||||
|
||||
(源码核对:`GroupBatchRequirementController.java:180-192`、`GroupVehicleRequirementRespVO.java`、`GroupVehicleRequirementService.java:2480-2545` `fillPlanRefreshObservation`/`resolvePlanRefreshStalledReason`、`GroupVehiclePlanRefreshStalledReason.java`)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期配车管理员编辑页打开时调用,回填该团当前正式用车需求(分组/逐日行/整份状态)。本次改动后,它**同时是"配车刷新死没死"在 admin 侧唯一的无副作用观测口**:页面可以在不触发任何写操作的前提下,展示"这个团的配车刷新是否需要人工介入、卡在哪一步、还能不能自己点确认重试"。该团期尚未形成正式需求时,`data` 整体为 `null`(这不是新行为,改动前就是如此),此时七个新字段自然也不存在。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
|
||||
|
||||
(本端点无请求体、无 Query 参数;本次改动未新增任何入参)
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | String | 正式需求主键(雪花) |
|
||||
| groupBatchId | String | 团期聚合主键 |
|
||||
| status | String | 需求状态:`DRAFT`/`CONFIRMED`/`DISPATCHED`/`DONE`/`PENDING_RECONFIRM`/`CANCELLED`;本次未改动 |
|
||||
| version | Integer | 乐观锁版本号 |
|
||||
| remark | String | 整份备注 |
|
||||
| confirmedBy | String | 整份确认人(`DRAFT` 时为 `null`) |
|
||||
| confirmedAt | LocalDateTime | 整份确认时间(`DRAFT` 时为 `null`) |
|
||||
| **planRefreshState** | String | **【本次新增】** 配车计划刷新状态原值:`null`=从未登记过刷新(多数团期的正常态,不要当异常画)/`PENDING`=命令已发出等 fleet 回来/`DONE`=本轮刷新已闭环/`FAILED`=刷新已被判死。⚠️ 单看本字段判不了"要不要处置",看 `planRefreshStalled` |
|
||||
| **planRefreshReplayCount** | Integer | **【本次新增】** 人工受控重投累计次数(管理员点确认触发的,不含自动重试);上限 5;从未重投过为 `0` 或 `null` |
|
||||
| **blockedStage** | String | **【本次新增】** 团期阻断阶段快照:`null`=未阻断;非空(`RESOURCE_PREPARING`/`MATERIAL_PREPARING`/`PENDING_DEPARTURE`)表示该团正卡在这一步等重新配车。展示口径,不是门禁输入——真正挡住团期推进的是 `order_group_batch.vehicle_ready=false` |
|
||||
| **planRefreshStalled** | Boolean | **【本次新增,本块的行动判据,恒非 null】** `true`=刷新不会自愈、必须有人处置(该团出不了团);`false`=无需动作(含从未登记刷新/正常在途/已闭环)。**判"要不要现在找人"看本字段,不要看 `planRefreshState`** |
|
||||
| **planRefreshStalledReason** | String | **【本次新增】** 停滞归因:`STATE_FAILED`=刷新已被判死(自动重试耗尽或被 fleet 判定性终结)/`COMMAND_FAILED`=命令行已失败但状态列没回写(同样是死了)/`TIMEOUT`=超过本轮窗口时限仍无结果。`planRefreshStalled=false` 时恒为 `null` |
|
||||
| **planRefreshTimeoutAt** | LocalDateTime | **【本次新增】** 本轮刷新的"再等就没意义"时刻 = 发起时刻 + 本轮窗口时长(管理员重开时自定,缺省 120 分钟)。仅 `planRefreshState=PENDING` 且已登记发起时刻时有值,其余为 `null` |
|
||||
| **planRefreshReplayExhausted** | Boolean | **【本次新增,恒非 null】** 人工重投额度是否已耗尽:`true`=已达 5 次上限,再点确认只会收到 `809210`,提示应改为"联系后台排查";`false`=停滞时仍可由管理员重新确认触发一次重投。只在 `planRefreshStalled=true` 时才有行动意义 |
|
||||
| groups[] | Array | 全部乘车分组(整团免车态为空数组);本次未改动 |
|
||||
| groups[].groupId / groupCode / vehicleType / serviceStartDate / serviceEndDate / days[] | - | 分组回显字段,本次未改动 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/7201/vehicle-requirement
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
数值取自单元测试常量(`GroupVehicleRequirementPlanRefreshObservationTest`,非网关抓包,见 frontmatter `status_note`),展示 `FAILED` 停滞态(`get_planRefreshFailed_projectsStateFailedStalled` 用例覆盖的场景):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"requirementId": "92001",
|
||||
"groupBatchId": "7201",
|
||||
"status": "CONFIRMED",
|
||||
"version": 3,
|
||||
"remark": null,
|
||||
"confirmedBy": "10086",
|
||||
"confirmedAt": "2026-09-18 09:00:00",
|
||||
"planRefreshState": "FAILED",
|
||||
"planRefreshReplayCount": 1,
|
||||
"blockedStage": "PENDING_DEPARTURE",
|
||||
"planRefreshStalled": true,
|
||||
"planRefreshStalledReason": "STATE_FAILED",
|
||||
"planRefreshTimeoutAt": null,
|
||||
"planRefreshReplayExhausted": false,
|
||||
"groups": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
补充第二种取值组合——`PENDING` 且未超时(`get_pendingWithinWindow_projectsNotStalled` 用例),只摘录七个新字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"planRefreshState": "PENDING",
|
||||
"planRefreshReplayCount": 0,
|
||||
"blockedStage": "PENDING_DEPARTURE",
|
||||
"planRefreshStalled": false,
|
||||
"planRefreshStalledReason": null,
|
||||
"planRefreshTimeoutAt": "2026-09-20 13:20:00",
|
||||
"planRefreshReplayExhausted": false
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ 与上一个响应对比:**`planRefreshState` 都可能是 `PENDING`(本例)或 `FAILED`(上例),但同为 `PENDING` 时 `planRefreshStalled` 还可能是 `true`**(命令已终态失败但状态列没回写、或已超时)——这正是本次改动要解决的"单看原值分不清三种现实"的问题,务必配合下面「四、契约约束」一起读。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
该团期尚未形成正式需求时,`data` 整体为 `null`(改动前既有行为,本次未改动)。**从未登记过任何刷新的团期**(多数团期的正常态)响应形如:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"requirementId": "92001",
|
||||
"groupBatchId": "7201",
|
||||
"planRefreshState": null,
|
||||
"planRefreshReplayCount": null,
|
||||
"blockedStage": null,
|
||||
"planRefreshStalled": false,
|
||||
"planRefreshStalledReason": null,
|
||||
"planRefreshTimeoutAt": null,
|
||||
"planRefreshReplayExhausted": false
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **`planRefreshState=null` 不是异常、不是数据缺失**,是状态机里的 `NONE`(列值 NULL),没走过受控重开的团期恒为此态,前端不应画成异常/报错样式。本端点无下游依赖,不存在降级路径(全程只读本地 DB,`planRefreshOutboxId` 非空时最多额外读一次 outbox 命令状态,见「业务边界」)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589500,
|
||||
"message": "团期不存在",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **鉴权**:`permissionGuard.require(..., PERMISSION_DEMAND_CONFIRM)`,与 `PUT`/`withdraw`/`waive` 同一权限码,本次未改动。
|
||||
- **全程只读,零写**:本次改动新增的装配逻辑 `fillPlanRefreshObservation` 只做 `select`(团期存在性 + 需求主表 + 分组/逐日行 + 至多一次按 `planRefreshOutboxId` 读命令状态),不写任何表、不入队、不触发重投、不取锁、不开事务——已有单测 `get_stalledRequirement_performsNoWrite`(`verifyNoMoreInteractions` 钉死"该发生的读之外一次交互都不许有")与反射断言 `get_declaredShape_carriesNoLockOrTransaction`(`get` 方法上没有 `@Lock4j`/`@Transactional`/`@Idempotent`)双重覆盖。
|
||||
- **命令状态查询是有条件的**:只有 `planRefreshOutboxId != null` 且当前不是已知终态(非 `NONE`/`DONE`)时才会去查一次 `order_fleet_command_outbox` 的命令状态;从未登记过刷新(`NONE`)或已闭环(`DONE`)时**不会**多发这次查询——`verifyNoInteractions(orderFleetCommandOutboxService)` 在两种场景下都被单测钉住,避免在存量团期上放大成全表的额外查询。
|
||||
- **`NONE`/`DONE` 一律不判停滞,哪怕 `planRefreshOutboxId` 指向一行已失败的旧命令**:`reopen` 把状态归零时并不清 `planRefreshOutboxId` 这一列,如果直接套用"命令终态失败即停滞"的判据,会把一个刚刚重开、还没登记新刷新的团期误报成"已死",而那正是最不该报警的时刻。
|
||||
- **`planRefreshTimeoutAt` 与受控重开守卫(`809209`)共用同一份阈值计算**:两处各算一遍会出现"页面说已超时、重开却报 809209 不让开"的自相矛盾,本次改动把阈值计算抽成同一个私有方法复用。
|
||||
- **`reconfigureScope` 解析失败时兜底 120 分钟**,不会因为一条脏 JSON 让团期在页面上永远显示"还在跑"。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节只写响应字段的正确解读方式,不写 UI 渲染建议之外的后端行为规则(本端点无写操作,没有 payload 校验规则可写)。
|
||||
|
||||
### 🔴 判"要不要现在找人"只看 `planRefreshStalled`,不要看 `planRefreshState`
|
||||
|
||||
`planRefreshState=PENDING` 对应三种互不相同的现实,在这一个字段上**逐字相同**:
|
||||
|
||||
| 现实 | `planRefreshState` | `planRefreshStalled` | `planRefreshStalledReason` |
|
||||
|---|---|---|---|
|
||||
| 正常在途,等 fleet 回调 | `PENDING` | `false` | `null` |
|
||||
| 命令已终态失败,但状态列没回写(回写钩子没落成) | `PENDING` | `true` | `COMMAND_FAILED` |
|
||||
| 超过本轮窗口时限仍无结果 | `PENDING` | `true` | `TIMEOUT` |
|
||||
| 已被判死(自动重试耗尽或被 fleet 终结,状态列已回写) | `FAILED` | `true` | `STATE_FAILED` |
|
||||
|
||||
只读 `planRefreshState` 拿到的 `PENDING` 无法区分前三行——这正是本单要解决的问题。前端渲染逻辑必须以 `planRefreshStalled` 作为唯一的"要不要报警"判据。
|
||||
|
||||
### 渲染口径
|
||||
|
||||
- `planRefreshStalled=true` 必须是**显式告警态**(不是静默):
|
||||
- `planRefreshReplayExhausted=false` → 引导文案「重新确认」
|
||||
- `planRefreshReplayExhausted=true` → 文案改为「联系后台排查」,**别让运营继续点确认**(再点只会收到 `809210`)
|
||||
- `planRefreshStalled=false` 且 `planRefreshState=PENDING` → 展示「刷新中,预计 `planRefreshTimeoutAt` 前出结果」
|
||||
- `planRefreshState=null` → 不展示刷新相关的任何提示条(这是多数团期的正常态,不是"缺数据")
|
||||
- `planRefreshState=DONE` → 不展示告警,可选展示"已完成"的静态标记
|
||||
|
||||
### 无新增错误码
|
||||
|
||||
本端点是纯读口,本次改动不引入任何新错误码;`809210`/`809209` 是既有错误码(`#7442`/`#7442 PR-C2` 已定义),仅在前端引导用户点击**其他写口**(`confirm`/`reopen`)时才可能命中,本端点自身不会抛出它们。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录/网关未透传角色 → 401(网关拦截),既有行为
|
||||
- 团期不存在 → `589500`,既有行为
|
||||
- 该团期尚未形成正式需求 → `data=null`,既有行为,本次未改动
|
||||
- 老数据兼容:改动前落库的存量需求行 `plan_refresh_state`/`plan_refresh_replay_count`/`blocked_stage` 三列均可能是 `NULL`(未登记过刷新),响应对应投影为 `planRefreshState=null`/`planRefreshReplayCount=null`/`blockedStage=null`、`planRefreshStalled=false`、`planRefreshReplayExhausted=false`,**不是异常**
|
||||
- `reconfigure_scope` 解析失败(脏 JSON)时,超时窗口按默认 120 分钟兜底计算,不影响响应本身返回成功
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
### `planRefreshState`(`GroupVehicleRequirementRespVO.planRefreshState`,源码 `GroupVehiclePlanRefreshState` 状态机枚举,列值 NULL 对应 `NONE`)
|
||||
|
||||
**所属字段**: `planRefreshState` | **类型**: `String`(可空)
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `null` | 未登记 | 状态机 `NONE`,尚未登记过任何刷新;多数团期的正常态 |
|
||||
| `PENDING` | 刷新中 | 刷新命令已登记,等 fleet 刷完并把就绪意图投回来;需结合 `planRefreshStalled` 判断是否正常 |
|
||||
| `DONE` | 已闭环 | 就绪回调已通过两级判定并应用,本轮刷新完成 |
|
||||
| `FAILED` | 已判死 | 刷新已耗尽重试预算或被 fleet 判定性终结;不放行团期,出团门②持续拒绝 |
|
||||
|
||||
### `planRefreshStalledReason`(`GroupVehicleRequirementRespVO.planRefreshStalledReason`,源码 `GroupVehiclePlanRefreshStalledReason` 枚举,仅在读口投影中使用,不落库)
|
||||
|
||||
**所属字段**: `planRefreshStalledReason` | **类型**: `String`(可空,`planRefreshStalled=false` 时恒为 `null`)
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|----|------|------|
|
||||
| `STATE_FAILED` | 状态已判死 | `planRefreshState=FAILED`;去查 fleet 为什么刷不动 |
|
||||
| `COMMAND_FAILED` | 命令已死未回写 | 状态列仍 `PENDING`,但命令行已终态失败;去查回写状态的终态钩子为什么没落成 |
|
||||
| `TIMEOUT` | 已超时 | 状态列仍 `PENDING`,命令未终态失败,但已超过本轮窗口时限;去查命令是否卡在队列里 |
|
||||
|
||||
⚠️ 本枚举**只进响应,不进库、不进 `WHERE`**——它是三条既有判据(状态列/命令行状态/超时)在读的那一刻算出来的结论,不是持久化状态。
|
||||
|
||||
### `blockedStage`(`GroupVehicleRequirementRespVO.blockedStage`,值域同团期状态机的阶段码)
|
||||
|
||||
**所属字段**: `blockedStage` | **类型**: `String`(可空)
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| `null` | 未阻断 |
|
||||
| `RESOURCE_PREPARING` | 团期阶段:资源准备中 |
|
||||
| `MATERIAL_PREPARING` | 团期阶段:物资准备中 |
|
||||
| `PENDING_DEPARTURE` | 团期阶段:待出团 |
|
||||
|
||||
⚠️ 本字段是**展示口径**,不是门禁输入:真正挡住团期推进的是 `order_group_batch.vehicle_ready=false`,本字段只是"卡在哪一步"的留痕快照。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `GET .../vehicle-requirement` 响应 `planRefreshState` | 不存在(只在 `confirm`/`reopen` 两个写口的响应里有) | **新增**只读投影 |
|
||||
| 同响应 `planRefreshReplayCount` | 不存在 | **新增** |
|
||||
| 同响应 `blockedStage` | 不存在 | **新增** |
|
||||
| 同响应 `planRefreshStalled` | 不存在(任何端点都没有这个派生结论) | **新增**,全站唯一来源 |
|
||||
| 同响应 `planRefreshStalledReason` | 不存在 | **新增**,全站唯一来源 |
|
||||
| 同响应 `planRefreshTimeoutAt` | 不存在 | **新增**,全站唯一来源 |
|
||||
| 同响应 `planRefreshReplayExhausted` | 不存在 | **新增**,全站唯一来源 |
|
||||
| `PUT`/`withdraw`/`waive` 三个写口的响应(共用同一 VO) | 同样没有这七个字段 | 同样新增(值 = 该次写入后的现场) |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 查看配车刷新是否卡住 | 只能调 `confirm`,而它会顺手做一次受控重投,把 `FAILED` 改回 `PENDING`——**查看动作本身会改变现场** | 调只读的 `GET` 即可看到,**零副作用** |
|
||||
| `PENDING` 的三种现实(在途/命令已死未回写/超时)是否可区分 | 不可区分(原值三者相同) | 可区分(`planRefreshStalled`/`planRefreshStalledReason`) |
|
||||
| `FAILED` 终态在 admin 侧是否可读到 | 事实上读不到(查看会把它改成 `PENDING`) | 可以,因为查看不再产生写 |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否——本次只在既有响应体上新增字段,不删除、不改名、不改变既有字段(`requirementId`/`status`/`groups[]` 等)的类型或取值规则;旧前端忽略这七个新字段,行为与改动前逐字节一致
|
||||
- **前端是否必须同步上线**: 否,是纯新增只读字段,前端可延后接入;接入前不影响现有编辑页回填逻辑
|
||||
- **前端 workaround 清理点**: 若前端此前为了看一眼 `FAILED` 状态而在编辑页刻意调用过 `confirm`(哪怕不点击真正的确认按钮,只为了读响应里的状态字段),这类 workaround **必须撤除**——继续这么用会重新触发受控重投,产生非预期的写副作用(把 `FAILED` 悄悄改回 `PENDING`,还会消耗一次人工重投额度)
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 响应体新增的七个只读字段(以及共用同一 VO 的 `PUT`/`withdraw`/`waive` 三个写口的响应)
|
||||
- **零影响**:
|
||||
- `POST .../requirement/confirm`、`POST .../vehicle-requirement/reopen` 两个写口的请求/响应契约**一字未改**(各自独立的 VO,本次未触碰)
|
||||
- `GET`/`PUT`/`withdraw`/`waive` 四端点原有字段(`requirementId`/`status`/`version`/`remark`/`confirmedBy`/`confirmedAt`/`groups[]` 等)**零变更**
|
||||
- 无新增错误码,既有错误码(`589500`/`809209`/`809210` 等)行为不变
|
||||
- **无新增 DB 列/表/索引**:`plan_refresh_state`/`plan_refresh_replay_count`/`blocked_stage`/`plan_refresh_requested_at`/`plan_refresh_outbox_id` 五列均由 `#7442 PR-C2` 建好,本次只是把已有列在只读口上多投影一次,外加一个只存在于响应层、不落库的派生枚举 `GroupVehiclePlanRefreshStalledReason`
|
||||
- 受控重开守卫(`809209`/`809210`)的判定逻辑本身未改,本次只是把它复用的阈值计算函数抽出来给只读投影共用
|
||||
- 团期配车其余端点(就绪判定、共用关系、派单预校验/候选等,见 #7444/#8013 changelog)零影响
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
⚠️ **本篇尚无测试服/网关读数**(`backend_status=pending`、`gateway_status=pending`,hl-order-service-v3 尚未部署到该提交),如实标注为**待部署后补**,不把单测读数伪装成测试环境验证。
|
||||
|
||||
以下是随 PR #8033 一并合入的单元测试证据(`GroupVehicleRequirementPlanRefreshObservationTest`,纯 Mockito,交接时未重新执行,仅列出用例名供核实其覆盖范围):
|
||||
|
||||
```
|
||||
get_planRefreshFailed_projectsStateFailedStalled — FAILED 终态可见且判为停滞(STATE_FAILED)
|
||||
get_noPlanRefreshRegistered_projectsQuiet — 从未登记过刷新一律不报警,且不多查 outbox
|
||||
get_planRefreshDoneWithStaleFailedCommand_projectsQuiet — DONE 不因残留失败命令行被误报,且不多查 outbox
|
||||
get_pendingWithinWindow_projectsNotStalled — PENDING 且未超时 = 不停滞,给出 timeoutAt
|
||||
get_pendingWithFailedCommand_projectsCommandFailed — PENDING 但命令已死 = 停滞(COMMAND_FAILED)
|
||||
get_pendingBeyondWindow_projectsTimeout — PENDING 且超时 = 停滞(TIMEOUT)
|
||||
get_pendingWithUnparsableScope_fallsBackToDefaultWindow — 脏 JSON 兜底默认 120 分钟窗口
|
||||
get_replayCount_marksExhaustedAtLimit(参数化 4 组) — replayCount≥5 时 planRefreshReplayExhausted=true
|
||||
get_stalledRequirement_performsNoWrite — 读链路零写(verifyNoMoreInteractions 收口)
|
||||
get_declaredShape_carriesNoLockOrTransaction — 方法签名上无 @Lock4j/@Transactional/@Idempotent
|
||||
```
|
||||
|
||||
**待办**:管理者安排 hl-order-service-v3 部署测试服到 `587af48cd` 之后,需补一次真实网关调用(至少覆盖"从未登记"「PENDING 未超时」「FAILED」三种形态),核实七个字段的真实返回值,再把 `backend_status`/`gateway_status` 置 `deployed`/`verified` 并推送本文件。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7988](https://git.1814.love:8443/wx/HL/issues/7988)
|
||||
- 关联 PR: [wx/HL#8033](https://git.1814.love:8443/wx/HL/pulls/8033)(squash 合并至 dev-v3 @`587af48cd`)
|
||||
- 配车刷新状态机与受控重开守卫的背景见 `#7442 PR-C2`(`GroupVehiclePlanRefreshState`/`GroupVehiclePlanRefreshStateMachineConfig` 源文件头注释)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7988](https://git.1814.love:8443/wx/HL/issues/7988)
|
||||
- **PR**: [#8033](https://git.1814.love:8443/wx/HL/pulls/8033)
|
||||
- **Merge commit**: [587af48cd](https://git.1814.love:8443/wx/HL/commit/587af48cda93139f3fd4aa7ca06bc7b9419be52b)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: wx(GIT)
|
||||
- **前端负责人**: mmg
|
||||
@@ -0,0 +1,443 @@
|
||||
---
|
||||
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
|
||||
在新工单中引用
屏蔽一个用户