changelog(#8665): 配房删除/修改/单日确认并发冲突新增 808932
changelog-filename-gate / validate (push) Failing after 2s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-10-02 09:42:38 +08:00
共同撰写人 Claude Opus 5.5
父节点 9ff97c8e4c
当前提交 9fe70242a6
@@ -0,0 +1,360 @@
---
schema: "hl-changelog/v2"
ticket: "8665"
title: "配房删改与单日确认并发收口:三个写口新增 808932 并发冲突码"
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: ""
updated_at: "2026-10-02"
base: "dev-v3"
---
# 配房删改与单日确认并发收口:新增 808932 并发状态码
> **存放目录**: 二期 → `changelogs-v2/2026-10/`
>
> **服务**: hl-order-service-v3
> **Issue**: #8665
> **日期**: 2026-10-02
> **影响范围**: 管理后台房务配房操作:删除、修改、单日确认三个写口,并发交错时新增返回错误码 808932
---
## ⚠️ 关键变化
- **新增错误码 808932**(房务状态已被并发修改,请刷新后重试):删除配房、修改配房、单日确认三个写口在并发交错时返回,不再产生孤儿应付台账行。
- **请求/响应结构无变化**:三个接口的方法、路径、入参字段均未变,仅错误码表新增一条。
- **808932 与其他业务错误码走同一套契约**:HTTP 200 + `code` + `message`,按 `code` 区分即可,无需为该码新增专门的前端处理分支;hl-ui v2.1 现有的通用业务错误拦截器(`src/utils/request.js` 的业务错误分支 + `errorBus.js`)会把后端返回的 `message` 原样呈现,不要求前端硬编码该文案。
---
## 一、背景
配房行的删除与修改两个写口(`DELETE /assignments/{id}` 与 `PUT /assignments/{id}`)原无互斥锁,与单日确认(`confirmDayPersist`)并发交错时,可能产生「配房行已软删,但应付台账仍留下可付款行」的孤儿记录。工单 #8665 为三个写口补充行级锁定读与版本控制,在并发修改被检测时返回 808932,整体回滚包括该行的任何台账产出。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 删除配房 | DELETE | `/v3/admin/order/assignments/{id}` | 修改 | 新增错误码 808932(并发冲突) |
| 2 | 修改配房 | PUT | `/v3/admin/order/assignments/{id}` | 修改 | 新增错误码 808932(并发冲突) |
| 3 | 单日确认 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}/confirm` | 修改 | 新增错误码 808932(并发冲突) |
---
## 三、接口详情
本节示例 JSON 的字段名、类型、错误码与文案逐一取自源码;ID、日期等取值为说明用的构造值。所有接口统一返回 `Result` 信封(`code` / `message` / `data` / `traceId` / `success`),示例省略 `traceId`;业务失败与入参校验失败均为 HTTP 200,靠 `code` 区分。
### 1. 删除配房 `DELETE /v3/admin/order/assignments/{id}`
**VO**: `无请求体 → Void`
#### 使用场景
房务管理员在管理后台删除已配置的某条配房行,通常在需要重新调整房型或取消某晚房务时使用。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 配房行 ID |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| (无数据体) | - | 删除成功时 `Result.data` 为 null |
#### 请求示例
```http
DELETE /v3/admin/order/assignments/2105709698592440321
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 空数据 / 降级响应
- 若配房行不存在(已被删除或 ID 无效):返回业务错误码 808120「配房不存在」。
#### 错误响应
```json
{
"code": 808932,
"message": "房务状态已被并发修改,请刷新后重试",
"data": null,
"success": false
}
```
#### 业务边界
- **触发 808932 的场景**:该配房行在读取后被并发确认(单日确认的保留行流程)或被并发修改(改配房),版本对不上或已被软删,带版本谓词的软删(`softDeleteWithVersion`)影响 0 行。
- **建议的前端处理**:收到 808932 时提示用户刷新该订单的配房列表后重试;无需为该码单独编码重试逻辑,交由通用错误提示呈现即可。
- **串行无 808932**:若删除在单日确认完全提交之后才发起,行已稳定,返回 200;若确认在删除完全提交之后才查询候选,会命中更早的 808118(该天无可确认的询房中候选),不会到达锁定复读分支。
### 2. 修改配房 `PUT /v3/admin/order/assignments/{id}`
**VO**: `AssignmentUpdateReqVO → Void`
#### 使用场景
房务管理员修改已配置配房行的协议价、结算价、支付方式、备注、早餐、酒店主数据快照同步开关,不支持修改酒店、房型、间数(需要换酒店/换房型走 §2.3b 调整配房端点,或删除后用 §2.2 重新创建)。EXCEPTION 异常态下的配房仍允许走本接口改价,用于异常桶的人工处置(如与酒店协商退款后的补偿调整)。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 配房行 ID |
| protoPrice | Body | BigDecimal | ❌ | ≥0.00 | 协议价 |
| settlementPrice | Body | BigDecimal | ❌ | ≥0.00 | 结算价 |
| settleType | Body | String | ❌ | 正则 `cash\|sign\|company` | 结算方式 |
| syncProtocolPrice | Body | Boolean | ❌ | - | 是否同步协议价到酒店主数据快照 |
| syncSettlementPrice | Body | Boolean | ❌ | - | 是否同步结算价到酒店主数据快照 |
| syncSettleType | Body | Boolean | ❌ | - | 是否同步结算方式到酒店主数据快照 |
| remark | Body | String | ❌ | 无长度校验注解 | 备注 |
| syncHotelSnapshot | Body | Boolean | ❌ | - | 是否整体同步酒店主数据快照 |
| breakfast | Body | String | ❌ | 正则(`HouseBreakfast.VALUE_REGEX`,即 INCLUDED/EXCLUDED/PENDING) | 早餐 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| (无数据体) | - | 修改成功时 `Result.data` 为 null |
#### 请求示例
```json
{
"settlementPrice": "320.50",
"remark": "与酒店协商调整"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 空数据 / 降级响应
- 若配房行不存在:返回 808120「配房不存在」。
#### 错误响应
```json
{
"code": 808932,
"message": "房务状态已被并发修改,请刷新后重试",
"data": null,
"success": false
}
```
#### 业务边界
- **触发 808932 的场景**:该配房行在读取后被并发删除或被并发修改(版本漂移),带 `@Version` 校验的 `updateById` 影响 0 行。
- **EXCEPTION 态改价**:本接口不经过「需求级写锁 + EXCEPTION 写闸」(`assertWritableForAssignment`)那条校验链,因此配房所属需求处于 EXCEPTION(异常态)时仍可调用本接口改价,用于异常桶的人工处置;并发删除/修改仍会返 808932。
- **建议的前端处理**:收到 808932 时提示用户刷新该配房行详情后重试,无需额外编码特殊逻辑。
### 3. 单日确认 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments/days/{dayNumber}/confirm`
**VO**: `AssignmentDayConfirmReqVO → Void`
#### 使用场景
房务确认某个住宿需求的某一晚配房方案,触发应付台账推送和库存更新。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| requirementId | Path | Long | ✅ | - | 住宿需求 ID |
| dayNumber | Path | Integer | ✅ | 超出该需求行程晚数上界返 808102,无下界校验注解 | 第几晚(从 1 开始计数) |
| keepAssignmentIds | Body | List<Long> | ❌ | 非空时每个 id 须属于该晚询房中候选集,否则返 808123 | 保留的配房行 ID 清单(待翻 CONFIRMED);整个请求体、本字段均可省略,或传空数组——两者语义相同,均表示保留该晚**全部**询房中候选(全部翻 CONFIRMED),不传则不会软删任何候选行;显式传非空列表时,列表外的该晚候选行才会被软删 |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| (无数据体) | - | 确认成功时 `Result.data` 为 null |
#### 请求示例
```json
{
"keepAssignmentIds": [2105709698592440321]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 空数据 / 降级响应
- 若该晚无询房中的候选(已被删除或已确认):返回 808118「该天无可确认的询房中候选, 请先配房再确认」。
- 若该需求所属订单已取消或处于异常处置中(`house_status=EXCEPTION`):返回 808119「订单已取消或异常处置中, 该需求不可新增或确认配房, 请在异常桶处理」。
- 若 `keepAssignmentIds` 非空且其中存在不属于该晚询房中候选的 id:返回 808123「保留的配房行不属于该天询房中候选(可能配房已更新), 请刷新后重试」。
#### 错误响应
```json
{
"code": 808932,
"message": "房务状态已被并发修改,请刷新后重试",
"data": null,
"success": false
}
```
#### 业务边界
- **触发 808932 的场景**:确认流程中保留行(`keepAssignmentIds` 对应的候选行)在候选列表读取之后被并发删除或修改,锁定复读时发现该行已不存在、已非询房中状态、版本不一致,或翻 CONFIRMED 的 `updateById` 影响 0 行。
- **整体回滚**:落选候选先软删、保留候选再逐行锁定复读,任一保留行复读发现不一致(抛 808932)时整个事务回滚——包括同一事务内已执行的落选行软删——该确认涉及的应付台账均不推送,确保终态一致,不留半截状态。
- **建议的前端处理**:收到 808932 时提示用户刷新该晚配房列表后重试,无需额外编码特殊逻辑。
- **串行与并发的区别**:若删除完全提交在确认发起之前,确认查询该晚候选时已无询房中的行,会命中更早的 808118(候选预检),不会到达锁定复读分支(808932 仅针对确认读取候选之后、锁定复读之前这段并发窗口);两条路径的共同效果一致:均不推送应付台账。
---
## 四、契约约束与正确调用方式
- 三个接口的请求方式、路径、参数均**无变化**,仅错误码表补充。
- 808932 与其他业务错误码(如 808118、808119、808120、808123)一样走 HTTP 200 + 业务码的行为,前端按 `code` 区分即可;hl-ui v2.1 的通用业务错误拦截器会将后端 `message` 原样呈现,不要求为 808932 单独编码处理分支。
- 单日确认(接口 3)请求体可以整体省略:省略或 `keepAssignmentIds` 传空数组语义相同,均表示保留该晚全部询房中候选;若需要落选部分候选行,必须显式传入要保留的 id 列表。
---
## 五、数据库行为
- 无表结构变更、无 Flyway 迁移(本次合并仅涉及 Service/测试代码与 API 文档,对比 #8665 合并提交的文件清单确认无 `db/migration` 变更)。
- 配房表 `house_hotel_assignment` 的 `version` 列与实体上的 `@Version` 注解系已有能力(早于本次改动),本次复用它为删除、修改、单日确认三个写口补齐「锁定读(`SELECT...FOR UPDATE`)+ 带版本更新/软删」的组合:先用锁定读拿到最新行与其 `version`,再执行带版本谓词的写操作(`updateById` 走 MyBatis-Plus 全局注册的乐观锁拦截器自动拼 `version` 条件;软删复用既有的 `softDeleteWithVersion(id, version)`),任一写操作影响 0 行即判定为并发冲突并抛 808932,整体事务回滚。
---
## 六、边界行为
- **离线与网络异常**:808932 不涉及网络/超时,纯业务并发码,离线时前端仍会收到错误响应。
- **灰度与开关**:本次改动无灰度开关,所有测试环境已包含该并发收口逻辑。
- **下游链路**:应付台账推送、库存扣减、其他异步链路仅当确认成功(code=200)才执行,808932 回滚后不产生任何账务记录。
---
## 六.5 枚举
不涉及新增枚举值;配房状态(INQUIRING/CONFIRMED/EXCEPTION 等)无变化。
---
## 六.6、修改前后对比
### 并发行为对比
| 项 | 改前 | 改后 |
|----|------|------|
| 删除配房并发于确认 | 可能双 200,留下孤儿应付台账行 | 后提交方返 808932,回滚,无台账产出 |
| 修改配房并发于确认 | 可能按过期快照推台账 | 后提交方返 808932,回滚,无台账产出 |
| 单日确认保留行被删 | 静默跳过已删行,继续推台账 | 锁定复读检测到行已删,返 808932,回滚,无台账产出 |
| 返回的错误码 | 无 808932 | 新增 808932(仅在并发窗口内返回) |
---
## 六.7、影响评估
- **是否破坏向后兼容**:否。新增错误码不影响其他业务流程,正常序列的删除、修改、确认仍返回 200。
- **前端是否必须同步上线**:否。hl-ui v2.1 现有的通用业务错误拦截器会把后端返回的 `message` 原样呈现,808932 走的是这条通用路径,不需要新增前端代码。
- **前端 workaround 清理点**:无。本次改动前 808932 这个码不存在,故前端不会有针对它的既有 workaround 需要清理。
**影响范围说明**:
- 前端 hl-ui:请求/响应字段无改动;808932 走通用业务错误展示路径,后端返回的 `message` 即为用户看到的提示文案,无需前端硬编码该文案。
- 管理后台网关:路由无变更,仅错误响应码增加,不影响路由规则和鉴权。
- 其他服务:本次改动是 order-v3 内部的行级锁定读 + 乐观锁版本校验,不新增分布式锁、不改 Feign 契约;finance、resource 等消费方零感知。
- 数据一致性:通过锁定读 + 版本控制,堵住了孤儿应付台账的产生根源,后续应付台账取消、对账等链路无需额外补丁。
---
## 七、不影响范围
- EXCEPTION 异常态下调用修改配房(接口 2)改价仍被允许——该接口本就不经过 `assertWritableForAssignment` 这条 EXCEPTION 写闸——未受本次新增版本控制的影响;本次改动只新增 808932 这一个并发冲突码,不改变任何既有的状态校验逻辑。
- 其他写口(如转房 `changeId`、取消级联删除等)未涉及本次改动,仍按既有逻辑。
- 只读接口(查询配房列表、查询单日候选等)逻辑无变化。
---
## 八、测试环境已验证
**部署状态**:
- hl-order-service-v3 @cd82a19ab(origin/dev-v3 的祖先,#8665 的合并提交 f8379ddfc3 已包含)
- hl-gateway @71def6dc5(网关落后 dev-v3 156 个提交,本次无路由变化,不影响)
- 部署时刻:2026-10-01 22:50:33
**测试链路 A:确认第 1 晚 → 删除该行**
- 操作序列:`POST /confirm` 返回 200 → `DELETE /assignments/{id}` 返回 200
- 终态:配房行 `deleted_at` 非空;应付台账该行已软删(无活跃 NORMAL 记录);日历库存回复
- 结论:PASS(两步均 200,无 808932)
**测试链路 B:删除该行 → 确认第 2 晚**
- 操作序列:`DELETE /assignments/{id}` 返回 200 → `POST .../days/2/confirm` 返回 808118(严格串行,无并发窗口)
- 终态:应付台账对该行零产出(`fin_payable_line WHERE source_ref_id=<该行id>` 为 0 行)
- 结论:PASS——判据是「第二步不再为该行产生台账行」,不要求第二步返回 200;完全串行操作下,confirmDay 在进入锁定复读之前先按该晚候选集过滤,发现该晚询房中候选集已为空,命中更早的 808118(该天无可确认的询房中候选),而不是 #8665 新增的 808932(锁定复读检查需要先有候选才会走到)。
**并发窗口说明**:
- 808932 是为「删除/修改读取之后、带版本的写操作之前」这段时间窗口的并发交错设计的,靠锁定读 + 乐观锁版本校验堵住。
- 完全串行操作(一方提交完成后另一方才查询)会先命中 808118(无候选)或 808120(配房不存在),不会到达锁定复读检查点;这两条路径的共同效果都是零台账产出。
- 测试环境部署状态:hl-order-service-v3 对 origin/dev-v3 零落后,#8665 的合并提交已在部署字节内,以上两条测试链路均为该部署字节下的实测结果。
---
## 十、相关文档
- API 规范:`docs/order-v3/api/API-SPEC-HOUSE-V1.1.html`(v1.1.20,2026-10-01)
- §2.2b 单日确认 / §2.3 修改配房 / §2.4 删除配房错误码表新增 808932
- §11.9 内部接口 + 跨服务(808900-808999)全表补 808932「房务状态已被并发修改,请刷新后重试」
- 工单正文:Gitea #8665(设计、验收、口径定案)
- 单测覆盖:
- `HouseAssignmentServiceTest#confirmDayPersist_keepUpdateAffectsZeroRows_throws808932AndNoPayablePush`:confirmDayPersist 中某 keep 行 updateById 影响 0 行时抛 808932,整体回滚,零推台账,落选不回补库存
- `HouseAssignmentServiceTest#delete_softDeleteAffectsZeroRows_throws808932NoPayableNoRestore`:delete() 在 softDeleteWithVersion 返 0 时抛 808932,不碰台账、不回补库存
- `HouseAssignmentServiceTest#updateTx_updateAffectsZeroRows_throws808932NoPayableRewrite`:updateTx() 乐观锁写库影响 0 行时抛 808932,不判台账、不作废不重推
- `HouseAssignmentDeleteConfirmDayOrderingIT`:固定时序交错 IT,覆盖「确认快照之后删配房提交→确认返 808932」与「删配房持行锁期间确认进入事务→确认被挡住随后 808932」两条交错,均断言无孤儿台账
---
## 关联 / 联系人
### 联系人
- **后端负责人**: @wx