xgb пре 1 недеља
родитељ
комит
7d1aee8ee5

+ 32 - 0
fs-admin/src/main/java/com/fs/admin/config/AdminWebMvcConfig.java

@@ -0,0 +1,32 @@
+package com.fs.admin.config;
+
+import com.fs.admin.interceptor.AdminTenantDataSourceInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class AdminWebMvcConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private AdminTenantDataSourceInterceptor adminTenantDataSourceInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(adminTenantDataSourceInterceptor)
+                .addPathPatterns(
+                        "/admin/callRecord/**",
+                        "/admin/voice-robotic/**",
+                        "/admin/sms-admin/**",
+                        "/admin/sms-order/**",
+                        "/admin/sms-package/**",
+                        "/admin/videoResource/**",
+                        "/admin/live/**",
+                        "/admin/liveVideo/**",
+                        "/admin/product/**",
+                        "/admin/storeOrder/**",
+                        "/admin/article/**"
+                );
+    }
+}

+ 7 - 8
fs-admin/src/main/java/com/fs/admin/controller/AdminVideoResourceController.java

@@ -2,31 +2,30 @@ package com.fs.admin.controller;
 
 
 import java.util.*;
 import java.util.*;
 
 
-import com.fs.admin.helper.AdminCrossTenantHelper;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.page.TableDataInfo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
 /**
 /**
  * 总后台视频资源审计控制器
  * 总后台视频资源审计控制器
- * 遍历所有租户库查询 fs_user_course_video 数据(视频资源部分)
+ * 通过拦截器切库后直接查询当前租户 fs_user_course_video 数据
  */
  */
 @RestController
 @RestController
 @RequestMapping("/admin/videoResource")
 @RequestMapping("/admin/videoResource")
 public class AdminVideoResourceController extends BaseController {
 public class AdminVideoResourceController extends BaseController {
 
 
     @Autowired
     @Autowired
-    private AdminCrossTenantHelper crossTenantHelper;
+    private JdbcTemplate jdbcTemplate;
 
 
     /**
     /**
-     * 查询所有租户的视频资源列表
+     * 查询指定租户的视频资源列表(由 AdminTenantDataSourceInterceptor 切库)
      */
      */
     @PreAuthorize("@ss.hasPermi('admin:videoResource:list')")
     @PreAuthorize("@ss.hasPermi('admin:videoResource:list')")
     @GetMapping("/list")
     @GetMapping("/list")
-    public TableDataInfo list(@RequestParam(required = false) Long companyId,
-                              @RequestParam(required = false) String companyName,
+    public TableDataInfo list(@RequestParam(required = false) Long tenantId,
                               @RequestParam(required = false) String videoName) {
                               @RequestParam(required = false) String videoName) {
         String sql = "SELECT id, title as videoName, video_type as videoType, " +
         String sql = "SELECT id, title as videoName, video_type as videoType, " +
                 "play_url as playUrl, duration, status, create_time as createTime " +
                 "play_url as playUrl, duration, status, create_time as createTime " +
@@ -37,7 +36,7 @@ public class AdminVideoResourceController extends BaseController {
         }
         }
         sb.append(" ORDER BY create_time DESC");
         sb.append(" ORDER BY create_time DESC");
 
 
-        List<Map<String, Object>> allList = crossTenantHelper.queryAcrossTenants(companyId, companyName, sb.toString());
-        return getDataTable(allList);
+        List<Map<String, Object>> list = jdbcTemplate.queryForList(sb.toString());
+        return getDataTable(list);
     }
     }
 }
 }

+ 1 - 3
fs-admin/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java → fs-admin/src/main/java/com/fs/admin/controller/audit/AdminCompanyBridgeController.java

@@ -1,4 +1,4 @@
-package com.fs.admin.controller;
+package com.fs.admin.controller.audit;
 
 
 import com.fs.common.annotation.Log;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.controller.BaseController;
@@ -6,9 +6,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.enums.BusinessType;
 import com.fs.company.domain.*;
 import com.fs.company.domain.*;
-import com.fs.company.param.*;
 import com.fs.company.service.*;
 import com.fs.company.service.*;
-import com.fs.company.vo.*;
 import com.fs.proxy.domain.TenantTrafficPricing;
 import com.fs.proxy.domain.TenantTrafficPricing;
 import com.fs.proxy.service.ITenantTrafficPricingService;
 import com.fs.proxy.service.ITenantTrafficPricingService;
 import com.fs.qw.service.IQwIpadServerService;
 import com.fs.qw.service.IQwIpadServerService;

+ 427 - 0
fs-admin/src/main/java/com/fs/admin/controller/tenant/TenantInfoController.java

@@ -0,0 +1,427 @@
+package com.fs.admin.controller.tenant;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.annotation.Log;
+import com.fs.common.constant.UserConstants;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysMenu;
+import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.dto.MenuDto;
+import com.fs.tenant.dto.SysConfigDto;
+import com.fs.tenant.mapper.TenantInfoMapper;
+import com.fs.tenant.service.TenantInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.CollectionUtils;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 租户基础信息Controller(SaaS 下租户表仅在主库,强制走主库)
+ *
+ * @author fs
+ * @date 2026-01-23
+ */
+@DataSource(DataSourceType.MASTER)
+@RestController
+@RequestMapping("/tenant/tenant")
+public class TenantInfoController extends BaseController
+{
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private TenantInfoMapper tenantInfoMapper;
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+    /**
+     * 查询租户基础信息列表
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(TenantInfo tenantInfo)
+    {
+        startPage();
+        List<TenantInfo> list = tenantInfoService.selectTenantInfoList(tenantInfo);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询所有租户id以及租户名称 租户编码
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:list')")
+    @GetMapping("/tenantList")
+    public R tenantList(TenantInfo tenantInfo)
+    {
+        return R.ok().put("rows",tenantInfoService.tenantList(tenantInfo));
+    }
+
+    /**
+     * 导出租户基础信息列表
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:export')")
+    @Log(title = "租户基础信息", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(TenantInfo tenantInfo)
+    {
+        List<TenantInfo> list = tenantInfoService.selectTenantInfoList(tenantInfo);
+        ExcelUtil<TenantInfo> util = new ExcelUtil<TenantInfo>(TenantInfo.class);
+        return util.exportExcel(list, "租户基础信息数据");
+    }
+
+    /**
+     * 获取租户基础信息详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") String id)
+    {
+        return AjaxResult.success(tenantInfoService.selectTenantInfoById(id));
+    }
+
+    /**
+     * 新增租户基础信息
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:add')")
+    @Log(title = "租户基础信息", businessType = BusinessType.INSERT)
+    @PostMapping
+    public R add(@RequestBody TenantInfo tenantInfo)
+    {
+        int i = tenantInfoService.insertTenantInfo(tenantInfo);
+        return i > 0 ? R.ok("租户数据库初始化中,请稍后") : R.error("租户创建失败");
+    }
+
+    /**
+     * 修改租户基础信息
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:edit')")
+    @Log(title = "修改租户基础信息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody TenantInfo tenantInfo)
+    {
+        return toAjax(tenantInfoService.updateTenantInfo(tenantInfo));
+    }
+
+    /**
+     * 删除租户基础信息
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:remove')")
+    @Log(title = "删除租户基础信息", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String[] ids)
+    {
+        return toAjax(tenantInfoService.deleteTenantInfoByIds(ids));
+    }
+
+    /**
+     * 租户菜单修改(获取租户菜单)
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:edit')")
+    @PostMapping("/menu")
+    public R menuChange(@RequestBody Map<String,Object> map)
+    {
+        String id = map.get("id").toString();
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(id);
+        if (tenantInfo.getStatus() == 2) {
+            throw new CustomException("租户初始化中");
+        }
+
+        // 先查一下标准菜单
+        String flag = map.get("flag").toString();
+        if ("sys".equals(flag)){
+            List<SysMenu> sysMenus = tenantInfoMapper.selectMenuList(new SysMenu());
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            return tenantInfoService.menuChange(flag, sysMenus,null);
+        }
+
+        List<TenantCompanyMenu> companyMenus = tenantInfoMapper.selectCompanyMenuList(new TenantCompanyMenu());
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        return tenantInfoService.menuChange(flag,null,companyMenus);
+
+    }
+
+    /**
+     * 租户菜单修改
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:tenant:edit')")
+    @PostMapping("/menu/edit")
+    public R menuEdit(@RequestBody MenuDto menuDto)
+    {
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(menuDto.getId());
+        if (tenantInfo.getStatus() == 2) {
+            throw new CustomException("租户初始化中");
+        }
+
+        List<Long> selected = menuDto.getSelected();
+        List<Long> unSelected = menuDto.getUnSelected();
+        if ("sys".equals(menuDto.getFlag())) {
+            List<SysMenu> addSysMenu = getAddSysMenu(tenantInfo, menuDto.getSelected());
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            return tenantInfoService.menuEdit(selected, unSelected, menuDto.getFlag(),addSysMenu,null);
+        }
+
+        List<TenantCompanyMenu> addCompanyMenu = getAddCompanyMenu(tenantInfo, menuDto.getSelected());
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        return tenantInfoService.menuEdit(selected, unSelected, menuDto.getFlag(),null,addCompanyMenu);
+    }
+
+
+    /**
+     * 租户配置修改
+     */
+    @PreAuthorize("@ss.hasPermi('tenant:config:edit')")
+    @PostMapping("/config/edit")
+    public R configEdit(@Validated @RequestBody SysConfigDto config)
+    {
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(config.getId());
+        if (tenantInfo.getStatus() == 2) {
+            throw new CustomException("租户初始化中");
+        }
+
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        //修复只能更新的BUG
+        if (null != config.getConfigId()) {
+            return R.ok().put("data", configService.updateConfig(config));
+        } else {
+           return R.ok().put("data", configService.insertConfig(config));
+        }
+    }
+
+    /**
+     * 根据租户id获取指定配置文件
+     *
+     * @param configKey
+     * @param id
+     * @return
+     */
+    @GetMapping(value = "/getConfigByKey/{configKey}")
+    public AjaxResult getConfigByKey(@PathVariable String configKey,String id) {
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(id);
+        if (tenantInfo.getStatus() == 2) {
+            throw new CustomException("租户初始化中");
+        }
+
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        SysConfig config = configService.selectConfigByConfigKey(configKey);
+        return AjaxResult.success(config);
+    }
+
+    /**
+     * 获取需要更新的后台菜单
+     */
+    private List<SysMenu> getAddSysMenu(TenantInfo tenantInfo,List<Long> selected){
+        // 切换到租户库
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        // 查询租户库里已经存在的menuId
+        List<Long> existIds = tenantInfoMapper.selectExistMenuIds();
+        // 不存在的menuId(就是要新增的)
+        List<Long> needAddIds = selected.stream()
+                .filter(id -> !existIds.contains(id))
+                .collect(Collectors.toList());
+        // 去总库查询详细的菜单详情
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (!CollectionUtils.isEmpty(needAddIds)){
+            List<SysMenu> addMenuList = tenantInfoMapper.getTenSysMenuByIds(needAddIds);
+            return addMenuList;
+        }
+
+        return new ArrayList<>();
+    }
+
+    /**
+     * 获取需要更新的销售菜单
+     */
+    private List<TenantCompanyMenu> getAddCompanyMenu(TenantInfo tenantInfo, List<Long> selected){
+        // 切换到租户库
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        // 查询租户库里已经存在的menuId
+        List<Long> existIds = tenantInfoMapper.selectExistComMenuIds();
+        // 不存在的menuId(就是要新增的)
+        List<Long> needAddIds = selected.stream()
+                .filter(id -> !existIds.contains(id))
+                .collect(Collectors.toList());
+        // 去总库查询详细的菜单详情
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (!CollectionUtils.isEmpty(needAddIds)) {
+            List<TenantCompanyMenu> addMenuList = tenantInfoMapper.getTenComMenuByIds(needAddIds);
+            return addMenuList;
+        }
+
+        return new ArrayList<>();
+    }
+
+    /**
+     * 获取租户总后台菜单列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:list')")
+    @GetMapping("/tenantMenu/list")
+    public AjaxResult list(SysMenu menu)
+    {
+        List<SysMenu> menus = tenantInfoService.selectMenuList(menu, getUserId());
+        return AjaxResult.success(menus);
+    }
+
+    /**
+     * 获取租户销售菜单列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:list')")
+    @GetMapping("/tenantComMenu/list")
+    public AjaxResult list(TenantCompanyMenu menu)
+    {
+        List<TenantCompanyMenu> menus = tenantInfoService.selectCompanyMenuList(menu, getUserId());
+        return AjaxResult.success(menus);
+    }
+
+
+    /**
+     * 根据菜单编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:query')")
+    @GetMapping(value = "/tenantMenu/{menuId}")
+    public AjaxResult getInfo(@PathVariable Long menuId)
+    {
+        return AjaxResult.success(tenantInfoService.selectMenuById(menuId));
+    }
+
+    /**
+     * 根据菜单编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:query')")
+    @GetMapping(value = "/getTenantComMenu/{menuId}")
+    public AjaxResult getTenantComMenu(@PathVariable Long menuId)
+    {
+        return AjaxResult.success(tenantInfoService.getTenantComMenu(menuId));
+    }
+
+
+    /**
+     * 新增菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:add')")
+    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
+    @PostMapping("/addTenantMenu")
+    public AjaxResult add(@Validated @RequestBody SysMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(tenantInfoService.checkMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        menu.setCreateBy(getUsername());
+        return toAjax(tenantInfoService.insertMenu(menu));
+    }
+
+    @PreAuthorize("@ss.hasPermi('system:menu:add')")
+    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
+    @PostMapping("/addTenantComMenu")
+    public AjaxResult addTenantComMenu(@Validated @RequestBody TenantCompanyMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(tenantInfoService.checkComMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        menu.setCreateBy(getUsername());
+        return toAjax(tenantInfoService.insertComMenu(menu));
+    }
+
+    /**
+     * 修改菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateTenantMenu")
+    public AjaxResult edit(@Validated @RequestBody SysMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(tenantInfoService.checkMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        else if (menu.getMenuId().equals(menu.getParentId()))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+        }
+        menu.setUpdateBy(getUsername());
+        return toAjax(tenantInfoService.updateMenu(menu));
+    }
+
+    /**
+     * 修改菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateTenantComMenu")
+    public AjaxResult updateTenantComMenu(@Validated @RequestBody TenantCompanyMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(tenantInfoService.checkComMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        else if (menu.getMenuId().equals(menu.getParentId()))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+        }
+        menu.setUpdateBy(getUsername());
+        return toAjax(tenantInfoService.updateComMenu(menu));
+    }
+
+    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
+    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/delTenantMenu/{menuId}")
+    public AjaxResult remove(@PathVariable("menuId") Long menuId)
+    {
+        if (tenantInfoService.hasChildByMenuId(menuId))
+        {
+            return AjaxResult.error("存在子菜单,不允许删除");
+        }
+        return toAjax(tenantInfoService.deleteMenuById(menuId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
+    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/delTenantComMenu/{menuId}")
+    public AjaxResult delTenantComMenu(@PathVariable("menuId") Long menuId)
+    {
+        if (tenantInfoService.hasChildByComMenuId(menuId))
+        {
+            return AjaxResult.error("存在子菜单,不允许删除");
+        }
+        return toAjax(tenantInfoService.deleteComMenuById(menuId));
+    }
+}

+ 91 - 0
fs-admin/src/main/java/com/fs/admin/interceptor/AdminTenantDataSourceInterceptor.java

@@ -0,0 +1,91 @@
+package com.fs.admin.interceptor;
+
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 内容审计模块数据源切换拦截器
+ * <p>
+ * 通过请求头 datasource-type 控制数据源切换:
+ * <ul>
+ *   <li><b>saas</b>  - 切换到指定租户的数据库(需配合请求参数 tenantId 指定租户ID)</li>
+ *   <li><b>admin</b> - 使用总库 MASTER 数据源(默认行为,不切库)</li>
+ * </ul>
+ * <p>
+ * 请求结束后自动清除租户数据源上下文,恢复到 MASTER。
+ */
+@Component
+public class AdminTenantDataSourceInterceptor extends HandlerInterceptorAdapter {
+
+    private static final Logger log = LoggerFactory.getLogger(AdminTenantDataSourceInterceptor.class);
+
+    /** 请求头: 数据源类型, saas=租户库, admin=总库 */
+    private static final String HEADER_DS_TYPE = "datasource-type";
+    /** 数据源类型: 租户库 */
+    private static final String DS_TYPE_SAAS = "saas";
+    /** 请求头: 租户ID */
+    private static final String HEADER_TENANT_ID = "tenant-id";
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+        // 读取请求头 datasource-type, 仅 saas 模式才切库
+        String dsType = request.getHeader(HEADER_DS_TYPE);
+        if (!DS_TYPE_SAAS.equals(dsType)) {
+            // admin 模式或不传该头, 保持总库 MASTER 数据源
+            return true;
+        }
+
+        // saas 模式: 优先从请求参数获取租户ID, 兜底从请求头获取
+        String tenantIdStr = request.getParameter("tenantId");
+        if (!StringUtils.hasText(tenantIdStr)) {
+            tenantIdStr = request.getHeader(HEADER_TENANT_ID);
+        }
+        if (!StringUtils.hasText(tenantIdStr)) {
+            log.warn("datasource-type=saas 但未传 tenantId 参数, 无法切库");
+            return true;
+        }
+
+        try {
+            Long tenantId = Long.valueOf(tenantIdStr);
+            TenantInfo tenant = tenantInfoService.getById(tenantId);
+            if (tenant != null) {
+                tenantDataSourceManager.switchTenant(tenant);
+                log.debug("切库成功: tenantId={}, tenantCode={}", tenantId, tenant.getTenantCode());
+            } else {
+                log.warn("租户不存在: tenantId={}", tenantId);
+            }
+        } catch (Exception e) {
+            log.error("切库失败: tenantId={}, error={}", tenantIdStr, e.getMessage());
+        }
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+                                Object handler, Exception ex) {
+        // 仅 saas 模式需要清理租户数据源上下文
+        String dsType = request.getHeader(HEADER_DS_TYPE);
+        if (DS_TYPE_SAAS.equals(dsType)) {
+            tenantDataSourceManager.clear();
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+}

+ 9 - 4
fs-admin/src/main/resources/logback.xml

@@ -68,10 +68,15 @@
 	<!-- Spring日志级别控制  -->
 	<!-- Spring日志级别控制  -->
 	<logger name="org.springframework" level="warn" />
 	<logger name="org.springframework" level="warn" />
 
 
-    <!-- log4j2.xml -->
-    <Logger name="com.fs.his.mapper" level="debug"/>
-    <Logger name="com.fs.company.mapper" level="debug"/>
-    <Logger name="org.apache.ibatis" level="debug"/>
+	<!-- MyBatis SQL调试日志 -->
+	<logger name="com.fs.his.mapper" level="debug" />
+	<logger name="com.fs.company.mapper" level="debug" />
+	<logger name="com.fs.admin.mapper" level="debug" />
+	<logger name="com.fs.tenant.mapper" level="debug" />
+	<logger name="com.fs.fee.mapper" level="debug" />
+	<logger name="org.apache.ibatis" level="debug" />
+	<logger name="com.baomidou.mybatisplus" level="debug" />
+	<logger name="com.fs.framework.datasource" level="debug" />
 
 
 
 
     <root level="info">
     <root level="info">

+ 15 - 15
fs-service/src/main/java/com/fs/admin/helper/AdminCrossTenantHelper.java

@@ -41,20 +41,20 @@ public class AdminCrossTenantHelper {
     /**
     /**
      * 获取过滤后的启用租户列表
      * 获取过滤后的启用租户列表
      *
      *
-     * @param companyId   指定租户ID(来自InlineTenantSelector),为null则不过滤
-     * @param companyName 租户名称模糊搜索,为null/空则不过滤
+     * @param tenantId   指定租户ID(来自InlineTenantSelector),为null则不过滤
+     * @param tenantName 租户名称模糊搜索,为null/空则不过滤
      * @return 过滤后的租户列表
      * @return 过滤后的租户列表
      */
      */
-    public List<TenantInfo> getFilteredTenants(Long companyId, String companyName) {
+    public List<TenantInfo> getFilteredTenants(Long tenantId, String tenantName) {
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         try {
         try {
             List<TenantInfo> tenants = tenantInfoService.selectTenantInfoList(new TenantInfo());
             List<TenantInfo> tenants = tenantInfoService.selectTenantInfoList(new TenantInfo());
             tenants.removeIf(t -> t.getStatus() == null || t.getStatus() != 1);
             tenants.removeIf(t -> t.getStatus() == null || t.getStatus() != 1);
-            if (companyId != null) {
-                tenants.removeIf(t -> !t.getId().equals(companyId));
+            if (tenantId != null) {
+                tenants.removeIf(t -> !t.getId().equals(tenantId));
             }
             }
-            if (companyName != null && !companyName.isEmpty()) {
-                String search = companyName.toLowerCase();
+            if (tenantName != null && !tenantName.isEmpty()) {
+                String search = tenantName.toLowerCase();
                 tenants.removeIf(t -> t.getTenantName() == null ||
                 tenants.removeIf(t -> t.getTenantName() == null ||
                         !t.getTenantName().toLowerCase().contains(search));
                         !t.getTenantName().toLowerCase().contains(search));
             }
             }
@@ -67,15 +67,15 @@ public class AdminCrossTenantHelper {
     /**
     /**
      * 跨租户查询并聚合结果(使用回调函数构建每租户SQL)
      * 跨租户查询并聚合结果(使用回调函数构建每租户SQL)
      *
      *
-     * @param companyId   指定租户ID(可选)
-     * @param companyName 租户名称模糊搜索(可选)
+     * @param tenantId   指定租户ID(可选)
+     * @param tenantName 租户名称模糊搜索(可选)
      * @param queryFunc   查询回调:(TenantInfo, JdbcTemplate) → 该租户的数据行
      * @param queryFunc   查询回调:(TenantInfo, JdbcTemplate) → 该租户的数据行
      * @return 聚合结果,每行自动附加 companyId / companyName / tenantCode
      * @return 聚合结果,每行自动附加 companyId / companyName / tenantCode
      */
      */
-    public List<Map<String, Object>> queryAcrossTenants(Long companyId, String companyName,
+    public List<Map<String, Object>> queryAcrossTenants(Long tenantId, String tenantName,
                                                         BiFunction<TenantInfo, JdbcTemplate, List<Map<String, Object>>> queryFunc) {
                                                         BiFunction<TenantInfo, JdbcTemplate, List<Map<String, Object>>> queryFunc) {
         List<Map<String, Object>> allList = new ArrayList<>();
         List<Map<String, Object>> allList = new ArrayList<>();
-        List<TenantInfo> tenants = getFilteredTenants(companyId, companyName);
+        List<TenantInfo> tenants = getFilteredTenants(tenantId, tenantName);
 
 
         for (TenantInfo tenant : tenants) {
         for (TenantInfo tenant : tenants) {
             try {
             try {
@@ -105,15 +105,15 @@ public class AdminCrossTenantHelper {
      * <p>
      * <p>
      * 便捷方法:直接传入SQL和参数,自动遍历每个租户执行。
      * 便捷方法:直接传入SQL和参数,自动遍历每个租户执行。
      *
      *
-     * @param companyId   指定租户ID(可选)
-     * @param companyName 租户名称模糊搜索(可选)
+     * @param tenantId   指定租户ID(可选)
+     * @param tenantName 租户名称模糊搜索(可选)
      * @param sql         查询SQL(SELECT ... FROM ... WHERE ...)
      * @param sql         查询SQL(SELECT ... FROM ... WHERE ...)
      * @param args        SQL参数
      * @param args        SQL参数
      * @return 聚合结果,每行自动附加 companyId / companyName / tenantCode
      * @return 聚合结果,每行自动附加 companyId / companyName / tenantCode
      */
      */
-    public List<Map<String, Object>> queryAcrossTenants(Long companyId, String companyName,
+    public List<Map<String, Object>> queryAcrossTenants(Long tenantId, String tenantName,
                                                         String sql, Object... args) {
                                                         String sql, Object... args) {
-        return queryAcrossTenants(companyId, companyName, (tenant, jdbc) -> {
+        return queryAcrossTenants(tenantId, tenantName, (tenant, jdbc) -> {
             if (args == null || args.length == 0) {
             if (args == null || args.length == 0) {
                 return jdbc.queryForList(sql);
                 return jdbc.queryForList(sql);
             }
             }