Files
WonderQ-Admin-UI/docs/admin-module-config-api.md

498 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 页面模块配置 Admin API 契约
本文档定义 WonderQ-Admin 后端需要为 WonderQ-Admin-UI 实现的页面模块配置 CRUD 接口。接口用于维护小程序/H5 前台页面模块中的配置数据,例如首页轮播、目的地宫格、贵州地图、主题卡片和底部运营入口。
## 适用模块
当前一期只纳入结构化页面配置模块:
| module | 名称 | 用途 |
| -------------- | ------------ | ---------------------------------------------- |
| `heroSlides` | 顶部轮播 | 首页首屏轮播图、标题短文案、展示排序和启用状态 |
| `destinations` | 目的地 | 首页/目的地页展示、搜索入口和热门标记 |
| `map` | 贵州地图 | 首页「探索贵州」区域内的地图图片素材 |
| `themes` | 主题甄选 | 首页主题卡片和跳转 |
| `ctaBanners` | 底部运营入口 | 权益卡、管家入口、需求入口等 CTA |
商品池、活动商品池和线索跟进继续走独立业务接口,不混入本契约。
## 通用约定
- 所有接口前缀为 `/api/admin`
- 所有接口需要校验 `Authorization: Bearer <token>`
- 请求和响应均为 `application/json`
- 字段使用 camelCase。
- `PATCH` 为部分更新,只修改请求体中出现的字段。
- 删除接口返回 JSON不能返回空 body因为当前前端请求封装会读取 JSON。
错误响应保持当前前端兼容格式:
```json
{
"message": "模块不存在或无权限操作",
"code": "MODULE_CONFIG_FORBIDDEN",
"details": {}
}
```
## 后端实现重点
WonderQ-Admin 后端实现页面模块配置接口时,需要把 `map` 作为正式模块接入,而不是只在前端展示:
- 模块白名单必须包含 `heroSlides``destinations``map``themes``ctaBanners`
- 权限校验、模块路由、服务层分发和数据模型映射都必须识别 `map`,否则前端会收到 `MODULE_CONFIG_FORBIDDEN` 并以 toast 展示失败原因。
- `GET /api/admin/site-config` 即使没有地图数据,也必须返回 `map: []`,不要省略 `map` 字段。
- `map` 只维护一张图片,只需要支持查询、创建、更新、删除,不需要排序接口。
- 图片上传仍走 `POST /api/admin/media-assets/upload`,模块保存接口只接收上传结果里的 OSS `url` 字段并写入 `image`
## 类型定义
```ts
type SiteModule = "heroSlides" | "destinations" | "map" | "themes" | "ctaBanners";
type SiteItemPatch = {
title?: string;
kicker?: string;
name?: string;
slug?: string;
region?: string | null;
label?: string;
alt?: string;
image?: string | null;
targetType?: string | null;
targetValue?: string | null;
isHot?: boolean;
isActive?: boolean;
sortOrder?: number;
};
type HeroSlide = {
id: string;
title: string;
kicker: string | null;
image: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
};
type HeroSlideCreateInput = {
title: string;
kicker?: string | null;
image?: string | null;
isActive?: boolean;
sortOrder?: number;
};
type HeroSlideUpdateInput = Partial<HeroSlideCreateInput>;
type MapImage = {
id: string;
image: string | null;
isActive: boolean;
createdAt?: string;
updatedAt?: string;
};
type MapImageCreateInput = {
image: string;
isActive?: boolean;
};
type MapImageUpdateInput = Partial<MapImageCreateInput>;
```
各模块字段要求:
| module | 创建必填 | 可选字段 |
| -------------- | -------- | ------------------------------------------------------------- |
| `heroSlides` | `title` | `kicker``image``isActive``sortOrder` |
| `destinations` | `name` | `slug``region``image``isHot``isActive``sortOrder` |
| `map` | `image` | `isActive` |
| `themes` | `label` | `image``targetType``targetValue``isActive``sortOrder` |
| `ctaBanners` | `alt` | `image``targetType``targetValue``isActive``sortOrder` |
后端可以在创建时补全 `id`、默认 `isActive=true`、默认 `sortOrder=当前模块最后一位`
## 顶部轮播 `heroSlides` 专用契约
顶部轮播对应当前管理端抽屉中的 3 个区域:
- 展示内容:`title``kicker`
- 资源图片:`image`
- 排序与状态:`sortOrder``isActive`
字段语义:
| 字段 | 类型 | 创建 | 更新 | 说明 |
| ----------- | ---------------- | -------- | ---------- | ---------------------------------------------------------------------- |
| `id` | `string` | 后端生成 | 不允许修改 | 顶部轮播配置项唯一 id |
| `title` | `string` | 必填 | 可选 | 前台轮播主标题,提交时需要去除首尾空格,不能为空 |
| `kicker` | `string \| null` | 可选 | 可选 | 副标题/短文案,空字符串可归一化为 `null``""`,前后端需保持响应一致 |
| `image` | `string \| null` | 可选 | 可选 | 单张轮播图地址或素材 URL未上传时为 `null` |
| `sortOrder` | `number` | 可选 | 可选 | 展示顺序,整数;未传时追加到当前模块末尾 |
| `isActive` | `boolean` | 可选 | 可选 | 前台是否展示;未传时默认 `true` |
| `createdAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
| `updatedAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
顶部轮播不定义跳转能力。`heroSlides` 的创建、更新、查询响应中不要返回 `targetType``targetValue`;兼容期如果请求体携带这两个字段,后端可以忽略,但不要写入顶部轮播业务数据。
### 顶部轮播 CRUD
新增顶部轮播:
```http
POST /api/admin/site-config/heroSlides
```
请求体:
```json
{
"title": "新轮播",
"kicker": "贵州小包团首选",
"image": "/assets/source/hero.jpg",
"isActive": true,
"sortOrder": 0
}
```
响应状态码 `201`
```json
{
"id": "slide_001",
"title": "新轮播",
"kicker": "贵州小包团首选",
"image": "/assets/source/hero.jpg",
"isActive": true,
"sortOrder": 0,
"createdAt": "2026-07-01T08:00:00.000Z",
"updatedAt": "2026-07-01T08:00:00.000Z"
}
```
更新顶部轮播:
```http
PATCH /api/admin/site-config/heroSlides/:id
```
请求体为 `HeroSlideUpdateInput`,只提交需要修改的字段:
```json
{
"title": "夏日贵州小包团",
"image": null,
"isActive": false
}
```
响应状态码 `200`,响应体返回更新后的完整 `HeroSlide`
删除顶部轮播:
```http
DELETE /api/admin/site-config/heroSlides/:id
```
响应状态码 `200`
```json
{
"id": "slide_001"
}
```
删除后后端需要重新整理 `heroSlides` 内剩余项的 `sortOrder`
调整顶部轮播顺序:
```http
PATCH /api/admin/site-config/heroSlides/reorder
```
请求体:
```json
{
"itemIds": ["slide_002", "slide_001", "slide_003"]
}
```
响应状态码 `200`
```json
{
"items": [
{
"id": "slide_002",
"title": "第二张",
"kicker": null,
"image": null,
"sortOrder": 0,
"isActive": true
},
{
"id": "slide_001",
"title": "第一张",
"kicker": null,
"image": null,
"sortOrder": 1,
"isActive": true
}
]
}
```
## 贵州地图 `map` 专用契约
贵州地图模块对应当前管理端抽屉中的 2 个区域:
- 地图图片:`image`
- 显示状态:`isActive`
字段语义:
| 字段 | 类型 | 创建 | 更新 | 说明 |
| ----------- | ---------------- | -------- | ---------- | ----------------------------------------- |
| `id` | `string` | 后端生成 | 不允许修改 | 地图图片配置项唯一 id |
| `image` | `string \| null` | 必填 | 可选 | 单张地图图片地址或素材 URL创建时不能为空 |
| `isActive` | `boolean` | 可选 | 可选 | 前台是否展示;未传时默认 `true` |
| `createdAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
| `updatedAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
贵州地图只维护一张图片,不提供排序能力。`map` 的查询响应返回数组是为了复用现有站点配置结构,但最多返回 1 项。创建第二张地图图片时,后端应返回 409 或改为更新当前唯一图片,具体以后端实现保持一致。贵州地图不定义标题、文案和跳转能力。`map` 的创建、更新、查询响应中不要返回 `title``name``label``alt``targetType``targetValue``sortOrder`
后端推荐策略:`POST /api/admin/site-config/map` 在不存在地图图片时创建;已存在时返回 `409 MAP_IMAGE_ALREADY_EXISTS`,或直接更新当前唯一图片。无论选择哪种策略,都要保证 `PATCH /api/admin/site-config/map/:id` 可以按 id 更新当前图片。
### 贵州地图 CRUD
新增贵州地图图片:
```http
POST /api/admin/site-config/map
```
请求体:
```json
{
"image": "https://bucket.oss-cn-example.aliyuncs.com/admin/map/2026/07/01/guizhou-map.webp",
"isActive": false
}
```
响应状态码 `201`
```json
{
"id": "map_001",
"image": "https://bucket.oss-cn-example.aliyuncs.com/admin/map/2026/07/01/guizhou-map.webp",
"isActive": false,
"createdAt": "2026-07-01T08:00:00.000Z",
"updatedAt": "2026-07-01T08:00:00.000Z"
}
```
更新贵州地图图片:
```http
PATCH /api/admin/site-config/map/:id
```
请求体为 `MapImageUpdateInput`,只提交需要修改的字段。
删除贵州地图图片:
```http
DELETE /api/admin/site-config/map/:id
```
## 接口列表
以下路径由五类页面模块复用;`heroSlides``map` 的请求体和响应体以各自专用契约为准。
### 获取完整站点配置
```http
GET /api/admin/site-config
```
响应:
```ts
type SiteConfig = {
heroSlides: HeroSlide[];
destinations: Destination[];
map: MapImage[];
themes: ThemeCard[];
ctaBanners: CtaBanner[];
};
```
`map` 字段必须稳定返回数组;无数据时返回空数组 `[]`
### 新增模块配置项
```http
POST /api/admin/site-config/:module
```
请求体为 `SiteItemPatch`。响应状态码 `201`,响应体返回创建后的完整配置项:
```json
{
"id": "slide_001",
"title": "新轮播",
"kicker": "",
"image": null,
"isActive": true,
"sortOrder": 5
}
```
### 更新模块配置项
```http
PATCH /api/admin/site-config/:module/:id
```
请求体为 `SiteItemPatch`。响应状态码 `200`,响应体返回更新后的完整配置项。
### 删除模块配置项
```http
DELETE /api/admin/site-config/:module/:id
```
响应状态码 `200`,响应体:
```json
{
"id": "slide_001"
}
```
删除后后端需要重新整理同模块内剩余项的 `sortOrder`
### 调整模块配置顺序
```http
PATCH /api/admin/site-config/:module/reorder
```
请求体:
```json
{
"itemIds": ["slide_002", "slide_001", "slide_003"]
}
```
约束:
- `itemIds` 必须包含该模块当前全部配置项 id。
- 不允许重复 id。
- 不允许混入其他模块 id。
- 后端按数组顺序写入 `sortOrder`,从 0 开始。
`map` 模块不提供排序能力。若收到 `PATCH /api/admin/site-config/map/reorder`,后端应返回 `400``405`,不要创建任何排序数据。
响应状态码 `200`
```json
{
"items": [
{ "id": "slide_002", "title": "第二张", "sortOrder": 0, "isActive": true },
{ "id": "slide_001", "title": "第一张", "sortOrder": 1, "isActive": true }
]
}
```
如果后端使用 Express/Fastify 等路由,`/:module/reorder` 需要注册在 `/:module/:id` 之前,避免 `reorder` 被当成 id。
## 状态码
| 状态码 | 场景 |
| ------ | ------------------------------------------------ |
| `200` | 查询、更新、删除、排序成功 |
| `201` | 创建成功 |
| `400` | module 非法、请求体格式错误、排序 id 不完整 |
| `401` | 未登录或 token 无效 |
| `403` | 无权限维护页面配置 |
| `404` | 配置项不存在 |
| `409` | 删除被发布版本、商品或活动引用的配置项时发生冲突 |
| `422` | 字段校验失败,例如必填标题为空 |
| `500` | 服务端异常 |
## 前端联调入口
WonderQ-Admin-UI 当前调用函数位于 `src/api.ts`
- `getSiteConfig()`
- `createSiteConfigItem(module, input)`
- `updateSiteConfigItem(module, id, input)`
- `deleteSiteConfigItem(module, id)`
- `reorderSiteConfigItems(module, itemIds)`
维护地图 UI 位于 `src/pages/structure/StructurePage.tsx`
## 图片素材上传
用于 WonderQ-Admin-UI 在维护页面模块图片时上传本地图片,并把返回的 OSS URL 写入 `image` 字段。
```http
POST /api/admin/media-assets/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
```
表单字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `file` | `File` | 是 | 图片文件,仅支持 JPG、PNG、WebP、GIF |
| `group` | `string` | 否 | 素材分组,只能包含字母、数字、下划线和中划线;默认 `general` |
限制:
- 单个文件最大 5MB。
- 后端会校验 `Content-Type` 和文件头魔数,拒绝 SVG、非图片文件和伪装类型。
- 后端将文件上传到 OSS返回可用于前台展示的 URL并写入 `MediaAsset` 素材记录。
响应状态码 `201`
```json
{
"id": "asset_001",
"url": "https://bucket.oss-cn-example.aliyuncs.com/admin/heroSlides/2026/07/01/example.png",
"name": "hero.png",
"mimeType": "image/png",
"sizeBytes": 102400,
"group": "heroSlides",
"createdAt": "2026-07-01T08:00:00",
"updatedAt": "2026-07-01T08:00:00"
}
```
常见错误:
| 状态码 | `code` | 场景 |
| --- | --- | --- |
| `400` | `MEDIA_UPLOAD_INVALID_TYPE` | 文件不是允许的图片类型 |
| `400` | `MEDIA_UPLOAD_TYPE_MISMATCH` | `Content-Type` 与文件头不一致 |
| `400` | `MEDIA_UPLOAD_INVALID_GROUP` | `group` 格式非法 |
| `413` | `MEDIA_UPLOAD_TOO_LARGE` | 文件超过 5MB |
| `503` | `MEDIA_STORAGE_NOT_CONFIGURED` | OSS 环境配置不完整 |
| `502` | `MEDIA_STORAGE_UPLOAD_FAILED` | OSS 上传失败 |
前端封装位于 `src/api.ts`
```ts
uploadMediaAsset(file: File, group?: string): Promise<MediaAsset>
```
`src/components/admin/SingleImageUploader.tsx` 已使用该封装;结构维护页会把当前模块 id 作为 `group` 上传。