docs: 新增 SaaS 多租户架构与编码约束

This commit is contained in:
zhoulei
2026-08-20 10:41:09 +08:00
parent d8da9c7b1a
commit a4fbb61e12
4 changed files with 142 additions and 2 deletions
+2 -2
View File
@@ -7,8 +7,8 @@
| 文档 | 内容 | 读者 |
|---|---|---|
| **coding-standards.md** | ★ 前后端开发约束规范(权威):架构边界、APP_NAME 四处一致、前端规范、后端规范、接口契约、AI 生成检查清单 | 全体成员 + AI |
| architecture.md | 动静分离架构总览、技术栈、交付链路、治理体系 | 新成员 onboarding |
| **coding-standards.md** | ★ 前后端开发约束规范(权威):架构边界、APP_NAME 四处一致、前端规范、后端规范、接口契约、AI 生成检查清单、**§9 SaaS 多租户约束** | 全体成员 + AI |
| architecture.md | 动静分离架构总览、技术栈、交付链路、治理体系、**§7 SaaS 多租户架构** | 新成员 onboarding |
| agent-guide.md | AI 生成代码工作流(界面→代码提示词协议)、联调纪律、Vibe Coding 协作约定 | 全体成员 + AI |
| README.md(本文件) | 文档中心索引与交叉引用映射表 | 所有人 |
+3
View File
@@ -18,6 +18,7 @@
【服务别名】<前端 URL 首段,如 sms>
【数据库】<MySQL / SQL Server,默认 MySQL>
【子系统编码】<public/data/data.js 的 currentSubsystem,默认沿用现有>
【租户上下文】<本系统为多租户 SaaS,默认行级隔离;AI 生成代码须自动带租户过滤,业务参数不传 tenantId>
```
### 2.2 AI 侧(开工前必须明确的默认值)
@@ -30,6 +31,7 @@
| 服务别名 | 沿用 `window.frameBaseConfig.backendServices` 中现有别名;新增服务需用户确认映射 |
| 数据库 | MySQL(建表脚本需与 sqlserver.sql 双份同步) |
| 页面归属 | 菜单结构按界面标题推断一级/二级菜单;无法推断时归入现有最近菜单 |
| 租户隔离 | 默认开启(行级);AI 生成的数据访问自动叠加 `tenant_id` 过滤;不在 URL / 业务参数中传递 tenantId(来自 token,见 `coding-standards.md §9` |
### 2.3 交付说明模板(AI 每次交付代码时附带)
@@ -88,3 +90,4 @@
| 新页面菜单不可见 | 后端菜单数据(`sql/data/`)注册 + dynamicRouter 匹配 url===path |
| 手动拼主键/UUID | 一律 `DaoIdGenerator` |
| 新增 Util/Helper 类 | 遵循 §8.4:可引不可增 |
| 漏写租户过滤 → 跨租户数据泄露 | 数据访问必须带 `tenant_id`JPA @Filter / Repository 基类 / 原生 SQL `?`),缓存 key 加 `tenant:{id}:` 前缀(§9 |
+100
View File
@@ -79,3 +79,103 @@ D:\workBuddySpace\member\
- 前端仓库:`.project.agents/`CLAUDE.md / AGENTS.md / SELF_CONSTRAINTS.md / VIBECODING_GUIDE.md / docs/context/{PRD,ARCHITECTURE,ROADMAP,CONVENTIONS,UIUX}.md
- 后端仓库:`.project.agents/`(同构,无 UIUX.md
- 单源化:共享约束只存在于 `docs/`,仓库内 CONVENTIONS 用指针引用,禁止复制正文。
## 7. SaaS 多租户架构(新增 · 默认形态)
> 所有基于本底座的业务系统**默认按多租户 SaaS 形态交付**。本节定义租户隔离策略、前后端协作方式与扩展能力;编码强制约束见 `coding-standards.md §9`。
### 7.1 多租户隔离策略与选择理由
**结论:行级隔离(`tenant_id` 列)为主,Schema/库级隔离为大型/合规租户的升级路径,统称"混合分层隔离"。**
| 隔离级别 | 实现方式 | 适用对象 | 成本 / 风险 |
|---|---|---|---|
| **行级(默认)** | 每张业务表含 `tenant_id` 列;查询统一附加 `tenant_id = :tenantId` | 绝大多数租户(试用 / 标准版) | 成本低、弹性好,单实例支撑数千租户;**需严防漏写过滤**(§9 强制) |
| Schema 级 | 同一 DB 实例下每租户独立 schema;经动态数据源路由 | 数据敏感 / 合规客户 | 隔离强、成本中;DDL 维护量上升 |
| 库级 | 每租户独立数据库实例 + 独立连接池 | 大型企业 / 集团、数据驻留要求 | 隔离最强、成本最高;运维复杂 |
**选择理由(为什么行级为主):**
- 底座已具备 **Druid + JPA + 多数据源**Nacos `abacus-database.yml``enableMultiSource` / `multidb0~9` / `defaultDB` / `readDB` / `dynamicRelation`)——该能力可直接复用作"Schema / 库级租户的路由底座",无需另起炉灶。
- 行级隔离与现有 `JpaRepository` / `JpaSpecificationExecutor`、原生 SQL`pi/impl`)天然契合,可**不动表结构范式**落地;AI 生成代码能自动带上租户过滤(见 §9)。
- 租户扩展时,通过 `tenant` 表的 `isolation_type` + `datasource_key` 字段,将"标准租户"逐步**升级**为 Schema / 库级,业务代码无需重构(查询仍走同一 `TenantContext`,仅数据源路由不同)。
**核心主数据表(新增,属平台租户,可豁免行级过滤但须标注 `@PlatformTenantOnly`):**
- `tenant`(租户主数据):`id`(业务主键 / DaoIdGenerator) / `tenant_code`(唯一,如 `acme`) / `name` / `tier`(trial / standard / enterprise) / `isolation_type`(row / schema / db) / `datasource_key`(指向 Nacos 数据源键) / `status`
- `tenant_config`(品牌 / 主题 / 模块):`tenant_id` / `logo_url` / `primary_color` / `product_name` / `theme` / `subscribed_modules`(逗号分隔功能码)。
- `tenant_quota`(配额):`tenant_id` / `max_users` / `storage_bytes` / `api_qps` / `feature_flags`
> 建表须遵循 §5.8`sql/table/{mysql,sqlserver}.sql` 双份同步 + `upgrade.xml` 追加版本段。
### 7.2 前端设计(按登录租户动态加载)
- **租户上下文来源**:登录成功后,认证返回 JWT(claims 含 `tenantId` / `tenantCode` / `roles`)与 `tenantConfig`。前端建立 `subsystem/store/tenant.ts``useTenantStore` Pinia),保存 `tenantId` + `tenantConfig`,与 token 一并持久化到 localStorage。
- **品牌与主题动态化**`framework/utils/theme.ts` 提供 `applyTenantTheme(config)`——将 `primary_color` 写入 Element Plus 运行时主题(`--el-color-primary` 及派生 6 档明度),替换 logo(`tenantConfig.logo_url`)、产品名(`product_name` 注入标题 / 页脚)。登录后调用一次,租户切换时重调。
- **功能模块与菜单**`permissionStore.filterAsyncRoutes` 在原有"权限过滤"基础上叠加**租户模块门禁**——仅当菜单所属模块的 feature code ∈ `tenantConfig.subscribed_modules` 时才保留(即"有权限但租户未订阅 → 不可见")。
- **租户切换(运营 / 平台管理员)**:`useTenantStore.switchTenant(code)` → 重新拉取 `tenantConfig``applyTenantTheme` → 重新 `filterAsyncRoutes` → 刷新菜单。
- **请求附带租户标识**:扩展 `framework/utils/request.ts`,在拦截器中自动附加 `X-Tenant-Id` 头(取自 `useTenantStore.tenantId`),与 token 一并发送;**业务代码不得手写租户头**。
### 7.3 后端设计(识别、隔离、缓存、异步全链路)
- **租户识别(token 为准)**:以 JWT 中的 `tenantId` 为数据隔离的**唯一真相源**`X-Tenant-Id` 头仅作下游服务透传 / 审计用,不单独作为隔离依据(防越权)。跨租户运营操作须走独立高权限接口(如 `@CrossTenant` 标注 + 二次鉴权)。
- **TenantContextHolder**`ThreadLocal<TenantContext>``tenantId` / `tenantCode` / `tier` / `isolationType` / `dataSourceKey`)。由 `TenantInterceptor``HandlerInterceptor.preHandle`)在请求入口从 `Authentication` 取出并写入,`afterCompletion``remove()` 防泄漏。
- **查询天然带租户过滤(核心)**:
- JPA:在 `dao` 实体启用 Hibernate `@Filter(name="tenantFilter", condition="tenant_id = :tenantId")`,由 `TenantEntityManager` / `OpenSessionInView` 拦截器统一激活并填入 `:tenantId`;或提供基类 `TenantRepository<T> extends JpaRepository<T,String>, JpaSpecificationExecutor<T>`,所有 `findByXxx` 自动叠加 `equal("tenantId", currentTenant())`
- 原生 SQL`pi/impl`):**每条查询必须** `... AND t.tenant_id = ?`,参数取自 `TenantContextHolder.getTenantId()`,沿用 §5.4 的 `?` 参数化(I2)。
- 硬规则:数据访问方法**禁止省略租户条件**;跨租户场景必须显式 `@CrossTenant` 且经审批。
- **缓存隔离(Redis)**:全部缓存 key 加前缀 `tenant:{tenantId}:<原key>``TenantCacheKeyGenerator`)。漏加前缀会跨租户串数据,属高危(P0)。
- **异步上下文传播**
- `@Async``TenantTaskDecorator`(实现 `TaskDecorator`)将父线程 `TenantContext` 传给子线程。
- KafkaProducer 打 `tenantId` 消息头;Consumer 在处理器开头据消息头重建 `TenantContext`
- XXL-JOB:分片 / 参数携带 `tenantId`Job Handler 处理每个租户数据前先 `setTenantContext`,且任务本身需"多租户感知"(按租户遍历或参数指定)。
- **ID 与数据源**`DaoIdGenerator` 全局唯一(prefix + 日期 + 流水),跨租户不冲突;`tenant_id` 作为行属性而非主键一部分。Schema / 库级租户经 `dynamicRelation` 路由到其 `datasource_key` 对应数据源。
### 7.4 前后端协作方式(端到端)
```mermaid
sequenceDiagram
participant U as 用户
participant FE as 前端(Vue)
participant Auth as 认证/登录
participant BE as 后端(微服务)
participant DB as 数据库/缓存
U->>FE: 登录(账号密码)
FE->>Auth: POST /login
Auth-->>FE: JWT(tenantId,tenantCode,roles) + tenantConfig
FE->>FE: useTenantStore 保存 + applyTenantTheme + filterAsyncRoutes(权限 ∩ 模块)
U->>FE: 业务操作
FE->>BE: 请求 + Authorization:Bearer + X-Tenant-Id
BE->>BE: TenantInterceptor → TenantContextHolder
BE->>DB: 查询自动带 tenant_id=? / Redis key=tenant:{id}:...
DB-->>BE: 仅本租户数据
BE-->>FE: {status:200, data: 本租户数据}
```
- **契约要点**:前端无需、也不应感知其他租户;所有隔离在服务端完成。URL / 业务参数中**不携带** `tenantId`tenant 来自 token / 头)。接口契约补充见 §6。
- **故障隔离**:任一层漏写租户条件由 §9 强制检查 + 评审拦截;缓存 / 异步串租户属 P0 事故。
### 7.5 扩展性(性能 / 计费 / 配额 / 自动化开通)
- **性能**:每张租户表建**复合索引 `(tenant_id, ...)`**;按 `tier` 配置连接池(企业版走独立 `datasource_key` 专属池);大表按 `tenant_id` 分区;读副本按租户流量分配。
- **计量计费**:新增 `billing` 能力(超出示例模板范围,给出设计钩子)——业务流通过 Kafka 异步发送 `tenant.usage` 事件(API 调用数 / 存储字节 / 活跃用户 / 功能用量),`usage-aggregation` 任务按月汇总生成账单。计量必须**异步、非阻塞**,不侵入主流程。
- **配额管理**`tenant_quota` 表 + `QuotaService` / `QuotaInterceptor`,在"影响配额的操作"(建用户、上传、大批量导出)前校验;超出返回 `9xxx` 业务异常(§6.2)。
- **自动化开通(Provisioning**`esb/TenantProvisioningService` 由运营接口 / `tenant.signup` Kafka 事件触发,**幂等 + 可重试(XXL-JOB 兜底)**
- 行级:插 `tenant` + 种子默认管理员 + 赋默认模块订阅 + 注册菜单 / 模块数据(沿用 `sql/data/` 模式)+ 默认配置。
- Schema / 库级:经专属数据源执行建 schema / DB + 基线 DDLmysql / sqlserver 双份)+ 种子数据;在 Nacos 注册该租户的数据源键。
- 完成后发 `tenant.provisioned` 事件。
### 7.6 落地实现建议(映射到 abacus 分层)
**后端新增(包前缀 `abacus.springboot.<应用名>.`):**
- `config/TenantConfig.java`:注册 `TenantInterceptor``TenantContextHolder``TenantTaskDecorator``TenantCacheKeyGenerator`、Hibernate tenant filter。
- `dao/``Tenant``TenantConfig``TenantQuota`(均 `@Entity`;主数据表本身由平台租户管理,可豁免过滤但需标注 `@PlatformTenantOnly`)。
- `api/TenantController.java``/tenant/config` 返回品牌 / 主题 / 模块)、`controller/` 下运营开通接口。
- `esb/TenantBizService.java``esb/TenantProvisioningService.java`
- `pi/TenantAccess.java`(原生 SQL 含 `tenant_id = ?`)。
**前端新增:**
- `subsystem/store/tenant.ts``useTenantStore`)。
- `framework/utils/theme.ts``applyTenantTheme`)。
- 扩展 `framework/utils/request.ts`:自动附加 `X-Tenant-Id`,登录后拉取并缓存 `/tenant/config`
- 扩展 `permissionStore.filterAsyncRoutes`:叠加模块门禁。
**AI 生成代码时**:涉及任何数据访问,`coding-standards.md §9` 强制要求带上租户过滤;新业务表 DDL 必须含 `tenant_id` 列(行级默认)。
+37
View File
@@ -448,6 +448,43 @@ public DaoIdGenerator jcBillIdGenerator() {
---
## 9. SaaS 多租户约束(强制)
> 所有业务系统默认多租户 SaaS。架构见 `architecture.md §7`。本节为编码时**必须 / 禁止**清单,AI 与人工共同遵守。
### 9.1 数据隔离(必须)
1. 行级默认:每张业务表必须含 `tenant_id` 列(DDL 双份同步,§5.8);主数据表(`tenant` / `tenant_config` / `tenant_quota`)本身由平台租户管理,可豁免过滤,但实体需标注 `@PlatformTenantOnly` 注释说明。
2. 任何数据访问(JPA Repository / `pi` 原生 SQL / `esb` 编排调用)都必须带租户条件:`tenant_id = :tenantId`JPA)或 `tenant_id = ?`(参数化,取 `TenantContextHolder`,沿用 §5.4)。
3. 禁止省略租户条件;跨租户操作必须 `@CrossTenant` 标注并经评审,且走独立高权限接口。
### 9.2 租户上下文(必须)
4. 请求入口由 `TenantInterceptor` 从 JWT 取 `tenantId` 写入 `TenantContextHolder``afterCompletion` 必须 `remove()` 防泄漏。
5. 数据访问禁止用 `null` 租户作为"查全部"的捷径;`TenantContextHolder.getTenantId()` 为空须显式按平台租户路径处理(带 `@PlatformTenantOnly`)。
6. 缓存 key 必须前缀 `tenant:{tenantId}:``TenantCacheKeyGenerator`);禁止裸 key 跨租户共享。
### 9.3 异步与跨进程(必须)
7. `@Async` 必须走带 `TenantTaskDecorator` 的线程池;Kafka Producer 打 `tenantId` 头、Consumer 头部重建上下文;XXL-JOB 参数 / 分片带 `tenantId` 且 Job 多租户感知。
### 9.4 前端(必须 / 禁止)
8. 必须:登录后保存 `useTenantStore`、调用 `applyTenantTheme`、菜单经"权限 ∩ 模块订阅"过滤。
9. 禁止:业务代码手写 `X-Tenant-Id` 头或硬编码租户标识;theme / logo 写死(须来自 `tenantConfig`)。
### 9.5 扩展(建议)
10. 租户表复合索引 `(tenant_id, ...)`;配额敏感操作前置 `QuotaService` 校验;开通走 `TenantProvisioningService`(幂等 + 可重试)。
### 9.6 检查清单补充(并入 §7.3)
- 新增数据访问:是否带租户过滤?缓存 key 是否前缀?
- 新增异步 / Kafka / Job:租户上下文是否传播?
- 新增前端页面:品牌 / 主题 / 菜单是否随租户动态?
---
## 附:文档导航
| 文档 | 内容 |