Просмотр исходного кода

docs(aidop): add SaaS multi-tenant architecture plan

Record the shared-table tenant architecture decision and include the selected dashboard changes while bumping the frontend and backend versions for this revision.

Made-with: Cursor
skygu 2 недель назад
Родитель
Сommit
ea8c69b7a4

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.100",
+	"version": "2.4.101",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 226 - 0
doc/plan/SaaS多租户架构方案.md

@@ -0,0 +1,226 @@
+# SaaS 多租户架构方案
+
+> 状态:**方案草案,待后续开工评审**
+> 适用范围:面向中小型工厂的 SaaS 系统(大型工厂走本地部署,不进 SaaS)
+
+---
+
+## 1. 背景与前提
+
+- 当前系统为 SaaS 模式,需要同时接入多家不同工厂。
+- 目标客户均为**中小型工厂**,每家 2–3 年的数据总量不会大于 40G。
+- **大型工厂统一走本地部署**,不进入 SaaS 平台。
+- 现有数据库中租户标识字段**不统一**:部分表使用 `domain`,部分表使用 `factory_id`。
+- 其中 `domain` 与 `factory_id` 当前为 **1:1** 关系。
+
+## 2. 目标
+
+- 建立清晰、统一的多租户数据隔离模型。
+- 不引入过度复杂度(不做分库分表、不做一租户一库)。
+- 对后续演进保留清晰的扩展点(独立库、大表归档、报表只读等)。
+- 为历史表中 `domain` / `factory_id` 字段不统一的问题,给出统一的租户标识规范。
+
+## 3. 最终架构选型
+
+采用 **共享实例 + 共享库 + 共享表 + `tenant_id` 逻辑隔离**。
+
+不采用:
+
+- 分库分表
+- 一工厂一库
+- 一工厂一套独立部署
+- 引入分布式数据库中间件
+
+### 3.1 选型理由
+
+- 目标客户体量小、数据量小,共享库多租户性价比最高。
+- 多租户 SaaS 的核心问题是**租户隔离、权限、配置管理**,分库分表主要解决的是超大规模吞吐,与当前场景不匹配。
+- 统一架构便于统一升级、统一运维、统一报表。
+- 大型工厂已走本地部署,不需要为了极端体量客户提前把 SaaS 做重。
+
+### 3.2 架构总览
+
+```mermaid
+flowchart TD
+    User[FactoryUser]
+    Web[WebOrApp]
+    Gateway[GatewayOrNginx]
+    App[SaaSApp]
+    TenantCtx[TenantContext]
+    SharedDb[(SharedBizDb)]
+    HotTables[(HotDetailTables)]
+    SummaryTables[(SummaryTables)]
+    ArchiveDb[(ArchiveOrHistoryDb)]
+    Cache[RedisCache]
+    FileStore[ObjectStorage]
+
+    User --> Web
+    Web --> Gateway
+    Gateway --> App
+    App --> TenantCtx
+    TenantCtx --> SharedDb
+    SharedDb --> HotTables
+    HotTables --> ArchiveDb
+    App --> SummaryTables
+    App --> Cache
+    App --> FileStore
+```
+
+## 4. 租户标识统一规范
+
+### 4.1 字段统一
+
+| 用途 | 字段 | 类型 | 说明 |
+|------|------|------|------|
+| 系统内部租户主键 | `tenant_id` | `bigint` | 所有业务表隔离、ORM 过滤、权限、缓存 Key、日志统一使用 |
+| 业务可读编码 | `tenant_code` | `varchar(64)` | 对外/对内可读编码,允许变更 |
+| 显示名称 | `tenant_name` | `varchar(128)` | 仅展示 |
+| 访问入口 | `domain` | `varchar(128)` | 仅用于登录入口解析,不作为关联键 |
+| 工厂业务主键 | `factory_id` | `bigint` | 仅属于 `factory` 表本身,不再承担租户隔离职责 |
+
+### 4.2 语义边界
+
+- `tenant_id`:**系统维度**。数据隔离、ORM 过滤、权限、缓存、日志、文件路径、消息主题统一以此为准。
+- `domain`:**入口维度**。只用于登录时解析成 `tenant_id`,不参与业务表关联。
+- `factory_id`:**业务维度**。只属于 `factory` 业务实体,不再兼任租户标识。
+
+### 4.3 为什么不继续用 `domain` 或 `factory_id` 作为租户键
+
+- `domain` 属于可变业务属性,客户可能更换域名或绑定多个域名,作为关联键稳定性差。
+- `factory_id` 语义偏业务,将来若出现「集团-工厂」或「一租户多工厂」扩展,命名会非常尴尬。
+- `tenant_id` 语义纯粹,与业务实体解耦,后续演进最稳。
+
+## 5. 设计要点
+
+### 5.1 表结构
+
+- 所有核心业务表统一增加:
+  - `tenant_id`
+  - `created_time` / `updated_time`
+  - `created_by` / `updated_by`
+- 唯一键升级为复合唯一:`tenant_id + 业务键`(例如 `tenant_id + code`)。
+- 常用索引以 `tenant_id` 打头,例如:
+  - `tenant_id + status`
+  - `tenant_id + created_time`
+  - `tenant_id + business_key`
+
+### 5.2 应用层
+
+- 登录后写入 `TenantContext`。
+- ORM/仓储层**统一自动注入 `tenant_id` 过滤**,防止漏写。
+- 写入操作自动带 `tenant_id`。
+- 跨租户后台(超管/运营)走独立数据访问入口,显式放行。
+
+### 5.3 外围资源
+
+- 对象存储路径前缀带租户维度。
+- 缓存 Key 命名带租户维度。
+- 消息队列主题/事件总线按租户隔离。
+- 操作日志、审计日志记录 `tenant_id`。
+
+## 6. 明细/日志表策略
+
+采用 **共享表起步 + 预留归档能力(archive-ready)**。适用于设备采集、日志、履历、追溯流水等高频写入表。
+
+- 初期仍放共享表,不分表、不分库。
+- 建表时预留:
+  - `tenant_id + created_time` 索引,支持时间范围查询。
+  - 按月/季度归档到历史表的能力。
+  - 冷数据可搬迁到归档库。
+- 只在单表确实出现性能或容量压力时,对**该表**单独启动归档,不动整体架构。
+
+## 7. 报表与统计策略
+
+采用 **汇总表 + 定时聚合**。
+
+- 不直接在交易表上做复杂统计。
+- 通过定时任务或事件驱动,维护汇总表。
+- 汇总表同样带 `tenant_id`。
+- 若未来数据量真正上来,再考虑引入只读副本或分析库。
+
+## 8. 迁移思路(基于 domain : factory_id = 1:1)
+
+> 当前仅为迁移**思路**记录,不包含实施步骤。实施前需单独评审。
+
+### 8.1 新建租户主表
+
+示意结构:
+
+```text
+sys_tenant
+- tenant_id           bigint        主键
+- tenant_code         varchar(64)   唯一
+- tenant_name         varchar(128)
+- domain              varchar(128)  唯一
+- factory_id_legacy   bigint        过渡期兼容用
+- status              tinyint
+- created_time / updated_time
+```
+
+由于当前 `domain` 与 `factory_id` 是 1:1,可以一次性建立 `tenant_id` 与旧 `domain`、`factory_id` 的映射。
+
+### 8.2 业务表 `tenant_id` 补齐
+
+- 所有核心业务表新增 `tenant_id` 列。
+- 写入层改为双写:同时填 `tenant_id` 与旧字段。
+- 读取层逐步切到 `tenant_id`。
+- ORM/仓储层统一改为基于 `tenant_id` 过滤。
+
+### 8.3 旧字段处置
+
+- `domain` 最终只保留在 `sys_tenant`,其他表不再冗余。
+- `factory_id` 仅保留在 `factory` 表本身的业务语义下,不再承担租户隔离职责。
+- 过渡期内,业务表中的旧字段可继续存在,仅作历史兼容,不用于关联。
+
+### 8.4 登录与入口
+
+- 用户通过 `domain` 访问。
+- 登录时将 `domain` 解析为 `tenant_id`,写入 `TenantContext`。
+- 后续所有查询均以 `tenant_id` 过滤。
+
+## 9. 未来演进预留
+
+仅作设计预留,当前不启用:
+
+- 某租户数据量远超预期时:支持将其**切到独立库**,通过租户→数据源路由实现。
+- 明细表压力上升时:对该表单独做**按时间归档或历史表拆分**。
+- 报表压力上升时:引入**只读副本或分析库**。
+
+```mermaid
+flowchart TD
+    Start[Start]
+    SingleDb[SingleDbMultiTenant]
+    Archive[ArchiveHotTablesByTime]
+    Summary[SummaryTables]
+    LocalDeploy[LargeFactoryLocalDeployment]
+    FutureTenantDb[OptionalTenantIsolatedDb]
+
+    Start --> SingleDb
+    SingleDb --> Archive
+    SingleDb --> Summary
+    SingleDb --> LocalDeploy
+    SingleDb --> FutureTenantDb
+```
+
+## 10. 风险与注意事项
+
+- **漏加 `tenant_id` 过滤**是最大风险,必须在 ORM/仓储层做统一拦截,不允许业务层自由决定。
+- **唯一键改造**要覆盖到所有历史业务唯一索引,避免不同工厂编码冲突或串数据。
+- **迁移期双写**要考虑幂等与补偿,避免新旧字段不一致。
+- **后台/超管/跨租户接口**必须单独显式放行,不能误走统一过滤入口。
+
+## 11. 暂不做的事
+
+- 不做分库分表。
+- 不做一租户一库。
+- 不做一租户一套独立部署。
+- 不引入分布式数据库中间件。
+- 不提前为“物理隔离客户”搭建路由体系,仅保留能力预留。
+
+## 12. 后续动作(未排期)
+
+- 方案评审通过后,再单独拉出「实施计划」文档,细化到:
+  - 表改造清单与顺序
+  - ORM/仓储层租户过滤改造点
+  - 数据迁移与双写方案
+  - 灰度与回滚方案

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.67</AssemblyVersion>
-    <FileVersion>1.0.67</FileVersion>
-    <Version>1.0.67</Version>
+    <AssemblyVersion>1.0.68</AssemblyVersion>
+    <FileVersion>1.0.68</FileVersion>
+    <Version>1.0.68</Version>
   </PropertyGroup>
 
   <ItemGroup>