user-app模块SaaS改造说明.md 7.4 KB

fs-user-app 模块 SaaS 改造说明(C 端用户端)

本文档说明用户端模块 fs-user-app 在 SaaS 场景下的改造点:如何按租户切库、如何兼容旧版本以及前端需要配合的地方。


一、改造目标与原则

  1. 按租户切库:同一套 user-app 服务实例支持多个租户,每个租户对应独立数据库(与 admin/company 一致)。
  2. 按租户加载配置:微信、支付等项目级配置继续通过 sys_config.projectConfig 在租户库中存储,并在请求链路中按租户加载。
  3. 兼容旧版本:未携带租户信息的请求,保持原有「单库」行为,避免一次性改动所有 C 端调用方。
  4. 尽量不侵入业务:现有 Controller 和 Service 尽量不改或少改,通过过滤器 + 动态数据源完成多租户能力。

二、整体设计

2.1 租户识别方式(C 端)

  • 通过 HTTP 请求头 X-Tenant-Code 传递租户编码:
    • 该编码对应主库 tenant_info.tenant_code 字段;
    • 适用于 app、小程序、H5 等前端统一接入方式。
  • 若请求未携带 X-Tenant-Code
    • 不做任何多租户切换,保持使用默认数据源(master + slave + sop),兼容单租户部署。

2.2 请求链路(简化示意)

Client(带 X-Tenant-Code) 
    ↓
AppTenantSwitchFilter(fs-user-app)
    1. 切到主库 MASTER
    2. 查 tenant_info(tenantCode)
    3. TenantDataSourceManager.switchTenant(tenant) → 切库到租户库
    4. 从租户库 sys_config.projectConfig 加载租户配置 → TenantConfigContext + ProjectConfig
    ↓
Controller / Service / Mapper
    - Mapper 所在数据源 = 当前租户库
    - 业务中读取租户配置(WxOpenProperties、ProjectConfig 等)= 当前租户配置

三、具体代码改造

3.1 DataSourceConfig:统一使用字符串 key

文件fs-user-app/src/main/java/com/fs/framework/config/DataSourceConfig.java

  • 原实现使用 DataSourceType.MASTER(枚举)作为 targetDataSources 的 key,而 DynamicDataSourceContextHolder 使用字符串(DataSourceType.MASTER.name())作为 lookup key,两者不一致。
  • 改造后:
// 使用字符串 key(MASTER/SLAVE),与 DynamicDataSourceContextHolder 中 setDataSourceType 保持一致
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);

效果
后续调用 DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name()) 时,能正确匹配到 master 数据源。


3.2 新增 TenantDataSourceManager(租户数据源管理)

文件fs-user-app/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

核心职责:

  • 基于租户信息 TenantInfo(主库 tenant_info 表)动态创建 DruidDataSource;
  • 将新创建的数据源挂载到当前 DynamicDataSourceresolvedDataSources 中;
  • 调用 DynamicDataSourceContextHolder.setDataSourceType("tenant:{id}") 切到指定租户库;
  • 提供 clear() 方法清理当前线程的数据源标记。

关键实现要点:

  • 缓存 map:private static final Map<String, DataSource> TENANT_DS_CACHE,避免重复创建连接池;
  • 通过反射访问 AbstractRoutingDataSource.resolvedDataSources,在运行时追加租户数据源;
  • 数据源 key 统一为 tenant:{tenantId},与 admin/company 的约定一致。

3.3 新增 AppTenantSwitchFilter(按 X-Tenant-Code 切租户库)

文件fs-user-app/src/main/java/com/fs/framework/filter/AppTenantSwitchFilter.java

类型:OncePerRequestFilter@Component@Order(5)

工作流程(每个 HTTP 请求):

  1. 从请求头读取 X-Tenant-Code
    • 若为空:直接放行,不做多租户处理,兼容老逻辑。
  2. 切到主库(MASTER):
    • DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
  3. 通过 TenantInfoService.selectTenantInfoList 根据 tenantCode 查询租户信息:
    • 若未查到:记录日志 + 放行(按单库行为);
    • status != 1 或已过期:记录日志 + 放行。
  4. 对有效租户调用 tenantDataSourceManager.switchTenant(tenant)
    • 如无对应数据源则创建,并将当前 ThreadLocal 标记为该租户数据源。
  5. 在租户库中读取项目配置:
    • 调用 sysConfigMapper.selectConfigByConfigKey("projectConfig")
    • 若有值,则 TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()))
    • 然后调用 ProjectConfig.loadTenantConfigsFromContext(),使 Wx / 支付等配置指向当前租户。
  6. 调用 filterChain.doFilter(request, response) 继续后续业务处理。
  7. finally 中清理:
    • ProjectConfig.clearTenantConfigs();
    • TenantConfigContext.clear();
    • tenantDataSourceManager.clear();(内部清理 DynamicDataSourceContextHolder)。

说明

  • 过滤器只负责租户与数据源上下文,不修改 C 端的登录 / 鉴权逻辑(APPToken 仍按原逻辑生成和校验)。
  • 只要前端在所有请求(含登录)中带上 X-Tenant-Code,对应业务就会在该租户库上执行、并使用当前租户的项目配置。

四、兼容性与前端改造建议

4.1 向下兼容

  • 未携带 X-Tenant-Code 的请求:
    • 不进入多租户分支,仅使用默认 master/slave/sop 数据源,与原逻辑完全一致。
    • 可用于单租户环境或过渡期。

4.2 前端需配合的改造

  1. 在所有 user-app 请求中统一增加请求头 X-Tenant-Code
    • 值为当前访问医院/企业的租户编码(与 admin 端租户编码一致)。
    • 登录、业务接口、登出等全部接口建议统一加上。
  2. 租户编码的获取方式
    • 可通过登录前选择医院 / 租户,或通过域名映射(如二级域名解析出 tenantCode),再由前端写入请求头。

五、自测点(建议)

  1. 多租户切库验证
    • 在主库 tenant_info 中配置两个租户 A/B,对应不同业务库。
    • 使用相同 user-app 代码,通过 Postman / App:
      • 分别带 X-Tenant-Code: AX-Tenant-Code: B 调用同一接口(例如获取课程、订单列表),确认结果数据来自不同库。
  2. 租户配置隔离验证
    • 在两个租户库的 sys_config.projectConfig 中配置不同的微信/支付参数,观察调用相关功能时是否生效为各自租户的配置。
  3. 无租户头兼容性
    • 不带 X-Tenant-Code 调用常用接口,确认行为与改造前一致(不报错、不串库)。
  4. 错误租户编码
    • 带错误的 X-Tenant-Code(不存在 / 已禁用 / 已过期),确认日志中有告警,且请求退回单库行为(不异常中断)。

六、与整体 SaaS 方案的关系

  • admin/company 模块已支持按租户切库并在 Token 中携带 tenantId
  • user-app 模块采用「请求头传递租户编码 + 过滤器按租户切库」的方式接入同一套多租户数据源与项目配置体系;
  • 后续若需要在 C 端 Redis 中也按租户隔离 key,可在 user-app 的 Redis 配置中接入 TenantKeyRedisSerializer,并结合安全上下文或 Jwt 承载 tenantId 进行扩展(当前版本暂保留原有 Redis 行为,避免对线上 C 端产生影响)。\n