62 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 | 7324 | 房务团期看板与整团按日订房计划 CRUD:H1-H6 + 整团释放共 7 端点;H1 total=-1 新契约、H5/release-all 各有一处「200 但看似没生效」的静默分支 | admin | wx(GIT) | 新增接口 | deployed | not_required | verified | mmg | 6710ad4a | 2026-09-11 | 2026-09-10 已部署测试服并实测通过(squash 07feaffe0,PR #7465)。⚠️ 部署顺序:本单唯一跨服务契约变更是 HouseRoomTypeDTO.roomCategory(resource 侧新增字段,HouseRoomTypeDTO.java:51)与 HouseRoomTypeFeignVO.roomCategory(order-v3 侧消费,HouseRoomTypeFeignVO.java:47),hl-resource-service 必须先于 hl-order-service-v3 部署——反序会让 order-v3 拿到 roomCategory=null,H4/H5 的 fail-closed 房型守卫(HouseRoomTypeGuard.requireRoomTypes)把每次提交全部拒成 808691。2026-09-10 实测确实撞过一次短暂不同步窗口(GET /v3/admin/house/group-batches 一度 404),重滚 order-v3 后恢复,未影响最终验收。⚠️ 与工单 #7324 核对:工单口径「HouseGroupBatchErrorCode 新增 12 个错误码」与源码不符,squash 07feaffe0 实际新增 17 个常量(808600-808603 共 4 个 + 808608 共 1 个 + 808611-808619 共 9 个 + 808690-808692 共 3 个 = 17,见 HouseGroupBatchErrorCode.java 本次 diff +165/-2 行),本篇按源码 17 个如实列出,以代码为准。 前端已交付(commit 6710ad4a):H1 看板/H2 详情/H3 逐户/H4-H6 写口/release-all 七端点全接入,total=-1 降级显「—」+压掉虚构共N条、H5 脏字段检测+version 乐观锁、release-all 假成功分支改文案、组长只读隐藏写口;已知缺口(用户拍板留 API):H1 后端忽略 CANCELLED 过滤,整团释放按钮暂无可达入口,待后端放开 H1 即自动激活。 整团释放可达性需求已单列提交后端:backend-requests/2026-09/11_7324_放开H1看板batchStatus过滤CANCELLED-整团释放可达.md(commit 4aa3037,状态 proposed 待后端排期),前端零改动等待。 2026-09-11 接线闭环(commit d487d2be):后端答复整团释放可达路径已存在(#7322 我的团不过滤 batch_status 透传,CANCELLED 团在列,H2 详情无阶段闸门),我的团行加「订房核对」入口打开 BoardDetailModal,CANCELLED 团由 canReleaseAll 出「整团释放」;前端零后端改动等待解除。 | 2026-09-10 | dev-v3 |
房务团期看板与整团按日订房计划 CRUD(H1-H6 + 整团释放)
服务:
hl-order-service-v3(H1-H6 + release-all 全部落在此服务);跨服务契约方hl-resource-service(房型大类字段回填) PR: #7465 Issue: #7324 日期: 2026-09-10 影响范围: 房务团期看板全新页面(列表 / 详情 / 逐日需求明细)+ 团期按日订房计划的新增 / 改 / 删 / 整团释放,管理后台全新功能,无存量前端调用方
⚠️ 关键变化
这是一批全新端点,没有存量调用方,但有四处「后端行为对,前端按直觉写会踩坑」的地方,逐一说明:
1.【新契约】H1 列表的 total 可能是 -1,语义是「未统计」不是负数
GET /v3/admin/house/group-batches 在不传 planStatus 时走普通数据库分页,total 与全仓其它分页接口一样恒 >= 0。
但一旦传了 planStatus(PENDING / CONFIRMED),后端走内存过滤降级路径(HouseGroupBatchBoardManager.java:99-114):先按认领/关键字/状态等条件扫一页最多 500 条候选(PLAN_STATUS_SCAN_LIMIT = 500,HouseGroupBatchBoardManager.java:72),逐条在内存里判定 planStatus 是否匹配再切页。若候选总数超过 500(扫描页被填满),真实总数已经无法算出,此时 PageResult.total 返回 -1(TOTAL_UNKNOWN,HouseGroupBatchBoardManager.java:81、268-275)。
全仓所有既有分页接口的 total 都保证非负,前端惯用它算「共 N 条」文案或总页数(Math.ceil(total / pageSize))。不识别这个新语义会渲染出「共 -1 条」或算出负数页码。前端拿到 total === -1 时应显示「—」或类似占位文案,并提示用户收窄筛选条件(缩小 stayDateFrom/To、departDateFrom/To 或去掉 planStatus),不要按负数处理分页控件。
同时注意 PageResult.total 是 Java 基本类型 int(PageResult.java:15),不是可空类型,正常场景下的 0 与「无匹配」是同一个值,只有 planStatus 降级路径的扫描填满态才会出现 -1。
2.【易漏分支】H5 只传 version 字段:在 PENDING 行是「静默成功」,在 CONFIRMED 行是 808692
PUT .../room-plans/{planId} 的请求体语义是「字段为 null = 本次不改」(GroupBatchRoomPlanItemReqVO.java:19-21)。若前端「保存」按钮无差别提交整个表单对象、但用户实际没有改任何字段,等价于只传了必填的 version:
- 若该计划行当前是
PENDING:五要素(入住日/酒店/房型/间数/大类)均判定为未变化,服务端进入「原地更新」分支(isIdentityChanged返回false),构造一个只设了planId/version(其余字段原样取自请求,未传的为null)的patch对象调用updateWithVersion(doUpdateInPlace,GroupBatchRoomPlanManager.java:230-249)。updateWithVersion的实现是roomPlanMapper.updateById(plan)(GroupBatchRoomPlanService.java:174-178),MyBatis-Plus(3.5.5,根pom.xml:46)updateById的默认FieldStrategy是NOT_NULL——null字段不会进 UPDATE 的 SET 子句;GroupBatchRoomPlanDO的protoPrice/settlementPrice/settleType/deductInventory/remark五个字段均未加@TableField(updateStrategy=...)覆盖(GroupBatchRoomPlanDO.java:46/48/50/52/61),全仓在 order-v3 与 hl-common 范围内也没有任何update-strategy/FieldStrategy全局覆盖。因此只传version的请求返回 200,SET 子句里只有乐观锁version(+1)与审计时间列,不会清空或改动任何业务字段——这与 VO 注释承诺的「字段为null= 本次不改」是一致的,是一次「空更新 +version自增」,不是字段清空。 - 若该计划行当前是
CONFIRMED:同样判定五要素未变化,但已确认行禁止原地更新(version是扣减幂等键的一部分,见「五、数据库行为」),服务端抛808692(GroupBatchRoomPlanManager.java:206-212)。
前端仍应只把用户实际改过的字段放进请求体(未改的留 null),但理由不是「防止字段被清空」——PENDING 行的原地更新不会清空任何字段。真正需要注意的是:只要走进这条分支,无论字段是否真的变化,服务端都会把 version +1(本地缓存的旧 version 随之失效,下次操作若仍带旧值会撞 808608),并在团期时间线上留下一条 UPDATE_IN_PLACE 记录(recordReviseTimeline,GroupBatchRoomPlanManager.java:245-246)——用户没有实际改动时不应发起这次 PUT,前端应做「脏字段检测」,没有脏字段就不提交。
3.【易漏分支】release-all 会返回「200 成功,但什么都没释放」
POST .../room-plans/release-all 只在该团完全没有 active 计划行也没有 active 分房行时才返回 808619(GB_ROOM_PLAN_RELEASE_NOTHING,幂等出口,零写入)。
但如果该团已流团(CANCELLED)、计划行全部是入住日早于操作当日的已确认(CONFIRMED)行——即整团都是「已发生的间夜」——服务端会判定为「有东西但全部该保留」,返回 200,且:
releasedPlanIds/releasedAllocationIds/markedLogIds均为空数组retainedPastPlanIds非空(列出被保留的已确认行 ID)warnings含"PAST_STAY_RETAINED"
(GroupBatchRoomPlanManager.java:394-409、GroupBatchRoomReleaseResult.java:11-16 显式把这两种情形拆成不同分支,理由见该 DTO 的类注释:「团里根本没有订房」要给 808619 的幂等出口,「订房全都是已发生的间夜、按规矩保留了」是正常业务态必须返 200。)
前端对 release-all 的成功响应,不能只看 HTTP 200 / success:true 就提示「已全部释放」,必须检查 releasedPlanIds.length === 0 && retainedPastPlanIds.length > 0 这一组合,命中时改用「已发生间夜已按规定保留,未做任何释放」这类文案,并建议展示 retainedPastPlanIds 供核对。
4. H4 响应体携带两个恒为空数组的字段
POST .../room-plans(H4)与 PUT .../room-plans/{planId}(H5)共用同一个响应 VO GroupBatchRoomPlanRespVO(既是数组元素类型也是 H5 单行响应类型)。这个 VO 上有 warnings: string[] 与 removedManualAllocIds: string[] 两个字段(GroupBatchRoomPlanRespVO.java:105-109)——它们对 H5 的「删旧建新」「原地更新(核单期)」分支有真实语义(见下方接口详情),但对 H4 新建恒为空数组 [](GroupBatchRoomPlanConverter.toRespVO 无条件 setWarnings(Collections.emptyList()) / setRemovedManualAllocIds(Collections.emptyList()),GroupBatchRoomPlanConverter.java:51-52,H4 的 doSave 未再覆盖)。
若前端按「H4 是纯新增,字段集合不会变」的假设去做响应类型/契约测试,会把这两个恒空字段判定为「意外新增字段」。它们是设计如此(VO 复用),不是漏改。
一、背景
#7324 是「团期房务」整体方案(#7323 定案)的第一张落地单,交付两块能力:
- 只读看板(H1-H3):房务在「已认领团」维度查看团期配房进度(列表 / 详情 / 逐日需求明细),供每日核对「需求 vs 已订」。
- 写口(H4-H6 + release-all):按日订房计划的新增 / 改 / 删,以及流团后的整团批量释放。
包落位与错误码段位按 #7323 定案 #25/#17 预先占号(HouseGroupBatchErrorCode.java 类头注释 :6-32):808600-808603/808608/808611-808619/808690-808692 归 #7324,808604/808605/808607/808609/808610 留给 #7325(按日确认与自动分房,尚未开发),808606 留给 #7326(分房与只读),808650-808662 已由已上线的 #7322(团期整团抢单)占用。本次交付前段位表本身有笔误已被修正:808611 此前误记在 #7325 行,与本单 GB_ROOM_PLAN_BATCH_DATE_MISSING 撞号;808612-808619、808650-808662、808690-808694 整段此前没写进表——已在本次 diff 里一并修正(HouseGroupBatchErrorCode.java:15-29)。
新增字段的跨服务契约:团期订房要在提交时把「房型大类」(roomCategory,判「对平」的分组键)以 hl-resource-service 权威值覆盖客户端传入值,防止客户端伪造已对平;为此 hl-resource-service 的 HouseRoomTypeDTO 与 hl-order-service-v3 消费侧的 HouseRoomTypeFeignVO 都新增了 roomCategory 字段(详见「五、数据库行为」与本文件顶部部署顺序说明)。网关侧 /v3/admin/** 路由块此前已通配到相应服务(#3264),本单零网关改动。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 房务团期看板列表(H1) | GET | /v3/admin/house/group-batches |
新增接口 | planStatus 过滤降级为内存扫描,候选超 500 时 total=-1 |
| 2 | 团期配房详情(H2) | GET | /v3/admin/house/group-batches/{groupBatchId} |
新增接口 | 逐日按房型需求/已订/差额 + 该日计划行 + 各户特殊诉求 |
| 3 | 团期逐日用房需求明细(H3) | GET | /v3/admin/house/group-batches/{groupBatchId}/room-requirements |
新增接口 | 含各户拆分,按最近一次放行房务的需求版本 |
| 4 | 整团按日提交订房计划(H4) | POST | /v3/admin/house/group-batches/{groupBatchId}/room-plans |
新增接口 | 追加式,不做集合对账;同格子拒 808614 |
| 5 | 修改单条订房计划(H5) | PUT | /v3/admin/house/group-batches/{groupBatchId}/room-plans/{planId} |
新增接口 | 字段 null=不改;五要素变化=删旧建新,否则原地更新 |
| 6 | 删除单条订房计划(软删,H6) | DELETE | /v3/admin/house/group-batches/{groupBatchId}/room-plans/{planId} |
新增接口 | 已流团团期也放行;已发生间夜仍冻结 |
| 7 | 整团批量释放订房计划 | POST | /v3/admin/house/group-batches/{groupBatchId}/room-plans/release-all |
新增接口 | 仅已流团(CANCELLED)团期可用;可能 200 但零释放 |
三、接口详情
1. 房务团期看板列表(H1) GET /v3/admin/house/group-batches
VO: HouseGroupBatchBoardPageReqVO(GET 直接绑定)→ Result<PageResult<HouseGroupBatchBoardSimpleRespVO>>
(Controller:HouseGroupBatchBoardController.java:52-59;编排:HouseGroupBatchBoardManager.page,:99-114)
使用场景
房务每日打开的「我的团」工作台首页,只显示已被房务整团认领的团(未认领的团在团期抢单池页,两页以认领动作为界互斥,HouseGroupBatchBoardController.java:48-50)。
入参字段表(HouseGroupBatchBoardPageReqVO.java)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
scope |
Query | String | 否 | ≤8 字;MINE(默认)/ALL |
ALL 是组长/超管监督视图,普通房务传 ALL 得 808092(:27-29) |
claimerAdminId |
Query | Long | 否 | 仅 scope=ALL 生效,scope=MINE 时忽略 |
按认领人筛(:31-32) |
planStatus |
Query | String | 否 | ≤16 字;PENDING/CONFIRMED,不传=全部 |
触发内存过滤降级路径,见「关键变化 1」(:42-44) |
batchStatus |
Query | String | 否 | ≤200 字,逗号分隔多选;不传取默认四态;传 CANCELLED 被忽略 |
默认四态=RESOURCE_PREPARING,MATERIAL_PREPARING,PENDING_DEPARTURE,TRAVELLING(HouseGroupBatchBoardManager.java:60-64);非法状态码同样被过滤掉,不报错(:224-236) |
stayDateFrom / stayDateTo |
Query | LocalDate(ISO) | 否 | — | 住期区间筛选:团期 [出发日,结束日](两端闭) 与查询窗口有交集即命中,谓词是 depart_date <= stayDateTo AND end_date >= stayDateFrom。⚠️ 这里是闭区间,与下文 H4/H5 校验入住日合法性用的 [出发日,结束日)(右开,结束日是离店日不产生间夜)不是同一个口径,别混用 |
departDateFrom / departDateTo |
Query | LocalDate(ISO) | 否 | — | 出发日区间 |
keyword |
Query | String | 否 | ≤32 字 | 团期号或产品名包含匹配 |
page |
Query | Long | 否 | >= 1,默认 1 |
— |
pageSize |
Query | Long | 否 | 1~50,默认 20 |
上限比房务其它列表(200)小得多——本页每行都要解析各户需求 JSON(:75-84) |
出参字段表 PageResult<HouseGroupBatchBoardSimpleRespVO>
PageResult(PageResult.java): records: T[]、total: int(⚠️ 见「关键变化 1」,可能为 -1)、page: int、pageSize: int。
HouseGroupBatchBoardSimpleRespVO 每项(HouseGroupBatchBoardSimpleRespVO.java:23-127):
| 字段 | 类型 | 说明 |
|---|---|---|
groupBatchId |
String(雪花 ID,ToStringSerializer) |
运营团期 ID |
batchNo |
String | 团期号,如 GB-26-000001 |
productId |
String | 产品 ID |
productName / batchName / batchLabel |
String | 产品名/班期名/第 N 期快照 |
batchStatus / batchStatusText |
String | 状态码/中文,如 RESOURCE_PREPARING/「资源准备中」 |
departDate / endDate |
LocalDate(可空) | 团期出发日/结束日(离店日) |
nights |
Integer(可空) | 间夜数 = 结束日 − 出发日 |
enrolledRooms / enrolledPeople |
Integer | 已报名房数(户数)/人数 |
requirementConfirmed |
Boolean | 团级需求已确认标记;false 不代表从列表消失,只提示「待管理员重新确认」 |
hotelReady |
Boolean | 配房完成标志;本单只在整团释放时置回 false,不置 true |
claimerAdminId / claimerName / claimedAt |
String/String/LocalDateTime | 认领房务信息 |
boardStatus |
String | NOT_STARTED/PARTIAL/ALL_CONFIRMED(:86-93) |
demandDays / plannedDays / confirmedDays / mismatchDays |
Integer | 有需求/有计划行/全确认/存在差额的日历日数 |
demandRoomNights / plannedRoomNights / confirmedRoomNights |
Integer | Σ需求/Σ已订/Σ已确认 间·晚 |
outOfRangeHouseholdCount |
Integer | 越界/日期缺失户数(阻塞整团配房完成,但不从需求分母里剔除) |
days[] |
DaySummary[] |
逐日摘要:stayDate、demandRooms、plannedRooms、confirmedRooms、dayStatus(NONE/PENDING/CONFIRMED/MIXED)、mismatch(Boolean) |
请求示例
GET /v3/admin/house/group-batches?scope=MINE&planStatus=PENDING&page=1&pageSize=20
响应示例(正常态)
{
"code": 200, "success": true,
"data": {
"records": [{
"groupBatchId": "20260610001", "batchNo": "GB-26-000001",
"batchStatus": "RESOURCE_PREPARING", "batchStatusText": "资源准备中",
"boardStatus": "PARTIAL", "demandDays": 5, "plannedDays": 3, "confirmedDays": 1,
"demandRoomNights": 20, "plannedRoomNights": 12, "confirmedRoomNights": 4,
"outOfRangeHouseholdCount": 0, "days": [ /* ... */ ]
}],
"total": 37, "page": 1, "pageSize": 20
}
}
响应示例(total 未统计,⚠️ 见关键变化 1)
{ "code": 200, "success": true, "data": { "records": [ /* 20 条 */ ], "total": -1, "page": 1, "pageSize": 20 } }
空数据 / 降级响应
- 无匹配团期时
records返回空数组[],total为0(不走降级路径时恒准确)。 planStatus降级扫描路径若扫描到 0 条匹配,同样total=0;total=-1仅在候选超过 500 条扫描上限时出现(见「关键变化 1」),不会与「无匹配」混淆。
错误响应
{ "code": 808092, "message": "无权查看全部房务订单(仅房务组长或超管可查看)", "success": false, "data": null }
(808092 属预先存在的 HouseGrabErrorCode,module="house-grab",非本单新增;本单读端点复用它,见「六.5」。)
业务边界
- 未认领团一个都不在本页,即便它满足所有筛选条件。
planStatus过滤是唯一会触发降级扫描的参数,其余筛选条件走数据库分页,total恒准确非负。
2. 团期配房详情(H2) GET /v3/admin/house/group-batches/{groupBatchId}
VO: 无请求体(仅 Path)→ Result<HouseGroupBatchBoardRespVO>
(Controller:HouseGroupBatchBoardController.java:64-71;编排:HouseGroupBatchBoardManager.detail,:122-158)
使用场景
点开某个团查看逐日按房型的需求/已订/差额,核对每天哪个房型还差几间。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
Path | Long | 是 | 雪花 ID | 团期主订单 ID |
出参字段表 HouseGroupBatchBoardRespVO(HouseGroupBatchBoardRespVO.java:26-258)
团级字段与 H1 列表项完全一致(同名同型,见上表),另加:
| 字段 | 类型 | 说明 |
|---|---|---|
maxRooms |
Integer | 班期最大房间数,0/空=不限 |
inGroupHouseholds |
Integer | 在团户数(只排已取消,含已完成) |
hotelHouseholds |
Integer | 需房户数(needs_hotel=true,已排除历史旧户冻结名单) |
releasedHouseholds / pendingReviewHouseholds / rejectedHouseholds / notSubmittedHouseholds |
Integer | 有基准需求版本 / 另有待确认新版本 / 无基准且最新被打回 / 需房但从未提过需求,各自户数 |
outOfRangeHouseholds[] |
OutOfRangeHousehold[] |
阻塞名单(不是剔除名单,需求已计入分母),字段:orderId(String)、orderNo、customerName、departDate(可空)、reason(OUT_OF_RANGE/DATE_MISSING) |
specialTags[] |
HouseholdSpecialTags[] |
各户特殊诉求(取自基准需求版本),字段:orderId(String)、customerName、tags: string[] |
days[] |
DayDetail[] |
逐日详情,可能出现团期区间之外的日历日(越界户按其实际入住日落格,此时 dayNumber=null) |
DayDetail(:155-189):stayDate、dayNumber(Integer 可空)、dayStatus(NONE/PENDING/CONFIRMED/MIXED)、demand[]/planned[](RoomCategoryCount[]:roomCategory+rooms)、mismatch[](CategoryMismatch[],只列 diff≠0 的:roomCategory/demanded/planned/diff)、plannedTotal、exceedsMaxRooms(Boolean)、plans[](该日 active 计划行,元素即 H4/H5 返回的 GroupBatchRoomPlanRespVO,字段见 H4 出参表)。
请求示例
GET /v3/admin/house/group-batches/20260610001
响应示例
{
"code": 200, "success": true,
"data": {
"groupBatchId": "20260610001", "batchNo": "GB-26-000001", "maxRooms": 30,
"outOfRangeHouseholds": [], "specialTags": [{ "orderId": "900001", "customerName": "张三", "tags": ["轮椅需求"] }],
"days": [{
"stayDate": "2026-06-12", "dayNumber": 1, "dayStatus": "PENDING",
"demand": [{ "roomCategory": "STANDARD", "rooms": 5 }],
"planned": [{ "roomCategory": "STANDARD", "rooms": 3 }],
"mismatch": [{ "roomCategory": "STANDARD", "demanded": 5, "planned": 3, "diff": -2 }],
"plannedTotal": 3, "exceedsMaxRooms": false, "plans": []
}]
}
}
空数据 / 降级响应
- 无空态:团期不存在直接返 589500;只要团期存在就必有完整响应体。
days[]、outOfRangeHouseholds[]、specialTags[]均可能是空数组(团期日期缺失或尚无需求提交时),前端应按空数组正常渲染而不是当作异常。
错误响应
{ "code": 808612, "message": "该团期尚未被房务认领,请先到团期抢单池认领", "success": false, "data": null }
{ "code": 808613, "message": "该团期由其他房务认领,无权操作", "success": false, "data": null }
(组长/超管不受此限,见「四、契约约束」。团期不存在为既有 589500。)
业务边界
- 普通房务只能看本人认领的团(
HouseGroupBatchClaimGuard.assertReadableByCurrentUser,HouseGroupBatchClaimGuard.java:42-54),组长/超管可看任意团(HouseReadGuard.canViewOthersClaims()放行)。
3. 团期逐日用房需求明细(H3) GET /v3/admin/house/group-batches/{groupBatchId}/room-requirements
VO: 无请求体(仅 Path)→ Result<HouseGroupBatchRoomRequirementRespVO>
(Controller:HouseGroupBatchBoardController.java:76-82;编排:HouseGroupBatchBoardManager.requirements,:166-199)
使用场景
核对「每户每晚要几间什么房」的逐户明细,供订房前对照。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
Path | Long | 是 | 雪花 ID | 团期主订单 ID(同 H2) |
出参字段表 HouseGroupBatchRoomRequirementRespVO(HouseGroupBatchRoomRequirementRespVO.java:20-183)
| 字段 | 类型 | 说明 |
|---|---|---|
groupBatchId / batchNo / departDate / endDate |
— | 团期基本信息 |
basisRule |
String | 固定文案「按各户最近一次放行房务的需求版本」,后端写死供前端原样展示,口径改动只改这一处 |
days[] |
Day[] |
逐日明细:stayDate、dayNumber(可空)、totals[](RoomCategoryCount[])、households[] |
householdsWithoutBasis[] |
HouseholdWithoutBasis[] |
需房但无放行基准版的户:orderId/orderNo/customerName、reason(NOT_SUBMITTED/PENDING_REVIEW/REJECTED)、referenceRooms[](仅展示不计入合计) |
outOfRangeHouseholds[] |
同 H2 | 阻塞名单 |
Household(:69-123):orderId(String)/orderNo/customerName/consultantId(String)/peopleCount/householdDayNumber/dateShifted(Boolean,该户出发日≠团期出发日)/roomControlStatus/basisRequirementId(String)/basisVersion/basisStatus(PENDING/PROCESSING/DONE)/pendingNewVersion(Boolean)/rooms[](RoomLine[]:roomCategory+roomCount)/specialTags: string[]。
请求示例
GET /v3/admin/house/group-batches/20260610001/room-requirements
响应示例
{
"code": 200, "success": true,
"data": {
"groupBatchId": "20260610001", "batchNo": "GB-26-000001",
"basisRule": "按各户最近一次放行房务的需求版本",
"days": [{
"stayDate": "2026-06-12", "dayNumber": 1,
"totals": [{ "roomCategory": "STANDARD", "rooms": 5 }],
"households": [{
"orderId": "900001", "orderNo": "ORD202606120001", "customerName": "张三",
"peopleCount": 2, "householdDayNumber": 1, "dateShifted": false,
"roomControlStatus": "PENDING", "basisVersion": 2, "basisStatus": "DONE",
"pendingNewVersion": false, "rooms": [{ "roomCategory": "STANDARD", "roomCount": 1 }],
"specialTags": []
}]
}],
"householdsWithoutBasis": [], "outOfRangeHouseholds": []
}
}
空数据 / 降级响应
- 无空态:团期不存在直接返 589500。
days[]/householdsWithoutBasis[]/outOfRangeHouseholds[]均可能是空数组(团尚无任何户提交过需求时),前端应按空数组正常渲染。
错误响应
{ "code": 808612, "message": "该团期尚未被房务认领,请先到团期抢单池认领", "success": false, "data": null }
{ "code": 808613, "message": "该团期由其他房务认领,无权操作", "success": false, "data": null }
(与 H2 同一组错误码,判定逻辑完全一致。团期不存在为既有 589500。)
业务边界
- 归属判定与 H2 完全一致:普通房务只能看本人认领的团,组长/超管可看任意团(
HouseGroupBatchClaimGuard.assertReadableByCurrentUser)。 basisRule文案由后端写死并原样透传,前端应直接展示该字段值,不要本地硬编码同义文案(口径变更只改后端一处)。
4. 整团按日提交订房计划(H4) POST /v3/admin/house/group-batches/{groupBatchId}/room-plans
VO: GroupBatchRoomPlanSaveReqVO → Result<List<GroupBatchRoomPlanRespVO>>
(Controller:HouseGroupBatchRoomPlanController.java:69-82;编排:GroupBatchRoomPlanManager.save/doSave,:120-173)
使用场景
房务批量登记某团多天 × 多酒店 × 多房型的订房安排。追加式,不做集合对账:库里有而本次没提交的行不会被删(避免超出 3 秒幂等窗后重放请求把未重复提交的行整批删掉)。
请求头
@Idempotent:keyPrefix="house:gb:room-plan:save"、keyArg=#groupBatchId、timeout=3 秒,窗口内重复提交返回「订房计划提交处理中,请勿重复提交」。角色门 @HouseWriteGuarded 在幂等/锁之前执行,非房务角色不会消耗幂等令牌。
入参字段表
GroupBatchRoomPlanSaveReqVO(GroupBatchRoomPlanSaveReqVO.java:35-40):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
items |
Body | GroupBatchRoomPlanItemReqVO[] |
是 | 1-200 行 | 上限 200 是团期级锁(30 秒租约)与按酒店聚合 Feign 调用次数的取舍 |
items[] 元素 GroupBatchRoomPlanItemReqVO(H4 走 @Validated({Default.class, RoomPlanValidationGroups.Create.class}),RoomPlanValidationGroups.java):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
stayDate |
Body | LocalDate(ISO) | 是(Create 组) | 须落在团期 [出发日,结束日) 内,否则 808602 |
入住日 |
hotelId |
Body | Long | 是(Create 组) | — | 酒店 ID |
roomTypeId |
Body | Long | 是(Create 组) | — | 房型 ID |
roomCategory |
Body | String | 否 | ≤32 字 | 仅用于前端回显,服务端一律以 resource 权威值覆盖,传与不传都不影响落库(GroupBatchRoomPlanItemReqVO.java:44-52) |
roomCount |
Body | Integer | 是(Create 组) | >= 1,否则 808603 |
订房间数 |
protoPrice / settlementPrice |
Body | BigDecimal | 否 | >= 0.00 |
不传按「日历价→协议价」链路兜底 |
settleType |
Body | String | 否 | 正则 ^(cash|sign|company)$ |
不传取酒店资源配置 |
deductInventory |
Body | Boolean | 否 | — | 是否扣减 resource 库存,语义默认 true(真正扣减在按日确认时,#7325) |
remark |
Body | String | 否 | ≤512 字 | 备注 |
version |
Body | Integer | 否(H4 忽略) | — | 仅 H5 用 |
replaceReason |
Body | String | 否(H4 忽略) | ≤256 字 | 仅 H5 删旧建新用 |
请求内同 (stayDate, hotelId, roomTypeId) 的多行合并间数(其余字段取首行),与库中已有 active 行撞同一格子拒 808614。
出参字段表 Result<List<GroupBatchRoomPlanRespVO>>
按 (入住日, 酒店, 房型) 升序排列。GroupBatchRoomPlanRespVO(GroupBatchRoomPlanRespVO.java:20-109):
| 字段 | 类型 | 说明 |
|---|---|---|
planId / groupBatchId |
String(雪花) | — |
stayDate |
LocalDate | — |
hotelId |
String | — |
hotelName |
String(可空) | Feign 抖动时可为 null |
roomTypeId |
String | — |
roomTypeName |
String(可空) | — |
roomCategory |
String | resource 权威值,非客户端传值 |
roomCount |
Integer | — |
protoPrice / settlementPrice |
String(可空,ToStringSerializer) |
价格快照 |
settleType |
String(可空) | — |
deductInventory |
Boolean | — |
planStatus |
String | H4 新建行恒为 PENDING |
confirmedAt / confirmedBy |
可空 | H4 新建行恒 null |
remark / replaceReason |
String | H4 新建行 replaceReason 恒 null |
replacedFromPlanId |
String(可空) | H4 新建行恒 null |
version |
Integer | H4 新建行恒 0 |
allocatedRooms |
Integer | 其下 active 分房行 Σ 间数,自动分房上线前恒 0 |
createTime / updateTime |
LocalDateTime | — |
warnings |
string[] |
⚠️ H4 恒为空数组 [],见「关键变化 4」 |
removedManualAllocIds |
string[] |
⚠️ H4 恒为空数组 [],见「关键变化 4」 |
请求示例
POST /v3/admin/house/group-batches/20260610001/room-plans
{
"items": [
{ "stayDate": "2026-06-12", "hotelId": 200001, "roomTypeId": 300001, "roomCount": 3, "settleType": "sign" }
]
}
响应示例
{
"code": 200, "success": true,
"data": [{
"planId": "500001", "groupBatchId": "20260610001", "stayDate": "2026-06-12",
"hotelId": "200001", "hotelName": "示例酒店", "roomTypeId": "300001", "roomTypeName": "标间",
"roomCategory": "STANDARD", "roomCount": 3, "protoPrice": "320.00", "settlementPrice": "300.00",
"settleType": "sign", "deductInventory": true, "planStatus": "PENDING",
"confirmedAt": null, "confirmedBy": null, "remark": null, "replaceReason": null,
"replacedFromPlanId": null, "version": 0, "allocatedRooms": 0,
"createTime": "2026-09-10 10:00:00", "updateTime": "2026-09-10 10:00:00",
"warnings": [], "removedManualAllocIds": []
}]
}
空数据 / 降级响应
- 无空态:
items非空是入参硬约束(@NotEmpty),不存在「空提交返回 200」的场景;items为空数组会被 Bean Validation 拒在参数绑定阶段。
错误响应(按触发顺序,见 GroupBatchRoomPlanManager.java:123-126 校验顺序注释)
{ "code": 589500, "message": "团期不存在", "success": false, "data": null }
{ "code": 808600, "message": "团期当前阶段({0})不允许修改订房计划", "success": false, "data": null }
{ "code": 808611, "message": "团期出发日或结束日缺失,无法录入订房计划", "success": false, "data": null }
{ "code": 808612, "message": "该团期尚未被房务认领,请先到团期抢单池认领", "success": false, "data": null }
{ "code": 808613, "message": "该团期由其他房务认领,无权操作", "success": false, "data": null }
{ "code": 808602, "message": "入住日 {0} 不在团期出行区间内", "success": false, "data": null }
{ "code": 808603, "message": "订房间数必须大于 0", "success": false, "data": null }
{ "code": 808614, "message": "该入住日({0})在酒店 {1} 的房型 {2} 上已有订房计划,请改用修改单行", "success": false, "data": null }
{ "code": 808112, "message": "房型不属于该酒店", "success": false, "data": null }
{ "code": 808691, "message": "无法确定房型大类(酒店 {0} 房型 {1}),请稍后重试", "success": false, "data": null }
业务边界
- 阶段闸门(808600)覆盖面含 CANCELLED:
HouseGroupBatchPlanGate.PLAN_WRITABLE_STATUSES只含RESOURCE_PREPARING/MATERIAL_PREPARING/PENDING_DEPARTURE/TRAVELLING/TRIP_FINISHED/REVIEWING六态(HouseGroupBatchPlanGate.java:42-48),RECRUITING/SETTLED/CANCELLED均抛 808600。 - 房型校验 fail-closed 两级:
808691=resource 压根没查到权威房型列表(Feign 失败/返回失败/空列表);808112=查到了非空列表但该roomTypeId不在其中(复用既有HouseAssignmentErrorCode)。二者优先级:先判"拿不到列表整体"(808691),再判"列表里不含该行"(808112),逐酒店批量校验(见HouseRoomTypeGuard.requireRoomTypes,:93-157)。 - 团期级
@Lock4j锁租约 30 秒,写口内不允许逐行 Feign,按酒店聚合调用。
5. 修改单条订房计划(H5) PUT /v3/admin/house/group-batches/{groupBatchId}/room-plans/{planId}
VO: GroupBatchRoomPlanItemReqVO → Result<GroupBatchRoomPlanRespVO>
(Controller:HouseGroupBatchRoomPlanController.java:91-102;编排:GroupBatchRoomPlanManager.update/doUpdateInPlace/doReplace,:177-294)
使用场景
改单条已录入的订房计划行。语义:字段为 null = 本次不改。五要素(stayDate/hotelId/roomTypeId/roomCount/roomCategory,已确认行还多算 deductInventory)任一变化 → 删旧建新(旧行软删登记库存待释放,新行落 PENDING、version 归零,replacedFromPlanId 指回旧行);只改价格/结算方式/deductInventory(仅 PENDING 行)/备注 → 原地更新(同 planId,version+1)。
入参字段表
GroupBatchRoomPlanItemReqVO(Path:groupBatchId/planId;Body 走 @Validated({Default.class, RoomPlanValidationGroups.Update.class})):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
Path | Long | 是 | 雪花 ID | 团期主订单 ID |
planId |
Path | Long | 是 | 雪花 ID | 计划行 ID |
stayDate/hotelId/roomTypeId/roomCount/roomCategory |
Body | 同 H4 | 否 | null=本次不改,非 null 时约束同 H4(808602/808603) |
五要素,任一非 null 且发生变化即触发删旧建新 |
protoPrice/settlementPrice/settleType/deductInventory/remark |
Body | 同 H4 | 否 | 同 H4 | null=本次不改 |
version |
Body | Integer | 是(Update 组强制,:83-86) |
须等于当前行版本号 | 不匹配抛 808608,不自动重试 |
replaceReason |
Body | String | 否 | ≤256 字;核单中团期(REVIEWING)强制填写且 >=10 字,否则 808617 |
走「删旧建新」时落到新行,原地更新时忽略 |
GroupBatchRoomPlanItemReqVO 字段基础定义同 H4 请求体,H5 差异点:
| 字段 | H5 差异 |
|---|---|
stayDate/hotelId/roomTypeId/roomCount |
均可为 null(不在 Update 组强制),非 null 时才参与「是否变化」判定 |
version |
必填(Update 组强制,:83-86),不匹配抛 808608,不自动重试 |
replaceReason |
走「删旧建新」时落到新行;核单中团期(REVIEWING)强制填写且 >=10 字,否则 808617 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| 全部字段 | — | 同 H4 出参表 GroupBatchRoomPlanRespVO(见「4. 整团按日提交订房计划(H4)」),逐字段定义不再重复,下表只列 H5 场景下的语义差异 |
同 H4 的 GroupBatchRoomPlanRespVO,但语义不同:
| 场景 | planId |
version |
warnings |
removedManualAllocIds |
|---|---|---|---|---|
| 原地更新 | 不变 | +1 |
REVIEWING 阶段含 "SETTLEMENT_IN_PROGRESS",否则 [] |
恒 [](原地更新不触发连锁重算) |
| 删旧建新(新行) | 新 ID | 0 |
同上,若旧行是 CONFIRMED 且触发连锁重算清了人工分房行,额外含 "MANUAL_ALLOCATION_REMOVED" |
若触发连锁重算,含被软删的人工分房行 ID;自动分房上线前恒 [] |
请求示例(原地改价)
PUT /v3/admin/house/group-batches/20260610001/room-plans/500001
{ "version": 0, "protoPrice": "330.00", "remark": "临时涨价" }
请求示例(五要素变化,删旧建新)
PUT /v3/admin/house/group-batches/20260610001/room-plans/500001
{ "version": 0, "roomCount": 4, "replaceReason": "酒店临时超售,加订一间" }
响应示例
{
"code": 200, "success": true,
"data": {
"planId": "500002", "groupBatchId": "20260610001", "stayDate": "2026-06-12",
"roomCount": 4, "planStatus": "PENDING", "version": 0,
"replacedFromPlanId": "500001", "replaceReason": "酒店临时超售,加订一间",
"warnings": [], "removedManualAllocIds": []
}
}
空数据 / 降级响应
- 无空态:修改必须定位到一条存在的
planId,不存在直接返 808601,不会有「修改了但返回空对象」的场景。
错误响应(按校验顺序,GroupBatchRoomPlanManager.java:180-185)
{ "code": 808601, "message": "订房计划行不存在或已删除", "success": false, "data": null }
{ "code": 808615, "message": "订房计划行不属于该团期", "success": false, "data": null }
{ "code": 808600, "message": "团期当前阶段({0})不允许修改订房计划", "success": false, "data": null }
{ "code": 808617, "message": "该团期正在核单,修改订房计划必须填写不少于 10 字的原因", "success": false, "data": null }
{ "code": 808612, "message": "该团期尚未被房务认领,请先到团期抢单池认领", "success": false, "data": null }
{ "code": 808613, "message": "该团期由其他房务认领,无权操作", "success": false, "data": null }
{ "code": 808690, "message": "入住日 {0} 已发生,订房记录不可修改或删除", "success": false, "data": null }
{ "code": 808608, "message": "订房计划已被其他操作修改,请刷新后重试", "success": false, "data": null }
{ "code": 808692, "message": "已确认的订房计划不支持直接改价或改备注,差异请在核单中登记", "success": false, "data": null }
{ "code": 808611, "message": "团期出发日或结束日缺失,无法录入订房计划", "success": false, "data": null }
{ "code": 808602, "message": "入住日 {0} 不在团期出行区间内", "success": false, "data": null }
{ "code": 808603, "message": "订房间数必须大于 0", "success": false, "data": null }
{ "code": 808614, "message": "该入住日({0})在酒店 {1} 的房型 {2} 上已有订房计划,请改用修改单行", "success": false, "data": null }
{ "code": 808616, "message": "已确认订房计划的改删连锁能力尚未上线,请联系管理员", "success": false, "data": null }
业务边界
- ⚠️ 完整校验顺序(不可假设某一步先于全局都先做完,前端做乐观 UI 时需注意):计划行存在 808601 → 属本团 808615 → 阶段闸门 808600 → 核单期原因门 808617 → 认领归属 808612/808613 → 已发生间夜冻结 808690(第 6 步,只看日期与行状态,与本次改什么字段无关)→ 版本一致 808608 → 五要素判定分叉 → 已确认行禁原地更新 808692(第 9 步)→(走替换分支时)团期日期齐 808611 → 区间/间数/同格子 808602/808603/808614 → 已确认行连锁重算未上线 808616。808690 始终先于 808692 判定:即便五要素完全没变、本该走「原地更新」分支,只要这一行是已发生的过去 CONFIRMED 行,也会先被 808690 拦下,不会等到走进"原地更新 vs 808692"分支再判断。
- 已确认行改删连锁重算(
#7325交付)当前未接入:五要素变化且旧行是CONFIRMED时,若触发到需要重算分房的场景会抛 808616、整事务回滚、afterCommit不执行、零副作用。
6. 删除单条订房计划(软删,H6) DELETE /v3/admin/house/group-batches/{groupBatchId}/room-plans/{planId}
VO: GroupBatchRoomPlanDeleteReqVO(请求体可选) → Result<GroupBatchRoomPlanDeleteRespVO>
(Controller:HouseGroupBatchRoomPlanController.java:107-117;编排:GroupBatchRoomPlanManager.delete/doDelete,:296-365)
使用场景
删掉一条订房安排。已流团(CANCELLED)团期同样放行删除(否则已扣的酒店库存会永久滞留),但已发生的间夜仍然删不掉。不动分房行——那一晚的房还要按需求重新分给各户,删除本身不做「全删重建」的决定(#7325 负责)。
入参字段表
GroupBatchRoomPlanDeleteReqVO(GroupBatchRoomPlanDeleteReqVO.java,请求体可整体不传):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
Path | Long | 是 | 雪花 ID | 团期主订单 ID |
planId |
Path | Long | 是 | 雪花 ID | 计划行 ID |
reason |
Body | String | 仅团期处于 REVIEWING(核单中)时必填 |
≤256 字,核单中团期需 ≥10 字,否则 808617 | 删除原因 |
出参字段表 GroupBatchRoomPlanDeleteRespVO(GroupBatchRoomPlanDeleteRespVO.java:20-35)
| 字段 | 类型 | 说明 |
|---|---|---|
planId |
String | 被软删的计划行 ID |
releasedLogId |
String(可空) | 本次由 NONE 标为 PENDING 的扣减日志 ID;从未扣过库存的行为 null |
warnings |
string[] |
SETTLEMENT_IN_PROGRESS(核单期)/MANUAL_ALLOCATION_REMOVED(连锁重算清了人工分房行),无告警为空数组 |
removedManualAllocIds |
string[] |
被连锁重算软删的人工分房行 ID,自动分房上线前恒为空 |
请求示例
DELETE /v3/admin/house/group-batches/20260610001/room-plans/500001
(核单中团期需带请求体:{ "reason": "客人退团,该晚不再需要" })
响应示例
{ "code": 200, "success": true, "data": { "planId": "500001", "releasedLogId": "700001", "warnings": [], "removedManualAllocIds": [] } }
空数据 / 降级响应
- 无空态:删除必须定位到一条存在的
planId,不存在(含二次删除)直接返 808601,不会有「删除了但返回空对象」的场景。
错误响应
{ "code": 808601, "message": "订房计划行不存在或已删除", "success": false, "data": null }
{ "code": 808615, "message": "订房计划行不属于该团期", "success": false, "data": null }
{ "code": 808600, "message": "团期当前阶段({0})不允许修改订房计划", "success": false, "data": null }
{ "code": 808617, "message": "该团期正在核单,修改订房计划必须填写不少于 10 字的原因", "success": false, "data": null }
{ "code": 808612, "message": "该团期尚未被房务认领,请先到团期抢单池认领", "success": false, "data": null }
{ "code": 808613, "message": "该团期由其他房务认领,无权操作", "success": false, "data": null }
{ "code": 808690, "message": "入住日 {0} 已发生,订房记录不可修改或删除", "success": false, "data": null }
(H6 不会抛 808692——已确认行禁原地更新是 H5 专属场景,H6 是软删不涉及原地更新分支。)
业务边界
- 阶段闸门比 H4/H5 宽一档:
assertDeletableStage(GroupBatchRoomPlanManager.java:422-429)允许「可写六态」∪「releaseOnly(仅CANCELLED)」,即CANCELLED团期的 H6 不抛 808600(与 H4/H5 相反)。仍只在RECRUITING/SETTLED抛 808600。 - 已发生间夜(CONFIRMED 行 ∧ 入住日早于操作当日 ∧ 团期处于
TRAVELLING/TRIP_FINISHED/REVIEWING/CANCELLED)一律 808690,不因软删豁免。 - 二次删除同一
planId同样返回 808601(天然幂等出口,不是新错误)。
7. 整团批量释放订房计划 POST /v3/admin/house/group-batches/{groupBatchId}/room-plans/release-all
VO: GroupBatchRoomPlanReleaseAllReqVO → Result<GroupBatchRoomPlanReleaseAllRespVO>
(Controller:HouseGroupBatchRoomPlanController.java:122-135;编排:GroupBatchRoomPlanManager.releaseAll,:369-410)
使用场景
团期流团(CANCELLED)后的收尾:一次性软删该团全部订房计划与分房、登记库存归还。是 #7325 流团自动钩子失败时的手动兜底入口,也是钩子上线前的过渡入口——两条路径调同一个实现方法。
请求头
@Idempotent:keyPrefix="house:gb:room-release"、keyArg=#groupBatchId、timeout=3 秒。团期级锁租约 60 秒(RELEASE_LOCK_EXPIRE_MILLIS,比 H4/H5/H6 的 30 秒更长——整团批量处置行数更多)。
入参字段表
GroupBatchRoomPlanReleaseAllReqVO(GroupBatchRoomPlanReleaseAllReqVO.java):
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
Path | Long | 是 | 雪花 ID | 团期主订单 ID |
reason |
Body | String | 是,无条件必填(与团期状态无关) | 10-256 字 | 释放原因。10 字下限用于挡掉「删」「测试」这类等于没写的原因,因为本端点一次会把整个团的订房与分房全部软删 |
出参字段表 GroupBatchRoomPlanReleaseAllRespVO(GroupBatchRoomPlanReleaseAllRespVO.java:16-46)
| 字段 | 类型 | 说明 |
|---|---|---|
groupBatchId |
String | 团期 ID |
releasedPlanIds |
string[] |
本次软删的计划行 ID;⚠️ 见「关键变化 3」,可能为空 |
releasedAllocationIds |
string[] |
本次软删的分房行 ID |
markedLogIds |
string[] |
本次由 NONE 标为 PENDING 的扣减日志 ID(真正归还在事务提交后异步进行,代表「已确保会还」不代表「已还完」) |
hotelReady |
Boolean | 处置后的配房完成标志,恒为 false |
retainedPastPlanIds |
string[] |
入住日早于操作当日、因已发生而未释放的已确认计划行 ID |
warnings |
string[] |
retainedPastPlanIds 非空时含 "PAST_STAY_RETAINED",否则 [] |
请求示例
POST /v3/admin/house/group-batches/20260610001/room-plans/release-all
{ "reason": "团期已流团,释放剩余订房" }
响应示例(正常全释放)
{
"code": 200, "success": true,
"data": {
"groupBatchId": "20260610001",
"releasedPlanIds": ["500001", "500002"], "releasedAllocationIds": [],
"markedLogIds": ["700001", "700002"], "hotelReady": false,
"retainedPastPlanIds": [], "warnings": []
}
}
响应示例(⚠️ 见关键变化 3:200 但零释放)
{
"code": 200, "success": true,
"data": {
"groupBatchId": "20260610001",
"releasedPlanIds": [], "releasedAllocationIds": [], "markedLogIds": [],
"hotelReady": false,
"retainedPastPlanIds": ["500001", "500002"], "warnings": ["PAST_STAY_RETAINED"]
}
}
空数据 / 降级响应
releasedPlanIds/releasedAllocationIds/markedLogIds均可能返回空数组但整体仍是 200(该团计划行全部是已发生的已确认行、全部转入retainedPastPlanIds),见「关键变化 3」——不要把空数组当作请求失败。- 该团完全没有可处置内容(无 active 计划行也无 active 分房行)时不返回空数据,改为业务错误码 808619(见下方错误响应),这是零写入的幂等出口。
错误响应
{ "code": 808618, "message": "团期当前阶段({0})不支持批量释放订房,仅已流团团期可用", "success": false, "data": null }
{ "code": 808613, "message": "该团期由其他房务认领,无权操作", "success": false, "data": null }
{ "code": 808619, "message": "该团期没有可释放的订房计划", "success": false, "data": null }
业务边界
- 仅
CANCELLED团期可用,含SETTLED在内的其它阶段一律 808618(HouseGroupBatchPlanGate.PLAN_RELEASE_ONLY_STATUSES只含CANCELLED,HouseGroupBatchPlanGate.java:58-60);SETTLED不放行的理由:已结算团每晚都住过,归还过去日期库存没有可售价值,还会让月度对账凭空少掉已发生房费。 - 归属校验与 H4/H5/H6 不同:
assertReleasableByCurrentUser(HouseGroupBatchClaimGuard.java:72-94)对无人认领的团直接放行(任意房务都能操作,不抛 808612)——团已是终态,谁来收尾都行;有认领人时仍只放行本人或超管,否则 808613。 808619vs 200+retainedPastPlanIds非空的区分逻辑见「关键变化 3」,GroupBatchRoomReleaseResult.nothingToRelease是显式标志而非靠列表判空推断(GroupBatchRoomReleaseResult.java:11-16)。- 只释放今日及以后的入住日,早于操作当日的已确认行一律保留进
retainedPastPlanIds(不因流团豁免,与 H6 的 808690 同一判据)。
四、契约约束与正确调用方式
✅ 正确 / ❌ 错误 payload 对照
- ✅ H5 只提交实际改动的字段(
version+ 变动字段),❌ 把整份读接口回显对象原样 PUT 回去——PENDING 行虽不会清空未提交字段(MyBatis-Plus 默认FieldStrategy.NOT_NULL,null不进 SET 子句),但仍会把version无意义地+1并留一条UPDATE_IN_PLACE时间线噪音记录;CONFIRMED 行会被直接拒 808692(见「关键变化 2」)。 - ✅ H1 判断
total === -1时改用占位文案,❌ 直接拿total算Math.ceil(total/pageSize)或拼「共 N 条」。 - ✅
release-all成功回调先判releasedPlanIds.length与retainedPastPlanIds.length再决定提示文案,❌ 只看success:true就提示「已全部释放」。 - ✅ H4 请求体里保留
roomCategory(供本地即时回显),但提交后以响应体里的roomCategory为准刷新本地态,❌ 假设服务端会原样接受客户端传入的roomCategory。 - ✅ H4/H5 的
settleType只能传cash/sign/company三值之一(大小写敏感),❌ 传其它字符串(会被 Bean Validation 拒在参数绑定阶段,报 400 级校验错误而非业务错误码)。
认领态与角色的显隐建议
scope=ALL仅组长/超管可用,前端应按角色隐藏该切换项,而不是等 808092 才提示。- H4/H5/H6 的操作入口只在「本人认领的团」(或超管)下展示;
release-all入口可对未认领的已流团团期展示(任意房务可操作)。
五、数据库行为(涉及写操作时必写)
- 新增表
group_batch_room_plan(团期按日订房计划,软删表;GroupBatchRoomPlanDO)与相关的团期分房支撑表(GroupBatchRoomAllocationDO),细节见后端 Flyway,前端不需要感知表结构。 - 写口全部落在团期级分布式锁内(
@Lock4j,锁名HouseGroupBatchLockConstants.GROUP_BATCH_HOUSE_LOCK_NAME,H4/H5/H6 租约 30 秒、release-all租约 60 秒),同一团期的并发写请求会串行化,锁等待超时对前端表现为请求超时/失败,可直接重试。 - 同日同酒店同房型只能有一条 active 计划行,这条不变量没有 DB 唯一键兜底(软删表软删列可空,MySQL 唯一索引里
NULL互不相等),只靠「请求内合并 + 团期级锁 + 应用层查重(808614)」三件事共同守住。 - 跨服务契约新增字段:
hl-resource-service的HouseRoomTypeDTO.roomCategory(HouseRoomTypeDTO.java:51)与hl-order-service-v3消费侧HouseRoomTypeFeignVO.roomCategory(HouseRoomTypeFeignVO.java:47),来源room_type.room_category,历史数据可能为null。H4/H5 写入时一律以此权威值覆盖客户端传入的roomCategory,取不到时 fail-closed 拒绝写入(808691),不同于同包内既有的assertRoomTypeBelongsToHotel(fail-open,用于越权写回价格场景)——两者口径刻意不同,理由是本字段是「对平」判定的分组键,放行等于允许伪造已对平。 - 库存扣减/归还是异步两阶段:写口内只登记扣减日志状态(
NONE→PENDING),真正归还发生在事务afterCommit;若那一步失败,兜底任务凭PENDING状态捞回重做。响应体里的releasedLogId/markedLogIds代表「已确保会还」,不代表「已经还完」。 - H5 原地更新走 MyBatis-Plus 默认
FieldStrategy.NOT_NULL:updateWithVersion底层是roomPlanMapper.updateById(plan)(GroupBatchRoomPlanService.java:174-178),GroupBatchRoomPlanDO的可选字段均未声明@TableField(updateStrategy=...),全仓也无全局覆盖——patch对象里为null的字段不会进 SET 子句。这是「H5 只传version在 PENDING 行返回 200 但不清空字段」的底层依据(见「关键变化 2」)。
六、边界行为
- H1
planStatus过滤降级为内存扫描,扫描上限 500 条(PLAN_STATUS_SCAN_LIMIT),超限时total=-1(见关键变化 1);不超限时total精确等于内存过滤后的匹配数。 - H1
batchStatus若传入非法状态码或全部被过滤掉(如只传CANCELLED),静默回退为默认四态,不报错。 - H2
days[]可能出现团期区间之外的日历日(越界户按其实际入住日落格,此时dayNumber=null)。 - H4 单次最多 200 行,超出报 400 级参数校验错误(
@Size(max=200)),不占用团期级锁。 - H5 删旧建新分支的「五要素比较」把已确认行的
deductInventory也算进比较范围(未确认行不算),因为它决定这一行是否占用 resource 库存,原地翻转会让库存持有与行状态脱节。 - H6 二次删除同一行幂等返回 808601,不是错误放大。
release-all的团期级锁租约(60 秒)比其它写口更长,且允许无人认领的团被任意房务操作。
六.5、枚举 / 数据字典
错误码新增清单(HouseGroupBatchErrorCode.java,段位 808600-808699 内本单实际占用 17 个常量)
⚠️ 与工单 #7324 核对发现的不符:工单口径写「新增 12 个错误码」,但源码(squash 07feaffe0 diff +165/-2)实际新增 17 个 IErrorCode 常量,以下按源码逐条列出:
| Code | 常量名 | Message 模板 | module | 触发条件 |
|---|---|---|---|---|
| 808600 | GB_ROOM_PLAN_BATCH_STATUS_INVALID |
团期当前阶段({0})不允许修改订房计划 | order | H4/H5 阶段闸门(六态之外,含 CANCELLED);H6/release-all 阶段闸门更宽(CANCELLED 不触发) |
| 808601 | GB_ROOM_PLAN_NOT_FOUND |
订房计划行不存在或已删除 | order | H5/H6 找不到 planId;二次删除同码天然幂等 |
| 808602 | GB_ROOM_PLAN_DATE_OUT_OF_RANGE |
入住日 {0} 不在团期出行区间内 | order | H4/H5 入住日不在 [出发日,结束日) |
| 808603 | GB_ROOM_PLAN_COUNT_INVALID |
订房间数必须大于 0 | order | H4/H5 roomCount < 1 |
| 808608 | GB_ROOM_PLAN_VERSION_CONFLICT |
订房计划已被其他操作修改,请刷新后重试 | order | H5 乐观锁 version 不匹配,不自动重试 |
| 808611 | GB_ROOM_PLAN_BATCH_DATE_MISSING |
团期出发日或结束日缺失,无法录入订房计划 | order | H4 新建 / H5 改日期时团期 departDate/endDate 为空 |
| 808612 | GB_ROOM_PLAN_BATCH_NOT_CLAIMED |
该团期尚未被房务认领,请先到团期抢单池认领 | order | H2/H3 只读与 H4/H5/H6 写操作,当前团期未认领(release-all 不抛此码) |
| 808613 | GB_ROOM_PLAN_BATCH_NOT_OWNED |
该团期由其他房务认领,无权操作 | order | 同上场景,团期由他人认领(组长/超管读、超管写免检) |
| 808614 | GB_ROOM_PLAN_DUPLICATE_CELL |
该入住日({0})在酒店 {1} 的房型 {2} 上已有订房计划,请改用修改单行 | order | H4/H5 与库中 active 行撞同一 (stayDate,hotelId,roomTypeId) 格子 |
| 808615 | GB_ROOM_PLAN_BATCH_MISMATCH |
订房计划行不属于该团期 | order | H5/H6 路径参数 groupBatchId 与行数据的 groupBatchId 不一致 |
| 808616 | GB_ROOM_PLAN_CONFIRMED_REVISION_UNAVAILABLE |
已确认订房计划的改删连锁能力尚未上线,请联系管理员 | order | H5/H6 对已确认(CONFIRMED)行做五要素变化的改/删,连锁重算能力(#7325)未接入 |
| 808617 | GB_ROOM_PLAN_REVISE_REASON_REQUIRED |
该团期正在核单,修改订房计划必须填写不少于 10 字的原因 | order | H5/H6 团期处于 REVIEWING,reason/replaceReason 缺失或 <10 字 |
| 808618 | GB_ROOM_PLAN_RELEASE_BATCH_NOT_CLOSED |
团期当前阶段({0})不支持批量释放订房,仅已流团团期可用 | order | release-all 团期非 CANCELLED(含 SETTLED) |
| 808619 | GB_ROOM_PLAN_RELEASE_NOTHING |
该团期没有可释放的订房计划 | order | release-all 该团既无 active 计划行也无 active 分房行,零写入幂等出口 |
| 808690 | GB_ROOM_PLAN_PAST_STAY_LOCKED |
入住日 {0} 已发生,订房记录不可修改或删除 | order | H5/H6:CONFIRMED 行 ∧ 入住日早于操作当日 ∧ 团期处于 TRAVELLING/TRIP_FINISHED/REVIEWING/CANCELLED;始终先于 808692 判定 |
| 808691 | GB_ROOM_PLAN_ROOM_TYPE_UNRESOLVED |
无法确定房型大类(酒店 {0} 房型 {1}),请稍后重试 | order | H4/H5 无法从 resource 取得权威房型行(Feign 失败/返回失败/空列表/该行 roomCategory 为空),fail-closed |
| 808692 | GB_ROOM_PLAN_CONFIRMED_IN_PLACE_UPDATE_FORBIDDEN |
已确认的订房计划不支持直接改价或改备注,差异请在核单中登记 | order | H5:五要素未变化(走原地更新分支)且该行是 CONFIRMED;在 808690 判定之后触发 |
(复用的既有错误码,非本单新增:808112「房型不属于该酒店」属 HouseAssignmentErrorCode,H4/H5 权威房型批量校验查到非空列表但目标 ID 不在其中时复用;808090/808091/808092 属 HouseGrabErrorCode(module="house-grab"),是房务角色只读/只写/全量监督视图的通用角色门,本单只读端点复用 HouseReadGuard、写端点复用 HouseWriteGuard 触发;589500 团期不存在为 order 域既有码。)
枚举字典
planStatus(H1 筛选 / H4-H6 响应):PENDING(未确认)/CONFIRMED(已确认)。H1 的PENDING档口径含「该团无 active 计划行」,不是单独一档。boardStatus(H1/H2):NOT_STARTED/PARTIAL/ALL_CONFIRMED。dayStatus(H1/H2 逐日):NONE/PENDING/CONFIRMED/MIXED。warnings取值:SETTLEMENT_IN_PROGRESS(H5 核单期改动)/MANUAL_ALLOCATION_REMOVED(H5/H6 连锁重算清人工分房)/PAST_STAY_RETAINED(release-all 保留已发生行)。settleType:cash/sign/company。batchStatus(9 态,GroupBatchStatus):RECRUITING招募中 /RESOURCE_PREPARING资源准备中 /MATERIAL_PREPARING物料准备中 /PENDING_DEPARTURE待出发 /TRAVELLING出行中 /TRIP_FINISHED出行完毕 /REVIEWING核单中 /SETTLED已结算 /CANCELLED已取消。H1 默认四态=前四个(不含RECRUITING);写口可写六态=RESOURCE_PREPARING~REVIEWING;仅释放态=CANCELLED。
七、不影响范围
- 团期需求(H09 系列)、团期抢单池/我的团/接管(
#7322)、团期核单/结算(#7411等)现有端点零改动,本单只新增。 - 逐户(非团单)房务配房(
HouseAssignmentService等)零改动,808112复用的是既有校验方法但走的是新的批量入口requireRoomTypes,不影响原有的assertRoomTypeBelongsToHotel调用点(询房回填/配房 submit)。 - 按日确认、自动分房、分房微调(
#7325/#7326)尚未交付,本单已预留错误码段位(808604/808605/808607/808609/808610、808606、808640)与阶段闸门单源(HouseGroupBatchPlanGate),但对应端点/能力均不在本次 PR 范围内——H5/H6 对已确认行的连锁重算目前 fail-closed 返回 808616。 - 网关路由零改动(
/v3/admin/**已由#3264通配)。
八、测试环境已验证
- 已部署测试服并实测通过(
squash 07feaffe0,PR #7465)。 - 部署过程中实测到一次跨服务不同步窗口:
hl-order-service-v3先于hl-resource-service完成部署的短暂时间段内,GET /v3/admin/house/group-batches一度 404(网关侧尚未完成新路由注册的过渡态),重滚order-v3后恢复正常,最终按「hl-resource-service先于hl-order-service-v3」的正确顺序验收通过。 - 兼容性结论:全部为新增端点,无存量调用方,无向后兼容负担;唯一的跨服务契约是
roomCategory字段新增(非破坏性字段新增),但部署顺序错误会导致新端点短暂不可用/写口被 fail-closed 拒绝,务必按本文件顶部status_note的顺序部署。
十、相关文档
#7323《团期房务实现方案 v1.0》§3.11(错误码段位分配定案)#7322团期整团抢单与抢单池分流(前置依赖:本单的「已认领」判定复用其house_claimer_id字段)#7325(按日确认与自动分房,未交付)/#7326(分房与只读,未交付)——本单为它们预留的段位与闸门单源
关联 / 联系人
链接
联系人
- 后端负责人: @wx