五条人员写路径补留痕(新增 5 个事件码)+ GB-ADM-096 同秒按 logId 排序; PR #8490(e24be7af8)、#8492(75e182710),TEST 网关验收通过,前端零改动。 Refs wx/HL#8482 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 8482 | 团期人员与报账人变更写进团期时间线:配置保存、设报账人、单人删除、单人更换、取消成团回收五条写路径补留痕(新增五个事件码),时间线同一秒内按 logId 排序 | admin | jw(GIT) | 修改接口 | deployed | verified | not_required | PR #8490 已合入 dev-v3(e24be7af8,五条写路径补留痕),PR #8492 已合入 dev-v3(75e182710,GB-ADM-096 同秒按 logId 排序);TEST 的 hl-order-service-v3 运行 dev-v3 75e182710。经网关 api.test.1814.love 用 admin token 实测:连设主报账人 A→B→C 三行可按时间还原更替、原样保存 / 只改排序 / 只改备注 / 设成已有等级零新增、单人删除与更换主(次)报账人各一行、保存移出主报账人一行、取消成团回收单独一行且排在取消成团之后、589508 / 589598 被拒零新增、CHECK 约束注入使留痕写失败时业务一起回滚、新增行无手机号,全部通过;排序修复部署后同一团期连读 5 次,同秒行全部按 logId 升序。frontend_status=not_required 的依据:#7987 时 mmg 已实证团期时间线组件 StatusLogsPanel 渲染 eventTypeName || eventType,中文名由后端直供,前端没有自建事件码映射表;2026-09-28 复核 hl-ui origin/v2.1@ce469f78 仍是这一写法,content 直接渲染,列表按接口返回顺序展示、不在前端重排,五个新事件码零改动即可正常显示。这一结论的前提是后端继续直供 eventTypeName:若后端对这些事件码返回 eventTypeName=null,页面会退化成显示原始事件码,那时本条须改回 pending。 | 2026-09-28 | dev-v3 |
团期人员与报账人变更写进团期时间线(管理后台)
服务: hl-order-service-v3(端口 8086) PR: #8490(合入 dev-v3 为
e24be7af8)、#8492(合入 dev-v3 为75e182710) Issue: #8482 日期: 2026-09-28 影响范围: 团期人员四个写接口与取消成团的副作用、团期状态流水(时间线 GB-ADM-096)
⚠️ 关键变化
- 五条人员写路径成功后各写一行时间线:配置人员保存、设报账人、单人删除、单人更换、取消成团回收人员。新增 5 个
eventType:BATCH_STAFF_CONFIG_SAVE「调整团期人员」、BATCH_REPORTER_CHANGE「报账人变更」、BATCH_STAFF_REMOVE「移除团期人员」、BATCH_STAFF_REPLACE「更换团期人员」、BATCH_STAFF_RECYCLED「取消成团·回收人员」,changeType都是DATA。 - 一次操作只写一行:设新主报账人时原主报账人自动降级、换人时报账人等级转给新人、被移出的人带走报账人身份,这些报账人变化都并进同一行的
content和extra,不另起一行。 - 没有实际变化不写:原样保存、只改排序或备注、只改服务日期或日薪、把人设成他已有的报账人等级,时间线都不新增。
- 被拒的请求不写;留痕写失败时业务一起回滚:接口返回 500,名册与子订单副本都不变。
- extra 里的 ID 一律是字符串;主 / 次报账人前后字段(
primaryReporterBefore/primaryReporterAfter/secondaryReporterBefore/secondaryReporterAfter)只在对应归属真的变了时出现。 - GB-ADM-096 同一秒内的多行按
logId升序返回。改前同秒行的先后由数据库决定,实测建团和成团会排反;改后「取消成团」与随后的回收行稳定是「取消成团」在前。 - 六个接口的请求、响应结构一字未改。前端无需改动,理由见第四节「前端交接清单」。
一、背景
团期缺口台账 g-062(2026-09-26 四方对账,P2,后端缺口):配置、删除、更换导游和摄影,改主 / 次报账人,以及取消成团时回收人员,这五条写路径都不写团期操作记录,运营在时间线上看不到「谁在什么时候把谁换成了谁」。
主报账人不只是一个标签:代收、垫付、报账的归属实时按当前主报账人计算,不存历史快照;团期确认逐户校验主报账人(581062),预支的领款人也限定为主报账人(589557)。主报账人一变,这些都跟着变,但改前时间线上找不到原因。
jw 2026-09-28 裁决:内容没变的操作不写;一次操作只写一行,报账人变化并入这一行;取消成团回收单独写一行。
| 问题 | 改前 | 改后 |
|---|---|---|
| 人员 / 报账人变更无留痕 | 五条写路径都不写时间线 | 各写一行,带前后差异 |
| 取消成团回收了谁 | 时间线只有「取消成团」,回收人数只在服务端日志 | 单独一行,列出回收的人和报账人身份 |
| 同秒行顺序 | 只按 changedAt(精确到秒)排,同秒先后不稳定 |
同秒按 logId 升序 |
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 保存团期人员配置 | PUT | /v3/admin/group-batch/{productBatchId}/staff |
修改行为 | 名单或报账人归属有变化时写 BATCH_STAFF_CONFIG_SAVE |
| 2 | 设置报账人等级 | PUT | /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank |
修改行为 | 主 / 次报账人归属有变化时写 BATCH_REPORTER_CHANGE |
| 3 | 团期单人删除 | DELETE | /v3/admin/group-batch/{productBatchId}/staff/{staffId} |
修改行为 | 删除成功写 BATCH_STAFF_REMOVE |
| 4 | 团期单人更换 | PUT | /v3/admin/group-batch/{productBatchId}/staff/{staffId}/replace |
修改行为 | 更换成功写 BATCH_STAFF_REPLACE |
| 5 | 取消成团 | POST | /v3/admin/order/group-batch/{groupBatchId}/cancel-group |
修改行为 | 回收到人时,在「取消成团」那行之后再写一行 BATCH_STAFF_RECYCLED |
| 6 | 团期状态流水(GB-ADM-096) | GET | /v3/admin/order/group-batch/{groupBatchId}/status-logs |
取值域扩展 + 排序 | eventType 新增五个值;同秒按 logId 升序 |
三、接口详情
1. 保存团期人员配置 PUT /v3/admin/group-batch/{productBatchId}/staff
VO: BatchStaffConfigReqVO → Result<BatchStaffConfigRespVO>
使用场景
「配置人员」弹窗保存(导游位、摄影位各一个弹窗)。请求与响应不变;保存成功后,名单或报账人归属有变化时,团期时间线多一行「调整团期人员」。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| productBatchId | Path | Long | ✅ | 雪花 ID | 产品侧排期 ID,不是 groupBatchId |
| scopeRoles | Body | String[] | 否 | 传了不能是空数组;导游位须同时传 GUIDE 与 LEADER | 本次覆盖的角色范围,不传 = 整期全量覆盖;也决定时间线里的位名 |
| staffList | Body | Array | ✅ | 可以是 [](清空该范围) |
覆盖范围内的最终名单 |
| staffList[].staffId | Body | Long | ✅ | - | 员工 ID |
| staffList[].staffRole | Body | String | ✅ | LEADER / GUIDE / PHOTOGRAPHER 等 | 落库角色 |
| 其余字段 | Body | - | - | - | sortOrder / remark / serviceStartDate / serviceEndDate,与现有契约一致,本次不变;这些字段的变化不写时间线 |
出参 Result<BatchStaffConfigRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| productBatchId | String | 回显,不变 |
| groupBatchId | String | 团期主键,不变 |
| staffList | Array | 保存后的名册,不变 |
| affectedOrderCount | Integer | 扇出影响的子订单数,不变 |
请求示例
TEST 实测「加一人减一人」(导游位原有李雪梅、白云飞、刘大山,本次移出李雪梅、加入萨仁高娃):
{
"staffList": [
{ "staffId": 1007, "staffRole": "LEADER", "sortOrder": 1 },
{ "staffId": 1005, "staffRole": "LEADER", "sortOrder": 2 },
{ "staffId": 1008, "staffRole": "LEADER", "sortOrder": 3 }
],
"scopeRoles": ["GUIDE", "LEADER"]
}
响应示例
响应结构不变(下为 TEST 原文节选,名册每人只列部分字段,略去手机号):
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": "2104527134162853890",
"groupBatchId": "2104527135173685249",
"staffList": [
{ "id": "2104527304799711233", "staffId": 1003, "staffRole": "PHOTOGRAPHER", "staffName": "王强", "sortOrder": 1, "reporterRank": "NONE", "reporterRankName": "非报账人" },
{ "id": "2104527526883930114", "staffId": 1007, "staffRole": "LEADER", "staffName": "白云飞", "sortOrder": 1, "reporterRank": "PRIMARY", "reporterRankName": "主报账人" },
{ "id": "2104527526888124417", "staffId": 1005, "staffRole": "LEADER", "staffName": "刘大山", "sortOrder": 2, "reporterRank": "SECONDARY", "reporterRankName": "次报账人" },
{ "id": "2104527526892318722", "staffId": 1008, "staffRole": "LEADER", "staffName": "萨仁高娃", "sortOrder": 3, "reporterRank": "NONE", "reporterRankName": "非报账人" }
],
"affectedOrderCount": 1
},
"traceId": null,
"success": true
}
同时 GB-ADM-096 新增的一行(原文):
{
"logId": "2104527526900707330",
"groupBatchId": "2104527135173685249",
"changeType": "DATA",
"eventType": "BATCH_STAFF_CONFIG_SAVE",
"eventTypeName": "调整团期人员",
"fromStatus": "RESOURCE_PREPARING",
"toStatus": "RESOURCE_PREPARING",
"content": "导游位:+萨仁高娃;−李雪梅",
"operatorType": "ADMIN",
"operatorId": "1001",
"operatorName": "admin",
"extra": {
"scopeRoles": ["GUIDE", "LEADER"],
"added": [{ "staffId": "1008", "staffName": "萨仁高娃", "staffRole": "LEADER" }],
"removed": [{ "staffId": "1002", "staffName": "李雪梅", "staffRole": "GUIDE", "reporterRank": "NONE" }],
"roleChanged": []
},
"changedAt": "2026-09-28 19:03:57"
}
空数据 / 降级响应
无空数据形态。staffList=[] 表示清空该范围:有人被移出时同样写一行,位名后只有「−」一段。
错误响应
{ "code": 589598, "message": "团期已确认,配置不可修改", "data": null, "traceId": null, "success": false }
582115 / 582116 / 582119 / 589553 等其余错误码与改前相同。所有被拒请求零写入,也不写时间线。
业务边界
- 写的条件:名单(staffId + 落库角色)有新增、移除或角色变化,或者主 / 次报账人归属变了。排序、备注、服务日期、日薪不参与比较。
- 原样保存(前端形态:保留行只带 staffId / staffRole / sortOrder)、只改排序、只改备注:返回 200,时间线不新增(TEST 实测三次都是 8→8)。
- content 格式:
{位名}:+新增的人;−移出的人;角色调整:某人(导游→领队),各段有内容才出现。位名:不传scopeRoles为「团期人员」,其余为「导游位」「摄影位」或「导游位、摄影位」。 - 被移出的人带走报账人身份时,名字后写明去向:团期因此没有主报账人写「(原主报账人,团期现无主报账人)」,团期仍有别的主报账人写「(原主报账人,团期现主报账人为某某)」;次报账人同理。extra 同时带上报账人前后字段。
- 报账人归属变了、却不是被移出的人带走的(少见,例如历史上同一人有重复行),content 末尾单列一段「主报账人:X → Y」。
- content 超过 512 个字符时截断,末尾加「…」;完整名单以 extra 为准。
2. 设置报账人等级 PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank
VO: SetReporterRankReqVO → Result<Void>
使用场景
名册里给某人设主报账人、次报账人,或取消报账人。团期内主、次报账人各唯一:设新的主(次)报账人时,原来那位自动降为非报账人。一次操作改动两个人,时间线只写一行。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| productBatchId | Path | Long | ✅ | 雪花 ID | 产品侧排期 ID |
| staffId | Path | Long | ✅ | 须是本团已配人员 | 员工 ID |
| reporterRank | Body | String | ✅ | PRIMARY / SECONDARY / NONE | 目标等级 |
出参 Result<Void>
| 字段 | 类型 | 说明 |
|---|---|---|
| data | null | 不变 |
请求示例
PUT /v3/admin/group-batch/2104527134162853890/staff/1005/reporter-rank(此前主报账人是李雪梅):
{ "reporterRank": "PRIMARY" }
响应示例
{ "code": 200, "message": "成功", "data": null, "traceId": null, "success": true }
同时 GB-ADM-096 新增的一行(原文节选):
{
"logId": "2104527347090894850",
"eventType": "BATCH_REPORTER_CHANGE",
"eventTypeName": "报账人变更",
"changeType": "DATA",
"content": "主报账人:李雪梅 → 刘大山",
"operatorType": "ADMIN",
"operatorId": "1001",
"operatorName": "admin",
"extra": {
"staffId": "1005",
"staffName": "刘大山",
"staffRole": "LEADER",
"reporterRankBefore": "NONE",
"reporterRankAfter": "PRIMARY",
"primaryReporterBefore": { "staffId": "1002", "staffName": "李雪梅" },
"primaryReporterAfter": { "staffId": "1005", "staffName": "刘大山" }
},
"changedAt": "2026-09-28 19:03:14"
}
空数据 / 降级响应
无空数据形态。把人设成他已有的等级:返回 200,时间线不新增(TEST 实测 7→7)。
错误响应
{ "code": 589508, "message": "报账人不是该团期已派员工", "data": null, "traceId": null, "success": false }
{ "code": 589598, "message": "团期已确认,配置不可修改", "data": null, "traceId": null, "success": false }
业务边界
- 主 / 次报账人归属都没变就不写,业务写入照常执行。
- content:「主报账人:张三 → 李四」「次报账人:无 → 王五」「取消主报账人:张三」。把次报账人升为主报账人时两段并在一行:「主报账人:无 → 乌日娜;次报账人:乌日娜 → 无」。
- 前任在同一事务内加锁读取,记下的前任不会是过期值。
- 历史数据里一团有两个主报账人时,重设其中一个会把另一个降级;归属因此变了,会写一行。
- 留痕写失败时整笔回滚,接口返回 500「系统繁忙,请稍后重试或联系客服」,团级行与子订单副本的等级都不变(TEST 用 CHECK 约束注入实测)。
3. 团期单人删除 DELETE /v3/admin/group-batch/{productBatchId}/staff/{staffId}
VO: Result<BatchStaffRemoveRespVO>(无请求体)
使用场景
名册里单独删掉一个人,其他人不动;同步删掉本团活跃子订单上此人的团期副本。本单在删除成功后写一行「移除团期人员」。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| productBatchId | Path | Long | ✅ | 雪花 ID | 产品侧排期 ID |
| staffId | Path | Long | ✅ | 须在本团 | 要删除的员工 ID |
出参 Result<BatchStaffRemoveRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| productBatchId / groupBatchId | String | 回显,不变 |
| removedStaffId / removedStaffRole | Long / String | 被删的人,不变 |
| primaryReporterRemoved | Boolean | 删的是否主报账人,不变 |
| affectedOrderCount | Integer | 同步删除副本的子订单数,不变 |
| staffList | Array | 删除后的名册,不变 |
请求示例
DELETE /v3/admin/group-batch/2104527134162853890/staff/1007
响应示例
响应结构不变(TEST 原文节选,略去名册):
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": "2104527134162853890",
"groupBatchId": "2104527135173685249",
"removedStaffId": 1007,
"removedStaffRole": "LEADER",
"primaryReporterRemoved": true,
"affectedOrderCount": 1
},
"traceId": null,
"success": true
}
同时 GB-ADM-096 新增的一行(原文节选):
{
"logId": "2104527571683274754",
"eventType": "BATCH_STAFF_REMOVE",
"eventTypeName": "移除团期人员",
"changeType": "DATA",
"content": "移除领队 白云飞(原主报账人,团期现无主报账人)",
"operatorType": "ADMIN",
"operatorId": "1001",
"extra": {
"staffId": "1007",
"staffName": "白云飞",
"staffRole": "LEADER",
"reporterRankBefore": "PRIMARY",
"primaryReporterBefore": { "staffId": "1007", "staffName": "白云飞" },
"primaryReporterAfter": null
},
"changedAt": "2026-09-28 19:04:08"
}
空数据 / 降级响应
无空数据形态。删的不是报账人时 content 只有「移除导游 张三」,extra 不带报账人前后字段。
错误响应
{ "code": 589508, "message": "报账人不是该团期已派员工", "data": null, "traceId": null, "success": false }
{ "code": 589598, "message": "团期已确认,配置不可修改", "data": null, "traceId": null, "success": false }
业务边界
- 每次删除成功写一行。加锁读到的名册里已经没有此人(并发下刚被别人删掉)时不写。
- 删除不在本团的人返回 589508,message 是错误码共用的「报账人不是该团期已派员工」,与改前相同。
- 响应里的
primaryReporterRemoved与时间线 extra 的primaryReporterAfter: null是同一件事的两处体现。
4. 团期单人更换 PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/replace
VO: BatchStaffReplaceReqVO → Result<BatchStaffReplaceRespVO>
使用场景
名册里把一个人换成另一个人;配置位、排序、备注、报账人等级、服务日期都转给新人。本单在更换成功后写一行「更换团期人员」。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| productBatchId | Path | Long | ✅ | 雪花 ID | 产品侧排期 ID |
| staffId | Path | Long | ✅ | 须在本团 | 被换下的员工 ID |
| newStaffId | Body | Long | ✅ | 须上架、类型符合原配置位、不在本团 | 换上的员工 ID |
出参 Result<BatchStaffReplaceRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| oldStaffId / newStaffId | Long | 换下 / 换上的人,不变 |
| staffRole | String | 新人落库角色,不变 |
| reporterRank | String | 新人继承的等级,不变 |
| primaryReporterTransferred | Boolean | 主报账人是否随之转给新人,不变 |
| affectedOrderCount / staffList | - | 不变 |
请求示例
PUT /v3/admin/group-batch/2104527134162853890/staff/1008/replace(萨仁高娃是主报账人):
{ "newStaffId": 1009 }
响应示例
响应结构不变(TEST 原文节选,略去名册):
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": "2104527134162853890",
"groupBatchId": "2104527135173685249",
"oldStaffId": 1008,
"newStaffId": 1009,
"staffRole": "LEADER",
"reporterRank": "PRIMARY",
"primaryReporterTransferred": true,
"affectedOrderCount": 1
},
"traceId": null,
"success": true
}
同时 GB-ADM-096 新增的一行(原文节选):
{
"logId": "2104527581611192321",
"eventType": "BATCH_STAFF_REPLACE",
"eventTypeName": "更换团期人员",
"changeType": "DATA",
"content": "领队 萨仁高娃 → 陈志远(主报账人随之转给陈志远)",
"operatorType": "ADMIN",
"operatorId": "1001",
"extra": {
"oldStaff": { "staffId": "1008", "staffName": "萨仁高娃", "staffRole": "LEADER", "reporterRank": "PRIMARY" },
"newStaff": { "staffId": "1009", "staffName": "陈志远", "staffRole": "LEADER", "reporterRank": "PRIMARY" },
"primaryReporterBefore": { "staffId": "1008", "staffName": "萨仁高娃" },
"primaryReporterAfter": { "staffId": "1009", "staffName": "陈志远" }
},
"changedAt": "2026-09-28 19:04:10"
}
空数据 / 降级响应
无空数据形态。换下的不是报账人时 content 只有「导游 张三 → 李四」,extra 不带报账人前后字段。
错误响应
{ "code": 589508, "message": "报账人不是该团期已派员工", "data": null, "traceId": null, "success": false }
{ "code": 589598, "message": "团期已确认,配置不可修改", "data": null, "traceId": null, "success": false }
100001 / 589582 / 582103 / 582118 / 582114 / 582119 / 589553 与改前相同。
业务边界
- 更换一定改了名单,每次成功都写一行。
- 次报账人同理:「领队 刘大山 → 乌日娜(次报账人随之转给乌日娜)」,extra 带
secondaryReporterBefore/secondaryReporterAfter。 - 落库角色变了时两边都带角色,如「领队 李四 → 导游 巴特尔」。
5. 取消成团 POST /v3/admin/order/group-batch/{groupBatchId}/cancel-group
VO: Result<Void>(无请求体)
使用场景
团期从「资源准备中」退回「招募中」,同时回收整团的导游 / 摄影配置。本单在回收之后单独写一行「取消成团·回收人员」,原来那行「取消成团」不动。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 雪花 ID | 团期主订单 ID(不是 productBatchId) |
出参 Result<Void>
| 字段 | 类型 | 说明 |
|---|---|---|
| data | null | 不变 |
请求示例
POST /v3/admin/order/group-batch/2104527135173685249/cancel-group
响应示例
{ "code": 200, "message": "成功", "data": null, "traceId": null, "success": true }
同时 GB-ADM-096 新增的两行(原文节选,同一秒,按 logId 排序):
[
{
"logId": "2104528117127360513",
"changeType": "STATUS",
"eventType": "BATCH_CANCEL_GROUP",
"eventTypeName": "取消成团",
"fromStatus": "RESOURCE_PREPARING",
"toStatus": "RECRUITING",
"content": "取消成团",
"extra": null,
"changedAt": "2026-09-28 19:06:18"
},
{
"logId": "2104528117160914946",
"changeType": "DATA",
"eventType": "BATCH_STAFF_RECYCLED",
"eventTypeName": "取消成团·回收人员",
"fromStatus": "RECRUITING",
"toStatus": "RECRUITING",
"content": "取消成团,回收团期人员 2 人:王强、乌日娜(主报账人)",
"operatorType": "ADMIN",
"operatorId": "1001",
"extra": {
"recycledCount": 2,
"staff": [
{ "staffId": "1003", "staffName": "王强", "staffRole": "PHOTOGRAPHER", "reporterRank": "NONE" },
{ "staffId": "1010", "staffName": "乌日娜", "staffRole": "LEADER", "reporterRank": "PRIMARY" }
],
"primaryReporterBefore": { "staffId": "1010", "staffName": "乌日娜" },
"primaryReporterAfter": null
},
"changedAt": "2026-09-28 19:06:18"
}
]
空数据 / 降级响应
团期没配人员时只写「取消成团」一行,不写回收行(TEST 实测团期 Z:2→3,只多 BATCH_CANCEL_GROUP)。
错误响应
与改前相同(下为源码 GroupBatchErrorCode 定义的文案,本轮验收没有触发):
{ "code": 589502, "message": "取消成团被阻塞(有已确认子订单或已派单资源)", "data": null }
{ "code": 589501, "message": "团期状态不允许当前操作", "data": null }
业务边界
- 回收行的
fromStatus/toStatus都是RECRUITING(取消成团之后的状态)。它与「取消成团」通常在同一秒,靠logId稳定排在后面。 - content:「取消成团,回收团期人员 N 人:张三(主报账人)、李四、王五(次报账人)」;回收的人里没有主报账人时末尾加「;其中无主报账人」。
- 回收名单在同一事务内、软删之前加锁读取。回收或留痕任何一步失败,取消成团整体回滚。
6. 团期状态流水 GET /v3/admin/order/group-batch/{groupBatchId}/status-logs
VO: GroupBatchStatusLogItemVO(返回 Result<List<GroupBatchStatusLogItemVO>>,无请求体)
使用场景
团期详情的时间线(GB-ADM-096)。结构不变;eventType 多了五个取值;同一秒内的行改为按 logId 升序。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 雪花 ID | 团期主订单 ID |
出参 Result<List<GroupBatchStatusLogItemVO>>
| 字段 | 类型 | 说明 |
|---|---|---|
| logId | String | 流水 ID;同秒排序的第二键 |
| eventType | String | 新增五个值,见六.5 |
| eventTypeName | String | 对应中文名,后端直供 |
| changeType | String | 五个新事件都是 DATA |
| fromStatus / toStatus | String | DATA 类两者相同,取写入时的团期状态 |
| content | String | 展示文本,前端直接渲染 |
| extra | Object | 结构化快照,按事件类型不同,见六.5 |
| operatorType / operatorId / operatorName | String | 五个新事件都是 ADMIN + 管理员 ID(字符串)+ 管理员名字 |
| 其余字段 | - | groupBatchId / fromStatusName / toStatusName / reason / changedAt 等,不变 |
请求示例
GET /v3/admin/order/group-batch/2104527135173685249/status-logs
响应示例
TEST 原文节选(团期 X,排序修复后)。前两行同为 19:02:24,按 logId 升序:
{
"code": 200,
"message": "成功",
"data": [
{
"logId": "2104527135194656770",
"changeType": "STATUS",
"eventType": "BATCH_CREATE",
"eventTypeName": "建团",
"content": "建团:2月18日海拉尔-呼伦湖2日团",
"changedAt": "2026-09-28 19:02:24"
},
{
"logId": "2104527136431976449",
"changeType": "STATUS",
"eventType": "BATCH_GROUP",
"eventTypeName": "成团",
"content": "手动成团 · 已售 1/15 户 · 理由:客户已全款,手动成团(#8482 验收造数)",
"changedAt": "2026-09-28 19:02:24"
},
{
"logId": "2104527342187737089",
"groupBatchId": "2104527135173685249",
"changeType": "DATA",
"eventType": "BATCH_REPORTER_CHANGE",
"eventTypeName": "报账人变更",
"fromStatus": "RESOURCE_PREPARING",
"fromStatusName": "资源准备中",
"toStatus": "RESOURCE_PREPARING",
"toStatusName": "资源准备中",
"content": "主报账人:无 → 李雪梅",
"reason": null,
"operatorType": "ADMIN",
"operatorId": "1001",
"operatorName": "admin",
"extra": {
"staffId": "1002",
"staffName": "李雪梅",
"staffRole": "GUIDE",
"reporterRankBefore": "NONE",
"reporterRankAfter": "PRIMARY",
"primaryReporterBefore": null,
"primaryReporterAfter": { "staffId": "1002", "staffName": "李雪梅" }
},
"changedAt": "2026-09-28 19:03:13"
}
],
"traceId": null,
"success": true
}
空数据 / 降级响应
无流水时 data=[],与改前相同。
错误响应
与改前相同(源码 GroupBatchErrorCode 文案):
{ "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null }
业务边界
- 排序:
changedAt升序,同一秒内logId升序(#8492)。TEST 同一团期修复前连读 5 次,都是「成团」排在「建团」之前;修复后连读 5 次,两组同秒行(建团 / 成团、取消成团 / 回收)全部按 logId 升序。 - 历史不回补:本单合入之前的人员变更查不回来。
- 库里认不出的事件码,
eventTypeName为 null,与改前相同。
四、契约约束与正确调用方式
本节只写后端接受/拒绝的规则和前端必须跟着做的事。
✅ 正确 / ❌ 错误用法对照
| 场景 | 做法 |
|---|---|
| ✅ 时间线展示新事件 | 渲染 eventTypeName,取不到回落 eventType;正文直接渲染 content |
| ✅ 读 extra 里的报账人前后字段 | 缺键按 null 处理;要判断「这一行有没有改主报账人」,看 extra 里有没有 primaryReporterAfter 这个键 |
| ❌ 把「缺键」当成「团期没有主报账人」 | 缺键只表示这一行没改该项归属;键在且值为 null 才表示那一端没有报账人 |
| ❌ 把 extra 里的 staffId 转成数字 | extra 里的 ID 一律是字符串,按字符串比较 |
| ❌ 用 productBatchId 调时间线或取消成团 | 人员四个接口的路径参数是 productBatchId,时间线与取消成团是 groupBatchId,两者不能混用 |
| ❌ 前端按 changedAt 重排时间线 | changedAt 只到秒,重排会打乱同秒行的先后;按接口返回顺序展示 |
前端交接清单
结论:无需改动。
- 团期时间线组件
StatusLogsPanel渲染的是eventTypeName || eventType || '事件',中文名由后端直供,前端没有自建事件码映射表(#7987 时 mmg 实证;2026-09-28 复核 hl-uiorigin/v2.1@ce469f78未变)。五个新事件码不改代码就显示中文名。 content由后端拼好,面板直接渲染;新事件的说明文字都在 content 里。- 面板对 extra 只挑
orderNo/schemeName/kind/orderId几个键,缺键兜住。新事件的 extra 不含这些键,不会多渲染文字,也不会报错。 - 面板按接口返回顺序展示,没有在前端重排,同秒按 logId 排序的修复不用前端配合。
- 如果要在页面上用 extra(例如单独展示主报账人更替历史):报账人前后四个字段只在对应归属变了时出现,缺键按 null 处理;ID 都是字符串。
- 以上结论的前提是后端继续直供
eventTypeName。
五、数据库行为
| 情形 | order_batch_staff | group_batch_status_log |
|---|---|---|
| 保存配置:名单或报账人归属有变化 | 写入(与改前相同) | +1 BATCH_STAFF_CONFIG_SAVE |
| 保存配置:原样、只改排序 / 备注 / 服务日期 / 日薪 | 写入(与改前相同) | 不写 |
| 设报账人:归属变了 | 更新(与改前相同) | +1 BATCH_REPORTER_CHANGE |
| 设报账人:设成已有等级 | 照常执行 | 不写 |
| 单人删除 / 单人更换成功 | 写入(与改前相同) | +1 BATCH_STAFF_REMOVE / BATCH_STAFF_REPLACE |
| 取消成团,团期配了人员 | 整团软删(与改前相同) | 「取消成团」之后 +1 BATCH_STAFF_RECYCLED |
| 取消成团,团期没配人员 | 不动 | 只有「取消成团」一行 |
| 被拒(589508 / 589598 / 582115 等) | 不动 | 不写 |
| 留痕写失败 | 回滚 | 不写(接口 500) |
- 零 DDL,不需要 Flyway:只往
group_batch_status_log追加行,新事件码是event_type列的新取值。 - 写之前先对
order_batch_staff做锁定读(SELECT ... FOR UPDATE)取前态:保存配置锁本次覆盖范围内的行,设报账人 / 单人删除 / 单人更换 / 取消成团回收锁整团行。时间线记下的前任不会是过期值。 - 留痕与业务写入在同一事务:业务回滚,留痕跟着回滚;留痕写失败,业务也回滚。
- 子订单上的团期人员副本照旧扇出,副本的变化不进订单时间线。
六、边界行为
- 未登录 → 网关返回
code=401(HTTP 200)。 - 无团期管理权限 → 589507,与改前相同。
- 操作人:五种新事件都由后台请求触发,
operatorType=ADMIN,operatorId是管理员 ID(字符串),operatorName是管理员名字。TEST 新增的 17 行全部是ADMIN/"1001"/admin。 - 手机号:content 和 extra 都不带手机号,明文、库列值都不带。
- 团期当前状态是库里认不出的历史值时,这一行的
fromStatus/toStatus为 null,业务照常写入。 - 订单自派人员(
source=ORDER)的变更不在本单范围。
六.5、枚举 / 数据字典
eventType 新增取值(GroupBatchLogEventType)
| 值 | 中文名(eventTypeName) | changeType | 写入接口 | 典型 content(TEST 原文) |
|---|---|---|---|---|
BATCH_STAFF_CONFIG_SAVE |
调整团期人员 | DATA | 保存团期人员配置 | 「导游位:+萨仁高娃;−李雪梅」「导游位:−陈志远(原主报账人,团期现无主报账人)」 |
BATCH_REPORTER_CHANGE |
报账人变更 | DATA | 设置报账人等级 | 「主报账人:李雪梅 → 刘大山」「次报账人:无 → 刘大山」「主报账人:无 → 乌日娜;次报账人:乌日娜 → 无」 |
BATCH_STAFF_REMOVE |
移除团期人员 | DATA | 团期单人删除 | 「移除领队 白云飞(原主报账人,团期现无主报账人)」 |
BATCH_STAFF_REPLACE |
更换团期人员 | DATA | 团期单人更换 | 「领队 萨仁高娃 → 陈志远(主报账人随之转给陈志远)」「领队 刘大山 → 乌日娜(次报账人随之转给乌日娜)」 |
BATCH_STAFF_RECYCLED |
取消成团·回收人员 | DATA | 取消成团 | 「取消成团,回收团期人员 2 人:王强、乌日娜(主报账人)」 |
content 里的角色中文名与名册 staffRoleName 同源(导游 / 领队 / 摄影等);报账人等级中文名为「主报账人」「次报账人」「非报账人」。
公共:报账人前后字段(五种事件都可能带)
| 字段 | 类型 | 说明 |
|---|---|---|
| primaryReporterBefore | Object 或 null | 主报账人归属变了才有这个键;值为 {staffId, staffName}(staffId 为字符串),那一端没有主报账人时为 null |
| primaryReporterAfter | Object 或 null | 同上,变更后 |
| secondaryReporterBefore | Object 或 null | 次报账人归属变了才有这个键;形态同上 |
| secondaryReporterAfter | Object 或 null | 同上,变更后 |
BATCH_STAFF_CONFIG_SAVE 的 extra
| 字段 | 类型 | 说明 |
|---|---|---|
| scopeRoles | String[] 或 null | 本次覆盖范围;null 表示整期全量保存 |
| added | Array(恒在,可为空) | 新增的人:staffId(String)/ staffName / staffRole |
| removed | Array(恒在,可为空) | 移出的人:staffId(String)/ staffName / staffRole / reporterRank(移出前等级) |
| roleChanged | Array(恒在,可为空) | 落库角色变了的人:staffId(String)/ staffName / staffRoleBefore / staffRoleAfter(一人多角色时用「/」连接) |
BATCH_REPORTER_CHANGE 的 extra
| 字段 | 类型 | 说明 |
|---|---|---|
| staffId | String | 被设等级的人 |
| staffName | String | 姓名快照 |
| staffRole | String | 落库角色 |
| reporterRankBefore | String | 此人原等级:PRIMARY / SECONDARY / NONE |
| reporterRankAfter | String | 此人新等级 |
被自动降级的原主(次)报账人体现在报账人前后字段的 Before 里。
BATCH_STAFF_REMOVE 的 extra
| 字段 | 类型 | 说明 |
|---|---|---|
| staffId | String | 被删的人 |
| staffName | String | 姓名快照 |
| staffRole | String | 落库角色 |
| reporterRankBefore | String | 删除前等级 |
BATCH_STAFF_REPLACE 的 extra
| 字段 | 类型 | 说明 |
|---|---|---|
| oldStaff | Object | 换下的人:staffId(String)/ staffName / staffRole / reporterRank(换下前的等级) |
| newStaff | Object | 换上的人:staffId(String)/ staffName / staffRole / reporterRank(继承到的等级) |
BATCH_STAFF_RECYCLED 的 extra
| 字段 | 类型 | 说明 |
|---|---|---|
| recycledCount | Integer | 回收人数(按人计) |
| staff | Array | 每人一项:staffId(String)/ staffName / staffRole / reporterRank |
回收到主(次)报账人时带报账人前后字段,After 恒为 null。
六.6、修改前后对比
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| 六个接口的请求 / 响应字段 | - | 不变 |
GB-ADM-096 eventType 取值 |
没有人员类事件 | 新增五个 |
| GB-ADM-096 同秒行顺序 | 不稳定 | logId 升序 |
行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 保存配置有增删、角色变化或报账人归属变化 | 时间线无记录 | +1 BATCH_STAFF_CONFIG_SAVE |
| 保存配置原样、只改排序或备注 | 时间线无记录 | 仍无记录 |
| 设报账人 | 时间线无记录 | +1 BATCH_REPORTER_CHANGE,被降级的原报账人写在同一行 |
| 单人删除 / 单人更换 | 时间线无记录 | +1 BATCH_STAFF_REMOVE / BATCH_STAFF_REPLACE |
| 取消成团回收人员 | 只有「取消成团」一行 | 「取消成团」之后 +1 回收行 |
| 同秒行顺序 | TEST 连读 5 次都是「成团」排在「建团」前 | 连读 5 次都按 logId 升序 |
六.7、影响评估
- 是否破坏向后兼容: 否(结构不变;新增的是
eventType取值,前端按eventTypeName渲染) - 前端是否必须同步上线: 否(前端零改动)
- 前端 workaround 清理点: 无
七、不影响范围
- 仅影响: 五个写接口成功后的时间线副作用;GB-ADM-096 的取值域和同秒顺序
- 零影响:
- 六个接口的请求 / 响应结构和错误码
- 保存配置时保留人员沿用原报账人等级和备注:这一行为随 #8468 发布(见
28_8468交接件),本篇不重复 - 名册、候选列表、扇出到子订单的逻辑
- 订单级时间线
- 订单自派人员(
source=ORDER) - 本单合入之前的历史变更(不回补)
八、测试环境已验证
部署:主轮(19:02–19:09)跑在 hl-order-service-v3 dev-v3 @ e24be7af8(#8490,18:53:31 部署),两实例经运行字节探针确认加载的是本次代码(BATCH_STAFF_CONFIG_SAVE、「取消成团·回收人员」命中,进程 jar 与磁盘 jar 同 inode)。排序复验(19:28)跑在 dev-v3 @ 75e182710(#8492)。
经网关 api.test.1814.love 用 admin token(operatorId 1001)实测。团期 X:groupBatchId 2104527135173685249(productBatchId 2104527134162853890,一户全款、手动成团);团期 Z:groupBatchId 2104528158369951745(productBatchId 2104528157774356482)。每步前后各读一次 GB-ADM-096 整段原文比对行数。
AC-B1 19:03:13~16 X 依次设主报账人 李雪梅 → 刘大山 → 白云飞
→ GB-ADM-096 4→7,新增「主报账人:无 → 李雪梅」「主报账人:李雪梅 → 刘大山」「主报账人:刘大山 → 白云飞」,
operatorType=ADMIN、operatorId=1001;同一次读取里 BATCH_CREATE / BATCH_GROUP 为阳性对照 ✓
AC-B3 19:03:16 X 对白云飞再设 PRIMARY(已有等级) → 200,7→7 ✓
AC-A2 19:03:31 X 设次报账人 刘大山 → 7→8「次报账人:无 → 刘大山」✓
AC-A1 19:03:36 X 按前端形态原样保存导游位(只带 staffId/staffRole/sortOrder)
AC-A2 → 200,8→8;团级行重新插入(白云飞 batch_staff_id 2104527299623956483 → 2104527440154095618)仍为 PRIMARY,
AC-B3 刘大山仍为 SECONDARY;子订单 2104527134926221314 的 GROUP_BATCH 副本同为 PRIMARY / SECONDARY,update_time 19:03:37 ✓
AC-B3 19:03:54 X 只改排序 → 8→8;19:03:56 只改备注 → 8→8 ✓
AC-B3 19:03:57 X 导游位加萨仁高娃、移出李雪梅 → 8→9「导游位:+萨仁高娃;−李雪梅」✓
AC-B2 19:04:07 X 单人删除主报账人白云飞 → 9→10「移除领队 白云飞(原主报账人,团期现无主报账人)」,
响应 primaryReporterRemoved=true,extra.primaryReporterAfter=null ✓
19:04:09 X 设主报账人 萨仁高娃 → 10→11「主报账人:无 → 萨仁高娃」
AC-B2 19:04:10 X 单人更换主报账人 萨仁高娃 → 陈志远 → 11→12「领队 萨仁高娃 → 陈志远(主报账人随之转给陈志远)」✓
AC-B2 19:04:11 X 单人更换次报账人 刘大山 → 乌日娜 → 12→13「领队 刘大山 → 乌日娜(次报账人随之转给乌日娜)」✓
AC-A3 19:04:23 X 保存导游位时移出主报账人陈志远 → 13→14「导游位:−陈志远(原主报账人,团期现无主报账人)」,
extra.primaryReporterAfter=null,导游位只剩乌日娜(SECONDARY)✓
AC-B5 19:04:24~26 X 对已不在本团的人 设报账人 / 删除 / 更换 → 三个都是 589508「报账人不是该团期已派员工」,14→14,名册不变 ✓
AC-B6 19:05:28 X 给 group_batch_status_log 临时加 CHECK 约束(只拦本团新的 BATCH_REPORTER_CHANGE,约 1.4 秒)后设主报账人乌日娜
→ 500「系统繁忙,请稍后重试或联系客服」;服务端日志:order_batch_staff、order_staff_assignment 两条 UPDATE 已执行,
随后 INSERT 时间线触发 3819;回滚后团级行与子订单副本的乌日娜仍为 SECONDARY,时间线 14→14;
约束删除后 SHOW CREATE TABLE 与注入前逐字一致 ✓
19:06:04 X 约束删除后再设一次 → 14→15「主报账人:无 → 乌日娜;次报账人:乌日娜 → 无」✓
AC-B4 19:06:18 X 取消成团(已配人员) → 15→17,BATCH_CANCEL_GROUP「取消成团」之后
BATCH_STAFF_RECYCLED「取消成团,回收团期人员 2 人:王强、乌日娜(主报账人)」✓
AC-B4 19:06:28 Z 取消成团(未配人员) → 2→3,只多 BATCH_CANCEL_GROUP,无回收行 ✓
AC-B5 19:07:23~27 Z 团期确认后(MATERIAL_PREPARING)保存 / 设报账人 / 删除 / 更换
→ 四个都是 589598「团期已确认,配置不可修改」,14→14,名册不变 ✓
AC-B7 19:08:21 X、Z 两团全部 31 行(新事件 17 行):11 个明文手机号逐个查、7 个库列值逐个查、掩码 **** 与 phone 键名,全部 0 命中;
带数字边界的 11 位手机号正则 0 命中(19:44 复查);不带边界时命中的 3 行都是既有事件 extra 里雪花 ID 的片段,新事件 0 命中 ✓
排序 19:21:19~23 排序修复部署前,X 连读 5 次:BATCH_GROUP 都排在 BATCH_CREATE 前(同为 19:02:24)
19:28:36~40 dev-v3 75e182710,X 连读 5 次:两组同秒行(建团 / 成团、取消成团 / 回收)全部 logId 升序 ✓
所有新增行 operatorType=ADMIN、operatorId="1001"、operatorName=admin、changedAt 有值,extra 里的 ID 全部是字符串。
单元层:五处留痕调用逐一去掉,对应用例各自转红(共 12 例);#8492 的排序键去掉后 GroupBatchStatusLogMapperOrderTest 转红。
九、相关历史 PR
- #3676:引入主 / 次报账人等级
- #7304:GB-ADM-096 团期状态流水只读端点
- #7987:时间线新事件由后端直供
eventTypeName,前端零改动的先例 - #8231:取消成团回收导游 / 摄影配置
- #8354:团期单人删除、单人更换
- #8468:保存配置时保留人员沿用原报账人等级和备注(PR #8475)
- #8481:团期物资留痕(同日,同一张时间线)
十、相关文档
- 关联 Issue: wx/HL#8482
- 关联 PR: wx/HL#8490、wx/HL#8492
- 同日交接件:
changelogs-v2/2026-09/28_8468_团期导领摄配置加服务日期与基础日薪并在选人列表显示占用-修改接口-管理后台.md、changelogs-v2/2026-09/28_8481_团期物资确认后改清单自动失效并补全物资留痕-修改接口-管理后台.md
关联 / 联系人
链接
联系人
- 后端负责人: @jw