forked from xiongyuxing/tiku-backend.net
12 KiB
12 KiB
TIKU Backend
题库 SaaS 后端,当前目标是从旧 PocketBase / Supabase 方案迁移到可控的 ASP.NET Core + PostgreSQL 架构。
这个仓库目前处在新后端初始化和安全底座建设阶段:Domain 实体、EF Core Fluent 配置、PostgreSQL 初始建库 migration、模型约束测试已经建立;API 层已经开始接入 ASP.NET Core 认证授权、本地登录、数据库 Session 和当前租户上下文。
为什么换掉 Supabase
旧方案的问题不只是“用了 Supabase”,而是安全、权限、业务约束分散在太多地方:
- 应用代码、Supabase RLS、SQL policy、Edge/脚本、前端约定之间边界不清。
- 多租户业务复杂后,RLS policy 会越来越难审查,调试成本高。
- 团队现在已经有后端开发,继续把核心权限和业务一致性绑在 BaaS 规则上,反而增加维护难度。
- 复杂导入、对账、退款、CRM、资产安全扫描、平台账单等后台任务,更适合由明确的 Worker + 应用层权限 + 数据库约束来承载。
- 新系统需要长期演进,直接掌控 PostgreSQL schema、迁移、索引、FK、事务边界,会比依赖 Supabase 平台约束更可控。
新的方向不是“数据库裸奔”,而是把职责重新分层:
- ASP.NET Core 负责认证、授权、租户上下文、业务流程、审计记录。
- PostgreSQL 负责数据完整性、外键、唯一约束、检查约束、索引、JSONB/数组/ltree/citext 等原生能力。
- Worker 负责导入、对账、退款、通知、资产扫描、统计等后台任务。
- 测试负责持续验证 EF 模型和数据库约束没有退化。
技术栈
- .NET / ASP.NET Core
- Entity Framework Core
- PostgreSQL
- Npgsql
- Serilog
- ZLinq
- xUnit
项目分层:
Tiku.Api # HTTP API 入口
Tiku.Application # 应用服务、用例编排
Tiku.Domain # 领域实体、枚举、基础类型
Tiku.Infrastructure # EF Core、PostgreSQL 持久化配置、migration
Tiku.DbMigrator # 数据库迁移启动项目
Tiku.Worker # 后台任务
Tiku.UnitTests # 单元测试
Tiku.IntegrationTests # EF 模型/持久化约束测试
运行时地基
这次迁移不只是把 Node/Nest/Supabase 换成 C#,而是把旧系统没有认真处理的运行时基础补起来。
数据库连接
- API 通过单例
NpgsqlDataSource管理 PostgreSQL 连接池。 - EF Core 使用
AddDbContextPool<TikuDbContext>,避免每个请求重复构造完整 DbContext 依赖图。 - 连接池参数继续交给 PostgreSQL/Npgsql connection string 配置,例如
Maximum Pool Size、Minimum Pool Size、Timeout、Command Timeout。 - 业务代码不直接 new 连接,不绕过统一的 EF / Npgsql 配置入口。
日志
- API 接入 Serilog 结构化日志。
- 启动阶段使用 bootstrap logger,避免应用启动失败时完全没有日志。
- 请求日志统一记录 HTTP method、path、status code、elapsed 等基础字段。
- 请求日志会补充当前用户、租户、Session、租户角色、TraceId 等上下文,后续排查“某个机构某个用户某次请求”会比旧方案清楚很多。
- EF SQL、Microsoft 框架日志默认降噪;开发环境可提高 EF command 日志等级。
跨域
- CORS 作为 API 安全边界配置,不在 Controller 里散写。
- 生产默认不放行任何 Origin,避免开发便利配置意外带到线上。
- 开发环境默认允许本地前端常用端口:
http://localhost:5173http://127.0.0.1:5173http://localhost:3000http://127.0.0.1:3000
- 不默认允许 credentials;如果后续需要 cookie 模式或管理后台单独域名,需要显式配置。
- 多租户正式域名确定后,可以把 CORS 白名单从静态配置升级为“租户域名 + 平台管理域名”的集中策略。
限流
- API 接入 ASP.NET Core RateLimiter,作为全局兜底防线。
- 匿名请求按 IP 分桶,已登录请求按用户分桶,减少同学校/同机构出口 NAT 下的误伤。
- 默认使用固定窗口限流:
- 生产默认
600 / minute - 开发默认
1200 / minute
- 生产默认
- 被限流时返回标准
429 Too Many Requests,响应体包含code = rate_limited和traceId。 - 当前先使用进程内限流;多实例部署时,需要上移到网关/负载均衡层,或接入 Redis 等集中式限流状态。
配置校验
- 关键配置使用 Options validation,并在启动阶段
ValidateOnStart()。 - JWT 配置会校验 issuer、audience、签名密钥长度、access/refresh token 有效期范围。
- CORS 配置会校验 Origin 必须是绝对
http/httpsorigin;开启 credentials 时必须显式配置 origin。 - RateLimit 配置会校验 permit/window/queue 范围,避免错误配置在运行时才暴露。
热路径集合处理
- API 项目引入 ZLinq,作为低分配集合处理工具。
- 当前先在小范围权限判断里落模板,后续题库筛选、权限集合、菜单/内容树投影等热路径再逐步使用。
- 不是为了炫技替换所有 LINQ;只在明确高频、低收益分配明显的路径使用。
数据库策略
当前数据库以 PostgreSQL 为核心能力,而不是把 PostgreSQL 当成普通 KV 存储:
- JSON 字段统一使用 C#
JsonElement,映射到 PostgreSQLjsonb。 - 树形内容路径使用
ltree。 - 大小写不敏感编码/域名/兑换码等使用
citext。 - PostgreSQL array 使用
text[]/uuid[],不把数组塞成字符串。 - 钱相关字段用整型 cents 或明确 decimal precision,避免浮点误差。
- 多租户表默认有
TenantId。 - 租户内跨表引用优先使用 composite FK,例如
(tenant_id, id)。 - 关键业务唯一性落数据库唯一索引或过滤唯一索引。
- 用 check constraint 保护金额、数量、状态范围等底线。
当前 migration 已压成一个 greenfield baseline:
Tiku.Infrastructure/Persistence/Migrations/20260725220742_InitialSchema.cs
因为新库还没正式上线,保留一堆开发过程 migration 意义不大。现在单个初始 migration 更适合作为新后端的起点。
新安全策略
这次迁移的核心安全思路是:不照搬 Supabase RLS,而是在应用层建立明确、可测试、可审计的安全边界,同时用 PostgreSQL 约束守住数据完整性底线。
已落地
- ASP.NET Core Controller API 已接入统一认证/授权管线。
- 本地认证不依赖 Supabase Auth:
- 手机号 + 密码登录
- 短信验证码登录
- 微信网页 OAuth 登录
- 微信小程序
wx.login登录 - JWT access token
- 数据库
auth_sessionsrefresh/session auth_login_events登录事件
- JWT 不只验签,也会校验对应
auth_sessions是否仍有效;登出或撤销 session 后,旧 access token 会被拒绝。 - 当前请求上下文已经拆成应用层抽象:
ICurrentUserICurrentTenant
- 第一批授权策略已经建立:
- 已登录用户
- 当前租户成员
- 租户管理员
- 第一批认证/租户接口已经建立:
GET /api/tenant/resolveGET /api/tenant/current-publicPOST /api/auth/login/passwordPOST /api/auth/login/smsPOST /api/auth/oauth/wechatPOST /api/auth/oauth/wechat-miniappPOST /api/auth/refreshPOST /api/auth/logoutGET /api/meGET /api/tenants/currentGET /api/health
- 多租户数据表普遍包含
TenantId。 - 大量租户内关系使用 composite FK,避免只靠应用代码约定租户一致性。
- 不迁移 Supabase RLS、
app_privateschema、生产保护 policy。 - 敏感配置不原样落明文字段,例如 CRM / 支付 / webhook 只保留
secret_ref或公开配置。 - PB 导入有专门审计表:
pb_import_runspb_raw_recordspb_import_issues
- 关键业务流保留审计/事件表,例如:
audit_logsauth_login_eventspayment_eventscommerce_refund_eventscommerce_reconciliation_issue_eventscontent_asset_access_eventscontent_asset_security_scan_eventsplatform_audit_alerts
- 模型测试覆盖:
- 表数量和表名
- JSONB 映射
ltree映射- 关键唯一索引
- decimal precision
- 租户关系 composite FK
- PB raw record 可追踪主键
- API / Auth 测试覆盖:
- 租户按
tenantCode/host解析 - 公开品牌、主题、功能开关和 public config 返回
- health smoke test
- 未登录返回 401
- 无权限返回 403
- 密码登录成功/失败
- 短信验证码登录成功/失败
- 微信登录 upsert 用户、身份和租户成员
- 登录后访问当前用户和当前租户
- 登出后旧 access token / refresh token 被拒绝
- 租户按
计划落地
后续 API / Worker 实现时,需要继续把下面这些策略固化成代码和测试:
- 业务 API 不直接信任请求 body 的
tenantId;登录阶段可选择租户,登录后业务请求以 JWT/session 中解析出的当前租户为准。 - API 层继续基于用户、租户成员、角色模板、权限 JSON 扩展授权策略。
- 平台管理员、租户管理员、教师/运营、学生等权限边界继续细化到业务模块。
- Repository / Query 层默认带租户过滤,平台级查询必须显式声明。
- 所有跨租户资源访问必须走应用服务校验,不允许 controller 直查。
- 审计日志覆盖高风险操作:
- 登录、认证失败、短信验证
- 订单/支付/退款/对账调整
- 题库导入/导出/公库采纳
- 资产下载签名和安全扫描
- 租户配置、域名、品牌、支付账号变更
- 平台账单、催收、告警处理
- Secret 管理独立化,只在应用层通过安全服务解析
secret_ref。 - 短信发送目前先完成验证码表、哈希校验和限流模型,真实供应商通过接口适配接入。
- 后台 Worker 使用最小权限的应用服务,不直接绕过业务规则写库。
- 对账、导入、批处理任务要求幂等键和可重跑设计。
- 生产环境连接串、密钥、对象存储凭据不进入仓库。
常用命令
dotnet build /home/xiong/Games/Anii/TIKU-BACKEND/TIKU-BACKEND.slnx
dotnet test /home/xiong/Games/Anii/TIKU-BACKEND/TIKU-BACKEND.slnx
dotnet format /home/xiong/Games/Anii/TIKU-BACKEND/TIKU-BACKEND.slnx --verify-no-changes
git -C /home/xiong/Games/Anii/TIKU-BACKEND diff --check
生成数据库 SQL:
dotnet ef migrations script \
--project Tiku.Infrastructure \
--startup-project Tiku.DbMigrator
API 迁移模板
后续从旧 Nest/Supabase 迁移 API 时,优先按当前认证和租户接口的模板推进:
- DTO 放到
Tiku.Api/Contracts,只包含 HTTP 输入输出形状、校验属性和 OpenAPI 描述。 - Request / Response DTO 字段必须写 XML documentation comments;API 项目生成 XML 文档,内置 OpenAPI 会把注释带到 Scalar schema。
- Controller 保持轻量,只做路由、授权、DTO 到 Application request 的映射。
- 业务流程放到
Tiku.Application,EF/外部服务实现放到Tiku.Infrastructure。 - 已登录业务接口默认从
ICurrentTenant/ICurrentUser取上下文,不直接信任 body 里的tenantId。 - 公开接口只返回 branding、feature flags、public config 等可暴露字段,不泄露 secret/refund/payment/internal metadata。
- 每迁一个小闭环就补集成测试和 Scalar/OpenAPI 描述,关键 request / response schema 要有字段 description 断言,测试通过后单独提交。
建议下一批迁移顺序:
Catalog 基础只读 API
-> 地区 / 模块 / 学校 / 专业 / 科目 / 分类
Content Navigation 只读 API
-> 内容入口 / 内容树 / 题集 / 练习蓝图
Question Bank 只读 API
-> 题库列表 / 题目详情 / 题目版本
当前状态
- 数据库模型迁移已完成到初始 schema。
- migration 已整理为单个初始建库 migration。
- API 安全底座已建立:JWT、Session、本地登录、当前用户、当前租户、基础授权策略。
- 租户公开入口已建立:tenant resolve、public config、health。
- 当前模型测试、认证服务测试、API 认证/租户闭环测试通过。
- 下一步重点是继续把题库、内容、导入、订单等业务 API 接入这套安全轨道,而不是重新散写权限判断。