惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
aimingoo的专栏
aimingoo的专栏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Docker
博客园 - 叶小钗
Recent Announcements
Recent Announcements
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
Hugging Face - Blog
Hugging Face - Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
美团技术团队
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
H
Heimdal Security Blog
有赞技术团队
有赞技术团队
The Cloudflare Blog
S
Secure Thoughts
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
N
News and Events Feed by Topic
小众软件
小众软件
C
Cybersecurity and Infrastructure Security Agency CISA
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - Franky
T
Tenable Blog
The Last Watchdog
The Last Watchdog
腾讯CDC
量子位
Google DeepMind News
Google DeepMind News
Help Net Security
Help Net Security
雷峰网
雷峰网
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Cyberwarzone
Cyberwarzone
S
Securelist
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
I
InfoQ
Spread Privacy
Spread Privacy
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Hackread – Cybersecurity News, Data Breaches, AI and More

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第十五章:多租户架构
我才是银古 · 2026-06-21 · via 博客园 - 我才是银古

第十五章:多租户架构

一、多租户概述

1.1 什么是多租户

多租户(Multi-Tenancy)是一种软件架构模式,在同一个应用实例中为多个租户(客户/组织)提供服务,每个租户的数据相互隔离。多租户架构是 SaaS(Software as a Service)平台的核心技术之一。

1.2 多租户架构类型

架构类型 说明 隔离级别 成本 复杂度
独立数据库 每个租户一个独立数据库 最高
共享数据库独立Schema 共享数据库,每个租户独立Schema
共享数据库共享Schema 通过 TenantId 字段区分数据
混合模式 大客户独立库,小客户共享库 可调 灵活

1.3 Furion 多租户支持

Furion 提供了灵活的多租户解决方案,支持多种租户识别策略和数据隔离方案:

var builder = WebApplication.CreateBuilder(args);

// 注册多租户服务
builder.Services.AddMultiTenant();

var app = builder.Build();
app.UseMultiTenant();
app.Run();

二、租户识别策略

通过请求头携带租户标识:

/// <summary>
/// 基于 Header 的租户识别策略
/// </summary>
public class HeaderTenantResolver : ITenantResolver
{
    private const string TenantHeaderKey = "X-Tenant-Id";

    public Task<string> ResolveAsync(HttpContext httpContext)
    {
        var tenantId = httpContext.Request.Headers[TenantHeaderKey].FirstOrDefault();

        if (string.IsNullOrEmpty(tenantId))
        {
            throw new TenantNotFoundException("请求头中缺少租户标识");
        }

        return Task.FromResult(tenantId);
    }
}

客户端请求示例:

GET /api/users HTTP/1.1
Host: api.example.com
X-Tenant-Id: tenant_001
Authorization: Bearer eyJhbGciOi...

2.2 Route 路由识别

通过 URL 路由中的租户参数识别:

/// <summary>
/// 基于路由的租户识别策略
/// </summary>
public class RouteTenantResolver : ITenantResolver
{
    public Task<string> ResolveAsync(HttpContext httpContext)
    {
        var tenantId = httpContext.Request.RouteValues["tenantId"]?.ToString();

        if (string.IsNullOrEmpty(tenantId))
        {
            throw new TenantNotFoundException("路由中缺少租户标识");
        }

        return Task.FromResult(tenantId);
    }
}

路由配置:

app.MapControllerRoute(
    name: "tenant",
    pattern: "{tenantId}/{controller}/{action=Index}/{id?}");

// 请求示例:GET /tenant_001/api/users

2.3 Domain 域名识别

通过子域名识别租户:

/// <summary>
/// 基于域名的租户识别策略
/// </summary>
public class DomainTenantResolver : ITenantResolver
{
    private readonly ITenantStore _tenantStore;

    public DomainTenantResolver(ITenantStore tenantStore)
    {
        _tenantStore = tenantStore;
    }

    public async Task<string> ResolveAsync(HttpContext httpContext)
    {
        var host = httpContext.Request.Host.Host;

        // 从子域名提取租户标识
        // 例如:tenant001.example.com -> tenant001
        var subdomain = host.Split('.').First();

        var tenant = await _tenantStore.FindByDomainAsync(subdomain);
        if (tenant == null)
        {
            throw new TenantNotFoundException($"未找到域名 {host} 对应的租户");
        }

        return tenant.TenantId;
    }
}

// 请求示例:
// GET https://company-a.example.com/api/users
// GET https://company-b.example.com/api/users

2.4 Query 查询参数识别

通过 URL 查询参数识别:

/// <summary>
/// 基于查询参数的租户识别策略
/// </summary>
public class QueryTenantResolver : ITenantResolver
{
    public Task<string> ResolveAsync(HttpContext httpContext)
    {
        var tenantId = httpContext.Request.Query["tenant"].FirstOrDefault();

        if (string.IsNullOrEmpty(tenantId))
        {
            throw new TenantNotFoundException("查询参数中缺少租户标识");
        }

        return Task.FromResult(tenantId);
    }
}

// 请求示例:GET /api/users?tenant=tenant_001

2.5 组合识别策略

/// <summary>
/// 组合租户识别策略(按优先级尝试多种方式)
/// </summary>
public class CompositeTenantResolver : ITenantResolver
{
    public Task<string> ResolveAsync(HttpContext httpContext)
    {
        // 优先级1:Header
        var tenantId = httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault();

        // 优先级2:Route
        if (string.IsNullOrEmpty(tenantId))
        {
            tenantId = httpContext.Request.RouteValues["tenantId"]?.ToString();
        }

        // 优先级3:Query
        if (string.IsNullOrEmpty(tenantId))
        {
            tenantId = httpContext.Request.Query["tenant"].FirstOrDefault();
        }

        // 优先级4:Claims
        if (string.IsNullOrEmpty(tenantId))
        {
            tenantId = httpContext.User?.FindFirst("tenant_id")?.Value;
        }

        if (string.IsNullOrEmpty(tenantId))
        {
            throw new TenantNotFoundException("无法识别租户");
        }

        return Task.FromResult(tenantId);
    }
}

2.6 识别策略对比

策略 优点 缺点 适用场景
Header 灵活、不影响URL 需前端配合 API 接口
Route 直观、RESTful URL 变长 网站/API
Domain 专业、品牌感 需配置DNS SaaS 平台
Query 简单 不够优雅 调试/简单场景
JWT Claims 安全 依赖认证 认证系统集成

三、数据库隔离方案

3.1 独立数据库方案

每个租户使用独立的数据库,数据完全隔离:

/// <summary>
/// 租户数据库上下文
/// </summary>
public class TenantDbContext : DbContext
{
    private readonly ITenantContext _tenantContext;

    public TenantDbContext(
        DbContextOptions<TenantDbContext> options,
        ITenantContext tenantContext)
        : base(options)
    {
        _tenantContext = tenantContext;
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<Product> Products { get; set; }
}

/// <summary>
/// 租户数据库连接字符串提供器
/// </summary>
public class TenantDbConnectionProvider : IDbConnectionProvider
{
    private readonly ITenantStore _tenantStore;
    private readonly ITenantContext _tenantContext;

    public TenantDbConnectionProvider(
        ITenantStore tenantStore,
        ITenantContext tenantContext)
    {
        _tenantStore = tenantStore;
        _tenantContext = tenantContext;
    }

    public async Task<string> GetConnectionStringAsync()
    {
        var tenantId = _tenantContext.TenantId;
        var tenant = await _tenantStore.GetByIdAsync(tenantId);

        if (tenant == null)
        {
            throw new TenantNotFoundException($"租户 {tenantId} 不存在");
        }

        return tenant.ConnectionString;
    }
}

注册独立数据库方案:

builder.Services.AddDbContext<TenantDbContext>((serviceProvider, options) =>
{
    var tenantContext = serviceProvider.GetRequiredService<ITenantContext>();
    var tenantStore = serviceProvider.GetRequiredService<ITenantStore>();

    var tenant = tenantStore.GetById(tenantContext.TenantId);
    options.UseSqlServer(tenant.ConnectionString);
});

3.2 共享数据库方案(行级隔离)

所有租户共享同一个数据库,通过 TenantId 字段区分数据:

/// <summary>
/// 多租户基础实体
/// </summary>
public abstract class TenantEntity
{
    public long Id { get; set; }

    /// <summary>
    /// 租户Id(用于数据隔离)
    /// </summary>
    public string TenantId { get; set; }

    public DateTime CreatedTime { get; set; }
    public DateTime? UpdatedTime { get; set; }
}

/// <summary>
/// 用户实体(继承租户基类)
/// </summary>
public class User : TenantEntity
{
    public string UserName { get; set; }
    public string Email { get; set; }
    public string RealName { get; set; }
    public int Status { get; set; }
}

/// <summary>
/// 订单实体
/// </summary>
public class Order : TenantEntity
{
    public string OrderNo { get; set; }
    public decimal TotalAmount { get; set; }
    public int Status { get; set; }
}

3.3 全局查询过滤器

使用 EF Core 的全局查询过滤器自动添加租户条件:

/// <summary>
/// 多租户 DbContext
/// </summary>
public class MultiTenantDbContext : DbContext
{
    private readonly ITenantContext _tenantContext;

    public MultiTenantDbContext(
        DbContextOptions<MultiTenantDbContext> options,
        ITenantContext tenantContext)
        : base(options)
    {
        _tenantContext = tenantContext;
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // 为所有实现 TenantEntity 的实体添加全局查询过滤器
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(TenantEntity).IsAssignableFrom(entityType.ClrType))
            {
                modelBuilder.Entity(entityType.ClrType)
                    .AddQueryFilter<TenantEntity>(e => e.TenantId == _tenantContext.TenantId);
            }
        }
    }

    /// <summary>
    /// 重写 SaveChanges,自动设置 TenantId
    /// </summary>
    public override int SaveChanges()
    {
        SetTenantId();
        return base.SaveChanges();
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        SetTenantId();
        return await base.SaveChangesAsync(cancellationToken);
    }

    private void SetTenantId()
    {
        var entries = ChangeTracker.Entries<TenantEntity>()
            .Where(e => e.State == EntityState.Added);

        foreach (var entry in entries)
        {
            entry.Entity.TenantId = _tenantContext.TenantId;
        }
    }
}

四、Schema 隔离

4.1 Schema 隔离实现

每个租户使用不同的 Schema(如 PostgreSQL/SQL Server 中的 Schema):

/// <summary>
/// 基于 Schema 的多租户 DbContext
/// </summary>
public class SchemaMultiTenantDbContext : DbContext
{
    private readonly ITenantContext _tenantContext;

    public SchemaMultiTenantDbContext(
        DbContextOptions<SchemaMultiTenantDbContext> options,
        ITenantContext tenantContext)
        : base(options)
    {
        _tenantContext = tenantContext;
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        var schema = $"tenant_{_tenantContext.TenantId}";

        // 设置所有表的 Schema
        modelBuilder.HasDefaultSchema(schema);

        // 或者单独设置
        modelBuilder.Entity<User>().ToTable("Users", schema);
        modelBuilder.Entity<Order>().ToTable("Orders", schema);
    }
}

4.2 Schema 自动创建

/// <summary>
/// 租户 Schema 管理服务
/// </summary>
public class TenantSchemaService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<TenantSchemaService> _logger;

    public TenantSchemaService(
        IServiceScopeFactory scopeFactory,
        ILogger<TenantSchemaService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <summary>
    /// 为新租户创建 Schema
    /// </summary>
    public async Task CreateTenantSchemaAsync(string tenantId)
    {
        using var scope = _scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<SchemaMultiTenantDbContext>();

        var schema = $"tenant_{tenantId}";

        // 创建 Schema
        await dbContext.Database.ExecuteSqlRawAsync($"CREATE SCHEMA IF NOT EXISTS \"{schema}\"");

        // 应用迁移
        await dbContext.Database.MigrateAsync();

        _logger.LogInformation("租户 {TenantId} 的 Schema 创建完成", tenantId);
    }

    /// <summary>
    /// 删除租户 Schema
    /// </summary>
    public async Task DropTenantSchemaAsync(string tenantId)
    {
        using var scope = _scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<SchemaMultiTenantDbContext>();

        var schema = $"tenant_{tenantId}";
        await dbContext.Database.ExecuteSqlRawAsync($"DROP SCHEMA IF EXISTS \"{schema}\" CASCADE");

        _logger.LogInformation("租户 {TenantId} 的 Schema 已删除", tenantId);
    }
}

五、租户上下文

5.1 ITenantContext 接口

/// <summary>
/// 租户上下文接口
/// </summary>
public interface ITenantContext
{
    /// <summary>
    /// 当前租户Id
    /// </summary>
    string TenantId { get; }

    /// <summary>
    /// 当前租户信息
    /// </summary>
    TenantInfo TenantInfo { get; }

    /// <summary>
    /// 是否已识别租户
    /// </summary>
    bool IsResolved { get; }
}

/// <summary>
/// 租户信息
/// </summary>
public class TenantInfo
{
    public string TenantId { get; set; }
    public string TenantName { get; set; }
    public string ConnectionString { get; set; }
    public string Schema { get; set; }
    public TenantPlan Plan { get; set; }
    public bool IsActive { get; set; }
    public DateTime ExpireTime { get; set; }
    public Dictionary<string, string> ExtraProperties { get; set; }
}

public enum TenantPlan
{
    Free,       // 免费版
    Basic,      // 基础版
    Pro,        // 专业版
    Enterprise  // 企业版
}

5.2 租户上下文实现

/// <summary>
/// 租户上下文实现
/// </summary>
public class TenantContext : ITenantContext
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ITenantStore _tenantStore;
    private TenantInfo _tenantInfo;

    public TenantContext(
        IHttpContextAccessor httpContextAccessor,
        ITenantStore tenantStore)
    {
        _httpContextAccessor = httpContextAccessor;
        _tenantStore = tenantStore;
    }

    public string TenantId
    {
        get
        {
            var httpContext = _httpContextAccessor.HttpContext;
            return httpContext?.Items["TenantId"]?.ToString();
        }
    }

    public TenantInfo TenantInfo
    {
        get
        {
            if (_tenantInfo == null && !string.IsNullOrEmpty(TenantId))
            {
                _tenantInfo = _tenantStore.GetById(TenantId);
            }
            return _tenantInfo;
        }
    }

    public bool IsResolved => !string.IsNullOrEmpty(TenantId);
}

5.3 租户中间件

/// <summary>
/// 租户识别中间件
/// </summary>
public class TenantMiddleware
{
    private readonly RequestDelegate _next;

    public TenantMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, ITenantResolver tenantResolver, ITenantStore tenantStore)
    {
        try
        {
            // 解析租户Id
            var tenantId = await tenantResolver.ResolveAsync(context);

            // 验证租户是否存在且有效
            var tenant = await tenantStore.GetByIdAsync(tenantId);
            if (tenant == null)
            {
                context.Response.StatusCode = 404;
                await context.Response.WriteAsJsonAsync(new { Message = $"租户 {tenantId} 不存在" });
                return;
            }

            if (!tenant.IsActive)
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsJsonAsync(new { Message = "租户已被禁用" });
                return;
            }

            if (tenant.ExpireTime < DateTime.Now)
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsJsonAsync(new { Message = "租户已过期" });
                return;
            }

            // 将租户信息存入 HttpContext
            context.Items["TenantId"] = tenantId;
            context.Items["TenantInfo"] = tenant;
        }
        catch (TenantNotFoundException)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new { Message = "无法识别租户" });
            return;
        }

        await _next(context);
    }
}

// 注册中间件
// app.UseMiddleware<TenantMiddleware>();

六、租户配置管理

6.1 租户存储接口

/// <summary>
/// 租户存储接口
/// </summary>
public interface ITenantStore
{
    Task<TenantInfo> GetByIdAsync(string tenantId);
    TenantInfo GetById(string tenantId);
    Task<TenantInfo> FindByDomainAsync(string domain);
    Task<List<TenantInfo>> GetAllAsync();
    Task CreateAsync(TenantInfo tenant);
    Task UpdateAsync(TenantInfo tenant);
    Task DeleteAsync(string tenantId);
}

6.2 数据库租户存储

/// <summary>
/// 基于数据库的租户存储
/// </summary>
public class DbTenantStore : ITenantStore
{
    private readonly MasterDbContext _masterDb;
    private readonly IMemoryCache _cache;

    public DbTenantStore(MasterDbContext masterDb, IMemoryCache cache)
    {
        _masterDb = masterDb;
        _cache = cache;
    }

    public async Task<TenantInfo> GetByIdAsync(string tenantId)
    {
        return await _cache.GetOrCreateAsync($"tenant:{tenantId}", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);

            var tenant = await _masterDb.Tenants
                .FirstOrDefaultAsync(t => t.TenantId == tenantId);

            return tenant?.ToTenantInfo();
        });
    }

    public TenantInfo GetById(string tenantId)
    {
        return _cache.GetOrCreate($"tenant:{tenantId}", entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);

            var tenant = _masterDb.Tenants
                .FirstOrDefault(t => t.TenantId == tenantId);

            return tenant?.ToTenantInfo();
        });
    }

    public async Task<TenantInfo> FindByDomainAsync(string domain)
    {
        var tenant = await _masterDb.Tenants
            .FirstOrDefaultAsync(t => t.Domain == domain);
        return tenant?.ToTenantInfo();
    }

    public async Task<List<TenantInfo>> GetAllAsync()
    {
        var tenants = await _masterDb.Tenants.ToListAsync();
        return tenants.Select(t => t.ToTenantInfo()).ToList();
    }

    public async Task CreateAsync(TenantInfo tenant)
    {
        _masterDb.Tenants.Add(new TenantEntity
        {
            TenantId = tenant.TenantId,
            TenantName = tenant.TenantName,
            ConnectionString = tenant.ConnectionString,
            IsActive = true,
            Plan = tenant.Plan,
            ExpireTime = tenant.ExpireTime
        });
        await _masterDb.SaveChangesAsync();
    }

    public async Task UpdateAsync(TenantInfo tenant)
    {
        var entity = await _masterDb.Tenants.FindAsync(tenant.TenantId);
        if (entity != null)
        {
            entity.TenantName = tenant.TenantName;
            entity.IsActive = tenant.IsActive;
            entity.Plan = tenant.Plan;
            entity.ExpireTime = tenant.ExpireTime;
            await _masterDb.SaveChangesAsync();

            // 清除缓存
            _cache.Remove($"tenant:{tenant.TenantId}");
        }
    }

    public async Task DeleteAsync(string tenantId)
    {
        var entity = await _masterDb.Tenants.FindAsync(tenantId);
        if (entity != null)
        {
            _masterDb.Tenants.Remove(entity);
            await _masterDb.SaveChangesAsync();
            _cache.Remove($"tenant:{tenantId}");
        }
    }
}

6.3 租户管理 API

/// <summary>
/// 租户管理接口(仅平台管理员可用)
/// </summary>
[ApiDescriptionSettings("租户管理")]
public class TenantAppService : IDynamicApiController
{
    private readonly ITenantStore _tenantStore;
    private readonly TenantSchemaService _schemaService;

    public TenantAppService(
        ITenantStore tenantStore,
        TenantSchemaService schemaService)
    {
        _tenantStore = tenantStore;
        _schemaService = schemaService;
    }

    /// <summary>
    /// 获取所有租户
    /// </summary>
    public async Task<List<TenantInfo>> GetAllTenants()
    {
        return await _tenantStore.GetAllAsync();
    }

    /// <summary>
    /// 创建租户
    /// </summary>
    public async Task CreateTenant(CreateTenantInput input)
    {
        var tenant = new TenantInfo
        {
            TenantId = Guid.NewGuid().ToString("N"),
            TenantName = input.TenantName,
            Plan = input.Plan,
            IsActive = true,
            ExpireTime = DateTime.Now.AddYears(1),
            ConnectionString = GenerateConnectionString(input)
        };

        // 创建租户记录
        await _tenantStore.CreateAsync(tenant);

        // 创建租户数据库/Schema
        await _schemaService.CreateTenantSchemaAsync(tenant.TenantId);
    }

    /// <summary>
    /// 禁用租户
    /// </summary>
    public async Task DisableTenant(string tenantId)
    {
        var tenant = await _tenantStore.GetByIdAsync(tenantId);
        if (tenant != null)
        {
            tenant.IsActive = false;
            await _tenantStore.UpdateAsync(tenant);
        }
    }

    private string GenerateConnectionString(CreateTenantInput input)
    {
        return $"Server=localhost;Database=tenant_{input.TenantName};Trusted_Connection=True;";
    }
}

七、租户数据迁移

7.1 租户数据库初始化

/// <summary>
/// 租户数据库迁移服务
/// </summary>
public class TenantMigrationService
{
    private readonly ITenantStore _tenantStore;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<TenantMigrationService> _logger;

    public TenantMigrationService(
        ITenantStore tenantStore,
        IServiceScopeFactory scopeFactory,
        ILogger<TenantMigrationService> logger)
    {
        _tenantStore = tenantStore;
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <summary>
    /// 迁移所有租户数据库
    /// </summary>
    public async Task MigrateAllTenantsAsync()
    {
        var tenants = await _tenantStore.GetAllAsync();

        foreach (var tenant in tenants.Where(t => t.IsActive))
        {
            try
            {
                await MigrateTenantAsync(tenant.TenantId);
                _logger.LogInformation("租户 {TenantId} 数据库迁移成功", tenant.TenantId);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "租户 {TenantId} 数据库迁移失败", tenant.TenantId);
            }
        }
    }

    /// <summary>
    /// 迁移指定租户数据库
    /// </summary>
    public async Task MigrateTenantAsync(string tenantId)
    {
        using var scope = _scopeFactory.CreateScope();

        // 设置租户上下文
        var httpContext = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext;
        if (httpContext != null)
        {
            httpContext.Items["TenantId"] = tenantId;
        }

        var dbContext = scope.ServiceProvider.GetRequiredService<TenantDbContext>();
        await dbContext.Database.MigrateAsync();
    }

    /// <summary>
    /// 为新租户初始化种子数据
    /// </summary>
    public async Task SeedTenantDataAsync(string tenantId)
    {
        using var scope = _scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TenantDbContext>();

        // 创建默认管理员
        dbContext.Users.Add(new User
        {
            UserName = "admin",
            RealName = "管理员",
            Email = "admin@tenant.com",
            Status = 1,
            TenantId = tenantId,
            CreatedTime = DateTime.Now
        });

        // 创建默认角色
        // ...

        await dbContext.SaveChangesAsync();
    }
}

7.2 应用启动时自动迁移

// Program.cs
var app = builder.Build();

// 应用启动时迁移所有租户数据库
using (var scope = app.Services.CreateScope())
{
    var migrationService = scope.ServiceProvider.GetRequiredService<TenantMigrationService>();
    await migrationService.MigrateAllTenantsAsync();
}

app.Run();

八、多租户与权限结合

8.1 租户级别的权限控制

/// <summary>
/// 租户权限检查过滤器
/// </summary>
public class TenantPermissionFilter : IAsyncAuthorizationFilter
{
    private readonly ITenantContext _tenantContext;

    public TenantPermissionFilter(ITenantContext tenantContext)
    {
        _tenantContext = tenantContext;
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var tenantInfo = _tenantContext.TenantInfo;
        if (tenantInfo == null)
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // 检查租户套餐是否允许访问当前功能
        var endpoint = context.HttpContext.GetEndpoint();
        var requiredPlan = endpoint?.Metadata.GetMetadata<RequirePlanAttribute>();

        if (requiredPlan != null && tenantInfo.Plan < requiredPlan.MinPlan)
        {
            context.Result = new ObjectResult(new
            {
                Message = $"当前套餐不支持此功能,请升级到 {requiredPlan.MinPlan} 以上"
            })
            {
                StatusCode = 403
            };
        }

        await Task.CompletedTask;
    }
}

/// <summary>
/// 套餐要求特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RequirePlanAttribute : Attribute
{
    public TenantPlan MinPlan { get; }

    public RequirePlanAttribute(TenantPlan minPlan)
    {
        MinPlan = minPlan;
    }
}

// 使用示例
[ApiDescriptionSettings("高级功能")]
public class AdvancedAppService : IDynamicApiController
{
    /// <summary>
    /// 数据分析(仅专业版以上可用)
    /// </summary>
    [RequirePlan(TenantPlan.Pro)]
    public async Task<AnalyticsResult> GetAnalytics()
    {
        // 仅 Pro 和 Enterprise 可访问
        return await Task.FromResult(new AnalyticsResult());
    }

    /// <summary>
    /// 自定义报表(仅企业版可用)
    /// </summary>
    [RequirePlan(TenantPlan.Enterprise)]
    public async Task<ReportResult> GetCustomReport()
    {
        // 仅 Enterprise 可访问
        return await Task.FromResult(new ReportResult());
    }
}

8.2 数据权限与租户结合

/// <summary>
/// 租户数据权限服务
/// </summary>
public class TenantDataPermissionService
{
    private readonly ITenantContext _tenantContext;
    private readonly IRepository<User> _userRepository;

    public TenantDataPermissionService(
        ITenantContext tenantContext,
        IRepository<User> userRepository)
    {
        _tenantContext = tenantContext;
        _userRepository = userRepository;
    }

    /// <summary>
    /// 获取当前租户的用户列表
    /// 全局过滤器会自动添加 TenantId 条件
    /// </summary>
    public async Task<List<User>> GetTenantUsers()
    {
        // 由于全局过滤器,这里只会返回当前租户的数据
        return await _userRepository.AsQueryable().ToListAsync();
    }

    /// <summary>
    /// 跨租户查询(仅平台管理员使用)
    /// </summary>
    public async Task<List<User>> GetAllTenantsUsers()
    {
        // 忽略全局过滤器
        return await _userRepository.AsQueryable()
            .IgnoreQueryFilters()
            .ToListAsync();
    }
}

九、SaaS 平台实践

9.1 完整的多租户架构示例

/// <summary>
/// SaaS 平台启动配置
/// </summary>
public static class SaaSStartup
{
    public static void ConfigureMultiTenant(this WebApplicationBuilder builder)
    {
        // 1. 注册租户服务
        builder.Services.AddScoped<ITenantResolver, CompositeTenantResolver>();
        builder.Services.AddScoped<ITenantContext, TenantContext>();
        builder.Services.AddSingleton<ITenantStore, DbTenantStore>();

        // 2. 注册主数据库(管理租户信息)
        builder.Services.AddDbContext<MasterDbContext>(options =>
        {
            options.UseSqlServer(builder.Configuration.GetConnectionString("Master"));
        });

        // 3. 注册租户数据库
        builder.Services.AddDbContext<TenantDbContext>((sp, options) =>
        {
            var tenantContext = sp.GetRequiredService<ITenantContext>();
            if (tenantContext.IsResolved)
            {
                var connStr = tenantContext.TenantInfo?.ConnectionString;
                if (!string.IsNullOrEmpty(connStr))
                {
                    options.UseSqlServer(connStr);
                }
            }
        });

        // 4. 注册迁移服务
        builder.Services.AddScoped<TenantMigrationService>();
        builder.Services.AddScoped<TenantSchemaService>();
    }

    public static void UseMultiTenant(this WebApplication app)
    {
        app.UseMiddleware<TenantMiddleware>();
    }
}

9.2 租户资源配额管理

/// <summary>
/// 租户资源配额
/// </summary>
public class TenantQuota
{
    public string TenantId { get; set; }
    public int MaxUsers { get; set; }       // 最大用户数
    public long MaxStorageMb { get; set; }   // 最大存储空间(MB)
    public int MaxApiCallsPerDay { get; set; } // 每日最大API调用次数
}

/// <summary>
/// 配额检查服务
/// </summary>
public class QuotaCheckService
{
    private readonly ITenantContext _tenantContext;
    private readonly IRepository<User> _userRepository;
    private readonly IMemoryCache _cache;

    public QuotaCheckService(
        ITenantContext tenantContext,
        IRepository<User> userRepository,
        IMemoryCache cache)
    {
        _tenantContext = tenantContext;
        _userRepository = userRepository;
        _cache = cache;
    }

    /// <summary>
    /// 检查用户数量配额
    /// </summary>
    public async Task<bool> CheckUserQuotaAsync()
    {
        var quota = GetQuotaByPlan(_tenantContext.TenantInfo.Plan);
        var currentUserCount = await _userRepository.CountAsync();
        return currentUserCount < quota.MaxUsers;
    }

    /// <summary>
    /// 检查API调用频率
    /// </summary>
    public bool CheckApiRateLimit()
    {
        var quota = GetQuotaByPlan(_tenantContext.TenantInfo.Plan);
        var cacheKey = $"api:count:{_tenantContext.TenantId}:{DateTime.Today:yyyyMMdd}";

        var currentCount = _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpiration = DateTime.Today.AddDays(1);
            return 0;
        });

        if (currentCount >= quota.MaxApiCallsPerDay)
        {
            return false;
        }

        _cache.Set(cacheKey, currentCount + 1);
        return true;
    }

    private TenantQuota GetQuotaByPlan(TenantPlan plan)
    {
        return plan switch
        {
            TenantPlan.Free => new TenantQuota
            { MaxUsers = 5, MaxStorageMb = 100, MaxApiCallsPerDay = 1000 },
            TenantPlan.Basic => new TenantQuota
            { MaxUsers = 50, MaxStorageMb = 1024, MaxApiCallsPerDay = 10000 },
            TenantPlan.Pro => new TenantQuota
            { MaxUsers = 500, MaxStorageMb = 10240, MaxApiCallsPerDay = 100000 },
            TenantPlan.Enterprise => new TenantQuota
            { MaxUsers = int.MaxValue, MaxStorageMb = 102400, MaxApiCallsPerDay = int.MaxValue },
            _ => new TenantQuota
            { MaxUsers = 5, MaxStorageMb = 100, MaxApiCallsPerDay = 1000 }
        };
    }
}

十、性能考虑

10.1 多租户性能优化策略

策略 说明 适用场景
租户信息缓存 缓存租户配置,减少数据库查询 所有方案
连接池管理 合理配置数据库连接池大小 独立数据库方案
查询优化 TenantId 列添加索引 行级隔离方案
读写分离 大租户使用读写分离 高并发场景
分库分表 超大租户数据分片 海量数据场景
CDN 加速 静态资源使用 CDN 所有方案

10.2 数据库索引优化

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 为 TenantId 创建索引
    modelBuilder.Entity<User>()
        .HasIndex(u => u.TenantId)
        .HasDatabaseName("IX_Users_TenantId");

    // 复合索引
    modelBuilder.Entity<User>()
        .HasIndex(u => new { u.TenantId, u.Status })
        .HasDatabaseName("IX_Users_TenantId_Status");

    modelBuilder.Entity<Order>()
        .HasIndex(o => new { o.TenantId, o.CreatedTime })
        .HasDatabaseName("IX_Orders_TenantId_CreatedTime");
}

10.3 连接管理优化

/// <summary>
/// 租户数据库连接池管理
/// </summary>
public class TenantConnectionManager
{
    private readonly ConcurrentDictionary<string, DbContextOptions<TenantDbContext>> _optionsCache = new();

    public DbContextOptions<TenantDbContext> GetOrCreateOptions(string tenantId, string connectionString)
    {
        return _optionsCache.GetOrAdd(tenantId, _ =>
        {
            var optionsBuilder = new DbContextOptionsBuilder<TenantDbContext>();
            optionsBuilder.UseSqlServer(connectionString, sqlOptions =>
            {
                sqlOptions.EnableRetryOnFailure(3); // 自动重试
                sqlOptions.CommandTimeout(30);       // 命令超时
            });

            return optionsBuilder.Options;
        });
    }

    /// <summary>
    /// 移除租户连接配置(用于租户删除或配置变更时)
    /// </summary>
    public void RemoveOptions(string tenantId)
    {
        _optionsCache.TryRemove(tenantId, out _);
    }
}

10.4 方案选择建议

因素 独立数据库 Schema 隔离 行级隔离
租户数量 < 100 ✅ 推荐 ✅ 可选 ✅ 可选
租户数量 100-1000 ⚠️ 成本高 ✅ 推荐 ✅ 推荐
租户数量 > 1000 ❌ 不建议 ⚠️ 管理复杂 ✅ 推荐
数据安全性要求极高 ✅ 推荐 ✅ 可选 ⚠️ 需额外措施
需要独立备份 ✅ 推荐 ⚠️ 较复杂 ❌ 困难
运维成本敏感 ❌ 高 ⚠️ 中 ✅ 低

多租户架构的选择应根据业务规模、安全要求、运维能力和成本预算综合考虑。Furion 框架提供了灵活的多租户支持,可以根据实际需求选择最适合的方案。在实际项目中,建议从行级隔离方案开始,随着业务增长和需求变化逐步升级到更高级别的隔离方案。


上一章: 第14章-对象映射 | 返回目录 | 下一章: 第16章-规范化接口与统一返回