From 71793a2de00819dc5a7ba46ebb9326e35363a50f Mon Sep 17 00:00:00 2001 From: xiong Date: Mon, 27 Jul 2026 17:56:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E8=A7=A3=E8=80=A6=E4=B8=8E=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E7=BA=A7=20Provider=20=E6=A8=A1=E5=9D=97=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20Supabase=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 93 ++++++++++-- docs/adr/0001-authoritative-dotnet-backend.md | 3 +- docs/migration-roadmap.md | 6 +- .../phase-4-external-provider-decoupling.md | 141 ++++++++++++++++++ 4 files changed, 227 insertions(+), 16 deletions(-) create mode 100644 docs/migration/phase-4-external-provider-decoupling.md diff --git a/README.md b/README.md index 5d13bf9..985e7b9 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,26 @@ 当前判断很明确:现在团队已经有后端开发,继续把核心认证、权限、多租户隔离、业务一致性交给 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/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) + ## 技术栈与分层 - ASP.NET Core Controller API @@ -35,6 +55,7 @@ Tiku.IntegrationTests # API / EF 模型集成测试 - 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 负责数据完整性底线。 - 审计事件表记录关键行为。 @@ -49,6 +70,7 @@ Tiku.IntegrationTests # API / EF 模型集成测试 - 跨租户引用优先使用 composite FK,例如 `(tenant_id, id)`。 - 业务 API 默认从当前请求上下文解析租户,不信任请求 body 里的 `tenantId`。 - 租户域名、品牌、设置、角色、班级、学生运营都已经有独立模型。 +- `IgnoreQueryFilters()`、`FromSql`、`ExecuteSql` 和直接 `NpgsqlCommand` 只能出现在受审计基础设施边界。 这比旧版在 API、RLS、前端之间反复拼 tenant 条件更可控。 @@ -72,13 +94,43 @@ Tiku.IntegrationTests # API / EF 模型集成测试 - Options `ValidateOnStart()` 启动校验。 - ZLinq 作为后续热路径低分配工具。 -### 5. 资源存储不绑 Supabase +### 5. 公共题库和租户私库统一闭环 -旧版资源层实际使用 Node `ali-oss`。新后端已经按这个方向迁移到 `AlibabaCloud.OSS.V2`: +题库不再按“租户复制公共题”建模,而是拆成所有权和消费引用: + +- 唯一 `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 写入公开配置。 + +资源存储方向: -- Application 只依赖 `IObjectStorageService`。 - Infrastructure 收敛阿里云 OSS SDK 细节。 -- 租户外部服务统一通过 `TenantExternalProvider` + `TenantSecret` 配置。 - 对象 key 默认要求租户前缀,避免资源混放。 - 上传签名前校验 MIME、大小和租户对象存储 provider。 - 下载/预览必须先过业务授权,再签发临时 URL。 @@ -105,6 +157,7 @@ Tiku.Infrastructure/Persistence/Migrations/20260727093301_InitialSchema.cs - 版本锁定练习、答题、收藏、错题、报告、统计 - 自定义域名 DNS/TLS 生命周期和版本化前端运行时配置 - 商品、订单、支付、权益、兑换码、优惠券 +- 统一外部 Provider 配置和租户密钥 - 推广、CRM、佣金 - Banner、FAQ、公告、通知、徽章、审计 - 平台账单、催收、审计告警 @@ -120,9 +173,11 @@ Tiku.Infrastructure/Persistence/Migrations/20260727093301_InitialSchema.cs - 短信验证码登录。 - 微信网页 OAuth 登录。 - 微信小程序登录。 +- 租户级身份 Provider 配置解析。 - 当前用户 `/api/me`。 - 当前租户 `/api/tenants/current`。 - 租户公开解析和公开配置。 +- Host 解析的前端运行时配置 `/api/runtime/bootstrap`。 - 统一异常响应和请求日志。 ### 已迁移 API @@ -139,6 +194,19 @@ Tiku.Infrastructure/Persistence/Migrations/20260727093301_InitialSchema.cs - 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 兼容层。 ## 还剩多少待迁移 @@ -151,17 +219,14 @@ Tiku.Infrastructure/Persistence/Migrations/20260727093301_InitialSchema.cs 两边路径设计并非逐字兼容,不能用 `342 - 237` 推算剩余工作量。详细机械比较和后续取舍入口见 [`docs/migration/contracts/operation-inventory.csv`](docs/migration/contracts/operation-inventory.csv)。旧版里有不少平台运营、商业化自动化、审计告警、积分、督导、对账等后段能力,应按新架构重新筛选。 -优先级建议: +当前阶段三和阶段四已经完成租户隔离、共享题库、学习闭环基础、运行时前端配置和外部服务解耦。后续不建议继续按旧 endpoint 数量机械补齐,应按业务闭环推进: -1. 运营内容只读:Banner、FAQ、公告、考试日期、商品/SVIP 套餐。 -2. 学习闭环:练习会话、提交答案、收藏、错题、单词进度。 -3. 资源管理:上传签名、上传确认、导入任务查询。 -4. 租户后台:角色、班级、学生、域名、品牌、登录 Provider。 -5. 商业化:订单、支付、权益、兑换码、优惠券。 -6. 内容管理:题目、题集、词汇、手册、视频的后台写接口。 -7. 推广/CRM/佣金。 -8. 平台后台:租户、账单、对账、退款、催收、审计告警。 -9. Worker:导入、统计、资产扫描、通知、对账、账单。 +1. 高级交易运营:退款、对账、调账凭证、支付异常处理。 +2. 租户内容导出与 Worker 骨架:导入异步化、导出任务、资源扫描、统计聚合。 +3. 平台后台基础:平台总览、租户管理、平台员工、平台公共题库运营。 +4. 平台账单、发票、催缴、审计告警。 +5. AI 推荐报告:学校推荐、报告生成、导出任务。 +6. 零散增强:视频观看进度、租户洞察、监督规则、更细 RBAC 权限点。 ## 常用命令 diff --git a/docs/adr/0001-authoritative-dotnet-backend.md b/docs/adr/0001-authoritative-dotnet-backend.md index 5d5fb6f..bb934d9 100644 --- a/docs/adr/0001-authoritative-dotnet-backend.md +++ b/docs/adr/0001-authoritative-dotnet-backend.md @@ -13,7 +13,7 @@ 2. 旧 NestJS 仓库冻结为业务行为、接口契约和数据迁移参考;除迁移阻断问题外,不再承接新功能。 3. EF Core Migration 是普通数据库结构变更的唯一权威历史。生产环境通过 `Tiku.DbMigrator` 显式执行经审查的迁移,API 不在启动时自动同步结构。 4. 应用只依赖标准 PostgreSQL 能力和 Npgsql,不再以 Supabase 作为运行时依赖或托管目标。 -5. 身份、对象存储、短信、支付和通知通过 Application 层接口隔离供应商,并通过租户级 `TenantExternalProvider` + `TenantSecret` 配置。 +5. 身份、对象存储、短信、支付和通知通过 Application 层接口隔离供应商,并通过租户级 `TenantExternalProvider` + `TenantSecret` 配置;不建设 Supabase Auth/Storage 兼容层。 6. Yudao 不作为运行时依赖,只参考其 RBAC、租户套餐、审计和后台产品设计。 7. 新功能按完整领域闭环迁移和验收,不以 Controller 或 endpoint 数量作为完成标准。 @@ -27,5 +27,6 @@ ## 结果 - 不再建设长期双后端或双写链路。 +- 不再保留 Supabase 作为认证、存储或数据库运行时备选目标。 - 更换 PostgreSQL 托管商或外部服务供应商不要求重写业务代码。 - 后续开发先补真实 PostgreSQL 测试、强制租户边界、生产配置 fail-fast 和敏感配置加密,再扩展业务模块。 diff --git a/docs/migration-roadmap.md b/docs/migration-roadmap.md index e59422d..5ce20bd 100644 --- a/docs/migration-roadmap.md +++ b/docs/migration-roadmap.md @@ -7,13 +7,17 @@ - [`docs/adr/0001-authoritative-dotnet-backend.md`](adr/0001-authoritative-dotnet-backend.md) - [`docs/migration/phase-1-repository-baseline.md`](migration/phase-1-repository-baseline.md) - [`docs/migration/phase-2-engineering-foundation.md`](migration/phase-2-engineering-foundation.md) +- [`docs/migration/phase-3-tenant-isolation-and-shared-question-bank.md`](migration/phase-3-tenant-isolation-and-shared-question-bank.md) +- [`docs/migration/phase-4-external-provider-decoupling.md`](migration/phase-4-external-provider-decoupling.md) 迁移原则: - 不追求旧接口逐字兼容,新前端按新 REST API 对接。 - 不照搬 Supabase Auth/RLS,权限在 ASP.NET Authentication / Authorization 和应用服务里收口。 +- 不保留 Supabase 运行时依赖、Storage provider 或 Auth 兼容层。 - 数据一致性落 PostgreSQL FK / unique / check / index 约束。 - 多租户数据默认带 `TenantId`,跨租户引用优先使用 composite FK。 +- 外部身份、短信、对象存储、支付和通知都通过 Application 接口与 `TenantExternalProvider` 配置解耦。 - JSON 字段使用 C# `JsonElement` + PostgreSQL `jsonb`,不落字符串。 - 旧版明显是占位、临时脚本或平台自动化的部分,不直接硬搬,先重新设计边界。 @@ -29,7 +33,7 @@ - CORS / RateLimiter / Options 校验。 - Scalar / OpenAPI 基础入口。 - 统一租户外部服务配置:`TenantExternalProvider` + `TenantSecret`。 -- 阿里云 OSS V2 资源存储抽象,业务层不感知 Supabase Storage 或 bucket 细节。 +- 身份、短信、阿里云 OSS、支付和通知 Provider 抽象,业务层不感知 Supabase Storage、OSS bucket、微信/支付/短信 SDK 或密钥读取细节。 - Senparc 微信登录与小程序码生成边界。 ### 数据库 diff --git a/docs/migration/phase-4-external-provider-decoupling.md b/docs/migration/phase-4-external-provider-decoupling.md new file mode 100644 index 0000000..f9d5396 --- /dev/null +++ b/docs/migration/phase-4-external-provider-decoupling.md @@ -0,0 +1,141 @@ +# 第四阶段:外部服务解耦与租户级 Provider 模块化 + +状态:已实施并通过构建、单元测试、真实 PostgreSQL 集成测试和空库迁移验收(2026-07-27)。 + +本阶段按“完全不用 Supabase、无正式业务数据”的前提实施。不保留 Supabase Auth、Supabase Storage、旧 Provider 配置或旧 DTO 兼容层。目标是让业务层只表达身份、短信、对象存储、支付和通知的业务意图,第三方 SDK、账号、bucket、密钥和 claim 结构都收敛到 Infrastructure provider 边界。 + +## 设计结论 + +- Identity 默认使用自有 JWT、数据库 Session、密码、短信验证码和微信认证。 +- Object Storage 默认使用阿里云 OSS,`local_dev` 只允许本地测试。 +- Database 固定为 EF Core + Npgsql + PostgreSQL。 +- Provider 配置统一使用 `TenantExternalProvider` + `TenantSecret`。 +- 不实现 Supabase Auth 兼容层。 +- 不实现 Supabase Storage provider。 +- 不继续扩散 `TenantAuthProvider`、`TenantPaymentAccount` 这类专用配置表。 + +## 统一 Provider 配置 + +新增统一租户外部服务配置模型 `TenantExternalProvider`,核心字段包括: + +- `TenantId` +- `Capability` +- `Provider` +- `Status` +- `DisplayName` +- `ConfigPublic` +- `SecretRef` +- `Priority` +- `Metadata` +- 审计字段 + +`Capability` 固定为: + +- `Identity` +- `ObjectStorage` +- `Sms` +- `Payment` +- `Notification` + +同一租户内 `Capability + Provider` 唯一。默认 Provider 选择规则是 `Status = Active` 且优先级最高;需要指定 provider code 的入口必须仍然受当前租户上下文约束。 + +`ConfigPublic` 只允许公开配置,例如: + +- identity:`appId` +- payment:`merchantId` +- object_storage:`region`、`endpoint`、`bucketAlias` +- sms / notification:`templateCode` + +密钥一律进入 `TenantSecret`,由 `SecretRef` 关联。公开配置写入路径必须拒绝 `secret`、`token`、`key`、`privateKey` 等敏感字段,避免把第三方凭据落到普通 JSON 配置里。 + +## Application 层接口边界 + +### `IIdentityProvider` + +封装 password、sms、wechat_web、wechat_miniapp 等身份解析,不直接把第三方 claim 结构散落在业务服务里,也不负责绕过统一 Session 签发流程。 + +微信登录通过当前租户的 `TenantExternalProvider(Capability = Identity)` 读取 `appId` 和 `SecretRef`,再由基础设施层完成第三方交互。`UserIdentity.Provider` 只保存标准 provider code。 + +### `ISmsProvider` + +只负责发送验证码或模板短信。验证码生成、哈希、频控、过期、校验和登录 Session 仍归自有业务服务负责。发送失败时记录失败状态,不能误判验证码可用。 + +### `IObjectStorageService` + +业务输入不接受任意 bucket。对象存储 provider、bucket alias、endpoint、region 由当前租户的 `TenantExternalProvider(Capability = ObjectStorage)` 解析。 + +资产表可以保存 `StorageProvider`、`Bucket`、`ObjectKey` 作为落库事实,但这些字段只允许由存储服务写入,不对业务 DTO 暴露成任意可写参数。 + +### `IPaymentProvider` + +支付账户配置从统一 Provider 配置加载。支付回调仍按 provider code 路由,但必须校验签名、租户、订单号和幂等事件。 + +旧 `TenantPaymentAccount` 表和旧支付专用配置模型已删除,管理侧应用模型只保留面向租户后台的 provider 视图。 + +### `INotificationProvider` + +默认实现为站内通知持久化。普通业务不直接跨模块 new `UserNotification`,后续 email、企微、短信模板等外发能力都应作为 Notification provider 扩展。 + +## 存储和认证的具体变化 + +- 删除生产代码中的 `SupabaseStorage` provider 枚举值。 +- 删除应用层所有 Supabase provider 配置路径。 +- 阿里云 OSS SDK 细节只保留在 Infrastructure 实现中。 +- 上传签名和上传确认通过当前租户 object storage provider 解析目标位置。 +- 微信登录不再从公开 JSON 直接读取密钥。 +- 短信登录继续复用自有验证码表、频控和 Session 体系,发送动作改由 `ISmsProvider` 执行。 + +## 数据库基线 + +项目尚无正式业务数据,因此本阶段继续采用 greenfield migration: + +- 删除旧 `TenantAuthProvider` 表。 +- 删除旧 `TenantPaymentAccount` 表。 +- 新增 `TenantExternalProvider` 表。 +- 保留并扩展 `TenantSecret` 用途。 +- 重建唯一 `InitialSchema` migration。 +- 使用空 PostgreSQL 通过 `Tiku.DbMigrator` 验证完整建库。 + +## 架构防线 + +架构测试需要阻止业务代码重新引入以下依赖: + +- `Supabase` +- `supabase_storage` +- `SUPABASE_` +- `TenantAuthProvider` +- `TenantPaymentAccount` +- 业务层直接引用阿里云 OSS、微信、支付或短信 SDK namespace +- 业务层直接读取第三方密钥 +- 业务 DTO 暴露任意 bucket / provider 细节 + +允许例外只应存在于: + +- Infrastructure provider 实现 +- EF Migration +- 测试 fake provider +- 明确说明历史迁移背景的文档 + +## 验收结果 + +本阶段完成时的验收口径: + +- `dotnet build TIKU-BACKEND.slnx --no-restore` +- `dotnet test Tiku.UnitTests/Tiku.UnitTests.csproj --no-restore` +- `dotnet test Tiku.IntegrationTests/Tiku.IntegrationTests.csproj --no-restore` +- 空 PostgreSQL 数据库执行 `Tiku.DbMigrator` +- `git diff --check` + +这些命令的目标不是证明“没有 Supabase 字符串”,而是证明新 Provider 模型、租户边界、迁移链和集成测试能共同支撑后续开发。 + +## 后续扩展规则 + +新增外部服务 provider 时,必须先判断它属于哪个 `Capability`,再补 Infrastructure provider 实现和租户配置解析。不要为每个第三方服务新建一套业务专用账号表;除非该能力已经形成独立领域模型,否则默认进入 `TenantExternalProvider`。 + +Provider 扩展时还必须同时补: + +- 租户 A/B 配置隔离测试。 +- 密钥只通过 `TenantSecret` 读取的测试。 +- `ConfigPublic` 敏感字段拒绝测试。 +- 架构扫描允许列表。 +- 空库 migration 验证。