From 8fa0204a435a82ebd6754bfad74e478267a5e351 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 21 Sep 2026 23:09:29 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=9B=A2=E6=9C=9F=20staff=20=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E=20scopeRoles=20?= =?UTF-8?q?=E5=85=A5=E5=8F=82=E4=B8=8E=E8=8C=83=E5=9B=B4=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=EF=BC=88#8006=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- ...f保存支持按角色范围覆盖-修改接口-管理后台.md | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 changelogs-v2/2026-09/21_8006_团期staff保存支持按角色范围覆盖-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/21_8006_团期staff保存支持按角色范围覆盖-修改接口-管理后台.md b/changelogs-v2/2026-09/21_8006_团期staff保存支持按角色范围覆盖-修改接口-管理后台.md new file mode 100644 index 00000000..9b1ede9f --- /dev/null +++ b/changelogs-v2/2026-09/21_8006_团期staff保存支持按角色范围覆盖-修改接口-管理后台.md @@ -0,0 +1,370 @@ +--- +schema: "hl-changelog/v2" +ticket: "8006" +title: "团期 staff 保存支持按角色范围覆盖,不再跨配置位清空" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "gateway_status: not_required 待确认——新增入参 scopeRoles 可能涉网关字段白名单,但无网关实测数据;错误码/响应体改动对网关无影响。PR #8117 已合入 dev-v3(合并提交 094521f0b),当前测试环境部署 commit d30cd9561,本次变更已在线。" +updated_at: "2026-09-21" +base: "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` | ❌ | 元素取值: `LEADER` / `GUIDE` / `DRIVER` / `PHOTOGRAPHER` / `OTHER` / `GUIDE_ASSISTANT` / `STUDY_TEACHER` / `LIFE_TEACHER`;**不传 = 整期全量覆盖**(历史行为),**传了就必须 ≥ 1 项**(传空数组 400 拒绝) | **新增**。本次保存覆盖的角色范围。不传时全量覆盖整期(与改前一致),传了则只在这些角色内覆盖,范围外的既有行不动。**特别注意:导游位并收 `GUIDE` 与 `LEADER` 两个角色,保存导游位时必须同时传这两个,只传其中一个会导致 `582115` 拒绝**(见下方错误响应) | +| `staffList` | Body | `List` | ✅ | — | 本范围内要保存的人员列表。传空数组 `[]` 表示清空该范围内的人员(与改前一致) | +| `staffList[].staffId` | Body | Long | ✅ | — | 资源域人员 ID | +| `staffList[].staffRole` | Body | String | ✅ | 同上 `scopeRoles` 的取值域 | 人员在本配置中的角色。如果传了 `scopeRoles`,这里的每一项都必须在范围内,否则 `582115` 拒绝 | +| `staffList[].sortOrder` | Body | Integer | ❌ | — | 展示排序(缺省 0) | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `productBatchId` | Long | 回显路径参数 | +| `groupBatchId` | Long | 运营团期 ID(由 productBatchId 反查得到) | +| `staffList` | `List` | **保存后的整期最终状态**(见下方说明) | +| `affectedOrderCount` | Integer | 扇出影响的活跃子订单数 | + +**`staffList` 回显语义变化**: +- **改前**:只回显本次提交的 items(即请求体的 `staffList`) +- **改后**:回显**保存完成后整个团期的最终状态**——无论你是全量保存还是按范围保存,返回都是整期全部人员的完整快照 + - 好处 1:前端无需再发第二个 GET 请求去拿最新名单 + - 好处 2:前端能看到"我这一存操作扇出到多少订单"和"团期现在全部配置是啥",更清楚整体状态 + - 注意:如果你传了 `scopeRoles=["PHOTOGRAPHER"]` 只保存摄影位,返回的 `staffList` 仍然包含导游位的人(如果有的话),这是正常的,代表团期的完整配置 + +#### 请求示例 + +**场景 1:按范围保存(新用法)** + +前端打开导游位弹窗,改了导游/领队名单,保存时只声明导游位: + +```json +{ + "scopeRoles": ["GUIDE", "LEADER"], + "staffList": [ + { + "staffId": 1005, + "staffRole": "LEADER", + "sortOrder": 0 + }, + { + "staffId": 1002, + "staffRole": "GUIDE", + "sortOrder": 1 + } + ] +} +``` + +**场景 2:全量保存(历史用法,不传 `scopeRoles`)** + +```json +{ + "staffList": [ + { + "staffId": 1005, + "staffRole": "LEADER", + "sortOrder": 0 + }, + { + "staffId": 1002, + "staffRole": "GUIDE", + "sortOrder": 1 + }, + { + "staffId": 2003, + "staffRole": "PHOTOGRAPHER", + "sortOrder": 0 + } + ] +} +``` + +#### 响应示例 + +```json +{ + "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` 传空数组清空覆盖范围内的人员: + +```json +{ + "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` 传了空数组**: + +```json +{ + "code": 400, + "message": "scopeRoles 传了就不能是空数组;要整期全量覆盖请整个字段不传", + "success": false, + "data": null +} +``` + +**提交的人员角色超出 `scopeRoles` 声明的范围**(新错误码 `582115`): + +```json +{ + "code": 582115, + "message": "提交的人员角色(PHOTOGRAPHER)超出本次保存声明的角色范围(GUIDE,LEADER),请检查配置位与人员是否匹配", + "success": false, + "data": null +} +``` + +**特别情况:导游位只声明了 `GUIDE` 却选了 `LEADER`**: + +```json +{ + "code": 582115, + "message": "提交的人员角色(LEADER)超出本次保存声明的角色范围(GUIDE),请检查配置位与人员是否匹配", + "success": false, + "data": null +} +``` + +**`staffList` 字段缺失**: + +```json +{ + "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-` 前缀,验证后已清理。 + +--- + +## 十、相关文档 + +- 工单:[#8006](https://git.1814.love:8443/wx/HL/issues/8006) — 团期人员配置跨角色清空风险 +- PR:[#8117](https://git.1814.love:8443/wx/HL/pulls/8117) — 本次修复 +- 相关缺口:[#8122](https://git.1814.love:8443/wx/HL/issues/8122) — `scopeRoles` 完整性校验(已记录,待处理) + +--- + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#8006](https://git.1814.love:8443/wx/HL/issues/8006) +- **PR**: [#8117](https://git.1814.love:8443/wx/HL/pulls/8117) +- **Merge commit**: [094521f0b](https://git.1814.love:8443/wx/HL/commit/094521f0b) + +### 联系人 + +- **后端负责人**: @wx