docs: simplify migration documentation

This commit is contained in:
2026-07-28 16:22:50 +08:00
parent b8d14e8a7e
commit 5732df8886
13 changed files with 484 additions and 2016 deletions

284
README.md
View File

@@ -1,267 +1,95 @@
# TIKU Backend
题库 SaaS 正式后端。项目已从旧 PocketBase / Supabase / Nest 方案收敛到 ASP.NET Core + PostgreSQL 架构。本仓库是后续开发的唯一目标后端架构决策见 [`docs/adr/0001-authoritative-dotnet-backend.md`](docs/adr/0001-authoritative-dotnet-backend.md)。
题库 SaaS 正式后端。项目已收敛到 ASP.NET Core + EF Core + PostgreSQL本仓库是后续开发的唯一目标后端架构决策见 [ADR 0001](docs/adr/0001-authoritative-dotnet-backend.md)。
当前判断很明确:现在团队已经有后端开发,继续把核心认证、权限、多租户隔离、业务一致性交给 BaaS 规则会让复杂度藏在平台、SQL policy 和前端约定之间。新后端把这些东西收回应用层和数据库约束里,开发、排查、审计都会更直接。
## 架构边界
## 当前架构定位
- 旧 NestJS / Supabase 仓库只作为业务行为和接口清单参考,不作为运行时依赖。
- 不兼容旧 Supabase 数据库、RLS、Storage bucket、Refresh Token 或旧题单 JSON。
- PostgreSQL 按标准 PostgreSQL 使用,不绑定 Supabase 托管能力。
- 普通 schema 由 EF Core entity、Fluent Configuration 和 Migration 管理;`Tiku.DbMigrator` 是迁移入口。
- 多租户隔离不使用 PostgreSQL RLS由 Host 租户解析、EF Query Filter、写入拦截器、PostgreSQL 约束和真实 PostgreSQL 测试共同保证。
- 身份、短信、对象存储、支付、通知和 AI 都通过 Application 层接口表达业务意图;第三方 SDK、密钥读取和 provider 细节只允许出现在 Infrastructure。
- 租户自定义域名由可信 Host 解析,不接受 query/header 伪造切换租户。
- 公共题库由唯一平台主体拥有;订阅有效租户可访问公共题,租户私题只属于本租户。
本项目按 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)
- [`docs/migration/phase-7-student-experience-and-content-consumption.md`](docs/migration/phase-7-student-experience-and-content-consumption.md)
- [`docs/migration/phase-8-ai-foundation.md`](docs/migration/phase-8-ai-foundation.md)
## 技术栈与分层
## 技术栈与目录
- ASP.NET Core Controller API
- Entity Framework Core + Npgsql
- PostgreSQL
- Serilog
- ZLinq
- AlibabaCloud.OSS.V2
- AlibabaCloud.SDK.Dysmsapi20170525
- AlibabaCloud OSS / SMS SDK
- Senparc.Weixin.*
- AlipaySDKNet.Standard
- Microsoft Semantic Kernel仅 Infrastructure AI 边界)
- xUnit
```text
Tiku.Api # HTTP API、认证授权、OpenAPI/Scalar
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 模型集成测试
Tiku.IntegrationTests # API / EF / PostgreSQL 集成测试
docs # ADR、架构说明和迁移路线
```
## 相比原版的主要优势
## 平台端静态原型
### 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 写入公开配置。
- AI 底座开始引入 Microsoft Semantic Kernel但包只放在 Infrastructure租户模型 API Key 走 `TenantExternalProvider(capability=ai)` + `TenantSecret`,业务层不直接引用 SK namespace。当前优先场景是租户教师后台对话和 AI 审核题目反馈。
资源存储方向:
- Infrastructure 收敛阿里云 OSS SDK 细节。
- 对象 key 默认要求租户前缀,避免资源混放。
- 上传签名前校验 MIME、大小和租户对象存储 provider。
- 下载/预览必须先过业务授权,再签发临时 URL。
- OSS HEAD metadata 用于上传确认、大小/MIME/ETag/安全扫描校验。
## 当前阶段成果
### 数据库
数据库模型迁移已经完成到 greenfield 初始 schema
平台端 demo 已作为静态文件挂到 API 项目:
```text
Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs
GET /platform-admin/
```
当前 EF 模型覆盖:
它是功能原型壳,不是正式视觉规范。默认 mock 模式;联调时通过 `runtime-config.js` 注入同源 `/api`、平台 access token provider 和 API 模式。
- 租户、用户、成员、认证、Session、短信验证码
- 地区、模块、院校、专业、科目、分类
- 平台公共题库、租户私有题库、题目引用和题目版本
- 公共分类主干、租户扩展分类、内容入口、题集和练习蓝图
- 词汇、手册、用户单词进度
- 资源、图片、App 资源、视频解析、导入任务
- 版本锁定练习、答题、收藏、错题、报告、统计
- 自定义域名 DNS/TLS 生命周期和版本化前端运行时配置
- 商品、订单、支付、权益、兑换码、优惠券
- 统一外部 Provider 配置和租户密钥
- 推广、CRM、佣金
- Banner、FAQ、公告、通知、徽章、审计
- 平台账单、催收、审计告警
- PocketBase 导入审计
当前不把后端改成 MVC/Razor也不为这个静态 demo 单独维护 Node 服务。后续平台端产品化时,建议迁为独立 React/Vite/Next 工程,.NET 继续提供 API。
### 安全与认证
## 数据库与 ORM 分工
已完成
EF Core code-first migration 负责
- 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`
- 统一异常响应和请求日志。
- 表、列、索引、普通外键;
- 普通 unique/check constraint
- 模型快照和迁移历史;
- 通过 `Tiku.DbMigrator` 显式执行迁移
完整安全策略、Host 判定矩阵和登录/Session 文字流程图见 [`docs/architecture/authentication-authorization-security.md`](docs/architecture/authentication-authorization-security.md)。
PostgreSQL guard 负责 EF 无法表达的跨表租户不变量:
### 已迁移 API
- `TenantQuestionReference` 只能引用平台公共题或当前租户私题;
- `TaxonomyNode` 父节点只能属于平台主体或当前租户;
- 需要读取 `tenants.mode` 或比较 owner 关系的条件约束。
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 只读
- student video search / play / progress
- question video list / batch query
- asset download / preview 授权签名
- profile check-in / score events
- tenant external providers / identity providers / payment providers 管理入口
- tenant-content generic import preview / sync import / async import job / import detail
- runtime bootstrap
1. SQL 只放在 `Tiku.Infrastructure/Persistence/PostgreSqlTenantConstraintSql.cs`
2. Migration 只调用 `migrationBuilder.EnsureTenantIsolationGuards()` / `DropTenantIsolationGuards()`
3. 重建 `InitialSchema` 后,`Up()` 末尾必须调用 `EnsureTenantIsolationGuards()``Down()` 开头必须调用 `DropTenantIsolationGuards()`
4. 新增 guard 前先判断能否用 EF FK / unique / check 表达;表达不了才加 PostgreSQL guard。
5. 每个 guard 必须有 migration script 断言和真实 PostgreSQL 越权测试。
## 开发约束
新增业务时默认遵守以下边界:
## 开发约束
- 新增租户实体必须实现租户 marker并通过模型测试确认 Query Filter、租户唯一索引和组合外键。
- 普通 Controller / Service 不接受可写 `tenantId`、任意 owner tenant GUID、任意 bucket 或任意 provider 细节
- 题目写接口使用 `QuestionLocator`答题接口使用 `sessionQuestionId`不得恢复裸 `QuestionId` 练习写入。
- 普通 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 测试兜底,禁止去数据库手工补
- 业务层不得直接引用第三方 SDK namespace、拼 OSS bucket、读取微信/支付/短信/AI 密钥
- 不新增 Supabase provider、Supabase URL 拼接、Supabase Storage 或 Supabase Auth 兼容层。
- AI 面向租户教师后台和题目反馈审核;租户 API Key 存 `TenantSecret`SK 类型不得进入 Domain/Application/API
## 数据库边界与 ORM 分工
## 文档入口
本项目仍然采用 EF Core code-first migration 管理数据库基线:实体、索引、外键、普通唯一约束和普通 check constraint 都应优先通过 `IEntityTypeConfiguration` 表达,并随 migration 进入代码库。
但 EF Core 不会、也不应该自动推导跨表业务不变量。以下规则必须保留为 PostgreSQL guard
- `TenantQuestionReference` 只能引用平台主体公共题或当前租户私题,不能引用其他租户私题。
- `TaxonomyNode` 的父节点只能属于平台主体或当前租户,不能挂到其他租户节点。
- 需要读取 `tenants.mode` 或按 owner 关系做条件判断的规则。
这些 guard 的维护约定固定如下:
1. 触发器 SQL 只放在 `Tiku.Infrastructure/Persistence/PostgreSqlTenantConstraintSql.cs`
2. Migration 不直接复制触发器 SQL只调用 `migrationBuilder.EnsureTenantIsolationGuards()``migrationBuilder.DropTenantIsolationGuards()`
3. 重建或压缩 `InitialSchema` 后,必须在 `Up()` 末尾调用 `EnsureTenantIsolationGuards()`,在 `Down()` 开头调用 `DropTenantIsolationGuards()`
4. 新增类似规则时,先判断能否用 EF FK / unique index / check constraint 表达;表达不了才新增 PostgreSQL guard。
5. 每个 guard 必须同时有 migration script 断言和真实 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 权限点。
- [当前认证、授权与 Host 安全策略](docs/architecture/authentication-authorization-security.md)
- [认证与授权待补强清单](docs/architecture/authentication-authorization-hardening-plan.md)
- [迁移路线与剩余范围](docs/migration-roadmap.md)
- [API 契约基线说明](docs/migration/contracts/README.md)
- [AI 底座设计](docs/migration/phase-8-ai-foundation.md)
## 常用命令
@@ -273,7 +101,7 @@ dotnet format TIKU-BACKEND.slnx --verify-no-changes --no-restore
git diff --check
```
生成数据库 SQL
生成迁移 SQL
```bash
dotnet ef migrations script \
@@ -281,7 +109,15 @@ dotnet ef migrations script \
--startup-project Tiku.DbMigrator
```
首次部署可在迁移完成后一次性创建平台超级管理员
检查模型是否有未生成 migration 的变更
```bash
dotnet ef migrations has-pending-model-changes \
--project Tiku.Infrastructure \
--startup-project Tiku.DbMigrator
```
首次部署可在迁移完成后创建平台超级管理员:
```bash
export TIKU_BOOTSTRAP_PLATFORM_ADMIN_EMAIL='admin@example.com'
@@ -290,4 +126,4 @@ export TIKU_BOOTSTRAP_PLATFORM_ADMIN_NAME='Platform Administrator'
dotnet run --project Tiku.DbMigrator -- --bootstrap-platform-admin
```
该命令只允许在不存在任何平台角色用户绑定时执行。创建的账号必须在首次登录时修改临时密码并完成 TOTP MFA 注册;检测到已有平台管理员时命令会拒绝重复引导。不要把临时密码写入仓库配置或命令行参数。
该命令只允许在不存在任何平台角色用户绑定时执行。不要把临时密码写入仓库配置或命令行参数。