--- schema: "hl-changelog/v2" ticket: "6950" title: "团期人员配置候选列表(导游位并收 GUIDE/LEADER)" consumer: "admin" author: "jw(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "44f51c13" target_release: "" verified_at: "2026-09-03" status_note: "PR #6962 已合入 dev-v3;2026-09-03 经测试环境网关实测,6 条正负向用例全部通过。前端尚未接入" updated_at: "2026-09-03" base: "dev-v3" --- # 团期人员配置: 新增人员配置候选列表接口,并修正 staffRole 取值校验 > **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/` > > **服务**: hl-order-service-v3 > **PR**: #6962 > **Issue**: #6950 > **日期**: 2026-09-02 > **影响范围**: 管理后台「团期详情 → 配置导游 / 配置摄影」弹窗的人员资源库列表 --- ## ⚠️ 关键变化 本次除新增候选列表接口外,**另有一处行为变更**,调用方必须知道: **保存团期人员配置 `PUT /v3/admin/group-batch/:groupBatchId/staff` 的 `staffList[].staffRole` 加了枚举白名单校验。** - 前端以前以为:`staffRole` 只做非空校验,传什么都能存进去。 - 实际现在是:白名单外的取值 **返回 400**,不再原样入库。 - 白名单:`LEADER` / `GUIDE` / `DRIVER` / `PHOTOGRAPHER` / `OTHER`。 另外该字段的 Swagger 描述此前 **漏了 `GUIDE`**,本次补全。若前端此前按旧描述认为只有 4 个取值,现在是 5 个。 --- ## 一、背景 「配置导游」「配置摄影」弹窗需要一份可选人员列表,此前 order-v3 侧没有这个接口,前端无从取候选池。 **资源域零改动**:`GET /internal/staff/list-available` 本就存在(其 Swagger 注释写明「供管理后台团批分配人员的下拉选择框使用」),order-v3 侧只补了 Feign 方法与降级处理。 **导游位为什么并收两类**(2026-09-02 jw 裁决):资源域字典里 `GUIDE` 是导游、`LEADER` 是领队,两者在团期现场都可能承担带团职责,由配置人按实际情况挑,**服务端不替业务做取舍**。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 查询团期人员配置候选列表 | GET | `/v3/admin/group-batch/:groupBatchId/staff/candidates` | **新增接口** | 导游位并收 GUIDE/LEADER,摄影位只收 PHOTOGRAPHER | | 2 | 保存团期人员配置 | PUT | `/v3/admin/group-batch/:groupBatchId/staff` | **请求体新增校验** | `staffList[].staffRole` 加枚举白名单,越界返 400 | --- ## 三、接口详情 ### 1. 查询团期人员配置候选列表 `GET /v3/admin/group-batch/:groupBatchId/staff/candidates` **VO**: `StaffCandidateRespVO` #### 使用场景 管理后台「团期详情 → 配置导游 / 配置摄影」弹窗打开时调用,用于渲染可选人员资源库列表。 弹窗按配置位分别调用:导游弹窗传 `role=GUIDE`,摄影弹窗传 `role=PHOTOGRAPHER`。 列表中 `assigned=true` 的项应预置为已勾选状态,供配置人在原有选择基础上增删。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `groupBatchId` | Path | Long | ✅ | — | 团期批次 ID。**这里是产品侧 `group_tour_batch.batch_id`**,不是订单侧运营团期主键,与 `order_batch_staff.group_batch_id` 同源 | | `role` | Query | String | ✅ | 只接受 `GUIDE` / `PHOTOGRAPHER` | 配置位。`GUIDE`=导游位(并收 GUIDE/LEADER,可选多人);`PHOTOGRAPHER`=摄影位。**其余取值(含 `DRIVER`)返回 582113** | > `DRIVER` 被显式拒绝:司机由车务派车产生,不在团期人员配置里手工指定。 #### 出参 `Result>` | 字段 | 类型 | 说明 | |------|------|------| | `staffId` | Long | 人员 ID | | `staffName` | String | 姓名 | | `staffPhone` | String | 手机号,**前 3 后 4 脱敏**(与 `getConfig` 口径一致) | | `staffType` | String | 资源域人员类型:`GUIDE` / `LEADER` / `PHOTOGRAPHER` | | `avatarUrl` | String | 头像 URL;资源域无头像或取头像失败时为 `null` | | `assigned` | Boolean | 是否已被本团期选中。**`true` 时前端应显示为已勾选** | | `assignedRole` | String | 已选中时对应的 `order_batch_staff.staff_role`;未选中为 `null` | > `assigned` 与 `assignedRole` 要分开判:存在「已选但角色为空」的历史数据,此时 `assigned=true` 而 `assignedRole=null`,前端**不能**用 `assignedRole != null` 判断勾选态。 #### 请求示例 ```http GET /v3/admin/group-batch/1823456789012345678/staff/candidates?role=GUIDE ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": [ { "staffId": 1823456789012345678, "staffName": "张三", "staffPhone": "138****8000", "staffType": "GUIDE", "avatarUrl": "https://oss.example.com/avatar/1.jpg", "assigned": true, "assignedRole": "LEADER" }, { "staffId": 1823456789012345679, "staffName": "李四", "staffPhone": "139****1234", "staffType": "LEADER", "avatarUrl": null, "assigned": false, "assignedRole": null } ] } ``` #### 空数据 / 降级响应 资源域无可用人员时返回空数组,不报错: ```json { "code": 200, "message": "成功", "success": true, "data": [] } ``` 头像服务取不到时**不阻断列表**,`avatarUrl` 降级为 `null`,其余字段照常返回。 #### 错误响应 `role` 传了 `GUIDE` / `PHOTOGRAPHER` 之外的值(如 `DRIVER`): ```json { "code": 582113, "message": "人员配置位不合法,只支持导游位与摄影位", "success": false, "data": null } ``` 资源域人员查询失败: ```json { "code": 582103, "message": "员工信息查询失败,请稍后重试", "success": false, "data": null } ``` #### 业务边界 - 只读接口,**不产生任何写入**,可安全重复调用 - 同一人同时属于 `GUIDE` 与 `LEADER` 时按 `staffId` 去重,只返回一条 - `assigned` 反映的是本团期当前配置状态,与 `role` 入参无关:传 `role=GUIDE` 时, 已被配成摄影位的人不会出现在结果里,但导游位候选中若有人已被选中则 `assigned=true` - 资源域仅返回启用状态人员,停用人员不在候选池 - 头像属展示增强,取不到时降级为 `null`,**不影响可选性** --- ### 2. 保存团期人员配置 `PUT /v3/admin/group-batch/:groupBatchId/staff` **VO**: `BatchStaffConfigReqVO` **本次只改请求体校验,路径、出参均不变。** #### 使用场景 「配置导游 / 配置摄影」弹窗点击保存时调用,整批覆盖该团期的人员配置。 本次变更后,前端必须保证 `staffRole` 取值落在白名单内,否则整个请求被拒、无一条生效。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `groupBatchId` | Path | Long | ✅ | — | 团期批次 ID,同接口 1 | | `staffList` | Body | Array | ✅ | — | 整批覆盖语义:传入列表即最终配置,未包含的人员被移除 | | `staffList[].staffId` | Body | Long | ✅ | `@NotNull` | 人员 ID | | `staffList[].staffRole` | Body | String | ✅ | **本次新增** `@Pattern`:`LEADER`\|`GUIDE`\|`DRIVER`\|`PHOTOGRAPHER`\|`OTHER` | **变更前**仅 `@NotBlank`,任意非空值原样入库;**变更后**越界返 400 | | `staffList[].sortOrder` | Body | Integer | ❌ | — | 展示排序 | | `staffList[].remark` | Body | String | ❌ | `@Size(max=500)` | 备注 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | `staffList` | Array | 保存后的人员配置列表,字段同 `getConfig` | | `affectedOrderCount` | Integer | 本次保存扇出影响的子订单数 | #### 请求示例 ```json { "staffList": [ { "staffId": 1002, "staffRole": "GUIDE", "sortOrder": 0 } ] } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "staffList": [ { "id": "2095403473712418817", "staffId": 1002, "staffRole": "GUIDE", "staffName": "李雪梅", "staffPhone": "138****1002", "avatarUrl": null, "sortOrder": 0, "remark": null } ], "affectedOrderCount": 0 } } ``` #### 空数据 / 降级响应 传入空 `staffList` 数组表示清空该团期的人员配置,返回 200 且 `staffList` 为空数组, 不报错。这也是回退误配置的正规手段。 #### 错误响应 `staffRole` 越界(**本次新增行为**): ```json { "code": 400, "message": "员工角色只能是 LEADER / GUIDE / DRIVER / PHOTOGRAPHER / OTHER", "success": false, "data": null } ``` #### 业务边界 - **整批覆盖**语义,不是增量追加;未包含在 `staffList` 里的既有人员会被移除 - 校验在 `@Valid` 阶段完成,**任一条目越界则整个请求被拒,不会部分写入** - `@Pattern` **区分大小写**,`guide` 不等于 `GUIDE` - 空串会同时触发 `@Pattern` 与 `@NotBlank`,消息合并返回 - 存量 `order_batch_staff` 数据不迁移,新校验只在下次保存时触发 --- ## 四、契约约束与正确调用方式 - **`role` 必传且只接受两个值**:`GUIDE`、`PHOTOGRAPHER`。不要传 `DRIVER`——司机由车务派车投影产生, 不在团期人员配置里手工指定,传了会返回 582113。 - **勾选态判断用 `assigned`,不要用 `assignedRole != null`**。存在「已选但角色为空」的历史数据, 用后者会漏掉这批人。 - **`groupBatchId` 是产品侧 `group_tour_batch.batch_id`**,不是订单侧运营团期主键。传错会返回空列表而非报错。 - **保存前先过白名单**:`staffRole` 只能是 `LEADER` / `GUIDE` / `DRIVER` / `PHOTOGRAPHER` / `OTHER`, 且区分大小写。整批中任一条越界会导致整个请求 400、无一条生效。 - **保存是整批覆盖**:每次提交需带上该团期的完整人员列表,只传增量会导致其余人员被清除。 - 错误码 `582113` 是业务码,HTTP 状态仍为 200,判断成败要读响应体的 `code` 字段。 --- ## 五、数据库行为 仅描述外部可观察行为: - 接口 1(GET candidates)**只读,不产生任何写入**。 - 接口 2(PUT staff)按整批覆盖语义重写该团期的人员配置:提交列表中的人员被保留或新增, 未包含的既有人员被移除;响应的 `affectedOrderCount` 表示随之扇出更新的子订单数量。 - 越界校验发生在写入之前,**校验失败时数据库无任何变更**。 - 本次变更**不涉及表结构调整**,也不对存量数据做迁移或回填。 --- ## 六、边界行为 - `role` 非法 → **582113**,不是 400(业务码,HTTP 仍 200) - 资源域无可用人员 → 返回 `[]`,不报错、不阻断弹窗 - 头像服务异常 → `avatarUrl` 为 `null`,列表照常返回(头像属展示增强) - 资源域只返回 `status=1` 的人员,按 `sortOrder`、`staffId` 排序 - 同一人同时命中 `GUIDE` 与 `LEADER` 两类 → **按 `staffId` 去重,只出现一次** - 已选人员 `staffRole` 为 `null`(历史脏数据)→ `assigned=true`、`assignedRole=null`,**接口不 500** - 未登录 → 401(网关拦截) --- ## 六.5、枚举 / 数据字典 ### `role`(Query 参数,配置位) | 取值 | 含义 | 实际拉取的资源域 `staffType` | |------|------|------------------------------| | `GUIDE` | 导游位 | **`GUIDE` + `LEADER` 两类并收**,按 `staffId` 去重,可选多人 | | `PHOTOGRAPHER` | 摄影位 | 只收 `PHOTOGRAPHER` | | 其他(含 `DRIVER`) | — | 拒绝,返回 582113 | ### `staffType`(响应字段,资源域人员类型) 本接口可能返回 `GUIDE`(导游)、`LEADER`(领队)、`PHOTOGRAPHER`(摄影)三种。 资源域完整字典还包含 `GUIDE_ASSISTANT` / `LIFE_TEACHER` / `STUDY_TEACHER` / `OTHER`,但**不会出现在本接口响应里**。 ### `staffRole`(保存接口入参,取值域对齐 `SettlementStaffRoleEnum`) | 取值 | 含义 | |------|------| | `LEADER` | 领队 | | `GUIDE` | 导游 | | `DRIVER` | 司机(**由车务派车投影产生,一般不由本接口写入**) | | `PHOTOGRAPHER` | 摄影 | | `OTHER` | 其他 | --- ## 六.6、错误码 段位 `582100-582199`,owner `hl-order-service-v3`(`AssignmentErrorCode`)。 | 码 | 符号 | 消息 | 触发 | |---|---|---|---| | `582113` | `STAFF_CANDIDATE_ROLE_INVALID` | 人员配置位不合法,只支持导游位与摄影位 | **本次新增**。`role` 不是 `GUIDE` / `PHOTOGRAPHER` | | `582103` | `STAFF_INFO_FETCH_FAILED` | 员工信息查询失败,请稍后重试 | 既有码。资源域 `list-available` 返回失败或空结果对象 | | `400` | — | 员工角色只能是 LEADER / GUIDE / DRIVER / PHOTOGRAPHER / OTHER | **本次新增**。保存接口 `staffRole` 越界 | --- ## 七、不影响范围 - **仅影响**:管理后台团期详情的「配置导游 / 配置摄影」弹窗 - **零影响**: - 资源域 `hl-resource-service`(**结构与接口零改动**,只是被新调用方使用) - 团期人员保存后的子订单扇出逻辑(`order_staff_assignment`) - 车务派车产生的司机行 - 已有的 `GET` / `PUT /v3/admin/group-batch/:groupBatchId/staff` 出参 - 存量 `order_batch_staff` 数据(不迁移,`staffRole` 新校验只在下次保存时触发) --- ## 八、测试环境已验证 ✅ **已验证。** 2026-09-03 于测试环境网关实测,真实鉴权(管理端 admin 账号)。 - 网关:`https://api.test.1814.love:9443` - 路由:`/v3/admin/**` 由网关直挂 hl-order-service-v3(**无 `/order-v3` 前缀**) - 分支:`dev-v3` | # | 用例 | 期望 | 实测 | |---|---|---|---| | 1 | `?role=GUIDE` | 200,并收 GUIDE+LEADER | ✅ 200,7 条,staffType 去重 = `[GUIDE, LEADER]`,staffId 唯一 | | 2 | `?role=PHOTOGRAPHER` | 200,只含摄影 | ✅ 200,1 条,staffType 去重 = `[PHOTOGRAPHER]` | | 3 | `?role=DRIVER` | 582113 | ✅ `582113 人员配置位不合法,只支持导游位与摄影位` | | 4 | `?role=XXX` 非法值 | 582113 | ✅ 同上 | | 5 | 缺 `role` 参数 | 400 | ✅ `400 缺少必要参数: role` | | 6 | 无 Authorization | 401 | ✅ `401 缺少有效的 Authorization 头` | **脱敏核对**:`138****1002` / `139****1011` / `139****1010`,前 3 后 4 生效。 **头像降级核对**:用例 1 中多条 `avatarUrl` 为 `null` 未阻断返回;用例 2 中摄影师返回真实 OSS 地址。 本地单元与架构测试(提交信息记载): ``` GroupBatchStaffConfigServiceTest 27 例全过 ✓ LayerEnforcement / MapperBoundary / RedLine / ErrorCodeRegistry 45 例全过 ✓ ``` ### 保存接口 `staffRole` 白名单实测(本次行为变更) 同日于同一网关实测 `PUT /v3/admin/group-batch/:groupBatchId/staff`: | # | payload `staffList[0].staffRole` | 期望 | 实测 | |---|---|---|---| | 7 | `"SUPERVISOR"` 越界值 | 400 | ✅ `400 员工角色只能是 LEADER / GUIDE / DRIVER / PHOTOGRAPHER / OTHER` | | 8 | `"guide"` 小写 | 400 | ✅ 同上(正则区分大小写) | | 9 | `""` 空串 | 400 | ✅ `400 …; 该字段不能为空`(@Pattern 与 @NotBlank 同时触发) | | 10 | `"GUIDE"` 白名单内 | 放行 | ✅ 200,`affectedOrderCount: 0` | 用例 7–9 在 `@Valid` 阶段即被拒,**不产生任何写入**。用例 10 会落库,测试后已用空 `staffList` 数组 PUT 还原, 复核 `GET .../staff` 返回 `data: []`,与测试前一致。 **仍待补**:空候选池分支未构造(需一个无可用人员的团期)。 --- ## 九、相关历史 PR | PR | Issue | 说明 | 是否仍有效 | |----|-------|------|------------| | **本 PR #6962** | **#6950** | 新增候选列表接口 + `staffRole` 白名单校验 | ✅ 最新 | --- ## 十、相关文档 - 关联 Issue: [wx/HL#6950](https://git.1814.love:8443/wx/HL/issues/6950) - 关联 PR: [wx/HL#6962](https://git.1814.love:8443/wx/HL/pulls/6962) - 合入提交: `809462321`(feat),merge `35d0a8f2c` → `dev-v3` - 需求与契约: `docs/group/团期模块接口文档-v2.0.html` GB-ADM-014 --- ## 关联 / 联系人 - **Issue**: #6950 - **PR**: #6962(合并提交 `35d0a8f2c`,落入 `dev-v3`) - **服务**: hl-order-service-v3 - **后端**: jw - **前端**: 待认领(`frontend_status: pending`) - **口径裁决**: 2026-09-02 jw —— 导游位并收 `GUIDE` 与 `LEADER`,摄影位只收 `PHOTOGRAPHER`,`DRIVER` 显式拒绝