# 订单投诉模块 - 小程序发起 + 后台只读列表

- **变更日期**: 2026-04-23
- **PR**: #1221（主） + #1222/#1223/#1225/#1235（配套 hotfix）
- **Closes**: #1220
- **影响面**: 小程序端（新增 5 接口） + 管理端（新增 2 接口 + 1 菜单）
- **兼容性**: **纯新增**，无既有接口变更

---

## 🎯 业务背景

客户在行程中任意一天（含"整程"）可发起投诉；后端自动通过**企业微信 + 短信**双链路通知订单负责的定制师（管理员）。管理后台侧边栏新增"投诉管理"一级菜单，定制师只读查看自己订单的投诉，超管/主管看全部。无审批流，客户本人可填/改解决备注。

---

## 🛒 小程序端接口（/mp/complaint/**）

统一前缀 `/mp/complaint`，userId 从网关注入的 token 取，前端无需传。

### 1. 发起投诉

```http
POST /mp/complaint/create
Authorization: Bearer <mp-token>
Content-Type: application/json

{
  "orderId": 99988,
  "dayNumber": 2,              // 0 或 null = "整程投诉"; 1..N = 第 N 天
  "types": ["ITINERARY", "VEHICLE"], // 字典 complaint_type: ITINERARY / VEHICLE / HOTEL / OTHER
  "content": "第二天酒店入住流程混乱...",  // 必填 1~500 字
  "imageUrls": ["https://oss/.../1.jpg"],  // ≤9,前端 OSS 直传后给后端 URL
  "videoUrl": "https://oss/.../1.mp4"      // 可空 ≤1
}

返回:
{"code":200,"data":{"complaintId":1024},"success":true}
```

**错误码**：
| Code | 含义 | 附加字段 |
|------|------|------|
| 20301 | 10 分钟冷却中（同一单同一天） | `remainingSeconds` |
| 20302 | 订单不属于当前用户 | — |
| 20303 | dayNumber 超出已发生范围 | `maxDayNumber` |

### 2. 撤回投诉（10 分钟内本人可）

```http
POST /mp/complaint/{id}/cancel
返回: {"code":200,"data":{"success":true}}

错误:
- 20305 非本人投诉
- 20306 已超 10 分钟
```

### 3. 我的投诉列表

```http
GET /mp/complaint/my-page?pageNo=1&pageSize=10

返回: Page<MpComplaintSimpleRespVO>:
- id / orderNo / groupCode
- dayNumber / dayLabel（"整程" 或 "第N天"）
- types: [{code, label}]
- contentSnippet（前 50 字）
- imageCount / videoCount
- isResolved（resolutionNote 非空）
- resolutionEditCount / createdAt
```

### 4. 投诉详情

```http
GET /mp/complaint/{id}

返回 MpComplaintRespVO（在 SimpleRespVO 基础上 + content/imageUrls/videoUrl/resolutionNote/resolutionUpdatedAt/canCancel/canEditResolution）

错误: 20304 不存在或已撤回 / 20305 非本人
```

### 5. 填/改解决备注（最多 2 次）

```http
PUT /mp/complaint/{id}/resolution
{
  "resolutionNote": "已与定制师沟通完毕"
}

返回: {"code":200,"data":{"editCountLeft":1}}

错误:
- 20305 非本人
- 20307 已达 2 次上限
```

---

## 🧑‍💼 管理端接口（/admin/complaint/**，只读）

### 1. 投诉列表（含权限过滤）

```http
GET /admin/complaint/page?pageNo=1&pageSize=10&keyword=&orderNo=&groupCode=&customizerNickname=&types=ITINERARY&isResolved=false&dateFrom=2026-04-01&dateTo=2026-04-23&dayScope=ALL

dayScope: ALL(默认) / WHOLE(整程=dayNumber=0) / SPECIFIC(具体天>=1)

返回 Page<ComplaintRespVO>:
- id / orderId / orderNo / groupCode
- customizerId / customizerNickname
- userId / userNickname
- contactName / contactPhoneMasked（列表脱敏后 4 位）
- dayNumber / dayLabel
- types / content / contentSnippet / imageUrls / videoUrl
- resolutionNote / resolutionEditCount / resolutionUpdatedAt / isResolved
- notifyWechatStatus / notifySmsStatus（PENDING/SUCCESS/FAIL/SKIP）
- status / createdAt / updatedAt
```

### 2. 投诉详情

```http
GET /admin/complaint/{id}

同 Page 返回结构 + contactPhoneDecrypted（明文，仅 complaint:detail 权限 + 非 :self 或本人订单才返）
```

### 3. 权限模型（R4）

| 按钮权限 | 效果 |
|---------|------|
| `complaint:list:self` | 定制师：强制 SQL WHERE customizer_id = currentAdminId |
| `complaint:list:all` | 超管/主管：无过滤 |
| `complaint:detail` | 查看详情（基础权限） |

已配菜单 + 角色绑定：
- SUPER_ADMIN(1) / ADMIN(2) → :all + :detail
- CUSTOMIZER(3) → :self + :detail

---

## 🔔 通知链路（后端自动，前端不关心）

投诉创建成功后 `afterCommit` → 异步双链路发给 `order.customizerId`：

- **企业微信**（已有 WechatMessageDTO + MQ 优先 / Feign 降级）：
  ```
  【客户投诉】
  团号：NM20260422001
  订单号：DD20260422001
  联系人：张三 13800138000
  第 2 天 · 行程/车
  内容：第二天酒店入住流程混乱（50字摘要）
  图片 3 视频 0
  ```
- **短信**（阿里云模板 `aliyun.sms.template.complaint-notify`，签名"呼籁旅行"）：
  ```
  【呼籁旅行】团号${groupCode}订单${orderNo}${day}收到客户投诉(${type})，请登录后台查看。
  ```
  ⚠️ 当前测试服 `SMS_MOCK_TBD`（不真发），运营去阿里云控制台申请模板后 Nacos 替换即生效。

---

## 📂 数据字典（已执行）

- `complaint_type`：ITINERARY=行程 / VEHICLE=车 / HOTEL=房 / OTHER=其他
- 菜单 `投诉管理`（1250 系列，侧边栏末尾压底）

---

## 🎨 前端对接清单

### 管理端（hl-ui admin）
- [ ] 侧边栏自动渲染"投诉管理"（后端 menu tree 已返回）
- [ ] 列表页：表格 + 筛选（6 条件 + dayScope）
- [ ] 详情页（只读）：图/视频预览 + 完整文字 + 解决备注（客人本人写的）+ 通知状态
- [ ] 字段精度：id/orderId 全部 Long 序列化为 String（防 JS 精度丢失，沿用项目全局 Jackson）
- [ ] 按钮权限：根据登录人 roleCodes 走 `complaint:list:self` 或 `:all` 两套菜单，无管理操作按钮

### 小程序端（hl-mp）
- [ ] 订单详情页"整程投诉"入口（dayNumber=null/0）
- [ ] 每日行程卡片"本日投诉"入口（dayNumber=具体天）
- [ ] 投诉表单：类型多选 + 文字（1~500）+ OSS 直传图片（≤9）/ 视频（≤1）
- [ ] 冷却拦截：20301 时展示"10 分钟后再试，剩余 {remainingSeconds}s"
- [ ] "我的投诉"列表 + 详情 + canCancel 显示撤回按钮 + canEditResolution 显示"填写解决备注"

---

## ⚠️ 注意

- 投诉表 `order_complaint` 已在测试服 `hl_order_service_v2` 建好（29 字段 + UNIQUE 防刷 + 4 索引）
- 企微通知依赖 `order.customizerId → admin_user.enterprise_wechat_id → wechat_user.mobile/name`，定制师未绑企微则自动 SKIP 短信/企微两路
- 同一用户 + 同一订单 + 同一 dayNumber 10 分钟只能一次（Redis SETNX + DB UNIQUE 双层兜底）

---

## 🚨 需要后端重启的服务（已完成，此处仅记录）

- hl-user-service（Feign: getAdminNotifyProfile + sendComplaintSms + 字典菜单）
- hl-order-service-v2（投诉主逻辑 + admin controller + internal api）
- hl-mp-service（MpComplaintController + Feign client + VO）
- hl-gateway（admin-complaint-service 路由）
