docs(changelog): #8122 团期 staff 保存 scopeRoles 必须整位覆盖(582116)
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
PUT /v3/admin/group-batch/{productBatchId}/staff 行为收紧:
scopeRoles 触及某配置位即须覆盖该位全部成员角色,半位声明拒绝且零写入。
配置位成员由字典决定,不是代码常量。
Refs #8122, PR #8147, 后续单 #8148 / #8150
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -0,0 +1,409 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8122"
|
||||
title: "团期 staff 保存:scopeRoles 必须整位覆盖,半位声明一律拒绝且零写入(新错误码 582116)"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-22"
|
||||
status_note: "backend_status=deployed: hl-order-service-v3 测试服双实例(8086 / 8186)当前运行 sha=ef7611138,即本单合并提交(PR #8147)本身,不是它的后代——无需 merge-base 推导。三条互相独立的判据:①deploy-status.sh 登记 dev-v3 / ef7611138 / BEHIND=0;②jar /opt/hulalv/jars/hl-order-service-v3-1.0.0-SNAPSHOT.jar mtime=epoch 1790039292(2026-09-22 09:08:12),两实例 /proc/<pid> 启动时刻 epoch 1790039294 与 1790039307 均晚于 jar mtime,且两进程 fd 均指向该 jar 路径、无 (deleted);③Nacos 两实例 healthy:true & enabled:true,启动日志 Flyway 正常、ERROR/Exception/APPLICATION FAILED TO START 零命中。⚠️ 覆盖边界:HTTP 级业务健康(本接口的端到端实际返回)未在测试服实测,本单取证在 Testcontainers 真库 IT 完成(见正文第八节),两者是不同环境、不可互相替代。gateway_status=not_required: 本单改动 6 个文件全在 hl-order-service-v3 模块内,未涉及 hl-gateway 任何路由/Nacos 配置;PUT /v3/admin/group-batch/{productBatchId}/staff 是存量端点、路径本身未变,命中既有通配路由(Path=/v3/admin/**),本单没有引入任何新路径段。frontend_status=pending: 按规范后端不代填 implemented/released/verified,是否需要改动与是否完成由前端自行回写 frontend_owner 与 verified_at。本单对现有前端实现的影响评估见正文「六.7 影响评估」——结论是 mmg 在 #8006 交付的 scopeRoles=roleMeta.memberRoles(GUIDE 位含 GUIDE+LEADER 双角色)本身已是整位覆盖,不会命中新错误码,但该结论基于 hl-ui origin/v2.1 @ b5666b38 的源码读数,请前端按自己分支的实际实现复核。"
|
||||
updated_at: "2026-09-22"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期 staff 保存,scopeRoles 必须整位覆盖(新错误码 582116)
|
||||
|
||||
> **存放目录**: `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3 (端口 8086 / 8186)
|
||||
> **PR**: #8147
|
||||
> **Issue**: #8122
|
||||
> **日期**: 2026-09-22
|
||||
> **影响范围**: 团期 staff 配置保存接口 `PUT /v3/admin/group-batch/{productBatchId}/staff`
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**这是一次行为收紧:一类此前返回 HTTP 200 的请求,现在会被拒绝(业务码 582116)。**
|
||||
|
||||
具体是:`scopeRoles` 只要**触及**某个配置位,就必须**覆盖该配置位的全部成员角色**。
|
||||
只声明半个配置位(例如导游位只传 `["GUIDE"]`、不带 `LEADER`)会被拒绝,且**零写入**。
|
||||
|
||||
改前这类请求返回 200,但只删掉并重建了被声明的那半边,位内其余人员**原样留在库里**,
|
||||
还会继续扇到每一张子订单——用户在回显里看见「领队怎么还在」。
|
||||
|
||||
✅ **对既有错误码零影响**:改前已被 582114 / 582115 拒掉的请求,错误码一字不变。
|
||||
本单只改变**改前返回 200** 的那些请求的结论。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
`scopeRoles` 由 #8006 引入,语义是「本次保存只覆盖这些角色」。它同时承担两件事:
|
||||
|
||||
1. **插入白名单**——提交的人只能落在声明的角色里(越界抛 582115);
|
||||
2. **删除窗口**——软删只删声明的那些角色的旧行。
|
||||
|
||||
此前只有 (1) 被守卫住,(2) 从没有任何一处校验过。于是「声明了半个配置位」这种请求,
|
||||
插入侧完全合法(提交的人确实都在范围内),删除侧却只擦掉了半边:
|
||||
|
||||
- 导游位原有 1 名领队(LEADER)+ 1 名导游(GUIDE);
|
||||
- 用户在弹窗里删掉领队、只留导游,前端发 `scopeRoles=["GUIDE"]`;
|
||||
- 服务端只删 GUIDE 行、插新 GUIDE 行,**那条 LEADER 一行没动**;
|
||||
- 接口返回 200,无任何错误码;该 LEADER 经异步扇出继续写进每一张子订单。
|
||||
|
||||
⚠️ #8006 的交接件里已写过「导游位必须同时传 `GUIDE` 与 `LEADER`」,但那当时只是**对前端的请求**,
|
||||
服务端没有对应守卫——把这句话从文档里整句删掉,代码行为一字不变、没有一个用例会红。
|
||||
本单把它变成服务端强制的不变量。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 保存团期 staff 配置 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 新增拒绝分支 | 新增业务码 `582116`;入参结构、出参结构、字段类型全部未变 |
|
||||
|
||||
**入参、出参、字段类型全部未变。** 本单只新增一条校验与一个业务错误码。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 保存团期 staff 配置(含扇出) `PUT /v3/admin/group-batch/{productBatchId}/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO → BatchStaffConfigRespVO`(两者结构均未变,见 21_8006 交接件)
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情页「导游位」「摄影位」两个弹窗各自维护本位人员时调用。每个弹窗用 `scopeRoles`
|
||||
声明自己负责的角色范围,只提交本位人员,互不影响(#8006)。
|
||||
本单新增的约束落在这个场景的正中间:**弹窗声明的范围必须是完整的配置位**,
|
||||
否则本位里没被声明的那些人会被静默留在库里。
|
||||
|
||||
#### 入参
|
||||
|
||||
**路径参数**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `productBatchId` | Long(路径) | 是 | 产品侧班期 ID |
|
||||
|
||||
**请求体** `BatchStaffConfigReqVO`
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| `scopeRoles` | body | `String[]` | 否 | 元素取值域 `LEADER\|GUIDE\|DRIVER\|PHOTOGRAPHER\|OTHER\|GUIDE_ASSISTANT\|STUDY_TEACHER\|LIFE_TEACHER`;传了就不能是空数组(`@Size(min=1)`,否则 `code=400`) | 本次覆盖的角色范围。**不传 = 整期全量覆盖**(历史行为)。🔴 **本单新增:传了则必须整位覆盖**,否则 582116 |
|
||||
| `staffList` | body | `Item[]` | **是** | `@NotNull`;显式传 `[]` = 清空覆盖范围 | 覆盖范围内的最终状态。**缺字段一律 `code=400`**(审计 F-03,Refs #6950 / #7377:改前缺字段会被当成"传空=清空",静音软删整团配置还返成功) |
|
||||
| `staffList[].staffId` | body | Long | 是 | `@NotNull` | 用户域员工 ID |
|
||||
| `staffList[].staffRole` | body | String | 是 | `@NotBlank` + 同上取值域 | 员工角色 |
|
||||
| `staffList[].sortOrder` | body | Integer | 否 | — | 展示排序,默认 0 |
|
||||
| `staffList[].remark` | body | String | 否 | `@Size(max=500)` | 备注 |
|
||||
|
||||
#### 出参
|
||||
|
||||
`BatchStaffConfigRespVO`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `productBatchId` | String | 回显路径参数(`@JsonSerialize(ToStringSerializer)`,**JSON 里是字符串**),恒非 null |
|
||||
| `groupBatchId` | String | 运营团期 ID(同上,字符串)。本端点入口有成团守卫,200 响应中恒非 null |
|
||||
| `staffList` | `BatchStaffItemVO[]` | 保存后的配置快照 |
|
||||
| `staffList[].id` | Number | 记录 ID(`batch_staff_id`) |
|
||||
| `staffList[].staffId` | Number | 用户域员工 ID |
|
||||
| `staffList[].staffRole` / `staffRoleName` | String | 角色码 / 中文名(取值不在枚举内时回落原 code) |
|
||||
| `staffList[].staffName` / `staffPhone` / `avatarUrl` | String | 姓名 / 手机(脱敏前 3 后 4)/ 头像,均为 Feign 反查后冻结的快照 |
|
||||
| `staffList[].sortOrder` | Integer | 展示排序 |
|
||||
| `staffList[].remark` | String | 备注 |
|
||||
| `staffList[].reporterRank` / `reporterRankName` | String | 报账人等级 `PRIMARY`/`SECONDARY`/`NONE` 及中文名,恒非 null(空值归一为 `NONE`) |
|
||||
| `affectedOrderCount` | int | 扇出影响的活跃订单数 |
|
||||
|
||||
🔴 本单对入参、出参的**字段集合与类型零改动**,上两表与改前逐字一致,列在此处只为本文件自包含。
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/group-batch/80001/staff
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"scopeRoles": ["GUIDE", "LEADER"],
|
||||
"staffList": [
|
||||
{ "staffId": 40001, "staffRole": "GUIDE", "sortOrder": 0, "remark": "首席导游" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"data": {
|
||||
"productBatchId": "80001",
|
||||
"groupBatchId": "90211",
|
||||
"staffList": [
|
||||
{
|
||||
"id": 770001,
|
||||
"staffId": 40001,
|
||||
"staffRole": "GUIDE",
|
||||
"staffRoleName": "导游",
|
||||
"staffName": "刘导游",
|
||||
"staffPhone": "138****6677",
|
||||
"avatarUrl": null,
|
||||
"sortOrder": 0,
|
||||
"remark": "首席导游",
|
||||
"reporterRank": "NONE",
|
||||
"reporterRankName": "非报账人"
|
||||
}
|
||||
],
|
||||
"affectedOrderCount": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
**清空某个配置位**(整位声明 + 空 `staffList`)后,`staffList` 返回的是**整期最终状态**
|
||||
(不是本次提交的空数组)——范围外角色的既有行仍在其中。整期一个人都没有时才是空数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"data": {
|
||||
"productBatchId": "80001",
|
||||
"groupBatchId": "90211",
|
||||
"staffList": [],
|
||||
"affectedOrderCount": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字典降级**:配置位成员读字典(Feign hl-system 字典)。字典读空或 Feign 失败时,
|
||||
服务端**回落到内置兜底成员**(导游位 `GUIDE`+`LEADER`、摄影位 `PHOTOGRAPHER`),
|
||||
不会沿用脏缓存、也不会因此放行半位声明 ⇒ 降级期间 582116 的判定按兜底成员进行。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 业务码 | 常量 | 文案(`{0}`/`{1}` 运行期填充) |
|
||||
|---|---|---|
|
||||
| **582116** | `AssignmentErrorCode.SCOPE_ROLES_SLOT_INCOMPLETE` | `本次保存声明的角色范围({0})只覆盖了配置位的一部分,还缺少 {1};同一配置位的角色必须一起声明,否则位内其余人员会被留在库里` |
|
||||
|
||||
- `{0}` = 本次请求的 `scopeRoles` 原样内容,以 `/` 连接(例:`GUIDE`)
|
||||
- `{1}` = **缺失的同位角色**,以 `/` 连接(例:`LEADER`)
|
||||
|
||||
⇒ 文案本身就把「该补什么」告诉了调用方,前端可直接透传 `message` 给用户。
|
||||
|
||||
**被拒响应体**(🔴 **HTTP 状态行是 200**,业务失败写在 `code` 里,别按状态码判成败)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 582116,
|
||||
"msg": "本次保存声明的角色范围(GUIDE)只覆盖了配置位的一部分,还缺少 LEADER;同一配置位的角色必须一起声明,否则位内其余人员会被留在库里",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
其余错误码本单**未新增也未改动**:
|
||||
|
||||
| 业务码 | 触发 |
|
||||
|---|---|
|
||||
| `400` | `@Valid` 参数级校验失败(`staffList` 缺字段、`scopeRoles: []`、角色不在取值域)。同样是 HTTP 200,`msg` 为各字段 `message` 用 `"; "` 拼接 |
|
||||
| `582114` | 所选人员的角色与其人员类型不符 |
|
||||
| `582115` | 提交的人员角色超出本次声明的 `scopeRoles`(与 582116 的分工见第四节) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
**判定规则**——对**每一个**配置位分别判定:
|
||||
|
||||
```
|
||||
touched = scopeRoles 里至少有一个元素属于该配置位
|
||||
missing = 该配置位的成员角色中,scopeRoles 没有声明的那些
|
||||
若 touched 且 missing 非空 ⇒ 抛 582116
|
||||
```
|
||||
|
||||
配置位当前有两个:
|
||||
|
||||
| 配置位 | 字典类型 | 成员角色(字典读不到时的兜底值) |
|
||||
|---|---|---|
|
||||
| 导游位 `GUIDE` | `group_batch_staff_slot_guide` | `GUIDE`、`LEADER` |
|
||||
| 摄影位 `PHOTOGRAPHER` | `group_batch_staff_slot_photographer` | `PHOTOGRAPHER` |
|
||||
|
||||
🔴 **成员角色由字典决定,不是代码常量。** 表中列的是「字典读不到时的兜底值」,
|
||||
运行期真实成员以字典 `dict_type` 下的行为准——业务在字典管理页面往导游位加一个角色,
|
||||
第二天起 `scopeRoles` 就必须带上它,**服务端不需要改代码,这个过程不产生任何接口变更**。
|
||||
⇒ 前端不应把成员角色写死在代码里,应按配置位动态取用;
|
||||
写死 `["GUIDE","LEADER"]` 的实现会在业务改字典的那一刻开始收到 582116。
|
||||
|
||||
其余边界:
|
||||
|
||||
- **`scopeRoles` 整个字段不传** = 整期全量覆盖,本守卫不参与判定,历史行为逐字不变。
|
||||
- **零写入**:582116 在任何库写动作之前抛出(守卫排在写入序列的 1.6 位),被拒的请求不产生
|
||||
任何插入 / 更新 / 软删,也不触发向订单的扇出,`affectedOrderCount` 不会被消耗。
|
||||
- **不属于任何配置位的角色**(`DRIVER`、`OTHER`、`STUDY_TEACHER`、`LIFE_TEACHER`):
|
||||
对两个配置位的 `touched` 都恒为 false,单独声明它们**不会**被本守卫拦下。
|
||||
- **字典判定带 5 分钟进程内缓存,服务多实例**:改完字典后最多 5 分钟内,
|
||||
两个实例对同一个 `scopeRoles` 可能给出不同判定(一个放行一个 582116)。重试即可收敛。
|
||||
- **`scopeRoles: [null]`** 当前绕得过本守卫(元素级 `@Pattern` 对 null 恒真),已立 **#8150**;
|
||||
前端不要构造含 null 的数组——它绕过的是保护,不是限制。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| # | payload(节选) | 结果 | 说明 |
|
||||
|---|---|---|---|
|
||||
| ✅ | `{"scopeRoles": ["GUIDE","LEADER"], "staffList": [...]}` | 200 | 导游位整位声明,本单的正确写法 |
|
||||
| ✅ | `{"scopeRoles": ["GUIDE","LEADER"], "staffList": []}` | 200 | **清空导游位**:整位声明 + 空人员表,位内两个角色的旧行一起被删 |
|
||||
| ✅ | `{"scopeRoles": ["PHOTOGRAPHER"], "staffList": [...]}` | 200 | 摄影位只有一个成员,单元素即整位 |
|
||||
| ✅ | `{"staffList": [...]}`(不传 `scopeRoles`) | 200 | 整期全量覆盖,守卫整体短路,行为与改前逐字一致 |
|
||||
| ✅ | `{"scopeRoles": ["DRIVER"], "staffList": [...]}` | 200 | `DRIVER` 不属于任何配置位 ⇒ 一个位都没 touched ⇒ 守卫不介入 |
|
||||
| ❌ | `{"scopeRoles": ["GUIDE"], "staffList": [...]}` | **582116** | 触到导游位却缺 `LEADER` |
|
||||
| ❌ | `{"scopeRoles": ["LEADER"], "staffList": []}` | **582116** | 同上,缺 `GUIDE`;**注意 `staffList` 为空也照样拒**(见下) |
|
||||
| ❌ | `{"scopeRoles": ["GUIDE","PHOTOGRAPHER"], "staffList": [...]}` | **582116** | 跨两个位,导游位缺 `LEADER` |
|
||||
|
||||
🔴 **`staffList` 为空不豁免。** 「清空导游位」正是本单最需要拦住的路径:
|
||||
它一个越界的人都没有(582115 永远不会触发),却会把位内没声明的那些人**永久留在库里**。
|
||||
清空某个位的正确写法是「整位声明 + 空 `staffList`」,见上表第 2 行。
|
||||
|
||||
### 与 582115 的分工(两码不可互换)
|
||||
|
||||
| | 582115 | 582116 |
|
||||
|---|---|---|
|
||||
| 说的是 | 你**提交的人**越界了 | 你声明的**范围本身**不是一个完整的配置位 |
|
||||
| 改法 | 改 `staffList`,或扩 `scopeRoles` | 只有一条:把缺的同位角色补进 `scopeRoles` |
|
||||
| `staffList` 可否为空 | 否(没有人就没有越界的人) | **是** |
|
||||
|
||||
⇒ 前端两个错误码要分开处理:582115 引导用户去改人员勾选,582116 是前端自己的 payload 拼错了。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 场景 | `group_batch_staff` | 子订单 `order_batch_staff` |
|
||||
|---|---|---|
|
||||
| 命中 582116 | **零写入** | **零写入** |
|
||||
| 通过 | 与改前一致(软删声明范围内旧行 + 插入新行) | 与改前一致(`afterCommit` 异步扇出) |
|
||||
|
||||
🔴 **「零写入」指的是软删从没有执行过,不是「执行了又回滚」。**
|
||||
守卫落在事务开启**之前**,走不到软删那一行。真库 IT 用 `openSession(true)`(autoCommit)
|
||||
从另一条连接读取,若软删跑过就会当场提交、读不到「原样」——该断言对两种情形有分辨力。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
1. **不传 / 传空数组 `scopeRoles`** ⇒ 守卫整体短路,整期全量覆盖语义与改前**逐字一致**。
|
||||
2. **`scopeRoles` 全是不属于任何配置位的角色**(如 `["DRIVER"]`)⇒ 一个位都没 touched,放行。
|
||||
3. **守卫顺序**:582114(人员类型不符)→ 582115(提交的人越界)→ **582116**(范围半位)→ 事务。
|
||||
⇒ 一个请求若同时满足 582115 与 582116 的条件,返回的是 **582115**(先到先抛)。
|
||||
4. 🔴 **配置位成员有 5 分钟本地缓存,且服务是双实例。**
|
||||
`GroupBatchStaffSlotResolver` 对字典结果做 5 分钟 TTL 的进程内缓存,测试服与生产的
|
||||
order-v3 均为多实例 + 网关轮询。⇒ **业务刚改完字典的 5 分钟内,同一个 payload 可能在一个实例上通过、
|
||||
在另一个实例上撞 582116。** 这个窗口过后两边一致。前端遇到这种抖动不必特殊处理,
|
||||
按 `{1}` 提示补齐 `scopeRoles` 即可——补齐后的 payload 在新旧两种口径下都是合法的。
|
||||
5. **`scopeRoles` 元素为 `null`**(`["GUIDE", null]` 这类):元素级校验目前对 `null` 恒真,
|
||||
该元素不会触及任何配置位。若整个数组只有 `null`,请求会成为一次返回 200 的空操作。
|
||||
属存量缺口,已立 **#8150** 跟进。正常调用方不会构造这种 payload。
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
| 字典类型 | 含义 | 维护方 |
|
||||
|---|---|---|
|
||||
| `group_batch_staff_slot_guide` | 导游位收哪些人员类型 | 业务在字典管理页面维护 |
|
||||
| `group_batch_staff_slot_photographer` | 摄影位收哪些人员类型 | 同上 |
|
||||
|
||||
服务端对字典值做白名单过滤(合法集合:`GUIDE`、`GUIDE_ASSISTANT`、`PHOTOGRAPHER`、`LEADER`、
|
||||
`OTHER`、`STUDY_TEACHER`、`LIFE_TEACHER`),字典里的非法值会被丢弃;字典读空或 Feign 失败时
|
||||
回落到上表的兜底值,不会沿用脏缓存。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
**行为级**
|
||||
|
||||
| 请求形态 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `scopeRoles` 半位声明,`staffList` 非空 | 200,位内未声明的旧行残留并扇到子订单 | **582116**,零写入 |
|
||||
| `scopeRoles` 半位声明,`staffList` 为空 | 200,位内未声明的旧行残留 | **582116**,零写入 |
|
||||
| `scopeRoles` 整位声明 | 200 | 200(不变) |
|
||||
| 不传 `scopeRoles` | 200,整期全量覆盖 | 200(不变) |
|
||||
| 命中 582114 / 582115 的请求 | 对应错误码 | **对应错误码不变** |
|
||||
|
||||
**字段级**:无变化(无新增/删除/改名字段,无类型变化)。
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
🔴 **任何当前发送「半个配置位」的调用方,都会从 200 变成 582116。** 这是本单的预期效果,
|
||||
但它是一次**破坏性收紧**,请前端按自己分支的实际实现逐处核对。
|
||||
|
||||
已做的评估(**源码读数,非联调实测**):mmg 在 #8006 交付的实现
|
||||
(`hl-ui` origin/v2.1 @ `b5666b38`,`GroupBatchStaffConfigModal`)按配置位取
|
||||
`scopeRoles = roleMeta.memberRoles`,导游位取到的是 `GUIDE + LEADER` 双角色
|
||||
⇒ 本身已是整位覆盖,**不会**命中 582116。该结论的效力止于那个 sha 的源码,
|
||||
若前端另有分支或后续改动,请以自己的实现为准。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 团期 staff **查询**接口 `GET /v3/admin/group-batch/{productBatchId}/staff`:零改动。
|
||||
- 单订单 staff 接口 `/v3/admin/order/{id}/staff`:零改动,不走本守卫。
|
||||
- 司机(`DRIVER`):由车务派车投影产生,不在任何配置位里,本守卫对它零影响。
|
||||
- 异步扇出链路、`guide_ready` / `photographer_ready` 回填、四 ready 闸门:逻辑零改动
|
||||
(通过的请求扇出行为与改前完全一致)。
|
||||
- 网关路由、Nacos 配置、数据库表结构:零改动。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**真库集成测试**(Testcontainers MySQL 8.0.33,非 H2、非 Mockito)
|
||||
`GroupBatchStaffSaveConfigSlotCompletenessMysqlTest`:**8/8 通过**
|
||||
|
||||
| 验证项 | 做法与读数 |
|
||||
|---|---|
|
||||
| 缺陷确实存在 | 临时注释掉守卫后跑,团期表与子订单表**都**残留 `LEADER` 行;前置由真实 `saveConfig` + `afterCommit()` 产生,非 SQL 直插 |
|
||||
| 零写入 | `SELECT *` 整行逐字段比对(含 `deleted_at`、`update_time`)均与保存前相同;用 `openSession(true)` 从另一条连接读,对「回滚」与「从没发生」有分辨力 |
|
||||
| 字典驱动**确实生效** | 把 `slotResolver.typesOf(slot)` 变异成写死的兜底表,重编译确认后 7 个用例里**唯一转红**的正是「字典新增 `GUIDE_ASSISTANT` 后旧 `scopeRoles` 被拒」那条(变异已还原) |
|
||||
| 阴性对照 ×4 | 整位声明放行 / 补齐字典新成员后放行 / `["DRIVER"]` 不触位放行 / 不传 `scopeRoles` 保持全量覆盖语义 |
|
||||
|
||||
**回归**:`GroupBatchStaffConfigServiceTest*` 77/77、`Baseline` 3/3、`Scoped` 2/2、
|
||||
`AdminControllerPermissionTest` 6/6、`ReqVOValidationTest` 6/6、`ConfigServiceGateTest` 5/5、
|
||||
`RedLineArchTest` 12/12。
|
||||
|
||||
**测试服部署**:双实例 8086 / 8186 运行 `ef7611138`(即 PR #8147 的合并提交本身),
|
||||
Nacos 两实例 `healthy:true`,启动日志无异常。判据与覆盖边界见 frontmatter 的 `status_note`。
|
||||
|
||||
---
|
||||
|
||||
## 九、已知缺口(不在本单范围,已立单跟进)
|
||||
|
||||
- **#8148**:`allowedStaffTypesForRole` 写死 `GUIDE`/`LEADER`/`PHOTOGRAPHER` 三个分支、不读字典(存量)。
|
||||
本单提升了它的影响:业务往导游位字典加 `GUIDE_ASSISTANT` 后,`scopeRoles` **必须**带上它(否则 582116),
|
||||
而真正 `staffType=GUIDE_ASSISTANT` 的人提交时仍会被 **582114** 挡死。
|
||||
🔴 **⇒ 现阶段「字典加一个角色」只启用了范围判定这一半,那类人员还配不进去。**
|
||||
前端若正在规划「新增人员类型」相关交互,这条是必须知道的边界。
|
||||
- **#8150**:`scopeRoles` 元素级校验对 `null` 恒真(存量,见「六、5」)。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- `22_8006_团期人员保存支持按角色范围覆盖-修改接口-管理后台.md` —— `scopeRoles` 入参的引入者,入参/出参/响应结构以它为准
|
||||
- `changelogs-v2/2026-09/` 下 #7079 相关条目 —— 配置位字典化
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- Issue: #8122 | PR: #8147 | 后续单: #8148、#8150
|
||||
- 后端: wx | 管理后台前端: mmg
|
||||
在新工单中引用
屏蔽一个用户