Files
tiku-backend.net/README.md

13 KiB
Raw Blame History

TIKU Backend

题库 SaaS 正式后端。项目已从旧 PocketBase / Supabase / Nest 方案收敛到 ASP.NET Core + PostgreSQL 架构。本仓库是后续开发的唯一目标后端,架构决策见 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 伪造切换租户。
  • 公共题库由唯一平台主体拥有,订阅有效租户可访问公共题,同时租户私题只属于本租户。

阶段设计文档:

技术栈与分层

  • ASP.NET Core Controller API
  • Entity Framework Core + Npgsql
  • PostgreSQL
  • Serilog
  • ZLinq
  • AlibabaCloud.OSS.V2
  • AlibabaCloud.SDK.Dysmsapi20170525
  • Senparc.Weixin.*
  • AlipaySDKNet.Standard
  • xUnit
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 采用 v1.{tenantId}.{sessionId}.{secret} 结构,刷新和退出先解析租户再按 TenantId + SessionId + TokenHash 定位。
  • 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()FromSqlExecuteSql 和直接 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 只能是 platformtenant
  • 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 配置:

  • IdentityIIdentityProvider,默认自有 JWT、Session、密码、短信和微信认证。
  • SMSISmsProvider,只负责发送验证码或模板短信,验证码生成、哈希和频控仍在业务服务。
  • Object StorageIObjectStorageService,默认阿里云 OSSlocal_dev 仅用于本地测试。
  • PaymentIPaymentProvider,通过统一 Provider 配置加载账户和密钥。
  • NotificationINotificationProvider,默认站内通知持久化,不让业务代码跨模块直接 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

Tiku.Infrastructure/Persistence/Migrations/20260727093301_InitialSchema.cs

当前 EF 模型覆盖:

  • 租户、用户、成员、认证、Session、短信验证码
  • 地区、模块、院校、专业、科目、分类
  • 平台公共题库、租户私有题库、题目引用和题目版本
  • 公共分类主干、租户扩展分类、内容入口、题集和练习蓝图
  • 词汇、手册、用户单词进度
  • 资源、图片、App 资源、视频解析、导入任务
  • 版本锁定练习、答题、收藏、错题、报告、统计
  • 自定义域名 DNS/TLS 生命周期和版本化前端运行时配置
  • 商品、订单、支付、权益、兑换码、优惠券
  • 统一外部 Provider 配置和租户密钥
  • 推广、CRM、佣金
  • Banner、FAQ、公告、通知、徽章、审计
  • 平台账单、催收、审计告警
  • PocketBase 导入审计

安全与认证

已完成:

  • JWT Bearer 认证。
  • 数据库 Session 校验;登出/撤销后旧 token 会被拒绝。
  • 手机号 + 密码登录。
  • 短信验证码登录。
  • 微信网页 OAuth 登录。
  • 微信小程序登录。
  • 租户级身份 Provider 配置解析。
  • 当前用户 /api/me
  • 当前租户 /api/tenants/current
  • 租户公开解析和公开配置。
  • Host 解析的前端运行时配置 /api/runtime/bootstrap
  • 统一异常响应和请求日志。

已迁移 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 练习写入。
  • 自定义域名请求不得通过 tenantCodehost 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 个操作:

旧版 endpoint 总量:约 342
当前 .NET 操作237

两边路径设计并非逐字兼容,不能用 342 - 237 推算剩余工作量。详细机械比较和后续取舍入口见 docs/migration/contracts/operation-inventory.csv。旧版里有不少平台运营、商业化自动化、审计告警、积分、督导、对账等后段能力,应按新架构重新筛选。

当前阶段三和阶段四已经完成租户隔离、共享题库、学习闭环基础、运行时前端配置和外部服务解耦。后续不建议继续按旧 endpoint 数量机械补齐,应按业务闭环推进:

  1. 高级交易运营:退款、对账、调账凭证、支付异常处理。
  2. 租户内容导出与 Worker 骨架:导入异步化、导出任务、资源扫描、统计聚合。
  3. 平台后台基础:平台总览、租户管理、平台员工、平台公共题库运营。
  4. 平台账单、发票、催缴、审计告警。
  5. AI 推荐报告:学校推荐、报告生成、导出任务。
  6. 零散增强:视频观看进度、租户洞察、监督规则、更细 RBAC 权限点。

常用命令

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

dotnet ef migrations script \
  --project Tiku.Infrastructure \
  --startup-project Tiku.DbMigrator