docs(changelog): #8023 需求汇总与确认预检不再只认 needs_hotel 标记位(修改接口)
changelog-filename-gate / validate (push) Failing after 1s
changelog-filename-gate / validate (push) Failing after 1s
这个提交包含在:
@@ -0,0 +1,266 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7965"
|
||||
title: "改派幂等重放:assignmentSlotId 不再被抹成 null,重放与首调同值"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "改派 POST /admin/fleet/assignments/{assignmentId}/change 带 requestId 时,同一 requestId 的幂等重放此前恒返回 assignmentSlotId=null——而响应用 NON_NULL 省略 null,等于这个字段在重放的响应体里直接消失,首调却带着真实值。本次把回执冻结口径改回照抄首调的值,重放与首调返回同一个 assignmentSlotId。入参、路径、其余出参字段、错误码、判权一律不变;首次调用的行为也完全不变,只有「同 requestId 重放」这一条路径的返回值变了。后端已合并 dev-v3(d31bb3209)并部署 TEST。"
|
||||
updated_at: "2026-09-20"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# fleet: 改派幂等重放不再抹掉 assignmentSlotId
|
||||
|
||||
> **存放目录**: `changelogs-v2/{YYYY-MM}/`(管理后台)
|
||||
>
|
||||
> **服务**: hl-fleet-service (端口 8087)
|
||||
> **PR**: [#7997](https://git.1814.love:8443/wx/HL/pulls/7997)
|
||||
> **Issue**: [#7965](https://git.1814.love:8443/wx/HL/issues/7965)
|
||||
> **日期**: 2026-09-20
|
||||
> **影响范围**: 派车「改派」提交的**重试/重放**路径
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. **只有「同一 `requestId` 重放」这条路径的返回值变了**:`assignmentSlotId` 从恒 `null`(响应体里该字段直接消失)改为**与首次调用同值**。
|
||||
2. **首次调用的行为一个字都没变**:它本来就返回真实值。
|
||||
3. **不是新增字段、不是删字段**:`assignmentSlotId` 一直在响应 VO 里;变的是它在重放时的取值。
|
||||
4. **入参、路径、方法、其余出参、错误码、判权全部不变。**
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
改派带 `requestId` 是幂等键:网络重试、前端重复提交时,第二次调用不会再改一次派单,而是把首次冻结的结果原样重放回来。
|
||||
|
||||
`#7067`「派单去槽位化」把回执写侧的 `assignmentSlotId` 无条件冻结成了 `null`,而产出侧(`AssignmentService` → `AssignmentConverter` → `ChangeAssignmentRespVO`)仍在计算并返回真实值。于是**同一个 `requestId` 调两次,拿到两份不一样的「同一个结果」**:首调有这个字段,重放没有(响应用 `NON_NULL` 省略 null,字段整个不出现)。
|
||||
|
||||
这不是「哪个值才对」的问题——`assignmentSlotId` 该不该退役是另一件事;**错的是两条路径不同值**,这是幂等回执的定义性失败。本次只把两侧拉齐:回执照抄首调的值。将来若真要退役这个字段,写口与回执一起退,两侧仍然同值。
|
||||
|
||||
缺陷自 2026-09-06(`913062466`)起带病运行 13 天没被发现,因为唯一能抓住它的用例 `AssignmentChangeReceiptMysqlTest` 被系统属性 `fleet.mysql.provider` 门控,平时整类跳过、PR 全绿。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 修改派单(改派) | POST | `/admin/fleet/assignments/{assignmentId}/change` | 修改 | 同 `requestId` 幂等重放的 `assignmentSlotId` 由恒 `null` 改为与首调同值 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 修改派单(改派) `POST /admin/fleet/assignments/{assignmentId}/change`
|
||||
|
||||
**VO**: `Result<ChangeAssignmentRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
派车管理后台改派:按车辆槽位和生效日换车、换司机或同时替换。前端提交时带上本次操作的 `requestId`;网络超时重试或用户重复点击时,用同一个 `requestId` 再调一次,后端不会重复改派,而是把首次的结果重放回来。
|
||||
|
||||
#### 入参
|
||||
|
||||
本次入参**一个没动**,下表只列与本次相关的字段。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| assignmentId | Path | Long | ✅ | 当前车辆槽位任一派单 ID | 不存在返回 605009(不变) |
|
||||
| effectiveDate | Body | LocalDate | ✅ | 须落在该派单服务日期区间内 | 不在区间返回 605028(不变) |
|
||||
| requestId | Body | String | ❌ | 幂等键,由前端为「本次操作」生成一次 | 传了才有重放语义;同键不同业务载荷返回 605059(不变) |
|
||||
|
||||
#### 出参
|
||||
|
||||
仅列与本次相关的字段,其余出参(`assignmentId`、`assignmentStatus`、`effectiveDate`、`affectedDays`、`protocolPrice`、`vehicleFeeTotal`、`dailyVehicleFees`、`otherVehicles`、`warningCode`、`sendItinerarySms` 等)本次**一个没动**。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| assignmentSlotId | Long | 展示用历史槽位号,不参与任何判定。**本次变更点**:首调一直是真实值;同 `requestId` 重放此前恒 `null`(字段在响应体里消失),现在与首调同值 |
|
||||
| previousAssignmentGroupId | Long | 改派前的派车组身份(不变) |
|
||||
| newAssignmentGroupId | Long | 改派后的新派车组身份(不变) |
|
||||
| assignmentStatus | String | 改派后派单状态(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"effectiveDate": "2027-08-28",
|
||||
"newDriverId": 2089686360926380033,
|
||||
"reason": "#7965 AC-1 取证",
|
||||
"requestId": "ac1-7965-20260920"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
(同一个 `requestId` 连调两次,两次响应体一致)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"assignmentId": 359948910304825344,
|
||||
"assignmentSlotId": 359947685400285184,
|
||||
"previousAssignmentGroupId": 359947685400285184,
|
||||
"newAssignmentGroupId": 359948910271270912,
|
||||
"assignmentStatus": "assigned",
|
||||
"effectiveDate": "2027-08-28",
|
||||
"affectedDays": 1
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口不返回空数据形态:要么改派成功返回上述结构,要么返回错误码。
|
||||
|
||||
`assignmentSlotId` 本身**可以是 `null`**(该派单没有历史槽位号、也取不到锚点组身份时),此时首调与重放**同为 `null`**——不变量是「两次同值」,不是「一定非空」。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"assignmentId": 359948910304825344,
|
||||
"previousAssignmentGroupId": null,
|
||||
"newAssignmentGroupId": 359948910271270912,
|
||||
"assignmentStatus": "assigned"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:响应用 `NON_NULL` 省略 null 字段,所以 `assignmentSlotId` 为 null 时**该键不出现**,不是出现一个 `null`。前端取值要按「键可能不存在」写。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 605059, "message": "同一请求标识的业务载荷不一致", "data": null, "success": false }
|
||||
```
|
||||
|
||||
| code | 触发条件 |
|
||||
|------|----------|
|
||||
| 605059 | 同 `requestId` 但业务载荷不同(本次不变,且不产生改派副作用) |
|
||||
| 605009 | 派单不存在(本次不变) |
|
||||
| 605020 | 当前状态不允许改派(本次不变) |
|
||||
| 605028 | 生效日不在派单服务日期范围内(本次不变) |
|
||||
| 605041 | 最终基线复核不一致,`data.dailyDifferences` 给差异(本次不变) |
|
||||
| 401 | 未登录(网关拦截) |
|
||||
|
||||
**本次不新增、不修改任何错误码。**
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **不变量是「同 `requestId` 的首调与重放返回同一个 `assignmentSlotId`」**,不是「该字段一定非空」。
|
||||
- 该字段是**展示用历史槽位号**,`#7067` 之后不参与任何判定;**不要拿它当主键、当筛选键、当幂等键**。
|
||||
- 不同 `requestId` 的两次改派是两次独立操作,`assignmentSlotId` 本来就可能不同,不在本不变量范围内。
|
||||
- 同 `requestId` + 不同业务载荷仍返回 605059,且不产生任何改派副作用(不变)。
|
||||
- **不传 `requestId` 时没有重放语义**,每次调用都是一次真实改派。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| ✅ 改派提交 | 为「本次操作」生成一个 `requestId`,重试时**原样复用**,不要每次重新生成 |
|
||||
| ✅ 重试后读 `assignmentSlotId` | 直接用,现在与首调同值;**不需要**为重放路径写兜底 |
|
||||
| ✅ 渲染 `assignmentSlotId` | 按「键可能不存在」判空(该字段为 null 时整个键被省略) |
|
||||
| ❌ 把 `assignmentSlotId` 当业务主键 / 幂等键 / 筛选键 | 它是展示用历史槽位号,不参与判定;身份走 `newAssignmentGroupId` |
|
||||
| ❌ 重试时换一个新 `requestId` | 会被当成一次新的改派,真的再改一次 |
|
||||
| ❌ 依赖「重放时该字段缺失」来区分首调与重放 | 旧行为,已被本次修复取消;要区分请用前端自己的请求状态 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
**无表变更、无 Flyway 迁移、无索引变更。**
|
||||
|
||||
- 唯一变化是改派回执表 `fleet_assignment_change_receipt` 的 `outcome_json` 列**新写入的内容**:`assignmentSlotId` 从「不写(NON_NULL 省略)」变为「照抄首调的值」。列本身不变,`outcome_schema_version` 仍是 2(字段集没变,只是这个键从恒缺失变为有值;旧回执缺该键仍反序列化为 null,向后兼容)。
|
||||
- **存量回执不订正**(详见「六、边界行为」):2026-09-06 ~ 2026-09-20 修复部署前落库的回执行,`outcome_json` 里没有这个键,重放它们仍返回 null。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **同 `requestId` 重放** → `assignmentSlotId` 与首调同值(本次修复点)。
|
||||
- **该字段本身为 null**(无历史槽位号且取不到锚点组身份)→ 首调与重放同为 null,两次响应体里该键都不出现,不变量仍成立。
|
||||
- **同 `requestId` + 不同载荷** → 605059,不产生改派副作用(不变)。
|
||||
- **不传 `requestId`** → 无重放语义,每次都是真实改派(不变)。
|
||||
- **存量回执(修复部署前落库的)** → 重放仍返回 null,**本次不订正**。理由:回执的语义是「冻结首调那一刻的答案」,事后回填等于用推导值覆盖冻结结果;而 `requestId` 的实际重放窗口是「调用方当场重试」的秒级,存量行早已过了这个窗口,订正的收益接近零、破坏冻结语义的代价是实在的。存量读数与可反推结论见工单 #7965 AC-5。
|
||||
- **改派本身的所有其他行为**(基线复核 605041、旧 HOLD 605042、车费校验 605045/605049、全程槽 605064、行程短信 `sendItinerarySms`、`ORDER_HAS_OTHER_VEHICLES` 提示)**一律不变**。
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `assignmentSlotId`(**首次调用**) | 真实值 | 真实值(**不变**) |
|
||||
| `assignmentSlotId`(**同 requestId 重放**) | 恒 `null`,`NON_NULL` 省略后该键在响应体里**不出现** | 与首调**同值**(首调为 null 时同为 null) |
|
||||
| 其余出参字段 | — | 不变(无新增、无删除、无改名、无类型变化) |
|
||||
| 入参 / 路径 / 方法 / 错误码 / 判权 | — | 不变 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 场景 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 前端提交改派后超时重试(同 `requestId`) | 第二次响应里少一个字段,页面上槽位号「重试一下就没了」 | 两次响应体一致 |
|
||||
| 调用方比对两次响应做一致性校验 | 必然不一致,且不报错(静默) | 一致 |
|
||||
| 修复部署前落库的旧 `requestId` 重放 | 返回 null | **仍返回 null**(存量不订正) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否。首次调用的行为完全不变;重放路径是从「少一个字段」变成「字段齐全」,是补齐不是削减。
|
||||
- **前端是否必须同步上线**: 否。此前若为重放路径写过「该字段可能缺失」的兜底,**那段兜底可以保留**(字段为 null 时仍会缺失),不需要改。
|
||||
- **回滚**: 回滚 PR #7997 即可。回滚后新写入的回执重新变回不带该字段;已写入的带值回执不受影响(读侧照常反序列化)。
|
||||
- **风险**: 低。改动是回执写侧一行取值,不触及改派本身的任何判定、状态机与资源占用。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 本接口在「同 `requestId` 幂等重放」路径上 `assignmentSlotId` 的取值。
|
||||
- **零影响**: 首次改派的全部行为与返回值;派单创建 / 取消 / 拒接 / 确认;车费计算与价格日历;行程短信;资源占用与 CAS;判权(`/admin/fleet/**` 仍要求 `VEHICLE_MANAGER` 或 `SUPER_ADMIN`);网关路由;数据库表结构。
|
||||
- `#7067`「槽位身份退役」的其余部分不在本次范围——该字段目前仍是**半退役**状态(`src/main` 里仍有多处写口),本次不推进也不回退它。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
<!-- AC-1 实测读数待填 -->
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#7965](https://git.1814.love:8443/wx/HL/issues/7965)
|
||||
- 关联 PR: [wx/HL#7997](https://git.1814.love:8443/wx/HL/pulls/7997)
|
||||
- 缺陷引入来源 `#7067` 派单去槽位化: [wx/HL#7067](https://git.1814.love:8443/wx/HL/issues/7067)
|
||||
- 门控测试类结构性盲区的处理见 [wx/HL#7444](https://git.1814.love:8443/wx/HL/issues/7444)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#7965](https://git.1814.love:8443/wx/HL/issues/7965)
|
||||
- **PR**: [#7997](https://git.1814.love:8443/wx/HL/pulls/7997)
|
||||
- **Merge commit**: [d31bb3209](https://git.1814.love:8443/wx/HL/commit/d31bb3209)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
- **前端负责人**: @mmg
|
||||
@@ -0,0 +1,430 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8023"
|
||||
title: "需求汇总与确认预检不再只认 needs_hotel 标记位 + 提交住宿需求时就地纠正该标记"
|
||||
consumer: "admin"
|
||||
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-20"
|
||||
status_note: "后端已合并 dev-v3(df66ec357)并部署 TEST,网关实测 AC-1~AC-8 全通过。一处口径放宽 + 一个纯新增出参 + 一处写侧副作用。口径:全团需求汇总 GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary 的 dailyRoomBreakdown 与整体确认预检 GET .../requirement/confirm-check,此前都只认订单上的 needs_hotel 标记位,导致「标记说不需要住宿、定制师却已提交完整需求」的户被整户静默丢弃——间数不进汇总(页面用房表空白)、确认时也不放行给房务(需求填了没人配房);现在计入条件改为「标记为真 或 已提交有效需求」,打回态仍不计。新增出参 hotelFlagMismatchOrderCount 把这种不一致显式暴露。写侧:PUT /v3/admin/order/{id}/hotel-requirement 提交成功后,若该单 needs_hotel 不为真则就地置 1(单向 0→1,同事务,已为真时不写)——该标记原先只在创单时按产品有没有配酒店派生一次、之后全仓无写通道,存量错单改不回来。既有字段一个没删没改,入参与路径不变。"
|
||||
updated_at: "2026-09-20"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 需求汇总/预检不再只认 needs_hotel,提交需求时就地纠正该标记
|
||||
|
||||
> **服务**: hl-order-service-v3 (端口 8086)
|
||||
> **PR**: [#8028](https://git.1814.love:8443/wx/HL/pulls/8028)
|
||||
> **Issue**: [#8023](https://git.1814.love:8443/wx/HL/issues/8023)
|
||||
> **日期**: 2026-09-20
|
||||
> **影响范围**: 管理后台「团期订单 → 查看需求」页的用房汇总、整体确认预检;定制师提交住宿需求的副作用
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
- **不再只认标记位**:汇总与预检的计入条件从「`needs_hotel=1`」放宽为「**标记为真 或 已提交有效需求**」。需求是事实,标记只是创单时的推测,两者冲突时以事实为准。
|
||||
- **新增出参 `hotelFlagMismatchOrderCount`**:「标记为假却已提交有效需求」的户数,把这种不一致显式暴露出来,不再静默吞掉。
|
||||
- **`hotelNeededOrderCount` 口径同步放宽**:改为「标记为真的户 ∪ 已提交有效需求的户」,保证 `hotelSubmittedOrderCount ≤ hotelNeededOrderCount` 恒成立。
|
||||
- **提交住宿需求会就地纠正标记**:`needs_hotel` 不为真时置 1,单向、同事务、已为真时不写。
|
||||
- **打回态仍不计**:#7925 立的那条判定原样保留,不受本次放宽影响。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
`needs_hotel` 只在**创建订单那一刻**由「产品有没有配酒店 / 有没有默认房数」派生一次,之后**全仓没有任何写通道**(`OrderCreateTransactionExecutor.deriveNeedsHotel`)。产品当时没配酒店、后来补配了,或定制师按客户实际情况提了住宿需求,这个标记都改不回来。
|
||||
|
||||
而汇总与预检都按它筛户,于是出现了自相矛盾的一屏:子订单列表显示该户「待审核」,需求汇总却说这个团「0 户需要住宿」,用房表全空。
|
||||
|
||||
TEST 实证(团期 `jw测试1期`):三户 `needs_hotel=0`,其中一户已提交 6 晚 12 间的 `PENDING_REVIEW` 需求,改前 `dailyRoomBreakdown` 为空数组。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 全团需求汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement-summary` | 修改 | 计入户口径放宽为「标记为真或已提交有效需求」;新增 1 个出参 |
|
||||
| 2 | 整体确认预检 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check` | 修改 | 取户口径同步放宽,字段结构不变 |
|
||||
| 3 | 提交住宿需求 | PUT | `/v3/admin/order/{id}/hotel-requirement` | 修改 | 新增副作用:提交成功后就地纠正 needs_hotel |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 全团需求汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary`
|
||||
|
||||
**VO**: `Result<GroupRequirementSummaryRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期详情「查看需求」页顶部的「用房 · 汇总」表:全团逐日要订几间、什么房型,运营据此向酒店报数。
|
||||
|
||||
#### 入参
|
||||
|
||||
本次入参**不变**。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期 ID | 团期不存在返回 589500 |
|
||||
|
||||
#### 出参
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| activeOrderCount | Integer | 在团户数(不变) |
|
||||
| hotelRequirementCount | Integer | 有效房需求条数(不变;不看标记位也不看打回状态) |
|
||||
| hotelNeededOrderCount | Integer | **口径放宽**:标记为真的户 **∪** 已提交有效需求的户 |
|
||||
| hotelSubmittedOrderCount | Integer | **口径放宽**:不再要求标记为真,只看需求是否有效 |
|
||||
| hotelFlagMismatchOrderCount | Integer | **新增**。标记不为真却已提交有效需求的户数,恒 ≥0 |
|
||||
| vehicleRequirementCount | Integer | 有效用车需求条数(不变) |
|
||||
| dailyRoomBreakdown | List | 逐日房间明细(**计入户集合本次放宽**) |
|
||||
| dailyRoomBreakdown[].dayNumber | Integer | 第几天(不变) |
|
||||
| dailyRoomBreakdown[].rooms[].roomCategory | String | 房型编码(不变) |
|
||||
| dailyRoomBreakdown[].rooms[].roomCategoryName | String | 房型中文名(不变,#7925 引入) |
|
||||
| dailyRoomBreakdown[].rooms[].totalRoomCount | Integer | 该天该房型合计(不变,数值因放宽可能变大) |
|
||||
| vehicleSeatSummary | List | 车型座位合计(不变) |
|
||||
| orderSpecialTags | List | 各户特殊需求标签(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2101506167098511362/requirement-summary
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"activeOrderCount": 3,
|
||||
"hotelRequirementCount": 1,
|
||||
"hotelNeededOrderCount": 1,
|
||||
"hotelSubmittedOrderCount": 1,
|
||||
"hotelFlagMismatchOrderCount": 1,
|
||||
"vehicleRequirementCount": 0,
|
||||
"dailyRoomBreakdown": [
|
||||
{"dayNumber": 1, "rooms": [{"roomCategory": "KING", "roomCategoryName": "豪华大床", "totalRoomCount": 2}]}
|
||||
],
|
||||
"vehicleSeatSummary": [],
|
||||
"orderSpecialTags": []
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
全团无有效需求且无标记为真的户时,三个户数均为 `0`、`dailyRoomBreakdown` 为 `[]`(**不是 null**),`hotelFlagMismatchOrderCount` 为 `0`,且不调房型字典。房型字典不可用时只有 `roomCategoryName` 为 `null`,编码与房间数照常返回,接口不报错。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null, "success": false }
|
||||
```
|
||||
|
||||
| code | 触发条件 |
|
||||
|------|----------|
|
||||
| 589507 | 当前角色没有 `group-batch:view`(本次不变) |
|
||||
| 589500 | 团期不存在(本次不变) |
|
||||
| 401 | 未登录(网关拦截) |
|
||||
|
||||
本次无新增错误码。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **计入的户**:标记为真 **或** 已提交有效需求(有效 = `PENDING_REVIEW` / `PENDING` / `PROCESSING` / `DONE`)。
|
||||
- **打回态仍不计**:退回定制师、退回管理员两种状态不是「有效需求」,不能靠它把户拉进计入集合。
|
||||
- **客户自订晚**整晚跳过,不进采购分母(不变)。
|
||||
- **户范围**仍是「在团」口径(含已完成的户),与预检刻意不同(#7316)。
|
||||
- `hotelFlagMismatchOrderCount` 仅用于提示与订正,**不要**用它去扣减间数——那批户的间数已经正常计入。
|
||||
|
||||
### 2. 整体确认预检 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm-check`
|
||||
|
||||
**VO**: `Result<GroupBatchRequirementCheckRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
团期管理员点「确认需求」之前的缺失预检;确认时用同一份判定决定放行哪些户的需求给房务。
|
||||
|
||||
#### 入参
|
||||
|
||||
本次入参**不变**。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| groupBatchId | Path | Long | ✅ | 团期 ID | 团期不存在返回 589500 |
|
||||
|
||||
#### 出参
|
||||
|
||||
字段结构**不变**(`ready` / `missing[]` / `checkedResourceTypes` 等一个没动)。变的是**取户口径**。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| ready | Boolean | 是否可确认(不变) |
|
||||
| missing | List | 缺失清单(结构不变;**新增**「标记为假但已提交」的户参与判定后,其缺项会如实出现在这里) |
|
||||
| checkedResourceTypes | List | 本次检查的资源类型(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2101506167098511362/requirement/confirm-check
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2101506167098511362",
|
||||
"batchStatus": "RECRUITING",
|
||||
"batchStatusName": "招募中",
|
||||
"ready": false,
|
||||
"missing": [],
|
||||
"checkedResourceTypes": ["HOTEL", "VEHICLE"]
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
无需订房户时 `missing` 为 `[]`(不是 null),`ready` 仍按车侧结果给出,行为不变。团内一户都没有时同样返回空清单而非报错。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 589500, "message": "团期不存在", "data": null, "success": false }
|
||||
```
|
||||
|
||||
| code | 触发条件 |
|
||||
|------|----------|
|
||||
| 589500 | 团期不存在(本次不变) |
|
||||
| 589507 | 无团期权限(本次不变) |
|
||||
|
||||
本次无新增错误码。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **为什么必须跟着汇总一起放宽**:只放宽汇总会变成「间数算进汇总了、确认时却不放行给房务」——需求填了、汇总也算了、房务收不到,比改前更难排查。
|
||||
- 放宽后这些户也参与「房型行填齐」「晚数与行程一致」等校验,填不全会如实报缺,这是应有的行为。
|
||||
- 打回态的户仍不参与,避免逼管理员对一份正在返工的需求反复确认。
|
||||
|
||||
### 3. 提交住宿需求 `PUT /v3/admin/order/{id}/hotel-requirement`
|
||||
|
||||
**VO**: `Result<HotelRequirementRespVO>`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
定制师逐晚填报住宿需求并提交。
|
||||
|
||||
#### 入参
|
||||
|
||||
本次入参**不变**。
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| id | Path | Long | ✅ | 订单 ID | 订单不存在返回 582001 |
|
||||
| days | Body | List | ✅ | 逐晚需求 | 结构不变 |
|
||||
|
||||
#### 出参
|
||||
|
||||
本次出参**不变**,列出以便自包含。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| requirementId | Long(String) | 本次写入的需求 ID(不变) |
|
||||
| version | Integer | 需求版本号(不变) |
|
||||
| status | String | 需求状态,团单为 `PENDING_REVIEW`、核心单为 `PENDING`(不变) |
|
||||
| isActive | Boolean | 是否当前生效版本(不变) |
|
||||
| branchTaken | String | 版本分支 `INIT_SUBMIT` / `PENDING_EDIT` / `DONE_ADJUST`(不变) |
|
||||
| previousVersion | Integer | 仅 `DONE_ADJUST` 分支返回上一版本号(不变) |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
PUT /v3/admin/order/2101506167043985410/hotel-requirement
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"days": [{"dayNumber": 1, "segments": [{"roomCount": 2, "candidates": [{"hotelId": "2023714929877450753", "rooms": [{"roomTypeId": "3002000000000000013", "roomCategory": "KING", "roomCount": 2}]}]}]}]}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {"requirementId": "2101558489333784578", "version": 1, "status": "PENDING_REVIEW", "isActive": true, "branchTaken": "INIT_SUBMIT"},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
不适用:本端点要么写入成功返回完整 `HotelRequirementRespVO`,要么抛业务异常,不存在空响应。响应体本次**一个字段都没改**,标记纠正是服务端副作用,不体现在返回值里——调用方若要确认标记已纠正,读汇总接口的 `hotelFlagMismatchOrderCount`。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{ "code": 582017, "message": "订单状态不允许提交需求", "data": null, "success": false }
|
||||
```
|
||||
|
||||
| code | 触发条件 |
|
||||
|------|----------|
|
||||
| 582017 | 订单非定制中(本次不变) |
|
||||
| 582099 | 团单房型行未填齐(本次不变) |
|
||||
| 582016 | 房间数必须大于 0(本次不变) |
|
||||
|
||||
本次无新增错误码。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **新增副作用**:提交成功后若该单 `needs_hotel` 不为真,则置 1,**与需求写入同一事务**——分开提交会留下「需求写进去了、标记没纠正」的中间态,而那正是本单要修的不一致。
|
||||
- **单向 0→1**:不提供反向置 0。反向意味着「这户不要住宿了」,那是退需求的业务动作,有自己的链路与副作用,不由提交需求顺带做掉。
|
||||
- **已为真时不写**:避免每次提交都产生一行无意义的行变更。
|
||||
- **提交被守卫拒绝时零写入**:标记也不动。
|
||||
- **为 null 的历史行**与为假同等对待,同样置位。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- 前端不需要改代码即可受益:既有字段一个没变,部署后用房汇总直接出数。
|
||||
- 若要展示不一致提示,读 `hotelFlagMismatchOrderCount`:大于 0 说明存在创单时被判定不需要住宿、事后却提了需求的存量户,其间数**已照常计入**,该字段只用于提示与订正,不要用它去扣减间数。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
只写外部可观察行为:
|
||||
|
||||
| 动作 | 外部可观察结果 |
|
||||
|------|----------------|
|
||||
| 提交住宿需求且该单此前「不需要住宿」 | 该单从此被视为需要住宿:其间数进入全团用房汇总、该户参与确认预检、确认时其需求被放行给房务 |
|
||||
| 提交住宿需求且该单本就「需要住宿」 | 无额外变化(不产生多余的行变更) |
|
||||
| 提交被守卫拒绝(如订单非定制中) | 零变化:需求没写入,「是否需要住宿」也不变 |
|
||||
| 读接口(汇总 / 预检) | 只读,无任何写入 |
|
||||
|
||||
- 纠正与需求写入在**同一事务**:需求回滚则纠正一并回滚,不会出现「需求没提上去、标记却变了」。
|
||||
- **单向**:只会从「不需要住宿」变成「需要住宿」,反向不会发生。
|
||||
- 无表结构变更、无 Flyway 脚本。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|------|------|
|
||||
| 标记为真 + 已提交 | 计入,`hotelFlagMismatchOrderCount` 不 +1 |
|
||||
| 标记为假 + 已提交(有效态) | **计入**,`hotelFlagMismatchOrderCount` +1 |
|
||||
| 标记为假 + 需求被打回 | 不计入,不报不一致 |
|
||||
| 标记为假 + 无任何需求 | 不计入,不报不一致(真的不需要住宿) |
|
||||
| 标记为 null(历史行) | 与为假同等对待;提交需求时同样置位 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| `hotelFlagMismatchOrderCount` | 无 | 新增,Integer |
|
||||
| `hotelNeededOrderCount` | 仅 `needsHotel=true` 的户数 | 标记为真的户 ∪ 已提交有效需求的户 |
|
||||
| `hotelSubmittedOrderCount` | 需标记为真且已提交 | 只看需求是否有效 |
|
||||
| 其余既有字段 | — | 不变(无删除、无改名、无类型变化) |
|
||||
| 入参 / 路径 / 错误码 | — | 不变 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 场景 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 标记为假、已提交完整需求的户 | 整户丢弃:间数不进汇总、确认时不放行房务 | 间数计入、参与预检、确认时放行 |
|
||||
| 汇总与子订单列表是否自洽 | 列表显示「待审核」、汇总说「0 户需住宿」 | 两处一致 |
|
||||
| 提交住宿需求后的 `needs_hotel` | 保持创单时的值,永远改不回来 | 不为真则就地置 1 |
|
||||
| 打回态需求 | 不计入 | 不计入(不变) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 字段层面兼容(只增不删);**数值层面**每日房间合计可能变大——这正是本次要修的漏算。
|
||||
- **前端是否必须同步上线**: 否。
|
||||
- **回滚**: 回滚本 PR 即可。注意写侧已置位的 `needs_hotel` 不会随回滚还原,但那些户本就应为 1,保留更正确。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 上述三个端点的取户口径、一个新增出参、提交需求的标记纠正副作用。
|
||||
- **零影响**: 用车需求与座位合计、各户特殊需求标签、房务侧订房与分房逻辑、数据库结构(无表变更、无 Flyway)。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
被测版本:hl-order-service-v3 = dev-v3 `df66ec357`(2026-09-20 15:2x 部署)。构建身份探针:部署后接口开始返回新字段 `hotelFlagMismatchOrderCount`。
|
||||
验收团期 `jw测试1期`(`2101506167098511362`),在团 3 户。
|
||||
|
||||
```
|
||||
AC-1 标记为假但已提交 → 计入
|
||||
张三 HL20260920105808925(6 晚 12 间 PENDING_REVIEW)needs_hotel 改回 0
|
||||
needed=1 submitted=1 mismatch=1
|
||||
D1 豪华大床×2 | D2 标间×1+豪华大床×1 | D3 标间×2 | D4 豪华大床×2 | D5 豪华大床×2 | D6 豪华房×1+标间×1
|
||||
改前同一数据 dailyRoomBreakdown = [] ✓
|
||||
|
||||
AC-2 标记为假且无需求(王五/李玉)→ 不计入、不报不一致 ✓
|
||||
AC-3 标记为真的既有场景:六天逐日间数与标记为 0 时逐字相同,mismatch=0 ✓
|
||||
AC-4 mismatch:标记1+已提交→0;标记0+已提交→1;无不一致时为 0 不是 null ✓
|
||||
|
||||
AC-5 写侧自动置位(真调提交接口,非 mock)
|
||||
PUT /v3/admin/order/2101507276118626306/hotel-requirement(6 晚)→ 200
|
||||
提交前 needs_hotel=0 → 提交后 needs_hotel=1
|
||||
汇总立即计入该户,逐日每晚 +1:needed=2 submitted=2 mismatch=1 ✓
|
||||
|
||||
AC-6 存量排查(只出数):TEST 上不一致户 21、涉及团期 10
|
||||
按状态 PENDING_REVIEW 16 / PENDING 5;改前这 21 户间数一条都没进过汇总 ✓
|
||||
生产侧按既有口径不查(无任意 SQL 通道),不作阻塞
|
||||
|
||||
AC-7 判权:ROOM_MANAGER / VEHICLE_MANAGER → 589507;
|
||||
GROUP_BATCH_MANAGER / SUPER_ADMIN → 200(超管短路放行,故用低权限角色验) ✓
|
||||
```
|
||||
|
||||
未做:写侧 WARN 留痕日志未在 TEST 取证(order-v3 日志窗口只有 200 行、SQL 刷屏会冲掉证据),该分支由 4 条单测覆盖;未跑 order-v3 全量单测,四域定向回归 4717 用例全绿。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 工单 [#8023](https://git.1814.love:8443/wx/HL/issues/8023)
|
||||
- 前置口径 [#7925](https://git.1814.love:8443/wx/HL/issues/7925)(本单放宽的正是该单引入的第一道过滤)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8023](https://git.1814.love:8443/wx/HL/issues/8023)
|
||||
- **PR**: [#8028](https://git.1814.love:8443/wx/HL/pulls/8028)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
- **前端负责人**: @mmg
|
||||
在新工单中引用
屏蔽一个用户