完成首页数据模型的统一重构,具体变更如下: 1. 重构公共首页API接口,移除`playRecommendations`字段,将玩法推荐数据统一放入`experiences`字段 2. 更新前后端类型定义、序列化逻辑与前端页面组件,适配新的API响应结构 3. 为微信登录相关接口添加`trust_env=True`配置,支持企业代理环境并新增`socksio`依赖 4. 新增图片画廊上传组件,重构SingleImageUploader组件支持自定义宽高比 5. 重构Toast与Select组件的实现与样式,统一后台UI设计系统 6. 移除个人中心页面不必要的返回事件与顶部标题组件,优化详情卡片布局 7. 新增微信接口单元测试,更新官方文档与测试用例适配变更 8. 删除过期的文档图片资源
22 KiB
首页内容管理 Admin API
适用端:
WonderQ-Admin、WonderQ-Admin-UI。状态:已实现。本文件约定首页内容的 Admin API,并记录 MiniAPP 使用的对应 Public API。首页玩法推荐不复制玩法文案,而是关联
WanfaCategory。
领域边界
首页内容管理维护四类首页内容:
- 体验推荐:徽标、分类、标题、英文标题、封面和需求关键词。
- 团队共创:标签、标题、描述、封面、需求关键词、详情副标题和详情正文段落。
- 极境视界:案例标题、封面、详情图片和需求关键词;首页卡片可进入客片案例详情。
- 玩法推荐:已关联的玩法分类、启用状态和首页展示顺序;分类名称及路线由玩法领域维护。
本领域不负责:
- 顶部轮播、车辆选项、需求表单或其他站点模块;这些内容遵循 Admin API 主契约。
- 商品、Product、ProductImage、详情、价格、库存、订单或预订。
- 线索创建和线索跟进。
demandKeyword 只是点击卡片后预填需求页的普通字符串,不是商品 ID、路线 ID、订单 ID,也不建立数据库外键。
首页正式接口返回的所有资源 id 都是稳定 UUID 字符串。不要把下方 fallback 文件中的语义 ID 当作服务端 ID,也不要在接口序列化时重新生成 ID;首页卡片跳转详情、排序和删除必须复用接口返回的同一 ID。
当前首页通过 GET /api/public/home 消费三类内容;团队共创和极境视界 mock 数组仍作为接口失败、空响应或字段缺失时的前台 fallback,experiences 中的玩法推荐无本地模拟数据时保持空态。Admin UI 仍通过本文件列出的 Admin API 分别维护体验推荐和玩法推荐关联。
本文件所有 JSON 示例的业务对象均位于统一响应的 data 字段内,完整包裹格式见 api-response-contract.md。
接口清单
API 前缀为 /api/admin,除登录接口外均需要后台 JWT。
| 方法 | 路径 | 用途 |
|---|---|---|
GET |
/api/admin/home/experiences |
获取全部体验推荐卡片 |
POST |
/api/admin/home/experiences |
新增体验推荐卡片 |
PATCH |
/api/admin/home/experiences/{experienceId} |
编辑体验推荐卡片 |
DELETE |
/api/admin/home/experiences/{experienceId} |
删除体验推荐卡片 |
PATCH |
/api/admin/home/experiences/reorder |
调整体验推荐卡片顺序 |
GET |
/api/admin/home/team-buildings |
获取全部团队共创卡片 |
POST |
/api/admin/home/team-buildings |
新增团队共创卡片 |
PATCH |
/api/admin/home/team-buildings/{teamBuildingId} |
编辑团队共创卡片 |
DELETE |
/api/admin/home/team-buildings/{teamBuildingId} |
删除团队共创卡片 |
PATCH |
/api/admin/home/team-buildings/reorder |
调整团队共创卡片顺序 |
GET |
/api/admin/home/wild-archives |
获取全部极境视界案例 |
POST |
/api/admin/home/wild-archives |
新增极境视界案例 |
PATCH |
/api/admin/home/wild-archives/{archiveId} |
编辑极境视界案例 |
DELETE |
/api/admin/home/wild-archives/{archiveId} |
删除极境视界案例 |
PATCH |
/api/admin/home/wild-archives/reorder |
调整极境视界案例顺序 |
GET |
/api/admin/home/play-recommendations |
获取首页已关联的玩法分类 |
POST |
/api/admin/home/play-recommendations |
关联一个玩法分类到首页 |
PATCH |
/api/admin/home/play-recommendations/{recommendationId} |
修改关联的启用状态或玩法分类 |
DELETE |
/api/admin/home/play-recommendations/{recommendationId} |
移除首页玩法分类关联,不删除玩法分类 |
PATCH |
/api/admin/home/play-recommendations/reorder |
调整首页玩法推荐顺序 |
MiniAPP Public API:
| 方法 | 路径 | 用途 |
|---|---|---|
GET |
/api/public/home |
获取已启用的首页内容和玩法推荐 |
GET |
/api/public/home/team-buildings/{teamBuildingId} |
获取单个团队共创详情 |
GET |
/api/public/home/wild-archives |
获取客片案例更多列表 |
GET |
/api/public/home/wild-archives/{archiveId} |
获取单个客片案例详情 |
通用约定
- 请求和响应使用 JSON,字段使用 camelCase。
- 所有管理接口需要
Authorization: Bearer <admin-jwt>。 GET返回启用和停用的全部记录,按sortOrder升序返回,供 Admin UI 完整维护。- 创建、编辑、删除和排序成功后写入审计日志,再提交事务。
- 创建和编辑返回最新记录;排序接口返回排序后的
items。 - ID 由后端生成并作为非空字符串返回。初始化迁移时应优先保留三个 mock 文件中的稳定 ID。
- 空集合返回
[],不能返回null或省略字段。 - 所有资源的
sortOrder从0开始,数值越小越靠前;新增未传排序时追加到末尾。 - 成功结果放入
data;失败返回数字code、msg、data: null,可选errorCode和details。
数据类型
体验推荐
homeExperienceData.ts 已包含前台渲染类型和 API 过渡类型。Admin API 的完整记录应补充管理元数据:
type HomeExperience = {
id: string;
badge: string;
category: string;
title: string;
englishTitle: string;
image: string;
demandKeyword: string;
};
type HomeExperienceApiItem = {
id?: string | null;
badge?: string | null;
category?: string | null;
title?: string | null;
englishTitle?: string | null;
image?: string | null;
demandKeyword?: string | null;
isActive?: boolean | null;
sortOrder?: number | null;
};
type HomeExperienceRecord = HomeExperience & {
isActive: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
};
type HomeExperienceCreate = Omit<HomeExperience, "id"> & {
isActive?: boolean;
sortOrder?: number;
};
type HomeExperiencePatch = Partial<HomeExperienceCreate>;
团队共创
homeTeamBuildingData.ts 当前只包含前台渲染字段,管理端记录增加状态、排序和审计时间:
type HomeTeamBuilding = {
id: string;
tag: string;
title: string;
description: string;
image: string;
demandKeyword: string;
detailSubtitle: string;
detailParagraphs: string[];
};
type HomeTeamBuildingRecord = HomeTeamBuilding & {
isActive: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
};
type HomeTeamBuildingCreate = Omit<HomeTeamBuilding, "id"> & {
isActive?: boolean;
sortOrder?: number;
};
type HomeTeamBuildingPatch = Partial<HomeTeamBuildingCreate>;
极境视界
homeWildArchivesData.ts 当前只包含前台渲染字段,管理端记录增加状态、排序和审计时间:
type HomeWildArchive = {
id: string;
title: string;
image: string;
images: string[];
demandKeyword: string;
};
type HomeWildArchiveRecord = HomeWildArchive & {
isActive: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
};
type HomeWildArchiveCreate = Omit<HomeWildArchive, "id"> & {
isActive?: boolean;
sortOrder?: number;
};
type HomeWildArchivePatch = Partial<HomeWildArchiveCreate>;
列表响应和排序请求统一使用以下结构:
type HomeListResponse<T> = {
items: T[];
};
type HomeReorderRequest = {
itemIds: string[];
};
字段约束
| 资源 | 字段 | 类型 | 必填 | 约束和用途 |
|---|---|---|---|---|
| 体验推荐 | badge |
string |
是 | 卡片徽标,去除首尾空白后不得为空。 |
| 体验推荐 | category |
string |
是 | 体验分类文案,去除首尾空白后不得为空。 |
| 体验推荐 | title |
string |
是 | 卡片主标题,去除首尾空白后不得为空。 |
| 体验推荐 | englishTitle |
string |
是 | 卡片英文标题,去除首尾空白后不得为空。 |
| 体验推荐 | image |
string |
是 | 可直接用于图片组件的封面 URL。 |
| 体验推荐 | demandKeyword |
string |
是 | 点击后预填需求页的关键词。 |
| 团队共创 | tag |
string |
是 | 卡片标签,去除首尾空白后不得为空。 |
| 团队共创 | title |
string |
是 | 卡片标题,去除首尾空白后不得为空。 |
| 团队共创 | description |
string |
是 | 卡片描述,去除首尾空白后不得为空。 |
| 团队共创 | image |
string |
是 | 可直接用于图片组件的封面 URL。 |
| 团队共创 | demandKeyword |
string |
是 | 点击后预填需求页的关键词。 |
| 团队共创 | detailSubtitle |
string |
是 | 详情页首屏副标题;旧数据回退为 description。 |
| 团队共创 | detailParagraphs |
string[] |
是 | 详情页正文段落;至少一段,旧数据回退为 [description]。 |
| 极境视界 | title |
string |
是 | 案例标题,去除首尾空白后不得为空。 |
| 极境视界 | image |
string |
是 | 案例封面图片 URL。 |
| 极境视界 | images |
string[] |
是 | 案例详情图片 URL 列表,至少一张;第一张建议与封面一致。 |
| 极境视界 | demandKeyword |
string |
是 | 点击后预填需求页的关键词。 |
| 全部资源 | isActive |
boolean |
响应必填 | 是否进入已发布前台内容,创建默认 true。 |
| 全部资源 | sortOrder |
number |
响应必填 | 非负整数,数值越小越靠前。 |
| 全部资源 | createdAt |
string |
响应必填 | ISO 8601 创建时间。 |
| 全部资源 | updatedAt |
string |
响应必填 | ISO 8601 最后更新时间。 |
服务端应校验所有必填文本、URL 格式和非负整数排序值;具体文本最大长度由后端 schema 统一定义,并同步到 Admin UI 表单校验。图片字段只保存最终 URL,不接受 base64,不在首页内容记录中保存图片二进制。
接口详情
以下规则适用于三类资源;路径中的资源名和 ID 参数以接口清单为准。
获取列表
GET /api/admin/home/experiences
Authorization: Bearer <admin-jwt>
团队共创和极境视界分别使用 /api/admin/home/team-buildings、/api/admin/home/wild-archives。
成功响应示例(统一响应包裹):
{
"code": 200,
"msg": "success",
"data": {
"items": [
{
"id": "waterfall-descent",
"badge": "玩过推荐",
"category": "瀑降体验",
"title": "悬崖瀑降",
"englishTitle": "WATERFALL DESCENT",
"image": "https://example.test/assets/waterfall-descent.jpg",
"demandKeyword": "悬崖瀑降",
"isActive": true,
"sortOrder": 0,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
]
}
}
接口返回启用和停用的全部记录;Admin UI 负责显示状态,不能让后端默认隐藏停用记录。
新增、编辑与删除
POST /api/admin/home/{resource}:请求体为对应资源的Create类型,成功返回201和data内的新建记录;未传sortOrder时追加到末尾。PATCH /api/admin/home/{resource}/{id}:请求体为对应资源的Patch类型,只更新提交字段;成功返回data内的更新记录,不存在返回404。DELETE /api/admin/home/{resource}/{id}:成功返回data: { "id": "..." },删除后重新规范化同一资源剩余记录的sortOrder。
其中 {resource} 只能是 experiences、team-buildings 或 wild-archives;对应路径参数分别为 experienceId、teamBuildingId、archiveId。删除不触发商品、订单、预订或线索级联操作。
调整顺序
PATCH /api/admin/home/experiences/reorder
Authorization: Bearer <admin-jwt>
Content-Type: application/json
团队共创和极境视界分别使用 /api/admin/home/team-buildings/reorder、/api/admin/home/wild-archives/reorder。请求必须完整包含当前资源的全部 ID,不能重复:
{ "itemIds": ["cave-exploration", "waterfall-descent"] }
成功响应为 data: { "items": [] },其中 items 是更新 sortOrder 后的完整记录列表。缺少 ID、包含未知 ID 或出现重复 ID 时返回 400,业务码为 HOME_REORDER_INVALID。
MiniAPP Public API
GET /api/public/home
无需鉴权。接口只返回 isActive === true 的记录,并移除管理端状态、排序和审计时间字段。为统一三端首页消费模型,玩法推荐数据放入 experiences 字段,响应中不再返回 playRecommendations:
type PublicHomeResponse = {
experiences: HomeWanfaRecommendation[];
teamBuildings: HomeTeamBuilding[];
wildArchives: HomeWildArchive[];
};
type HomeWanfaRecommendation = {
id: string;
categoryId: string;
label: string;
routes: WanfaRoute[];
};
type WanfaRoute = {
id: string;
title: string;
subtitle: string;
image: string;
routeCount: number;
demandKeyword: string;
};
注意:/api/admin/home/experiences 仍是后台体验推荐 CRUD 资源;Public /api/public/home 的 experiences 字段已统一承载玩法推荐数据,不能按后台体验推荐字段解读。
三组列表始终返回数组;没有可用内容时返回空数组。experiences 只返回启用的玩法推荐关联记录,并展开关联分类当前的路线。MiniAPP 应在数据层将 experiences 归一化为玩法推荐,不再读取或维护 playRecommendations 字段。
首页玩法推荐点击行为:当 routes 存在第一条路线时,MiniAPP 使用该路线的 id 调用 goWanfaRouteDetail,进入 /pages/detail/index?routeId={route.id};没有关联路线时才使用 demandKeyword 或分类名称进入需求页。路线详情字段和 Public 详情接口以 详情展示契约 为准。
玩法推荐关联
POST /api/admin/home/play-recommendations 请求体:
type HomeWanfaRecommendationCreate = {
categoryId: string;
isActive?: boolean;
sortOrder?: number;
};
type HomeWanfaRecommendationPatch = Partial<HomeWanfaRecommendationCreate>;
Admin 响应记录包含 id、categoryId、categoryLabel、routeCount、isActive、sortOrder、createdAt 和 updatedAt。同一个玩法分类只能关联一次;分类不存在或已关联时分别返回 404 或 409 HOME_WANFA_CATEGORY_DUPLICATE。移除关联不会删除 WanfaCategory 或其路线。被首页推荐关联的玩法分类不能直接删除,需先移除首页关联。
当前 fallback 与迁移映射
以下语义 ID 仅用于 MiniAPP 本地 fallback 和迁移前的内容识别;执行 0022_opaque_ids 后,正式 API 返回对应记录的稳定 UUID,字段值和当前数组顺序保持不变:
体验推荐
| 顺序 | ID | 标题 | 需求关键词 |
|---|---|---|---|
| 0 | waterfall-descent |
悬崖瀑降 | 悬崖瀑降 |
| 1 | cave-exploration |
森林&探洞 | 地心探险 |
homeExperienceData.ts 当前的 normalizeHomeExperiences 具有以下过渡行为:
isActive === false的 API 项被过滤。- 缺少有效
title的 API 项被过滤。 - 其余项目按
sortOrder升序排列,未传排序时保持 API 原顺序。 - 文案和图片字段为空时使用当前 fallback 对应位置的值。
- API 没有有效项目时返回当前
homeExperienceMocks的副本。
这些规则用于前台接入过渡,不应替代服务端校验。后端返回正式记录后,Admin UI 应提交完整字段,避免依赖位置 fallback。
团队共创
| 顺序 | ID | 标题 | 需求关键词 |
|---|---|---|---|
| 0 | wild-challenge |
山野挑战,共创极境 | 户外团建 |
| 1 | canyon-teamwork |
峡谷溯溪,默契同行 | 峡谷团建 |
| 2 | village-gathering |
苗寨共聚,认识彼此 | 贵州团建 |
极境视界
| 顺序 | ID | 标题 | 需求关键词 |
|---|---|---|---|
| 0 | hundred-meter-descent |
百米自降 | 悬崖瀑降 |
| 1 | shilong-cave |
石龙洞 | 地心探险 |
| 2 | cliff-current |
绝壁迎流 | 峡谷探险 |
| 3 | canyon-streaming |
峡谷溯溪 | 峡谷溯溪 |
三个 mock 文件中的图片 URL 只作为初始化内容来源。正式数据应通过媒体上传接口获得最终 URL;不要把 mock 文件中的远程图片地址当成图片存储协议。
图片与素材
Admin UI 使用现有素材上传接口获取图片 URL:
POST /api/admin/media-assets/upload
建议首页内容使用 group=home,上传成功后将返回的 url 写入对应的 image 字段。接口只保存 URL,不接受 base64,也不创建 ProductImage 或商品图片关联。
Admin UI 对接要求
- 进入首页内容管理时请求三类首页卡片列表,并同时请求玩法分类和首页玩法推荐关联,按
sortOrder渲染对应分组。 - 体验推荐表单维护
badge、category、title、englishTitle、image和demandKeyword。 - 团队共创表单维护
tag、title、description、image、demandKeyword、detailSubtitle和detailParagraphs;正文使用空行分隔多个段落。 - 极境视界表单维护
title、image、images和demandKeyword;详情图片按一行一个 URL 编辑。 - 三类首页卡片提供启用/停用、编辑、删除和上移/下移操作;玩法推荐提供启用/停用、移除和上移/下移操作;排序时提交完整 ID 列表。
- 每次变更成功后以接口返回的记录或列表更新本地状态,不直接假设本地修改已经保存。
- 删除前要求二次确认;删除成功后接受服务端返回的重新排序结果。
- 处理
401、404、409、422和5xx,保存、上传和排序进行中禁用重复提交。 - 不在首页表单中出现 Product ID、ProductImage ID、价格、库存、订单或预订字段。
- 预览点击行为使用
demandKeyword;该字段为空时禁止提交,而不是由前端猜测或拼接商品标识。 - 玩法推荐管理应先加载玩法分类,再加载首页关联;新增只能从未关联分类中选择,移除只取消关联,不能删除玩法分类。
建议的 Admin UI API 封装函数:
getHomeExperiences();
createHomeExperience(input: HomeExperienceCreate);
updateHomeExperience(experienceId: string, input: HomeExperiencePatch);
deleteHomeExperience(experienceId: string);
reorderHomeExperiences(itemIds: string[]);
getHomeTeamBuildings();
createHomeTeamBuilding(input: HomeTeamBuildingCreate);
updateHomeTeamBuilding(teamBuildingId: string, input: HomeTeamBuildingPatch);
deleteHomeTeamBuilding(teamBuildingId: string);
reorderHomeTeamBuildings(itemIds: string[]);
getHomeWildArchives();
createHomeWildArchive(input: HomeWildArchiveCreate);
updateHomeWildArchive(archiveId: string, input: HomeWildArchivePatch);
deleteHomeWildArchive(archiveId: string);
reorderHomeWildArchives(itemIds: string[]);
getHomeWanfaRecommendations();
createHomeWanfaRecommendation(input: HomeWanfaRecommendationCreate);
updateHomeWanfaRecommendation(recommendationId: string, input: HomeWanfaRecommendationPatch);
deleteHomeWanfaRecommendation(recommendationId: string);
reorderHomeWanfaRecommendations(itemIds: string[]);
当前首页的“查看更多”按钮跳转客片案例列表;首页案例卡片跳转 /pages/wild-archives/detail?id={archiveId},不再跳转需求页。
前后台数据边界
- Admin API 返回启用和停用的完整记录,供管理端维护。
- 未来 Public API 只返回已发布且启用的首页内容;Public API 的响应字段应与本契约的渲染字段兼容,但不应暴露
createdAt、updatedAt等管理元数据。 - MiniAPP 已在
src/lib/api.ts、src/lib/types.ts和src/lib/store.ts接入 Public API;首页组件通过共享状态消费归一化后的三类内容,experiences按关联分类展示玩法推荐。 - 后端不得把
demandKeyword解析成 Product 或订单关联;如需线路、详情或预订能力,应另立领域契约。
团队共创详情
团队共创首页卡片点击后跳转 MiniAPP /pages/team-buildings/detail?id={teamBuildingId},详情页再按 ID 请求 Public API。首页接口只返回卡片摘要,不携带正文段落。
GET /api/public/home/team-buildings/{teamBuildingId}
成功响应在首页摘要基础上增加详情字段:
type PublicHomeTeamBuildingDetail = HomeTeamBuilding & {
detailSubtitle: string;
detailParagraphs: string[];
};
规则:
- 仅返回
isActive === true的团队共创;不存在或已停用返回404。 detailSubtitle为空时回退为description。detailParagraphs为空时回退为[description]。- MiniAPP 请求失败时按
teamBuildingId使用本地模拟详情,并提示当前为模拟数据;找不到对应 fallback 时展示未找到状态。 - 详情封面复用
image字段,不新增独立 hero 图片或详情表。
后端落地边界
当前实现由 WonderQ-Admin 提供三张首页内容表、HomeWanfaRecommendation 关联表、0017_home_content、0018_home_wanfa_recommendations、0019_home_wild_archive_images 与 0020_home_team_building_details 迁移、schema、Admin/Public 路由、序列化和审计日志;由 WonderQ-Admin-UI 提供首页玩法推荐入口、分类关联、启停、移除、排序、客片详情图片和团队共创详情维护;由 WonderQ-MiniAPP 调用 /api/public/home、团队共创详情和客片案例列表/详情接口。
相关文档: