Files
gongxue-base/docs/refactor/taro-frontend-integration.md
2026-06-29 05:05:42 +08:00

1038 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Taro 前端对接指南
更新时间2026-06-28
目标:用一套 Taro 工程同时服务微信小程序和 H5 Web 题库并采用“Supabase Auth/JWT + `apps/api` 业务 API 优先”的混合架构,支持多租户、品牌主题、地区题库、会员权益、销售追踪和对象存储资源。
建议新建:
```text
F:\project\apps\taro
```
旧前端参考:
```text
F:\project\参考\旧题库项目\src
```
## 启动流程
### H5
1.`window.location.host` 获取当前域名。
2. 调用 `GET /api/tenant/resolve?host=<host>`
3. 保存 `tenant.id``tenant.slug``branding``features``publicConfig`
4. 初始化主题、Logo、页面标题、功能开关。
5. 检查本地 session token调用 `GET /api/auth/me`
6. 如果未登录,进入登录页;如果已登录,加载个人中心和首页数据。
### 微信小程序
1. 从编译环境或小程序启动参数读取 `tenantCode`
2. 推广码、销售码、分享码从 `options``scene` 中解析。
3. 调用 `GET /api/tenant/resolve?tenantCode=<tenantCode>`
4. 如存在 referral 参数,先调用 `/api/referral/resolve``/api/referral/track-event`
5. 登录后再调用 `/api/referral/bind` 完成首绑保护。
## 请求封装
本项目不采用“前端直接写 Supabase 表替代业务命令层”的模式。Supabase 官方允许前端在 RLS 和最小权限下使用 Data API但本系统的订单、支付、权益、租户后台、内容导入、CRM、对象存储签名等都需要服务端事务、密钥、审计和幂等所以复杂业务命令默认调用 RPC、`apps/api`、Edge Function 或 worker。
前端可以使用 Supabase client 的范围:
- H5 Auth session/JWT。
- 小程序端在兼容性验证通过后的 Auth session/JWT。
- 低风险公开只读数据,且必须已经有 RLS、grant、跨租户测试。
- Realtime 非敏感通知。
前端必须调用 `apps/api` 的范围:
- 题库练习、答题、错题、收藏。
- 订单、支付、激活码、优惠券、权益。
- 私有 PDF、资料、视频、对象存储签名。
- 租户后台、平台后台、内容导入、CRM、销售/代理、数据看板。
前端应封装一个统一 API client所有页面禁止直接散写 `Taro.request`
本地迁移期仍可兼容旧请求头,但新的 Taro 请求封装必须按下面目标实现:
```text
Authorization: Bearer <tk_session>
x-tenant-id: <tenantId> # 仅作为登录前/公开目录租户上下文;登录后必须与 session 租户一致
```
生产目标:
```text
Authorization: Bearer <supabase_access_token>
x-tenant-id: <tenantId> # 作为租户上下文,不能作为身份依据
```
前端不应再传 `x-user-id`、query/body `userId` 来表示当前用户。后端已经实现 Supabase JWT 和迁移 session 优先解析:如果 Authorization 存在,用户态接口以 token 映射出的业务用户为准;如果请求里伪造了不同的 `userId` 会返回 `AUTH_USER_MISMATCH`,伪造不同租户会返回 `AUTH_TENANT_MISMATCH``AUTH_SESSION_INVALID`
H5 使用 Supabase Auth 时,推荐请求流程:
```ts
const { data } = await supabase.auth.getSession();
const accessToken = data.session?.access_token;
await api.request('/api/profile/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
'x-tenant-id': tenantStore.tenantId,
},
});
```
后端会通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射用户身份。`x-tenant-id` 只能帮助确定当前租户上下文,不能让用户访问自己没有 membership 的租户。
生产或云端测试建议设置:
```text
ALLOW_LEGACY_AUTH_HEADERS=false
ALLOW_PLATFORM_ADMIN_KEY=false
```
这样旧式 `x-user-id` 和平台管理 key 会被拒绝,前端可以提前发现未按 session 接入的页面。
前端环境变量只允许包含:
```text
TARO_APP_API_BASE_URL
TARO_APP_SUPABASE_URL
TARO_APP_SUPABASE_PUBLISHABLE_KEY
```
禁止把 Supabase secret key、service role key、数据库连接串、对象存储密钥、支付私钥放进 Taro。
统一错误处理:
| HTTP | 前端动作 |
| --- | --- |
| 400 | 展示表单错误或参数错误 |
| 401 | 清 session跳登录 |
| 403 | 展示无权限或会员升级 |
| 404 | 展示空状态 |
| 409 | 展示业务冲突,例如激活码已用 |
| 413 | 提示上传/导入文件过大 |
| 429 | 倒计时重试,例如短信冷却 |
| 500 | 展示系统异常并上报日志 |
## 全局状态建议
| Store | 内容 |
| --- | --- |
| tenantStore | tenant、branding、theme、features、publicConfig |
| authStore | session、user、roles、permissions、loginState |
| regionStore | 当前地区、可选地区、地区权益 |
| catalogStore | content entries、nodes、collections、blueprints |
| entitlementStore | SVIP 权益、视频权益、资料下载权益 |
| referralStore | inviteCode、referrer、scene、bindState |
| uiStore | 当前主题、tab、loading、toast、modal |
注意缓存必须带租户维度,例如:
```text
tenant:<tenantId>:catalog:entries
tenant:<tenantId>:profile
tenant:<tenantId>:theme
```
切换租户或切换小程序环境时必须清理旧租户缓存。
## 页面/API 映射
| 页面 | 主要接口 |
| --- | --- |
| 启动页 | `GET /api/tenant/resolve` |
| 登录页 | `POST /api/auth/sms/send``POST /api/auth/sms/verify``POST /api/auth/oauth/wechat-miniapp`、后续微信网页/QQ provider |
| 首页 | `/api/catalog/content-entries``/api/catalog/banners``/api/catalog/announcements``/api/catalog/exam-dates``/api/profile/me` |
| 选地区 | `/api/catalog/regions``/api/commerce/entitlements/check` |
| 题库入口 | `/api/catalog/content-entries` |
| 分类树 | `/api/catalog/content-nodes?entryId=...&parentId=root` |
| 题目列表 | `/api/catalog/question-collections``/api/catalog/question-collections/questions` |
| 开始练习 | `POST /api/learning/practice-sessions` |
| 提交答案 | `POST /api/learning/answers` |
| 交卷/报告 | `POST /api/learning/practice-sessions/submit``GET /api/learning/practice-sessions/report``GET /api/learning/practice-reports` |
| 错题本 | `GET /api/learning/wrong-questions``POST /api/learning/wrong-questions/resolve` |
| 错题复习 | `GET /api/learning/wrong-questions/review-plan``POST /api/learning/practice-sessions` with `mode=wrong_review` |
| 收藏夹 | `GET/POST /api/learning/favorites/questions` |
| 练习历史/统计 | `GET /api/learning/practice-sessions/history``GET /api/learning/stats``GET /api/learning/trend` |
| 学习排行榜 | `GET /api/learning/leaderboard?metric=questions&period=all` |
| 题目视频 | `GET /api/questions/{questionId}/videos``POST /api/questions/videos/batch``POST /api/videos/play` |
| 题目反馈 | `POST /api/profile/feedbacks``GET /api/profile/feedbacks` |
| 分佣结算 | `GET /api/commission/settings``PUT /api/commission/settings``PUT /api/commission/member-rate``GET /api/commission/summary``GET /api/commission/orders``GET /api/commission/settlements``POST /api/commission/settlements/generate``POST /api/commission/settlements/status` |
| 背单词 | `/api/catalog/vocabulary-units``/api/catalog/vocabulary-words` |
| 单词进度/计划 | `/api/learning/vocabulary/progress``/api/learning/vocabulary/stats``/api/learning/vocabulary/review-plan``POST /api/learning/vocabulary/review` |
| 单词收藏 | `/api/learning/vocabulary/favorites` |
| 知识手册 | `/api/catalog/handbook-subjects``handbook-chapters``handbook-entries` |
| 分数线 | `/api/scoreline/fields``schools``majors``records``trend``years` |
| 资料下载/预览 | `/api/catalog/assets``/api/catalog/assets/preview``/api/catalog/assets/download` |
| 商城 | `/api/catalog/svip-plans``POST /api/commerce/coupons/claim``POST /api/commerce/orders``POST /api/commerce/payments/create` |
| 订单/权益 | `/api/commerce/orders``/api/commerce/orders/detail``/api/commerce/orders/status``/api/commerce/entitlements` |
| 激活码 | `POST /api/commerce/activation-codes/check``POST /api/commerce/activation-codes/redeem` |
| 个人中心 | `GET/PATCH /api/profile/me``POST /api/profile/check-in``GET /api/profile/score-events``GET /api/profile/exam-countdowns` |
| 销售分享 | `/api/referral/resolve``track-event``bind` |
| 租户数据看板 | `GET /api/tenant-admin/dashboard?timeRange=30d&regionId=...` |
| 租户班级 | `GET/PUT /api/tenant-admin/classes``POST /api/tenant-admin/classes/disable` |
| 班级成员 | `GET/PUT /api/tenant-admin/classes/members``POST /api/tenant-admin/classes/members/remove``POST /api/tenant-admin/classes/members/bulk-assign` |
| 租户学生 | `GET/PUT /api/tenant-admin/students``POST /api/tenant-admin/students/bulk-upsert``POST /api/tenant-admin/students/status` |
| 学生备注 | `GET/PUT /api/tenant-admin/students/notes` |
| 学生跟进任务 | `GET/PUT /api/tenant-admin/students/followups` |
| 租户教师 | `GET /api/tenant-admin/teachers` |
| 租户考试日期 | `GET/PUT /api/tenant-admin/exam-dates` |
| 租户反馈处理 | `GET /api/tenant-admin/feedbacks``POST /api/tenant-admin/feedbacks/status``GET /api/tenant-admin/feedbacks/events` |
| 公共题库采纳 | `GET /api/tenant-content/public-question-banks``POST /api/tenant-content/public-question-banks/adopt` |
## 练习访问控制契约
前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 `POST /api/learning/practice-sessions`,后端会根据 `content_entries.accessRules``content_nodes.accessRules``question_collections.accessRules``practice_blueprints.accessRules` 和当前用户权益决定最终题目快照。
请求示例:
```json
{
"mode": "sequential",
"collectionId": "00000000-0000-0000-0000-000000000615",
"questionLimit": 50
}
```
响应关键字段:
```json
{
"item": {
"id": "...",
"mode": "sequential",
"questionIds": ["..."],
"questionCount": 25,
"accessMode": "free",
"consumedFreeQuota": 25,
"accessSnapshot": {
"grantedBy": "free_quota",
"requestedCount": 50,
"grantedCount": 25,
"truncated": true,
"dailyLimit": 25
}
}
}
```
前端处理规则:
- 以返回的 `questionIds` 为准渲染本次练习,不要自行追加题目。
- `accessSnapshot.truncated=true` 时,可提示“今日免费额度有限,已为你开放 N 题”并引导开通 SVIP。
- `PRACTICE_FREE_LIMIT_REACHED`:弹出会员购买/激活码兑换入口。
- `PRACTICE_SVIP_REQUIRED`:提示该内容需要对应地区/科目/题库 SVIP。
- `PRACTICE_SESSION_QUESTION_FORBIDDEN`:说明提交答案的题目不在本次 session 快照内,应清理本地异常进度并重新开始。
- 提交答案必须传 `practiceSessionId`;后端会拒绝不属于本人有效 session 的题目。
### 模考交卷与报告
全真模拟、试卷模式、顺序练习的最终报告都走后端交卷接口。前端不得传分数、正确数或题目范围;后端只信任 `practice_sessions.question_ids` 快照和 `answer_records` 最新答题记录。
交卷请求:
```json
{
"practiceSessionId": "00000000-0000-0000-0000-000000000000"
}
```
响应关键字段:
```json
{
"item": {
"id": "...",
"practiceSessionId": "...",
"mode": "mock_exam",
"totalQuestions": 3,
"answeredCount": 2,
"correctCount": 1,
"wrongCount": 1,
"unansweredCount": 1,
"score": 2,
"totalScore": 100,
"accuracy": 0.3333,
"sectionStats": [
{
"key": "choice",
"title": "单选题",
"questionCount": 3,
"correctCount": 1,
"score": 2,
"totalScore": 6
}
],
"wrongQuestionIds": ["..."],
"questionResults": [
{
"questionId": "...",
"sectionKey": "choice",
"answered": true,
"isCorrect": false,
"score": 0,
"totalScore": 2,
"selectedOptions": ["0"],
"correctOptionIndices": [1],
"explanation": "..."
}
]
}
}
```
前端处理规则:
- 重复交卷是幂等的,后端会返回同一份报告。
- 报告页刷新时调用 `GET /api/learning/practice-sessions/report?practiceSessionId=...`
- 个人中心/模考历史调用 `GET /api/learning/practice-reports?mode=mock_exam&limit=20`,也可以传 `blueprintId` 筛选某套模拟卷。
- `score` 是逐题得分合计;`totalScore` 保留后台配置的卷面总分。测试或预发数据题量不足时,两者不一定按百分制等比换算,前端展示时不要自行重算。
- 错题复盘优先使用 `wrongQuestionIds``questionResults`,题目详情仍可按现有题目接口或 session 快照加载。
### 练习历史、统计和错题复习
个人中心和学习报告页优先使用后端聚合接口,不要让前端遍历全部答题记录自行统计。
接口用途:
| 页面/组件 | 接口 | 说明 |
| --- | --- | --- |
| 练习历史列表 | `GET /api/learning/practice-sessions/history?limit=20` | 返回 session、报告、已答数量、正确数、状态 |
| 学习概览卡片 | `GET /api/learning/stats?days=30` | 返回总答题、正确率、报告数、错题数、收藏数、题型分布 |
| 正确率趋势图 | `GET /api/learning/trend?days=14` | 返回每日答题数、正确数、错题数、session 数、报告数 |
| 错题复习入口 | `GET /api/learning/wrong-questions/review-plan?limit=20` | 返回建议复习题和后端组卷 nextAction |
| 排行榜 | `GET /api/learning/leaderboard?metric=questions&period=7d&regionId=...&classId=...` | 返回排名、用户展示信息、当前用户排名和范围信息 |
错题复习创建 session
```json
{
"mode": "wrong_review",
"questionLimit": 20
}
```
前端处理规则:
- 不要把错题 ID 列表从前端传回后端组卷;`wrong_review` 会由后端按当前用户错题本安全组卷。
- `review-plan.nextAction` 可直接用于按钮配置,但仍需使用当前登录 session 调用。
- 收藏夹复习同理可调用 `POST /api/learning/practice-sessions`body 为 `{ "mode": "favorite_review", "questionLimit": 20 }`
- 趋势图以接口返回日期桶为准,缺失日期后端会补 0不需要前端补点。
### 排行榜
排行榜由后端统一聚合,前端不要读取答题记录、单词进度或模考报告后自行排名,避免越权、口径漂移和跨租户数据泄露。
可选参数:
| 参数 | 可选值 | 说明 |
| --- | --- | --- |
| `metric` | `questions``score``vocabulary``mock_exam` | 分别表示累计答题、积分、掌握单词、模考最高分 |
| `period` | `all``7d``30d` | 统计周期 |
| `regionId` | UUID | 地区范围,可选 |
| `classId` | UUID | 班级范围,可选,后端按当前租户校验 |
| `limit` / `page` | 正整数 | 分页 |
响应会包含 `items``currentUser`。即使当前用户未进入前 N 名,也应优先展示 `currentUser` 作为“我的排名”。后台后续会补日/周榜预聚合和防刷策略,前端只消费接口返回口径。
### 租户数据看板
租户后台数据看板由后端统一聚合,前端不要直接读取订单、答题记录、学生列表后自行统计,避免权限越权、敏感信息外泄和各页面口径不一致。
请求:
```http
GET /api/tenant-admin/dashboard?timeRange=30d&regionId=<可选地区ID>&limit=10
```
可选参数:
| 参数 | 可选值 | 说明 |
| --- | --- | --- |
| `timeRange` | `7d``30d``90d` | 统计区间,默认 `30d` |
| `regionId` | UUID | 可选地区筛选,后端会校验地区属于当前租户 |
| `limit` | 1-50 | 题型、科目、地区、套餐和运营动态的返回条数 |
响应主要结构:
```json
{
"item": {
"scope": {
"tenantId": "...",
"regionId": "...",
"timeRange": "30d",
"timezone": "Asia/Shanghai"
},
"cards": {
"students": {},
"learning": {},
"content": {},
"activationCodes": {},
"feedback": {}
},
"paymentStats": {},
"trends": [],
"activeHours": [],
"questionDistribution": [],
"subjectTop": [],
"regionStats": [],
"planSales": [],
"recentActivities": []
}
}
```
前端处理规则:
- 管理台菜单显示可按 `/api/tenant-admin/permissions``dashboard:read` 判断,但真正权限以后端返回为准。
- `trends` 已补齐自然日桶,`activeHours` 固定 24 项,前端不需要补点。
- `revenueCents``amountCents` 都是分,前端统一格式化成人民币展示,不要自行重算订单金额。
- `recentActivities.details` 只包含可展示的低敏汇总信息,不包含手机号、支付密钥、对象存储 key 等敏感字段。
- 大租户正式上线后会补预聚合 worker前端不应依赖任何临时 SQL 口径或自己维护缓存口径。
### 销售/代理分佣结算
分佣结算由后端统一计算,前端不要读取订单、激活码或客资后自行算佣金。当前后端已支持订单和激活码两类来源,并且只统计客资首绑保护后的成交,避免后绑抢单。
常用接口:
| 页面/动作 | 接口 | 权限 |
| --- | --- | --- |
| 查看租户分佣设置 | `GET /api/commission/settings` | `commission:read` |
| 修改默认分佣设置 | `PUT /api/commission/settings` | `commission:write` |
| 设置销售/代理个人比例 | `PUT /api/commission/member-rate` | `commission:write` |
| 分佣汇总 | `GET /api/commission/summary?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD&referrerUserId=...` | `commission:read``commission:self` |
| 分佣来源明细 | `GET /api/commission/orders?...` | `commission:read``commission:self` |
| 结算单列表 | `GET /api/commission/settlements?...` | `commission:read``commission:self` |
| 生成结算单 | `POST /api/commission/settlements/generate` | `commission:write` |
| 审核/打款状态 | `POST /api/commission/settlements/status` | `commission:review` |
金额字段统一为分:
```text
grossAmountCents
commissionAmountCents
minSettlementCents
```
比例字段统一为 0 到 1 的数字:
```text
defaultRate = 0.2
commissionRate = 0.35
```
结算状态:
```text
draft -> pending_review -> approved -> paid
pending_review -> rejected/cancelled
approved -> cancelled
```
前端处理规则:
- 销售/代理默认只有 `commission:self`,只能查看自己的分佣;租户运营/管理员拥有 `commission:read` 才能查看全局。
- `startDate/endDate` 使用 `YYYY-MM-DD`,后端按 `Asia/Shanghai` 业务日计算账期。
- 分佣比例优先级由后端处理:激活码批次比例 > 成员个人比例 > 租户默认比例。
- `sourceType=order` 表示学生订单;`sourceType=activation_code` 表示激活码兑换。
- 已进入结算单的来源会返回 `settlementId/settlementStatus`,前端不要重复发起生成。
- 已打款结算单不可再修改状态;遇到 `COMMISSION_SETTLEMENT_LOCKED` 展示“已打款,不可变更”。
- `COMMISSION_NO_UNSETTLED_SOURCES` 表示当前账期无未结算来源,不是系统异常。
- 当前版本仅支持线下打款状态登记;真实银行/微信/支付宝打款、导出、发票/凭证和财务复核后续由 worker/provider 增强。
### 背单词计划与复习上报
背单词页面分三类数据:单元列表、每日计划、单词进度。前端不需要计算下次复习日期,只提交“认识/不认识”,由后端统一更新 `nextReviewDate`、连续正确、掌握状态和每日复习计划。
取今日计划:
```http
GET /api/learning/vocabulary/review-plan?unitId=<unitId>&reviewLimit=30&newLimit=20
```
响应关键字段:
```json
{
"item": {
"dueCount": 3,
"newCount": 20,
"totalPlanned": 23,
"dueWords": [{ "wordId": "...", "status": "reviewing", "dueLevel": "soon" }],
"newWords": [{ "wordId": "...", "status": "new", "dueLevel": "new" }],
"words": []
}
}
```
上报单词复习结果:
```json
{
"wordId": "00000000-0000-0000-0000-000000000812",
"result": "known"
}
```
`result` 可传:
- `known`:认识/答对。
- `unknown`:不认识/答错。
前端处理规则:
- `review-plan.words` 是本轮学习队列;卡片翻转、上一个、跳转、收藏状态属于前端交互。
- 每个单词点击“认识/不认识”后调用 `POST /api/learning/vocabulary/review`
- 返回的 `status``dueLevel``nextReviewDate` 作为后续展示依据,不在前端重算间隔。
- 旧的 `POST /api/learning/vocabulary/progress` 保留给兼容和后台手工修正;普通学习流优先用 `vocabulary/review`
## 视频播放契约
题目视频分为 `free``svip``video_quota` 三种访问模式。列表接口只用于展示标题、封面、时长、访问模式和试看秒数;除免费公开视频外,列表和搜索接口不会返回可播放 URL。
播放步骤:
1. 进入题目页后调用 `GET /api/questions/{questionId}/videos` 或批量预加载 `POST /api/questions/videos/batch`
2. 用户点击播放时调用 `POST /api/videos/play`
3. 后端校验当前 session 用户、租户、题目绑定关系、SVIP 权益或视频次数权益。
4. 后端返回短期签名 URL、播放 token、权益来源和过期时间。
5. 前端播放器只使用本次返回的 `playback.url`,不要缓存为长期资源地址。
请求示例:
```json
{
"videoId": "00000000-0000-0000-0000-000000000821",
"questionId": "00000000-0000-0000-0000-000000000401"
}
```
响应关键字段:
```json
{
"item": {
"id": "...",
"title": "...",
"accessMode": "svip",
"freePreviewSeconds": 15
},
"playToken": "vp_...",
"playback": {
"url": "https://...",
"expiresAt": "2026-06-28T12:00:00.000Z",
"signatureMode": "signed"
},
"access": {
"mode": "svip",
"entitlementId": "...",
"quotaAccountId": null,
"consumedQuota": 0
}
}
```
前端处理规则:
- `VIDEO_SVIP_REQUIRED`:弹出开通或升级会员。
- `VIDEO_QUOTA_REQUIRED`:提示购买视频次数包或套餐。
- `VIDEO_ASSET_REQUIRED`:展示“视频暂不可播放”,同时上报前端日志。
- 签名 URL 过期后必须重新调用 `/api/videos/play`,不要重试旧 URL。
- 小程序/H5 不保存对象存储真实 key不把播放 URL 写入本地持久缓存。
## 资料上传、预览和下载契约
学生端资料只读取目录、预览和下载签名,不接触对象存储真实密钥,也不自行拼接私有 bucket 地址。
学生端展示资料列表:
```http
GET /api/catalog/assets?assetType=pdf&regionId=<regionId>&includeLocked=true
```
学生端 PDF/图片预览:
```http
GET /api/catalog/assets/preview?assetId=<assetId>
```
学生端下载:
```http
GET /api/catalog/assets/download?assetId=<assetId>
```
前端处理规则:
- `preview.url` 是短期 inline URL只给预览组件使用不持久化。
- `download.url` 是短期 attachment URL只给下载动作使用。
- `ASSET_SVIP_REQUIRED`:提示开通对应地区/科目权益。
- `ASSET_UPLOAD_NOT_VERIFIED`:展示“资料正在处理中”,并上报前端日志。
- `ASSET_PREVIEW_NOT_SUPPORTED`:隐藏预览按钮,仅保留下载或提示不支持预览。
- `previewUrl` 字段只作为公开/托管预览提示,不代表可以绕过接口直接访问。
租户后台上传资料必须走五步:
```text
sign-upload -> 直传对象存储 -> PUT assets 登记草稿 -> confirm-upload -> sign-preview 验收
```
后台上传确认:
```json
{
"assetId": "<assetId>",
"fileSizeBytes": 4096,
"mimeType": "application/pdf",
"checksumSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"publish": true
}
```
托管对象在确认前会保持 `status=draft``uploadStatus=pending`,学生端不会看到。确认失败时后端返回 `UPLOAD_VERIFICATION_FAILED`,后台必须展示失败原因并允许重新上传,不能前端强行改为已发布。
## 考试倒计时、签到积分和反馈
首页可用 `GET /api/catalog/exam-dates?regionId=<regionId>` 展示地区公开考试日期;个人中心优先用 `GET /api/profile/exam-countdowns`,后端会按学生当前 `regionId/selectedSchoolId` 返回匹配倒计时。
签到入口调用:
```http
POST /api/profile/check-in
```
返回关键字段:
```json
{
"item": {
"checkedIn": true,
"alreadyCheckedIn": false,
"pointsAdded": 10,
"streak": 1,
"score": 10,
"lastCheckInDate": "2026-06-29"
}
}
```
前端处理规则:
- `alreadyCheckedIn=true` 时展示今日已签到,不要本地再加分。
- 积分明细调用 `GET /api/profile/score-events`
- 积分最终余额以后端 `score` 和流水为准,前端只做展示。
题目页、资料页或视频页可提交反馈:
```json
{
"questionId": "...",
"type": "question_error",
"category": "answer",
"title": "题目解析有误",
"description": "请填写具体问题",
"attachments": []
}
```
前端处理规则:
- `questionId` 如存在,后端会校验题目必须属于当前租户。
- 反馈状态由租户后台处理,学生可用 `GET /api/profile/feedbacks` 查看自己的反馈历史。
- 租户后台处理反馈时,奖励积分由后端 `idempotency_key` 保证不会重复发放,前端不要重复叠加。
## 题库新模型接入方式
旧项目常按“地区 -> 科目 -> 章节/试卷”固定层级处理。新项目不要写死层级,按下面模型渲染:
```text
content_entries
-> content_nodes 任意深度分类树
-> question_collections 题目列表/试卷/章节/题型集合
-> practice_blueprints 顺序/随机/全真模拟规则
```
前端建议:
- `entryType=question_practice` 渲染为刷题入口。
- `entryType=vocabulary` 渲染为背单词入口。
- `entryType=handbook` 渲染为知识手册入口。
- `markerType=school``markerType=exam_track` 可作为学生目标院校/专业意向采集。
- 不同地区节点层级可以不同,页面组件必须支持递归树和面包屑。
## 多租户前端优化
- Logo、标题、主题色、客服信息全部来自 `tenant/resolve`
- 功能开关控制菜单显示,但接口权限仍以后端为准。
- 私有图片、PDF、视频不要直接拼 URL一律通过后端签名。
- 支付渠道从后端返回或租户配置读取,不在页面硬编码。
- 小程序分享路径必须带 tenantCode 和 referral code。
- 用户首绑归属由后端保护,前端不要提供“换绑销售”入口。
- 管理后台菜单按 `GET /api/tenant-admin/permissions` 返回的 `current.permissions``current.templatePermissions``current.menuPermissions``current.modulePermissions` 渲染;接口权限仍以后端校验为准。
- 教师、班主任、助教类账号进入租户后台时,学生列表以 `GET /api/tenant-admin/students` 返回的 `scoped``items` 为准;前端不要自行用本地班级 ID 放大查询范围。
- 学生手机号、订单金额、客资归属等敏感字段按 `fieldPermissions` 控制显示;字段被后端返回为 `null` 时前端展示脱敏占位,不要从其它接口补取。
- 学生批量导入和批量分班接口会返回 `total/successCount/errorCount/results`,前端必须展示逐行错误,不要在浏览器端静默丢弃失败行。
- 教师可以为范围内学生创建备注和跟进任务,但是否能禁用学生、批量导入、查看手机号由后端权限和字段权限决定;前端只按返回值渲染。
- H5 自定义域名下要注意缓存隔离,不能把 A 租户主题缓存用到 B 租户。
## 公共题库采纳对接
平台超级管理员后台使用:
```text
GET /api/platform-admin/question-banks
GET /api/platform-admin/question-bank-grants
PUT /api/platform-admin/question-bank-grants
```
授权参数建议:
```json
{
"sourceQuestionBankId": "<平台公共题库ID>",
"grantScope": "plans",
"allowedPlanCodes": ["starter_yearly", "pro_yearly"],
"status": "active"
}
```
`grantScope` 可选:
- `plans`:按 SaaS 套餐授权。
- `tenants`:指定租户授权。
- `mixed`:套餐和指定租户同时生效。
- `all_active_tenants`:所有有效订阅租户可见。
租户内容后台使用:
```text
GET /api/tenant-content/public-question-banks
POST /api/tenant-content/public-question-banks/adopt
```
采纳请求:
```json
{
"grantId": "<授权ID>",
"entryName": "天津专升本公共题库",
"collectionName": "天津专升本公共题目",
"copyLimit": 500
}
```
前端处理规则:
- 租户只能看到后端判定为已授权的公共题库,不要在前端用套餐码自行过滤。
- 采纳成功后后端会生成本租户自己的 `questionBankId``entryId``collectionId` 和题目快照,学生端直接按普通 `/api/catalog/content-entries``question-collections``practice-sessions` 接入。
- 重复采纳返回 `QUESTION_BANK_ALREADY_ADOPTED`,前端展示“已采纳”即可。
- 当前版本是快照复制;平台公共题库后续更新不会自动进入租户题库,后续会由 worker 做版本同步、冲突处理和租户确认。
## 登录对接
### 短信登录
开发环境可以先使用 mock 短信,接口会返回 `debugCode`。生产环境禁止依赖 `debugCode`
```text
POST /api/auth/sms/send
body: { "phone": "13800000000", "purpose": "login" }
POST /api/auth/sms/verify
body: { "phone": "13800000000", "code": "123456", "purpose": "login" }
```
成功后保存:
```text
session.token
session.expiresAt
user
```
后续请求统一带:
```text
Authorization: Bearer <session.token>
x-tenant-id: <tenantId>
```
### 微信小程序登录
微信小程序端调用 `Taro.login()` 获取 code然后交给后端
```text
POST /api/auth/oauth/wechat-miniapp
body: {
"code": "<wx.login code>",
"profile": {
"nickName": "...",
"avatarUrl": "..."
}
}
```
成功响应包含:
```text
provider
user
isNewUser
session.token
session.expiresAt
identity.openId
identity.unionId
```
注意:
- 前端不接触 `appSecret`
- 前端不会拿到微信 `session_key`
- 如果登录前已经解析到推广码,登录成功后再调用 `/api/referral/bind` 完成首绑保护。
- 手机号授权后续应走独立的“绑定手机号”接口,不要把微信手机号解密逻辑写在页面里。
## 支付对接
支付流程必须以后端订单金额和后端回调为准,前端只负责拉起支付。
### 创建订单
```text
POST /api/commerce/orders
body: {
"planId": "<svipPlanId>",
"quantity": 1,
"payProvider": "wechat_pay | alipay",
"payMethod": "jsapi | wap",
"regionId": "<regionId>",
"couponCode": "<可选,优惠券码>",
"couponRedemptionId": "<可选,已领取优惠券 redemptionId>"
}
```
前端可以先领取优惠券,再下单:
```text
POST /api/commerce/coupons/claim
body: {
"code": "<couponCode>",
"planId": "<svipPlanId>",
"regionId": "<regionId>"
}
```
`coupons/claim` 对同一用户同一优惠券是幂等的;已使用的券会返回 `COUPON_ALREADY_USED`。下单时后端会重新计算套餐原价、优惠金额和最终应付,前端展示金额只能使用接口返回的 `originalAmountCents``discountCents``amountCents`
如果优惠后 `amountCents=0`,后端会立即把订单置为 `paid` 并发放权益,前端不要再调用 `payments/create`
返回未支付 `orderNo` 后,再创建支付参数:
```text
POST /api/commerce/payments/create
body: {
"orderNo": "<orderNo>",
"provider": "wechat_pay",
"openId": "<微信小程序登录后的 openId>"
}
```
微信小程序返回的 `paymentParams` 可直接映射到 `Taro.requestPayment`
```text
appId
timeStamp
nonceStr
package
signType
paySign
```
支付宝 H5/WAP 返回:
```text
paymentParams.url
```
H5 可以跳转到该 URL。小程序端如果后续要接支付宝小程序需要新增独立 provider/method不要复用 H5 WAP URL。
支付完成后前端不要自行开通会员。前端应轮询或重新请求:
```text
GET /api/commerce/orders/status?orderNo=<orderNo>
GET /api/commerce/orders/detail?orderNo=<orderNo>
GET /api/commerce/entitlements
```
订单详情会返回 `pricing``payments``items``couponRedemptions`,可用于收银台、订单详情页和售后排查。订单状态轮询页只需消费 `status/payment`,避免频繁拉取全量明细。
### 退款和售后
学生端不直接发起后台退款命令。普通用户订单页只展示 `GET /api/commerce/orders/status``GET /api/commerce/orders/detail` 返回的订单状态、支付状态、`refundedAmountCents`,并提供客服/工单入口。租户后台或运营后台才接退款接口。
租户后台退款列表:
```text
GET /api/commerce/refunds?status=requested&orderNo=<orderNo>
权限tenant:refund:read
```
创建退款申请:
```text
POST /api/commerce/refunds
权限tenant:refund:write
body: {
"orderNo": "<orderNo>",
"refundNo": "<可选,前端幂等键>",
"amountCents": 500,
"reason": "用户协商退款",
"entitlementAction": "revoke_on_success | none"
}
```
退款状态流转:
```text
POST /api/commerce/refunds/status
body: {
"refundId": "<refundId>",
"action": "approve | reject | submit_provider_refund | mark_processing | mark_succeeded | mark_failed | cancel",
"providerRefundNo": "<支付平台退款单号,可选>",
"providerNotifyUrl": "<微信退款通知地址,可选>",
"note": "<处理备注>"
}
```
状态说明:
```text
requested -> approved -> processing -> succeeded
requested -> approved -> submit_provider_refund -> processing/succeeded
requested/approved -> rejected
requested/approved -> cancelled
approved/processing -> failed
```
注意:
- 金额单位一律是分,前端不要传元。
- `refundNo` 是幂等键;同一订单同一金额重复提交会返回原退款申请。
- 后端会限制累计退款金额不能超过实付金额。
- 全额退款成功后订单和支付会进入 `refunded`,相关订单权益会被置为 `revoked`;部分退款进入 `partially_refunded`,默认不撤销权益。
- `submit_provider_refund` 会由后端使用租户支付账户密钥调用微信/支付宝前端不要保存商户私钥、API v3 key 或支付宝应用私钥。
- 微信退款通常先进入 `processing`,需要后续退款通知/查询确认;支付宝普通退款成功会同步进入 `succeeded`。前端应按接口返回状态展示,不要假设点击后立即到账。
- 退款通知/查询确认、自动对账和补偿 worker 后续接入;当前生产联调时仍需运营后台保留人工确认/失败登记入口。
### 激活码预检查与兑换
兑换前建议先调用:
```text
POST /api/commerce/activation-codes/check
body: {
"code": "<activationCode>",
"regionId": "<regionId>"
}
```
可根据返回的 `valid/reasonCode/days/regionName/saleType` 展示确认弹窗。常见 `reasonCode`
```text
ACTIVATION_CODE_NOT_FOUND
ACTIVATION_CODE_USED
ACTIVATION_CODE_SELF_REDEEM_FORBIDDEN
ACTIVATION_CODE_REGION_MISMATCH
```
用户确认后再调用 `POST /api/commerce/activation-codes/redeem`。兑换成功后重新请求 `/api/commerce/entitlements` 和个人中心,不要在前端本地伪造会员状态。
后端支付回调地址由租户支付账户配置:
```text
/api/commerce/payments/notify/wechat_pay?tenantId=<tenantId>
/api/commerce/payments/notify/alipay?tenantId=<tenantId>
```
前端禁止:
- 传入自定义金额。
- 伪造支付成功状态。
- 调用 `/api/commerce/payments/manual-confirm`;这个接口只给租户后台线下收款/迁移期使用,后端要求 `tenant:payment:write`
- 保存商户号私钥、API v3 key、支付宝应用私钥。
- 在页面里实现 webhook 验签或权益开通。
## 第一阶段页面建议
1. `pages/bootstrap/index`
- 租户解析、主题初始化、登录态恢复。
2. `pages/login/index`
- 先接短信登录;后续接微信小程序登录。
3. `pages/home/index`
- Banner、公告、题库入口、会员入口、资料入口。
4. `pages/region/index`
- 地区选择和权益提示。
5. `pages/catalog/index`
- entry/node/collection/blueprint 通用导航。
6. `pages/practice/index`
- 刷题、答题、解析、错题、收藏。
7. `pages/vocabulary/index`
- 单词单元、学习、收藏。
8. `pages/handbook/index`
- 手册目录和阅读。
9. `pages/scoreline/index`
- 动态字段筛选和趋势。
10. `pages/profile/index`
- 会员、订单、激活码、学习数据。
## 租户后台前端建议
租户后台可以先做 H5 管理台,也可以后续使用 Taro H5 复用部分组件。优先页面:
- 概览:`/api/tenant-admin/overview`
- 数据看板:`/api/tenant-admin/dashboard`展示收益、注册、学习、内容、激活码、反馈、趋势、24h 活跃、套餐销量和运营动态
- 品牌/主题/域名/公开设置
- 支付账户/登录 provider/密钥引用
- 用户与成员权限
- 班级/教师/学生:`/api/tenant-admin/classes``classes/members``students``teachers``students/notes``students/followups`
- 角色模板:`GET/PUT /api/tenant-admin/role-templates``POST /api/tenant-admin/role-templates/disable`
- 内容入口/分类树/题目集合/练习蓝图
- 题目/单词/知识手册/分数线/视频维护
- JSON 导入 preview/import/issues
- Banner/FAQ/公告/激活码/优惠券
- 考试日期:`GET/PUT /api/tenant-admin/exam-dates`
- 题目反馈:`GET /api/tenant-admin/feedbacks``POST /api/tenant-admin/feedbacks/status``GET /api/tenant-admin/feedbacks/events`
- 销售/代理/CRM 队列
租户后台不应在前端自行决定权限;隐藏菜单只是体验优化,接口仍会校验权限。角色模板用于让租户配置“运营、教师、销售、代理”等自定义后台体验,成员绑定模板后,前端按模板的菜单/模块/字段权限渲染,后端按 permission keys 执行真正的访问控制。班级/学生范围权限由后端根据角色、模板 `dataScope.classIds``tenant_class_members` 计算,教师默认只能看到自己负责班级。
## 联调顺序
1. 启动页和租户解析。
2. 短信登录和 `auth/me`
3. 首页、地区、内容入口、题库树。
4. 练习 session、答题、错题、收藏。
5. 背单词、知识手册、分数线。
6. 会员套餐、订单、激活码。
7. 资料下载、视频解析。
8. 销售追踪和分享链路。
9. 租户后台内容维护和导入。
10. 正式鉴权、真实支付、对象存储生产联调。
## Supabase Client 验证任务
前端 scaffold 后先做一个最小兼容性验证:
- H5`@supabase/supabase-js` 初始化、session 持久化、token refresh、logout。
- 微信小程序:验证自定义 storage/fetch/URL polyfill 是否稳定。
- API用 Supabase access token 调 `apps/api`,后端解析出可信用户。
- 安全:确认前端 bundle 中不存在 secret/service role/database/payment/storage 私钥。
如果微信小程序端 `supabase-js` 兼容性不稳定,小程序端改走 `apps/api/auth/*` 登录适配层H5 继续使用 Supabase client 管理 Auth。