# product-v2: 节点 description 按产品线 seasons[0] 自动回填季节亮点

> **服务**: hl-product-service-v2 (端口 8083) + hl-resource-service (端口 8082)
> **PR**: #2250
> **Issue**: #2243
> **日期**: 2026-05-14
> **影响范围**: 管理端 产品编辑 → 补充信息 → 产品卖点（节点 description 字段）

---

## ⚠️ 关键变化

紧接 #2229(景区季节)/#2232(游玩项目季节)/#2096(节点 description 默认从资源 highlights 回填)。

**之前**: 节点 description 为空时, 后端自动回填 = 资源**本体** `highlights`。

**现在**: 节点 description 为空时, 后端自动回填 = **`scenic_season/activity_season.highlights`（按产品线 seasons[0]）→ 缺失再回落本体 `highlights`**。

**保持不变(独立快照)**: 节点 description **非空时不动**, 用户手填的卖点永远保留, 不会被后端覆盖。

---

## 一、回填规则

```
节点 description = (
  ( scenic_season.highlights WHERE scenic_id=X AND season_type=seasons[0] )   -- SCENIC 优先
  || scenic_spot.highlights                                                   -- SCENIC 回落
  || ( activity_season.highlights WHERE activity_id=X AND season_type=seasons[0] )  -- ACTIVITY 优先
  || activity.highlights                                                      -- ACTIVITY 回落
  || null
)
```

季节匹配规则: 取**产品对应产品线** `productLine.seasons[0]`(首个季节), 与封面切换 #2229/#2234 行为一致。

**触发时机**: 仅在保存行程节点(`POST /admin/product/item/{id}/itinerary` 或同等接口)且节点 `description` 为空或空白字符时触发。已经填了内容的节点保存时不会被覆盖。

---

## 二、影响接口

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存行程(快速理解节点) | POST/PUT | `/admin/product/item/{id}/itinerary` | 内部行为变更 | 入参/响应字段无变化 |
| 2 | 产品详情 | GET | `/admin/product/{id}` | 数据来源变化 | `itineraryDays[].nodes[].description` 字段值来源由"本体 highlights"改为"季节 highlights 优先" |
| 3 | 资源详情批量(internal) | POST | `/internal/resource/batch-details` | 入参加 `season` | 透明改动, 调用方为 product-v2 |

跨服务 DTO `ResourceDetailDTO` 新增字段 `seasonHighlights`(可为 null), 老消费者(Jackson 默认 ignore unknown)兼容, 不需要 redeploy。

---

## 三、契约约束 - 前端无需任何改动

- 保存接口 payload 字段不变
- 详情响应字段不变(还是 `description`)
- 前端在编辑页填的内容**永远不会**被后端覆盖, 不需要任何防御逻辑

---

## 四、边界行为

| 场景 | 节点 description 自动回填值 |
|------|----------------------------|
| productLine.seasons = `["spring"]` + 节点指向有 spring 配置的景区 + description 空 | `scenic_season(spring).highlights` |
| productLine.seasons = `["winter"]` + 节点指向**无 winter** 配置的景区 + description 空 | 回落 `scenic_spot.highlights`(本体) |
| productLine.seasons = null/`[]` + description 空 | 回落本体 `highlights`(向前兼容, 与 #2096 行为一致) |
| productLine.seasons = `["spring","autumn"]`(多季节) + description 空 | 取 seasons[0]=spring 匹配 |
| ACTIVITY 节点 | 同款行为(`activity_season.highlights` 优先, 回落 `activity.highlights`) |
| 节点 description **非空** | **不回填, 保留原值**(任何场景下都不覆盖) |
| 资源详情查询失败/异常 | 静默降级(`seasonHighlights=null`), 走本体 highlights |

---

## 五、老数据 / 存量产品

**重要**: 此次改动**不会**主动遍历存量产品节点修改 description。

- 老产品节点 description 都是非空的(之前由 #2096 用本体 highlights 自动填了, 或用户手填的) → 保存时不会触发回填, **看起来"季节回落没生效"是正常的**
- 想让某节点切换到季节亮点 → 编辑时**清空** description 文本框再保存即可
- 不接受"自动覆盖所有老节点"的需求(已与 wx 确认: 独立快照原则保护用户手填内容)

---

## 六、不影响范围

- **仅影响**: 管理端产品编辑保存时的节点 description 默认值
- **零影响**:
  - 小程序端 C 端展示(读 description 字段, 来源透明)
  - 订单创建/详情/退款
  - 算价 / 价格日历
  - 历史产品节点的现存 description 值

---

## 七、测试环境已验证

```
GET /admin/scenic/spots?page=1&pageSize=5&status=1  → 200 + seasons[] 字段 ✓ (PR #2229)
GET /admin/activity/items?page=1&pageSize=5         → 200 + seasons[] 字段 ✓ (PR #2232)
GET /admin/product/2045345825172639746              → 200 + itinerary.nodes[].description ✓
```

测试样本: 产品"游牧的森林-短途版"(`productLineId=2045004175602868226`, seasons=["autumn"])。3 个手填过 description 的 SCENIC 节点保持原值不变, 符合独立快照设计。

---

## 八、相关历史 PR

| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #2076 | #2068 | 资源 3 类 + 行程节点新增「图标」「卖点简介」2 字段 | ✅ 有效 |
| #2091 | #2090 | 复用 description 删冗余 sellingPoint | ✅ 有效 |
| #2096 | #2095 | 节点选资源时, 资源本体 highlights 默认带到节点 description | ✅ 有效, 被本 PR 增强 |
| #2229 | #2227 | 产品设计选景区按产品线季节过滤 + 节点 seasons 标签 | ✅ 有效 |
| #2234 | #2232 | 游玩项目同款季节过滤 + 标签 | ✅ 有效 |
| **本 PR #2250** | **#2243** | 节点 description 默认值由"本体 highlights"升级为"季节 highlights 优先 + 回落本体" | ✅ 最新 |

---

## 九、相关文档

- Issue: [wx/HL#2243](https://git.1814.love:8443/wx/HL/issues/2243)
- PR: [wx/HL#2250](https://git.1814.love:8443/wx/HL/pulls/2250)
