|
|
@@ -0,0 +1,130 @@
|
|
|
+package com.fs.framework.filter;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.fs.common.enums.DataSourceType;
|
|
|
+import com.fs.common.utils.StringUtils;
|
|
|
+import com.fs.config.saas.ProjectConfig;
|
|
|
+import com.fs.core.config.TenantConfigContext;
|
|
|
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
|
|
|
+import com.fs.framework.datasource.TenantDataSourceManager;
|
|
|
+import com.fs.system.domain.SysConfig;
|
|
|
+import com.fs.system.mapper.SysConfigMapper;
|
|
|
+import com.fs.tenant.domain.TenantInfo;
|
|
|
+import com.fs.tenant.service.TenantInfoService;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.core.annotation.Order;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.filter.OncePerRequestFilter;
|
|
|
+
|
|
|
+import javax.servlet.FilterChain;
|
|
|
+import javax.servlet.ServletException;
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+/**
|
|
|
+ * C 端 user-app 多租户过滤器。
|
|
|
+ *
|
|
|
+ * <p>职责:</p>
|
|
|
+ * <ul>
|
|
|
+ * <li>从请求头 <b>X-Tenant-Code</b> 解析当前访问的租户编码(tenantCode)。</li>
|
|
|
+ * <li>先切到主库(MASTER),根据 tenantCode 查询租户表 {@code tenant_info} 获取 {@link TenantInfo}。</li>
|
|
|
+ * <li>调用 {@link TenantDataSourceManager#switchTenant(TenantInfo)} 将当前线程切换到对应租户库。</li>
|
|
|
+ * <li>在租户库中读取 {@code sys_config.projectConfig},填充 {@link TenantConfigContext} 并调用 {@link ProjectConfig#loadTenantConfigsFromContext()}。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p>注意:</p>
|
|
|
+ * <ul>
|
|
|
+ * <li>若未携带 X-Tenant-Code,则保持原有逻辑(使用默认数据源,不做多租户切换),兼容老版本。</li>
|
|
|
+ * <li>若 tenantCode 无效 / 被禁用 / 已过期,可根据需要返回错误码;当前实现仅记录日志并回退主库。</li>
|
|
|
+ * <li>过滤器在请求结束时会清理 TenantConfigContext 与 DynamicDataSourceContextHolder,避免线程复用导致串库。</li>
|
|
|
+ * </ul>
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+@Order(5) // 在业务处理前尽早完成数据源和租户配置的上下文初始化
|
|
|
+public class AppTenantSwitchFilter extends OncePerRequestFilter {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 租户编码请求头名称。
|
|
|
+ * 前端需在每次请求中携带该头,才能启用多租户能力。
|
|
|
+ */
|
|
|
+ public static final String HEADER_TENANT_CODE = "X-Tenant-Code";
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TenantInfoService tenantInfoService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TenantDataSourceManager tenantDataSourceManager;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private SysConfigMapper sysConfigMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void doFilterInternal(HttpServletRequest request,
|
|
|
+ HttpServletResponse response,
|
|
|
+ FilterChain filterChain) throws ServletException, IOException {
|
|
|
+ String tenantCode = request.getHeader(HEADER_TENANT_CODE);
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (StringUtils.isBlank(tenantCode)) {
|
|
|
+ // 未携带租户编码:保持单库行为,直接放行
|
|
|
+ filterChain.doFilter(request, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Step 1:切到主库,根据 tenantCode 查询租户信息
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
|
|
|
+
|
|
|
+ TenantInfo query = new TenantInfo();
|
|
|
+ query.setTenantCode(tenantCode);
|
|
|
+ List<TenantInfo> tenants = tenantInfoService.selectTenantInfoList(query);
|
|
|
+ if (tenants == null || tenants.isEmpty()) {
|
|
|
+ log.warn("[SaaS user-app] 请求携带的 tenantCode={} 未找到租户记录,按单库模式处理。", tenantCode);
|
|
|
+ filterChain.doFilter(request, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ TenantInfo tenant = tenants.get(0);
|
|
|
+
|
|
|
+ // 基础校验:启用状态 + 未过期
|
|
|
+ if (!Integer.valueOf(1).equals(tenant.getStatus())) {
|
|
|
+ log.warn("[SaaS user-app] 租户 tenantCode={} 已禁用,按单库模式处理。", tenantCode);
|
|
|
+ filterChain.doFilter(request, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (tenant.getExpireTime() != null && tenant.getExpireTime().before(new Date())) {
|
|
|
+ log.warn("[SaaS user-app] 租户 tenantCode={} 已过期,按单库模式处理。", tenantCode);
|
|
|
+ filterChain.doFilter(request, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Step 2:切换到租户库(如无则动态创建数据源)
|
|
|
+ tenantDataSourceManager.switchTenant(tenant);
|
|
|
+
|
|
|
+ // Step 3:从租户库读取项目配置(projectConfig),填充 TenantConfigContext 和 ProjectConfig
|
|
|
+ SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
|
|
|
+ if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
|
|
|
+ TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
|
|
|
+ } else {
|
|
|
+ TenantConfigContext.set(null);
|
|
|
+ }
|
|
|
+ ProjectConfig.loadTenantConfigsFromContext();
|
|
|
+
|
|
|
+ // Step 4:继续后续过滤器链和业务处理(此时 Mapper 操作的就是当前租户库)
|
|
|
+ filterChain.doFilter(request, response);
|
|
|
+ } finally {
|
|
|
+ // 确保请求结束后清理租户上下文和数据源标记,避免线程复用时串库
|
|
|
+ try {
|
|
|
+ ProjectConfig.clearTenantConfigs();
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ TenantConfigContext.clear();
|
|
|
+ tenantDataSourceManager.clear();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|