多商户多租户系统架构设计文档(Java版)

多商户多租户系统架构设计文档(Java版)

版本 :v1.0

日期 :2026-07-31

技术栈 :Java 17 + Spring Boot 3.x + Spring Cloud + MySQL + Redis + RocketMQ

文档目标:提供一套可落地、可实施的多商户多租户系统架构方案,包含架构设计、数据库设计、核心代码、部署方案与实施路线图。


目录

  1. 概述
  2. 多租户架构模式对比与选型
  3. 系统总体架构设计
  4. 技术栈选型
  5. 数据库设计
  6. 租户隔离方案详解
  7. 权限体系设计
  8. 核心业务模块设计
  9. API设计规范
  10. 缓存策略设计
  11. 消息队列与异步处理
  12. 部署架构
  13. 高可用与扩展性设计
  14. 安全设计
  15. 监控与运维
  16. 实施路线图
  17. 关键代码示例

1. 概述

1.1 什么是多商户多租户系统

多商户(Multi-Merchant):系统中存在多个独立经营的商户主体,每个商户拥有自己的商品、订单、用户、资金等业务数据。

多租户(Multi-Tenant):一套软件系统同时为多个租户(Tenant)提供服务,每个租户的数据相互隔离,租户之间互不感知。

在本方案中,租户 = 商户,每个入驻平台的商户即为一个租户,拥有独立的业务空间和数据隔离。

1.2 适用场景

  • SaaS 电商平台(多商家入驻)
  • 企业服务平台(多公司/多部门)
  • 连锁门店管理系统
  • 教育培训平台(多机构入驻)
  • 医疗健康平台(多医院/诊所)

1.3 核心设计目标

目标 说明
数据隔离 租户间数据严格隔离,防止越权访问
弹性扩展 支持租户数量从几十到十万级平滑扩展
独立配置 每个租户可独立配置业务规则、界面风格等
统一运维 平台方统一部署、监控、升级,降低运维成本
高性能 单租户请求响应时间 < 200ms,系统支持 10万+ QPS
高可用 系统可用性 99.99%,单租户故障不影响其他租户

2. 多租户架构模式对比与选型

2.1 三种主流模式对比

维度 独立数据库(Database per Tenant) 共享数据库独立Schema(Schema per Tenant) 共享数据库共享表(Shared Database Shared Table)
隔离级别 ⭐⭐⭐⭐⭐ 最高 ⭐⭐⭐⭐ 高 ⭐⭐ 低
数据安全 物理隔离,最安全 Schema隔离,较安全 逻辑隔离,依赖应用层
租户数量 几十~几百 几百~几千 几千~百万级
运维复杂度 高(每个库独立运维)
成本
定制化能力 最强(可独立改表结构) 较强 弱(统一Schema)
迁移难度 简单(整库迁移) 中等 困难(数据筛选)
备份恢复 独立备份,粒度细 Schema级备份 全量备份,恢复困难

2.2 本方案选型:混合模式

推荐采用「共享数据库共享表 + 重要租户独立Schema」的混合模式

复制代码
┌─────────────────────────────────────────────┐
│              平台统一接入层                    │
├──────────┬──────────┬───────────────────────┤
│  租户A   │  租户B   │     普通租户池          │
│ (VIP)   │ (VIP)   │  (共享表 + tenant_id)   │
│ 独立Schema│ 独立Schema│  C / D / E / F / ...  │
└──────────┴──────────┴───────────────────────┘

选型理由

  1. 90% 中小租户使用共享表模式,成本低、扩展快
  2. 10% 大客户/VIP租户使用独立Schema,满足数据安全与定制化需求
  3. 平台可根据租户等级动态切换隔离策略
  4. 兼顾成本与安全,适合大多数 SaaS 场景

3. 系统总体架构设计

3.1 分层架构图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                        接入层 (Gateway)                       │
│  Nginx + Spring Cloud Gateway + 限流 + 鉴权 + 路由            │
├─────────────────────────────────────────────────────────────┤
│                      业务服务层 (Services)                     │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│  │ 租户服务 │ │ 用户服务 │ │ 商品服务 │ │ 订单服务 │ │支付服务│ │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └────────┘ │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│  │ 营销服务 │ │ 库存服务 │ │ 消息服务 │ │ 文件服务 │ │搜索服务│ │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └────────┘ │
├─────────────────────────────────────────────────────────────┤
│                     公共服务层 (Common)                       │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐            │
│  │ 认证中心 │ │ 权限中心 │ │ 租户上下文│ │ 配置中心 │            │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘            │
├─────────────────────────────────────────────────────────────┤
│                      数据存储层 (Storage)                     │
│  MySQL集群 │ Redis集群 │ RocketMQ │ Elasticsearch │ MinIO    │
├─────────────────────────────────────────────────────────────┤
│                     基础设施层 (Infrastructure)               │
│  Kubernetes │ Prometheus │ SkyWalking │ ELK │ Nacos          │
└─────────────────────────────────────────────────────────────┘

3.2 架构设计原则

  1. 租户上下文贯穿全链路:每次请求必须携带租户标识,从网关到数据库全程传递
  2. 服务无状态化:业务服务不保存租户状态,水平扩展不受限
  3. 数据隔离优先:隔离方案在架构层面强制保证,不依赖业务代码自觉
  4. 故障隔离:单租户故障(如慢查询、流量突增)不影响其他租户
  5. 配置外置:租户级配置统一管理,支持热更新

4. 技术栈选型

4.1 后端技术栈

分类 技术选型 版本 说明
语言 Java 17 (LTS) 长期支持版本,性能稳定
框架 Spring Boot 3.2.x 微服务基础框架
微服务 Spring Cloud 2023.0.x 微服务全家桶
网关 Spring Cloud Gateway 4.1.x 响应式网关
注册中心 Nacos 2.3.x 服务注册与配置中心
ORM MyBatis-Plus 3.5.x 增强版 MyBatis,支持多租户插件
数据库 MySQL 8.0 主从复制 + 分库分表
缓存 Redis 7.x 集群模式,缓存 + 分布式锁
消息队列 RocketMQ 5.x 事务消息 + 延时消息
搜索引擎 Elasticsearch 8.x 全文检索 + 日志存储
对象存储 MinIO - 私有云对象存储
认证授权 Spring Security + JWT - OAuth2 + JWT
分布式事务 Seata 1.7.x AT 模式 + TCC 模式
链路追踪 SkyWalking 9.x 全链路追踪
监控 Prometheus + Grafana - 指标监控
日志 ELK (Elasticsearch + Logstash + Kibana) - 日志收集分析
容器编排 Kubernetes 1.28+ 容器化部署
API文档 Knife4j 4.x Swagger增强版

4.2 前端技术栈(参考)

分类 技术选型 说明
框架 Vue 3 + TypeScript 渐进式框架
UI组件库 Element Plus / Ant Design Vue 企业级组件库
状态管理 Pinia Vue官方推荐
构建工具 Vite 极速构建
微前端 qiankun 多应用集成

5. 数据库设计

5.1 数据库分库策略

复制代码
┌─────────────────────────────────────────┐
│          platform_db (平台库)             │
│  租户信息 / 平台配置 / 字典 / 运营数据     │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│        tenant_shared_db (共享租户库)       │
│  普通租户业务数据,通过 tenant_id 隔离      │
│  按 tenant_id 哈希分表(如 16 张分表)      │
└─────────────────────────────────────────┘

┌──────────┐ ┌──────────┐ ┌──────────┐
│ tenant_  │ │ tenant_  │ │ tenant_  │  ...
│  1001_db │ │  1002_db │ │  1003_db │  (VIP租户独立库)
└──────────┘ └──────────┘ └──────────┘

5.2 平台库核心表结构

5.2.1 租户表(sys_tenant)
sql 复制代码
CREATE TABLE `sys_tenant` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID',
  `tenant_code` varchar(32) NOT NULL COMMENT '租户编码(唯一标识,如企业简称拼音)',
  `tenant_name` varchar(128) NOT NULL COMMENT '租户名称',
  `tenant_type` tinyint NOT NULL DEFAULT '1' COMMENT '租户类型:1-普通租户 2-VIP租户 3-平台自营',
  `isolation_level` tinyint NOT NULL DEFAULT '1' COMMENT '隔离级别:1-共享表 2-独立Schema 3-独立数据库',
  `schema_name` varchar(64) DEFAULT NULL COMMENT '独立Schema名称(isolation_level=2/3时使用)',
  `db_url` varchar(512) DEFAULT NULL COMMENT '独立数据库连接URL',
  `db_username` varchar(64) DEFAULT NULL COMMENT '数据库用户名(加密存储)',
  `db_password` varchar(256) DEFAULT NULL COMMENT '数据库密码(加密存储)',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常 2-过期 3-欠费',
  `expire_time` datetime DEFAULT NULL COMMENT '到期时间',
  `max_user_count` int DEFAULT '100' COMMENT '最大用户数',
  `max_storage_mb` bigint DEFAULT '1024' COMMENT '最大存储空间(MB)',
  `contact_name` varchar(64) DEFAULT NULL COMMENT '联系人',
  `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
  `contact_email` varchar(128) DEFAULT NULL COMMENT '联系邮箱',
  `domain` varchar(128) DEFAULT NULL COMMENT '绑定域名',
  `logo_url` varchar(512) DEFAULT NULL COMMENT '租户Logo',
  `theme_config` json DEFAULT NULL COMMENT '主题配置JSON',
  `extra_config` json DEFAULT NULL COMMENT '扩展配置JSON',
  `creator` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除:0-未删除 1-已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_code` (`tenant_code`),
  KEY `idx_status` (`status`),
  KEY `idx_tenant_type` (`tenant_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户信息表';
5.2.2 租户套餐表(sys_tenant_package)
sql 复制代码
CREATE TABLE `sys_tenant_package` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '套餐ID',
  `package_code` varchar(32) NOT NULL COMMENT '套餐编码',
  `package_name` varchar(64) NOT NULL COMMENT '套餐名称',
  `package_type` tinyint NOT NULL DEFAULT '1' COMMENT '套餐类型:1-基础版 2-专业版 3-企业版 4-定制版',
  `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格(元/月)',
  `year_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '年付价格(元/年)',
  `max_user_count` int NOT NULL DEFAULT '10' COMMENT '最大用户数',
  `max_storage_mb` bigint NOT NULL DEFAULT '1024' COMMENT '最大存储空间(MB)',
  `menu_ids` json DEFAULT NULL COMMENT '包含菜单ID列表',
  `permissions` json DEFAULT NULL COMMENT '包含权限标识列表',
  `features` json DEFAULT NULL COMMENT '功能特性列表',
  `description` varchar(512) DEFAULT NULL COMMENT '套餐描述',
  `sort` int DEFAULT '0' COMMENT '排序',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-下架 1-上架',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_package_code` (`package_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户套餐表';
5.2.3 数据源配置表(sys_datasource_config)
sql 复制代码
CREATE TABLE `sys_datasource_config` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `ds_name` varchar(64) NOT NULL COMMENT '数据源名称',
  `ds_type` varchar(32) NOT NULL DEFAULT 'mysql' COMMENT '数据源类型:mysql/postgresql/oracle',
  `driver_class_name` varchar(128) NOT NULL COMMENT '驱动类名',
  `url` varchar(512) NOT NULL COMMENT '连接URL',
  `username` varchar(64) NOT NULL COMMENT '用户名',
  `password` varchar(256) NOT NULL COMMENT '密码(AES加密)',
  `initial_size` int DEFAULT '5' COMMENT '初始连接数',
  `max_active` int DEFAULT '20' COMMENT '最大活跃连接数',
  `min_idle` int DEFAULT '5' COMMENT '最小空闲连接数',
  `max_wait` bigint DEFAULT '60000' COMMENT '获取连接最大等待时间(ms)',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-启用',
  `remark` varchar(256) DEFAULT NULL COMMENT '备注',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据源配置表';

5.3 租户业务库核心表结构

以下表结构适用于「共享表模式」,每张业务表都包含 tenant_id 字段用于租户隔离。

5.3.1 用户表(sys_user)
sql 复制代码
CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `username` varchar(64) NOT NULL COMMENT '用户名',
  `password` varchar(128) NOT NULL COMMENT '密码(BCrypt加密)',
  `nickname` varchar(64) DEFAULT NULL COMMENT '昵称',
  `real_name` varchar(64) DEFAULT NULL COMMENT '真实姓名',
  `avatar` varchar(512) DEFAULT NULL COMMENT '头像URL',
  `email` varchar(128) DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
  `gender` tinyint DEFAULT '0' COMMENT '性别:0-未知 1-男 2-女',
  `user_type` tinyint NOT NULL DEFAULT '2' COMMENT '用户类型:1-租户管理员 2-普通用户 3-子账号',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `last_login_ip` varchar(64) DEFAULT NULL COMMENT '最后登录IP',
  `remark` varchar(512) DEFAULT NULL COMMENT '备注',
  `creator` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_username` (`tenant_id`, `username`),
  KEY `idx_tenant_id` (`tenant_id`),
  KEY `idx_mobile` (`mobile`),
  KEY `idx_dept_id` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
5.3.2 角色表(sys_role)
sql 复制代码
CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `role_name` varchar(64) NOT NULL COMMENT '角色名称',
  `role_code` varchar(64) NOT NULL COMMENT '角色编码',
  `role_type` tinyint NOT NULL DEFAULT '2' COMMENT '角色类型:1-系统内置 2-自定义',
  `data_scope` tinyint NOT NULL DEFAULT '1' COMMENT '数据权限范围:1-全部数据 2-本部门 3-本部门及以下 4-仅本人 5-自定义',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
  `sort` int DEFAULT '0' COMMENT '排序',
  `remark` varchar(512) DEFAULT NULL COMMENT '备注',
  `creator` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_role_code` (`tenant_id`, `role_code`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
5.3.3 用户角色关联表(sys_user_role)
sql 复制代码
CREATE TABLE `sys_user_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_user_role` (`tenant_id`, `user_id`, `role_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';
5.3.4 部门表(sys_dept)
sql 复制代码
CREATE TABLE `sys_dept` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父部门ID',
  `ancestors` varchar(512) DEFAULT '' COMMENT '祖级列表(逗号分隔)',
  `dept_name` varchar(64) NOT NULL COMMENT '部门名称',
  `dept_code` varchar(64) DEFAULT NULL COMMENT '部门编码',
  `sort` int DEFAULT '0' COMMENT '排序',
  `leader` varchar(64) DEFAULT NULL COMMENT '负责人',
  `phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
  `email` varchar(128) DEFAULT NULL COMMENT '邮箱',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
  `creator` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  KEY `idx_tenant_id` (`tenant_id`),
  KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表';
5.3.5 商品表(biz_product)
sql 复制代码
CREATE TABLE `biz_product` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `product_name` varchar(256) NOT NULL COMMENT '商品名称',
  `product_no` varchar(64) NOT NULL COMMENT '商品编号',
  `category_id` bigint DEFAULT NULL COMMENT '分类ID',
  `brand_id` bigint DEFAULT NULL COMMENT '品牌ID',
  `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '销售价',
  `market_price` decimal(10,2) DEFAULT '0.00' COMMENT '市场价',
  `cost_price` decimal(10,2) DEFAULT '0.00' COMMENT '成本价',
  `stock` int NOT NULL DEFAULT '0' COMMENT '库存数量',
  `stock_warning` int DEFAULT '0' COMMENT '库存预警值',
  `main_image` varchar(512) DEFAULT NULL COMMENT '主图URL',
  `images` json DEFAULT NULL COMMENT '商品图片列表JSON',
  `description` text COMMENT '商品描述',
  `specs` json DEFAULT NULL COMMENT '规格JSON',
  `attributes` json DEFAULT NULL COMMENT '属性JSON',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-下架 1-上架 2-售罄',
  `sales_count` int DEFAULT '0' COMMENT '销量',
  `sort` int DEFAULT '0' COMMENT '排序',
  `creator` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_product_no` (`tenant_id`, `product_no`),
  KEY `idx_tenant_id` (`tenant_id`),
  KEY `idx_category_id` (`category_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';
5.3.6 订单表(biz_order)
sql 复制代码
CREATE TABLE `biz_order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `order_no` varchar(32) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '下单用户ID',
  `total_amount` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额',
  `pay_amount` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额',
  `freight_amount` decimal(10,2) DEFAULT '0.00' COMMENT '运费',
  `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额',
  `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-待付款 1-待发货 2-已发货 3-已完成 4-已取消 5-退款中',
  `pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态:0-未支付 1-已支付 2-部分退款 3-全额退款',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `pay_type` tinyint DEFAULT NULL COMMENT '支付方式:1-微信 2-支付宝 3-银行卡',
  `consignee_name` varchar(64) DEFAULT NULL COMMENT '收货人姓名',
  `consignee_phone` varchar(20) DEFAULT NULL COMMENT '收货人电话',
  `consignee_address` varchar(512) DEFAULT NULL COMMENT '收货地址',
  `remark` varchar(512) DEFAULT NULL COMMENT '订单备注',
  `creator` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_order_no` (`tenant_id`, `order_no`),
  KEY `idx_tenant_id` (`tenant_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_order_status` (`order_status`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';

5.4 分库分表策略

大表分表策略(以订单表为例)

复制代码
按 tenant_id 哈希分库(8个库)+ 按订单创建时间分表(每月一张)

db_order_0: biz_order_202607, biz_order_202608, ...
db_order_1: biz_order_202607, biz_order_202608, ...
...
db_order_7: biz_order_202607, biz_order_202608, ...

分库分表中间件:使用 ShardingSphere-JDBC


6. 租户隔离方案详解

6.1 租户标识传递机制

6.1.1 租户标识来源优先级
复制代码
1. 请求域名(tenant.example.com)→ 最高优先级
2. 请求头(X-Tenant-Id / X-Tenant-Code)→ 次优先级
3. JWT Token 中的 tenantId 字段 → 默认
4. 请求参数(tenantId)→ 最低优先级(仅开发环境)
6.1.2 全链路传递流程
复制代码
客户端请求
    │
    ▼
[网关层] 解析租户标识 → 写入请求头 X-Tenant-Id
    │
    ▼
[服务层] TenantInterceptor 拦截 → 写入 ThreadLocal
    │
    ▼
[DAO层] MyBatis-Plus 多租户插件 → 自动拼接 tenant_id 条件
    │
    ▼
[数据库] 按 tenant_id 隔离数据

6.2 MyBatis-Plus 多租户插件配置

java 复制代码
/**
 * MyBatis-Plus 多租户配置
 */
@Configuration
@MapperScan("com.example.**.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 1. 多租户插件(必须放在最前面)
        interceptor.addInnerInterceptor(tenantLineInnerInterceptor());
        
        // 2. 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        
        // 3. 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        
        return interceptor;
    }

    @Bean
    public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
        TenantLineInnerInterceptor interceptor = new TenantLineInnerInterceptor();
        interceptor.setTenantLineHandler(new TenantLineHandler() {
            
            @Override
            public Expression getTenantId() {
                // 从 ThreadLocal 获取当前租户ID
                Long tenantId = TenantContextHolder.getTenantId();
                if (tenantId == null) {
                    throw new BizException("租户信息不存在,请重新登录");
                }
                return new LongValue(tenantId);
            }

            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            @Override
            public boolean ignoreTable(String tableName) {
                // 忽略不需要租户隔离的表(平台级表)
                return TenantIgnoreTableHolder.isIgnored(tableName);
            }
        });
        return interceptor;
    }

    @Bean
    public PaginationInnerInterceptor paginationInnerInterceptor() {
        PaginationInnerInterceptor interceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        interceptor.setMaxLimit(500L); // 单页最大条数
        return interceptor;
    }
}

6.3 租户上下文持有器

java 复制代码
/**
 * 租户上下文持有器
 * 使用 TransmittableThreadLocal 支持线程池场景下的上下文传递
 */
public class TenantContextHolder {

    private static final TransmittableThreadLocal<Long> TENANT_ID_HOLDER = 
        new TransmittableThreadLocal<>();
    
    private static final TransmittableThreadLocal<String> TENANT_CODE_HOLDER = 
        new TransmittableThreadLocal<>();
    
    private static final TransmittableThreadLocal<TenantInfo> TENANT_INFO_HOLDER = 
        new TransmittableThreadLocal<>();

    public static void setTenantId(Long tenantId) {
        TENANT_ID_HOLDER.set(tenantId);
    }

    public static Long getTenantId() {
        return TENANT_ID_HOLDER.get();
    }

    public static void setTenantCode(String tenantCode) {
        TENANT_CODE_HOLDER.set(tenantCode);
    }

    public static String getTenantCode() {
        return TENANT_CODE_HOLDER.get();
    }

    public static void setTenantInfo(TenantInfo tenantInfo) {
        TENANT_INFO_HOLDER.set(tenantInfo);
    }

    public static TenantInfo getTenantInfo() {
        return TENANT_INFO_HOLDER.get();
    }

    public static void clear() {
        TENANT_ID_HOLDER.remove();
        TENANT_CODE_HOLDER.remove();
        TENANT_INFO_HOLDER.remove();
    }
}

6.4 租户拦截器

java 复制代码
/**
 * 租户上下文拦截器
 * 在请求处理前设置租户上下文,请求结束后清理
 */
@Component
@Slf4j
public class TenantInterceptor implements HandlerInterceptor {

    @Autowired
    private TenantCacheService tenantCacheService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                            Object handler) throws Exception {
        // 1. 从请求头获取租户ID
        String tenantIdStr = request.getHeader("X-Tenant-Id");
        String tenantCode = request.getHeader("X-Tenant-Code");
        
        Long tenantId = null;
        
        // 2. 优先使用租户ID
        if (StringUtils.hasText(tenantIdStr)) {
            tenantId = Long.parseLong(tenantIdStr);
        }
        // 3. 其次使用租户编码查询
        else if (StringUtils.hasText(tenantCode)) {
            TenantInfo tenantInfo = tenantCacheService.getByCode(tenantCode);
            if (tenantInfo != null) {
                tenantId = tenantInfo.getId();
            }
        }
        // 4. 从JWT Token中解析(兜底)
        else {
            tenantId = SecurityUtils.getTenantIdFromToken();
        }
        
        // 5. 设置租户上下文
        if (tenantId != null) {
            TenantContextHolder.setTenantId(tenantId);
            TenantInfo tenantInfo = tenantCacheService.getById(tenantId);
            if (tenantInfo != null) {
                TenantContextHolder.setTenantInfo(tenantInfo);
                TenantContextHolder.setTenantCode(tenantInfo.getTenantCode());
                
                // 6. 校验租户状态
                if (tenantInfo.getStatus() != 1) {
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(JSON.toJSONString(
                        Result.fail("租户状态异常,请联系管理员")));
                    return false;
                }
            }
        }
        
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) {
        // 请求结束后清理上下文,防止内存泄漏
        TenantContextHolder.clear();
    }
}

6.5 动态数据源切换(独立Schema租户)

java 复制代码
/**
 * 动态数据源路由
 * 支持VIP租户切换到独立数据源
 */
@Component
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Autowired
    private TenantCacheService tenantCacheService;

    // 数据源缓存:tenantId -> DataSource
    private final Map<Long, DataSource> tenantDataSourceMap = new ConcurrentHashMap<>();

    @Override
    protected Object determineCurrentLookupKey() {
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            return "default"; // 默认数据源(共享表模式)
        }
        
        TenantInfo tenantInfo = tenantCacheService.getById(tenantId);
        if (tenantInfo != null && tenantInfo.getIsolationLevel() >= 2) {
            // VIP租户使用独立数据源
            return "tenant_" + tenantId;
        }
        
        return "default";
    }

    /**
     * 动态添加租户数据源
     */
    public void addTenantDataSource(Long tenantId, DataSourceProperties properties) {
        if (tenantDataSourceMap.containsKey(tenantId)) {
            return;
        }
        
        // 创建新的数据源
        HikariDataSource dataSource = createDataSource(properties);
        
        // 添加到动态数据源
        Map<Object, Object> targetDataSources = new HashMap<>(
            this.getResolvedDataSources());
        targetDataSources.put("tenant_" + tenantId, dataSource);
        this.setTargetDataSources(targetDataSources);
        this.afterPropertiesSet();
        
        tenantDataSourceMap.put(tenantId, dataSource);
        log.info("已添加租户[{}]的独立数据源", tenantId);
    }

    private HikariDataSource createDataSource(DataSourceProperties properties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(properties.getUrl());
        dataSource.setUsername(properties.getUsername());
        dataSource.setPassword(properties.getPassword());
        dataSource.setDriverClassName(properties.getDriverClassName());
        dataSource.setMaximumPoolSize(properties.getMaxActive());
        dataSource.setMinimumIdle(properties.getMinIdle());
        dataSource.setConnectionTimeout(properties.getMaxWait());
        return dataSource;
    }
}

7. 权限体系设计

7.1 权限模型:RBAC + 数据权限

复制代码
┌─────────┐     ┌─────────┐     ┌─────────┐
│  用户   │────>│  角色   │────>│  权限   │
│ (User)  │ N:M │ (Role)  │ N:M │ (Perm)  │
└─────────┘     └─────────┘     └─────────┘
     │              │
     │              ▼
     │         ┌─────────┐
     │         │ 数据权限 │
     │         │(DataScope)
     │         └─────────┘
     ▼
┌─────────┐
│  部门   │
│ (Dept)  │
└─────────┘

7.2 数据权限范围

范围 说明 SQL条件示例
全部数据 可查看所有租户内数据 无额外条件
本部门 只能看本部门数据 dept_id = 当前部门ID
本部门及以下 本部门及子部门数据 dept_id IN (当前部门及所有子部门ID)
仅本人 只能看自己创建的数据 creator = 当前用户ID
自定义 可指定部门范围 dept_id IN (指定的部门ID列表)

7.3 数据权限拦截器实现

java 复制代码
/**
 * 数据权限拦截器
 * 基于MyBatis-Plus插件实现数据行级权限控制
 */
@Component
@Slf4j
public class DataScopeInterceptor implements InnerInterceptor {

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, 
                           RowBounds rowBounds, ResultHandler resultHandler, 
                           BoundSql boundSql) throws SQLException {
        // 获取方法上的 @DataScope 注解
        DataScope dataScope = getDataScopeAnnotation(ms);
        if (dataScope == null) {
            return;
        }
        
        // 获取当前用户的数据权限
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (loginUser == null) {
            return;
        }
        
        // 管理员跳过数据权限
        if (loginUser.isAdmin()) {
            return;
        }
        
        // 构建数据权限SQL
        String sql = boundSql.getSql();
        String dataScopeSql = buildDataScopeSql(loginUser, dataScope);
        
        if (StringUtils.hasText(dataScopeSql)) {
            // 拼接数据权限条件
            String newSql = "SELECT * FROM (" + sql + ") temp_data_scope WHERE " + dataScopeSql;
            ReflectionUtils.setFieldValue(boundSql, "sql", newSql);
        }
    }

    private String buildDataScopeSql(LoginUser loginUser, DataScope dataScope) {
        Integer dataScopeType = loginUser.getDataScope();
        String deptAlias = dataScope.deptAlias();
        String userAlias = dataScope.userAlias();
        
        StringBuilder sql = new StringBuilder();
        
        switch (dataScopeType) {
            case 1: // 全部数据
                return "";
            case 2: // 本部门
                sql.append(deptAlias).append(".dept_id = ").append(loginUser.getDeptId());
                break;
            case 3: // 本部门及以下
                List<Long> deptIds = getDeptAndChildIds(loginUser.getDeptId());
                sql.append(deptAlias).append(".dept_id IN (")
                   .append(StringUtils.join(deptIds, ",")).append(")");
                break;
            case 4: // 仅本人
                sql.append(userAlias).append(".user_id = ").append(loginUser.getUserId());
                break;
            case 5: // 自定义
                List<Long> customDeptIds = loginUser.getCustomDeptIds();
                if (CollectionUtils.isNotEmpty(customDeptIds)) {
                    sql.append(deptAlias).append(".dept_id IN (")
                       .append(StringUtils.join(customDeptIds, ",")).append(")");
                } else {
                    sql.append("1=0"); // 无权限
                }
                break;
            default:
                sql.append("1=0"); // 默认无权限
        }
        
        return sql.toString();
    }
}

7.4 菜单权限表设计

sql 复制代码
-- 菜单表
CREATE TABLE `sys_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户ID(0表示平台通用菜单)',
  `parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父菜单ID',
  `menu_name` varchar(64) NOT NULL COMMENT '菜单名称',
  `menu_type` tinyint NOT NULL COMMENT '菜单类型:1-目录 2-菜单 3-按钮',
  `path` varchar(256) DEFAULT NULL COMMENT '路由路径',
  `component` varchar(256) DEFAULT NULL COMMENT '组件路径',
  `permission` varchar(128) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(64) DEFAULT NULL COMMENT '菜单图标',
  `sort` int DEFAULT '0' COMMENT '排序',
  `visible` tinyint DEFAULT '1' COMMENT '是否可见:0-隐藏 1-显示',
  `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
  `is_platform` tinyint DEFAULT '0' COMMENT '是否平台菜单:0-否 1-是',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  KEY `idx_tenant_id` (`tenant_id`),
  KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单权限表';

-- 角色菜单关联表
CREATE TABLE `sys_role_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `menu_id` bigint NOT NULL COMMENT '菜单ID',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_role_menu` (`tenant_id`, `role_id`, `menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜单关联表';

8. 核心业务模块设计

8.1 模块划分

复制代码
multi-tenant-platform/
├── platform-gateway/          # 网关服务
├── platform-auth/             # 认证授权服务
├── platform-tenant/           # 租户管理服务
├── platform-system/           # 系统管理服务(用户/角色/菜单/部门)
├── platform-product/          # 商品服务
├── platform-order/            # 订单服务
├── platform-user/             # C端用户服务
├── platform-payment/          # 支付服务
├── platform-marketing/        # 营销服务
├── platform-file/             # 文件服务
├── platform-message/          # 消息服务
├── platform-search/           # 搜索服务
└── platform-common/           # 公共模块
    ├── common-core/           # 核心工具类
    ├── common-redis/          # Redis封装
    ├── common-mybatis/        # MyBatis封装(多租户插件)
    ├── common-security/       # 安全模块
    ├── common-feign/          # Feign封装
    └── common-log/            # 日志模块

8.2 租户管理模块

8.2.1 租户创建流程
复制代码
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ 提交申请  │───>│ 平台审核  │───>│ 初始化租户│───>│ 开通成功  │
│          │    │          │    │  数据     │    │ 发送通知  │
└──────────┘    └──────────┘    └──────────┘    └──────────┘
                                      │
                                      ▼
                    ┌──────────────────────────────┐
                    │ 1. 创建租户记录               │
                    │ 2. 创建租户管理员账号          │
                    │ 3. 初始化角色/菜单/权限        │
                    │ 4. 初始化业务配置              │
                    │ 5. 创建独立Schema(VIP租户)   │
                    │ 6. 分配资源配额                │
                    └──────────────────────────────┘
8.2.2 租户初始化服务
java 复制代码
@Service
@Slf4j
public class TenantInitService {

    @Autowired
    private SysTenantMapper tenantMapper;
    
    @Autowired
    private SysUserMapper userMapper;
    
    @Autowired
    private SysRoleMapper roleMapper;
    
    @Autowired
    private DynamicDataSource dynamicDataSource;

    /**
     * 初始化新租户
     */
    @Transactional(rollbackFor = Exception.class)
    public Long initTenant(TenantInitDTO dto) {
        // 1. 创建租户记录
        SysTenant tenant = new SysTenant();
        tenant.setTenantCode(dto.getTenantCode());
        tenant.setTenantName(dto.getTenantName());
        tenant.setTenantType(dto.getTenantType());
        tenant.setIsolationLevel(dto.getIsolationLevel());
        tenant.setStatus(1);
        tenant.setExpireTime(calculateExpireTime(dto.getPackageId()));
        tenantMapper.insert(tenant);
        
        Long tenantId = tenant.getId();
        
        try {
            // 2. 如果是独立Schema模式,创建Schema
            if (dto.getIsolationLevel() >= 2) {
                createTenantSchema(tenantId, dto);
            }
            
            // 3. 初始化管理员账号
            initAdminUser(tenantId, dto);
            
            // 4. 初始化角色
            initRoles(tenantId);
            
            // 5. 初始化菜单权限
            initMenus(tenantId, dto.getPackageId());
            
            // 6. 初始化业务配置
            initBusinessConfig(tenantId);
            
            // 7. 发送开通通知
            sendTenantCreatedNotification(tenant);
            
            log.info("租户[{}]初始化成功", tenantId);
            return tenantId;
            
        } catch (Exception e) {
            log.error("租户[{}]初始化失败", tenantId, e);
            // 回滚:删除已创建的资源
            rollbackTenantInit(tenantId, dto.getIsolationLevel());
            throw new BizException("租户初始化失败: " + e.getMessage());
        }
    }

    private void createTenantSchema(Long tenantId, TenantInitDTO dto) {
        String schemaName = "tenant_" + tenantId;
        
        // 1. 创建数据库
        try (Connection conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306?useSSL=false", "root", "password");
             Statement stmt = conn.createStatement()) {
            
            stmt.executeUpdate("CREATE DATABASE `" + schemaName + 
                             "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
            
            // 2. 执行建表SQL脚本
            runSchemaScript(schemaName, "classpath:db/tenant_schema.sql");
            
            // 3. 注册到动态数据源
            DataSourceProperties dsProps = new DataSourceProperties();
            dsProps.setUrl("jdbc:mysql://localhost:3306/" + schemaName + 
                          "?useUnicode=true&characterEncoding=utf8mb4");
            dsProps.setUsername("root");
            dsProps.setPassword("password");
            dsProps.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dynamicDataSource.addTenantDataSource(tenantId, dsProps);
            
        } catch (SQLException e) {
            throw new BizException("创建租户Schema失败: " + e.getMessage());
        }
    }

    private void initAdminUser(Long tenantId, TenantInitDTO dto) {
        SysUser adminUser = new SysUser();
        adminUser.setTenantId(tenantId);
        adminUser.setUsername("admin");
        adminUser.setPassword(passwordEncoder.encode("Admin@123")); // 初始密码
        adminUser.setNickname("管理员");
        adminUser.setUserType(1); // 租户管理员
        adminUser.setStatus(1);
        adminUser.setMobile(dto.getContactPhone());
        adminUser.setEmail(dto.getContactEmail());
        userMapper.insert(adminUser);
        
        // 分配超级管理员角色
        assignAdminRole(tenantId, adminUser.getId());
    }

    private void initRoles(Long tenantId) {
        // 预置角色:超级管理员、运营、财务、客服等
        List<SysRole> roles = Arrays.asList(
            createRole(tenantId, "超级管理员", "super_admin", 1, 1),
            createRole(tenantId, "运营管理员", "operation", 2, 1),
            createRole(tenantId, "财务", "finance", 2, 4),
            createRole(tenantId, "客服", "customer_service", 2, 4)
        );
        roles.forEach(roleMapper::insert);
    }
}

8.3 认证授权模块

8.3.1 登录流程
复制代码
用户输入用户名密码 + 租户标识
        │
        ▼
┌─────────────────┐
│  1. 解析租户标识  │
│     (域名/请求头)  │
└────────┬────────┘
         ▼
┌─────────────────┐
│  2. 查询用户信息  │
│  (tenant_id +    │
│   username)      │
└────────┬────────┘
         ▼
┌─────────────────┐
│  3. 校验密码      │
│  (BCrypt匹配)    │
└────────┬────────┘
         ▼
┌─────────────────┐
│  4. 加载权限列表  │
│  (角色+菜单+按钮) │
└────────┬────────┘
         ▼
┌─────────────────┐
│  5. 生成JWT Token│
│  (含tenantId)    │
└────────┬────────┘
         ▼
┌─────────────────┐
│  6. 返回登录结果  │
└─────────────────┘
8.3.2 JWT Token 结构
json 复制代码
{
  "sub": "1234567890",
  "userId": 10001,
  "tenantId": 1001,
  "tenantCode": "acme_corp",
  "username": "admin",
  "userType": 1,
  "authorities": ["system:user:list", "system:role:add", "..."],
  "iat": 1722400000,
  "exp": 1722486400
}

9. API设计规范

9.1 RESTful API 规范

9.1.1 URL 设计
复制代码
# 平台管理API(平台管理员使用)
/api/platform/v1/tenants           # 租户管理
/api/platform/v1/packages          # 套餐管理
/api/platform/v1/statistics        # 平台统计

# 租户业务API(租户内使用)
/api/{tenantCode}/v1/users         # 用户管理
/api/{tenantCode}/v1/roles         # 角色管理
/api/{tenantCode}/v1/products      # 商品管理
/api/{tenantCode}/v1/orders        # 订单管理
9.1.2 统一响应格式
json 复制代码
{
  "code": 200,
  "message": "操作成功",
  "data": {
    // 具体业务数据
  },
  "traceId": "abc123def456",
  "timestamp": 1722400000000
}
9.1.3 分页响应格式
json 复制代码
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "list": [],
    "total": 100,
    "pageNum": 1,
    "pageSize": 10,
    "pages": 10
  }
}

9.2 网关层租户路由配置

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        # 平台管理路由
        - id: platform-admin
          uri: lb://platform-tenant
          predicates:
            - Path=/api/platform/**
          filters:
            - StripPrefix=2
        
        # 租户业务路由(通过子域名识别租户)
        - id: tenant-service
          uri: lb://platform-system
          predicates:
            - Host={tenantCode}.example.com
            - Path=/api/v1/**
          filters:
            - AddRequestHeader=X-Tenant-Code, {tenantCode}
            - StripPrefix=1
        
        # 商品服务路由
        - id: product-service
          uri: lb://platform-product
          predicates:
            - Host={tenantCode}.example.com
            - Path=/api/v1/products/**
          filters:
            - AddRequestHeader=X-Tenant-Code, {tenantCode}
            - StripPrefix=1

10. 缓存策略设计

10.1 缓存架构

复制代码
┌─────────────────────────────────────────┐
│           应用层本地缓存 (Caffeine)        │
│   租户配置 / 字典数据 / 菜单权限           │
├─────────────────────────────────────────┤
│           分布式缓存 (Redis Cluster)       │
│   用户会话 / 热点数据 / 分布式锁 / 计数器   │
├─────────────────────────────────────────┤
│              数据库 (MySQL)                │
│           持久化存储,最终一致              │
└─────────────────────────────────────────┘

10.2 缓存Key命名规范

复制代码
# 格式:{业务前缀}:{租户ID}:{数据类型}:{唯一标识}

tenant:info:1001                    # 租户基本信息
tenant:config:1001:business         # 租户业务配置
user:info:1001:10001                # 用户信息
user:token:1001:abc123              # 用户Token
product:detail:1001:20001           # 商品详情
product:list:1001:category:100      # 商品列表
order:stat:1001:202607              # 订单统计
dict:type:1001:order_status         # 字典数据
menu:user:1001:10001                # 用户菜单

10.3 缓存配置

java 复制代码
@Configuration
public class CacheConfig {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
            .initialCapacity(1000)
            .maximumSize(10000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .recordStats()
            .build();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // Key序列化:String
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        template.setKeySerializer(keySerializer);
        template.setHashKeySerializer(keySerializer);
        
        // Value序列化:JSON
        Jackson2JsonRedisSerializer<Object> valueSerializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
                                    ObjectMapper.DefaultTyping.NON_FINAL);
        valueSerializer.setObjectMapper(mapper);
        
        template.setValueSerializer(valueSerializer);
        template.setHashValueSerializer(valueSerializer);
        template.afterPropertiesSet();
        
        return template;
    }
}

10.4 缓存与数据库一致性策略

场景 策略 说明
读操作 Cache Aside 先读缓存,未命中读DB,回写缓存
写操作 先更新DB,再删缓存 最终一致性,延迟双删
强一致性 分布式锁 + 缓存更新 适用于库存等强一致场景
统计数据 定时刷新 + 主动失效 适用于报表统计

11. 消息队列与异步处理

11.1 消息队列选型:RocketMQ

选型理由

  • 支持事务消息,保证分布式事务一致性
  • 支持延时消息,适用于订单超时取消等场景
  • 支持消息轨迹,便于排查问题
  • 高吞吐、低延迟,适合电商场景

11.2 消息Topic设计

复制代码
# 租户相关
TENANT_EVENTS                    # 租户生命周期事件(创建/停用/续费)

# 订单相关
ORDER_CREATED                    # 订单创建
ORDER_PAID                       # 订单支付
ORDER_CANCELLED                  # 订单取消
ORDER_DELAY_CHECK                # 订单超时检查(延时消息)

# 用户相关
USER_REGISTERED                  # 用户注册
USER_LOGIN                       # 用户登录

# 支付相关
PAYMENT_CALLBACK                 # 支付回调
REFUND_REQUEST                   # 退款申请

# 通知相关
NOTIFICATION_EMAIL               # 邮件通知
NOTIFICATION_SMS                 # 短信通知
NOTIFICATION_PUSH                # 推送通知

# 数据同步
DATA_SYNC_ES                     # ES数据同步
DATA_SYNC_CACHE                  # 缓存数据同步

11.3 消息体规范

json 复制代码
{
  "msgId": "msg_20260731_abc123",
  "tenantId": 1001,
  "eventType": "ORDER_CREATED",
  "timestamp": 1722400000000,
  "source": "platform-order",
  "data": {
    "orderId": 20001,
    "orderNo": "ORD202607310001",
    "userId": 10001,
    "amount": 299.00
  }
}

12. 部署架构

12.1 生产环境部署拓扑

复制代码
                        ┌──────────────┐
                        │    CDN/WAF   │
                        └──────┬───────┘
                               │
                        ┌──────▼───────┐
                        │  Nginx集群   │  (负载均衡 + 静态资源)
                        └──────┬───────┘
                               │
                 ┌─────────────┼─────────────┐
                 ▼             ▼             ▼
          ┌──────────┐  ┌──────────┐  ┌──────────┐
          │ Gateway  │  │ Gateway  │  │ Gateway  │  (网关集群)
          │  Pod x3  │  │  Pod x3  │  │  Pod x3  │
          └─────┬────┘  └─────┬────┘  └─────┬────┘
                └──────────────┼──────────────┘
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
        ┌──────────┐    ┌──────────┐    ┌──────────┐
        │  认证服务  │    │  用户服务  │    │  订单服务  │  (业务服务集群)
        │  Pod x2  │    │  Pod x3  │    │  Pod x4  │
        └──────────┘    └──────────┘    └──────────┘
              │                │                │
              └────────────────┼────────────────┘
                               │
        ┌──────────┬───────────┼───────────┬──────────┐
        ▼          ▼           ▼           ▼          ▼
   ┌─────────┐┌─────────┐┌──────────┐┌─────────┐┌─────────┐
   │ MySQL   ││ Redis   ││ RocketMQ ││   ES    ││  MinIO  │  (中间件集群)
   │  主从   ││ Cluster ││  Cluster ││ Cluster ││ Cluster │
   └─────────┘└─────────┘└──────────┘└─────────┘└─────────┘

12.2 Kubernetes 部署配置示例

yaml 复制代码
# 网关服务 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: platform-gateway
  namespace: multi-tenant
spec:
  replicas: 3
  selector:
    matchLabels:
      app: platform-gateway
  template:
    metadata:
      labels:
        app: platform-gateway
    spec:
      containers:
      - name: gateway
        image: registry.example.com/multi-tenant/platform-gateway:v1.0.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "2000m"
            memory: "1Gi"
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        - name: NACOS_SERVER_ADDR
          value: "nacos.default.svc.cluster.local:8848"
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 15
---
# Service
apiVersion: v1
kind: Service
metadata:
  name: platform-gateway
  namespace: multi-tenant
spec:
  selector:
    app: platform-gateway
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP
---
# HPA 自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: platform-gateway-hpa
  namespace: multi-tenant
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: platform-gateway
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

12.3 数据库部署方案

复制代码
MySQL 集群架构:
┌─────────────────────────────────────────┐
│           ProxySQL (读写分离)             │
├──────────────┬──────────────────────────┤
│   主库 (Master)│   从库1 (Slave1)         │
│   写操作      │   读操作                  │
│              │   半同步复制               │
├──────────────┼──────────────────────────┤
│              │   从库2 (Slave2)          │
│              │   读操作 / 备份            │
│              │   异步复制                 │
└──────────────┴──────────────────────────┘

Redis 集群架构:
3主3从 Cluster 模式,每个主节点负责一部分slot
Sentinel 哨兵模式监控主从切换

13. 高可用与扩展性设计

13.1 高可用设计

13.1.1 服务层高可用
措施 说明
多实例部署 每个服务至少 2 个实例,避免单点故障
健康检查 K8s readiness/liveness 探针,自动摘除异常实例
自动扩缩容 HPA 根据 CPU/内存/QPS 自动扩缩容
熔断降级 Sentinel 熔断保护,防止级联故障
限流 网关层 + 服务层双重限流,保护系统稳定
超时控制 全链路超时设置,避免资源耗尽
13.1.2 数据层高可用
组件 高可用方案
MySQL 主从复制 + MHA 自动故障转移
Redis Cluster 模式 + 哨兵监控
RocketMQ 多副本 + Dledger 自动选主
Elasticsearch 多节点 + 副本分片
Nacos 集群部署(至少3节点)

13.2 扩展性设计

13.2.1 水平扩展
  • 服务层:无状态设计,可无限水平扩展
  • 数据层:分库分表,按 tenant_id 哈希分片
  • 缓存层:Redis Cluster 模式,在线扩容
13.2.2 租户级资源隔离
复制代码
# 资源池隔离策略

1. 连接池隔离
   - 每个VIP租户独立数据库连接池
   - 普通租户共享连接池,但设置租户级连接数上限

2. 线程池隔离
   - 关键服务使用租户级线程池
   - 防止单租户慢请求耗尽线程

3. 缓存隔离
   - 按租户划分缓存namespace
   - 设置租户级缓存容量上限

4. MQ隔离
   - VIP租户使用独立Topic
   - 普通租户共享Topic,但按租户ID消费
13.2.3 租户级限流配置
java 复制代码
/**
 * 租户级限流过滤器
 */
@Component
public class TenantRateLimitFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String tenantCode = exchange.getRequest().getHeaders().getFirst("X-Tenant-Code");
        
        if (StringUtils.hasText(tenantCode)) {
            // 获取租户限流配置
            TenantRateLimitConfig config = getTenantRateLimitConfig(tenantCode);
            
            // 令牌桶限流
            String key = "gateway:ratelimit:" + tenantCode;
            Long count = redisTemplate.opsForValue().increment(key, 1);
            
            if (count == 1) {
                redisTemplate.expire(key, 1, TimeUnit.SECONDS);
            }
            
            if (count > config.getQpsLimit()) {
                exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                return exchange.getResponse().setComplete();
            }
        }
        
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

14. 安全设计

14.1 认证安全

措施 说明
JWT Token 无状态认证,含租户ID、用户ID、权限信息
Token刷新 Access Token(2小时) + Refresh Token(7天)
密码加密 BCrypt 加密,强度因子 12
登录保护 密码错误5次锁定15分钟,IP限流
双因素认证 支持短信/邮箱验证码二次验证
HTTPS 全站 HTTPS,TLS 1.3

14.2 数据安全

措施 说明
数据隔离 租户间数据严格隔离,MyBatis插件强制
敏感数据加密 手机号、身份证、银行卡等AES加密存储
数据脱敏 日志、接口返回中敏感数据脱敏
数据备份 每日全量备份 + 实时binlog备份
数据删除 逻辑删除 + 定期物理清理
SQL注入防护 MyBatis参数化查询 + 输入校验

14.3 接口安全

措施 说明
鉴权 基于RBAC的权限控制,接口级权限校验
签名 开放API使用签名验证,防止篡改
防重放 时间戳 + nonce 防止重放攻击
XSS防护 输入过滤 + 输出转义
CSRF防护 Token验证 + SameSite Cookie
接口限流 全局限流 + 租户级限流 + 用户级限流

14.4 敏感数据加密工具类

java 复制代码
/**
 * 敏感数据加密工具类
 * 支持AES加密/解密,用于手机号、身份证等敏感字段
 */
@Component
public class SensitiveDataEncryptor {

    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH = 128;
    private static final int IV_LENGTH = 12;
    
    @Value("${security.encrypt.key}")
    private String encryptKey;

    /**
     * 加密
     */
    public String encrypt(String plainText) {
        if (StringUtils.isEmpty(plainText)) {
            return plainText;
        }
        
        try {
            byte[] key = getKeyBytes();
            SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
            
            // 生成随机IV
            byte[] iv = new byte[IV_LENGTH];
            SecureRandom random = new SecureRandom();
            random.nextBytes(iv);
            
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
            
            byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
            
            // IV + 密文 一起存储(Base64编码)
            byte[] result = new byte[IV_LENGTH + encrypted.length];
            System.arraycopy(iv, 0, result, 0, IV_LENGTH);
            System.arraycopy(encrypted, 0, result, IV_LENGTH, encrypted.length);
            
            return Base64.getEncoder().encodeToString(result);
            
        } catch (Exception e) {
            throw new BizException("数据加密失败");
        }
    }

    /**
     * 解密
     */
    public String decrypt(String cipherText) {
        if (StringUtils.isEmpty(cipherText)) {
            return cipherText;
        }
        
        try {
            byte[] key = getKeyBytes();
            SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
            
            byte[] decoded = Base64.getDecoder().decode(cipherText);
            
            // 提取IV
            byte[] iv = new byte[IV_LENGTH];
            System.arraycopy(decoded, 0, iv, 0, IV_LENGTH);
            
            // 提取密文
            byte[] encrypted = new byte[decoded.length - IV_LENGTH];
            System.arraycopy(decoded, IV_LENGTH, encrypted, 0, encrypted.length);
            
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
            
            byte[] decrypted = cipher.doFinal(encrypted);
            return new String(decrypted, StandardCharsets.UTF_8);
            
        } catch (Exception e) {
            throw new BizException("数据解密失败");
        }
    }
}

15. 监控与运维

15.1 监控体系

复制代码
┌─────────────────────────────────────────────────────┐
│                    Grafana 可视化                     │
├──────────┬──────────┬──────────┬──────────┬─────────┤
│  指标监控  │  链路追踪  │  日志监控  │  业务监控  │ 告警通知 │
│Prometheus│SkyWalking│  ELK     │  自定义   │ AlertMgr│
└──────────┴──────────┴──────────┴──────────┴─────────┘

15.2 关键监控指标

15.2.1 系统指标
指标 说明 告警阈值
CPU使用率 服务CPU使用率 > 80% 持续5分钟
内存使用率 JVM堆内存使用率 > 85%
GC次数/耗时 Full GC频率和耗时 Full GC > 5次/小时
线程数 活跃线程数 > 500
连接池 数据库连接池使用率 > 80%
15.2.2 业务指标
指标 说明
租户数量 总租户数、新增租户数、活跃租户数
接口QPS 总QPS、按租户QPS、按接口QPS
接口响应时间 P50/P95/P99 响应时间
错误率 接口错误率、按租户错误率
订单量 每日订单数、GMV
15.2.3 租户级监控面板
复制代码
# 每个租户独立监控视图
- 租户基本信息(套餐、到期时间、用户数)
- 接口调用量(日/周/月趋势)
- 接口响应时间(P50/P95/P99)
- 错误请求统计
- 资源使用情况(存储、API调用次数)
- 最近异常日志

15.3 日志规范

15.3.1 日志格式
json 复制代码
{
  "timestamp": "2026-07-31T10:00:00.000+08:00",
  "level": "INFO",
  "traceId": "abc123def456",
  "spanId": "span789",
  "tenantId": 1001,
  "tenantCode": "acme_corp",
  "userId": 10001,
  "service": "platform-order",
  "class": "com.example.order.service.OrderService",
  "method": "createOrder",
  "message": "订单创建成功",
  "duration": 150,
  "exception": null
}
15.3.2 MDC 配置
java 复制代码
/**
 * 日志MDC过滤器,注入租户信息和traceId
 */
@Component
public class MdcFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        try {
            // 注入traceId
            String traceId = httpRequest.getHeader("X-Trace-Id");
            if (StringUtils.isEmpty(traceId)) {
                traceId = IdUtil.fastSimpleUUID();
            }
            MDC.put("traceId", traceId);
            
            // 注入租户信息
            String tenantId = httpRequest.getHeader("X-Tenant-Id");
            if (StringUtils.hasText(tenantId)) {
                MDC.put("tenantId", tenantId);
            }
            
            // 注入用户ID
            String userId = httpRequest.getHeader("X-User-Id");
            if (StringUtils.hasText(userId)) {
                MDC.put("userId", userId);
            }
            
            chain.doFilter(request, response);
            
        } finally {
            MDC.clear();
        }
    }
}

16. 实施路线图

16.1 分阶段实施计划

第一阶段:基础框架搭建(4周)
周次 任务 交付物
第1周 项目脚手架搭建、技术选型确认、代码规范制定 项目骨架、规范文档
第2周 网关服务、认证服务、租户管理服务开发 基础服务可用
第3周 多租户插件开发、权限体系开发 租户隔离验证通过
第4周 系统管理模块(用户/角色/菜单/部门)开发 后台管理基础功能

里程碑:完成基础框架,支持租户注册、登录、权限管理

第二阶段:核心业务开发(6周)
周次 任务 交付物
第5-6周 商品服务、分类管理、库存管理 商品模块完整功能
第7-8周 订单服务、购物车、支付对接 订单全流程打通
第9-10周 用户服务、营销服务、消息通知 核心业务闭环

里程碑:核心业务功能完整,可下单、支付、发货

第三阶段:高级特性开发(4周)
周次 任务 交付物
第11周 独立Schema模式支持、动态数据源 VIP租户功能
第12周 数据权限、字段权限、审计日志 安全增强
第13周 搜索服务、报表统计、数据大屏 数据增值功能
第14周 租户套餐、计费系统、续费管理 商业化能力

里程碑:高级特性完成,支持多租户模式、商业化运营

第四阶段:优化与上线(4周)
周次 任务 交付物
第15周 性能压测、慢查询优化、缓存优化 性能达标
第16周 监控体系搭建、告警配置、压测调优 监控完善
第17周 安全测试、渗透测试、漏洞修复 安全合规
第18周 生产环境部署、灰度发布、上线验证 正式上线

里程碑:系统正式上线,稳定运行

16.2 团队配置建议

角色 人数 职责
架构师 1 技术架构设计、技术选型、核心代码
后端开发 4-6 业务服务开发
前端开发 2-3 管理后台 + 租户端页面
测试工程师 2 功能测试、性能测试、安全测试
运维工程师 1 部署、监控、运维
产品经理 1 需求分析、产品设计

17. 关键代码示例

17.1 统一响应结果

java 复制代码
/**
 * 统一响应结果
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {

    private Integer code;
    private String message;
    private T data;
    private String traceId;
    private Long timestamp;

    public static <T> Result<T> success() {
        return Result.<T>builder()
            .code(ResultCode.SUCCESS.getCode())
            .message(ResultCode.SUCCESS.getMessage())
            .traceId(TraceIdUtil.get())
            .timestamp(System.currentTimeMillis())
            .build();
    }

    public static <T> Result<T> success(T data) {
        Result<T> result = success();
        result.setData(data);
        return result;
    }

    public static <T> Result<T> fail(String message) {
        return Result.<T>builder()
            .code(ResultCode.FAIL.getCode())
            .message(message)
            .traceId(TraceIdUtil.get())
            .timestamp(System.currentTimeMillis())
            .build();
    }

    public static <T> Result<T> fail(Integer code, String message) {
        return Result.<T>builder()
            .code(code)
            .message(message)
            .traceId(TraceIdUtil.get())
            .timestamp(System.currentTimeMillis())
            .build();
    }
}

17.2 全局异常处理

java 复制代码
/**
 * 全局异常处理器
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 业务异常
     */
    @ExceptionHandler(BizException.class)
    public Result<Void> handleBizException(BizException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.fail(e.getCode(), e.getMessage());
    }

    /**
     * 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining("; "));
        log.warn("参数校验异常: {}", message);
        return Result.fail(ResultCode.PARAM_ERROR.getCode(), message);
    }

    /**
     * 权限异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
        log.warn("权限不足: {}", e.getMessage());
        return Result.fail(ResultCode.FORBIDDEN.getCode(), "权限不足");
    }

    /**
     * 未认证异常
     */
    @ExceptionHandler(AuthenticationException.class)
    public Result<Void> handleAuthenticationException(AuthenticationException e) {
        log.warn("认证失败: {}", e.getMessage());
        return Result.fail(ResultCode.UNAUTHORIZED.getCode(), "未登录或登录已过期");
    }

    /**
     * 其他未知异常
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.fail(ResultCode.SYSTEM_ERROR.getCode(), "系统繁忙,请稍后重试");
    }
}

17.3 基础实体类

java 复制代码
/**
 * 租户基础实体类
 * 所有租户业务表实体都继承此类
 */
@Data
public abstract class TenantBaseEntity implements Serializable {

    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 租户ID(由多租户插件自动填充,业务代码不需要手动设置)
     */
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;

    @TableField(fill = FieldFill.INSERT)
    private Long creator;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updater;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableLogic
    @TableField(fill = FieldFill.INSERT)
    private Integer deleted;
}

/**
 * 自动填充处理器
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        // 填充租户ID
        if (metaObject.hasGetter("tenantId")) {
            Long tenantId = TenantContextHolder.getTenantId();
            if (tenantId != null) {
                this.strictInsertFill(metaObject, "tenantId", Long.class, tenantId);
            }
        }
        
        // 填充创建人
        Long userId = SecurityUtils.getCurrentUserId();
        this.strictInsertFill(metaObject, "creator", Long.class, userId);
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        
        // 填充更新人
        this.strictInsertFill(metaObject, "updater", Long.class, userId);
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        
        // 填充逻辑删除
        this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        Long userId = SecurityUtils.getCurrentUserId();
        this.strictUpdateFill(metaObject, "updater", Long.class, userId);
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

17.4 Feign 租户信息传递

java 复制代码
/**
 * Feign 拦截器,传递租户信息
 */
@Component
public class FeignTenantInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 传递租户ID
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            template.header("X-Tenant-Id", String.valueOf(tenantId));
        }
        
        // 传递租户编码
        String tenantCode = TenantContextHolder.getTenantCode();
        if (StringUtils.hasText(tenantCode)) {
            template.header("X-Tenant-Code", tenantCode);
        }
        
        // 传递TraceId
        String traceId = MDC.get("traceId");
        if (StringUtils.hasText(traceId)) {
            template.header("X-Trace-Id", traceId);
        }
        
        // 传递用户Token
        String token = SecurityUtils.getToken();
        if (StringUtils.hasText(token)) {
            template.header("Authorization", "Bearer " + token);
        }
    }
}

17.5 租户信息缓存服务

java 复制代码
/**
 * 租户信息缓存服务
 */
@Service
@Slf4j
public class TenantCacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private SysTenantMapper tenantMapper;

    private static final String TENANT_ID_KEY = "tenant:info:id:";
    private static final String TENANT_CODE_KEY = "tenant:info:code:";
    private static final long EXPIRE_HOURS = 24;

    /**
     * 根据ID获取租户信息
     */
    public TenantInfo getById(Long tenantId) {
        if (tenantId == null) {
            return null;
        }
        
        String key = TENANT_ID_KEY + tenantId;
        TenantInfo tenantInfo = (TenantInfo) redisTemplate.opsForValue().get(key);
        
        if (tenantInfo == null) {
            // 缓存未命中,从数据库加载
            SysTenant tenant = tenantMapper.selectById(tenantId);
            if (tenant != null) {
                tenantInfo = convertToTenantInfo(tenant);
                redisTemplate.opsForValue().set(key, tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
            }
        }
        
        return tenantInfo;
    }

    /**
     * 根据编码获取租户信息
     */
    public TenantInfo getByCode(String tenantCode) {
        if (StringUtils.isEmpty(tenantCode)) {
            return null;
        }
        
        String key = TENANT_CODE_KEY + tenantCode;
        TenantInfo tenantInfo = (TenantInfo) redisTemplate.opsForValue().get(key);
        
        if (tenantInfo == null) {
            LambdaQueryWrapper<SysTenant> wrapper = Wrappers.lambdaQuery();
            wrapper.eq(SysTenant::getTenantCode, tenantCode);
            SysTenant tenant = tenantMapper.selectOne(wrapper);
            
            if (tenant != null) {
                tenantInfo = convertToTenantInfo(tenant);
                redisTemplate.opsForValue().set(key, tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
                // 同时写入ID缓存
                redisTemplate.opsForValue().set(TENANT_ID_KEY + tenant.getId(), 
                    tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
            }
        }
        
        return tenantInfo;
    }

    /**
     * 刷新租户缓存
     */
    public void refreshCache(Long tenantId) {
        SysTenant tenant = tenantMapper.selectById(tenantId);
        if (tenant != null) {
            TenantInfo tenantInfo = convertToTenantInfo(tenant);
            
            // 删除旧缓存
            redisTemplate.delete(TENANT_ID_KEY + tenantId);
            redisTemplate.delete(TENANT_CODE_KEY + tenant.getTenantCode());
            
            // 写入新缓存
            redisTemplate.opsForValue().set(TENANT_ID_KEY + tenantId, 
                tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
            redisTemplate.opsForValue().set(TENANT_CODE_KEY + tenant.getTenantCode(), 
                tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
        }
    }

    private TenantInfo convertToTenantInfo(SysTenant tenant) {
        TenantInfo info = new TenantInfo();
        info.setId(tenant.getId());
        info.setTenantCode(tenant.getTenantCode());
        info.setTenantName(tenant.getTenantName());
        info.setTenantType(tenant.getTenantType());
        info.setIsolationLevel(tenant.getIsolationLevel());
        info.setStatus(tenant.getStatus());
        info.setExpireTime(tenant.getExpireTime());
        info.setSchemaName(tenant.getSchemaName());
        info.setMaxUserCount(tenant.getMaxUserCount());
        info.setMaxStorageMb(tenant.getMaxStorageMb());
        info.setThemeConfig(tenant.getThemeConfig());
        info.setExtraConfig(tenant.getExtraConfig());
        return info;
    }
}

17.6 租户状态检查切面

java 复制代码
/**
 * 租户状态检查切面
 * 在执行业务方法前检查租户状态是否正常
 */
@Aspect
@Component
@Slf4j
public class TenantStatusAspect {

    @Autowired
    private TenantCacheService tenantCacheService;

    /**
     * 切入点:所有带 @TenantCheck 注解的方法
     */
    @Pointcut("@annotation(com.example.common.annotation.TenantCheck)")
    public void tenantCheckPointcut() {}

    @Around("tenantCheckPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Long tenantId = TenantContextHolder.getTenantId();
        
        if (tenantId == null) {
            throw new BizException("租户信息不存在");
        }
        
        TenantInfo tenantInfo = tenantCacheService.getById(tenantId);
        if (tenantInfo == null) {
            throw new BizException("租户不存在");
        }
        
        // 检查状态
        if (tenantInfo.getStatus() == 0) {
            throw new BizException("租户已被禁用,请联系管理员");
        }
        if (tenantInfo.getStatus() == 2) {
            throw new BizException("租户服务已过期,请续费");
        }
        if (tenantInfo.getStatus() == 3) {
            throw new BizException("租户已欠费,请充值");
        }
        
        // 检查到期时间
        if (tenantInfo.getExpireTime() != null && 
            tenantInfo.getExpireTime().isBefore(LocalDateTime.now())) {
            throw new BizException("租户服务已到期,请续费");
        }
        
        return joinPoint.proceed();
    }
}

附录

A. 项目依赖 pom.xml 关键依赖

xml 复制代码
<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!-- Spring Cloud -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    
    <!-- MySQL -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- RocketMQ -->
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-spring-boot-starter</artifactId>
        <version>2.3.0</version>
    </dependency>
    
    <!-- Security + JWT -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.5</version>
    </dependency>
    
    <!-- 工具类 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.25</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <!-- 监控 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

B. 关键配置文件 application.yml

yaml 复制代码
spring:
  application:
    name: platform-system
  
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
        namespace: multi-tenant
      config:
        server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
        namespace: multi-tenant
        file-extension: yaml
  
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/tenant_shared?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver
  
  data:
    redis:
      host: localhost
      port: 6379
      password: password
      database: 0
      lettuce:
        pool:
          max-active: 20
          max-idle: 10
          min-idle: 5

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  type-aliases-package: com.example.**.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

# 多租户配置
tenant:
  # 忽略租户隔离的表名
  ignore-tables:
    - sys_tenant
    - sys_tenant_package
    - sys_datasource_config
    - sys_dict_type
    - sys_dict_data
  # 默认租户ID(平台管理使用)
  default-tenant-id: 0
  # 是否开启租户校验
  enable-check: true

# JWT配置
jwt:
  secret: your-256-bit-secret-key-here-must-be-long-enough
  expiration: 7200  # 2小时
  refresh-expiration: 604800  # 7天
  header: Authorization
  prefix: "Bearer "

# 安全配置
security:
  encrypt:
    key: your-encryption-key-32bytes-long-here
  # 放行的URL
  ignore-urls:
    - /api/auth/login
    - /api/auth/register
    - /api/auth/refresh-token
    - /actuator/**
    - /doc.html
    - /webjars/**
    - /v3/api-docs/**

logging:
  level:
    com.example: debug
    org.springframework: info
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] [%X{tenantId}] %logger{50} - %msg%n"

文档说明:本架构设计文档基于 Java 17 + Spring Boot 3.x 技术栈,提供了从架构设计到代码实现的完整方案。实际项目中可根据业务需求进行调整和扩展。

相关推荐
落苜蓿蓝1 小时前
Java 循环中对象复用导致属性覆盖?从 JVM 内存模型讲解原因
java·jvm·python
weixin_440784112 小时前
Android基础知识汇总
android·java·android studio
布鲁飞丝2 小时前
从零实现富文本编辑器#-浏览器选区与编辑器选区模型同步
java·前端·编辑器
鱼子星_2 小时前
【C++】深入剖析list:list及其双向迭代器实现
开发语言·数据结构·c++·笔记·stl·list
脚踏实地皮皮晨2 小时前
003002004_WPF Panel 基类 官方类定义
开发语言·windows·算法·c#·wpf·visual studio
jun_bai2 小时前
使用java安全的移动文件
java
caishenzhibiao2 小时前
市场同步系统 同花顺期货通指标
java·c语言·c#
Devin~Y2 小时前
从本地生活电商到 AI RAG:互联网大厂 Java 面试场景完整实战
java·spring boot·redis·elasticsearch·spring cloud·kafka·rag
520拼好饭被践踏2 小时前
JAVA+Agent学习day25
java·开发语言·学习