From 9fe70242a6e300c59332ffa47fa363a80c86049f Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Fri, 2 Oct 2026 09:42:38 +0800 Subject: [PATCH] =?UTF-8?q?changelog(#8665):=20=E9=85=8D=E6=88=BF=E5=88=A0?= =?UTF-8?q?=E9=99=A4/=E4=BF=AE=E6=94=B9/=E5=8D=95=E6=97=A5=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E5=B9=B6=E5=8F=91=E5=86=B2=E7=AA=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20808932?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- ...房删改并发冲突返808932-修改接口-管理后台.md | 360 ++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 changelogs-v2/2026-10/02_8665_配房删改并发冲突返808932-修改接口-管理后台.md diff --git a/changelogs-v2/2026-10/02_8665_配房删改并发冲突返808932-修改接口-管理后台.md b/changelogs-v2/2026-10/02_8665_配房删改并发冲突返808932-修改接口-管理后台.md new file mode 100644 index 00000000..cf99b049 --- /dev/null +++ b/changelogs-v2/2026-10/02_8665_配房删改并发冲突返808932-修改接口-管理后台.md @@ -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 | ❌ | 非空时每个 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