diff --git a/changelogs-v2/2026-09/08_7284_团期下单校验改判实时库存与库存聚合failclosed-修改接口-管理后台.md b/changelogs-v2/2026-09/08_7284_团期下单校验改判实时库存与库存聚合failclosed-修改接口-管理后台.md new file mode 100644 index 00000000..8e97bc5b --- /dev/null +++ b/changelogs-v2/2026-09/08_7284_团期下单校验改判实时库存与库存聚合failclosed-修改接口-管理后台.md @@ -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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | Long | 订单 ID;**响应结构完全不变** | +| orderNo | String | 订单号 | +| orderStatus | String | PENDING_PAY 等 | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +POST /v3/admin/order HTTP/1.1 +Authorization: Bearer +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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| 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 的新触发源说明。**接口结构零变化,不改也不会报错。** diff --git a/changelogs-v2/2026-09/08_7287_团期达门槛自动成团与未成团动作闸-修改接口-管理后台.md b/changelogs-v2/2026-09/08_7287_团期达门槛自动成团与未成团动作闸-修改接口-管理后台.md new file mode 100644 index 00000000..9c0938fd --- /dev/null +++ b/changelogs-v2/2026-09/08_7287_团期达门槛自动成团与未成团动作闸-修改接口-管理后台.md @@ -0,0 +1,722 @@ +--- +schema: "hl-changelog/v2" +ticket: "7287" +title: "团期达门槛自动成团 + 未成团动作闸 589552/589553;团期详情新增成团口径三件套并改取实时门槛" +consumer: "multiple" +author: "jw(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-08" +status_note: "后端已部署测试服并逐条实测(TEST 实测 26 条 AC 通过、4 条部分、本地 7 条;AC-12/AC-25/AC-28c 及 AC-23 的 resume 未跑)。前端有三项待办:①团期详情新增 formingRooms/formingPeople/thresholdSource 三个字段,成团弹窗的「已售」须改读 formingRooms;②班期表单新增最低成团人数 minParticipants;③错误码字典补录 589552/589553。定时任务落地即暂停,需运营在用户中心手动开启。" +updated_at: "2026-09-08" +base: "dev-v3" +--- + +# 团期: 达门槛自动成团 + 未成团动作闸 + 门槛链路实时化 + +> **服务**: hl-order-service-v3、hl-product-service-v2、hl-user-service(**必须一起部署**,次序 product-v2 → order-v3 → user-service) +> **PR**: #7307 +> **Issue**: #7287 +> **日期**: 2026-09-08 +> **影响范围**: 团期运营台成团、团期详情、班期维护(新增/编辑/批量创建)、房务与车务与导摄的资源动作入口 + +--- + +## ⚠️ 关键变化 + +**已有接口的出入参没有删改,只有新增字段。** 但有四件事必须知道: + +1. **系统会自动成团了**。达到最低成团户数**或**最低成团人数任一门槛的团期, + 由定时任务每 5 分钟自动推进到「资源准备中」。**任务落地即暂停**,需运营在用户中心手动开启。 +2. **团期详情新增三个字段**:`formingRooms` / `formingPeople` / `thresholdSource`。 + ⚠️ **成团弹窗的「已售」必须改读 `formingRooms`**,不能继续读 `enrolledRooms`—— + 两者口径不同(见下方对照表),继续读旧字段会出现「弹窗显示未达标、系统却自动成团了」。 +3. **未成团不能做资源动作了**:招募中的团期调派房务 / 派车务 / 配导摄 / 设报账人一律拒绝, + 新增错误码 **589552**(团期尚未成团)与 **589553**(团期尚未创建)。 +4. **班期表单要加一个字段**:`minParticipants`(最低成团人数)。 + ⚠️ **编辑班期时必须把两个门槛都回传**,否则会被清零——这是本次修掉的一个既有缺陷, + 但前端表单若不加这个字段,用户就没法设置人数门槛。 + +--- + +## 一、背景 + +现在团期无论卖到多少户都停在「招募中」,必须有人盯着看板手动点成团; +而成团是后面所有资源动作的起点(派房务、配车、配导摄、出合同保险、备物料), +漏点一次整条链路就停摆。 + +另一头是相反的风险:团还没成,房务 / 车务 / 导摄配置就能被提前发起, +资源成本先于成团发生,团若流掉这些成本收不回。 + +同时门槛链路本身有两个洞:**最低成团人数在产品域根本没有落库映射**; +**团期详情读的是建团那天的冻结快照**——产品域把门槛从 6 改成 3 后, +系统按实时值 3 判定成团(判定是对的),弹窗却显示「满 6 户 · 未达标」, +成团流水还会落库一条「未达标(满6户)」的错误审计记录。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 团期详情 | GET | `/v3/admin/order/group-batch/:groupBatchId` | 新增字段 | 新增 formingRooms / formingPeople / thresholdSource;minToForm、minGroupPeople 改取实时值 | +| 2 | 新增/编辑班期 | PUT | `/admin/product/item/:id/schedule` | 新增字段 | 新增 minParticipants;两个门槛改为「传了才覆盖、没传保留旧值」 | +| 3 | 批量创建班期 | POST | `/admin/product/item/:id/schedule/batch-create` | 新增字段 | 新增 minParticipants,逐期落库 | +| 4 | 班期列表 | GET | `/admin/product/item/:id/schedule/list` | 新增字段 | 响应新增 minParticipants 回显 | +| 5 | 逐单提交房务需求 | POST | `/v3/admin/order/:id/hotel-requirement/dispatch` | 行为变化 | 未成团拒 589552 / 未建团拒 589553 | +| 6 | 逐单提交车务需求 | POST | `/v3/admin/order/:id/vehicle-requirement/dispatch` | 行为变化 | 同上 | +| 7 | 团期导摄配置保存 | PUT | `/v3/admin/group-batch/:productBatchId/staff` | 行为变化 | 同上 | + +--- + +## 三、接口详情 + +### 1. 团期详情 `GET /v3/admin/order/group-batch/:groupBatchId` + +**VO**: `GroupBatchDetailRespVO` + +#### 使用场景 + +管理后台团期详情页 / 成团弹窗。调用方 hl-ui 团期运营台。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | — | 团期主订单 ID;**入参一个都没变** | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| formingRooms | Integer | **新增**。成团判定用户数 = 线上**已付款**活跃订单数 + 产品域线下占位。**成团弹窗的「已售」请读这个** | +| formingPeople | Integer | **新增**。成团判定用人数 = 线上已付款活跃订单总人数 + 产品域线下报名人数 | +| thresholdSource | String | **新增**。本次响应里两个门槛的来源:`PRODUCT_REALTIME`(产品域实时值,正常)/ `SNAPSHOT_FALLBACK`(产品域暂不可达,回退建团快照) | +| minToForm | Integer | **口径变化**:改为产品域**实时值**(取不到才回退快照)。0/null = 不限 | +| minGroupPeople | Integer | **口径变化**:历史上恒为 null 的字段,现已启用,含义是「最低成团人数」,与 minToForm 对称 | +| enrolledRooms / enrolledPeople | Integer | **未变**,仍是库存口径(含未付款、不含线下占位)。⚠️ 与 formingRooms/formingPeople 不是一回事 | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2097209881592266753 HTTP/1.1 +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "groupBatchId": "2097209881592266753", + "minToForm": 3, + "minGroupPeople": 3, + "formingRooms": 6, + "formingPeople": 18, + "thresholdSource": "PRODUCT_REALTIME", + "enrolledRooms": 1, + "enrolledPeople": 2 + } +} +``` + +注意 `formingRooms=6` 与 `enrolledRooms=1` 同时出现且**都不是错的**—— +前者含线下占位、不含未付款;后者是库存口径,正好相反。 + +#### 空数据 / 降级响应 + +产品域暂时取不到时,两个门槛**一起**回退建团快照,`thresholdSource` 标成 `SNAPSHOT_FALLBACK`, +线下占位按 0 计。**接口仍返 200**,不会因为产品域抖动而整个详情页打不开。 +前端可据此提示「门槛可能非最新」。两个门槛要么都实时、要么都回退,不会一个实时一个快照。 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "groupBatchId": "2097209881592266753", + "minToForm": 4, + "minGroupPeople": 7, + "formingRooms": 0, + "formingPeople": 0, + "thresholdSource": "SNAPSHOT_FALLBACK" + } +} +``` + +#### 错误响应 + +```json +{ + "code": 589500, + "message": "团期不存在", + "success": false, + "data": null +} +``` + +**错误码集合无新增**。 + +#### 业务边界 + +- **判定、成团弹窗、成团流水三处共用同一份计数与门槛**,不会出现「弹窗说未达标、系统却成团了」。 +- **`enrolledRooms` 的语义没变**,超卖闸、对账任务、调名额校验仍旧读它,前端沿用旧口径的地方不受影响。 +- **0 或 null 表示「不限」**,两个门槛都为 0 时系统不会自动成团,只能人工成团。 + +### 2. 新增/编辑班期 `PUT /admin/product/item/:id/schedule` + +**VO**: `ScheduleSaveReqVO` + +#### 使用场景 + +管理后台产品维护 → 班期新增 / 编辑。调用方 hl-ui 产品班期表单。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | — | 产品 ID | +| batchId | Body | Long | 编辑必填 | — | 班期 ID;新增时不传 | +| departureDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 出发日期 | +| singleRoomDiff | Body | BigDecimal | ✅ | ≥0 | 单房差 | +| minToForm | Body | Integer | ❌ | ≥0 | 最低成团户数,0/不传视场景见下 | +| minParticipants | Body | Integer | ❌ | ≥0 | **新增字段**:最低成团人数,0=不限 | +| maxRooms / maxParticipants | Body | Integer | ❌ | ≥0 | 满团房数 / 人数,未变 | +| 其余字段 | — | — | — | — | **完全不变** | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Long | 班期 ID;**响应结构完全不变** | + +#### 请求示例 + +```http +PUT /admin/product/item/2056947670512971778/schedule HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{ + "batchId": 2097209609465864195, + "departureDate": "2026-11-11", + "singleRoomDiff": "0", + "adultPrice": "3000.00", + "childPrice": "2500.00", + "maxParticipants": 30, + "maxRooms": 9, + "minToForm": 6, + "minParticipants": 10 +} +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": "2097209609465864195" } +``` + +#### 空数据 / 降级响应 + +保存成功后会尽最大努力把新门槛推给订单域刷新展示兜底值。 +**这一步失败不影响保存**——接口仍返 200,只记警告日志,班期本身已经存好了。 + +```json +{ "code": 200, "message": "成功", "success": true, "data": "2097209609465864195" } +``` + +#### 错误响应 + +```json +{ + "code": 410105, + "message": "成人售价(1000.00元)必须大于产品订金(1000.00元/人),请调整定价或修改基础信息中的订金", + "success": false, + "data": null +} +``` + +**错误码集合无新增**。 + +#### 业务边界 + +- ⚠️ **编辑时两个门槛都要回传**:语义是「传了才覆盖、没传保留旧值」。 + 改前只要表单不回传就会被**静默清零**(自动成团随之关闭且页面看不出异常),本次已修。 + 但前端仍应把两个字段放进表单并原样回传,否则用户改不了门槛。 +- **显式传 0 是「不限」**,不会被旧值覆盖回去。 +- **新增班期时不传按 0(不限)兜底**。 + +### 3. 批量创建班期 `POST /admin/product/item/:id/schedule/batch-create` + +**VO**: `ScheduleBatchCreateReqVO` + +#### 使用场景 + +管理后台按重复规则一次性建多期班期。调用方 hl-ui 产品班期批量创建弹窗。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | — | 产品 ID | +| startDate / endDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 日期区间 | +| repeatMode | Body | String | ✅ | WEEKLY / BIWEEKLY / MONTHLY | 重复模式 | +| dayOfWeek | Body | Integer | 按模式 | 1-7 | 周几 | +| minToForm | Body | Integer | ❌ | ≥0 | 最低成团户数,逐期落库 | +| minParticipants | Body | Integer | ❌ | ≥0 | **新增字段**:最低成团人数,**逐期落库** | +| 其余字段 | — | — | — | — | **完全不变** | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | Integer | 创建成功的期数;**响应结构完全不变** | + +#### 请求示例 + +```http +POST /admin/product/item/2056947670512971778/schedule/batch-create HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{ + "startDate": "2027-01-21", + "endDate": "2027-02-05", + "repeatMode": "WEEKLY", + "dayOfWeek": 4, + "adultPrice": "3000.00", + "childPrice": "2500.00", + "singleRoomDiff": "0", + "maxParticipants": 30, + "maxRooms": 9, + "minToForm": 2, + "minParticipants": 6 +} +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": 3 } +``` + +#### 空数据 / 降级响应 + +日期区间内没有匹配重复规则的日期时返回 `data: 0`,不是错误。 + +```json +{ "code": 200, "message": "成功", "success": true, "data": 0 } +``` + +#### 错误响应 + +```json +{ + "code": 410107, + "message": "批量创建日期跨度超过上限", + "success": false, + "data": null +} +``` + +**错误码集合无新增**。 + +#### 业务边界 + +- **`minParticipants` 会落到每一期**。改前该字段不存在,批量建出来的班期人数门槛**静默落 0**。 +- 单期上限与既有的跨度校验未变。 + +### 4. 班期列表 `GET /admin/product/item/:id/schedule/list` + +**VO**: `ScheduleRespVO` + +#### 使用场景 + +管理后台产品详情 → 班期列表。调用方 hl-ui 产品班期表格。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | — | 产品 ID;**入参一个都没变** | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| minParticipants | Integer | **新增**:最低成团人数,0=不限 | +| minToForm | Integer | 最低成团户数,未变 | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +GET /admin/product/item/2056947670512971778/schedule/list HTTP/1.1 +Authorization: Bearer +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": [ + { "batchId": "2097221229113991170", "departureDate": "2027-01-21", "minToForm": 2, "minParticipants": 6 } + ] +} +``` + +#### 空数据 / 降级响应 + +产品下无班期时返回空数组,不是错误。 + +```json +{ "code": 200, "message": "成功", "success": true, "data": [] } +``` + +#### 错误响应 + +```json +{ "code": 410001, "message": "产品不存在", "success": false, "data": null } +``` + +**错误码集合无新增**。 + +#### 业务边界 + +- 该字段与班期表单的 `minParticipants` 同源,用于表格回显与编辑回填。 + +### 5. 逐单提交房务需求 `POST /v3/admin/order/:id/hotel-requirement/dispatch` + +**VO**: `HotelRequirementRespVO` + +#### 使用场景 + +管理后台把某户的房型需求派给房务。调用方 hl-ui 团期子订单操作区。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | — | 子订单 ID;**入参一个都没变** | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| requirementId | Long | 需求 ID;**响应结构完全不变** | +| status | String | 派发后为 PENDING | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +POST /v3/admin/order/2097223133588041729/hotel-requirement/dispatch HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { "requirementId": "2097223397887913985", "status": "PENDING" } +} +``` + +#### 空数据 / 降级响应 + +该单尚未提交过房需求时返 582031「订单无有效需求行」,属业务前置条件不满足,未变。 + +```json +{ "code": 582031, "message": "订单无有效需求行", "success": false, "data": null } +``` + +#### 错误响应 + +```json +{ + "code": 589552, + "message": "团期尚未成团,请先完成成团后再操作", + "success": false, + "data": null +} +``` + +**新增错误码**:589552(团期尚未成团)、589553(团期尚未创建,即该班期还没有任何订单)。 + +#### 业务边界 + +- **定制师提交 / 修改房需求不受影响**:招募中照常放行,只有「派给房务」这一步被拦。 +- **未建团与未成团分开报码**,让运营能区分「团建了但没成」与「团根本还没建」—— + 后者的下一步动作完全不同。 +- **存量需求也被拦**:上线前已经放行到房务的需求,其后续配房 / 单日确认动作在团期回到 + 招募中时同样返 589552。 + +### 6. 逐单提交车务需求 `POST /v3/admin/order/:id/vehicle-requirement/dispatch` + +**VO**: `VehicleRequirementRespVO` + +#### 使用场景 + +管理后台把某户的用车需求派给车务。调用方 hl-ui 团期子订单操作区。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | — | 子订单 ID;**入参一个都没变** | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| requirementId | Long | 需求 ID;**响应结构完全不变** | +| status | String | 派发后状态 | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +POST /v3/admin/order/2097223133588041729/vehicle-requirement/dispatch HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{} +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": { "requirementId": "2097223397887913986" } } +``` + +#### 空数据 / 降级响应 + +该单尚未提交过车需求时返 582031「订单无有效需求行」,未变。 + +```json +{ "code": 582031, "message": "订单无有效需求行", "success": false, "data": null } +``` + +#### 错误响应 + +```json +{ + "code": 589552, + "message": "团期尚未成团,请先完成成团后再操作", + "success": false, + "data": null +} +``` + +**新增错误码**:589552 / 589553,同房务。 + +#### 业务边界 + +- 与房务同一套判据、同一个方法,不会出现两边口径漂移。 +- 定制师提交 / 修改车需求同样不受影响。 + +### 7. 团期导摄配置保存 `PUT /v3/admin/group-batch/:productBatchId/staff` + +**VO**: `GroupBatchStaffConfigRespVO` + +#### 使用场景 + +管理后台团期详情 → 导游 / 摄影师配置整体保存。调用方 hl-ui 团期资源配置页。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| productBatchId | Path | Long | ✅ | — | 产品侧班期 ID;**入参一个都没变** | +| guides | Body | Array | ❌ | — | 导游列表 | +| photographers | Body | Array | ❌ | — | 摄影师列表 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| guides / photographers | Array | 保存后的配置;**响应结构完全不变** | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +PUT /v3/admin/group-batch/2097223022807400450/staff HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{"guides":[{"staffId":1,"staffName":"张导"}],"photographers":[]} +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "success": true, "data": { "guides": [{ "staffId": "1" }] } } +``` + +#### 空数据 / 降级响应 + +两个列表都传空表示清空配置,返 200,不是错误。 + +```json +{ "code": 200, "message": "成功", "success": true, "data": { "guides": [], "photographers": [] } } +``` + +#### 错误响应 + +```json +{ + "code": 589553, + "message": "团期尚未创建(该班期还没有任何订单),请先建团并完成成团后再操作", + "success": false, + "data": null +} +``` + +**新增错误码**:589552 / 589553。设置报账人等级 `PUT /v3/admin/group-batch/:productBatchId/staff/:staffId/reporter-rank` 同样受这两个码约束。 + +#### 业务边界 + +- **未成团时保存被拒且不留副作用**:实测被拒后导游就绪标记仍为未就绪。 +- **成团后同一请求即可保存成功**,就绪标记随之置位。 + +--- + +## 四、契约约束与正确调用方式 + +- **成团弹窗的「已售」请读 `formingRooms`**,不要读 `enrolledRooms`。 + 前者是成团口径(线上已付款 + 线下占位),后者是库存口径(含未付款、不含线下占位)。 + 继续读旧字段会出现「弹窗说未达标、系统却自动成团了」。 +- **门槛请读 `minToForm` / `minGroupPeople`**,并用 `thresholdSource` 判断要不要给用户提示 + 「门槛可能非最新」。两个门槛的来源恒定一致,不会一个实时一个快照。 +- **编辑班期时两个门槛都要回传**,不传等于「不改」,传 0 等于「不限」。 +- **589552 / 589553 是可恢复的拒绝**,提示运营「请先完成成团」/「请先建团」, + 不要提示「系统繁忙」。 + +--- + +## 五、数据库行为 + +- 本次**无表结构变更**;最低成团人数复用班期已有的列,团期侧复用既有的「最低成团人数」列 + (该列此前恒为空,本次开始写入)。 +- 新增一条定时任务配置(团期达门槛自动成团,每 5 分钟),**落地即暂停**,需人工开启。 +- 自动成团会写团期状态流水一条,操作人记为「系统」。 + +--- + +## 六、边界行为 + +- **两个门槛都为 0 / 未设**:不自动成团,只能人工成团。 +- **未付款的户不计入达标判定**;**产品域线下占位计入**。 +- **自动成团后退单掉回门槛以下**:保持成团,不退回招募中;是否流团由运营人工判断。 +- **产品域暂时不可达**:本轮跳过该产品名下团期,**绝不误成团**;下一轮自动重试。 +- **产品域班期已取消 / 已过出发日**:不自动成团;产品域显示「已满额」的班期**照常自动成团** + (满员正是该成团的状态)。 +- **纯线下、零线上订单的班期**不在扫描范围(订单域还没有这个团),需运营先建团。 +- **人工成团仍然保留**:未达门槛也可提前成团,流水会记「未达标(满N户)」与理由。 + +## 六.6、修改前后对比 + +| 场景 | 改前 | 改后 | +|------|------|------| +| 团期达到最低成团户数 / 人数 | 一直停在「招募中」,等人工点成团 | 定时任务自动推进到「资源准备中」(任务需先开启) | +| 团期详情的成团门槛 | 读建团那天的**冻结快照**;产品域改了门槛也不变 | 读产品域**实时值**;取不到才回退快照并标 `SNAPSHOT_FALLBACK` | +| 团期详情的「已售」 | 只有 `enrolledRooms`(含未付款、不含线下占位) | 新增 `formingRooms`(成团口径),`enrolledRooms` 保留不变 | +| 最低成团人数 | 产品域**没有这个字段**,无法设置 | 班期表单 / 批量创建 / 列表回显全链路可用 | +| 编辑班期不回传门槛 | 门槛被**静默清零**,自动成团随之关闭且页面看不出异常 | 保留旧值;显式传 0 才是「不限」 | +| 批量创建带最低成团人数 | 字段不存在,逐期**静默落 0** | 逐期正确落库 | +| 招募中派房务 / 车务 / 配导摄 / 设报账人 | **放行**,资源成本先于成团发生 | 拒 **589552**(未建团拒 **589553**) | +| 定制师提交 / 修改房车需求 | 放行 | **仍然放行**,未收紧 | + +## 六.7、影响评估 + +- **前端有三项必做**: + 1. 团期详情消费三个新字段,**成团弹窗的「已售」改读 `formingRooms`**; + 2. 班期表单 / 批量创建弹窗新增 `minParticipants`,且编辑时两个门槛都要回传; + 3. 错误码字典补录 589552 / 589553。 +- **不做会怎样**:接口不会报错,但①成团弹窗的数字会与系统判定对不上; + ②用户无法设置最低成团人数;③被拒时只看到默认错误提示。 +- **对运营的净效果**:不用再盯看板手动点成团;未成团时不会误配资源。 +- **需要运营做一件事**:到用户中心把「团期达门槛自动成团」任务从暂停改为启用。 +- **风险**:任务开启后会对存量招募中团期批量成团。两个门槛都为 0 的存量班期不受影响 + (不自动成团),但已设门槛且已达标的团期会在开启后的第一轮全部推进——**建议先在低峰期开启并观察一轮**。 + +--- + +## 七、不影响范围 + +- 人工成团入口与权限未变(未达标仍可提前成团)。 +- 满团名额调整的校验规则未变(仍按含未付款的库存口径判)。 +- 团期看板列表、超卖闸、名额对账任务未变。 +- 下单、支付、退款链路未变。 +- 定制师提交 / 修改房车需求的权限未收紧。 + +--- + +## 八、测试环境已验证 + +三服务按次序一起滚:product-v2 → order-v3 → user-service。验收后已滚回主干,造数已清理。 + +| 验证项 | 结果 | +|--------|------| +| 未付款不计入 | 2 单都不付款 → 不成团;付款后再跑 → 成团 | +| 线下占位计入 | 门槛 6、线上已付 1 户 + 线下占位 5 → 自动成团 | +| 人数维度 | 户数门槛 0 / 人数门槛 4,1 单 3 人 + 线下 1 人 → 自动成团 | +| 两个门槛都为 0 | 3 单全付款、连跑两轮 → 仍「招募中」 | +| 自动成团留痕 | 操作人「系统」,文案「系统自动成团 · 已售 6/9 户(线上已付 1 + 线下 5) · 达标(满6户)」 | +| 判定与展示同源 | 详情 `formingRooms=6` 与流水记录完全一致;`enrolledRooms` 保持库存口径不受影响 | +| 幂等 | 对已成团团期连跑两轮,成团流水仍只有 1 条 | +| 人工成团文案 | 「手动成团 · 已售 1/9 户 · 未达标(满3户) · 理由:客户催促」,门槛取的是实时值 | +| 权限未被削弱 | 无权限账号手动成团被拒;同一团期由任务自动成团仍成功 | +| 退单不回退 | 已成团团期退掉已付款户后仍「资源准备中」 | +| 产品域不可达 | 门槛已达标的团期**不误成团**;恢复后同一团期同一任务立即成团 | +| 详情门槛实时化 | 产品域 6→3 且不下新单 → 详情返 3,来源标 `PRODUCT_REALTIME` | +| 详情降级 | 产品域取不到 → 仍 200,两个门槛一起回退快照,来源标 `SNAPSHOT_FALLBACK` | +| 门槛变更回推 | 产品域改门槛且不下新单 → 团期侧展示兜底值同步更新 | +| 编辑不再洗掉门槛 | 只改备注 → 仍 6/10;显式传 0/0 → 0/0 | +| 批量创建 | 3 期全部落 `minParticipants=6`,列表回显一致 | +| 未成团闸 | 招募中派房务 / 派车务 / 配导摄均 589552;成团后房务与导摄同请求返 200 | +| 未建团闸 | 无任何订单的班期配导摄 / 设报账人均 589553 | +| 定制师未被收紧 | 招募中提交房需求返 200 | +| 候选排除 | 产品域已取消 / 已过出发日 → 不成团;产品域「已满额」→ 正常成团 | +| 取消成团后 | 团期回到招募中,再配房返 589552 | +| 单元测试 | product-v2 1620 例、order-v3 9060+ 例、user-service 3828 例,均 0 失败 0 错误 | + +--- + +## 十、相关文档 + +- Issue #7287、PR #7307 +- 关联工单:#7158(人工成团与户数门槛)、#7178(满团名额调整)、#7196(流团审批) +- 设计文档已同步:团期订单设计说明中「系统不自动成团」的表述已改写, + 「未达标也可提前成团」保留 + +--- + +## 关联 / 联系人 + +- 后端:jw +- 前端:mmg(hl-ui) +- 前端待办(三项):①团期详情消费 formingRooms / formingPeople / thresholdSource, + **成团弹窗「已售」改读 formingRooms**;②班期表单与批量创建新增 minParticipants, + 编辑时两个门槛都回传;③错误码字典补录 589552 / 589553。 +- 运营待办:到用户中心开启「团期达门槛自动成团」定时任务(落地即暂停)。 diff --git a/changelogs-v2/2026-09/08_7294_流团对已付款户也取消并发起退款-修改接口-管理后台.md b/changelogs-v2/2026-09/08_7294_流团对已付款户也取消并发起退款-修改接口-管理后台.md new file mode 100644 index 00000000..b3d646bb --- /dev/null +++ b/changelogs-v2/2026-09/08_7294_流团对已付款户也取消并发起退款-修改接口-管理后台.md @@ -0,0 +1,373 @@ +--- +schema: "hl-changelog/v2" +ticket: "7294" +title: "批准流团后已付款户会被自动取消并发起退款;审批单实退金额不再恒为 0;新增 589554 出行中户硬闸" +consumer: "hl-ui" +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-10、AC-12、AC-15 及改判后的 AC-17 全过,改前对照同批取证;AC-13/14/16 因需 kill 进程或并发构造未在共享测试环境跑)。前端待办一项:错误码字典补录 589554「团期下有出行中的子订单,不可流团」。接口出入参结构零变化,前端不改也不会报错。" +updated_at: "2026-09-08" +base: "dev-v3" +--- + +# 团期: 流团对已付款户也取消并发起退款 + 出行中户硬闸 589554 + +> **服务**: hl-order-service-v3(只需部署这一个) +> **PR**: #7333 +> **Issue**: #7294 +> **日期**: 2026-09-08 +> **影响范围**: 团期流团申请与审批(GB-ADM-060 / GB-ADM-062) + +--- + +## ⚠️ 关键变化 + +**出入参结构一个字段都没改。** 前端不改不报错,但有三件事必须知道: + +1. **批准流团后,已付款的户现在会被自动取消并发起退款**。改前只有「待支付」的户被取消, + 付过定金或全款的户**既不取消也不退款**——团已解散,活跃子订单和客户的钱都还留着。 +2. **审批单的「实退金额」不再恒为 0**。改前 `actualRefundAmount` 是系统性的 0, + 与「预估退款」形成无人负责的落差;现在它等于**本次实际发起的退款诉求合计**。 + ⚠️ 这是**退款诉求**不是**实际到账**,界面文案建议写「已发起退款金额」。 +3. **新增错误码 589554**「团期下有出行中的子订单,不可流团」,前端错误码字典需补录。 + 发起流团与批复流团两个入口都会返这个码。 + +改前实测(AC-4,在部署本次改动**之前**于测试服复现):三户(未付款 / 付订金 2000 / 已补尾款 6000) +批准流团后,只有未付款那户被取消,另两户仍是「制作中」,审批单 `estimated=8000 / actual=0`, +团期已「已取消」而底下**残留 2 张活跃单**。 + +--- + +## 一、背景 + +流团在 #7196 之后是一条完整审批链:发起申请 → 管理员审批 → 通过后系统自动置团期取消、 +批量退各户定金、释放配车占用。设计目标是「批准后系统一次做完,不留手工尾巴」。 + +但「批量退各户定金」这一步从未真正实现:执行时逐户调的是「系统自动取消」, +而它**只处理待支付订单**,其余状态一律跳过。而待支付订单的已付金额恒为 0 +(付款成功即离开待支付态),所以实退恒为 0。 + +**这不是低频边界,是每次流团都会发生**:只要团里有一户付过钱,这一户就会被跳过。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 同意流团申请 | POST | `/v3/admin/order/group-batch/disband/:approvalId/approve` | 行为变化 | 已付款户一并取消并发起退款;`actualRefundAmount` 口径修正;新增 589544 / 589554 两条拒绝路径 | +| 2 | 提交流团申请 | POST | `/v3/admin/order/group-batch/:groupBatchId/disband` | 行为变化 | 新增 589554 前置拒绝;出入参与响应结构不变 | + +--- + +## 三、接口详情 + +### 1. 同意流团申请 `POST /v3/admin/order/group-batch/disband/:approvalId/approve` + +**VO**: `DisbandApprovalRespVO` + +#### 使用场景 + +管理后台团期「流团审批」中同意一条待审流团申请。调用方 hl-ui 团期审批页。 +权限:仅管理员角色。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| approvalId | Path | Long | ✅ | — | 流团审批单 ID;**入参一个都没变** | +| remark | Body | String | ❌ | ≤256 | 批复备注;请求体整体可不传 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| approvalId | Long | 审批单 ID;**响应结构完全不变** | +| approvalStatus | String | PENDING / APPROVED / REJECTED | +| affectedOrderCount | Integer | 影响子订单户数 | +| estimatedRefundAmount | BigDecimal | 提交时算的预估退款(Σ已付),口径未变 | +| actualRefundAmount | BigDecimal | **口径修正**:本次实际发起的退款诉求合计。改前系统性恒为 0 | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/disband/2097220075659415553/approve HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{"remark":"确认无法成团"} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "approvalId": "2097220075659415553", + "approvalStatus": "APPROVED", + "affectedOrderCount": 3, + "estimatedRefundAmount": 8000.0, + "actualRefundAmount": 8000.0, + "approvedByName": "admin", + "approvedAt": "2026-09-08 15:06:46" + } +} +``` + +上面这单里,三户分别是「未付款 / 付订金 2000 / 已补尾款 6000」, +执行后三户**全部取消**,退款单 2000 + 6000 与 `actualRefundAmount` 逐笔对上。 + +#### 空数据 / 降级响应 + +**单户失败不中断整批**:某户因状态不允许取消、或用车需求正处于最终确认中而被跳过时, +接口仍返 200、其余户照常取消、审批单与团期状态**不回滚**; +`actualRefundAmount` 只统计成功发起退款的户,因此**可能小于 `estimatedRefundAmount`**, +差额由服务端日志里的「未处理清单」解释。 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "approvalId": "2097216263326466050", + "approvalStatus": "APPROVED", + "affectedOrderCount": 3, + "estimatedRefundAmount": 6000.0, + "actualRefundAmount": 4000.0 + } +} +``` + +#### 错误响应 + +```json +{ + "code": 589554, + "message": "团期下有出行中的子订单,不可流团;请等其出行完毕,或先对该户单独终止行程", + "success": false, + "data": null +} +``` + +**错误码新增一条、新增两条触发路径**: + +| 码 | 含义 | 变化 | +|----|------|------| +| 589554 | 团期下有出行中的子订单,不可流团 | **本次新增**。发起与批复两个入口都会返 | +| 589544 | 团期已出行,不可发起或批复流团 | **新增触发路径**:此前只在发起入口出现,现在批复时也会按当时的团期状态重判 | +| 589545 / 589546 / 589547 / 589500 / 589501 | 申请不存在 / 已处理 / 非管理员 / 团期不存在 / 团期已终态 | 均未变 | + +#### 业务边界 + +- **退款按「已付全额」算**,不扣违约金:流团是平台方单方面解散,客人无过错。 + 金额 = 该户已付款 − 已退款。**不是**按应付订金退——已补尾款的户按订金退会少退, + 全款户更会因为没有订金而直接失败。 +- **已付款为 0 的户只取消不建退款单**,属正常情形,不是失败。 +- **退款单是「诉求」不是「到账」**:由取消事件的异步监听器创建、由渠道异步打款, + 都可能后续失败。界面文案请写「已发起退款金额」。 +- **退款不再挂第二道审批**:流团本身已经过一道管理员审批,退款单直接放行。 +- **有出行中户就整笔拒绝**:拒的时候什么都不改——团期状态、子订单、审批单全部零变动, + 审批单仍留在「待审批」,运营处理完那一户后可以直接再批一次。 + +### 2. 提交流团申请 `POST /v3/admin/order/group-batch/:groupBatchId/disband` + +**VO**: `DisbandApprovalRespVO` + +#### 使用场景 + +管理后台团期详情页发起流团申请,只建待审批单、不执行。调用方 hl-ui 团期详情页。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | — | 团期主订单 ID;**入参一个都没变** | +| reason | Body | String | ✅ | 非空 | 流团理由 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| approvalId | Long | 新建的审批单 ID;**响应结构完全不变** | +| approvalStatus | String | 恒为 PENDING | +| affectedOrderCount | Integer | 影响子订单户数 | +| estimatedRefundAmount | BigDecimal | 预估退款(Σ已付),口径未变 | +| 其余全部字段 | — | **完全不变** | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2097220073544073217/disband HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{"reason":"临近出团仍未达最低成团数"} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "approvalId": "2097220075659415553", + "approvalStatus": "PENDING", + "affectedOrderCount": 3, + "estimatedRefundAmount": 8000.0 + } +} +``` + +#### 空数据 / 降级响应 + +被 589554 拒时**不会建出审批单**,团期审批列表里不会多出一条待处理项, +运营处理完出行中那户后重新发起即可。 + +```json +{ + "code": 589554, + "message": "团期下有出行中的子订单,不可流团;请等其出行完毕,或先对该户单独终止行程", + "success": false, + "data": null +} +``` + +#### 错误响应 + +```json +{ + "code": 589543, + "message": "该团期已有在途的流团申请", + "success": false, + "data": null +} +``` + +错误码:589543(已有在途申请)、589544(团期已出行)、589500(团期不存在)、 +**589554(有出行中的子订单,本次新增)**。 + +#### 业务边界 + +- 本入口的 589554 是**前置提示**,让运营在提单那一刻就知道拦在哪; + 权威的那道在批复侧——提单到批复之间订单还可能被出发任务推成出行中。 +- 提交阶段的「预估退款」口径未变,仍是全部活跃户的已付金额之和。 + +--- + +## 四、契约约束与正确调用方式 + +- **`actualRefundAmount` 请按「已发起退款金额」展示**,不要写成「实退合计」。 + 它可能小于「预估退款」——差额来自被跳过的户,属正常情形而非数据错误。 +- **589554 是可恢复的拒绝**:提示运营「该团有客人正在行程中,请等其出行完毕, + 或先单独为该户办理终止行程」,而不是提示「系统繁忙,稍后重试」。 +- **589544 与 589554 是两件事**:前者是「这个团期本身已经出发了」, + 后者是「团期还没出发,但底下有客人已经在行程中」。两个提示语不要合并。 + +--- + +## 五、数据库行为 + +- 本次**无表结构变更、无数据迁移**。 +- 批准流团后:已付款户的订单状态推进为「已取消」并写取消时间;审批单回填「已发起退款金额」; + 退款申请由取消事件异步创建。 +- 被 589544 / 589554 拒时**零写入**:团期、子订单、审批单全部不变。 + +--- + +## 六、边界行为 + +- **只含待支付户的团期**:行为与改前逐字节一致——订单取消、无退款单、实退金额 0。 +- **已被全额退过的户**:可退额为 0,只取消不建退款单。 +- **有历史部分退款的户**:可退额按「已付 − 已退」算,不会撞退款复核的上限校验。 +- **用车需求正在最终确认中的户**:本轮跳过(该冲突有 10 分钟租约会自动过期), + 其余户照常处理,审批单与团期不回滚。 + +## 六.6、修改前后对比 + +| 场景 | 改前 | 改后 | +|------|------|------| +| 批准流团,团里有付过定金 / 全款的户 | 那些户**既不取消也不退款**,团期已「已取消」而底下残留活跃单 | **全部取消并发起退款**,团期下无残留 | +| 审批单「实退金额」 | 系统性恒为 **0** | 等于本次**已发起退款金额合计**,与退款单逐笔对得上 | +| 已补尾款的户(已付 6000 / 订金 2000) | 不取消不退款 | 取消并退 **6000**(按已付款,不是按订金) | +| 全款支付户 | 不取消不退款 | 取消并退全部已付款 | +| 只含待支付户的团期 | 取消、无退款单、实退 0 | **完全一致**,无回归 | +| 批复时团期已出行 | **放行**,团被流掉 | **589544 拒绝**,零变动 | +| 团期下有出行中的子订单 | 放行,团被流掉而该户残留 | **589554 拒绝**,零变动 | +| 单户因状态或用车确认冲突被跳过 | — | 其余户照常成功,审批单与团期**不回滚**,接口仍 200 | + +## 六.7、影响评估 + +- **前端零改动**:两个接口的出入参与响应结构完全不变。 +- **需要前端做的一件事**:错误码字典补录 **589554**。不补也不会报错,只是拿不到友好文案。 +- **建议改一处文案**:审批单上的 `actualRefundAmount` 若显示为「实退金额」, + 建议改为「已发起退款金额」——它是退款诉求,不是到账确认。 +- **对客户的净效果**:团解散后钱会自动退,不用等客户来问。 +- **对运营的净效果**:审批单上的金额第一次有了真实含义; + 「团期已取消却还挂着活跃单」这种需要人工善后的现场大幅减少。 +- **风险**:户数多时批复会串行处理各户退款,接口耗时随户数上升;正确性有 CAS 兜底, + 不会双执行。 + +--- + +## 七、不影响范围 + +- 拒绝流团(reject)未改。 +- 退单户审批(withdraw)未改。 +- C 端自主取消、管理后台单笔取消订单未改——新增的「已付全额退」模式**不进后台取消下拉**, + 只由流团执行链路内部使用。 +- 团期名额计数、对账 Job 未改。 +- 定制师提交房 / 车需求的权限未收紧。 + +--- + +## 八、测试环境已验证 + +只部署 `hl-order-service-v3`。验收后已滚回主干,造数已清理。 + +| 验证项 | 结果 | +|--------|------| +| 三户(未付款 / 付订金 / 已补尾款)批准流团 | **三户全部「已取消」**,`cancelled_at` 非空 | +| 退款单落库 | 付订金户 2000、已补尾款户 **6000**(按已付款不是订金)、未付款户**无退款单** | +| 审批单金额 | `estimated=8000 / actual=8000`,与两张退款单逐笔对上 | +| 改前对照 | 同样三户 → 只取消未付款那户,另两户仍「制作中」,`actual=0`,残留 2 张活跃单 | +| 单户不可取消时 | 接口 200、其余户全成功、审批单与团期**未回滚**、`actual` 只算成功户(4000 / 预估 6000) | +| 用车最终确认在途的户 | 该户跳过、其余成功、审批单与团期**未回滚**、接口 200 | +| 团期下无残留 | 三户全可取消的场景下,活跃单数 = **0** | +| 只含待支付户 | 取消、无退款单、实退 0——与改前一致 | +| 全款户(无订金) | 取消并退全部已付款 6000 | +| 批复时团期已出行 | **589544**,团期 / 子订单 / 审批单零变动 | +| 团期下有出行中子订单(提单侧 / 批复侧) | 均 **589554**,且提单侧不建审批单、批复侧审批单仍「待审批」 | +| 待办同步真实失败时 | 流团照常提交(真数据库 + 真事务代理的集成测试坐实) | +| 单元测试 | order-v3 9060 例,0 失败 0 错误 | + +--- + +## 十、相关文档 + +- Issue #7294、PR #7333 +- 关联工单:#7196(流团申请与审批)、#7283(流团后班期置为不可售) +- 未覆盖项:退款建单失败注入、进程中断恢复、批复与出发任务并发—— + 三条需要 kill 进程或精确并发构造,未在共享测试环境执行 + +--- + +## 关联 / 联系人 + +- 后端:jw +- 前端:mmg(hl-ui) +- 前端待办:错误码字典补录 **589554**;建议把审批单的「实退金额」文案改为「已发起退款金额」。 + **接口结构零变化,不改也不会报错。**