diff --git a/changelogs-v2/2026-07/25_5237_酒店候选补齐房型结算价-修改接口-管理后台.md b/changelogs-v2/2026-07/25_5237_酒店候选补齐房型结算价-修改接口-管理后台.md new file mode 100644 index 0000000..5ce520f --- /dev/null +++ b/changelogs-v2/2026-07/25_5237_酒店候选补齐房型结算价-修改接口-管理后台.md @@ -0,0 +1,421 @@ +# 【修改接口·管理后台】酒店候选补齐房型结算价 (#5237) + +> **PR**: #5240 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-25 10:58 + +## 1. 接口背景 + +管理后台酒店候选列表原来只在候选酒店顶层返回 `protoPrice`,前端无法确认这个价格来自哪个真实房型,也拿不到同一房型同一天的结算价。配房时如果只看房型列表或自行匹配最低价,容易把协议价和结算价口径拆到不同房型。 + +本次在候选酒店顶层补齐: + +- `protoPriceRoomTypeId`:产生顶层 `protoPrice` 的真实房型 ID。 +- `settlementPrice`:与 `protoPriceRoomTypeId` 同一房型、同一天的结算价。 + +顶层 `protoPrice`、`protoPriceRoomTypeId`、`settlementPrice` 是同一代表房型口径。未维护结算价时 `settlementPrice = null`,不会用协议价兜底。 + +## 2. 变更清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 查询酒店候选(4 场景统一入口) | GET | `/v3/admin/hotel-candidates` | 修改接口 | 候选酒店项新增 `protoPriceRoomTypeId`、`settlementPrice` 两个出参字段;入参不变。 | + +## 3. 接口详情 + +### 3.1 查询酒店候选(4 场景统一入口) + +- **使用场景**:管理后台在订单维度查询某一晚的候选酒店,用于配房选酒店、回显当前已配酒店、按产品池/定制师点名/资源库候选排序。 +- **认证**:需要管理后台 JWT。 +- **幂等性**:只读查询,幂等。 +- **限流**:无接口级特殊限流;受网关与服务通用限流策略约束。 + +## 4. 接口入参 + +### 4.1 路径参数 / Query 参数 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `orderId` | String | 是 | 订单 ID。后端 Long,JSON/Query 建议按字符串传,避免长 ID 精度问题。 | +| `dayNumber` | Integer | 否 | 第几天,从 1 开始;用于推算 `stayDate = departDate + dayNumber - 1`。最小值 1。 | +| `stayDate` | String | 否 | 入住日期,格式 `yyyy-MM-dd`;直接指定时优先于 `dayNumber` 推算。 | +| `city` | String | 否 | 城市代码或城市名;未传且非关键词模式时默认不按城市限制。 | +| `keyword` | String | 否 | 关键词;非空时跨城/省匹配酒店名、城市、省份、地址,此时 `city` 可不传。 | +| `limit` | Integer | 否 | 返回候选条数上限,默认 30,最小 1,最大 50。 | +| `roomCategory` | String | 否 | 房型字典 code。 | +| `roomCount` | Integer | 否 | 需要的房间数;最小 1。 | +| `preferredHotelId` | String | 否 | 定制师指定的优先酒店 ID。后端 Long,建议字符串传。 | +| `requirementId` | String | 否 | 用房需求 ID;传入后将该需求 days JSON 中当前天的酒店候选作为定制师指定候选。后端 Long,建议字符串传。 | + +### 4.2 请求体字段 + +GET 接口无请求体。 + +## 5. 出参字段 + +统一响应结构: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `code` | Integer | 业务状态码,成功为 `200`。 | +| `message` | String | 响应消息,成功为 `成功`。 | +| `data` | Object | 酒店候选查询出参。 | +| `traceId` | String | 链路追踪 ID,可能为空。 | +| `success` | Boolean | `code == 200` 时为 `true`。 | + +`data` 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `stayDate` | String | 入住日期,格式 `yyyy-MM-dd`。 | +| `city` | String / null | 本次查询使用的城市;关键词模式或默认不限城市时可为 `null`。 | +| `productType` | String | 产品类型:`CORE` / `GROUP` / `CUSTOM`。 | +| `candidates` | Array | 候选酒店列表,已按产品类型分流排序。 | + +`data.candidates[]` 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `hotelId` | String | 酒店 ID。 | +| `hotelName` | String | 酒店名称。 | +| `level` | String / null | 酒店等级。 | +| `form` | String / null | 住宿形态。 | +| `address` | String / null | 地址。 | +| `tags` | Array | 运营标签;无标签时为空数组或 `null`。 | +| `contactPerson` | String / null | 联系人。 | +| `contactWechat` | String / null | 联系微信。 | +| `settleType` | String / null | 结算类型,取值见 §6.1。 | +| `city` | String / null | 酒店所在城市。 | +| `district` | String / null | 酒店所在区/县。 | +| `roomTypes` | Array | 该酒店当日真实房型列表;无房型数据时为空数组。 | +| `protoPrice` | String / null | 代表房型协议价。与 `protoPriceRoomTypeId`、顶层 `settlementPrice` 同一房型同一天。 | +| `protoPriceRoomTypeId` | String / null | 产生顶层 `protoPrice` 的真实房型 ID。无有效可售协议价时为 `null`。 | +| `settlementPrice` | String / null | 与 `protoPriceRoomTypeId` 同一房型、同一天的结算价。未维护时为 `null`,不会用 `protoPrice` 兜底。 | +| `todayAvailable` | Integer / null | 今日全房型可用房数合计。 | +| `availFreshness` | String / null | 可用数数据时效:`fresh` / `stale` / `never_checked`。 | +| `lastCheckedAt` | String / null | 最近一次核房时间,格式 `yyyy-MM-dd'T'HH:mm:ss`。 | +| `matchedRoomTypeAvailable` | Integer / null | 匹配房型今日可用数。 | +| `matchedRoomTypeId` | String / null | 匹配的房型 ID。 | +| `matchedRoomTypeLabel` | String / null | 匹配的房型中文。 | +| `quickPickEnabled` | Boolean / null | 是否支持快速配房。 | +| `quickPickDisabledReason` | String / null | 置灰原因。 | +| `isPoolMatch` | Boolean / null | 是否产品池内。 | +| `poolMatchBadge` | Object / null | 产品池内徽章。 | +| `isConsultantRecommended` | Boolean / null | 是否被定制师点名。 | +| `consultantRecommendBadge` | Object / null | 定制师点名徽章。 | +| `historyMatchScore` | Number / null | 历史匹配度,范围 0-1。 | +| `score` | Number / null | 排序分数。 | +| `recommendation` | String / null | 推荐理由。 | +| `recommended` | Boolean / null | 是否为推荐候选。 | +| `recommendSource` | String / null | 推荐来源,见 §6.4。 | +| `historyScoreStub` | Boolean / null | 历史命中分数是否为 stub。 | +| `isCurrentlyAssigned` | Boolean / null | 是否为本天当前已配酒店。 | +| `assignedRoomTypeId` | String / null | 本天当前已配的房型 ID;`isCurrentlyAssigned=true` 时可用于预填原房型。 | + +`data.candidates[].roomTypes[]` 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `roomTypeId` | String | 房型 ID。 | +| `name` | String / null | 房型名称。 | +| `roomCategory` | String / null | 房型分类字典值。 | +| `bedType` | String / null | 床型,已按字典尽量翻译;字典缺失时可回退为 code。 | +| `maxOccupancy` | Integer / null | 最大入住人数。 | +| `available` | Integer / null | 今日可用房数;`unlimited=true` 时为 `null`,语义为不限。 | +| `unlimited` | Boolean | 是否不限库存。 | +| `stock` | Integer / null | 当前可用房;`unlimited=true` 时为 `null`。 | +| `protocolPrice` | String / null | 该房型当日协议价。 | +| `settlementPrice` | String / null | 该房型当日结算价。 | +| `basePrice` | String / null | 标价/挂牌价。 | +| `inventoryStatus` | String | 库存状态,见 §6.2。 | + +徽章对象字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `label` | String | 中文徽章文字。 | +| `color` | String | 徽章色,见 §6.5。 | +| `tooltip` | String | 悬浮提示。 | + +## 6. 枚举 / 数据字典 + +### 6.1 `settleType` + +**所属字段**:`data.candidates[].settleType` | **类型**:String | **必填**:否 + +| 值 | 中文 | 说明 | +|----|------|------| +| `cash` | 现付 | 到店或线下现金类结算。 | +| `sign` | 签单 | 供应商签单结算。 | +| `company` | 公司付 | 公司统一付款结算。 | + +### 6.2 `inventoryStatus` + +**所属字段**:`data.candidates[].roomTypes[].inventoryStatus` | **类型**:String | **必填**:是 + +| 值 | 中文 | 说明 | +|----|------|------| +| `AVAILABLE` | 可售 | 有余量,或 `unlimited=true` 不限库存。 | +| `FULL` | 满房 | 有日历记录,但库存为 0。 | +| `CLOSED` | 未开放 | 无该日价格日历记录。 | + +### 6.3 `availFreshness` + +**所属字段**:`data.candidates[].availFreshness` | **类型**:String | **必填**:否 + +| 值 | 中文 | 说明 | +|----|------|------| +| `fresh` | 最新 | 可用于快速配房判断。 | +| `stale` | 过期 | 核房数据过期。 | +| `never_checked` | 从未核房 | 无可用核房数据。 | + +### 6.4 `recommendSource` + +**所属字段**:`data.candidates[].recommendSource` | **类型**:String | **必填**:否 + +| 值 | 中文 | 说明 | +|----|------|------| +| `PRODUCT_POOL` | 产品池 | 来自产品池候选。 | +| `CONSULTANT` | 定制师点名 | 来自定制师指定候选。 | +| `RESOURCE_LIB` | 资源库 | 来自资源库候选。 | + +### 6.5 `Badge.color` + +**所属字段**:`poolMatchBadge.color` / `consultantRecommendBadge.color` | **类型**:String | **必填**:否 + +| 值 | 中文 | 说明 | +|----|------|------| +| `blue` | 蓝色 | 普通推荐或池内标识。 | +| `gold` | 金色 | 高优先级推荐标识。 | +| `gray` | 灰色 | 弱提示标识。 | + +## 7. 错误码 + +| code | 含义 | 触发场景 | +|------|------|----------| +| `200` | 成功 | 查询成功。 | +| `400` | 参数错误 | `orderId` 为空、`dayNumber < 1`、`limit` 超出 1-50、`roomCount < 1`、日期格式不是 `yyyy-MM-dd` 等参数绑定或校验失败。 | +| `401` | 未认证 | JWT 缺失或无效。 | +| `403` | 无权限 | 当前账号无权访问该管理后台接口或订单数据。 | +| `581007` | 订单不存在 | `orderId` 对应订单不存在。 | +| `500` | 服务内部错误 | 非预期异常。 | + +## 8. 示例(3 组:典型 / 边界 / 异常) + +### 8.1 典型成功 + +**请求**: + +```http +GET /v3/admin/hotel-candidates?orderId=100001&dayNumber=1&stayDate=2026-07-25&limit=30&roomCount=2 HTTP/1.1 +Authorization: Bearer +``` + +无请求体。 + +**响应**: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "stayDate": "2026-07-25", + "city": null, + "productType": "CORE", + "candidates": [ + { + "hotelId": "2023714929877450753", + "hotelName": "测试酒店", + "level": "舒适型", + "form": "HOTEL", + "address": "呼伦贝尔市海拉尔区测试路 1 号", + "tags": ["协议酒店"], + "contactPerson": "张经理", + "contactWechat": "hotel_mgr", + "settleType": "sign", + "city": "呼伦贝尔市", + "district": "海拉尔区", + "protoPrice": "280.00", + "protoPriceRoomTypeId": "2023727403196502017", + "settlementPrice": "279.00", + "todayAvailable": 7, + "availFreshness": "fresh", + "lastCheckedAt": null, + "matchedRoomTypeAvailable": 7, + "matchedRoomTypeId": "2023727403196502017", + "matchedRoomTypeLabel": "豪华大床房", + "quickPickEnabled": true, + "quickPickDisabledReason": null, + "isPoolMatch": true, + "poolMatchBadge": { + "label": "产品池内", + "color": "blue", + "tooltip": "本酒店在产品池内,优先推荐" + }, + "isConsultantRecommended": false, + "consultantRecommendBadge": null, + "historyMatchScore": 0.85, + "score": 1185.0, + "recommendation": "池内 · 历史合作 8 单成功率 95%", + "recommended": true, + "recommendSource": "PRODUCT_POOL", + "historyScoreStub": true, + "isCurrentlyAssigned": false, + "assignedRoomTypeId": null, + "roomTypes": [ + { + "roomTypeId": "2023727403196502017", + "name": "豪华大床房", + "roomCategory": "KING", + "bedType": "大床", + "maxOccupancy": 2, + "available": 7, + "unlimited": false, + "stock": 7, + "protocolPrice": "280.00", + "settlementPrice": "279.00", + "basePrice": "568.00", + "inventoryStatus": "AVAILABLE" + } + ] + } + ] + }, + "traceId": "trace-20260725-0001", + "success": true +} +``` + +### 8.2 边界情况 + +**场景说明**:代表房型有协议价但未维护结算价,顶层 `settlementPrice` 返回 `null`,不使用 `protoPrice` 兜底。 + +**请求**: + +```http +GET /v3/admin/hotel-candidates?orderId=100001&stayDate=2026-07-25&keyword=%E6%B5%B7%E6%8B%89%E5%B0%94&limit=1 HTTP/1.1 +Authorization: Bearer +``` + +无请求体。 + +**响应**: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "stayDate": "2026-07-25", + "city": null, + "productType": "CUSTOM", + "candidates": [ + { + "hotelId": "2023714929877450753", + "hotelName": "测试酒店", + "settleType": "cash", + "protoPrice": "280.00", + "protoPriceRoomTypeId": "2023727403196502017", + "settlementPrice": null, + "roomTypes": [ + { + "roomTypeId": "2023727403196502017", + "name": "豪华大床房", + "available": 7, + "unlimited": false, + "protocolPrice": "280.00", + "settlementPrice": null, + "basePrice": "568.00", + "inventoryStatus": "AVAILABLE" + } + ], + "quickPickEnabled": true, + "recommended": true, + "recommendSource": "RESOURCE_LIB" + } + ] + }, + "traceId": "trace-20260725-0002", + "success": true +} +``` + +### 8.3 业务失败(异常) + +**场景说明**:`orderId` 未传,触发参数校验失败。 + +**请求**: + +```http +GET /v3/admin/hotel-candidates?stayDate=2026-07-25 HTTP/1.1 +Authorization: Bearer +``` + +无请求体。 + +**响应**: + +```json +{ + "code": 400, + "message": "orderId 不能为空", + "data": null, + "traceId": "trace-20260725-0003", + "success": false +} +``` + +## 9. 业务边界 + +- **适用场景**:管理后台按订单和入住日查询酒店候选;`stayDate` 可直接传,也可通过 `dayNumber` 和订单出发日推算。 +- **不适用场景**:不用于前端直接查询内部资源服务;本文只描述管理后台 `/v3/admin/hotel-candidates`。 +- **特殊边界**:顶层 `protoPrice`、`protoPriceRoomTypeId`、`settlementPrice` 必须按同一代表房型理解;`settlementPrice = null` 表示该代表房型当天未维护结算价。 +- **特殊边界**:`roomTypes[].settlementPrice` 是每个房型自己的当日结算价;顶层 `settlementPrice` 只对应 `protoPriceRoomTypeId` 指向的代表房型。 + +## 10. 修改前后对比 + +### 10.1 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| `data.candidates[].protoPriceRoomTypeId` | 不返回 | 返回产生顶层 `protoPrice` 的真实房型 ID;无有效可售协议价为 `null`。 | +| `data.candidates[].settlementPrice` | 不返回 | 返回与 `protoPriceRoomTypeId` 同一房型、同一天的结算价;未维护为 `null`。 | +| `data.candidates[].protoPrice` | 已返回,但无法判断来自哪个房型 | 仍返回原字段,并与新增的 `protoPriceRoomTypeId`、顶层 `settlementPrice` 组成同一代表房型口径。 | + +### 10.2 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 候选酒店顶层价格展示 | 只能拿到代表协议价 `protoPrice`。 | 可同时拿到代表协议价、代表房型 ID、该代表房型结算价。 | +| 结算价为空 | 顶层没有结算价字段。 | 顶层 `settlementPrice` 返回 `null`;不使用 `protoPrice` 兜底。 | + +## 11. 影响评估 / 回滚 + +### 11.1 影响评估 + +- **是否破坏向后兼容**:否。只新增出参字段,已有字段名、类型、入参不变。 +- **前端是否必须同步上线**:否。老前端可忽略新增字段;需要展示或回填结算价的页面可读取新增字段。 +- **影响已有数据**:无数据迁移要求;历史未维护结算价的房型按 `settlementPrice = null` 返回。 + +### 11.2 回滚方案 + +- **回滚方式**:回滚 PR #5240 后,顶层新增字段不再返回。 +- **回滚后清理**:无前端数据清理要求。 +- **回滚耗时**:按常规服务回滚流程处理。 + +## 12. 注意事项 + +- 前端读取顶层 `settlementPrice` 时,不要把 `null` 当作 `protoPrice`;`null` 表示未维护结算价。 +- 如需定位价格来自哪个房型,使用顶层 `protoPriceRoomTypeId` 去匹配 `roomTypes[].roomTypeId`。 +- 金额和长 ID 在响应 JSON 中按字符串处理,例如 `"280.00"`、`"2023727403196502017"`。 + +## 13. 关联 / 联系人 + +### 13.1 链接 + +- **Issue**: [#5237](https://git.1814.love:8443/wx/HL/issues/5237) +- **PR**: [#5240](https://git.1814.love:8443/wx/HL/pulls/5240) +- **Merge commit**: [dc6e2ef](https://git.1814.love:8443/wx/HL/commit/dc6e2ef6c2b49bd503353814f85723566d4413c6) + +### 13.2 联系人 + +- **后端负责人**: @yst