diff --git a/changelogs-v2/2026-09/07_7080_团期人员配置扇出补同步订单侧staff状态-修改接口-管理后台.md b/changelogs-v2/2026-09/07_7080_团期人员配置扇出补同步订单侧staff状态-修改接口-管理后台.md new file mode 100644 index 00000000..708dacdc --- /dev/null +++ b/changelogs-v2/2026-09/07_7080_团期人员配置扇出补同步订单侧staff状态-修改接口-管理后台.md @@ -0,0 +1,307 @@ +--- +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: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-07" +status_note: "PR #7115 合入 dev-v3(468df6fa7)。2026-09-07 部署 dev-v3 到测试服并过网关实测:产品 2044306857534636034 第7期(55 户活跃子订单)经团期配置一名导游后 guide 芯片由 TODO(0/55) → DONE(55/55),清空配置后回落到未配置(0)。本地端到端另覆盖流水/闸门/手工行共存/大团期性能。" +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 diff --git a/changelogs-v2/2026-09/07_7095_团期转订单-新增接口-管理后台.md b/changelogs-v2/2026-09/07_7095_团期转订单-新增接口-管理后台.md new file mode 100644 index 00000000..72360bae --- /dev/null +++ b/changelogs-v2/2026-09/07_7095_团期转订单-新增接口-管理后台.md @@ -0,0 +1,390 @@ +--- +schema: "hl-changelog/v2" +ticket: "7095" +title: "团期转订单:同产品其他期的子订单跨期转入本期" +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-07" +status_note: "PR #7103 合入 dev-v3(6c1124db5);后续修复 #7270(订单侧 timeline,验收要点5漏实现)、#7278(容量守卫 0=不限口径,网关实测暴露)均已合入。2026-09-07 部署 dev-v3 到测试服并过网关实测:候选查询、成功转期(计数守恒/金额重算/订单归属/订单timeline)、容量守卫拒绝路径全部通过。" +updated_at: "2026-09-07" +base: "dev-v3" +--- + +# 团期: 转订单——同产品其他期的子订单跨期转入本期 + +> **服务**: hl-order-service-v3 +> **PR**: #7103 +> **Issue**: #7095 +> **日期**: 2026-09-04 +> **影响范围**: 管理后台团期详情底部操作条「转订单」弹窗 + +--- + +## ⚠️ 关键变化 + +原型上的「转订单」按钮此前**没有后端可调**(全仓零命中),候选下拉是前端本地 mock。本次两个端点补齐。 + +三条前端必读: + +1. **手机号搜索只支持整串精确匹配**,输前几位搜不到——`customer_phone` 是 AES 加密列,LIKE 在密文上无意义。 +2. **「转期原因」是选填**。原型弹窗没有这个输入框,后端不强制,前端零改动也能上线(不传则记「管理员手动转期」)。 +3. **通知客户本期不实发**。勾选「转入后通知客户」只影响响应 `warnings` 里的提示文案,后端不发公众号/短信(模板 ID 未提供)。 + +--- + +## 一、背景 + +转订单 = 把**同一产品其他期**的一个子订单(= 一户)转到本期:客户无需重新下单, +**订金不变、尾款按本期价重算、不可跨产品转**。与已上线的「退单户」`POST .../withdraw` 是两件事: + +| 维度 | 退单户(已有) | 转订单(本次) | +|---|---|---| +| 退不退钱 | 退该户定金 | **不退**,钱跟着人走 | +| 审批 | 走(定制师审) | **不走** | +| 该户去向 | 离团进退款流程 | 转入目标期,订单继续有效 | +| 团期计数 | 已报名户/人数回落 | 源期回落 + 目标期增加,总数守恒 | + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 转订单(子订单跨期转入) | POST | `/v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in` | 新增 | 把源期一个子订单转入本期,尾款按本期价重算 | +| 2 | 转订单候选子订单列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/transfer-candidates` | 新增 | 弹窗搜索下拉的候选池,替掉前端本地 mock | + +--- + +## 三、接口详情 + +### 1. 转订单(子订单跨期转入) `POST /v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in` + +**VO**: `TransferSubOrderReqVO` / `TransferSubOrderRespVO` + +#### 使用场景 + +团期详情底部操作条点「转订单」→ 弹窗里搜到源期的某个子订单 → 点「转入本期并通知」时调用。 +path 上的 `groupBatchId` 是**转入**期(当前打开的这一期),源期在 body 的 `fromGroupBatchId`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 转入期(本期)团期主订单 ID | +| orderId | Path | Long | ✅ | - | 待转入的子订单 ID(取自候选列表的 `orderId`) | +| fromGroupBatchId | Body | Long | ✅ | 非空 | 源期团期主订单 ID(取自候选列表的 `fromGroupBatchId`,**不是产品侧班期 ID**) | +| reason | Body | String | ❌ | ≤512 字 | 转期原因;不传后端记「管理员手动转期」 | +| notify | Body | Boolean | ❌ | 默认 true | 对应弹窗复选框;本期只落标志位并在 warnings 提示,**不实发通知** | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId | String | 被转子订单 ID(雪花,序列化为字符串) | +| fromBatchNo | String | 源期团期号 | +| toBatchNo | String | 目标期团期号 | +| oldOrderAmount | BigDecimal | 转期前应收 | +| newOrderAmount | BigDecimal | 转期后应收(按目标期报价重算) | +| paidAmount | BigDecimal | 已付金额(转期不动,原样跟随) | +| newBalanceDue | BigDecimal | 转期后待收尾款 = 新应收 − 已付 | +| warnings | String[] | 非阻断提示,需人工跟进的事项;无则为空数组 | + +#### 请求示例 + +```json +{ "fromGroupBatchId": 1932847562341, "reason": "客户改期,转到第 7 期", "notify": true } +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "orderId": "1955001234567890", + "fromBatchNo": "GT-26-0005", + "toBatchNo": "GT-26-0007", + "oldOrderAmount": 8000.00, + "newOrderAmount": 8600.00, + "paidAmount": 3000.00, + "newBalanceDue": 5600.00, + "warnings": ["需人工经公众号 + 短信通知客户团期变更与新行程"] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +本接口是写操作,无空数据形态。产品域报价 / 库存调用降级时**不静默成功**,一律按错误响应返回并整事务回滚。 +`warnings` 可能为空数组(该户无合同保险、且 `notify=false`): + +```json +{ "code": 200, "data": { "warnings": [] }, "success": true } +``` + +#### 错误响应 + +```json +{ + "code": 589528, + "message": "目标团期价低于该户已付金额,需先走退款流程", + "success": false, + "data": null +} +``` + +八类拒绝: + +| code | 含义 | +|------|------| +| 589500 | 团期不存在(源期或目标期) | +| 589501 | 该订单当前状态不可转(只放行待支付 / 待出行) | +| 589510 | 源期或目标期状态不满足(须为招募中 / 资源筹备中) | +| 589512 | 子订单不属于所填源期 | +| 589524 | 转出与转入是同一团期 | +| 589525 | 跨产品转(只能同产品不同期) | +| 589526 | 目标期名额或房间数不足 | +| 589527 | 获取目标期报价失败 | +| 589528 | 目标期价低于该户已付金额 | + +#### 业务边界 + +- **鉴权**:管理后台端点,须带网关注入的 `X-Admin-Id`;与既有团期动作端点(成团 / 流团 / 退单户)同口径。 +- **幂等**:同一 `orderId` 5 秒窗口内重复提交只成功一次(双击防重)。 +- **并发**:按团期 ID 数值升序对两期加分布式锁,两个管理员对拉互转不会死锁。 +- **失败零写入**:任一守卫不过或产品域调用失败,整事务回滚——两期名额、两期已报名计数、订单本身**分文未动**。 +- **钱不动**:`paid_amount` 全程不变,本接口不产生任何退款单或收款单;差额体现在待收尾款上。 +- **兼容**:不传 `reason` / `notify` 均可,老前端零改动可调。 + +--- + +### 2. 转订单候选子订单列表 `GET /v3/admin/order/group-batch/:groupBatchId/transfer-candidates` + +**VO**: `TransferCandidateVO` + +#### 使用场景 + +转订单弹窗里「搜索要转入的订单」输入框的数据源,替掉原型阶段的前端本地 mock 数据。 +返回的每一行可直接渲染成原型那种候选行:客户名 + 期号徽标 + 订单号 + 人数。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | - | 转入期(本期)团期主订单 ID | +| keyword | Query | String | ❌ | - | 订单号 / 客户名 / 期号 → 模糊;**手机号 → 必须整串(11 位),前缀搜不到** | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| orderId | String | 子订单 ID,转入时原样回传为 path 的 `orderId` | +| orderNo | String | 订单号,如 `GT-26-0085` | +| customerName | String | 客户姓名 | +| peopleCount | Integer | 该户人数(成人+儿童+小童+婴儿) | +| paidAmount | BigDecimal | 该户已付金额 | +| fromGroupBatchId | String | 源期团期主订单 ID,转入时原样回传为 body 的 `fromGroupBatchId` | +| batchNo | String | 源期团期号 | +| batchName | String | 源期期名,原型徽标「第 5 期」取此列 | +| departDate | LocalDate | 源期出发日期 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/1932847562341/transfer-candidates?keyword=%E8%B5%B5%E6%95%8F HTTP/1.1 +X-Admin-Id: 1 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": [ + { + "orderId": "1955001234567890", + "orderNo": "GT-26-0085", + "customerName": "赵敏", + "peopleCount": 4, + "paidAmount": 3000.00, + "fromGroupBatchId": "1932847562341", + "batchNo": "GT-26-0005", + "batchName": "第 5 期", + "departDate": "2026-10-01" + } + ], + "success": true +} +``` + +#### 空数据 / 降级响应 + +无候选(同产品没有其他可转期、或关键字无命中、或本期自身状态已不可转)时返回空数组,不报错: + +```json +{ "code": 200, "data": [], "success": true } +``` + +#### 错误响应 + +```json +{ + "code": 589500, + "message": "团期不存在", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **候选池口径**:同 `productId` 的**其他**期(排除本期自身),且源期状态 ∈ 招募中 / 资源筹备中, + 订单状态 ∈ 待支付 / 待出行——与转入接口的守卫完全同源,**列表里出现的都是能转的**。 +- **本期不可转时返回空**:本期已进物资准备及以后,直接返回 `[]`,前端应据此禁用弹窗而不是靠调用结果试探。 +- **不分页**:同产品可转期数 × 每期户数量级很小(原型「满 9 户满团」),一次返回全量供前端本地筛。 +- **手机号是加密列**:整串走加密等值匹配,非 11 位数字的关键字走订单号 / 客户名 / 期号三路模糊。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误 payload 对照 + +| 场景 | payload | +|------|---------| +| ✅ 最小请求 | `{ "fromGroupBatchId": 1932847562341 }` | +| ✅ 带原因不通知 | `{ "fromGroupBatchId": 1932847562341, "reason": "客户改期", "notify": false }` | +| ❌ 缺源期 | `{ "reason": "客户改期" }` → 非 200 业务码,message 含「源团期 ID 不能为空」 | +| ❌ 传产品侧班期 ID 当源期 | `{ "fromGroupBatchId": 92001 }` → 589500 团期不存在 | +| ❌ 源期 = 本期 | `{ "fromGroupBatchId": }` → 589524 | + +### 两个 ID 不要混用 + +`fromGroupBatchId` 与 path 上的 `groupBatchId` **同一口径**,都是团期主订单 ID +(候选列表返回的 `fromGroupBatchId` 直接回传即可)。不要传产品侧班期 ID。 + +--- + +## 五、数据库行为 + +| 动作 | 落库效果 | +|------|----------| +| 转期成功 | `order_main.product_batch_id` 改为目标期班期 ID;`order_amount` 改为目标期报价;`depart_date` / `return_date` / `trip_days` / `trip_nights` 同步刷成目标期值 | +| 团号 | `order_main.team_no` **不变**(订金支付时生成的团号跟订单走) | +| 已付 | `order_main.paid_amount` **不变** | +| 流水 | `group_batch_transfer` 新增一行(新表),记录两期 ID、人数房数、新旧应收、已付快照、原因、操作人 | +| 计数 | 源期 `enrolled_people/enrolled_rooms` 减、目标期加,总数守恒;产品域两期 `enrolled_count/booked_rooms` 同步 | +| 时间线 | 源期一条「子订单转出」、目标期一条「子订单转入」(`order_group_batch_status_log`,DATA 类) | +| 失败 | 任一步失败整事务回滚,以上全部不发生 | + +--- + +## 六、边界行为 + +- 未登录 / 缺 `X-Admin-Id` → 401(网关拦截) +- 团期或子订单不存在 → 589500 / 订单域 not found,不 500 +- 产品域报价或名额调用失败 → 589527 / 名额失败码 + 整事务回滚,不产生半搬状态 +- 时间线写失败 → 降级 WARN,**不影响转期成功** +- 候选列表下游无数据 → 返回 `[]`,不 500 不阻断弹窗 + +--- + +## 六.5、枚举 / 数据字典 + +### 可转的订单状态(`com.hulalv.order.core.enums.OrderStatus`) + +**所属字段**: 服务端守卫用,不在出参中 | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `PENDING_PAY` | 待支付 | 允许转期 | +| `PENDING_DEPARTURE` | 待出行 | 允许转期 | +| `TRAVELLING` | 出行中 | 拒绝(589501) | +| `COMPLETED` | 已完成 | 拒绝(589501) | +| `CANCELLED` | 已取消 | 拒绝(589501) | + +### 可转的团期状态(`com.hulalv.order.groupbatch.enums.GroupBatchStatus`) + +**所属字段**: 服务端守卫用,不在出参中 | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `RECRUITING` | 招募中 | 源期 / 目标期均允许 | +| `RESOURCE_PREPARING` | 资源准备中 | 源期 / 目标期均允许 | +| `MATERIAL_PREPARING` 及以后 | 物资准备中 / 待出发 / 出行中 / 核团中 / 已结算 / 已流团 | 任一方处于该区间即拒(589510) | + +--- + +## 七、不影响范围 + +- **仅影响**: 管理后台团期详情的「转订单」弹窗 +- **零影响**: + - 退单户 `POST .../sub-order/:orderId/withdraw`(本次未改一行) + - 创单与支付链路(不产生退款单 / 收款单,`paid_amount` 不动) + - C 端小程序全部接口 + - 团期人员 / 物资 / 财务等其余团期端点 + - 存量数据(新表为空表,不迁移历史) + +--- + +## 八、测试环境已验证 + +✅ **已验证。** 2026-09-07 部署 dev-v3 到测试服并经网关实测(`https://api.test.1814.love:9443`,真实管理端鉴权)。 + +产品 2056947670512971778 的两个同产品期(源期 gbId 2096510069465088002 / 目标期 2096495107078328322),转入一户 2 人 1 房: + +| # | 用例 | 期望 | 实测 | +|---|---|---|---| +| 1 | 候选查询(目标期视角) | 列出源期该户 | ✅ 返回 orderNo HL20260906160526197、2 人、fromGroupBatchId 源期 | +| 2 | 成功转期 | code 200 + 金额三件套 | ✅ oldOrderAmount 4000 / newOrderAmount 4000 / paidAmount 0 / newBalanceDue 4000 + warnings 通知提示 | +| 3 | 计数守恒 | 源期 −2/−1、目标期 +2/+1 | ✅ 源期 people/rooms 2/1 → 0/0,目标期 2/1 → 4/2 | +| 4 | 订单归属改到目标期 | 转走后源期候选不再有它 | ✅ 目标期候选查询转后为 0 条(该户已在本期) | +| 5 | **订单侧 timeline**(#7270 补) | 一条 GROUP_BATCH_TRANSFER | ✅ order status-log 事件序列 [CREATE, GROUP_BATCH_TRANSFER],content「由 {源期号} 转入 {目标期号},应收 4000.00 → 4000.00」 | +| 6 | 容量守卫拒绝路径 | 目标期满时 589526 | ✅ 修容量口径前实测触发 589526(目标期 maxParticipants=0 曾被误判满,已由 #7278 修正「0=不限」) | + +候选查询四路 keyword(订单号/客户名/期号 LIKE + 手机号加密等值)由单测 +`GroupBatchTransferServiceTest.phoneKeywordGoesEncryptedEqualsBranch` 等覆盖; +八类拒绝路径、时间线降级、库存补偿由该测试类 30 例覆盖。 + +单测:全量 `mvn -o -pl hl-order-service-v3 -am test` 8782 例 Failures 0(含 #7270 timeline、#7278 容量修复)。 + +--- + +## 九、相关历史 PR + +| PR | Issue | 说明 | 是否仍有效 | +|----|-------|------|------------| +| #7103 | #7095 | 首次落地 GB-ADM-071 转订单两个端点 | ✅ | +| #7257 | #7250/#7095 | 修 dev-v3 测试编译中断(并发签名冲突) | ✅ | +| #7270 | #7095 | 补写订单侧 timeline(验收要点 5 漏实现) | ✅ | +| **#7278** | **#7095** | 容量守卫「0=不限」口径修正(网关实测暴露) | ✅ 最新 | + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7095](https://git.1814.love:8443/wx/HL/issues/7095) +- 关联 PR: [wx/HL#7103](https://git.1814.love:8443/wx/HL/pulls/7103) +- 后续计划: 通知实发(模板 ID 到位后)、审批流、差额自动退款——均见工单 #7095 §7「明确不做」 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7095](https://git.1814.love:8443/wx/HL/issues/7095) +- **PR**: [#7103](https://git.1814.love:8443/wx/HL/pulls/7103) +- **Merge commit**: [6c1124db5](https://git.1814.love:8443/wx/HL/commit/6c1124db5) + +### 联系人 + +- **后端负责人**: @jw diff --git a/changelogs-v2/2026-09/07_7250_团期看板芯片透出chipStats计数-修改接口-管理后台.md b/changelogs-v2/2026-09/07_7250_团期看板芯片透出chipStats计数-修改接口-管理后台.md index b578021f..77cceb76 100644 --- a/changelogs-v2/2026-09/07_7250_团期看板芯片透出chipStats计数-修改接口-管理后台.md +++ b/changelogs-v2/2026-09/07_7250_团期看板芯片透出chipStats计数-修改接口-管理后台.md @@ -288,25 +288,22 @@ Authorization: Bearer ## 八、测试环境已验证 -⏳ **尚未部署测试服,本条 changelog 未发布**(`backend_status: pending`)。 -部署并过网关实测后,此处补真实请求响应片段与 ✓ 标记,同时把 `backend_status` 改为 `deployed`、 -`gateway_status` 改为 `verified`、`verified_at` 填实测日期,再推送。 +✅ **已验证。** 2026-09-07 部署 dev-v3 到测试服并过网关实测,AC-1 ~ AC-9 全部通过 +(实测执行与回写见提交 `16e0133`;AC-6d 缺失用例由 PR #7258 补齐 `6d3b38de7`)。 -待测项(对应工单 AC): +网关实测要点: -| AC | 待测项 | -|----|--------| -| AC-1 | `GET /v3/admin/order/group-batch?productId=2044306857534636034&scope=ALL&pageSize=50`:10-01 期行 `chips.hotel=ERROR` 且 `chipStats.hotel={55,0,1}`,与 `GET .../2096412454643802114/chips/hotel` 的 `totalCount`/`doneCount` 及 items 失败户数逐一相等 | -| AC-2 | 同一响应里普通分支行满足严格等价;覆盖分支行按例外矩阵核对;服务端日志无计数不一致告警 | -| AC-4 | 改前/改后同一请求逐项 diff,除新增键外完全一致 | -| AC-5 | board / 详情 / 导出 / 092 / 093 响应不变 | -| AC-8 | 开 SQL 日志请求一页 20 行,芯片相关查询次数与改前一致 | +| # | 用例 | 结果 | +|---|---|---| +| 1 | `chipStats` 六项与 `chips/{chip}` 明细端点的 `totalCount` / `doneCount` 逐一相等 | ✅ | +| 2 | `error` 计数与明细 `items` 里失败态户数相等——vehicle 芯片 55 户中 1 户 `REJECTED_TO_CONSULTANT`,`chipStats.vehicle.error=1` | ✅ | +| 3 | 跨 6 个产品 55 行普通聚合分支满足「`error > 0` 当且仅当 `chips.X = ERROR`」,**违例 0** | ✅ | -本地证据(非测试环境):`JAVA_TOOL_OPTIONS=-Xmx3g mvn -o -pl hl-order-service-v3 -am test` -全量 **8734 例 Failures: 0**(7 例 Testcontainers 因本机无 Docker 报错,与基线一致); +单测证据:`JAVA_TOOL_OPTIONS=-Xmx3g mvn -o -pl hl-order-service-v3 -am test` +全量 **8734 例 Failures: 0**(7 例 Testcontainers 因无 Docker 报错,与基线一致); `GroupBatchChipResolverTest` 47 例(新增 16:AC-6 七例 + AC-6b 四例 + AC-6c 五例)、 `GroupBatchQueryServiceTest` 41 例(新增 2,含「芯片投影只查一次」断言)、 -`GroupBatchConverterTest` 50 例(新增 1);ArchTest 六道门禁全绿。 +`GroupBatchConverterTest` 50 例;ArchTest 六道门禁全绿。 ---