19 KiB
页面模块配置 Admin API 契约
本文档定义 WonderQ-Admin 后端需要为 WonderQ-Admin-UI 实现的页面模块配置 CRUD 接口。接口用于维护小程序/H5 前台页面模块中的配置数据,例如首页轮播、目的地宫格、贵州地图、主题卡片、特价优惠和底部运营入口。
适用模块
当前一期只纳入结构化页面配置模块:
| module | 名称 | 用途 |
|---|---|---|
heroSlides |
顶部轮播 | 首页首屏轮播图、标题短文案、展示排序和启用状态 |
destinations |
目的地 | 首页/目的地页展示、搜索入口和热门标记 |
map |
贵州地图 | 首页「探索贵州」区域内的地图图片素材 |
themes |
主题甄选 | 首页主题卡片和跳转 |
campaigns |
特价优惠 | 首页特价优惠活动元信息 |
ctaBanners |
底部运营入口 | 权益卡、管家入口、需求入口等 CTA |
商品池、活动商品池和线索跟进继续走独立业务接口,不混入本契约。
通用约定
- 所有接口前缀为
/api/admin。 - 所有接口需要校验
Authorization: Bearer <token>。 - 请求和响应均为
application/json。 - 字段使用 camelCase。
PATCH为部分更新,只修改请求体中出现的字段。- 删除接口返回 JSON,不能返回空 body,因为当前前端请求封装会读取 JSON。
错误响应保持当前前端兼容格式:
{
"message": "模块不存在或无权限操作",
"code": "MODULE_CONFIG_FORBIDDEN",
"details": {}
}
后端实现重点
WonderQ-Admin 后端实现页面模块配置接口时,需要把 map 和 campaigns 作为正式模块接入,而不是只在前端展示:
- 模块白名单必须包含
heroSlides、destinations、map、themes、campaigns、ctaBanners。 - 权限校验、模块路由、服务层分发和数据模型映射都必须识别
map和campaigns,否则前端会收到MODULE_CONFIG_FORBIDDEN并以 toast 展示失败原因。 GET /api/admin/site-config即使没有地图数据,也必须返回map: [],不要省略map字段。GET /api/admin/site-config即使没有特价优惠数据,也必须返回campaigns: [],不要省略campaigns字段。map只维护一张图片,只需要支持查询、创建、更新、删除,不需要排序接口。campaigns维护活动元信息,只需要支持查询、创建、更新、删除,不需要排序接口。- 图片上传仍走
POST /api/admin/media-assets/upload,模块保存接口只接收上传结果里的 OSSurl字段并写入image。
类型定义
type SiteModule = "heroSlides" | "destinations" | "map" | "themes" | "campaigns" | "ctaBanners";
type SiteItemPatch = {
title?: string;
kicker?: string;
name?: string;
slug?: string;
region?: string | null;
label?: string;
alt?: string;
image?: string | null;
description?: string | null;
coverImage?: string | null;
targetType?: string | null;
targetValue?: string | null;
isHot?: boolean;
isActive?: boolean;
sortOrder?: number;
status?: "draft" | "published";
startsAt?: string | null;
endsAt?: string | null;
};
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>;
type Campaign = {
id: string;
slug: string;
title: string;
description: string | null;
coverImage: string | null;
priceAmount: number | null;
priceUnit: string | null;
tags: string[];
status: "draft" | "published";
startsAt: string | null;
endsAt: string | null;
createdAt?: string;
updatedAt?: string;
};
type CampaignCreateInput = {
title: string;
slug: string;
description?: string | null;
coverImage?: string | null;
priceAmount?: number | null;
priceUnit?: string | null;
tags?: string[];
status?: "draft" | "published";
startsAt?: string | null;
endsAt?: string | null;
};
type CampaignUpdateInput = Partial<CampaignCreateInput>;
各模块字段要求:
| 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 |
campaigns |
title、slug |
description、coverImage、priceAmount、priceUnit、tags、status、startsAt、endsAt |
ctaBanners |
alt |
image、targetType、targetValue、isActive、sortOrder |
后端可以在创建时补全 id、默认 isActive=true、默认 sortOrder=当前模块最后一位。
campaigns 创建时默认 status="draft",不会进入 Public site-config.campaigns;只有 status="published" 的活动会进入 H5 Public API。
Admin UI 面向运营只展示活动标题、活动描述、封面图和前台启用状态;slug 是接口必填技术标识,前端可在新建时自动生成,编辑时复用原值。
顶部轮播 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
新增顶部轮播:
POST /api/admin/site-config/heroSlides
请求体:
{
"title": "新轮播",
"kicker": "贵州小包团首选",
"image": "/assets/source/hero.jpg",
"isActive": true,
"sortOrder": 0
}
响应状态码 201:
{
"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"
}
更新顶部轮播:
PATCH /api/admin/site-config/heroSlides/:id
请求体为 HeroSlideUpdateInput,只提交需要修改的字段:
{
"title": "夏日贵州小包团",
"image": null,
"isActive": false
}
响应状态码 200,响应体返回更新后的完整 HeroSlide。
删除顶部轮播:
DELETE /api/admin/site-config/heroSlides/:id
响应状态码 200:
{
"id": "slide_001"
}
删除后后端需要重新整理 heroSlides 内剩余项的 sortOrder。
调整顶部轮播顺序:
PATCH /api/admin/site-config/heroSlides/reorder
请求体:
{
"itemIds": ["slide_002", "slide_001", "slide_003"]
}
响应状态码 200:
{
"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
新增贵州地图图片:
POST /api/admin/site-config/map
请求体:
{
"image": "https://bucket.oss-cn-example.aliyuncs.com/admin/map/2026/07/01/guizhou-map.webp",
"isActive": false
}
响应状态码 201:
{
"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"
}
更新贵州地图图片:
PATCH /api/admin/site-config/map/:id
请求体为 MapImageUpdateInput,只提交需要修改的字段。
删除贵州地图图片:
DELETE /api/admin/site-config/map/:id
特价优惠 campaigns 专用契约
特价优惠模块对应 H5 Public API 的 site-config.campaigns,只维护活动元信息,不维护活动商品关联列表。
字段语义:
| 字段 | 类型 | 创建 | 更新 | 说明 |
|---|---|---|---|---|
id |
string |
后端生成 | 不允许修改 | 活动唯一 id |
slug |
string |
必填 | 可选 | 活动标识,需要全局唯一 |
title |
string |
必填 | 可选 | 活动标题,后台列表主标题 |
description |
string | null |
可选 | 可选 | 活动描述,后台列表副文案 |
coverImage |
string | null |
可选 | 可选 | 活动封面图 OSS URL |
priceAmount |
number | null |
可选 | 可选 | 参考起价,单位按 priceUnit 展示 |
priceUnit |
string | null |
可选 | 可选 | 价格单位文案,默认 起/人 |
tags |
string[] |
可选 | 可选 | 活动卡片标签,最多 3 个 |
status |
"draft" | "published" |
可选 | 可选 | 新建默认 draft;Public API 只返回 published |
startsAt |
string | null |
可选 | 可选 | 活动开始时间 |
endsAt |
string | null |
可选 | 可选 | 活动结束时间 |
createdAt |
string |
后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
updatedAt |
string |
后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
特价优惠 CRUD
新增特价优惠:
POST /api/admin/site-config/campaigns
请求体:
{
"slug": "classic-deal",
"title": "经典打卡特惠",
"description": "经典首游活动",
"coverImage": "https://bucket.oss-cn-example.aliyuncs.com/admin/campaigns/2026/07/02/classic.webp",
"priceAmount": 162500,
"priceUnit": "起/人",
"tags": ["臻藏旅位", "赛事庆典"],
"status": "draft",
"startsAt": null,
"endsAt": null
}
更新特价优惠:
PATCH /api/admin/site-config/campaigns/:id
删除特价优惠:
DELETE /api/admin/site-config/campaigns/:id
特价优惠不定义 sortOrder、isActive、targetType 和 targetValue;兼容期如果请求体携带这些字段,后端可以忽略,但不要写入 Campaign 业务数据。
tags 保存前需要 trim 并过滤空字符串;有效标签超过 3 个时返回 422 MODULE_CONFIG_VALIDATION_ERROR,details 为 { "field": "tags", "max": 3 }。
接口列表
以下路径由六类页面模块复用;heroSlides、map、campaigns 的请求体和响应体以各自专用契约为准。
获取完整站点配置
GET /api/admin/site-config
响应:
type SiteConfig = {
heroSlides: HeroSlide[];
destinations: Destination[];
map: MapImage[];
themes: ThemeCard[];
campaigns: Campaign[];
ctaBanners: CtaBanner[];
};
map 字段必须稳定返回数组;无数据时返回空数组 []。
campaigns 字段必须稳定返回数组;无数据时返回空数组 []。
新增模块配置项
POST /api/admin/site-config/:module
请求体为 SiteItemPatch。响应状态码 201,响应体返回创建后的完整配置项:
{
"id": "slide_001",
"title": "新轮播",
"kicker": "",
"image": null,
"isActive": true,
"sortOrder": 5
}
更新模块配置项
PATCH /api/admin/site-config/:module/:id
请求体为 SiteItemPatch。响应状态码 200,响应体返回更新后的完整配置项。
删除模块配置项
DELETE /api/admin/site-config/:module/:id
响应状态码 200,响应体:
{
"id": "slide_001"
}
删除后后端需要重新整理同模块内剩余项的 sortOrder。
调整模块配置顺序
PATCH /api/admin/site-config/:module/reorder
请求体:
{
"itemIds": ["slide_002", "slide_001", "slide_003"]
}
约束:
itemIds必须包含该模块当前全部配置项 id。- 不允许重复 id。
- 不允许混入其他模块 id。
- 后端按数组顺序写入
sortOrder,从 0 开始。
map 和 campaigns 模块不提供排序能力。若收到 PATCH /api/admin/site-config/map/reorder 或 PATCH /api/admin/site-config/campaigns/reorder,后端应返回 400 或 405,不要创建任何排序数据。
响应状态码 200:
{
"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 或 coverImage。
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:
{
"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:
uploadMediaAsset(file: File, group?: string): Promise<MediaAsset>
src/components/admin/SingleImageUploader.tsx 已使用该封装;结构维护页会把当前模块 id 作为 group 上传。