--- schema: "hl-changelog/v2" ticket: "7304" title: "新增团期状态流水(时间线)只读端点 GB-ADM-096:GET /v3/admin/order/group-batch/:groupBatchId/status-logs" consumer: "admin" author: "jw(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "51ca6946" target_release: "" verified_at: "2026-09-09" status_note: "新增一个只读端点,把此前只写不可读的 group_batch_status_log 暴露给管理端。后端已部署 TEST 并经真实网关实测 AC-1~AC-6(含读回 #7283 的 BATCH_PRODUCT_CANCEL_FAILED 兜底留痕)。前端待接:团期详情加一个「操作记录/时间线」区块——STATUS 类画 fromStatusName→toStatusName 箭头,DATA 类只渲染 content;中文名一律用后端给的 eventTypeName/fromStatusName/toStatusName,不要自建映射表(枚举会继续新增);operatorType 可能是 SYSTEM/MQ,此时 operatorId 为 null、operatorName 是「系统」之类固定文案。权限与团期看板/芯片同码 group-batch:view。" updated_at: "2026-09-09" base: "dev-v3" --- # 团期状态流水(时间线)只读端点 ## 一、给前端的一句话 团期的「操作记录 / 时间线」现在有接口了。**一次 GET 拿到该团期从建团到当前的全部事件**,按时间升序,中文名后端已经拼好,前端直接渲染即可。这是个纯新增端点,不动任何既有接口。 ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|---|---|---|---|---| | 1 | 团期状态流水(时间线) | GET | `/v3/admin/order/group-batch/:groupBatchId/status-logs` | 新增 | 一次返回该团期全部状态流水,按时间升序;此前该表只写不可读 | ## 三、接口详情 ### 1. 团期状态流水(时间线) `GET /v3/admin/order/group-batch/:groupBatchId/status-logs` **VO**: `GroupBatchStatusLogItemVO` #### 使用场景 团期详情页的「操作记录 / 时间线」区块。此前这张表只写不读——建团、成团、取消成团、流团、退单审批、转期、导出、合同保险十余条链路都往里写,但页面上一条也看不到。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `groupBatchId` | path | Long | 是 | 雪花 ID,按字符串传 | 团期 ID | 无 query、无 body。**不分页**——单团期的流水条数由其生命周期事件数决定(量级几十条),不是无界列表。 #### 出参 `Result>`,按 `changedAt` **升序**。 | 字段 | 类型 | 说明 | |---|---|---| | `logId` | String | 流水 ID(雪花) | | `groupBatchId` | String | 团期 ID(雪花) | | `changeType` | String | `STATUS`=状态流转 / `DATA`=状态内数据变更 | | `eventType` | String | 事件类型值,如 `BATCH_GROUP` | | `eventTypeName` | String | 事件类型中文名,如「成团」;枚举外的历史值为 `null` | | `fromStatus` | String | 变更前团期状态;首次建团为 `null` | | `fromStatusName` | String | 变更前状态中文名,如「招募中」 | | `toStatus` | String | 变更后团期状态 | | `toStatusName` | String | 变更后状态中文名,如「资源准备中」 | | `content` | String | 展示文本,后端已拼好,前端直接渲染 | | `reason` | String | 理由 / 备注,可为 `null` | | `operatorType` | String | `USER` / `ADMIN` / `SYSTEM` / `MQ` | | `operatorId` | String | 操作人 ID(雪花);`SYSTEM` / `MQ` 场景为 `null` | | `operatorName` | String | 操作人姓名快照;系统场景为「系统」之类固定文案 | | `extra` | Object | 附加快照,**已解析成对象**(不是转义 JSON 字符串);无附加数据时为 `null` | | `changedAt` | String | `yyyy-MM-dd HH:mm:ss` | #### 请求示例 ```http GET /v3/admin/order/group-batch/2097500233511362561/status-logs Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": [ { "logId": "2097500233519751171", "groupBatchId": "2097500233511362561", "changeType": "STATUS", "eventType": "BATCH_CREATE", "eventTypeName": "建团", "fromStatus": null, "fromStatusName": null, "toStatus": "RECRUITING", "toStatusName": "招募中", "content": "建团:秋日川西小环线", "reason": null, "operatorType": "ADMIN", "operatorId": "1001", "operatorName": "admin", "extra": null, "changedAt": "2026-09-09 09:40:00" }, { "logId": "2097501083860418561", "groupBatchId": "2097500233511362561", "changeType": "DATA", "eventType": "BATCH_CONTRACT_ISSUE", "eventTypeName": "手动开合同/保险", "fromStatus": "RESOURCE_PREPARING", "fromStatusName": "资源准备中", "toStatus": "RESOURCE_PREPARING", "toStatusName": "资源准备中", "content": "手动开合同 · 张三(HL20260909094000073)", "reason": null, "operatorType": "ADMIN", "operatorId": "1001", "operatorName": "admin", "extra": { "orderId": "2097500234186702849", "orderNo": "HL20260909094000073", "kind": "合同", "schemeName": "标准国内电子签约方案" }, "changedAt": "2026-09-09 09:43:23" } ] } ``` #### 空数据 / 降级响应 团期不存在、或该团期尚无任何流水,一律返回空列表,**不报错**: ```json { "code": 200, "message": "成功", "data": [] } ``` > 注意这一点与团期详情不同:`GET /v3/admin/order/group-batch/:groupBatchId` 对不存在的团期抛 `589500`,本端点回空数组。原因是这张表是 append-only 存证表,「查不到」在读语义上就等于「空时间线」。 单条记录的降级:`extra` 为空 / 空串 / 非法 JSON 时该条 `extra` 回 `null`,其余字段正常;事件类型或状态是枚举外的历史值时对应 `*Name` 回 `null`、code 原样返回。**一条脏数据不会让整条时间线查不出来。** #### 错误响应 当前角色没有 `group-batch:view` 权限(失败关闭,与团期看板 / 芯片明细同码): ```json { "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "data": null, "success": false } ``` 缺少或无效的 token 由网关拦截,返回 `401`。本端点不产生其它业务错误码。 #### 业务边界 - 只读端点,不改任何数据。 - 返回全量,不分页、不截断。 - 中文名由后端给:`eventTypeName` / `fromStatusName` / `toStatusName`;枚举外的历史值为 `null`。 - 团期不存在与无流水不区分,都回空数组。 - 权限码 `group-batch:view`,与团期看板、芯片逐户明细一致。 ## 四、契约约束与正确调用方式 1. **中文名一律用后端返回的字段渲染,不要在前端自建 code→中文 映射表。** 团期事件类型会持续新增(#7283 刚加过 `BATCH_PRODUCT_CANCEL_FAILED`),后端给名字前端就零改动;自建映射表会漏。 2. **按 `changeType` 分叉渲染**:`STATUS` 画 `fromStatusName → toStatusName` 箭头;`DATA` 的 `fromStatus === toStatus`,只渲染 `content`。 3. **`operatorType` 可能是 `SYSTEM` / `MQ`**(AFTER_COMMIT 或异步链路写入),此时 `operatorId` 为 `null`,`operatorName` 是「系统」之类的固定文案,照常渲染即可,别按「一定有操作人」写死。 4. **`extra` 是对象或 `null`**,不是字符串,不要再 `JSON.parse`。不同事件的 `extra` 结构不同,按需取键、缺键要兜住。 5. **雪花 ID 一律按字符串处理**(`logId` / `groupBatchId` / `operatorId` 都以字符串下发)。 6. 名字字段可能为 `null`(库里存的是当前枚举外的历史值),此时前端兜底显示对应的 code。 ## 六、边界行为 | 场景 | 行为 | |---|---| | 团期不存在 | `code=200`,`data=[]` | | 团期存在但无流水 | `code=200`,`data=[]` | | `extra` 为空 / 空串 / 非法 JSON | 该条 `extra` 回 `null`,其余字段正常,不影响整条时间线 | | 事件类型 / 状态是枚举外的历史值 | 对应 `*Name` 回 `null`,`eventType` / `fromStatus` / `toStatus` 原样返回 | | 无 `group-batch:view` 权限 | `589507` | ## 七、不影响范围 - 不改任何既有接口的路径、入参、出参、错误码。 - 不改团期状态流水的**写入**侧:十余条写入链路一行未动。 - 无表变更、无 Flyway、无数据迁移。 - 网关路由不新增(`/v3/admin/**` 已覆盖)。 - 只滚 `hl-order-service-v3` 一个服务。 ## 八、测试环境已验证 TEST(`api.test.1814.love`)真实网关逐条取证: | 验收项 | 实测 | |---|---| | 全量返回、按 `changedAt` 升序、条数与 `SELECT COUNT(*)` 一致 | ✅ | | `STATUS` 与 `DATA` 两类各有实例,`DATA` 类 `fromStatus == toStatus` | ✅ | | 能读回 #7283 的 `BATCH_PRODUCT_CANCEL_FAILED`,content 为「班期置不可售失败,请到产品后台手动取消该班期(班期 ID:…)」 | ✅ 本端点存在的直接理由 | | `extra` 为结构化对象;空值 / 脏值回 `null` 不报错 | ✅ | | 团期不存在 → `code=200, data=[]`(对照:团期详情同 id 抛 589500) | ✅ | | `CUSTOMIZER` 角色 → `589507`,与 `chips/contract` 逐角色一致 | ✅ | ## 十、相关文档 - 实施单编号:GB-ADM-096(新增) - 前序 #7283:本端点是它「同步失败只记时间线让运营看见」那条兜底的读回出口,解开其 AC-9 的阻塞 ## 关联 / 联系人 - 工单:https://git.1814.love:8443/wx/HL/issues/7304 - PR:https://git.1814.love:8443/wx/HL/pulls/7359 - 后端:jw;前端:mmg