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

51 KiB
Raw Blame History

Taro 前端对接指南

更新时间2026-06-29

目标:用一套 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/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/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/learning/leaderboard?metric=questions&period=all
题目视频 GET /api/questions/{questionId}/videosPOST /api/questions/videos/batchPOST /api/videos/play
题目反馈 POST /api/profile/feedbacksGET /api/profile/feedbacks
分佣结算 GET /api/commission/settingsPUT /api/commission/settingsPUT /api/commission/member-rateGET /api/commission/summaryGET /api/commission/ordersGET /api/commission/settlementsPOST /api/commission/settlements/generatePOST /api/commission/settlements/status
背单词 /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/preview/api/catalog/assets/download
商城 /api/catalog/svip-plansPOST /api/commerce/coupons/claimPOST /api/commerce/ordersPOST /api/commerce/payments/create
订单/权益 /api/commerce/orders/api/commerce/orders/detail/api/commerce/orders/status/api/commerce/entitlements
激活码 POST /api/commerce/activation-codes/checkPOST /api/commerce/activation-codes/redeem
个人中心 GET/PATCH /api/profile/mePOST /api/profile/check-inGET /api/profile/score-eventsGET /api/profile/exam-countdownsGET /api/profile/badges
销售分享 /api/referral/resolvetrack-eventbind
租户数据看板 GET /api/tenant-admin/dashboard?timeRange=30d&regionId=...
租户班级 GET/PUT /api/tenant-admin/classesPOST /api/tenant-admin/classes/disable
班级成员 GET/PUT /api/tenant-admin/classes/membersPOST /api/tenant-admin/classes/members/removePOST /api/tenant-admin/classes/members/bulk-assign
租户学生 GET/PUT /api/tenant-admin/studentsPOST /api/tenant-admin/students/bulk-upsertPOST /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/feedbacksPOST /api/tenant-admin/feedbacks/statusGET /api/tenant-admin/feedbacks/events
租户勋章 GET/PUT /api/tenant-admin/badgesGET/POST /api/tenant-admin/badge-grants
公共题库采纳/同步 GET /api/tenant-content/public-question-banksPOST /api/tenant-content/public-question-banks/adoptPOST /api/tenant-content/public-question-banks/sync
题库导出 POST /api/tenant-content/exports/questionsGET /api/tenant-content/exports/jobs

练习访问控制契约

前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 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 的题目。

勋章

学生个人中心或学习成就页调用:

GET /api/profile/badges?includeLocked=true&category=practice

说明:

  • includeLocked=true 时返回已解锁和未解锁勋章;不传时只返回已解锁。
  • category 可选:learningpracticevocabularymock_examactivityfeedbacksalessystemcustom
  • 前端只展示后端返回的 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 幂等更新;如果 idlegacyId 指向不同记录会返回 BADGE_ID_CONFLICTPOST /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 保留后台配置的卷面总分。测试或预发数据题量不足时,两者不一定按百分制等比换算,前端展示时不要自行重算。
  • 错题复盘优先使用 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
排行榜 GET /api/learning/leaderboard?metric=questions&period=7d&regionId=...&classId=... 返回排名、用户展示信息、当前用户排名和范围信息

错题复习创建 session

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

前端处理规则:

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

排行榜

排行榜由后端统一聚合,前端不要读取答题记录、单词进度或模考报告后自行排名,避免越权、口径漂移和跨租户数据泄露。

可选参数:

参数 可选值 说明
metric questionsscorevocabularymock_exam 分别表示累计答题、积分、掌握单词、模考最高分
period all7d30d 统计周期
regionId UUID 地区范围,可选
classId UUID 班级范围,可选,后端按当前租户校验
limit / page 正整数 分页

响应会包含 itemscurrentUser。即使当前用户未进入前 N 名,也应优先展示 currentUser 作为“我的排名”。后台后续会补日/周榜预聚合和防刷策略,前端只消费接口返回口径。

租户数据看板

租户后台数据看板由后端统一聚合,前端不要直接读取订单、答题记录、学生列表后自行统计,避免权限越权、敏感信息外泄和各页面口径不一致。

请求:

GET /api/tenant-admin/dashboard?timeRange=30d&regionId=<可选地区ID>&limit=10

可选参数:

参数 可选值 说明
timeRange 7d30d90d 统计区间,默认 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/permissionsdashboard:read 判断,但真正权限以后端返回为准。
  • trends 已补齐自然日桶,activeHours 固定 24 项,前端不需要补点。
  • revenueCentsamountCents 都是分,前端统一格式化成人民币展示,不要自行重算订单金额。
  • 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:readcommission:self
分佣来源明细 GET /api/commission/orders?... commission:readcommission:self
结算单列表 GET /api/commission/settlements?... commission:readcommission: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
  • 返回的 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 写入本地持久缓存。

资料上传、预览和下载契约

学生端资料只读取目录、预览和下载签名,不接触对象存储真实密钥,也不自行拼接私有 bucket 地址。

学生端展示资料列表:

GET /api/catalog/assets?assetType=pdf&regionId=<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=draftuploadStatus=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=schoolmarkerType=exam_track 可作为学生目标院校/专业意向采集。
  • 不同地区节点层级可以不同,页面组件必须支持递归树和面包屑。

多租户前端优化

  • Logo、标题、主题色、客服信息全部来自 tenant/resolve
  • 功能开关控制菜单显示,但接口权限仍以后端为准。
  • 私有图片、PDF、视频不要直接拼 URL一律通过后端签名。
  • 支付渠道从后端返回或租户配置读取,不在页面硬编码。
  • 小程序分享路径必须带 tenantCode 和 referral code。
  • 用户首绑归属由后端保护,前端不要提供“换绑销售”入口。
  • 管理后台菜单按 GET /api/tenant-admin/permissions 返回的 current.permissionscurrent.templatePermissionscurrent.menuPermissionscurrent.modulePermissions 渲染;接口权限仍以后端校验为准。
  • 教师、班主任、助教类账号进入租户后台时,学生列表以 GET /api/tenant-admin/students 返回的 scopeditems 为准;前端不要自行用本地班级 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

前端流程:

  1. 页面初始化调用 field-mapping,渲染字段说明、别名、必填项和示例。
  2. 下载模板调用 templates?importType=...&format=csv|json,用 contentBase64 生成文件。
  3. 上传或粘贴 JSON/CSV/Excel先调用对应 preview。
  4. 展示 job.totalCount/validCount/errorCount/warningCount
  5. 展示 job.sourceFormatjob.parserMetadata、逐行 issues,错误行必须让运营修正;如果后端允许 allowPartial,也要二次确认。
  6. 小批量确认后直接调用 import大批量确认时传 executionMode=async 排队,前端轮询 job 状态。
  7. 导入进入 completed/completed_with_errors 后调用 POST /api/tenant-content/imports/post-check
  8. 展示 summary.importPostCheckGET /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 可用 fieldsschoolsmajorsrecords 多 Sheet。
  • 页面筛选字段仍以 /api/scoreline/fields 为准,不要从导入 JSON 临时生成筛选 UI。
  • record 至少需要 schoolIdschoolLegacyIdschoolName,否则 preview 会返回 issue。

视频导入前端注意:

  • 列表和搜索接口不会给付费视频可播放 URL播放统一调 POST /api/videos/play
  • 绑定题目必须提供 questionIdlegacyQuestionId
  • 生产建议把私有视频先入 content_assets,导入时传 assetId,避免长期暴露源站 URL。

题库导出对接

租户后台题库导出统一走后端生成结构化 payload前端不要直接查 Supabase 表拼导出文件。当前后端已支持 JSON、paper_jsonprint_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

采纳请求:

{
  "grantId": "<授权ID>",
  "entryName": "天津专升本公共题库",
  "collectionName": "天津专升本公共题目",
  "copyLimit": 500
}

前端处理规则:

  • 租户只能看到后端判定为已授权的公共题库,不要在前端用套餐码自行过滤。
  • 采纳成功后后端会生成本租户自己的 questionBankIdentryIdcollectionId 和题目快照,学生端直接按普通 /api/catalog/content-entriesquestion-collectionspractice-sessions 接入。
  • 重复采纳返回 QUESTION_BANK_ALREADY_ADOPTED,前端展示“已采纳”即可。
  • 已采纳公共题库可以手动同步平台后续新增/更新题目;同步会重新校验当前租户仍有授权,且只写入租户自己的题目副本。

同步请求:

{
  "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=conflictitem.syncStatus=failed:展示冲突数量和冲突题目,不要把它当系统异常。冲突表示租户已经改过这道采纳题,后端已跳过并保留租户内容。
  • action=conflict 的记录可以进入后续“冲突处理”页面:展示平台源题 ID、租户目标题 ID、上次平台 hash、当前平台 hash、租户当前 hash。当前后端只负责保护不覆盖批量接受平台版本/保留租户版本的操作台后续补。
  • 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>

微信小程序登录

微信小程序端调用 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>",
  "couponCode": "<可选,优惠券码>",
  "couponRedemptionId": "<可选,已领取优惠券 redemptionId>"
}

前端可以先领取优惠券,再下单:

POST /api/commerce/coupons/claim
body: {
  "code": "<couponCode>",
  "planId": "<svipPlanId>",
  "regionId": "<regionId>"
}

coupons/claim 对同一用户同一优惠券是幂等的;已使用的券会返回 COUPON_ALREADY_USED。下单时后端会重新计算套餐原价、优惠金额和最终应付,前端展示金额只能使用接口返回的 originalAmountCentsdiscountCentsamountCents

如果优惠后 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

订单详情会返回 pricingpaymentsitemscouponRedemptions,可用于收银台、订单详情页和售后排查。订单状态轮询页只需消费 status/payment,避免频繁拉取全量明细。

后端已提供 commerce worker 作为兜底补偿:如果微信/支付宝支付成功但 webhook 漏通知worker 会按租户商户配置查询供应商订单并幂等更新订单、支付和权益。前端仍然只轮询 orders/statusorders/detail,不要直接调用供应商查询接口,也不要在页面里自行开通会员。

退款和售后

学生端不直接发起后台退款命令。普通用户订单页只展示 GET /api/commerce/orders/statusGET /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 验签或权益开通。

第一阶段页面建议

  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/classesclasses/membersstudentsteachersstudents/notesstudents/followups
  • 角色模板:GET/PUT /api/tenant-admin/role-templatesPOST /api/tenant-admin/role-templates/disable
  • 内容入口/分类树/题目集合/练习蓝图
  • 题目/单词/知识手册/分数线/视频维护
  • 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入 preview/import/issues
  • Banner/FAQ/公告/激活码/优惠券
  • 勋章:GET/PUT /api/tenant-admin/badgesGET/POST /api/tenant-admin/badge-grants
  • 考试日期:GET/PUT /api/tenant-admin/exam-dates
  • 题目反馈:GET /api/tenant-admin/feedbacksPOST /api/tenant-admin/feedbacks/statusGET /api/tenant-admin/feedbacks/events
  • 销售/代理/CRM 队列

租户后台不应在前端自行决定权限;隐藏菜单只是体验优化,接口仍会校验权限。角色模板用于让租户配置“运营、教师、销售、代理”等自定义后台体验,成员绑定模板后,前端按模板的菜单/模块/字段权限渲染,后端按 permission keys 执行真正的访问控制。班级/学生范围权限由后端根据角色、模板 dataScope.classIdstenant_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。