# product-v2: 产品快照 P2.3 — 保存放宽 + GROUP 班期完整 + 弹窗交互（mmg 必看）

> **服务**: hl-product-service-v2 (端口 8083)
> **PR**: #2478
> **Issue**: #2470
> **日期**: 2026-05-18
> **影响范围**: 产品快照保存权限/状态放宽 + GROUP 类产品班期完整保存 + 提交上架/完成设计 UI 加保存版本弹窗

---

## ⚠️ 关键变化（前端 mmg 必看）

### 1. 保存版本权限：Service 层校验删除（但外层 @DataScope 仍生效）
- **旧**：Service 层 `checkPriceEditPermission` 校验 SUPER_ADMIN 或 CUSTOMIZER+owner
- **新**：Service 层校验**已删除**（saveSnapshot 不再调 checkPriceEditPermission）
- ⚠️ **实际效果（2026-05-18 测试服 e2e 验证）**：
  - SUPER_ADMIN：任何产品都能保存 ✅
  - CUSTOMIZER：**只能保存自己创建的产品**（外层 `@DataScope` 数据权限拦截非 owner 产品 → HTTP 403 "无操作权限"）
  - OPERATOR / 其他角色：受外层数据权限+菜单权限制约，**通常不能操作产品快照接口**
- **真实业务收益**：解锁了「定制师本人 + 该产品所属公司其他 CUSTOMIZER 同事」的多角色保存场景（之前只 SUPER_ADMIN 或单一 owner）。彻底"所有 admin 都能保存任意产品"**没达成**（@DataScope 拦着，符合业务安全考虑——CUSTOMIZER 不该备份别人的产品）
- 还原/删除/标记永久**仍保留 Service 层 checkPriceEditPermission 严格校验**

### 2. 状态校验彻底取消（PR #2487 → PR #2493 修正最终态）
- ~~P2.3 初版：`DRAFT / UNPUBLISHED / COMPLETED` 三种~~
- ~~PR #2487 (错的, 已被 #2493 推翻)：保存 = 备份 + 强制退草稿组合~~
- **最终态（PR #2493）**：**任意状态都可保存, 且保存动作绝不改产品状态**
  - PUBLISHED 产品保存 → 仍 PUBLISHED, mp 端**不下架**
  - UNPUBLISHED / 审批中产品保存 → 状态**保持不变**
  - DRAFT 产品保存 → 仍 DRAFT
- ⚠️ **退草稿只发生在「还原版本」(restoreSnapshot)**, 不在「保存版本」(saveSnapshot)
- mmg 弹窗文案修正:
  - 「保存版本」弹窗: **去掉**"保存后产品将退回草稿状态"的警告 (PR #2487 时让 mmg 加, 现在错的要去掉)
  - 「还原版本」弹窗: **保留**"还原后产品状态将变为草稿"警告 (这是真实行为)

### 3. GROUP 小蒙马班期完整保存（修复缺陷）
- **旧版 P2.2 漏存** `group_tour_batch` 表 → GROUP 还原后班期数据丢失
- **新版** ProductSnapshotPayload 加 `productType` + `groupTourBatches` 两字段，GROUP 产品快照含完整班期，还原可恢复

### 4. 还原时类型一致性校验
- 还原前校验 `payload.productType == 当前 product.productType`
- 跨类型还原（如 CORE 快照试图还原到 GROUP 产品）返 `SNAPSHOT_TYPE_MISMATCH (410128)`
- 老快照 `productType=null` 兼容放行（不强制）

---

## 🎨 前端弹窗交互（mmg 必做）

P2.3 用户体验目标：用户在「提交上架」或「完成设计」前，前端弹窗提示「是否保存当前版本」，避免上架后想还原但没快照。

### 交互流程

```
┌────────────────────────────────────┐
│ [提交上架] 按钮 / [完成设计] 按钮 │
└─────────────────┬──────────────────┘
                  │ 点击
                  ▼
┌────────────────────────────────────┐
│ 弹窗 "是否保存当前版本？"           │
│                                     │
│ 上架/完成设计后想要恢复到此版本     │
│ 必须先保存。                        │
│                                     │
│  [保存并提交]  [直接提交]  [取消]  │
└──┬──────────────┬──────────────┬───┘
   │              │              │
   │ 选保存并提交  │ 选直接提交    │ 选取消
   ▼              ▼              ▼
1. POST /admin/product/item/{id}/snapshots
   body: { name: "上架前自动备份-2026-05-18 14:30",
           description: "提交上架前自动保存",
           keepForever: false }
2. POST /admin/product/item/{id}/toggle-publish  → 直接 POST /toggle-publish
   或 /complete                                    或 /complete                  无操作
```

### 实现建议
- 弹窗组件可以**默认勾选「保存并提交」**（鼓励保存）
- 版本名可让用户填，也可前端自动生成（如 `"上架前自动备份-${yyyy-MM-dd HH:mm}"`）
- 描述选填，留空自动填触发动作（如 `"提交上架前自动保存"` / `"完成设计前自动保存"`）
- 用户选「直接提交」时也可以记录到操作日志，便于审计

### 触发时机的接口
| 触发动作 | URL | Method |
|---|---|---|
| 提交上架 | `/admin/product/item/{id}/toggle-publish` | POST |
| 完成设计（仅 CUSTOM） | `/admin/product/item/{id}/complete` | POST |
| 保存快照 | `/admin/product/item/{id}/snapshots` | POST |

---

## 一、变更接口清单

| # | 接口 | 方法 | 路径 | 变更 |
|---|------|------|------|------|
| 1 | 保存快照 | POST | `/admin/product/item/{id}/snapshots` | **权限放宽** + **状态扩** + **GROUP 含班期** |
| 2 | 还原快照 | POST | `/admin/product/item/{id}/snapshots/{snapshotId}/restore` | **加类型一致性校验** |

---

## 二、接口详情

### 1. 保存快照（权限+状态放宽）

**入参 VO**（无变化）：name + description + keepForever，详见 PR #2431 changelog。

**校验变化**：
| 校验项 | 旧 | 新 |
|---|---|---|
| 角色 | SUPER_ADMIN 或 CUSTOMIZER+owner | **所有 admin 登录态** |
| 状态 | DRAFT / COMPLETED | **DRAFT / UNPUBLISHED / COMPLETED** |
| 同名 | 拒绝 | 同 |
| 大小 | >1MB 拒绝 | 同 |
| keep_forever 上限 | 3 个/产品 | 同 |

**出参**：`Result<Long>` (snapshotId) 无变化

### 2. 还原快照（加类型一致性校验）

**新校验**：快照保存时记的 `productType` 必须等于当前产品的 `productType`

**新错误码**：`410128 SNAPSHOT_TYPE_MISMATCH` — `"快照类型与当前产品不一致, 快照=CORE, 当前=GROUP"`

**老快照兼容**：PR #2431 时代保存的快照 `productType=null`，还原时**放行**（不强制校验）

---

## 三、新增字段（payload 结构变化）

`ProductSnapshotPayload` JSON 结构新增 2 字段：

```json
{
  "schemaVersion": 1,
  "productType": "GROUP",                       // ⬅ 新增, 用于还原时一致性校验
  "product": { ... },
  "basic": { ... },
  "supplement": { ... },
  ...
  "groupTourBatches": [                          // ⬅ 新增, GROUP 才有非空, CORE/CUSTOM 为 []
    { "batchId": "...", "departureDate": "2026-07-15", "adultSellPrice": 3800.00, ... }
  ]
}
```

前端**不需要解析这个 JSON**（在 OSS 上 + 后端透明处理），仅需知道：
- 新版 GROUP 快照含完整班期信息
- 还原可能返 `410128` 类型不一致错误

---

## 四、向后兼容

- ✅ 接口路径、入参、HTTP 方法、原有出参字段全部不变
- ✅ 老 payload（productType=null）反序列化兼容（已加 @JsonIgnoreProperties(ignoreUnknown=true)）
- ✅ 老快照还原时不强制类型校验
- ✅ 前端不实施弹窗也不影响（只是少了 UX 改进）

---

## 五、部署

- 测试服：自动同步 dev 部署
- 正式服：等 Deploy Panel /prod 触发
- nacos: 无需配置（OSS 配置 PR #2435 已对齐全局 oss.*）

---

## 六、不在本期范围

- 集合深 diff（P2.1 #2428 单独跟）
- 调价告警（用户已决策不做）
- 跨产品聚合可视化页（P2-4 #2426，mmg 前端工作）
