forked from wangziqi/gongxue-base
637 lines
22 KiB
Markdown
637 lines
22 KiB
Markdown
# Taro 前端对接指南
|
||
|
||
更新时间:2026-06-28
|
||
|
||
目标:用一套 Taro 工程同时服务微信小程序和 H5 Web 题库,并采用“Supabase Auth/JWT + `apps/api` 业务 API 优先”的混合架构,支持多租户、品牌主题、地区题库、会员权益、销售追踪和对象存储资源。
|
||
|
||
建议新建:
|
||
|
||
```text
|
||
F:\project\apps\taro
|
||
```
|
||
|
||
旧前端参考:
|
||
|
||
```text
|
||
F:\project\参考\旧题库项目\src
|
||
```
|
||
|
||
## 启动流程
|
||
|
||
### H5
|
||
|
||
1. 从 `window.location.host` 获取当前域名。
|
||
2. 调用 `GET /api/tenant/resolve?host=<host>`。
|
||
3. 保存 `tenant.id`、`tenant.slug`、`branding`、`features`、`publicConfig`。
|
||
4. 初始化主题、Logo、页面标题、功能开关。
|
||
5. 检查本地 session token,调用 `GET /api/auth/me`。
|
||
6. 如果未登录,进入登录页;如果已登录,加载个人中心和首页数据。
|
||
|
||
### 微信小程序
|
||
|
||
1. 从编译环境或小程序启动参数读取 `tenantCode`。
|
||
2. 推广码、销售码、分享码从 `options` 或 `scene` 中解析。
|
||
3. 调用 `GET /api/tenant/resolve?tenantCode=<tenantCode>`。
|
||
4. 如存在 referral 参数,先调用 `/api/referral/resolve` 和 `/api/referral/track-event`。
|
||
5. 登录后再调用 `/api/referral/bind` 完成首绑保护。
|
||
|
||
## 请求封装
|
||
|
||
本项目不采用“前端直接写 Supabase 表替代业务命令层”的模式。Supabase 官方允许前端在 RLS 和最小权限下使用 Data API,但本系统的订单、支付、权益、租户后台、内容导入、CRM、对象存储签名等都需要服务端事务、密钥、审计和幂等,所以复杂业务命令默认调用 RPC、`apps/api`、Edge Function 或 worker。
|
||
|
||
前端可以使用 Supabase client 的范围:
|
||
|
||
- H5 Auth session/JWT。
|
||
- 小程序端在兼容性验证通过后的 Auth session/JWT。
|
||
- 低风险公开只读数据,且必须已经有 RLS、grant、跨租户测试。
|
||
- Realtime 非敏感通知。
|
||
|
||
前端必须调用 `apps/api` 的范围:
|
||
|
||
- 题库练习、答题、错题、收藏。
|
||
- 订单、支付、激活码、优惠券、权益。
|
||
- 私有 PDF、资料、视频、对象存储签名。
|
||
- 租户后台、平台后台、内容导入、CRM、销售/代理。
|
||
|
||
前端应封装一个统一 API client,所有页面禁止直接散写 `Taro.request`。
|
||
|
||
本地迁移期仍可兼容旧请求头,但新的 Taro 请求封装必须按下面目标实现:
|
||
|
||
```text
|
||
Authorization: Bearer <tk_session>
|
||
x-tenant-id: <tenantId> # 仅作为登录前/公开目录租户上下文;登录后必须与 session 租户一致
|
||
```
|
||
|
||
生产目标:
|
||
|
||
```text
|
||
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 时,推荐请求流程:
|
||
|
||
```ts
|
||
const { data } = await supabase.auth.getSession();
|
||
const accessToken = data.session?.access_token;
|
||
|
||
await api.request('/api/profile/me', {
|
||
headers: {
|
||
Authorization: `Bearer ${accessToken}`,
|
||
'x-tenant-id': tenantStore.tenantId,
|
||
},
|
||
});
|
||
```
|
||
|
||
后端会通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射用户身份。`x-tenant-id` 只能帮助确定当前租户上下文,不能让用户访问自己没有 membership 的租户。
|
||
|
||
生产或云端测试建议设置:
|
||
|
||
```text
|
||
ALLOW_LEGACY_AUTH_HEADERS=false
|
||
ALLOW_PLATFORM_ADMIN_KEY=false
|
||
```
|
||
|
||
这样旧式 `x-user-id` 和平台管理 key 会被拒绝,前端可以提前发现未按 session 接入的页面。
|
||
|
||
前端环境变量只允许包含:
|
||
|
||
```text
|
||
TARO_APP_API_BASE_URL
|
||
TARO_APP_SUPABASE_URL
|
||
TARO_APP_SUPABASE_PUBLISHABLE_KEY
|
||
```
|
||
|
||
禁止把 Supabase secret key、service role key、数据库连接串、对象存储密钥、支付私钥放进 Taro。
|
||
|
||
统一错误处理:
|
||
|
||
| HTTP | 前端动作 |
|
||
| --- | --- |
|
||
| 400 | 展示表单错误或参数错误 |
|
||
| 401 | 清 session,跳登录 |
|
||
| 403 | 展示无权限或会员升级 |
|
||
| 404 | 展示空状态 |
|
||
| 409 | 展示业务冲突,例如激活码已用 |
|
||
| 413 | 提示上传/导入文件过大 |
|
||
| 429 | 倒计时重试,例如短信冷却 |
|
||
| 500 | 展示系统异常并上报日志 |
|
||
|
||
## 全局状态建议
|
||
|
||
| Store | 内容 |
|
||
| --- | --- |
|
||
| tenantStore | tenant、branding、theme、features、publicConfig |
|
||
| authStore | session、user、roles、permissions、loginState |
|
||
| regionStore | 当前地区、可选地区、地区权益 |
|
||
| catalogStore | content entries、nodes、collections、blueprints |
|
||
| entitlementStore | SVIP 权益、视频权益、资料下载权益 |
|
||
| referralStore | inviteCode、referrer、scene、bindState |
|
||
| uiStore | 当前主题、tab、loading、toast、modal |
|
||
|
||
注意缓存必须带租户维度,例如:
|
||
|
||
```text
|
||
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`、后续微信网页/QQ provider |
|
||
| 首页 | `/api/catalog/content-entries`、`/api/catalog/banners`、`/api/catalog/announcements`、`/api/profile/me` |
|
||
| 选地区 | `/api/catalog/regions`、`/api/commerce/entitlements/check` |
|
||
| 题库入口 | `/api/catalog/content-entries` |
|
||
| 分类树 | `/api/catalog/content-nodes?entryId=...&parentId=root` |
|
||
| 题目列表 | `/api/catalog/question-collections`、`/api/catalog/question-collections/questions` |
|
||
| 开始练习 | `POST /api/learning/practice-sessions` |
|
||
| 提交答案 | `POST /api/learning/answers` |
|
||
| 交卷/报告 | `POST /api/learning/practice-sessions/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/questions/{questionId}/videos`、`POST /api/questions/videos/batch`、`POST /api/videos/play` |
|
||
| 背单词 | `/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/download` |
|
||
| 商城 | `/api/catalog/svip-plans`、`POST /api/commerce/orders`、`POST /api/commerce/payments/create` |
|
||
| 订单/权益 | `/api/commerce/orders`、`/api/commerce/entitlements` |
|
||
| 激活码兑换 | `POST /api/commerce/activation-codes/redeem` |
|
||
| 个人中心 | `GET/PATCH /api/profile/me` |
|
||
| 销售分享 | `/api/referral/resolve`、`track-event`、`bind` |
|
||
|
||
## 练习访问控制契约
|
||
|
||
前端不要先拉完整题目列表再自行判断免费额度。用户点击顺序刷题、随机刷题、全真模拟时,统一调用 `POST /api/learning/practice-sessions`,后端会根据 `content_entries.accessRules`、`content_nodes.accessRules`、`question_collections.accessRules`、`practice_blueprints.accessRules` 和当前用户权益决定最终题目快照。
|
||
|
||
请求示例:
|
||
|
||
```json
|
||
{
|
||
"mode": "sequential",
|
||
"collectionId": "00000000-0000-0000-0000-000000000615",
|
||
"questionLimit": 50
|
||
}
|
||
```
|
||
|
||
响应关键字段:
|
||
|
||
```json
|
||
{
|
||
"item": {
|
||
"id": "...",
|
||
"mode": "sequential",
|
||
"questionIds": ["..."],
|
||
"questionCount": 25,
|
||
"accessMode": "free",
|
||
"consumedFreeQuota": 25,
|
||
"accessSnapshot": {
|
||
"grantedBy": "free_quota",
|
||
"requestedCount": 50,
|
||
"grantedCount": 25,
|
||
"truncated": true,
|
||
"dailyLimit": 25
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
前端处理规则:
|
||
|
||
- 以返回的 `questionIds` 为准渲染本次练习,不要自行追加题目。
|
||
- `accessSnapshot.truncated=true` 时,可提示“今日免费额度有限,已为你开放 N 题”并引导开通 SVIP。
|
||
- `PRACTICE_FREE_LIMIT_REACHED`:弹出会员购买/激活码兑换入口。
|
||
- `PRACTICE_SVIP_REQUIRED`:提示该内容需要对应地区/科目/题库 SVIP。
|
||
- `PRACTICE_SESSION_QUESTION_FORBIDDEN`:说明提交答案的题目不在本次 session 快照内,应清理本地异常进度并重新开始。
|
||
- 提交答案必须传 `practiceSessionId`;后端会拒绝不属于本人有效 session 的题目。
|
||
|
||
### 模考交卷与报告
|
||
|
||
全真模拟、试卷模式、顺序练习的最终报告都走后端交卷接口。前端不得传分数、正确数或题目范围;后端只信任 `practice_sessions.question_ids` 快照和 `answer_records` 最新答题记录。
|
||
|
||
交卷请求:
|
||
|
||
```json
|
||
{
|
||
"practiceSessionId": "00000000-0000-0000-0000-000000000000"
|
||
}
|
||
```
|
||
|
||
响应关键字段:
|
||
|
||
```json
|
||
{
|
||
"item": {
|
||
"id": "...",
|
||
"practiceSessionId": "...",
|
||
"mode": "mock_exam",
|
||
"totalQuestions": 3,
|
||
"answeredCount": 2,
|
||
"correctCount": 1,
|
||
"wrongCount": 1,
|
||
"unansweredCount": 1,
|
||
"score": 2,
|
||
"totalScore": 100,
|
||
"accuracy": 0.3333,
|
||
"sectionStats": [
|
||
{
|
||
"key": "choice",
|
||
"title": "单选题",
|
||
"questionCount": 3,
|
||
"correctCount": 1,
|
||
"score": 2,
|
||
"totalScore": 6
|
||
}
|
||
],
|
||
"wrongQuestionIds": ["..."],
|
||
"questionResults": [
|
||
{
|
||
"questionId": "...",
|
||
"sectionKey": "choice",
|
||
"answered": true,
|
||
"isCorrect": false,
|
||
"score": 0,
|
||
"totalScore": 2,
|
||
"selectedOptions": ["0"],
|
||
"correctOptionIndices": [1],
|
||
"explanation": "..."
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
前端处理规则:
|
||
|
||
- 重复交卷是幂等的,后端会返回同一份报告。
|
||
- 报告页刷新时调用 `GET /api/learning/practice-sessions/report?practiceSessionId=...`。
|
||
- 个人中心/模考历史调用 `GET /api/learning/practice-reports?mode=mock_exam&limit=20`,也可以传 `blueprintId` 筛选某套模拟卷。
|
||
- `score` 是逐题得分合计;`totalScore` 保留后台配置的卷面总分。测试或预发数据题量不足时,两者不一定按百分制等比换算,前端展示时不要自行重算。
|
||
- 错题复盘优先使用 `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 |
|
||
|
||
错题复习创建 session:
|
||
|
||
```json
|
||
{
|
||
"mode": "wrong_review",
|
||
"questionLimit": 20
|
||
}
|
||
```
|
||
|
||
前端处理规则:
|
||
|
||
- 不要把错题 ID 列表从前端传回后端组卷;`wrong_review` 会由后端按当前用户错题本安全组卷。
|
||
- `review-plan.nextAction` 可直接用于按钮配置,但仍需使用当前登录 session 调用。
|
||
- 收藏夹复习同理可调用 `POST /api/learning/practice-sessions`,body 为 `{ "mode": "favorite_review", "questionLimit": 20 }`。
|
||
- 趋势图以接口返回日期桶为准,缺失日期后端会补 0,不需要前端补点。
|
||
|
||
### 背单词计划与复习上报
|
||
|
||
背单词页面分三类数据:单元列表、每日计划、单词进度。前端不需要计算下次复习日期,只提交“认识/不认识”,由后端统一更新 `nextReviewDate`、连续正确、掌握状态和每日复习计划。
|
||
|
||
取今日计划:
|
||
|
||
```http
|
||
GET /api/learning/vocabulary/review-plan?unitId=<unitId>&reviewLimit=30&newLimit=20
|
||
```
|
||
|
||
响应关键字段:
|
||
|
||
```json
|
||
{
|
||
"item": {
|
||
"dueCount": 3,
|
||
"newCount": 20,
|
||
"totalPlanned": 23,
|
||
"dueWords": [{ "wordId": "...", "status": "reviewing", "dueLevel": "soon" }],
|
||
"newWords": [{ "wordId": "...", "status": "new", "dueLevel": "new" }],
|
||
"words": []
|
||
}
|
||
}
|
||
```
|
||
|
||
上报单词复习结果:
|
||
|
||
```json
|
||
{
|
||
"wordId": "00000000-0000-0000-0000-000000000812",
|
||
"result": "known"
|
||
}
|
||
```
|
||
|
||
`result` 可传:
|
||
|
||
- `known`:认识/答对。
|
||
- `unknown`:不认识/答错。
|
||
|
||
前端处理规则:
|
||
|
||
- `review-plan.words` 是本轮学习队列;卡片翻转、上一个、跳转、收藏状态属于前端交互。
|
||
- 每个单词点击“认识/不认识”后调用 `POST /api/learning/vocabulary/review`。
|
||
- 返回的 `status`、`dueLevel`、`nextReviewDate` 作为后续展示依据,不在前端重算间隔。
|
||
- 旧的 `POST /api/learning/vocabulary/progress` 保留给兼容和后台手工修正;普通学习流优先用 `vocabulary/review`。
|
||
|
||
## 视频播放契约
|
||
|
||
题目视频分为 `free`、`svip`、`video_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`,不要缓存为长期资源地址。
|
||
|
||
请求示例:
|
||
|
||
```json
|
||
{
|
||
"videoId": "00000000-0000-0000-0000-000000000821",
|
||
"questionId": "00000000-0000-0000-0000-000000000401"
|
||
}
|
||
```
|
||
|
||
响应关键字段:
|
||
|
||
```json
|
||
{
|
||
"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 写入本地持久缓存。
|
||
|
||
## 题库新模型接入方式
|
||
|
||
旧项目常按“地区 -> 科目 -> 章节/试卷”固定层级处理。新项目不要写死层级,按下面模型渲染:
|
||
|
||
```text
|
||
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` 渲染;接口权限仍以后端校验为准。
|
||
- H5 自定义域名下要注意缓存隔离,不能把 A 租户主题缓存用到 B 租户。
|
||
|
||
## 登录对接
|
||
|
||
### 短信登录
|
||
|
||
开发环境可以先使用 mock 短信,接口会返回 `debugCode`。生产环境禁止依赖 `debugCode`。
|
||
|
||
```text
|
||
POST /api/auth/sms/send
|
||
body: { "phone": "13800000000", "purpose": "login" }
|
||
|
||
POST /api/auth/sms/verify
|
||
body: { "phone": "13800000000", "code": "123456", "purpose": "login" }
|
||
```
|
||
|
||
成功后保存:
|
||
|
||
```text
|
||
session.token
|
||
session.expiresAt
|
||
user
|
||
```
|
||
|
||
后续请求统一带:
|
||
|
||
```text
|
||
Authorization: Bearer <session.token>
|
||
x-tenant-id: <tenantId>
|
||
```
|
||
|
||
### 微信小程序登录
|
||
|
||
微信小程序端调用 `Taro.login()` 获取 code,然后交给后端:
|
||
|
||
```text
|
||
POST /api/auth/oauth/wechat-miniapp
|
||
body: {
|
||
"code": "<wx.login code>",
|
||
"profile": {
|
||
"nickName": "...",
|
||
"avatarUrl": "..."
|
||
}
|
||
}
|
||
```
|
||
|
||
成功响应包含:
|
||
|
||
```text
|
||
provider
|
||
user
|
||
isNewUser
|
||
session.token
|
||
session.expiresAt
|
||
identity.openId
|
||
identity.unionId
|
||
```
|
||
|
||
注意:
|
||
|
||
- 前端不接触 `appSecret`。
|
||
- 前端不会拿到微信 `session_key`。
|
||
- 如果登录前已经解析到推广码,登录成功后再调用 `/api/referral/bind` 完成首绑保护。
|
||
- 手机号授权后续应走独立的“绑定手机号”接口,不要把微信手机号解密逻辑写在页面里。
|
||
|
||
## 支付对接
|
||
|
||
支付流程必须以后端订单金额和后端回调为准,前端只负责拉起支付。
|
||
|
||
### 创建订单
|
||
|
||
```text
|
||
POST /api/commerce/orders
|
||
body: {
|
||
"planId": "<svipPlanId>",
|
||
"quantity": 1,
|
||
"payProvider": "wechat_pay | alipay",
|
||
"payMethod": "jsapi | wap",
|
||
"regionId": "<regionId>"
|
||
}
|
||
```
|
||
|
||
返回 `orderNo` 后,再创建支付参数:
|
||
|
||
```text
|
||
POST /api/commerce/payments/create
|
||
body: {
|
||
"orderNo": "<orderNo>",
|
||
"provider": "wechat_pay",
|
||
"openId": "<微信小程序登录后的 openId>"
|
||
}
|
||
```
|
||
|
||
微信小程序返回的 `paymentParams` 可直接映射到 `Taro.requestPayment`:
|
||
|
||
```text
|
||
appId
|
||
timeStamp
|
||
nonceStr
|
||
package
|
||
signType
|
||
paySign
|
||
```
|
||
|
||
支付宝 H5/WAP 返回:
|
||
|
||
```text
|
||
paymentParams.url
|
||
```
|
||
|
||
H5 可以跳转到该 URL。小程序端如果后续要接支付宝小程序,需要新增独立 provider/method,不要复用 H5 WAP URL。
|
||
|
||
支付完成后前端不要自行开通会员。前端应轮询或重新请求:
|
||
|
||
```text
|
||
GET /api/commerce/orders
|
||
GET /api/commerce/entitlements
|
||
```
|
||
|
||
后端支付回调地址由租户支付账户配置:
|
||
|
||
```text
|
||
/api/commerce/payments/notify/wechat_pay?tenantId=<tenantId>
|
||
/api/commerce/payments/notify/alipay?tenantId=<tenantId>
|
||
```
|
||
|
||
前端禁止:
|
||
|
||
- 传入自定义金额。
|
||
- 伪造支付成功状态。
|
||
- 保存商户号私钥、API v3 key、支付宝应用私钥。
|
||
- 在页面里实现 webhook 验签或权益开通。
|
||
|
||
## 第一阶段页面建议
|
||
|
||
1. `pages/bootstrap/index`
|
||
- 租户解析、主题初始化、登录态恢复。
|
||
2. `pages/login/index`
|
||
- 先接短信登录;后续接微信小程序登录。
|
||
3. `pages/home/index`
|
||
- Banner、公告、题库入口、会员入口、资料入口。
|
||
4. `pages/region/index`
|
||
- 地区选择和权益提示。
|
||
5. `pages/catalog/index`
|
||
- entry/node/collection/blueprint 通用导航。
|
||
6. `pages/practice/index`
|
||
- 刷题、答题、解析、错题、收藏。
|
||
7. `pages/vocabulary/index`
|
||
- 单词单元、学习、收藏。
|
||
8. `pages/handbook/index`
|
||
- 手册目录和阅读。
|
||
9. `pages/scoreline/index`
|
||
- 动态字段筛选和趋势。
|
||
10. `pages/profile/index`
|
||
- 会员、订单、激活码、学习数据。
|
||
|
||
## 租户后台前端建议
|
||
|
||
租户后台可以先做 H5 管理台,也可以后续使用 Taro H5 复用部分组件。优先页面:
|
||
|
||
- 概览:`/api/tenant-admin/overview`
|
||
- 品牌/主题/域名/公开设置
|
||
- 支付账户/登录 provider/密钥引用
|
||
- 用户与成员权限
|
||
- 角色模板:`GET/PUT /api/tenant-admin/role-templates`、`POST /api/tenant-admin/role-templates/disable`
|
||
- 内容入口/分类树/题目集合/练习蓝图
|
||
- 题目/单词/知识手册/分数线/视频维护
|
||
- JSON 导入 preview/import/issues
|
||
- Banner/FAQ/公告/激活码/优惠券
|
||
- 销售/代理/CRM 队列
|
||
|
||
租户后台不应在前端自行决定权限;隐藏菜单只是体验优化,接口仍会校验权限。角色模板用于让租户配置“运营、教师、销售、代理”等自定义后台体验,成员绑定模板后,前端按模板的菜单/模块/字段权限渲染,后端按 permission keys 执行真正的访问控制。
|
||
|
||
## 联调顺序
|
||
|
||
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。
|