diff --git a/changelogs-v2/2026-09/04_7063_团期导游位下游按配置位归组-修改接口-管理后台.md b/changelogs-v2/2026-09/04_7063_团期导游位下游按配置位归组-修改接口-管理后台.md new file mode 100644 index 00000000..d38e2de4 --- /dev/null +++ b/changelogs-v2/2026-09/04_7063_团期导游位下游按配置位归组-修改接口-管理后台.md @@ -0,0 +1,537 @@ +--- +schema: "hl-changelog/v2" +ticket: "7063" +title: "团期导游位下游按配置位归组,GUIDE 与 LEADER 一视同仁" +consumer: "admin" +author: "jw(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "PR #7065 已合入 dev-v3(合并提交 f37b6ee0f);2026-09-04 部署测试环境并经网关实测 AC-2/AC-3/AC-4/AC-5/AC-9。第 2 波配置位字典化未做" +updated_at: "2026-09-04" +base: "dev-v3" +--- + +# 团期人员配置: 导游位下游判定由单一角色改为按配置位归组 + +> **服务**: hl-order-service-v3 +> **PR**: #7065 | **Issue**: #7063 | **合并提交**: `f37b6ee0f` +> **影响范围**: 管理后台「团期详情 → 配置导游/摄影」、「订单详情 → 人员」、行程单打印 + +--- + +## ⚠️ 关键变化 + +**「导游位并收导游与领队」这条裁决此前只落到了候选列表一层,保存之后的下游仍只认单一角色。** + +结果是配置人怎么选都会踩一边: + +| 存成 | 丢什么 | +|---|---| +| `GUIDE` | `guide_ready` 不置位、`guide_status` 不写、`GUIDE_DONE` 流水不写、订单资源就绪闸门推不动 | +| `LEADER` | 行程单「导游」栏为空 | + +**本机实测还暴露了第二处更深的缺陷**:团期人员配置保存时,`ready` 回填用的是**产品侧 batchId**, +而 `order_group_batch` 的 ready 列按**主键**更新,两者不是同一个 ID 空间。 +Mapper `rows=0` 只打 WARN 不抛,所以这个洞一直没被发现—— +**改前不论配 GUIDE 还是 LEADER,`guide_ready` / `photographer_ready` 从来就没被置位过。** + +--- + +## 一、背景 + +人员类型 `staff_type` 在服务人员管理里是维护好的字典,团期现场谁带团由业务按实际安排: +有的团派导游,有的团派领队,也有两人都上。2026-09-02 已裁决「导游位并收 GUIDE 与 LEADER, +服务端不替业务做取舍」,本次把这条裁决贯彻到保存之后的全部下游链路。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 全量保存团期人员配置 | PUT | `/v3/admin/group-batch/:groupBatchId/staff` | **副作用变更** | 导游位配 `GUIDE` 也置 `guide_ready`;并修正 ready 回填的 ID 空间 | +| 2 | 订单新增人员 | POST | `/v3/admin/order/:id/staff` | **副作用变更** | `staffRole=GUIDE` 也写 `guide_status` 与 `GUIDE_DONE` 流水 | +| 3 | 订单删除人员 | DELETE | `/v3/admin/order/:id/staff/:staffAssignmentId` | **副作用变更** | 计数按导游位整组,删其一不再误置空 `guide_status` | +| 4 | 打印行程单 | GET | `/v3/admin/order/:id/print-itinerary` | **出参取值口径变更** | 导游栏 `GUIDE` 优先、缺位回退 `LEADER` | + +--- + +## 三、接口详情 + +### 1. 全量保存团期人员配置 `PUT /v3/admin/group-batch/:groupBatchId/staff` + +**VO**: `BatchStaffConfigReqVO` + +#### 使用场景 + +团期详情「配置导游 / 配置摄影」弹窗点保存时调用。全量覆盖语义:传入列表即为最终配置。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `groupBatchId` | Path | Long | ✅ | — | **产品侧团期 batchId**,非 `order_group_batch` 主键 | +| `staffList` | Body | Array | ❌ | 传空则清空 | 人员配置列表,全量覆盖 | +| `staffList[].staffId` | Body | Long | ✅ | — | 资源域人员 ID | +| `staffList[].staffRole` | Body | String | ✅ | `LEADER` `GUIDE` `DRIVER` `PHOTOGRAPHER` `OTHER` | 团期角色 | +| `staffList[].sortOrder` | Body | Integer | ❌ | 缺省 0 | 展示排序 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `staffList` | Array | 落库后的人员快照,**结构未变** | +| `affectedOrderCount` | Integer | 扇出到的活跃子订单数,**未变** | + +> **出参结构完全没有变化**,变的是写库之后的副作用。 + +#### 请求示例 + +```json +{ + "staffList": [ + { + "staffId": 1002, + "staffRole": "GUIDE", + "sortOrder": 0 + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "staffList": [ + { + "staffId": 1002, + "staffRole": "GUIDE", + "staffName": "李雪梅", + "staffPhone": "138****1002", + "sortOrder": 0 + } + ], + "affectedOrderCount": 2 + } +} +``` + +#### 空数据 / 降级响应 + +`staffList` 传空数组即清空该团期全部人员配置,返回空列表: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "staffList": [], + "affectedOrderCount": 2 + } +} +``` + +#### 错误响应 + +团期不存在时 ready 回填跳过并打 WARN,接口本身仍成功;未携带令牌时: + +```json +{ + "code": 401, + "message": "缺少有效的 Authorization 头", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 导游位认 `GUIDE` 与 `LEADER` 两种角色,任一有人即置 `guide_ready` +- 摄影位只认 `PHOTOGRAPHER` +- 同时配了导游和领队时 `markGuideReady` 只调一次,幂等 +- 团期查不到(未成团 / 已解散)时 ready 回填跳过,不抛异常 +- `DRIVER` 不参与任何配置位判定,由车务派车投影产生 + +### 2. 订单新增人员 `POST /v3/admin/order/:id/staff` + +**VO**: `StaffAssignReqVO` + +#### 使用场景 + +订单详情「人员」区手工新增一名服务人员时调用。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `id` | Path | Long | ✅ | — | 订单 ID | +| `staffId` | Body | Long | ✅ | — | 资源域人员 ID | +| `staffRole` | Body | String | ✅ | `LEADER` `GUIDE` `DRIVER` `PHOTOGRAPHER` `OTHER` | 团期角色 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `assignmentId` | Long | 新建行 ID,**未变** | +| `staffRole` | String | 角色,**未变** | +| `staffPhone` | String | 脱敏手机号,**未变** | + +#### 请求示例 + +```json +{ + "staffId": 1002, + "staffRole": "GUIDE" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "assignmentId": 2095758914858471425, + "staffId": 1002, + "staffRole": "GUIDE", + "staffName": "李雪梅", + "staffPhone": "138****1002" + } +} +``` + +#### 空数据 / 降级响应 + +本接口为写入,无空数据形态;资源域人员查询失败时整批 fail-fast: + +```json +{ + "code": 582103, + "message": "员工信息查询失败,请稍后重试", + "success": false, + "data": null +} +``` + +#### 错误响应 + +同一订单同一角色重复添加同一人: + +```json +{ + "code": 582101, + "message": "该员工已分配此角色到此订单", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- `staffRole=GUIDE` 现在与 `LEADER` 等价地触发 `guide_status=DONE` 与 `GUIDE_DONE` 流水 +- 流水姓名脱敏为首字符加星号,操作人由 `operatorResolver` 自动带出 +- 写完状态后触发 `maybeAdvanceToConfirm`,尝试推进订单资源就绪 +- `DRIVER` 与 `OTHER` 不落任何 staff 状态列 +- 团期来源行(`source=GROUP_BATCH`)不允许在订单侧增删改 + +### 3. 订单删除人员 `DELETE /v3/admin/order/:id/staff/:staffAssignmentId` + +**VO**: `StaffAssignmentVO` + +#### 使用场景 + +订单详情「人员」区移除一名手工添加的服务人员。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `id` | Path | Long | ✅ | — | 订单 ID | +| `staffAssignmentId` | Path | Long | ✅ | — | 人员配置行 ID | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data` | Null | 无返回体,**未变** | + +#### 请求示例 + +```http +DELETE /v3/admin/order/990706300101/staff/2095758914858471425 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": null +} +``` + +#### 空数据 / 降级响应 + +行不存在或不属于该订单: + +```json +{ + "code": 582102, + "message": "员工分配记录不存在", + "success": false, + "data": null +} +``` + +#### 错误响应 + +团期共享行不可在订单侧删除: + +```json +{ + "code": 582109, + "message": "团期共享员工不可在订单侧增删改,请到团期配置页操作", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 删除后按**导游位整组**重新计数,而不是只数被删的那个角色 +- 导游位同时有导游和领队时,删掉其一 `guide_status` 保持 `DONE` +- 整组归零时才把 `guide_status` 置空,且不写状态流水 +- 摄影位口径不变,仍只数 `PHOTOGRAPHER` + +### 4. 打印行程单 `GET /v3/admin/order/:id/print-itinerary` + +**VO**: `PrintItineraryRespVO` + +#### 使用场景 + +订单详情点「打印行程单」,抬头区展示司机 / 导游 / 领队的姓名与电话。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `id` | Path | Long | ✅ | — | 订单 ID | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `guideName` | String | 导游姓名。**取值口径变更**:导游位内 `GUIDE` 优先,缺位回退 `LEADER` | +| `guidePhone` | String | 导游电话,同上口径 | +| `leaderName` | String | 领队姓名,仍只取 `LEADER` 行,**未变** | +| `leaderPhone` | String | 领队电话,**未变** | + +#### 请求示例 + +```http +GET /v3/admin/order/990706300101/print-itinerary +``` + +#### 响应示例 + +导游位只配了领队时,导游栏回退取到领队: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "header": { + "guideName": "刘大山", + "guidePhone": "13800001005", + "leaderName": "刘大山", + "leaderPhone": "13800001005" + } + } +} +``` + +#### 空数据 / 降级响应 + +导游位无人时四个字段均为 null,接口仍成功: + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "header": { + "guideName": null, + "guidePhone": null, + "leaderName": null, + "leaderPhone": null + } + } +} +``` + +#### 错误响应 + +非本人名下订单且无数据权限: + +```json +{ + "code": 581008, + "message": "无权查看此订单", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 只读接口,不产生任何写入 +- 导游位同时有导游和领队时,导游栏取导游,不取领队 +- `leaderName` 与 `leaderPhone` 语义不变,仍是领队本身 +- 导游与领队是同一人时两栏显示同一姓名,属预期 + +--- + +## 四、契约约束与正确调用方式 + +- **导游位的角色集合是 `GUIDE` 与 `LEADER` 两个**,前端配置弹窗可以把两类人员一起列出来,服务端不做取舍。 +- **`guideName` 不再等价于「`staffRole=GUIDE` 那一行」**,它是「导游位上的人」。要精确区分导游与领队请分别读 `guideName` 与 `leaderName`。 +- **`groupBatchId` 在人员配置接口里是产品侧 batchId**,不是 `order_group_batch` 主键,两者不可互换。 +- 判断「导游是否配齐」请读团期的 `guide_ready` 或订单的 `guide_status`,不要自行按角色字符串比对。 + +--- + +## 五、数据库行为 + +无 DDL、无迁移、无新增列。写入行为的变化: + +| 表 | 列 | 变化 | +|---|---|---| +| `order_group_batch` | `guide_ready` | 改前因 ID 空间用错**从未被置位**;改后按主键正确置 true | +| `order_group_batch` | `photographer_ready` | 同上 | +| `order_main` | `guide_status` | `staffRole=GUIDE` 的行现在也会写 `DONE`;删除时按导游位整组判空 | +| `order_status_log` | — | `staffRole=GUIDE` 现在也会产生一条 `GUIDE_DONE` 数据流水 | +| `order_batch_staff` / `order_staff_assignment` | — | 结构与写入内容均**未变** | + +--- + +## 六、边界行为 + +- 导游位配 `GUIDE` → `guide_ready=true` +- 导游位配 `LEADER` → `guide_ready=true` +- 导游位同时配两者 → `markGuideReady` 只调一次 +- 团期查不到 → ready 回填跳过并 WARN,接口不报错 +- 导游位删剩一人 → `guide_status` 保持 `DONE` +- 导游位清空 → `guide_status` 置空且不写流水 +- 行程单导游位只有领队 → 导游栏取领队 + +## 六.5、枚举 / 数据字典 + +本次无新增枚举。`staffRole` 取值域与 `SettlementStaffRoleEnum` 保持一致,未扩展。 + +## 六.6、修改前后对比 + +| 项 | 变更前 | 变更后 | +|---|---|---| +| `guide_ready` 回填 | **从未生效**(用产品侧 batchId 打主键,rows=0 只 WARN) | 反查真实主键后正确置位 | +| 导游位判定 | 只认 `LEADER` | 认 `GUIDE` 与 `LEADER` | +| `guide_status` 写入 | `GUIDE` 行不写 | `GUIDE` 行同样写 | +| `GUIDE_DONE` 流水 | `GUIDE` 行不写 | `GUIDE` 行同样写 | +| 删除后计数 | 只数被删角色,可能误置空 | 按导游位整组计数 | +| 行程单导游栏 | 只取 `GUIDE` 行,配领队时为空 | `GUIDE` 优先,缺位回退 `LEADER` | +| 接口路径 / 入参 / 出参结构 | — | **全部不变** | + +## 六.7、影响评估 + +| 维度 | 评估 | +|---|---| +| 兼容性 | **只放宽不收紧**。原来不触发的现在会触发,存量 `staffRole=LEADER` 的配置行行为与改前完全一致 | +| 前端 | **无需改动**。路径、入参、出参结构均未变;行程单导游栏由「可能为空」变为「有值」,是修复不是破坏 | +| 数据 | 无 DDL、无迁移、无回填 | +| 性能 | 删除路径的计数由单值等值查询改为 `IN` 两值;行程单导游栏由一次点查改为一次整组查询后按序挑选,**查询次数不增** | +| 回滚 | `git revert`,单一 PR,无数据侧残留 | +| 风险 | 低。`guide_ready` 修复后会让原本推不动的团期开始推进资源就绪闸门,属预期恢复 | + +## 七、不影响范围 + +- **零影响**:所有接口的路径、HTTP 方法、入参、出参结构 +- **零影响**:摄影位口径,仍只收 `PHOTOGRAPHER` +- **零影响**:`leaderName` / `leaderPhone` 的语义与取值 +- **零影响**:`DRIVER` 与 `OTHER` 角色的处理 +- **零影响**:staff-fees 导游费用录入分流逻辑(裁决 D-2,配在导游位的领队不录导游费用,属预期) +- **未新建端点**,未改动任何路径与入参 + +--- + +## 八、测试环境已验证 + +✅ 2026-09-04 于测试环境网关实测,真实鉴权(管理端 `test_admin`,角色定制师)。 + +- 网关 `https://api.test.1814.love:9443`,分支 `dev-v3`,合并提交 `f37b6ee0f` +- 部署方式:双实例滚动更新(8186 → 8086),各 11s 就绪,零停机 +- 部署内容经部署面板 `/api/git/backend` 交叉核对:构建时点 `f37b6ee0f` 已是 `dev-v3` 顶端 + +| # | 用例 | 期望 | 实测 | +|---|---|---|---| +| 1 | 配 `staffType=GUIDE` 到导游位 | `guide_ready` 置 1 | ✅ 0 → 1(AC-2) | +| 2 | 配 `staffType=LEADER` 到导游位 | `guide_ready` 置 1 | ✅ 0 → 1(AC-4) | +| 3 | 配摄影师 | `photographer_ready` 置 1 | ✅ 0 → 1(AC-9) | +| 4 | 订单侧配 `staffRole=GUIDE` | `guide_status=DONE` + `GUIDE_DONE` 流水 | ✅ 流水操作人 `test_admin`,内容脱敏为首字加星 | +| 5 | 导游位删剩领队 | `guide_status` 保持 `DONE` | ✅ 未被置空 | +| 6 | 行程单,导游位只有领队 | 导游栏取到领队 | ✅ `guideName` 为领队姓名,电话一致 | +| 7 | 行程单,导游位两者都有 | 导游栏取导游 | ✅ 导游优先生效 | +| 8 | 扇出 | 团期配置扇出到活跃子订单 | ✅ 2 单,`source=GROUP_BATCH` | + +**观察**:部署后双实例持续 running 无重启,服务日志最近 200 行 `ERROR` / `Exception` 命中 **0** 条。 +日志中可见 `Parameters: 990706300101(Long), GUIDE(String), LEADER(String)`,即按导游位整组计数的查询确已在测试环境执行。 + +**本地单测**:`AssignmentServiceTest` 65 例、`GroupBatchStaffConfigServiceTest` 31 例、 +`AssignmentServiceCallbackDriverTest` 42 例、`OrderServiceTest` 205 例、 +架构与错误码门禁 51 例,合计 **401 例全过**,其中新增 7 例覆盖本次全部行为变化。 + +**测试数据**:测试环境所造数据一律 `T7063-` 前缀,验证后已物理删除,残留合计 0 条。 +所用 `test_admin` 口令为一次性置换,取到令牌后立即按备份还原,与备份逐字节一致。 + +> ⚠️ **一处未做实测**:团期人员配置的扇出链路(`doFanOutForOrder`)**不调用** `syncStaffStatusToOrder`, +> 因此经团期配置这条路进来时 `order_main.guide_status` 始终为 null。用例 4 走的是**订单侧**新增人员端点。 +> 这是先于本次变更就存在的第三处缺口,不在本波范围,已在工单中记录待另议。 + +--- + +## 九、相关历史 PR + +- #7065 本次变更(第 1 波 P1 止血) +- #6962(Refs #6950)团期人员配置候选列表——本次修的正是它的下游遗漏 +- 裁决来源:2026-09-02「导游位并收 GUIDE 与 LEADER,服务端不替业务做取舍」 + +--- + +## 十、相关文档 + +- 团期需求文档:`docs/group/`(dev-v3 分支) +- 一期实施拆分详细设计 v1.0 §0.26.4(AC-TD-13~16) + +--- + +## 关联 / 联系人 + +- **Issue**: #7063 +- **PR**: #7065(合并提交 `f37b6ee0f`)