From fc80176a90a6c79d0be966f2efefe49618910ec6 Mon Sep 17 00:00:00 2001 From: xiong Date: Sun, 26 Jul 2026 15:00:21 +0800 Subject: [PATCH] docs: summarize migration status and architecture advantages --- README.md | 342 +++++++++++++++++++----------------------------------- 1 file changed, 117 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index 3167ec4..04c2a65 100644 --- a/README.md +++ b/README.md @@ -1,241 +1,167 @@ # TIKU Backend -题库 SaaS 后端,当前目标是从旧 PocketBase / Supabase 方案迁移到可控的 ASP.NET Core + PostgreSQL 架构。 +题库 SaaS 新后端。目标是从旧 PocketBase / Supabase / Nest 方案迁到更可控的 ASP.NET Core + PostgreSQL 架构。 -这个仓库目前处在新后端初始化和安全底座建设阶段:Domain 实体、EF Core Fluent 配置、PostgreSQL 初始建库 migration、模型约束测试已经建立;API 层已经开始接入 ASP.NET Core 认证授权、本地登录、数据库 Session 和当前租户上下文。 +当前判断很明确:现在团队已经有后端开发,继续把核心认证、权限、多租户隔离、业务一致性交给 BaaS 规则,会让复杂度藏在平台、SQL policy 和前端约定之间。新后端把这些东西收回应用层和数据库约束里,开发、排查、审计都会更直接。 -## 为什么换掉 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 +- ASP.NET Core Controller API +- Entity Framework Core + Npgsql - PostgreSQL -- Npgsql - Serilog - ZLinq +- AlibabaCloud.OSS.V2 - xUnit -项目分层: - ```text -Tiku.Api # HTTP API 入口 -Tiku.Application # 应用服务、用例编排 +Tiku.Api # HTTP API、认证授权、OpenAPI/Scalar +Tiku.Application # 应用服务、用例编排、接口抽象 Tiku.Domain # 领域实体、枚举、基础类型 -Tiku.Infrastructure # EF Core、PostgreSQL 持久化配置、migration +Tiku.Infrastructure # EF Core、PostgreSQL、外部服务实现 Tiku.DbMigrator # 数据库迁移启动项目 -Tiku.Worker # 后台任务 +Tiku.Worker # 后台任务入口 Tiku.UnitTests # 单元测试 -Tiku.IntegrationTests # EF 模型/持久化约束测试 +Tiku.IntegrationTests # API / EF 模型集成测试 ``` -## 运行时地基 +## 相比原版的主要优势 -这次迁移不只是把 Node/Nest/Supabase 换成 C#,而是把旧系统没有认真处理的运行时基础补起来。 +### 1. 安全边界更清楚 -### 数据库连接 +旧版把安全逻辑分散在 Nest API、Supabase Auth/RLS、SQL policy、脚本和前端约定里。新后端改为: -- API 通过单例 `NpgsqlDataSource` 管理 PostgreSQL 连接池。 -- EF Core 使用 `AddDbContextPool`,避免每个请求重复构造完整 DbContext 依赖图。 -- 连接池参数继续交给 PostgreSQL/Npgsql connection string 配置,例如 `Maximum Pool Size`、`Minimum Pool Size`、`Timeout`、`Command Timeout`。 -- 业务代码不直接 new 连接,不绕过统一的 EF / Npgsql 配置入口。 +- ASP.NET Authentication 负责身份认证。 +- ASP.NET Authorization 负责权限策略。 +- JWT + 数据库 `auth_sessions` 负责 access/refresh/session 闭环。 +- `ICurrentUser` / `ICurrentTenant` 统一当前用户和租户上下文。 +- PostgreSQL FK / unique / check / index 负责数据完整性底线。 +- 审计事件表记录关键行为。 -### 日志 +新系统不照搬 Supabase RLS、`app_private` schema 和生产防护 SQL,权限主要在应用层实现,数据库负责硬约束。 -- API 接入 Serilog 结构化日志。 -- 启动阶段使用 bootstrap logger,避免应用启动失败时完全没有日志。 -- 请求日志统一记录 HTTP method、path、status code、elapsed 等基础字段。 -- 请求日志会补充当前用户、租户、Session、租户角色、TraceId 等上下文,后续排查“某个机构某个用户某次请求”会比旧方案清楚很多。 -- EF SQL、Microsoft 框架日志默认降噪;开发环境可提高 EF command 日志等级。 +### 2. 多租户约束不再靠“大家小心” -### 跨域 +新库以多租户为一等设计: -- CORS 作为 API 安全边界配置,不在 Controller 里散写。 -- 生产默认不放行任何 Origin,避免开发便利配置意外带到线上。 -- 开发环境默认允许本地前端常用端口: - - `http://localhost:5173` - - `http://127.0.0.1:5173` - - `http://localhost:3000` - - `http://127.0.0.1:3000` -- 不默认允许 credentials;如果后续需要 cookie 模式或管理后台单独域名,需要显式配置。 -- 多租户正式域名确定后,可以把 CORS 白名单从静态配置升级为“租户域名 + 平台管理域名”的集中策略。 +- 租户数据表默认带 `TenantId`。 +- 跨租户引用优先使用 composite FK,例如 `(tenant_id, id)`。 +- 业务 API 默认从当前请求上下文解析租户,不信任请求 body 里的 `tenantId`。 +- 租户域名、品牌、设置、角色、班级、学生运营都已经有独立模型。 -### 限流 +这比旧版在 API、RLS、前端之间反复拼 tenant 条件更可控。 -- API 接入 ASP.NET Core RateLimiter,作为全局兜底防线。 -- 匿名请求按 IP 分桶,已登录请求按用户分桶,减少同学校/同机构出口 NAT 下的误伤。 -- 默认使用固定窗口限流: - - 生产默认 `600 / minute` - - 开发默认 `1200 / minute` -- 被限流时返回标准 `429 Too Many Requests`,响应体包含 `code = rate_limited` 和 `traceId`。 -- 当前先使用进程内限流;多实例部署时,需要上移到网关/负载均衡层,或接入 Redis 等集中式限流状态。 +### 3. PostgreSQL 能力被正经使用 -### 配置校验 +- JSON 字段统一使用 C# `JsonElement`,映射 PostgreSQL `jsonb`。 +- 内容树路径使用 `ltree`。 +- 域名、兑换码等大小写不敏感字段使用 `citext`。 +- 数组使用 PostgreSQL array,不塞字符串。 +- 钱相关字段使用 cents 或明确 decimal precision。 +- 关键唯一性和状态底线落数据库约束。 -- 关键配置使用 Options validation,并在启动阶段 `ValidateOnStart()`。 -- JWT 配置会校验 issuer、audience、签名密钥长度、access/refresh token 有效期范围。 -- CORS 配置会校验 Origin 必须是绝对 `http/https` origin;开启 credentials 时必须显式配置 origin。 -- RateLimit 配置会校验 permit/window/queue 范围,避免错误配置在运行时才暴露。 +### 4. 运行时地基补齐 -### 热路径集合处理 +旧版对连接池、日志、跨域、限流、配置校验这些工程底座比较薄。新后端已经补上: -- API 项目引入 ZLinq,作为低分配集合处理工具。 -- 当前先在小范围权限判断里落模板,后续题库筛选、权限集合、菜单/内容树投影等热路径再逐步使用。 -- 不是为了炫技替换所有 LINQ;只在明确高频、低收益分配明显的路径使用。 +- 单例 `NpgsqlDataSource` + `AddDbContextPool`。 +- Serilog 结构化日志和请求上下文日志。 +- 集中 CORS 配置,生产默认不放开 Origin。 +- ASP.NET RateLimiter 全局限流。 +- Options `ValidateOnStart()` 启动校验。 +- ZLinq 作为后续热路径低分配工具。 -### 对象存储 +### 5. 资源存储不绑 Supabase -- 旧 Nest 后端资源层使用 Node `ali-oss`,不是 Supabase Storage 专用模型。 -- 新后端使用 `AlibabaCloud.OSS.V2` 作为阿里云 OSS SDK,对齐旧版的 provider / bucket / objectKey / signed URL / HEAD metadata 语义。 -- Application 层只依赖 `IObjectStorageService`,不直接依赖阿里云 SDK;SDK 细节收敛在 Infrastructure。 -- 默认对象 key 要带租户前缀,例如 `{tenantId}/...`,避免多租户资源混放后靠人工约定隔离。 -- 上传签名前会校验 MIME allowlist、文件大小、provider 是否支持托管上传。 -- 下载时如果资源已有可信 `cdnUrl`,直接返回 provider-managed URL;否则由后端在通过业务授权后签发临时 URL。 -- 上传确认使用 OSS HEAD metadata,后续可用于校验大小、MIME、ETag、SHA256 metadata 和安全扫描状态。 -- 不再把 Supabase Storage 作为默认后端存储;如果历史数据里存在 Supabase provider,只作为迁移兼容对象处理,不作为新架构依赖。 +旧版资源层实际使用 Node `ali-oss`。新后端按这个方向迁移到 `AlibabaCloud.OSS.V2`: -## 数据库策略 +- Application 只依赖 `IObjectStorageService`。 +- Infrastructure 收敛阿里云 OSS SDK 细节。 +- 对象 key 默认要求租户前缀,避免资源混放。 +- 上传签名前校验 MIME、大小和 provider。 +- 下载/预览必须先过业务授权,再签发临时 URL。 +- OSS HEAD metadata 用于上传确认、大小/MIME/ETag/安全扫描校验。 -当前数据库以 PostgreSQL 为核心能力,而不是把 PostgreSQL 当成普通 KV 存储: +## 当前阶段成果 -- JSON 字段统一使用 C# `JsonElement`,映射到 PostgreSQL `jsonb`。 -- 树形内容路径使用 `ltree`。 -- 大小写不敏感编码/域名/兑换码等使用 `citext`。 -- PostgreSQL array 使用 `text[]` / `uuid[]`,不把数组塞成字符串。 -- 钱相关字段用整型 cents 或明确 decimal precision,避免浮点误差。 -- 多租户表默认有 `TenantId`。 -- 租户内跨表引用优先使用 composite FK,例如 `(tenant_id, id)`。 -- 关键业务唯一性落数据库唯一索引或过滤唯一索引。 -- 用 check constraint 保护金额、数量、状态范围等底线。 +### 数据库 -当前 migration 已压成一个 greenfield baseline: +数据库模型迁移已经完成到 greenfield 初始 schema: ```text Tiku.Infrastructure/Persistence/Migrations/20260725220742_InitialSchema.cs ``` -因为新库还没正式上线,保留一堆开发过程 migration 意义不大。现在单个初始 migration 更适合作为新后端的起点。 +当前 EF 模型包含 124 个 DbSet,覆盖: -## 新安全策略 +- 租户、用户、成员、认证、Session、短信验证码 +- 地区、模块、院校、专业、科目、分类 +- 题库、题目、题目版本 +- 内容入口、内容节点、题集、练习蓝图 +- 词汇、手册、用户单词进度 +- 资源、图片、App 资源、视频解析、导入任务 +- 练习、答题、收藏、错题、报告、统计 +- 商品、订单、支付、权益、兑换码、优惠券 +- 推广、CRM、佣金 +- Banner、FAQ、公告、通知、徽章、审计 +- 平台账单、催收、审计告警 +- PocketBase 导入审计 -这次迁移的核心安全思路是:不照搬 Supabase RLS,而是在应用层建立明确、可测试、可审计的安全边界,同时用 PostgreSQL 约束守住数据完整性底线。 +### 安全与认证 -### 已落地 +已完成: -- ASP.NET Core Controller API 已接入统一认证/授权管线。 -- 本地认证不依赖 Supabase Auth: - - 手机号 + 密码登录 - - 短信验证码登录 - - 微信网页 OAuth 登录 - - 微信小程序 `wx.login` 登录 - - JWT access token - - 数据库 `auth_sessions` refresh/session - - `auth_login_events` 登录事件 -- JWT 不只验签,也会校验对应 `auth_sessions` 是否仍有效;登出或撤销 session 后,旧 access token 会被拒绝。 -- 当前请求上下文已经拆成应用层抽象: - - `ICurrentUser` - - `ICurrentTenant` -- 第一批授权策略已经建立: - - 已登录用户 - - 当前租户成员 - - 租户管理员 -- 第一批认证/租户接口已经建立: - - `GET /api/tenant/resolve` - - `GET /api/tenant/current-public` - - `POST /api/auth/login/password` - - `POST /api/auth/login/sms` - - `POST /api/auth/oauth/wechat` - - `POST /api/auth/oauth/wechat-miniapp` - - `POST /api/auth/refresh` - - `POST /api/auth/logout` - - `GET /api/me` - - `GET /api/tenants/current` - - `GET /api/health` -- 多租户数据表普遍包含 `TenantId`。 -- 大量租户内关系使用 composite FK,避免只靠应用代码约定租户一致性。 -- 不迁移 Supabase RLS、`app_private` schema、生产保护 policy。 -- 敏感配置不原样落明文字段,例如 CRM / 支付 / webhook 只保留 `secret_ref` 或公开配置。 -- PB 导入有专门审计表: - - `pb_import_runs` - - `pb_raw_records` - - `pb_import_issues` -- 关键业务流保留审计/事件表,例如: - - `audit_logs` - - `auth_login_events` - - `payment_events` - - `commerce_refund_events` - - `commerce_reconciliation_issue_events` - - `content_asset_access_events` - - `content_asset_security_scan_events` - - `platform_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 被拒绝 +- JWT Bearer 认证。 +- 数据库 Session 校验;登出/撤销后旧 token 会被拒绝。 +- 手机号 + 密码登录。 +- 短信验证码登录。 +- 微信网页 OAuth 登录。 +- 微信小程序登录。 +- 当前用户 `/api/me`。 +- 当前租户 `/api/tenants/current`。 +- 租户公开解析和公开配置。 +- 统一异常响应和请求日志。 -### 计划落地 +### 已迁移 API -后续 API / Worker 实现时,需要继续把下面这些策略固化成代码和测试: +当前新后端有 43 个 endpoint,其中 3 个是安全诊断接口。已完成的业务/API 闭环: -- 业务 API 不直接信任请求 body 的 `tenantId`;登录阶段可选择租户,登录后业务请求以 JWT/session 中解析出的当前租户为准。 -- API 层继续基于用户、租户成员、角色模板、权限 JSON 扩展授权策略。 -- 平台管理员、租户管理员、教师/运营、学生等权限边界继续细化到业务模块。 -- Repository / Query 层默认带租户过滤,平台级查询必须显式声明。 -- 所有跨租户资源访问必须走应用服务校验,不允许 controller 直查。 -- 审计日志覆盖高风险操作: - - 登录、认证失败、短信验证 - - 订单/支付/退款/对账调整 - - 题库导入/导出/公库采纳 - - 资产下载签名和安全扫描 - - 租户配置、域名、品牌、支付账号变更 - - 平台账单、催收、告警处理 -- Secret 管理独立化,只在应用层通过安全服务解析 `secret_ref`。 -- 短信发送目前先完成验证码表、哈希校验和限流模型,真实供应商通过接口适配接入。 -- 后台 Worker 使用最小权限的应用服务,不直接绕过业务规则写库。 -- 对账、导入、批处理任务要求幂等键和可重跑设计。 -- 生产环境连接串、密钥、对象存储凭据不进入仓库。 -- OSS 凭据通过配置/环境变量/secret 注入,兼容旧版环境变量: - - `STORAGE_DEFAULT_PROVIDER` - - `STORAGE_DEFAULT_BUCKET` - - `STORAGE_PUBLIC_BASE_URL` - - `STORAGE_REQUIRE_TENANT_PREFIX` - - `STORAGE_MAX_UPLOAD_BYTES` - - `ALIYUN_OSS_REGION` - - `ALIYUN_OSS_ENDPOINT` - - `ALIYUN_OSS_ACCESS_KEY_ID` - - `ALIYUN_OSS_ACCESS_KEY_SECRET` - - `ALIYUN_OSS_STS_TOKEN` - - `ALIYUN_OSS_INTERNAL` +- 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 授权签名 + +## 还剩多少待迁移 + +按旧 Nest API 粗略统计,旧版约 342 个 HTTP endpoint;新后端目前 43 个,其中安全诊断 3 个不算旧业务迁移。按 endpoint 数量估算: + +```text +旧版 endpoint 总量:约 342 +新后端已实现:43 +其中诊断接口:3 +已覆盖旧业务接口:约 40 +待迁移旧业务接口:约 302 +``` + +这个数字只是迁移工作量参考,不代表 302 个都应该原样照搬。旧版里有不少平台运营、商业化自动化、审计告警、积分、督导、对账等后段能力,应该按新架构重新筛选。 + +优先级建议: + +1. 运营内容只读:Banner、FAQ、公告、考试日期、商品/SVIP 套餐。 +2. 学习闭环:练习会话、提交答案、收藏、错题、单词进度。 +3. 资源管理:上传签名、上传确认、导入任务查询。 +4. 租户后台:角色、班级、学生、域名、品牌、登录 Provider。 +5. 商业化:订单、支付、权益、兑换码、优惠券。 +6. 内容管理:题目、题集、词汇、手册、视频的后台写接口。 +7. 推广/CRM/佣金。 +8. 平台后台:租户、账单、对账、退款、催收、审计告警。 +9. Worker:导入、统计、资产扫描、通知、对账、账单。 ## 常用命令 @@ -253,37 +179,3 @@ dotnet ef migrations script \ --project Tiku.Infrastructure \ --startup-project Tiku.DbMigrator ``` - -## API 迁移模板 - -后续从旧 Nest/Supabase 迁移 API 时,优先按当前认证和租户接口的模板推进: - -1. DTO 放到 `Tiku.Api/Contracts`,只包含 HTTP 输入输出形状、校验属性和 OpenAPI 描述。 -2. Request / Response DTO 字段必须写 XML documentation comments;API 项目生成 XML 文档,内置 OpenAPI 会把注释带到 Scalar schema。 -3. Controller 保持轻量,只做路由、授权、DTO 到 Application request 的映射。 -4. 业务流程放到 `Tiku.Application`,EF/外部服务实现放到 `Tiku.Infrastructure`。 -5. 已登录业务接口默认从 `ICurrentTenant` / `ICurrentUser` 取上下文,不直接信任 body 里的 `tenantId`。 -6. 公开接口只返回 branding、feature flags、public config 等可暴露字段,不泄露 secret/refund/payment/internal metadata。 -7. 每迁一个小闭环就补业务集成测试;OpenAPI 做轻量 smoke,字段注释靠迁移时同步维护,不做逐字段断言。 - -建议下一批迁移顺序: - -```text -Assets / Video 只读 API - -> 内容资源 / 图片 / 视频解析 -Operations Content 只读 API - -> 横幅 / FAQ / 公告 -``` - -## 当前状态 - -- 数据库模型迁移已完成到初始 schema。 -- migration 已整理为单个初始建库 migration。 -- API 安全底座已建立:JWT、Session、本地登录、当前用户、当前租户、基础授权策略。 -- 租户公开入口已建立:tenant resolve、public config、health。 -- Catalog 基础只读 API 已建立:地区、地区模块、模块节点、院校、专业、科目、题目分类。 -- Content Navigation 只读 API 已建立:内容入口、内容节点、题集、题集题目、练习蓝图。 -- Question Bank 只读 API 已建立:题库列表、题目列表、题目详情、题目版本。 -- Vocabulary / Handbook 只读 API 已建立:词汇单元、词汇单词、知识手册科目、章节、条目。 -- 当前模型测试、认证服务测试、API 认证/租户闭环测试、Catalog / Content Navigation / Question Bank / Study Content 只读测试通过。 -- 下一步重点是继续把题库、内容、导入、订单等业务 API 接入这套安全轨道,而不是重新散写权限判断。