docs(product-v2): 操作日志中文化 + 字段级 diff (PR #2355)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot 2026-05-15 20:01:21 +08:00
父节点 330fc6e1a2
当前提交 d7be5c267e

查看文件

@ -0,0 +1,210 @@
# 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`(旧→新状态值已翻译为中文) |
**未接入的 actionchanges=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 字段全覆盖)、长文本摘要策略、集合字段 diffseasons/tags/tiers/days/feeItems
- **P2**:「还原到此版本」按钮、跨产品聚合查询页、调价超阈值钉钉/企微告警