# 【修改接口·管理后台】核单 Step1 住宿成本字段 (#5043)

> **PR**: #5047 / #5162 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-23 09:53

## 1. 接口背景

核单 Step1 住宿成本明细需要对齐原型里的酒店资源、房型资源、核算单价、来源和确认状态展示。现有接口保留原路径，在 `GET/PUT /v3/admin/order/{orderId}/settlement/step1` 上做兼容增强。

### 1.1 2026-07-23 前端对接补充（数据来源提交约定）

`sourceType` 只用于区分住宿明细的数据来源，不决定行是否可编辑。行的可编辑性继续由 `settlementConfirmStatus` 控制，本次不新增 `deletable` 或 `sourceFieldsEditable` 字段。

| 数据来源 | GET 返回 / PUT 回传的 `sourceType` | PUT 回传的 `sourceId` | PUT 回传的 `hotelAssignmentId` |
|----------|------------------------------------------|----------------------------|---------------------------------------|
| 后台配房自动行 | `HOUSE_ASSIGNMENT` | 原样回传 GET 返回的配房记录 ID | 原样回传 GET 返回的配房记录 ID |
| 前端手动新增行 | `MANUAL` | `null` | `null` |

`sourceType` 当前仍为非必填字段：未传时，后端可根据 `hotelAssignmentId` 推断数据来源。为了稳定保留来源信息，前端保存时应按上表显式回传。

**后台配房自动行 PUT 关键字段**：

```json
{
  "sourceType": "HOUSE_ASSIGNMENT",
  "sourceId": "2077233886282088401",
  "hotelAssignmentId": "2077233886282088401",
  "settlementConfirmStatus": "UNCONFIRMED"
}
```

**前端手动新增行 PUT 关键字段**：

```json
{
  "sourceType": "MANUAL",
  "sourceId": null,
  "hotelAssignmentId": null,
  "settlementConfirmStatus": "UNCONFIRMED"
}
```

## 2. 变更清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询住宿核算明细 | GET | `/v3/admin/order/{orderId}/settlement/step1` | 修改接口 | `HotelItemVO` 出参新增 10 个字段 |
| 2 | 保存住宿核算明细 | PUT | `/v3/admin/order/{orderId}/settlement/step1` | 修改接口 | `HotelItemVO` 入参支持保存酒店/房型资源、单价、来源和确认状态字段 |

## 3. 接口详情

### 3.1 查询住宿核算明细

- **使用场景**：进入核单 Step1 住宿页签时查询住宿成本明细。
- **认证**：需要管理后台 JWT。
- **幂等性**：幂等，只读查询。
- **响应结构**：`data` 为 `HotelItemVO[]` 数组。

### 3.2 保存住宿核算明细

- **使用场景**：保存核单 Step1 住宿成本明细。
- **认证**：需要管理后台 JWT。
- **幂等性**：全量替换保存；同一份 `items` 重复提交后，以最后一次提交为准。
- **请求体兼容**：推荐使用 `{ "items": [...] }`；历史数组 body `[...]` 仍兼容。

## 4. 接口入参

### 4.1 路径参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | string | 是 | 订单 ID，长整型字符串 |

### 4.2 PUT 请求体字段

| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | array | 是 | 住宿成本明细行数组，全量替换保存 | 不允许为 `null` |
| `items[].id` | string | 否 | 已存在行 ID；全量替换保存时可不传 | 长整型字符串 |
| `items[].hotelAssignmentId` | string/null | 否 | 配房 assignment ID；后台配房自动行应原样回传，手动行传 `null` | 长整型字符串或 `null` |
| `items[].hotelId` | string/null | 否 | 酒店资源 ID；本次新增 | 长整型字符串或 `null` |
| `items[].roomTypeId` | string/null | 否 | 房型资源 ID；本次新增 | 长整型字符串或 `null` |
| `items[].stayDate` | string | 是 | 入住日期 | `yyyy-MM-dd`，不能早于订单出发日 |
| `items[].hotelName` | string | 是 | 酒店名称 | 1-200 字符 |
| `items[].roomType` | string/null | 否 | 房型分类或旧展示字段 | 最大 64 字符 |
| `items[].roomTypeName` | string/null | 否 | 房型/规格名称；本次新增 | 最大 64 字符 |
| `items[].roomCount` | integer | 是 | 总间数 | 正整数 |
| `items[].unitPrice` | number/null | 否 | 核算单价，单位元/间夜；本次新增；不传时按 `actualCost / roomCount` 降级计算 | `>= 0` |
| `items[].plannedCost` | number | 是 | 计划成本，单位元 | `>= 0` |
| `items[].actualCost` | number | 是 | 实际成本，单位元 | `>= 0` |
| `items[].paymentMethod` | string | 否 | 付款方式；与 `settleType` 二选一 | `SIGNED` / `COMPANY_PAID` / `CASH_PAID` |
| `items[].paymentMethodName` | string | 否 | 付款方式中文名，仅展示字段；保存时可不传；本次新增 | 最大 32 字符 |
| `items[].settleType` | string | 否 | 配房结算类型；与 `paymentMethod` 二选一 | `cash` / `sign` / `company` |
| `items[].sourceType` | string | 否 | 仅标识数据来源；后台配房行回传 `HOUSE_ASSIGNMENT`，手动行传 `MANUAL`；不传时后端按 `hotelAssignmentId` 推断 | `HOUSE_ASSIGNMENT` / `MANUAL` / `TEMPLATE` |
| `items[].sourceTypeName` | string | 否 | 来源类型中文名，仅展示字段；保存时可不传；本次新增 | 最大 32 字符 |
| `items[].sourceId` | string/null | 否 | 来源业务 ID；后台配房自动行应原样回传配房记录 ID，手动行传 `null` | 长整型字符串或 `null` |
| `items[].settlementConfirmStatus` | string | 否 | 核单确认状态；本次新增；不传默认 `CONFIRMED` | `UNCONFIRMED` / `CONFIRMED` |
| `items[].settlementConfirmStatusName` | string | 否 | 核单确认状态中文名，仅展示字段；保存时可不传；本次新增 | 最大 32 字符 |
| `items[].remark` | string/null | 否 | 备注 | 最大 500 字符 |
| `items[].voucherUrls` | array | 否 | 凭证图片 URL 数组 | 字符串数组 |

## 5. 出参

### 5.1 GET 响应字段：`HotelItemVO`

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string/null | 核单住宿明细行 ID；首次派生未保存的行可为 `null` |
| `hotelAssignmentId` | string/null | 配房 assignment ID；手工行可为 `null` |
| `hotelId` | string/null | 酒店资源 ID；本次新增 |
| `roomTypeId` | string/null | 房型资源 ID；本次新增 |
| `stayDate` | string | 入住日期，`yyyy-MM-dd` |
| `hotelName` | string | 酒店名称 |
| `roomType` | string/null | 房型分类或旧展示字段 |
| `roomTypeName` | string/null | 房型/规格名称；本次新增 |
| `roomCount` | integer | 总间数 |
| `unitPrice` | number/null | 核算单价，单位元/间夜；本次新增 |
| `plannedCost` | number | 计划成本，单位元 |
| `actualCost` | number | 实际成本，单位元 |
| `paymentMethod` | string | 付款方式 |
| `paymentMethodName` | string/null | 付款方式中文名；本次新增 |
| `settleType` | string/null | 配房结算类型；保存草稿后可能为空 |
| `sourceType` | string | 来源类型；本次新增 |
| `sourceTypeName` | string/null | 来源类型中文名；本次新增 |
| `sourceId` | string/null | 来源业务 ID；本次新增 |
| `settlementConfirmStatus` | string | 核单确认状态；本次新增 |
| `settlementConfirmStatusName` | string/null | 核单确认状态中文名；本次新增 |
| `remark` | string/null | 备注 |
| `voucherUrls` | array | 凭证图片 URL 数组 |

### 5.2 PUT 响应字段：`SettlementHotelSaveRespVO`

| 字段 | 类型 | 说明 |
|------|------|------|
| `addedIds` | string[] | 本次保存新增的核单住宿明细行 ID 列表 |
| `updatedIds` | string[] | 本次保存更新的核单住宿明细行 ID 列表；当前全量替换语义下通常为空数组 |
| `deletedIds` | string[] | 本次保存删除的核单住宿明细行 ID 列表；当前返回通常为空数组 |
| `totalActualCost` | string | 保存后 Step1 实际成本合计，单位元 |

## 6. 枚举 / 数据字典

### 6.1 `paymentMethod`

**所属字段**：`items[].paymentMethod`、`data[].paymentMethod` | **类型**：String | **必填**：否

| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 现场签单 |
| `COMPANY_PAID` | 公司付款 | 公司统一付款 |
| `CASH_PAID` | 现付 | 现场现金或线下现付 |

### 6.2 `settleType`

**所属字段**：`items[].settleType`、`data[].settleType` | **类型**：String | **必填**：否

| 值 | 中文 | 映射后的 `paymentMethod` |
|----|------|--------------------------|
| `cash` | 现付 | `CASH_PAID` |
| `sign` | 签单 | `SIGNED` |
| `company` | 公司付款 | `COMPANY_PAID` |

### 6.3 `sourceType`

**所属字段**：`items[].sourceType`、`data[].sourceType` | **类型**：String | **必填**：否

| 值 | 中文 | 说明 |
|----|------|------|
| `HOUSE_ASSIGNMENT` | 配房结果 | 来自房务配房结果 |
| `MANUAL` | 手工 | 核单手工补充住宿行 |
| `TEMPLATE` | 模板 | 模板来源住宿行，当前预留 |

### 6.4 `settlementConfirmStatus`

**所属字段**：`items[].settlementConfirmStatus`、`data[].settlementConfirmStatus` | **类型**：String | **必填**：否

| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 核单住宿明细未确认 |
| `CONFIRMED` | 已确认 | 核单住宿明细已确认；不传时默认该值 |

## 7. 错误码

| code | 含义 | 触发场景 |
|------|------|----------|
| `584002` | 当前核单状态不允许录住宿核单 | PUT 保存时，订单不是「待核单」或「核单中」 |
| `584008` | 订单缺出发日期，无法派生 dayNumber | PUT 保存时订单出发日期为空 |
| `584009` | `stayDate` 早于订单出发日期 | PUT 保存时日期越界 |
| `584062` | 临时行必须指定付款方式 | `paymentMethod` 和 `settleType` 都为空 |
| `584064` | 配房记录 `settleType` 字典值非法 | `settleType` 不是 `cash/sign/company` |
| `100001` | 参数非法 | 字段格式不符合校验，例如枚举值不在允许范围内、金额小于 0 |

## 8. 示例

### 8.1 典型成功：GET 查询

**请求**

```http
GET /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
```

**响应**

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": [
    {
      "id": "2077328317421187001",
      "hotelAssignmentId": "2077233886282088401",
      "hotelId": "50001",
      "roomTypeId": "51001",
      "stayDate": "2026-07-18",
      "hotelName": "海拉尔海棠酒店",
      "roomType": "STANDARD",
      "roomTypeName": "精品标间",
      "roomCount": 2,
      "unitPrice": 440.00,
      "plannedCost": 880.00,
      "actualCost": 880.00,
      "paymentMethod": "CASH_PAID",
      "paymentMethodName": "现付",
      "settleType": "cash",
      "sourceType": "HOUSE_ASSIGNMENT",
      "sourceTypeName": "配房结果",
      "sourceId": "2077233886282088401",
      "settlementConfirmStatus": "CONFIRMED",
      "settlementConfirmStatusName": "已确认",
      "remark": "已核对",
      "voucherUrls": []
    }
  ]
}
```

### 8.2 边界成功：PUT 保存手工住宿行

**请求**

```http
PUT /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
Content-Type: application/json
```

```json
{
  "items": [
    {
      "hotelAssignmentId": null,
      "hotelId": null,
      "roomTypeId": null,
      "stayDate": "2026-07-18",
      "hotelName": "临时补充酒店",
      "roomType": "STANDARD",
      "roomTypeName": "标准间",
      "roomCount": 1,
      "unitPrice": 300.00,
      "plannedCost": 300.00,
      "actualCost": 300.00,
      "paymentMethod": "COMPANY_PAID",
      "sourceType": "MANUAL",
      "sourceId": null,
      "settlementConfirmStatus": "UNCONFIRMED",
      "voucherUrls": [],
      "remark": "核单临时补充"
    }
  ]
}
```

**响应**

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "addedIds": ["2078398065684692001"],
    "updatedIds": [],
    "deletedIds": [],
    "totalActualCost": "300.00"
  }
}
```

### 8.3 业务失败：缺付款方式

**请求**

```http
PUT /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
Content-Type: application/json
```

```json
{
  "items": [
    {
      "hotelAssignmentId": null,
      "stayDate": "2026-07-18",
      "hotelName": "临时补充酒店",
      "roomType": "STANDARD",
      "roomCount": 1,
      "plannedCost": 300.00,
      "actualCost": 300.00
    }
  ]
}
```

**响应**

```json
{
  "code": 584062,
  "message": "临时行（无配房关联）必须指定 paymentMethod",
  "data": null,
  "success": false
}
```

## 9. 业务边界

- `PUT` 是全量替换保存；前端保存时应提交页面当前完整明细列表。
- GET 返回的后台配房自动行，PUT 保存时应原样回传 `sourceType=HOUSE_ASSIGNMENT`、`sourceId` 和 `hotelAssignmentId`，两个 ID 都是对应配房记录 ID。
- 前端手动新增行，PUT 保存时传 `sourceType=MANUAL`、`sourceId=null`、`hotelAssignmentId=null`。
- `sourceType` 不传时，`hotelAssignmentId` 有值默认 `HOUSE_ASSIGNMENT`，否则默认 `MANUAL`。
- `sourceType` 只区分数据来源，不决定可编辑性；可编辑性继续由 `settlementConfirmStatus` 控制。
- `sourceId` 不传且 `sourceType=HOUSE_ASSIGNMENT` 时，默认使用 `hotelAssignmentId`；其他来源可为 `null`。
- `unitPrice` 不传且 `roomCount > 0`、`actualCost` 有值时，返回时会按 `actualCost / roomCount` 保留 2 位小数。
- `settlementConfirmStatus` 不传时默认 `CONFIRMED`。
- `paymentMethod` 与 `settleType` 二选一；`paymentMethod` 优先，`settleType` 会映射成 `paymentMethod`。
- 订单核单状态必须是「待核单」或「核单中」才允许保存 Step1。

## 10. 修改前后对比

### 10.1 字段级对比

| 字段 | 修改前 | 修改后 |
|------|--------|--------|
| `hotelId` | 无 | 新增，酒店资源 ID |
| `roomTypeId` | 无 | 新增，房型资源 ID |
| `roomTypeName` | 无 | 新增，房型/规格名称 |
| `unitPrice` | 无 | 新增，核算单价 |
| `paymentMethodName` | 无 | 新增，付款方式中文名 |
| `sourceType` | 无 | 新增，来源类型 |
| `sourceTypeName` | 无 | 新增，来源类型中文名 |
| `sourceId` | 无 | 新增，来源业务 ID |
| `settlementConfirmStatus` | 无 | 新增，核单确认状态 |
| `settlementConfirmStatusName` | 无 | 新增，核单确认状态中文名 |

### 10.2 行为级对比

| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 住宿来源展示 | 只能通过 `hotelAssignmentId` 粗略判断 | 返回 `sourceType/sourceTypeName/sourceId` |
| 单价展示 | 前端只能根据总价和间数自行推算 | 返回 `unitPrice`，缺失时后端按实际成本和间数降级计算 |
| 确认状态展示 | 无独立字段 | 返回 `settlementConfirmStatus/settlementConfirmStatusName` |

## 11. 影响评估 / 回滚

### 11.1 影响评估

- **是否破坏向后兼容**：否。新增字段为兼容性新增，旧字段保留。
- **前端是否必须同步上线**：否。旧页面可继续按原字段展示；需要原型新增列时读取新增字段。
- **影响已有数据**：历史行新增字段可能为 `null`，前端需要保留空值展示逻辑。

### 11.2 回滚方案

- 回滚接口代码后，前端不要再依赖本次新增字段。
- 如果页面已使用新增列，回滚期间新增列需要降级为空态展示。

## 12. 注意事项

- `paymentMethodName`、`sourceTypeName`、`settlementConfirmStatusName` 都是展示字段，保存时可不传。
- `roomType` 是旧字段，`roomTypeName` 是本次新增的房型/规格名称；两者可能同时存在。
- `settlementConfirmStatus` 是核单明细确认状态，与房务配房确认状态不是同一个字段。

## 13. 关联 / 联系人

### 13.1 链接

- **Issue**: [#5043](https://git.1814.love:8443/wx/HL/issues/5043)
- **PR**: [#5047](https://git.1814.love:8443/wx/HL/pulls/5047)
- **Merge commit**: [0a9e83b39](https://git.1814.love:8443/wx/HL/commit/0a9e83b390d135c05b43bc18afe1b190329bf85c)
- **对接补充 Issue**: [#5157](https://git.1814.love:8443/wx/HL/issues/5157)
- **对接补充 PR**: [#5162](https://git.1814.love:8443/wx/HL/pulls/5162)
- **对接补充 Merge commit**: [bc5669fd5](https://git.1814.love:8443/wx/HL/commit/bc5669fd5)

### 13.2 联系人

- **后端负责人**: @yaosutu
