changelog(8194): 配置位缺失守卫 582117 触发集合扩大(管理后台)
changelog-filename-gate / validate (push) Failing after 2s

工单 #8194 / PR #8259。PUT /v3/admin/group-batch/{productBatchId}/staff:
582117 的触发角色集合由「内置兜底名单 GUIDE/LEADER/PHOTOGRAPHER」扩大为
「除 DRIVER / OTHER 以外的全部取值域角色」,字典启用过的
GUIDE_ASSISTANT / STUDY_TEACHER / LIFE_TEACHER 被删回后不再静默放行。

契约零变更(路径 / 入参 / 权限 / 成功响应结构不动),属行为变化。
测试服已实测并已还原现场。
这个提交包含在:
API Changelog Bot
2026-09-23 16:10:21 +08:00
父节点 5951522b47
当前提交 40165d4e47
@@ -0,0 +1,340 @@
---
schema: "hl-changelog/v2"
ticket: "8194"
title: "配置位守卫 582117 的触发集合扩大到「除 DRIVER / OTHER 以外的全部角色」"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-09-23"
status_note: "工单 #8194 补的是 #8148 留下的残留缺口。角色↔人员类型的守卫 582117 判的是「该角色在配置位字典里查不到归属位」,而它的分流判据(GroupBatchStaffSlotResolver#participatesInSlotsByFallback)读的是编译期常量兜底名单,只认 GUIDE / LEADER / PHOTOGRAPHER。问题是 #7079 / #8148 立下的能力恰恰是「业务在字典页面加一行就能把某个人员类型配进配置位、不发版」——靠字典新增出来的角色永远不在这张常量名单里,于是「字典给某位加一行 → 有人按它配了人 → 那行又被删掉」这一形态仍走静默放行分支:接口 200、脏行落库、并异步扇出到团内全部活跃订单,该角色的 582114 当场失效,要到结算对账才发现人岗对不上。关键一点是撞上它不需要有人手搓请求——resolveStoredStaffRole 会把落库的 staff_role 改写成人员的真实类型(字典角色),前端回显后原样提交回来,系统自己就会产出那个落进缺口的 staffRole 值。修法(2026-09-23 定案 B,判据翻面):把「本该有位」的判据从「在不在兜底名单里」换成「不在结构性豁免闭集 STRUCTURALLY_SLOT_EXEMPT_ROLES(= DRIVER / OTHER)里」,豁免闭集落成显式常量集合并从 SettlementStaffRoleEnum 取值,方法名同步改为 requiresSlotMembership(改完它不再读 fallbackTypes(),名字与语义必须同批改)。行为变化:GUIDE_ASSISTANT / STUDY_TEACHER / LIFE_TEACHER 三个「只可能靠字典纳入配置位」的角色,在「字典里查不到归属位」时由静默 200 落库变为返回 582117;DRIVER / OTHER 照旧放行(OTHER 的常态就是不属于任何配置位,保护它会拒掉每一次正常的杂项人员配置,那是删功能不是补洞)。已知边界(按 #8194 决策点 4 有意保留、不在本单修):OTHER 在 VALID_STAFF_TYPES 里,业务能把 OTHER 写进某个位的字典再删掉,那一形态仍走静默放行。生产代码由 PR #8259 合入 dev-v3(squash 提交 f6d21648b)。2026-09-23 测试服已实测:PUT /v3/admin/group-batch/2102640646949105666/staff 提交 staffRole=GUIDE_ASSISTANT 返回 code=582117(HTTP 200,文案完整),读回确认零写入;同端点 staffRole=DRIVER 与 staffRole=OTHER 均返回 code=200 成功;探测插入的两行已用 scopeRoles=[DRIVER,OTHER] + staffList=[] 还原,读回与探测前逐字段一致。契约(路径 / 方法 / 权限 / 入参字段与取值域 / 成功响应结构)零变更——BatchStaffConfigReqVO 只加了 javadoc(含该表)。"
updated_at: "2026-09-23"
base: "dev-v3"
---
# 团期人员配置: 配置位缺失守卫 582117 的触发集合扩大到全部非豁免角色(管理后台)
> **服务**: hl-order-service-v3(端口 8086/8186)
> **PR**: #8259(squash 提交 `f6d21648b`)
> **Issue**: #8194(前置 #8148 / #8122 / #7079)
> **日期**: 2026-09-23
> **影响范围**: 管理后台团期详情页的「配导游 / 配摄影」弹窗保存动作
---
## ⚠️ 关键变化
**这个端点返回 `582117` 的角色集合变大了**,路径、入参、成功响应结构一律不变。
以前只有 `GUIDE` / `LEADER` / `PHOTOGRAPHER` 三个内置角色在「字典里查不到归属位」时会被 582117 拒掉;现在**除 `DRIVER` 与 `OTHER` 以外的任何角色**都会被拒,其中就包括业务自己通过配置位字典启用过的 `GUIDE_ASSISTANT` / `STUDY_TEACHER` / `LIFE_TEACHER`。
调用方以前以为的是:`staffRole=GUIDE_ASSISTANT` 这类「字典角色」提交时不会触发 582117(它压根不在守卫的名单里)。
实际现在是:**它同样会触发 582117**,而且这批请求本来就是脏数据(角色没有任何人员类型校验背书)。
`DRIVER` / `OTHER` 的行为**不变**,仍放行。
---
## 一、背景
配置位成员集合自 #7079 起由数据字典决定(`group_batch_staff_slot_guide` / `group_batch_staff_slot_photographer`),业务在字典管理页面加一行就能把某个人员类型配进这个位,**不发版**。`GroupBatchStaffSlotResolver` 的类 javadoc 原文就是这句:「业务在字典管理页面给 `group_batch_staff_slot_guide` 加一行 `STUDY_TEACHER`,研学老师就能配进导游位,不发版」。
#8148 把「角色 → 允许的人员类型」这半边也迁到了字典,并引入 582117:当某角色在字典里查不到归属位时,本该有位的角色要被拒,而不是沿袭改前的「一律不校验」。但 #8148 的分流判据读的是**编译期常量兜底名单**(`GUIDE` / `LEADER` / `PHOTOGRAPHER`),它恰恰**不包含**任何「靠字典新增出来的角色」——那正是这个新能力的产物。结果是:
| 「查不到归属位」的成因 | #8148 之后的行为 | 是否正确 |
|---|---|---|
| `DRIVER` / `OTHER` 这类结构性不参与配置位的角色 | 放行 | ✅ 正确 |
| 内置角色(`GUIDE` / `LEADER` / `PHOTOGRAPHER`)被从仍非空的字典里删掉 | 拒绝 582117 | ✅ 正确 |
| **字典启用过的角色**(`GUIDE_ASSISTANT` / `STUDY_TEACHER` / `LIFE_TEACHER`)被删回 | **静默放行** | ❌ 缺口 |
第三种形态的失败后果与加 582117 之前**逐字相同**:HTTP 200、`Result.success`、前端看不到任何异常;落库成功并异步扇出到团内全部活跃订单;静默放行分支**一行日志都不打**。
而撞上它**不需要有人手搓请求**:
1. 业务给导游位字典加一行 `GUIDE_ASSISTANT`;
2. 前端按导游位提交 `staffRole=GUIDE`、该人真实 `staffType=GUIDE_ASSISTANT` → 校验通过 → `resolveStoredStaffRole` 把**落库的 `staff_role` 改写成 `GUIDE_ASSISTANT`**;
3. 下次打开弹窗,回显的既有行角色就是 `GUIDE_ASSISTANT`,提交回来 `staffRole=GUIDE_ASSISTANT`;
4. 此后运维把那行字典删掉 ⇒ 第 3 步那种请求走进静默分支,**且此时 `staffRole=GUIDE_ASSISTANT` 配任何 `staffType` 都能过**。
**修法(#8194 决策点 5 定案 B:判据翻面)**:把「本该有位」的判据从「在不在兜底名单里」换成「**不在结构性豁免闭集里**」。豁免闭集 = `{DRIVER, OTHER}`(#8194 决策点 4 定案),落成 `GroupBatchStaffSlotResolver#STRUCTURALLY_SLOT_EXEMPT_ROLES` 这一个显式常量集合。方法随之改名 `participatesInSlotsByFallback` → `requiresSlotMembership`——改完它不再读 `fallbackTypes()`,名字与语义必须同批改,否则留下「命名断言别处行为」的静默漂移点。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存团期人员配置 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 修改接口 | 582117 的触发角色集合扩大;路径 / 入参 / 权限 / 成功响应结构不变 |
网关无改动(没有新增 `/admin/` Controller,路由不动)。
---
## 三、接口详情
### 1. 保存团期人员配置 `PUT /v3/admin/group-batch/{productBatchId}/staff`
**VO**: `BatchStaffConfigReqVO`
#### 使用场景
团期详情页「配导游 / 配摄影」弹窗点保存。按 `scopeRoles`(不传即整期)软删旧配置、写入新配置、异步扇出到各活跃子订单,并按配置结果回填 `guide_ready` / `photographer_ready`。
🔴 **路径参数是产品侧排期 ID,不是团期聚合主键**。端点名叫 `group-batch`,但 `{productBatchId}` 吃的是 `group_tour_batch.batch_id`。响应体里另有一个 `groupBatchId`,那才是订单侧 `order_group_batch` 的主键,两者不是同一个值。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| productBatchId | path | Long | 是 | 正整数,产品侧排期 ID | 团期所属班期;未建团返 589553 |
| scopeRoles | body | List<String> | 否 | 传了就不能是空数组;元素非空白且须在员工角色取值域内 | 限定本次覆盖的角色范围,不传则整期覆盖 |
| staffList | body | List<Item> | 是(可在服务层为空数组) | `@NotNull`;`[]` 表示清空覆盖范围内的配置 | 覆盖范围内的最终状态 |
| staffList[].staffId | body | Long | 是 | 须命中候选人员 | 人员 ID |
| staffList[].staffRole | body | String | 是 | `@NotBlank` + `@Pattern`,须与人员类型相符,否则 582114 | 角色取值域见「六.5」 |
| staffList[].sortOrder | body | Integer | 否 | — | 展示排序,默认 0 |
| staffList[].remark | body | String | 否 | `@Size(max=500)` | 备注 |
#### 出参 `Result<BatchStaffConfigRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| productBatchId | Long | 回显路径参数 |
| groupBatchId | Long | 团期聚合主键,与路径参数不是同一个值 |
| staffList | List | 保存后的整期最终状态(含 `staffRoleName` / `reporterRankName` 等中文 Label) |
| affectedOrderCount | Integer | 本次扇出触及的活跃子订单数 |
#### 请求示例
```http
PUT /v3/admin/group-batch/2102640646949105666/staff HTTP/1.1
Host: api.test.1814.love:9443
Authorization: Bearer <admin token>
Content-Type: application/json
{"staffList":[{"staffId":1002,"staffRole":"GUIDE_ASSISTANT","sortOrder":0,"remark":"hl8194-gateway-ac1"}]}
```
#### 响应示例
成功时:
```json
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": "2102640646949105666",
"groupBatchId": "2102640736900116481",
"affectedOrderCount": 1,
"staffList": [
{"id": "2102640914386284545", "staffId": 1002, "staffRole": "GUIDE", "staffRoleName": "导游", "staffName": "李雪梅", "reporterRank": "NONE"}
]
},
"success": true
}
```
#### 空数据 / 降级响应
`staffList` 传空数组即清空覆盖范围内的配置,返回 200,`staffList` 为空数组、`affectedOrderCount` 为实际扇出订单数。
配置位字典读挂 / 读空时**不会**变成一律拒绝:`typesOf` 回落内置默认值(导游位 `GUIDE+LEADER`、摄影位 `PHOTOGRAPHER`),内置角色的保存照常成功——这是 #7079 写下的降级保证,本单未动。
#### 错误响应
新增会命中的情境:**字典里查不到该角色的归属位,且该角色不在豁免闭集里**(`{0}` 由运行期填入角色名):
```json
{"code": 582117, "message": "角色 GUIDE_ASSISTANT 未归属任何人员配置位,无法校验人员类型;请检查配置位字典是否被误删", "data": null, "success": false}
```
本端点此前已有、本次不改动的其余业务码(一并列出便于对照):
```json
{"code": 582114, "message": "所选人员的角色与其人员类型不符,请重新选择", "data": null, "success": false}
{"code": 582115, "message": "提交的人员角色超出本次保存声明的范围", "data": null, "success": false}
{"code": 582116, "message": "本次保存声明的角色范围(GUIDE)只覆盖了配置位的一部分,还缺少 LEADER;同一配置位的角色必须一起声明,否则位内其余人员会被留在库里", "data": null, "success": false}
{"code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false}
```
全部走 **HTTP 200**,不是 5xx。
#### 业务边界
- **582117 不是「字典读挂」的兜底**:Feign 失败或字典整表为空时 `typesOf` 回落内置默认值,归属位照样查得到,走不到这个码。能触发它的只有「**字典非空、但这个角色被从里面删掉了**」(或该角色从没在任何位的字典里出现过)。
- **被判定的不是「你这个角色对不对」,而是「这个角色的归属位是不是丢了」**。角色本身合法(在 `@Pattern` 取值域内)但字典里没有它的位 → 582117;位找得到但人岗不符 → 582114。两者互补。
- **修复动作在配置位字典页面,不在这个弹窗里**,所以文案点名角色并指向字典,适合原样透出给运营。
- **豁免闭集是 `DRIVER` 与 `OTHER`**,两者在任何字典状态下都不做角色↔人员类型校验,582117 与 582114 都不触发。
- 被 582114 / 582115 / 582116 / 582117 任一码拒绝时**零写入**——所有校验闸都在写库之前,且发生在注册 `afterCommit` 扇出回调之前,回读会看到与提交前逐字相同的配置。
- 导游位并收 `GUIDE` 与 `LEADER`:只传 `["GUIDE"]` 却选了领队会命中 582115,只传 `["GUIDE"]` 而位内还有 LEADER 会命中 582116。
---
## 四、契约约束与正确调用方式
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
### 契约面零变化,行为面有变化
| 面 | 是否变化 | 依据 |
|---|---|---|
| 路径 / HTTP 方法 / 权限码 | 不变 | Controller 未动 |
| 入参字段、类型、必填性、`@Pattern` 取值域 | 不变 | `BatchStaffConfigReqVO` 只加了 javadoc |
| 成功响应结构 | 不变 | `BatchStaffConfigRespVO` 未动 |
| **582117 的触发角色集合** | **变化** | Service 的分流判据翻面 |
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload | 结果 |
|------|---------|------|
| ✅ 内置角色、字典正常 | `{"staffList":[{"staffId":1002,"staffRole":"GUIDE"}]}` | 200,落库 `staffRole=GUIDE` |
| ✅ 结构性豁免角色 | `{"scopeRoles":["DRIVER"],"staffList":[{"staffId":1002,"staffRole":"DRIVER"}]}` | 200,落库 `staffRole=DRIVER` |
| ✅ 结构性豁免角色 | `{"scopeRoles":["OTHER"],"staffList":[{"staffId":1002,"staffRole":"OTHER"}]}` | 200,落库 `staffRole=OTHER` |
| ✅ 字典已收录该角色 | 字典里 `group_batch_staff_slot_guide` 含 `GUIDE_ASSISTANT`,提交 `staffRole=GUIDE` 且该人 `staffType=GUIDE_ASSISTANT` | 200,落库 `staffRole=GUIDE_ASSISTANT` |
| ❌ 字典角色但字典里已查不到位 | `{"staffList":[{"staffId":1002,"staffRole":"GUIDE_ASSISTANT"}]}` | 200 + `code=582117`,零写入 |
| ❌ 提交越界的人 | `{"scopeRoles":["GUIDE"],"staffList":[{"staffId":1002,"staffRole":"LEADER"}]}` | 200 + `code=582115`,零写入 |
### 调用方的正确动作
- **命中 582117 时不要重试**,也不要自动改写 `staffRole`——正确动作是**去配置位字典页面把缺的那一行补回来**(或确认为什么它被删了),再重试保存。文案里的角色名就是缺的那一个。
- **判成败一律看 `code`,不要看 HTTP status**:本端点的业务失败全是 HTTP 200。
- 保存成功后若调用方缓存了 `guide_ready` / `photographer_ready`,需重新拉取团期详情。
---
## 五、数据库行为
- 保存:按 `scopeRoles` 范围(不传即整期)软删旧配置行 → 写入新配置行 → 注册 `afterCommit` → 异步扇出到各活跃子订单(`order_staff_assignment` 中 `source=GROUP_BATCH` 的行整删整插)→ 回填团期行的两个 ready 标志。
- 本次改动**不改变任何一步的写内容**,只改变「哪些请求会被挡在写库之前」。
- 被 582114 / 582115 / 582116 / 582117 任一码拒绝时**零写入**:校验发生在 `buildToInsert` 与 `doSaveInTransaction` 之前,且 `afterCommit` 扇出回调**尚未注册**(注册动作在事务段内部、软删与插入之后)。
---
## 六、边界行为
- **`DRIVER` / `OTHER` 不受本单影响**:两者在任何字典状态下都放行,与改前一致。
- **字典读挂 / 读空不降级成拒绝**:`typesOf` 回落内置默认值,内置角色(`GUIDE` / `LEADER` / `PHOTOGRAPHER`)照常保存。
- **将来往 `@Pattern` 里加第 9 个取值**而忘了判断它属不属于豁免闭集时,新角色默认落进「本该有位」侧 ⇒ 那一侧是**拒绝**(响亮失败、当场被发现),不是静默放行。失败方向正确。
- **已知边界,不在本单修**:`OTHER` 在 `VALID_STAFF_TYPES` 里,业务**能**把 `OTHER` 写进某个位的字典再删掉,那一形态仍走静默放行。判据是:`OTHER` 的常态化就是不属于任何配置位,把它移出豁免闭集会拒掉每一次正常的杂项人员配置;而「把『其他』纳入『导游位』」本身是配置位语义的误用(等于宣告「其他类型的人算导游」),错的是那行字典。
- **存量脏行不在本单范围**:字典行被删之后库里已有的 `staff_role='GUIDE_ASSISTANT'` 行不再属于任何配置位,整位守卫 582116 也判不到它,这些行不会被 `scopeRoles` 的删除窗口覆盖或重建。治理另立工单。
---
## 六.5、枚举 / 数据字典
### staffRole(取值域来自 `BatchStaffConfigReqVO.STAFF_ROLE_PATTERN`)
**所属字段**: `staffList[].staffRole` / `scopeRoles[]` | **类型**: `String`
下表说的是「**该角色在配置位字典里查不到归属位时**」修复后走哪条分支。**分母就是从 `@Pattern` 常量抄下来的这 8 个**(并有单测 `GroupBatchStaffRoleSlotGuardTest#patternValueDomain_classifiedOneByOne` 机械守住:常量改了而表没跟,用例先红)。
| 取值 | 分支 | 理由 |
|---|---|---|
| `GUIDE` | 拒绝 582117 | 导游位内置成员;默认字典下查得到位,查不到说明那一行被删了 |
| `LEADER` | 拒绝 582117 | 同上(导游位并收导游与领队) |
| `PHOTOGRAPHER` | 拒绝 582117 | 摄影位内置成员;查不到说明那一行被删了 |
| `GUIDE_ASSISTANT` | **拒绝 582117(本单新增覆盖)** | 只可能靠字典被纳入配置位;「加过再删回」正是本单修的形态 |
| `STUDY_TEACHER` | **拒绝 582117(本单新增覆盖)** | 同上 |
| `LIFE_TEACHER` | **拒绝 582117(本单新增覆盖)** | 同上 |
| `DRIVER` | 放行 | 结构性豁免:车务派车投影产生,资源域没有这个人员类型,也配不进任何位的字典 |
| `OTHER` | 放行 + 已知边界 | 结构性豁免:常态就是不属于任何配置位,保护它会拒掉正常的杂项人员配置。代价是「`OTHER` 被某位字典纳入后再删掉」仍静默放行,本单不修 |
---
## 六.6、修改前后对比
### 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| `staffRole=GUIDE_ASSISTANT`(字典里查不到归属位) | **静默 200 + 落库 + 异步扇出**,582114 当场失效 | 582117,零写入 |
| `staffRole=STUDY_TEACHER` / `LIFE_TEACHER`(同上) | 同上 | 582117,零写入 |
| `staffRole=LEADER` 被从非空字典删掉 | 582117(#8148 已建立) | **不变** |
| `staffRole=DRIVER` / `OTHER` | 放行 | **不变** |
| 字典读挂 / 读空 | 回落内置默认值,内置角色放行 | **不变** |
| 判据实现 | `participatesInSlotsByFallback` 读编译期常量 `fallbackTypes()` | `requiresSlotMembership` 判「不在结构性豁免闭集 `{DRIVER, OTHER}` 里」 |
| 路径 / 入参 / 权限 / 成功响应结构 | — | **零变化** |
### 字段级对比
无字段变化(`BatchStaffConfigReqVO` 只增加 javadoc,`BatchStaffConfigRespVO` 未动)。
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否(契约零变化)。但存在**面向管理后台的行为变化**:原本能保存成功的几个角色的请求会开始被 582117 拒——那批请求本就是脏数据(角色没有任何人员类型校验背书)。
- **前端是否必须同步上线**: **否**。5821xx 这一族在 hl-ui v2.1 上没有专门分支,统一由 `src/utils/request.js` 的响应拦截器按「code 非成功值」toast 后端 message,故 582117 在这几个角色上出现时前端零改动即有基本行为(文案本身已是可直接展示的完整句子,并指向修复动作所在处)。
- **前端可选优化**:命中 582117 时提示语可引导运营去「配置位字典」页面,并保留用户已填表单;不引导也不会误操作(重试仍会被拒)。
- **前端 workaround 清理点**: 无。
---
## 七、不影响范围
- **零影响**:
- 路径、HTTP 方法、权限码、网关配置
- 入参字段、类型、必填性、`@Pattern` 取值域
- 成功响应结构与字段
- 候选人查询(`/staff/candidates`、`/staff/candidates/page`)与配置列表查询(`GET /staff`)
- 报账人等级设置口 `PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank`
- 子订单自己单独派的人员(`order_staff_assignment` 中 `source` 非 `GROUP_BATCH` 的行)
- `582114` / `582115` / `582116` / `100503` 四个既有码的触发条件与文案
- `assertScopeCoversWholeSlots`(582116)与 `scopeRoles` 的任何判定逻辑
- `DRIVER` / `OTHER` 的放行口径
- 数据库表结构与 Flyway(本单无表变更、无迁移脚本)
- 小程序端(团期人员配置无小程序写口)
---
## 八、测试环境已验证
2026-09-23,`api.test.1814.love:9443`(网关),分支 `dev-v3`,order-v3 活体 `f6d21648b`(双实例 8086 / 8186,滚动部署后均 UP)。
**取证批次**: `productBatchId=2102640646949105666`(#8123 / #8148 同一条取证批次),人员 `staffId=1002`(资源域真实 `staffType=GUIDE`)。
| 项 | 请求 | 结果 |
|---|---|---|
| 探测前快照 | `GET /v3/admin/group-batch/2102640646949105666/staff` | 200,1 行(`staffId=1002, staffRole=GUIDE`) |
| **本单核心(修复后)** | `PUT` 同端点,`{"staffList":[{"staffId":1002,"staffRole":"GUIDE_ASSISTANT"}]}` | **HTTP 200,`code=582117`**,message「角色 GUIDE_ASSISTANT 未归属任何人员配置位,无法校验人员类型;请检查配置位字典是否被误删」 |
| **零写入** | 上一步之后 `GET` 同端点读回 | 与探测前快照**逐字段一致**(拒绝路径未注册 afterCommit 扇出回调,未进事务段) |
| 阴性对照(`DRIVER`) | `PUT` `{"scopeRoles":["DRIVER"],"staffList":[{"staffId":1002,"staffRole":"DRIVER"}]}` | HTTP 200,`code=200` 成功 |
| 阴性对照(`OTHER`) | `PUT` `{"scopeRoles":["OTHER"],"staffList":[{"staffId":1002,"staffRole":"OTHER"}]}` | HTTP 200,`code=200` 成功 |
| 现场还原 | `PUT` `{"scopeRoles":["DRIVER","OTHER"],"staffList":[]}` | 200;读回与探测前快照**逐字段一致** |
**测试环境上无法构造、因此未取活体样本的两项**(原因是环境性的、与实现无关):
- **AC-3「字典读挂回落内置默认值」**:需要让测试服的 `DictFeignClient` 整体失败,属环境级故障注入,会波及同环境其他使用者。该行为由真库用例 `GroupBatchStaffSaveConfigSlotCompletenessMysqlTest#saveConfig_dictFeignFails_fallbackKeepsSlotRolesAccepted` 与单元用例 `GroupBatchStaffRoleSlotGuardTest#dictUnavailable_fallbackKeepsBuiltInRolesAccepted` 覆盖(真实解析器 + 打桩 Feign,回落链路真跑)。
- **AC-4 的测试服活体样本**(把内置角色从**仍非空**的配置位字典里删掉):字典是全站共享数据,删行的副作用会落到所有人的角色下拉框上,未构造。该行为由真库用例 `#saveConfig_dictDropsLeaderFromGuideSlot_mismatchedStaffTypeRejectedNotSilentlySkipped` 覆盖。
**证据文件**(脱敏、非空,已登记进 task capsule 的 evidence manifest):`D:/work2/.tmp/issue8194/evidence/`(`gateway-verification.json` / `gateway-8194.log` / `gw-1-before.json` … `gw-5-after-cleanup.json` / `gw-summary.json`)。
---
## 十、相关文档
- Issue #8194(本单)、#8148(582117 的引入与残留缺口)、#8122(582116 整位覆盖守卫)、#7079(配置位字典化)
- PR #8259(squash 提交 `f6d21648b`)
- 同端点上一版条目:`changelogs-v2/2026-09/23_8123_团期staff保存新增并发抢锁与配置位字典守卫错误码-修改接口-管理后台.md`
- 错误码定义:`hl-order-service-v3/src/main/java/com/hulalv/order/assignment/errorcode/AssignmentErrorCode.java`(582117)
- 判据与豁免闭集:`hl-order-service-v3/src/main/java/com/hulalv/order/assignment/service/GroupBatchStaffSlotResolver.java`(`STRUCTURALLY_SLOT_EXEMPT_ROLES` / `requiresSlotMembership`)
---
## 关联 / 联系人
### 链接
- **Issue**: [#8194](https://git.1814.love/wx/HL/issues/8194)
- **PR**: [#8259](https://git.1814.love/wx/HL/pulls/8259)
- **Merge commit**: `f6d21648bada5d0d465cd454390ba6bad923869a`
### 联系人
- **后端负责人**: @wx
- **前端**: mmg(582117 提示语可引导至配置位字典页面,非必须)