---
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@adff10ea74198f4e89a1488e631463bedbcd4eea"
updated_at: "2026-07-25T03:37:10.934Z"
---
# 【修改接口·管理后台】酒店候选补齐房型结算价 (#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<String> | 运营标签；无标签时为空数组或 `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 <admin-jwt>
```

无请求体。

**响应**：

```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 <admin-jwt>
```

无请求体。

**响应**：

```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 <admin-jwt>
```

无请求体。

**响应**：

```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
