112 KiB
Taro 前端对接指南
更新时间:2026-06-30
目标:用一套 Taro 工程同时服务微信小程序和 H5 Web 题库,并采用“Supabase Auth/JWT + apps/api 业务 API 优先”的混合架构,支持多租户、品牌主题、地区题库、会员权益、销售追踪和对象存储资源。
建议新建:
F:\project\apps\taro
旧前端参考:
F:\project\参考\旧题库项目\src
启动流程
H5
- 从
window.location.host获取当前域名。 - 调用
GET /api/tenant/resolve?host=<host>。 - 保存
tenant.id、tenant.slug、branding、features、publicConfig。 - 使用
branding.theme和branding.publicAssets初始化主题、Logo、分享图、页面标题、功能开关。 - 检查本地 session token,调用
GET /api/auth/me。 - 如果未登录,进入登录页;如果已登录,加载个人中心和首页数据。
微信小程序
- 从编译环境或小程序启动参数读取
tenantCode。 - 推广码、销售码、分享码从
options或scene中解析。 - 调用
GET /api/tenant/resolve?tenantCode=<tenantCode>。 - 如存在 referral 参数,先调用
/api/referral/resolve和/api/referral/track-event。 - 登录后再调用
/api/referral/bind完成首绑保护。
请求封装
本项目不采用“前端直接写 Supabase 表替代业务命令层”的模式。Supabase 官方允许前端在 RLS 和最小权限下使用 Data API,但本系统的订单、支付、权益、租户后台、内容导入、CRM、对象存储签名等都需要服务端事务、密钥、审计和幂等,所以复杂业务命令默认调用 RPC、apps/api、Edge Function 或 worker。
前端可以使用 Supabase client 的范围:
- H5 Auth session/JWT。
- 小程序端在兼容性验证通过后的 Auth session/JWT。
- 低风险公开只读数据,且必须已经有 RLS、grant、跨租户测试。
- Realtime 非敏感通知。
前端必须调用 apps/api 的范围:
- 题库练习、答题、错题、收藏。
- 订单、支付、激活码、优惠券、权益。
- 私有 PDF、资料、视频、对象存储签名。
- 租户后台、平台后台、内容导入、CRM、销售/代理、数据看板。
前端应封装一个统一 API client,所有页面禁止直接散写 Taro.request。当前统一入口是:
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 请求封装必须按下面目标实现:
Authorization: Bearer <tk_session>
x-tenant-id: <tenantId> # 仅作为登录前/公开目录租户上下文;登录后必须与 session 租户一致
生产目标:
Authorization: Bearer <supabase_access_token>
x-tenant-id: <tenantId> # 作为租户上下文,不能作为身份依据
前端不应再传 x-user-id、query/body userId 来表示当前用户。后端已经实现 Supabase JWT 和迁移 session 优先解析:如果 Authorization 存在,用户态接口以 token 映射出的业务用户为准;如果请求里伪造了不同的 userId 会返回 AUTH_USER_MISMATCH,伪造不同租户会返回 AUTH_TENANT_MISMATCH 或 AUTH_SESSION_INVALID。
H5 使用 Supabase Auth 时,不要在页面里手写 Authorization,推荐请求流程是由统一 client 完成:
await apiRequest('/api/profile/me');
apps/taro/src/services/api-auth.ts 会读取 Supabase session 并生成 Authorization: Bearer <supabase_access_token>。页面层禁止通过 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 的租户。
生产或云端测试建议设置:
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 是已发布 token,branding.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/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 持久缓存成长期可访问地址。
学生端资料流程:
- 列表页调用
GET /api/catalog/assets,只展示后端返回的 active 资源。 - 预览 PDF/图片时调用
GET /api/catalog/assets/preview?assetId=...。 - 下载资料时调用
GET /api/catalog/assets/download?assetId=...。 - 使用响应里的
preview.url或download.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_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。
租户后台排查:
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-events和video-events回查用户、时间、IP、UA、资源或视频。 - 这些接口不会返回签名 URL、播放 token、云厂商密钥或支付密钥;前端不要把它们当成下载/播放接口。
练习访问控制契约
前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 POST /api/learning/practice-sessions,后端会根据 content_entries.accessRules、content_nodes.accessRules、question_collections.accessRules、practice_blueprints.accessRules 和当前用户权益决定最终题目快照。
请求示例:
{
"mode": "sequential",
"collectionId": "00000000-0000-0000-0000-000000000615",
"questionLimit": 50
}
响应关键字段:
{
"item": {
"id": "...",
"mode": "sequential",
"questionIds": ["..."],
"questionCount": 25,
"accessMode": "free",
"consumedFreeQuota": 25,
"accessSnapshot": {
"grantedBy": "free_quota",
"requestedCount": 50,
"grantedCount": 25,
"truncated": true,
"dailyLimit": 25
}
}
}
前端处理规则:
- 以返回的
questionIds为准渲染本次练习,不要自行追加题目。 accessSnapshot.truncated=true时,可提示“今日免费额度有限,已为你开放 N 题”并引导开通 SVIP。PRACTICE_FREE_LIMIT_REACHED:弹出会员购买/激活码兑换入口。PRACTICE_SVIP_REQUIRED:提示该内容需要对应地区/科目/题库 SVIP。PRACTICE_SESSION_QUESTION_FORBIDDEN:说明提交答案的题目不在本次 session 快照内,应清理本地异常进度并重新开始。- 提交答案必须传
practiceSessionId;后端会拒绝不属于本人有效 session 的题目。
题干、解析和知识手册富文本
学生端已经新增统一渲染组件:
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
当前接入页面:
pages/student/practice/index 题干、选项、子题、参考答案、解析
pages/student/reports/index 逐题复盘、子题明细、参考答案、解析
pages/student/handbook/index 知识点摘要和正文
第一版支持:
- 纯文本和换行。
- Markdown 图片
、站内/...路径,或私有资源引用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/index 和 pages/student/order-detail/index 已接第一版。前端只传递套餐、地区、优惠券和支付 provider;最终金额、优惠抵扣、订单状态、支付记录、权益发放都以后端返回为准。
推荐流程:
GET /api/catalog/svip-plans加载可购买套餐。- 如有优惠券,先调
POST /api/commerce/coupons/claim,仅用于领取、占用一个未核销 redemption 和展示预计抵扣。 - 调
POST /api/commerce/orders创建订单,后端会重新计算最终金额和抵扣。 - 非零元订单调
POST /api/commerce/payments/create获取支付参数。 - H5 支付可跳转 provider 返回的 URL;微信小程序支付用 provider 返回参数调用
Taro.requestPayment。 - 支付后调
/api/commerce/orders/status轮询状态,已支付订单的权益由后端 webhook/补偿 worker 幂等发放。
普通学生端不直接调用退款接口。退款申请、审核、供应商退款、全额退款权益撤销均在租户后台权限流中完成;学生端只展示订单详情、状态和售后联系入口。
勋章
学生个人中心或学习成就页调用:
GET /api/profile/badges?includeLocked=true&category=practice
说明:
includeLocked=true时返回已解锁和未解锁勋章;不传时只返回已解锁。category可选:learning、practice、vocabulary、mock_exam、activity、feedback、sales、system、custom。- 前端只展示后端返回的
unlocked/grantId/grantedAt,不要在本地自行认定用户已经获得勋章。
租户后台勋章管理:
GET /api/tenant-admin/badges?category=practice&includeInactive=true
PUT /api/tenant-admin/badges
GET /api/tenant-admin/badge-grants?userId=...&badgeId=...
POST /api/tenant-admin/badge-grants
PUT /api/tenant-admin/badges 支持同租户内 legacyId 幂等更新;如果 id 与 legacyId 指向不同记录会返回 BADGE_ID_CONFLICT。POST /api/tenant-admin/badge-grants 对同一用户同一勋章幂等,不会重复生成多条发放记录。
当前后端已支持第一批自动发放规则:
| unlockType | 推荐 conditionField | 触发时机 |
|---|---|---|
check_in |
checkInStreak |
POST /api/profile/check-in 真实签到成功后 |
score |
score |
签到加分、反馈奖励或积分活动奖励成功后 |
feedback_resolved |
feedbackResolvedCount |
租户后台把反馈处理为 resolved 后 |
activity_reward |
activityRewardCount 或 score |
学生领取积分活动任务且后端证据校验通过后 |
规则使用 conditionOperator 的合法值 gte、gt、lte、lt、eq;conditionValue 为数字。触发成功的接口会返回 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保留后台配置的卷面总分。测试或预发数据题量不足时,两者不一定按百分制等比换算,前端展示时不要自行重算。- 错题复盘优先使用
wrongQuestionIds和questionResults,题目详情仍可按现有题目接口或 session 快照加载。
练习历史、统计和错题复习
个人中心和学习报告页优先使用后端聚合接口,不要让前端遍历全部答题记录自行统计。
接口用途:
| 页面/组件 | 接口 | 说明 |
|---|---|---|
| 练习历史列表 | GET /api/learning/practice-sessions/history?limit=20 |
返回 session、报告、已答数量、正确数、状态 |
| 学习概览卡片 | GET /api/learning/stats?days=30 |
返回总答题、正确率、报告数、错题数、收藏数、题型分布 |
| 正确率趋势图 | GET /api/learning/trend?days=14 |
返回每日答题数、正确数、错题数、session 数、报告数 |
| 错题复习入口 | GET /api/learning/wrong-questions/review-plan?limit=20 |
返回建议复习题和后端组卷 nextAction |
| 排行榜 | GET /api/learning/leaderboard?metric=questions&period=7d®ionId=...&classId=... |
返回排名、用户展示信息、当前用户排名和范围信息 |
错题复习创建 session:
{
"mode": "wrong_review",
"questionLimit": 20
}
前端处理规则:
- 不要把错题 ID 列表从前端传回后端组卷;
wrong_review会由后端按当前用户错题本安全组卷。 review-plan.nextAction可直接用于按钮配置,但仍需使用当前登录 session 调用。- 收藏夹复习同理可调用
POST /api/learning/practice-sessions,body 为{ "mode": "favorite_review", "questionLimit": 20 }。 - 趋势图以接口返回日期桶为准,缺失日期后端会补 0,不需要前端补点。
排行榜
排行榜由后端统一聚合,前端不要读取答题记录、单词进度或模考报告后自行排名,避免越权、口径漂移和跨租户数据泄露。
可选参数:
| 参数 | 可选值 | 说明 |
|---|---|---|
metric |
questions、score、vocabulary、mock_exam |
分别表示累计答题、积分、掌握单词、模考最高分 |
period |
all、7d、30d |
统计周期 |
regionId |
UUID | 地区范围,可选 |
classId |
UUID | 班级范围,可选,后端按当前租户校验 |
limit / page |
正整数 | 分页 |
响应会包含 items 和 currentUser。即使当前用户未进入前 N 名,也应优先展示 currentUser 作为“我的排名”。后台后续会补日/周榜预聚合和防刷策略,前端只消费接口返回口径。
租户数据看板
租户后台数据看板由后端统一聚合,前端不要直接读取订单、答题记录、学生列表后自行统计,避免权限越权、敏感信息外泄和各页面口径不一致。
请求:
GET /api/tenant-admin/dashboard?timeRange=30d®ionId=<可选地区ID>&limit=10
可选参数:
| 参数 | 可选值 | 说明 |
|---|---|---|
timeRange |
7d、30d、90d |
统计区间,默认 30d |
regionId |
UUID | 可选地区筛选,后端会校验地区属于当前租户 |
limit |
1-50 | 题型、科目、地区、套餐和运营动态的返回条数 |
响应主要结构:
{
"item": {
"scope": {
"tenantId": "...",
"regionId": "...",
"timeRange": "30d",
"timezone": "Asia/Shanghai"
},
"cards": {
"students": {},
"learning": {},
"content": {},
"activationCodes": {},
"feedback": {}
},
"paymentStats": {},
"trends": [],
"activeHours": [],
"questionDistribution": [],
"subjectTop": [],
"regionStats": [],
"planSales": [],
"recentActivities": []
}
}
前端处理规则:
- 管理台菜单显示可按
/api/tenant-admin/permissions的dashboard:read判断,但真正权限以后端返回为准。 trends已补齐自然日桶,activeHours固定 24 项,前端不需要补点。revenueCents、amountCents都是分,前端统一格式化成人民币展示,不要自行重算订单金额。recentActivities.details只包含可展示的低敏汇总信息,不包含手机号、支付密钥、对象存储 key 等敏感字段。- 大租户正式上线后会补预聚合 worker,前端不应依赖任何临时 SQL 口径或自己维护缓存口径。
销售/代理分佣结算
分佣结算由后端统一计算,前端不要读取订单、激活码或客资后自行算佣金。当前后端已支持订单和激活码两类来源,并且只统计客资首绑保护后的成交,避免后绑抢单。
常用接口:
| 页面/动作 | 接口 | 权限 |
|---|---|---|
| 查看租户分佣设置 | GET /api/commission/settings |
commission:read |
| 修改默认分佣设置 | PUT /api/commission/settings |
commission:write |
| 设置销售/代理个人比例 | PUT /api/commission/member-rate |
commission:write |
| 分佣汇总 | GET /api/commission/summary?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD&referrerUserId=... |
commission:read 或 commission:self |
| 分佣来源明细 | GET /api/commission/orders?... |
commission:read 或 commission:self |
| 结算单列表 | GET /api/commission/settlements?... |
commission:read 或 commission:self |
| 导出结算明细 | GET /api/commission/settlements/export?settlementId=...&format=csv |
commission:read 或 commission:self |
| 生成结算单 | POST /api/commission/settlements/generate |
commission:write |
| 审核/打款状态 | POST /api/commission/settlements/status |
commission:review |
| 查看凭证 | GET /api/commission/settlements/proofs?settlementId=... |
commission:read 或 commission: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/mimeType,H5 可转成下载,微信小程序端建议后续使用文件系统保存;前端不要直接查询commission_settlement_items拼文件。 - 凭证支持
assetId或externalUrl。如果使用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。 - 返回的
status、dueLevel、nextReviewDate作为后续展示依据,不在前端重算间隔。 - 收藏列表调用
GET /api/learning/vocabulary/favorites?unitId=<unitId>;收藏/取消收藏调用POST /api/learning/vocabulary/favorites。 - 发音当前使用前端
services/pronunciation.ts:H5 优先播放有道 dictvoice HTTPS 音频并用 Web Speech 兜底,小程序优先使用Taro.createInnerAudioContext。这里不保存任何密钥;如果后续租户需要自定义发音源,应改为后端返回可配置的公开 provider URL 或资源台账引用。 - 旧的
POST /api/learning/vocabulary/progress保留给兼容和后台手工修正;普通学习流优先用vocabulary/review。
视频播放契约
题目视频分为 free、svip、video_quota 三种访问模式。列表接口只用于展示标题、封面、时长、访问模式和试看秒数;除免费公开视频外,列表和搜索接口不会返回可播放 URL。
播放步骤:
- 进入题目页后调用
GET /api/questions/{questionId}/videos或批量预加载POST /api/questions/videos/batch。 - 用户点击播放时调用
POST /api/videos/play。 - 后端校验当前 session 用户、租户、题目绑定关系、SVIP 权益或视频次数权益。
- 后端返回短期签名 URL、播放 token、权益来源和过期时间。
- 前端播放器只使用本次返回的
playback.url,不要缓存为长期资源地址。 - 播放器开始、周期心跳和播放完成时调用
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 秒或进度变化明显时上报heartbeat,ended或观看进度超过 90% 时上报complete。
进度上报示例:
{
"playToken": "vp_...",
"eventType": "heartbeat",
"progressSeconds": 45,
"watchedSeconds": 48,
"durationSeconds": 90
}
后端会校验 playToken 必须属于当前登录用户和当前租户,其他用户不能拿 token 改播放状态。返回的 item.playback 会包含 watchedSeconds、completionRate、startedAt、completedAt,租户后台媒体运营报表会读取这些字段计算完成率和观看时长。
资料上传、预览和下载契约
学生端资料只读取目录、预览和下载签名,不接触对象存储真实密钥,也不自行拼接私有 bucket 地址。
学生端展示资料列表:
GET /api/catalog/assets?assetType=pdf®ionId=<regionId>&includeLocked=true
学生端 PDF/图片预览:
GET /api/catalog/assets/preview?assetId=<assetId>
学生端下载:
GET /api/catalog/assets/download?assetId=<assetId>
前端处理规则:
preview.url是短期 inline URL,只给预览组件使用,不持久化。download.url是短期 attachment URL,只给下载动作使用。ASSET_SVIP_REQUIRED:提示开通对应地区/科目权益。ASSET_UPLOAD_NOT_VERIFIED:展示“资料正在处理中”,并上报前端日志。ASSET_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=draft、uploadStatus=pending、securityScanStatus=pending,学生端不会看到。确认成功后仍保持 status=draft,并进入 uploadStatus=verified、securityScanStatus=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=failed、securityScanStatus=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 返回匹配倒计时。
签到入口调用:
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_complete、mock_exam_submit、vocabulary_review等任务需要传sourceId,后端会校验证据属于当前学生当前租户。feedback_resolved等系统流程任务禁止学生自领,前端应展示为“系统发放”或不展示领取按钮。- 兑换返回
order和最新积分余额后,再刷新profile/me、score-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>¬ificationType=point_exchange_pending_fulfillment
- 租户后台需要
notifications:read权限。 - 后台只能查看租户内用户通知,不代学生改已读状态。
- 前端可以按
actionPath做站内跳转,但跳转后的页面仍要重新请求对应业务接口,不能信任通知 metadata 作为最终业务数据。 - 当前支持的通知类型包括
feedback_status_updated、feedback_reward_granted、badge_granted、point_exchange_completed、point_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=school或markerType=exam_track可作为学生目标院校/专业意向采集。- 不同地区节点层级可以不同,页面组件必须支持递归树和面包屑。
多租户前端优化
- Logo、标题、主题色、客服信息全部来自
tenant/resolve。 - 功能开关控制菜单显示,但接口权限仍以后端为准。
- 私有图片、PDF、视频不要直接拼 URL,一律通过后端签名。
- 支付渠道从后端返回或租户配置读取,不在页面硬编码。
- 小程序分享路径必须带 tenantCode 和 referral code。
- 用户首绑归属由后端保护,前端不要提供“换绑销售”入口。
- 管理后台菜单按
GET /api/tenant-admin/permissions返回的current.permissions、current.templatePermissions、current.menuPermissions、current.modulePermissions渲染;接口权限仍以后端校验为准。 - 教师、班主任、助教类账号进入租户后台时,学生列表以
GET /api/tenant-admin/students返回的scoped和items为准;前端不要自行用本地班级 ID 放大查询范围。 - 学生手机号、订单金额、客资归属等敏感字段按
fieldPermissions控制显示;字段被后端返回为null时前端展示脱敏占位,不要从其它接口补取。 - 学生批量导入和批量分班接口会返回
total/successCount/errorCount/results,前端必须展示逐行错误,不要在浏览器端静默丢弃失败行。 - 教师可以为范围内学生创建备注和跟进任务,但是否能禁用学生、批量导入、查看手机号由后端权限和字段权限决定;前端只按返回值渲染。
- H5 自定义域名下要注意缓存隔离,不能把 A 租户主题缓存用到 B 租户。
租户内容导入对接
租户后台导入统一使用 preview -> issues -> import 流程,前端不要直接写 Supabase 表或绕过 apps/api。当前 JSON、CSV 和 Excel 都进入同一套后端规范化、逐行 issue、幂等和审计管线。
当前可联调:
POST /api/tenant-content/imports/preview/questions
POST /api/tenant-content/imports/questions
POST /api/tenant-content/imports/preview/vocabulary
POST /api/tenant-content/imports/vocabulary
POST /api/tenant-content/imports/preview/handbook
POST /api/tenant-content/imports/handbook
POST /api/tenant-content/imports/preview/scoreline
POST /api/tenant-content/imports/scoreline
POST /api/tenant-content/imports/preview/videos
POST /api/tenant-content/imports/videos
GET /api/tenant-content/imports
GET /api/tenant-content/imports/issues
GET /api/tenant-content/imports/field-mapping
GET /api/tenant-content/imports/templates
POST /api/tenant-content/imports/post-check
GET /api/tenant-content/imports/post-check
前端流程:
- 页面初始化调用
field-mapping,渲染字段说明、别名、必填项和示例。 - 下载模板调用
templates?importType=...&format=csv|json,用contentBase64生成文件。 - 上传或粘贴 JSON/CSV/Excel,先调用对应 preview。
- 展示
job.totalCount/validCount/errorCount/warningCount。 - 展示
job.sourceFormat、job.parserMetadata、逐行issues,错误行必须让运营修正;如果后端允许allowPartial,也要二次确认。 - 小批量确认后直接调用 import;大批量确认时传
executionMode=async排队,前端轮询 job 状态。 - 导入进入
completed/completed_with_errors后调用POST /api/tenant-content/imports/post-check。 - 展示
summary.importPostCheck或GET /api/tenant-content/imports/post-check?jobId=...返回的复检结果,再刷新内容列表、分数线列表或题目视频列表。
当前 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 可用fields、schools、majors、records多 Sheet。 - 页面筛选字段仍以
/api/scoreline/fields为准,不要从导入 JSON 临时生成筛选 UI。 record至少需要schoolId、schoolLegacyId或schoolName,否则 preview 会返回 issue。
视频导入前端注意:
- 列表和搜索接口不会给付费视频可播放 URL;播放统一调
POST /api/videos/play。 - 绑定题目必须提供
questionId或legacyQuestionId。 - 生产建议把私有视频先入
content_assets,导入时传assetId,避免长期暴露源站 URL。
题库导出对接
租户后台题库导出统一走后端生成结构化 payload,前端不要直接查 Supabase 表拼导出文件。当前后端支持三种交付形态:
- 同步结构化导出:
json、paper_json、print_payload,接口直接返回 base64 JSON/payload。 - 异步二进制导出:
pdf、docx,接口先返回pendingjob,由apps/worker --job exports渲染 PDF/Word、水印并写入content_assets,前端轮询 job 后再走资源签名下载或预览。 - 异步运营素材包:
daily_practice_zip,仅支持exportType=daily_practice,worker 会生成九宫格 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 可为 pdf、docx。publishToAssets=true 表示导出文件可作为资料资源展示给对应可见范围用户;不传时默认生成后台私有资源,仅后台可下载。assetVisibility 支持 public、tenant、members、svip、private,生产默认建议用 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=false,payload.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
采纳请求:
{
"grantId": "<授权ID>",
"entryName": "天津专升本公共题库",
"collectionName": "天津专升本公共题目",
"copyLimit": 500
}
前端处理规则:
- 租户只能看到后端判定为已授权的公共题库,不要在前端用套餐码自行过滤。
- 采纳成功后后端会生成本租户自己的
questionBankId、entryId、collectionId和题目快照,学生端直接按普通/api/catalog/content-entries、question-collections、practice-sessions接入。 - 重复采纳返回
QUESTION_BANK_ALREADY_ADOPTED,前端展示“已采纳”即可。 - 已采纳公共题库可以手动同步平台后续新增/更新题目;同步会重新校验当前租户仍有授权,且只写入租户自己的题目副本。
- 后端也可以由
apps/worker --job public-banks自动同步待更新的采纳题库;前端不需要轮询平台源库,只需要在租户后台展示同步状态、最近同步时间和冲突数量。
同步请求:
{
"adoptionId": "<tenant_question_bank_adoptions.id>",
"copyLimit": 1000
}
同步响应关键字段:
{
"item": {
"id": "...",
"syncStatus": "synced | failed",
"copiedQuestionCount": 120,
"targetQuestionBankId": "...",
"targetEntryId": "...",
"targetCollectionId": "..."
},
"sync": {
"status": "synced | conflict",
"counts": {
"inserted": 1,
"updated": 3,
"skipped": 116,
"conflicts": 0
},
"results": [
{
"sourceQuestionId": "...",
"targetQuestionId": "...",
"action": "inserted | updated | skipped | conflict",
"sourceHash": "...",
"previousSourceHash": "...",
"targetHash": "..."
}
]
}
}
前端处理规则:
sync.status=synced:刷新公共题库列表、题目集合和题目列表。sync.status=conflict或item.syncStatus=failed:展示冲突数量和冲突题目,不要把它当系统异常。冲突表示租户已经改过这道采纳题,后端已跳过并保留租户内容。action=conflict的记录可以进入冲突处理区域:展示平台源题 ID、租户目标题 ID、上次平台 hash、当前平台 hash、租户当前 hash。当前后端已支持单条和批量处理。- 页面初始化或 worker 后台同步完成后,可以调用
GET /api/tenant-content/public-question-banks/conflicts?adoptionId=...查询最近一次同步状态、counts和conflicts。这个接口只返回当前租户自己的采纳记录,跨租户会返回QUESTION_BANK_ADOPTION_NOT_FOUND。 - 单条冲突处理调用
POST /api/tenant-content/public-question-banks/conflicts/resolve,body 为{ "adoptionId": "...", "sourceQuestionId": "...", "resolution": "accept_platform | keep_local" }。accept_platform会把租户副本写成平台当前版本并生成新题目版本;keep_local会记录本地保留决策,同一平台 hash 和本地 hash 后续同步不再反复提示。两种操作都会写审计日志。 - 批量冲突处理调用
POST /api/tenant-content/public-question-banks/conflicts/resolve-batch,body 为{ "adoptionId": "...", "sourceQuestionIds": ["..."], "resolution": "accept_platform | keep_local", "limit": 50 }。后端最多处理 100 条,仍会重新校验租户授权、锁定采纳记录和目标题,逐条写审计;前端只提交当前冲突列表中明确展示给操作者的 source id。 - 同步产生新增/更新时,后端会写入
public_question_bank_synced通知;同步产生冲突时,会写入public_question_bank_conflict通知。通知只包含同步摘要、题库 ID、题目 hash 和操作入口,不保存题目答案或解析。 - 租户后台可调用
GET /api/tenant-content/notifications?notificationType=public_question_bank_conflict&status=unread&limit=20展示待处理同步消息;也可带adoptionId查看某个采纳记录的通知。 - 通知状态更新调用
POST /api/tenant-content/notifications/status,body 为{ "notificationIds": ["..."], "status": "read | dismissed | resolved" }。冲突被单条或批量全部处理后,后端会自动把相关冲突通知标记为resolved。 QUESTION_BANK_GRANT_NOT_AVAILABLE:说明 SaaS 套餐/授权已失效,提示联系平台或升级套餐。QUESTION_BANK_ADOPTION_NOT_FOUND:说明不是当前租户的采纳记录或记录已归档,前端不要跨租户重试。- 当前租户后台可以提供手动“同步平台更新”按钮,并展示 worker 自动同步后的通知、冲突查询结果、单条处理和批量处理按钮。后续继续补更完整运营消息、失败告警和生产定时调度。
登录对接
短信登录
开发环境可以先使用 mock 短信,接口会返回 debugCode。生产环境禁止依赖 debugCode。
POST /api/auth/sms/send
body: { "phone": "13800000000", "purpose": "login" }
POST /api/auth/sms/verify
body: { "phone": "13800000000", "code": "123456", "purpose": "login" }
成功后保存:
session.token
session.expiresAt
user
后续请求统一带:
Authorization: Bearer <session.token>
x-tenant-id: <tenantId>
绑定或更换手机号
微信/QQ 登录后强制绑定手机号、个人中心更换手机号,都走同一个后端命令。前端先发送 bind_phone 用途验证码,再提交绑定:
POST /api/auth/sms/send
body: { "phone": "13800000000", "purpose": "bind_phone" }
POST /api/auth/phone/bind
Authorization: Bearer <session.token>
body: { "phone": "13800000000", "code": "123456" }
前端规则:
- 绑定接口必须带当前登录态,不能用
x-user-id伪造用户。 - 绑定接口只接受
bind_phone验证码,不接受login验证码。 - 新手机号如果已属于其它账号,后端返回
PHONE_ALREADY_BOUND。 - 换绑成功后旧手机号登录身份会被移除;迁移期
tk_其它设备 session 会被撤销,当前 session 继续可用。 - 微信手机号授权后也应由后端 adapter 换取手机号,再复用同一类绑定命令;不要在页面里持久化明文手机号授权中间数据。
微信小程序登录
微信小程序端调用 Taro.login() 获取 code,然后交给后端:
POST /api/auth/oauth/wechat-miniapp
body: {
"code": "<wx.login code>",
"profile": {
"nickName": "...",
"avatarUrl": "..."
}
}
成功响应包含:
provider
user
isNewUser
session.token
session.expiresAt
identity.openId
identity.unionId
注意:
- 前端不接触
appSecret。 - 前端不会拿到微信
session_key。 - 如果登录前已经解析到推广码,登录成功后再调用
/api/referral/bind完成首绑保护。 - 如果用户没有手机号,跳转到上面的“绑定或更换手机号”流程。
微信网页登录
H5 端在微信开放平台授权回调页拿到 code 后,交给后端:
POST /api/auth/oauth/wechat
body: {
"code": "<wechat oauth code>",
"lang": "zh_CN"
}
成功响应与小程序登录一致,包含 provider=user/identity/session。后端会使用租户 wechat-web/wechat_web/wechat provider 配置换取 access_token 和 openid,再拉取用户资料;如果返回 unionid,会和同一开放平台下的小程序身份合并。
前端注意:
- H5 回调页只短暂读取
code/state,不要持久化微信access_token。 state应在前端本地或服务端中转页校验,避免跨站授权回调混淆。- 多租户自定义域名下,授权回调域名必须与租户微信开放平台配置一致;如果未来使用统一授权中转域名,需要在回调后再解析目标租户。
QQ 登录
H5 端在 QQ 互联授权回调页拿到 code 后,交给后端:
POST /api/auth/oauth/qq
body: {
"code": "<qq oauth code>",
"redirectUri": "https://h5.example.com/auth/qq/callback"
}
后端会完成 code -> access_token -> openid -> userinfo,并签发本项目 session。
前端注意:
redirectUri必须与 QQ 互联后台登记地址一致;也可以由租户后台 provider 配置固定,前端不传。- 前端不要接触 QQ
clientSecret/AppKey或access_token。 - 登录后如果没有手机号,同样进入“绑定或更换手机号”流程。
支付对接
支付流程必须以后端订单金额和后端回调为准,前端只负责拉起支付。
创建订单
POST /api/commerce/orders
body: {
"planId": "<svipPlanId>",
"quantity": 1,
"payProvider": "wechat_pay | alipay",
"payMethod": "jsapi | wap",
"regionId": "<regionId>",
"couponCode": "<可选,优惠券码>",
"couponRedemptionId": "<可选,已领取优惠券 redemptionId>"
}
前端可以先领取优惠券,再下单:
POST /api/commerce/coupons/claim
body: {
"code": "<couponCode>",
"planId": "<svipPlanId>",
"regionId": "<regionId>"
}
coupons/claim 对同一用户同一优惠券的未核销记录是幂等的;如果优惠券允许 perUserLimit > 1,前一次 redemption 已经下单核销后,用户可以再次领取直到达到限额。已超过单用户限额会返回 COUPON_ALREADY_USED 或 COUPON_USER_LIMIT_REACHED。下单时后端会重新计算套餐原价、优惠金额和最终应付,前端展示金额只能使用接口返回的 originalAmountCents、discountCents、amountCents。
优惠券规则由后端执行,前端只做展示和提示:
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
订单详情会返回 pricing、payments、items、couponRedemptions,可用于收银台、订单详情页和售后排查。订单状态轮询页只需消费 status/payment,避免频繁拉取全量明细。
后端已提供 commerce worker 作为兜底补偿:如果微信/支付宝支付成功但 webhook 漏通知,worker 会按租户商户配置查询供应商订单并幂等更新订单、支付和权益。前端仍然只轮询 orders/status 或 orders/detail,不要直接调用供应商查询接口,也不要在页面里自行开通会员。
退款和售后
学生端不直接发起后台退款命令。普通用户订单页只展示 GET /api/commerce/orders/status 和 GET /api/commerce/orders/detail 返回的订单状态、支付状态、refundedAmountCents,并提供客服/工单入口。租户后台或运营后台才接退款接口。
租户后台退款列表:
GET /api/commerce/refunds?status=requested&orderNo=<orderNo>
权限:tenant:refund:read
创建退款申请:
POST /api/commerce/refunds
权限:tenant:refund:write
body: {
"orderNo": "<orderNo>",
"refundNo": "<可选,前端幂等键>",
"amountCents": 500,
"reason": "用户协商退款",
"entitlementAction": "revoke_on_success | none"
}
退款状态流转:
POST /api/commerce/refunds/status
body: {
"refundId": "<refundId>",
"action": "approve | reject | submit_provider_refund | query_provider_refund | mark_processing | mark_succeeded | mark_failed | cancel",
"providerRefundNo": "<支付平台退款单号,可选>",
"providerNotifyUrl": "<微信退款通知地址,可选>",
"note": "<处理备注>"
}
状态说明:
requested -> approved -> processing -> succeeded
requested -> approved -> submit_provider_refund -> processing/succeeded
processing -> query_provider_refund -> processing/succeeded/failed
provider refund notify -> processing/succeeded/failed
requested/approved -> rejected
requested/approved -> cancelled
approved/processing -> failed
注意:
- 金额单位一律是分,前端不要传元。
refundNo是幂等键;同一订单同一金额重复提交会返回原退款申请。- 后端会限制累计退款金额不能超过实付金额。
- 全额退款成功后订单和支付会进入
refunded,相关订单权益会被置为revoked;部分退款进入partially_refunded,默认不撤销权益。 submit_provider_refund会由后端使用租户支付账户密钥调用微信/支付宝;前端不要保存商户私钥、API v3 key 或支付宝应用私钥。- 微信退款通常先进入
processing,租户后台可以调用query_provider_refund主动向微信查询,确认成功后后端才会更新订单退款金额和权益。 - 支付宝普通退款如果响应
fund_change=Y会同步进入succeeded;处于processing的退款也可以用query_provider_refund调用alipay.trade.fastpay.refund.query确认。 - 退款通知地址由支付账户或
submit_provider_refund.providerNotifyUrl配置,后端公开接收路径为POST /api/commerce/refunds/notify/wechat_pay?tenantId=<tenantId>、POST /api/commerce/refunds/notify/alipay?tenantId=<tenantId>。这是支付平台回调地址,Taro 前端不要主动调用。 - 退款通知只会推进已经审核/处理中的退款申请;未审核的
requested退款不能被外部通知直接落账。 - 已经
succeeded的退款不能再次查询或再次标记成功,避免订单退款金额重复累加。前端应按接口返回状态展示,不要假设点击后立即到账。 - 自动补偿 worker 已接入:支付漏通知和处理中退款会由后端定时查询供应商并幂等落账。资金对账已支持租户后台手工/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=true。matched/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": "<userId,assign 时必填>",
"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 验签或权益开通。
第一阶段页面建议
pages/bootstrap/index- 租户解析、主题初始化、登录态恢复。
pages/login/index- 先接短信登录;后续接微信小程序登录。
pages/home/index- Banner、公告、题库入口、会员入口、资料入口。
pages/region/index- 地区选择和权益提示。
pages/catalog/index- entry/node/collection/blueprint 通用导航。
pages/practice/index- 刷题、答题、解析、错题、收藏。
pages/vocabulary/index- 单词单元、学习、收藏。
pages/handbook/index- 手册目录和阅读。
pages/scoreline/index- 动态字段筛选和趋势。
pages/ai-school/index
- SVIP AI 择校推荐、报告历史和 JSON 报告渲染。
pages/profile/index
- 会员、订单、激活码、学习数据、勋章。
当前 Taro 实现进度
截至 2026-06-29,apps/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 平台后台租户、租户详情、账务资料、平台审计、套餐账单、订阅账单候选/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 工程既有警告,不阻断联调。
下一批前端开发重点:
- 学生端:地区选择、题目视频播放、题目反馈、错题/收藏专题页、模考交卷报告、收银台、订单详情、售后入口、题干/解析/知识手册 RichContent 安全渲染、逐题复盘、背单词卡片学习/发音/收藏练习、资料短签名水印预览/下载确认、个人中心消息中心、积分任务、积分兑换和积分明细第一版已接;下一批继续补独立消息中心增强、真正 KaTeX/小程序公式方案、私有题图签名资源映射、背单词更细统计、小程序支付容器和分享场景。
- 租户后台:工作台已接权限驱动模块入口;学生运营页已接学生创建/更新、禁用/恢复、批量导入、批量分班、学生备注、跟进任务和完成跟进第一版;题库内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、模板预览/下载、异步任务轮询和导入后复检第一版;营销中心已接 CRM 配置保存、CRM 队列按状态查看、分佣默认规则、成员分佣比例、分佣订单明细、结算单生成、审核通过/驳回、标记线下打款、优惠券规则表单、筛选、核销明细、核销报表、积分任务/兑换操作台和用户通知查看第一版;财务运营页已接退款申请/审核/供应商提交与查询、官方账单任务、对账批次/异常明细、差错工单处理、人工调整凭证提交/复核和异常订单运营台第一版;租户设置页已接主题模板、草稿预览、发布、角色模板创建、编辑、停用、权限点、菜单、模块、字段、基础数据范围、成员搜索/新建、成员绑定模板、成员状态和额外权限覆盖第一版;下一批继续补更精细的学生导入模板体验、真实生产账单抽样验收、真实打款 provider、发票、更细数据范围 UI 和主题素材库。
- 平台后台:租户创建、租户详情、状态变更、账务资料维护、最近平台审计、订阅开通、账单生成、订阅账单候选预览、dry-run、批量生成、自动计费生成结果查看、人工收款确认、用量录入、公共题库授权编辑已接第一版;后端会跳过已开票订阅并记录
platform.invoice.subscription_batch_created审计,platform-billingworker 会自动生成即将到期订阅账单并记录platform.invoice.subscription_auto_created审计,前端只展示候选、预览结果、跳过结果和生成后的账单/审计;继续补租户基础资料编辑增强、平台审计报表导出/告警和催缴/收款流。 - 小程序:验证
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 支持 safe、balanced、sprint。regionId 不传时后端会使用 /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/index,service 在src/services/ai.ts。
租户后台前端建议
租户后台可以先做 H5 管理台,也可以后续使用 Taro H5 复用部分组件。优先页面:
- 概览:
/api/tenant-admin/overview - 数据看板:
/api/tenant-admin/dashboard,展示收益、注册、学习、内容、激活码、反馈、趋势、24h 活跃、套餐销量和运营动态 - 品牌/主题/域名/公开设置
- 支付账户/登录 provider/密钥引用
- 用户与成员权限
- 班级/教师/学生:
/api/tenant-admin/classes、classes/members、students、teachers、students/notes、students/followups - 角色模板与成员:
GET/PUT /api/tenant-admin/role-templates、POST /api/tenant-admin/role-templates/disable、GET/PUT /api/tenant-admin/members、POST /api/tenant-admin/members/disable;当前 Taro 租户设置页已提供第一版创建、编辑、停用、权限点、菜单、模块、字段、基础数据范围、成员绑定模板和成员状态配置。 - 主题模板:
GET /api/tenant-admin/theme-templates、GET /api/tenant-admin/theme、POST /api/tenant-admin/theme/preview、POST /api/tenant-admin/theme/publish;当前 Taro 租户设置页已提供第一版模板选择、主色/强调色、Logo/分享图、安全草稿预览和发布。 - 内容入口/分类树/题目集合/练习蓝图
- 题目/单词/知识手册/分数线/视频维护
- 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入 preview/import/issues、字段映射、模板下载和导入后复检
- Banner/FAQ/公告/激活码/优惠券/积分任务/积分兑换,当前 Taro 租户营销中心已接
GET/PUT /api/tenant-admin/coupons、GET /api/tenant-admin/coupons/redemptions、GET /api/tenant-admin/coupons/report、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/PUT /api/tenant-admin/badges、GET/POST /api/tenant-admin/badge-grants - 考试日期:
GET/PUT /api/tenant-admin/exam-dates - 题目反馈:
GET /api/tenant-admin/feedbacks、POST /api/tenant-admin/feedbacks/status、GET /api/tenant-admin/feedbacks/events - 销售/代理/CRM 队列、CRM 配置、CRM 跟进分配策略、分佣规则、成员分佣比例、分佣订单、结算单审核和线下打款登记;当前 Taro 租户营销中心已接 CRM、分佣、优惠券规则和核销报表第一版,财务运营页已接退款、官方账单、对账异常、差错工单和调整凭证第一版。真实打款 provider、发票、生产账单抽样验收和更完整财务复核体验后续增强。
CRM 分配策略由后端执行,前端只提交配置:
{
"assignmentMode": "round_robin",
"assignmentPool": ["<salesUserId>", "<agentUserId>"]
}
assignmentMode 支持 none、direct、round_robin、referrer。后端会校验 assignmentPool 中的用户必须是当前租户内 active 的销售、代理、运营、教师或管理员;跨租户成员会返回 CRM_ASSIGNMENT_POOL_INVALID。首绑客资成功后,lead.item.assignedToUserId 是 CRM 跟进负责人,lead.item.referrerUserId 仍是受首绑保护的推广/分佣归属,两者不要在前端混用。CRM 队列 payload.assignee 可用于展示推送目标,但前端不能自行改写客资归属或分配游标。
租户后台不应在前端自行决定权限;隐藏菜单只是体验优化,接口仍会校验权限。角色模板用于让租户配置“运营、教师、销售、代理”等自定义后台体验,成员绑定模板后,前端按模板的菜单/模块/字段权限渲染,后端按 permission keys 执行真正的访问控制。班级/学生范围权限由后端根据角色、模板 dataScope.classIds 和 tenant_class_members 计算,教师默认只能看到自己负责班级。
联调顺序
- 启动页和租户解析。
- 短信登录和
auth/me。 - 首页、地区、内容入口、题库树。
- 练习 session、答题、错题、收藏。
- 背单词、知识手册、分数线。
- 会员套餐、订单、激活码。
- 资料下载、视频解析。
- 销售追踪和分享链路。
- 租户后台内容维护和导入。
- 正式鉴权、真实支付、对象存储生产联调。
Supabase Client 验证任务
前端 scaffold 后先做一个最小兼容性验证:
- H5:
@supabase/supabase-js初始化、session 持久化、token refresh、logout。 - 微信小程序:验证自定义 storage/fetch/URL polyfill 是否稳定。
- API:用 Supabase access token 调
apps/api,后端解析出可信用户。 - 安全:确认前端 bundle 中不存在 secret/service role/database/payment/storage 私钥。
如果微信小程序端 supabase-js 兼容性不稳定,小程序端改走 apps/api/auth/* 登录适配层,H5 继续使用 Supabase client 管理 Auth。