| 能力 | 位置 | 说明 |
|---|---|---|
| 租户表与 CRUD | fs-service / tenant_info |
TenantInfo 含 dbUrl/dbAccount/dbPwd,支持按库隔离 |
| 动态租户数据源 | fs-framework / TenantDataSourceManager |
按 tenantId 动态创建并缓存 Druid 数据源,注入 DynamicDataSource |
| 管理端多租户登录 | fs-admin / SysLoginController + SysLoginService |
登录支持 LoginBody.tenantCode,校验后切租户库并设置 LoginUser.tenantId |
| 请求级切库 + 租户配置 | fs-framework / JwtAuthenticationTokenFilter |
从 Token 取 tenantId → 设置数据源 + 从当前库查 projectConfig 写入 TenantConfigContext |
| 租户配置存储 | 各租户库 sys_config(config_key=projectConfig) |
微信、支付、云存储等按租户隔离 |
| Redis 租户前缀 | fs-common / TenantKeyRedisSerializer |
Key 前缀 tenantid:{tenantId},需确保取 tenantId 的上下文一致 |
fs-admin、部分 fs-company 使用 framework 的 JwtAuthenticationTokenFilter + DynamicDataSource;fs-user-app、fs-doctor-app、fs-store 等 C 端/应用端未接入。JwtUtils + 请求头 APPToken,不经过 framework,无 tenantId、无切库、无 TenantConfigContext。tenant_id 字段;若未来要支持「共享库 + tenant_id」,需单独规划。X-Tenant-Code、二级域名、或登录接口选租户)。tenantId,并统一用 framework 的过滤器做切库与 TenantConfigContext 注入。sys_config + TenantConfigContext;Redis 使用处统一带租户前缀(TenantKeyRedisSerializer 依赖的 tenantId 需在请求链中可用)。 ┌─────────────────────────────────────────────────────────┐
│ 统一租户识别入口 │
│ (域名 / X-Tenant-Code / 登录参数 → tenantId) │
└─────────────────────────────────────────────────────────┘
│
┌────────────────────────────────────┼────────────────────────────────────┐
▼ ▼ ▼
fs-admin (已有) fs-user-app (需改) fs-doctor-app 等
- 登录带 tenantCode - 接入 framework 过滤器 - 同 user-app
- JWT 含 tenantId - Token 含 tenantId - Token 含 tenantId
- 请求切库 + TenantConfigContext - 请求切库 + TenantConfigContext - 请求切库 + TenantConfigContext
│ │ │
└────────────────────────────────────┼────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ fs-framework:JwtAuthenticationTokenFilter │
│ - 解析 Token → LoginUser(tenantId) │
│ - DynamicDataSourceContextHolder.set("tenant:"+tenantId) │
│ - TenantConfigContext.set(projectConfig) │
│ - ProjectConfig.loadTenantConfigsFromContext() │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
主库 (tenant_info 等) 租户库 A (业务数据) 租户库 B (业务数据)
| 入口 | 推荐方式 | 说明 |
|---|---|---|
| 管理端 fs-admin | 登录参数 tenantCode(已有) |
保持不变 |
| C 端 fs-user-app / H5/小程序 | 方案 A:登录接口增加 tenantCode,与账号一起校验后写进 Token;方案 B:请求头 X-Tenant-Code,网关/过滤器先解析再查主库得到 tenantId |
二选一或组合(如 B 做兜底) |
| 企业端 fs-company-app | 同 C 端或与管理端一致 | 视是否同套登录 |
| 各 API(fs-wx-api、fs-store 等) | 请求头或 Token 中的 tenantId/tenantCode | 与网关/过滤器约定 |
落地要点:
X-Tenant-Code 或参数从主库查 tenant_info 得到 tenantId,并放入 ThreadLocal 或请求属性,供过滤器和业务使用。tenantCode,校验通过后调用与 admin 类似的「查租户 → switchTenant → 生成 Token 并设置 LoginUser.tenantId」逻辑(可抽到 framework 的 SysLoginService 或新服务中复用)。目标:C 端请求也按租户切库并使用 TenantConfigContext,与 admin 行为一致。
JwtAuthenticationTokenFilter、DynamicDataSource、TenantDataSourceManager、TenantConfigContext。DataSourceConfig(即主库 + 可选 SOP/从库 + 租户动态数据源),或在本模块复制一份并保证 DynamicDataSource 注入 TenantDataSourceManager 使用的同一实例,这样 TenantDataSourceManager 动态加入的租户库才会生效。tenantCode(或从请求头 X-Tenant-Code 取);tenant_info 校验租户状态;TenantDataSourceManager.switchTenant(tenantInfo) 切换到该租户库(便于本次登录逻辑里查该租户下的用户等);LoginUser 并 setTenantId(tenantInfo.getId());TokenService.createToken(loginUser) 生成 JWT,这样 JWT 中会带 tenantId。APPToken 作为 Token 传递方式;在 fs-framework 的 TokenService.getLoginUser(request) 中支持从 APPToken 解析(或 user-app 单独一个 Filter 先取 APPToken 再塞到 Authorization),保证后续 Filter 拿到的 LoginUser 含 tenantId。SecurityConfig 中,加入与 fs-admin 相同的 JwtAuthenticationTokenFilter(并注入 TokenService、DynamicDataSource、SysConfigMapper),保证每个请求:
LoginUser(含 tenantId);DynamicDataSourceContextHolder 和 TenantConfigContext;AuthorizationInterceptor)+ 自有 JwtUtils,可逐步替换:先让 Filter 只做「解析 Token → 设置租户上下文」,业务仍用现有 getUserId();再逐步把 getUserId/getTenantId 统一为从 SecurityUtils.getLoginUser() 取。getTenantId(),从 SecurityUtils.getLoginUser().getTenantId() 或从当前请求的租户 ThreadLocal 取;现有 getUserId() 可改为优先从 LoginUser 取,便于与 framework 统一。| 序号 | 改动项 | 说明 |
|---|---|---|
| 1 | 引入 fs-framework 依赖 | 使用其 DataSourceConfig、JwtAuthenticationTokenFilter、TokenService、TenantDataSourceManager |
| 2 | 数据源改为 DynamicDataSource + 租户 | 与 framework 一致,保证租户库被动态注入 |
| 3 | C 端登录入参/请求头带 tenantCode | 登录时解析租户并 setTenantId,Token 用 TokenService 生成 |
| 4 | Security 中注册 JwtAuthenticationTokenFilter | 顺序在认证之前,确保每次请求设好数据源与 TenantConfigContext |
| 5 | TokenService 支持从 APPToken 取 Token | 或 Filter 内兼容 APPToken 请求头 |
| 6 | AppBaseController 提供 getTenantId() | 业务按需使用 |
| 7 | Redis 使用 TenantKeyRedisSerializer | 若 user-app 有独立 RedisConfig,需与 common 一致,并保证 tenantId 在上下文可用 |
| 增强项 | 说明 |
|---|---|
| 租户解析器 | 提供 TenantResolver 接口:从 HttpServletRequest(域名/请求头/参数)解析出 tenantCode 或 tenantId,查主库得到 TenantInfo,供各模块 Filter 调用 |
| TokenService 扩展 | getLoginUser(request) 支持从 APPToken 等请求头取 Token,避免各端重复写 |
| 租户数据源生命周期 | 对 TENANT_DS_CACHE 做容量与过期策略,或增加「租户禁用时移除数据源」的联动 |
| 健康检查 | 定时检测各租户数据源可用性,不可用时从缓存移除或告警 |
sys_config.projectConfig + TenantConfigContext,无需改表结构;若新增配置项,在 projectConfig 的 JSON 中扩展即可。TokenService.getLoginUser() 并设置好 LoginUser.tenantId 之后,再执行业务逻辑(这样 TenantKeyRedisSerializer 从 ThreadLocal 或 SecurityContext 取到的 tenantId 才正确)。tenant_id。tenant_id 字段;确定租户识别策略
fs-framework 小增强
先改 fs-user-app
再改 fs-doctor-app、fs-company-app、fs-store 等
各 API 模块(fs-wx-api、fs-ad-api 等)
回归与监控
| 模块 | 主要改动文件/目录 |
|---|---|
| fs-common | LoginUser/LoginBody 已有 tenantId/tenantCode;RedisConfig 确保 TenantKeyRedisSerializer 生效 |
| fs-framework | JwtAuthenticationTokenFilter、TenantDataSourceManager、DataSourceConfig、TokenService;新增 TenantResolver(可选) |
| fs-admin | 已支持多租户,仅需保证登录/Token 与文档一致,无需大改 |
| fs-user-app | pom.xml、DataSourceConfig/数据源、SecurityConfig、登录 Controller/Service、AppBaseController、各业务 Controller 若需 tenantId 则用 getTenantId() |
| fs-service | 租户与系统配置相关已存在;若 C 端登录要查主库 tenant_info,可复用 TenantInfoService |
按上述方案实施后,全代码模块可统一为「SaaS 版」:各入口带租户身份,按租户切库与使用租户配置,为后续计费、限流、租户级功能开关等打好基础。
私有化部署时:每个客户独立库、独立服务器,定时任务跑在该客户自己的进程里,天然只操作本库。SaaS 化后:一套进程、多租户多库,定时任务需要「按租户执行」,即每个任务逻辑要对每个启用租户各执行一遍,且执行时数据源和租户配置为该租户的库与配置。
现状:
SysJobServiceImpl.init() 从该库 selectJobAll() 加载任务并注册到 Quartz。LoginUser,无租户上下文;JobInvokeUtil.invokeMethod(sysJob) 调用的 Bean 方法访问的也是「当前默认数据源」。SaaS 下的矛盾:
若仍用「一个 DataSource」且用主库,则读不到各租户的 sys_job;若用某一租户库,则只能跑该租户任务。因此需要显式按租户调度 + 按租户执行。
tenant_info)。TenantDataSourceManager.switchTenant(tenantInfo) 切到该租户库;sys_job 中状态为「正常」且本分钟应触发的任务(或查全部,在内存里用 Cron 判断是否到点);JobInvokeUtil.invokeMethod(sysJob)(此时线程上下文已是该租户库);sys_job_log 时仍在当前租户库,无需改表。DynamicDataSourceContextHolder、TenantConfigContext。0 * * * * ? 每分钟)。TenantJobDispatcherJob,实现 Quartz Job,内部逻辑为上述 1~3。tenantId + jobId 保证唯一;执行时从 JobDataMap 取出 tenantId,先 switchTenant + 设置 TenantConfigContext,再执行 invokeMethod。建议:优先采用 方案 A,对现有私有化表结构零侵入,仅增加一个分发器 Job 和按租户循环执行的逻辑。
现状:
SaaS 下目标:
同一套代码、同一进程,但每个定时任务要对每个启用租户各执行一遍,且执行时该租户的库、企微配置等来自该租户的 sys_config(TenantConfigContext)。
引入多租户能力
tenant_info 的 Mapper)。封装「按租户执行」工具
TenantTaskRunner.runForEachTenant(Consumer<TenantInfo> action)TenantDataSourceManager.switchTenant(tenantInfo);sys_config.projectConfig 写入 TenantConfigContext(可复用现有 ProjectConfig.loadTenantConfigsFromContext 逻辑);action.accept(tenantInfo);DynamicDataSourceContextHolder、TenantConfigContext。改造每个 @Scheduled 方法
tenantTaskRunner.runForEachTenant(tenantInfo -> { 原有逻辑 });qwCheckSopRuleTime() → tenantTaskRunner.runForEachTenant(t -> qwSopService.checkSopRuleTime());addTag() → tenantTaskRunner.runForEachTenant(t -> qwSopTagService.addTag());Redis
小结:fs-qw-task 不改任务业务逻辑,只加一层「按租户执行」的壳;数据源与租户配置与 fs-admin 对齐即可。
| 模块 | 改动要点 |
|---|---|
| fs-framework | 可选:提供 TenantTaskRunner(或放在 fs-qw-task 内),依赖 TenantDataSourceManager、TenantInfoService/Mapper、TenantConfigContext。 |
| fs-admin | Quartz 使用 DynamicDataSource;新增 TenantJobDispatcherJob;init() 只注册分发器或按策略注册。 |
| fs-quartz | 若采用方案 A,可不改表与 Mapper,仅被分发器通过 JobInvokeUtil 调用;若采用方案 B,需 sys_job 增加 tenant_id 及迁移脚本。 |
| fs-qw-task | 依赖 fs-framework;DataSource 改为 DynamicDataSource + 租户;新增 TenantTaskRunner;各 @Scheduled 改为 runForEachTenant(原有逻辑)。 |