# API 目录规范

`apps/api` 是 Web、Taro 小程序、管理后台共用的业务命令 API。项目采用 Supabase-first 架构：简单安全的数据访问可以走 Supabase table/view/RPC + RLS；复杂业务写入、第三方 provider、密钥、webhook、导入、支付、权益、审计进入 `apps/api`、Edge Functions 或 worker。

## 当前结构

```text
apps/api/src/
  server.ts                 HTTP 服务入口，只负责请求生命周期
  core/
    config.ts               环境变量和运行配置
    db.ts                   PostgreSQL 连接池和查询封装
    http.ts                 CORS、JSON 响应、统一错误
    router.ts               汇总注册各业务域路由
  features/
    auth/                   短信验证码、迁移期 session、微信小程序登录、OAuth provider 预留
    health/                 健康检查
    tenant/                 租户解析、品牌配置、域名识别
    catalog/                公开题库、内容入口、分类树、题目集合、练习蓝图、手册、商城、资料资源只读接口
    learning/               组卷 session、答题、错题、收藏、练习进度、排行榜（租户默认关闭）
    commerce/               订单、支付确认、退款、激活码、优惠券、权益
    referral/               销售/代理客资追踪、首绑保护、团队关系、CRM 队列
    storage/                对象存储签名 provider
    video/                  题目视频列表、搜索、SVIP/次数校验和签名播放
    platform-admin/         平台方权限、SaaS 租户、订阅、订阅账单候选/批量生成、账单、使用量
    tenant-admin/           租户品牌、域名、公开设置、登录/商户配置、成员权限、活动/兑换码运营
    tenant-content/         租户后台内容维护：入口、分类树、集合、练习蓝图、题目、视频、分数线、单词、知识手册、资料资源、批量导入
```

## 新业务域落位

后续按下面方式增加目录：

```text
features/
  auth/                     登录、绑定手机、OAuth 回调、会话换取
  learning/                 顺序/随机/模考组卷、答题记录、错题、收藏、学习进度、排行榜（租户默认关闭）
  commerce/                 商品、订单、优惠券、支付、退款、权益开通
  referral/                 销售/代理增长链路、客资归属、分佣依据、CRM 入队
  platform-admin/           平台租户管理、年费、服务费、批量开票、账务审计
  tenant-admin/             合作商后台配置、品牌、域名、收款账户、登录 provider、密钥掩码、成员权限、审计、活动、兑换码、优惠券
  tenant-content/           合作商内容导航、题库维护、批量导入、资源绑定、内容审计
```

每个 feature 默认包含：

```text
index.ts                   导出 RouteDefinition[]
routes.ts                  HTTP handler
service.ts                 业务编排和事务
repository.ts              SQL 查询和写入
types.ts                   仅本领域使用的类型
```

## 规则

- `server.ts` 不直接 import 业务 handler，只 import `createRouter()`。
- `features/*/index.ts` 只注册路由，不写 SQL。
- `routes.ts` 做参数解析、鉴权上下文、HTTP 错误，不写复杂事务。
- `service.ts` 承接订单、支付、权益、答题判定等业务规则。
- `repository.ts` 才写 SQL，所有 SQL 必须带明确租户边界。
- 可预期错误用 `HttpError`，生产环境不向前端暴露内部异常。
- 写接口必须考虑幂等、审计和租户隔离；支付 webhook 必须先设计幂等键。
- 不要为了少写 API 而让前端直写复杂业务表。新增前端直连 table/view/RPC 必须先满足 RLS、最小 grant、跨租户测试、权限测试和索引要求。
- 新接口优先使用 `Authorization: Bearer <supabase_access_token>`；后端通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射业务身份。
- 迁移期仍支持 `Authorization: Bearer tk_*` session；session 明文只返回客户端，数据库只保存 hash。
- `x-user-id` 和 `x-platform-admin-key` 只允许在非生产兼容模式使用；生产必须关闭 `ALLOW_LEGACY_AUTH_HEADERS` 和 `ALLOW_PLATFORM_ADMIN_KEY`。
- `platform-admin` 路由必须用 `requirePlatformAdmin(ctx, '<platform:scope:action>')` 校验平台权限；前端可用 `GET /api/platform-admin/permissions` 读取权限目录和当前账号 `effective` 能力，但不能把菜单隐藏当成安全边界。
- `platform-admin` 管平台与合作商之间的 SaaS 账务，`tenant-admin` 管合作商自己的品牌、域名、公开配置、登录/商户配置、活动和兑换码，`tenant-content` 管合作商自己的题库和学习内容维护。
- `tenant-admin` 的敏感配置必须拆分：公开字段进入 `config_public`，商户密钥、短信密钥、OAuth app secret 进入 `app_private.tenant_secrets` 或生产 KMS/Vault；对前端只返回 `secretRef` 和掩码状态。
- `tenant-admin` 权限由 `tenant_memberships.role` 的默认权限和 `permissions` JSON 覆盖共同决定；后端接口必须校验具体权限点，不能只依赖前端菜单隐藏。
- `referral` 是增长/客资业务域，负责邀请码、扫码事件、首绑保护、销售/代理团队归属和 CRM 入队；真实 CRM webhook 发送应由 worker 处理，API 只负责幂等入队。
- 题库前端入口不再只依赖旧 `module_nodes/subjects/categories`；新业务主模型是 `content_entries/content_nodes/question_collections/practice_blueprints`，用于表达可视化入口、多级分类、考试意向标记、题目列表和顺序/随机/全真模拟规则。
- 平台公共题库不能被租户前端直接跨租户读取；平台侧通过 `/api/platform-admin/question-bank-grants` 授权，租户侧通过 `/api/tenant-content/public-question-banks/adopt` 采纳为本租户题库、入口、集合和题目快照。后续版本同步必须走 worker 和审计。
- `learning` 创建练习 session 时必须保存 `question_ids` 快照，避免随机刷题和模考过程中题目集合变化导致答题记录无法复盘。
- 排行榜必须由后端按租户、地区、班级和可信用户上下文聚合，并受 `tenant_settings.feature_flags.enableLeaderboard` 控制，默认关闭；前端不能自行扫描答题记录、积分流水或单词进度后排名，也不能在个人中心默认请求排行榜；后续高流量场景再通过 worker/materialized view 做日榜、周榜和防刷。
- 学生头像是产品级安全边界：学生端、租户后台学生运营、批量导入、批量分班都不能写 `avatarUrl/avatar_url/avatar/headimgurl/figureurl` 等字段，也不能通过学生接口写 `primaryRole/primary_role`；学生展示头像统一来自 `student_profiles.avatar_preset` 的男女默认头像。
- 资料、PDF、视频等对象存储资源必须先进入 `content_assets` 台账，再通过 API/Edge Function 做权限校验和签名 URL 下发；前端不能直接拼 OSS/COS/Supabase Storage 私有地址。
- 批量导入必须先写 `content_import_jobs/items/issues`，保留原始 payload、规范化 payload、逐行问题和审计记录；题目、单词、知识手册、分数线和视频 JSON/CSV/Excel 都应进入同一管线，复杂大批量导入通过 `executionMode=async` 交给 imports worker 消费。
