---
date: 2026-05-12
type: backend-fix
service: hl-product-service-v2
priority: high
notify: ["@mmg"]
status: verified
---

# 行程编辑「时间段」等可清空字段清空保存后未生效

> **PR**: [#2122](https://git.1814.love:8443/wx/HL/pulls/2122) — 已合并 dev (squash `afeff93`) + 部署测试服 (Deploy Panel 任务 `2c62aa31`)
> **Issue**: [#2119](https://git.1814.love:8443/wx/HL/issues/2119)
> **接口**: `PUT /admin/product/item/{id}/itinerary` — 保存整套行程
> **状态**: ✅ **测试服已 round-trip 验证** — 产品 `2045345825172639746` 真测

---

## 是否破坏性 API 变更

**否**。请求/响应字段、类型、嵌套层数全部不变,仅修复 UPDATE 时 null 字段被默认策略跳过导致旧值残留的 bug。

## 影响范围

`PUT /admin/product/item/{id}/itinerary` 的 itinerary 节点 / Day 共 25 个可清空字段, 在前端置 null 后, DB 现在会真正写 NULL(此前 MyBatis-Plus 默认 `FieldStrategy.NOT_NULL` 跳过 null → 旧值残留)。

### `product_itinerary_node` (9 个字段加 `@TableField(updateStrategy = FieldStrategy.IGNORED)`)
`timePeriod` / `startTime` / `durationMinutes` / `description` / `emojiIcon` / `extraData` / `resourceId` / `resourceType` / `resourceName` / `images`

### `product_itinerary_day` (16 个字段加 IGNORED)
`dayTitle` / `quoteText` / `description` / `coverImageUrl` / `breakfast` / `lunch` / `dinner` / `diningRemark` / `gatherPlace` / `dismissalPlace` / `dailyMileage` / `dailyDuration` / `mileageManualOverride` / `photoUrls` / `customFields`

> 关键关联字段(`productId` / `dayId` / `sortOrder` / `nodeName` / `nodeType`)**保留默认 NOT_NULL**, 避免误清关联。

## 前端 @mmg 无需改动

接口契约不变, 前端继续按现有 contract 提交 null/空数组即可, 后端会真正写 NULL。

## 测试服真测 (硬性凭证)

产品 `2045345825172639746` DAY 2 黑山头节点, 三阶段 round-trip(update_time 跨阶段递增 17:35:33 → 17:36:30 → 17:36:35):

| 阶段 | timePeriod | breakfast | update_time |
|------|-----------|-----------|-------------|
| 起始 | EARLY_MORNING | HOTEL | 17:35:33 |
| 置 null 保存 + 重查 | NULL ✓ | NULL ✓ | 17:36:30 |
| 还原 EARLY_MORNING / HOTEL | EARLY_MORNING | HOTEL | 17:36:35 |

现场已恢复, 无遗留数据污染。

## 风险

极小。仅是 UPDATE 时把 null 真正下发, 不影响新增 / 查询 / 关联字段。已加反射单测断言每个 IGNORED 字段带注解, 防回归。
