From 5e993298e7cec4b34ea48c7d0725860cf8803c24 Mon Sep 17 00:00:00 2001 From: xiong Date: Tue, 28 Jul 2026 14:45:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E9=9A=94=E7=A6=BB=E4=BF=9D=E6=8A=A4=E8=A7=A6=E5=8F=91=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E7=AE=A1=E7=90=86=E6=96=B9=E6=B3=95=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=BF=81=E7=A7=BB=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +++++++++++++++++ .../20260728031410_InitialSchema.cs | 5 ++--- .../MigrationBuilderTenantGuardExtensions.cs | 18 +++++++++++++++++ .../MigrationExecutionTests.cs | 20 +++++++++++++++++++ ...nant-isolation-and-shared-question-bank.md | 4 +++- 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderTenantGuardExtensions.cs diff --git a/README.md b/README.md index 70aa76e..ec12724 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,24 @@ Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs - 不新增 Supabase provider、Supabase URL 拼接、Supabase Storage bucket 逻辑或 Supabase Auth 兼容层。 - EF Core 管实体和 migration 生命周期;跨表租户不变量不能指望 ORM 自动推导。比如“题目引用只能指向平台或本租户”“分类父节点只能属于平台或本租户”,必须用集中 PostgreSQL trigger / constraint trigger SQL helper + migration 调用 + 真实 PostgreSQL 测试兜底,禁止去数据库手工补。 +## 数据库边界与 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 个操作: diff --git a/Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs b/Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs index 1265dc2..bbff296 100644 --- a/Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs +++ b/Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs @@ -9200,14 +9200,13 @@ namespace Tiku.Infrastructure.Persistence.Migrations principalColumns: new[] { "tenant_id", "id" }, onDelete: ReferentialAction.SetNull); - migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTenantQuestionReferenceGuard); - migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTaxonomyParentGuard); + migrationBuilder.EnsureTenantIsolationGuards(); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql(PostgreSqlTenantConstraintSql.DropTenantGuards); + migrationBuilder.DropTenantIsolationGuards(); migrationBuilder.DropForeignKey( name: "fk_content_entries_regions_tenant_id_region_id", diff --git a/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderTenantGuardExtensions.cs b/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderTenantGuardExtensions.cs new file mode 100644 index 0000000..fff037a --- /dev/null +++ b/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderTenantGuardExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tiku.Infrastructure.Persistence.Migrations; + +internal static class MigrationBuilderTenantGuardExtensions +{ + public static void EnsureTenantIsolationGuards(this MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(PostgreSqlTenantConstraintSql.DropTenantGuards); + migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTenantQuestionReferenceGuard); + migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTaxonomyParentGuard); + } + + public static void DropTenantIsolationGuards(this MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(PostgreSqlTenantConstraintSql.DropTenantGuards); + } +} diff --git a/Tiku.IntegrationTests/MigrationExecutionTests.cs b/Tiku.IntegrationTests/MigrationExecutionTests.cs index 6909546..3d03734 100644 --- a/Tiku.IntegrationTests/MigrationExecutionTests.cs +++ b/Tiku.IntegrationTests/MigrationExecutionTests.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; using Tiku.IntegrationTests.Api; using Tiku.Infrastructure.Persistence; @@ -22,4 +24,22 @@ public sealed class MigrationExecutionTests Assert.NotEmpty(appliedMigrations); Assert.True(await dbContext.Database.CanConnectAsync()); } + + [Fact] + public async Task Migration_script_contains_tenant_isolation_guard_triggers() + { + await using var factory = new ApiTestFactory(); + using var scope = factory.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var migrator = dbContext.GetService(); + + var script = migrator.GenerateScript(); + + Assert.Contains("drop trigger if exists trg_tenant_question_references_platform_or_self_owner", script); + Assert.Contains("create or replace function tiku_guard_tenant_question_reference()", script); + Assert.Contains("create trigger trg_tenant_question_references_platform_or_self_owner", script); + Assert.Contains("drop trigger if exists trg_taxonomy_nodes_parent_platform_or_self_owner", script); + Assert.Contains("create or replace function tiku_guard_taxonomy_parent_owner()", script); + Assert.Contains("create trigger trg_taxonomy_nodes_parent_platform_or_self_owner", script); + } } diff --git a/docs/migration/phase-3-tenant-isolation-and-shared-question-bank.md b/docs/migration/phase-3-tenant-isolation-and-shared-question-bank.md index e8e451e..05a2623 100644 --- a/docs/migration/phase-3-tenant-isolation-and-shared-question-bank.md +++ b/docs/migration/phase-3-tenant-isolation-and-shared-question-bank.md @@ -179,8 +179,10 @@ EF Core 负责实体、Fluent Configuration、Migration 生成和迁移执行入 实现要求: - 触发器 SQL 必须集中在基础设施层,例如 `Tiku.Infrastructure/Persistence/PostgreSqlTenantConstraintSql.cs`。 -- Migration 只调用集中 SQL helper,例如 `migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTenantQuestionReferenceGuard)`。 +- Migration 只调用集中 helper:`migrationBuilder.EnsureTenantIsolationGuards()` / `migrationBuilder.DropTenantIsolationGuards()`。 +- 重建或压缩 `InitialSchema` 后,必须在 `Up()` 末尾调用 `EnsureTenantIsolationGuards()`,在 `Down()` 开头调用 `DropTenantIsolationGuards()`。 - 不允许把触发器 SQL 零散复制到多个 migration。 +- Migration script 测试必须断言 guard function 和 trigger 存在,避免重建基线时漏掉数据库硬防线。 - 每个触发器必须有真实 PostgreSQL 集成测试覆盖允许路径和拒绝路径。 - 如果将来新增类似“平台或本租户”的 owner 规则,优先补集中 SQL helper 和模型/集成测试,不要只靠 Service 手写校验。