feat: 小程序配置模块重构(联系我们/常见问题/协议政策)- PR #773
这个提交包含在:
父节点
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 同步更新**,否则管理端会出现大面积报错。
|
||||
正在加载...
x
在新工单中引用
屏蔽一个用户