docs(changelog): #8615 转房记录作废原因与退房给酒店口径调整
changelog-filename-gate / validate (push) Failing after 2s

- 转房列表 cancelReason 新增三个系统作废取值(本团已再分配 / 本团已撤销该晚计划 / 团期已解散)
- 退房给酒店(I-21)对团期来源行补批次门禁与超量码 808324
- 附测试环境实测结论

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-30 11:05:20 +08:00
共同撰写人 Claude Opus 5.5
父节点 3f3f5e7558
当前提交 d1ab03e7c0
@@ -0,0 +1,314 @@
---
schema: hl-changelog/v2
ticket: "8615"
title: "退团转房:作废原因新增三个系统取值,向酒店取消追加团期栅栏与联动缩减"
consumer: admin
author: wx(GIT)
change_type: 修改接口
backend_status: deployed
gateway_status: not_required
frontend_status: pending
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-09-30"
base: dev-v3
---
# 退团转房:作废原因新增三个系统取值,向酒店取消追加团期栅栏与联动缩减
## ⚠️ 关键变化
- 退团转房池列表(#8491 I-17)的 `records[].cancelReason` 由原来只有一个取值 `HOTEL_CANCELLED`,扩展为**四个**取值,新增 `GROUP_REALLOCATED` / `GROUP_PLAN_REMOVED` / `GROUP_DISBANDED` 三个由团期写口在同一事务内系统作废时写入的取值;这三种情况下 `handlerName` 恒为 `null`(无人工处理人),`handledAt` 仍会写入系统作废发生的时刻。前端按 `cancelReason` 分支展示原因文案的地方需要扩展这三个分支,否则会落到未知分支的兜底展示。
- 退团房向酒店取消(#8491 I-21,`POST .../room-transfers/{id}/cancel-hotel`)新增两个错误码:**团期来源行**在源团期已不在「资源准备中」阶段时返回 `808323`(此前 I-21 对团期来源行不做团期状态校验,任何状态都能直接取消,改动后与转房接口 I-20 同口径);团期来源行的源计划空余不足以覆盖本行待转间数时返回 `808324`。这两个码此前只出现在 I-20 的错误表里,I-21 的错误表新增了它们。
- 退团房向酒店取消**成功后**,若该行是团期来源行,会在同一事务内按取消的间数**同步缩减**源团期计划行的 `roomCount`(联动 `shrinkForTransfer`);这一步不改变本接口的响应结构,只影响该房间此后是否还会被团期自动分配算进可用余量。
- 转房接口(#8491 I-20)不在本次变更接口清单内——其错误表已发布过 `808324`,本次只是把此前一个应报 `808324`(源计划空余不足)却会先报到 `808932`(并发冲突)的边界情况改为直接报 `808324`,错误码本身与文案都没有新增,属于既有契约内的分支修正,前端已有的 `808324`/`808932` 处理分支不需要改动。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 退团转房池分页 | GET | `/v3/admin/order/house-console/room-transfers` | 响应字段取值扩展 | `cancelReason` 新增三个系统作废取值,系统作废行 `handlerName` 为空 |
| 2 | 退团房向酒店取消 | POST | `/v3/admin/order/house-console/room-transfers/{id}/cancel-hotel` | 新增错误码 + 联动行为 | 团期来源行新增 808323/808324 校验;取消成功后同步缩减源团期计划间数 |
## 三、接口详情
### 1. 退团转房池分页 `GET /v3/admin/order/house-console/room-transfers`
**VO**: `HouseRoomTransferPageReqVO → HouseRoomTransferPageRespVO`
#### 使用场景
房务控制台「退团房」页签展示当前待处理 / 已转出 / 已取消的转房行列表。本次改动只涉及列表行里 `cancelReason` 取值集合的扩展,请求参数与响应结构均未变化。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| page | Query | Integer | ❌ | 默认 1 | 页码 |
| pageSize | Query | Integer | ❌ | 1~100,默认 20 | 每页条数 |
| status | Query | String | ❌ | PENDING / TRANSFERRED / CANCELLED,默认 PENDING | 状态筛选 |
| risk | Query | String | ❌ | OVERDUE / NEAR / NO_DEADLINE / NORMAL | 风险筛选,仅对 PENDING 行有意义 |
| cityCode | Query | String | ❌ | ≤64 | 城市中文名 |
| keyword | Query | String | ❌ | ≤64 | 团号或酒店名关键词 |
| stayDateFrom | Query | LocalDate | ❌ | yyyy-MM-dd | 入住晚起(含) |
| stayDateTo | Query | LocalDate | ❌ | yyyy-MM-dd | 入住晚止(含) |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| records | List<HouseRoomTransferRespVO> | 当前页转房行 |
| records[].id | Long(String) | 转房行 ID |
| records[].sourceType | String | 来源类型 ORDER / GROUP_BATCH |
| records[].sourceGroupBatchId / sourceBatchNo | Long(String) / String | 团期来源行的源团期 ID 与批次号 |
| records[].stayDate | LocalDate | 入住晚 |
| records[].hotelName / roomTypeName | String | 酒店名 / 房型名 |
| records[].roomCount | Integer | 原始间数 |
| records[].remainingCount | Integer | 剩余待处理间数;部分被系统收敛时会减少,行仍保持 PENDING |
| records[].status / statusLabel | String | PENDING / TRANSFERRED / CANCELLED 及中文 |
| records[].cancelReason | 【改动】String | 仅 CANCELLED 行有值。`HOTEL_CANCELLED`=房务人工向酒店取消(不变);新增 `GROUP_REALLOCATED`=本团已再分配、`GROUP_PLAN_REMOVED`=本团已撤销该晚计划、`GROUP_DISBANDED`=团期已解散,三者都是团期写口系统作废,见「六.5」 |
| records[].handlerName | 【改动】String | 处理人姓名;`cancelReason` 为三个新增取值之一时恒为 `null`(无人工处理人) |
| records[].handledAt | LocalDateTime | 处理时间;系统作废时同样写入作废发生的时刻,不为 null |
| records[].readOnly / readOnlyReason | Boolean / String | 对当前登录人是否只读及理由 |
| total / page / pageSize | long / int / int | 分页信息 |
| summary.pendingRooms 等 4 项 | int | 全部 PENDING 行的风险汇总,恒不受本次筛选条件影响 |
#### 请求示例
```http
GET /v3/admin/order/house-console/room-transfers?status=CANCELLED&cityCode=海拉尔&page=1&pageSize=20
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "9100234", "sourceType": "GROUP_BATCH", "sourceOrderId": null, "teamNo": null,
"sourceTeamNo": "HLT-20261012-003", "sourceGroupBatchId": "500321", "sourceBatchNo": "HLT-20261012-003",
"stayDate": "2026-10-12", "cityName": "海拉尔", "hotelId": "60088", "hotelName": "海拉尔国际大酒店",
"roomTypeId": "70012", "roomTypeName": "高级大床房", "roomCount": 2, "remainingCount": 2,
"status": "CANCELLED", "statusLabel": "已取消", "cancelDays": 3, "cancelCutoff": "18:00", "remindDays": 1,
"deadlineAt": "2026-10-09T18:00:00", "risk": "NORMAL", "riskLabel": "正常",
"targetType": null, "targetOrderId": null, "targetTeamNo": null, "targetRequirementId": null,
"targetGroupBatchId": null, "targetBatchNo": null, "hotelConfirmNo": null, "proofFileIds": [],
"cancelFee": null, "cancelReason": "GROUP_REALLOCATED", "handlerName": null,
"handledAt": "2026-09-30T10:02:11", "remark": null, "readOnly": false, "readOnlyReason": null
}
],
"total": 1,
"page": 1,
"pageSize": 20,
"summary": { "pendingRooms": 6, "overdueRooms": 0, "nearRooms": 1, "noDeadlineRooms": 0 }
},
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "message": "成功", "data": { "records": [], "total": 0, "page": 1, "pageSize": 20, "summary": { "pendingRooms": 0, "overdueRooms": 0, "nearRooms": 0, "noDeadlineRooms": 0 } }, "success": true }
```
#### 错误响应
```json
{
"code": 400,
"message": "status 取值非法",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | status 取值非法 / risk 取值非法 | 入参校验,未变 |
| 808090 | 未登录或非房务角色,无权操作 | 非房务角色,未变 |
#### 业务边界
- 团期写口(分房重算、加入团期自动分房、改计划间数、删计划、团期解散)触发系统收敛时:某计划行下全部 PENDING 转房行都被覆盖,则行整体变为 CANCELLED 并写入对应 `cancelReason`;只覆盖了一部分,则行仍是 PENDING,仅 `remainingCount` 减少,`cancelReason`/`handlerName`/`handledAt` 均不变。
- 系统作废(`cancelReason` 为 `GROUP_REALLOCATED`/`GROUP_PLAN_REMOVED`/`GROUP_DISBANDED`)的行没有人工处理人:`handlerName` 为 `null`;触发它的管理员记在操作日志里,不在本接口暴露。
- `summary` 恒统计全部 PENDING 行,不受本次筛选条件影响。
### 2. 退团房向酒店取消 `POST /v3/admin/order/house-console/room-transfers/{id}/cancel-hotel`
**VO**: `HouseRoomTransferCancelReqVO → HouseRoomTransferRespVO`
#### 使用场景
退团房没有合适的转入对象时,房务直接向酒店办理取消,上传凭证并可录入取消费用。本次改动只影响**团期来源行**(`sourceType=GROUP_BATCH`):新增源团期状态与源计划余量两道校验,取消成功后联动缩减源团期计划间数;常规单来源行(`sourceType=ORDER`)的路径与校验顺序不变。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 转房行 ID |
| proofFileIds | Body | List<Long> | ✅ | 1~9 个,元素非空 | 取消凭证文件 ID |
| cancelFee | Body | BigDecimal | ❌ | ≥0,最多 2 位小数 | 取消费用(元) |
| remark | Body | String | ❌ | ≤200 字 | 备注 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| (整行) | HouseRoomTransferRespVO | 取消后的该行,字段同接口 1 的 `records[]` |
| status / statusLabel | String | CANCELLED / 已取消 |
| cancelReason | String | 恒为 `HOTEL_CANCELLED`;本接口触发的取消不会写入三个系统作废取值,那三个只由团期写口的系统收敛写入 |
| hotelConfirmNo | String | 本接口不接收确认号,恒为 `null`(未变) |
| cancelFee / proofFileIds | BigDecimal(String) / List<String> | 录入的取消费与凭证 |
#### 请求示例
```json
{
"proofFileIds": [88031, 88032],
"cancelFee": 0,
"remark": "酒店已免费取消"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "9100235", "sourceType": "GROUP_BATCH", "sourceOrderId": null, "teamNo": null,
"sourceTeamNo": "HLT-20261012-004", "sourceGroupBatchId": "500322", "sourceBatchNo": "HLT-20261012-004",
"stayDate": "2026-10-15", "cityName": "满洲里", "hotelId": "60090", "hotelName": "满洲里丽景大酒店",
"roomTypeId": "70020", "roomTypeName": "行政大床房", "roomCount": 3, "remainingCount": 3,
"status": "CANCELLED", "statusLabel": "已取消", "risk": null, "riskLabel": null,
"hotelConfirmNo": null, "proofFileIds": ["88031", "88032"], "cancelFee": "0.00",
"cancelReason": "HOTEL_CANCELLED", "handlerName": "王芳", "handledAt": "2026-09-30T10:15:32",
"remark": "酒店已免费取消", "readOnly": false, "readOnlyReason": null
},
"success": true
}
```
#### 空数据 / 降级响应
本接口无列表数据;任何失败都返回非 200 的 `code`,该行状态不变。
#### 错误响应
```json
{
"code": 808324,
"message": "转出间数超过剩余 1 间",
"data": null,
"success": false
}
```
| code | message | 触发 |
|------|---------|------|
| 400 | 凭证最多 9 个 / 凭证文件 ID 不能为空 / 取消费用不能为负 / 取消费用最多 2 位小数 / 备注不能超过 200 字 | 入参校验,未变 |
| 808090 | 未登录或非房务角色,无权操作 | 非房务角色,未变 |
| 808320 | 转房记录不存在 | id 不存在(未变;锁前锁后各判一次,同一个码) |
| 808326 | 只有原单处理人可以处理退团房间 | 非源单持有人且非超管,未变 |
| 808325 | 请填写酒店确认号并上传凭证 | 未上传凭证(本接口不要求确认号,文案沿用同一错误码),未变 |
| 808323 | 目标团期已确认,不能转入 | 【新增,仅团期来源行】源团期已不在「资源准备中」阶段。文案沿用 I-20 已发布的措辞,这里没有「目标」,实际含义是「源团期已不可再改动,不能取消这一行」 |
| 808321 | 该房间已处理 | 行已不是 PENDING,未变 |
| 808932 | 房务状态已被并发修改,请刷新后重试 | 【团期来源行新增触发场景】源团期或源计划行在读取时已不存在(并发被删/改);以及原有的 CAS 并发冲突 |
| 808324 | 转出间数超过剩余 {0} 间 | 【新增,仅团期来源行】源计划行已分配间数 > `roomCount − 本行剩余间数`,即空余不足以覆盖本行待取消间数;`{0}` = `max(roomCount − 已分配间数, 0)` |
| 100502 | 取消处理中,请勿重复提交 | 3 秒内重复提交,未变 |
#### 业务边界
- 只有源单持有人或超管可操作;凭证为空返回 808325(业务码,不是 400)。
- 校验顺序(团期来源行):808320(不锁定读)→ 808326 → 808325 → 808323(源团期栅栏)→ 808320(锁定读复验,同一个码)→ 808321 → 808932(源团期/源计划缺失)→ 808324(源计划空余不足)→ 808932(CAS 并发冲突)。常规单来源行没有 808323/808324 两步,其余顺序不变。
- 团期来源行取消成功后,源团期计划行的 `roomCount` 会在同一事务内按取消间数同步缩减;这一步对本接口响应体不可见,影响的是该房间此后是否还计入团期自动分配的可用余量。
- 成功后该行整行变为 CANCELLED,不再出现在待处理汇总里;已部分转出的行取消的是剩余部分。
## 四、契约约束与正确调用方式
- 接口 2 的团期来源行取消前,前端应先确认该行所属团期仍处于允许改动的阶段;若已收到 808323,不应重试,应提示房务该团期已不可再取消这一行。
- 接口 2 收到 808324 时,`message` 里的数字是此刻可取消的上限间数(可能为 0),不代表本行 `remainingCount`;不要直接拿 `remainingCount` 去重试提交。
- 接口 1 展示已取消行的原因文案时必须按 `cancelReason` 四个取值分支处理,不要假设该字段只有一个可能值;系统作废三种取值下 `handlerName` 为 `null` 是正常状态,不是数据缺失。
## 五、数据库行为
| 场景 | 团期计划行 `room_count` | 转房行 `status` / `remaining_count` |
|------|------|------|
| 常规单来源行(ORDER)取消 | 不涉及团期计划 | CANCELLED,其余字段照旧写入 |
| 团期来源行取消,源计划空余充足 | 按本次取消间数同步减少(`shrinkForTransfer`) | CANCELLED |
| 团期来源行取消,源计划空余不足(命中 808324) | 不变 | 不变,整体事务回滚 |
| 团期写口触发系统收敛(分房重算 / 自动分房 / 改计划间数 / 删计划 / 团期解散) | 视触发写口而定 | 全部覆盖:CANCELLED + 对应 `cancel_reason`;部分覆盖:仍 PENDING,`remaining_count` 减少 |
## 六、边界行为
- 转房行不存在或已被删除 → 808320(接口 2,未变)。
- 团期整团解散时,其下全部 PENDING 转房行在同一事务内作废(`cancelReason=GROUP_DISBANDED`),此后对这些行调用接口 2 会先命中 808321(已非 PENDING)。
- 源团期推进出「资源准备中」阶段后,其团期来源的 PENDING 行调用接口 2 会命中 808323;同一场景下调用 I-20(转房)此前已经是 808323,两个接口现在口径一致。
- 本单上线前已存在、且所属计划行已被删除但转房行仍是 PENDING 的存量脏数据,不会被本次新增的系统收敛机制回溯处理,仍会出现在接口 1 的列表里;对这类行调用接口 2,源团期已不在「资源准备中」时返回 808323,否则因源计划不存在返回 808932。
## 六.5、枚举
**cancelReason**(`house_room_transfer.cancel_reason`,仅 `status=CANCELLED` 时有值)
| 取值 | 中文 | 说明 | 是否本次新增 |
|------|------|------|------|
| HOTEL_CANCELLED | 房务人工向酒店取消 | 房务通过接口 2 主动办理 | 否 |
| GROUP_REALLOCATED | 本团已再分配 | 计划行还在,但空余已不足以覆盖待转间数——本团别的户分到了这批房,或计划间数被调少 | 是 |
| GROUP_PLAN_REMOVED | 本团已撤销该晚计划 | 待转房挂的计划行已不在活跃计划里:被删除、被替换成不同房型/酒店的新行,或缩减到 0 被整行删除 | 是 |
| GROUP_DISBANDED | 团期已解散 | 流团/解散时该团全部待转房一并作废 | 是 |
## 六.6、修改前后对比
**字段取值**
| 字段 | 改前 | 改后 |
|------|------|------|
| 接口 1 `records[].cancelReason` | 仅 `HOTEL_CANCELLED` 一个取值 | 新增 `GROUP_REALLOCATED` / `GROUP_PLAN_REMOVED` / `GROUP_DISBANDED` 三个取值 |
| 接口 2 错误码集合 | 808320 / 808321 / 808325 / 808326 / 808932(另有 400 / 808090 / 100502) | 团期来源行新增 808323、808324 |
**行为**
| 行为 | 改前 | 改后 |
|------|------|------|
| 本团某户分到已释放的团期房 | 原 PENDING 转房行照常留在池里,房务仍可在接口 2 / I-20 继续处理,存在一房两卖风险 | 团期写口在同一事务内收敛该计划行下的 PENDING 转房行,全部覆盖则整行作废并写入对应 `cancelReason`,部分覆盖则 `remaining_count` 减少 |
| 接口 2 对团期来源行的源团期状态 | 不做校验,源团期任意状态都能直接取消 | 源团期不在「资源准备中」时返回 808323 |
| 接口 2 取消团期来源行后源计划间数 | 不变 | 按取消间数同步缩减 |
| 接口 2 团期来源行空余不足 | 无此校验 | 返回 808324,行与源计划均不写入 |
## 六.7、影响评估
- 是否破坏向后兼容:否。`cancelReason` 是新增取值,不是重命名或语义变更;两个错误码是接口 2 的新增分支,不是复用/覆盖已有码的语义。
- 前端是否必须同步上线:是。不同步的话,系统作废行在列表上会落入未处理的 `cancelReason` 分支;团期来源行取消撞上 808323/808324 时若没有对应分支,会呈现为未识别错误。
- 前端 workaround 清理点:若此前把「`cancelReason` 非 `HOTEL_CANCELLED`」当异常兜底处理,需要改为按四个取值分别展示;否则无需清理,只需新增分支。
## 七、不影响范围
- 常规单来源行(`sourceType=ORDER`)在接口 2 的校验顺序、错误码与响应结构均未变化。
- 接口 1 的入参、分页结构、`summary` 汇总口径未变化。
- 转房接口 I-20 的请求/响应结构、错误码集合未新增,本单不改变其契约,仅修正一处原本应报 808324 却先报到 808932 的边界分支,不在本次变更接口清单内。
- 房务分房重建(H11)等团期写口自身的请求/响应契约未变化,只是这些写口在检测到需要收敛的 PENDING 转房行时会按本单的规则写入新的 `cancelReason`。
## 八、测试环境已验证
测试环境已验证(部署提交 405fc8db0,验证时刻 2026-09-30 10:22~10:41):
1. 转出间数超过计划剩余:`POST /v3/admin/order/house-console/room-transfers/{id}/cancel-hotel`,body code=808324("转出间数超过剩余 0 间"),转房行与计划行均无变化。
2. 团内户取消离团后另一户经重算分到房:`POST /v3/admin/order/{id}/cancel/pre-trip` + `POST /v3/admin/house/group-batches/{id}/allocations/rebuild`,转房行由 PENDING 变为 CANCELLED,`cancelReason=GROUP_REALLOCATED`,处理人为空,有对应操作日志。
3. 计划仍有空余时人工办理转出:`POST .../cancel-hotel`,body code=200,转房行变 CANCELLED/HOTEL_CANCELLED,计划 roomCount 由 2 减为 0 并软删(预期)。
## 十、相关文档
- 工单 #8615
- 参考发布契约:`changelogs-v2/2026-09/30_8491_房务控制台接口-新增接口-管理后台.md`(I-17 / I-20 / I-21 的既有字段与错误码定义)
## 关联 / 联系人
- 关联工单:#8615
- 关联历史 changelog:#8491