比较提交

..
2 次代码提交
作者 SHA1 备注 提交日期
API Changelog Bot和Claude Opus 5 41c0f752f9 docs(changelog): #7972 接送机的免车闸与结算闸改为非对称判据,809114 放宽 / 584131 新增拒绝
changelog-filename-gate / validate (push) Failing after 2s
PR #7976(572dc4037)改了两个既有错误码的抛出条件,零新增错误码:

- 809114(整团免车被派车阻塞)改为只看 TRAVEL——「只订接送机、没有团车」
  是合法的在团户,不该被上游拒绝免车,属于放宽。
- 584131(团期用车未就绪)对 TRANSFER 补独立的非对称判据:不存在放行 /
  DONE 放行 / 其余拒。原 TRAVEL 的抛出与放行结论逐字不变。
  整团免车现在只短路 TRAVEL 一支,两类互不豁免。

🔴 运维注意已写进正文:存量里停在 PENDING 的 TRANSFER 需求行,其所在户的
finalize 会从 200 变 584131——那是本次改动的预期行为,不是回归。

backend_status=deployed 依据 572dc4037 是测试服 order-v3 所在提交的祖先;
gateway_status=not_required 依据本次不涉及网关路由变更(仅后端判定条件)。
校验器 PASS,校验对象 4 个端点。

Refs #7972

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 17:35:19 +08:00
API Changelog Bot和Claude Opus 5 89d7e4f75d docs(changelog): 订正 01_6905 里「estimatedCost 已可用」这条错误交接(共 4 处,非 1 处)
前端会话核出 01_6905 第 446 行写着「预计毛利 = totalPrice − estimatedCost 现可直接算
(estimatedCost 已可用)」,而该字段从未有过数据。前端据此规划的「预计毛利」列在任何时点都算不出来。

本会话独立复核成立,并补上两个原 grep 会漏的口径:
- setEstimatedCost 在 main 代码 0 命中;
- getEstimatedCost 也是 0 命中——这条关键,MyBatis-Plus 的
  LambdaUpdateWrapper.set(Entity::getXxx, v) 用的是 getter 引用,只 grep setter 会整类漏掉;
- 字段名在整个 Java 侧只出现在实体 OrderInfo.java 自身的声明里,其余全是 docs 与建表 DDL
  ⇒ 没有任何别的 DTO/VO 带这个属性名,连 BeanUtil 那种反射拷贝也无从填它。
⇒ order_main.estimated_cost 全仓零写入点,estimatedCost 自 #6905(47aaff0be) 透出那天起恒 null,
不是后来才失效;已于 #7536(6182d566d) 从 003 出参删除。

⚠️ 同一条错话在正文共 4 处(字段表 :281、totalPrice 行的毛利公式 :282、
新增/派生对照表 :390、联调口径第 4 条 :446),外加在途返工表 :455 的半句。
只改被举报的那一处会让订正本身成为新的不一致来源,故五处一并改,并做了全文残留断言:
无订正标记而仍出现 estimatedCost 的行 = 0。

订正一律保留原文加删除线、不抹除,旧串仍可被 grep 接住(estimatedCost 出现 11 次)。
同时指向现行宿主:逐户毛利在核团接口 GET /v3/admin/order/group-batch/{groupBatchId}/audit 的
allocs[].grossProfit / costAmount(GroupBatchAuditRespVO:204-208 声明,
GroupBatchAuditService:742-743 真实填值,见 18_7932,亦即 14_7536:97「毛利改核单页」所指)。
顺带订正出处:estimatedCost 由 #6905(47aaff0be) 引入,不是 df8dbea0c——后者只加了 totalPrice。

发现来源:前端问题会话对 origin/dev-v3 的两轮核查(核查 + 对抗复核)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 17:30:44 +08:00
共修改 2 个文件,包含 548 行新增和 7 行删除
@@ -10,10 +10,10 @@ gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "f6b849eb"
target_release: ""
target_release: "hl-ui@f6b849eb"
verified_at: "2026-09-02"
status_note: "regionText 由上游 product-v2 提供后透传,当前测试环境返回 null 属预期;001 排序保持 create_time DESC 未变(depart_date ASC 变更待 wx 确认);#6929 已实现:003 totalPrice 应收字段 + birthdayInTrip 跨年修正(详见 §十一)"
updated_at: "2026-09-04"
updated_at: "2026-09-20"
base: "dev-v3"
---
@@ -278,8 +278,8 @@ GET /v3/admin/order/group-batch/<groupBatchId>
| 字段 | 类型 | 说明 |
|------|------|------|
| estimatedCost | BigDecimal | 估算成本(无成本数据可 null) |
| totalPrice | BigDecimal(字符串) | 本户应收 = orderAmount + surchargeAmount − discountAmount(下限 0,`OrderAmountUtil.payableForDisplay` 口径;**取消单返 "0.00"**,#7097 已补两位小数);预计毛利 = totalPrice − estimatedCost(#6929) |
| ~~estimatedCost~~ | ~~BigDecimal~~ | 🔴 **2026-09-20 订正:该字段恒为 `null`,且已于 #7536 从 003 出参删除。**原说明「估算成本(无成本数据可 null)」会让人以为只是暂时没数据——**实际是从未有过数据**。实测 `origin/dev-v3`:`setEstimatedCost` 与 `getEstimatedCost` 在 main 代码里**各 0 命中**,字段名在整个 Java 侧只出现在实体 `OrderInfo.java` 自身的声明里,其余全是 docs 与建表 DDL (`V20260511_001__init_core_tables.sql:83` 等)⇒ **`order_main.estimated_cost` 全仓零写入点**。逐户毛利的现行宿主见下方订正条。 |
| totalPrice | BigDecimal(字符串) | 本户应收 = orderAmount + surchargeAmount − discountAmount(下限 0,`OrderAmountUtil.payableForDisplay` 口径;**取消单返 "0.00"**,#7097 已补两位小数);~~预计毛利 = totalPrice − estimatedCost(#6929)~~ 🔴 **2026-09-20 订正:此式不成立**,`estimatedCost` 恒 null(见上行),任何时点都算不出来。毛利改用核团接口,见「联调口径」第 4 条。 |
| tierCode/tierName | String | tier 组合(成人A/儿童C/幼童Y/婴儿B,如 2A1C→"2成人1儿童";全零→null,映射表待 wx 确认) |
| participantCount | Integer | 人数聚合(adult+child+youngChild+baby) |
| youngChildCount/babyCount | Integer | 幼童/婴儿数 |
@@ -387,7 +387,7 @@ GET /v3/admin/order/group-batch/<groupBatchId>/orders?includeTravelers=true&incl
| 001 | chips/orderCount | 无 | 六芯片聚合/批量计数 |
| 002 | totalReceivable/totalReceived | 实体快照 | sumBatchAmounts 实时聚合(非恒 0) |
| 002 | primaryReporter | 无 | reporter_rank=PRIMARY 批量取值 |
| 003 | estimatedCost/tier/人数 | 无 | 批量派生 |
| 003 | ~~estimatedCost~~/tier/人数 | 无 | 批量派生(🔴 **`estimatedCost` 部分已于 2026-09-20 订正作废**:它虽在出参里出现过,但**从未被赋值**,且已由 #7536 删除;tier 与人数不受影响) |
| 003 | contactPhone | 原文 | 全量掩码 |
| 003 | totalPrice | 无 | 本户应收(payableForDisplay 口径,取消单返 "0.00",#6929/#7097) |
| 003 | travelers | 无 | name/type/age/birthdayInTrip(无证件号,跨年修正 #6929) |
@@ -443,7 +443,10 @@ GET /v3/admin/order/group-batch/<groupBatchId>/orders?includeTravelers=true&incl
1. **【001】无活跃子订单的团期 `chips` 整体为 `null`**(不是六键全「待办」)。有单团期六键全在(聚合态四值:待办/处理中/已完成/异常;个别键可为 null)。渲染芯片前判 `chips != null`,null 时按「未开始」占位。
2. **【000】`productType` 实际只能看团期产品**:上游产品域只返回 GROUP 产品,传非 GROUP 值得空列表;「不限类型查普通产品」是面向隐式团的规划能力,当前不可达。§三.1 入参表原「不传=不限」描述有误,已就地勘误,以本条为准。
3. **【003】`demandStatus`(本户需求态 SUBMITTED/CONFIRMED/REJECTED)不下发**:契约卡 GB-ADM-003 有该字段但本期未实现,响应中不存在;原型名单表「打回 / 已重提」列暂无数据源,请先隐藏或恒占位,勿依赖。
4. **【003】`totalPrice`(本户应收)已实现(#6929)**:「预计毛利 = totalPrice − estimatedCost」现可直接算(`estimatedCost` 已可用)。`totalPrice` 为 JSON 字符串(ToStringSerializer),活跃单 = orderAmount + surchargeAmount − discountAmount(下限 0),**取消单返 `"0.00"`**(#7097 已补两位小数;JSON 为字符串,勿数值化)。**请勿用 `paidAmount + balanceAmount` 自算应收**(含退款场景口径不对)。
4. **【003】`totalPrice`(本户应收)已实现(#6929)**:~~「预计毛利 = totalPrice − estimatedCost」现可直接算(`estimatedCost` 已可用)。~~
🔴 **2026-09-20 订正:上面这句是错误交接,请勿据此开发。**`estimatedCost` **从 #6905(`47aaff0be`)透出那天起就恒为 `null`**,不是后来才失效的——实测 `origin/dev-v3`:`setEstimatedCost` / `getEstimatedCost` 在 main 代码里**各 0 命中**(后者尤其关键:MyBatis-Plus 的 `LambdaUpdateWrapper.set(Entity::getXxx, v)` 用的是 **getter 引用**,只 grep setter 会整类漏掉),且该字段名在整个 Java 侧**只出现在实体自身的声明里**,没有任何别的 DTO/VO 带这个属性名 ⇒ 连 `BeanUtil` 那种反射拷贝也无从填它。该字段已于 **#7536(`6182d566d`)从 003 出参删除**。
⇒ **「预计毛利」列在本接口上任何时点都算不出来。**逐户毛利的现行宿主是**核团接口** `GET /v3/admin/order/group-batch/{groupBatchId}/audit` 的 `allocs[].grossProfit` / `allocs[].costAmount`(`GroupBatchAuditRespVO.java:204-208` 声明,`GroupBatchAuditService.java:742-743` 真实填值;见 changelog `18_7932`,亦即 `14_7536:97`「毛利改核单页」所指)。
📌 顺带订正出处:`estimatedCost` 由 **#6905(`47aaff0be`)** 引入,**不是** `df8dbea0c`——后者只加了 `totalPrice`。`totalPrice` 为 JSON 字符串(ToStringSerializer),活跃单 = orderAmount + surchargeAmount − discountAmount(下限 0),**取消单返 `"0.00"`**(#7097 已补两位小数;JSON 为字符串,勿数值化)。**请勿用 `paidAmount + balanceAmount` 自算应收**(含退款场景口径不对)。
5. **【003】`include*=false` 时扩展字段「键在、值为 null」**:`travelers / roomCount / roomType / specialNeeds` 键仍存在、值为 `null`,判 `null` 即可,勿用 `key in obj` 判断。
6. **【003】`birthdayInTrip` 跨年已修复(#6929)**:行程跨年(12 月~1 月)时,出团年与返团年分别年化比较,任一落在行程闭区间即 `true`(如 12-28~01-03 行程内 01-02 生日 → true);2-29 生日在非闰年落 2-28 不抛异常。
@@ -452,7 +455,7 @@ GET /v3/admin/order/group-batch/<groupBatchId>/orders?includeTravelers=true&incl
| 工单 | 内容 | 前端影响 |
|------|------|----------|
| #6926 | 008 导出补 `opsStage`;009 自校验修正;month 非法值容错 | 见 `01_6904_*` 第十一节 |
| ~~#6929~~ | ~~003 补 `totalPrice`;`birthdayInTrip` 跨年修正;内部双包装收敛~~ | ✅ 已实现(PR #7057,部署验收后生效):`totalPrice − estimatedCost` 可直接算预计毛利;跨年生日不再漏报;其余无感 |
| ~~#6929~~ | ~~003 补 `totalPrice`;`birthdayInTrip` 跨年修正;内部双包装收敛~~ | ✅ 已实现(PR #7057,部署验收后生效):~~`totalPrice − estimatedCost` 可直接算预计毛利~~ 🔴 **该半句 2026-09-20 订正作废**(`estimatedCost` 恒 null,见「联调口径」第 4 条);`totalPrice` 本身已实现属实;跨年生日不再漏报;其余无感 |
## 关联 / 联系人
@@ -0,0 +1,538 @@
---
schema: "hl-changelog/v2"
ticket: "7972"
title: "接送机用车的免车闸与结算闸改为非对称判据"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "本次改动为后端行为调整(错误码判定条件变化),网关无需变更。gateway_status not_required 意味着网关侧无需部署(网关只转发,错误码处理由后端定义)。"
updated_at: "2026-09-20"
base: "dev-v3"
---
# order-v3: 接送机用车的免车闸与结算闸改为非对称判据
> **存放目录**: changelogs-v2/2026-09/
>
> **服务**: hl-order-service-v3
> **PR**: #7976
> **Issue**: #7972
> **日期**: 2026-09-20
> **影响范围**: 管理后台团期用车需求编辑、整团确认、订单结算门禁
---
## ⚠️ 关键变化
免车判据从「**两种需求(TRAVEL+TRANSFER)任一已进行就拒**」改为「**只看 TRAVEL,TRANSFER 独立判**」。
- **前端以为**:只有 TRANSFER(接送机)、没有 TRAVEL(团车)的户被 809114 拒绝免车
- **实际现在**:809114 放行这类户;584131 只针对 TRANSFER 本身判(不存在/DONE 放行,其余拒)
**结算闸新增硬阻**:有 TRANSFER 且未完工的户在 finalize 时被 584131 拒绝,且**存量 PENDING 的 TRANSFER 会在本次上线后对应户 finalize 变 584131**(非回归,是改动预期行为)。
---
## 一、背景
**只订接送机、不订团车** 是合法的在团户(存在真实订单),但被原判据的两支 OR 条件挡住。
免车意在「本团无需用车」,而只有 TRANSFER 的户仍有接送机需求,整团免车不能替接送机放行。
改为非对称判据后:整团免车只豁免 TRAVEL,接送机必须逐户推进到 DONE。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存团期用车需求 | PUT | `/admin/group-batch/{groupBatchId}/vehicle-requirement` | 行为变化 | 809114 放宽(不挡仅 TRANSFER 户) |
| 2 | 整团确认 | POST | `/admin/group-batch/{groupBatchId}/vehicle-requirement/confirm` | 行为变化 | 同上 |
| 3 | 订单结算 | POST | `/admin/order/{orderId}/submit` | 行为变化 | 新增 584131 拒绝条件 |
| 4 | 读团期用车需求 | GET | `/admin/group-batch/{groupBatchId}/vehicle-requirement` | 行为变化 | 只读,无副作用 |
---
## 三、接口详情
### 1. 保存团期用车需求 `PUT /admin/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO`
#### 使用场景
管理员在团期用车需求编辑页保存草稿或点「整团免车」。
本次改动只影响点「整团免车」按钮时的 809114 门禁判定:只有 TRANSFER(接送机)、没有 TRAVEL(团车)的户现在能通过。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
| reason | Body | String | ✅ | 不含空格、不超 200 字 | 免车原因(写入备注留痕) |
| operatorId | 上下文 | String | ✅ | 取决于登录用户 | 操作人 ID(管理员工号) |
#### 出参 `Result<GroupVehicleRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | Long | 正式需求聚合主键 |
| status | String | 状态(DRAFT/CONFIRMED/DISPATCHED/DONE) |
| groups | Array | 乘车分组列表,免车声明时为空数组 |
| version | Integer | 乐观锁版本号 |
| confirmedBy | String | 确认人 ID(免车时为当前操作人) |
| confirmedAt | LocalDateTime | 确认时刻 |
#### 请求示例
```json
{
"reason": "只订接送机,不需要团车"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": 7200000000000001,
"groupBatchId": 7100000000000001,
"status": "CONFIRMED",
"groups": [],
"version": 2,
"confirmedBy": "admin001",
"confirmedAt": "2026-09-20 10:30:00",
"remark": "[2026-09-19 15:22:00 撤回] 改房型; [2026-09-20 10:30:00 免车] 只订接送机,不需要团车"
},
"success": true
}
```
#### 空数据 / 降级响应
无(必返回需求对象)。
#### 错误响应
```json
{
"code": 809114,
"message": "车务已开工(子订单 5100000000000001:PROCESSING),不能再声明整团免车",
"success": false,
"data": null
}
```
#### 业务边界
- **809114 新判据**(仅 TRAVEL 分支):检查全部在团户的 active TRAVEL(非 TRANSFER)需求,若任一已进入 PENDING/PROCESSING/DONE,整团免车被拒
- **TRANSFER 不挡免车**:即使某户有 active TRANSFER 处于上述状态,809114 不再拦住,转由结算闸(584131)独立硬阻
- **幂等**:整团已经是免车态(零分组已确认版本)时重复点免车返回 200,不抛错、不推版本
- **无副作用**:本端点不调 fleet,不修改户级需求状态,只操作团期层面的正式需求聚合根
---
### 2. 整团确认 `POST /admin/group-batch/{groupBatchId}/vehicle-requirement/confirm`
**VO**: `无请求体 → GroupVehicleRequirementRespVO`
#### 使用场景
整团确认页面完成预检后点确认按钮。整团确认作为保存需求的上游,同样需要过 809114 门禁(只看 TRAVEL)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
#### 出参 `Result<GroupVehicleRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | Long | 正式需求聚合主键 |
| status | String | 状态(DRAFT/CONFIRMED/DISPATCHED/DONE) |
| groups | Array | 乘车分组列表 |
| version | Integer | 乐观锁版本号 |
| confirmedBy | String | 确认人 ID |
| confirmedAt | LocalDateTime | 确认时刻 |
#### 请求示例
```json
{}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": 7200000000000001,
"status": "CONFIRMED",
"groups": [
{
"groupCode": "BUS",
"memberCount": 4,
"days": [
{ "tripDate": "2026-10-01", "headcount": 4 }
]
}
],
"version": 1,
"confirmedBy": "admin002",
"confirmedAt": "2026-09-20 11:00:00"
},
"success": true
}
```
#### 空数据 / 降级响应
无。
#### 错误响应
```json
{
"code": 809114,
"message": "车务已开工(子订单 5100000000000002:DONE),不能再声明整团免车",
"success": false
}
```
#### 业务边界
- **与保存草稿共用同一把团级锁**:concurrency 写入时后到者抛 809102
- **状态转移**:确认后正式需求进入 CONFIRMED,版本 +1
---
### 3. 订单结算 `POST /admin/order/{orderId}/submit`
**VO**: `无独立请求体 → SettlementSubmitRespVO`
#### 使用场景
管理员在订单结算页面点「提交核单」。本次改动新增 584131 硬阻:有 TRANSFER(接送机)且未完工的户无法 finalize。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | - | 子订单 ID |
#### 出参 `Result<SettlementSubmitRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | Long | 子订单 ID |
| status | String | finalize 后的订单状态 |
#### 请求示例
```json
{}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": 5100000000000001,
"status": "SETTLED"
},
"success": true
}
```
#### 空数据 / 降级响应
无。
#### 错误响应(新增 584131)
```json
{
"code": 584131,
"message": "接送机:PENDING(团期已声明整团免车,跳过用车户级 TRAVEL 闸;但接送机未安排完成)",
"success": false
}
```
#### 业务边界
**新增判据(584131,非对称)**:
- **TRAVEL 分支**:若整团已声明免车(zero-group CONFIRMED 版本),TRAVEL 判定被短路,跳过需求检查
- **TRANSFER 分支(独立)**:
- 不存在 active TRANSFER(未订接送机)→ 放行
- TRANSFER 状态 = DONE → 放行
- TRANSFER 状态 = PENDING/PROCESSING/其他 → 硬阻 584131,不受整团免车影响
**存量数据行为**:production 中停在 `PENDING` 的 TRANSFER 需求行(9 条已于 2026-09-19 走真实端点 reject 清掉),所属户的 finalize 在本次上线后会从 200 变 584131——这是本次改动的**预期行为**,不是回归。
---
### 4. 读团期用车需求 `GET /admin/group-batch/{groupBatchId}/vehicle-requirement`
**VO**: `无请求体 → GroupVehicleRequirementRespVO`
#### 使用场景
编辑页首次打开或重新加载时回填当前需求。本接口是配车刷新状态在 admin 侧唯一的**无副作用观测口**(#7988),不取锁、不进事务、无任何写操作。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
#### 出参 `Result<GroupVehicleRequirementRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | Long | 正式需求聚合主键 |
| status | String | 状态(DRAFT/CONFIRMED/DISPATCHED/DONE) |
| groups | Array | 乘车分组列表 |
| version | Integer | 乐观锁版本号 |
| confirmedBy | String | 确认人 ID |
| confirmedAt | LocalDateTime | 确认时刻 |
#### 请求示例
```json
{}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"requirementId": 7200000000000001,
"status": "CONFIRMED",
"groups": [],
"version": 2,
"confirmedBy": "admin001",
"confirmedAt": "2026-09-20 10:30:00",
"planRefreshStatus": "FAILED",
"planRefreshFailReason": "fleet timeout"
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 错误响应
```json
{
"code": 589500,
"message": "团期不存在",
"success": false,
"data": null
}
```
#### 业务边界
- **零写入**:不锁、不改库、不进事务、不调 fleet、不推版本
- **存在性**:返回 null 表示「尚未形成正式需求」(正常),不区分「本该有但丢了」
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | 端点 | payload | 结果 |
|------|------|---------|------|
| ✅ 仅 TRANSFER,免车通过 | PUT `/admin/group-batch/{id}/vehicle-requirement` | `{"reason":"只订接送机"}` | 200,状态 CONFIRMED,groups=[] |
| ✅ 仅 TRAVEL,整团确认通过 | POST `/admin/group-batch/{id}/vehicle-requirement/confirm` | (无体) | 200,状态 CONFIRMED,groups=[{BUS}] |
| ❌ 仅 TRANSFER,TRAVEL 已 PROCESSING,免车拒绝 | PUT `/admin/group-batch/{id}/vehicle-requirement` | `{"reason":"..."}` | 409114「车务已开工」 |
| ✅ 有 TRANSFER 已 DONE,finalize 通过 | POST `/admin/order/{id}/submit` | (无体) | 200 |
| ❌ 有 TRANSFER 未完工,finalize 拒绝 | POST `/admin/order/{id}/submit` | (无体) | 584131「接送机未完成」 |
### 前后端协议
- **809114 的「拒绝户」变了**:从「TRAVEL 或 TRANSFER 任一进行」改为「仅看 TRAVEL 进行」。前端如有针对 809114 的兜底提示,需复查文案准确性
- **584131 是新条件**:不是对 809114 的替代,而是针对 TRANSFER 独立的硬阻(结算端)
- **两道分离**:免车判定(团期层,809114)与结算判定(订单层,584131)是两套独立的门禁,分别对 TRAVEL 与 TRANSFER 行为
---
## 五、数据库行为
### 免车声明
| 前置条件 | 结果 |
|----------|------|
| 用户A: TRAVEL=PENDING, TRANSFER=null | 809114 拒绝(TRAVEL 已进行) |
| 用户B: TRAVEL=null, TRANSFER=PENDING | 809114 放行(TRANSFER 不挡) → 免车声明成功 |
| 用户C: TRAVEL=DONE, TRANSFER=DONE | 809114 拒绝(TRAVEL 已进行) |
### finalize 闸门
| 前置条件 | 结果 |
|----------|------|
| TRANSFER=null | 584131 放行 |
| TRANSFER=DONE | 584131 放行 |
| TRANSFER=PENDING | 584131 拒绝(新判据) |
### 版本化
- 整团免车时若目标版本是「带分组已确认」,会先失活旧版本(CAS),再插入一条「零分组已确认」的新版本
- `version` 字段自动 +1,乐观锁冲突返 809102
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 团期不存在 → 589500
- 正式需求不存在 → 809100
- 下游 fleet 超时 → 返 200,不阻断 finalize(结算与车配解耦)
- 团期被锁(并发写) → 等待后重试
---
## 六.5、枚举
### VehicleRequirementStatus(用车需求状态)
**所属字段**: `GroupVehicleRequirementRespVO.status` | **类型**: `String`
| 值 | 中文 | 说明 |
|-----|------|------|
| `DRAFT` | 草稿 | 编辑中 |
| `CONFIRMED` | 已确认 | 整团确认/免车声明后 |
| `DISPATCHED` | 已派发 | fleet 配车完成 |
| `DONE` | 已完成 | 订单 finalize 后 |
| `PENDING_RECONFIRM` | 待重确认 | 被打回重新编辑后的中间态 |
### VehicleRequirementKind(需求类型)
**所属字段**: 内部枚举,不直接暴露给前端 | **类型**: `String`
| 值 | 中文 | 说明 |
|-----|------|------|
| `TRAVEL` | 团车 | 整团用车需求 |
| `TRANSFER` | 接送机 | 机场接送 |
---
## 六.6、修改前后对比
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 809114 判定 | 检查 TRAVEL 和 TRANSFER,任一进行则拒 | 只检查 TRAVEL,TRANSFER 不挡 |
| 接送机只户的免车 | 被 809114 拒绝 | 809114 放行(若 TRANSFER 未完工则被 584131 拒) |
| finalize 对 TRANSFER | 无专项检查 | 新增 584131:不存在/DONE 放行,其余拒 |
| 整团免车对 TRANSFER | 整户豁免 | 不豁免,TRANSFER 仍需逐户到 DONE |
| flow 推进(行程清单) | 只看 TRAVEL | 追加 TRANSFER 检查:即使整团免车也要看 TRANSFER |
---
## 六.7、影响评估
- **是否破坏向后兼容**:是(行为变化)
- **前端是否必须同步上线**:否(后端改动前兼容,后端改动后错误码体验改善但不影响流程)
- **前端 workaround 清理点**:
- 若有针对 809114「TRANSFER 已进行」的兜底提示,改成只提「TRAVEL 已进行」
- 若有假设「整团免车豁免一切需求」的逻辑,补充 TRANSFER 的检查(与结算闸 584131 同口径)
---
## 七、不影响范围
- **仅影响**:管理后台团期车需求编辑、整团确认、订单结算三处门禁
- **零影响**:
- 小程序(MP 端点)
- 户级用车需求的创建/更新(只改 validation 判定,不改流程)
- 价格、保险、酒店等其他模块
- 一期业务(dev 分支无此改动)
---
## 八、测试环境已验证
部署版本: `8cdb9ad97`(包含提交 572dc4037)
```
✓ PUT /admin/group-batch/7100000000000001/vehicle-requirement
{"reason":"只订接送机"}
→ 200 + status=CONFIRMED + groups=[]
✓ POST /admin/group-batch/7100000000000001/vehicle-requirement/confirm
→ 200 + CONFIRMED
✓ POST /admin/order/5100000000000001/submit
(TRANSFER=DONE)
→ 200
✓ POST /admin/order/5100000000000002/submit
(TRANSFER=PENDING)
→ 584131 (新判据)
✓ GET /admin/group-batch/7100000000000001/vehicle-requirement
→ 200 + 当前需求
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|----------|
| #7763 | #7441 PR-3 | 首次引入 584131 硬拦「缺团车需求」 | ✅ 有效(本次改动收紧范围,只看 TRANSFER) |
| #8033 | #7988 | 补只读观测口(本端点) | ✅ 有效 |
| **本 PR #7976** | **#7972** | 收口 809114/584131 的 kind 口径,非对称判定 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#7972](https://git.1814.love:8443/wx/HL/issues/7972)
- 关联 PR: [wx/HL#7976](https://git.1814.love:8443/wx/HL/pulls/7976)
- Merge commit: [wx/HL@572dc4037](https://git.1814.love:8443/wx/HL/commit/572dc4037)
---
## 关联 / 联系人
### 链接
- **Issue**: [#7972](https://git.1814.love:8443/wx/HL/issues/7972)
- **PR**: [#7976](https://git.1814.love:8443/wx/HL/pulls/7976)
- **Merge commit**: [572dc4037](https://git.1814.love:8443/wx/HL/commit/572dc4037)
### 联系人
- **后端负责人**: @wx