56 KiB
Taro 前端对接指南
更新时间:2026-06-29
目标:用一套 Taro 工程同时服务微信小程序和 H5 Web 题库,并采用“Supabase Auth/JWT + apps/api 业务 API 优先”的混合架构,支持多租户、品牌主题、地区题库、会员权益、销售追踪和对象存储资源。
建议新建:
F:\project\apps\taro
旧前端参考:
F:\project\参考\旧题库项目\src
启动流程
H5
- 从
window.location.host获取当前域名。 - 调用
GET /api/tenant/resolve?host=<host>。 - 保存
tenant.id、tenant.slug、branding、features、publicConfig。 - 初始化主题、Logo、页面标题、功能开关。
- 检查本地 session token,调用
GET /api/auth/me。 - 如果未登录,进入登录页;如果已登录,加载个人中心和首页数据。
微信小程序
- 从编译环境或小程序启动参数读取
tenantCode。 - 推广码、销售码、分享码从
options或scene中解析。 - 调用
GET /api/tenant/resolve?tenantCode=<tenantCode>。 - 如存在 referral 参数,先调用
/api/referral/resolve和/api/referral/track-event。 - 登录后再调用
/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 请求封装必须按下面目标实现:
Authorization: Bearer <tk_session>
x-tenant-id: <tenantId> # 仅作为登录前/公开目录租户上下文;登录后必须与 session 租户一致
生产目标:
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 时,推荐请求流程:
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 的租户。
生产或云端测试建议设置:
ALLOW_LEGACY_AUTH_HEADERS=false
ALLOW_PLATFORM_ADMIN_KEY=false
这样旧式 x-user-id 和平台管理 key 会被拒绝,前端可以提前发现未按 session 接入的页面。
前端环境变量只允许包含:
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 |
注意缓存必须带租户维度,例如:
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、POST /api/auth/oauth/wechat、POST /api/auth/oauth/qq |
| 首页 | /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、GET /api/profile/badges |
| 销售分享 | /api/referral/resolve、track-event、bind |
| 租户数据看板 | GET /api/tenant-admin/dashboard?timeRange=30d®ionId=... |
| 租户班级 | 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/PUT /api/tenant-admin/badges、GET/POST /api/tenant-admin/badge-grants |
| 公共题库采纳/同步 | GET /api/tenant-content/public-question-banks、POST /api/tenant-content/public-question-banks/adopt、POST /api/tenant-content/public-question-banks/sync、GET /api/tenant-content/public-question-banks/conflicts?adoptionId=... |
| 题库导出 | POST /api/tenant-content/exports/questions、GET /api/tenant-content/exports/jobs |
练习访问控制契约
前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 POST /api/learning/practice-sessions,后端会根据 content_entries.accessRules、content_nodes.accessRules、question_collections.accessRules、practice_blueprints.accessRules 和当前用户权益决定最终题目快照。
请求示例:
{
"mode": "sequential",
"collectionId": "00000000-0000-0000-0000-000000000615",
"questionLimit": 50
}
响应关键字段:
{
"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 的题目。
勋章
学生个人中心或学习成就页调用:
GET /api/profile/badges?includeLocked=true&category=practice
说明:
includeLocked=true时返回已解锁和未解锁勋章;不传时只返回已解锁。category可选:learning、practice、vocabulary、mock_exam、activity、feedback、sales、system、custom。- 前端只展示后端返回的
unlocked/grantId/grantedAt,不要在本地自行认定用户已经获得勋章。
租户后台勋章管理:
GET /api/tenant-admin/badges?category=practice&includeInactive=true
PUT /api/tenant-admin/badges
GET /api/tenant-admin/badge-grants?userId=...&badgeId=...
POST /api/tenant-admin/badge-grants
PUT /api/tenant-admin/badges 支持同租户内 legacyId 幂等更新;如果 id 与 legacyId 指向不同记录会返回 BADGE_ID_CONFLICT。POST /api/tenant-admin/badge-grants 对同一用户同一勋章幂等,不会重复生成多条发放记录。当前后端支持手动发放,自动发放规则后续由 worker/事件流补齐。
模考交卷与报告
全真模拟、试卷模式、顺序练习的最终报告都走后端交卷接口。前端不得传分数、正确数或题目范围;后端只信任 practice_sessions.question_ids 快照和 answer_records 最新答题记录。
交卷请求:
{
"practiceSessionId": "00000000-0000-0000-0000-000000000000"
}
响应关键字段:
{
"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®ionId=...&classId=... |
返回排名、用户展示信息、当前用户排名和范围信息 |
错题复习创建 session:
{
"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 作为“我的排名”。后台后续会补日/周榜预聚合和防刷策略,前端只消费接口返回口径。
租户数据看板
租户后台数据看板由后端统一聚合,前端不要直接读取订单、答题记录、学生列表后自行统计,避免权限越权、敏感信息外泄和各页面口径不一致。
请求:
GET /api/tenant-admin/dashboard?timeRange=30d®ionId=<可选地区ID>&limit=10
可选参数:
| 参数 | 可选值 | 说明 |
|---|---|---|
timeRange |
7d、30d、90d |
统计区间,默认 30d |
regionId |
UUID | 可选地区筛选,后端会校验地区属于当前租户 |
limit |
1-50 | 题型、科目、地区、套餐和运营动态的返回条数 |
响应主要结构:
{
"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 |
金额字段统一为分:
grossAmountCents
commissionAmountCents
minSettlementCents
比例字段统一为 0 到 1 的数字:
defaultRate = 0.2
commissionRate = 0.35
结算状态:
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、连续正确、掌握状态和每日复习计划。
取今日计划:
GET /api/learning/vocabulary/review-plan?unitId=<unitId>&reviewLimit=30&newLimit=20
响应关键字段:
{
"item": {
"dueCount": 3,
"newCount": 20,
"totalPlanned": 23,
"dueWords": [{ "wordId": "...", "status": "reviewing", "dueLevel": "soon" }],
"newWords": [{ "wordId": "...", "status": "new", "dueLevel": "new" }],
"words": []
}
}
上报单词复习结果:
{
"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。
播放步骤:
- 进入题目页后调用
GET /api/questions/{questionId}/videos或批量预加载POST /api/questions/videos/batch。 - 用户点击播放时调用
POST /api/videos/play。 - 后端校验当前 session 用户、租户、题目绑定关系、SVIP 权益或视频次数权益。
- 后端返回短期签名 URL、播放 token、权益来源和过期时间。
- 前端播放器只使用本次返回的
playback.url,不要缓存为长期资源地址。
请求示例:
{
"videoId": "00000000-0000-0000-0000-000000000821",
"questionId": "00000000-0000-0000-0000-000000000401"
}
响应关键字段:
{
"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 地址。
学生端展示资料列表:
GET /api/catalog/assets?assetType=pdf®ionId=<regionId>&includeLocked=true
学生端 PDF/图片预览:
GET /api/catalog/assets/preview?assetId=<assetId>
学生端下载:
GET /api/catalog/assets/download?assetId=<assetId>
前端处理规则:
preview.url是短期 inline URL,只给预览组件使用,不持久化。download.url是短期 attachment URL,只给下载动作使用。ASSET_SVIP_REQUIRED:提示开通对应地区/科目权益。ASSET_UPLOAD_NOT_VERIFIED:展示“资料正在处理中”,并上报前端日志。ASSET_NOT_FOUND或列表中资源从active消失:展示“资源异常已下架”或刷新列表,不要继续使用旧签名 URL。ASSET_PREVIEW_NOT_SUPPORTED:隐藏预览按钮,仅保留下载或提示不支持预览。previewUrl字段只作为公开/托管预览提示,不代表可以绕过接口直接访问。
租户后台上传资料必须走五步:
sign-upload -> 直传对象存储 -> PUT assets 登记草稿 -> confirm-upload -> sign-preview 验收
后台上传确认:
{
"assetId": "<assetId>",
"fileSizeBytes": 4096,
"mimeType": "application/pdf",
"checksumSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"publish": true
}
托管对象在确认前会保持 status=draft、uploadStatus=pending,学生端不会看到。确认失败时后端返回 UPLOAD_VERIFICATION_FAILED,后台必须展示失败原因并允许重新上传,不能前端强行改为已发布。
生产环境会定时运行 assets worker 复检对象存储元数据。复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致时,后端会把资源置为 uploadStatus=failed 并从 active 退回 draft,同时写入 securityFlags.assetRecheckFailed=true。租户后台资源列表应对 failed 资源展示异常原因和重新上传入口;学生端不要缓存资料列表和签名 URL 作为长期状态。
考试倒计时、签到积分和反馈
首页可用 GET /api/catalog/exam-dates?regionId=<regionId> 展示地区公开考试日期;个人中心优先用 GET /api/profile/exam-countdowns,后端会按学生当前 regionId/selectedSchoolId 返回匹配倒计时。
签到入口调用:
POST /api/profile/check-in
返回关键字段:
{
"item": {
"checkedIn": true,
"alreadyCheckedIn": false,
"pointsAdded": 10,
"streak": 1,
"score": 10,
"lastCheckInDate": "2026-06-29"
}
}
前端处理规则:
alreadyCheckedIn=true时展示今日已签到,不要本地再加分。- 积分明细调用
GET /api/profile/score-events。 - 积分最终余额以后端
score和流水为准,前端只做展示。
题目页、资料页或视频页可提交反馈:
{
"questionId": "...",
"type": "question_error",
"category": "answer",
"title": "题目解析有误",
"description": "请填写具体问题",
"attachments": []
}
前端处理规则:
questionId如存在,后端会校验题目必须属于当前租户。- 反馈状态由租户后台处理,学生可用
GET /api/profile/feedbacks查看自己的反馈历史。 - 租户后台处理反馈时,奖励积分由后端
idempotency_key保证不会重复发放,前端不要重复叠加。
题库新模型接入方式
旧项目常按“地区 -> 科目 -> 章节/试卷”固定层级处理。新项目不要写死层级,按下面模型渲染:
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 租户。
租户内容导入对接
租户后台导入统一使用 preview -> issues -> import 流程,前端不要直接写 Supabase 表或绕过 apps/api。当前 JSON、CSV 和 Excel 都进入同一套后端规范化、逐行 issue、幂等和审计管线。
当前可联调:
POST /api/tenant-content/imports/preview/questions
POST /api/tenant-content/imports/questions
POST /api/tenant-content/imports/preview/vocabulary
POST /api/tenant-content/imports/vocabulary
POST /api/tenant-content/imports/preview/handbook
POST /api/tenant-content/imports/handbook
POST /api/tenant-content/imports/preview/scoreline
POST /api/tenant-content/imports/scoreline
POST /api/tenant-content/imports/preview/videos
POST /api/tenant-content/imports/videos
GET /api/tenant-content/imports
GET /api/tenant-content/imports/issues
GET /api/tenant-content/imports/field-mapping
GET /api/tenant-content/imports/templates
POST /api/tenant-content/imports/post-check
GET /api/tenant-content/imports/post-check
前端流程:
- 页面初始化调用
field-mapping,渲染字段说明、别名、必填项和示例。 - 下载模板调用
templates?importType=...&format=csv|json,用contentBase64生成文件。 - 上传或粘贴 JSON/CSV/Excel,先调用对应 preview。
- 展示
job.totalCount/validCount/errorCount/warningCount。 - 展示
job.sourceFormat、job.parserMetadata、逐行issues,错误行必须让运营修正;如果后端允许allowPartial,也要二次确认。 - 小批量确认后直接调用 import;大批量确认时传
executionMode=async排队,前端轮询 job 状态。 - 导入进入
completed/completed_with_errors后调用POST /api/tenant-content/imports/post-check。 - 展示
summary.importPostCheck或GET /api/tenant-content/imports/post-check?jobId=...返回的复检结果,再刷新内容列表、分数线列表或题目视频列表。
CSV 请求示例:
{
"sourceFormat": "csv",
"sourceName": "questions.csv",
"csvText": "legacyId,题型,题干,选项A,选项B,答案\nq1,choice,题干,A,B,B",
"subjectId": "...",
"categoryId": "...",
"entryId": "...",
"contentNodeId": "...",
"collectionId": "..."
}
Excel 请求示例:
{
"sourceFormat": "excel",
"sourceName": "scoreline.xlsx",
"fileBase64": "<xlsx base64>",
"sheetName": "records",
"regionId": "..."
}
异步确认导入示例:
{
"previewJobId": "uuid",
"executionMode": "async",
"allowPartial": false
}
异步导入状态:
pending/importing:展示处理中,不允许重复同步执行同一 job。
completed:刷新目标内容列表。
completed_with_errors:刷新成功内容,并提示查看 issues。
failed/rejected:展示 errorMessage 和 issues,允许运营修正后重新 preview。
导入复检状态:
passed:可以展示为导入验收通过。
warning:导入已落库,但存在可运营确认的风险,例如 allowPartial 导入。
failed:导入结果和目标表不一致,必须提示管理员排查,不要静默刷新页面。
前端不要自行判断导入成功率,也不要只看 completed 就认为可上线;以复检结果和目标内容刷新结果共同作为运营提示。
前端文件限制应与后端一致:单文件最大 8MB,最多 5000 行、160 列。后端不会保存原始 fileBase64,但前端仍不要把含隐私的导入文件写入长期缓存。
分数线导入前端注意:
- 后端支持
fields/schools/majors/records分桶,也支持items列表;Excel 可用fields、schools、majors、records多 Sheet。 - 页面筛选字段仍以
/api/scoreline/fields为准,不要从导入 JSON 临时生成筛选 UI。 record至少需要schoolId、schoolLegacyId或schoolName,否则 preview 会返回 issue。
视频导入前端注意:
- 列表和搜索接口不会给付费视频可播放 URL;播放统一调
POST /api/videos/play。 - 绑定题目必须提供
questionId或legacyQuestionId。 - 生产建议把私有视频先入
content_assets,导入时传assetId,避免长期暴露源站 URL。
题库导出对接
租户后台题库导出统一走后端生成结构化 payload,前端不要直接查 Supabase 表拼导出文件。当前后端已支持 JSON、paper_json 和 print_payload 三类基础导出,适合先做后台“导出 JSON/试卷预览”功能;PDF/Word 二进制、水印和发布到资料下载后续由 worker 增强。
可用接口:
POST /api/tenant-content/exports/questions
GET /api/tenant-content/exports/jobs
导出范围:
| scopeType | scopeId | 用途 |
|---|---|---|
collection |
question_collections.id |
导出某个题目列表或试卷集合 |
entry |
content_entries.id |
导出某个题库入口下全部已发布题目 |
content_node |
content_nodes.id |
导出某个分类节点及其子节点下全部已发布题目 |
普通题库 JSON 导出:
{
"scopeType": "collection",
"scopeId": "<questionCollectionId>",
"format": "json",
"exportType": "questions",
"includeAnswers": false,
"includeExplanations": false,
"options": {
"title": "天津专升本题库导出"
}
}
试卷 payload 导出:
{
"scopeType": "content_node",
"scopeId": "<contentNodeId>",
"format": "paper_json",
"exportType": "paper",
"includeAnswers": true,
"includeExplanations": true,
"options": {
"title": "全真模拟试卷",
"durationMinutes": 120,
"watermarkText": "仅供内部使用"
}
}
响应关键结构:
{
"job": {
"id": "...",
"status": "completed",
"questionCount": 1,
"outputHash": "..."
},
"export": {
"_tikuExport": "3.0",
"jobId": "...",
"summary": {
"questionCount": 1,
"sectionCount": 1
},
"sections": [],
"questions": [],
"files": [
{
"filename": "天津专升本题库导出.json",
"mimeType": "application/json",
"encoding": "base64",
"contentBase64": "..."
}
],
"renderHints": {
"pdfLayout": "paper",
"pageSize": "A4",
"answerPlacement": "inline_or_appendix"
}
}
}
前端处理规则:
- 导出按钮只给具备租户内容编辑权限的后台成员展示;接口仍以后端
TENANT_CONTENT_EDITOR_REQUIRED为准。 - 下载 JSON 时使用
files[0].contentBase64生成 Blob,文件名使用后端返回的filename。 includeAnswers=false时,顶层答案字段和阅读理解/案例分析的子题答案都会被后端脱敏;前端不要在本地重新合并答案。includeExplanations=false时,不展示解析,也不要从题目详情接口额外补解析。paper_json可先用于后台试卷预览和打印;正式 PDF/Word 导出等后端 worker 完成后再接二进制文件下载。GET /api/tenant-content/exports/jobs?scopeType=collection&scopeId=...用于后台导出历史;当前记录 metadata 和输出 hash,不长期保存完整导出内容。- 跨租户导出会返回 404 或 403,前端不要重试其它租户 ID。
公共题库采纳对接
平台超级管理员后台使用:
GET /api/platform-admin/question-banks
GET /api/platform-admin/question-bank-grants
PUT /api/platform-admin/question-bank-grants
授权参数建议:
{
"sourceQuestionBankId": "<平台公共题库ID>",
"grantScope": "plans",
"allowedPlanCodes": ["starter_yearly", "pro_yearly"],
"status": "active"
}
grantScope 可选:
plans:按 SaaS 套餐授权。tenants:指定租户授权。mixed:套餐和指定租户同时生效。all_active_tenants:所有有效订阅租户可见。
租户内容后台使用:
GET /api/tenant-content/public-question-banks
POST /api/tenant-content/public-question-banks/adopt
POST /api/tenant-content/public-question-banks/sync
GET /api/tenant-content/public-question-banks/conflicts?adoptionId=...
采纳请求:
{
"grantId": "<授权ID>",
"entryName": "天津专升本公共题库",
"collectionName": "天津专升本公共题目",
"copyLimit": 500
}
前端处理规则:
- 租户只能看到后端判定为已授权的公共题库,不要在前端用套餐码自行过滤。
- 采纳成功后后端会生成本租户自己的
questionBankId、entryId、collectionId和题目快照,学生端直接按普通/api/catalog/content-entries、question-collections、practice-sessions接入。 - 重复采纳返回
QUESTION_BANK_ALREADY_ADOPTED,前端展示“已采纳”即可。 - 已采纳公共题库可以手动同步平台后续新增/更新题目;同步会重新校验当前租户仍有授权,且只写入租户自己的题目副本。
- 后端也可以由
apps/worker --job public-banks自动同步待更新的采纳题库;前端不需要轮询平台源库,只需要在租户后台展示同步状态、最近同步时间和冲突数量。
同步请求:
{
"adoptionId": "<tenant_question_bank_adoptions.id>",
"copyLimit": 1000
}
同步响应关键字段:
{
"item": {
"id": "...",
"syncStatus": "synced | failed",
"copiedQuestionCount": 120,
"targetQuestionBankId": "...",
"targetEntryId": "...",
"targetCollectionId": "..."
},
"sync": {
"status": "synced | conflict",
"counts": {
"inserted": 1,
"updated": 3,
"skipped": 116,
"conflicts": 0
},
"results": [
{
"sourceQuestionId": "...",
"targetQuestionId": "...",
"action": "inserted | updated | skipped | conflict",
"sourceHash": "...",
"previousSourceHash": "...",
"targetHash": "..."
}
]
}
}
前端处理规则:
sync.status=synced:刷新公共题库列表、题目集合和题目列表。sync.status=conflict或item.syncStatus=failed:展示冲突数量和冲突题目,不要把它当系统异常。冲突表示租户已经改过这道采纳题,后端已跳过并保留租户内容。action=conflict的记录可以进入后续“冲突处理”页面:展示平台源题 ID、租户目标题 ID、上次平台 hash、当前平台 hash、租户当前 hash。当前后端只负责保护不覆盖,批量接受平台版本/保留租户版本的操作台后续补。- 页面初始化或 worker 后台同步完成后,可以调用
GET /api/tenant-content/public-question-banks/conflicts?adoptionId=...查询最近一次同步状态、counts和conflicts。这个接口只返回当前租户自己的采纳记录,跨租户会返回QUESTION_BANK_ADOPTION_NOT_FOUND。 QUESTION_BANK_GRANT_NOT_AVAILABLE:说明 SaaS 套餐/授权已失效,提示联系平台或升级套餐。QUESTION_BANK_ADOPTION_NOT_FOUND:说明不是当前租户的采纳记录或记录已归档,前端不要跨租户重试。- 后续会补版本通知、冲突操作台和批量确认策略;当前租户后台可以先提供手动“同步平台更新”按钮,并展示 worker 自动同步后的冲突查询结果。
登录对接
短信登录
开发环境可以先使用 mock 短信,接口会返回 debugCode。生产环境禁止依赖 debugCode。
POST /api/auth/sms/send
body: { "phone": "13800000000", "purpose": "login" }
POST /api/auth/sms/verify
body: { "phone": "13800000000", "code": "123456", "purpose": "login" }
成功后保存:
session.token
session.expiresAt
user
后续请求统一带:
Authorization: Bearer <session.token>
x-tenant-id: <tenantId>
绑定或更换手机号
微信/QQ 登录后强制绑定手机号、个人中心更换手机号,都走同一个后端命令。前端先发送 bind_phone 用途验证码,再提交绑定:
POST /api/auth/sms/send
body: { "phone": "13800000000", "purpose": "bind_phone" }
POST /api/auth/phone/bind
Authorization: Bearer <session.token>
body: { "phone": "13800000000", "code": "123456" }
前端规则:
- 绑定接口必须带当前登录态,不能用
x-user-id伪造用户。 - 绑定接口只接受
bind_phone验证码,不接受login验证码。 - 新手机号如果已属于其它账号,后端返回
PHONE_ALREADY_BOUND。 - 换绑成功后旧手机号登录身份会被移除;迁移期
tk_其它设备 session 会被撤销,当前 session 继续可用。 - 微信手机号授权后也应由后端 adapter 换取手机号,再复用同一类绑定命令;不要在页面里持久化明文手机号授权中间数据。
微信小程序登录
微信小程序端调用 Taro.login() 获取 code,然后交给后端:
POST /api/auth/oauth/wechat-miniapp
body: {
"code": "<wx.login code>",
"profile": {
"nickName": "...",
"avatarUrl": "..."
}
}
成功响应包含:
provider
user
isNewUser
session.token
session.expiresAt
identity.openId
identity.unionId
注意:
- 前端不接触
appSecret。 - 前端不会拿到微信
session_key。 - 如果登录前已经解析到推广码,登录成功后再调用
/api/referral/bind完成首绑保护。 - 如果用户没有手机号,跳转到上面的“绑定或更换手机号”流程。
微信网页登录
H5 端在微信开放平台授权回调页拿到 code 后,交给后端:
POST /api/auth/oauth/wechat
body: {
"code": "<wechat oauth code>",
"lang": "zh_CN"
}
成功响应与小程序登录一致,包含 provider=user/identity/session。后端会使用租户 wechat-web/wechat_web/wechat provider 配置换取 access_token 和 openid,再拉取用户资料;如果返回 unionid,会和同一开放平台下的小程序身份合并。
前端注意:
- H5 回调页只短暂读取
code/state,不要持久化微信access_token。 state应在前端本地或服务端中转页校验,避免跨站授权回调混淆。- 多租户自定义域名下,授权回调域名必须与租户微信开放平台配置一致;如果未来使用统一授权中转域名,需要在回调后再解析目标租户。
QQ 登录
H5 端在 QQ 互联授权回调页拿到 code 后,交给后端:
POST /api/auth/oauth/qq
body: {
"code": "<qq oauth code>",
"redirectUri": "https://h5.example.com/auth/qq/callback"
}
后端会完成 code -> access_token -> openid -> userinfo,并签发本项目 session。
前端注意:
redirectUri必须与 QQ 互联后台登记地址一致;也可以由租户后台 provider 配置固定,前端不传。- 前端不要接触 QQ
clientSecret/AppKey或access_token。 - 登录后如果没有手机号,同样进入“绑定或更换手机号”流程。
支付对接
支付流程必须以后端订单金额和后端回调为准,前端只负责拉起支付。
创建订单
POST /api/commerce/orders
body: {
"planId": "<svipPlanId>",
"quantity": 1,
"payProvider": "wechat_pay | alipay",
"payMethod": "jsapi | wap",
"regionId": "<regionId>",
"couponCode": "<可选,优惠券码>",
"couponRedemptionId": "<可选,已领取优惠券 redemptionId>"
}
前端可以先领取优惠券,再下单:
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 后,再创建支付参数:
POST /api/commerce/payments/create
body: {
"orderNo": "<orderNo>",
"provider": "wechat_pay",
"openId": "<微信小程序登录后的 openId>"
}
微信小程序返回的 paymentParams 可直接映射到 Taro.requestPayment:
appId
timeStamp
nonceStr
package
signType
paySign
支付宝 H5/WAP 返回:
paymentParams.url
H5 可以跳转到该 URL。小程序端如果后续要接支付宝小程序,需要新增独立 provider/method,不要复用 H5 WAP URL。
支付完成后前端不要自行开通会员。前端应轮询或重新请求:
GET /api/commerce/orders/status?orderNo=<orderNo>
GET /api/commerce/orders/detail?orderNo=<orderNo>
GET /api/commerce/entitlements
订单详情会返回 pricing、payments、items、couponRedemptions,可用于收银台、订单详情页和售后排查。订单状态轮询页只需消费 status/payment,避免频繁拉取全量明细。
后端已提供 commerce worker 作为兜底补偿:如果微信/支付宝支付成功但 webhook 漏通知,worker 会按租户商户配置查询供应商订单并幂等更新订单、支付和权益。前端仍然只轮询 orders/status 或 orders/detail,不要直接调用供应商查询接口,也不要在页面里自行开通会员。
退款和售后
学生端不直接发起后台退款命令。普通用户订单页只展示 GET /api/commerce/orders/status 和 GET /api/commerce/orders/detail 返回的订单状态、支付状态、refundedAmountCents,并提供客服/工单入口。租户后台或运营后台才接退款接口。
租户后台退款列表:
GET /api/commerce/refunds?status=requested&orderNo=<orderNo>
权限:tenant:refund:read
创建退款申请:
POST /api/commerce/refunds
权限:tenant:refund:write
body: {
"orderNo": "<orderNo>",
"refundNo": "<可选,前端幂等键>",
"amountCents": 500,
"reason": "用户协商退款",
"entitlementAction": "revoke_on_success | none"
}
退款状态流转:
POST /api/commerce/refunds/status
body: {
"refundId": "<refundId>",
"action": "approve | reject | submit_provider_refund | query_provider_refund | mark_processing | mark_succeeded | mark_failed | cancel",
"providerRefundNo": "<支付平台退款单号,可选>",
"providerNotifyUrl": "<微信退款通知地址,可选>",
"note": "<处理备注>"
}
状态说明:
requested -> approved -> processing -> succeeded
requested -> approved -> submit_provider_refund -> processing/succeeded
processing -> query_provider_refund -> processing/succeeded/failed
provider refund notify -> processing/succeeded/failed
requested/approved -> rejected
requested/approved -> cancelled
approved/processing -> failed
注意:
- 金额单位一律是分,前端不要传元。
refundNo是幂等键;同一订单同一金额重复提交会返回原退款申请。- 后端会限制累计退款金额不能超过实付金额。
- 全额退款成功后订单和支付会进入
refunded,相关订单权益会被置为revoked;部分退款进入partially_refunded,默认不撤销权益。 submit_provider_refund会由后端使用租户支付账户密钥调用微信/支付宝;前端不要保存商户私钥、API v3 key 或支付宝应用私钥。- 微信退款通常先进入
processing,租户后台可以调用query_provider_refund主动向微信查询,确认成功后后端才会更新订单退款金额和权益。 - 支付宝普通退款如果响应
fund_change=Y会同步进入succeeded;处于processing的退款也可以用query_provider_refund调用alipay.trade.fastpay.refund.query确认。 - 退款通知地址由支付账户或
submit_provider_refund.providerNotifyUrl配置,后端公开接收路径为POST /api/commerce/refunds/notify/wechat_pay?tenantId=<tenantId>、POST /api/commerce/refunds/notify/alipay?tenantId=<tenantId>。这是支付平台回调地址,Taro 前端不要主动调用。 - 退款通知只会推进已经审核/处理中的退款申请;未审核的
requested退款不能被外部通知直接落账。 - 已经
succeeded的退款不能再次查询或再次标记成功,避免订单退款金额重复累加。前端应按接口返回状态展示,不要假设点击后立即到账。 - 自动补偿 worker 已接入:支付漏通知和处理中退款会由后端定时查询供应商并幂等落账。完整资金流水对账、账单下载比对和异常订单运营台后续继续补;生产联调时仍需保留人工确认/失败登记入口。
激活码预检查与兑换
兑换前建议先调用:
POST /api/commerce/activation-codes/check
body: {
"code": "<activationCode>",
"regionId": "<regionId>"
}
可根据返回的 valid/reasonCode/days/regionName/saleType 展示确认弹窗。常见 reasonCode:
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 和个人中心,不要在前端本地伪造会员状态。
后端支付回调地址由租户支付账户配置:
/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 验签或权益开通。
第一阶段页面建议
pages/bootstrap/index- 租户解析、主题初始化、登录态恢复。
pages/login/index- 先接短信登录;后续接微信小程序登录。
pages/home/index- Banner、公告、题库入口、会员入口、资料入口。
pages/region/index- 地区选择和权益提示。
pages/catalog/index- entry/node/collection/blueprint 通用导航。
pages/practice/index- 刷题、答题、解析、错题、收藏。
pages/vocabulary/index- 单词单元、学习、收藏。
pages/handbook/index- 手册目录和阅读。
pages/scoreline/index- 动态字段筛选和趋势。
pages/profile/index
- 会员、订单、激活码、学习数据、勋章。
当前 Taro 实现进度
截至 2026-06-29,apps/taro 已完成学生端第一阶段页面:
pages/student/login/index
pages/student/home/index
pages/student/catalog/index
pages/student/practice/index
pages/student/vocabulary/index
pages/student/handbook/index
pages/student/scoreline/index
pages/student/assets/index
pages/student/profile/index
已新增服务层:
src/services/catalog.ts 目录、题库、单词、手册、分数线、资料
src/services/learning.ts 练习 session、答题、收藏、单词复习
src/services/commerce.ts 套餐、订单、权益、激活码
src/services/profile.ts 个人中心、签到、勋章、倒计时
src/services/tenantAdmin.ts 租户后台看板、学生、内容、营销、设置
验证命令:
npm run check:taro
npm run build:taro:h5:student
npm run build:taro:h5:tenant
已通过。构建仍有 Taro H5 入口体积 warning,属于当前 Taro 工程既有警告,不阻断联调。
下一批前端开发重点:
- 学生端:选地区、题目视频播放、题目反馈、错题/收藏专题页、模考交卷报告、订单收银台和订单详情。
- 租户后台:写入表单、字段映射 UI、导入 preview/import/issues 操作台、公共题库采纳/同步、学生批量导入、角色模板配置 UI、CRM 分配和分佣结算操作。
- 平台后台:租户、SaaS 套餐、订阅账单、公共题库授权、平台审计。
- 小程序:验证
Taro.login、微信支付、分享 scene/referral、Supabase client 兼容性;如不稳定,保留apps/api/auth/*作为小程序登录适配层。
租户后台前端建议
租户后台可以先做 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/CSV/Excel 导入 preview/import/issues
- Banner/FAQ/公告/激活码/优惠券
- 勋章:
GET/PUT /api/tenant-admin/badges、GET/POST /api/tenant-admin/badge-grants - 考试日期:
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 计算,教师默认只能看到自己负责班级。
联调顺序
- 启动页和租户解析。
- 短信登录和
auth/me。 - 首页、地区、内容入口、题库树。
- 练习 session、答题、错题、收藏。
- 背单词、知识手册、分数线。
- 会员套餐、订单、激活码。
- 资料下载、视频解析。
- 销售追踪和分享链路。
- 租户后台内容维护和导入。
- 正式鉴权、真实支付、对象存储生产联调。
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。