1045 lines
40 KiB
Markdown
1045 lines
40 KiB
Markdown
# 页面模块配置 Admin API 契约
|
||
|
||
本文档定义 WonderQ-Admin 后端需要为 WonderQ-Admin-UI 实现的页面模块配置 CRUD 接口。接口用于维护小程序/H5 前台页面模块中的配置数据,例如首页轮播、目的地宫格、贵州地图、主题卡片、特价优惠、精选线路分组、特色酒店、万趣用车和更多服务。
|
||
|
||
## 适用模块
|
||
|
||
当前一期只纳入结构化页面配置模块:
|
||
|
||
| module | 名称 | 用途 |
|
||
| -------------- | ------------ | ---------------------------------------------- |
|
||
| `heroSlides` | 顶部轮播 | 首页首屏轮播图、标题短文案、展示排序和启用状态 |
|
||
| `destinations` | 目的地 | 首页/目的地页展示、搜索入口和热门标记 |
|
||
| `destinationHero` | 目的地页顶部主视觉 | 目的地页顶部大图、标题、小标题、启用状态和排序 |
|
||
| `destinationRegions` | 目的地页热门区域 | 目的地页“热门区域”快捷入口、搜索关键词和景点摘要 |
|
||
| `demandHero` | 需求页顶部定制说明 | 需求页顶部标题、副标题、说明文案和步骤 |
|
||
| `demandFeatureCards` | 需求页服务说明卡 | 需求页服务说明卡标题、描述、启用状态和排序 |
|
||
| `demandForm` | 需求页表单配置 | 固定表单字段标签、占位符、提交按钮和快捷选项 |
|
||
| `demandRecommendations` | 需求页热门推荐线路 | 需求页推荐标题、副文案和商品 ID 关联顺序 |
|
||
| `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`、`destinationHero`、`destinationRegions`、`demandHero`、`demandFeatureCards`、`demandForm`、`demandRecommendations`、`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` 必须返回 `destinationHero`、`destinationRegions`、`demandHero`、`demandFeatureCards`、`demandForm`、`demandRecommendations`、`routeSections`,包含未启用配置。`hotelGroups`、`vehicleOptions` 也必须稳定返回数组,无数据时返回 `[]`。
|
||
- `map` 只维护一张图片,只需要支持查询、创建、更新、删除,不需要排序接口。
|
||
- `campaigns` 维护活动元信息,只需要支持查询、创建、更新、删除,不需要排序接口。
|
||
- `destinationHero`、`destinationRegions`、`demandHero`、`demandFeatureCards`、`demandRecommendations`、`routeSections` 允许按运营任务新增多项;已创建配置支持更新字段、删除和分组顺序。`demandForm` 是单例配置,只支持一条表单配置,不提供排序。`hotelGroups`、`vehicleOptions` 支持新增、更新、删除和排序。
|
||
- 同一商品不能同时出现在多个 `routeSections` 子分组;更新 `productIds` 时后端需要校验互斥。
|
||
- 图片上传仍走 `POST /api/admin/media-assets/upload`,模块保存接口只接收上传结果里的 OSS `url` 字段并写入 `image`。
|
||
|
||
## 类型定义
|
||
|
||
```ts
|
||
type SiteModule = "heroSlides" | "destinations" | "destinationHero" | "destinationRegions" | "demandHero" | "demandFeatureCards" | "demandForm" | "demandRecommendations" | "map" | "themes" | "campaigns" | "routeSections" | "hotelGroups" | "vehicleOptions" | "ctaBanners";
|
||
|
||
type SiteItemPatch = {
|
||
title?: string;
|
||
subtitle?: string | null;
|
||
kicker?: string | null;
|
||
name?: string;
|
||
slug?: string;
|
||
region?: string | null;
|
||
label?: string;
|
||
keyword?: string | null;
|
||
spots?: string | null;
|
||
alt?: string;
|
||
image?: string | null;
|
||
description?: string | null;
|
||
coverImage?: string | null;
|
||
steps?: string[];
|
||
destinationLabel?: string;
|
||
destinationPlaceholder?: string | null;
|
||
phoneLabel?: string;
|
||
phonePlaceholder?: string | null;
|
||
noteLabel?: string;
|
||
notePlaceholder?: string | null;
|
||
submitLabel?: string;
|
||
chips?: string[];
|
||
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 DestinationHero = {
|
||
id: string;
|
||
title: string;
|
||
kicker: string | null;
|
||
image: string | null;
|
||
isActive: boolean;
|
||
sortOrder: number;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
};
|
||
|
||
type DestinationRegion = {
|
||
id: string;
|
||
label: string;
|
||
keyword: string | null;
|
||
spots: string | null;
|
||
isActive: boolean;
|
||
sortOrder: number;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
};
|
||
|
||
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` |
|
||
| `destinationHero` | `title` | `kicker`、`image`、`isActive`、`sortOrder` |
|
||
| `destinationRegions` | `label` | `keyword`、`spots`、`isActive`、`sortOrder` |
|
||
| `demandHero` | `title` | `kicker`、`description`、`steps`、`isActive`、`sortOrder` |
|
||
| `demandFeatureCards` | `title` | `description`、`isActive`、`sortOrder` |
|
||
| `demandForm` | `submitLabel` | `destinationLabel`、`destinationPlaceholder`、`phoneLabel`、`phonePlaceholder`、`noteLabel`、`notePlaceholder`、`chips`、`isActive` |
|
||
| `demandRecommendations` | `title` | `subtitle`、`productIds`、`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
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## 目的地页主视觉 `destinationHero` 专用契约
|
||
|
||
`destinationHero` 对应 MiniAPP 目的地页顶部大图和标题文案。后台允许维护多条主视觉配置;Public API 只输出启用项,MiniAPP 按 `sortOrder` 升序取第一条渲染顶部主视觉,无有效配置时回退本地静态主视觉。
|
||
|
||
字段语义:
|
||
|
||
| 字段 | 类型 | 创建 | 更新 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `string` | 后端生成 | 不允许修改 | 主视觉配置项唯一 ID |
|
||
| `title` | `string` | 必填 | 可选 | 主视觉标题,不能为空 |
|
||
| `kicker` | `string \| null` | 可选 | 可选 | 标题上方小文案 |
|
||
| `image` | `string \| null` | 可选 | 可选 | 主视觉图片 URL;为空时客户端使用兜底图 |
|
||
| `sortOrder` | `number` | 可选 | 可选 | 展示顺序,未传时追加到末尾 |
|
||
| `isActive` | `boolean` | 可选 | 可选 | 是否前台启用,创建默认 `true` |
|
||
| `createdAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
|
||
| `updatedAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
|
||
|
||
接口:
|
||
|
||
```http
|
||
POST /api/admin/site-config/destinationHero
|
||
PATCH /api/admin/site-config/destinationHero/:id
|
||
DELETE /api/admin/site-config/destinationHero/:id
|
||
PATCH /api/admin/site-config/destinationHero/reorder
|
||
```
|
||
|
||
创建请求示例:
|
||
|
||
```json
|
||
{
|
||
"title": "山水、苗寨、古城与野咖同程安排",
|
||
"kicker": "贵州小包团目的地",
|
||
"image": "/assets/guizhou/huangguoshu-waterfall.jpg",
|
||
"isActive": true,
|
||
"sortOrder": 0
|
||
}
|
||
```
|
||
|
||
## 目的地页热门区域 `destinationRegions` 专用契约
|
||
|
||
`destinationRegions` 对应 MiniAPP 目的地页“热门区域”快捷入口。该模块只维护区域卡片文本和搜索关键词,不绑定商品本体;点击后的商品命中仍由 `/api/public/products?keyword=<keyword>` 及商品目的地、标题、标签、别名匹配完成。
|
||
|
||
字段语义:
|
||
|
||
| 字段 | 类型 | 创建 | 更新 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `string` | 后端生成 | 不允许修改 | 热门区域配置项唯一 ID |
|
||
| `label` | `string` | 必填 | 可选 | 区域卡片名称,不能为空 |
|
||
| `keyword` | `string \| null` | 可选 | 可选 | 点击搜索关键词;为空时 MiniAPP 使用 `label` |
|
||
| `spots` | `string \| null` | 可选 | 可选 | 卡片下方景点摘要 |
|
||
| `sortOrder` | `number` | 可选 | 可选 | 展示顺序,未传时追加到末尾 |
|
||
| `isActive` | `boolean` | 可选 | 可选 | 是否前台启用,创建默认 `true` |
|
||
| `createdAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
|
||
| `updatedAt` | `string` | 后端生成 | 后端维护 | ISO 时间字符串,可选返回 |
|
||
|
||
接口:
|
||
|
||
```http
|
||
POST /api/admin/site-config/destinationRegions
|
||
PATCH /api/admin/site-config/destinationRegions/:id
|
||
DELETE /api/admin/site-config/destinationRegions/:id
|
||
PATCH /api/admin/site-config/destinationRegions/reorder
|
||
```
|
||
|
||
创建请求示例:
|
||
|
||
```json
|
||
{
|
||
"label": "黔南",
|
||
"keyword": "黔南荔波",
|
||
"spots": "荔波小七孔、茂兰",
|
||
"isActive": true,
|
||
"sortOrder": 2
|
||
}
|
||
```
|
||
|
||
## 目的地 `destinations` 专用契约
|
||
|
||
目的地模块对应 MiniAPP 首页和目的地页的目的地卡片,也为商品维护页提供目的地下拉。目的地只维护字典数据和前台入口,不直接维护商品详情;商品是否进入目的地搜索结果由商品本体的 `destinationId` 和 `status` 决定。
|
||
|
||
字段语义:
|
||
|
||
| 字段 | 类型 | 创建 | 更新 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `string` | 后端生成 | 不允许修改 | 目的地配置项唯一 id |
|
||
| `name` | `string` | 必填 | 可选 | 目的地名称,保存时去掉首尾空格,不能为空;Public API 仍返回 `name`,MiniAPP 会归一化为页面内部 `label` |
|
||
| `slug` | `string` | 可选 | 可选 | 目的地技术标识;创建时为空由后端按 `name` 自动生成,更新时传入空字符串应返回 `422` |
|
||
| `region` | `string \| null` | 可选 | 可选 | 所属区域,可为空 |
|
||
| `image` | `string \| null` | 可选 | 可选 | 目的地卡片背景图 URL |
|
||
| `isHot` | `boolean` | 可选 | 可选 | 是否热门;首页和目的地页可据此展示热门优先内容 |
|
||
| `isActive` | `boolean` | 可选 | 可选 | 前台启用状态;Public API 只返回启用目的地 |
|
||
| `sortOrder` | `number` | 可选 | 可选 | 前台展示顺序;Public API 按该字段升序输出 |
|
||
| `aliases` | `Array<{ id: string; alias: string }>` | 后端返回 | 当前接口不维护 | 搜索别名;当前 Admin UI 只展示,别名维护不在本次目的地页 CRUD 范围 |
|
||
| `_count.products` | `{ products: number }` | 后端返回 | 不允许修改 | Admin `GET /api/admin/destinations` 可返回,用于判断目的地是否仍被商品引用 |
|
||
|
||
### 目的地 CRUD
|
||
|
||
新增目的地:
|
||
|
||
```http
|
||
POST /api/admin/site-config/destinations
|
||
```
|
||
|
||
请求体:
|
||
|
||
```json
|
||
{
|
||
"name": "新目的地",
|
||
"slug": "",
|
||
"region": "黔南",
|
||
"image": "/assets/guizhou/libo-xiaoqikong.jpg",
|
||
"isHot": true,
|
||
"isActive": true,
|
||
"sortOrder": 0
|
||
}
|
||
```
|
||
|
||
响应状态码 `201`,响应体返回创建后的完整 `Destination`。创建时 `slug` 可以为空字符串,后端会按 `name` 自动生成;`isHot` 未传默认 `false`,`isActive` 未传默认 `true`,`sortOrder` 未传时追加到当前模块末尾。
|
||
|
||
更新目的地:
|
||
|
||
```http
|
||
PATCH /api/admin/site-config/destinations/:id
|
||
```
|
||
|
||
请求体为目的地字段的部分对象,只修改请求体中出现的字段。`name` 和 `slug` 如果出现在请求体中,保存后不能为空。
|
||
|
||
删除目的地:
|
||
|
||
```http
|
||
DELETE /api/admin/site-config/destinations/:id
|
||
```
|
||
|
||
响应状态码 `200`:
|
||
|
||
```json
|
||
{
|
||
"id": "dest_001"
|
||
}
|
||
```
|
||
|
||
删除只删除省内目的地配置项,不删除商品本体、图片素材或线索记录。若仍有商品引用该目的地,后端返回 `409 MODULE_CONFIG_CONFLICT`;运营应先在商品维护页将相关商品归档或解除 `destinationId` 关联。
|
||
|
||
调整目的地顺序:
|
||
|
||
```http
|
||
PATCH /api/admin/site-config/destinations/reorder
|
||
```
|
||
|
||
请求体:
|
||
|
||
```json
|
||
{
|
||
"itemIds": ["dest_002", "dest_001", "dest_003"]
|
||
}
|
||
```
|
||
|
||
约束:
|
||
|
||
- `itemIds` 必须刚好包含当前全部目的地 id。
|
||
- 不允许重复 id,不允许混入其他模块 id。
|
||
- 后端按数组顺序写入 `sortOrder`,从 0 开始。
|
||
- Public `GET /api/public/site-config` 只输出启用目的地,并继续按 `sortOrder` 升序返回。
|
||
|
||
## 贵州地图 `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;只移除首页分组,不删除关联商品本体。
|
||
|
||
## 需求页配置专用契约
|
||
|
||
需求页在 Admin UI “需求线索”结构页下维护,模块名称必须与前台区块一一对应:
|
||
|
||
| module | 前台区块 | CRUD 说明 |
|
||
| --- | --- | --- |
|
||
| `demandHero` | 顶部定制说明 | 支持新增、编辑、删除、排序、启停;MiniAPP 取启用列表第一项 |
|
||
| `demandFeatureCards` | 服务说明卡 | 支持新增、编辑、删除、排序、启停 |
|
||
| `demandForm` | 需求表单配置 | 单例数组;支持新增第一条、编辑、删除、启停;不支持排序 |
|
||
| `demandRecommendations` | 热门推荐线路 | 支持新增、编辑、删除、排序、启停;`productIds` 只维护商品关联顺序 |
|
||
|
||
字段语义:
|
||
|
||
| module | 字段 | 说明 |
|
||
| --- | --- | --- |
|
||
| `demandHero` | `title`、`kicker`、`description`、`steps` | 顶部主标题、上方短文案、说明文字和步骤文案;`steps/chips` 保存前 trim 并过滤空字符串 |
|
||
| `demandFeatureCards` | `title`、`description` | 服务说明卡标题和说明 |
|
||
| `demandForm` | `destinationLabel`、`destinationPlaceholder`、`phoneLabel`、`phonePlaceholder`、`noteLabel`、`notePlaceholder`、`submitLabel`、`chips` | 固定表单文案配置,不改变 Public lead 必填字段 |
|
||
| `demandRecommendations` | `title`、`subtitle`、`productIds` | 推荐分组标题、副文案和商品 ID 顺序 |
|
||
|
||
约束:
|
||
|
||
- `demandForm` 重复创建时返回 `409 MODULE_CONFIG_SINGLETON_EXISTS`。
|
||
- `demandRecommendations.productIds` 中的商品必须存在,重复商品返回 `422 DEMAND_RECOMMENDATION_PRODUCT_DUPLICATE`。
|
||
- `demandRecommendations` 不限制商品是否已属于首页 `routeSections`,也不删除商品本体。
|
||
- Public `GET /api/public/site-config` 只返回启用的需求页配置;`demandRecommendations.productIds` 只包含 `status="published"` 的商品 ID。
|
||
- MiniAPP 表单提交仍调用 `POST /api/public/leads`,`sourcePage="demand_page"`,不新增必填字段。
|
||
|
||
## 特色酒店 `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`、`destinationHero`、`destinationRegions`、`demandHero`、`demandFeatureCards`、`demandForm`、`demandRecommendations`、`map`、`campaigns`、`routeSections`、`hotelGroups`、`vehicleOptions`、`ctaBanners` 的请求体和响应体以各自专用契约为准。
|
||
|
||
### 获取完整站点配置
|
||
|
||
```http
|
||
GET /api/admin/site-config
|
||
```
|
||
|
||
响应:
|
||
|
||
```ts
|
||
type SiteConfig = {
|
||
heroSlides: HeroSlide[];
|
||
destinations: Destination[];
|
||
destinationHero: DestinationHero[];
|
||
destinationRegions: DestinationRegion[];
|
||
demandHero: DemandHero[];
|
||
demandFeatureCards: DemandFeatureCard[];
|
||
demandForm: DemandForm[];
|
||
demandRecommendations: DemandRecommendation[];
|
||
map: MapImage[];
|
||
themes: ThemeCard[];
|
||
campaigns: Campaign[];
|
||
routeSections: RouteSection[];
|
||
hotelGroups: HotelGroupItem[];
|
||
vehicleOptions: SiteCardItem[];
|
||
ctaBanners: CtaBanner[];
|
||
};
|
||
```
|
||
|
||
`destinationHero`、`destinationRegions`、`demandHero`、`demandFeatureCards`、`demandForm`、`demandRecommendations`、`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` 和 `demandForm` 模块不提供排序能力。若收到对应 reorder 请求,后端应返回 `400` 或 `405`,不要创建任何排序数据。`destinations`、`destinationHero`、`destinationRegions`、`demandHero`、`demandFeatureCards`、`demandRecommendations`、`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` 上传。
|