chore: clean up unused docs and stray system files

Remove deprecated project documentation files and accidental system-committed binary files from the repository.
This commit is contained in:
duanshuwen
2026-07-04 10:50:35 +08:00
parent f7401d8dc6
commit f1fed91675
6 changed files with 0 additions and 1366 deletions

152
AGENTS.md
View File

@@ -1,152 +0,0 @@
# WonderQ-Admin-UI Agent 工作说明
## 项目定位
WonderQ-Admin-UI 是独立的 WonderQ 后台管理前端,用于维护小程序/H5 前台的首页结构、目的地、线路商品、活动入口和客户需求线索。
当前项目是一个轻量单页后台应用,技术栈为 Vite + React + TypeScript + Tailwind CSS 4 + shadcn/ui 风格组件 + 普通 CSS。前端通过 `/api/admin/...` 调用后端管理接口,本地开发时由 Vite 代理到后端服务。
## 目录结构
```text
WonderQ-Admin-UI/
├─ AGENTS.md # Agent 项目规则
├─ README.md # 项目启动说明
├─ components.json # shadcn/ui 组件配置
├─ docs/
│ ├─ README.md # 文档索引
│ └─ admin-backend-plan.md # 后台管理规划文档
├─ public/
│ └─ assets/
│ ├─ guizhou/ # 贵州目的地展示图
│ └─ source/ # 原始/通用静态素材与 manifest
├─ src/
│ ├─ App.tsx # 后台主界面、页面状态、表单和业务 UI
│ ├─ api.ts # Admin API 类型、Token 管理和请求封装
│ ├─ components/
│ │ └─ ui/ # 本地 shadcn/ui 风格基础组件
│ ├─ lib/
│ │ └─ utils.ts # className 合并工具
│ ├─ main.tsx # React 挂载入口
│ ├─ styles.css # Tailwind 入口、设计变量、全局样式与响应式布局
│ └─ vite-env.d.ts # Vite 类型声明
├─ index.html # Vite HTML 入口
├─ package.json # 脚本与依赖
├─ tsconfig.json # TypeScript 严格模式配置
├─ vite.config.ts # Vite/React/本地 API 代理配置
└─ yarn.lock # Yarn 依赖锁文件
```
## 模块分工
- `src/App.tsx`:集中实现登录页、侧边导航、结构维护、首页/目的地维护、商品维护、线索跟进、Toast 提示等后台 UI并优先复用 `src/components/ui/` 基础组件。
- `src/api.ts`:定义 `Product``Destination``Lead``SiteConfig` 等接口类型,封装登录、商品、目的地、站点配置、线索、发布和重置接口。
- `src/components/ui/`:本地 shadcn/ui 风格组件目录,当前包含 Button、Card、Input、Textarea、Badge、Alert、Switch、NativeSelect、Separator 等基础组件。
- `src/lib/utils.ts`:封装 `clsx` + `tailwind-merge``cn` 工具。
- `src/styles.css`Tailwind CSS 4 入口、设计变量、全局布局、后台工作台、表格/表单、商品编辑器、移动端适配等样式。
- `public/assets/`:前台/后台预览用静态图片资源,代码中保留 `/assets/...` 路径引用。
- `docs/`:后台规划与后续协作边界,涉及后端 API 和数据模型时先参考这里,再看后端仓库文档。
## 启动与构建
依赖安装:
```bash
yarn install
```
本仓库使用 Yarn 1 锁文件。Windows 本地构建依赖 Vite/Rolldown、Tailwind Oxide 和 lightningcss 的 native binding`package.json` 中固定了对应 Windows 包;未完整验证前不要随意移除这些依赖。
本地开发:
```bash
yarn dev
```
默认访问地址:
```text
http://localhost:5602
```
本地联调要求:
- 后端 Admin API 默认运行在 `http://localhost:4000`
- `vite.config.ts``/api` 代理到本地后端。
- `vite.config.ts` 同时接入 `@tailwindcss/vite``@/*` 路径别名。
- 如需改 API 地址,优先使用环境变量 `VITE_API_BASE_URL`,不要硬编码生产地址。
类型检查与生产构建:
```bash
yarn build
```
构建产物预览:
```bash
yarn preview
```
默认预览端口为 `5603`
## 测试流程
当前 `package.json` 未配置独立测试脚本。提交前至少执行:
```bash
yarn build
```
该命令会先运行 `tsc --noEmit`,再执行 Vite 生产构建。涉及 UI 布局、交互或接口行为时,还需本地启动页面并手动验证关键流程。
## 部署规范
- 生产发布使用 `yarn build` 生成 `dist/`
- `dist/` 可交给 Nginx、CDN 或静态托管服务部署。
- 生产环境 API 地址必须通过部署环境变量配置,不要提交 `.env``.env.local` 或任何包含密钥的配置。
- 后台接口、数据库、权限、发布/回滚能力属于后端仓库边界,本仓库只维护管理前端。
## 代码风格
- 使用 TypeScript 严格模式,新增代码必须有明确类型,避免 `any`
- React 采用函数组件和 Hooks不引入 class component。
- 遵循现有单文件轻量结构;未获授权前不要主动拆分大型组件或引入状态管理库。
- UI 图标优先复用 `lucide-react`
- 通用按钮、输入框、卡片、提示、徽标、开关等基础控件优先复用 `src/components/ui/`,保持 shadcn/ui 风格一致。
- 样式优先沿用 `src/styles.css` 的设计变量、类名、色彩、间距和 6-10px 圆角习惯;避免绕过组件体系写一套重复按钮/表单样式。
- API 访问统一走 `src/api.ts``request` 封装,不在组件里重复拼接鉴权逻辑。
- 中文文案文件按 UTF-8 处理;修改前确认编辑器编码,避免造成乱码。
## 命名规范
- 组件、类型使用 PascalCase例如 `ProductManager``SiteConfig`
- 函数、变量使用 camelCase例如 `getProducts``selectedId`
- 联合类型字面量使用小写英文,例如 `"draft"``"published"``"leads"`
- CSS class 沿用 kebab-case例如 `.admin-shell``.product-table`
- 组件文件命名沿用 shadcn/ui 习惯,基础组件放在 `src/components/ui/`,工具函数放在 `src/lib/`
- API 类型与后端字段保持同名,避免在前端私自改字段语义。
## 开发准则
- 默认先读相关文件和现有模式,再修改。
- 只处理用户明确要求的范围,不额外加功能。
- 修改前确认是否涉及接口契约、登录鉴权、发布操作或静态资源路径。
- 不展示、不输出、不提交 Token、密码、密钥、真实客户手机号等敏感信息。
- 不新增依赖,除非用户明确授权并说明理由。
- 不提交 `node_modules/``dist/``.env``.env.local`、日志或覆盖率文件。
- 涉及删除、批量移动、重命名资源文件时必须先征得用户明确授权。
## 锁定文件与高风险区域
未经用户明确授权,不得改动:
- `.env``.env.local`、任何生产环境变量或密钥配置。
- `vite.config.ts` 中的代理、端口和构建配置。
- `tsconfig.json` 的严格类型配置。
- `package.json``yarn.lock` 的依赖与脚本。
- `src/api.ts` 的接口路径、Token 存储键、鉴权头和类型契约。
- `public/assets/` 下已被页面引用的图片资源和 `manifest.json`
- `docs/admin-backend-plan.md` 中的后端规划,除非任务明确要求更新文档。
本仓库没有数据库配置文件数据库、Prisma、权限和生产后端配置在后端仓库维护。

View File

@@ -1,8 +0,0 @@
# WonderQ-Admin-UI 文档
本目录存放后台管理前端相关文档。
- `admin-backend-plan.md`:从原 MiniAPP 迁出的后台管理规划,保留管理模块、页面能力和与后端 API 的协作边界。
- `admin-module-config-api.md`:页面模块配置 CRUD 接口契约,供 WonderQ-Admin 后端实现首页轮播、目的地、贵州地图、主题卡、特价优惠和 CTA 等模块配置接口。
后端 API 文档位于 `D:\www\znkj\WonderQ-Admin\docs`

View File

@@ -1,405 +0,0 @@
# H5 后台管理系统规划
## 目标
为当前鸿鹄逸游 H5 项目建设一套可真实上线维护的后台系统,让运营人员可以管理首页内容、目的地、线路产品、活动专题、客户需求、订单线索、消息和客服配置,而不是继续依赖改代码发布。
当前 H5 主要是一个静态 Vite + React 应用:
- 首页配置来自 `src/content.ts`:轮播、目的地、主题卡、线路分区、底部 CTA。
- 线路产品来自 `src/generated-products.json`48 条产品卡片。
- 目的地搜索、活动页、需求表单、消息、订单、客服经理等流程目前集中写在 `src/App.tsx`
- 图片资源在 `public/assets/source`,约 115 个文件。
后台建设的核心不是简单加一个管理页面,而是把这些静态内容拆成稳定的数据模型,并提供发布、审核、回滚、权限、日志和运维能力。
## 推荐架构
### 代码组织
建议从单一前端项目演进为 TypeScript monorepo
```text
apps/
h5/ # 当前 Vite React H5可从现有 src/ 迁移
admin/ # 后台管理端React + Ant Design Pro 或 shadcn/ui
api/ # 后端 APINestJS 或 Fastify
packages/
shared/ # 共享类型、校验 schema、接口 DTO
config/ # eslint/tsconfig 等公共配置
```
保守可落地方案:
- 前台 H5继续 Vite + React先只改数据来源。
- 后台 AdminReact + Ant Design Pro。原因是后台表格、筛选、表单、权限、布局成熟开发快。
- APINestJS + Prisma。原因是 TypeScript 体系一致权限、模块化、OpenAPI、测试更规整。
- 数据库PostgreSQL。
- 缓存/队列Redis先用于发布缓存、短信/通知任务、操作频控。
- 文件存储:阿里 OSS、腾讯 COS 或 S3 兼容对象存储。
- 部署Docker + Nginx + CDN后续可上云托管或 Kubernetes。
### 服务边界
```mermaid
flowchart LR
H5["H5 前台"] --> API["Public API"]
Admin["后台管理端"] --> AdminAPI["Admin API"]
API --> DB[("PostgreSQL")]
AdminAPI --> DB
AdminAPI --> OSS["对象存储"]
AdminAPI --> Redis[("Redis")]
CDN["CDN"] --> H5
CDN --> OSS
```
## 后台功能模块
### 1. 工作台
- 今日新增需求、待跟进需求、待确认订单、上架线路数。
- 最近发布记录、失败任务、异常日志。
- 快捷入口:新建线路、新建活动、发布首页、导出需求。
### 2. 首页装修
对应当前 `src/content.ts`
- 轮播管理:图片、标题、副标题、跳转类型、排序、上下架。
- 锚点分类:探索世界、主题甄选、精选线路、一生一次、鸿鹄稀缺。
- 目的地宫格:名称、图片、跳转关键词、排序、是否推荐。
- 主题卡片:主题名、封面、关联线路或活动专题。
- 线路分区:分区标题图、分区名称、展示线路、排序规则。
- 底部 CTA权益卡、公众号、目的地搜索、需求表单等入口。
- 页脚与品牌图:图片、服务承诺、备案信息。
- 预览与发布:草稿预览、定时发布、回滚上一版。
### 3. 线路产品管理
对应当前 `src/generated-products.json` 和产品详情页。
- 基础信息:标题、副标题、目的地、产品类型、价格、起价单位、标签。
- 封面与图集:列表图、详情头图、行程图片。
- 详情内容:特色、玩法、美食、行程日程、费用包含/不含、预订须知。
- 售卖配置:上架状态、推荐权重、活动归属、库存/席位说明。
- 搜索配置:关键词、别名、目的地关联、主题标签。
- SEO/分享:分享标题、描述、海报图。
- 批量能力:导入、导出、批量上下架、批量改标签。
### 4. 目的地管理
对应当前首页目的地、目的地搜索页和搜索别名逻辑。
- 国家/城市/地区层级。
- 热门目的地、出境/国内分类。
- 搜索别名:例如 `马代 -> 马尔代夫``内蒙 -> 内蒙古`
- 目的地封面图、推荐线路、活动专题绑定。
- 排序和上下架。
### 5. 活动专题管理
对应当前端午甄选、早鸟尊享、鸿鹄逸游等活动页。
- 专题标题、封面、介绍文案、活动时间。
- 关联线路列表。
- CTA提交需求、联系客服、跳转线路。
- 发布状态:草稿、待审核、已发布、已下线。
- 合规文案:例如活动免责声明。
### 6. 客户需求管理
对应当前 `DemandPage`,上线后这是最重要的转化入口。
- 表单字段:目的地、手机号、出行时间、预算、人数、备注、来源页面、来源线路。
- 线索状态:新建、已分配、已联系、方案中、已成交、无效。
- 客户经理分配:手动分配、自动轮询分配。
- 跟进记录:电话、微信、备注、下次提醒。
- 防刷手机号频控、验证码、IP 风控、黑名单。
- 导出Excel/CSV按时间、状态、顾问筛选。
### 7. 客户与订单管理
对应当前消息、订单中心的真实业务化。
- 客户档案:手机号、姓名、微信、偏好、历史需求。
- 订单/方案:关联客户、线路、顾问、出行时间、人数、报价、状态。
- 状态流转:需求单 -> 方案 -> 合同/支付 -> 出行中 -> 已完成/售后。
- 附件:合同、方案 PDF、发票、签证材料。
- 订单备注和操作日志。
### 8. 消息与客服配置
- 后台通知:需求分配、订单状态变化、发布失败。
- 前台消息:活动通知、顾问消息、订单提醒。
- 客服入口:在线联系、电话、服务时间、企业微信/IM 配置。
- 短信:验证码、需求提交确认、顾问跟进提醒。
### 9. 媒体库
- 图片上传、裁剪、压缩、WebP 转换。
- 图片分组:轮播、目的地、线路、活动、品牌。
- 文件引用关系:防止删除正在使用的图片。
- CDN URL、缩略图、多尺寸版本。
### 10. 权限与审计
- 角色:超级管理员、运营、产品、客服主管、客户经理、只读审计。
- 权限粒度:菜单权限、按钮权限、数据范围权限。
- 审核流:重要内容先提交审核再发布。
- 操作日志:谁在什么时候改了什么,从什么值改到什么值。
- 登录安全:强密码、二次验证可选、登录 IP 记录。
## 核心数据模型
第一期建议覆盖这些表:
| 表 | 用途 |
| --- | --- |
| `admin_users` | 后台用户 |
| `roles` / `permissions` | RBAC 权限 |
| `media_assets` | 图片和文件资源 |
| `site_versions` | 首页/站点配置发布版本 |
| `home_sections` | 首页模块配置 |
| `hero_slides` | 首页轮播 |
| `destinations` | 目的地 |
| `map_images` | 贵州地图单图配置 |
| `destination_aliases` | 搜索别名 |
| `themes` | 主题甄选 |
| `products` | 线路产品 |
| `product_images` | 产品图片 |
| `product_itineraries` | 行程日程 |
| `product_fee_items` | 费用说明 |
| `campaigns` | 活动专题 |
| `campaign_products` | 专题与线路关联 |
| `leads` | 客户需求/线索 |
| `lead_followups` | 跟进记录 |
| `customers` | 客户档案 |
| `orders` | 订单/方案 |
| `messages` | 前台消息 |
| `audit_logs` | 操作审计 |
### 示例:产品表
```sql
products (
id uuid primary key,
title text not null,
subtitle text,
destination_id uuid,
price_amount integer,
price_unit varchar(20),
tags text[],
cover_asset_id uuid,
summary text,
status varchar(20), -- draft, published, archived
sort_weight integer default 0,
published_at timestamptz,
created_at timestamptz not null,
updated_at timestamptz not null
)
```
### 示例:线索表
```sql
leads (
id uuid primary key,
destination text,
phone varchar(30) not null,
travel_date date,
people_count integer,
budget_min integer,
budget_max integer,
source_page text,
source_product_id uuid,
status varchar(30) not null,
assigned_user_id uuid,
created_at timestamptz not null,
updated_at timestamptz not null
)
```
## API 设计
### Public API 给 H5 使用
- `GET /api/public/site-config`:首页配置、模块排序、底部导航。
- `GET /api/public/products`:线路列表,支持目的地、主题、活动、关键词筛选。
- `GET /api/public/products/:id`:线路详情。
- `GET /api/public/destinations`:目的地和热门分类。
- `GET /api/public/campaigns/:slug`:活动专题详情。
- `POST /api/public/leads`:提交出行需求。
- `POST /api/public/sms/send-code`:发送验证码。
### Admin API 给后台使用
- `POST /api/admin/auth/login`
- `GET /api/admin/dashboard`
- `CRUD /api/admin/products`
- `CRUD /api/admin/destinations`
- `CRUD /api/admin/campaigns`
- `CRUD /api/admin/site-config/:module`:页面模块配置,覆盖 `heroSlides``destinations``map``themes``campaigns``ctaBanners`
- `CRUD /api/admin/media-assets`
- `GET /api/admin/leads`
- `PATCH /api/admin/leads/:id/status`
- `POST /api/admin/leads/:id/followups`
- `CRUD /api/admin/orders`
- `GET /api/admin/audit-logs`
- `POST /api/admin/publish`
- `POST /api/admin/rollback`
## 前台改造路径
### 第一步:数据层抽离
先不大改 UI把硬编码内容替换成数据适配层。
- 新建 `src/api/client.ts``src/api/types.ts`
- 新建 `src/adapters/siteConfig.ts`,把接口数据转成当前组件需要的结构。
- 保留本地 JSON fallback便于本地开发和接口故障降级。
-`heroSlides``destinations``map``themeCards``campaigns``sectionHeaders``bottomCtas` 从静态 import 改成接口加载。
### 第二步:产品和目的地接口化
- `generated-products.json` 改为接口 seed 数据。
- 搜索逻辑从前端 `keywordAliases` 迁移到后端。
- 产品详情页从 `getDetailMeta` 这类本地推断改为真实详情字段。
### 第三步:需求表单真实提交
- `DemandPage` 接入 `POST /api/public/leads`
- 增加手机号校验、验证码、提交成功页。
- 后台线索列表能看到来源页面和来源产品。
### 第四步:消息与订单真实化
- 登录态、客户身份、订单列表需要独立规划。
- 如果短期不做会员体系,可先保留“提交需求后的查询链接/手机号验证码查询”。
## 发布与运维
### 发布策略
- 内容发布与代码发布分离。
- 运营修改内容后生成草稿版本。
- 审核通过后写入 `site_versions`Public API 默认返回当前发布版本。
- CDN 缓存使用短 TTL 或发布后主动刷新。
- 每次发布可回滚到上一版本。
### 环境
| 环境 | 用途 |
| --- | --- |
| local | 本地开发 |
| dev | 联调环境 |
| staging | 预发布,连接准生产数据或脱敏数据 |
| production | 正式环境 |
### 监控与备份
- API 错误率、响应时间、数据库连接数。
- 需求提交成功率、短信发送成功率。
- 每日数据库自动备份,至少保留 14 到 30 天。
- 对象存储开启版本控制或回收站。
- 关键操作日志不可被普通管理员删除。
### 安全
- 后台必须 HTTPS。
- 管理端登录限流。
- 所有 Admin API 校验权限。
- Public API 做参数校验和频控。
- 手机号等隐私信息后台脱敏展示,导出需要高权限。
- 防止任意文件上传:限制 MIME、大小、后缀图片重新编码。
## 分阶段交付计划
### Phase 1后台 MVP约 2 到 3 周
目标运营可以维护首页和线路H5 可以读取接口。
- 搭建 monorepo、API、Admin、数据库。
- 建立媒体库、产品、目的地、首页配置数据表。
- 写 seed 脚本,把当前 `content.ts``generated-products.json` 导入数据库。
- 后台完成登录、产品列表/编辑、首页配置、图片上传。
- H5 接入 `site-config``products`,保留本地 fallback。
### Phase 2线索闭环约 2 周
目标:出行需求可以真实提交、分配、跟进。
- 需求表单接口。
- 后台线索列表、状态流转、跟进记录。
- 客户经理分配。
- 短信验证码和提交通知。
- 导出和基础统计。
### Phase 3活动和发布体系约 2 到 3 周
目标:专题页和首页可以安全发布。
- 活动专题管理。
- 草稿/审核/发布/回滚。
- 发布日志和操作审计。
- CDN 刷新。
- 预发布环境验收。
### Phase 4订单、客户和消息约 3 到 5 周
目标:从线索进一步进入服务履约。
- 客户档案。
- 订单/方案管理。
- 前台消息和订单查询。
- 附件上传。
- 顾问工作台。
### Phase 5生产加固持续迭代
目标:稳定上线维护。
- 监控告警、备份恢复演练。
- 权限细化、数据脱敏。
- 性能优化和缓存。
- 自动化测试、CI/CD。
- 数据分析看板。
## MVP 优先级
必须先做:
1. 后台登录与权限。
2. 媒体库。
3. 线路产品管理。
4. 首页配置管理。
5. H5 读取接口并保留 fallback。
6. 需求表单真实提交。
7. 线索后台跟进。
8. 发布、回滚、操作日志。
可以后做:
1. 完整会员登录。
2. 真实支付。
3. 复杂订单履约。
4. 企业微信深度集成。
5. 多语言/多品牌站点。
## 近期可执行清单
1. 建立 `apps/h5``apps/admin``apps/api` 结构。
2. 设计 Prisma schema并先覆盖产品、首页、媒体、目的地、线索。
3. 编写 seed 脚本,把现有静态内容导入数据库。
4. 做 Public API先返回与当前 H5 兼容的数据。
5. 改造 H5 数据加载层,保证 UI 不变。
6. 做 Admin 的产品管理和首页装修。
7. 接通需求表单和后台线索列表。
8. 补充发布版本、回滚、审计日志。
## 风险与建议
- 不建议一开始就做完整电商订单和支付,旅游定制业务更适合先做线索和顾问跟进闭环。
- 不建议让后台直接编辑任意 JSON短期快但长期容易把数据结构搞乱。
- 不建议 H5 只依赖实时接口,首页配置应支持发布版本和缓存,避免后台故障影响前台展示。
- 不建议图片继续只放在代码仓库里,真实上线后要进入对象存储和 CDN。
- 如果预算有限,第一期可以先用单台云服务器 + PostgreSQL + 对象存储但备份、HTTPS、日志和权限不能省。

View File

@@ -1,801 +0,0 @@
# 页面模块配置 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` 上传。