docs(changelog): #7284 / #7287 / #7294 团期三单交接 mmg(均已部署测试服并实测)
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
- #7284 团期下单校验改判实时库存:满员班期退单后能重新下单;满员时错误码由 581028 改为 581031 / 581034;产品域库存聚合失败改 fail-closed(下单返 581027,展示链路不变)。 前端待办:若对 581028 有特殊文案需改判;接口结构零变化。 - #7287 团期达门槛自动成团 + 未成团动作闸:新增自动成团定时任务(落地即暂停,需运营开启); 团期详情新增 formingRooms / formingPeople / thresholdSource 且两个门槛改取实时值; 班期表单与批量创建新增 minParticipants;未成团 / 未建团的资源动作拒 589552 / 589553。 前端三项待办:成团弹窗「已售」改读 formingRooms、班期表单加最低成团人数、补录两个错误码。 - #7294 流团对已付款户也取消并发起退款:审批单「实退金额」不再恒为 0,改为「已发起退款金额」; 新增 589554「团期下有出行中的子订单,不可流团」,发起与批复两个入口都会返。 前端待办:补录 589554,并把「实退金额」文案改为「已发起退款金额」。 三份均按 v2 模板逐接口八子节撰写,本地校验器 PASS。
这个提交包含在:
@@ -0,0 +1,373 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7284"
|
||||
title: "团期下单校验改判实时库存:满员班期退单后能重新下单,满员时错误码由 581028 改为 581031/581034"
|
||||
consumer: "multiple"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "not_required"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: "2026-09-08"
|
||||
status_note: "后端已部署测试服并逐条实测(AC-1~AC-9、AC-11~AC-15 全过,改前对照同批取证)。前端待办两项:①若对 581028「团期状态不允许报名」有特殊文案,需知悉满员场景已改由 581031/581034 承担;②错误码字典可选补录 581027 的新触发源(产品域库存聚合失败)。接口出入参结构零变化,前端不改也不会报错。"
|
||||
updated_at: "2026-09-08"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期: 下单校验改判实时库存 + 产品域库存聚合 fail-closed
|
||||
|
||||
> **服务**: hl-order-service-v3、hl-product-service-v2、hl-order-service-v2(**必须按序部署**: product-v2 → order-v3 → order-v2)
|
||||
> **PR**: #7330
|
||||
> **Issue**: #7284
|
||||
> **日期**: 2026-09-08
|
||||
> **影响范围**: GROUP 产品下单(管理后台代客下单 + 小程序下单 + order-v2 admin 创单)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**出入参结构一个字段都没改。** 前端不改不报错,但有两件事必须知道:
|
||||
|
||||
1. **「满员班期退单后卖不出去」这个 bug 消失了**——班期一旦被推成「已满额」,
|
||||
即使客户退单腾出真实空位,下单仍会被 **581028** 拒;现在改判实时库存,有余位就能下。
|
||||
小程序日历上那个「点进去能选、下单却报错」的分裂也随之消失。
|
||||
2. **真正满员时的错误码变了**:由 **581028**「团期状态不允许报名」改为
|
||||
**581031**「团期剩余房间不足」或 **581034**「团期剩余名额不足」。
|
||||
前端若对 581028 有特殊文案,需知悉该码现在只在「班期已取消 / 已结束 / 状态未知」时出现。
|
||||
|
||||
改前实测(AC-1,在部署本次改动**之前**于测试服复现):`maxRooms=2` 的班期下满 2 单后
|
||||
`batch_status=FULL`,退掉 1 单后同一次 `getBatchInfo` 响应里 `remainingRooms=1`(余量已自愈)
|
||||
而 `batchStatus` 仍是 `FULL`(死缓存不自愈),再下单返回 `code=581028`。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
GROUP 下单校验读的是产品域**持久列** `group_tour_batch.batch_status`,只放行「报名中 / 即将满额」。
|
||||
而这个持久列翻回「报名中」的唯一入口是产品服务的 `unenroll`,**order-v3 的普通取消链路
|
||||
(超时自动取消 / 退款 / admin 取消 / C 端取消)一个都不调它**;唯一能翻回去的 `enroll`
|
||||
又只在**创单成功后**才调——形成死结。
|
||||
|
||||
同时这是一个「展示与下单两套真相」的问题:小程序价格日历的可选性由**实时库存**算,
|
||||
所以退单后那一天在日历上是可选的,客户点进去下单才被 581028 拒。
|
||||
|
||||
第二件事:产品域 `GET /internal/product/batch/{batchId}/info` 的库存聚合原本在失败时
|
||||
**静默降级为「无活跃订单」**,返回偏大的剩余名额。本次去掉了下单闸对持久状态的依赖后,
|
||||
这两个余量数就是创单路径上**唯一**的超卖闸,故同单把它改成 fail-closed。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 管理后台代客下单 | POST | `/v3/admin/order` | 行为变化 | 满员判定改用实时库存;出入参与响应结构**不变**,错误码触发集合收窄 |
|
||||
| 2 | 小程序下单 | POST | `/v3/internal/mp/order/create` | 行为变化 | 同上 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 管理后台代客下单 `POST /v3/admin/order`
|
||||
|
||||
**VO**: `OrderCreateRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台定制师代客下单。调用方 hl-ui 订单创建页。GROUP 产品必须带 `productBatchId`。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productId | Body | Long | ✅ | — | 产品 ID;**入参一个都没变** |
|
||||
| productBatchId | Body | Long | GROUP 必填 | — | 产品侧班期 ID;为空抛 581026 |
|
||||
| tierSeq | Body | Integer | ✅ | — | 档位序号 |
|
||||
| departureDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 出发日期 |
|
||||
| adultCount / childCount / youngChildCount | Body | Integer | ✅ / ❌ / ❌ | ≥0 | 参与名额校验的三项(babyCount 不参与) |
|
||||
| roomCount | Body | Integer | ❌ | ≥0 | 房间(户)数,参与房间库存校验 |
|
||||
| customerName / customerPhone | Body | String | ✅ | — | 客户姓名 / 手机号 |
|
||||
|
||||
#### 出参 `Result<OrderCreateRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Long | 订单 ID;**响应结构完全不变** |
|
||||
| orderNo | String | 订单号 |
|
||||
| orderStatus | String | PENDING_PAY 等 |
|
||||
| 其余全部字段 | — | **完全不变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/admin/order HTTP/1.1
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"productId": 2056947670512971778,
|
||||
"productBatchId": 2097208296820678658,
|
||||
"tierSeq": 1,
|
||||
"departureDate": "2026-11-07",
|
||||
"adultCount": 2,
|
||||
"childCount": 0,
|
||||
"roomCount": 1,
|
||||
"customerName": "张三",
|
||||
"customerPhone": "13800002043"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "2097211100872290306",
|
||||
"orderNo": "HL20260908143105038",
|
||||
"orderStatus": "PENDING_PAY",
|
||||
"orderStatusName": "待支付"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上面这一单下在了一个 `batch_status=FULL`、`remainingRooms=1` 的班期上——**改前它会被 581028 拒**。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
**产品域库存聚合失败时本接口 fail-closed 拒单**,返回 581027,不再放行。
|
||||
改前是静默按「无活跃订单」降级、返回偏大的剩余名额从而放行,属超卖风险。
|
||||
展示链路(小程序团期列表 / 详情、管理后台班期列表 / 价格日历)**保持原有降级不变**,
|
||||
聚合失败时照常 200 返回,不会整片报错。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581027,
|
||||
"message": "团期信息获取失败",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581031,
|
||||
"message": "团期剩余房间不足",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**错误码集合无新增**,但**触发条件变了**:
|
||||
|
||||
| 码 | 含义 | 变化 |
|
||||
|----|------|------|
|
||||
| 581028 | 团期状态不允许报名 | **触发集收窄**:只在班期「取消中 / 已取消 / 已结束 / 状态未知」时出现。满员不再走这个码 |
|
||||
| 581031 | 团期剩余房间不足 | **新增触发场景**:真正满员(房间维度)由它承担 |
|
||||
| 581034 | 团期剩余名额不足 | **新增触发场景**:真正满员(人数维度)由它承担 |
|
||||
| 581027 | 团期信息获取失败 | **新增触发源**:产品域库存聚合失败(fail-closed) |
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **未知状态一律拒单**:班期状态为空或不在字典内时按 581028 拒。产品侧将来新增状态值而订单侧
|
||||
镜像未同步时,应当暴露成拒单而不是静默放行——跨服务枚举漂移不该由客户承担。
|
||||
- **满员仍然拒得住**:改的只是「用哪个数判满」,不是「不判满」。实测真满员时返 581031。
|
||||
- **产品域仍会继续写「已满额」持久值**,只是不再有人拿它做下单闸门;管理后台班期列表上
|
||||
该状态照常显示,前端无需改动。
|
||||
- **并发超卖窗口客观存在**:本次不做 TCC 强一致,创单入口仍无按班期的分布式锁,
|
||||
预查是 TOCTOU 不可靠防护——这是既有限制,本次未改变。
|
||||
|
||||
### 2. 小程序下单 `POST /v3/internal/mp/order/create`
|
||||
|
||||
**VO**: `MpOrderDetailVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
小程序下单,经 mp BFF 转发到 order-v3。与管理后台代客下单共用同一套 GROUP 校验逻辑。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productId | Body | Long | ✅ | — | 产品 ID;**入参一个都没变** |
|
||||
| productBatchId | Body | Long | GROUP 必填 | — | 产品侧班期 ID |
|
||||
| tierSeq | Body | Integer | ✅ | — | 档位序号 |
|
||||
| departureDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 出发日期 |
|
||||
| adultCount / childCount / youngChildCount | Body | Integer | ✅ / ❌ / ❌ | ≥0 | 参与名额校验的三项 |
|
||||
| roomCount | Body | Integer | ❌ | ≥0 | 房间(户)数 |
|
||||
|
||||
#### 出参 `Result<MpOrderDetailVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| orderId | Long | 订单 ID;**响应结构完全不变** |
|
||||
| orderNo | String | 订单号 |
|
||||
| orderStatus | String | 订单状态 |
|
||||
| 其余全部字段 | — | **完全不变** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
POST /v3/internal/mp/order/create HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"productId": 2056947670512971778,
|
||||
"productBatchId": 2097208296820678658,
|
||||
"tierSeq": 1,
|
||||
"departureDate": "2026-11-07",
|
||||
"adultCount": 2,
|
||||
"roomCount": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"success": true,
|
||||
"data": { "orderId": "2097211100872290306", "orderNo": "HL20260908143105038", "orderStatus": "PENDING_PAY" }
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
同管理后台下单:产品域库存聚合失败时返 581027 拒单;展示链路不受影响,
|
||||
价格日历与班期列表在同一场景下仍正常 200 返回。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581027,
|
||||
"message": "团期信息获取失败",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 581034,
|
||||
"message": "团期剩余名额不足",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
错误码变化同管理后台下单那张表。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **「日历可选、下单报错」这个分裂消失**:日历可选性本来就按实时库存算,现在下单闸也按实时库存判,
|
||||
两边终于是同一个真相。
|
||||
- **小程序侧无需改代码**:出入参与响应结构零变化;只是原本会拿到 581028 的场景,
|
||||
现在要么下单成功,要么拿到 581031 / 581034。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **不要再把 581028 当作「满员」的信号**。满员请判 581031 / 581034。
|
||||
581028 从此只表示「这个班期已取消 / 已结束 / 状态异常」,属于不可恢复的拒绝,
|
||||
提示客户换一期,而不是提示「稍后再试」。
|
||||
- **581027 是可重试的**:它表示产品域暂时算不出库存,客户稍后重试通常会成功。
|
||||
- 班期列表 / 价格日历的可选性判定**不要改**:它们本来就读实时库存,与下单闸现在一致。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
- 本次**无表结构变更、无数据迁移**。
|
||||
- 产品域班期的「已满额」持久值仍照常写入,只是不再参与下单判定。
|
||||
- 订单创建、名额计数的写入行为一字未改。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **班期不属于该产品**:仍返 581055,未变。
|
||||
- **已过报名截止日 / 已出发**:仍分别返 581029 / 581030,未变。
|
||||
- **未选班期**:GROUP 产品未传 `productBatchId` 仍返 581026,未变。
|
||||
- **零元班期 / 不限名额班期**:`maxRooms` 或 `maxParticipants` 为 0/空表示不限,
|
||||
对应的余量为 null,跳过该维度校验,行为未变。
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 场景 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 班期已满员被推成「已满额」,客户退单腾出真实空位后再下单 | **581028 拒单**,且永远解不开(取消链路不回写产品域状态) | **200 创单成功** |
|
||||
| 班期真正满员(剩余房间为 0)再下单 | 581028「团期状态不允许报名」 | **581031「团期剩余房间不足」** |
|
||||
| 班期真正满员(剩余名额为 0)再下单 | 581028 | **581034「团期剩余名额不足」** |
|
||||
| 班期已取消 / 取消中 / 已结束 | 581028 | 581028(**不变**) |
|
||||
| 班期状态为空或不在字典内 | 581028 | 581028(**不变**,fail-closed) |
|
||||
| 产品域库存聚合失败时下单 | 按「无活跃订单」降级,**放行**(超卖风险) | **581027 拒单**(fail-closed) |
|
||||
| 产品域库存聚合失败时看班期列表 / 价格日历 | 正常 200 | 正常 200(**不变**,展示链路保持 fail-open) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **前端零改动**:两个下单接口的出入参与响应结构完全不变。
|
||||
- **需要前端知悉的一点**:若对 581028 做过「团期状态不允许报名」的特殊文案或埋点,
|
||||
满员场景现在走 581031 / 581034,文案分支需相应调整(不调也只是提示词不够精准,不会报错)。
|
||||
- **对客户的净效果**:原本会被莫名其妙拒掉的下单现在能成功;原本模糊的「团期状态不允许报名」
|
||||
换成了明确的「剩余房间/名额不足」。
|
||||
- **对运营的净效果**:不再需要靠「调整满团名额」或「去产品后台编辑一次班期」来玄学解锁班期。
|
||||
- **风险**:产品域库存聚合失败期间 GROUP 下单会整体拒单(581027)。这是刻意的取舍——
|
||||
宁可拒单,不可超卖。该失败本身是产品域 → 订单域的内部调用异常,正常情况下不发生。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- 非 GROUP 产品(自由行 / 定制)的下单校验**完全未触碰**。
|
||||
- 订单取消、退款、转期链路未改。
|
||||
- 产品域班期的展示态计算(`GroupBatchDisplayStatusResolver`)未改。
|
||||
- 团期名额计数与对账 Job 未改。
|
||||
- 管理后台「调整满团名额」的校验规则未改。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
部署次序(硬约束):**product-v2 14:27:25 DONE → order-v3 14:28:43 STEP1 → order-v2 14:30:14 DONE**,
|
||||
三服务同一 commit。回滚反序(先 order-v3 后 product-v2)也已实测走通。
|
||||
|
||||
| 验证项 | 结果 |
|
||||
|--------|------|
|
||||
| 满员班期退单后再下单(改前 → 改后) | **581028 → 200 创单成功** |
|
||||
| 下满即查产品域状态 | 仍「报名中」——填满班期的那一单不会把状态推成「已满额」 |
|
||||
| 真满员再下单 | **581031「团期剩余房间不足」** |
|
||||
| 班期「已取消」/「取消中」/「已结束」 | 均 **581028** |
|
||||
| 班期状态置为字典外的值 | **581028**(fail-closed) |
|
||||
| 产品域库存聚合失败时 `getBatchInfo` | **480203**(产品域内部码,订单侧翻成 581027) |
|
||||
| 同场景下 order-v2 admin 创单 | **503102** 拒单(锁内库存硬守卫不再拿到虚高余量) |
|
||||
| 同场景下团期转期 | **200 成功**,`warnings` 含「目标团期行程天数未取到,订单沿用原天数,请人工核对」 |
|
||||
| 同场景下展示链路 | 小程序班期列表 / 产品详情、管理后台班期列表 / 价格日历**全部仍 200** |
|
||||
| 单元测试 | product-v2 1606 例、order-v3 9048 例,均 0 失败 0 错误 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue #7284、PR #7330
|
||||
- 关联工单:#7283(流团后班期置为不可售,本单的前置)、#7293(库存聚合失败不再持久改写班期状态)
|
||||
- 设计文档校正:SRS 与 HOUSE 详设中「订单取消时主 API 调 product unenroll」的描述与代码不符,
|
||||
已在本次一并标注校正(order-v3 的取消链路从不调用该端点)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端:mmg(hl-ui / 小程序)
|
||||
- 前端待办:①若对 581028 有特殊文案,满员场景改判 581031 / 581034;
|
||||
②可选补录 581027 的新触发源说明。**接口结构零变化,不改也不会报错。**
|
||||
在新工单中引用
屏蔽一个用户