feat: 小程序配置模块重构(联系我们/常见问题/协议政策)- PR #773

这个提交包含在:
API Changelog Bot 2026-04-17 22:41:43 +08:00
父节点 a1a6a3ca01
当前提交 f0f2cd8fe7

查看文件

@ -0,0 +1,300 @@
# 小程序配置模块重构:联系我们 / 常见问题 / 协议政策
> **服务**: hl-user-service (端口 8081)
> **PR**: https://git.1814.love:8443/wx/HL/pulls/773
> **Issue**: https://git.1814.love:8443/wx/HL/issues/766
> **日期**: 2026-04-17
> **影响范围**: 管理端三大配置模块(联系我们 / 常见问题 / 协议政策)的所有写接口和大部分读接口
---
## 背景
小程序配置模块历史遗留问题较多:
- Status 字段混用 Integer(0/1) 与 String"启用/停用"
- VO 命名不统一:`FaqCategoryVO` / `AgreementRequest` 等不符合团队规范
- 协议政策每类型应只有一条记录,但历史接口允许多记录 CRUD,数据可能出现同类型多条
- 没有字典校验 / 条件必填 / 幂等 / 富文本 XSS 过滤,业务错常见 500
本次整改按团队规范重构三个模块,统一 VO 命名、字段类型、错误码、安全策略。
## 字段统一变更(全三模块)
### Status 字段Integer → String
| 旧值 | 新值 |
|------|------|
| `1` | `"ACTIVE"` |
| `0` | `"INACTIVE"` |
影响所有请求体 / 响应体中的 `status` / `enabled` 字段,前端请求与展示都需切换成大写英文常量。
数据库迁移由 DDL 完成(幂等 SQL,已在 PR 中附带)。
### 业务错误码500 → 400
字典值非法、条件校验失败、资源不存在等业务错从 `code=500` 改为 `code=400`,并附中文消息。HTTP 始终 200,前端按 `code` 区分。
### 写接口全部加 @Idempotent
9 个写接口启用幂等控制3~5 秒窗口),前端连点 / 重试会被返:
```json
{
"code": 429,
"success": false,
"message": "请勿重复提交"
}
```
改 body 后可再次提交。
### 富文本 XSS 过滤(后端自动处理)
以下字段提交时后端自动清洗 `<script>` / `<iframe>` / `on*` 事件属性等危险标签:
- `Contact.content`
- `FaqItem.answer`
- `Agreement.content`
前端展示时可直接 `v-html` 渲染,无需额外过滤。
---
## Contact联系我们
### 字段重命名
| 旧字段 | 新字段 | 说明 |
|--------|--------|------|
| `title` | `name` | 渠道名称 |
| `icon` | `iconUrl` | 图标 URL |
### 字典校验 `channelType`
| 值 | 说明 | 必填的关联字段 |
|----|------|----------------|
| `ABOUT` | 关于我们 | `content`(富文本介绍)必填 |
| `ONLINE_CS` | 在线客服 | `value`(链接/账号)必填 |
| `PHONE` | 客服电话 | `value`(电话号码)必填 |
非上述 3 值 → `code=400` + "渠道类型不支持"。
### 接口清单
#### 管理端
| 方法 | 路径 | 说明 | 变更 |
|------|------|------|------|
| POST | `/admin/contact` | 创建 | **Body 结构变更**:原 `ContactRequest` 废弃,改为 `ContactSaveReqVO`(字段见下) |
| PUT | `/admin/contact/{id}` | 修改 | 同上,Body 用 `ContactSaveReqVO`id 由路径传) |
| DELETE | `/admin/contact/{id}` | 删除 | 签名不变 |
| GET | `/admin/contact/{id}` | 详情 | 响应用 `ContactRespVO`(字段重命名) |
| GET | `/admin/contact/page` | 分页 | 响应项为 `ContactRespVO` |
| GET | `/admin/contact/channel-types/enabled` | **新增**:渠道类型下拉 | `List<Map<value,label>>` |
#### `ContactSaveReqVO`(请求)
```json
{
"channelType": "ABOUT", // 必填。ABOUT / ONLINE_CS / PHONE
"name": "关于呼籁", // 必填。旧名 title
"iconUrl": "https://...", // 选填。旧名 icon
"value": null, // ABOUT 可空;ONLINE_CS/PHONE 必填
"content": "<p>...</p>", // ABOUT 必填;其他可空。富文本 XSS 过滤
"sortOrder": 1, // 选填
"status": "ACTIVE" // 选填,默认 ACTIVE
}
```
#### `ContactRespVO`(响应)
```json
{
"id": 1,
"channelType": "ABOUT",
"name": "关于呼籁", // 旧名 title
"iconUrl": "https://...", // 旧名 icon
"value": null,
"content": "<p>...</p>",
"sortOrder": 1,
"status": "ACTIVE", // 旧为 Integer 0/1
"createTime": "2026-04-17 10:00:00",
"updateTime": "2026-04-17 10:00:00"
}
```
---
## Faq常见问题
### 分类Category
| 方法 | 路径 | 说明 | 变更 |
|------|------|------|------|
| POST | `/admin/faq/categories` | 创建分类 | Body 改为 `FaqCategorySaveReqVO`(原 `FaqCategoryRequest` 废弃) |
| PUT | `/admin/faq/categories/{id}` | 修改分类 | 同上 |
| DELETE | `/admin/faq/categories/{id}` | 删除分类 | **新增业务校验**:分类下有条目时 `code=400` + "该分类下仍有常见问题,无法删除" |
| GET | `/admin/faq/categories/{id}` | 详情 | 响应 `FaqCategoryRespVO` |
| GET | `/admin/faq/categories/page` | 分页 | 响应 `FaqCategoryRespVO` |
| GET | `/admin/faq/categories/enabled` | **新增**:分类下拉 | `List<Map<value,label>>`,只返回 ACTIVE |
#### `FaqCategorySaveReqVO`
```json
{
"name": "预订须知",
"sortOrder": 1,
"status": "ACTIVE"
}
```
### 条目Item
| 方法 | 路径 | 说明 | 变更 |
|------|------|------|------|
| POST | `/admin/faq/items` | 创建条目 | Body 改为 `FaqItemSaveReqVO`(原 `FaqItemRequest` 废弃) |
| PUT | `/admin/faq/items/{id}` | 修改条目 | 同上 |
| DELETE | `/admin/faq/items/{id}` | 删除 | 签名不变 |
| GET | `/admin/faq/items/{id}` | 详情 | 响应 `FaqItemRespVO` |
| GET | `/admin/faq/items/page` | 分页 | 响应 `FaqItemRespVO` |
#### `FaqItemSaveReqVO`
```json
{
"categoryId": 1, // 必填,必须是存在且 ACTIVE 的分类
"question": "如何退订?",
"answer": "<p>...</p>", // 富文本,XSS 过滤
"sortOrder": 1,
"status": "ACTIVE"
}
```
> 若 `categoryId` 指向不存在 / 已停用的分类 → `code=400`
### VO 命名调整(前端 TypeScript 定义需要改)
| 旧类型 | 新类型 |
|--------|--------|
| `FaqCategoryVO` | `FaqCategoryRespVO` |
| `FaqItemVO` | `FaqItemRespVO` |
| `FaqCategoryRequest` | `FaqCategorySaveReqVO` |
| `FaqItemRequest` | `FaqItemSaveReqVO` |
---
## Agreement协议政策
**核心变化**:每个 `type` 只有一条记录saveByType 模式),废弃多记录 CRUD。
### 废弃接口(前端必须下线)
| 方法 | 路径 | 处理 |
|------|------|------|
| ~~POST~~ | ~~`/admin/agreement`~~ | **删除**。改用 `/save-by-type` |
| ~~PUT~~ | ~~`/admin/agreement/{id}`~~ | **删除**。改用 `/save-by-type` |
| ~~DELETE~~ | ~~`/admin/agreement/{id}`~~ | **删除**。协议不允许删除,只能停用或覆盖 |
### 新接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/admin/agreement/save-by-type` | 按类型保存:该 type 不存在则创建,存在则覆盖(幂等) |
| GET | `/admin/agreement/by-type/{type}` | 按类型查询,type 参数为字典值 |
| GET | `/admin/agreement/list` | 返回所有启用协议(简要列表) |
### 字典 `agreement_type`
| 值 | 说明 |
|----|------|
| `USER_AGREEMENT` | 用户协议 |
| `PRIVACY_POLICY` | 隐私政策 |
非上述两值 → `code=400` + "协议类型不支持"。
### 旧类型迁移DDL 完成)
| 旧值 | 新值 / 处理 |
|------|-------------|
| `user` | `USER_AGREEMENT` |
| `privacy` | `PRIVACY_POLICY` |
| `travel` | 软删(`deleted_at` 设置) |
| `refund` | 软删(`deleted_at` 设置) |
### `AgreementSaveReqVO`(请求)
```json
{
"type": "USER_AGREEMENT", // 必填。USER_AGREEMENT / PRIVACY_POLICY
"title": "用户协议", // 必填
"content": "<p>...</p>", // 必填,XSS 过滤
"updateDate": "2026-04-17", // 必填,LocalDate
"sortOrder": 1 // 选填
}
```
> 原 `AgreementRequest` 废弃。
### `AgreementRespVO`(响应)
```json
{
"id": 1,
"type": "USER_AGREEMENT",
"title": "用户协议",
"content": "<p>...</p>",
"updateDate": "2026-04-17", // 新增字段LocalDate
"sortOrder": 1,
"status": "ACTIVE",
"createTime": "2026-04-17 10:00:00",
"updateTime": "2026-04-17 10:00:00"
}
```
---
## 前端适配清单
### 必须改
- [ ] **所有模块**:请求体 / 响应体 `status` 字段由 `Integer(0/1)` 换为 `String("ACTIVE"/"INACTIVE")`
- [ ] **Contact**:请求体字段 `title → name``icon → iconUrl`,响应同步
- [ ] **Contact**:新建/编辑表单中 `channelType` 切换时,校验关联字段必填逻辑ABOUT→content,ONLINE_CS/PHONE→value
- [ ] **Contact**:渠道类型改用 `/admin/contact/channel-types/enabled` 下拉接口,不要写死前端枚举
- [ ] **Faq**:分类下拉改用 `/admin/faq/categories/enabled`
- [ ] **Faq**:删除分类时处理"分类下有条目"的 400 错误提示
- [ ] **Faq**TypeScript 类型改名 `FaqCategoryVO → FaqCategoryRespVO`
- [ ] **Agreement**:三个废弃接口全部下线,统一改用 `/save-by-type`
- [ ] **Agreement**:表单类型下拉只保留 `USER_AGREEMENT` / `PRIVACY_POLICY`,清除 `travel` / `refund`
- [ ] **Agreement**:响应 VO 新字段 `updateDate`,列表需要展示(建议列)
- [ ] **全局**:业务错处理从 `code === 500` 改为 `code === 400`500 留给真正的系统异常)
- [ ] **全局**:连点/重试按钮要处理 429 "请勿重复提交"
### 无需改
- 列表/详情接口路径不变(除 Agreement 删除的 3 个)
- 富文本字段不用在前端做 XSS 过滤(后端处理了)
---
## 部署步骤(后端已完成,前端配合验证)
由用户/测试服务器管理员在测试环境执行:
1. 执行 DDL`sql/mp_config_refactor_v2.sql`hl_user_service 库,幂等可重跑)
2. 清 Redis 缓存:`redis-cli DEL hl:menu:tree:* hl:dict:*`
3. 重启 `hl-user-service`
4. 管理员重新登录加载新菜单
---
## 向后兼容性
| 场景 | 兼容性 |
|------|--------|
| 旧前端继续发 Integer status | ❌ **不兼容**。会触发 `@Pattern` 校验返回 400 |
| 旧前端继续用 `title`/`icon` 字段名 | ❌ **不兼容**。响应 VO 不再返这两个字段 |
| 旧前端调 `POST/PUT/DELETE /admin/agreement` | ❌ **不兼容**。接口已删除,404 |
| 旧前端不传 `channelType` 必填字段 | ❌ 400 + 中文消息 |
**前端必须随本 PR 同步更新**,否则管理端会出现大面积报错。