# product-v2: 产品操作日志全中文化 + 字段级 diff（旧值→新值）

> **服务**: hl-product-service-v2 (端口 8083)
> **PR**: #2355
> **Issue**: #2354
> **日期**: 2026-05-15
> **影响范围**: 管理端「产品 → 操作日志」侧边栏（admin.1814.love `/product/edit?id=X` 右侧抽屉）

---

## ⚠️ 关键变化

`GET /admin/product/item/{id}/operation-logs` **出参新增 3 字段**（`actionLabel` / `stepLabel` / `changes`），接口路径、入参、HTTP 方法**完全没变**。

- 旧逻辑（mmg 现在）：把 `action="EDIT"` / `step="STEP1"` 当字符串裸渲染，用户看不懂
- 新逻辑：直接读 `actionLabel="编辑"` / `stepLabel="基础信息"` 即可；新增 `changes` 数组渲染字段差异表格
- **老日志兼容**：2026-05-15 之前的历史日志 `changes` 字段为 `null`，前端**不渲染 diff 区块**，只显示 `actionLabel + detail`（向后兼容）

---

## 一、背景

张老板 2026-05-15 反馈：产品 `2055138817265897473` 改价后无法追溯具体金额变化。
- 旧版日志只记 `detail="价格区间设置 2026-07-12~2026-07-19"`，金额变化要靠 RDS binlog 还原（1 小时排查）
- 20 种 action 落库为英文 `EDIT/PRICE_SET/COMPLETE`，前端裸出导致每次都要询问含义
- `operatorName` 列存在但 Service 没赋值，所有日志显示 null

P0 解决：action 中文化 + 字段级 diff（前端表格可直接展示「成人售价 5800 → 9400」）。

---

## 二、变更接口清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查产品操作日志 | GET | `/admin/product/item/{id}/operation-logs` | **出参 EXTEND** | 新增 actionLabel / stepLabel / changes 三字段 |

---

## 三、接口详情

### 1. 查产品操作日志 `GET /admin/product/item/{id}/operation-logs`

**VO**: `ProductOperationLogRespVO`

#### 入参（无变化）

| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| id | Path | Long | ✅ | productId |

#### 出参（新增 3 字段，加粗为新）

```json
{
  "code": 200,
  "data": [
    {
      "id": "2055231522046218242",
      "action": "EDIT",
      "actionLabel": "编辑",
      "step": "STEP1",
      "stepLabel": "基础信息",
      "detail": "基础信息",
      "operatorId": "2033350100000000006",
      "operatorName": "wangyu",
      "createTime": "2026-05-15 18:19:14",
      "changes": [
        {
          "field": "name",
          "fieldLabel": "产品名称",
          "oldValue": "Tina空想2大1小 私人定制线路",
          "newValue": "Tina空想2大1小 私人定制线路 8天7晚",
          "changeType": "MODIFY"
        },
        {
          "field": "depositAmount",
          "fieldLabel": "订金金额",
          "oldValue": "1000.00",
          "newValue": "1500.00",
          "changeType": "MODIFY"
        }
      ]
    },
    {
      "id": "2055222984284573697",
      "action": "PRICE_SET",
      "actionLabel": "设置价格",
      "step": "STEP4",
      "stepLabel": "价格与班期",
      "detail": "价格区间设置 2026-07-12~2026-07-19",
      "operatorId": "2033350100000000006",
      "operatorName": "wangyu",
      "createTime": "2026-05-15 12:01:59",
      "changes": [
        {
          "field": "adultSellPrice",
          "fieldLabel": "成人售价",
          "oldValue": "5800.00",
          "newValue": "9400.00",
          "changeType": "MODIFY"
        },
        {
          "field": "childSellPrice",
          "fieldLabel": "儿童售价",
          "oldValue": "3000.00",
          "newValue": "3980.00",
          "changeType": "MODIFY"
        }
      ]
    },
    {
      "id": "2055216483381071873",
      "action": "COMPLETE",
      "actionLabel": "完成设计",
      "step": null,
      "stepLabel": null,
      "detail": "",
      "operatorId": "2033350100000000006",
      "operatorName": "wangyu",
      "createTime": "2026-05-15 17:43:16",
      "changes": null
    }
  ]
}
```

#### `changes` 字段定义

| 字段 | 类型 | 说明 |
|------|------|------|
| `changes` | `Array<FieldChange> \| null` | 字段差异列表；老日志为 `null` 时不渲染 |
| `changes[].field` | `string` | 字段英文名（用于排序/筛选/调试） |
| `changes[].fieldLabel` | `string` | **字段中文名（前端直接展示这个）** |
| `changes[].oldValue` | `string \| number \| null` | 旧值（已转中文：枚举值如 `FULL` 已翻译为 `"全款"`） |
| `changes[].newValue` | `string \| number \| null` | 新值 |
| `changes[].changeType` | `"ADD" \| "MODIFY" \| "DELETE"` | ADD=新增（oldValue=null）/MODIFY=修改/DELETE=删除（newValue=null） |

#### 本期接入字段级 diff 的 3 类操作

| action / step | 接入字段（5 个/类） |
|--------------|------|
| `PRICE_SET` / `BATCH_SAVE`（STEP4） | `adultSellPrice` / `childSellPrice` / `toddlerDiscount` / `dailyStock` / `priceType` |
| `EDIT` STEP1 | `name` / `subtitle` / `paymentType` / `depositAmount` / `defaultDailyStock` |
| 状态变更（`SUBMIT_PUBLISH`/`APPROVE`/`REJECT` 等） | `status`（旧→新状态值已翻译为中文） |

**未接入的 action（changes=null，按老日志渲染）**：CREATE / EDIT STEP2-5 / COPY / DELETE / EDIT_MAP_AUTO / EDIT_MAP_MODE / SAVE_DETAIL_BLOCKS / TEAM_SAVE 等。
**P1 后续**会全字段 diff 覆盖 STEP2-5 + 长文本摘要 + 集合 diff，单独发 changelog 通知。

---

## 四、前端改动建议（mmg）

### 1. 日志侧边栏渲染逻辑（伪代码）

```jsx
{logs.map(log => (
  <Timeline.Item>
    <div className="log-header">
      <span className="action-tag">{log.actionLabel}</span>
      {log.stepLabel && <span className="step-tag">{log.stepLabel}</span>}
      <span className="time">{log.createTime}</span>
      <span className="operator">{log.operatorName ?? '系统'}</span>
    </div>
    {log.detail && <div className="detail">{log.detail}</div>}
    {log.changes && log.changes.length > 0 && (
      <table className="diff-table">
        <thead><tr><th>字段</th><th>旧值</th><th>新值</th></tr></thead>
        <tbody>
          {log.changes.map(c => (
            <tr key={c.field}>
              <td>{c.fieldLabel}</td>
              <td className="old">{c.oldValue ?? '—'}</td>
              <td className="new">{c.newValue ?? '—'}</td>
            </tr>
          ))}
        </tbody>
      </table>
    )}
  </Timeline.Item>
))}
```

### 2. action 颜色建议（前端可自行决定）

| 类别 | 取值 | 建议色 |
|------|------|--------|
| 价格类 | PRICE_SET / PRICE_DELETE / BATCH_SAVE / ADJUST_PRICE | 橙 |
| 状态类 | PUBLISH / SUBMIT_PUBLISH / APPROVE / DIRECT_PUBLISH | 绿 |
| 状态类（负向） | REJECT / WITHDRAW / UNPUBLISH / FORCE_UNPUBLISH / DELETE | 红 |
| 编辑类 | EDIT / EDIT_MAP_AUTO / EDIT_MAP_MODE | 蓝 |
| 其它 | CREATE / COPY / COMPLETE / TRANSFER / TEAM_SAVE | 灰 |

---

## 五、向后兼容性

- ✅ 接口路径、入参、HTTP 方法、原有出参字段全部不变
- ✅ 老日志 `changes=null`，前端 `if(changes)` 守卫即可
- ✅ 后端可独立先行发布（前端不改也不会出错，只是不显示新功能）
- ✅ 新日志的 actionLabel/stepLabel 即使前端不读也不影响

**部署顺序**：Flyway 先行 → 后端 → 前端（任意顺序都安全）。

---

## 六、不在本期范围（P1/P2 后续单独工单）

- **P1**：全字段 diff（>100 字段全覆盖）、长文本摘要策略、集合字段 diff（seasons/tags/tiers/days/feeItems）
- **P2**：「还原到此版本」按钮、跨产品聚合查询页、调价超阈值钉钉/企微告警
