方案.md 6.6 KB

现状结论

  • 管理端(fs-admin) 已具备多租户:tenant_info、按库隔离、登录带 tenantCode、JWT 带 tenantId、请求里切库并注入 TenantConfigContext
  • C 端(fs-user-app 等) 还未接多租户:用自有 JwtUtils + APPToken,没有 tenantId、不切库、也不用租户配置。

改造目标(SaaS 版要达成什么)

  1. 所有入口(admin、user-app、doctor-app、company、store、各 API)请求都带租户身份,按租户切库或使用租户配置。
  2. 租户识别方式统一(管理端继续用登录 tenantCode,C 端用登录参数和/或请求头如 X-Tenant-Code)。
  3. Token/请求上下文中统一带 tenantId,用 framework 的过滤器做切库和 TenantConfigContext 注入。

主要要改的地方与改法

类别 改什么 怎么改
租户识别 C 端如何知道是哪个租户 方案 A:登录接口增加 tenantCode,写入 Token;方案 B:请求头 X-Tenant-Code,在 Filter 里解析出 tenantId。可 A+B 组合。
fs-user-app 接入多租户链路 1)依赖 fs-framework;2)数据源改为用 framework 的 DynamicDataSource(含租户库);3)登录时查主库 tenant_info、切租户、LoginUser.setTenantId(),用 TokenService.createToken() 生成 JWT;4)在 Security 里加入 JwtAuthenticationTokenFilter,保证每次请求按 tenantId 切库并设置 TenantConfigContext;5)TokenService.getLoginUser() 支持从 APPToken 取 Token;6)AppBaseController 增加 getTenantId()
fs-doctor-app / company / store 等 与 user-app 一致支持多租户 同样:依赖 framework、用 DynamicDataSource、登录/请求带 tenantCode 或 tenantId、使用 JwtAuthenticationTokenFilter、业务从 SecurityUtils/TenantConfigContext 取租户。
fs-framework(可选增强) 租户解析与 Token 兼容 新增 TenantResolver(从请求头/域名解析 tenantCode→tenantId);TokenService 支持从 APPToken 取 Token。
Redis 多租户下 Key 隔离 使用已配置 TenantKeyRedisSerializer 的 RedisTemplate,并保证在 Filter 里先设置好 LoginUser.tenantId 再执行业务。
数据库 保持现有隔离方式 继续「每租户一库」,无需给业务表加 tenant_id;若以后要做共享库再单独规划。

建议实施顺序

  1. 定好 C 端租户识别策略(登录参数 vs 请求头)。
  2. 在 fs-framework 做小增强:租户解析(可选)、Token 支持 APPToken。
  3. 先改 fs-user-app:接 framework、改数据源、登录写 tenantId、加 Filter、基类加 getTenantId(),联调不同 tenantCode 验证隔离。
  4. 再按同一模式改 fs-doctor-app、fs-company-app、fs-store 等。
  5. 各 API 模块在入口 Filter 或网关上解析 tenantId 并设置数据源与 TenantConfigContext

完整步骤、架构图和涉及文件清单都在 docs/SaaS改造方案.md 里,可按该文档逐步落地。如果你希望从某一块先动手(例如先只改 user-app 或先做 TenantResolver),可以说一下,我可以按那一块给出更具体的改法或示例代码。

查看 fs-quartz 和 fs-qw-task 的当前实现,以便给出 SaaS 下定时任务的处理方案。

一、问题本质

  • 私有化:每个客户独立库、独立服务器,定时任务只连本客户库,没有多租户问题。
  • SaaS:一套进程、多租户多库,定时任务必须「按租户执行」——同一任务逻辑要对每个启用租户各跑一遍,且每次跑的是该租户的库和配置。

二、fs-quartz(管理端可配置的定时任务)

  • 现状:任务配置在 sys_job(当前是单库),Quartz 用单一 DataSource 加载并触发,执行时没有租户上下文。
  • 推荐做法(方案 A:租户任务分发器)
    • Quartz 只注册一个定时任务(例如每分钟一次),不直接执行业务,只做「按租户分发」:
    • 主库查所有启用租户;
    • 对每个租户:TenantDataSourceManager.switchTenant(tenantInfo) 切到该租户库 → 从该租户库sys_job 里查出本分钟该触发的任务 → 对每个任务执行 JobInvokeUtil.invokeMethod(sysJob)
    • 执行完后清理数据源和租户上下文。
    • 这样 sys_job / sys_job_log 仍保留在各租户库,表结构不用改,只是多了一个「分发器」Job。
  • 需要改的地方
    • fs-admin:Quartz 使用 DynamicDataSource(主库 + 租户动态库);新增一个 Bean(如 TenantJobDispatcherJob),实现上述 1~3,并在启动时只把该分发器注册到 Scheduler。
    • fs-quartz:SysJobServiceImpl.init() 改为只注册这一条「分发器」任务,不再从当前库 selectJobAll 注册所有任务(具体是否保留 init 可根据你是否把 sys_job 迁主库而定)。

三、fs-qw-task(企微等 @Scheduled 定时任务)

  • 现状:独立应用,用 @Scheduled 写死 cron,连的是单 master + SOP,没有租户。
  • 做法:按租户遍历 + 租户上下文
    1. 接入多租户:引入 fs-framework,数据源改为 DynamicDataSource(主库 + 租户动态库),与 fs-admin 一致。
    2. 封装执行器:提供 TenantTaskRunner.runForEachTenant(Consumer<TenantInfo> action):从主库查启用租户 → 对每个租户切库、设置 TenantConfigContext → 执行 action → 清理上下文。
    3. 改造每个 @Scheduled:原来整段任务逻辑包一层,例如:
      • tenantTaskRunner.runForEachTenant(t -> qwSopService.checkSopRuleTime());
      • 其它如 addTag()wxSop()sendQwGroupMsgTask() 等同理,都包在 runForEachTenant 里。
    4. 业务 Mapper/Service 不用改,只要在「已切到该租户库 + 已设置租户配置」的线程里执行即可;若用 Redis,需保证带租户前缀(TenantKeyRedisSerializer)且上下文中有 tenantId。

四、实施顺序建议

  1. 先做 fs-qw-task:接 framework、DynamicDataSource、实现 TenantTaskRunner、逐个 @Scheduled 包一层 runForEachTenant,验证多租户数据与配置隔离。
  2. 再做 fs-quartz:在 fs-admin 里实现「租户任务分发器」Job,Quartz 只调度这一条;分发器内按租户切库并执行各租户库中到点的 sys_job。

更细的步骤、方案 B(sys_job 上迁主库 + tenant_id)以及涉及的文件清单,都在 docs/SaaS改造方案.md第七章 里,可直接按文档落地。