33 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 | 房务月度对账三个只读端点并入团期订房计划行与分房行,核单实际花销无权威桶时不再静默丢弃 | admin | wx(GIT) | 修改接口 | deployed | not_required | not_required | backend_status=deployed 依据:PR #7645(Issue #7327 PR-3)已 squash 合并 dev-v3,squash 提交 0573c1a78,服务 hl-order-service-v3。⚠️ 本条尚未做测试服真实网关调用取证——正文“八、测试环境已验证”全部是自动化单测断言(MonthlyReconciliationServiceTest.java 21 个用例,含本次新增 11 个),不是测试服抓包,这点与 CLAUDE_WORKFLOW 对“接口类条目发布前须测试服实测”的要求之间有差距,按本仓同类先例(见 2026-09-13 13_7327 文件同样只用单测断言)先行推送,测试服真实调用取证请在工单 #7327 的验收项里补。gateway_status=not_required:三个端点均为已有路由(`/v3/admin/**` 通配到 order-service-v3),PR-3 未新增/修改任何路径或方法,未做也无需做网关联调。frontend_status=pending:三个端点的响应字段结构与错误码均未变,但团期订单首次出现在这三个端点的返回数据里、且部分月份的总成本/间夜/订单数取值会比改动前更大(详见六.6),需要 mmg 确认月度对账页对“团期酒店行”“0 间夜订单行”两类新数据形态的展示是否需要适配,故不用 not_required。【前端 2026-09-14 闭环 not_required】grep 实证 ledger/index.vue 通用消费已覆盖两类新形态:roomNights ?? 0(0 间夜行正常显)、formatActualExpense 只认 expenseEntered+actualExpenseAmount != null(仅实际花销桶正常显)、productType null 不渲染标签、hotelName 兜底;无团期隐藏/间夜0过滤 workaround;字段结构零变,环比 totalActualExpense 直显后端权威值不反推。零业务代码改动。 | 2026-09-14 | dev-v3 |
房务模块: 月度对账三个只读端点并入团期计划行与分房行,核单实际花销修复静默丢弃
存放目录:
- 一期(v2,无
order-v3标签的工单)→changelogs/{YYYY-MM}/- 二期(v3,
order-v3标签的工单)→changelogs-v2/{YYYY-MM}/服务: hl-order-service-v3 (端口 8086) PR: #7645(Issue #7327 PR-3) Issue: #7327 日期: 2026-09-14 影响范围: 管理后台房务月度对账三个只读端点——酒店汇总列表、酒店订单明细、Excel/PDF 导出
⚠️ 关键变化(本版与上版行为不同)
一行红字说清:
- 本次变了什么:月度对账三个端点的聚合数据源从「仅
house_hotel_assignment已确认配房行」扩展为「配房行 + 团期订房计划行/分房行 + 核单已录入的实际花销」三路合并;同时修复了一个静默丢数据的旧 bug(见下)。 - 前端/调用方以前以为的是什么:①团期订单在这三个端点里永远不出现(数据源里没有它);②某个月只要
house_hotel_assignment没有已确认行,接口就返回空列表/全 0 概览,哪怕那个月存在核单实际花销记录;③每个酒店桶只会来自「已存在的配房权威行」,实际花销记录如果对不上任何一个已有酒店桶,会被后端悄悄丢掉,不影响页面报错、但总成本会比真实值小。 - 实际现在是什么:①团期订单第一次会出现在这三个端点的返回结果里,其间夜/金额按团期口径计算(见六.6);②只要三路数据源合并后不是空的,就会返回非空结果——纯团期月、或只剩实际花销记录的月份也会出现内容;③换酒店之后旧酒店本月即使没有任何权威行,之前已录入的那笔实付现在会新建一个只有实际花销的酒店桶/订单行展示出来(
roomNights=0),不再被丢弃。这会让部分月份的overview.totalActualExpense、overview.orderCount、hotels[].orderCount比改动前的历史观察值更大——这不是新 bug,是修复了旧的漏计。
一、背景(选填)
房务月度对账(工单 #4074 起)原口径是「配房完毕订单酒店汇总」,数据源只有 house_hotel_assignment 的已确认配房行。团期业务(Issue #7322/#7324/#7327 系列)落地后,团期订单的住宿走的是独立的两张表——group_batch_room_plan(团期整体按日订房计划)与 group_batch_room_allocation(分到具体户/订单的分房行),这两张表此前完全没有并入月度对账,导致团期订单在对账页「查无此单」。
团期口径是两层,不是一层:
| 维度 | 数据来源 | 计量单位 | 用途 |
|---|---|---|---|
酒店级汇总(monthly.hotels[]) |
group_batch_room_plan(CONFIRMED 计划行) |
整团订房间数(含未分配余量) | 要付给酒店的是订房量,不是已分到户的量 |
订单级明细(monthly/hotel-orders) |
group_batch_room_allocation(active 分房行) |
到户间数 | 未分配余量没有归属订单,不能算进某个订单头上 |
两者的差额就是「未分配余量」——房务已经按团期规模向酒店订了、但还没分给任何一户的房间;这部分间夜与金额只在酒店级体现,订单明细里不会出现(Issue #7327 AC-6 / AC-7)。
同时,本次修了一个和团期无关、但影响面更广的旧 bug:核单已录入的实际花销(order_settlement_hotel.actual_cost)匹配酒店桶时,如果既查不到对应的配房行,酒店名又对不上(或名字为空/重名),这笔钱此前会被整条丢弃(continue),既不进总成本也不进任何酒店/订单行,页面完全看不出异常。本次改为查不到桶就新建一个只有实际花销的桶(间夜与配房金额为 0),并让该核单行自带的 hotel_id 快照参与匹配(这是新增的 DTO 字段,此前该 DTO 没有这一列,匹配只能靠配房行 ID 回查或酒店名),减少「查不到」的情况。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 月度对账酒店汇总列表 | GET | /v3/admin/house/reconciliation/monthly |
聚合数据源扩展 | 并入团期计划行;实际花销无权威桶时新建桶而非丢弃 |
| 2 | 月度对账酒店订单明细 | GET | /v3/admin/house/reconciliation/monthly/hotel-orders |
聚合数据源扩展 | 并入团期分房行;实际花销无权威行时新建订单明细行 |
| 3 | 月度对账导出(xlsx/pdf) | GET | /v3/admin/house/reconciliation/monthly/export |
导出内容随口径联动 | 与端点 1 同源数据,团期行与实际花销新建行会出现在导出文件里 |
三、接口详情
三个端点的请求参数、鉴权、错误码均未变化;本次只改变了返回数据的内容(不改变响应结构、字段名、字段类型)。以下逐接口自包含描述。
1. 月度对账酒店汇总列表 GET /v3/admin/house/reconciliation/monthly
VO: MonthlyReconciliationReqVO → Result<MonthlyReconciliationRespVO>
使用场景
管理后台「房务月度对账」页面进入或切换月份时调用,返回该月的概览统计(overview,含环比)与逐酒店汇总列表(hotels[])。本次起,hotels[] 可能包含仅由团期订房计划行贡献的酒店桶,也可能包含仅由核单实际花销贡献的酒店桶(配房权威行为 0)。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| month | Query | String | ✅ | 正则 ^\d{4}-(0[1-9]|1[0-2])$ |
对账月份,如 2026-05 |
出参 Result<MonthlyReconciliationRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data.month | String | 回传的对账月份 |
| data.overview.hotelCount | Integer | 本月酒店家数(按 hotelId/hotelName 去重,含团期与实际花销新建桶) |
| data.overview.orderCount | Integer | 本月去重订单数(本次起:只有实际花销记录、无任何配房/团期权威行的订单也计入) |
| data.overview.roomNights | Integer | 间夜合计(团期部分按整团订房间数,含未分配余量) |
| data.overview.totalAmount | String(金额) | 配房金额合计(结算价优先、协议价兜底) |
| data.overview.paidAmount | String(金额) | 兼容旧字段,恒等于 totalAmount |
| data.overview.unpaidAmount | String(金额) | 兼容旧字段,恒为 0 |
| data.overview.totalActualExpense | String(金额)/null | 已录入实际花销合计;全部未录入时为 null |
| data.overview.monthOverMonth | Object | 与上月同口径环比(上月数据同样按本次扩展后的三路口径重算) |
| data.hotels[].hotelId | String/null | 酒店 ID |
| data.hotels[].hotelName | String | 酒店名快照 |
| data.hotels[].orderCount | Integer | 该酒店本月去重订单数(团期按分房行去重,未分配余量不计入订单数) |
| data.hotels[].roomNights | Integer | 该酒店本月间夜数;新建的实际花销桶恒为 0 |
| data.hotels[].totalAmount | String(金额) | 配房金额合计;新建的实际花销桶恒为 0 |
| data.hotels[].avgPrice | String(金额) | 配房均价,间夜为 0 时返回 0 |
| data.hotels[].paidAmount / unpaidAmount | String(金额) | 兼容旧字段,语义同 overview 同名字段 |
| data.hotels[].actualExpenseAmount | String(金额)/null | 该酒店本月实际花销合计,未录入为 null |
| data.hotels[].expenseEntered | Boolean | 是否已录入实际花销 |
| data.hotels[].expenseOperatorName | String/null | 实际花销录入人快照(当前核单表未存姓名时为 null,本次未改) |
| data.hotels[].expenseUpdatedAt | String(时间) | 实际花销最近更新时间 |
请求示例
GET /v3/admin/house/reconciliation/monthly?month=2026-05
Authorization: Bearer <token>
响应示例
以下数值取自自动化测试夹具(MonthlyReconciliationServiceTest#applyActualExpenses_revokedRowWithoutActiveBucket_createsExpenseOnlyBucket),不是测试服抓包报文;场景为「订单 31 本月从旧酒店(901,旧酒店)换到新酒店(801,新酒店),旧酒店已录入的一笔实付 800.00 元本次起不再被丢弃」:
{
"code": 200,
"message": "成功",
"data": {
"month": "2026-05",
"overview": {
"hotelCount": 2,
"orderCount": 1,
"roomNights": 1,
"totalAmount": "900.00",
"paidAmount": "900.00",
"unpaidAmount": "0.00",
"totalActualExpense": "800.00",
"monthOverMonth": null
},
"hotels": [
{
"hotelId": "901",
"hotelName": "旧酒店",
"orderCount": 1,
"roomNights": 0,
"totalAmount": "0.00",
"avgPrice": "0.00",
"paidAmount": "0.00",
"unpaidAmount": "0.00",
"actualExpenseAmount": "800.00",
"expenseEntered": true,
"expenseOperatorName": null,
"expenseUpdatedAt": "2026-05-18T15:00:00"
},
{
"hotelId": "801",
"hotelName": "新酒店",
"orderCount": 1,
"roomNights": 1,
"totalAmount": "900.00",
"avgPrice": "900.00",
"paidAmount": "900.00",
"unpaidAmount": "0.00",
"actualExpenseAmount": null,
"expenseEntered": false,
"expenseOperatorName": null,
"expenseUpdatedAt": null
}
]
},
"success": true
}
空数据 / 降级响应
本月三路数据源(配房行 / 团期计划行 / 核单实际花销)全部为空时,返回空列表 + 全 0 概览,这一点未变(getMonthlyReconciliation_emptyMonth_returnsEmptyAndZeroOverview 覆盖):
{
"code": 200,
"message": "成功",
"data": {
"month": "2026-05",
"overview": {
"hotelCount": 0, "orderCount": 0, "roomNights": 0,
"totalAmount": "0.00", "paidAmount": "0.00", "unpaidAmount": "0.00",
"totalActualExpense": null, "monthOverMonth": null
},
"hotels": []
},
"success": true
}
错误响应
month 不是 YYYY-MM 格式(既有错误码,本次未改):
{
"code": 808170,
"message": "月份格式错误(应为 YYYY-MM)",
"data": null,
"success": false
}
业务边界
- 鉴权:走网关统一鉴权,未登录 401;本端点本身不带角色禁入判断(与改动前一致)。
- 团期酒店级口径:按 CONFIRMED 状态的团期订房计划行汇总,间夜与金额按整团订房间数(含未分配到户的余量),不是按已分房到户的间数。
- 实际花销新建桶:只在该酒店本月没有任何配房/团期权威行、但存在核单实际花销记录时出现;这类桶的
roomNights/totalAmount/avgPrice/paidAmount/unpaidAmount恒为 0,只有actualExpenseAmount/expenseEntered/expenseUpdatedAt有值。 - 酒店归属匹配优先级:核单实际花销行按「配房/分房行 ID 回查」→「行自带的 hotelId 快照」→「本月同名酒店唯一 ID 收敛」的顺序确定归属酒店,任一步命中即停止;只有全部落空才会以「无归属」被跳过(此时既不进任何酒店桶,也不进总成本,属于设计内的最终兜底,不是本次要修的那类丢弃)。
- 只读:本端点不提供任何写口,不影响核单/配房任何写路径。
2. 月度对账酒店订单明细 GET /v3/admin/house/reconciliation/monthly/hotel-orders
VO: HotelReconciliationOrderReqVO → Result<List<HotelReconciliationOrderVO>>
使用场景
管理后台月度对账页点击某个酒店汇总行「查看订单」时调用,按 hotelId(优先)或 hotelName 过滤,返回该酒店本月的订单级明细。本次起,命中的团期订单会以「按到户间数」的口径出现在明细里;只有实际花销、没有任何配房/分房权威行的订单也会以 0 间夜出现。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| month | Query | String | ✅ | 正则 ^\d{4}-(0[1-9]|1[0-2])$ |
对账月份 |
| hotelId | Query | Long | ❌ | hotelId 与 hotelName 至少填一个 | 酒店 ID,有值时优先按 ID 查 |
| hotelName | Query | String | ❌ | hotelId 为空时必填 | 酒店名快照,也用于合并同名无 ID 历史配房行 |
出参 Result<List<HotelReconciliationOrderVO>>
| 字段 | 类型 | 说明 |
|---|---|---|
| data[].orderId | String | 订单 ID |
| data[].orderNo | String | 订单号 |
| data[].teamNo | String/null | 规范团号,来自 order_main.team_no |
| data[].productName | String | 产品名 |
| data[].productType | String/null | 产品类型:CORE/ROUTE/CUSTOM/GROUP;既有字段,本次起团期分房贡献的行会取到 GROUP |
| data[].departDate | String | 订单出发日期 |
| data[].hotelId | String/null | 酒店 ID |
| data[].hotelName | String | 酒店名快照 |
| data[].roomNights | Integer | 该订单在该酒店本月间夜数;团期行按到户间数;只有实际花销无权威行时恒为 0 |
| data[].totalAmount | String(金额)/null | 兼容旧字段,等于 actualExpenseAmount,未核单录入时为 null |
| data[].actualExpenseAmount | String(金额)/null | 该订单在该酒店的实际花销 |
| data[].expenseEntered | Boolean | 是否已录入实际花销 |
请求示例
GET /v3/admin/house/reconciliation/monthly/hotel-orders?month=2026-05&hotelId=701
Authorization: Bearer <token>
响应示例
以下取自 listHotelOrders_groupOrder_rowsWithProductTypeGroup 用例夹具(酒店 701「团期酒店」本月有两条 CONFIRMED 团期计划行、四条分房行,分给订单 31、32 两户):
{
"code": 200,
"message": "成功",
"data": [
{
"orderId": "31",
"orderNo": "HL31",
"teamNo": null,
"productName": "团期产品",
"productType": "GROUP",
"departDate": "2026-05-10",
"hotelId": "701",
"hotelName": "团期酒店",
"roomNights": 4,
"totalAmount": null,
"actualExpenseAmount": null,
"expenseEntered": false
},
{
"orderId": "32",
"orderNo": "HL32",
"teamNo": null,
"productName": "团期产品",
"productType": "GROUP",
"departDate": "2026-05-10",
"hotelId": "701",
"hotelName": "团期酒店",
"roomNights": 2,
"totalAmount": null,
"actualExpenseAmount": null,
"expenseEntered": false
}
],
"success": true
}
「只有实际花销、0 间夜」的形态(取自 listHotelOrders_expenseOnlyOrder_listedWithZeroRoomNights,按 hotelId=901 查询):
{
"code": 200,
"message": "成功",
"data": [
{
"orderId": "31",
"orderNo": "HL31",
"teamNo": null,
"productName": "团期产品",
"productType": "GROUP",
"departDate": "2026-05-16",
"hotelId": "901",
"hotelName": "旧酒店",
"roomNights": 0,
"totalAmount": "800.00",
"actualExpenseAmount": "800.00",
"expenseEntered": true
}
],
"success": true
}
空数据 / 降级响应
三路数据源合并后该酒店在本月没有任何订单(既无配房/团期权威行,也无实际花销)→ data 为空数组:
{ "code": 200, "message": "成功", "data": [], "success": true }
错误响应
hotelId 与 hotelName 均未传(既有错误码,本次未改):
{
"code": 808172,
"message": "酒店ID或酒店名称至少填写一个",
"data": null,
"success": false
}
month 格式非法:
{
"code": 808170,
"message": "月份格式错误(应为 YYYY-MM)",
"data": null,
"success": false
}
业务边界
- 鉴权:同端点 1,走网关统一鉴权。
- 未分配余量不出现在这里:团期订房计划行超出分房行的部分(未分给任何户的余量)只体现在端点 1 的酒店级
roomNights,不会在本端点凭空造一行无主订单(listHotelOrders_remainderRow_notListed覆盖)。 - 团期行归并只认分房行:本端点遍历的是
group_batch_room_allocation(分房行),不遍历计划行;一条分房行对应一个订单。 - 实际花销补建行:命中查询酒店但本月无任何权威行的订单,会以
roomNights=0的行补建出来,而不是整行消失或整体返回空数组。 - 过滤优先级:
hotelId有值时按 ID 精确匹配;为空时按hotelName做本月同名收敛匹配。 - 只读:不提供写口。
3. 月度对账导出(xlsx/pdf) GET /v3/admin/house/reconciliation/monthly/export
VO: MonthlyReconciliationReqVO(+format 查询参数)→ application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 或 application/pdf 附件流(非 JSON 信封)
使用场景
管理后台月度对账页「导出」按钮:按 format 导出该月的酒店汇总列表为 Excel(xlsx,默认) 或 PDF 文件。导出内容与端点 1 的 hotels[] 完全同源,本次起同样会包含团期贡献的酒店行与「仅实际花销」新建的酒店行。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| month | Query | String | ✅ | 正则 ^\d{4}-(0[1-9]|1[0-2])$ |
对账月份 |
| format | Query | String | ❌ | xlsx(默认)/ pdf,大小写不敏感 |
不传或传 xlsx 走 Excel,向后兼容 |
出参(附件流,非 JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
| Content-Type | String | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet(xlsx)或 application/pdf |
| Content-Disposition | String | attachment; filename="house-reconciliation-{month}.{xlsx|pdf}"(中文安全编码) |
| body | Binary | Excel/PDF 文件字节 |
xlsx 数据区列头(自动化用例断言,见 exportMonthlyXlsx_withGroupRows_headersUnchanged):酒店名称、订单、间夜、实际花销——四列列头本次未改,团期行与「仅实际花销」新建行会作为新增数据行出现在同一张表里,间夜列取值与端点 1 的 hotels[].roomNights 同源。
⚠️ PDF 版式未在本次自动化用例里逐字段断言(仅断言 exportMonthlyPdf 透传渲染器返回的字节,未展开渲染内容),本条不对 PDF 的具体列布局做保证,只保证其数据来源与 xlsx/端点 1 一致。
请求示例
GET /v3/admin/house/reconciliation/monthly/export?month=2026-05&format=xlsx
Authorization: Bearer <token>
响应示例
(响应体为二进制文件流,非 JSON 信封;下面以 JSON 字符串形式呈现响应头示意,不代表真实报文格式)
{
"headers": {
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": "attachment; filename=\"house-reconciliation-2026-05.xlsx\""
},
"note": "body 为 xlsx 二进制字节,数据区列头固定为:酒店名称、订单、间夜、实际花销"
}
空数据 / 降级响应
本月三路数据源全部为空时,仍返回 200 + 只有表头、无数据行的 xlsx/pdf 文件(不是 400/404)。
错误响应
format 既非 xlsx 也非 pdf(既有错误码,本次未改):
{
"code": 808171,
"message": "导出格式非法(仅支持Excel/PDF)",
"data": null,
"success": false
}
业务边界
- 鉴权:同端点 1。
- 数据同源:导出内容直接复用端点 1 的
aggregate()结果,不做二次过滤,端点 1 的所有本次变更(团期并入、实际花销新建桶)在导出文件里同样生效。 - 格式大小写不敏感:
XLSX/Xlsx/PDF均可识别,本次未改。 - 只读:不提供写口。
四、契约约束与正确调用方式(接口类必写)
本节只写后端接受/拒绝查询参数的规则,不写 UI 渲染建议。
✅ 正确 / ❌ 错误 查询组合对照
| 场景 | 查询参数 | 结果 |
|---|---|---|
| ✅ 标准月度汇总查询 | month=2026-05 |
200,hotels[] 含配房/团期/实际花销三路合并结果 |
| ✅ 按酒店 ID 查订单明细 | month=2026-05&hotelId=701 |
200,优先按 ID 精确匹配,忽略同时传入的 hotelName |
| ✅ 按酒店名兜底查订单明细 | month=2026-05&hotelName=旧酒店(hotelId 为空) |
200,按本月同名酒店唯一收敛匹配 |
| ✅ 导出 PDF | month=2026-05&format=pdf |
200,application/pdf 附件 |
| ❌ 酒店订单明细缺过滤条件 | month=2026-05(hotelId 与 hotelName 均未传) |
808172 |
| ❌ 月份格式错误 | month=2026/05 或 month=2026-13 |
808170 |
| ❌ 导出格式非法 | month=2026-05&format=csv |
808171 |
切换状态时的必要动作
无状态切换(三个端点均为只读查询),刷新页面/重新查询即可拿到最新聚合结果;本次改动不引入任何缓存或异步生成步骤。
六、边界行为
- 未登录 → 401(网关拦截)
month格式非法 → 808170,HTTP 恒 200- 酒店订单明细缺
hotelId/hotelName→ 808172 - 导出
format非法 → 808171 - 三路数据源全部为空 → 200 + 空列表/全 0 概览/只有表头的空文件,不报错
- 老数据兼容 → 存量配房行未受影响,本次改动只做数据源新增合并,不改写任何存量行为
六.6、修改前后对比(修改类接口必写)
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
monthly.hotels[] / hotel-orders 响应 |
字段结构不变 | 字段结构完全不变——本次只改变数据内容,不新增/删除/改类型任何字段 |
hotels[].roomNights / totalAmount(团期酒店) |
团期订单永远不出现在这两个端点,故不存在对应行 | 出现新的酒店行,按整团订房间数(酒店级)/到户间数(订单级)计算 |
hotels[].orderCount / overview.orderCount |
只有存在配房权威行的订单才计数;实际花销记录不参与订单计数 | 团期分房行贡献的订单计数;实际花销记录本次起也参与订单去重计数(无论该订单是否已有其他权威行贡献) |
| 实际花销无匹配酒店桶时 | 该笔花销被整条丢弃,不进任何字段 | 新建一个 roomNights=0/totalAmount=0 的酒店桶或订单行承接该笔花销 |
HotelSettlementActualCostReadDTO.hotelId(内部 Feign 契约,非本次三端点响应字段) |
无此字段 | 新增该字段,供酒店归属匹配使用(不影响管理后台任何响应结构) |
行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 团期订单在月度对账三个端点里 | 不出现 | 出现(按各自口径聚合) |
| 本月无配房权威行但有团期/实际花销数据 | /monthly、/monthly/hotel-orders 均提前短路返回空 |
三路数据源合并后判空,不再因缺配房行提前短路 |
| 核单实际花销找不到匹配酒店桶 | 静默丢弃,总成本/利润被低估,无任何提示 | 新建酒店桶/订单行承接,overview.totalActualExpense、overview.orderCount、部分 hotels[].orderCount 可能比改动前的历史观察值更大 |
| 核单实际花销的酒店归属匹配 | 只能靠配房行 ID 回查或酒店名收敛,两者都失败即丢弃 | 新增按核单行自带 hotel_id 快照直接匹配,减少「查不到」的情况;hotelId 优先于酒店名兜底 |
环比(monthOverMonth) |
上月数据只按配房权威行计算 | 上月数据同样按本次扩展后的三路口径重算,两个月份口径保持一致 |
| 导出 xlsx/pdf | 只含配房权威行贡献的酒店行 | 与端点 1 同源,团期行与实际花销新建行同步出现在导出文件里 |
六.7、影响评估(修改类必写)
- 是否破坏向后兼容: 否。三个端点的路径、方法、请求参数、响应字段结构、错误码均未变化;改动完全体现在返回数值上。
- 前端是否必须同步上线: 否(不同步上线不会报错,接口本身不因前端未适配而失败),但月度对账页会开始出现两类此前从未出现过的数据形态:①
hotelName为团期酒店、roomNights按整团/到户口径的行;②roomNights=0但actualExpenseAmount有值的酒店/订单行。若前端此前对「间夜为 0」或「零权威行酒店」做过隐藏/过滤类的展示假设,需要重新检查。 - 前端 workaround 清理点: 若管理后台此前因为「团期订单永远不出现在月度对账」而对该页面做过针对性隐藏或提示,本次可以撤除;具体是否存在这类 workaround 未核实,请 mmg 自查。
七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- 仅影响: 管理后台房务月度对账三个只读端点(酒店汇总列表 / 酒店订单明细 / Excel/PDF 导出)的返回数值。
- 零影响:
- 核单结算 Step1/Step2/Step3 的读写接口(含 #7327 PR-1/PR-2 涉及的住宿核单 Step1,那是另一组端点,见
13_7327_团期配房行合流核单结算-修改接口-管理后台.md) - 团期配房/分房本身的写口(房务分房台账、抢单池等)——本次只新增只读汇总,不写团期任何表
- 房务配房行
house_hotel_assignment的写路径与既有已确认配房行的口径 - C 端算价、下单、支付链路
- 团期整单核单闸(584130/584131)等其余核单相关业务规则
- 历史数据:存量对账结果不迁移、不重算,下一次查询/导出时才按新口径实时聚合(对账本身不落库,每次都是实时查询)
- 核单结算 Step1/Step2/Step3 的读写接口(含 #7327 PR-1/PR-2 涉及的住宿核单 Step1,那是另一组端点,见
八、测试环境已验证
接口行为以自动化用例断言实证,带 ✓ 标记(MonthlyReconciliationServiceTest.java,测试夹具:MONTH=2026-05、GROUP_HOTEL_ID=701「团期酒店」、REVOKED_HOTEL_ID=901「旧酒店」):
aggregate 团期计划行并入同一酒店累加器 → 间夜=6(3+3)、金额=5400.00、订单数按分房行去重=2 ✓
aggregate_groupPlanRows_mergedIntoHotelAccumulator
aggregate 未分配余量只计酒店级不计订单 → 间夜=3(全部整团量)、金额=2700.00、订单数=1 ✓
aggregate_unallocatedRemainder_countedAtHotelLevelOnly
aggregate 同酒店house行与团期行合并成一个累加器 → 间夜=5(2+3)、金额=3300.00、订单数=2 ✓
aggregate_groupAndHouseRowsSameHotel_singleAccumulator
listHotelOrders 团期子订单按户出行且productType为GROUP → 订单31间夜4/订单32间夜2、productType=GROUP ✓
listHotelOrders_groupOrder_rowsWithProductTypeGroup
listHotelOrders 未分配余量不出现在订单明细 → 明细只1行、间夜=2(不含余量1间) ✓
listHotelOrders_remainderRow_notListed
applyActualExpenses 团期核单行按allocId归并到计划行所属酒店 → 归并到701团期酒店、花销850.00 ✓
applyActualExpenses_groupSettlementRow_matchedByAllocId
exportMonthlyXlsx 含团期行列头不变且团期数据落在数据区 → 列头[酒店名称,订单,间夜,实际花销]不变、间夜=3 ✓
exportMonthlyXlsx_withGroupRows_headersUnchanged
applyActualExpenses 失效行无对应active桶新建只有实际花销的酒店桶 → 旧酒店桶roomNights=0/花销800.00,
新酒店桶roomNights=1/花销null,overview.totalActualExpense=800.00 ✓
applyActualExpenses_revokedRowWithoutActiveBucket_createsExpenseOnlyBucket
listHotelOrders 只有实际花销的订单以0间夜出现在明细 → 订单31/hotelId=901/roomNights=0/actualExpenseAmount=800.00 ✓
listHotelOrders_expenseOnlyOrder_listedWithZeroRoomNights
aggregate 本月零权威行但有实际花销仍返回实际花销酒店桶 → roomNights=0、花销合计1000.00(800+200)、订单数=2 ✓
aggregate_noConfirmedRowsButExpenses_returnsExpenseBuckets
actualExpenseGroupKey 核单行hotelId优先于酒店名兜底 → 酒店名对不上时仍靠hotelId=601归位、花销500.00 ✓
actualExpenseGroupKey_hotelIdBeforeNameFallback
aggregate 权威行只有酒店名而核单行带hotelId收敛成同一酒店桶 → 间夜=2、金额200.00、花销800.00、订单数=2 ✓
aggregate_nameOnlyRowsAndHotelIdExpense_convergedIntoSingleHotelBucket
网关:本次无新增路径,三个端点沿用既有路由 /v3/admin/** 到 hl-order-service-v3,无需网关改动,未做测试服真实网关调用取证(见 frontmatter status_note)。
上表为自动化用例断言原文;正文响应示例中的 ID/金额取自同一批用例夹具,不是测试服抓包报文。测试服真实网关的验收取证请留在工单 #7327 的验收项里补。
九、相关历史 PR(功能演进时必写)
| PR | Issue | 说明 | 是否仍有效 |
|---|---|---|---|
#4826(对应 changelog 43_4826_...,旧版无 v2 frontmatter) |
#4826 | 月度对账首次加入核单实际花销只读展示 | ✅ 有效,本次是在此基础上修复其静默丢弃缺陷 |
04_7070_... |
#7070 | 订单明细补 productType 字段(直读 order_main.product_type) |
✅ 有效,本次团期行同样会带上 productType=GROUP |
| 本 PR #7645(#7327 PR-3) | #7327 | 并入团期计划行/分房行 + 实际花销静默丢弃修复 | ✅ 最新 |
十、相关文档
- 关联 Issue: wx/HL#7327
- 关联 PR: wx/HL#7645
- 同一 Issue 的另一组端点(住宿核单 Step1,与本文件三个端点无重叠):
changelogs-v2/2026-09/13_7327_团期配房行合流核单结算-修改接口-管理后台.md - 月度对账历史演进:
changelogs-v2/2026-07/43_4826_房务月度对账增加核单实际花销-前端待处理-管理后台.md、changelogs-v2/2026-09/04_7070_房务列表-详情补产品类型标签-productType-修改接口-管理后台.md
关联 / 联系人
链接
联系人
- 后端负责人: @wx