10 KiB
10 KiB
小程序配置模块重构:联系我们 / 常见问题 / 协议政策
服务: hl-user-service (端口 8081) PR: wx/HL#773 Issue: wx/HL#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 秒窗口),前端连点 / 重试会被返:
{
"code": 429,
"success": false,
"message": "请勿重复提交"
}
改 body 后可再次提交。
富文本 XSS 过滤(后端自动处理)
以下字段提交时后端自动清洗 <script> / <iframe> / on* 事件属性等危险标签:
Contact.contentFaqItem.answerAgreement.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(请求)
{
"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(响应)
{
"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
{
"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
{
"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。
废弃接口(前端必须下线)
| 方法 | 路径 | 处理 |
|---|---|---|
/admin/agreement |
删除。改用 /save-by-type |
|
/admin/agreement/{id} |
删除。改用 /save-by-type |
|
/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(请求)
{
"type": "USER_AGREEMENT", // 必填。USER_AGREEMENT / PRIVACY_POLICY
"title": "用户协议", // 必填
"content": "<p>...</p>", // 必填,XSS 过滤
"updateDate": "2026-04-17", // 必填,LocalDate
"sortOrder": 1 // 选填
}
原
AgreementRequest废弃。
AgreementRespVO(响应)
{
"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 过滤(后端处理了)
部署步骤(后端已完成,前端配合验证)
由用户/测试服务器管理员在测试环境执行:
- 执行 DDL:
sql/mp_config_refactor_v2.sql(hl_user_service 库,幂等可重跑) - 清 Redis 缓存:
redis-cli DEL hl:menu:tree:* hl:dict:* - 重启
hl-user-service - 管理员重新登录加载新菜单
向后兼容性
| 场景 | 兼容性 |
|---|---|
| 旧前端继续发 Integer status | ❌ 不兼容。会触发 @Pattern 校验返回 400 |
旧前端继续用 title/icon 字段名 |
❌ 不兼容。响应 VO 不再返这两个字段 |
旧前端调 POST/PUT/DELETE /admin/agreement |
❌ 不兼容。接口已删除,404 |
旧前端不传 channelType 必填字段 |
❌ 400 + 中文消息 |
前端必须随本 PR 同步更新,否则管理端会出现大面积报错。