Files
tiku-backend.net/README.md

269 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TIKU Backend
题库 SaaS 正式后端。项目已从旧 PocketBase / Supabase / Nest 方案收敛到 ASP.NET Core + PostgreSQL 架构。本仓库是后续开发的唯一目标后端,架构决策见 [`docs/adr/0001-authoritative-dotnet-backend.md`](docs/adr/0001-authoritative-dotnet-backend.md)。
当前判断很明确:现在团队已经有后端开发,继续把核心认证、权限、多租户隔离、业务一致性交给 BaaS 规则会让复杂度藏在平台、SQL policy 和前端约定之间。新后端把这些东西收回应用层和数据库约束里,开发、排查、审计都会更直接。
## 当前架构定位
本项目按 greenfield 后端推进,不兼容旧 Supabase 数据库、旧 RLS、旧 Storage bucket 约定、旧 Refresh Token 或旧题单 JSON。旧 NestJS/Supabase 仓库只作为业务行为和接口清单参考,不再作为运行时依赖。
核心设计取舍:
- PostgreSQL 只作为标准 PostgreSQL 使用,不绑定 Supabase 托管能力。
- 数据结构由 EF Core entity、Fluent Configuration 和 Migration 管理,`Tiku.DbMigrator` 是执行迁移的入口。
- 多租户隔离不使用 PostgreSQL RLS通过请求租户上下文、EF Core Query Filter、写入拦截器、PostgreSQL 约束和真实集成测试共同兜底。
- 身份、短信、对象存储、支付和通知全部通过 Application 层接口表达业务意图,第三方 SDK 和密钥读取只允许出现在 Infrastructure provider 边界。
- 租户自定义域名由可信 Host 解析,不接受客户端通过 query/header 伪造切换租户。
- 公共题库由唯一平台主体拥有,订阅有效租户可访问公共题,同时租户私题只属于本租户。
阶段设计文档:
- [`docs/architecture/authentication-authorization-security.md`](docs/architecture/authentication-authorization-security.md)当前认证、RBAC、DataScope、MFA、Session 与 Host 安全策略)
- [`docs/migration/phase-1-repository-baseline.md`](docs/migration/phase-1-repository-baseline.md)
- [`docs/migration/phase-2-engineering-foundation.md`](docs/migration/phase-2-engineering-foundation.md)
- [`docs/migration/phase-3-tenant-isolation-and-shared-question-bank.md`](docs/migration/phase-3-tenant-isolation-and-shared-question-bank.md)
- [`docs/migration/phase-4-external-provider-decoupling.md`](docs/migration/phase-4-external-provider-decoupling.md)
- [`docs/migration/phase-5-backoffice-worker-operations.md`](docs/migration/phase-5-backoffice-worker-operations.md)
## 技术栈与分层
- ASP.NET Core Controller API
- Entity Framework Core + Npgsql
- PostgreSQL
- Serilog
- ZLinq
- AlibabaCloud.OSS.V2
- AlibabaCloud.SDK.Dysmsapi20170525
- Senparc.Weixin.*
- AlipaySDKNet.Standard
- xUnit
```text
Tiku.Api # HTTP API、认证授权、OpenAPI/Scalar
Tiku.Application # 应用服务、用例编排、接口抽象
Tiku.Domain # 领域实体、枚举、基础类型
Tiku.Infrastructure # EF Core、PostgreSQL、外部服务实现
Tiku.DbMigrator # 数据库迁移启动项目
Tiku.Worker # 后台任务入口
Tiku.UnitTests # 单元测试
Tiku.IntegrationTests # API / EF 模型集成测试
```
## 相比原版的主要优势
### 1. 安全边界更清楚
旧版把安全逻辑分散在 Nest API、Supabase Auth/RLS、SQL policy、脚本和前端约定里。新后端改为
- ASP.NET Authentication 负责身份认证。
- ASP.NET Authorization 负责权限策略。
- JWT + 数据库 `auth_sessions` 负责 access/refresh/session 闭环。
- `ICurrentUser` / 只读 `ITenantContext` 统一当前用户和请求租户上下文。
- Refresh Token 采用 `v2.{t|p}.{tenantId|-}.{sessionId}.{secret}` 结构,刷新和退出先验证 realm/Host/tenant再通过统一 Session Store 定位并撤销 token family。
- EF Core Query Filter、写入拦截器和 PostgreSQL 组合约束共同阻断跨租户读写。
- PostgreSQL FK / unique / check / index 负责数据完整性底线。
- 审计事件表记录关键行为。
新系统不照搬 Supabase RLS、`app_private` schema 和生产防护 SQL权限主要在应用层实现数据库负责硬约束。
### 2. 多租户约束不再靠“大家小心”
新库以多租户为一等设计:
- 租户数据表默认带 `TenantId`
- 跨租户引用优先使用 composite FK例如 `(tenant_id, id)`
- 业务 API 默认从当前请求上下文解析租户,不信任请求 body 里的 `tenantId`
- 租户域名、品牌、设置、角色、班级、学生运营都已经有独立模型。
- `IgnoreQueryFilters()``FromSql``ExecuteSql` 和直接 `NpgsqlCommand` 只能出现在受审计基础设施边界。
这比旧版在 API、RLS、前端之间反复拼 tenant 条件更可控。
### 3. PostgreSQL 能力被正经使用
- JSON 字段统一使用 C# `JsonElement`,映射 PostgreSQL `jsonb`
- 内容树路径使用 `ltree`
- 域名、兑换码等大小写不敏感字段使用 `citext`
- 数组使用 PostgreSQL array不塞字符串。
- 钱相关字段使用 cents 或明确 decimal precision。
- 关键唯一性和状态底线落数据库约束。
### 4. 运行时地基补齐
旧版对连接池、日志、跨域、限流、配置校验这些工程底座比较薄。新后端已经补上:
- 单例 `NpgsqlDataSource` + scoped `AddDbContext<TikuDbContext>`,避免租户状态跨请求复用。
- Serilog 结构化日志和请求上下文日志。
- 集中 CORS 配置,生产默认不放开 Origin。
- ASP.NET RateLimiter 全局限流。
- Options `ValidateOnStart()` 启动校验。
- ZLinq 作为后续热路径低分配工具。
### 5. 公共题库和租户私库统一闭环
题库不再按“租户复制公共题”建模,而是拆成所有权和消费引用:
- 唯一 `PlatformOwned` 平台主体拥有公共题库、公共题、公共题版本和公共分类主干。
- 有效订阅租户自动访问公共题库,不通过逐题库 grant 表做主授权。
- 租户私有题库、私有题和扩展分类只属于上传租户。
- `TenantQuestionReference` 作为租户消费公共题或本租户私题的受控引用,禁止引用其他租户私题。
- API 对外只暴露 `QuestionLocator { source, questionId }`,其中 `source` 只能是 `platform``tenant`
- `PracticeSessionQuestion` 锁定题目版本,确保公共题发布新版本后,历史答题和进行中练习仍按原版本回放。
### 6. 统一前端运行时
租户通过自定义域名访问统一托管前端,后端只信任 Host 解析结果:
- 自定义域名走 `CNAME -> 统一前端/网关`,浏览器使用同域 `/api` 调后端。
- `TenantResolutionMiddleware` 在认证之前解析租户未知、Pending、禁用域名直接 404。
- JWT tenant claim 必须和 Host 解析结果一致,否则 403。
- `GET /api/runtime/bootstrap` 根据当前 Host 返回品牌、主题、功能开关、导航和首页模块。
- 配置采用 Draft / Preview / Publish公开配置禁止任意 HTML、JavaScript、外部脚本和内部密钥。
### 7. 外部服务不绑 Supabase
新后端已经完全脱离 Supabase Auth / Storage 兼容层。业务层只依赖接口和统一租户 Provider 配置:
- Identity`IIdentityProvider`,默认自有 JWT、Session、密码、短信和微信认证。
- SMS`ISmsProvider`,只负责发送验证码或模板短信,验证码生成、哈希和频控仍在业务服务。
- Object Storage`IObjectStorageService`,默认阿里云 OSS`local_dev` 仅用于本地测试。
- Payment`IPaymentProvider`,通过统一 Provider 配置加载账户和密钥。
- Notification`INotificationProvider`,默认站内通知持久化,不让业务代码跨模块直接 new 通知实体。
- Provider 配置统一落 `TenantExternalProvider` + `TenantSecret`
- `ConfigPublic` 只保存公开字段,例如 appId、merchantId、region、endpoint、bucketAlias、templateCode。
- 密钥只通过 `SecretRef` 关联 `TenantSecret`,禁止把 secret/token/key/privateKey 写入公开配置。
资源存储方向:
- Infrastructure 收敛阿里云 OSS SDK 细节。
- 对象 key 默认要求租户前缀,避免资源混放。
- 上传签名前校验 MIME、大小和租户对象存储 provider。
- 下载/预览必须先过业务授权,再签发临时 URL。
- OSS HEAD metadata 用于上传确认、大小/MIME/ETag/安全扫描校验。
## 当前阶段成果
### 数据库
数据库模型迁移已经完成到 greenfield 初始 schema
```text
Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs
```
当前 EF 模型覆盖:
- 租户、用户、成员、认证、Session、短信验证码
- 地区、模块、院校、专业、科目、分类
- 平台公共题库、租户私有题库、题目引用和题目版本
- 公共分类主干、租户扩展分类、内容入口、题集和练习蓝图
- 词汇、手册、用户单词进度
- 资源、图片、App 资源、视频解析、导入任务
- 版本锁定练习、答题、收藏、错题、报告、统计
- 自定义域名 DNS/TLS 生命周期和版本化前端运行时配置
- 商品、订单、支付、权益、兑换码、优惠券
- 统一外部 Provider 配置和租户密钥
- 推广、CRM、佣金
- Banner、FAQ、公告、通知、徽章、审计
- 平台账单、催收、审计告警
- PocketBase 导入审计
### 安全与认证
已完成:
- ASP.NET Core Identity 密码、锁定、SecurityStamp、强制改密、TOTP 和恢复码。
-`kid` 的 RSA JWT Bearer 认证和旧公钥轮换验证。
- tenant/platform 双 realm 与 Host、tenant claim、数据库 Session 联合校验。
- Session family 原子 refresh、重放撤销、logout 和 logout-all。
- 手机号/邮箱/用户名 + 密码登录、短信验证码登录和安全短信发送入口。
- 微信网页 OAuth 和微信小程序登录,外部身份不保存 `session_key`
- 数据库 tenant/platform RBAC、MFA policy、资源型授权和 DataScope SQL。
- 按有效权限生成 tenant/platform UI 菜单 bootstrap菜单不作为 API 授权依据。
- 租户级身份 Provider 配置解析。
- 当前用户 `/api/me`
- 当前租户 `/api/tenants/current`
- 租户公开解析和公开配置。
- Host 解析的前端运行时配置 `/api/runtime/bootstrap`
- 统一异常响应和请求日志。
完整安全策略、Host 判定矩阵和登录/Session 文字流程图见 [`docs/architecture/authentication-authorization-security.md`](docs/architecture/authentication-authorization-security.md)。
### 已迁移 API
2026-07-27 运行时 OpenAPI 基线包含 192 个路径、237 个操作。已完成的业务/API 闭环包括:
- health
- tenant resolve / current public
- auth password / sms / wechat / refresh / logout
- me / current tenant
- catalog 基础只读
- content navigation 只读
- question bank 只读
- vocabulary / handbook 只读
- asset / image / app asset / video catalog 只读
- asset download / preview 授权签名
- tenant external providers / identity providers / payment providers 管理入口
- runtime bootstrap
## 新开发约束
新增业务时默认遵守以下边界:
- 新增租户实体必须实现租户 marker并通过模型测试确认 Query Filter、租户唯一索引和组合外键。
- 普通 Controller / Service 不接受可写 `tenantId`、任意 owner tenant GUID、任意 bucket 或任意 provider 细节。
- 题目写接口使用 `QuestionLocator`,答题接口使用 `sessionQuestionId`,不得恢复裸 `QuestionId` 练习写入。
- 自定义域名请求不得通过 `tenantCode``host` query 或客户端转发头切换租户。
- 第三方 SDK、密钥读取、OSS bucket 拼接、微信/支付/短信 provider 细节只允许在 Infrastructure provider 实现中出现。
- 不新增 Supabase provider、Supabase URL 拼接、Supabase Storage bucket 逻辑或 Supabase Auth 兼容层。
- EF Core 管实体和 migration 生命周期;跨表租户不变量不能指望 ORM 自动推导。比如“题目引用只能指向平台或本租户”“分类父节点只能属于平台或本租户”,必须用集中 PostgreSQL trigger / constraint trigger SQL helper + migration 调用 + 真实 PostgreSQL 测试兜底,禁止去数据库手工补。
## 还剩多少待迁移
运行时契约基线显示,旧 NestJS 有 342 个操作,当前 .NET 有 237 个操作:
```text
旧版 endpoint 总量:约 342
当前 .NET 操作237
```
两边路径设计并非逐字兼容,不能用 `342 - 237` 推算剩余工作量。详细机械比较和后续取舍入口见 [`docs/migration/contracts/operation-inventory.csv`](docs/migration/contracts/operation-inventory.csv)。旧版里有不少平台运营、商业化自动化、审计告警、积分、督导、对账等后段能力,应按新架构重新筛选。
当前阶段三和阶段四已经完成租户隔离、共享题库、学习闭环基础、运行时前端配置和外部服务解耦。后续不建议继续按旧 endpoint 数量机械补齐,应按业务闭环推进:
1. 高级交易运营:退款、对账、调账凭证、支付异常处理。
2. 租户内容导出与 Worker 骨架:导入异步化、导出任务、资源扫描、统计聚合。
3. 平台后台基础:平台总览、租户管理、平台员工、平台公共题库运营。
4. 平台账单、发票、催缴、审计告警。
5. AI 推荐报告:学校推荐、报告生成、导出任务。
6. 零散增强:视频观看进度、租户洞察、监督规则、更细 RBAC 权限点。
## 常用命令
```bash
dotnet restore TIKU-BACKEND.slnx
dotnet build TIKU-BACKEND.slnx --no-restore
dotnet test TIKU-BACKEND.slnx --no-build
dotnet format TIKU-BACKEND.slnx --verify-no-changes --no-restore
git diff --check
```
生成数据库 SQL
```bash
dotnet ef migrations script \
--project Tiku.Infrastructure \
--startup-project Tiku.DbMigrator
```
首次部署可在迁移完成后一次性创建平台超级管理员:
```bash
export TIKU_BOOTSTRAP_PLATFORM_ADMIN_EMAIL='admin@example.com'
export TIKU_BOOTSTRAP_PLATFORM_ADMIN_PASSWORD='replace-with-a-strong-temporary-password'
export TIKU_BOOTSTRAP_PLATFORM_ADMIN_NAME='Platform Administrator'
dotnet run --project Tiku.DbMigrator -- --bootstrap-platform-admin
```
该命令只允许在不存在任何平台角色用户绑定时执行。创建的账号必须在首次登录时修改临时密码并完成 TOTP MFA 注册;检测到已有平台管理员时命令会拒绝重复引导。不要把临时密码写入仓库配置或命令行参数。