diff --git a/changelogs-v2/2026-09/10_7323_团期房务地基核心配房新增可选原因字段replaceReason-修改接口-管理后台.md b/changelogs-v2/2026-09/10_7323_团期房务地基核心配房新增可选原因字段replaceReason-修改接口-管理后台.md new file mode 100644 index 00000000..88d5beab --- /dev/null +++ b/changelogs-v2/2026-09/10_7323_团期房务地基核心配房新增可选原因字段replaceReason-修改接口-管理后台.md @@ -0,0 +1,332 @@ +--- +schema: "hl-changelog/v2" +ticket: "7323" +title: "团期房务地基:新建 group_batch_room_plan / group_batch_room_allocation / order_group_batch_house_legacy 三表 + house_hotel_assignment.replace_reason 列 + house_dual_deduction_log.order_id 放宽可空/加 source_type;核心配房提交 items[].replaceReason 新增可选字段(重复提交不覆写);新增内部错误码 808620-808622" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "PR #7413(合并提交 1a3d3c43f)已合并 dev-v3, 已部署 TEST 环境: Deploy Panel 记录 hl-order-service-v3 <- dev-v3 @ 50c93199e(1a3d3c43f 是其祖先, 已核实包含), 双实例 8186/8086 均 UP, 健康检查通过; 核心配房提交端点已经网关四步实测(AC-9), DDL 已在测试服 SHOW CREATE TABLE 逐列比对零差异。backend_status=deployed、gateway_status=verified 均为真实态。" +updated_at: "2026-09-10" +base: "dev-v3" +--- + +# 团期房务:地基库表 + 核心配房替换原因字段 replaceReason + +> **服务**: hl-order-service-v3 +> **PR**: #7413 +> **Issue**: #7323 +> **日期**: 2026-09-10 +> **影响范围**: 核心/定制订单房务配房提交接口新增 1 个可选字段;团期房务地基(3 张新表 + 2 处 ALTER + 3 个内部错误码,暂无任何外部可达接口读写) + +--- + +## ⚠️ 关键变化 + +1. **items[].replaceReason 是"仅记首次、不覆写"的语义,不是"每次提交都会更新"**:同一天对同一配房重复提交(未产生新的换店/改间数替换行,即 swap_from_id 不变的 UNCHANGED 情形)时,即使这次 payload 里的 replaceReason 文案与上次不同,后端**也不会更新** house_hotel_assignment.replace_reason,数据库里仍是第一次落库的值。已在测试服网关实测复现(见"三、接口详情"与"八、测试环境已验证")。**要修改已落库的原因文案,必须先撤销/删除该天已有配房,让新的替换动作产生新行,新行才会重新落 replaceReason**。 +2. **提示(本次实测发现,非本单引入,既有缺陷)**:配房提交响应 items[].arrange 的 Swagger 文档(AssignmentSubmitRespVO.Item.arrange 的 @ApiModelProperty)只列了 pending / waiting / confirmed / problem 四个值,但本次网关实测(AC-9)复现响应里 arrange 实际返回过 inquiring——该值来自另一条既有派生逻辑(house_hotel_assignment.confirm_status = INQUIRING 时派生为 inquiring,HouseAssignmentService.arrangeOf),文档一直没跟着补。**前端对 arrange 做分支/switch 时不要写成不带 default 的穷举**,务必对未列举值做兜底展示。详见"六.5、枚举"。 +3. 本单是"地基单":**不新增任何端点、不下线任何端点、网关零改动**。3 张新表 + 2 处 ALTER + 段位 808600-808699 下新增的 3 个错误码(808620/808621/808622)目前只服务于尚未合入的后续单(#7324 订房 CRUD、#7325 按日确认与自动分房)的团期分支扣减逻辑,**当前任何已上线业务流程都不会命中这三个码**,列出仅为错误码表完整、供后续单核对。 + +--- + +## 一、背景 + +团期房务(房务按团整体订一次房,系统按各户已确认需求自动还原到户)此前在 order-v3 没有任何表承载,#7324-#7327 一行业务代码都写不了。本单铺两张地基表(按日订房计划 group_batch_room_plan、分房到户 group_batch_room_allocation)及其 Flyway/实体/Mapper,顺带做两条配套 ALTER:house_hotel_assignment 加 replace_reason(wx 2026-09-04 拍板"替换可选填原因,核心配房也要加")、house_dual_deduction_log.order_id 放宽可空 + 加 source_type(团期计划行扣库存复用既有 HouseDualDeductionService 链路,但该链路的审计表此前强绑 order_id,团期计划行没有 order_id,不松绑就插不进去)。同一 PR 里还做了旧户冻结名单(order_group_batch_house_legacy + order_group_batch.house_legacy_frozen_at),承接团期归属误判问题(另一复审修订,不影响本 changelog 面向的对外接口)。 + +对前端唯一直接可见的变化,是核心配房提交端点新增了 1 个可选字段 items[].replaceReason。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 提交配房方案 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/assignments` | 请求体新增可选字段 | items[].replaceReason 选填 ≤256 字;仅当该项在本次提交中被判定为"替换"(新行携带 swap_from_id)时落库;重复提交同一配房不覆写 | + +--- + +## 三、接口详情 + +### 1. 提交配房方案 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments` + +**VO**: `AssignmentSubmitReqVO → AssignmentSubmitRespVO` + +#### 使用场景 + +房务配房工作台(hl-ui)为某条用房需求批量提交/调整逐日房型方案;房务角色抢单后,逐天选择酒店房型并提交,配房行进入"询房中"状态。同一天再次提交且换了酒店/改了间数会被判定为"替换"(原行软删、新行携带 swap_from_id),此时可选带上 replaceReason 记录换店原因。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| requirementId | Path | Long | 是 | 逻辑 FK 到 order_hotel_requirement.requirement_id | 用房需求 ID | +| items | Body | List of AssignmentItemReqVO | 是 | NotEmpty | 一项 = 一晚 乘 一组房间 | +| items[].dayNumber | Body | Integer | 是 | Min 1 | 第几天(1=Day1) | +| items[].hotelId | Body | Long | 是 | 非空 | resource 酒店 ID | +| items[].roomTypeId | Body | Long | 是 | 非空;不属于该酒店抛 808112 | resource 房型 ID | +| items[].roomCategory | Body | String | 是 | NotBlank | 房型字典 code(room_category) | +| items[].roomCount | Body | Integer | 是 | Min 1 | 间数 | +| items[].protoPrice | Body | BigDecimal | 否 | DecimalMin 0.00 | 协议价快照(元/间·晚);不传按房型当日协议价兜底 | +| items[].settlementPrice | Body | BigDecimal | 否 | DecimalMin 0.00 | 结算价快照;不传按结算价再兜底协议价 | +| items[].settleType | Body | String | 否 | cash / sign / company | 支付方式快照 | +| items[].deductInventory | Body | Boolean | 是(业务层) | 缺失抛 808124 | 是否扣减资源房型库存 | +| items[].syncProtocolPrice | Body | Boolean | 否 | 默认 false | 是否把 protoPrice 同步写回价格日历 | +| items[].syncSettlementPrice | Body | Boolean | 否 | 默认 false | 是否把 settlementPrice 同步写回价格日历 | +| items[].syncSettleType | Body | Boolean | 否 | 默认 false | 是否把 settleType 同步写回酒店资源 | +| items[].remark | Body | String | 否 | 无 | 备注 | +| items[].replaceReason | Body | String | 否 | 新增;Size max 256 | 替换原因。仅当本项在本次提交中被判定为替换(该天已有旧行、本项换店或改间数,新行写了 swap_from_id)时落库;NEW(无对应旧行)与未变项后端忽略该字段;同一天两个 item 各自换店时,原因按提交顺序与被替换的旧行按位置配对 | + +#### 出参字段表 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.successCount | Integer | 成功条数 | +| data.failCount | Integer | 失败条数 | +| data.items | List of Item | 每条配房结果 | +| data.items[].dayNumber | Integer | 第几天 | +| data.items[].assignmentId | Long(JSON 字符串) | 配房行 ID(雪花) | +| data.items[].arrange | String | 配房状态;Swagger 文档列 pending/waiting/confirmed/problem,实测另有 inquiring(既有缺陷,见"关键变化"第 2 条与"六.5、枚举") | +| data.items[].deductInventory | Boolean | 本行是否扣减资源库存 | + +#### 请求示例 + +```json +{ + "items": [ + { + "dayNumber": 1, + "hotelId": "2029926129876320258", + "roomTypeId": "2029944763138936833", + "roomCategory": "STANDARD", + "roomCount": 1, + "deductInventory": true, + "replaceReason": "酒店A满房" + } + ] +} +``` + +(以上为测试服网关实测 AC-9 第二步真实请求体:Day1 已有酒店 A 的配房,本次改配酒店 B 并带 replaceReason。) + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "successCount": 1, + "failCount": 0, + "items": [ + { + "dayNumber": 1, + "assignmentId": "2097856298618941442", + "arrange": "inquiring", + "deductInventory": true + } + ] + }, + "success": true +} +``` + +(测试服网关实测 AC-9 第二步真实响应;arrange 实测返回 inquiring,与 Swagger 文档列举的四值不同,见"关键变化"第 2 条。) + +#### 空数据 / 降级响应 + +本接口不存在"空数据"语义:items 为空数组在契约层已被 NotEmpty 拦截为 400/808111,不会返回成功空列表;下游资源服务(价格日历库存扣减)不可用时直接返回 808900 错误,不做静默降级或返回部分成功的伪结果。 + +#### 错误响应 + +```json +{ + "code": 808111, + "message": "配房项不能为空, 请至少选择一家酒店/房型", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- replaceReason 仅在本项被 reconcileDay 判定为"替换"(新行携带非空 swap_from_id)时写入新行;NEW(该天此前无配房行)与"无对应旧行的纯新增房型"两种情形该字段恒被清空/忽略。 +- UNCHANGED(原地更新旧行,未产生新的替换行)不合并该字段——重复提交同一配房、仅改 replaceReason 文案,不会覆盖已落库的值(已实测确认,见"关键变化"第 1 条)。 +- 同一天提交多个换店 item 时,原因文本按提交顺序与被替换的旧行按位置(zip)配对,不按酒店名匹配;同日多项替换请按提交顺序对应理解归属。 +- 权限:HouseWriteGuarded 注解——ROOM_MANAGER/SUPER_ADMIN 可写;其余角色(含 HOUSE_KEEPER_LEAD 组长)拒绝写入,走 808090/808091(Controller 入口前置校验,先于 Idempotent/Lock4j)。 +- 幂等:Idempotent 同 requirementId 3 秒窗口内重复提交直接拒绝(100502 处理中);并发保护:Lock4j 需求级锁 house:req:write:{requirementId},与单日确认/释放/转单互斥。 +- 需求不属于当前用户 → 808110;需求已作废 → 808113;未抢单 → 808116;同一次提交内同天同酒店同房型重复 → 808117;订单异常处置中 → 808119;deductInventory 缺失 → 808124。 +- arrange 字段可能返回文档未列举的 inquiring(见"六.5、枚举"),前端渲染该字段务必留兜底分支。 + +--- + +## 四、契约约束与正确调用方式 + +> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。 + +### 正确 / 错误 payload 对照 + +| 场景 | payload | 结果 | +|------|---------|------| +| 正确:首次配房,不带原因 | items[0] 不含 replaceReason 字段 | 200;house_hotel_assignment.replace_reason 为 NULL | +| 正确:同天换店并带原因 | items[0].replaceReason = 酒店A满房 | 200;产生新行,swap_from_id 指向旧行,replace_reason 落库该文案,旧行软删(deleted_at 非空) | +| 需注意:对同一未变配房重放请求并改 replaceReason 文案 | 与上次请求内容一致(同酒店同房型,即 UNCHANGED),仅 replaceReason 文案不同 | 200,但不覆写:assignmentId 不变,replace_reason 仍是第一次落库的值 | +| 错误:replaceReason 超 256 字 | items[0].replaceReason 长度 257 | 400,Size 校验拒绝 | + +### 切换状态时的必要动作 + +若需要修改一条配房行已经落库的 replaceReason 文案,不能寄望于"再传一次不同的值"——后端只在产生新的替换行时才写该字段。正确做法:先撤销/删除该天配房(走已有的 DELETE 端点或触发新的换店动作产生新行),让替换重新发生,新行才会带上新的 replaceReason。 + +--- + +## 五、数据库行为 + +- 本次 Flyway(V20260908_311)新建 3 张表:group_batch_room_plan(25 列,团期按日订房计划)、group_batch_room_allocation(18 列,分房到户)、order_group_batch_house_legacy(6 列,历史旧户冻结名单);order_group_batch 加列 house_legacy_frozen_at;house_hotel_assignment 加列 replace_reason;house_dual_deduction_log.order_id 由 NOT NULL 放宽为可空,并加列 source_type(VARCHAR(32) NOT NULL,默认值 HOUSE_ASSIGNMENT)。已在测试服 SHOW CREATE TABLE 逐列比对零差异,flyway_schema_history 记录 version=20260908.311、success=1,无 312/313 残留草稿。 +- 以上表/列当前均无任何外部可达接口读写(地基单,专供尚未合入的 #7324/#7325/#7326/#7327 使用),本次不改变任何既有查询接口的返回结果。 +- house_dual_deduction_log.source_type 加默认值后,存量行 100% 落默认值 HOUSE_ASSIGNMENT(已实测 COUNT(source_type 不等于 HOUSE_ASSIGNMENT) = 0),不影响既有幂等键与对账逻辑。 +- house_hotel_assignment.replace_reason 写入规则(外部可观察): + +| 提交场景 | swap_from_id | replace_reason | +|----------|-----------------|-------------------| +| Day1 首次配酒店 A(不带 replaceReason) | 空 | 空 | +| 同一天改配酒店 B,带 replaceReason=酒店A满房 | 指向 A 行 id | 酒店A满房 | +| 再次提交同一酒店 B(UNCHANGED),带 replaceReason=换一个理由试试 | 不变(仍指向 A 行) | 仍是酒店A满房,未被覆写 | + +--- + +## 六、边界行为 + +- 未登录/无权限 → 401(网关)或 808090/808091(房务写权限门禁,先于业务逻辑)。 +- requirementId 对应需求不存在或不属于当前用户 → 808110。 +- 需求已作废 → 808113;未抢单 → 808116。 +- 同一次提交内出现同天同酒店同房型重复项 → 808117,禁止静默合并,需前端自己合并间数后再提交。 +- 下游资源服务(库存/价格日历)异常 → 808900/808901/808902,不做静默降级、不返回部分成功。 +- 团期计划行扣减来源相关的 808620/808621/808622 三个错误码当前不会被本端点触发(仅服务尚未合入的团期分支,见"关键变化"第 3 条)。 + +--- + +## 六.5、枚举 / 数据字典 + +### arrange(AssignmentSubmitRespVO.Item.arrange) + +**所属字段**: data.items[].arrange | **类型**: String + +| 值 | Swagger 文档是否列出 | 触发条件 | +|----|----|------| +| pending | 是 | 该天尚未配房/待处理 | +| waiting | 是 | 候选待定 | +| confirmed | 是 | 行 confirm_status 非 INQUIRING(HouseAssignmentService.arrangeOf 派生) | +| problem | 是 | 存在异常需处理 | +| inquiring | 否,文档遗漏(既有缺陷,非本单引入) | 行 confirm_status = INQUIRING(询房中候选,HouseAssignmentService.arrangeOf 方法派生)。本单网关实测(AC-9)两次真实复现该返回值 | + +**前端处理建议**:对 arrange 做展示/分支逻辑时不要写不带 default 的穷举 switch;未列举值(至少已知有 inquiring)应有兜底展示,不能因为遇到未知值而崩溃或空白。 + +### source_type(house_dual_deduction_log.source_type,新列,内部审计字段,不对外暴露) + +**所属字段**: 数据库列,无对应 API 出参字段 | **类型**: VARCHAR(32) + +| 值 | 含义 | +|----|------| +| HOUSE_ASSIGNMENT | 逐单配房扣减(assignment_id = house_hotel_assignment.id);当前所有扣减记录均为此值 | +| GROUP_BATCH_PLAN | 团期计划行扣减(assignment_id = group_batch_room_plan.plan_id);供 #7325 落地后使用,当前无记录 | + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| items[].replaceReason | 不存在 | 新增,选填,Size max 256 | +| house_hotel_assignment.replace_reason 列 | 不存在 | 新增,VARCHAR(256) NULL | +| house_dual_deduction_log.order_id | BIGINT NOT NULL | BIGINT NULL | +| house_dual_deduction_log.source_type | 不存在 | 新增,VARCHAR(32) NOT NULL,默认值 HOUSE_ASSIGNMENT | +| 端点其余请求/响应字段 | 无 | 完全不变 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 换店/改间数替换 | 只记 swap_from_id/swap_count,无原因文本 | 可选记录 replace_reason | +| 重复提交同一配房、只改 replaceReason 文案 | 字段不存在,无此场景 | 后端不覆写,仍是第一次落库的值(UNCHANGED 语义) | +| 团期计划行扣库存 | 无此来源(表不存在) | 复用既有扣减链路,按 source_type=GROUP_BATCH_PLAN 与 orderId=null 走团期分支(当前无调用方,供 #7325 使用) | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**:否。items[].replaceReason 是新增可选字段,老前端不传该字段完全不受影响;新表新列对现有查询零影响。 +- **前端是否必须同步上线**:否。不传 replaceReason 时端点行为与改前逐字一致。 +- **前端 workaround 清理点**:无强制清理项。建议(非强制):一是在配房提交 UI 中,仅当"该天已有配房"(即本次提交会产生替换)时显示 replaceReason 输入框;二是对 arrange 字段的展示/分支逻辑补一个 default 兜底,覆盖 inquiring 等文档未列举的值。 + +--- + +## 七、不影响范围 + +- **仅影响**:核心/定制订单房务配房提交端点新增 1 个可选请求字段;团期房务地基库表与内部扣减错误码段位(当前无外部可达入口)。 +- **零影响**: + - 无新增端点、无端点下线、无路径变化、网关路由零改动。 + - 该端点其余请求字段(dayNumber/hotelId/roomTypeId/roomCategory/roomCount/protoPrice/settlementPrice/settleType/deductInventory/syncProtocolPrice/syncSettlementPrice/syncSettleType/remark)与响应字段(successCount/failCount/items[].dayNumber/items[].assignmentId/items[].arrange/items[].deductInventory)一个都没删、没改类型。 + - 内部端点 POST /v3/internal/house/deduct-room-stock、POST /v3/internal/house/restore-room-stock 请求/响应结构不变,source_type 走默认值 HOUSE_ASSIGNMENT,键格式与落库行为逐字一致。 + - 契约 HotelAssignmentReadDTO 的既有 15 个字段(assignmentId/requirementId/hotelId/roomTypeId/dayNumber/stayDate/hotelName/roomCategory/roomTypeName/protoPrice/settlementPrice/settleType/deductInventory/roomCount/remark)不动;新增的 sourceType/replaceReason 两个字段未映射到任何对外 VO,11 个消费方(OrderDetailService/OrderDetailConverter/SettlementService 等)本单未改,对外 JSON 零变化。 + - 已有酒店/房型/价格日历读取接口、订单创建/支付/详情、抢单/单日确认/删除配房等其余房务端点不受影响。 +- **历史数据**:存量 house_dual_deduction_log 行 100% 落 source_type=HOUSE_ASSIGNMENT 默认值,不迁移不回填。 + +--- + +## 八、测试环境已验证 + +**环境**:测试服网关,hl-order-service-v3 @ 1a3d3c43f(Deploy Panel 记录 dev-v3 @ 50c93199e,1a3d3c43f 为其祖先),双实例 8186/8086 均 UP,健康检查通过。 + +```text +POST /v3/admin/order/hotel-requirements/{requirementId}/assignments + 第一步 Day1 提交酒店A(不带 replaceReason) -> 200, successCount=1, assignmentId=2097856273935433730, arrange=inquiring [验证通过] + 第二步 同日改提酒店B + replaceReason=酒店A满房 -> 200, successCount=1, 新 assignmentId=2097856298618941442, arrange=inquiring [验证通过] + DB 核对: 新行(2097856298618941442) swap_from_id=2097856273935433730(回指旧行), replace_reason=酒店A满房 [验证通过] + DB 核对: 旧行(2097856273935433730) deleted_at=2026-09-10 09:14:52(非空,已软删) [验证通过] + house_dual_deduction_log 核对: 两条记录 source_type 均为 HOUSE_ASSIGNMENT, + 幂等键分别为 asgn-2097856215588499458-d1-rt2023727403196502017-h2023714929877450753(基础键) + 与 asgn-2097856215588499458-d1-rt2029944763138936833-h2029926129876320258-s1(替换叠 -s1) [验证通过] + 第三步 再次提交与第二步完全相同的配房(同酒店B)但 replaceReason=换一个理由试试 -> 200, + successCount=1, 返回同一个 assignmentId=2097856298618941442(未新建行) [验证通过] + DB 核对: 该行 replace_reason 仍是酒店A满房, 未被换一个理由试试覆写(UNCHANGED 语义) [验证通过] +``` + +DDL 验证: + +```text +SHOW CREATE TABLE group_batch_room_plan / group_batch_room_allocation -> 与 Flyway V20260908_311 定义列/索引/注释逐列比对零差异 [验证通过] +SHOW COLUMNS FROM house_hotel_assignment LIKE replace_reason -> 有该行 [验证通过] +SHOW COLUMNS FROM house_dual_deduction_log WHERE Field IN (order_id, source_type) + -> order_id Null=YES, source_type Default=HOUSE_ASSIGNMENT [验证通过] +SELECT COUNT(*) FROM house_dual_deduction_log WHERE source_type 不等于 HOUSE_ASSIGNMENT -> 0 [验证通过] +flyway_schema_history: version=20260908.311, success=1, execution_time=397ms, 无 312/313 残留 [验证通过] +``` + +验证订单:orderId=2097856215588499458,requirementId=2097856250409582593(AC-9 造数,测试用户 AC9测试客户,标签 ac9-7323)。 + +--- + +## 十、相关文档 + +- 关联 Issue: wx/HL#7323 https://git.1814.love:8443/wx/HL/issues/7323 +- 关联 PR: wx/HL#7413 https://git.1814.love:8443/wx/HL/pulls/7413 +- 后续依赖本单地基表/错误码/契约字段的单据:#7324(订房 CRUD 与团期看板)、#7325(按日确认与自动分房,落地团期分支扣减,808620-808622 起真正可达)、#7326(分房与只读)、#7327(契约合流,sourceType=GROUP_BATCH_PLAN 起真正出现在 HotelAssignmentReadDTO)。 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7323](https://git.1814.love:8443/wx/HL/issues/7323) +- **PR**: [#7413](https://git.1814.love:8443/wx/HL/pulls/7413) +- **Merge commit**: [1a3d3c43f](https://git.1814.love:8443/wx/HL/commit/1a3d3c43f) + +### 联系人 + +- **后端负责人**: @wx