From 899c18de8457438b582eb1755bff2d6ac5e5ac12 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 14 Sep 2026 03:17:43 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20=E6=9C=88=E5=BA=A6=E5=AF=B9?= =?UTF-8?q?=E8=B4=A6=E5=B9=B6=E5=85=A5=E5=9B=A2=E6=9C=9F=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E8=A1=8C=E4=B8=8E=E5=88=86=E6=88=BF=E8=A1=8C=EF=BC=88#7327=20P?= =?UTF-8?q?R-3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补 PR-3(#7645 / 0573c1a78)的接口行为变更。原 13_7327 那份只覆盖 PR-0/1/2 的 settlement/step1,月度对账三个端点一个字没提,而前端拿 changelog 当契约用。 覆盖 GET /v3/admin/house/reconciliation/monthly 及其 hotel-orders、 export 三个既有端点:字段结构未变,变的是返回数值——团期计划行与分房行 首次并入聚合(此前数据源只有 house_hotel_assignment),失效实付不再被 静默丢弃(原 continue 改 computeIfAbsent 新建桶),以及 addOrder 无条件 执行导致 orderCount 口径变化。 测试服实测(dev-v3 @ 0573c1a78 = PR-3 squash 提交,取证前后 commit 未变): 三端点经网关全部 200;2026-10 该月 house 侧 CONFIRMED 为 0 行而团期侧 3 行 9 间,响应返回 2 家酒店 5 单 —— PR-3 之前该月必然空响应。 REVOKED 实付 800 与有效实付 758 合计 1558,与响应逐字段吻合。 frontend_status 取 pending:响应结构未变但数值口径变了,需 mmg 确认 页面对「0 间夜订单行」「团期酒店行」两种新形态的展示。 Refs #7327 Co-Authored-By: Claude Opus 5 (1M context) --- ...¯¹账并入团期计划与分房行-修改接口-管理后台.md | 585 ++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 changelogs-v2/2026-09/14_7327_月度对账并入团期计划与分房行-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/14_7327_月度对账并入团期计划与分房行-修改接口-管理后台.md b/changelogs-v2/2026-09/14_7327_月度对账并入团期计划与分房行-修改接口-管理后台.md new file mode 100644 index 00000000..b6edb0ef --- /dev/null +++ b/changelogs-v2/2026-09/14_7327_月度对账并入团期计划与分房行-修改接口-管理后台.md @@ -0,0 +1,585 @@ +--- +schema: "hl-changelog/v2" +ticket: "7327" +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: "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。" +updated_at: "2026-09-14" +base: "dev-v3" +--- + +# 房务模块: 月度对账三个只读端点并入团期计划行与分房行,核单实际花销修复静默丢弃 + +> **存放目录**: +> - 一期(v2,无 `order-v3` 标签的工单)→ `changelogs/{YYYY-MM}/` +> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/{YYYY-MM}/` +> +> **服务**: hl-order-service-v3 (端口 8086) +> **PR**: [#7645](https://git.1814.love:8443/wx/HL/pulls/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` + +#### 使用场景 + +管理后台「房务月度对账」页面进入或切换月份时调用,返回该月的概览统计(`overview`,含环比)与逐酒店汇总列表(`hotels[]`)。本次起,`hotels[]` 可能包含仅由团期订房计划行贡献的酒店桶,也可能包含仅由核单实际花销贡献的酒店桶(配房权威行为 0)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| month | Query | String | ✅ | 正则 `^\d{4}-(0[1-9]\|1[0-2])$` | 对账月份,如 `2026-05` | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| 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(时间) | 实际花销最近更新时间 | + +#### 请求示例 + +```http +GET /v3/admin/house/reconciliation/monthly?month=2026-05 +Authorization: Bearer +``` + +#### 响应示例 + +以下数值取自自动化测试夹具(`MonthlyReconciliationServiceTest#applyActualExpenses_revokedRowWithoutActiveBucket_createsExpenseOnlyBucket`),不是测试服抓包报文;场景为「订单 31 本月从旧酒店(901,旧酒店)换到新酒店(801,新酒店),旧酒店已录入的一笔实付 800.00 元本次起不再被丢弃」: + +```json +{ + "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` 覆盖): + +```json +{ + "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` 格式(既有错误码,本次未改): + +```json +{ + "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>` + +#### 使用场景 + +管理后台月度对账页点击某个酒店汇总行「查看订单」时调用,按 `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>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| 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 | 是否已录入实际花销 | + +#### 请求示例 + +```http +GET /v3/admin/house/reconciliation/monthly/hotel-orders?month=2026-05&hotelId=701 +Authorization: Bearer +``` + +#### 响应示例 + +以下取自 `listHotelOrders_groupOrder_rowsWithProductTypeGroup` 用例夹具(酒店 701「团期酒店」本月有两条 CONFIRMED 团期计划行、四条分房行,分给订单 31、32 两户): + +```json +{ + "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` 查询): + +```json +{ + "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` 为空数组: + +```json +{ "code": 200, "message": "成功", "data": [], "success": true } +``` + +#### 错误响应 + +`hotelId` 与 `hotelName` 均未传(既有错误码,本次未改): + +```json +{ + "code": 808172, + "message": "酒店ID或酒店名称至少填写一个", + "data": null, + "success": false +} +``` + +`month` 格式非法: + +```json +{ + "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 一致。 + +#### 请求示例 + +```http +GET /v3/admin/house/reconciliation/monthly/export?month=2026-05&format=xlsx +Authorization: Bearer +``` + +#### 响应示例 + +(响应体为二进制文件流,非 JSON 信封;下面以 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`(既有错误码,本次未改): + +```json +{ + "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)等其余核单相关业务规则 + - 历史数据:存量对账结果不迁移、不重算,下一次查询/导出时才按新口径实时聚合(对账本身不落库,每次都是实时查询) + +--- + +## 八、测试环境已验证 + +接口行为以自动化用例断言实证,带 ✓ 标记(`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](https://git.1814.love:8443/wx/HL/issues/7327) +- 关联 PR: [wx/HL#7645](https://git.1814.love:8443/wx/HL/pulls/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` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7327](https://git.1814.love:8443/wx/HL/issues/7327) +- **PR**: [#7645](https://git.1814.love:8443/wx/HL/pulls/7645) +- **Merge commit**: [0573c1a78](https://git.1814.love:8443/wx/HL/commit/0573c1a78) + +### 联系人 + +- **后端负责人**: @wx