--- 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`)