文件
hl-api-changelog/changelogs-v2/2026-09/06_7135_团期创单收紧productBatchId校验-修改接口-管理后台.md
T
2026-09-06 16:33:44 +08:00

352 行
19 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "7135"
title: "团期创单收紧 productBatchId 校验(班期归属产品 / 非 GROUP 拒收 / tierSeq 存在性 / 日期按班期)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "03a985b6"
target_release: ""
verified_at: "2026-09-06"
status_note: "服务端对 POST /v3/admin/order 新增三个校验错误码 581055/581056/581057,响应 departureDate/returnDate 改为落库值;前端新建订单向导须仅对 GROUP 产品传 productBatchId 且班期必须属于所选产品。"
updated_at: "2026-09-06"
base: "dev-v3"
---
# 订单模块:团期创单校验收紧(班期归属产品 / 非 GROUP 拒收 / tierSeq 存在性 / 日期按班期)
管理后台创单接口 `POST /v3/admin/order` 对 GROUP 产品的团期校验收紧,新增三个拒单错误码(581055/581056/581057),同时出发日期、返团日期改为班期的权威值。修复跨产品班期串号导致订单错误归团、CORE/CUSTOM 单被误命中团期逻辑等问题。
## ⚠️ 关键变化
- **仅 GROUP 产品可传 productBatchId**:CORE/CUSTOM 请求含 productBatchId → 拒单 581056「非团期产品不能指定团期」(改前静默落库并误命中团期分支)。
- **班期必须属于所选产品**:productBatchId 对应班期的 productId ≠ 请求 productId → 拒单 581055「所选团期不属于该产品」(改前会按别家班期计价并挂到别家的团)。
- **tierSeq 必须在产品配置内**(全产品类型):不在 tierPrices ∪ tiers 并集中 → 拒单 581057「所选档位不存在」;产品未配档位时不拦(改前 tier_name 落 NULL)。
- **响应 departureDate/returnDate 改为班期权威值**:GROUP 单出发日 = 班期出发日;返团日 = 班期 endDate,或 班期出发日 + 行程天数 − 1(班期无 endDate 时)。改前响应回显请求日期,落库日期与班期脱钩。
- **GROUP 单响应 tierName 现有值**:来自产品 tiers 配置,改前恒 null。
- **人数超班期剩余名额拒单(581034)**(#7159 并入本 PR):`成人+儿童+小童 > 班期剩余名额` → 581034;此前订单侧读的 `remainingSlots` 字段在产品侧「库存改造 Phase 2」后已无来源、恒 null,致该预查长期 no-op,现改读产品侧权威字段 `remainingParticipants`(null=人数不限,跳过校验)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 创建订单(管理端) | POST | `/v3/admin/order` | 请求新增校验 / 响应日期改值 | productBatchId 仅 GROUP 可传;班期归属;tierSeq 存在性;日期按班期 |
---
## 三、接口详情
### 1. 创建订单(管理端) `POST /v3/admin/order`
**VO**: `OrderCreateReqVO → OrderCreateRespVO`
#### 使用场景
管理端新建订单向导或团期看板新增子订单调用。创建跟团游、定制游、线路游订单,GROUP 产品必须指定班期,CORE/CUSTOM 产品不允许指定班期。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `productId` | Body | Long(String)| ✅ | 正整数 | 产品 ID |
| `tierSeq` | Body | Integer | ✅ | ≥ 1;必须存在于产品配置档位 | 档位序号,不在产品 tierPrices ∪ tiers 配置内 → 581057 |
| `departureDate` | Body | yyyy-MM-dd | ✅ | 不早于当天;日期格式 | 出发日期;出发日早于今天 → 581011 |
| `adultCount` | Body | Integer | ✅ | ≥ 1 | 成人数 |
| `childCount` | Body | Integer | 否 | ≥ 0,默认 0 | 儿童数(5-12 岁) |
| `youngChildCount` | Body | Integer | 否 | ≥ 0,默认 0 | 小童数(3-4 岁) |
| `babyCount` | Body | Integer | 否 | ≥ 0,默认 0 | 婴儿数(0-2 岁) |
| `customerName` | Body | String | ✅ | @NotBlank | 客户姓名 |
| `customerPhone` | Body | String | ✅ | ^1[3-9]\d{9}$ | 客户手机号(明文传,DB 层 AES 加密) |
| `customerRemark` | Body | String | 否 | ≤ 500 | 客户备注 |
| `createSource` | Body | String | 否 | ≤ 20 字;枚举:CUSTOMER / CONSULTANT / OTA / WALK_IN / B2B / VIP_REPURCHASE / REFERRAL / PROMOTION / INTERNAL;默认 CONSULTANT | 创建来源 |
| `productBatchId` | Body | Long(String) | 条件必填 | 仅 GROUP 产品可传;CORE/CUSTOM 传了 → 581056;班期 productId 必须等于请求 productId,否则 → 581055 | 团期 ID(product 侧班期 batchId);**GROUP 产品必传,CORE/CUSTOM/ROUTE 禁传**;值须属于请求 productId 对应产品 |
| `roomCount` | Body | Integer | 否 | ≥ 1 | 房间数 |
| `tags` | Body | List<String> | 否 | 无约束 | 订单标签名列表 |
| `sharerOpenid` | Body | String | 否 | 无约束 | 分享人 openid(C 端裂变追踪) |
| `customizerId` | Body | Long | 否 | 无约束 | 分享归因定制师 ID |
#### 出参 `Result<OrderCreateRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `data.id` | Long | 订单主键(雪花 ID 序列化为 String) |
| `data.orderNo` | String | 订单号(创单瞬间生成,永不变) |
| `data.orderStatus` | String | 粗状态枚举(创单后 = PENDING_PAY) |
| `data.orderStatusName` | String | 粗状态中文名(创单后 = 待支付) |
| `data.flowStatus` | String | 细状态枚举值(创单后 = AWAITING_PAY) |
| `data.flowStatusName` | String | 细状态中文名 |
| `data.flowStep` | Integer | 线性 6 步当前步序号(创单初态 = 0) |
| `data.flowStepTotal` | Integer | 线性 6 步总步数(固定 6) |
| `data.flowStepName` | String | 线性 6 步当前步中文名 |
| `data.consultantId` | Long | 实际绑定定制师 ID(序列化为 String) |
| `data.consultantSource` | String | 定制师来源(DEFAULT_ASSIGNED / LINK_BOUND / MANUAL / SHARED) |
| `data.tags` | List<String> | 系统自动打的标签 |
| `data.createdAt` | yyyy-MM-ddTHH:mm:ss | 创单时间 |
| `data.productName` | String | 产品名称 |
| `data.tierName` | String | 档位名(v5.18 新增,**GROUP 单现有值来自产品 tiers 配置,CORE/CUSTOM/ROUTE 仍为 null**) |
| `data.groupBatchName` | String | 拼团批次名(仅供兼容,恒 null,勿读) |
| `data.departureDate` | yyyy-MM-dd | **出发日期(v5.18;改后 = 班期权威出发日,与请求值可能不同)** |
| `data.returnDate` | yyyy-MM-dd | **返团日期(v5.18;改后 = 班期 endDate 或班期出发日 + 行程天数 − 1,与请求值可能不同)** |
| `data.totalAmount` | BigDecimal(String) | 订单总价(元) |
| `data.depositAmount` | BigDecimal(String) | 建议定金金额(元) |
| `data.depositRatio` | Integer | 定金比例百分比;RATIO 模式有值,FIXED 模式为 null,FULL 模式 = 100 |
| `data.depositMode` | String | 定金计算模式(FIXED / RATIO / FULL) |
| `data.paymentMode` | String | 支付模式(DEPOSIT / FULL) |
| `data.expiryMinutes` | Integer | 支付时限分钟数(默认 1440 = 24h) |
| `data.payUrl` | String | 支付页绝对 URL |
| `data.customerName` | String | 客户姓名(回显) |
| `data.groupBatchId` | Long(String) | **运营团期 ID(order 侧),非空 = 团订单,为空 = 普通订单,这是唯一判别** |
| `data.productBatchId` | Long(String) | 团期产品排期 ID(product 侧 batchId,仅供溯源) |
| `data.groupOrder` | Boolean | 是否团订单(= groupBatchId 非空的派生值) |
#### 请求示例
```json
{
"productId": "2044306857534636034",
"tierSeq": 1,
"departureDate": "2026-10-01",
"adultCount": 1,
"childCount": 1,
"youngChildCount": 0,
"babyCount": 0,
"customerName": "张三",
"customerPhone": "13800009601",
"createSource": "CONSULTANT",
"productBatchId": "2052935476557328386"
}
```
#### 响应示例(成功)
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2096414445365338113",
"orderNo": "HL20260906094527867",
"orderStatus": "PENDING_PAY",
"orderStatusName": "待支付",
"flowStatus": "AWAITING_PAY",
"flowStatusName": "待支付",
"flowStep": 0,
"flowStepTotal": 6,
"flowStepName": "待支付",
"consultantId": "50001234567890",
"consultantSource": "DEFAULT_ASSIGNED",
"tags": [],
"createdAt": "2026-09-06T09:45:27",
"productName": "冻干粉发短信给",
"tierName": "标准档",
"groupBatchName": null,
"departureDate": "2026-10-01",
"returnDate": "2026-10-03",
"totalAmount": "5850.00",
"depositAmount": "1000.00",
"depositRatio": null,
"depositMode": "FIXED",
"paymentMode": "DEPOSIT",
"expiryMinutes": 1440,
"payUrl": "https://pay.hulalv.com/pay/HL20260906094527867",
"customerName": "张三",
"groupBatchId": "2096412454643802114",
"productBatchId": "2052935476557328386",
"groupOrder": true
},
"success": true
}
```
#### 空数据 / 降级响应
创建接口无「空数据」场景(成功即返回订单对象)。降级路径:所依赖的产品服务 Feign 不可用时按错误码降级而非返回空——拉班期失败返 581027、拉报价失败返 581032,前端据 `code` 提示重试,不会返回 `data=null` 的成功包。
#### 错误响应
**出发日期早于今天**
```json
{
"code": 581011,
"message": "出发日期不能早于今天",
"data": null,
"success": false
}
```
**档位不存在(tierSeq 未在产品配置内)**
```json
{
"code": 581057,
"message": "所选档位不存在",
"data": null,
"success": false
}
```
**非团期产品不能指定团期**
```json
{
"code": 581056,
"message": "非团期产品不能指定团期",
"data": null,
"success": false
}
```
**所选团期不属于该产品**
```json
{
"code": 581055,
"message": "所选团期不属于该产品",
"data": null,
"success": false
}
```
**既有 GROUP 校验错误(团期缺失)**
```json
{
"code": 581026,
"message": "团期产品必须选择团期",
"data": null,
"success": false
}
```
**参数校验错误(如手机号格式错误)**
```json
{
"code": 400,
"message": "手机号格式错误",
"data": null,
"success": false
}
```
#### 业务边界
- **productBatchId 传值规则**:仅 GROUP 产品可传且必传(除非为空表示不下单);CORE/CUSTOM/ROUTE 产品绝不能带。
- **班期归属校验**:productBatchId 对应班期的 productId 必须等于请求 productId;不同则拒单 581055,不会创建订单或部分落库。
- **tierSeq 校验**:必须存在于产品的 tierPrices JSON(CORE/CUSTOM/ROUTE)或 tiers JSON(GROUP);产品未配任何档位时保持放行(存量产品兼容)。
- **出发日期与返团日期**:响应值为班期或产品的权威日期,可能与请求值不同。前端展示及后续行程渲染必须以响应值为准,不要缓存请求值。
- **错误码优先级顺序**:出发日期 581011 → 非 GROUP 拒收 581056 → tierSeq 581057 → GROUP 缺 productBatchId 581026 → 拉班期 581027 → 班期归属 581055 → 后续班期状态 / 截止 / 库存检查。
- **鉴权**:网关注入 `X-Admin-Id` 头,consultantId 无法解析返 581013。
- **幂等性**:客户端不得重试;同一 orderNo 存量幂等托管由 order-v3 内核保证。
---
## 四、契约约束与正确调用方式
| 场景 | 正确调用 | 错误调用 | 结果 |
|------|---------|---------|------|
| GROUP 产品创单 | 传 productBatchId,值取价格日历 items[].batchId | 不传 productBatchId | 581026 团期产品必须选择团期 |
| CORE 产品创单 | 不传 productBatchId / productBatchId = null | 传 productBatchId(任何值) | 581056 非团期产品不能指定团期 |
| CUSTOM 产品创单 | 不传 productBatchId / productBatchId = null | 传 productBatchId(任何值) | 581056 非团期产品不能指定团期 |
| 档位选择 | tierSeq 必须在产品已配档位内 | tierSeq 超过产品最大档位序号 | 581057 所选档位不存在 |
| 班期串号防护 | 班期 batchId 必须属于所选 productId | 前端用不同产品的 batchId | 581055 所选团期不属于该产品 |
| 日期展示 | 使用响应 departureDate / returnDate | 使用请求的出发日期 | 行程日期与班期脱钩,退款档位错位 |
---
## 五、数据库行为
| 操作 | 落库字段 | 说明 |
|------|---------|------|
| GROUP 创单(班期出发 2026-10-01,endDate 2026-10-03,tripDays=3) | order_main.depart_date = 2026-10-01(班期值) | 请求可能为 2026-10-02,但落库为班期出发日 |
| GROUP 创单(班期无 endDate) | order_main.return_date = 班期出发日 + tripDays - 1 | 如班期 2026-10-01,tripDays=3,则 return_date=2026-10-03 |
| GROUP 创单(班期有 endDate) | order_main.return_date = 班期 endDate | endDate 为准,与 tripDays 无关 |
| GROUP 创单 | order_main.tier_name = 产品 tiers JSON 对应 tierSeq 的值 | 改前恒 null;CORE/CUSTOM 仍为 null(无 tiers JSON) |
| CORE 创单带 productBatchId | 不创建,拒单 581056 | 改前会落库 productBatchId,误命中团期逻辑 |
| 班期串号 productBatchId 错 | 不创建,拒单 581055 | 改前会按班期产品计价,订单挂错团 |
---
## 六、边界行为
- 业务失败仍 HTTP 200,**必须检查 `code` 字段判成败**。
- 存量订单不回溯修正;仅新建订单应用新规则。
- 产品未配档位时保持放行(向后兼容存量产品);即使 tierSeq 传 99 也不拦。
- 班期 productId 缺失时仅告警日志,不拦单(product 侧老数据未回填,宁缺毋滥)。
- 网关注入 `X-Admin-Id` 为 null 时返 581013,改前静默取 JWT;两者互斥。
---
## 六.6、修改前后对比
针对 `POST /v3/admin/order`(GROUP 产品):
| 维度 | 修改前 | 修改后 |
|------|--------|--------|
| CORE/CUSTOM 带 productBatchId | 静默落库 product_batch_id,误命中团期 staff 扇出分支 | 拒单 581056「非团期产品不能指定团期」,不落库 |
| 跨产品班期(batchId 属别的产品) | 按别家班期计价,订单挂错团 | 拒单 581055「所选团期不属于该产品」(任一侧 productId 为空时仅告警放行) |
| tierSeq 不在产品档位内 | 不校验,tier_name 落 NULL | 拒单 581057「所选档位不存在」(产品未配档位时不拦) |
| GROUP 单响应/落库出发日 | 回显请求出发日,可能与班期脱钩 | = 班期出发日 |
| GROUP 单响应/落库返团日 | 按请求出发日推算 | = 班期 endDate(缺则 班期出发日 + 行程天数 − 1) |
| GROUP 单响应 tierName | 恒 null | 现有值(来自产品 tiers 配置) |
| 人数超班期剩余名额(#7159) | 预查读 remainingSlots 恒 null,整段 no-op(不拦) | 读 remainingParticipants,超额拒单 581034(人数不限的班期跳过) |
## 六.7、影响评估
- **前端必改**:新建订单向导与团期看板「新增子订单」仅对 GROUP 产品传 `productBatchId`,且取该产品价格日历的 `items[].batchId`;CORE/CUSTOM 绝不能带,否则 581056。
- **前端展示**:出发日期 / 返团日期以创单响应值为准(GROUP 单被班期覆盖),不要回显请求值。
- **向后兼容**:正常 CORE/CUSTOM 创单(不带 productBatchId)行为完全不变;错误码新增不影响既有成功路径。
- **其他端**:小程序 `POST /v3/mp/order` 共用内核,四个校验同样生效,请求契约不变。
- **数据安全**:跨产品串号单此前会把订单挂到别家团、扣错名额,本次从源头拒绝。
- **回滚**:回滚本次发布即恢复旧行为;已按新规则创建的订单不受影响。
## 七、不影响范围
- **仅影响**:管理后台「新建订单向导」和「团期看板新增子订单」流程。
- **零影响**:
- 小程序端 `POST /v3/mp/order` 共用内核,新校验同样生效但请求契约不变。
- 表结构:无 Flyway 变更、无新列新表。
- 订单列表、详情、订单编辑等读操作。
- CORE/CUSTOM 正常创单(不带 productBatchId)完全不变。
- 团期相关接口(看板、价格日历、班期详情)。
---
## 八、测试环境已验证
**测试服创单(产品「冻干粉发短信给」productId=2044306857534636034,班期 2026-10-01 productBatchId=2052935476557328386,1 成人 1 儿童,2026-09-06 测试)**
- ✓ GROUP 单正例:响应 departureDate/returnDate 与班期一致(班期 2026-10-01 出发、2026-10-03 返);groupBatchId / productBatchId / groupOrder 三字段已填值。
- ✓ CORE 产品 2056944461216100353 带 productBatchId → HTTP 200,code 581056「非团期产品不能指定团期」。
- ✓ productId=2044306857534636034 + productBatchId=2089667212070612995(属另一产品) → HTTP 200,code 581055「所选团期不属于该产品」。
- ✓ tierSeq=9(产品只配 1-3 档) → HTTP 200,code 581057「所选档位不存在」。
- ✓ GROUP 单不传 productBatchId → HTTP 200,code 581026「团期产品必须选择团期」。
**单测覆盖**(定向复测全绿):OrderServiceTest 193/193 ✓ | GroupOrderStrategyTest 30/30 ✓(含 581034 三例)| OrderCreateTransactionExecutorTest 21/21 ✓ | ProductTierResolverTest 11/11 ✓ | OrderMpCreateServiceTest 6/6 ✓ | BatchInfoVODeserializationTest 2/2 ✓ | OrderMpReadServiceTest 15/15 ✓ | InternalOrderSnapshotControllerTest 4/4 ✓ | E2eScopedOrderCreateServiceTest 19/19 ✓;ArchTest 全绿(LayerEnforcement 5 / RedLine 9 / MapperBoundary 26 / HouseModuleBoundary 4 / DashboardLayer 2 / LocalCacheVetting 1)。
**网关验证(已部署 dev-v3 复测,2026-09-06 15:00)**:合并提交 977be08e 部署测试服,双实例滚动重启完成(8086/8186 新 PID、jar 已更新)。网关实测 15/15 通过:S2 正例 200(日期=班期 10-01/10-03)、S10 跨产品班期 581055、S11 日期不一致回显班期日期、S13 tierSeq=9 581057、S14 CORE 带班期 581056、S16 GROUP tierName=轻奢;#7142 判团字段在创单响应/详情/列表三处透出、#7143 productId 在看板/分页/详情三接口透出,均已核。581034(#7159)判定逻辑经三条单测证实生效;线上端到端受测试账号对产品 schedule 无编辑权限(403)与限额班期 getBatchInfo 存量异常(581027)所限未实跑,详见 Issue #7159 评论。
---
## 十、相关文档
- 关联 Issue: [wx/HL#7135](https://git.1814.love:8443/wx/HL/issues/7135)
- 关联 PR: [wx/HL#7169](https://git.1814.love:8443/wx/HL/pulls/7169)(Closes #7135、#7159;合并提交 977be08e)
- 关联工单 #7142(判团字段 groupBatchId / productBatchId / groupOrder)、#7143(看板 productId 显示)。
- 团期接口文档:GB-ADM-00B(OpenWiki)。
---
## 关联 / 联系人
### 链接
- **Issue**: [#7135](https://git.1814.love:8443/wx/HL/issues/7135)
- **PR**: [#7169](https://git.1814.love:8443/wx/HL/pulls/7169)(含 #7159)
- **Merge commit**: 977be08eccd0a32e27c01dce41de210a53f13e5d
### 联系人
- **后端负责人**: @wx