--- schema: "hl-changelog/v2" ticket: "7080" title: "团期人员配置扇出补同步订单侧 staff 状态,guide_status 不再恒为 NULL" 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: "2026-09-07" status_note: "PR #7115 合入 dev-v3(468df6fa7),2026-09-07 测试服网关实测通过(配导游后 guide 芯片 TODO(0/55)→DONE(55/55),清空回落)。出参结构一个字段没变,只改扇出副作用(guide_status/photographer_status 不再恒 NULL、补 DONE 流水、触发就绪闸门)。前端 mmg 已实查为 not_required:全仓无 guide_status/photographer_status 消费点,也无「团期配过但订单侧未配置」的兼容逻辑或提示文案可撤,无需改码。" updated_at: "2026-09-07" base: "dev-v3" --- # 团期: 人员配置扇出补同步订单侧 staff 状态 > **服务**: hl-order-service-v3 > **PR**: #7115 > **Issue**: #7080 > **日期**: 2026-09-04 > **影响范围**: 管理后台团期人员配置保存后,团内各子订单的导游/摄影资源状态与时间线 --- ## ⚠️ 关键变化 **出参结构一个字段没变,变的是写库之后的副作用。** 改前:经「团期详情 → 配置导游/摄影」配的人,扇出只写 `order_staff_assignment` 表, `order_main.guide_status` / `photographer_status` **恒为 NULL**, `GUIDE_DONE` / `PHOTOGRAPHER_DONE` 流水不产生,订单资源就绪闸门 `maybeAdvanceToConfirm` 不被触发。 只有走订单侧「订单详情 → 人员 → 新增」才会写这些。 改后:扇出末尾按配置位全量同步订单侧状态,两条路口径一致。 **前端要注意的是**:以前拿到 `guide_status=null` 不代表没配人(可能是团期配的), 现在 null 就是真的没人。原先若有「团期配过但订单侧显示未配置」的兼容逻辑或提示文案,可以撤掉。 --- ## 一、背景 `GroupBatchStaffConfigService#doFanOutForOrder` 只做两件事:软删 `source=GROUP_BATCH` 旧行 + 批量 INSERT 新行。 而状态回写、DONE 流水、资源就绪闸门三件事挂在 `AssignmentService` 的 private `syncStaffStatusToOrder` 上, 扇出链路从来没调过它。 | 路径 | 写 order_staff_assignment | 写 order_main 状态 | 写 DONE 流水 | 触发就绪闸门 | |---|---|---|---|---| | 订单侧「订单详情 → 人员 → 新增」 | ✅ | ✅ | ✅ | ✅ | | 团期侧「团期详情 → 配置导游」(改前) | ✅ | ❌ | ❌ | ❌ | | 团期侧(**改后**) | ✅ | ✅ | ✅ | ✅ | 这个缺口先于 #7063 就存在,是 #7063 把「角色判定只认单一角色」和「ready 回填 ID 空间用错」修好后才浮出来的。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 全量保存团期人员配置 | PUT | `/v3/admin/group-batch/:groupBatchId/staff` | 副作用变更 | 扇出后逐子订单同步 `guide_status`/`photographer_status` + DONE 流水 + 就绪闸门 | --- ## 三、接口详情 ### 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 | 扇出到的活跃子订单数,**未变** | > 本次**不改任何出参字段**。要观察效果请查子订单的 `guide_status` / `photographer_status` 与订单时间线。 #### 请求示例 ```json { "staffList": [ { "staffId": 1002, "staffRole": "GUIDE", "sortOrder": 0 } ] } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "staffList": [ { "staffId": 1002, "staffRole": "GUIDE", "staffName": "李雪梅", "sortOrder": 0 } ], "affectedOrderCount": 2 } } ``` #### 空数据 / 降级响应 `staffList` 传空数组即清空配置,响应照常 200: ```json { "code": 200, "data": { "staffList": [], "affectedOrderCount": 2 }, "success": true } ``` 清空后各子订单的 `guide_status` / `photographer_status` 会被置回 `null`(该位若无订单侧手工行)。 #### 错误响应 ```json { "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "success": false, "data": null } ``` 本次**未新增任何错误码**,错误语义与改前完全一致。 #### 业务边界 - **异步**:扇出是 `@Async`,状态同步在扇出的单订单子事务内完成,不阻塞本接口响应。 即接口返回 200 时,子订单状态可能尚未写完——前端不要在收到响应的同一刻立即读子订单状态做断言。 - **失败容忍**:单订单同步失败 → 该订单整单回滚(`REQUIRES_NEW` 子事务),记 ERROR 日志后继续下一单, 不影响其他子订单,也不影响本接口的 200。 - **清空语义**:按配置位全量同步,清空时把无人的位置回 `null` 并且**不写流水**。 - **与订单侧手工行共存**:状态按「配置位整组」计数(导游位 = GUIDE + LEADER),与 `source` 无关。 团期行被软删后若该订单仍有 `source=ORDER` 的手工导游行,`guide_status` 保持 `DONE`,两边不互相覆盖。 - **鉴权与幂等**:均未变。 --- ## 四、契约约束与正确调用方式 ### ✅ 正确 / ❌ 错误 payload 对照 | 场景 | payload | |------|---------| | ✅ 配一名导游 | `{ "staffList": [{ "staffId": 1002, "staffRole": "GUIDE" }] }` | | ✅ 清空配置 | `{ "staffList": [] }` | | ❌ 收到 200 后立刻断言子订单状态 | 扇出是异步的,需轮询或稍后再查 | ### 调用方式没有变化 本次是纯副作用修复,**前端不需要改任何请求**。 --- ## 五、数据库行为 | 动作 | 改前 | 改后 | |------|------|------| | 团期配一名导游 | `order_staff_assignment` 新增 `source=GROUP_BATCH` 行;`order_main.guide_status` 保持 `NULL` | 同样新增行;**`order_main.guide_status` 置 `DONE`** | | 同上 | `order_status_log` 无记录 | **每张子订单新增一条 `GUIDE_DONE`**(内容含脱敏姓名,如「李*」) | | 同上 | 就绪闸门不触发 | **触发 `maybeAdvanceToConfirm`**,满足条件的订单可推进到 `PENDING_CONFIRM` | | 团期清空配置 | 软删 `GROUP_BATCH` 行;状态字段不动 | 软删同上;**状态置回 `NULL`**(该位无手工行时),不写流水 | | 订单侧另有手工导游行 | — | 计数按位整组,状态保持 `DONE`,不被团期清空覆盖 | --- ## 六、边界行为 - 未登录 → 401(网关拦截) - 团期不存在 / 无权限 → 与改前一致(589500 / 589507) - 单订单同步失败 → 该订单回滚并记 ERROR,其他订单不受影响,接口仍 200 - 无活跃子订单 → 不做任何同步,`affectedOrderCount=0` - 已取消 / 已完成的子订单 → 本就不在扇出范围(`selectActiveOrderIdsByProductBatchId` 已过滤) --- ## 六.5、枚举 / 数据字典 ### guide_status / photographer_status(order_main 状态字段) **所属字段**: `order_main.guide_status`、`order_main.photographer_status` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `DONE` | 已配置 | 该配置位至少有一人(不分 source) | | `null` | 未配置 | 该配置位无人。**改后 null 才是真的没人**,改前团期配的人也显示 null | ### 配置位与成员(`GroupBatchStaffSlot`,成员由字典决定,见 #7079) **所属字段**: 服务端内部判定 | **类型**: `String` | 配置位 | 默认成员 | 对应 order_main 字段 | |----|------|------| | `guide` | `GUIDE`、`LEADER` | `guide_status` | | `photographer` | `PHOTOGRAPHER` | `photographer_status` | > `DRIVER` / `OTHER` 不属于任何配置位,不关联 order_main 状态字段。 ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `order_main.guide_status`(经团期配置) | 恒 `NULL` | 有人 `DONE` / 无人 `NULL` | | `order_main.photographer_status`(经团期配置) | 恒 `NULL` | 有人 `DONE` / 无人 `NULL` | | 接口出参 | — | **完全未变** | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 团期配人 → 订单时间线 | 无记录 | 每子订单一条 `GUIDE_DONE` / `PHOTOGRAPHER_DONE` | | 团期配人 → 资源就绪闸门 | 不触发 | 触发 `maybeAdvanceToConfirm` | | 团期清空 → 订单状态 | 不动(本来就是 NULL) | 置回 `NULL`(该位无手工行时) | | 订单侧 addStaff / editStaff / deleteStaff | — | **行为完全不变**(实现搬到公共组件,断言未改) | ## 六.7、影响评估 - **是否破坏向后兼容**: 否。出参与入参均未变,只是原本恒为 NULL 的字段开始有值。 - **前端是否必须同步上线**: 否。 - **前端 workaround 清理点**: 若存在「团期已配人但订单侧 guide_status 为空,故不显示导游」这类兼容判断或提示文案,可以撤掉。 ## 七、不影响范围 - **仅影响**: 团期人员配置保存后的扇出副作用 - **零影响**: - 订单侧新增/编辑/删除人员三个端点的行为(实现搬家,断言一字未改) - 本接口的入参、出参、错误码、鉴权、幂等 - 配车回调反写司机链路(DRIVER 不属任何配置位) - 报账人等级设置 - C 端小程序全部接口 --- ## 八、测试环境已验证 ✅ **已验证。** 2026-09-07 部署 dev-v3 到测试服并经网关实测(`https://api.test.1814.love:9443`,真实管理端鉴权)。 产品 2044306857534636034 第 7 期(gbId 2096412454643802114,**55 户活跃子订单**): | # | 用例 | 期望 | 实测 | |---|---|---|---| | AC-1 | 经团期配置一名导游(staffId 1002)后全部子订单 guide 状态 DONE | guide 芯片 `DONE(55/55)` | ✅ 配置前 `TODO(0/55)` → 扇出后 `DONE(55/55)`,`affectedOrderCount=55` | | AC-5 | 清空配置(空 staffList)后状态回落 | guide 芯片回 `TODO(0)` | ✅ 撤回后经中间态 `DOING(54)` 逐单回退至 `TODO(0)`,现场已复原 | 改前对比:经团期配置扇出只写 `order_staff_assignment` 表、不碰订单侧状态,guide 芯片恒 `TODO`—— 本次实测 DONE(55/55) 即修复生效的直接证据。 AC-2(GUIDE_DONE 流水)/ AC-3(摄影位)/ AC-4(就绪闸门推进)/ AC-6(手工行共存不覆盖)/ AC-7(单订单失败隔离)/ AC-8(大团期性能:52 单接口响应 0.11s、扇出落库 0.47s、均摊 9ms) 由本地端到端实测 + 单测覆盖(见工单 #7080 验收总结 comment)。 单测:全量 `mvn -o -pl hl-order-service-v3 -am test` 8167 例 Failures 0; `OrderStaffStatusSyncerTest` 9 例、`GroupBatchStaffConfigServiceTest` 38 例、`AssignmentServiceTest` 65 例全绿。 ## 九、相关历史 PR | PR | Issue | 说明 | 是否仍有效 | |----|-------|------|------------| | #7085 | #7079 | 配置位成员改由字典决定 | ✅ 有效 | | #7076 | #7063 | 导游位下游按配置位归组(第 1 波) | ✅ 有效 | | **本 PR #7115** | **#7080** | 补上扇出链路缺失的状态同步(#7063 AC-3 在团期路径上不成立的真因) | ✅ 最新 | --- ## 十、相关文档 - 关联 Issue: [wx/HL#7080](https://git.1814.love:8443/wx/HL/issues/7080) - 关联 PR: [wx/HL#7115](https://git.1814.love:8443/wx/HL/pulls/7115) - 前序: [wx/HL#7063](https://git.1814.love:8443/wx/HL/issues/7063)(本单从其测试环境验收中分离) ## 关联 / 联系人 ### 链接 - **Issue**: [#7080](https://git.1814.love:8443/wx/HL/issues/7080) - **PR**: [#7115](https://git.1814.love:8443/wx/HL/pulls/7115) - **Merge commit**: [468df6fa7](https://git.1814.love:8443/wx/HL/commit/468df6fa7) ### 联系人 - **后端负责人**: @jw