391 行
16 KiB
Markdown
391 行
16 KiB
Markdown
---
|
||
schema: "hl-changelog/v2"
|
||
ticket: "7095"
|
||
title: "团期转订单:同产品其他期的子订单跨期转入本期"
|
||
consumer: "admin"
|
||
author: "jw(GIT)"
|
||
change_type: "新增接口"
|
||
backend_status: "deployed"
|
||
gateway_status: "verified"
|
||
frontend_status: "verified"
|
||
frontend_owner: "mmg"
|
||
frontend_ref: "cc07a40d"
|
||
target_release: ""
|
||
verified_at: "2026-09-08"
|
||
status_note: "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。"
|
||
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<TransferSubOrderRespVO>`
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| 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<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 | 源期出发日期 |
|
||
|
||
#### 请求示例
|
||
|
||
```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": <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](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
|