Files
WonderQ-Project/docs/detail-api.md
duanshuwen e082bd2d98 feat(api): 实现三端统一的JSON API响应契约
- 新增`api_response.py`统一响应封装工具类,提供标准成功/错误响应构造方法
- 重构WonderQ-Admin全局异常处理器,将所有异常转换为标准响应格式
- 修改所有公共和管理端接口的返回逻辑,统一使用`code`(与HTTP状态码一致)、`msg`和`data`的三层结构
- 新增`api-response-contract.md`文档,定义完整的三端统一JSON响应规范
- 更新所有领域API文档,明确业务数据需位于`data`字段内,补充响应格式说明
- 为WonderQ-MiniAPP和WonderQ-Admin-UI新增响应解析逻辑和类型定义,自动完成协议校验和错误处理
- 更新所有测试用例,适配新的响应结构确保接口符合契约要求
- 新增`module-config-api.md`模块配置API文档,补充站点模块配置的接口约定
- 更新项目README文档,调整文档分类顺序将响应契约置于首位
2026-08-19 22:02:20 +08:00

12 KiB
Raw Blame History

详情展示管理 Admin API

适用端:WonderQ-AdminWonderQ-Admin-UI

状态:已实现契约。本文件定义独立路线详情展示模型,供 WonderQ-AdminWonderQ-Admin-UIWonderQ-MiniAPP 三端联调使用。详情不恢复 Product、ProductImage 或订单关联。

领域边界

详情管理只维护详情页可编辑的展示内容:

  • 详情页识别键、标题眉标、出行时长和标题文案。
  • 详情介绍、行程亮点、费用包含、费用不含和注意事项。
  • 详情页图片画廊及图片顺序。
  • 启用状态和详情列表顺序。

本接口不负责:

  • 商品、商品价格、商品库存或商品详情表。
  • Product、ProductImage 或任何商品外键。
  • 订单、预订、收藏、评价或线索。
  • 详情页底部的电话、管家联系和预订动作。

key 固定使用玩法路线 IDWanfaRoute.id,例如 family-water),但 DetailRecord 不建立数据库外键。详情页通过 /pages/detail/index?routeId={key} 定位内容;详情记录删除不会跨领域级联删除路线或其他数据。

所有 JSON 响应遵循 三端统一 API 响应契约,成功业务对象位于 data,失败时 datanull

detailPresentation.ts 的关系

detailPresentation.ts 是前台展示适配器,不是持久化模型:

  • eyebrowdurationtitlesubtitleintrohighlightsincludedexcludednotesgallery 组成最终展示对象。
  • 当前实现优先消费 Public API 返回的最终展示字段。
  • 接口失败、字段不完整或详情未配置时MiniAPP 按路线 ID 使用本地网络图片和模拟文案兜底。

新的 Admin API 应直接维护最终展示字段Admin UI 不应复刻这些推导逻辑,也不应依赖已移除的 Product 字段。

接口清单

API 前缀为 /api/admin,除登录接口外均需要后台 JWT。

方法 路径 用途
GET /api/admin/details 获取全部详情展示配置
GET /api/admin/details/{detailId} 获取单个详情展示配置
POST /api/admin/details 新增详情展示配置
PATCH /api/admin/details/{detailId} 编辑详情展示配置
DELETE /api/admin/details/{detailId} 删除详情展示配置
PATCH /api/admin/details/reorder 调整详情展示配置顺序

通用约定

  • 请求和响应使用 JSON字段使用 camelCase。
  • 所有管理接口需要 Authorization: Bearer <admin-jwt>
  • GET /details 返回启用和停用的全部配置,按 sortOrder 升序返回。
  • 创建、编辑、删除和排序成功后写入审计日志,再提交事务。
  • 创建和编辑返回最新详情对象;排序接口返回排序后的 items
  • ID 由后端生成并作为非空字符串返回;key 由调用方提供并保持稳定。
  • 空列表返回 [],不能返回 null 或省略字段。
  • 成功结果放入 data;失败返回数字 codemsgdata: null,可选 errorCodedetails

数据类型

以下内容字段与 DetailPresentation 保持兼容,管理端补充 idkey、状态和审计时间:

type DetailPresentation = {
  key: string;
  eyebrow: string;
  duration: string;
  title: string;
  subtitle: string;
  intro: string;
  highlights: string[];
  included: string[];
  excluded: string[];
  notes: string[];
  gallery: string[];
};

type DetailRecord = DetailPresentation & {
  id: string;
  key: string;
  isActive: boolean;
  sortOrder: number;
  createdAt: string;
  updatedAt: string;
};

type PublicDetail = Omit<DetailPresentation, "key"> & {
  key: string;
};

type DetailCreate = DetailPresentation & {
  key: string;
  isActive?: boolean;
  sortOrder?: number;
};

type DetailPatch = Partial<DetailCreate>;

type DetailListResponse = {
  details: DetailRecord[];
};

type DetailReorderRequest = {
  itemIds: string[];
};

字段约束

字段 类型 必填 约束和用途
id string 响应必填 后端生成的记录 ID仅供管理端识别记录。
key string 玩法路线 ID例如 family-water;必须唯一,不建立数据库外键。
eyebrow string 详情页顶部眉标,例如“玩法推荐”。
duration string 展示用时长例如“5天4晚”接口保存最终文案不要求前端从标题正则提取。
title string 详情页主标题。
subtitle string 详情页副标题或目的地说明。
intro string “详细介绍”区域的主介绍文案。
highlights string[] “行程亮点”列表,保留数组顺序。
included string[] “费用包含”列表,保留数组顺序。
excluded string[] “费用不含”列表,保留数组顺序。
notes string[] “注意事项”列表,保留数组顺序。
gallery string[] 详情图片 URL 列表,按展示顺序返回;建议最多 6 张以匹配当前前台逻辑。
isActive boolean 响应必填 是否进入已发布前台内容,创建默认 true
sortOrder number 响应必填 非负整数,数值越小越靠前;创建时未传则追加到末尾。
createdAt string 响应必填 ISO 8601 创建时间。
updatedAt string 响应必填 ISO 8601 最后更新时间。

服务端应校验 key 唯一、文本字段去除首尾空白后不为空、数组元素为非空字符串、gallery 为 URL 列表、sortOrder 为非负整数。具体文本最大长度由后端 schema 统一定义,并同步到 Admin UI 表单校验。

接口详情

获取详情列表

GET /api/admin/details
Authorization: Bearer <admin-jwt>

成功响应:

{
  "code": 200,
  "msg": "success",
  "data": {
    "details": [
      {
        "id": "detail-001",
        "key": "classic-panorama",
        "eyebrow": "玩法推荐",
        "duration": "5天4晚",
        "title": "经典贵州全景",
        "subtitle": "贵州·瀑布、苗寨、古城与山地风光",
        "intro": "沿着贵州山地的自然纹理深入探索。",
        "highlights": ["核心景观串联", "小团出行,按同行人节奏调整"],
        "included": ["行程内用车与接送服务"],
        "excluded": ["往返大交通及个人消费"],
        "notes": ["贵州多山多雨,请准备防滑鞋和轻便雨具。"],
        "gallery": ["https://example.test/assets/detail-01.jpg"],
        "isActive": true,
        "sortOrder": 0,
        "createdAt": "2026-01-01T00:00:00Z",
        "updatedAt": "2026-01-01T00:00:00Z"
      }
    ]
  }
}

获取单个详情

GET /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>

成功返回 data 内的单个 DetailRecord。记录不存在返回 404,业务码为 DETAIL_NOT_FOUND

新增详情

POST /api/admin/details
Authorization: Bearer <admin-jwt>
Content-Type: application/json

请求体为 DetailCreate。成功返回 201data 内新建的 DetailRecord;未传 sortOrder 时追加到当前列表末尾。

编辑详情

PATCH /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>
Content-Type: application/json

请求体为 DetailPatch,只更新提交的字段。修改 key 时仍须保证全局唯一。成功返回 data 内更新后的 DetailRecord;记录不存在返回 404,业务码为 DETAIL_NOT_FOUNDkey 冲突返回 409,业务码为 DETAIL_KEY_EXISTS

删除详情

DELETE /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>

成功返回:

{
  "code": 200,
  "msg": "success",
  "data": { "id": "detail-001" }
}

删除后应重新规范化剩余记录的 sortOrder,从 0 开始连续编号。详情记录没有 Product、订单或线索外键因此删除不触发跨领域级联操作。

调整详情顺序

PATCH /api/admin/details/reorder
Authorization: Bearer <admin-jwt>
Content-Type: application/json

请求必须完整包含当前全部详情 ID不能重复

{ "itemIds": ["detail-002", "detail-001"] }

成功返回:

{
  "code": 200,
  "msg": "success",
  "data": { "items": [] }
}

其中 items 为更新 sortOrder 后的 DetailRecord[]。缺少 ID、包含未知 ID 或出现重复 ID 时返回 400 DETAIL_REORDER_INVALID

MiniAPP Public API

获取路线详情

GET /api/public/details/{key}

无需鉴权。keyWanfaRoute.id,例如 family-water。接口只返回启用且已配置的展示字段,不返回管理端 ID、状态、排序和审计时间

type PublicDetail = {
  key: string;
  eyebrow: string;
  duration: string;
  title: string;
  subtitle: string;
  intro: string;
  highlights: string[];
  included: string[];
  excluded: string[];
  notes: string[];
  gallery: string[];
};

不存在、停用或未配置详情时返回 404。MiniAPP 请求失败或字段不完整时,按 key 查找本地 fallback找不到 fallback 时展示未找到和重试状态。该页面暂不提供价格、收藏、在线订阅、预订、订单或管家联系动作。

图片与素材

Admin UI 使用现有素材上传接口获取图片 URL

POST /api/admin/media-assets/upload

建议详情页图片使用 group=detail,将返回的 url 按用户排列顺序写入 gallery。接口只保存图片 URL不创建 ProductImage 表或商品图片关联。

当前 detailPresentation.ts 的 fallback 图片属于前台兜底逻辑。Admin API 正式接入后Admin UI 不应把 fallback 图片自动写入数据库;应由运营人员明确上传和排序详情图片。

Admin UI 对接要求

Admin UI 应按以下方式调用:

  1. 玩法页加载时同时调用玩法分类和 GET /api/admin/details;详情编辑器嵌入现有路线编辑抽屉,不新增侧边菜单。
  2. 新增和编辑表单维护眉标、时长、标题、副标题、介绍和四组列表文案;保存路线时以已保存的路线 ID 作为详情 key
  3. highlightsincludedexcludednotes 使用可增删的重复字段编辑器,提交时保留数组顺序,不拼接成换行字符串。
  4. 使用图片上传接口维护 gallery,支持新增、删除和调整图片顺序。
  5. 上移或下移详情时提交完整详情 ID 列表,不直接修改本地 sortOrder 后假设保存成功。
  6. 删除前要求二次确认;删除成功后以接口返回或重新查询的数据更新列表。
  7. 处理 4014044094225xx,保存、上传或排序进行中禁用重复提交。
  8. 详情编辑器不得出现 Product ID、ProductImage ID、库存、订单、价格或预订字段详情保存失败时明确提示路线摘要已保存、详情需要重试。

建议的 Admin UI API 封装函数:

getDetails();
getDetail(detailId: string);
createDetail(input: DetailCreate);
updateDetail(detailId: string, input: DetailPatch);
deleteDetail(detailId: string);
reorderDetails(itemIds: string[]);

与前台展示模型的映射

Admin API 返回的 DetailRecord 可以映射为 DetailPresentation

const presentation: DetailPresentation = {
  key: record.key,
  eyebrow: record.eyebrow,
  duration: record.duration,
  title: record.title,
  subtitle: record.subtitle,
  intro: record.intro,
  highlights: record.highlights,
  included: record.included,
  excluded: record.excluded,
  notes: record.notes,
  gallery: record.gallery,
};

接入时应优先使用接口已保存的最终文案和图片顺序;本地 fallback 只用于接口失败、字段不完整或详情未配置的前台容错。

后端落地边界

当前实现由 WonderQ-AdminDetailRecord 模型、0021_detail_records 迁移、Admin/Public 路由和审计日志提供能力;由 WonderQ-Admin-UI 在玩法路线编辑抽屉中维护详情;由 WonderQ-MiniAPP 调用 Public 详情接口并按路线 ID fallback。迁移文件只负责生成和初始化详情记录不自动执行数据库升级。

相关文档: