# 小程序配置模块对接方案

> 模块：小程序配置 (Banner 首页配置 / 我的页面配置 / 协议政策 / FAQ)
> 后端服务：hl-user-service（Admin + Internal）+ hl-mp-service（小程序透传）
> 文档日期：2026-04-18
> 适用：dev 分支当前代码（PR #773 整改后）

---

## 0. 通用约定（必读）

### 0.1 响应包装 `Result<T>`
```json
{
  "code": 200,        // 200=成功；非 200=业务异常（HTTP 始终 200）
  "message": "成功",
  "data": { ... }
}
```
**所有接口 HTTP 状态码始终是 200**，业务成败看 `code`。401 = 未登录/Token 过期；403 = 无权限；500 = 业务/系统异常。

### 0.2 分页响应 `PageResult<T>`
```json
{ "records": [...], "total": 0, "page": 1, "pageSize": 20 }
```
分页请求统一字段：`page`（默认 1，≥1）、`pageSize`（默认 20，1~100）。

### 0.3 Long ID 序列化
所有 Long 类型 ID 通过 `@JsonSerialize(ToStringSerializer)` 序列化为 **字符串**（防 JS 大数精度丢失）。前端接收/回传统一按字符串处理。

### 0.4 富文本 XSS 过滤
`content`/`answer` 字段后端会经 `XssUtils` 过滤：
- 禁用：`<script>` `<iframe>` `<img>` `on*` 事件属性
- 允许：常规标签 + 样式属性
- Agreement.content 单字段最大 100KB；Faq.answer 最大 10KB

### 0.5 幂等保护
管理端 POST/PUT 接口大多带 `@Idempotent(timeout=5)`：5 秒内同参数请求会被拦截。前端**无需自行去重**。

### 0.6 小程序端缓存
所有 `/mp/**` 接口使用 `@MpCache` 缓存 1800s（30 分钟）。**管理端修改后小程序最多 30 分钟生效**，无主动失效机制。

### 0.7 端口/路由
| 类型 | 路径前缀 | 说明 |
|---|---|---|
| 管理端 | `/admin/...` | 走 gateway → hl-user-service，需 ADMIN Token |
| 小程序 | `/mp/...` | 走 gateway → hl-mp-service → Feign → hl-user-service，需 USER Token（部分公开） |
| 内部 | `/internal/...` | 服务间 Feign 专用，**外部访问被网关拦截**（403） |

---

## 1. Banner 首页配置（轮播图）

### 1.1 业务说明
首页顶部轮播位（图片或视频），支持时间窗、链接跳转（产品/景点/活动/H5/小程序内页）、排序。

### 1.2 数据库表 `sys_banner`
关键字段：
| 列 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 雪花 ID |
| title / subtitle | VARCHAR | 标题/副标题 |
| media_type | VARCHAR(20) | IMAGE / VIDEO（字典 banner_media_type）|
| image_url / video_url | VARCHAR(500) | 资源 URL |
| material_id | BIGINT | 素材库关联 ID |
| tags | VARCHAR(200) | 标签，逗号分隔 |
| link_type | VARCHAR(20) | 字典 banner_link_type |
| link_id / link_url / link_target_name | VARCHAR | 跳转参数 |
| sort_order | INT | 越小越前 |
| status | INT | 0=下线 1=上线（**注意：本表 status 是整数，与其他表的字符串不同**）|
| start_time / end_time | DATETIME | 上下线时间窗 |
| create_time / update_time / deleted_at | DATETIME | 审计 + 软删 |

### 1.3 管理端接口（前缀 `/admin/banner`）

| 方法 | 路径 | 入参 | 返参 |
|---|---|---|---|
| GET | `/admin/banner` | Query: page, pageSize, keyword, status | `Result<PageResult<BannerVO>>` |
| GET | `/admin/banner/{id}` | Path: id | `Result<BannerVO>` |
| POST | `/admin/banner` | Body: BannerRequest | `Result<BannerVO>` |
| PUT | `/admin/banner/{id}` | Path: id + Body: BannerRequest | `Result<BannerVO>` |
| DELETE | `/admin/banner/{id}` | Path: id | `Result<Void>` |

⚠️ Banner 模块的请求体类名是 `BannerRequest`（历史遗留，未按 `XxxSaveReqVO` 规范）。

### 1.4 BannerRequest 字段（创建+更新合一）

| 字段 | 类型 | 必填 | 校验/字典 | 说明 |
|---|---|---|---|---|
| title | String | ✅ | @NotBlank, ≤100 | 标题 |
| subtitle | String | | ≤200 | 副标题 |
| mediaType | String | | banner_media_type, ≤20 | IMAGE / VIDEO |
| imageUrl | String | ✅ | @NotBlank, ≤500 | 图片或视频封面 URL |
| videoUrl | String | | ≤500 | 视频 URL（mediaType=VIDEO 时有效）|
| materialId | Long | | | 素材库 ID（关联）|
| tags | String | | ≤200 | 逗号分隔标签 |
| linkType | String | | banner_link_type, ≤20 | 跳转类型，见字典 |
| linkId | String | | ≤50 | 跳转目标 ID（PRODUCT/SCENIC 等用）|
| linkUrl | String | | ≤500 | 外部 URL（WEBVIEW/PAGE 用）|
| linkTargetName | String | | ≤100 | 跳转名称（后台展示用）|
| sortOrder | Integer | | | 排序，越小越前 |
| status | Integer | | 0/1 | 0=下线，1=上线 |
| startTime | LocalDateTime | | ISO 8601 | 展示开始时间 |
| endTime | LocalDateTime | | ISO 8601 | 展示结束时间 |

### 1.5 BannerVO 返回字段
含上述全部字段 + `id`(String)、`createdAt`、`tags`(返回时为 `List<String>` 已拆好)。

### 1.6 小程序端接口（前缀 `/mp/banner`）

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/mp/banner/active` | 公开接口（无需登录），返回当前生效的轮播列表，缓存 30 分钟 |

返回：`Result<List<BannerVO>>`（结构同管理端 BannerVO，已按 `sortOrder` 升序，已过滤 `status=1` 且在 `startTime~endTime` 时间窗内的）。

---

## 2. 我的页面配置 - Contact（联系我们 / 关于我们）

### 2.1 业务说明
"我的"页面联系方式区块。三种渠道：
- **ABOUT**：关于我们（用 `content` 富文本）
- **ONLINE_CS**：在线客服（用 `value` 存 URL）
- **PHONE**：电话咨询（用 `value` 存电话号）

### 2.2 数据库表 `sys_contact`
| 列 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 雪花 ID |
| channel_type | VARCHAR(32) | 字典 contact_channel_type |
| name | VARCHAR(100) | 标签文案 |
| subtitle | VARCHAR(200) | 副标题（保留字段，前端可不展示）|
| icon_url | VARCHAR(500) | 图标 URL（来自素材库 SYSTEM 分类）|
| value | VARCHAR(500) | 渠道值（URL/电话）|
| content | TEXT | 富文本（仅 ABOUT 用）|
| sort_order | INT | 排序 |
| status | VARCHAR(32) | ACTIVE / INACTIVE |
| create_time/update_time/created_by/updated_by/deleted_at | | 审计 + 软删 |

### 2.3 管理端接口（前缀 `/admin/contact`）

| 方法 | 路径 | 入参 | 返参 |
|---|---|---|---|
| GET | `/admin/contact` | Query: ContactPageReqVO | `Result<PageResult<ContactRespVO>>` |
| GET | `/admin/contact/{id}` | Path: id | `Result<ContactRespVO>` |
| POST | `/admin/contact` | Body: ContactSaveReqVO | `Result<Long>`（返回新 ID）|
| PUT | `/admin/contact/{id}` | Path: id + Body: ContactSaveReqVO | `Result<Void>` |
| DELETE | `/admin/contact/{id}` | Path: id | `Result<Void>` |
| GET | `/admin/contact/channel-types/enabled` | — | `Result<List<{value,label}>>` 渠道下拉框 |

### 2.4 ContactPageReqVO 查询字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page / pageSize | Integer | | 见 0.2 |
| keyword | String | | 模糊匹配 name/value |
| status | String | | ACTIVE/INACTIVE 不传=全部 |
| channelType | String | | ABOUT/ONLINE_CS/PHONE 不传=全部 |

### 2.5 ContactSaveReqVO 字段（创建+更新合一）
| 字段 | 类型 | 必填 | 校验/字典 | 说明 |
|---|---|---|---|---|
| id | Long | | | 不传=创建，传=更新 |
| channelType | String | ✅ | @NotBlank, ≤32, contact_channel_type | ABOUT/ONLINE_CS/PHONE |
| name | String | ✅ | @NotBlank, ≤100 | 标签文案 |
| subtitle | String | | ≤200 | 副标题 |
| iconUrl | String | | ≤500 | 图标 URL |
| value | String | | ≤500 | 渠道值（ABOUT 可空）|
| content | String | | ≤50000 | 富文本，仅 ABOUT 用 |
| sortOrder | Integer | | | 排序，默认 0 |

### 2.6 ContactRespVO 返回字段
全字段 + `status`（字典）、`createTime`、`updateTime`。

### 2.7 小程序端接口（前缀 `/mp/common`）

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/mp/common/contact` | 返回全部 ACTIVE 联系方式列表，按 sortOrder 升序，缓存 30 分钟 |

返回：`Result<List<MpContactRespVO>>`，字段：
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | |
| channelType | String | ABOUT/ONLINE_CS/PHONE |
| name | String | |
| iconUrl | String | |
| value | String | |
| content | String | 富文本（ABOUT 才有）|
| sortOrder | Integer | |

> **小程序端不返回** subtitle/status/创建时间，瘦身处理。

---

## 3. 我的页面配置 - FAQ（常见问题）

### 3.1 业务说明
两级结构：分类 → 条目。小程序"帮助中心"按分类聚合展示。

### 3.2 数据库表
- `sys_faq_category`：分类（id / name / sort_order / status / 审计）
- `sys_faq_item`：条目（id / category_id / question / answer / sort_order / status / 审计）

### 3.3 管理端接口 - 分类（前缀 `/admin/faq/categories`）

| 方法 | 路径 | 入参 | 返参 |
|---|---|---|---|
| GET | `/admin/faq/categories` | — | `Result<List<FaqCategoryRespVO>>` 含全部（含停用），带 itemCount |
| GET | `/admin/faq/categories/enabled` | — | `Result<List<{value,label}>>` 仅启用，下拉框用 |
| GET | `/admin/faq/categories/{id}` | Path: id | `Result<FaqCategoryRespVO>` |
| POST | `/admin/faq/categories` | Body: FaqCategorySaveReqVO | `Result<FaqCategoryRespVO>` |
| PUT | `/admin/faq/categories/{id}` | Path + Body | `Result<FaqCategoryRespVO>` |
| PUT | `/admin/faq/categories/{id}/status` | Path + Query: status=ACTIVE/INACTIVE | `Result<Void>` |
| DELETE | `/admin/faq/categories/{id}` | Path: id | `Result<Void>` 软删，**前置校验：分类下不能有条目** |

### 3.4 管理端接口 - 条目（前缀 `/admin/faq/items`）

| 方法 | 路径 | 入参 | 返参 |
|---|---|---|---|
| GET | `/admin/faq/items` | Query: FaqItemPageReqVO | `Result<PageResult<FaqItemRespVO>>` |
| GET | `/admin/faq/items/{id}` | Path: id | `Result<FaqItemRespVO>` |
| POST | `/admin/faq/items` | Body: FaqItemSaveReqVO | `Result<FaqItemRespVO>` |
| PUT | `/admin/faq/items/{id}` | Path + Body | `Result<FaqItemRespVO>` |
| PUT | `/admin/faq/items/{id}/status` | Path + Query: status=ACTIVE/INACTIVE | `Result<Void>` |
| DELETE | `/admin/faq/items/{id}` | Path: id | `Result<Void>` 软删 |

### 3.5 VO 字段

**FaqCategorySaveReqVO**
| 字段 | 类型 | 必填 | 校验 |
|---|---|---|---|
| id | Long | | 不传=创建 |
| name | String | ✅ | @NotBlank, ≤50 |
| sortOrder | Integer | | |

**FaqCategoryRespVO**：id(String) / name / sortOrder / status / statusLabel / itemCount / createTime / updateTime

**FaqItemPageReqVO**
| 字段 | 类型 | 说明 |
|---|---|---|
| page / pageSize | Integer | |
| categoryId | Long | 可选，按分类筛 |
| keyword | String | 模糊匹配 question |
| status | String | ACTIVE/INACTIVE |

**FaqItemSaveReqVO**
| 字段 | 类型 | 必填 | 校验 |
|---|---|---|---|
| id | Long | | 不传=创建 |
| categoryId | Long | ✅ | @NotNull |
| question | String | ✅ | @NotBlank, ≤200 |
| answer | String | ✅ | @NotBlank, ≤10000，富文本 XSS 过滤 |
| sortOrder | Integer | | |

**FaqItemRespVO**：id(String) / categoryId(String) / categoryName / question / answer / sortOrder / status / statusLabel / createTime / updateTime

### 3.6 小程序端接口

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/mp/common/faq` | 返回全部启用 FAQ（按分类嵌套），缓存 30 分钟 |

返回：`Result<List<MpFaqCategoryRespVO>>`
- `MpFaqCategoryRespVO`：`id`(String) / `name` / `items`（条目列表）
- `MpFaqItemRespVO`：`id`(String) / `question` / `answer`

> **小程序端只返回启用的分类和条目**，按 sortOrder 升序。

---

## 4. 协议政策 - Agreement

### 4.1 业务说明
法律协议，按 `type` 唯一（每类型最多一条记录）。前端"用户协议"、"隐私政策"链接打开。
**UPSERT 模式**：管理端只有保存接口（按 type 自动判断创建/更新），无独立 DELETE。

### 4.2 数据库表 `sys_agreement`
| 列 | 类型 | 说明 |
|---|---|---|
| agreement_id | BIGINT | 雪花 ID |
| type | VARCHAR(64) | **UNIQUE**，字典 mp_agreement_type |
| title | VARCHAR(100) | 标题 |
| content | MEDIUMTEXT | 正文，HTML 富文本 |
| update_date | DATE | 显式更新日期，C 端副标题用 |
| version | VARCHAR | 版本号（保留字段，默认 1.0）|
| status | VARCHAR(32) | ACTIVE/INACTIVE |
| sort_order | INT | 排序 |
| create_time/update_time/created_by/updated_by/deleted_at | | 审计 + 软删 |

### 4.3 管理端接口（前缀 `/admin/agreement`）

| 方法 | 路径 | 入参 | 返参 |
|---|---|---|---|
| GET | `/admin/agreement/page` | Query: AgreementPageReqVO | `Result<IPage<AgreementRespVO>>` |
| GET | `/admin/agreement/{id}` | Path: id | `Result<AgreementRespVO>` |
| GET | `/admin/agreement/type/{type}` | Path: type | `Result<AgreementRespVO>` 按 type 取，编辑页用 |
| POST | `/admin/agreement/save-by-type` | Body: AgreementSaveReqVO | `Result<Void>` UPSERT，幂等 |

> **没有 DELETE 接口**。下线请改 status=INACTIVE。

### 4.4 AgreementSaveReqVO 字段
| 字段 | 类型 | 必填 | 校验/字典 |
|---|---|---|---|
| type | String | ✅ | @NotBlank, @Pattern `^(USER_AGREEMENT|PRIVACY_POLICY)$`, mp_agreement_type |
| title | String | ✅ | @NotBlank, ≤100 |
| content | String | ✅ | @NotBlank, ≤100000（100KB），HTML 富文本，XSS 白名单过滤 |
| updateDate | LocalDate | | 不传默认今天 |
| sortOrder | Integer | | 默认 0 |

### 4.5 AgreementRespVO 返回字段
id(String) / type / typeLabel / title / content / version / updateDate / sortOrder / status / statusLabel / createTime / updateTime / createdBy(String) / updatedBy(String)

### 4.6 小程序端接口（前缀 `/mp/common`）

| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/mp/common/agreement/{type}` | 取指定类型协议详情（含正文），缓存 30 分钟 |
| GET | `/mp/common/agreement/list` | 取全部 ACTIVE 协议列表（**不含 content**，轻量），缓存 30 分钟 |

**MpAgreementDetailVO**（详情接口）：type / title / content / version / updateDate / updateTime
**MpAgreementSimpleVO**（列表接口）：type / title / version / updateDate / updateTime

---

## 5. 字典完整清单

### 5.1 `banner_media_type`
| value | label |
|---|---|
| IMAGE | 图片 |
| VIDEO | 视频 |

### 5.2 `banner_link_type`
| value | label |
|---|---|
| NONE | 无跳转 |
| PRODUCT | 产品详情 |
| SCENIC | 景点详情 |
| ACTIVITY | 活动详情 |
| HOTEL | 酒店详情 |
| PAGE | 小程序页面 |
| WEBVIEW | H5 外链 |

> 说明：linkType=PRODUCT/SCENIC/ACTIVITY/HOTEL 时，`linkId` 必填（资源 ID）。linkType=WEBVIEW/PAGE 时，`linkUrl` 必填。

### 5.3 `contact_channel_type`
| value | label | 用法 |
|---|---|---|
| ABOUT | 关于我们 | `content` 存富文本 |
| ONLINE_CS | 在线客服 | `value` 存 URL |
| PHONE | 电话咨询 | `value` 存电话号 |

### 5.4 `mp_agreement_type`
| value | label |
|---|---|
| USER_AGREEMENT | 用户协议 |
| PRIVACY_POLICY | 隐私政策 |

### 5.5 `common_status`
| value | label |
|---|---|
| ACTIVE | 启用 |
| INACTIVE | 停用 |

> 适用：sys_contact / sys_faq_category / sys_faq_item / sys_agreement 的 status 字段。
> **特例**：`sys_banner.status` 是整数 0/1（0=下线，1=上线），与本字典不同，不要混淆。

---

## 6. 前端对接 Checklist

### 管理端
- [ ] 列表/详情页直接调 `/admin/...`，使用 ADMIN Token
- [ ] 表单字段按本文档 SaveReqVO 严格对齐（必填项、长度限制）
- [ ] 字典下拉用各模块的 `/enabled` 接口（不要硬编码）
- [ ] Long 类型 ID 接收/回传统一按字符串处理
- [ ] 富文本编辑器输出 HTML，后端会过滤危险标签
- [ ] 5 秒内同参数提交会被幂等拦截（无需自行去重）
- [ ] 软删除后该数据列表自动消失（无需特殊处理）

### 小程序端
- [ ] 调 `/mp/...` 接口，使用 USER Token（部分公开如 `/mp/banner/active` 可不传）
- [ ] **缓存 30 分钟**：管理端改完后小程序最多 30 分钟生效，前端体验需告知用户
- [ ] Banner/FAQ 已按 sortOrder 排好，前端按返回顺序展示即可
- [ ] FAQ 返回结构是嵌套的（分类→条目数组），可直接渲染分组列表
- [ ] Agreement 详情页用 `/mp/common/agreement/{type}` 取 content；列表（如登录页同意勾选）用 `/list` 节省流量

---

## 7. 已知坑/注意事项

1. **status 字段两种格式**：sys_banner 是 0/1 整数；其他三表（contact/faq/agreement）是 ACTIVE/INACTIVE 字符串。前端列表筛选时勿混。
2. **Banner 用 BannerRequest 类名**（历史遗留），不是 BannerSaveReqVO。
3. **FAQ 删除分类有前置校验**：分类下有条目时返回业务异常 `code=500`，需先清空条目。
4. **Agreement 不能删**：只能 status=INACTIVE 软下线。
5. **小程序端 30 分钟缓存**无主动失效机制，灰度调试期建议用临时账号清测试用户的 Redis Key。
6. **OperatorHolder 已修复**（PR #777）：所有写操作的 `created_by/updated_by` 现在能正确填入操作人 ID（之前是 NULL）。

---

## 8. 联系/反馈

后端代码位置：
- Admin Controller：`hl-user-service/src/main/java/com/hulalv/user/controller/Admin{Banner,Contact,Faq,Agreement}Controller.java`
- Internal Controller：`hl-user-service/src/main/java/com/hulalv/user/controller/Internal{Banner,Contact,Faq,MpCommon}Controller.java`
- MP Controller：`hl-mp-service/src/main/java/com/hulalv/mp/controller/Mp{Banner,Common}Controller.java`
- VO：`hl-user-service/src/main/java/com/hulalv/user/vo/{admin,mp}/`

如有字段缺失或行为不符，**直接反馈到本仓库 Issue**（带接口路径 + 实际响应 + 期望响应）。
