[17 tools called]
先修改 user-app 的数据源 key,然后新增租户数据源管理类和租户切换过滤器,最后编译+写文档。
[22 tools called]
下面是这次对 C 端 fs-user-app 的 SaaS 改造情况、用法和说明。
com.fs.framework.config.DataSourceConfigjava
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);
- 原因:DynamicDataSourceContextHolder.setDataSourceType() 传的是字符串(如 "MASTER"),以前 targetDataSources 用的是 DataSourceType.MASTER(枚举),两者不相等,后面做动态切库会出问题,所以先把 key 统一成字符串。
#### 2. 新增租户数据源管理类 TenantDataSourceManager
- 文件:com.fs.framework.datasource.TenantDataSourceManager
- 作用:
- 根据主库里的 TenantInfo(tenant_info 表)动态创建租户库的数据源(DruidDataSource);
- 把新数据源挂到当前 DynamicDataSource 的 resolvedDataSources 上;
- 调用 DynamicDataSourceContextHolder.setDataSourceType("tenant:{tenantId}") 把当前线程切到对应租户库;
- 提供 clear() 方法在请求结束时清理 ThreadLocal,避免线程复用串库。
- 实现要点:
- 内部用 TENANT_DS_CACHE 缓存租户数据源,避免重复创建连接池;
- 用反射访问 AbstractRoutingDataSource.resolvedDataSources,运行时追加数据源;
- 租户 key 统一为 tenant:{id},和 admin/company 一致;
- 注释里已经详细解释了用途和注意点。
#### 3. 新增租户切换过滤器 AppTenantSwitchFilter
- 文件:com.fs.framework.filter.AppTenantSwitchFilter
- 注解:@Component、@Order(5),继承 OncePerRequestFilter。
- 入口约定:
- 使用请求头 X-Tenant-Code 作为 C 端的租户标识(和 admin 的 tenantCode 含义一致)。
- 前端 app / H5 / 小程序只要在请求头上带上这个租户编码,本次改造就会按该租户切库并加载租户配置。
- 过滤逻辑(按请求):
1. 从请求头读取 X-Tenant-Code:
- 为空:直接放行,不做任何租户切换,兼容老逻辑(单库)。
2. 切到主库 MASTER:
- DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
3. 用 TenantInfoService 在主库根据 tenantCode 查 tenant_info:
- 找不到 / 状态不为启用 / 已过期:记录日志,放行但不切库(保持单库行为)。
4. 对有效租户调用 tenantDataSourceManager.switchTenant(tenant) 切到该租户库:
- 如无数据源则创建一个 DruidDataSource 并挂到 DynamicDataSource;
- 当前线程的数据源 key 变为 tenant:{id}。
5. 在租户库中查询租户级项目配置:
- SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
- 若有值:TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
- 然后调用 ProjectConfig.loadTenantConfigsFromContext(),让 Wx/支付等配置指向当前租户。
6. 调用 filterChain.doFilter(request, response),后面的 Controller/Service/Mapper 就跑在该租户库上,并使用该租户的项目配置。
7. finally 中清理上下文:
- ProjectConfig.clearTenantConfigs();
- TenantConfigContext.clear();
- tenantDataSourceManager.clear();(内部清除数据源标记)。
- 兼容性:
- 不带 X-Tenant-Code:一切照旧(单库)。
- 错误/禁用/过期租户:仅打日志,不中断请求,退回单库行为(你可以后面再根据需要调整为直接返回错误)。
---
### 二、C 端前端需要怎么配合(非常关键)
1. 每个 C 端请求都带上 X-Tenant-Code:
- header 名:X-Tenant-Code;
- 值:你的租户编码(主库 tenant_info.tenant_code)。
2. 包括以下类型的请求:
- 登录接口(/app/app/login、/store/app/app/login 等);
- 用户信息、课程、订单、SOP 等所有业务接口;
- 登出(如有)等。
3. 租户编码获取方式(你可以按业务决定):
- 用户进入 app 之前选择医院/企业;
- 二级域名解析(如 tenantA.xxx.com → tenantCode=tenantA),前端启动时就把这个值带在 header 里。
只要 header 带得上,多租户这层就自动生效。
---
### 三、现有登录 / Token / Redis 行为说明
- user-app 目前的登录逻辑(AppLoginController / AppLoginScrmController)没有改:
- 还是用 JwtUtils.generateToken(userId) 生成 APPToken;
- 还是把 token 存到 Redis 的 userToken:{userId};
- 鉴权还是用 header 里的 APPToken 解出 userId。
- 这次的 SaaS 改造:
- 不改 token 结构,不强行在 token 里塞 tenantId;
- 按请求头的 X-Tenant-Code 切库,把“用户归属哪个租户”的责任交给前端来决定;
- Redis 这块暂时也不按租户做前缀,保证旧逻辑无感知(如果后面你要对 C 端 Redis 也做租户隔离,可以再单独一轮改造)。
---
### 四、如何自测 user-app C 端的多租户
你可以用 Postman 或直接在 app 上做几组简单验证:
1. 多租户切库验证
- 在主库的 tenant_info 中配置两个租户 A / B,对应不同业务库,两个库中插入不同的测试数据;
- 调用同一个 C 端接口(比如课程列表),分别带:
- X-Tenant-Code: A
- X-Tenant-Code: B
- 确认两个请求看到的是各自库中的数据(比如医院名/课程名不同)。
2. 租户配置隔离验证
- 在租户 A/B 的各自库的 sys_config.projectConfig 里,配置不同的微信 / 支付参数(可以改一个明显的字段);
- 调用依赖这些配置的接口(如微信登录、支付相关接口),看是否按不同租户配置生效。
3. 兼容性验证
- 不带 X-Tenant-Code 调接口,确保行为与改造前一致(不报错,不异常,仍然能正常访问)。
4. 异常租户验证
- 用一个不存在的 X-Tenant-Code 请求,观察日志里有告警(租户不存在 / 禁用 / 过期),同时请求不会抛异常,只是退回用主库。
---
### 五、user-app 模块改造说明文档
我已经帮你在项目里生成了一个专门的文档:
- 路径:docs/user-app模块SaaS改造说明.md
里面详细写了:
- 本次改造的目标、原则;
- DataSourceConfig 的改动;
- TenantDataSourceManager 和 AppTenantSwitchFilter 的设计与实现细节;
- 前端如何带 X-Tenant-Code;
- 推荐的自测场景。
你可以直接把这个文档发给前端和其他后端同事,对接起来会更顺畅。
---
### 六、总结(给你一个操作要点版)
- 后端:
- 已经支持通过 X-Tenant-Code 切换到对应租户库,并加载该租户的 projectConfig。
- 老逻辑兼容,不带头还是单库。
- 前端需要做的一件事:
- 所有 user-app 请求(含登录)统一加上请求头:
- X-Tenant-Code: <对应的租户编码>。
如果你希望下一步把 C 端的 Token 和 Redis 也做成像 admin/company 那种“token 里带 tenantId、Redis key 按租户隔离”的模式,我也可以再帮你设计一套不影响现有用户的渐进改法。