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

19 KiB
Raw Blame History

页面模块配置 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 后端实现页面模块配置接口时,需要把 mapcampaigns 作为正式模块接入,而不是只在前端展示:

  • 模块白名单必须包含 heroSlidesdestinationsmapthemescampaignsctaBanners
  • 权限校验、模块路由、服务层分发和数据模型映射都必须识别 mapcampaigns,否则前端会收到 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,模块保存接口只接收上传结果里的 OSS url 字段并写入 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 kickerimageisActivesortOrder
destinations name slugregionimageisHotisActivesortOrder
map image isActive
themes label imagetargetTypetargetValueisActivesortOrder
campaigns titleslug descriptioncoverImagepriceAmountpriceUnittagsstatusstartsAtendsAt
ctaBanners alt imagetargetTypetargetValueisActivesortOrder

后端可以在创建时补全 id、默认 isActive=true、默认 sortOrder=当前模块最后一位campaigns 创建时默认 status="draft",不会进入 Public site-config.campaigns;只有 status="published" 的活动会进入 H5 Public API。 Admin UI 面向运营只展示活动标题、活动描述、封面图和前台启用状态;slug 是接口必填技术标识,前端可在新建时自动生成,编辑时复用原值。

顶部轮播 heroSlides 专用契约

顶部轮播对应当前管理端抽屉中的 3 个区域:

  • 展示内容:titlekicker
  • 资源图片:image
  • 排序与状态:sortOrderisActive

字段语义:

字段 类型 创建 更新 说明
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 的创建、更新、查询响应中不要返回 targetTypetargetValue;兼容期如果请求体携带这两个字段,后端可以忽略,但不要写入顶部轮播业务数据。

顶部轮播 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 的创建、更新、查询响应中不要返回 titlenamelabelalttargetTypetargetValuesortOrder

后端推荐策略: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" 可选 可选 新建默认 draftPublic 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

特价优惠不定义 sortOrderisActivetargetTypetargetValue;兼容期如果请求体携带这些字段,后端可以忽略,但不要写入 Campaign 业务数据。 tags 保存前需要 trim 并过滤空字符串;有效标签超过 3 个时返回 422 MODULE_CONFIG_VALIDATION_ERRORdetails{ "field": "tags", "max": 3 }

接口列表

以下路径由六类页面模块复用;heroSlidesmapcampaigns 的请求体和响应体以各自专用契约为准。

获取完整站点配置

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 开始。

mapcampaigns 模块不提供排序能力。若收到 PATCH /api/admin/site-config/map/reorderPATCH /api/admin/site-config/campaigns/reorder,后端应返回 400405,不要创建任何排序数据。

响应状态码 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 写入对应模块图片字段,例如 imagecoverImage

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 上传。