Files
WonderQ-Admin/docs/admin-ui-api-requirements.md
duanshuwen 201e835eab feat: add hotel and vehicle option site modules
Add full support for hotel group and vehicle option site management features:
- Define SQLAlchemy models and alembic migration for the new database tables
- Add default sample content entries in content.py
- Extend admin and public API routes to support the new modules
- Update seed script to populate default hotel and vehicle data
- Update all relevant documentation and test cases
2026-07-03 20:26:44 +08:00

631 lines
18 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` | 商品目的地下拉、目的地页 | 已覆盖 | 需要返回别名和商品数 |
| `GET /api/admin/site-config` | 首页/目的地/活动结构维护 | 已覆盖 | 需要包含未启用内容、已保存的 `routeSections``hotelGroups``vehicleOptions``ctaBanners` |
| `PATCH /api/admin/site-config/{module}/{item_id}` | 模块内容编辑 | 已覆盖 | 模块名需保持一致 |
| `GET /api/admin/leads` | 需求线索页 | 已覆盖 | UI 当前不传筛选参数 |
| `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" | "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[];
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;
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;
name?: string;
slug?: string;
region?: string | null;
label?: string;
alt?: string;
image?: string | null;
description?: string | null;
coverImage?: string | null;
priceAmount?: number | null;
priceUnit?: string | null;
tags?: 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``image``isActive` |
| `themes` | `label``image``targetType``targetValue``isActive` |
| `campaigns` | `title``description``coverImage``priceAmount``priceUnit``tags``status` |
| `routeSections` | `title``subtitle``productIds``isActive``sortOrder` |
| `hotelGroups` | `title``description``image``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而不是只返回成功标记。
### 目的地列表
```http
GET /api/admin/destinations
```
响应体:
```ts
{
items: Destination[];
}
```
要求:
- 返回所有目的地,包括未启用项,便于后台维护。
-`sortOrder` 升序。
- 每个目的地包含 `aliases`
- 每个目的地建议包含 `_count.products`,用于后台判断关联商品数量。
### 站点配置
```http
GET /api/admin/site-config
```
响应体:`SiteConfig`
要求:
- 返回 `heroSlides``destinations``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``map``themes``campaigns``routeSections``hotelGroups``vehicleOptions``ctaBanners` |
| `item_id` | `string` | 对应模块内容项 ID |
请求体:`SiteItemPatch`
响应体:更新后的内容项对象。
状态码要求:
- `200`:更新成功。
- `400`:模块不存在。
- `404`:内容项不存在。
- `422`:字段校验失败。
当前 UI 保存后会重新调用 `GET /api/admin/site-config` 刷新页面,响应体只需保证是合法 JSON。
#### 精选线路 `routeSections`
`routeSections` 是首页“精选线路”的动态运营分组配置,不再限制为固定三组。管理端通过新增、更新、删除和排序接口维护闭环;商品本体仍由 `/api/admin/products` 维护,这里只保存首页分组、标题、副文案、启用状态、排序和关联商品 ID 顺序。
```http
POST /api/admin/site-config/routeSections
PATCH /api/admin/site-config/routeSections/{section_id}
DELETE /api/admin/site-config/routeSections/{section_id}
PATCH /api/admin/site-config/routeSections/reorder
```
新增请求至少包含 `title`,可包含 `subtitle``isActive``sortOrder``productIds``id` 由后端生成,不再使用 `routes` / `routes-outdoor` / `routes-mix` 固定槽位。更新请求可包含 `title``subtitle``isActive``sortOrder``productIds``productIds` 表示该分组关联的线路商品及展示顺序。
后端约束:
- `GET /api/admin/site-config` 返回全部后台分组,包含停用分组和后台配置的全部 `productIds`
- `productIds` 中的商品必须存在,且同一请求内不能重复。
- 同一商品不能同时出现在多个精选线路分组;冲突时返回 `409 ROUTE_SECTION_PRODUCT_CONFLICT`
- `DELETE /api/admin/site-config/routeSections/{section_id}` 只删除分组配置并解除关联,不删除商品本体;删除后后端重新整理剩余分组 `sortOrder`
- `PATCH /api/admin/site-config/routeSections/reorder``itemIds` 必须完整覆盖当前全部分组 ID不能缺失、重复或包含未知 ID。
- `GET /api/public/site-config` 只返回启用分组,且 `productIds` 只包含已发布商品;未发布、归档或不存在的商品不进入 Public 响应。
#### 特色酒店和万趣用车 `hotelGroups` / `vehicleOptions`
`hotelGroups` 是首页“特色酒店”卡片配置,`vehicleOptions` 是首页“万趣用车”卡片配置。两者只维护首页模块卡片,不维护商品本体或商品关联。
```http
POST /api/admin/site-config/hotelGroups
PATCH /api/admin/site-config/hotelGroups/{item_id}
DELETE /api/admin/site-config/hotelGroups/{item_id}
PATCH /api/admin/site-config/hotelGroups/reorder
POST /api/admin/site-config/vehicleOptions
PATCH /api/admin/site-config/vehicleOptions/{item_id}
DELETE /api/admin/site-config/vehicleOptions/{item_id}
PATCH /api/admin/site-config/vehicleOptions/reorder
```
字段规则:
- 新增请求至少包含 `title`,可包含 `description``image``isActive``sortOrder`
- 更新请求可包含 `title``description``image``isActive``sortOrder`
- 删除只删除首页卡片配置,不删除商品、目的地或素材库资源;删除后后端重新整理剩余项 `sortOrder`
- `PATCH /reorder``itemIds` 必须完整覆盖当前同模块全部配置项 ID不能缺失、重复或包含未知 ID。
- `GET /api/public/site-config` 只返回启用卡片,并按 `sortOrder` 升序MiniAPP 在字段缺失或空数组时使用本地内容兜底。
#### 更多服务 `ctaBanners`
`ctaBanners` 对应首页“更多服务”模块,维护权益、服务管家、目的地和需求入口等服务卡片。管理端按顶部轮播相同的配置方式提供新增、编辑、删除和排序;`alt` 是前台卡片标题,`image` 是卡片背景图。
```http
POST /api/admin/site-config/ctaBanners
PATCH /api/admin/site-config/ctaBanners/{item_id}
DELETE /api/admin/site-config/ctaBanners/{item_id}
PATCH /api/admin/site-config/ctaBanners/reorder
```
字段规则:
- 新增请求至少包含 `alt`,可包含 `image``targetType``targetValue``isActive``sortOrder`
- 更新请求可包含 `alt``image``targetType``targetValue``isActive``sortOrder`
- 删除只删除首页更多服务卡片配置,不删除素材库资源;删除后后端重新整理剩余项 `sortOrder`
- `GET /api/public/site-config` 只返回启用卡片,并按 `sortOrder` 升序。
### 线索列表
```http
GET /api/admin/leads
```
查询参数:
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
| --- | --- | --- | --- |
| `status` | `LeadStatus` | 否 | 后端可支持状态筛选 |
| `take` | `number` | 否 | 后端当前可限制返回条数 |
响应体:
```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媒体库接口暂不属于本需求范围。