diff --git a/changelogs-v2/2026-09/03_6950_团期人员配置候选列表-新增接口-管理后台.md b/changelogs-v2/2026-09/03_6950_团期人员配置候选列表-新增接口-管理后台.md new file mode 100644 index 00000000..d7840345 --- /dev/null +++ b/changelogs-v2/2026-09/03_6950_团期人员配置候选列表-新增接口-管理后台.md @@ -0,0 +1,441 @@ +--- +schema: "hl-changelog/v2" +ticket: "6950" +title: "团期人员配置候选列表(导游位并收 GUIDE/LEADER)" +consumer: "admin" +author: "jw(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +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` 显式拒绝