# product-v2: 行程操作日志展开节点/天/住宿具体变化

> **服务**: hl-product-service-v2 (端口 8083)
> **PR**: #2511
> **Issue**: #2510
> **日期**: 2026-05-18
> **影响范围**: STEP2 行程编辑操作日志 `changes` 字段输出形态

---

## mmg 必看 — 接口契约不变,changes 内部条数和内容形态变化

接口 URL / Method / 入参 / 出参字段类型**完全不变**。仅 `changes[]` 数组的条数和每条 `fieldLabel`/`oldValue`/`newValue` 内容形态优化。

### 修复前(聚合,看不出动了哪个节点)

```json
"changes": [
  {
    "fieldLabel": "活动节点列表",
    "oldValue": "共 11 条",
    "newValue": "共 8 条 (新增 0 / 删除 3)",
    "changeType": "MODIFY"
  }
]
```

用户原话:**"没法直观的发现变化应该写具体的节点变化"**。

### 修复后(展开,每个节点变化各一条)

```json
"changes": [
  { "fieldLabel": "删除 活动节点", "oldValue": "DAY 1 #3 中俄边境公路（卡线）", "newValue": null, "changeType": "DELETE" },
  { "fieldLabel": "删除 活动节点", "oldValue": "DAY 2 #1 雾凇画廊", "newValue": null, "changeType": "DELETE" },
  { "fieldLabel": "删除 活动节点", "oldValue": "DAY 4 #2 那达慕狂欢", "newValue": null, "changeType": "DELETE" },
  { "fieldLabel": "新增 活动节点", "oldValue": null, "newValue": "DAY 1 #5 海拉尔国家森林公园", "changeType": "ADD" },
  { "fieldLabel": "修改 活动节点", "oldValue": "DAY 1 #1 礼仪送机", "newValue": "DAY 1 #1 礼仪送机+迎宾", "changeType": "MODIFY" }
]
```

### 三个集合的 display 格式

| 集合 | display 格式 | 例 |
|---|---|---|
| 行程天 | `DAY X` | `DAY 1` |
| 活动节点 | `DAY X #序号 节点名称` | `DAY 1 #5 海拉尔国家森林公园` |
| 住宿 | `DAY X 档位N 酒店ID=...` | `DAY 1 档位1 酒店ID=2046123456789` |

### 上限保护

单次行程编辑变化 > 30 条时,前 30 条展开,剩余聚合成一条 `"其他 N 项变更未展开 (日志条数超上限 30)"`。

---

## 服务端改动

- `EntityFieldDiffUtil.diffCollectionDetailed(...)`: 新方法,接受 `oldDisplay/newDisplay` 抽取器,把集合 diff 展开成 ADD/DELETE/MODIFY 各一条 FieldChange
- `ProductItineraryService.buildItineraryChanges`: 三集合都改用 detailed 版,提供 display 抽取器
- 由于 NodeItem/HotelItem VO 没 `dayId` 字段,用 `IdentityHashMap<NodeItem, Integer>` 在遍历 newDays 时建反向映射
- ProductItineraryServiceTest 17/17 全绿

---

## 部署

- 测试服: 已部署 (deploy task `e88d639e`)
- 正式服: 等 Deploy Panel /prod 触发
- nacos: 无配置变化
