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
18 KiB
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存入本地,并在后续请求头中发送:
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
type ProductStatus = "draft" | "published" | "archived";
LeadStatus
type LeadStatus = "new" | "assigned" | "contacted" | "planning" | "won" | "invalid";
SiteModule
type SiteModule = "heroSlides" | "destinations" | "map" | "themes" | "campaigns" | "routeSections" | "hotelGroups" | "vehicleOptions" | "ctaBanners";
公共数据结构
Destination
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
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
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 可以只提交需要修改的字段。
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
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
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
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 |
接口明细
登录
POST /api/admin/auth/login
请求体:
{
"email": "admin@example.com",
"password": "example-password"
}
响应体:
{
token: string;
user: {
id?: string;
name: string;
email: string;
role: string;
};
}
状态码要求:
200:登录成功。401:账号不存在、密码错误或账号停用。
工作台统计
GET /api/admin/dashboard
当前前端客户端已封装该接口,但页面暂未展示。后端保持兼容即可。
响应体:
{
stats: {
productCount: number;
publishedProductCount: number;
destinationCount: number;
newLeadCount: number;
leadCount: number;
campaignCount: number;
};
recentLeads: Lead[];
}
商品列表
GET /api/admin/products?keyword=<keyword>
查询参数:
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
|---|---|---|---|
keyword |
string |
是 | 搜索商品标题、短标题或标签 |
status |
ProductStatus |
否 | 后端可支持状态筛选 |
take |
number |
否 | 后端当前可限制返回条数 |
响应体:
{
items: Product[];
}
排序建议:sortWeight 升序,再按 updatedAt 倒序。结构维护页和商品维护页都会读取该接口。
新建商品
POST /api/admin/products
请求体:ProductInput
响应体:Product
状态码要求:
201:创建成功。422:字段校验失败。
更新商品
PATCH /api/admin/products/{id}
请求体:Partial<ProductInput>
响应体:Product
状态码要求:
200:更新成功。404:商品不存在。422:字段校验失败。
当前 UI 保存商品后会使用响应体刷新编辑状态,因此后端需要返回完整 Product,而不是只返回成功标记。
目的地列表
GET /api/admin/destinations
响应体:
{
items: Destination[];
}
要求:
- 返回所有目的地,包括未启用项,便于后台维护。
- 按
sortOrder升序。 - 每个目的地包含
aliases。 - 每个目的地建议包含
_count.products,用于后台判断关联商品数量。
站点配置
GET /api/admin/site-config
响应体:SiteConfig
要求:
- 返回
heroSlides、destinations、map、themes、campaigns、routeSections、hotelGroups、vehicleOptions、ctaBanners九个模块。 - Admin API 需要返回未启用内容;Public API 才按发布/启用状态过滤。
- 各模块按
sortOrder升序。 routeSections返回当前已保存的子分组,包含未启用分组和后台配置的全部productIds;无数据时返回空数组。hotelGroups、vehicleOptions返回全部后台卡片,包含停用项;无数据时返回空数组。
更新站点配置项
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 顺序。
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 是首页“万趣用车”卡片配置。两者只维护首页模块卡片,不维护商品本体或商品关联。
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 是卡片背景图。
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升序。
线索列表
GET /api/admin/leads
查询参数:
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
|---|---|---|---|
status |
LeadStatus |
否 | 后端可支持状态筛选 |
take |
number |
否 | 后端当前可限制返回条数 |
响应体:
{
items: Lead[];
}
排序建议:createdAt 倒序。当前 UI 展示客户手机号、创建时间、目的地/备注、来源商品/来源页面和状态。
更新线索状态
PATCH /api/admin/leads/{id}/status
请求体:
{
status: LeadStatus;
}
响应体:更新后的 Lead,至少需要包含 id 和 status。
状态码要求:
200:更新成功。404:线索不存在。422:状态值非法。
当前 UI 更新后会重新调用 GET /api/admin/leads,因此响应体不会直接用于渲染列表。
发布站点配置
POST /api/admin/publish
请求体:空 JSON 对象或无请求体均可兼容。
响应体:
{
id: string;
title: string;
publishedAt: string;
}
当前 UI 只读取 title 展示发布结果。后端可额外返回 status、snapshot 等字段。
重置贵州内容
POST /api/admin/reset-guizhou-content
请求体:空 JSON 对象或无请求体均可兼容。
响应体:
{
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;媒体库接口暂不属于本需求范围。