# 详情展示管理 Admin API > 适用端:`WonderQ-Admin`、`WonderQ-Admin-UI`。 > > 状态:待实现契约。本文件参考 `WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts` 定义详情页展示数据。当前 `detailPresentation.ts` 仍依赖已移除的 `Product` 类型,后端也没有详情管理接口;本契约采用与商品领域解耦的详情展示模型,不恢复 Product、ProductImage 或订单关联。 ## 领域边界 详情管理只维护详情页可编辑的展示内容: - 详情页识别键、标题眉标、出行时长和标题文案。 - 详情介绍、行程亮点、费用包含、费用不含和注意事项。 - 详情页图片画廊及图片顺序。 - 启用状态和详情列表顺序。 本接口不负责: - 商品、商品价格、商品库存或商品详情表。 - Product、ProductImage 或任何商品外键。 - 订单、预订、收藏、评价或线索。 - 详情页底部的电话、管家联系和预订动作。 `key` 是详情展示内容自己的稳定业务键,不得设计为 Product ID 外键。详情页如何从玩法、页面入口或其他前台上下文定位 `key`,由前台导航契约另行约定。 ## 与 `detailPresentation.ts` 的关系 `detailPresentation.ts` 当前是前台展示适配器,不是持久化模型: - `eyebrow`、`duration`、`title`、`subtitle`、`intro`、`highlights`、`included`、`excluded`、`notes`、`gallery` 组成最终展示对象。 - 当前实现从 `Product` 的标题、摘要、标签、详情区块和图片数组推导部分字段。 - 当前实现对洞穴/探险路线生成另一组固定亮点,并使用固定费用说明和注意事项。 - 当前实现会将主图、接口图片和 fallback 图片去重后截取前 6 张。 新的 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 `。 - `GET /details` 返回启用和停用的全部配置,按 `sortOrder` 升序返回。 - 创建、编辑、删除和排序成功后写入审计日志,再提交事务。 - 创建和编辑返回最新详情对象;排序接口返回排序后的 `items`。 - ID 由后端生成并作为非空字符串返回;`key` 由调用方提供并保持稳定。 - 空列表返回 `[]`,不能返回 `null` 或省略字段。 - 失败响应沿用 Admin API 约定,包含 `message`、`code` 和可选的 `details`。 ## 数据类型 以下内容字段与 `DetailPresentation` 保持兼容,管理端补充 `id`、`key`、状态和审计时间: ```ts type DetailPresentation = { 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 DetailCreate = DetailPresentation & { key: string; isActive?: boolean; sortOrder?: number; }; type DetailPatch = Partial; type DetailListResponse = { details: DetailRecord[]; }; type DetailReorderRequest = { itemIds: string[]; }; ``` ## 字段约束 | 字段 | 类型 | 必填 | 约束和用途 | | --- | --- | --- | --- | | `id` | `string` | 响应必填 | 后端生成的记录 ID,仅供管理端识别记录。 | | `key` | `string` | 是 | 详情展示稳定键;建议使用小写字母、数字和中划线,例如 `classic-panorama`。不得关联 Product 表。 | | `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 表单校验。 ## 接口详情 ### 获取详情列表 ```http GET /api/admin/details Authorization: Bearer ``` 成功响应: ```json { "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" } ] } ``` ### 获取单个详情 ```http GET /api/admin/details/{detailId} Authorization: Bearer ``` 成功返回单个 `DetailRecord`。记录不存在返回 `404 DETAIL_NOT_FOUND`。 ### 新增详情 ```http POST /api/admin/details Authorization: Bearer Content-Type: application/json ``` 请求体为 `DetailCreate`。成功返回 `201` 和新建的 `DetailRecord`;未传 `sortOrder` 时追加到当前列表末尾。 ### 编辑详情 ```http PATCH /api/admin/details/{detailId} Authorization: Bearer Content-Type: application/json ``` 请求体为 `DetailPatch`,只更新提交的字段。修改 `key` 时仍须保证全局唯一。成功返回更新后的 `DetailRecord`;记录不存在返回 `404 DETAIL_NOT_FOUND`,`key` 冲突返回 `409 DETAIL_KEY_EXISTS`。 ### 删除详情 ```http DELETE /api/admin/details/{detailId} Authorization: Bearer ``` 成功返回: ```json { "id": "detail-001" } ``` 删除后应重新规范化剩余记录的 `sortOrder`,从 `0` 开始连续编号。详情记录没有 Product、订单或线索外键,因此删除不触发跨领域级联操作。 ### 调整详情顺序 ```http PATCH /api/admin/details/reorder Authorization: Bearer Content-Type: application/json ``` 请求必须完整包含当前全部详情 ID,不能重复: ```json { "itemIds": ["detail-002", "detail-001"] } ``` 成功返回: ```json { "items": [] } ``` 其中 `items` 为更新 `sortOrder` 后的 `DetailRecord[]`。缺少 ID、包含未知 ID 或出现重复 ID 时返回 `400 DETAIL_REORDER_INVALID`。 ## 图片与素材 Admin UI 使用现有素材上传接口获取图片 URL: ```http 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`,按 `sortOrder` 渲染详情列表。 2. 新增和编辑表单维护 `key`、眉标、时长、标题、副标题、介绍和四组列表文案。 3. `highlights`、`included`、`excluded`、`notes` 使用可增删的重复字段编辑器,提交时保留数组顺序,不拼接成换行字符串。 4. 使用图片上传接口维护 `gallery`,支持新增、删除和调整图片顺序。 5. 上移或下移详情时提交完整详情 ID 列表,不直接修改本地 `sortOrder` 后假设保存成功。 6. 删除前要求二次确认;删除成功后以接口返回或重新查询的数据更新列表。 7. 处理 `401`、`404`、`409`、`422` 和 `5xx`,保存、上传或排序进行中禁用重复提交。 8. 详情编辑器不得出现 Product ID、ProductImage ID、库存、订单或预订字段。 建议的 Admin UI API 封装函数: ```ts getDetails(); getDetail(detailId: string); createDetail(input: DetailCreate); updateDetail(detailId: string, input: DetailPatch); deleteDetail(detailId: string); reorderDetails(itemIds: string[]); ``` ## 与前台展示模型的映射 Admin API 返回的 `DetailRecord` 可以映射为 `DetailPresentation`: ```ts const presentation: DetailPresentation = { 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, }; ``` 接入时应优先使用接口已保存的最终文案和图片顺序,不再依赖 `stripTitle`、标题时长正则、洞穴路线分支或 fallbackGallery 生成同一字段。 ## 后端落地边界 本契约落地时需要由 `WonderQ-Admin` 补充对应 ORM 模型、迁移、schema、路由、序列化和审计日志;由 `WonderQ-Admin-UI` 补充 API 类型、请求封装、详情列表、编辑器、图片管理和排序交互。当前后端没有 `/api/admin/details` 路由,Admin UI 没有详情管理入口,`detailPresentation.ts` 也不是可直接作为后端契约的完整类型来源。 相关文档: - [Admin API 主契约](./admin-api-requirements.md) - [管家管理 Admin API](./concierge-api.md) - [详情展示适配器](../WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts)