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

21 KiB
Raw Blame History

Taro 前端对接指南

更新时间2026-06-28

目标:用一套 Taro 工程同时服务微信小程序和 H5 Web 题库并采用“Supabase Auth/JWT + apps/api 业务 API 优先”的混合架构,支持多租户、品牌主题、地区题库、会员权益、销售追踪和对象存储资源。

建议新建:

F:\project\apps\taro

旧前端参考:

F:\project\参考\旧题库项目\src

启动流程

H5

  1. window.location.host 获取当前域名。
  2. 调用 GET /api/tenant/resolve?host=<host>
  3. 保存 tenant.idtenant.slugbrandingfeaturespublicConfig
  4. 初始化主题、Logo、页面标题、功能开关。
  5. 检查本地 session token调用 GET /api/auth/me
  6. 如果未登录,进入登录页;如果已登录,加载个人中心和首页数据。

微信小程序

  1. 从编译环境或小程序启动参数读取 tenantCode
  2. 推广码、销售码、分享码从 optionsscene 中解析。
  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 请求封装必须按下面目标实现:

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_MISMATCHAUTH_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/sendPOST /api/auth/sms/verifyPOST /api/auth/oauth/wechat-miniapp、后续微信网页/QQ provider
首页 /api/catalog/content-entries/api/catalog/banners/api/catalog/announcements/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/submitGET /api/learning/practice-sessions/reportGET /api/learning/practice-reports
错题本 GET /api/learning/wrong-questionsPOST /api/learning/wrong-questions/resolve
错题复习 GET /api/learning/wrong-questions/review-planPOST /api/learning/practice-sessions with mode=wrong_review
收藏夹 GET/POST /api/learning/favorites/questions
练习历史/统计 GET /api/learning/practice-sessions/historyGET /api/learning/statsGET /api/learning/trend
题目视频 GET /api/questions/{questionId}/videosPOST /api/questions/videos/batchPOST /api/videos/play
背单词 /api/catalog/vocabulary-units/api/catalog/vocabulary-words
单词进度/计划 /api/learning/vocabulary/progress/api/learning/vocabulary/stats/api/learning/vocabulary/review-planPOST /api/learning/vocabulary/review
单词收藏 /api/learning/vocabulary/favorites
知识手册 /api/catalog/handbook-subjectshandbook-chaptershandbook-entries
分数线 /api/scoreline/fieldsschoolsmajorsrecordstrendyears
资料下载 /api/catalog/assets/api/catalog/assets/download
商城 /api/catalog/svip-plansPOST /api/commerce/ordersPOST /api/commerce/payments/create
订单/权益 /api/commerce/orders/api/commerce/entitlements
激活码兑换 POST /api/commerce/activation-codes/redeem
个人中心 GET/PATCH /api/profile/me
销售分享 /api/referral/resolvetrack-eventbind

练习访问控制契约

前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 POST /api/learning/practice-sessions,后端会根据 content_entries.accessRulescontent_nodes.accessRulesquestion_collections.accessRulespractice_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 的题目。

模考交卷与报告

全真模拟、试卷模式、顺序练习的最终报告都走后端交卷接口。前端不得传分数、正确数或题目范围;后端只信任 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 保留后台配置的卷面总分。测试或预发数据题量不足时,两者不一定按百分制等比换算,前端展示时不要自行重算。
  • 错题复盘优先使用 wrongQuestionIdsquestionResults,题目详情仍可按现有题目接口或 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

错题复习创建 session

{
  "mode": "wrong_review",
  "questionLimit": 20
}

前端处理规则:

  • 不要把错题 ID 列表从前端传回后端组卷;wrong_review 会由后端按当前用户错题本安全组卷。
  • review-plan.nextAction 可直接用于按钮配置,但仍需使用当前登录 session 调用。
  • 收藏夹复习同理可调用 POST /api/learning/practice-sessionsbody 为 { "mode": "favorite_review", "questionLimit": 20 }
  • 趋势图以接口返回日期桶为准,缺失日期后端会补 0不需要前端补点。

背单词计划与复习上报

背单词页面分三类数据:单元列表、每日计划、单词进度。前端不需要计算下次复习日期,只提交“认识/不认识”,由后端统一更新 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
  • 返回的 statusdueLevelnextReviewDate 作为后续展示依据,不在前端重算间隔。
  • 旧的 POST /api/learning/vocabulary/progress 保留给兼容和后台手工修正;普通学习流优先用 vocabulary/review

视频播放契约

题目视频分为 freesvipvideo_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,不要缓存为长期资源地址。

请求示例:

{
  "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 写入本地持久缓存。

题库新模型接入方式

旧项目常按“地区 -> 科目 -> 章节/试卷”固定层级处理。新项目不要写死层级,按下面模型渲染:

content_entries
  -> content_nodes 任意深度分类树
    -> question_collections 题目列表/试卷/章节/题型集合
      -> practice_blueprints 顺序/随机/全真模拟规则

前端建议:

  • entryType=question_practice 渲染为刷题入口。
  • entryType=vocabulary 渲染为背单词入口。
  • entryType=handbook 渲染为知识手册入口。
  • markerType=schoolmarkerType=exam_track 可作为学生目标院校/专业意向采集。
  • 不同地区节点层级可以不同,页面组件必须支持递归树和面包屑。

多租户前端优化

  • Logo、标题、主题色、客服信息全部来自 tenant/resolve
  • 功能开关控制菜单显示,但接口权限仍以后端为准。
  • 私有图片、PDF、视频不要直接拼 URL一律通过后端签名。
  • 支付渠道从后端返回或租户配置读取,不在页面硬编码。
  • 小程序分享路径必须带 tenantCode 和 referral code。
  • 用户首绑归属由后端保护,前端不要提供“换绑销售”入口。
  • 管理后台菜单按 GET /api/tenant-admin/permissions 和用户权限渲染。
  • H5 自定义域名下要注意缓存隔离,不能把 A 租户主题缓存用到 B 租户。

登录对接

短信登录

开发环境可以先使用 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>

微信小程序登录

微信小程序端调用 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 完成首绑保护。
  • 手机号授权后续应走独立的“绑定手机号”接口,不要把微信手机号解密逻辑写在页面里。

支付对接

支付流程必须以后端订单金额和后端回调为准,前端只负责拉起支付。

创建订单

POST /api/commerce/orders
body: {
  "planId": "<svipPlanId>",
  "quantity": 1,
  "payProvider": "wechat_pay | alipay",
  "payMethod": "jsapi | wap",
  "regionId": "<regionId>"
}

返回 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
GET /api/commerce/entitlements

后端支付回调地址由租户支付账户配置:

/api/commerce/payments/notify/wechat_pay?tenantId=<tenantId>
/api/commerce/payments/notify/alipay?tenantId=<tenantId>

前端禁止:

  • 传入自定义金额。
  • 伪造支付成功状态。
  • 保存商户号私钥、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
  • 品牌/主题/域名/公开设置
  • 支付账户/登录 provider/密钥引用
  • 用户与成员权限
  • 内容入口/分类树/题目集合/练习蓝图
  • 题目/单词/知识手册/分数线/视频维护
  • JSON 导入 preview/import/issues
  • Banner/FAQ/公告/激活码/优惠券
  • 销售/代理/CRM 队列

租户后台不应在前端自行决定权限;隐藏菜单只是体验优化,接口仍会校验权限。

联调顺序

  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。