Files
gongxue-base/docs/refactor/taro-frontend-integration.md

127 KiB
Raw Blame History

Taro 前端对接指南

更新时间2026-06-30

目标:用一套 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. 使用 branding.themebranding.publicAssets 初始化主题、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。当前统一入口是:

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/resolvecatalog/regionscatalog/content-entriescatalog/svip-plansauth/sms/send。平台全局接口或租户解析如不应带租户上下文,必须显式传 tenantId: null;不能依赖当前本地缓存的租户。

本地迁移期仍可兼容旧请求头,但新的 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 时,不要在页面里手写 Authorization,推荐请求流程是由统一 client 完成:

await apiRequest('/api/profile/me');

apps/taro/src/services/api-auth.ts 会读取 Supabase session 并生成 Authorization: Bearer <supabase_access_token>。页面层禁止通过 headers.Authorizationheaders['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 的租户。

生产或云端测试建议设置:

ALLOW_LEGACY_AUTH_HEADERS=false
ALLOW_PLATFORM_ADMIN_KEY=false

这样旧式 x-user-id 和平台管理 key 会被拒绝,前端可以提前发现未按 session 接入的页面。

前端构建变量或 H5 runtime-config.json 只允许包含:

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 是已发布 tokenbranding.publicAssets 是公开素材引用,前端可以安全消费;租户后台草稿不会出现在公开解析响应里。

主题 token 示例:

{
  "primaryColor": "#2563eb",
  "accentColor": "#0f766e",
  "backgroundColor": "#f8fafc",
  "surfaceColor": "#ffffff",
  "textColor": "#0f172a",
  "borderRadius": 8,
  "buttonRadius": 8,
  "layoutDensity": "comfortable"
}

公开素材示例:

{
  "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 的要求。

租户后台主题配置接口:

GET  /api/tenant-admin/theme-templates
GET  /api/tenant-admin/theme
POST /api/tenant-admin/theme/preview
POST /api/tenant-admin/theme/publish

权限:

tenant:theme:read
tenant:theme:write

POST /api/tenant-admin/theme/preview 只保存草稿:

{
  "templateCode": "focus",
  "theme": {
    "primaryColor": "#123abc",
    "accentColor": "#f59e0b"
  },
  "publicAssets": {
    "logoUrl": "/assets/tenant/logo.png",
    "iconSet": "focus",
    "shareCardStyle": "study"
  }
}

POST /api/tenant-admin/theme/publish 发布草稿:

{
  "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

注意缓存必须带租户维度,例如:

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-miniappPOST /api/auth/oauth/wechatPOST /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/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,仅当租户 features.enableLeaderboard=true 时启用
题目视频 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/badgesGET /api/profile/activity-tasksPOST /api/profile/activity-tasks/claimGET /api/profile/exchange-itemsPOST /api/profile/exchange-items/redeemGET /api/profile/notificationsPOST /api/profile/notifications/status
销售分享 /api/referral/resolvetrack-eventbind
租户数据看板 GET /api/tenant-admin/dashboard?timeRange=30d&regionId=...
租户主题模板 GET /api/tenant-admin/theme-templatesGET /api/tenant-admin/themePOST /api/tenant-admin/theme/previewPOST /api/tenant-admin/theme/publish
租户班级 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/PUT /api/tenant-admin/point-activity-tasksGET /api/tenant-admin/point-activity-claimsGET/PUT /api/tenant-admin/point-exchange-itemsGET /api/tenant-admin/point-exchange-orders
公共题库采纳/同步 GET /api/tenant-content/public-question-banksPOST /api/tenant-content/public-question-banks/adoptPOST /api/tenant-content/public-question-banks/syncGET /api/tenant-content/public-question-banks/conflicts?adoptionId=...POST /api/tenant-content/public-question-banks/conflicts/resolvePOST /api/tenant-content/public-question-banks/conflicts/resolve-batch
题库导出 POST /api/tenant-content/exports/questionsGET /api/tenant-content/exports/jobs
资料/视频运营审计 GET /api/tenant-content/media-analytics/summaryasset-eventsvideo-events
平台权限目录 GET /api/platform-admin/permissions

资料、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.urldownload.url 立即打开;不要写入本地长期缓存。若响应包含 watermark.required=true,必须先渲染可见水印覆盖层,再打开或展示签名资源。

签名有效期规则:

  • 学生 inline 预览、SVIP/会员资料、视频和资料包通常只有 300 秒左右有效期。
  • 后台预览有效期也不是永久 URL租户后台应在用户点击时重新请求签名。
  • 响应里的 expiresInSec/expiresAt/signatureMode 只用于 UI 提示和排查,不要自行延长有效期。

动态水印响应:

{
  "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_overlayPDF/图片预览、H5 视频播放器和资料打开页都要显示覆盖水印。
  • 水印必须包含 texttraceId,不能只显示品牌名。
  • repeat=true 建议做斜向重复水印;position=bottom-rightcenter 可作为单水印模式。
  • 不要把 traceId 当隐私信息隐藏;它是外泄追踪码,会同步写入后端访问事件。
  • 小程序端如果原生 PDF/video 组件覆盖层能力受限,应使用自定义容器包裹组件,至少在可视区域显示固定水印和 traceId。
  • 当前 apps/taro/src/pages/student/assets/index.tsx 已按该契约接入:预览和下载都先向后端申请短期签名,页面展示过期时间、签名模式、watermark.traceId 和可见水印。若资源要求 watermark.required=trueH5 预览不提供脱离水印容器的外部打开入口;下载会先展示水印确认面板,再由用户确认打开/复制签名链接。小程序端如无法保证原生组件覆盖层,应提示使用 H5 资料页或只展示水印确认,不直接嵌入私有文件。

锁定资源 CDN 规则:

  • visibility=members/svip/private 的外部 cdnUrl 默认会被后端拒绝,返回 ASSET_CDN_ACCESS_NOT_ALLOWED
  • 只有后台明确登记 metadata.providerManagedAccess=truemetadata.cdnAccessMode='signed_by_provider',后端才允许把外部 URL 作为 provider-managed 资源返回。
  • 商用环境更推荐把锁定资料登记为 objectKey,由后端生成 OSS/COS/Supabase Storage 私有签名 URL。

租户后台排查:

GET /api/tenant-content/assets/access-events?assetId=<assetId>&limit=100

该接口返回资源访问事件,包括学生下载、学生预览、后台下载、后台预览、上传签名、上传确认以及 denied 原因。租户后台可以在资源详情页增加“访问记录/异常记录”面板。

访问事件 metadata.watermark.traceId 可用于后台按截图上的追踪码回查访问记录。视频播放不走 content_asset_access_events,但 POST /api/videos/play 返回同样的 watermark 对象,后端会把 traceId 写入 video_play_events.metadata.watermark.traceId

租户后台运营报表:

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=<videoId>&limit=100

这些接口用于租户后台资料/视频运营面板,权限为 content:analytics:read,租户 owner/admin/operator 默认可访问。普通教师默认不可见,除非绑定了带该权限的角色模板。

前端展示建议:

  • summary.assetAccess 展示下载、预览、拒绝访问、水印事件数。
  • summary.videoPlay 展示视频播放、SVIP 播放、次数播放、消耗次数。
  • assetTop / videoTop 做热门资料和热门视频排行。
  • daily 做资料访问和视频播放趋势。
  • 搜索框支持输入截图上的 traceId,同时请求 asset-eventsvideo-events 回查用户、时间、IP、UA、资源或视频。
  • 这些接口不会返回签名 URL、播放 token、云厂商密钥或支付密钥前端不要把它们当成下载/播放接口。

练习访问控制契约

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

题干、解析和知识手册富文本

学生端已经新增统一渲染组件:

apps/taro/src/components/RichContent.tsx
apps/taro/src/components/rich-content.css

官方参考:

  • KaTeX optionshttps://katex.org/docs/options
  • Taro RichTexthttps://docs.taro.zone/en/docs/components/base/rich-text

当前接入页面:

pages/student/practice/index      题干、选项、子题、参考答案、解析
pages/student/reports/index       逐题复盘、子题明细、参考答案、解析
pages/student/handbook/index      知识点摘要和正文

第一版支持:

  • 纯文本和换行。
  • Markdown 图片 ![alt](https://...)、站内 /... 路径,或私有资源引用 asset:<uuid>content_asset:<uuid>/asset/<uuid>
  • 基础表格。
  • **加粗**、行内代码和代码块。
  • H5 端用 KaTeX 渲染 $...$$$...$$\(...\)\[...\];渲染失败时降级显示公式原文。
  • 长题干、长单词、长公式自动换行或横向滚动。

安全边界:

  • 组件会剥离 <script><style> 和普通 HTML 标签,不执行后端或导入内容中的 HTML/JS。
  • 图片只允许 HTTPS、站内相对路径、本地开发 localhost HTTP或明确的 content_assets 资源 ID 引用;拒绝 javascript:data:、协议相对 URL 等危险来源。
  • 私有题图通过 GET /api/catalog/assets/preview?assetId=... 申请短期签名后展示,不把私有 OSS/COS/Supabase Storage URL 长期写进题干或本地缓存。
  • 公式 HTML 只来自 KaTeX renderToString,不要把题库导入的原始 HTML 直接交给 RichText。小程序端还要真机验收 KaTeX 生成 HTML 的兼容性;如兼容性不足,保持同一 parser替换为服务端公式图片或小程序专用公式组件。

断点续练

Taro 可以缓存当前题号和答题卡用于刷新恢复体验,但跨设备、清缓存、小程序重启后的权威恢复必须调用:

GET /api/learning/practice-sessions/detail?practiceSessionId=<sessionId>

响应会返回:

{
  "item": {
    "id": "...",
    "status": "active",
    "questionIds": ["..."],
    "questions": [],
    "answersByQuestion": {
      "<questionId>": {
        "selectedOptions": ["1"],
        "answerText": null,
        "answerPayload": {
          "mode": "composite",
          "subAnswers": [],
          "subResults": []
        },
        "isCorrect": true,
        "answeredAt": "..."
      }
    },
    "expiresAt": "..."
  }
}

前端处理规则:

  • 继续练习入口优先从 GET /api/learning/practice-sessions/history?status=active 获取未完成 session再带 practiceSessionId 进入练习页。
  • 练习页如果 URL 有 practiceSessionId,先调用 detail 恢复后端题目快照和最新答案,不要新建 session。
  • answersByQuestion 是同一题的最新答题记录,答题卡、正确/错误统计和解析展示以它为准。
  • 阅读理解、案例分析等复合题会在 answerPayload.subAnswers/subResults 中返回子题作答、判分、解析和分值;继续练习时按该结构恢复每个子题状态。
  • 倒计时以 expiresAt 计算剩余时间;不要用本地启动时间重新生成考试时长。
  • detail 只返回当前用户自己的 session跨用户或跨租户读取会返回 PRACTICE_SESSION_NOT_FOUND

提交答案契约

客观题、主观题都统一调用:

POST /api/learning/answers

单选/判断题示例:

{
  "practiceSessionId": "...",
  "questionId": "...",
  "selectedOptions": ["1"]
}

多选题示例:

{
  "practiceSessionId": "...",
  "questionId": "...",
  "selectedOptions": ["0", "2"]
}

填空、简答、翻译、案例分析等无客观选项的主观题,前端可以先展示参考答案,再让学生自评:

{
  "practiceSessionId": "...",
  "questionId": "...",
  "answerText": "学生自己的作答或备注",
  "selfJudgedCorrect": true
}

阅读理解、案例分析、组合题等带 subQuestions 的复合题必须使用 subAnswers,不能混用顶层 selectedOptions/answerText/selfJudgedCorrect

{
  "practiceSessionId": "...",
  "questionId": "...",
  "subAnswers": [
    {
      "subQuestionId": "main-idea",
      "selectedOptions": ["1"]
    },
    {
      "subQuestionId": "reason",
      "answerText": "学生自己的作答或备注",
      "selfJudgedCorrect": true
    }
  ]
}

复合题响应会额外返回:

{
  "item": {
    "isCorrect": true,
    "answerPayload": {
      "mode": "composite",
      "subAnswers": [
        { "subQuestionId": "main-idea", "selectedOptions": ["1"], "answerText": null },
        { "subQuestionId": "reason", "selectedOptions": [], "answerText": "学生自己的作答或备注", "selfJudgedCorrect": true }
      ],
      "subResults": [
        {
          "subQuestionId": "main-idea",
          "order": 1,
          "type": "choice",
          "selectedOptions": ["1"],
          "isCorrect": true,
          "correctOptionIndices": [1],
          "explanation": "..."
        }
      ],
      "summary": {
        "answeredCount": 2,
        "correctCount": 2,
        "wrongCount": 0,
        "unansweredCount": 0
      }
    },
    "subResults": []
  }
}

响应关键字段:

{
  "item": {
    "id": "...",
    "questionId": "...",
    "selectedOptions": [],
    "answerText": "学生自己的作答或备注",
    "isCorrect": true,
    "answeredAt": "2026-06-29T00:00:00.000Z",
    "selfJudged": true
  }
}

前端处理规则:

  • 客观题不要传 selfJudgedCorrect。后端会用题库标准答案判分,传了会返回 SELF_JUDGMENT_NOT_ALLOWED
  • 客观子题同样不要传 selfJudgedCorrect;主观子题可以传 selfJudgedCorrect
  • 复合题如果缺少 subAnswers 会返回 SUB_ANSWERS_REQUIRED;空提交会返回 SUB_ANSWERS_EMPTY;未知子题 id 会返回 UNKNOWN_SUB_ANSWER
  • 主观题自评也由后端落库为 answer_records.is_correct,错题本、练习统计、模考报告都以后端返回为准。
  • 前端可以在本地缓存当前 session 的答题卡和当前题号,用于刷新恢复体验;但交卷报告只以后端 answer_records 和 session 快照计算。
  • answerText 只保存学生作答或备注,不要为了让后端判对而把参考答案塞进去。
  • 重复答题时,报告会取同一题最新一条 answer_records,页面应以最近一次提交结果展示。

学生端支付与售后契约

学生端 pages/student/checkout/indexpages/student/order-detail/index 已接第一版。前端只传递套餐、地区、优惠券和支付 provider最终金额、优惠抵扣、订单状态、支付记录、权益发放都以后端返回为准。

推荐流程:

  1. GET /api/catalog/svip-plans 加载可购买套餐。
  2. 如有优惠券,先调 POST /api/commerce/coupons/claim,仅用于领取、占用一个未核销 redemption 和展示预计抵扣。
  3. POST /api/commerce/orders 创建订单,后端会重新计算最终金额和抵扣。
  4. 非零元订单调 POST /api/commerce/payments/create 获取支付参数。
  5. H5 支付可跳转 provider 返回的 URL微信小程序支付用 provider 返回参数调用 Taro.requestPayment
  6. 支付后调 /api/commerce/orders/status 轮询状态,已支付订单的权益由后端 webhook/补偿 worker 幂等发放。

普通学生端不直接调用退款接口。退款申请、审核、供应商退款、全额退款权益撤销均在租户后台权限流中完成;学生端只展示订单详情、状态和售后联系入口。

勋章

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

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 对同一用户同一勋章幂等,不会重复生成多条发放记录。

当前后端已支持第一批自动发放规则:

unlockType 推荐 conditionField 触发时机
check_in checkInStreak POST /api/profile/check-in 真实签到成功后
score score 签到加分、反馈奖励或积分活动奖励成功后
feedback_resolved feedbackResolvedCount 租户后台把反馈处理为 resolved
activity_reward activityRewardCountscore 学生领取积分活动任务且后端证据校验通过后

规则使用 conditionOperator 的合法值 gtegtltelteqconditionValue 为数字。触发成功的接口会返回 autoBadges,前端可据此弹出“获得勋章”提示;如果是重复签到、重复处理反馈或已获得过同一勋章,后端不会重复返回同一发放记录。

模考交卷与报告

全真模拟、试卷模式、顺序练习的最终报告都走后端交卷接口。前端不得传分数、正确数或题目范围;后端只信任 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": "..."
      }
    ]
  }
}

复合题报告规则:

  • totalQuestions 仍按顶层大题计数,阅读理解/案例分析不会按子题拆成多题。
  • questionResults[].subResults 返回每个子题的 selectedOptions/answerText/isCorrect/explanation/score/totalScore
  • 若导入数据没有给子题分值,后端默认把该大题分值平均分给所有子题;如果后续导入模板提供子题 score,报告会按子题分值再缩放到大题配置分。
  • 顶层 isCorrect=true 表示所有子题都判为正确;若部分正确,顶层为 false,但 score 会保留部分得分。

前端处理规则:

  • 重复交卷是幂等的,后端会返回同一份报告。
  • 报告页刷新时调用 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=... 仅租户 features.enableLeaderboard=true 时请求;返回排名、用户展示信息、当前用户排名和范围信息

错题复习创建 session

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

前端处理规则:

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

排行榜

排行榜由后端统一聚合,但当前产品默认关闭,租户 feature_flags.enableLeaderboard 未开启时接口返回 LEADERBOARD_DISABLED。前端默认不要在个人中心请求或展示排行榜;只有租户明确开启并完成压测后,再进入独立排行榜页或活动页。前端不要读取答题记录、单词进度或模考报告后自行排名,避免越权、口径漂移和跨租户数据泄露。

可选参数:

参数 可选值 说明
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
导出结算明细 GET /api/commission/settlements/export?settlementId=...&format=csv commission:readcommission:self
生成结算单 POST /api/commission/settlements/generate commission:write
审核/打款状态 POST /api/commission/settlements/status commission:review
查看凭证 GET /api/commission/settlements/proofs?settlementId=... commission:readcommission:self
登记凭证 POST /api/commission/settlements/proofs commission:review
凭证复核 POST /api/commission/settlements/proofs/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 表示当前账期无未结算来源,不是系统异常。
  • 导出接口返回 contentBase64/sha256/filename/mimeTypeH5 可转成下载,微信小程序端建议后续使用文件系统保存;前端不要直接查询 commission_settlement_items 拼文件。
  • 凭证支持 assetIdexternalUrl。如果使用 assetId,必须先通过资料/对象存储台账上传凭证,后端会校验资源属于当前租户;externalUrl 只接受 http/https。
  • referrerUserId 是受首绑保护的推广/分佣归属CRM 的 assignedToUserId 只是跟进负责人,不能作为分佣结算依据。
  • 当前版本支持线下打款状态登记、结算导出、凭证登记和凭证复核;真实银行/微信/支付宝打款 provider、发票和批量凭证上传后续增强。

背单词计划与复习上报

背单词页面分三类数据:单元列表、每日计划、单词进度。前端不需要计算下次复习日期,只提交“认识/不认识”,由后端统一更新 nextReviewDate、连续正确、掌握状态和每日复习计划。

当前 Taro 页面:

apps/taro/src/pages/student/vocabulary/index.tsx
apps/taro/src/services/pronunciation.ts

已支持三种队列:

  • 今日计划:调用 review-plan,优先学习后端计划中的待复习/新词。
  • 单元学习:调用 vocabulary-words,用于按单元顺序学习。
  • 收藏练习:调用 vocabulary/favorites,用于复习本人收藏单词。

页面交互已包含卡片翻转、上一个/下一个、单词列表跳转、发音、美/英音切换、收藏/取消收藏和按 unitId + mode 保存本地当前位置。权威掌握状态仍以后端 vocabulary/review 返回和后续统计为准。

取今日计划:

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 作为后续展示依据,不在前端重算间隔。
  • 收藏列表调用 GET /api/learning/vocabulary/favorites?unitId=<unitId>;收藏/取消收藏调用 POST /api/learning/vocabulary/favorites
  • 发音当前使用前端 services/pronunciation.tsH5 优先播放有道 dictvoice HTTPS 音频并用 Web Speech 兜底,小程序优先使用 Taro.createInnerAudioContext。这里不保存任何密钥;如果后续租户需要自定义发音源,应改为后端返回可配置的公开 provider URL 或资源台账引用。
  • 旧的 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,不要缓存为长期资源地址。
  6. 播放器开始、周期心跳和播放完成时调用 POST /api/videos/progress 上报进度。

请求示例:

{
  "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 写入本地持久缓存。
  • playToken 只用于当前播放会话进度上报,不写入长期缓存,不暴露到页面 URL。
  • H5 video 组件建议在 play 上报 eventType=start,每 15-30 秒或进度变化明显时上报 heartbeatended 或观看进度超过 90% 时上报 complete

进度上报示例:

{
  "playToken": "vp_...",
  "eventType": "heartbeat",
  "progressSeconds": 45,
  "watchedSeconds": 48,
  "durationSeconds": 90
}

后端会校验 playToken 必须属于当前登录用户和当前租户,其他用户不能拿 token 改播放状态。返回的 item.playback 会包含 watchedSecondscompletionRatestartedAtcompletedAt,租户后台媒体运营报表会读取这些字段计算完成率和观看时长。

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

学生端资料只读取目录、预览和下载签名,不接触对象存储真实密钥,也不自行拼接私有 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_SECURITY_SCAN_REQUIRED:展示“资料安全扫描中,请稍后再试”,并重新拉取资源列表或提示后台处理。
  • ASSET_SECURITY_SCAN_FAILED:展示“资料安全校验未通过,已下架”,学生端不要继续重试旧签名。
  • ASSET_NOT_FOUND 或列表中资源从 active 消失:展示“资源异常已下架”或刷新列表,不要继续使用旧签名 URL。
  • ASSET_PREVIEW_NOT_SUPPORTED:隐藏预览按钮,仅保留下载或提示不支持预览。
  • previewUrl 字段只作为公开/托管预览提示,不代表可以绕过接口直接访问。

租户后台上传资料必须走六步:

sign-upload -> 直传对象存储 -> PUT assets 登记草稿 -> confirm-upload -> 等待 assets worker 安全扫描 -> PUT assets 发布 -> sign-preview 验收

后台上传确认:

{
  "assetId": "<assetId>",
  "fileSizeBytes": 4096,
  "mimeType": "application/pdf",
  "checksumSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "publish": true
}

托管对象在确认前会保持 status=draftuploadStatus=pendingsecurityScanStatus=pending,学生端不会看到。确认成功后仍保持 status=draft,并进入 uploadStatus=verifiedsecurityScanStatus=pending;即使传 publish=true,后端也不会直接发布。确认失败时后端返回 UPLOAD_VERIFICATION_FAILED,后台必须展示失败原因并允许重新上传,不能前端强行改为已发布。

后台资源列表建议展示这些状态:

状态 前端展示 可执行动作
draft/pending/pending 待上传确认 重新上传、确认上传
draft/verified/pending 安全扫描排队中 刷新状态、查看扫描事件
draft/verified/scanning 安全扫描中 刷新状态、查看扫描事件
draft/verified/passed 可发布 发布、预览、下载
active/verified/passed 已发布 预览、下载、下架
draft/verified/failed 安全扫描失败 查看扫描事件、重新上传
draft/failed/skipped 上传复检失败 查看复检/扫描事件、重新上传

租户后台可通过下面接口排查扫描过程:

GET /api/tenant-content/assets/security-scan-events?assetId=<assetId>&limit=100

GET /api/tenant-content/assets 和学生端 GET /api/catalog/assets 都会返回 securityScanStatus。前端可以展示状态,但最终能否下载、预览、播放仍以后端签名接口为准。

生产环境会定时运行 assets worker 复检对象存储元数据并执行内置 metadata_rules 安全扫描;上线配置应启用 WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http,由 worker 调用外部 HTTP 杀毒/内容安全服务。复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致时,后端会把资源置为 uploadStatus=failedsecurityScanStatus=skipped 并从 active 退回 draft,同时写入 securityFlags.assetRecheckFailed=true。扫描发现 MIME 不允许、扩展名/MIME 不匹配、外部 scanner 判定风险,或外部 scanner 不可用且 fail-closed 时,会把资源置为 securityScanStatus=failed,并写入 securityFlags.assetSecurityScanFailed=true

前端处理规则:

  • 租户后台资源列表应展示 securityScanStatus=pending/scanning/failed/skipped/passed,其中 failed/skipped 展示异常原因和重新上传入口。
  • 租户后台不能用前端状态绕过发布;即使 UI 显示处理中,发布/下载/预览仍以后端接口返回为准。
  • 学生端不要缓存资料列表和签名 URL 作为长期状态;每次预览/下载/播放前重新请求后端签名。
  • 前端不调用外部扫描服务,也不接触 scanner endpoint/token扫描证据只通过 GET /api/tenant-content/assets/security-scan-events 给后台排查。

考试倒计时、签到积分和反馈

首页可用 GET /api/catalog/exam-dates?regionId=<regionId> 展示地区公开考试日期;个人中心优先用 GET /api/profile/exam-countdowns,后端会按学生当前 regionId/selectedSchoolId 返回匹配倒计时。

个人中心预设头像

学生端不支持用户上传头像。系统只提供男女两种默认头像预设,避免头像上传带来的存储、内容安全、隐私和审核复杂度。前端不要展示文件选择器,不要调用对象存储上传链路,也不要把微信/QQ 返回的头像 URL 写入学生资料。

当前 Taro service 使用:

apps/taro/src/services/profile.ts

loadProfile
updateProfile({ avatarPreset: 'male' | 'female' })

GET /api/profile/me 会返回:

{
  "item": {
    "avatarPreset": "male",
    "avatar": {
      "preset": "male",
      "displayUrl": "/assets/avatars/default-male.svg"
    }
  }
}

前端展示规则:

  • 直接使用 item.avatar.displayUrl 展示头像。
  • 允许用户在 UI 中二选一切换 male / female,调用 PATCH /api/profile/meavatarPreset 字段保存。
  • PATCH /api/profile/meavatarUrl 会返回 PROFILE_AVATAR_URL_DIRECT_UPDATE_REJECTED,前端不应提供该输入。
  • avatarPreset 只能是 malefemale;其它值会返回 INVALID_AVATAR_PRESET
  • 默认头像资源由前端静态资产或 CDN 提供,路径保持 /assets/avatars/default-male.svg/assets/avatars/default-female.svg

签到入口调用:

POST /api/profile/check-in

返回关键字段:

{
  "item": {
    "checkedIn": true,
    "alreadyCheckedIn": false,
    "pointsAdded": 10,
    "streak": 1,
    "score": 10,
    "lastCheckInDate": "2026-06-29",
    "autoBadges": [
      {
        "badgeId": "...",
        "badge": {
          "name": "连续签到",
          "category": "activity",
          "iconUrl": "https://..."
        }
      }
    ]
  }
}

前端处理规则:

  • alreadyCheckedIn=true 时展示今日已签到,不要本地再加分。
  • 积分明细调用 GET /api/profile/score-events
  • 积分最终余额以后端 score 和流水为准,前端只做展示。
  • 如响应包含 autoBadges,可展示获得勋章弹层;不要在前端根据连续天数或积分自行判断勋章是否解锁。

积分活动任务和积分兑换入口:

GET  /api/profile/activity-tasks
POST /api/profile/activity-tasks/claim
GET  /api/profile/exchange-items
POST /api/profile/exchange-items/redeem

POST /api/profile/activity-tasks/claim 示例:

{
  "taskId": "<taskId>",
  "sourceId": "<practiceSessionId 或 vocabularyReviewId可选>",
  "idempotencyKey": "activity-claim-20260629-001"
}

POST /api/profile/exchange-items/redeem 示例:

{
  "itemId": "<exchangeItemId>",
  "quantity": 1,
  "idempotencyKey": "exchange-20260629-001"
}

前端处理规则:

  • 积分任务领取、积分扣减、库存扣减、个人限购、优惠券兑换履约都由后端事务处理。
  • practice_completemock_exam_submitvocabulary_review 等任务需要传 sourceId,后端会校验证据属于当前学生当前租户。
  • feedback_resolved 等系统流程任务禁止学生自领,前端应展示为“系统发放”或不展示领取按钮。
  • 兑换返回 order 和最新积分余额后,再刷新 profile/mescore-events 和兑换列表;不要在本地先扣积分。
  • 如果响应包含 autoBadges,按获得勋章提示处理;重复领取、库存不足、积分不足、限购等错误按后端 code 展示业务提示。

当前 Taro 第一版已经把积分任务、积分兑换和积分明细嵌入 apps/taro/src/pages/student/profile/index.tsx。个人中心只对 manual 且仍有领取次数的任务展示直接领取按钮;练习、单词、模考等需要证据的任务只展示状态,由对应学习流程携带后端 sourceId 触发,避免前端伪造学习完成证据。兑换商品会先按后端返回的库存、限购和当前积分做按钮状态提示,但最终仍以后端事务校验为准。

租户后台积分任务/兑换操作台:

GET /api/tenant-admin/point-activity-tasks?status=active
PUT /api/tenant-admin/point-activity-tasks
GET /api/tenant-admin/point-activity-claims?taskId=<task-id>&userId=<student-id>
GET /api/tenant-admin/point-exchange-items?status=active
PUT /api/tenant-admin/point-exchange-items
GET /api/tenant-admin/point-exchange-orders?itemId=<item-id>&status=pending_fulfillment

当前 Taro 第一版已经把租户积分运营嵌入 apps/taro/src/pages/tenant-admin/marketing/index.tsx,支持任务配置、兑换商品配置、任务领取记录和兑换订单查看。前端只提交配置和筛选条件,权限、优惠券/资源归属、库存、限购、积分扣减和审计仍由后端处理。

题目页、资料页或视频页可提交反馈:

{
  "questionId": "...",
  "type": "question_error",
  "category": "answer",
  "title": "题目解析有误",
  "description": "请填写具体问题",
  "attachments": []
}

前端处理规则:

  • questionId 如存在,后端会校验题目必须属于当前租户。
  • 反馈状态由租户后台处理,学生可用 GET /api/profile/feedbacks 查看自己的反馈历史。
  • 租户后台处理反馈时,奖励积分由后端 idempotency_key 保证不会重复发放,前端不要重复叠加;状态处理响应可能返回 autoBadges。反馈状态、反馈奖励和自动勋章提醒会由后端写入学生站内通知,前端消息中心只负责展示和标记状态。

用户站内通知

站内通知用于承接反馈处理、反馈奖励、勋章发放、积分兑换完成或待履约等学生可见事件。它不是权限来源,也不替代订单、权益、积分或勋章接口的真实状态。

学生端消息中心:

GET /api/profile/notifications?status=unread&limit=50
GET /api/profile/notifications?notificationType=badge_granted
POST /api/profile/notifications/status

当前 Taro 第一版已经把学生消息中心嵌入 apps/taro/src/pages/student/profile/index.tsx:展示未读/已处理统计,支持全部、未读、已读、归档筛选,并允许学生把自己的通知标记为已读或归档。后续如果拆成独立消息中心页面,仍复用同一组接口,不要在前端根据通知 metadata 推导权益、积分或订单状态。

状态更新请求:

{
  "notificationIds": ["<notification-id>"],
  "status": "read | dismissed | archived"
}

返回项包含:

{
  "items": [
    {
      "id": "<uuid>",
      "notificationType": "badge_granted",
      "status": "unread",
      "severity": "success",
      "title": "获得新勋章",
      "message": "你获得了「连续签到」勋章。",
      "actionLabel": "查看勋章",
      "actionPath": "/student/profile?tab=badges",
      "sourceType": "badges",
      "sourceId": "<uuid>",
      "metadata": {},
      "readAt": null,
      "createdAt": "2026-06-29T00:00:00.000Z"
    }
  ],
  "summary": {
    "unread": 1,
    "read": 0,
    "dismissed": 0,
    "archived": 0
  }
}

租户后台通知查看:

GET /api/tenant-admin/user-notifications?status=unread&limit=100
GET /api/tenant-admin/user-notifications?userId=<student-id>&notificationType=point_exchange_pending_fulfillment
  • 租户后台需要 notifications:read 权限。
  • 后台只能查看租户内用户通知,不代学生改已读状态。
  • 前端可以按 actionPath 做站内跳转,但跳转后的页面仍要重新请求对应业务接口,不能信任通知 metadata 作为最终业务数据。
  • 当前支持的通知类型包括 feedback_status_updatedfeedback_reward_grantedbadge_grantedpoint_exchange_completedpoint_exchange_pending_fulfillment
  • 当前 Taro 第一版已经把租户用户通知查看嵌入 apps/taro/src/pages/tenant-admin/marketing/index.tsx,支持按状态、类型和学生用户 ID 筛选。该页面只读展示,不提供代学生标记已读、归档或删除的操作。

题库新模型接入方式

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

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=... 返回的复检结果,再刷新内容列表、分数线列表或题目视频列表。

当前 apps/taro/src/pages/tenant-admin/content/index.tsx 已接第一版 H5 操作台:可切换导入类型和 JSON/CSV/Excel 格式,选择本地文件或粘贴内容,预览/下载模板,编辑本次字段别名,调用后端 preview再同步执行或传 executionMode=async 入队;选中任务后调用 /api/tenant-content/imports/detail 查看 worker 状态、item 状态统计、最近 issue 和复检结果,并会对 pending/importing 异步任务自动轮询。下一步继续补目标入口/集合选择的完整表单。

fieldMappingOverrides 只影响 CSV/Excel 表头归一化,不改变 JSON 导入 schema。后端会按导入类型校验目标字段白名单并拒绝危险对象键前端可以用它提高旧表格兼容性但不能用它制造新业务字段或绕过后端校验。

CSV 请求示例:

{
  "sourceFormat": "csv",
  "sourceName": "questions.csv",
  "csvText": "legacyId,题型,题干,选项A,选项B,答案\nq1,choice,题干,A,B,B",
  "fieldMappingOverrides": {
    "content": ["自定义题干"],
    "answer": ["正确项"]
  },
  "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 表拼导出文件。当前后端支持三种交付形态:

  • 同步结构化导出:jsonpaper_jsonprint_payload,接口直接返回 base64 JSON/payload。
  • 异步二进制导出:pdfdocx,接口先返回 pending jobapps/worker --job exports 渲染 PDF/Word、水印并写入 content_assets,前端轮询 job 后再走资源签名下载或预览。
  • 异步运营素材包:daily_practice_zip,仅支持 exportType=daily_practiceworker 会生成九宫格 PNG/SVG 卡片、拼图 PNG/SVG、manifest.json 和脱敏后的 payload.json,并作为 asset_type=package 写入 content_assets

可用接口:

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": "仅供内部使用"
  }
}

PDF/Word 异步导出:

{
  "scopeType": "collection",
  "scopeId": "<questionCollectionId>",
  "format": "pdf",
  "exportType": "paper",
  "includeAnswers": false,
  "includeExplanations": false,
  "options": {
    "title": "天津专升本模拟试卷",
    "watermarkText": "仅供内部使用",
    "publishToAssets": true,
    "assetVisibility": "tenant"
  }
}

format 可为 pdfdocxpublishToAssets=true 表示导出文件可作为资料资源展示给对应可见范围用户;不传时默认生成后台私有资源,仅后台可下载。assetVisibility 支持 publictenantmemberssvipprivate,生产默认建议用 tenant/private/svip,不要轻易公开带题目的文件。

每日一练运营素材导出 PDF/Word

{
  "scopeType": "collection",
  "scopeId": "<questionCollectionId>",
  "format": "pdf",
  "exportType": "daily_practice",
  "includeAnswers": true,
  "includeExplanations": false,
  "limit": 8,
  "options": {
    "title": "每日一练 第 1 期",
    "issue": "每日一练 第 1 期",
    "date": "2026-06-29",
    "theme": "ink",
    "cardFormat": "1:1",
    "watermarkText": "恭学教育",
    "publishToAssets": true,
    "assetVisibility": "tenant",
    "brand": {
      "name": "恭学教育",
      "english": "GONGXUE EDU",
      "slogan": "专注高职升本",
      "ctaLine": "每日一练 · 精选八题 · 稳步上岸"
    },
    "centerSlot": {
      "type": "cta",
      "title": "恭学教育",
      "subtitle": "每日一练 · 稳步上岸"
    }
  }
}

每日一练图片 ZIP 素材包:

{
  "scopeType": "collection",
  "scopeId": "<questionCollectionId>",
  "format": "daily_practice_zip",
  "exportType": "daily_practice",
  "includeAnswers": false,
  "includeExplanations": false,
  "limit": 8,
  "options": {
    "title": "每日一练 第 2 期",
    "issue": "每日一练 第 2 期",
    "date": "2026-06-29",
    "theme": "default",
    "cardFormat": "1:1",
    "publishToAssets": true,
    "assetVisibility": "tenant",
    "brand": {
      "name": "恭学教育",
      "english": "GONGXUE EDU",
      "slogan": "专注高职升本",
      "ctaLine": "每日一练 · 精选八题 · 稳步上岸"
    }
  }
}

daily_practice 会自动限制最多 8 道题,并生成第 5 格中央品牌/CTA 位。同步 json 导出会返回 export.dailyPractice.slots,适合前端做九宫格预览;异步 pdf/docx 会由 worker 生成运营素材文件并进入 content_assets;异步 daily_practice_zip 会生成运营图片包,适合租户后台“一键下载每日一练素材”。

daily_practice_zip 产物结构:

manifest.json          导出任务、品牌、主题、文件清单和脱敏策略
payload.json           后端导出 payload遵守 includeAnswers/includeExplanations
collage.png            3x3 九宫格拼图
collage.svg            拼图 SVG 源文件
cards/card-01.png      单张卡片 PNG
cards/card-01.svg      单张卡片 SVG 源文件
...
cards/card-09.png
cards/card-09.svg

前端只需要轮询导出 job 并下载 ZIP不需要在 Taro 端重写图片渲染。后台可读取 manifest.json.files.cards 做下载后预览;若导出时 includeAnswers=falsepayload.json 和卡片都不会包含答案/解析。

响应关键结构:

{
  "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"
    }
  }
}

异步二进制导出初始响应:

{
  "job": {
    "id": "...",
    "status": "pending",
    "questionCount": 20,
    "outputHash": "..."
  },
  "export": null
}

worker 完成后,GET /api/tenant-content/exports/jobs 返回:

{
  "items": [
    {
      "id": "...",
      "format": "daily_practice_zip",
      "status": "completed",
      "assetId": "...",
      "attemptCount": 1,
      "outputMetadata": {
        "delivery": "content_asset",
        "fileName": "每日一练-第-2-期.zip",
        "mimeType": "application/zip",
        "sizeBytes": 123456,
        "checksumSha256": "...",
        "assetId": "..."
      }
    }
  ]
}

前端处理规则:

  • 导出按钮只给具备租户内容编辑权限的后台成员展示;接口仍以后端 TENANT_CONTENT_EDITOR_REQUIRED 为准。
  • 下载 JSON 时使用 files[0].contentBase64 生成 Blob文件名使用后端返回的 filename
  • includeAnswers=false 时,顶层答案字段和阅读理解/案例分析的子题答案都会被后端脱敏;前端不要在本地重新合并答案。
  • includeExplanations=false 时,不展示解析,也不要从题目详情接口额外补解析。
  • paper_json 可用于后台试卷预览和打印;pdf/docx 用于正式文件下载、资料发布和带水印留档;daily_practice_zip 用于每日一练运营图片包。
  • GET /api/tenant-content/exports/jobs?scopeType=collection&scopeId=... 用于后台导出历史;结构化导出只记录 metadata 和输出 hash二进制导出记录 assetId、文件名、大小和 checksum。
  • status=pending/rendering 时展示生成中;completed 且存在 assetId 后,后台可调用 POST /api/tenant-content/assets/sign-download 下载PDF/图片资源也可调用 POST /api/tenant-content/assets/sign-preview 预览。ZIP 包通常直接下载,不做 inline 预览。
  • 学生资料页只展示 content_assets.status=active 且非 private 的资源;后台私有导出不会出现在学生资料列表。
  • 跨租户导出会返回 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=...
POST /api/tenant-content/public-question-banks/conflicts/resolve
POST /api/tenant-content/public-question-banks/conflicts/resolve-batch
GET /api/tenant-content/notifications
POST /api/tenant-content/notifications/status

平台公共题库运营台使用:

GET /api/platform-admin/question-bank-sync-status
GET /api/platform-admin/question-bank-sync-status?tenantId=<tenantId>&syncStatus=failed&onlyOpenIssues=true
GET /api/platform-admin/question-bank-sync-status?sourceQuestionBankId=<questionBankId>&limit=100

该接口需要 platform:question_bank:ops 权限。它用于平台超级管理员查看各租户采纳公共题库后的同步状态、失败/冲突通知数量和 worker 最近执行摘要;不返回题目正文、答案、解析或对象存储签名。

采纳请求:

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

前端处理规则:

  • 租户只能看到后端判定为已授权的公共题库,不要在前端用套餐码自行过滤。后端会同时校验 question_bank_grants、有效 tenant_subscriptionsplatform_saas_plans.feature_flags.publicQuestionBanks 和订阅 metadata.publicQuestionBankAccess;基础版默认只允许订阅 metadata 中 allowedRegionIds 覆盖的单地区公共题库,专业版默认 national 可见全国地区题库。列表响应中的 accessPlanCodeaccessMode 仅用于展示“由哪个 SaaS 套餐授予访问”,不能作为前端权限来源。
  • 采纳成功后后端会生成本租户自己的 questionBankIdentryIdcollectionId 和题目快照,学生端直接按普通 /api/catalog/content-entriesquestion-collectionspractice-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=conflictitem.syncStatus=failed:展示冲突数量和冲突题目,不要把它当系统异常。冲突表示租户已经改过这道采纳题,后端已跳过并保留租户内容。
  • action=conflict 的记录可以进入冲突处理区域:展示平台源题 ID、租户目标题 ID、上次平台 hash、当前平台 hash、租户当前 hash。当前后端已支持单条和批量处理。
  • 页面初始化或 worker 后台同步完成后,可以调用 GET /api/tenant-content/public-question-banks/conflicts?adoptionId=... 查询最近一次同步状态、countsconflicts。这个接口只返回当前租户自己的采纳记录,跨租户会返回 QUESTION_BANK_ADOPTION_NOT_FOUND
  • 单条冲突处理调用 POST /api/tenant-content/public-question-banks/conflicts/resolvebody 为 { "adoptionId": "...", "sourceQuestionId": "...", "resolution": "accept_platform | keep_local" }accept_platform 会把租户副本写成平台当前版本并生成新题目版本;keep_local 会记录本地保留决策,同一平台 hash 和本地 hash 后续同步不再反复提示。两种操作都会写审计日志。
  • 批量冲突处理调用 POST /api/tenant-content/public-question-banks/conflicts/resolve-batchbody 为 { "adoptionId": "...", "sourceQuestionIds": ["..."], "resolution": "accept_platform | keep_local", "limit": 50 }。后端最多处理 100 条,仍会重新校验租户授权、锁定采纳记录和目标题,逐条写审计;前端只提交当前冲突列表中明确展示给操作者的 source id。
  • 同步产生新增/更新时,后端会写入 public_question_bank_synced 通知;同步产生冲突时,会写入 public_question_bank_conflict 通知。通知只包含同步摘要、题库 ID、题目 hash 和操作入口,不保存题目答案或解析。
  • worker 自动同步因授权失效、目标题库缺失等原因失败时,后端会写入 public_question_bank_sync_failed 通知,severity=errormetadata 只包含 errorCode/errorMessage/workerId/failedAt 等脱敏运维摘要。后续同步恢复成功后,后端会自动把未 dismissed 的失败通知标记为 resolved
  • 租户后台可调用 GET /api/tenant-content/notifications?notificationType=public_question_bank_conflict&status=unread&limit=20 展示待处理同步消息;也可用 public_question_bank_sync_failed 展示自动同步失败消息,并带 adoptionId 查看某个采纳记录的通知。
  • 通知状态更新调用 POST /api/tenant-content/notifications/statusbody 为 { "notificationIds": ["..."], "status": "read | dismissed | resolved" }。冲突被单条或批量全部处理后,后端会自动把相关冲突通知标记为 resolved
  • QUESTION_BANK_GRANT_NOT_AVAILABLE:说明 SaaS 套餐/授权已失效,提示联系平台或升级套餐。
  • QUESTION_BANK_ADOPTION_NOT_FOUND:说明不是当前租户的采纳记录或记录已归档,前端不要跨租户重试。
  • 当前租户后台可以提供手动“同步平台更新”按钮,并展示 worker 自动同步后的通知、失败消息、冲突查询结果、单条处理和批量处理按钮。平台后台可用 question-bank-sync-status 做跨租户运营看板优先展示失败、pending 和开放冲突。

登录对接

短信登录

开发环境可以先使用 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_tokenopenid,再拉取用户资料;如果返回 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/AppKeyaccess_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 对同一用户同一优惠券的未核销记录是幂等的;如果优惠券允许 perUserLimit > 1,前一次 redemption 已经下单核销后,用户可以再次领取直到达到限额。已超过单用户限额会返回 COUPON_ALREADY_USEDCOUPON_USER_LIMIT_REACHED。下单时后端会重新计算套餐原价、优惠金额和最终应付,前端展示金额只能使用接口返回的 originalAmountCentsdiscountCentsamountCents

优惠券规则由后端执行,前端只做展示和提示:

COUPON_DISABLED                   优惠券已停用或归档
COUPON_NOT_STARTED                未到开始时间
COUPON_EXPIRED                    已过期
COUPON_QUOTA_EXHAUSTED            总库存已用完
COUPON_PLAN_MISMATCH              不适用当前套餐
COUPON_REGION_MISMATCH            不适用当前地区
COUPON_MIN_ORDER_AMOUNT_NOT_MET   未达到最低订单金额
COUPON_FIRST_ORDER_ONLY           仅限首单
COUPON_USER_LIMIT_REACHED         已达到单用户可用次数

租户后台优惠券配置字段:

{
  "code": "SUMMER80",
  "planId": "<默认绑定套餐,可选>",
  "discountType": "fixed | percent",
  "discountValue": 800,
  "status": "active | disabled | archived",
  "campaignName": "暑期活动",
  "minOrderAmountCents": 3000,
  "maxDiscountCents": 1000,
  "perUserLimit": 2,
  "firstOrderOnly": false,
  "allowedPlanIds": ["<svipPlanId>"],
  "allowedRegionIds": ["<regionId>"],
  "maxUses": 500,
  "metadata": {
    "channel": "poster"
  }
}

租户后台核销和报表:

GET /api/tenant-admin/coupons?status=active&campaignName=暑期活动
GET /api/tenant-admin/coupons/redemptions?couponId=<couponId>&status=used
GET /api/tenant-admin/coupons/report?startDate=2026-06-01&endDate=2026-06-29&campaignName=暑期活动
权限:`coupons:read` 可查看配置,`coupons:write` 可维护配置,核销明细和报表需要 `coupons:redemptions:read`

coupons/report 返回 claimCount/usedCount/discountCents/paidAmountCents/conversionRate/byCoupon/byCampaign/daily。金额均为分,报表只读;前端不要用报表数据反向修改订单、支付、权益或优惠券使用次数。

如果优惠后 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 已接入:支付漏通知和处理中退款会由后端定时查询供应商并幂等落账。资金对账已支持租户后台手工/API 导入供应商账单、微信/支付宝官方账单下载任务、查询差异、差错工单处理、异常订单运营台、人工调整凭证和复核报表。生产联调时仍需保留人工确认/失败登记入口。

租户后台资金对账

资金对账是租户后台/财务运营能力,学生端不要接。对账接口只生成差异台账、差错工单和审计,不会自动修改订单、支付、退款或权益。前端不能根据对账结果或工单状态自行开通、退款或撤销权益。

预览账单:

POST /api/commerce/reconciliation/preview
权限tenant:reconciliation:read
body: {
  "provider": "wechat_pay | alipay | manual",
  "billDate": "2026-06-29",
  "billType": "payment | refund | combined",
  "sourceName": "wechat-bill-20260629.csv",
  "rows": [
    {
      "transactionType": "payment",
      "orderNo": "<本地 orderNo 或 out_trade_no>",
      "providerTradeNo": "<微信/支付宝交易号>",
      "amountCents": 990,
      "providerStatus": "SUCCESS"
    },
    {
      "transactionType": "refund",
      "orderNo": "<orderNo>",
      "refundNo": "<本地 refundNo 或 out_refund_no>",
      "providerRefundNo": "<支付平台退款单号>",
      "refundAmountCents": 100,
      "providerStatus": "REFUND_SUCCESS"
    }
  ],
  "previewLimit": 200
}

确认导入:

POST /api/commerce/reconciliation/import
权限tenant:reconciliation:write

查询批次、明细和异常:

GET /api/commerce/reconciliation/batches?provider=wechat_pay&billDate=2026-06-29
GET /api/commerce/reconciliation/items?batchId=<batchId>&matchStatus=missing_provider
GET /api/commerce/reconciliation/anomalies?provider=wechat_pay

从异常明细创建差错工单:

POST /api/commerce/reconciliation/issues/create
权限tenant:reconciliation:write
body: {
  "itemId": "<reconciliationItemId>",
  "assignedTo": "<tenantStaffUserId可选>",
  "dueAt": "2026-06-30T10:00:00.000Z",
  "summary": "微信账单金额不一致核对",
  "note": "先交给财务核对供应商流水",
  "metadata": {
    "source": "tenant-admin"
  }
}

重复对同一未关闭异常明细创建工单时,后端会返回原工单并带 idempotent=truematched/ignored 明细不可创建工单。

查询工单:

GET /api/commerce/reconciliation/issues?status=open&assignedTo=<userId>&batchId=<batchId>&orderNo=<orderNo>
权限tenant:reconciliation:read

工单状态流转:

POST /api/commerce/reconciliation/issues/status
权限tenant:reconciliation:write
body: {
  "issueId": "<issueId>",
  "action": "start | assign | resolve | ignore | escalate | reopen",
  "assignedTo": "<userIdassign 时必填>",
  "resolutionType": "provider_confirmed | local_corrected | manual_adjustment | false_positive | duplicate | write_off",
  "note": "处理备注",
  "metadata": {
    "voucherNo": "ADJ-20260629-001"
  }
}

查看事件轨迹:

GET /api/commerce/reconciliation/issues/events?issueId=<issueId>
权限tenant:reconciliation:read

官方账单下载:

POST /api/commerce/reconciliation/provider-bills/request
权限tenant:reconciliation:download
body: {
  "provider": "wechat_pay | alipay",
  "billDate": "2026-06-29",
  "billType": "payment | refund | combined",
  "metadata": {
    "remark": "财务手动申请"
  }
}

返回:

{
  "item": {
    "id": "<jobId>",
    "provider": "wechat_pay",
    "billDate": "2026-06-29",
    "billType": "payment",
    "status": "queued",
    "sourceName": "provider-bill:wechat_pay:2026-06-29:payment",
    "sourceHash": null,
    "rowCount": 0,
    "downloadUrlHost": null,
    "reconciliationBatchId": null
  },
  "idempotent": false
}

轮询任务:

GET /api/commerce/reconciliation/provider-bills/jobs?provider=wechat_pay&billDate=2026-06-29
权限tenant:reconciliation:read

任务状态:

queued      已排队,等待 provider-bills worker
running     worker 正在申请和下载官方账单
completed   已下载、校验、导入对账批次
failed      下载、hash 校验、解析或导入失败
cancelled   已取消

前端处理规则:

  • 前端只创建任务和轮询状态,不直接请求微信/支付宝账单 URL。
  • 后端响应只会返回 downloadUrlHost,不会返回完整下载 URL、商户私钥、API v3 key、支付宝应用私钥。
  • completed 后用 reconciliationBatchId 跳转到对账批次明细。
  • failed 时展示 errorCode/errorMessage,让财务重新发起或联系技术处理。
  • 官方账单下载由服务器定时运行 apps/worker --job provider-bills,前端不要自行触发供应商接口。

工单状态:

open             新建待处理
investigating    处理中
escalated        已升级
resolved         已解决
ignored          已忽略

处理结论:

none                未处理
provider_confirmed  已按供应商确认
local_corrected     已通过专门业务命令修正本地记录
manual_adjustment   已登记人工调整凭证
false_positive      误报
duplicate           重复账单/重复工单
write_off           财务核销

matchStatus 取值:

matched             本地和供应商账单匹配
amount_mismatch     金额不一致
status_mismatch     状态不一致
missing_local       供应商账单有,本地没有
missing_provider    本地已支付/退款成功,供应商账单没有
duplicate           供应商账单重复行
ignored             无效行或不符合本次 billType

前端处理规则:

  • 财务导入页建议使用 preview -> 人工确认 -> import -> anomalies 的流程。
  • sourceHash 可作为同一文件内容的识别线索,但当前接口不会阻止重复导入;前端应展示最近同名/同 hash 批次提醒。
  • 金额统一是分,前端不要传元。
  • 对账差异和差错工单只是运营判断依据,resolve/ignore 不会落账。最终订单修正必须走退款、补偿、人工确认或后续专门的人工调整接口。
  • 当前后端支持 JSON 行手工导入;官方账单下载 worker 支持微信/支付宝账单 JSON/CSV/ZIP 解析,并复用同一套对账导入逻辑。真实生产接入时仍要用真实账单文件抽样验收字段映射。

异常订单运营台和调整凭证

租户后台财务/售后页可以用异常运营台作为入口。该页只展示待处理风险和凭证复核状态,不允许前端直接修改订单、支付、退款或权益。

异常运营台:

GET /api/commerce/operations/anomalies?provider=wechat_pay&limit=50
权限tenant:reconciliation:read

返回中 items[].type 可能是:

reconciliation_issue    未关闭对账差错工单
provider_bill_job       官方账单下载失败或运行超时
payment_event_error     支付/退款通知处理异常
stuck_pending_payment   长时间 pending 支付
stuck_refund            长时间待处理/处理中退款

创建人工调整凭证:

POST /api/commerce/adjustment-vouchers
权限tenant:reconciliation:write
body: {
  "voucherNo": "ADJ-20260629-001",
  "reconciliationIssueId": "<issueId>",
  "adjustmentType": "write_off",
  "direction": "decrease",
  "amountCents": 990,
  "title": "供应商缺失账单人工核销凭证",
  "description": "仅作为财务复核证据",
  "assetId": "<contentAssetId可选>",
  "externalUrl": "https://finance.example.com/proofs/ADJ-20260629-001",
  "metadata": {
    "operatorRemark": "后台上传凭证"
  }
}

可关联的来源字段:

reconciliationIssueId   对账差错工单
reconciliationItemId    对账明细
orderId / orderNo       订单
paymentId               支付记录
refundRequestId/refundNo 退款申请
sourceType=manual       纯人工凭证

凭证字段:

adjustmentType:
  manual_payment_confirm | refund_correction | provider_confirmed |
  local_corrected | write_off | duplicate | other

direction:
  increase | decrease | none

status:
  draft | submitted | approved | rejected | voided

查询凭证:

GET /api/commerce/adjustment-vouchers?status=submitted&orderNo=<orderNo>
权限tenant:reconciliation:read

复核凭证:

POST /api/commerce/adjustment-vouchers/status
权限tenant:reconciliation:review
body: {
  "voucherId": "<voucherId>",
  "status": "approved | rejected | voided",
  "reviewNote": "财务复核意见",
  "metadata": {
    "reviewChannel": "tenant-admin"
  }
}

查看凭证轨迹和报表:

GET /api/commerce/adjustment-vouchers/events?voucherId=<voucherId>
GET /api/commerce/adjustment-vouchers/report?startDate=2026-06-01&endDate=2026-06-29
权限tenant:reconciliation:read

前端处理规则:

  • 学生端不要接这些接口。
  • tenant:reconciliation:write 可创建凭证,tenant:reconciliation:review 才能审批、驳回或作废凭证。
  • 后端会校验凭证来源和附件 content_assets 必须属于当前租户。
  • approved/rejected/voided 的凭证不能翻转到其它关闭状态。
  • 凭证审批不会自动改订单、支付、退款和权益;真正落账仍要走退款状态机、支付补偿、手工支付确认或后续专门落账命令。
  • 前端可在差错工单 resolve 时把 metadata.voucherNo 一并传入,用于人读检索,但不要把它当成落账动作。

激活码预检查与兑换

兑换前建议先调用:

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/ai-school/index
  • SVIP AI 择校推荐、报告历史和 JSON 报告渲染。
  1. pages/profile/index
  • 会员、订单、激活码、学习数据、勋章。

当前 Taro 实现进度

截至 2026-06-29apps/taro 已完成学生端第一阶段页面:

pages/student/login/index
pages/student/home/index
pages/student/region/index
pages/student/catalog/index
pages/student/practice/index
pages/student/review/index
pages/student/reports/index
pages/student/video/index
pages/student/checkout/index
pages/student/order-detail/index
pages/student/vocabulary/index
pages/student/handbook/index
pages/student/scoreline/index
pages/student/ai-school/index
pages/student/assets/index
pages/student/profile/index

已新增服务层:

src/components/RichContent.tsx 安全富文本渲染:题干、选项、解析、知识手册、报告复盘
src/services/catalog.ts    目录、题库、单词、手册、分数线、资料
src/services/learning.ts   练习 session、答题、收藏、错题/收藏复习、交卷报告、单词复习和单词收藏列表
src/services/commerce.ts   套餐、优惠券、下单、支付参数、订单详情、权益、激活码
src/services/profile.ts    个人中心、地区目标、签到、反馈、勋章、倒计时、积分活动任务、积分兑换
src/services/video.ts      题目视频列表、播放签名
src/services/ai.ts         AI 择校推荐生成、报告列表、报告详情
src/services/pronunciation.ts H5/小程序单词发音适配
src/services/tenantAdmin.ts 租户后台看板、权限矩阵、成员、学生创建/批量导入/分班/备注/跟进、内容、营销、设置、角色模板写操作、公共题库采纳/同步/单条和批量冲突处理、导入详情/复检、CRM 配置/队列、分佣规则/成员比例/订单/结算、优惠券规则/核销报表、积分任务/兑换配置和记录
src/services/tenantFinance.ts 租户财务运营:退款状态机、官方账单任务、对账批次/明细、差错工单、异常订单和人工调整凭证
src/services/platformAdmin.ts 平台后台租户、租户详情、账务资料、平台审计查询/CSV 导出、平台审计告警规则/列表/状态更新、审计告警外部通知渠道/发送事件、套餐账单、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、逾期预览/内部催缴记录、催缴外部通知渠道/发送事件、用量、公共题库授权

验证命令:

npm run check:taro
npm run build:taro:h5:student
npm run build:taro:h5:tenant
npm run build:taro:h5:platform

已通过。构建仍有 Taro H5 入口体积 warning属于当前 Taro 工程既有警告,不阻断联调。

平台审计导出

平台审计属于超级管理员后台能力,学生端和租户后台不要接。前端只能调用后端命令式接口,不允许直接读 Supabase 表或拼 SQL 导出审计。

查询审计:

GET /api/platform-admin/audit-logs?tenantId=<tenantId>&action=platform.tenant&limit=100

导出审计:

GET /api/platform-admin/audit-logs/export?format=csv&limit=1000
GET /api/platform-admin/audit-logs/export?format=json&tenantId=<tenantId>&startDate=2026-06-01&endDate=2026-06-30

可用筛选:

tenantId
action       前缀匹配,例如 platform.tenant
targetType
actorUserId
q            匹配 action/targetType/targetId
startDate    YYYY-MM-DD
endDate      YYYY-MM-DD
limit        查询最大 500导出最大 5000
format       csv | json

导出响应:

{
  "item": {
    "filename": "platform-audit-20260630T120000Z.csv",
    "format": "csv",
    "mimeType": "text/csv",
    "rowCount": 25,
    "exportedAt": "2026-06-30T12:00:00.000Z",
    "sha256": "<content-sha256>",
    "sizeBytes": 4096,
    "contentBase64": "<base64>",
    "filters": {
      "tenantId": "<tenantId>",
      "action": "platform.tenant",
      "startDate": "2026-06-01",
      "endDate": "2026-06-30"
    }
  }
}

前端处理规则:

  • 只允许平台管理员页面调用,普通学生或租户成员会返回 PLATFORM_ADMIN_REQUIRED
  • 后端会把导出动作再次写入 platform.audit.exported,前端不需要额外记日志。
  • 导出内容中的 details 会递归脱敏 token、secret、password、key、authorization、cookie、session、cert、signature 等敏感字段。
  • H5 可用 contentBase64 生成 data:<mimeType>;base64,... 下载;下载后可以显示 sha256 方便人工校验。
  • 小程序端如果无法稳定写入文件系统,建议展示“已生成,请到 H5 平台后台下载”,不要把完整 base64 长文本复制到剪贴板。
  • 平台后台不要把 contentBase64 持久存入本地缓存、日志或埋点。

平台审计告警

平台审计告警属于超级管理员后台能力。内部告警由 apps/worker --job platform-audit-alerts 根据 platform_audit_alert_rules 从平台审计日志生成,外部通知由 apps/worker --job platform-audit-notifications 根据 platform_audit_notification_channels 入队并发送到 generic、钉钉、飞书或企业微信 webhook。前端不要直接读写 Supabase 表。

平台后台权限

平台后台不再假设所有 platform_admin 都是全权限。Taro 平台后台启动后应先调用:

GET /api/platform-admin/permissions

响应结构:

{
  "item": {
    "primaryRole": "platform_admin",
    "permissions": {
      "platform:tenant:read": true,
      "platform:billing:read": true
    },
    "effective": {
      "platform:tenant:read": true,
      "platform:tenant:write": false
    },
    "catalog": [
      {
        "key": "platform:tenant:read",
        "group": "tenant",
        "label": "查看租户"
      }
    ]
  }
}

前端处理规则:

  • effective 只用于隐藏菜单、按钮和表单,不是安全边界。
  • 真实权限由后端在每个 /api/platform-admin/* 接口强制校验;越权会返回 PLATFORM_PERMISSION_REQUIRED
  • 平台菜单推荐映射:租户中心用 platform:tenant:read,创建租户用 platform:tenant:write,状态变更用 platform:tenant:status,账务资料用 platform:tenant:billing_profile,账务中心用 platform:billing:read/write/payment/dunning,审计中心用 platform:audit:read/export/alert/notification,公共题库用 platform:question_bank:read/grant
  • permissions 里可能出现 {"*":true}platform:billing:* 这类通配权限,前端只消费后端返回的 effective,不要自己重新实现权限匹配规则。
  • 平台后台 H5 不允许保存 x-platform-admin-key;生产必须使用 Supabase Auth access token。

查询启用规则:

GET /api/platform-admin/audit-alert-rules?enabled=true

查询开放告警:

GET /api/platform-admin/audit-alerts?status=open&limit=50

可用筛选:

tenantId=<tenant uuid>
status=open|acknowledged|resolved|ignored
severity=low|medium|high|critical
ruleCode=platform_tenant_status_changed
q=<keyword>
limit=1..500

更新告警状态:

POST /api/platform-admin/audit-alerts/status
Content-Type: application/json

{
  "alertId": "<alert uuid>",
  "status": "acknowledged",
  "resolutionNote": "已人工确认"
}

status 只允许 acknowledgedresolvedignored。前端不能通过这个接口把告警改回 open;需要重新打开时后续另设计复核接口,避免随意撤销安全处理痕迹。

安全边界:

  • 只允许平台管理员调用,普通学生、租户管理员和租户成员会返回 PLATFORM_ADMIN_REQUIRED
  • 后端会对告警 details 递归脱敏 token、secret、password、key、authorization、cookie、session、cert、signature 等敏感字段;前端仍不要把 details 原样写入日志或埋点。
  • 确认、解决和忽略都会写入 platform.audit.alert_status_updated 审计。
  • 当前工作台展示开放告警并提供“确认/解决”按钮,同时展示已启用外部通知渠道和最近发送事件摘要;更完整筛选、批量处理和升级策略属于下一阶段。

平台审计告警外部通知

查询通知渠道:

GET /api/platform-admin/audit-notification-channels?enabled=true&limit=50

渠道响应不会返回原始 webhookUrl,只返回安全摘要:

{
  "items": [
    {
      "id": "...",
      "channelCode": "security_ops",
      "name": "安全值班群",
      "enabled": true,
      "provider": "wecom",
      "secretRef": "app_private.platform_secrets:webhook:security_ops",
      "minSeverity": "high",
      "statusFilter": ["open"],
      "actionPatterns": ["platform.*"],
      "webhook": {
        "protocol": "https",
        "host": "qyapi.weixin.qq.com",
        "pathname": "/cgi-bin/webhook/send"
      }
    }
  ]
}

保存通知渠道:

PUT /api/platform-admin/audit-notification-channels
Content-Type: application/json

{
  "channelCode": "security_ops",
  "name": "安全值班群",
  "provider": "wecom",
  "webhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...",
  "secret": "optional-signing-secret",
  "minSeverity": "high",
  "statusFilter": ["open"],
  "actionPatterns": ["platform.*"],
  "timeoutSec": 10
}

查询发送事件:

GET /api/platform-admin/audit-notification-events?status=failed&limit=50

前端安全边界:

  • secret 只允许在保存渠道时一次性提交给后端,后端写入 app_private.platform_secrets;页面、日志、状态管理和本地缓存都不能保存原文。
  • API 响应只展示 secretRef、webhook host/path、发送状态、HTTP code 和脱敏后的 request payload。前端不要自行拼 webhook 请求。
  • 生产环境 readiness 会阻断启用的非 HTTPS webhook、localhost webhook以及缺少平台私密 secret 的钉钉/飞书签名渠道。
  • 小程序端平台后台如果后续开放,只建议做查看和告警处理;通知渠道配置建议优先放在 H5 平台后台完成。

平台账务逾期催缴

平台后台账务页只调用命令式 API不直接改 tenant_invoices.statustenants.billing_statustenant_invoice_reminders。后端会做平台管理员鉴权、行锁、每日催缴去重和审计。

预览逾期账单:

POST /api/platform-admin/invoices/process-overdue
{
  "dryRun": true,
  "limit": 100
}

执行内部催缴:

{
  "dryRun": false,
  "channel": "internal",
  "limit": 100
}

响应核心字段:

{
  "item": {
    "dryRun": false,
    "processed": 3,
    "markedOverdue": 2,
    "reminderCreated": 3,
    "skippedReminder": 0
  }
}

查询催缴记录:

GET /api/platform-admin/invoices/reminders?tenantId=<tenantId>&invoiceId=<invoiceId>&limit=100

前端展示建议:

  • dry-run 只展示“将标记逾期/将生成提醒”,不要写本地状态。
  • 执行成功后重新加载 /api/platform-admin/invoices/api/platform-admin/invoices/reminders
  • platform-dunning worker 可每天在 platform-billing 之后运行一次;它不会自动停用租户,停用仍走平台管理员状态变更流程。

平台账务催缴外部通知

平台催缴外部通知用于把 tenant_invoice_reminders 中的内部催缴记录推送到平台自己的值班群、财务群或运营 webhook。它属于平台超级管理员能力前端只能调用后端 API 查看和配置渠道,不要直接写 Supabase 表,也不要在页面状态、日志、埋点或本地缓存保存 webhook secret。

查询催缴通知渠道:

GET /api/platform-admin/dunning-notification-channels?enabled=true&limit=50

渠道响应不会返回原始 webhookUrl,只返回安全摘要:

{
  "items": [
    {
      "id": "...",
      "channelCode": "finance_ops",
      "name": "平台财务催缴群",
      "enabled": true,
      "provider": "wecom",
      "secretRef": "app_private.platform_secrets:webhook:platform_dunning_finance_ops",
      "reminderTypes": ["overdue", "final_notice"],
      "reminderChannels": ["internal"],
      "minReminderLevel": 1,
      "tenantIds": [],
      "webhook": {
        "protocol": "https",
        "host": "qyapi.weixin.qq.com",
        "pathname": "/cgi-bin/webhook/send"
      }
    }
  ]
}

保存催缴通知渠道:

PUT /api/platform-admin/dunning-notification-channels
Content-Type: application/json

{
  "channelCode": "finance_ops",
  "name": "平台财务催缴群",
  "provider": "wecom",
  "webhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...",
  "secret": "optional-signing-secret",
  "reminderTypes": ["overdue", "final_notice"],
  "reminderChannels": ["internal"],
  "minReminderLevel": 1,
  "tenantIds": [],
  "timeoutSec": 10
}

查询发送事件:

GET /api/platform-admin/dunning-notification-events?status=failed&limit=50
GET /api/platform-admin/dunning-notification-events?invoiceId=<invoiceId>&limit=50

前端展示建议:

  • 工作台可展示启用渠道数、最近发送事件、失败数和待重试数;当前 apps/taro/src/pages/platform-admin/workbench 已接摘要第一版。
  • 账务中心后续可在催缴记录旁展示外部通知状态,但不要把事件状态当作账单真实付款状态。
  • secret 只在保存渠道时提交一次;后端写入 app_private.platform_secrets,响应只回显 secretRef
  • API 会对事件 requestPayload 递归脱敏worker 会对联系人电话、邮箱做掩码。前端仍不要把 payload 原样写入日志。
  • 生产 readiness 会阻断启用的非 HTTPS/localhost webhook钉钉/飞书签名渠道必须有平台私密 secret。
  • platform-dunning-notifications worker 适合在 platform-dunning 后每 5 到 15 分钟运行一次;发送成功后会把该催缴记录标记为 sent,发送失败会按退避策略重试并在终止失败后标记 failed

下一批前端开发重点:

  • 学生端:地区选择、题目视频播放、题目反馈、错题/收藏专题页、模考交卷报告、收银台、订单详情、售后入口、题干/解析/知识手册 RichContent 安全渲染、逐题复盘、背单词卡片学习/发音/收藏练习、资料短签名水印预览/下载确认、个人中心消息中心、积分任务、积分兑换和积分明细第一版已接;下一批继续补独立消息中心增强、真正 KaTeX/小程序公式方案、私有题图签名资源映射、背单词更细统计、小程序支付容器和分享场景。
  • 租户后台:工作台已接权限驱动模块入口;学生运营页已接学生创建/更新、禁用/恢复、批量导入、批量分班、学生备注、跟进任务和完成跟进第一版;题库内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、模板预览/下载、异步任务轮询和导入后复检第一版;营销中心已接 CRM 配置保存、CRM 队列按状态查看、分佣默认规则、成员分佣比例、分佣订单明细、结算单生成、审核通过/驳回、标记线下打款、优惠券规则表单、筛选、核销明细、核销报表、积分任务/兑换操作台和用户通知查看第一版;财务运营页已接退款申请/审核/供应商提交与查询、官方账单任务、对账批次/异常明细、差错工单处理、人工调整凭证提交/复核和异常订单运营台第一版;租户设置页已接主题模板、草稿预览、发布、角色模板创建、编辑、停用、权限点、菜单、模块、字段、基础数据范围、成员搜索/新建、成员绑定模板、成员状态和额外权限覆盖第一版;下一批继续补更精细的学生导入模板体验、真实生产账单抽样验收、真实打款 provider、发票、更细数据范围 UI 和主题素材库。
  • 平台后台:租户创建、租户详情、状态变更、账务资料维护、最近平台审计查询/CSV 导出、开放审计告警展示/确认/解决、审计告警外部通知渠道/事件状态摘要、催缴外部通知渠道/事件状态摘要、订阅开通、账单生成、订阅账单候选预览、dry-run、批量生成、自动计费生成结果查看、人工收款确认、逾期预览、内部催缴生成、催缴记录查看、用量录入、公共题库授权编辑已接第一版后端会跳过已开票订阅并记录 platform.invoice.subscription_batch_created 审计,platform-billing worker 会自动生成即将到期订阅账单并记录 platform.invoice.subscription_auto_created 审计,platform-dunning worker 会标记已过期未结清服务费账单、生成 tenant_invoice_reminders 并记录 platform.invoice.overdue_processed 审计,platform-dunning-notifications worker 会把内部催缴记录按渠道发送外部通知,platform-audit-alerts worker 会把高风险平台审计动作转换为内部告警,platform-audit-notifications worker 会把开放告警按渠道发送外部通知;前端只展示候选、预览结果、跳过结果、逾期处理结果、开放告警、通知事件和生成后的账单/审计,不要直接更新账单状态、租户 billing_status、告警表或通知事件表;继续补租户基础资料编辑增强、平台审计告警升级策略、平台催缴通知配置操作台细节和平台在线收款。
  • 小程序:验证 Taro.login、微信支付、分享 scene/referral、Supabase client 兼容性;如不稳定,保留 apps/api/auth/* 作为小程序登录适配层。

AI 择校推荐接入

AI 择校推荐是学生端 SVIP 功能,前端只调用 apps/api,不要在 H5/小程序内保存任何 AI provider key、prompt secret 或服务端模型配置。当前后端默认使用 deterministic local_rules provider基于学生目标地区和 scoreline_records 生成稳定 JSON 报告;后续真实 AI provider 仍保持同一接口和 JSON schema。

生成报告:

POST /api/ai/school-recommendations/generate

请求体:

{
  "regionId": "<regionId>",
  "estimatedScore": 210,
  "riskPreference": "balanced",
  "constraints": "优先考虑计算机相关专业",
  "recommendationLimit": 5
}

riskPreference 支持 safebalancedsprintregionId 不传时后端会使用 /api/profile/me 中学生档案的目标地区。后端会校验该地区属于当前租户,并要求当前学生有有效 SVIP无权限时返回 AI_SVIP_REQUIRED

响应核心结构:

{
  "item": {
    "id": "<reportId>",
    "status": "generated",
    "provider": "local_rules",
    "model": "local-scoreline-rules-v1",
    "promptVersion": "school-recommendation-v1",
    "resultPayload": {
      "schemaVersion": "school-recommendation-report-v1",
      "summary": "基于当前地区历年分数线...",
      "riskLevel": "balanced",
      "recommendedSchools": [
        {
          "schoolId": "<schoolId>",
          "schoolName": "烟测学院",
          "majorId": "<majorId>",
          "majorName": "计算机科学与技术",
          "latestYear": 2026,
          "latestScore": 188,
          "scoreGap": 22,
          "riskLevel": "safe",
          "confidence": 0.9,
          "reason": "预估分与最新参考线差值约 22 分...",
          "scorelineTrend": {
            "years": [2026],
            "scores": [188],
            "direction": "unknown"
          },
          "tags": ["稳妥", "趋势不足", "样本较少"]
        }
      ],
      "actionPlan": [],
      "disclaimers": [],
      "dataCoverage": {}
    }
  }
}

报告历史:

GET /api/ai/school-recommendations?regionId=<regionId>&limit=20
GET /api/ai/school-recommendations/detail?reportId=<reportId>

前端渲染建议:

  • resultPayload.schemaVersion 必须等于 school-recommendation-report-v1,未知版本先降级为只展示 summary 和原始 JSON。
  • recommendedSchools 是服务端已排序结果,前端不要重新按分数线做业务排序。
  • disclaimers 必须展示在报告底部或导出 PDF 中。
  • 报告详情只能展示当前登录学生自己的报告,遇到 AI_REPORT_NOT_FOUND 按“报告不存在或无权访问”处理。
  • 当前 Taro 基础页在 pages/student/ai-school/indexservice 在 src/services/ai.ts

租户后台前端建议

租户后台可以先做 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/disableGET/PUT /api/tenant-admin/membersPOST /api/tenant-admin/members/disable;当前 Taro 租户设置页已提供第一版创建、编辑、停用、权限点、菜单、模块、字段、基础数据范围、成员绑定模板和成员状态配置。
  • 主题模板:GET /api/tenant-admin/theme-templatesGET /api/tenant-admin/themePOST /api/tenant-admin/theme/previewPOST /api/tenant-admin/theme/publish;当前 Taro 租户设置页已提供第一版模板选择、主色/强调色、Logo/分享图、安全草稿预览和发布。
  • 内容入口/分类树/题目集合/练习蓝图
  • 题目/单词/知识手册/分数线/视频维护
  • 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入 preview/import/issues、字段映射、模板下载和导入后复检
  • Banner/FAQ/公告/激活码/优惠券/积分任务/积分兑换,当前 Taro 租户营销中心已接 GET/PUT /api/tenant-admin/couponsGET /api/tenant-admin/coupons/redemptionsGET /api/tenant-admin/coupons/reportGET/PUT /api/tenant-admin/point-activity-tasksGET /api/tenant-admin/point-activity-claimsGET/PUT /api/tenant-admin/point-exchange-itemsGET /api/tenant-admin/point-exchange-orders,用于配置复杂规则、查看核销明细、活动效果、任务领取和兑换记录。
  • 勋章: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 队列、CRM 配置、CRM 跟进分配策略、分佣规则、成员分佣比例、分佣订单、结算单审核和线下打款登记;当前 Taro 租户营销中心已接 CRM、分佣、优惠券规则和核销报表第一版财务运营页已接退款、官方账单、对账异常、差错工单和调整凭证第一版。真实打款 provider、发票、生产账单抽样验收和更完整财务复核体验后续增强。

CRM 分配策略由后端执行,前端只提交配置:

{
  "assignmentMode": "round_robin",
  "assignmentPool": ["<salesUserId>", "<agentUserId>"]
}

assignmentMode 支持 nonedirectround_robinreferrer。后端会校验 assignmentPool 中的用户必须是当前租户内 active 的销售、代理、运营、教师或管理员;跨租户成员会返回 CRM_ASSIGNMENT_POOL_INVALID。首绑客资成功后,lead.item.assignedToUserId 是 CRM 跟进负责人,lead.item.referrerUserId 仍是受首绑保护的推广/分佣归属两者不要在前端混用。CRM 队列 payload.assignee 可用于展示推送目标,但前端不能自行改写客资归属或分配游标。

租户后台不应在前端自行决定权限;隐藏菜单只是体验优化,接口仍会校验权限。角色模板用于让租户配置“运营、教师、销售、代理”等自定义后台体验,成员绑定模板后,前端按模板的菜单/模块/字段权限渲染,后端按 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。