37 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 | 7327 | 团期配房行合流进住宿核单 Step1:sourceType 新增取值 GROUP_BATCH_PLAN、新增行级确认闸 584129 | admin | wx(GIT) | 修改接口 | deployed | verified | verified | mmg | c966fd5b7b0f33de6fb0667e2038f060f0f95f54 | 2026-09-13 | ⚠️ 2026-09-13 订正:PR-2(PR #7603,终态提交 d916986de,同日 09:19 已合 dev-v3)把本篇四处结论推翻了——「权威消失即软删」改为「不删、就地转 GROUP_BATCH_PLAN_REVOKED、原样返回、权威回归时三段认回」,且 REVOKED 取值本期就会出现。详见正文顶部「🔴 2026-09-13 订正」段。前端原 2026-09-13 not_required 闭环是在「REVOKED 本期产生不出来」这个前提下做的,前提已失效,故退回 pending 等 mmg 重新判一次(要重判的具体问题见订正段末尾)。 【以下为 PR-1 原文】Issue #7327 PR-1 已合并 dev-v3(PR #7600,终态提交 49ce99cc8),服务 hl-order-service-v3。住宿核单 Step1 的团期订单首次出现团期配房派生行;sourceType/sourceTypeName 是既有字段,本次只扩取值域,新增业务错误码 584129。接口路径、HTTP 方法、VO 字段集合均未变化,网关无新增路由。 前端 2026-09-13 闭环 not_required:逐块 grep 实证。①核单 Step1 住宿无独立来源列,来源合并「备注/来源」note 列 returnDetailAdapter.js firstValue(remark,note,sourceTypeName) 纯透传后端中文名,前端无 sourceType 映射表,新值自带中文「团期配房」天然兼容;行为分支只按 MANUAL 判手工行,新取值落入派生行语义不可删,保存 normalizeSource 原样回传不丢值。②584129 不带 silentError 走 request.js 通用业务码兜底 message.error 透后端原文,__handled 防双弹,无专属分支。③无「团期无住宿」workaround 可撤。零业务代码改动。【前端 2026-09-13 订正重判交付 verified】侦查三路实证核单 Step1 住宿现状后用户拍板两项:(1) REVOKED 行给删除入口——rowAllowsDelete 放行 GROUP_BATCH_PLAN_REVOKED(仍受已确认闸约束),删除确认文案点明「删除后权威回归无法认回」,删除走本地剔除+下次 PUT 全量覆写生效;(2) 合计/「共 N 条」剔除 REVOKED 行——categoryTotal 按 source.sourceType 剔除,calculateReports 兜底自算与 detail.vue 行数同步回落,页面仍展示该行。回传 sourceType/sourceId/hotelAssignmentId 锚点保持透传,权威回归认回不被前端截断。新增 model.spec.js 与 CategoryTable.spec.js REVOKED 4 例,settlement 全域 115/115,checkpoint medium 7 项全绿。 | 2026-09-13 | dev-v3 |
订单模块: 团期配房行合流进住宿核单 Step1(sourceType 新增取值 + 行级确认闸 584129)
存放目录:
- 一期(v2,无
order-v3标签的工单)→changelogs/{YYYY-MM}/- 二期(v3,
order-v3标签的工单)→changelogs-v2/{YYYY-MM}/服务: hl-order-service-v3 (端口 8086) PR: #7600(PR-1)、#7603(PR-2,2026-09-13 订正来源) Issue: #7327 日期: 2026-09-12 影响范围: 管理后台核单 Step1 住宿明细(团期订单);订单详情 / 行程单 / 小程序行程的住宿段行数
🔴 2026-09-13 订正(PR-2 把本篇四处结论推翻了,请以本段为准)
本篇写于 PR-1(#7600,49ce99cc8)当天。同日 09:19 合入的 PR-2(#7603,d916986de)
把其中四处行为改成了相反的。下面四行已在正文就地改正,这里集中列出供对账:
| 本篇原来说 | 实际行为(PR-2 后) |
|---|---|
GROUP_BATCH_PLAN_REVOKED 本期产生不出来,下一期才会出现 |
本期就会出现。 团期订房计划行一被删/改,对应核单行当场转成该取值 |
| 权威消失即软删,返回结果里不再出现 | 不删。 就地改 sourceType=GROUP_BATCH_PLAN_REVOKED、remark 打上前缀 [团期来源已失效] ,该行照样随 GET step1 返回 |
| (无) | 新增三段认回:权威回归时按 source_id 精确认回原行,sourceType 改回 GROUP_BATCH_PLAN、剔掉一层前缀,实付与凭证不丢、不新建重复行 |
用例 listHotel_groupAuthorityGone_**softDeletes**DerivedRowAndKeepsRevokedAndManual |
真实用例名是 listHotel_groupAuthorityGone_**revokes**DerivedRowAndKeepsRevokedAndManual(SettlementServiceGroupBatchReconcileTest.java:219) |
前端实际要处理什么
sourceType会真的出现GROUP_BATCH_PLAN_REVOKED,sourceTypeName= 团期配房(已失效)。后端直接给中文名,不需前端做映射表。- 该行的
remark会被后端加上前缀[团期来源已失效](末尾含一个空格,常量原文见SettlementService.java:178)。 前缀只加一层、只剔一层、只剔开头(核单员可能在前缀后面写了自己的备注,整段清空会丢掉他的字)。 前端不要拿这个前缀做任何判断,它只供人读;要判失效一律看sourceType。 - 行数不会因失效而减少。之前按本篇做了「失效后行会消失」预期的地方(空态、合计、分页总数)要重新看一眼。
⏸ 这次订正引出一个需要拍板的新问题
本篇原来的 frontend_status: not_required 闭环结论,是在「REVOKED 本期产生不出来」这个前提下做的,
而那个前提现在没了。其中一条原话是「行为分支只按 MANUAL 判手工行,新取值落入派生行语义不可删」——
一条权威已经消失的 REVOKED 行,核单员到底能不能删? 按现有前端逻辑它是「不可删的派生行」,但它的上游已经不存在了——这意味着它会永久留在核单页上。 后端这么设计是故意的(要保住已录的实付与凭证、以及权威回归时的认回锚点),但前端侧怎么展现、要不要给删除入口,本篇没定过。
故 frontend_status 从 not_required 退回 pending,请 mmg 在新前提下重新判一次。
(这不是说原来那份分析做错了——它在它的前提下是对的;失效的是前提,不是推理。)
⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
一行红字说清:
sourceType/sourceTypeName不是新增字段,是既有字段。这两个字段在本次改动之前就存在于HotelItemVO上,前端按「新增字段」去找会找不到。本次变的是取值域:sourceType多出GROUP_BATCH_PLAN(sourceTypeName= 团期配房)与GROUP_BATCH_PLAN_REVOKED(sourceTypeName= 团期配房(已失效))两个取值。- 前端/调用方以前以为的是:核单 Step1 住宿明细只有两类行——配房派生行(
HOUSE_ASSIGNMENT)与手工行(MANUAL);团期订单的住宿明细是空的。 - 实际现在是:团期订单的住宿明细第一次出现团期配房派生行,
sourceType=GROUP_BATCH_PLAN。这类行按「住宿事实」聚合成一行(同订单 + 同入住日 + 同酒店 + 同房型的多条分房记录合并为一行,间数求和、计划成本按合计间数重算)。 - ✅ (2026-09-13 订正)
GROUP_BATCH_PLAN_REVOKED本期就会出现(PR-2d916986de起)。团期订房计划行被删或改成别的酒店/房型时,对应核单行不删、就地转成该取值并照样返回,sourceTypeName= 团期配房(已失效)。前端本期就要能展示它。 - 新增业务错误码 584129:把一条「团期配房未分平」的住宿行置为已确认时被拒绝。
一、背景(选填)
团期业务的房是「账面上按团订房、核算时还原到户」。住宿成本必须逐户进核单,否则团期订单在核单页看不到任何住宿行、应付算不出来。本次由房务侧的只读契约把团期分房行合流进既有的住宿读取链路,核单 Step1 因此第一次看得到团期住宿。
| 维度 | 改前(团期订单) | 改后(团期订单) |
|---|---|---|
| Step1 住宿行来源 | 仅 HOUSE_ASSIGNMENT 派生行 + MANUAL 手工行 |
增加 GROUP_BATCH_PLAN 派生行 |
| 团期订单住宿行数 | 0 行(团期分房不进核单) | 按住宿事实分组,一组一行 |
| 行级确认前置条件 | 无团期相关前置 | 团期行须「该户该日已按房型分平」才允许置已确认 |
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | Step 1 查询住宿核单明细 | GET | /v3/admin/order/{orderId}/settlement/step1 |
响应取值域扩展 | 团期订单新增 sourceType=GROUP_BATCH_PLAN 的聚合行 |
| 2 | Step 1 录住宿核单明细 | PUT | /v3/admin/order/{orderId}/settlement/step1 |
入参白名单扩展 + 新错误码 | items 内 sourceType 放行两个新取值;未分平的团期行置已确认返 584129 |
三、接口详情
1. Step 1 查询住宿核单明细 GET /v3/admin/order/{orderId}/settlement/step1
VO: 无 ReqVO(仅路径参数 orderId) → List<HotelItemVO>
使用场景
管理后台「核单结算 - Step1 住宿」页面进入或刷新时调用,拿到该订单当前应展示的全部住宿明细行(派生行 + 手工行)。返回的每一行都带 id,前端后续 PUT 回写时必须原样带回该 id,否则会被当成新增行。
本接口是读写混合的:服务端在返回前会把库里的核单草稿与房务权威对平(缺的补、变的改、团期权威消失的转 GROUP_BATCH_PLAN_REVOKED 而非软删 ——2026-09-13 订正,详见正文顶部订正段),所以刷新页面本身会改变库里的行集合与确认状态。前端不需要额外调「同步」动作。
团期订单从本次起会在结果里出现 sourceType=GROUP_BATCH_PLAN 的行。出现条件:该户存在有效的团期分房记录、其所属的团期订房计划已确认、且该户不在团期历史旧户冻结名单内;三者任一不满足则该户仍只有原来的行。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| orderId | Path | Long | ✅ | 路径变量,非数字将被框架判为参数错误 | 订单 ID(团期场景下是子订单/户的订单 ID,不是团 ID) |
出参 Result<List<HotelItemVO>>
| 字段 | 类型 | 说明 |
|---|---|---|
| data[].id | String | 核单住宿行 ID(雪花 ID,强制序列化为字符串)。行身份只认它,回写必须原样带回 |
| data[].hotelAssignmentId | String | 派生行的权威锚点 ID;手工行为 null。团期行取该组内最小的分房记录 ID,会随房务拆/合行漂移,前端不得用它做行身份 |
| data[].hotelId | String | 酒店 ID |
| data[].roomTypeId | String | 房型 ID |
| data[].stayDate | String | 入住日期,yyyy-MM-dd |
| data[].hotelName | String | 酒店名快照 |
| data[].roomType | String | 房型字典 code(团期行取团期订房计划行快照上的房型大类) |
| data[].roomTypeName | String | 房型名称快照 |
| data[].roomCount | Integer | 间数。团期行等于该组内各分房记录间数之和 |
| data[].unitPrice | Number | 核算单价(元/间夜)。团期行取团期订房计划行的结算价快照 |
| data[].plannedCost | Number | 计划成本(元)。团期行等于 unitPrice 乘 roomCount(按合计间数重算) |
| data[].actualCost | Number | 实际成本(元)。新建行初始等于 plannedCost,之后由核单员维护 |
| data[].paymentMethod | String | 付款方式:SIGNED / COMPANY_PAID / CASH_PAID;结算类型未知时为 null |
| data[].paymentMethodName | String | 付款方式中文名:签单 / 公司付款 / 现付 |
| data[].settleType | String | 本接口恒为 null(读取路径不回填该字段,它只用于写入时代替 paymentMethod) |
| data[].sourceType | String | 来源类型。既有字段,本次新增取值 GROUP_BATCH_PLAN / GROUP_BATCH_PLAN_REVOKED(详见六.5) |
| data[].sourceTypeName | String | 来源类型中文名,与 sourceType 一一对应 |
| data[].sourceId | String | 来源业务 ID;团期行等于 hotelAssignmentId(组内最小分房记录 ID),同样会漂移 |
| data[].settlementConfirmStatus | String | 核单确认状态:UNCONFIRMED / CONFIRMED |
| data[].settlementConfirmStatusName | String | 确认状态中文名:未确认 / 已确认 |
| data[].remark | String | 备注。团期行间数变化时,系统会在原备注前加上提示前缀(见业务边界) |
| data[].voucherUrls | String[] | 凭证图片 URL 数组;无凭证时为空数组 |
请求示例
GET /v3/admin/order/71001/settlement/step1
Authorization: Bearer <token>
响应示例
{
"code": 200,
"message": "成功",
"data": [
{
"id": "93001",
"hotelAssignmentId": "99001",
"hotelId": "201",
"roomTypeId": "202",
"stayDate": "2026-08-01",
"hotelName": "布达拉宫酒店",
"roomType": "TWIN",
"roomTypeName": "标准双床房",
"roomCount": 2,
"unitPrice": 400.00,
"plannedCost": 800.00,
"actualCost": 800.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"settleType": null,
"sourceType": "GROUP_BATCH_PLAN",
"sourceTypeName": "团期配房",
"sourceId": "99001",
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"voucherUrls": []
}
],
"success": true
}
空数据 / 降级响应
- 该订单没有任何住宿行(散客单未配房、团期订房计划未确认、该户在历史旧户冻结名单内)→
data为空数组,不是 null,不报错。 - 房务只读契约返回 null(下游读取失败)→ 降级返回库里已有的核单草稿行,不做对平,同时把该订单的住宿类目标记为「来源同步失败」;页面照常渲染,团期行不会凭空消失,也不会在这一次刷新里被删。
{ "code": 200, "message": "成功", "data": [], "success": true }
错误响应
{
"code": 500,
"message": "系统繁忙,请稍后重试",
"data": null,
"success": false
}
业务边界
- 鉴权:走网关统一鉴权,未登录 401;本端点本身不带角色禁入判断(与同控制器其他只读端点一致)。
- 团期行聚合:分组键是「订单 + 入住日 + 酒店 + 房型」,一组只返回一行。房务把一条分房记录拆成两条、或把两条合成一条,只要酒店/房型/日期/总间数不变,返回的行
id不变,实际成本、凭证、备注、确认状态四项也不变,只有hotelAssignmentId与sourceId这两个锚点会变。 - 事实变更即打回未确认:团期行的酒店、房型、入住日、酒店名、房型名、间数、单价、计划成本、付款方式任一与权威不一致时,该行
settlementConfirmStatus被重置为UNCONFIRMED,实际成本与凭证保留不清空。 - 间数变化加备注前缀:间数变化时
remark前面被系统加上[住宿事实变更 间数 {旧}→{新},请复核实付](末尾含一个空格)。连续变化只保留最新一层前缀,不叠加。该前缀仅供人读,前端不要用它做任何判断。 - ✅ (2026-09-13 订正)权威消失不软删,转
GROUP_BATCH_PLAN_REVOKED:团期订房计划行被删除或改成了别的酒店/房型时,对应的核单行仍然返回,只是sourceType变成GROUP_BATCH_PLAN_REVOKED、remark被打上前缀[团期来源已失效](只加一层);实付、凭证、确认状态与source_id锚点全部保留。权威回归时按source_id三段认回原行(改回GROUP_BATCH_PLAN、剔一层前缀),不新建重复行。手工行始终不受影响。 - 失败零写入:对平过程在一个事务内,中途异常整体回滚,同时把住宿类目标记为来源同步失败。
- 兼容:
sourceType的历史取值CUSTOM_ASSIGNMENT/TEMPLATE会被归一化为MANUAL/SYSTEM后再返回,前端不会读到这两个旧值。
2. Step 1 录住宿核单明细 PUT /v3/admin/order/{orderId}/settlement/step1
VO: SettlementHotelSaveReqVO(items 为 HotelItemVO 数组) → SettlementHotelSaveRespVO
使用场景
核单员在 Step1 住宿页编辑实际成本、付款方式、凭证、备注、确认状态后点保存时调用。全量替换语义:请求里没带的现库行视为删除,所以必须把页面上的全部行(含未改动的派生行)一起回传。
本次起,请求里可以出现 sourceType=GROUP_BATCH_PLAN 的行;把这类行置为已确认时会经过一道新的闸门(584129)。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| orderId | Path | Long | ✅ | - | 订单 ID |
| items | Body | HotelItemVO[] | ✅ | 不能为 null;逐元素校验 | 住宿明细行全量数组。请求体也可以直接是数组,裸数组与对象包裹两种形态都接受 |
| items[].id | Body | String/Long | ❌ | - | 现库行 ID;不传或为 null 表示新增手工行 |
| items[].stayDate | Body | String | ✅ | yyyy-MM-dd,不得早于出发日 | 入住日期 |
| items[].hotelName | Body | String | ✅ | 非空白,长度 ≤200 | 酒店名 |
| items[].roomType | Body | String | ❌ | 长度 ≤64 | 房型字典 code |
| items[].roomTypeName | Body | String | ❌ | 长度 ≤128 | 房型名称 |
| items[].roomCount | Body | Integer | ✅ | - | 间数 |
| items[].unitPrice | Body | Number | ❌ | ≥ 0 | 核算单价 |
| items[].plannedCost | Body | Number | ✅ | ≥ 0 | 计划成本 |
| items[].actualCost | Body | Number | ✅ | ≥ 0 | 实际成本 |
| items[].paymentMethod | Body | String | ❌ | 枚举 SIGNED / COMPANY_PAID / CASH_PAID |
与 settleType 二选一;手工行必填其一 |
| items[].settleType | Body | String | ❌ | 枚举 cash / sign / company |
派生行可用它让后端映射 paymentMethod |
| items[].sourceType | Body | String | ❌ | 枚举 HOUSE_ASSIGNMENT / MANUAL / SYSTEM / TEMPLATE / GROUP_BATCH_PLAN / GROUP_BATCH_PLAN_REVOKED,长度 ≤32 |
本次新增放行后两个取值。派生行落库的来源以库里既有行为准,入参该字段不改变行的来源归属 |
| items[].sourceId | Body | String/Long | ❌ | - | 派生行落库时取库里既有值,入参值被忽略 |
| items[].settlementConfirmStatus | Body | String | ❌ | 枚举 UNCONFIRMED / CONFIRMED,长度 ≤32 |
确认状态。新增行只能是未确认 |
| items[].remark | Body | String | ❌ | 长度 ≤500 | 备注 |
| items[].voucherUrls | Body | String[] | ❌ | - | 凭证图片 URL 数组 |
| items[].hotelId | Body | String/Long | ❌ | - | 酒店 ID,回传即可 |
| items[].roomTypeId | Body | String/Long | ❌ | - | 房型 ID,回传即可 |
| items[].hotelAssignmentId | Body | String/Long | ❌ | - | 派生行锚点;落库时以库里既有值为准 |
出参 Result<SettlementHotelSaveRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data.addedIds | String[] | 本次落库后全部行的新 ID(全量替换实现:每次保存所有行都会重新写入并拿到新 ID)。雪花 ID 超出 JS 安全整数范围,按全局规则序列化为字符串 |
| data.updatedIds | String[] | 恒为空数组(全量替换语义下不区分更新) |
| data.deletedIds | String[] | 恒为空数组(被删除的行不单独列出,未回传即删除) |
| data.totalActualCost | String | 本次提交的 actualCost 之和,强制序列化为字符串 |
请求示例
{
"items": [
{
"id": "93001",
"hotelAssignmentId": "99001",
"hotelId": "201",
"roomTypeId": "202",
"stayDate": "2026-08-01",
"hotelName": "布达拉宫酒店",
"roomType": "TWIN",
"roomTypeName": "标准双床房",
"roomCount": 2,
"unitPrice": 400.00,
"plannedCost": 800.00,
"actualCost": 650.00,
"paymentMethod": "COMPANY_PAID",
"sourceType": "GROUP_BATCH_PLAN",
"settlementConfirmStatus": "CONFIRMED",
"remark": "财务备注",
"voucherUrls": ["https://oss.example.com/a.jpg"]
}
]
}
响应示例
{
"code": 200,
"message": "成功",
"data": {
"addedIds": ["93011"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "650.00"
},
"success": true
}
空数据 / 降级响应
- 提交空的 items 数组是合法的,语义是「清空该订单全部住宿核单行」,返回空的 addedIds 与合计 0。派生行会在下一次
GET step1对平时按权威重新生成,届时实际成本与凭证已丢失,前端不要用空数组做「取消编辑」。 - 本端点不提供下游降级:确认团期行时若房务只读契约不可用,保存整体失败并回滚,不会写入半个结果。
{
"code": 200,
"message": "成功",
"data": { "addedIds": [], "updatedIds": [], "deletedIds": [], "totalActualCost": "0" },
"success": true
}
错误响应
{
"code": 584129,
"message": "团期配房未分平,该住宿行暂不能确认",
"data": null,
"success": false
}
sourceType 传了白名单以外的值时(HTTP 仍为 200):
{
"code": 400,
"message": "sourceType 必须是 HOUSE_ASSIGNMENT / MANUAL / SYSTEM / GROUP_BATCH_PLAN / GROUP_BATCH_PLAN_REVOKED 之一",
"data": null,
"success": false
}
业务边界
- 鉴权:走网关统一鉴权;另有核单财务写权限校验,无权限时按既有权限错误码返回。
- 订单状态门禁:订单的核单审核状态必须是 PENDING 或 IN_PROGRESS 才允许保存,否则返回既有的「订单状态不可结算」错误码(本次未改)。
- 并发:按 orderId 加分布式锁(30 秒),同一订单的并发保存串行执行。
- 584129 触发条件(三个条件同时成立):① 该行带
id(是现库行);② 本次提交把它置为CONFIRMED;③ 库里该行的来源是GROUP_BATCH_PLAN,且房务侧该户该入住日未按房型大类分平,或该行对应的团期分房记录已经不存在了。 - 判来源只信库、不信入参:把
sourceType改成MANUAL再提交绕不开这道闸门。 GROUP_BATCH_PLAN_REVOKED行不受该闸门约束:它已经没有权威可比,成本由核单员自己维护,允许确认。- 未分平的团期行仍可保存:只要不把它置成
CONFIRMED,实际成本、凭证、备注照常可以录入并保存。 - 新增行必须从未确认起步:
id为空的行提交CONFIRMED会被既有错误码拒绝(与本次改动无关,该校验在闸门之前执行)。 - 派生行事实变更再打回:即便 584129 放行,若提交内容与库里该派生行的系统事实不一致,落库后的确认状态仍会被强制写成
UNCONFIRMED;前端保存后应以下一次GET step1的返回为准渲染状态。 - 前端处置建议(584129):提示「该团期住宿尚未分房完成,请等房务分平后再确认」,并引导用户刷新 Step1 重新拉取,而不是原样重试。
四、契约约束与正确调用方式(接口类必写)
本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|---|---|
| ✅ 团期行只录实付、不确认 | { "id": "93001", "sourceType": "GROUP_BATCH_PLAN", "actualCost": 650.00, "settlementConfirmStatus": "UNCONFIRMED" } |
| ✅ 团期行已分平后确认 | { "id": "93001", "sourceType": "GROUP_BATCH_PLAN", "settlementConfirmStatus": "CONFIRMED" } → 200 |
| ✅ 已失效团期行确认 | { "id": "93002", "sourceType": "GROUP_BATCH_PLAN_REVOKED", "settlementConfirmStatus": "CONFIRMED" } → 200,且不查房务契约 |
| ✅ 裸数组请求体 | [ { "id": "93001" }, { "id": "93002" } ],与对象包裹形态等价 |
| ❌ 未分平的团期行置确认 | { "id": "93001", "sourceType": "GROUP_BATCH_PLAN", "settlementConfirmStatus": "CONFIRMED" } → 584129 |
| ❌ 改来源绕闸门 | { "id": "93001", "sourceType": "MANUAL", "settlementConfirmStatus": "CONFIRMED" } → 仍 584129(来源以库里为准) |
| ❌ 未知来源取值 | { "sourceType": "GROUP_BATCH" } → code 400,sourceType 校验文案 |
| ❌ 只回传被改的那一行 | { "items": [ 单行 ] } → 200,但其余行全部被删除(全量替换) |
切换状态时的必要动作
- 把一行从未确认切到已确认,请求里必须带上该行的
id;不带id会被当成新增行,直接被「新增行必须未确认」规则拒绝。 - 团期行确认失败(584129)后,正确动作是重新
GET step1拉取最新事实再确认,而不是把sourceType改掉或把id去掉重试——这两条路都会破坏该行已录的实付与凭证。 - 付款方式二选一:派生行可只传
settleType(cash / sign / company)让后端映射,手工行请直接传paymentMethod;两者都不传时该行付款方式为空。
五、数据库行为(涉及写操作时必写)
| 前端提交 | 落库后的来源 | 落库后的确认状态 | 落库后的实付与凭证 |
|---|---|---|---|
团期派生行,事实与库内一致,置 CONFIRMED,且已分平 |
GROUP_BATCH_PLAN(取库内值,忽略入参) |
CONFIRMED |
按入参写入 |
团期派生行,事实与库内不一致,置 CONFIRMED |
GROUP_BATCH_PLAN |
强制 UNCONFIRMED |
按入参写入 |
团期派生行,未分平,置 CONFIRMED |
不落库(整个请求回滚) | 不落库 | 不落库 |
已失效团期行置 CONFIRMED |
GROUP_BATCH_PLAN_REVOKED |
CONFIRMED |
按入参写入 |
| 新增手工行 | MANUAL |
强制 UNCONFIRMED |
按入参写入 |
| 现库行未在 items 中回传 | 该行被软删 | — | — |
派生行字段以库为准说明: 派生行的来源、来源 ID 与配房锚点三项一律取库内既有值,入参里携带的对应字段被忽略;只有手工行这三项才落 null 与 MANUAL。
读接口也会写库说明: GET step1 在对平阶段会新建、更新或软删住宿行(⚠️ 2026-09-13 订正:团期派生行的权威消失走「转 REVOKED」不走软删,此处的软删指其它来源的行)——间数或单价变化会写回计划成本并把确认状态打回未确认,锚点漂移会写回新的锚点 ID。对平只在「锚点变了」或「事实变了」时才发生写入,两者都没变时读接口一行库也不写。
六、边界行为
- 未登录 → 401 (网关拦截)
- 订单不存在 / 无权访问 → 按既有订单访问错误码返回,HTTP 仍为 200
- 房务只读契约不可用 →
GET step1降级返回库内草稿行并标记来源同步失败;PUT step1在需要判分平时整体失败回滚 - 老数据兼容 → 历史
TEMPLATE/CUSTOM_ASSIGNMENT来源归一化为SYSTEM/MANUAL后返回 - 非团期订单 → 行为与改动前完全一致,不会出现
GROUP_BATCH_PLAN行 - 团期历史旧户(在团期模型启用前已按旧模式办完住宿的户)→ 不产生团期行,住宿段仍是原来的配房派生行
六.5、枚举 / 数据字典(接口出现枚举时必写)
每个枚举单独一个子节,不混表。字段+枚举类对应关系写在子节开头。
sourceType(com.hulalv.order.settlement.enums.SettlementDetailSourceType)
所属字段: HotelItemVO.sourceType(请求与响应同名同义) | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
HOUSE_ASSIGNMENT |
配房结果 | 逐户配房派生行,与配房记录一一对应 |
MANUAL |
手工 | 核单员手工新增行,无权威源 |
SYSTEM |
系统 | 系统生成行(历史 TEMPLATE 归一化到此值) |
GROUP_BATCH_PLAN |
团期配房 | 本次新增取值。团期订房计划派生行,按住宿事实分组聚合,一组一行 |
GROUP_BATCH_PLAN_REVOKED |
团期配房(已失效) | 本次新增取值。团期来源已失效但成本仍保留的行。✅(2026-09-13 订正)本期就会出现:团期订房计划行被删/改时,对应核单行当场转成本取值并照样返回(remark 前会被加上 [团期来源已失效] );权威回归时按 source_id 三段认回、改回 GROUP_BATCH_PLAN |
该枚举的其余取值(景区 / 游玩项目 / 餐饮安排 / 车务 / 人员安排 / 订单增费)用于其他核单步骤,住宿 Step1 不会返回,住宿入参白名单也不接受它们。
settlementConfirmStatus(核单确认状态)
所属字段: HotelItemVO.settlementConfirmStatus | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
UNCONFIRMED |
未确认 | 新建行的初始值;事实变更会被打回该值 |
CONFIRMED |
已确认 | 团期行置该值需过 584129 闸门 |
paymentMethod(付款方式)
所属字段: HotelItemVO.paymentMethod | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
SIGNED |
签单 | 对应结算类型 sign |
COMPANY_PAID |
公司付款 | 对应结算类型 company |
CASH_PAID |
现付 | 对应结算类型 cash;该类行可上传凭证 |
六.6、修改前后对比(修改/删除类接口必写,新增跳过)
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
sourceType(响应) |
取值域 HOUSE_ASSIGNMENT / MANUAL / SYSTEM |
增加 GROUP_BATCH_PLAN、GROUP_BATCH_PLAN_REVOKED(字段本身早已存在,不是新增字段) |
sourceTypeName(响应) |
配房结果 / 手工 / 系统 | 增加「团期配房」「团期配房(已失效)」 |
sourceType(入参白名单) |
HOUSE_ASSIGNMENT / MANUAL / SYSTEM / TEMPLATE |
再加 GROUP_BATCH_PLAN / GROUP_BATCH_PLAN_REVOKED |
hotelAssignmentId 与 sourceId(团期行) |
团期订单无此类行 | 取该组内最小分房记录 ID,随房务拆/合行漂移,不可作为行身份 |
remark(团期行) |
仅核单员自填内容 | 间数变化时被系统加上 [住宿事实变更 间数 {旧}→{新},请复核实付] 前缀 |
| 其余字段 | — | 无增删、无类型变化 |
行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
团期订单 GET step1 住宿行 |
0 行 | 按「订单+入住日+酒店+房型」分组各一行,间数求和、计划成本按合计重算 |
| 团期订单行确认 | 无限制(因为没有团期行) | 未分平或权威已消失时返回 584129 |
| 团期行锚点漂移(房务拆/合分房) | — | 只换锚点,行 ID、实付、凭证、备注、确认状态五项不变 |
| 团期行事实变更 | — | 确认状态打回 UNCONFIRMED,实付与凭证保留 |
| 团期权威消失 | — | ✅(2026-09-13 订正)不软删:就地转 GROUP_BATCH_PLAN_REVOKED + remark 加前缀,行照样返回;权威回归时三段认回,实付/凭证不丢 |
| 逐户配房派生行与手工行 | 现有行为 | 完全不变 |
六.7、影响评估(修改/删除类必写)
- 是否破坏向后兼容: 否。路径、方法、VO 字段集合、既有取值语义均未变化;老前端把
GROUP_BATCH_PLAN当未知来源忽略时,仍能正常读写散客单。 - 前端是否必须同步上线: 否(不同步上线不会报错),但团期订单的核单页在同步前会有两个问题:来源列渲染成空白或未知值;核单员点确认时收到未映射的 584129 错误码。建议同期处理取值域映射与 584129 文案。
- 前端 workaround 清理点: 若管理后台此前对团期订单隐藏了 Step1 住宿页、或写死了「团期无住宿」的提示,本次可撤除。
七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- 仅影响: 管理后台核单结算 Step1 住宿明细的读写。
- 连带变化(字段结构不变,行数变多): 订单详情、行程单、小程序行程的住宿段与核单读同一份房务配房契约,因此团期订单在这三处同样会多出团期配房行。字段名、类型、层级一个都没变,只是数组元素变多了;散客单这三处零变化。
- 零影响:
- 核单 Step2 门票、Step3 车辆及其余核单步骤
- 团期整单核单闸(584130 住宿 / 584131 用车)的语义,本次未改
- C 端算价、下单、支付链路
- 逐户配房(非团期)派生行的对平逻辑
- 历史数据:存量核单行不迁移,下一次打开 Step1 对平时才按新规则处理
八、测试环境已验证
接口行为以自动化用例断言实证,带 ✓ 标记(用例夹具: orderId=71001、departDate=2026-08-01、hotelId=201、roomTypeId=202、分房记录 99001 与 99002、单价 400.00):
GET step1 团期权威存在 → 新建行 sourceType=GROUP_BATCH_PLAN、间数 2、计划与实际成本 800.00、
UNCONFIRMED ✓ listHotel_groupAuthority_insertsDerivedRowWithGroupSourceAndAggregatedCost
GET step1 同事实两条分房行 → 只返回一行、间数求和、锚点取最小 allocId 99001 ✓
listHotel_splitAllocationsInSameFactGroup_aggregatesIntoSingleRow
GET step1 仅锚点漂移 → 重绑锚点,实付/凭证/备注/确认状态四项零改动 ✓
listHotel_allocIdDriftedOnly_persistsNewAnchorAndKeepsFinancialColumns
GET step1 事实与锚点均未变 → 读路径一行库也不写 ✓
listHotel_anchorAndFactsUnchanged_doesNotWriteOnReadPath
GET step1 间数 2 变 1 → 计划成本 400.00、实付 650.00 与凭证保留、状态回 UNCONFIRMED、
remark = "[住宿事实变更 间数 2→1,请复核实付] 财务备注" ✓
listHotel_roomCountChanged_keepsActualCostResetsStatusAndPrefixesRemark
GET step1 权威消失 → 派生行转 REVOKED(不删),已失效行与手工行保留 ✓ ← 2026-09-13 订正
listHotel_groupAuthorityGone_revokesDerivedRowAndKeepsRevokedAndManual
PUT step1 未分平置确认 → 584129 ✓ saveHotel_groupRowUnbalanced_confirmRejectedWith584129
PUT step1 已分平置确认 → 放行 ✓ saveHotel_groupRowBalanced_confirmAllowed
PUT step1 权威已消失置确认 → 584129 ✓ saveHotel_groupRowSourceIdMissingFromContract_rejectedWith584129
PUT step1 未分平但保持未确认 → 放行 ✓ saveHotel_groupRowUnbalancedButStaysUnconfirmed_allowed
PUT step1 已失效行置确认 → 放行且不查房务契约 ✓
saveHotel_groupBatchPlanRevokedRow_confirmAllowedWithoutContractLookup
PUT step1 逐户配房行置确认 → 不被团期闸门拦 ✓ saveHotel_houseAssignmentRow_confirmNotGatedByGroupBalance
PUT step1 团期行事实变更 → 落库强制 UNCONFIRMED ✓ saveHotel_groupRowFactChanged_resetToUnconfirmed
sourceType 入参白名单 → 两个新取值通过、未知取值被拒 ✓ SettlementHotelSourceTypeValidationTest
网关:本次无新增路径,两个端点沿用既有路由 /v3/admin/** 到 hl-order-service-v3(hl-gateway/src/main/resources/application.yml 中的 order-service-v3 路由),无需网关改动。
上表为自动化用例断言原文;示例 JSON 中的 ID 与金额取自同一批用例夹具,不是测试服抓包报文。测试服真实网关的验收取证留在工单 #7327 的验收项里。
九、相关历史 PR(纠错 / 功能演进时必写)
| PR | Issue | 说明 | 是否仍有效 |
|---|---|---|---|
| 本 PR #7600 | #7327 | PR-1:团期配房行合流进住宿核单 Step1 + 584129 行级确认闸 | ✅ 最新 |
十、相关文档
- 关联 Issue: wx/HL#7327
- 关联 PR: wx/HL#7600
- 团期整单住宿闸(584130): 见
changelogs-v2/2026-09/11_7446_住宿结算闸判团口径-修改接口-管理后台.md - 团期住宿户级 finalize 闸门: 见
changelogs-v2/2026-09/10_7347_finalize团期住宿户级闸门-修改接口-管理后台.md - 后续计划:
GROUP_BATCH_PLAN_REVOKED的产出与「失效行不计入金额」在下一期一起落地,届时另发条目
关联 / 联系人
链接
联系人
- 后端负责人: @wx