diff --git a/changelogs-v2/2026-09/23_8194_配置位守卫判据翻面字典角色被删回后不再静默放行-修改接口-管理后台.md b/changelogs-v2/2026-09/23_8194_配置位守卫判据翻面字典角色被删回后不再静默放行-修改接口-管理后台.md new file mode 100644 index 00000000..6370dcee --- /dev/null +++ b/changelogs-v2/2026-09/23_8194_配置位守卫判据翻面字典角色被删回后不再静默放行-修改接口-管理后台.md @@ -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` + +| 字段 | 类型 | 说明 | +|---|---|---| +| 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 +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 提示语可引导至配置位字典页面,非必须)