# admin 与 company 模块 SaaS 改造说明 本文档说明 **fs-admin**、**fs-company** 两模块在已有部分改造基础上的补充与完善点,以及本次已完成的改动。 --- ## 一、改造前已有能力 ### fs-admin - 依赖 **fs-framework**,登录走 `SysLoginService.login(..., tenantCode)`,支持传 `tenantCode` 查租户、切库、`LoginUser.setTenantId`。 - 请求链使用 framework 的 **JwtAuthenticationTokenFilter**:按 `tenantId` 切数据源、设置 **TenantConfigContext**、**ProjectConfig.loadTenantConfigsFromContext()**。 - 租户管理入口:**TenantInfoController**(租户 CRUD)。 ### fs-company - 自有 **CompanyLoginService**:支持 `tenantCode` 登录、主库查租户、**TenantDataSourceManager.switchTenant**、登录成功后 **loginUser.setTenantId**。 - 自有 **JwtAuthenticationTokenFilter**:按 `tenantId` 切库、设置 **TenantConfigContext**、**ProjectConfig.loadTenantConfigsFromContext()**。 - 自有 **TenantDataSourceManager**、**DynamicDataSource**,数据源配置含 master + sop。 --- ## 二、本次补充与完善 ### 1. fs-common:SecurityUtils.getTenantId() 兼容 company - **问题**:company 使用的 `LoginUser` 是 **framework.security.LoginUser**,不是 common 的 **LoginUser**。Redis 的 **TenantKeyRedisSerializer** 依赖 `SecurityUtils.getTenantId()` 给 key 加租户前缀,若只判断 `principal instanceof LoginUser`(common),company 登录后 principal 不匹配,导致 Redis key 无租户前缀。 - **改动**:在 **fs-common** 的 `SecurityUtils.getTenantId()` 中,除 `LoginUser` 外,对任意 principal 用反射调用 `getTenantId()`,若返回 `Long` 则使用,从而兼容 framework 的 LoginUser。 - **文件**:`fs-common/.../SecurityUtils.java`。 ### 2. fs-admin:租户管理接口强制走主库 - **问题**:租户表 **tenant_info** 只在主库;若管理员以某租户身份登录,当前数据源为租户库,访问「租户管理」接口会查租户库,可能无该表或数据错误。 - **改动**:在 **TenantInfoController** 上增加 **@DataSource(DataSourceType.MASTER)**,该类所有接口强制走主库。 - **文件**:`fs-admin/.../tenant/TenantInfoController.java`。 ### 3. fs-company:数据源映射统一为字符串 key - **问题**:**DataSourceConfig** 中 `targetDataSources` 使用 `DataSourceType.MASTER`(枚举)为 key,而 **DynamicDataSourceContextHolder** 使用 **MASTER.name()**(字符串)。为与 framework 及后续维护一致,建议统一为字符串 key。 - **改动**:将 `targetDataSources.put(DataSourceType.MASTER, ...)` 改为 `put(DataSourceType.MASTER.name(), ...)`。 - **文件**:`fs-company/.../config/DataSourceConfig.java`。 ### 4. fs-company:登录后 Redis Token 按租户隔离 - **问题**:Token 缓存的 Redis key 为 `company_login_tokens:uuid`,多租户下不同租户相同 uuid 会互相覆盖;且 **TenantKeyRedisSerializer** 对 `login_tokens` 有特殊处理,而 company 使用 `company_login_tokens`,存/取时若未带租户信息,无法按租户隔离。 - **改动**: 1. **TokenService**: - 生成 token 时若 `loginUser.getTenantId() != null`,将 **tenantId 写入 JWT claims**。 - **getTokenKey** 改为 `getTokenKey(Long tenantId, String uuid)`:tenantId 非空时返回 `tenantid:{tenantId}:company_login_tokens:{uuid}`(与 TenantKeyRedisSerializer 约定一致,以 `tenantid:` 开头不再加前缀)。 - **refreshToken** 使用 `getTokenKey(loginUser.getTenantId(), loginUser.getToken())` 写入 Redis。 - **getLoginUser** 从 JWT 解析出 uuid 与 tenantId,用 `getTokenKey(tenantId, uuid)` 取 Redis。 - **delLoginUser** 先解析 token 得到 tenantId 与 uuid,再按 `getTokenKey(tenantId, uuid)` 删除。 2. **CompanyLoginService**:在调用 **tokenService.createToken(loginUser)** 前,将当前 **LoginUser** 设置到 **SecurityContext**,便于其他依赖 `getTenantId()` 的逻辑(如 Redis 序列化)在需要时能拿到租户。 - **文件**:`fs-company/.../service/TokenService.java`、`CompanyLoginService.java`。 --- ## 三、改造后行为小结 | 模块 | 能力 | 说明 | |------|------|------| | **fs-admin** | 登录带 tenantCode | 与原有一致,framework 的 SysLoginService 处理。 | | **fs-admin** | 请求按 tenantId 切库 + 租户配置 | 与原有一致,framework 的 JwtAuthenticationTokenFilter 处理。 | | **fs-admin** | 租户管理接口 | 强制主库,避免租户身份下误查租户库。 | | **fs-company** | 登录带 tenantCode | 与原有一致,CompanyLoginService 处理。 | | **fs-company** | 请求按 tenantId 切库 + 租户配置 | 与原有一致,company 的 JwtAuthenticationTokenFilter 处理。 | | **fs-company** | Redis Token 按租户隔离 | Token 存/取/删均带 tenantId,多租户下互不覆盖。 | | **fs-common** | getTenantId() | 兼容 admin(common LoginUser)与 company(framework LoginUser)。 | --- ## 四、建议自测点 1. **admin** - 带 tenantCode 登录 → 后续请求是否走对应租户库、租户配置是否生效。 - 以租户身份登录后访问「租户管理」列表/编辑 → 是否始终查主库、数据是否正确。 2. **company** - 不同 tenantCode 登录 → 两户的 token 是否互不影响(Redis key 不同、互不踢出)。 - 带 tenantCode 登录后请求业务接口 → 是否走对应租户库、租户配置是否生效。 - 登出 → 对应 Redis key 是否按租户正确删除。 3. **Redis** - 多租户下 Redis 缓存(含 token、业务 key)是否均带租户前缀或按租户隔离,无串户。 --- ## 五、涉及文件清单 | 文件 | 改动说明 | |------|----------| | `fs-common/.../SecurityUtils.java` | getTenantId() 增加反射兼容 framework LoginUser | | `fs-admin/.../tenant/TenantInfoController.java` | 类上增加 @DataSource(DataSourceType.MASTER) | | `fs-company/.../config/DataSourceConfig.java` | MASTER 使用 .name() 作为 map key | | `fs-company/.../service/TokenService.java` | JWT 带 tenantId、getTokenKey(tenantId,uuid)、存/取/删按租户 key | | `fs-company/.../service/CompanyLoginService.java` | 生成 token 前设置 SecurityContext | 与整体 SaaS 方案的关系见 [SaaS改造方案.md](./SaaS改造方案.md)。