22 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7325 | 团期越界户:预检 outOfRangeOrders 改按越界晚一行;旧单户 finalize 新增拒绝与错误码 808632 | admin | wx(GIT) | 修改接口 | deployed | not_required | verified | mmg | 917b1848abed2657ac4aea5a3f88c87b6b36e4d3 | 2026-09-15 | PR #7700(Issue #7325)已 squash 合并 dev-v3(合并提交 e7e11cf7d,2026-09-15 09:13)。2026-09-15 09:39-09:47 在测试服部署基线 hl-order-service-v3@40f08d9be(经 git merge-base --is-ancestor 核实为 e7e11cf7d 的后代,含本次改动)上做了真实网关取证:GET confirm-check 两次独立抓包(09:42:39、09:43:26)均命中 outOfRangeOrders[0] 含 tripNights=2、stayDate=2026-12-18、reason=OUT_OF_RANGE,新字段契约坐实。POST finalize 对同一团期三次尝试(户未抢单态、抢单重试、对照户)全部先撞既有守卫 808116(订单未抢单)——团期订单不支持逐户抢单(808650),越界户走不到抢单态,因而本轮未能端到端触达 808632;808632 的正确性目前只有单测 finalize_groupBatchOutOfRangeHousehold_throws808632WithoutAnyWrite 覆盖,非测试服实测。backend_status 记 deployed(代码已部署且核心新字段已实测),808632 分支覆盖情况见八、测试环境已验证。gateway_status=not_required:两个端点均为已有路由,未新增/修改路径或方法。frontend_status=pending:待前端确认 outOfRangeOrders 改为按越界晚一行后,页面渲染的行 key 与去重逻辑是否已按 orderId+stayDate 调整。 前端已闭环(917b1848):BoardDetailModal 越界名单按 orderId 去重计户、每户附越界晚 stayDate 列表、出发日缺失行兜底「出发日缺失」;808632 文案自解释走拦截器透 message 不建字典;BoardDetailModal.spec 3 例。 | 2026-09-15 | dev-v3 |
团期越界户: 预检列出末晚,旧单户配房 finalize 拒绝越界户
存放目录:
- 一期(v2,无 order-v3 标签的工单)→ changelogs/{YYYY-MM}/
- 二期(v3,order-v3 标签的工单)→ changelogs-v2/{YYYY-MM}/
服务: hl-order-service-v3 (端口 8086) PR: #7700 Issue: #7325 日期: 2026-09-15 影响范围: 管理后台团期订房确认预检 outOfRangeOrders 的字段与行粒度;旧版单户配房工作台 finalize 新增一类拒绝
关键变化
- 本次变了什么:GET room-plans/confirm-check 的 outOfRangeOrders 新增 tripNights(Integer)、stayDate(yyyy-MM-dd)两个字段,并且行粒度从一户一行改为一晚一行——同一户如果有多晚落在团期区间外,会出现多行、orderId 重复;出发日缺失的户仍整户一行(stayDate 为空)。旧版单户配房工作台的最终确认端点 finalize,此前对团期越界户没有任何拦截,会把该户需求直接置 DONE;本次新增拒绝,越界户点确认会拿到新错误码 808632。
- 前端调用方以前以为的是什么:outOfRangeOrders 一户一行,orderId 在这个数组里唯一;只有 orderId、orderNo、departDate、reason 四个字段。旧单户工作台的 finalize 只要满足配房覆盖度和状态闸口就能确认,不会因为团期越界被拦。
- 实际现在是什么:同一户多晚越界会重复出现多行,前端如果拿 orderId 当行 key 或做去重,会漏渲染除第一晚外的其余越界晚;正确的行 key 是 orderId 加 stayDate。旧单户工作台对团期越界户点 finalize 会被 808632 拒绝、零写入,需求状态不变;这是新发现并修复的一个缺陷(此前会静默放过,把越界户的需求错误地标记为已完成)。
一、背景(选填)
工单 7325 AC-18 与 AC-54:团期看板新引入的按日确认与重算分房链路已经能正确识别越界户并阻塞团级配房完成标志,但两处配套能力没跟上——预检列表只报户不报晚,房务看不出订不了的具体是哪一晚;而另一条历史更早的入口(旧版单户配房工作台的 finalize,工单 2770 遗留)完全不检查团期越界,会绕过团期看板的判定直接把越界户标记完成。
| 维度 | 证据 |
|---|---|
| outOfRangeOrders 展开逻辑 | GroupBatchRoomDayConfirmManager.java 第 533 至 550 行 toOutOfRangeOrders:按越界入住日逐晚生成一行,晚列表为空时降级为整户一行 |
| finalize 新拒绝点 | HouseAssignmentService.java 第 4305 至 4324 行 assertNotGroupBatchOutOfRangeHousehold:判定依据与预检共用同一个契约 |
| 808632 撞号核查 | PR 正文自报:合入前对 origin dev-v3 全仓 grep 808632 零命中 |
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 订房确认预检 | GET | /v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm-check |
响应字段新增+行粒度变更 | outOfRangeOrders 新增 tripNights、stayDate,改为按越界晚一行 |
| 2 | 旧单户最终确认 | POST | /admin/house/assignments/requirements/{requirementId}/finalize |
新增拒绝分支 | 团期越界户新增 808632 拒绝,零写入 |
三、接口详情
1. 订房确认预检 GET /v3/admin/house/group-batches/{groupBatchId}/room-plans/confirm-check
VO: GroupBatchRoomConfirmCheckReqVO → GroupBatchRoomConfirmCheckRespVO
使用场景
房务在团期看板发起按日确认或整团确认之前调用,零副作用,展示逐日差额表、阻塞名单(无基线户、越界户)与整团能否确认的判定。本次起,越界户名单里能看到具体是哪一晚越界,便于房务判断是该团期管理员改期、还是等房务自己释放分房。完整端点契约(stayDate 查询参数、days、noBaselineOrders 等其余字段、错误码、角色门)见既有 changelog:changelogs-v2/2026-09/11_7325_团期房务按日订房确认H7-H8-预检-新增接口-管理后台.md,本文件只描述 outOfRangeOrders 的变化。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | 是 | - | 团期主订单 ID |
| stayDate | Query | LocalDate | 否 | yyyy-MM-dd,不传则返回全部日 | 本次未改;只影响 days,不影响 outOfRangeOrders(后者恒返回全团越界名单) |
出参字段表 Result<GroupBatchRoomConfirmCheckRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data.outOfRangeOrders | Array | 本次改动:越界或出发日缺失户,按越界晚展开的行列表 |
| data.outOfRangeOrders[].orderId | String | 子订单 ID;同一户多晚越界时在数组内重复出现 |
| data.outOfRangeOrders[].orderNo | String | 订单号 |
| data.outOfRangeOrders[].departDate | String | 该户出发日,缺失为空 |
| data.outOfRangeOrders[].tripNights | Integer | 新增字段,该户行程晚数,两者皆空则为空 |
| data.outOfRangeOrders[].stayDate | String | 新增字段,越界的入住日;出发日缺失时为空 |
| data.outOfRangeOrders[].reason | String | 原因 OUT_OF_RANGE 或 DATE_MISSING,取值域本次未变 |
请求示例
GET /v3/admin/house/group-batches/9001/room-plans/confirm-check
响应示例
2026-09-15 09:42:39 测试服真实网关抓包(roleId=4 房务,团期 2099674449308545025,户 2099674449094635522 出发日被 SQL 改到团期区间外制造越界,落在区间外的一晚是 2026-12-18):
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2099674449308545025",
"batchStatus": "RESOURCE_PREPARING",
"stageAllowed": true,
"baselineExists": true,
"hotelReady": false,
"maxRooms": 10,
"ready": false,
"blockedByOutOfRange": true,
"days": [
{ "stayDate": "2026-12-16", "planStatus": "NONE", "plannedTotal": 0, "demandedTotal": 2, "exceedsMaxRooms": false, "demand": { "STANDARD": 2 }, "mismatch": [ { "roomCategory": "STANDARD", "planned": 0, "demanded": 2, "diff": -2 } ], "stale": false, "dayReady": false, "outOfBatchRange": false },
{ "stayDate": "2026-12-17", "planStatus": "NONE", "plannedTotal": 0, "demandedTotal": 3, "exceedsMaxRooms": false, "demand": { "STANDARD": 3 }, "mismatch": [ { "roomCategory": "STANDARD", "planned": 0, "demanded": 3, "diff": -3 } ], "stale": false, "dayReady": false, "outOfBatchRange": false },
{ "stayDate": "2026-12-18", "planStatus": "NONE", "plannedTotal": 0, "demandedTotal": 1, "exceedsMaxRooms": false, "demand": { "STANDARD": 1 }, "mismatch": [ { "roomCategory": "STANDARD", "planned": 0, "demanded": 1, "diff": -1 } ], "stale": false, "dayReady": false, "outOfBatchRange": true }
],
"noBaselineOrders": [],
"outOfRangeOrders": [
{ "orderId": "2099674449094635522", "orderNo": "HL20260915093933212", "departDate": "2026-12-17", "tripNights": 2, "stayDate": "2026-12-18", "reason": "OUT_OF_RANGE" }
]
},
"success": true
}
第二次独立抓包(09:43:26,H7 逐日确认 12-16/12-17 两天订房后重新查询,outOfRangeOrders 内容不变,证明新字段不是偶然值):
{ "orderId": "2099674449094635522", "orderNo": "HL20260915093933212", "departDate": "2026-12-17", "tripNights": 2, "stayDate": "2026-12-18", "reason": "OUT_OF_RANGE" }
出发日缺失的户(整户一行,stayDate 为空;本次未实测,结构取自源码定义):
{ "orderId": "900004", "orderNo": "ORD202606120004", "departDate": null, "tripNights": null, "stayDate": null, "reason": "DATE_MISSING" }
空数据 / 降级响应
无越界户或出发日缺失户时 outOfRangeOrders 为空数组,本次未改此形态:
{ "code": 200, "data": { "outOfRangeOrders": [] }, "success": true }
错误响应
本端点错误码本次未改,见既有 changelog(808611、808612、808613 归属门,808090、808091 角色门):
{ "code": 808612, "message": "该团期尚未被房务认领,请先到团期抢单池认领", "data": null, "success": false }
业务边界
- outOfRangeOrders 不再是一户一条的唯一名单:调用方若把它当 Map 用 orderId 做 key 去重,会丢掉同一户的其余越界晚;正确用法是把 orderId 加 stayDate 当联合 key。
- 名单本身仍按户去重:只在出参这一层展开成多行,阻塞判定 blockedByOutOfRange 不受展开影响,仍是名单非空即阻塞。
- tripNights 与 stayDate 可能同时为空:当该户出发日缺失(DATE_MISSING)时两个字段都为空,不代表数据异常。
- 鉴权、角色门、其余字段行为:与既有 confirm-check 契约一致,本次未改。
2. 旧单户最终确认 POST /admin/house/assignments/requirements/{requirementId}/finalize
VO: 无请求体(仅 requirementId 路径参数) → Result<FinalizeRespVO>
使用场景
房务在旧版单户配房工作台(非团期看板)对某个用房需求点最终确认时调用,触发状态机 ASSIGNMENT_CONFIRM_FULL 到 FINALIZE,把需求推进到 DONE。本次起,若该需求所属订单是团期子订单、且被团期侧判定为越界户,点确认会被拒绝,前端需要引导房务改走团期看板处理(改期或等团期管理员处理),不能再从这个旧入口把越界户标记完成。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| requirementId | Path | Long | 是 | - | 用房需求主键,本次未改 |
出参字段表 Result<FinalizeRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data.newStatus | String | 需求新状态,DONE 表示已最终确认,本次未改 |
| data.finalizedAt | String | 确认时间戳,格式 yyyy-MM-ddTHH:mm:ss,本次未改 |
请求示例
POST /admin/house/assignments/requirements/2099319285540110337/finalize
响应示例
正常放行(散客单或团期非越界户,行为与改动前逐字节相同):
{ "code": 200, "message": "成功", "data": { "newStatus": "DONE", "finalizedAt": "2026-09-15T15:30:00" }, "success": true }
空数据 / 降级响应
本端点不存在成功但空数据的形态,校验全部通过即返回 newStatus 与 finalizedAt。
错误响应
新增错误码,源码定义在 HouseGroupBatchErrorCode.java 第 312 行,占位符为订单号(下方 JSON 取自源码定义,不是测试服抓包——本轮 2026-09-15 三次真实调用 finalize 全部先被更前置的既有守卫 808116(订单未抢单,请先抢单再配房)拦下,没能走到本次新增的 808632 判定;越界拒绝码 808632 的正确性目前只由单测 finalize_groupBatchOutOfRangeHousehold_throws808632WithoutAnyWrite 覆盖,见八、测试环境已验证):
{
"code": 808632,
"message": "该户(订单 HL20260915115141720)有住宿晚落在团期出行区间之外或出发日缺失,需团期管理员处理后才能最终确认",
"data": null,
"success": false
}
既有错误码,本次未改,举例:
{ "code": 808182, "message": "该需求已最终确认,请勿重复操作", "data": null, "success": false }
2026-09-15 测试服真实抓包(更前置的既有守卫拦截,说明团期越界户走这条入口时更常先撞到抢单守卫而不是本次新增的越界拒绝):
{ "code": 808116, "message": "订单未抢单, 请先抢单再配房", "data": null, "success": false }
业务边界
- 拒绝时机在状态闸口之后、任何写之前:HouseAssignmentService.java 第 2884 至 2890 行——先判需求当前 status 必须是 PROCESSING,否则既有 808182 类错误码;status 闸口通过后立刻判团期越界,808632 抛出前该请求零写入,不改需求状态、不改配房、不写操作日志。
- 只拒团期越界户,不拒散客单:判定入口先判是否团期订单,散客单直接跳过,零额外开销。
- 判定名单与团期看板预检共用同一个契约:与本文件端点 1 的 outOfRangeOrders 名单同源,不会出现预检说没越界、finalize 却拒绝的口径分裂。
- 团期未成团时走既有闸口,不会误判越界:越界判定只在团期已成团、能解析出 groupBatchId 时才生效。
四、契约约束与正确调用方式(接口类必写)
本节只写后端接受拒绝 payload 的规则与调用后必须知道的取值规则,不写 UI 渲染建议。
正确与错误调用方式对照
| 场景 | 说明 |
|---|---|
| 正确:用 orderId 加 stayDate 作为 outOfRangeOrders 的行 key | 同一户多晚越界会重复出现多行,orderId 单独不唯一 |
| 正确:旧单户工作台点 finalize 前,先看该需求所属订单是否团期子订单 | 团期越界户会被 808632 拒绝,可提前在 UI 上禁用按钮或给出引导文案 |
| 错误:把 outOfRangeOrders 当一户一条处理并直接渲染成列表 | 会重复渲染同一户多次,且遗漏除第一晚外的其余越界晚信息 |
| 错误:认为旧单户工作台 finalize 只要 200 就代表已完成 | 团期越界户会先拿到 808632,需要处理引导后才能重试 |
切换状态时的必要动作
端点 1(预检)是只读查询,无状态切换。端点 2(finalize)被 808632 拒绝时状态不发生任何变化,需求仍停留在拒绝前的状态,前端不需要做任何回滚或刷新之外的动作,正常刷新一次该需求详情即可。
五、数据库行为(涉及写操作时必写)
| 端点 | 场景 | 写行为 |
|---|---|---|
| 端点 1(confirm-check) | 任意 | 无写操作,本端点全程只读 |
| 端点 2(finalize) | 团期越界户,808632 拒绝 | 零写入:不改需求状态与配房,不写操作日志 |
| 端点 2(finalize) | 非越界户,正常放行 | 与改动前逐字节相同,本次未改 |
六、边界行为
- 端点 1:未登录返回 401;角色门与归属门错误码见既有契约(808090、808091、808611、808612、808613)
- 端点 2:未登录返回 401;需求不存在或非本人抢单为既有错误码,本次未改;状态非 PROCESSING 为既有 808182 类错误码,本次未改;团期越界户新增 808632,零写入
- 团期未成团:端点 2 的越界判定不生效,未成团时既有闸口已先拒绝
六.5、枚举 / 数据字典(接口出现枚举时必写)
越界原因(outOfRangeOrders 的 reason 字段)
所属字段: outOfRangeOrders[].reason | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
OUT_OF_RANGE |
出发日外 | 该户出发日落在团期区间之外;本次起该原因下会按越界晚展开多行 |
DATE_MISSING |
日期缺失 | 该户出发日为空,无法判定是否在团期内;整户一行,stayDate 为空 |
本次未新增枚举取值,仅改变了同一取值域下的行粒度。
六.6、修改前后对比(修改/删除类接口必写,新增跳过)
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| outOfRangeOrders 行粒度 | 一户一行 | 一户一晚一行,多晚越界重复出现,orderId 不唯一 |
| outOfRangeOrders 的 tripNights | 无此字段 | 新增,Integer,该户行程晚数 |
| outOfRangeOrders 的 stayDate | 无此字段 | 新增,String,具体越界的入住日 |
| outOfRangeOrders 的 orderId、orderNo、departDate、reason | 存在 | 不变 |
| finalize 响应体字段结构 | newStatus、finalizedAt | 不变,新增的是拒绝分支,不是字段 |
行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 旧单户工作台对团期越界户点 finalize | 放行,需求置 DONE,绕过团期看板判定,属缺陷 | 拒绝(808632),零写入 |
| outOfRangeOrders 展示同一户多晚越界 | 只看得到户,看不出具体哪几晚 | 每晚一行,可直接对照 days 定位 |
六.7、影响评估(修改/删除类必写)
- 是否破坏向后兼容:端点 1 的字段是新增,旧前端忽略未知字段不受影响,但行粒度变化是破坏性的——若前端按 orderId 做 Map 或去重,会静默丢数据,不报错,只是少渲染。端点 2 新增的 808632 是新错误码,前端若无兜底分支会按未知错误码展示,不会崩溃但文案可能不友好。
- 前端是否必须同步上线:端点 1 建议同步,否则多晚越界的户可能只展示其中一晚。端点 2 不同步上线也不会导致调用失败,只是越界户点 finalize 会看到不认识的错误码文案。
- 前端 workaround 清理点:无。
七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- 仅影响:团期订房确认预检 outOfRangeOrders 的字段与行粒度;旧版单户配房工作台 finalize 对团期越界户新增拒绝。
- 零影响:
- confirm-check 的 days、noBaselineOrders、ready、hotelReady、blockedByOutOfRange 等其余字段
- 团期看板其余端点(按日确认、整团确认、人工微调分房、重算分房)
- 散客单(非团期子订单)的 finalize 行为
- finalize 既有的状态闸口、配房覆盖度校验、幂等窗口
- 工单 7459(订单取消 outbox 改造)、工单 7326(分房重算已完成户回退)等同批次改动,见另外的 changelog 文件
八、测试环境已验证
部署:PR #7700(合并提交 e7e11cf7d,2026-09-15 09:13)经 git merge-base --is-ancestor e7e11cf7d 40f08d9be 核实是测试服 2026-09-15 09:39 部署基线 hl-order-service-v3@40f08d9be 的祖先;三次独立 deploy-status.sh 探测(09:39/09:42/09:43)均返回同一版本号,取证期间部署未漂移。
端点 1(confirm-check)新字段真实网关取证:
| 时刻 | 场景 | 结果 |
|---|---|---|
| 09:42:39 | 团期 2099674449308545025,户 2099674449094635522(出发日被 SQL 改到 2026-12-17,返程日 2026-12-19,制造 2026-12-18 越界晚) | outOfRangeOrders[0] 含 tripNights=2、stayDate=2026-12-18、reason=OUT_OF_RANGE,字段真实存在 |
| 09:43:26 | 同一团期,H7 逐日确认 12-16/12-17 两天订房后重新查询 | outOfRangeOrders 内容不变(仍是同一条越界记录),证明字段不受确认动作影响、非偶然值 |
端点 2(finalize)拒绝路径实测:
| 时刻 | 场景 | 返回 |
|---|---|---|
| 09:46:07 | 户 2099674451917402113 首次 finalize(未抢单态) | 808116「订单未抢单,请先抢单再配房」 |
| 09:46:29 | 同户先尝试 POST .../claim 转单 | 808650「团期订单不支持逐户抢单/转单,请到团期抢单池整团认领」——团期子订单结构性走不了逐户抢单入口 |
| 09:46:30 | 同户重试 finalize | 仍 808116 |
| 09:47:19 | 对照户 2099674452341026817(同团另一户,同样未抢单) | 仍 808116 |
结论(如实标注):本轮三次尝试全部先撞更前置的既有守卫 808116,团期越界户在这条入口下没有能触达 808632 的可行路径(团期订单不支持逐户抢单,808650 直接堵死了让户进入「已抢单」态从而继续走到越界判定的路子)。808632 的正确性目前只有单测覆盖,未在测试服端到端验证:
finalize_groupBatchOutOfRangeHousehold_throws808632WithoutAnyWrite(HouseAssignmentServiceTest.java:3797-3798)
→ 团期越界户经单户入口最终确认被拒且零写入 ✓(本机自动化单测,非测试服抓包)
PR 正文自报的其余本机定向验证:
定向 6 类(AdjustmentServiceSubmitTest 78 例、AdjustmentServiceTest 10 例、AdjustmentSnapshotTest 43 例、
HouseAssignmentServiceTest 183 例含 Skipped 6、RedLineArchTest 12 例、HouseModuleBoundaryArchTest 5 例)
基线与复绿一致,MVN_EXIT=0
网关:本次未新增/修改路径,两个端点均沿用既有路由,取证均经网关(via: gateway)完成。
十、相关文档
- 关联 Issue: wx/HL#7325
- 关联 PR: wx/HL#7700
- confirm-check 端点完整契约(本次未变部分): changelogs-v2/2026-09/11_7325_团期房务按日订房确认H7-H8-预检-新增接口-管理后台.md
- 同一 Issue 下的另一改动(团期子订单改期出口,PR #7743,已合并): changelogs-v2/2026-09/15_7325b_团期越界户改期出口只能回团期出发日-修改接口-管理后台.md
关联 / 联系人
链接
联系人
- 后端负责人: @wx