Files
WonderQ-Project/docs/admin-api-requirements.md
2026-08-11 19:19:11 +08:00

779 lines
23 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.

# WonderQ-Admin-UI Admin API 接口需求
本文档用于指导 `WonderQ-Admin` 后端按当前 `WonderQ-Admin-UI` 管理端完成 Admin API 对接。接口需求来源于前端 `src/api.ts``src/App.tsx` 的实际类型、请求封装和页面调用。
## 范围
- 本文只覆盖当前后台管理前端必需的 Admin API。
- 不包含 H5 Public API、订单、媒体库、审计日志、活动专题等后续规划能力。
- 后端现有 `GET /api/admin/me``GET /api/admin/media-assets` 不在当前 UI 必需范围内。
## 通用约定
- 基础路径:`/api/admin`
- 请求与响应均使用 JSON。
-`POST /api/admin/auth/login` 外,其余接口都需要后台登录态。
- 登录成功后前端会把返回的 `token` 存入本地,并在后续请求头中发送:
```http
Authorization: Bearer <token>
Content-Type: application/json
```
- 日期时间字段返回 ISO 8601 字符串。
- ID 字段按字符串处理。
- 前端错误提示优先读取响应体的 `message` 字段;如后端沿用 FastAPI 默认 `detail`,建议同时兼容输出 `message`,避免管理端展示兜底错误。
## 当前对接状态
| 接口 | 前端依赖 | 后端现状 | 备注 |
| --- | --- | --- | --- |
| `POST /api/admin/auth/login` | 登录页 | 已覆盖 | 返回 token 和 user |
| `GET /api/admin/dashboard` | 客户端已封装 | 已覆盖 | 当前 UI 暂未展示 |
| `GET /api/admin/products` | 商品维护、结构维护 | 已覆盖 | UI 当前只传 `keyword` |
| `POST /api/admin/products` | 新建商品 | 已覆盖 | 返回完整 Product |
| `PATCH /api/admin/products/{id}` | 编辑商品 | 已覆盖 | 返回完整 Product |
| `GET /api/admin/destinations` | 商品目的地下拉、目的地页 | 已覆盖 | 需要返回别名和商品数 |
| `POST /api/admin/site-config/destinations` | 目的地页新增目的地 | 已覆盖 | 新增后返回完整 Destination |
| `PATCH /api/admin/site-config/destinations/{id}` | 目的地页编辑目的地 | 已覆盖 | 支持名称、图片、热门、启停、排序等字段 |
| `DELETE /api/admin/site-config/destinations/{id}` | 目的地页删除目的地 | 已覆盖 | 有商品引用时返回 `409`,不级联删除商品 |
| `PATCH /api/admin/site-config/destinations/reorder` | 目的地页排序 | 已覆盖 | `itemIds` 必须完整且不能重复 |
| `POST /api/admin/site-config/destinationHero` | 目的地页新增顶部主视觉 | 已覆盖 | 返回完整 DestinationHero |
| `PATCH /api/admin/site-config/destinationHero/{id}` | 目的地页编辑顶部主视觉 | 已覆盖 | 支持标题、小标题、图片、启停、排序 |
| `DELETE /api/admin/site-config/destinationHero/{id}` | 目的地页删除顶部主视觉 | 已覆盖 | 删除后重排同模块 `sortOrder` |
| `PATCH /api/admin/site-config/destinationHero/reorder` | 目的地页顶部主视觉排序 | 已覆盖 | MiniAPP 使用启用列表第一项 |
| `POST /api/admin/site-config/destinationRegions` | 目的地页新增热门区域 | 已覆盖 | 返回完整 DestinationRegion |
| `PATCH /api/admin/site-config/destinationRegions/{id}` | 目的地页编辑热门区域 | 已覆盖 | 支持名称、搜索关键词、景点摘要、启停、排序 |
| `DELETE /api/admin/site-config/destinationRegions/{id}` | 目的地页删除热门区域 | 已覆盖 | 删除后重排同模块 `sortOrder` |
| `PATCH /api/admin/site-config/destinationRegions/reorder` | 目的地页热门区域排序 | 已覆盖 | `itemIds` 必须完整且不能重复 |
| `GET /api/admin/site-config` | 首页/目的地/活动/需求页结构维护 | 已覆盖 | 需要包含未启用内容、已保存的 `routeSections``hotelGroups``vehicleOptions``ctaBanners`、需求页配置 |
| `PATCH /api/admin/site-config/{module}/{item_id}` | 模块内容编辑 | 已覆盖 | 模块名需保持一致 |
| `GET /api/admin/leads` | 需求线索页 | 已覆盖 | UI 传 `status``sourcePage``keyword``createdFrom``createdTo``take` |
| `PATCH /api/admin/leads/{id}/status` | 线索状态流转 | 已覆盖 | UI 更新后会重新拉列表 |
| `POST /api/admin/publish` | 结构维护发布 | 已覆盖 | UI 使用 `title` 提示发布结果 |
| `POST /api/admin/reset-guizhou-content` | 贵州内容重置 | 已覆盖 | 高风险操作,需鉴权和审计 |
## 枚举
### ProductStatus
```ts
type ProductStatus = "draft" | "published" | "archived";
```
### LeadStatus
```ts
type LeadStatus = "new" | "assigned" | "contacted" | "planning" | "won" | "invalid";
```
### SiteModule
```ts
type SiteModule = "heroSlides" | "destinations" | "destinationHero" | "destinationRegions" | "demandHero" | "demandFeatureCards" | "demandForm" | "demandRecommendations" | "map" | "themes" | "campaigns" | "routeSections" | "hotelGroups" | "vehicleOptions" | "ctaBanners";
```
## 公共数据结构
### Destination
```ts
type Destination = {
id: string;
name: string;
slug: string;
region?: string | null;
image?: string | null;
isHot: boolean;
isActive: boolean;
sortOrder: number;
aliases?: Array<{ id: string; alias: string }>;
_count?: { products: number };
};
```
### Product
```ts
type Product = {
id: string;
sourceId?: number | null;
title: string;
subtitle?: string | null;
priceAmount?: number | null;
priceUnit: string;
tags: string[];
coverImage?: string | null;
summary?: string | null;
images?: Array<{ id?: string; url: string; alt?: string | null; sortOrder: number }>;
detailSections?: ProductDetailSection[] | null;
status: ProductStatus;
sortWeight: number;
updatedAt: string;
destination?: Destination | null;
destinationId?: string | null;
};
```
### ProductDetailSection
```ts
type ProductDetailBlock =
| { type: "text"; text: string }
| { type: "image"; url: string; alt?: string | null };
type ProductDetailSection = {
key: string;
label: string;
title?: string | null;
blocks: ProductDetailBlock[];
};
```
### ProductInput
`POST /products``PATCH /products/{id}` 复用该结构;`PATCH` 可以只提交需要修改的字段。
```ts
type ProductInput = {
title: string;
subtitle?: string;
destinationId?: string | null;
priceAmount?: number | null;
priceUnit?: string;
tags: string[];
coverImage?: string | null;
summary?: string | null;
images?: Array<{ url: string; alt?: string | null; sortOrder: number }>;
detailSections?: ProductDetailSection[];
status: ProductStatus;
sortWeight: number;
};
```
字段处理要求:
- `title` 必填,后端至少应校验非空;当前后端 schema 为最少 2 个字符。
- `priceAmount` 可为空;不为空时应为大于等于 0 的整数。
- `priceUnit` 为空时后端默认使用 `起/人`
- `images` 保存前按数组顺序重排 `sortOrder`
- `detailSections` 中空 key、空 label、空 blocks 的模块不应保存为有效详情模块。
-`status` 首次变为 `published` 时,后端可写入发布时间。
### Lead
```ts
type Lead = {
id: string;
destination?: string | null;
phone: string;
note?: string | null;
sourcePage?: string | null;
status: LeadStatus;
createdAt: string;
sourceProduct?: { id: string; title: string } | null;
assignedUser?: { id: string; name: string } | null;
};
```
后端可额外返回 `travelDate``peopleCount``budgetMin``budgetMax` 等字段,但以上字段是当前管理端展示所需的最小集合。手机号属于隐私信息,日志、错误和文档示例中不得输出真实号码。
### SiteConfig
```ts
type SiteConfig = {
heroSlides: Array<{
id: string;
title: string;
kicker?: string | null;
image: string;
targetType?: string | null;
targetValue?: string | null;
isActive: boolean;
}>;
destinations: Destination[];
destinationHero: Array<{
id: string;
title: string;
kicker?: string | null;
image?: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
}>;
destinationRegions: Array<{
id: string;
label: string;
keyword?: string | null;
spots?: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
}>;
demandHero: Array<{
id: string;
title: string;
kicker?: string | null;
description?: string | null;
steps: string[];
isActive: boolean;
sortOrder: number;
}>;
demandFeatureCards: Array<{
id: string;
title: string;
description?: string | null;
isActive: boolean;
sortOrder: number;
}>;
demandForm: Array<{
id: string;
destinationLabel: string;
destinationPlaceholder?: string | null;
phoneLabel: string;
phonePlaceholder?: string | null;
noteLabel: string;
notePlaceholder?: string | null;
submitLabel: string;
chips: string[];
isActive: boolean;
}>;
demandRecommendations: Array<{
id: string;
title: string;
subtitle?: string | null;
productIds: string[];
isActive: boolean;
sortOrder: number;
}>;
themes: Array<{
id: string;
label: string;
image: string;
targetType?: string | null;
targetValue?: string | null;
isActive: boolean;
}>;
campaigns: Array<{
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;
}>;
routeSections: Array<{
id: string;
title: string;
subtitle?: string | null;
productIds: string[];
isActive: boolean;
sortOrder: number;
}>;
hotelGroups: Array<{
id: string;
title: string;
description?: string | null;
image?: string | null;
coverImage?: string | null;
priceAmount?: number | null;
priceUnit?: string | null;
tags?: string[];
status: "draft" | "published";
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
}>;
vehicleOptions: Array<{
id: string;
title: string;
description?: string | null;
image?: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
updatedAt?: string;
}>;
ctaBanners: Array<{
id: string;
alt: string;
image: string;
targetType: string;
targetValue?: string | null;
isActive: boolean;
sortOrder: number;
}>;
};
```
### SiteItemPatch
```ts
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;
priceAmount?: number | null;
priceUnit?: string | null;
tags?: string[];
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;
};
```
模块字段映射:
| module | 可编辑字段 |
| --- | --- |
| `heroSlides` | `title``kicker``image``targetType``targetValue``isActive` |
| `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` | `destinationLabel``destinationPlaceholder``phoneLabel``phonePlaceholder``noteLabel``notePlaceholder``submitLabel``chips``isActive` |
| `demandRecommendations` | `title``subtitle``productIds``isActive``sortOrder` |
| `themes` | `label``image``targetType``targetValue``isActive` |
| `campaigns` | `title``description``coverImage``priceAmount``priceUnit``tags``status` |
| `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` |
## 接口明细
### 登录
```http
POST /api/admin/auth/login
```
请求体:
```json
{
"email": "admin@example.com",
"password": "example-password"
}
```
响应体:
```ts
{
token: string;
user: {
id?: string;
name: string;
email: string;
role: string;
};
}
```
状态码要求:
- `200`:登录成功。
- `401`:账号不存在、密码错误或账号停用。
### 工作台统计
```http
GET /api/admin/dashboard
```
当前前端客户端已封装该接口,但页面暂未展示。后端保持兼容即可。
响应体:
```ts
{
stats: {
productCount: number;
publishedProductCount: number;
destinationCount: number;
newLeadCount: number;
leadCount: number;
campaignCount: number;
};
recentLeads: Lead[];
}
```
### 商品列表
```http
GET /api/admin/products?keyword=<keyword>
```
查询参数:
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
| --- | --- | --- | --- |
| `keyword` | `string` | 是 | 搜索商品标题、短标题或标签 |
| `status` | `ProductStatus` | 否 | 后端可支持状态筛选 |
| `take` | `number` | 否 | 后端当前可限制返回条数 |
响应体:
```ts
{
items: Product[];
}
```
排序建议:`sortWeight` 升序,再按 `updatedAt` 倒序。结构维护页和商品维护页都会读取该接口。
### 新建商品
```http
POST /api/admin/products
```
请求体:`ProductInput`
响应体:`Product`
状态码要求:
- `201`:创建成功。
- `422`:字段校验失败。
### 更新商品
```http
PATCH /api/admin/products/{id}
```
请求体:`Partial<ProductInput>`
响应体:`Product`
状态码要求:
- `200`:更新成功。
- `404`:商品不存在。
- `422`:字段校验失败。
当前 UI 保存商品后会使用响应体刷新编辑状态,因此后端需要返回完整 Product而不是只返回成功标记。
当前管理端不提供商品物理删除接口。目的地页联调中若需要让商品从前台消失,应通过 `PATCH /api/admin/products/{id}``status` 改为 `archived` 或解除 `destinationId` 关联;不要删除商品本体。
### 目的地列表
```http
GET /api/admin/destinations
```
响应体:
```ts
{
items: Destination[];
}
```
要求:
- 返回所有目的地,包括未启用项,便于后台维护。
-`sortOrder` 升序。
- 每个目的地包含 `aliases`
- 每个目的地建议包含 `_count.products`,用于后台判断关联商品数量。
### 目的地模块 CRUD
目的地页维护入口位于 WonderQ-Admin-UI 侧边栏“目的地页”,固定进入 `StructurePage fixedPage="destination"`。后台模块名称与前台三个区块一一对应为“顶部主视觉”“热门区域”“省内目的地”:`destinationHero` 维护顶部主视觉,`destinationRegions` 维护热门区域快捷入口,`destinations` 维护省内目的地卡片;商品归属通过商品维护页绑定 `destinationId`
顶部主视觉:
```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
```
字段要求:`title` 必填;`kicker``image``isActive``sortOrder` 可选。创建时 `isActive` 默认 `true`,未传 `sortOrder` 时追加到模块末尾。Public API 只返回启用项MiniAPP 按 `sortOrder` 升序取第一项渲染目的地页顶部主视觉。
热门区域:
```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
```
字段要求:`label` 必填;`keyword``spots``isActive``sortOrder` 可选。`keyword` 为空时 MiniAPP 使用 `label` 作为点击搜索关键词;`spots` 用于卡片摘要展示。Public API 只返回启用项并按 `sortOrder` 升序输出。
新增目的地:
```http
POST /api/admin/site-config/destinations
```
请求体字段:
```ts
{
name: string;
slug?: string;
region?: string | null;
image?: string | null;
isHot?: boolean;
isActive?: boolean;
sortOrder?: number;
}
```
要求:
- `name` 必填,后端保存前去除首尾空白,不能为空。
- `slug` 为空时后端按 `name` 自动生成;如果传入则不能为空字符串。
- `isHot` 默认 `false``isActive` 默认 `true``sortOrder` 未传时追加到模块末尾。
- 响应状态码为 `201`,响应体返回完整 `Destination`
更新目的地:
```http
PATCH /api/admin/site-config/destinations/{id}
```
请求体为上述字段的部分对象;响应体返回更新后的完整 `Destination`
删除目的地:
```http
DELETE /api/admin/site-config/destinations/{id}
```
删除只移除目的地配置,不删除商品本体。若仍有商品引用该目的地,后端返回 `409 MODULE_CONFIG_CONFLICT`;运营应先在商品维护页将相关商品归档或解除 `destinationId` 关联。
调整目的地顺序:
```http
PATCH /api/admin/site-config/destinations/reorder
```
请求体:
```json
{
"itemIds": ["dest-a", "dest-b", "dest-c"]
}
```
`itemIds` 必须完整包含当前全部目的地 ID不能重复或混入其他模块 ID。后端按数组顺序重写 `sortOrder`Public `site-config.destinations` 也按该顺序输出启用项。
### 站点配置
```http
GET /api/admin/site-config
```
响应体:`SiteConfig`
要求:
- 返回 `heroSlides``destinations``destinationHero``destinationRegions``map``themes``campaigns``routeSections``hotelGroups``vehicleOptions``ctaBanners` 十一个模块。
- Admin API 需要返回未启用内容Public API 才按发布/启用状态过滤。
- 各模块按 `sortOrder` 升序。
- `routeSections` 返回当前已保存的子分组,包含未启用分组和后台配置的全部 `productIds`;无数据时返回空数组。`hotelGroups``vehicleOptions` 返回全部后台卡片,包含停用项;无数据时返回空数组。
### 更新站点配置项
```http
PATCH /api/admin/site-config/{module}/{item_id}
```
路径参数:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `module` | `SiteModule` | 只能为 `heroSlides``destinations``destinationHero``destinationRegions``demandHero``demandFeatureCards``demandForm``demandRecommendations``map``themes``campaigns``routeSections``hotelGroups``vehicleOptions``ctaBanners` |
| `item_id` | `string` | 对应模块内容项 ID |
请求体:`SiteItemPatch`
响应体:更新后的内容项对象。
状态码要求:
- `200`:更新成功。
- `400`:模块不存在。
- `404`:内容项不存在。
- `422`:字段校验失败。
当前 UI 保存后会重新调用 `GET /api/admin/site-config` 刷新页面,响应体只需保证是合法 JSON。
### 页面模块配置 CRUD
页面模块的新增、更新、删除、排序、专用字段、状态码和媒体上传规则,以 `module-config-api.md` 为唯一权威文档。
本文件只保留 Admin UI 必需的主接口视角:
```http
GET /api/admin/site-config
POST /api/admin/site-config/{module}
PATCH /api/admin/site-config/{module}/{item_id}
DELETE /api/admin/site-config/{module}/{item_id}
PATCH /api/admin/site-config/{module}/reorder
```
如果需要维护以下模块的专用创建、删除、排序或字段规则,请直接查看 `module-config-api.md`
- `heroSlides`
- `destinations`
- `destinationHero`
- `destinationRegions`
- `map`
- `themes`
- `campaigns`
- `routeSections`
- `hotelGroups`
- `vehicleOptions`
- `ctaBanners`
### 线索列表
```http
GET /api/admin/leads
```
查询参数:
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
| --- | --- | --- | --- |
| `status` | `LeadStatus` | 是 | 按线索状态筛选 |
| `sourcePage` | `string` | 是 | 按来源页面筛选,例如 `demand_page` |
| `keyword` | `string` | 是 | 匹配联系方式、目的地、备注和来源商品标题 |
| `createdFrom` | `YYYY-MM-DD` 或 ISO 字符串 | 是 | 按提交时间起始筛选 |
| `createdTo` | `YYYY-MM-DD` 或 ISO 字符串 | 是 | 按提交时间截止筛选;日期格式覆盖当天 |
| `take` | `number` | 是 | 返回条数,当前 UI 使用 200 上限 |
响应体:
```ts
{
items: Lead[];
}
```
排序建议:`createdAt` 倒序。当前 UI 展示客户手机号、创建时间、目的地/备注、来源商品/来源页面和状态。
### 更新线索状态
```http
PATCH /api/admin/leads/{id}/status
```
请求体:
```ts
{
status: LeadStatus;
}
```
响应体:更新后的 `Lead`,至少需要包含 `id``status`
状态码要求:
- `200`:更新成功。
- `404`:线索不存在。
- `422`:状态值非法。
当前 UI 更新后会重新调用 `GET /api/admin/leads`,因此响应体不会直接用于渲染列表。
### 发布站点配置
```http
POST /api/admin/publish
```
请求体:空 JSON 对象或无请求体均可兼容。
响应体:
```ts
{
id: string;
title: string;
publishedAt: string;
}
```
当前 UI 只读取 `title` 展示发布结果。后端可额外返回 `status``snapshot` 等字段。
### 重置贵州内容
```http
POST /api/admin/reset-guizhou-content
```
请求体:空 JSON 对象或无请求体均可兼容。
响应体:
```ts
{
heroSlides: number;
destinations: number;
themes: number;
ctaBanners: number;
routeSections?: number;
hotelGroups?: number;
vehicleOptions?: number;
products: number;
}
```
要求:
- 该接口会重置内容数据,必须走后台鉴权。
- 后端需要记录审计日志。
- 生产环境调用前应通过部署流程或权限控制额外确认。
## 后端实现注意事项
- Admin API 默认使用 `require_admin`,登录接口除外。
- 所有外部输入通过 Pydantic schema 校验。
- 变更类接口需要继续写入 `AuditLog`
- 响应字段使用 camelCase以匹配当前前端类型。
- 允许后端返回额外字段,但不要移除本文列出的前端依赖字段。
- 当前管理端不会直接上传图片,只维护图片 URL媒体库接口暂不属于本需求范围。