Files
WonderQ-Admin-UI/docs/admin-module-config-api.md
duanshuwen f7401d8dc6 feat(hotel groups): add price, tags and publish status support
extend HotelGroup type with new fields including coverImage, priceAmount, priceUnit, tags and draft/published status. refactor admin utilities to separate hotel group and vehicle option handling, add proper payload normalization for hotel group submissions including syncing coverImage and image fields. update admin UI components to support hotel group-specific configurations, including default "起/晚" price unit, trimmed and limited tags, and publish status toggle. update admin documentation to reflect new hotel group API, split hotel groups docs from vehicle options, and document all new fields.
2026-07-03 22:45:16 +08:00

802 lines
29 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` | 主题甄选 | 首页主题卡片和跳转 |
| `campaigns` | 特价优惠 | 首页特价优惠活动元信息 |
| `routeSections` | 精选线路子分组 | 首页“精选线路”按运营任务新增分组,维护标题、副文案、启用状态和关联商品 |
| `hotelGroups` | 特色酒店 | 首页“特色酒店”卡片,维护标题、描述、价格、标签、封面图、启用状态和排序 |
| `vehicleOptions` | 万趣用车 | 首页“万趣用车”卡片,维护标题、描述、封面图、启用状态和排序 |
| `ctaBanners` | 更多服务 | 权益、管家、目的地和需求入口等更多服务卡片 |
商品本体、活动商品池和线索跟进继续走独立业务接口,不混入本契约。`routeSections` 只维护精选线路子分组和商品 ID 关联,不重复编辑商品详情;`hotelGroups``vehicleOptions` 只维护首页卡片内容,不绑定商品本体。
## 通用约定
- 所有接口前缀为 `/api/admin`
- 所有接口需要校验 `Authorization: Bearer <token>`
- 请求和响应均为 `application/json`
- 字段使用 camelCase。
- `PATCH` 为部分更新,只修改请求体中出现的字段。
- 删除接口返回 JSON不能返回空 body因为当前前端请求封装会读取 JSON。
错误响应保持当前前端兼容格式:
```json
{
"message": "模块不存在或无权限操作",
"code": "MODULE_CONFIG_FORBIDDEN",
"details": {}
}
```
## 后端实现重点
WonderQ-Admin 后端实现页面模块配置接口时,需要把 `map``campaigns` 作为正式模块接入,而不是只在前端展示:
- 模块白名单必须包含 `heroSlides``destinations``map``themes``campaigns``routeSections``hotelGroups``vehicleOptions``ctaBanners`
- 权限校验、模块路由、服务层分发和数据模型映射都必须识别 `map``campaigns`,否则前端会收到 `MODULE_CONFIG_FORBIDDEN` 并以 toast 展示失败原因。
- `GET /api/admin/site-config` 即使没有地图数据,也必须返回 `map: []`,不要省略 `map` 字段。
- `GET /api/admin/site-config` 即使没有特价优惠数据,也必须返回 `campaigns: []`,不要省略 `campaigns` 字段。
- `GET /api/admin/site-config` 必须返回 `routeSections`,包含未启用分组和后台已配置的全部商品 ID。`hotelGroups``vehicleOptions` 也必须稳定返回数组,无数据时返回 `[]`
- `map` 只维护一张图片,只需要支持查询、创建、更新、删除,不需要排序接口。
- `campaigns` 维护活动元信息,只需要支持查询、创建、更新、删除,不需要排序接口。
- `routeSections` 允许按运营任务新增多个子分组;已创建分组支持更新字段、商品关联、删除和分组顺序。`hotelGroups``vehicleOptions` 支持新增、更新、删除和排序。
- 同一商品不能同时出现在多个 `routeSections` 子分组;更新 `productIds` 时后端需要校验互斥。
- 图片上传仍走 `POST /api/admin/media-assets/upload`,模块保存接口只接收上传结果里的 OSS `url` 字段并写入 `image`
## 类型定义
```ts
type SiteModule = "heroSlides" | "destinations" | "map" | "themes" | "campaigns" | "routeSections" | "hotelGroups" | "vehicleOptions" | "ctaBanners";
type SiteItemPatch = {
title?: string;
subtitle?: string | null;
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;
productIds?: string[];
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>;
type RouteSection = {
id: string;
title: string;
subtitle: string | null;
productIds: string[];
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
};
type SiteCardItem = {
id: string;
title: string;
description: string | null;
image: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
};
type HotelGroupItem = SiteCardItem & {
coverImage?: string | null;
priceAmount?: number | null;
priceUnit?: string | null;
tags?: string[];
status?: "draft" | "published";
};
type CtaBanner = {
id: string;
alt: string;
image: string;
targetType: string | null;
targetValue: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
};
```
各模块字段要求:
| 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` |
| `routeSections` | `title` | `subtitle``productIds``isActive``sortOrder` |
| `hotelGroups` | `title` | `description``image``coverImage``priceAmount``priceUnit``tags``status``isActive``sortOrder` |
| `vehicleOptions` | `title` | `description``image``isActive``sortOrder` |
| `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
新增顶部轮播:
```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
```
## 特价优惠 `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
新增特价优惠:
```http
POST /api/admin/site-config/campaigns
```
请求体:
```json
{
"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
}
```
更新特价优惠:
```http
PATCH /api/admin/site-config/campaigns/:id
```
删除特价优惠:
```http
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 }`
## 精选线路子分组 `routeSections` 专用契约
`routeSections` 对应首页“精选线路”的运营分组。运营可按任务新增分组,并维护分组标题、副文案、启用状态、分组顺序、关联线路商品和商品顺序。
字段语义:
| 字段 | 类型 | 更新 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 不允许修改 | 系统生成分组 ID |
| `title` | `string` | 可选 | 分组标题,不能为空 |
| `subtitle` | `string \| null` | 可选 | 分组副文案 |
| `productIds` | `string[]` | 可选 | 关联商品 ID按数组顺序展示 |
| `isActive` | `boolean` | 可选 | 用户侧是否展示该分组 |
| `sortOrder` | `number` | 可选 | 分组展示顺序 |
更新分组:
```http
POST /api/admin/site-config/routeSections
```
请求体示例:
```json
{
"title": "经典人文打卡线路",
"subtitle": "黄果树、荔波小七孔、千户苗寨、镇远古城、梵净山一次串联"
}
```
响应状态码 `201`,返回创建后的子分组。`id` 由后端生成;精选线路分组按运营任务动态新增,不再限制为固定三组,也不再使用 `routes` / `routes-outdoor` / `routes-mix` 作为固定槽位。
```http
PATCH /api/admin/site-config/routeSections/:id
```
请求体示例:
```json
{
"title": "经典人文打卡线路",
"subtitle": "黄果树、荔波小七孔、千户苗寨、镇远古城、梵净山一次串联",
"isActive": true,
"productIds": ["product-uuid-1", "product-uuid-2"]
}
```
调整分组顺序:
```http
PATCH /api/admin/site-config/routeSections/reorder
```
约束:
- `productIds` 中的商品必须存在。
- 同一商品不能出现在其他精选线路子分组;冲突时返回 `409 ROUTE_SECTION_PRODUCT_CONFLICT`
- `DELETE /api/admin/site-config/routeSections/:id` 删除分组配置并返回被删除 ID只移除首页分组不删除关联商品本体。
## 特色酒店 `hotelGroups` 专用契约
`hotelGroups` 对应首页“特色酒店”卡片。它复用“特价优惠”的数据配置体验,支持新增、编辑、删除、排序,并维护标题、描述、价格、标签、封面图、启用状态;不维护商品详情和商品关联。
字段语义:
| 字段 | 类型 | 创建 | 更新 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `string` | 后端生成 | 不允许修改 | 酒店卡片配置项 ID |
| `title` | `string` | 必填 | 可选 | 酒店卡片标题,保存时去掉首尾空格,不能为空 |
| `description` | `string \| null` | 可选 | 可选 | 酒店卡片描述,空字符串可归一为 `null` |
| `image` | `string \| null` | 可选 | 可选 | 兼容旧字段;后端应与 `coverImage` 保持一致 |
| `coverImage` | `string \| null` | 可选 | 可选 | 酒店封面图主字段,管理端上传组件写入该字段 |
| `priceAmount` | `number \| null` | 可选 | 可选 | 价格数值 |
| `priceUnit` | `string \| null` | 可选 | 可选 | 价格单位文案,默认建议 `起/晚` |
| `tags` | `string[]` | 可选 | 可选 | 标签数组,最多 3 个,保存时去掉空标签 |
| `status` | `"draft" \| "published"` | 可选 | 可选 | 发布状态;管理端“前台启用”开关同步维护该字段 |
| `isActive` | `boolean` | 可选 | 可选 | 前台启用状态Public API 只返回 `status="published"``isActive=true` 的项 |
| `sortOrder` | `number` | 可选 | 可选 | 首页展示顺序 |
```http
POST /api/admin/site-config/hotelGroups
PATCH /api/admin/site-config/hotelGroups/:id
DELETE /api/admin/site-config/hotelGroups/:id
PATCH /api/admin/site-config/hotelGroups/reorder
```
删除只删除首页酒店卡片配置不删除任何商品、目的地或素材库资源。Public API 只返回 `published` 且启用项,并按 `sortOrder` 升序输出;无启用项时 MiniAPP 使用本地 `src/content.ts` 兜底内容。
## 万趣用车 `vehicleOptions` 专用契约
`vehicleOptions` 对应首页“万趣用车”卡片,仍是普通首页内容卡片,不维护商品详情和商品关联。
字段语义:
| 字段 | 类型 | 创建 | 更新 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `string` | 后端生成 | 不允许修改 | 用车卡片配置项 ID |
| `title` | `string` | 必填 | 可选 | 卡片标题,保存时去掉首尾空格,不能为空 |
| `description` | `string \| null` | 可选 | 可选 | 卡片描述,空字符串可归一为 `null` |
| `image` | `string \| null` | 可选 | 可选 | 卡片封面图 URL |
| `isActive` | `boolean` | 可选 | 可选 | 前台启用状态 |
| `sortOrder` | `number` | 可选 | 可选 | 首页展示顺序 |
| `createdAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
| `updatedAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
```http
POST /api/admin/site-config/vehicleOptions
PATCH /api/admin/site-config/vehicleOptions/:id
DELETE /api/admin/site-config/vehicleOptions/:id
PATCH /api/admin/site-config/vehicleOptions/reorder
```
删除只删除首页用车卡片配置不删除任何商品、目的地或素材库资源。Public API 只返回启用项,并按 `sortOrder` 升序输出;无启用项时 MiniAPP 使用本地 `src/content.ts` 兜底内容。
## 更多服务 `ctaBanners` 专用契约
`ctaBanners` 对应首页“更多服务”模块,用于维护权益、管家、目的地和需求入口等服务卡片。管理端复用顶部轮播的列表式操作:新增卡片、编辑标题和背景图、删除卡片、上移/下移排序,以及控制前台启用状态。
字段语义:
| 字段 | 类型 | 创建 | 更新 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `string` | 后端生成 | 不允许修改 | 服务卡片唯一 id |
| `alt` | `string` | 必填 | 可选 | 服务标题,展示在“更多服务”卡片上,提交时 trim 后不能为空 |
| `image` | `string \| null` | 可选 | 可选 | 服务卡片背景图 OSS URL上传仍走媒体接口 |
| `targetType` | `string \| null` | 可选 | 可选 | 点击目标类型,例如权益、管家、目的地或需求入口 |
| `targetValue` | `string \| null` | 可选 | 可选 | 点击目标值;无额外参数时可为空 |
| `isActive` | `boolean` | 可选 | 可选 | 用户侧是否展示;未传默认 `true` |
| `sortOrder` | `number` | 可选 | 可选 | 展示顺序;未传时追加到模块末尾 |
复用通用接口:
```http
POST /api/admin/site-config/ctaBanners
PATCH /api/admin/site-config/ctaBanners/:id
DELETE /api/admin/site-config/ctaBanners/:id
PATCH /api/admin/site-config/ctaBanners/reorder
```
删除只删除首页“更多服务”卡片配置不删除任何素材库资源。Public API 只返回启用项,并按 `sortOrder` 升序输出;无启用项时 MiniAPP 使用本地 `src/content.ts` 兜底内容。
## 接口列表
以下路径由九类页面模块复用;`heroSlides``map``campaigns``routeSections``hotelGroups``vehicleOptions``ctaBanners` 的请求体和响应体以各自专用契约为准。
### 获取完整站点配置
```http
GET /api/admin/site-config
```
响应:
```ts
type SiteConfig = {
heroSlides: HeroSlide[];
destinations: Destination[];
map: MapImage[];
themes: ThemeCard[];
campaigns: Campaign[];
routeSections: RouteSection[];
hotelGroups: HotelGroupItem[];
vehicleOptions: SiteCardItem[];
ctaBanners: CtaBanner[];
};
```
`map` 字段必须稳定返回数组;无数据时返回空数组 `[]`
`campaigns` 字段必须稳定返回数组;无数据时返回空数组 `[]`
`routeSections` 字段必须稳定返回数组;无数据时返回 `[]`,由管理端通过“新增”逐个创建子分组。`hotelGroups``vehicleOptions` 字段也必须稳定返回数组;无数据时返回 `[]`
### 新增模块配置项
```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``campaigns` 模块不提供排序能力。若收到 `PATCH /api/admin/site-config/map/reorder``PATCH /api/admin/site-config/campaigns/reorder`,后端应返回 `400``405`,不要创建任何排序数据。`routeSections``hotelGroups``vehicleOptions` 支持排序,但 `itemIds` 必须刚好包含当前已保存的同模块配置项 ID。
响应状态码 `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``coverImage`
```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` 上传。