7510/7511/7512 代码早已随供应商关系系列交付(a17a2c2a/4bedc8be),实证后回写 verified 挂 a17a2c2a; 11_7510 为正本 12_7510 的重复副本(canonical_path),标 not_required 消除 pending; 7513/7530/7531/7535 实证前端零改动 not_required。
14 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7513 | 团期转期同步回写归属两列(group_batch_id 不再停留源期)+ 在途退单转期守卫 589589 | admin | jw(GIT) | 修改接口 | deployed | verified | not_required | mmg | 2026-09-12 | 转订单端点的请求/响应结构一行未动,前端无需改造即可继续运行。两处需要知道:①转期后订单详情的团期编号/名称会正确显示为【目标期】(此前错显源期,属修复非回归);②新增拒绝码 589589——该户有在途退单审核时转期会被拒,前端需按普通业务错误弹提示,文案后端已给全。 前端 2026-09-13 闭环 not_required:转期 TransferOrderModal 纯透传,589589 走拦截器通用业务码兜底弹后端原文;订单详情团期编号/名称由后端按 group_batch_id 解析下发回写后自动正确;历史脏数据归后端 #7586。零业务代码改动。 | 2026-09-13 | 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[] | 非阻断提示 |
本次未改动任何出参字段。
请求示例
POST /v3/admin/order/group-batch/2096412454643802114/sub-order/2097498673159028737/transfer-in
Content-Type: application/json
{
"fromGroupBatchId": 2097498512387104770,
"reason": "客户改期,转到第 7 期",
"notify": false
}
响应示例
{
"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 网关实测原样):
{
"code": 589589,
"message": "该户有退单审核在途,请先在原团期驳回或完成审核后再转期",
"data": null,
"success": false
}
业务边界
- 单单语义:一次调用只转一个子订单,请求体内没有订单列表。
- 幂等:按
orderId5 秒防重;同时按两期 ID 升序各下一次行栅栏,避免两期互转死锁。 - 可转订单状态白名单:
PENDING_PAY/PENDING_DEPARTURE。 - 可转团期状态白名单:
RECRUITING/RESOURCE_PREPARING。因此已出账 / 已配房 / 已出行的团在守卫层就转不了。
四、契约约束与正确调用方式
✅ 正确 / ❌ 错误 payload 对照
❌ 错误——把源期写在 path 上(这是最容易搞反的一处):
POST /v3/admin/order/group-batch/{源期ID}/sub-order/{orderId}/transfer-in
{"fromGroupBatchId": 目标期ID}
✅ 正确——path 是转入期,body 是源期:
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