# 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 请求链路(简化示意) ```text 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,两者不一致。 - 改造后: ```java // 使用字符串 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; - 将新创建的数据源挂载到当前 `DynamicDataSource` 的 `resolvedDataSources` 中; - 调用 `DynamicDataSourceContextHolder.setDataSourceType("tenant:{id}")` 切到指定租户库; - 提供 `clear()` 方法清理当前线程的数据源标记。 关键实现要点: - 缓存 map:`private static final Map 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: A` 和 `X-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