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

18 KiB
Raw Blame History

WonderQ-Admin-UI Admin API 接口需求

本文档用于指导 WonderQ-Admin 后端按当前 WonderQ-Admin-UI 管理端完成 Admin API 对接。接口需求来源于前端 src/api.tssrc/App.tsx 的实际类型、请求封装和页面调用。

范围

  • 本文只覆盖当前后台管理前端必需的 Admin API。
  • 不包含 H5 Public API、订单、媒体库、审计日志、活动专题等后续规划能力。
  • 后端现有 GET /api/admin/meGET /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 首页/目的地/活动结构维护 已覆盖 需要包含未启用内容、已保存的 routeSectionshotelGroupsvehicleOptionsctaBanners
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 /productsPATCH /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;
};

后端可额外返回 travelDatepeopleCountbudgetMinbudgetMax 等字段,但以上字段是当前管理端展示所需的最小集合。手机号属于隐私信息,日志、错误和文档示例中不得输出真实号码。

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 titlekickerimagetargetTypetargetValueisActive
destinations nameimageisActive
themes labelimagetargetTypetargetValueisActive
campaigns titledescriptioncoverImagepriceAmountpriceUnittagsstatus
routeSections titlesubtitleproductIdsisActivesortOrder
hotelGroups titledescriptionimageisActivesortOrder
vehicleOptions titledescriptionimageisActivesortOrder
ctaBanners alt(服务标题)、imagetargetTypetargetValueisActivesortOrder

接口明细

登录

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

要求:

  • 返回 heroSlidesdestinationsmapthemescampaignsrouteSectionshotelGroupsvehicleOptionsctaBanners 九个模块。
  • Admin API 需要返回未启用内容Public API 才按发布/启用状态过滤。
  • 各模块按 sortOrder 升序。
  • routeSections 返回当前已保存的子分组,包含未启用分组和后台配置的全部 productIds;无数据时返回空数组。hotelGroupsvehicleOptions 返回全部后台卡片,包含停用项;无数据时返回空数组。

更新站点配置项

PATCH /api/admin/site-config/{module}/{item_id}

路径参数:

参数 类型 说明
module SiteModule 只能为 heroSlidesdestinationsmapthemescampaignsrouteSectionshotelGroupsvehicleOptionsctaBanners
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,可包含 subtitleisActivesortOrderproductIdsid 由后端生成,不再使用 routes / routes-outdoor / routes-mix 固定槽位。更新请求可包含 titlesubtitleisActivesortOrderproductIdsproductIds 表示该分组关联的线路商品及展示顺序。

后端约束:

  • 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/reorderitemIds 必须完整覆盖当前全部分组 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,可包含 descriptionimageisActivesortOrder
  • 更新请求可包含 titledescriptionimageisActivesortOrder
  • 删除只删除首页卡片配置,不删除商品、目的地或素材库资源;删除后后端重新整理剩余项 sortOrder
  • PATCH /reorderitemIds 必须完整覆盖当前同模块全部配置项 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,可包含 imagetargetTypetargetValueisActivesortOrder
  • 更新请求可包含 altimagetargetTypetargetValueisActivesortOrder
  • 删除只删除首页更多服务卡片配置,不删除素材库资源;删除后后端重新整理剩余项 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,至少需要包含 idstatus

状态码要求:

  • 200:更新成功。
  • 404:线索不存在。
  • 422:状态值非法。

当前 UI 更新后会重新调用 GET /api/admin/leads,因此响应体不会直接用于渲染列表。

发布站点配置

POST /api/admin/publish

请求体:空 JSON 对象或无请求体均可兼容。

响应体:

{
  id: string;
  title: string;
  publishedAt: string;
}

当前 UI 只读取 title 展示发布结果。后端可额外返回 statussnapshot 等字段。

重置贵州内容

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媒体库接口暂不属于本需求范围。