Files
smartDriveEE/src/main/sql/多租户技术方案.md
2026-03-28 11:15:13 +08:00

24 KiB
Raw Blame History

多租户改造实施方案文档AIDriveEEForAudio

  1. 背景与目标 当前现状 技术栈Spring Boot 3.5 + MyBatis-Plus + MyBatis XML + MySQL。 ORM@TableName + BaseMapper 为主XML 主要用于 resultMap 与列列表。 现有系统未引入任何 租户字段/多租户插件/租户上下文,为 单租户系统。 改造目标 在不大幅重构现有业务的前提下,引入稳定可扩展的 多租户能力,实现不同租户数据的逻辑隔离; 对现有 CRUD 影响尽量小,大部分业务代码可复用; 为后续扩展租户级限流、权限、配置等能力奠定基础。
  2. 多租户模式与设计原则 2.1 多租户模式选择 本项目推荐采用: 模式:单库单 schema + 每张业务表增加 tenant_id 字段 + MyBatis-Plus 多租户插件自动拼接租户条件。 理由: 不需要为每个租户单独建库或动态数据源,改造成本可控; 利用 MP 官方 TenantLineInnerInterceptor绝大多数查询/更新可自动加上 tenant_id 条件; 便于后续做运营侧的跨租户统计(可选择性绕过多租户拦截器)。 2.2 租户边界与业务含义 推荐按 企业 / 经销商 / 机构 维度作为租户边界,例如: 一个 4S 店/经销商/医院/公司 = 一个租户; 一个租户下包含多个用户、销售、设备、项目等。 租户维度对象: 用户user归属一个租户 门店/项目/客户/音频记录等业务实体,均挂在某个 tenant_id 下。
  3. 数据库层改造方案 3.1 新增租户表 新表tenant 字段建议: id主键BIGINT 或 VARCHAR(36),与 Java 实现保持一致) tenant_code租户编码如公司英文缩写、唯一 tenant_name租户名称 status状态启用/禁用) create_time / update_time创建与更新时间 其他扩展字段:联系人、电话、过期时间

CREATE TABLE tenant ( id VARCHAR(36) NOT NULL COMMENT '租户主键ID', tenant_code VARCHAR(64) NOT NULL COMMENT '租户编码,唯一', tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态1-启用0-禁用', contact_name VARCHAR(64) DEFAULT NULL COMMENT '联系人姓名', contact_phoneVARCHAR(32) DEFAULT NULL COMMENT '联系人电话', expire_time DATETIME 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 '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_tenant_code (tenant_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户表';

3.2 业务表增加 tenant_id 字段 需挂租户的表(示例,实际以 SQL 目录为准扩展): 音频类: audio_management、audio_segment、audio_management_statistics、audio_text_analysis_furniture、audio_text_analysis_sop、video_image_analysis、video_management、video_synthesis_log 等。 客户与销售类: customer_management、customer_profile_analysis、communication_record、sales_management 等。 门店与项目类: dealership、project_management。 日志与其他: yhy_audio_upload_log、heartbeat_log、tts_request_log、face_detect_log 等业务相关日志。 用户与权限: user、user_role、role 等(至少 user 需有 tenant_id 字段)。 字段规范建议: 字段名统一tenant_id 字段类型:与 tenant.id 保持一致(推荐 BIGINT 或 VARCHAR(36) 约束: 第一阶段:允许 NULL兼容旧数据 数据迁移完成后:设置为 NOT NULL 索引: 大表添加复合索引: 如customer_management 增加 INDEX idx_cm_tenant_dealership (tenant_id, dealership_id) 如audio_management 增加 INDEX idx_am_tenant_ctime (tenant_id, create_time)

-- ====================== -- 音频类 -- ====================== ALTER TABLE audio_management ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_am_tenant_ctime (tenant_id, create_time);

ALTER TABLE audio_segment ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_as_tenant_ctime (tenant_id, create_time);

ALTER TABLE audio_management_statistics ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_ams_tenant_ctime (tenant_id, create_time);

ALTER TABLE audio_text_analysis_furniture ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_ataf_tenant_ctime (tenant_id, create_time);

ALTER TABLE audio_text_analysis_sop ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_atas_tenant_ctime (tenant_id, create_time);

ALTER TABLE video_image_analysis ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_via_tenant_ctime (tenant_id, create_time);

ALTER TABLE video_management ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_vm_tenant_ctime (tenant_id, create_time);

ALTER TABLE video_synthesis_log ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_vsl_tenant_ctime (tenant_id, create_time);

-- ====================== -- 客户与销售类 -- ====================== ALTER TABLE customer_management ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_cm_tenant_dealership (tenant_id, dealership_id);

ALTER TABLE customer_profile_analysis ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_cpa_tenant_ctime (tenant_id, create_time);

ALTER TABLE communication_record ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_cr_tenant_ctime (tenant_id, create_time);

ALTER TABLE sales_management ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_sm_tenant_sales (tenant_id, sales_id);

ALTER TABLE device_management ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_dm_tenant_device_management (tenant_id, id);

-- ====================== -- 门店与项目类 -- ====================== ALTER TABLE dealership ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_dealer_tenant (tenant_id);

ALTER TABLE project_management ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_pm_tenant_ctime (tenant_id, create_time);

-- ====================== -- 日志与其他 -- ====================== ALTER TABLE yhy_audio_upload_log ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_yhy_tenant_ctime (tenant_id, create_time);

ALTER TABLE heartbeat_log ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_hb_tenant_ctime (tenant_id, create_time);

ALTER TABLE tts_request_log ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_tts_tenant_ctime (tenant_id, create_time);

ALTER TABLE face_detect_log ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_fdl_tenant_ctime (tenant_id, create_time);

-- ====================== -- 用户与权限 -- ====================== ALTER TABLE user ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_user_tenant (tenant_id);

ALTER TABLE user_role ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_ur_tenant_role (tenant_id, role_id);

ALTER TABLE role ADD COLUMN tenant_id VARCHAR(36) NULL COMMENT '租户ID' AFTER id, ADD INDEX idx_role_tenant (tenant_id);

3.3 历史数据迁移策略 插入一个默认租户DEFAULT_TENANT INSERT INTO tenant (id, tenant_code, tenant_name, status, ...) VALUES (...); 对所有新增了 tenant_id 的表执行数据补齐: UPDATE 表名 SET tenant_id = 'DEFAULT_TENANT_ID' WHERE tenant_id IS NULL; 需要细分历史数据进入多个租户时,可后续再做拆分迁移脚本。 -- 1. 插入默认租户(示例) INSERT INTO tenant ( id, tenant_code, tenant_name, status, contact_name, contact_phone, expire_time, create_time, update_time ) VALUES ( 'TENANT_ID_CST_2026', -- 建议改成真实的 UUID 或雪花ID '北京移多旺科技有限公司', '默认租户', 1, NULL, NULL, NULL, NOW(), NOW() );

-- 2. 为所有新增了 tenant_id 的表补齐历史数据 -- 音频类 UPDATE audio_management SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE audio_segment SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE audio_management_statistics SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE audio_text_analysis_furniture SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE audio_text_analysis_sop SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE video_image_analysis SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE video_management SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE video_synthesis_log SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL;

-- 客户与销售类 UPDATE customer_management SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE customer_profile_analysis SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE communication_record SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE sales_management SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL;

-- 门店与项目类 UPDATE dealership SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE project_management SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE device_management SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL;

-- 日志与其他 UPDATE yhy_audio_upload_log SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE heartbeat_log SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE tts_request_log SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE face_detect_log SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL;

-- 用户与权限 UPDATE user SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE user_role SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL; UPDATE role SET tenant_id = 'TENANT_ID_CST_2026' WHERE tenant_id IS NULL;

  1. 应用层多租户实现方案 4.1 实体类增加 tenantId 字段 以 CustomerManagement 为例: 在 com.rj.entity.CustomerManagement 中增加: @Schema(description = "租户ID") @TableField("tenant_id") private String tenantId; @Schema(description = "租户ID")@TableField("tenant_id")private String tenantId; 对所有挂租户的实体(如 AudioManagement、Dealership、ProjectManagement、SalesManagement 等)增加同名字段,映射 tenant_id。

原则:和数据库字段一一对应,命名统一为 tenantId + @TableField("tenant_id"),方便在 MyBatis-Plus 插件中统一处理。

4.2 租户上下文设计TenantContextHolder 在 com.rj.common 或新包 com.rj.tenant 下新增 TenantContextHolder 功能: 存放当前请求的租户 ID基于 ThreadLocal 提供 set/get/clear 三个静态方法; 供: TenantLineHandler 从中读取租户; 部分业务中获取当前租户信息。

public final class TenantContextHolder {

private static final ThreadLocal<String> TENANT_ID_HOLDER = new ThreadLocal<>();

private TenantContextHolder() {
    // 工具类,禁止实例化
}

/**
 * 设置当前线程的租户ID。
 *
 * @param tenantId 租户ID允许为 null表示无租户上下文
 */
public static void setTenantId(String tenantId) {
    TENANT_ID_HOLDER.set(tenantId);
}

/**
 * 获取当前线程的租户ID。
 *
 * @return 当前租户ID可能为 null
 */
public static String getTenantId() {
    return TENANT_ID_HOLDER.get();
}

/**
 * 清理当前线程保存的租户ID防止线程复用导致的租户串用。
 */
public static void clear() {
    TENANT_ID_HOLDER.remove();
}

}

4.3 请求到租户的映射方式 推荐方案HTTP Header 传递租户 ID。 前端在每次请求头中带上X-Tenant-Id: <当前租户ID> 该租户 ID 一般来自: 登录时从用户表查出的 tenant_id 登录成功后前端缓存(或由 token 中解析得到)。 4.4 请求过滤器 / 拦截器 在 com.rj.config 新增 TenantFilter或使用 Spring MVC HandlerInterceptor 获取请求头 X-Tenant-Id 校验租户 ID 是否存在、是否合法(可选校验是否为启用状态); 成功则 TenantContextHolder.setTenantId(tenantId) 在 finally 阶段调用 TenantContextHolder.clear() 防止线程复用污染。

要求:任何 Mapper/Service 层执行 SQL 前,当前线程中必须已经设置好了租户 ID。

@Component @Order(Ordered.HIGHEST_PRECEDENCE + 10) public class TenantFilter extends OncePerRequestFilter {

private static final Logger log = LoggerFactory.getLogger(TenantFilter.class);

/**
 * Header 名称常量,便于前后端约定与维护
 */
public static final String TENANT_HEADER = "X-Tenant-Id";

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain) throws ServletException, IOException {
    String tenantId = request.getHeader(TENANT_HEADER);

    try {
        if (tenantId != null && !tenantId.isEmpty()) {
            TenantContextHolder.setTenantId(tenantId);
        } else {
            // 允许为 null 的情况:例如某些公共接口或尚未登录
            TenantContextHolder.setTenantId(null);
        }

        filterChain.doFilter(request, response);
    } finally {
        // 无论请求是否成功,都要清理线程变量
        TenantContextHolder.clear();
    }
}

}

  1. MyBatis-Plus 多租户插件集成 5.1 当前配置情况 com.rj.config.MybatisPlusConfig 中已有: PaginationInnerInterceptor分页插件 OptimisticLockerInnerInterceptor乐观锁 暂未配置多租户拦截器。 5.2 引入 TenantLineInnerInterceptor 思路:在 MybatisPlusInterceptor 中加入一个 TenantLineInnerInterceptor并实现 TenantLineHandler 接口,定义: 当前租户 ID 的获取方式; 租户字段名(统一为 tenant_id 哪些表不需要租户隔离。 配置要点: 添加顺序:一般先添加多租户拦截器,再添加分页拦截器; 在 ignoreTable 中配置哪些表不参与多租户,例如: tenant租户表自身 公共字典表/行业标签表等(如认为是全局共享)。

效果:所有走 MyBatis-Plus 的查询/更新语句,在生成 SQL 时自动追加 tenant_id = 当前租户 条件,实现物理数据隔离。

/**
 * 多租户处理器定义如何获取租户ID、租户字段名以及忽略的表
 */
@Bean
public TenantLineHandler tenantLineHandler() {
    // 需要忽略多租户的表(不自动拼接 tenant_id
    Set<String> ignoreTables = new HashSet<>();
    ignoreTables.add("tenant");          // 租户表本身
    ignoreTables.add("industry_tags");   // 行业标签(示例:如认为是公共字典)

    return new TenantLineHandler() {

        @Override
        public Expression getTenantId() {
            String tenantId = TenantContextHolder.getTenantId();
            // 没有租户ID时返回 null 由 MyBatis-Plus 决定处理策略,通常为不过滤或抛错(视版本而定)
            return tenantId == null ? null : new StringValue(tenantId);
        }

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

        @Override
        public boolean ignoreTable(String tableName) {
            // 忽略名单内的表不做租户隔离
            return ignoreTables.contains(tableName);
        }
    };
}

5.3 XML / 自定义 SQL 兼容性说明 当前项目 XML 多为: resultMap Base_Column_List 等列定义。 这些 XML 并不影响多租户插件的生效,插件对最终 SQL 生效。 需关注的场景: 若存在大量手写复杂 SQLjoin、多表统计等 插件会尝试对所有包含 tenant_id 字段的表自动加条件; 对于需要跨租户查询的统计/后台管理接口,可通过: 使用 MyBatis-Plus 提供的注解(如 @InterceptorIgnore(tenantLine = "true") 或对应版本配置)在 Mapper 方法上跳过租户拦截; 或单独定义不走多租户的 Mapper/方法,仅用于运营后台。

  1. 数据写入与审计机制 6.1 新增数据的 tenant_id 自动填充 新增数据INSERT时需明确写入当前 tenant_id。可选方案 在各业务 Service 中,创建实体时手动设置: entity.setTenantId(TenantContextHolder.getTenantId()); 使用 MyBatis-Plus MetaObjectHandler 在 insertFill 中统一为 tenantId 字段填充当前租户,避免业务层遗漏。 推荐方案:使用 MetaObjectHandler + 在特殊场景手动覆盖。

6.2 更新数据的安全性 一般不允许修改 tenant_id 字段; 更新语句只在 WHERE 条件中带上 tenant_id = 当前租户,确保: 不会更新到其他租户的数据; 逻辑删除/批量更新等操作同样受租户约束。 /**

  • MyBatis-Plus 公共字段自动填充处理器
  • 仅负责在插入时为带有 tenantId 字段的实体自动填充当前租户ID
  • 更新时不修改 tenantId确保租户ID一旦写入不可被业务层随意修改。

*/ @Component public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {

private static final String TENANT_FIELD = "tenantId";

@Override
public void insertFill(MetaObject metaObject) {
    // 仅在实体中存在 tenantId 字段且当前值为空时进行填充
    if (metaObject.hasSetter(TENANT_FIELD)) {
        Object currentValue = getFieldValByName(TENANT_FIELD, metaObject);
        if (currentValue == null) {
            String tenantId = TenantContextHolder.getTenantId();
            if (tenantId != null) {
                this.strictInsertFill(metaObject, TENANT_FIELD, String.class, tenantId);
            }
        }
    }
}

@Override
public void updateFill(MetaObject metaObject) {
    // 不自动更新 tenantId保持租户ID稳定
}

}

6.3 审计字段补充(可选) 在已有 create_time / update_time 基础上扩展: create_by / update_by记录用户 ID 与租户联动后,便于定位“哪个租户、哪个用户、何时”操作了数据。

  1. 登录认证与租户绑定 7.1 用户表改造 在 user 表和对应实体中新增 tenant_id 字段; 模型设计: 简单模式:一个用户只属于一个租户(适合大多数 B 端场景); 复杂模式:一个账号可归属多个租户,则需 user_tenant 关联表(如后期有此需求可扩展)。

7.2 登录流程中的租户处理 登录接口逻辑: 用户名密码校验通过后,查询用户的 tenant_id 在返回的 tokenJWT 或自定义)中写入 tenantId 声明; 前端在发起业务请求时: 将 token 放入 Authorization 将 tenantId 放入 X-Tenant-Id可到期从 token 中解析填充)。 服务端过滤器逻辑: 从 token 与 Header 中解析并对比 tenantId保持一致性 决定本次请求的租户上下文(设置 TenantContextHolder。 // TODO 当前已经按你文档里的登录多租户方案,把后端改造完成: 登录返回携带租户 ID在 LoginResponse 中新增 tenantId 字段,并在 UserServiceImpl.login 里从 user.getTenantId() 赋值,前端拿到后即可将其放入 X-Tenant-Id。 通过 token 绑定租户TenantFilter 现在会从 Authorization 头中解析 token支持 Bearer xxx 或直接 token 调用 IUserService.getUserByToken 查出 User得到 tokenTenantId。 Header 与 token 的一致性与上下文设置: 同时有 token 和 X-Tenant-Id 且不一致时会打出告警日志; 优先使用 token 中的租户 ID其次才用 X-Tenant-Id然后调用 TenantContextHolder.setTenantId(finalTenantId),在整个请求周期内为 MyBatis 多租户拦截器提供正确租户上下文。

  1. 外部依赖与多租户适配(可选/后续) 项目中已使用: Redis会话、缓存、RAG 等) MinIO音频/图片等文件存储) 建议逐步考虑多租户隔离策略: 8.1 Redis Key 命名规范 为避免不同租户共享同一 Key 带来数据串扰: 统一增加租户前缀::<业务Key> 封装统一的 Redis 工具类,使调用侧只传业务 Key由工具类自动添加租户前缀 对于确实需要跨租户的缓存(全局配置等),使用特殊前缀如 global:。 8.2 MinIO 存储路径策略 建议在对象路径或 bucket 中加入租户维度: 对象前缀方式tenant-/audio/... 或 bucket 方式audio-tenant- 将路径构造封装在 MinIOUtil 或相关 service 中,避免业务代码直接拼接。
  2. 实施步骤(落地路线) 9.1 设计与评审阶段 明确业务侧租户定义(按公司/门店/项目等); 确认需要隔离的表列表及需共享的公共表; 设计 tenant 表结构以及 tenant_id 字段规范(类型、索引策略)。 9.2 数据库改造 编写 SQL 脚本: 创建 tenant 表; 为业务表增加 tenant_id 字段及索引; 在测试环境执行、验证 SQL 插入默认租户,并为所有历史数据补齐 tenant_id。 9.3 应用代码第一阶段改造 为所有挂租户实体类添加 tenantId 字段及 @TableField("tenant_id") 新增 TenantContextHolder 编写 TenantFilter/HandlerInterceptor从请求头或 token 中解析租户,并设置/清理上下文; 在 MybatisPlusConfig 中注册 TenantLineInnerInterceptor 与 TenantLineHandler配置忽略表列表 (可选)实现 MetaObjectHandler 自动填充 tenantId。 9.4 应用代码第二阶段改造与测试 排查: 是否有直接使用 JdbcTemplate 或原生 SQL 的地方,手动加 tenant_id 条件; 需跨租户查询的运营接口,为 Mapper 方法配置忽略多租户拦截。 改造登录模块: 用户实体和数据库增加 tenant_id 登录成功后,将 tenantId 写入 token并在前端请求中携带。 编写联调/集成测试: 准备两个租户数据,验证: A 租户的用户只看到 A 的数据; A、B 租户数据在分页、统计接口中互不干扰。 9.5 外部服务与性能调优(可并行/后续) 优化 Redis / MinIO 的多租户隔离Key/路径规范); 对热点业务表基于 tenant_id 索引进行慢 SQL 分析与优化; 监控系统表现,如单库压力过高,后续可考虑按租户/时间进行分库分表。
  3. 总结 本方案以 “租户字段 + 请求租户上下文 + MyBatis-Plus 多租户插件” 为核心,对现有项目做 最小侵入 的多租户改造; 改造重点集中在: 数据库新增 tenant_id 字段及索引; 实体类字段同步; 引入租户上下文、过滤器以及 MP 多租户插件; 登录与用户表中引入租户绑定。 在此基础上,后续可以平滑扩展: 租户级配置中心; 租户级限流、配额; 租户级报表与运营管理。 如果你需要,我可以基于本方案,进一步输出一份 具体到文件名/类名/方法名的“改造清单 + 示例代码片段”,方便你直接在 IDE 中按步骤实施。