16 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 | 7095 | 团期转订单:同产品其他期的子订单跨期转入本期 | admin | jw(GIT) | 新增接口 | deployed | verified | verified | mmg | cc07a40d | 2026-09-08 | PR #7103 合入 dev-v3(6c1124db5),后续修复 #7270(订单侧 timeline)、#7278(容量守卫 0=不限)均已合入,2026-09-07 部署测试服过网关实测(候选查询/成功转期/容量守卫拒绝全通过)。前端待接:团期详情底部「转订单」弹窗接真——候选下拉替掉本地 mock 改调 GET transfer-candidates(手机号仅整串精确匹配,加密列),转期调 POST transfer-in(body fromGroupBatchId,转期原因选填,勾选通知仅影响 warnings 文案不实发)。决策:入 backlog 排在 #7067 U3-U7 与 #7244 之后统一汇总审派发。本条保持 pending。 | 2026-09-07 | dev-v3 |
团期: 转订单——同产品其他期的子订单跨期转入本期
服务: hl-order-service-v3 PR: #7103 Issue: #7095 日期: 2026-09-04 影响范围: 管理后台团期详情底部操作条「转订单」弹窗
⚠️ 关键变化
原型上的「转订单」按钮此前没有后端可调(全仓零命中),候选下拉是前端本地 mock。本次两个端点补齐。
三条前端必读:
- 手机号搜索只支持整串精确匹配,输前几位搜不到——
customer_phone是 AES 加密列,LIKE 在密文上无意义。 - 「转期原因」是选填。原型弹窗没有这个输入框,后端不强制,前端零改动也能上线(不传则记「管理员手动转期」)。
- 通知客户本期不实发。勾选「转入后通知客户」只影响响应
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<TransferSubOrderRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| orderId | String | 被转子订单 ID(雪花,序列化为字符串) |
| fromBatchNo | String | 源期团期号 |
| toBatchNo | String | 目标期团期号 |
| oldOrderAmount | BigDecimal | 转期前应收 |
| newOrderAmount | BigDecimal | 转期后应收(按目标期报价重算) |
| paidAmount | BigDecimal | 已付金额(转期不动,原样跟随) |
| newBalanceDue | BigDecimal | 转期后待收尾款 = 新应收 − 已付 |
| warnings | String[] | 非阻断提示,需人工跟进的事项;无则为空数组 |
请求示例
{ "fromGroupBatchId": 1932847562341, "reason": "客户改期,转到第 7 期", "notify": true }
响应示例
{
"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):
{ "code": 200, "data": { "warnings": [] }, "success": true }
错误响应
{
"code": 589528,
"message": "目标团期价低于该户已付金额,需先走退款流程",
"success": false,
"data": null
}
八类拒绝:
| code | 含义 |
|---|---|
| 589500 | 团期不存在(源期或目标期) |
| 589501 | 该订单当前状态不可转(只放行待支付 / 待出行) |
| 589510 | 源期或目标期状态不满足(须为招募中 / 资源筹备中) |
| 589512 | 子订单不属于所填源期 |
| 589524 | 转出与转入是同一团期 |
| 589525 | 跨产品转(只能同产品不同期) |
| 589526 | 目标期名额或房间数不足 |
| 589527 | 获取目标期报价失败 |
| 589528 | 目标期价低于该户已付金额 |
业务边界
- 鉴权:管理后台端点,须带网关注入的
X-Admin-Id;与既有团期动作端点(成团 / 流团 / 退单户)同口径。 - 幂等:同一
orderId5 秒窗口内重复提交只成功一次(双击防重)。 - 并发:按团期 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<List<TransferCandidateVO>>
| 字段 | 类型 | 说明 |
|---|---|---|
| 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 | 源期出发日期 |
请求示例
GET /v3/admin/order/group-batch/1932847562341/transfer-candidates?keyword=%E8%B5%B5%E6%95%8F HTTP/1.1
X-Admin-Id: 1
响应示例
{
"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
}
空数据 / 降级响应
无候选(同产品没有其他可转期、或关键字无命中、或本期自身状态已不可转)时返回空数组,不报错:
{ "code": 200, "data": [], "success": true }
错误响应
{
"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": <path 上同一个值> } → 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
- 关联 PR: wx/HL#7103
- 后续计划: 通知实发(模板 ID 到位后)、审批流、差额自动退款——均见工单 #7095 §7「明确不做」
关联 / 联系人
链接
联系人
- 后端负责人: @jw