15 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 | 8006 | 团期 staff 保存支持按角色范围覆盖,不再跨配置位清空 | admin | wx(GIT) | 修改接口 | deployed | verified | verified | mmg | 2e8850800bc0715c35d77a9496aab39128cf8e64 | v2.1 | 2026-09-21 | 2026-09-21 于网关 https://api.test.1814.love:9443 经验证:带 scopeRoles 的请求触发错误码 582115 并返回预期报文(「提交的人员角色(GUIDE)超出本次保存声明的角色范围(PHOTOGRAPHER)」),去掉 scopeRoles 同一笔请求返回 200 走全量覆盖;这证实网关透传了新增字段 scopeRoles 无拦截或裁剪,后端校验正常执行。PR #8117 已合入 dev-v3(合并提交 094521f0b),当前测试环境部署 commit d30cd9561。 前端 2026-09-21 已交付(hl-admin v2.1 2e885080,commit 全哈希见 frontend_ref):saveGroupBatchStaff 加 scopeRoles 第三参(空数组不下发),配置弹窗改范围覆盖只提交本位行、导游位 GUIDE+LEADER 同传;checkpoint 全量绿。 | 2026-09-21 | dev-v3 |
团期人员配置:保存接口新增角色范围声明,支持按配置位分部分保存
服务: hl-order-service-v3 PR: #8117 | Issue: #8006 | 合并提交:
094521f0b影响范围: 管理后台「团期详情 → 配置导游/摄影」弹窗保存按钮的后端接口
⚠️ 关键变化
团期 staff 配置是按弹窗(导游位 + 摄影位)分两次各自维护的。改前,任何一次保存都会全量覆盖整期所有角色,导致保存导游位时漏带摄影位就把摄影师清空——即使摄影位根本没动。
现在可选择:不传 scopeRoles 时行为不变(整期全量覆盖),传了则只覆盖指定的几个角色,别的角色既有人员保持不动。导游位与摄影位终于能互不干扰地维护。
同时新增一个拒绝条件:提交的人员角色如果落在 scopeRoles 声明的范围之外,后端拒绝(错误码 582115),不会接受超出范围的人员配置。
一、背景
团期 staff 配置分两个弹窗:
- 导游位:选导游/领队,对应
GUIDE+LEADER两个角色 - 摄影位:选摄影师,对应
PHOTOGRAPHER一个角色
全量覆盖的模式要求"保存时把你要的所有人员一口气提交",否则没提交的就被清空。但实际场景是前端打开弹窗改一处、保存一次,改另一处、再保存一次,两个弹窗分离维护。导致只要有一次漏带了另一弹窗的既有人员,那一弹窗的人就被静默清空。
本次改动让后端对保存范围更聪慧:前端只需声明"我这次改的是哪些角色",后端就只删那个范围内的旧行、只插那个范围内的新行,其他角色的人员一行不动。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 保存团期人员配置 | PUT | /v3/admin/group-batch/{productBatchId}/staff |
请求体新增字段 | 新增 scopeRoles;错误码新增 582115 |
三、接口详情
1. 保存团期人员配置 PUT /v3/admin/group-batch/{productBatchId}/staff
VO: BatchStaffConfigReqVO → BatchStaffConfigRespVO
使用场景
「配置导游」或「配置摄影」弹窗点保存时调用,将本弹窗的人员配置提交到后端。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
productBatchId |
Path | Long | ✅ | — | 产品侧班期 ID |
scopeRoles |
Body | List<String> |
❌ | 元素取值: LEADER / GUIDE / DRIVER / PHOTOGRAPHER / OTHER / GUIDE_ASSISTANT / STUDY_TEACHER / LIFE_TEACHER;不传 = 整期全量覆盖(历史行为),传了就必须 ≥ 1 项(传空数组 400 拒绝) |
新增。本次保存覆盖的角色范围。不传时全量覆盖整期(与改前一致),传了则只在这些角色内覆盖,范围外的既有行不动。特别注意:导游位并收 GUIDE 与 LEADER 两个角色,保存导游位时必须同时传这两个,只传其中一个会导致 582115 拒绝(见下方错误响应) |
staffList |
Body | List<Item> |
✅ | — | 本范围内要保存的人员列表。传空数组 [] 表示清空该范围内的人员(与改前一致) |
staffList[].staffId |
Body | Long | ✅ | — | 资源域人员 ID |
staffList[].staffRole |
Body | String | ✅ | 同上 scopeRoles 的取值域 |
人员在本配置中的角色。如果传了 scopeRoles,这里的每一项都必须在范围内,否则 582115 拒绝 |
staffList[].sortOrder |
Body | Integer | ❌ | — | 展示排序(缺省 0) |
出参 Result<BatchStaffConfigRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
productBatchId |
Long | 回显路径参数 |
groupBatchId |
Long | 运营团期 ID(由 productBatchId 反查得到) |
staffList |
List<Item> |
保存后的整期最终状态(见下方说明) |
affectedOrderCount |
Integer | 扇出影响的活跃子订单数 |
staffList 回显语义变化:
- 改前:只回显本次提交的 items(即请求体的
staffList) - 改后:回显保存完成后整个团期的最终状态——无论你是全量保存还是按范围保存,返回都是整期全部人员的完整快照
- 好处 1:前端无需再发第二个 GET 请求去拿最新名单
- 好处 2:前端能看到"我这一存操作扇出到多少订单"和"团期现在全部配置是啥",更清楚整体状态
- 注意:如果你传了
scopeRoles=["PHOTOGRAPHER"]只保存摄影位,返回的staffList仍然包含导游位的人(如果有的话),这是正常的,代表团期的完整配置
请求示例
场景 1:按范围保存(新用法)
前端打开导游位弹窗,改了导游/领队名单,保存时只声明导游位:
{
"scopeRoles": ["GUIDE", "LEADER"],
"staffList": [
{
"staffId": 1005,
"staffRole": "LEADER",
"sortOrder": 0
},
{
"staffId": 1002,
"staffRole": "GUIDE",
"sortOrder": 1
}
]
}
场景 2:全量保存(历史用法,不传 scopeRoles)
{
"staffList": [
{
"staffId": 1005,
"staffRole": "LEADER",
"sortOrder": 0
},
{
"staffId": 1002,
"staffRole": "GUIDE",
"sortOrder": 1
},
{
"staffId": 2003,
"staffRole": "PHOTOGRAPHER",
"sortOrder": 0
}
]
}
响应示例
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"productBatchId": 80001,
"groupBatchId": 90211,
"staffList": [
{
"staffId": 1005,
"staffRole": "LEADER",
"staffName": "刘大山",
"staffPhone": "138****6677",
"sortOrder": 0
},
{
"staffId": 1002,
"staffRole": "GUIDE",
"staffName": "李雪梅",
"staffPhone": "138****8888",
"sortOrder": 1
},
{
"staffId": 2003,
"staffRole": "PHOTOGRAPHER",
"staffName": "王摄影",
"staffPhone": "188****9999",
"sortOrder": 0
}
],
"affectedOrderCount": 3
}
}
空数据 / 降级响应
staffList 传空数组清空覆盖范围内的人员:
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"productBatchId": 80001,
"groupBatchId": 90211,
"staffList": [
{
"staffId": 2003,
"staffRole": "PHOTOGRAPHER",
"staffName": "王摄影",
"staffPhone": "188****9999",
"sortOrder": 0
}
],
"affectedOrderCount": 2
}
}
(如果 scopeRoles=["GUIDE","LEADER"] 且 staffList=[],则导游位人员全清,摄影位保留)
错误响应
scopeRoles 传了空数组:
{
"code": 400,
"message": "scopeRoles 传了就不能是空数组;要整期全量覆盖请整个字段不传",
"success": false,
"data": null
}
提交的人员角色超出 scopeRoles 声明的范围(新错误码 582115):
{
"code": 582115,
"message": "提交的人员角色(PHOTOGRAPHER)超出本次保存声明的角色范围(GUIDE,LEADER),请检查配置位与人员是否匹配",
"success": false,
"data": null
}
特别情况:导游位只声明了 GUIDE 却选了 LEADER:
{
"code": 582115,
"message": "提交的人员角色(LEADER)超出本次保存声明的角色范围(GUIDE),请检查配置位与人员是否匹配",
"success": false,
"data": null
}
staffList 字段缺失:
{
"code": 400,
"message": "staff 配置列表不能缺失;确要清空请显式传空数组 []",
"success": false,
"data": null
}
业务边界
- 覆盖范围不校验完整性:你可以只传
scopeRoles=["GUIDE"]而保存时含有领队,后端照做。只覆盖 GUIDE 那一行,领队那一行在范围外。这个设计缺口(没有校验"GUIDE+LEADER 必须同时出现")已在工单 #8122 记录,前端若需保证配置位完整性,当前需自己在前端侧把关。 - 范围外人员拒绝率 100%:如果 staffList 里有任何一项的
staffRole不在scopeRoles声明的范围内,整个请求582115拒绝。失败时前端建议重新拉取当前配置以确保数据一致性。 - 导游位的特殊性:导游位由
GUIDE和LEADER两个角色共同维护。保存导游位配置时,scopeRoles必须同时包含["GUIDE", "LEADER"]。只传其中一个(如["GUIDE"])会导致另一个角色的人员被拒(582115)。
四、契约约束与正确调用方式
✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload | 结果 |
|---|---|---|
| ✅ 保存导游位(两个角色都声明) | scopeRoles: ["GUIDE","LEADER"], staffList: [{staffRole:"LEADER",...}] |
200,导游位按新值覆盖,摄影位保留 |
| ✅ 保存摄影位 | scopeRoles: ["PHOTOGRAPHER"], staffList: [{staffRole:"PHOTOGRAPHER",...}] |
200,摄影位按新值覆盖,导游位保留 |
| ✅ 全量覆盖(不传 scopeRoles) | staffList: [{staffRole:"GUIDE",...}, {staffRole:"PHOTOGRAPHER",...}] |
200,整期全量覆盖(与改前一致) |
| ✅ 清空导游位 | scopeRoles: ["GUIDE","LEADER"], staffList: [] |
200,导游位人员全清,摄影位保留 |
| ❌ 导游位只声明一个角色 | scopeRoles: ["GUIDE"], staffList: [{staffRole:"LEADER",...}] |
582115,拒绝 |
| ❌ 传空 scopeRoles | scopeRoles: [], staffList: [...] |
400,拒绝 |
| ❌ staffList 里有超出范围的角色 | scopeRoles: ["GUIDE","LEADER"], staffList: [{staffRole:"PHOTOGRAPHER",...}] |
582115,拒绝 |
五、数据库行为
无 DDL、无 Flyway 迁移。全量覆盖与范围覆盖的删除窗口不同:
| 操作 | 删除哪些旧行 | 插入哪些新行 |
|---|---|---|
不传 scopeRoles |
整期所有角色 | staffList 的全部项 |
传 scopeRoles: ["GUIDE","LEADER"] |
只删这两个角色的旧行 | staffList 的全部项(但必须都在范围内) |
范围外的既有行保持:如果某个角色的人员不在覆盖范围内,本次保存对它们零影响,仍留在数据库。
六、边界行为
- 未登录 → 401(网关拦截)
- 团期不存在/未成团 → 589500 或 589552
- 资源域 Feign 调用慢或失败 → 内部重试或超时返 500;不会静默降级造成快照不完整
- 扇出异常 → 主事务已提交(数据已存盘),只是下游不知道;用户需手工检查或等待重试周期
六.6、修改前后对比
| 项 | 改前 | 改后 |
|---|---|---|
| 覆盖范围 | 全量(整期所有角色全删再全插) | 可选:不传 = 全量,传 scopeRoles = 按范围 |
| 删除窗口 | DELETE FROM order_batch_staff WHERE product_batch_id=? |
按 scopeRoles 缩小范围 |
staffList 回显 |
本次提交的 items | 保存后的整期最终状态 |
| 扇出数据 | 本次提交的 items(错误:只含本范围) | 整期最终状态(正确:全部角色) |
| 导游位保存风险 | 漏带摄影位 → 摄影师清空 | 不再互相干扰 |
| 错误码集合 | — | 新增 582115(范围校验失败) |
| 入参 | — | 新增 scopeRoles(可选) |
六.7、影响评估
| 维度 | 评估 |
|---|---|
| 兼容性 | 完全向后兼容。不传 scopeRoles 时行为逐字不变,现有调用无需改动 |
| 前端 | 需要改动。弹窗打开时自动填充 scopeRoles(导游位 → ["GUIDE","LEADER"],摄影位 → ["PHOTOGRAPHER"]),保存时传上去。后端已在测试环境部署在线,可立即改代码进行联调与实测 |
| 数据 | 无 DDL;存量 order_batch_staff 数据不动 |
| 回滚 | git revert 后,不传 scopeRoles 的调用照常工作;已传 scopeRoles 的调用会因新字段不认而 400(但改前没人用它,实际无影响) |
| 必须同步上线 | 是。前后端一起上,否则前端新代码传 scopeRoles 到旧后端是 400 |
七、不影响范围
- 零影响:接口的 HTTP 方法、URL 路径、路径参数
- 零影响:不传
scopeRoles时的全量覆盖行为(与改前一致) - 零影响:存量
order_batch_staff行(本次不迁移);下次保存时按新逻辑处理 - 零影响:其他 staff 相关接口(查询、删除等)
- 零影响:扇出至子订单的链路(改的只是快照的构成方式,语义不变)
八、测试环境已验证
✅ PR #8117 已合入 dev-v3(合并提交 094521f0b),当前测试环境部署 commit d30cd9561,本次变更已在线。
编译验证:CI 全绿,含 ArchTest / 单测 / 静态检查
单测覆盖(241 examples new + existing,全过):
- 范围保存的删除窗口(0.5 → 1.5 间隔)
- 导游位必须同时声明两个角色(缺一触发 582115)
- staffList 超出范围拒绝(582115)
- 回显值切换(toInsert → finalState)
- 扇出快照来源(同上)
- ready 回填按整期算(不按本范围算)
- 不传 scopeRoles 时全量行为不变
测试数据:一律 T8006- 前缀,验证后已清理。
十、相关文档
关联 / 联系人
链接
联系人
- 后端负责人: @wx