# 住宿安排保存：前端实现指南（BUG修复）

**日期**: 2026-03-20
**类型**: 前端BUG修复指南
**影响模块**: 产品管理 → 住宿安排

---

## 问题描述

住宿安排页面：
1. **一个家庭保存多个房型保存不上**（如：家庭1 → 大床房 + 普通标间）
2. **第二个家庭的房型也保存不上**

**原因**：后端的住宿数据模型是 **每条记录 = 一个酒店 + 一个房型**，前端需要为每个房型单独调用接口保存。

---

## 后端数据模型

`product_day_hotel` 表结构（每条记录 = 一个房型分配）：

| 字段 | 说明 |
|------|------|
| id | 记录ID |
| productId | 产品ID |
| dayNumber | 第几天 |
| hotelId | 酒店ID |
| hotelName | 酒店名称 |
| roomTypeId | **房型ID**（每条记录一个房型） |
| roomTypeName | 房型名称 |
| roomCount | 房间数量 |
| familyIds | 所属家庭ID数组 JSON |

**关键**：同一天、同一家庭、同一酒店可以有多条记录（不同房型），每个房型是独立的一条记录。

### 数据示例

家庭1 住香格里拉（大床房1间 + 标间1间）= **2条记录**：

```
记录1: hotelId=香格里拉, roomTypeId=大床房, roomCount=1, familyIds=["家庭1"]
记录2: hotelId=香格里拉, roomTypeId=标间,   roomCount=1, familyIds=["家庭1"]
```

家庭2 住海棠酒店（标间1间）= **1条记录**：

```
记录3: hotelId=海棠酒店, roomTypeId=标间, roomCount=1, familyIds=["家庭2"]
```

---

## 后端接口清单

### 1. 添加住宿（每个房型调一次）

```
POST /admin/product/item/{productId}/day/{dayNumber}/hotel
```

请求体：
```json
{
  "hotelId": "2023714929877450753",
  "hotelName": "呼伦贝尔香格里拉大酒店",
  "roomTypeId": "3002000000000000013",
  "roomTypeName": "大床房",
  "roomCount": 1,
  "familyIds": ["2034467435683393537"]
}
```

### 2. 更新住宿

```
PUT /admin/product/hotel/{id}
```

### 3. 删除住宿

```
DELETE /admin/product/hotel/{id}
```

### 4. 查询某天的住宿列表

```
GET /admin/product/item/{productId}/day/{dayNumber}/hotels
```

响应：
```json
{
  "code": 200,
  "data": [
    {
      "id": "2034468900841975809",
      "dayNumber": 1,
      "hotelId": "2023714929877450753",
      "hotelName": "呼伦贝尔香格里拉大酒店",
      "roomTypeId": "3002000000000000013",
      "roomTypeName": "大床房",
      "roomCount": 1,
      "familyIds": ["2034467435683393537"],
      "familyRoomConfig": null
    },
    {
      "id": "2034817340914429954",
      "dayNumber": 1,
      "hotelId": "2023714929877450753",
      "hotelName": "呼伦贝尔香格里拉大酒店",
      "roomTypeId": "3002000000000000014",
      "roomTypeName": "普通标间",
      "roomCount": 1,
      "familyIds": ["2034467435683393537"],
      "familyRoomConfig": null
    }
  ]
}
```

---

## 前端实现方案

### 页面数据结构

前端页面按「家庭 → 酒店 → 房型列表」组织，但后端是扁平的（每个房型一条记录）。需要做转换：

```js
// 前端UI数据结构（按家庭分组）
const uiData = {
  familyId: '2034467435683393537',
  hotelId: '2023714929877450753',
  hotelName: '呼伦贝尔香格里拉大酒店',
  roomTypes: [
    { roomTypeId: '3002000000000000013', roomTypeName: '大床房', roomCount: 1 },
    { roomTypeId: '3002000000000000014', roomTypeName: '普通标间', roomCount: 1 }
  ]
}

// 需要转换为后端的扁平结构（每个房型一条记录）
const apiRequests = uiData.roomTypes.map(rt => ({
  hotelId: uiData.hotelId,
  hotelName: uiData.hotelName,
  roomTypeId: rt.roomTypeId,
  roomTypeName: rt.roomTypeName,
  roomCount: rt.roomCount,
  familyIds: [uiData.familyId]
}))
```

### 保存逻辑（推荐：增量模式）

```js
async function saveAccommodation(productId, dayNumber, familyId, hotelId, hotelName, roomTypes) {
  // 1. 查询当前该天已有的住宿记录
  const { data: existing } = await request.get(
    `/admin/product/item/${productId}/day/${dayNumber}/hotels`
  )

  // 2. 筛选出属于当前家庭的已有记录
  const familyExisting = existing.filter(h =>
    h.familyIds && h.familyIds.includes(familyId)
  )

  // 3. 对比：哪些要删、哪些要更新、哪些要新增
  const existingMap = new Map(familyExisting.map(h => [h.roomTypeId, h]))

  for (const rt of roomTypes) {
    const old = existingMap.get(rt.roomTypeId)
    if (old) {
      // 已存在 → 更新
      await request.put(`/admin/product/hotel/${old.id}`, {
        hotelId, hotelName,
        roomTypeId: rt.roomTypeId,
        roomTypeName: rt.roomTypeName,
        roomCount: rt.roomCount,
        familyIds: [familyId]
      })
      existingMap.delete(rt.roomTypeId)
    } else {
      // 不存在 → 新增
      await request.post(
        `/admin/product/item/${productId}/day/${dayNumber}/hotel`,
        {
          hotelId, hotelName,
          roomTypeId: rt.roomTypeId,
          roomTypeName: rt.roomTypeName,
          roomCount: rt.roomCount,
          familyIds: [familyId]
        }
      )
    }
  }

  // 4. 剩余的旧记录 → 删除（用户已移除的房型）
  for (const [, old] of existingMap) {
    await request.delete(`/admin/product/hotel/${old.id}`)
  }
}
```

### 加载回显逻辑

```js
async function loadAccommodation(productId, dayNumber, families) {
  // 1. 查询该天所有住宿记录（扁平）
  const { data: hotels } = await request.get(
    `/admin/product/item/${productId}/day/${dayNumber}/hotels`
  )

  // 2. 按家庭ID分组
  const familyHotelMap = {}
  for (const family of families) {
    familyHotelMap[family.familyId] = []
  }

  for (const hotel of hotels) {
    const fids = hotel.familyIds || []
    for (const fid of fids) {
      if (!familyHotelMap[fid]) familyHotelMap[fid] = []
      familyHotelMap[fid].push(hotel)
    }
  }

  // 3. 按家庭+酒店分组，合并同酒店的多个房型
  for (const [familyId, hotelRecords] of Object.entries(familyHotelMap)) {
    const hotelGrouped = {}
    for (const h of hotelRecords) {
      if (!hotelGrouped[h.hotelId]) {
        hotelGrouped[h.hotelId] = {
          hotelId: h.hotelId,
          hotelName: h.hotelName,
          roomTypes: []
        }
      }
      hotelGrouped[h.hotelId].roomTypes.push({
        id: h.id,  // 后端记录ID，更新/删除时需要
        roomTypeId: h.roomTypeId,
        roomTypeName: h.roomTypeName,
        roomCount: h.roomCount
      })
    }
    // hotelGrouped 就是该家庭的UI展示数据
  }
}
```

---

## 总结

| 操作 | 接口 | 说明 |
|------|------|------|
| 添加房型 | `POST .../day/{dayNumber}/hotel` | **每个房型调一次**，同一酒店不同房型 = 多次调用 |
| 更新房型 | `PUT .../hotel/{id}` | 按记录ID更新 |
| 删除房型 | `DELETE .../hotel/{id}` | 用户移除某个房型时调用 |
| 查询回显 | `GET .../day/{dayNumber}/hotels` | 返回扁平列表，前端按 familyIds + hotelId 分组展示 |

**核心要点**：后端是扁平的（每个房型一条记录），前端需要做 UI分组结构 ↔ 扁平记录 的转换。
