# feat(评价): 已通过↔已拒绝评价可互相切换(必填理由) — admin 端新增反转按钮

> **类型**: feat (后端 + 前端均需改动)
> **关联 PR/Issue**: PR #2985 (Closes #2983) 已合 dev,测试服已部署
> **日期**: 2026-05-25
> **影响范围**: admin 端「评价审核」(详情/列表),小程序端无影响
> **接收方**: mmg
> **前端**: **admin 端需改造**(已通过/已拒绝状态新增反转按钮 + 必填理由弹窗)

---

## 🎯 背景

现状管理员审核错了不能回退 —— APPROVED/REJECTED 都是终态。本次:
- 已通过的评价可改为已拒绝
- 已拒绝的评价可改为已通过
- **互转必填理由**(写入新字段 `revertRemark` 审计)
- **机审拒绝(MACHINE_REJECTED)不在本次范围**,仍走原有「覆盖通过」按钮 → `/override-approve`(不变)

---

## 📊 新增 1 个接口 `POST /admin/review/{reviewId}/revert`

### Request body

```json
{
  "targetStatus": "APPROVED",         // 必填,枚举 APPROVED / REJECTED
  "remark": "经核实评价内容真实,改为通过"  // 必填,1-500 字
}
```

### Response

成功:`{ "code": 200, "message": "成功", "data": null }`

失败:
| 场景 | code | message |
|------|------|---------|
| remark 空 | 400 | 反转备注不能为空 |
| targetStatus 不是 APPROVED/REJECTED | 400 | 目标状态仅支持 APPROVED 或 REJECTED |
| 当前状态非 APPROVED/REJECTED | 551076 | 评价状态不允许反转,仅 APPROVED ↔ REJECTED 可互转 |
| targetStatus 与当前相同 | 551076 | 同上 |
| 评价不存在 | 404(NotFoundException) | 评价不存在 |

### 允许的状态转换

| 当前 | targetStatus | 结果 |
|------|--------------|------|
| APPROVED | REJECTED | ✓ 走 ADMIN_REVERT_TO_REJECT |
| REJECTED | APPROVED | ✓ 走 ADMIN_REVERT_TO_APPROVE |
| PENDING_MANUAL | * | ✗ 仍走 `/approve` 或 `/reject` |
| MACHINE_REJECTED | * | ✗ 仍走 `/override-approve` |
| PENDING_MODERATION | * | ✗ 机审中,不操作 |
| APPROVED | APPROVED | ✗ 同状态 |

---

## 📊 现有 3 个接口完全不动

| 接口 | 允许态 | body | 说明 |
|------|--------|------|------|
| `/approve` | PENDING_MANUAL → APPROVED | 无 | 维持现状 |
| `/reject` | PENDING_MANUAL → REJECTED | rejectReason 必填 | 维持现状 |
| `/override-approve` | MACHINE_REJECTED → APPROVED | 无 | 维持现状 |

---

## 📊 VO 新增字段 `revertRemark`

`ReviewVO`(详情)和 `ReviewListVO`(列表)新增:

```diff
{
  "reviewId": 100001,
  "status": "REJECTED",
  "rejectReason": "原始拒绝原因",
+ "revertRemark": "经核实评价内容真实,改为通过"   // 反转备注(可能 null)
}
```

**`MpReviewVO` 不加**(小程序无需展示)。

`rejectReason` 字段语义保持「PENDING_MANUAL → REJECTED 时的初次拒绝原因」,不动。`revertRemark` 是「终态反转操作的备注」,独立字段。

---

## 🎨 admin 前端期望改造

### 按钮联动

当前状态 → 显示的操作按钮:

| status | 显示按钮 |
|--------|---------|
| PENDING_MANUAL | 「通过」(`/approve`) / 「拒绝」(`/reject` 必填 rejectReason) |
| MACHINE_REJECTED | 「覆盖通过」(`/override-approve`) — **不变** |
| APPROVED | **新增**「改为拒绝」按钮 → 必填理由弹窗 → `/revert {REJECTED, remark}` |
| REJECTED | **新增**「改为通过」按钮 → 必填理由弹窗 → `/revert {APPROVED, remark}` |
| PENDING_MODERATION | 无操作按钮(机审中) |

### 详情展示

如果 `revertRemark` 非空,展示「最近一次反转备注:xxx(由 xxx 于 xxx 时间操作)」。审计字段 `auditedAt` / `auditorName` 现有接口已返回。

---

## ✅ 后端验证记录

### 本地 (gateway 8080, order-v2 8094)

```
[1] APPROVED→REJECTED        code=200  DB: REJECTED + revert_remark 写入
[2] REJECTED→APPROVED        code=200  DB: APPROVED + revert_remark 更新
[3] 缺 remark                code=400  Bean Validation
[4] APPROVED→APPROVED        code=551076
[5] PENDING_MANUAL→APPROVED  code=551076
GET /admin/review/list       响应字段含 revertRemark
```

### 测试服 (api.test.1814.love:9443)

```
[1] APPROVED→REJECTED  code=200 msg=成功
[2] REJECTED→APPROVED  code=200 msg=成功
[3] APPROVED→APPROVED  code=551076 msg=评价状态不允许反转,仅 APPROVED ↔ REJECTED 可互转
[4] 缺 remark          code=400 msg=反转备注不能为空(Bean Validation)
[5] GET /admin/review/list  响应字段含 revertRemark ✓
```

---

## 🔒 业务规则细节

1. **反转不联动定制师评价**:产品评价反转时,**不会**自动同步 PENDING_MANUAL 状态的定制师评价(避免反向操作意外波及)。`/approve` 仍保留 cascadeApproveCustomizerReviews 联动逻辑。
2. **精选评价缓存**:反转后清 `featuredReviewCache`,防止已拒绝的评价仍在精选中显示。
3. **审计字段**:反转会更新 `auditorAdminId` / `auditorName` / `auditedAt` 为最新操作人,**不更新** `rejectReason`(语义保留)。

---

## 🗂️ Schema

```sql
ALTER TABLE `order_review`
ADD COLUMN `revert_remark` VARCHAR(500) NULL COMMENT '终态反转备注(APPROVED↔REJECTED 互转时填)';
```

Flyway: `V20260525_002__add_revert_remark_to_order_review.sql`,测试服已落库 success=1。
