# product-v2: 产品全量快照 — 按需保存 + 一键还原（新功能）

> **服务**: hl-product-service-v2 (端口 8083)
> **PR**: #2431
> **Issue**: #2425
> **日期**: 2026-05-16
> **影响范围**: 产品编辑页右侧新增「版本管理」入口（保存/列表/还原/删除/标记永久）

---

## ⚠️ 关键新功能（mmg 前端需新增 UI）

产品支持「按需手动保存版本」+「一键还原到任意历史版本」。**这是个全新模块，前端需要新增 3 个组件**：

1. **保存版本按钮**（产品编辑页右上角）
2. **版本列表页/抽屉**（点按钮看历史版本）
3. **还原对话框**（必填还原原因 ≥10 字符）

---

## 一、设计要点

| 项 | 设计 |
|---|---|
| 触发方式 | **按需手动**（不是每次 EDIT 自动） |
| 存储位置 | 阿里云 OSS（`hlgl-test` bucket，prefix `hl-product-snapshots/`） |
| 压缩 | gzip（典型 40KB JSON 压到 8-10KB） |
| 版本上限 | 每产品最多 10 个；keep_forever 标记每产品最多 3 个 |
| 超阈值 | 自动删最老的非 keep_forever 快照 |
| 还原后 | 状态强制 DRAFT（避免与审批/上架冲突），保留 id/created_by/createdAt |
| 权限 | SUPER_ADMIN 或 CUSTOMIZER+owner（复用 P2 改价权限校验） |

---

## 二、5 个新接口

### 1. 保存快照
`POST /admin/product/item/{productId}/snapshots`

入参 VO `ProductSnapshotSaveReqVO`:
| 字段 | 类型 | 必填 | 校验 |
|---|---|---|---|
| name | String | ✅ | ≤64 字符，同产品下唯一 |
| description | String | ❌ | ≤255 字符 |
| keepForever | Boolean | ❌ | 默认 false |

出参：`Result<Long>` (snapshotId)

**错误码**：
- `410120` SNAPSHOT_NOT_FOUND
- `410121` SNAPSHOT_INVALID_STATE（仅 DRAFT/COMPLETED 可保存）
- `410122` SNAPSHOT_NAME_DUPLICATE
- `410123` SNAPSHOT_SIZE_EXCEED（序列化 >1MB 拒绝）
- `410124` SNAPSHOT_KEEP_FOREVER_LIMIT（>3 个 keep_forever 拒绝）
- `410115` PRICE_EDIT_PERMISSION_DENIED（复用，非 SUPER_ADMIN/CUSTOMIZER+owner）
- `510100` SNAPSHOT_OSS_FAIL（OSS 服务端错误）

### 2. 列表
`GET /admin/product/item/{productId}/snapshots?page=1&size=20`

出参（分页）：
```json
{
  "code": 200,
  "data": {
    "list": [{
      "id": "...",
      "productId": "...",
      "name": "上架前最终版",
      "description": "改价前备份",
      "fileSize": 9876,
      "keepForever": true,
      "createdBy": "...",
      "createdByName": "wangyu",
      "createdAt": "2026-05-16 17:23:11"
    }],
    "total": 5,
    "pageNum": 1,
    "pageSize": 20
  }
}
```

### 3. 还原
`POST /admin/product/item/{productId}/snapshots/{snapshotId}/restore`

入参 VO `ProductSnapshotRestoreReqVO`:
| 字段 | 必填 | 校验 |
|---|---|---|
| reason | ✅ | ≥10 字符 |

**还原后效果**：
- 产品全字段 + 所有子表（priceCalendars/days/dayNodes/dayHotels/feeItems/supplies/tiers/...）恢复到快照时点
- 产品状态强制设回 `DRAFT`
- 产品 id / created_by / createdAt 保持不变
- 自动写一条 `RESTORE` 操作日志，detail = `"还原至 {name} ({createdAt}), 原因: {reason}"`

**错误码**：
- `410125` SNAPSHOT_RESTORE_PUBLISHED（已上架不能还原，必须先 UNPUBLISH）
- `410126` SNAPSHOT_RESTORE_HAS_ORDER（有报名班期不能还原）

### 4. 删除
`DELETE /admin/product/item/{productId}/snapshots/{snapshotId}`

软删 MySQL + 异步删 OSS 对象（OSS 删失败 log.warn 不阻塞）。

### 5. 标记/取消永久保留
`PUT /admin/product/item/{productId}/snapshots/{snapshotId}/keep-forever?keepForever=true`

切换 keep_forever 标记。设 true 时校验 keep_forever 总数 < 3。

---

## 三、前端 UI 建议（mmg）

### 保存按钮（编辑页右上角）

```jsx
<Button
  type="primary"
  icon={<SaveOutlined />}
  disabled={!isDraft && !isCompleted}  // PUBLISHED/审批中禁用
  onClick={openSaveDialog}
>
  保存版本
</Button>
```

保存对话框：
- 输入版本名（如"上架前最终版" / "调价前备份"）
- 选填描述
- 复选框「永久保留（不被阈值清理）」

### 版本列表（侧边抽屉）

| 版本名 | 描述 | 大小 | 创建人 | 时间 | 操作 |
|---|---|---|---|---|---|
| 上架前最终版 📌 | 改价前备份 | 9.6 KB | 王宇 | 17:23 | 还原 / 删除 / 取消📌 |
| 改 6 月价之前 | — | 8.2 KB | 王宇 | 16:01 | 还原 / 删除 / 📌 |

📌 = keep_forever 标记，不会被阈值清理删除。

### 还原对话框

```
⚠️ 还原到「上架前最终版」(2026-05-16 17:23)

还原后：
- 产品所有字段会回到该版本（包括价格、行程、备品等）
- 产品状态会强制设回"草稿"
- 操作日志会记录一条 RESTORE

还原原因（必填，≥10 字）:
[                                       ]

[ 确认还原 ]  [ 取消 ]
```

---

## 四、部署前置（运维必做）

合并部署前 **nacos 必须配 OSS 凭据**，否则保存/还原接口返 `SNAPSHOT_OSS_FAIL`：

```yaml
# hl-common-{dev,test,prod}.yml
product:
  snapshot:
    oss:
      access-key-id: <AK>
      access-key-secret: <SK>
      bucket: hlgl-test                            # 或新建专用 bucket
      endpoint: oss-cn-beijing.aliyuncs.com        # 已有默认值
      key-prefix: hl-product-snapshots             # 已有默认值
    max-per-product: 10                            # 已有默认值
    max-keep-forever-per-product: 3                # 已有默认值
    max-size-bytes: 1048576                        # 已有默认值
```

---

## 五、向后兼容

- ✅ 新增功能，不影响任何现有接口
- ✅ Flyway V20260516_004 只建新表 product_snapshot
- ✅ 老日志/老产品完全不受影响
- ✅ 前端不实施新 UI 也不会出错（只是不显示新功能）

---

## 六、不在本期范围（P2.1 #2428 + P2-4 #2426）

- 集合深 diff（dayHotels 子项变化）— P2.1 #2428
- 跨产品聚合可视化页 — P2-4 #2426
