--- schema: "hl-changelog/v2" ticket: "8023" title: "需求汇总与确认预检不再只认 needs_hotel 标记位 + 提交住宿需求时就地纠正该标记" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "9a14ad866b3ef6ac9acb0ca9ab9e9f0fe98f6559" 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,同事务,已为真时不写)——该标记原先只在创单时按产品有没有配酒店派生一次、之后全仓无写通道,存量错单改不回来。既有字段一个没删没改,入参与路径不变。 前端实证维持 not_required(mmg 2026-09-20):requirement-summary/dailyRoomBreakdown/hotelNeededOrderCount/hotelFlagMismatchOrderCount 全仓零命中(汇总端点未接入,同 #7925/#7937 结论);confirm-check 已消费(orderV2GroupBatch.js)但出参结构不变,ready/missing 直渲自动受益;hotel-requirement 标记纠正为服务端同事务副作用、响应不变,零感知。 2026-09-20 前端接入(mmg):「用房 · 汇总」建页随 hl-admin v2.1 交付——requirement-summary 经 getGroupRequirementSummary 接入「查看需求」页 RoomSummarySection:户数条(在团/需订房/已提交)+不一致只提示不扣减+Day N|酒店|房型|所需间数扁平表;stayDate null 按 Day N、hotelName null 显「未知酒店」不隐藏行、roomCategoryName null 回落编码、全天合计直读 rooms[] 禁跨酒店累加、hotelId null 组间数不丢;整团确认/按户打回后自动重拉。定向 Vitest 17/17,checkpoint 全绿。" 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` #### 使用场景 团期详情「查看需求」页顶部的「用房 · 汇总」表:全团逐日要订几间、什么房型,运营据此向酒店报数。 #### 入参 本次入参**不变**。 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | 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 ``` #### 响应示例 ```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` #### 使用场景 团期管理员点「确认需求」之前的缺失预检;确认时用同一份判定决定放行哪些户的需求给房务。 #### 入参 本次入参**不变**。 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | 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 ``` #### 响应示例 ```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` #### 使用场景 定制师逐晚填报住宿需求并提交。 #### 入参 本次入参**不变**。 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | 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 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