# 计划广场与 Token 商店前端对接说明 ## 1. 文档目标 本文面向前端页面设计和接口联调,先把 P0 页面所需接口、字段、状态和交互流程定成初版。 当前确认口径: 1. 论坛展示名使用“计划广场”。 2. 前端正常展示评论,评论支持多层回复。 3. 用户可以删除自己的评论;P0 暂不引入管理员删评、举报和审核流。 4. 点赞奖励只给帖子作者。 5. 同一用户对同一计划只允许导入一次,奖励随该次导入记录一次。 6. Token 发放本轮不改 `user/auth`,先在 `token-store` 内封装 Token 获取途径和后续发放出口。 ## 2. 通用约定 ### 2.1 鉴权 P0 所有接口都需要登录态。 ```http Authorization: Bearer ``` ### 2.2 统一响应壳 成功响应: ```json { "status": "10000", "info": "success", "data": {} } ``` 失败响应: ```json { "status": "40004", "info": "计划不存在或已下架", "data": null } ``` 前端处理建议: 1. `status === "10000"` 视为成功。 2. 非成功状态优先展示 `info`。 3. 表单类错误可按接口返回的 `info` 直接提示,不需要前端硬编码太多文案。 ### 2.3 幂等请求头 以下写操作建议前端带 `X-Idempotency-Key`: 1. 发布计划。 2. 发表评论。 3. 一键导入。 4. 创建订单。 5. mock paid。 ```http X-Idempotency-Key: plan_square_publish_1710000000000_abcd ``` ### 2.4 时间格式 所有时间字段使用 ISO 8601 字符串,带时区。 ```json "2026-05-04T20:30:00+08:00" ``` ### 2.5 分页结构 分页接口统一返回: ```json { "items": [], "page": 1, "page_size": 20, "total": 125, "has_more": true } ``` ## 3. 页面总览 ### 3.1 计划广场 前端建议页面: 1. 计划广场列表页:展示公开计划卡片,支持排序、标签筛选和搜索。 2. 计划详情页 / 详情抽屉:展示计划说明、任务条目预览、点赞、评论和导入按钮。 3. 发布计划弹窗 / 页面:选择自己的 TaskClass,填写标题、简介、标签后发布。 4. 评论区:展示多层评论树,支持回复和删除自己的评论。 ### 3.2 Token 商店 前端建议页面: 1. Token 商品页:展示 Token 包商品卡片。 2. 订单确认 / mock paid 页:P0 可直接展示“确认支付”按钮。 3. Token 获取记录页:展示购买、点赞奖励、导入奖励等账本记录。 4. Token 概览组件:展示本服务记录的累计获取 Token。P0 不承诺已经同步到 `user/auth` 可用额度。 ## 4. 路由总览 ### 4.1 计划广场接口 | 功能 | 方法 | 路径 | | --- | --- | --- | | 计划列表 | `GET` | `/api/v1/plan-square/posts` | | 热门标签 | `GET` | `/api/v1/plan-square/tags` | | 发布计划 | `POST` | `/api/v1/plan-square/posts` | | 计划详情 | `GET` | `/api/v1/plan-square/posts/{post_id}` | | 点赞 | `POST` | `/api/v1/plan-square/posts/{post_id}/like` | | 取消点赞 | `DELETE` | `/api/v1/plan-square/posts/{post_id}/like` | | 评论树 | `GET` | `/api/v1/plan-square/posts/{post_id}/comments` | | 发表评论 / 回复 | `POST` | `/api/v1/plan-square/posts/{post_id}/comments` | | 删除自己的评论 | `DELETE` | `/api/v1/plan-square/comments/{comment_id}` | | 一键导入 | `POST` | `/api/v1/plan-square/posts/{post_id}/import` | 发布计划时,TaskClass 下拉列表复用现有接口: ```http GET /api/v1/task-class/list ``` ### 4.2 Token 商店接口 | 功能 | 方法 | 路径 | | --- | --- | --- | | Token 概览 | `GET` | `/api/v1/token-store/summary` | | 商品列表 | `GET` | `/api/v1/token-store/products` | | 创建订单 | `POST` | `/api/v1/token-store/orders` | | 订单列表 | `GET` | `/api/v1/token-store/orders` | | 订单详情 | `GET` | `/api/v1/token-store/orders/{order_id}` | | mock paid | `POST` | `/api/v1/token-store/orders/{order_id}/mock-paid` | | Token 获取记录 | `GET` | `/api/v1/token-store/grants` | ## 5. 计划广场接口详情 ### 5.1 计划列表 ```http GET /api/v1/plan-square/posts?page=1&page_size=20&sort=latest&keyword=数学&tag=考研 ``` 查询参数: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `page` | number | 否 | 默认 `1` | | `page_size` | number | 否 | 默认 `20`,最大建议 `50` | | `sort` | string | 否 | `latest` / `likes` / `imports` | | `keyword` | string | 否 | 搜索标题和简介 | | `tag` | string | 否 | 标签筛选 | 成功响应: ```json { "status": "10000", "info": "success", "data": { "items": [ { "post_id": 10001, "title": "30 天高数强化复习计划", "summary": "适合期末前一个月快速过完重点题型。", "tags": ["高数", "期末", "30天"], "author": { "user_id": 88, "nickname": "小鹿同学", "avatar_url": "" }, "template_summary": { "task_count": 24, "mode": "date_range", "start_date": "2026-05-05", "end_date": "2026-06-04", "strategy_labels": ["每日推进", "错题复盘"] }, "counters": { "like_count": 128, "comment_count": 32, "import_count": 45 }, "viewer_state": { "liked": false, "imported_once": true }, "status": "published", "created_at": "2026-05-04T20:30:00+08:00" } ], "page": 1, "page_size": 20, "total": 125, "has_more": true } } ``` 前端展示建议: 1. 卡片主标题用 `title`。 2. 副文案用 `summary`,最多展示两到三行。 3. 标签 chips 用 `tags`。 4. 统计区展示点赞、评论、导入次数。 5. `viewer_state.liked` 控制点赞按钮初始态。 ### 5.2 热门标签 ```http GET /api/v1/plan-square/tags?limit=20 ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "items": [ { "tag": "考研", "post_count": 30 }, { "tag": "高数", "post_count": 18 } ] } } ``` ### 5.3 发布计划 ```http POST /api/v1/plan-square/posts Content-Type: application/json X-Idempotency-Key: plan_square_publish_1710000000000_abcd ``` 请求体: ```json { "task_class_id": 42, "title": "30 天高数强化复习计划", "summary": "适合期末前一个月快速过完重点题型。", "tags": ["高数", "期末", "30天"] } ``` 字段限制: | 字段 | 规则 | | --- | --- | | `task_class_id` | 必填,只能选择当前用户自己的 TaskClass | | `title` | 必填,建议 4 到 40 字 | | `summary` | 可选,建议 0 到 300 字 | | `tags` | 可选,最多 5 个,每个最多 12 字 | 成功响应: ```json { "status": "10000", "info": "success", "data": { "post_id": 10001, "title": "30 天高数强化复习计划", "status": "published", "created_at": "2026-05-04T20:30:00+08:00" } } ``` 前端流程建议: 1. 先调用 `GET /api/v1/task-class/list` 获取用户自己的计划。 2. 用户选择一个 TaskClass 后填写发布信息。 3. 发布成功后跳转到详情页或刷新计划广场列表。 ### 5.4 计划详情 ```http GET /api/v1/plan-square/posts/{post_id} ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "post": { "post_id": 10001, "title": "30 天高数强化复习计划", "summary": "适合期末前一个月快速过完重点题型。", "tags": ["高数", "期末", "30天"], "author": { "user_id": 88, "nickname": "小鹿同学", "avatar_url": "" }, "counters": { "like_count": 128, "comment_count": 32, "import_count": 45 }, "viewer_state": { "liked": false, "imported_once": true }, "created_at": "2026-05-04T20:30:00+08:00" }, "template": { "mode": "date_range", "start_date": "2026-05-05", "end_date": "2026-06-04", "strategy_labels": ["每日推进", "错题复盘"], "task_count": 24, "items_preview": [ { "item_id": 90001, "order": 1, "content": "复习极限与连续,完成基础例题。" }, { "item_id": 90002, "order": 2, "content": "整理导数公式,完成 10 道综合题。" } ] } } } ``` 说明: 1. `items_preview` 是模板快照,不是原作者当前 TaskClass。 2. 不展示 `embedded_time`、schedule 绑定和用户私有排程状态。 3. 评论区单独调用评论树接口,避免详情首屏过重。 ### 5.5 点赞 ```http POST /api/v1/plan-square/posts/{post_id}/like ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "post_id": 10001, "liked": true, "like_count": 129, "reward_hint": { "receiver": "author", "status": "recorded", "amount": 1 } } } ``` 说明: 1. 同一用户对同一帖子只能点赞一次。 2. 点赞奖励只给作者。 3. 取消点赞不回滚已经记录的作者奖励,避免账本反复冲正。 ### 5.6 取消点赞 ```http DELETE /api/v1/plan-square/posts/{post_id}/like ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "post_id": 10001, "liked": false, "like_count": 128 } } ``` ### 5.7 评论树 ```http GET /api/v1/plan-square/posts/{post_id}/comments?page=1&page_size=20&sort=oldest ``` 查询参数: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `page` | number | 否 | 根评论分页,默认 `1` | | `page_size` | number | 否 | 根评论分页大小,默认 `20` | | `sort` | string | 否 | `oldest` / `latest`,默认 `oldest` | 成功响应: ```json { "status": "10000", "info": "success", "data": { "items": [ { "comment_id": 50001, "post_id": 10001, "parent_comment_id": null, "content": "这个计划很适合期末冲刺。", "status": "visible", "author": { "user_id": 91, "nickname": "西瓜同学", "avatar_url": "" }, "can_delete": true, "created_at": "2026-05-04T20:40:00+08:00", "deleted_at": null, "children": [ { "comment_id": 50002, "post_id": 10001, "parent_comment_id": 50001, "content": "我也准备照这个导入一份。", "status": "visible", "author": { "user_id": 92, "nickname": "青柠同学", "avatar_url": "" }, "can_delete": false, "created_at": "2026-05-04T20:42:00+08:00", "deleted_at": null, "children": [] } ] } ], "page": 1, "page_size": 20, "total": 32, "has_more": true } } ``` 评论状态: | `status` | 前端展示 | | --- | --- | | `visible` | 正常展示 `content` | | `deleted` | 展示“该评论已删除”,保留子回复 | ### 5.8 发表评论 / 回复 ```http POST /api/v1/plan-square/posts/{post_id}/comments Content-Type: application/json X-Idempotency-Key: plan_square_comment_1710000000000_abcd ``` 请求体: ```json { "content": "这个计划很适合期末冲刺。", "parent_comment_id": null } ``` 回复某条评论时: ```json { "content": "我也准备照这个导入一份。", "parent_comment_id": 50001 } ``` 字段限制: | 字段 | 规则 | | --- | --- | | `content` | 必填,建议 1 到 500 字 | | `parent_comment_id` | 可选,传 `null` 表示根评论 | 成功响应: ```json { "status": "10000", "info": "success", "data": { "comment_id": 50003, "post_id": 10001, "parent_comment_id": 50001, "content": "我也准备照这个导入一份。", "status": "visible", "can_delete": true, "created_at": "2026-05-04T20:45:00+08:00" } } ``` ### 5.9 删除自己的评论 ```http DELETE /api/v1/plan-square/comments/{comment_id} ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "comment_id": 50003, "status": "deleted", "content": "", "deleted_at": "2026-05-04T20:50:00+08:00" } } ``` 说明: 1. 只能删除自己的评论。 2. 删除是软删除,不删除子回复。 3. 非本人删除返回失败,前端展示 `info` 即可。 ### 5.10 一键导入 ```http POST /api/v1/plan-square/posts/{post_id}/import Content-Type: application/json X-Idempotency-Key: plan_square_import_1710000000000_abcd ``` 请求体: ```json { "target_title": "我的 30 天高数强化计划" } ``` 字段说明: | 字段 | 必填 | 说明 | | --- | --- | --- | | `target_title` | 否 | 导入后的 TaskClass 名称;不传则后端生成默认名称 | 成功响应: ```json { "status": "10000", "info": "success", "data": { "import_id": 70001, "post_id": 10001, "new_task_class_id": 430, "task_class_title": "我的 30 天高数强化计划", "import_count": 46, "reward_hint": { "receiver": "author", "status": "recorded", "amount": 2 }, "next_action": { "type": "open_task_class", "task_class_id": 430 }, "created_at": "2026-05-04T20:55:00+08:00" } } ``` 说明: 1. 一键导入只创建当前用户自己的 TaskClass 副本。 2. 不直接写 schedule。 3. 同一用户同一帖子只允许导入一次;导入成功后前端根据返回结果刷新 `imported_once` 和按钮状态。 ## 6. Token 商店接口详情 ### 6.1 Token 概览 ```http GET /api/v1/token-store/summary ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "recorded_token_total": 120, "applied_token_total": 0, "pending_apply_token_total": 120, "quota_sync_status": "not_connected", "tip": "当前为 Token 获取记录,后续会切换到 user/auth 权威额度。" } } ``` 字段说明: | 字段 | 说明 | | --- | --- | | `recorded_token_total` | `token-store` 内已经记录的累计获取 Token | | `applied_token_total` | 已同步到权威额度的 Token,P0 通常为 `0` | | `pending_apply_token_total` | 已记录但尚未同步到权威额度的 Token | | `quota_sync_status` | `not_connected` / `partial` / `synced` | 前端展示建议: 1. P0 可以展示“累计获取 Token”,不要写成“当前可用 Token”。 2. `quota_sync_status === "not_connected"` 时,展示轻提示,不阻塞购买流程。 ### 6.2 商品列表 ```http GET /api/v1/token-store/products ``` 说明: 1. 商品从 `token_products` 表读取。 2. P0 商品由后端 seed 初始化,不做商品管理后台。 成功响应: ```json { "status": "10000", "info": "success", "data": { "items": [ { "product_id": 1, "name": "基础 Token 包", "description": "适合轻量使用 Agent。", "token_amount": 100, "price_cent": 990, "price_text": "¥9.90", "currency": "CNY", "badge": "入门", "status": "active", "sort_order": 10 }, { "product_id": 2, "name": "进阶 Token 包", "description": "适合高频规划和复盘。", "token_amount": 300, "price_cent": 1990, "price_text": "¥19.90", "currency": "CNY", "badge": "推荐", "status": "active", "sort_order": 20 } ] } } ``` 前端展示建议: 1. 只展示 `status === "active"` 的商品。 2. `badge` 可作为角标,不保证一定有值。 ### 6.3 创建订单 ```http POST /api/v1/token-store/orders Content-Type: application/json X-Idempotency-Key: token_order_1710000000000_abcd ``` 请求体: ```json { "product_id": 1, "quantity": 1 } ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "order_id": 80001, "order_no": "TS202605042055000001", "status": "pending", "product_snapshot": { "product_id": 1, "name": "基础 Token 包", "token_amount": 100 }, "quantity": 1, "token_amount": 100, "amount_cent": 990, "price_text": "¥9.90", "currency": "CNY", "payment_mode": "mock", "payment_action": { "type": "mock_paid", "label": "确认支付" }, "created_at": "2026-05-04T20:55:00+08:00" } } ``` ### 6.4 订单列表 ```http GET /api/v1/token-store/orders?page=1&page_size=20&status=pending ``` 查询参数: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `page` | number | 否 | 默认 `1` | | `page_size` | number | 否 | 默认 `20` | | `status` | string | 否 | `pending` / `paid` / `granted` / `closed` | 成功响应: ```json { "status": "10000", "info": "success", "data": { "items": [ { "order_id": 80001, "order_no": "TS202605042055000001", "status": "pending", "product_name": "基础 Token 包", "token_amount": 100, "price_text": "¥9.90", "created_at": "2026-05-04T20:55:00+08:00", "paid_at": null, "granted_at": null } ], "page": 1, "page_size": 20, "total": 1, "has_more": false } } ``` 订单状态: | `status` | 前端含义 | | --- | --- | | `pending` | 待支付 | | `paid` | 已支付,等待写入获取记录 | | `granted` | 已写入 Token 获取记录 | | `closed` | 已关闭 | ### 6.5 订单详情 ```http GET /api/v1/token-store/orders/{order_id} ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "order_id": 80001, "order_no": "TS202605042055000001", "status": "pending", "product_snapshot": { "product_id": 1, "name": "基础 Token 包", "token_amount": 100 }, "quantity": 1, "token_amount": 100, "amount_cent": 990, "price_text": "¥9.90", "currency": "CNY", "payment_mode": "mock", "grant": null, "created_at": "2026-05-04T20:55:00+08:00", "paid_at": null, "granted_at": null } } ``` ### 6.6 mock paid ```http POST /api/v1/token-store/orders/{order_id}/mock-paid Content-Type: application/json X-Idempotency-Key: token_mock_paid_1710000000000_abcd ``` 请求体: ```json { "mock_channel": "dev" } ``` 成功响应: ```json { "status": "10000", "info": "success", "data": { "order_id": 80001, "order_no": "TS202605042055000001", "status": "granted", "paid_at": "2026-05-04T21:00:00+08:00", "granted_at": "2026-05-04T21:00:01+08:00", "grant": { "grant_id": 90001, "event_id": "order:80001:paid", "source": "purchase", "amount": 100, "status": "recorded", "quota_applied": false } } } ``` 说明: 1. P0 不接真实支付网关。 2. `status === "granted"` 表示已经写入 `token-store` 获取记录。 3. `quota_applied === false` 表示本轮未同步到 `user/auth` 权威额度。 ### 6.7 Token 获取记录 ```http GET /api/v1/token-store/grants?page=1&page_size=20&source=purchase ``` 查询参数: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `page` | number | 否 | 默认 `1` | | `page_size` | number | 否 | 默认 `20` | | `source` | string | 否 | `purchase` / `forum_like` / `forum_import` / `manual` | 成功响应: ```json { "status": "10000", "info": "success", "data": { "items": [ { "grant_id": 90001, "event_id": "order:80001:paid", "source": "purchase", "source_label": "购买 Token 包", "amount": 100, "status": "recorded", "quota_applied": false, "description": "购买基础 Token 包", "created_at": "2026-05-04T21:00:01+08:00" }, { "grant_id": 90002, "event_id": "forum.post.liked:10001:91", "source": "forum_like", "source_label": "计划被点赞", "amount": 1, "status": "recorded", "quota_applied": false, "description": "你的计划《30 天高数强化复习计划》获得点赞", "created_at": "2026-05-04T21:05:00+08:00" } ], "page": 1, "page_size": 20, "total": 2, "has_more": false } } ``` 获取记录状态: | `status` | 前端含义 | | --- | --- | | `recorded` | 已记录获取事实,P0 默认状态 | | `applied` | 已同步到权威额度,后续切 `user/auth` 后使用 | | `skipped` | 幂等重复或规则不发放 | | `failed` | 写入或后续发放失败 | ## 7. 页面交互建议 ### 7.1 计划广场列表页 1. 首屏调用计划列表接口,默认 `sort=latest`。 2. 顶部筛选可放搜索框、排序切换和热门标签。 3. 卡片 CTA 建议包含“查看详情”和“一键导入”。 4. 点赞按钮可在卡片上直接操作,成功后用接口返回的 `like_count` 覆盖本地计数。 ### 7.2 计划详情页 1. 进入详情页先拉计划详情。 2. 评论区单独拉评论树。 3. 发表评论或删除评论成功后,建议重新拉评论树,避免多层结构局部更新出错。 4. 一键导入成功后,可弹出成功反馈,并提供“查看我的 TaskClass”入口。 ### 7.3 发布计划 1. 打开发布弹窗时调用现有 `GET /api/v1/task-class/list`。 2. 用户选择 TaskClass 后填写标题、简介、标签。 3. 发布按钮需要防重复点击,并发送 `X-Idempotency-Key`。 4. 发布成功后跳转详情页或刷新列表。 ### 7.4 Token 商店 1. 首屏并行调用 Token 概览和商品列表。 2. 商品卡片点击“购买”后创建订单。 3. P0 下单后展示 mock paid 确认按钮。 4. mock paid 成功后刷新 Token 概览和获取记录。 5. P0 文案建议使用“累计获取 Token”,暂不写“可用 Token 已到账”。 ## 8. 前端类型参考 ```ts type ApiResponse = { status: string info: string data: T } type PageData = { items: T[] page: number page_size: number total: number has_more: boolean } type UserBrief = { user_id: number nickname: string avatar_url: string } type PlanSquarePostCard = { post_id: number title: string summary: string tags: string[] author: UserBrief template_summary: { task_count: number mode: string start_date: string end_date: string strategy_labels: string[] } counters: { like_count: number comment_count: number import_count: number } viewer_state: { liked: boolean imported_once: boolean } status: 'published' created_at: string } type CommentNode = { comment_id: number post_id: number parent_comment_id: number | null content: string status: 'visible' | 'deleted' author: UserBrief can_delete: boolean created_at: string deleted_at: string | null children: CommentNode[] } type TokenProduct = { product_id: number name: string description: string token_amount: number price_cent: number price_text: string currency: 'CNY' badge: string status: 'active' | 'inactive' sort_order: number } type TokenGrant = { grant_id: number event_id: string source: 'purchase' | 'forum_like' | 'forum_import' | 'manual' source_label: string amount: number status: 'recorded' | 'applied' | 'skipped' | 'failed' quota_applied: boolean description: string created_at: string } ``` ## 9. P0 不需要前端处理的内容 1. 不做计划推荐算法,只做排序和筛选。 2. 不做关注、私信、用户主页。 3. 不做富文本,简介和评论都是纯文本。 4. 不做管理员删评、举报和审核后台。 5. 不接真实支付网关。 6. 不直接展示 `user/auth` 权威可用额度,除非后续接口已经切通。