# Taro 前端对接指南 更新时间:2026-06-30 目标:用一套 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=`。 3. 保存 `tenant.id`、`tenant.slug`、`branding`、`features`、`publicConfig`。 4. 使用 `branding.theme` 和 `branding.publicAssets` 初始化主题、Logo、分享图、页面标题、功能开关。 5. 检查本地 session token,调用 `GET /api/auth/me`。 6. 如果未登录,进入登录页;如果已登录,加载个人中心和首页数据。 ### 微信小程序 1. 从编译环境或小程序启动参数读取 `tenantCode`。 2. 推广码、销售码、分享码从 `options` 或 `scene` 中解析。 3. 调用 `GET /api/tenant/resolve?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`。当前统一入口是: ```text apps/taro/src/services/api.ts apps/taro/src/services/api-auth.ts ``` `apiRequest` 的默认鉴权模式是 `authMode='auto'`: | authMode | 行为 | 适用场景 | | --- | --- | --- | | `auto` | H5 先读取 Supabase Auth access token;没有 Supabase token 时才兜底迁移期 `tk_` session | 绝大多数登录后业务接口 | | `supabase` | 只发送 Supabase access token,没有 token 也不回退 `tk_` | 云端 JWT/RLS 回归、需要提前发现迁移 token 依赖的页面 | | `legacy` | 只发送迁移期 `tk_` session | 本地迁移、旧数据导入演练、临时内网联调 | | `none` | 不发送 Authorization | 租户解析、短信发送/验证、公开目录、公开套餐等接口 | 公共接口必须显式传 `authMode: 'none'`,例如 `tenant/resolve`、`catalog/regions`、`catalog/content-entries`、`catalog/svip-plans`、`auth/sms/send`。平台全局接口或租户解析如不应带租户上下文,必须显式传 `tenantId: null`;不能依赖当前本地缓存的租户。 本地迁移期仍可兼容旧请求头,但新的 Taro 请求封装必须按下面目标实现: ```text Authorization: Bearer x-tenant-id: # 仅作为登录前/公开目录租户上下文;登录后必须与 session 租户一致 ``` 生产目标: ```text Authorization: Bearer x-tenant-id: # 作为租户上下文,不能作为身份依据 ``` 前端不应再传 `x-user-id`、query/body `userId` 来表示当前用户。后端已经实现 Supabase JWT 和迁移 session 优先解析:如果 Authorization 存在,用户态接口以 token 映射出的业务用户为准;如果请求里伪造了不同的 `userId` 会返回 `AUTH_USER_MISMATCH`,伪造不同租户会返回 `AUTH_TENANT_MISMATCH` 或 `AUTH_SESSION_INVALID`。 H5 使用 Supabase Auth 时,不要在页面里手写 `Authorization`,推荐请求流程是由统一 client 完成: ```ts await apiRequest('/api/profile/me'); ``` `apps/taro/src/services/api-auth.ts` 会读取 Supabase session 并生成 `Authorization: Bearer `。页面层禁止通过 `headers.Authorization` 或 `headers['x-tenant-id']` 覆盖身份和租户上下文;如确实要切换租户上下文,必须使用 `tenantId` 显式参数。该规则由 `scripts/taro-api-auth-mode-test.js` 纳入 `npm run test:readiness`。 后端会通过 `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 接入的页面。 前端构建变量或 H5 `runtime-config.json` 只允许包含: ```text TARO_APP_PORTAL / portal TARO_APP_API_BASE_URL / apiBaseUrl TARO_APP_SUPABASE_URL / supabaseUrl TARO_APP_SUPABASE_PUBLISHABLE_KEY / supabasePublishableKey TARO_APP_TENANT_CODE / tenantCode ``` H5 线上优先使用每个静态目录根部的 `runtime-config.json` 覆盖公开配置,避免 API/Auth 域名变化时重打包。完整部署、Nginx、CSP、缓存和 CORS 规则见 `docs/refactor/taro-h5-deployment.md`。禁止把 Supabase secret key、service role key、数据库连接串、对象存储密钥、支付私钥放进 Taro 构建变量或 `runtime-config.json`。 统一错误处理: | HTTP | 前端动作 | | --- | --- | | 400 | 展示表单错误或参数错误 | | 401 | 清 session,跳登录 | | 403 | 展示无权限或会员升级 | | 404 | 展示空状态 | | 409 | 展示业务冲突,例如激活码已用 | | 413 | 提示上传/导入文件过大 | | 429 | 倒计时重试,例如短信冷却 | | 500 | 展示系统异常并上报日志 | ## 租户主题与品牌契约 学生端、租户后台、平台后台启动时都通过 `GET /api/tenant/resolve` 获取已发布主题。响应中的 `branding.theme` 是已发布 token,`branding.publicAssets` 是公开素材引用,前端可以安全消费;租户后台草稿不会出现在公开解析响应里。 主题 token 示例: ```json { "primaryColor": "#2563eb", "accentColor": "#0f766e", "backgroundColor": "#f8fafc", "surfaceColor": "#ffffff", "textColor": "#0f172a", "borderRadius": 8, "buttonRadius": 8, "layoutDensity": "comfortable" } ``` 公开素材示例: ```json { "logoUrl": "/assets/tenant/logo.png", "shareImageUrl": "https://static.example.com/share.png", "iconSet": "focus", "shareCardStyle": "study" } ``` 前端处理规则: - 只读取 `/api/tenant/resolve` 返回的已发布主题来渲染学生端和公开页面。 - 租户后台主题草稿只调用 `GET /api/tenant-admin/theme` 展示,不能让学生端读取草稿。 - 不允许前端把任意 CSS、HTML、JS 或远程脚本当作主题执行;后端已经限制主题为颜色、半径、安全 CSS 变量、图标 token 和 HTTPS/站内公开素材。 - Logo、分享图、启动图等长期素材后续应从后台上传进入 `content_assets` 或静态公共资源,再把公开 URL/路径写入主题 `publicAssets`。 - 小程序端用 `branding.publicAssets.shareImageUrl` 作为分享图时,仍要遵守微信平台对图片尺寸、域名和 HTTPS 的要求。 租户后台主题配置接口: ```text GET /api/tenant-admin/theme-templates GET /api/tenant-admin/theme POST /api/tenant-admin/theme/preview POST /api/tenant-admin/theme/publish ``` 权限: ```text tenant:theme:read tenant:theme:write ``` `POST /api/tenant-admin/theme/preview` 只保存草稿: ```json { "templateCode": "focus", "theme": { "primaryColor": "#123abc", "accentColor": "#f59e0b" }, "publicAssets": { "logoUrl": "/assets/tenant/logo.png", "iconSet": "focus", "shareCardStyle": "study" } } ``` `POST /api/tenant-admin/theme/publish` 发布草稿: ```json { "useDraft": true } ``` 发布成功后,下一次 `GET /api/tenant/resolve` 会返回新主题。主题预览和发布都会写入租户审计日志,越权角色会返回 `TENANT_PERMISSION_REQUIRED`。 ## 全局状态建议 | 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::catalog:entries tenant::profile tenant::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` | | 恢复练习 | `GET /api/learning/practice-sessions/detail?practiceSessionId=...` | | 提交答案 | `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`、`GET /api/profile/activity-tasks`、`POST /api/profile/activity-tasks/claim`、`GET /api/profile/exchange-items`、`POST /api/profile/exchange-items/redeem`、`GET /api/profile/notifications`、`POST /api/profile/notifications/status` | | 销售分享 | `/api/referral/resolve`、`track-event`、`bind` | | 租户数据看板 | `GET /api/tenant-admin/dashboard?timeRange=30d®ionId=...` | | 租户主题模板 | `GET /api/tenant-admin/theme-templates`、`GET /api/tenant-admin/theme`、`POST /api/tenant-admin/theme/preview`、`POST /api/tenant-admin/theme/publish` | | 租户班级 | `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/PUT /api/tenant-admin/point-activity-tasks`、`GET /api/tenant-admin/point-activity-claims`、`GET/PUT /api/tenant-admin/point-exchange-items`、`GET /api/tenant-admin/point-exchange-orders` | | 公共题库采纳/同步 | `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/public-question-banks/conflicts/resolve`、`POST /api/tenant-content/public-question-banks/conflicts/resolve-batch` | | 题库导出 | `POST /api/tenant-content/exports/questions`、`GET /api/tenant-content/exports/jobs` | | 资料/视频运营审计 | `GET /api/tenant-content/media-analytics/summary`、`asset-events`、`video-events` | ## 资料、PDF 和视频资源契约 前端必须把 `content_assets` 当成资源唯一台账。学生端资料、PDF 预览和题目视频播放都不能直接拼接私有 OSS/COS/Supabase Storage URL,也不能把后台配置的 `cdnUrl` 持久缓存成长期可访问地址。 学生端资料流程: 1. 列表页调用 `GET /api/catalog/assets`,只展示后端返回的 active 资源。 2. 预览 PDF/图片时调用 `GET /api/catalog/assets/preview?assetId=...`。 3. 下载资料时调用 `GET /api/catalog/assets/download?assetId=...`。 4. 使用响应里的 `preview.url` 或 `download.url` 立即打开;不要写入本地长期缓存。若响应包含 `watermark.required=true`,必须先渲染可见水印覆盖层,再打开或展示签名资源。 签名有效期规则: - 学生 inline 预览、SVIP/会员资料、视频和资料包通常只有 300 秒左右有效期。 - 后台预览有效期也不是永久 URL,租户后台应在用户点击时重新请求签名。 - 响应里的 `expiresInSec/expiresAt/signatureMode` 只用于 UI 提示和排查,不要自行延长有效期。 动态水印响应: ```json { "watermark": { "mode": "visible_overlay", "required": true, "text": "仅限本人学习 账号:AB12CD34 7D2A9C3E1B0F", "traceId": "7D2A9C3E1B0F", "position": "diagonal", "opacity": 0.16, "repeat": true, "expiresAt": "2026-06-29T10:00:00.000Z", "renderHint": "render_visible_overlay_before_opening_signed_url" } } ``` 前端处理规则: - `mode=visible_overlay` 时,PDF/图片预览、H5 视频播放器和资料打开页都要显示覆盖水印。 - 水印必须包含 `text` 和 `traceId`,不能只显示品牌名。 - `repeat=true` 建议做斜向重复水印;`position=bottom-right` 或 `center` 可作为单水印模式。 - 不要把 `traceId` 当隐私信息隐藏;它是外泄追踪码,会同步写入后端访问事件。 - 小程序端如果原生 PDF/video 组件覆盖层能力受限,应使用自定义容器包裹组件,至少在可视区域显示固定水印和 traceId。 - 当前 `apps/taro/src/pages/student/assets/index.tsx` 已按该契约接入:预览和下载都先向后端申请短期签名,页面展示过期时间、签名模式、`watermark.traceId` 和可见水印。若资源要求 `watermark.required=true`,H5 预览不提供脱离水印容器的外部打开入口;下载会先展示水印确认面板,再由用户确认打开/复制签名链接。小程序端如无法保证原生组件覆盖层,应提示使用 H5 资料页或只展示水印确认,不直接嵌入私有文件。 锁定资源 CDN 规则: - `visibility=members/svip/private` 的外部 `cdnUrl` 默认会被后端拒绝,返回 `ASSET_CDN_ACCESS_NOT_ALLOWED`。 - 只有后台明确登记 `metadata.providerManagedAccess=true` 或 `metadata.cdnAccessMode='signed_by_provider'`,后端才允许把外部 URL 作为 provider-managed 资源返回。 - 商用环境更推荐把锁定资料登记为 `objectKey`,由后端生成 OSS/COS/Supabase Storage 私有签名 URL。 租户后台排查: ```text GET /api/tenant-content/assets/access-events?assetId=&limit=100 ``` 该接口返回资源访问事件,包括学生下载、学生预览、后台下载、后台预览、上传签名、上传确认以及 denied 原因。租户后台可以在资源详情页增加“访问记录/异常记录”面板。 访问事件 `metadata.watermark.traceId` 可用于后台按截图上的追踪码回查访问记录。视频播放不走 `content_asset_access_events`,但 `POST /api/videos/play` 返回同样的 `watermark` 对象,后端会把 traceId 写入 `video_play_events.metadata.watermark.traceId`。 租户后台运营报表: ```text GET /api/tenant-content/media-analytics/summary?timeRange=30d&limit=10 GET /api/tenant-content/media-analytics/asset-events?traceId=7D2A9C3E1B0F&limit=100 GET /api/tenant-content/media-analytics/video-events?videoId=&limit=100 ``` 这些接口用于租户后台资料/视频运营面板,权限为 `content:analytics:read`,租户 owner/admin/operator 默认可访问。普通教师默认不可见,除非绑定了带该权限的角色模板。 前端展示建议: - `summary.assetAccess` 展示下载、预览、拒绝访问、水印事件数。 - `summary.videoPlay` 展示视频播放、SVIP 播放、次数播放、消耗次数。 - `assetTop` / `videoTop` 做热门资料和热门视频排行。 - `daily` 做资料访问和视频播放趋势。 - 搜索框支持输入截图上的 `traceId`,同时请求 `asset-events` 和 `video-events` 回查用户、时间、IP、UA、资源或视频。 - 这些接口不会返回签名 URL、播放 token、云厂商密钥或支付密钥;前端不要把它们当成下载/播放接口。 ## 练习访问控制契约 前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 `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 的题目。 ### 题干、解析和知识手册富文本 学生端已经新增统一渲染组件: ```text apps/taro/src/components/RichContent.tsx apps/taro/src/components/rich-content.css ``` 官方参考: - KaTeX options:`https://katex.org/docs/options` - Taro RichText:`https://docs.taro.zone/en/docs/components/base/rich-text` 当前接入页面: ```text pages/student/practice/index 题干、选项、子题、参考答案、解析 pages/student/reports/index 逐题复盘、子题明细、参考答案、解析 pages/student/handbook/index 知识点摘要和正文 ``` 第一版支持: - 纯文本和换行。 - Markdown 图片 `![alt](https://...)`、站内 `/...` 路径,或私有资源引用 `asset:`、`content_asset:`、`/asset/`。 - 基础表格。 - `**加粗**`、行内代码和代码块。 - H5 端用 KaTeX 渲染 `$...$`、`$$...$$`、`\(...\)`、`\[...\]`;渲染失败时降级显示公式原文。 - 长题干、长单词、长公式自动换行或横向滚动。 安全边界: - 组件会剥离 `