docs(order-v3): #7513 团期转期同步回写归属两列 + 在途退单守卫 589589
changelog-filename-gate / validate (push) Successful in 2s

转订单端点请求/响应结构零改动,前端无需改造。两处需告知:

1. 转期后订单详情的团期编号/名称会正确显示为【目标期】——此前因只改了
   product_batch_id、没改 group_batch_id,展示的是源期。属修复非回归。
2. 新增拒绝码 589589:该户有在途 PENDING 退单审核时转期被拒,前端按普通
   业务错误弹 message 即可,文案已含出口指引(先在源期驳回再重提)。

历史已转期订单的存量分叉本次不修,见 #7586。

frontmatter: backend_status=deployed / gateway_status=verified / frontend_status=pending
校验:validateV2Document 0 错误;validateChangelogPath 通过。

Refs #7513
PR wx/HL#7589

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
jw
2026-09-12 17:10:02 +08:00
共同撰写人 Claude Opus 5
父节点 47544d190e
当前提交 d32665518f
@@ -0,0 +1,302 @@
---
schema: "hl-changelog/v2"
ticket: "7513"
title: "团期转期同步回写归属两列(group_batch_id 不再停留源期)+ 在途退单转期守卫 589589"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-09-12"
status_note: "转订单端点的请求/响应结构一行未动,前端无需改造即可继续运行。两处需要知道:①转期后订单详情的团期编号/名称会正确显示为【目标期】(此前错显源期,属修复非回归);②新增拒绝码 589589——该户有在途退单审核时转期会被拒,前端需按普通业务错误弹提示,文案后端已给全。"
updated_at: "2026-09-12"
base: "dev-v3"
---
# order-v3: 团期转期同步回写归属两列
**服务**: hl-order-service-v3
**PR**: #7589
**Issue**: #7513
---
## ⚠️ 关键变化
🟢 **请求与响应结构零改动。** `transfer-in` 的 path、body、`Result<TransferSubOrderRespVO>` 的字段名/类型/null 语义一行没动,前端**不改任何代码**即可继续工作。
🔴 **转期后订单的团期归属显示会变**:此前转期只改了 `order_main.product_batch_id`、没改 `order_main.group_batch_id`,导致同一订单两列指向两个不同的期。凡按 `group_batch_id` 取团期编号/名称的展示位(订单详情、聊天会话归属等)此前**显示的是源期**。本次修复后显示**目标期**。**这是纠错,不是回归**——如果 QA 对比历史截图发现团号变了,是本次修好了。
🔴 **新增拒绝码 `589589`**:该户存在**在途(PENDING)退单审核**时,转期会被直接拒绝。
```
{"code": 589589, "message": "该户有退单审核在途,请先在原团期驳回或完成审核后再转期", "success": false}
```
**前端按普通业务错误弹 `message` 即可**,文案已含出口指引,无需自造提示。
**为什么加**:此前「先提退单 → 被转走 → 审批通过」这条路能跑通,但跑出来是**错账**——按源期阶段算退款、时间线记源期,而人数扣减按 `product_batch_id` 扣到目标期,钱与账落在两个团,接口却返回成功。修好归属两列后,这种审批会在归属校验处抛 `589512`,那张申请**再也批不掉**;故把冲突提前到转期入口,给运营看得懂的提示与出口。
🔴 **历史脏数据本次不修**:本次只保证**新发生的转期**两列一致。修复上线**之前**已转过期的订单,两列仍分叉,需 **#7586** 单独处理(不能与本次同版发迁移,理由见该单)。
---
## 一、背景
转订单功能(#7095)合入于 2026-09-04 17:32,而 `order_main.group_batch_id` 这一「一跳直连」列由 #7083 于**同日 18:09** 才加上——相差 37 分钟,两个工单并行落地,此后无人回头补转期链路的回写。
后果比「两列不一致」更重:团期在团名册走的是「一跳列 ∪ 两跳列(且一跳为 NULL)」的双通道并集读,回退通道带 `group_batch_id IS NULL` 硬闸。转期户该列非空被排除,于是**在目标期的在团名册里整体消失**(房务需求、整团需求确认、团期核单、用车派单全看不到),同时仍留在源期;而人数计数走 `product_batch_id` 已正确搬走——「人数」与「成员名单」互相矛盾。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 转订单(子订单跨期转入) | POST | `/v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in` | 修改接口 | 请求/响应结构不变;写库时同步回写归属第二列,并新增拒绝码 589589 |
---
## 三、接口详情
### 1. 转订单(子订单跨期转入) `POST /v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in`
**VO**: `TransferSubOrderReqVO` → `Result<TransferSubOrderRespVO>`
#### 使用场景
团期管理员把同一产品其他期的一个子订单转入本期:不退钱、订金跟人走、尾款按目标期价重算、不可跨产品转。path 上的 `:groupBatchId` 是**转入**期,源期在 body 的 `fromGroupBatchId`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `groupBatchId` | path | Long | 是 | 团期主订单 ID,非产品侧班期 ID | **转入**期 |
| `orderId` | path | Long | 是 | 须为源期下的在团子订单 | 待转入的子订单 |
| `fromGroupBatchId` | body | Long | 是 | 团期主订单 ID;须与订单当前排期一致 | 源期 |
| `reason` | body | String | 否 | ≤512 字 | 转期原因;不传记「管理员手动转期」 |
| `notify` | body | Boolean | 否 | — | 转入后是否通知客户;只落标志位,**不实发** |
**本次未改动任何入参。**
#### 出参 `Result<TransferSubOrderRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | String | 子订单 ID |
| `fromBatchNo` / `toBatchNo` | String | 源期 / 目标期期号 |
| `oldOrderAmount` / `newOrderAmount` | BigDecimal | 转期前 / 后应收 |
| `paidAmount` | BigDecimal | 已付(转期不动钱) |
| `newBalanceDue` | BigDecimal | 新待收尾款 |
| `warnings` | String[] | 非阻断提示 |
**本次未改动任何出参字段。**
#### 请求示例
```http
POST /v3/admin/order/group-batch/2096412454643802114/sub-order/2097498673159028737/transfer-in
Content-Type: application/json
{
"fromGroupBatchId": 2097498512387104770,
"reason": "客户改期,转到第 7 期",
"notify": false
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "2097498673159028737",
"fromBatchNo": "Q202611202097498511120478209",
"toBatchNo": "Q202610012052935476548939777",
"oldOrderAmount": 1000.0,
"newOrderAmount": 3425.0,
"paidAmount": 0.0,
"newBalanceDue": 3425.0,
"warnings": []
},
"success": true
}
```
#### 空数据 / 降级响应
- 目标期班期天数取不到(产品域 Feign 失败)时**不阻断**:订单沿用原天数,`warnings` 追加「目标团期行程天数未取到,订单沿用原天数,请人工核对」,`code` 仍为 200。
- 本接口无「空数据」形态:要么整体成功,要么抛业务错误码,不返回半截数据。
#### 错误响应
| code | message | 何时出现 | 本次变化 |
|---|---|---|---|
| **589589** | 该户有退单审核在途,请先在原团期驳回或完成审核后再转期 | 该子订单存在 PENDING 退单审批 | **🆕 本次新增** |
| 589510 | 团期状态不允许转期 | 源期或目标期不在「招募中 / 资源准备中」 | 不变 |
| 589512 | 该订单不属于本团期 | 源期与订单当前排期不一致 | 不变 |
| 589560 | 该户在源期已分房,不可转期({0} 条) | 源期已有生效分房行 | 不变 |
| 589527 | 获取目标团期报价失败,请稍后重试 | 目标期报价取不到或非正 | 不变 |
| 589528 | 目标团期价低于该户已付金额,需先走退款流程 | 目标期价 < 已付 | 不变 |
本次新增的 `589589` 实际响应体(TEST 网关实测原样):
```json
{
"code": 589589,
"message": "该户有退单审核在途,请先在原团期驳回或完成审核后再转期",
"data": null,
"success": false
}
```
#### 业务边界
- **单单语义**:一次调用只转一个子订单,请求体内没有订单列表。
- **幂等**:按 `orderId` 5 秒防重;同时按两期 ID 升序各下一次行栅栏,避免两期互转死锁。
- 可转订单状态白名单:`PENDING_PAY` / `PENDING_DEPARTURE`。
- 可转团期状态白名单:`RECRUITING` / `RESOURCE_PREPARING`。因此**已出账 / 已配房 / 已出行的团在守卫层就转不了**。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
❌ **错误——把源期写在 path 上**(这是最容易搞反的一处):
```http
POST /v3/admin/order/group-batch/{源期ID}/sub-order/{orderId}/transfer-in
{"fromGroupBatchId": 目标期ID}
```
✅ **正确——path 是转入期,body 是源期**:
```http
POST /v3/admin/order/group-batch/{目标期ID}/sub-order/{orderId}/transfer-in
{"fromGroupBatchId": 源期ID}
```
两个 ID 都是**团期主订单 ID(`groupBatchId`)**,不是产品侧班期 ID(`productBatchId`)。一个接口不混用两套口径。
### 切换状态时的必要动作
前端收到 `589589` 时的正确引导:**先在源期把该户的退单申请驳回**(驳回路径不校验归属,任何时候都可用),再重新发起转期;或等该退单审批走完(走完即离团,也就不需要转期了)。**不要**引导用户去目标期重新提交退单——会被 `589529`「该户已有退单审核在途」拦。
---
## 五、数据库行为
| 表 | 列 | 变更 |
|---|---|---|
| `order_main` | `product_batch_id` | 转期时改写为目标期排期 ID(**行为不变**) |
| `order_main` | `group_batch_id` | **🆕 本次起同步改写为目标期团期 ID**;此前不写,停留源期 |
两列在**同一条 UPDATE、同一个事务**内落库(转期方法整体 `@Transactional(rollbackFor = Exception.class)`),不存在「只更新了一列」的中间态。**未新增 SQL 语句、未加表、未加列、无 DDL。**
`group_batch_transfer` 转期流水表行为不变,仍逐次留痕。
---
## 六、边界行为
- **多次转期**:每次转期两列同步搬到当次目标期,流水表逐条累积,不做合并。
- **目标期 ID 不可能为空**:目标期对象按主键查出,查不到直接抛「团期不存在」,因此不存在「把归属清成 null」的路径。
- **归属守卫仍比 `product_batch_id`**(不比 `group_batch_id`):这样 `group_batch_id` 尚未回填的历史订单仍能正常转期,不会被挡死。
- **人数计数(enrolled)口径不变**:仍按显式 ±N 搬移,本次未改。
- **在途退单守卫只拦 `PENDING`**:已通过 / 已驳回 / 已撤销的审批行不影响转期。
## 六.6、修改前后对比
### 字段级对比
| 数据位 | 修复前 | 修复后 |
|---|---|---|
| `order_main.product_batch_id` | 目标期排期 | 目标期排期(不变) |
| `order_main.group_batch_id` | **停留源期** ❌ | **目标期** ✅ |
### 行为级对比
| 场景 | 修复前 | 修复后 |
|---|---|---|
| 目标期在团名册是否含该户 | **否**(被两跳回退的空值闸排除) | **是** |
| 源期在团名册是否仍含该户 | **是**(错误滞留) | **否** |
| 订单详情展示的团期编号 | **源期**(错) | **目标期** |
| 该户有在途退单时转期 | 允许,且后续审批产生错账且返回成功 | **拒绝 589589**,给出口 |
| 每日对账 `inconsistent` 指标 | 每转一次 +1,永不归零 | 新转期不再增加 |
## 六.7、影响评估
| 受影响面 | 影响 | 需要前端动作 |
|---|---|---|
| `transfer-in` 请求/响应结构 | 无变化 | **无** |
| 转期后团期编号/名称展示 | 由源期改为目标期(纠错) | **无**,但 QA 对比历史截图时需知情 |
| 转期错误码集合 | 新增 589589 | **有**:按普通业务错误弹 `message` |
| 房务需求 / 整团需求 / 团期核单 / 用车派单的在团名册 | 转期户从此正确出现在目标期 | 无(后端口径修正) |
---
## 七、不影响范围
- **不影响**任何接口的请求参数、响应字段名/类型/null 语义。
- **不影响**判团读写法:本次一行未动各处「这张订单属于哪个团」的判据写法,只保证两列数据一致。
- **不影响**退款金额计算、退改政策、订金/尾款口径。
- **不影响**人数计数(enrolled)的搬移口径。
- **不影响**小程序端:本次改动不涉及 mp 链路。
- **不影响**历史已转期订单的存量数据(见 #7586)。
- **无 DDL、无新增表/列、无网关路由变更、无 Feign 契约变更、无 MQ 变更。**
---
## 八、测试环境已验证
部署:`hl-order-service-v3` ← 分支 `fix/7513-transfer-groupbatchid`,双实例滚动,2026-09-12 17:01 在线。取证经真实网关 `api.test.1814.love` + TEST 库直查。
| # | 用例 | 结果 |
|---|---|---|
| 1 | 真实转期 `transfer-in` | `code 200`,金额 1000.0 → 3425.0 |
| 2 | 转期后两列归属 | `group_batch_id` 与 `product_batch_id` **同指目标期**(SQL 判定 CONSISTENT) |
| 3 | 全局两列分叉计数 | 转期前 1 → 转期后 1(**新转期未制造新分叉**;那 1 条是历史脏行,归 #7586) |
| 4 | 在团名册(双通道并集口径,经 `requirement-summary` 的 `activeOrderCount`) | 目标期 56→55、源期 3→4,**名册随转期正确移动** |
| 5 | 五种判团形态一致性 | A / B / C / D / E 五种口径**全部指向同一个团** |
| 6 | 人数计数 | 源期 6→5、目标期 110→111 |
| 7 | **589589 负向** | 先提交退单(200)→ 转期得 **589589**,文案一致 |
| 8 | **589589 逃生口** | 驳回退单(200)→ 再转期得 **200**,确认是卡壳不是死锁 |
| 9 | 全量单测 + ArchTest | 基线 10132/0/13/49 → 本分支 10135/0/13/49,**仅 +3(本次新增用例),零新增失败**;13 条 Errors 两边逐类一致,均为本机无 Docker 的 Testcontainers 类 |
取证用的既有订单已**转出后转回**,两列、金额、两侧人数、分叉计数全部回到基线值。
---
## 九、相关历史 PR
- #7095 转订单功能初版(`V20260904_002`,2026-09-04 17:32)——设计时 `order_main.group_batch_id` 尚不存在
- #7083 新增 `order_main.group_batch_id` 一跳直连列(`V20260904_004`,同日 18:09)——回填了当时存量、接管创单写入,但未回头改转期链路
---
## 十、相关文档
- 工单 #7513(缺陷与口径定案)
- 工单 #7586(存量数据修复,代码全量滚完后执行)
- `docs/group/团期模块详细设计-v3.0.html` — 归属列写入时机与一致性约束的冻结口径
---
## 关联 / 联系人
### 链接
- Issue: #7513
- PR: #7589
- 后续单: #7586
### 联系人
- 后端: jw
- 前端: mmg