Преглед изворни кода

销售日志处理、cid绑定代码迁移等....

yjwang пре 1 дан
родитељ
комит
99a1808d96
16 измењених фајлова са 533 додато и 51 уклоњено
  1. 6 4
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java
  2. 4 4
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentAccountController.java
  3. 121 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java
  4. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java
  5. 3 3
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java
  6. 1 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  7. 1 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java
  8. 9 2
      fs-company/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  9. 23 0
      fs-company/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java
  10. 56 37
      fs-company/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  11. 38 0
      fs-framework/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java
  12. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  13. 18 0
      fs-service/src/main/java/com/fs/company/param/BindCidServerParam.java
  14. 78 0
      fs-service/src/main/java/com/fs/company/service/ICompanyAiWorkflowServerService.java
  15. 165 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyAiWorkflowServerServiceImpl.java
  16. 7 0
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

+ 6 - 4
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java

@@ -67,7 +67,7 @@ public class AiSipCallOutboundCdrController extends BaseController
      * 导出aiSIP手动外呼通话记录列表
      */
     @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:export')")
-    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.EXPORT)
+    @Log(title = "导出aiSIP手动外呼通话记录", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(AiSipCallOutboundCdr aiSipCallOutboundCdr)
     {
@@ -90,7 +90,7 @@ public class AiSipCallOutboundCdrController extends BaseController
      * 新增aiSIP手动外呼通话记录
      */
     @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:add')")
-    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.INSERT)
+    @Log(title = "新增aiSIP手动外呼通话记录", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody AiSipCallOutboundCdr aiSipCallOutboundCdr)
     {
@@ -101,7 +101,7 @@ public class AiSipCallOutboundCdrController extends BaseController
      * 修改aiSIP手动外呼通话记录
      */
     @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:edit')")
-    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.UPDATE)
+    @Log(title = "修改aiSIP手动外呼通话记录", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody AiSipCallOutboundCdr aiSipCallOutboundCdr)
     {
@@ -112,7 +112,7 @@ public class AiSipCallOutboundCdrController extends BaseController
      * 删除aiSIP手动外呼通话记录
      */
     @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:remove')")
-    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.DELETE)
+    @Log(title = " 删除aiSIP手动外呼通话记录", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable String[] ids)
     {
@@ -134,6 +134,7 @@ public class AiSipCallOutboundCdrController extends BaseController
      */
     @PostMapping("/add/custcallrecord")
     @ResponseBody
+    @Log(title = "保存手动外呼沟通记录", businessType = BusinessType.INSERT)
     public AjaxResult addCustcallrecord(@RequestBody CcCustInfo ccCustInfo)
     {
         return aiSipCallOutboundCdrService.addCustcallrecord(ccCustInfo);
@@ -145,6 +146,7 @@ public class AiSipCallOutboundCdrController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:manualPull')")
     @GetMapping("/manualPull")
+    @Log(title = "SIP外呼记录同步数据", businessType = BusinessType.OTHER)
     public AjaxResult manualPull()
     {
         log.info("开始拉取 人工外呼通话记录");

+ 4 - 4
fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentAccountController.java

@@ -152,7 +152,7 @@ public class CcLlmAgentAccountController extends BaseController
      * 新增保存机器人参数配置
      */
     @PreAuthorize("@ss.hasPermi('aicall:account:add')")
-    @Log(title = "机器人参数配置", businessType = BusinessType.INSERT)
+    @Log(title = "配置模型新增", businessType = BusinessType.INSERT)
     @PostMapping("/add")
     @ResponseBody
     public AjaxResult addSave(@RequestBody CcLlmAgentAccount ccLlmAgentAccount, HttpServletRequest request)
@@ -181,7 +181,7 @@ public class CcLlmAgentAccountController extends BaseController
                 }
             }
         }
-        
+
         // 新增模型
         int result = ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount);
 
@@ -199,7 +199,7 @@ public class CcLlmAgentAccountController extends BaseController
         if (result > 0 && companyId != null) {
             companyBindAiModelService.bindCompanyToModel(ccLlmAgentAccount.getId().longValue(), companyId);
         }
-        
+
         return toAjax(result);
     }
 
@@ -256,7 +256,7 @@ public class CcLlmAgentAccountController extends BaseController
      * 修改保存机器人参数配置
      */
     @PreAuthorize("@ss.hasPermi('aicall:account:edit')")
-    @Log(title = "机器人参数配置", businessType = BusinessType.UPDATE)
+    @Log(title = "配置模型修改", businessType = BusinessType.UPDATE)
     @PostMapping("/edit")
     @ResponseBody
     public AjaxResult editSave(@RequestBody CcLlmAgentAccount ccLlmAgentAccount)

+ 121 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java

@@ -0,0 +1,121 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.annotation.Log;
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyAiWorkflowServer;
+import com.fs.company.param.BindCidServerParam;
+import com.fs.company.service.ICompanyAiWorkflowServerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * cid服务Controller
+ * 
+ * @author fs
+ * @date 2026-02-26
+ */
+@RestController
+@RequestMapping("/company/cid/server")
+public class CompanyAiWorkflowServerController extends BaseController
+{
+    @Autowired
+    private ICompanyAiWorkflowServerService companyAiWorkflowServerService;
+
+    /**
+     * 查询cid服务列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        startPage();
+        List<CompanyAiWorkflowServer> list = companyAiWorkflowServerService.selectCompanyAiWorkflowServerList(companyAiWorkflowServer);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出cid服务列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:export')")
+    @Log(title = "cid服务", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        List<CompanyAiWorkflowServer> list = companyAiWorkflowServerService.selectCompanyAiWorkflowServerList(companyAiWorkflowServer);
+        ExcelUtil<CompanyAiWorkflowServer> util = new ExcelUtil<CompanyAiWorkflowServer>(CompanyAiWorkflowServer.class);
+        return util.exportExcel(list, "cid服务数据");
+    }
+
+    /**
+     * 获取cid服务详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(companyAiWorkflowServerService.selectCompanyAiWorkflowServerById(id));
+    }
+
+    /**
+     * 新增cid服务
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:add')")
+    @Log(title = "cid服务", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        return toAjax(companyAiWorkflowServerService.insertCompanyAiWorkflowServer(companyAiWorkflowServer));
+    }
+
+    /**
+     * 修改cid服务
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:edit')")
+    @Log(title = "cid服务", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        return toAjax(companyAiWorkflowServerService.updateCompanyAiWorkflowServer(companyAiWorkflowServer));
+    }
+
+    /**
+     * 删除cid服务
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:remove')")
+    @Log(title = "cid服务", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(companyAiWorkflowServerService.deleteCompanyAiWorkflowServerByIds(ids));
+    }
+
+    /**
+     * 绑定cid服务
+     * @param param
+     * @return
+     */
+    @PostMapping("/bindCidServer")
+    public R bindCidServer(@RequestBody BindCidServerParam param){
+        return companyAiWorkflowServerService.bindCidServer(param);
+    }
+
+    /**
+     * 解绑cid服务
+     * @param param
+     * @return
+     */
+    @PostMapping("/unbindCidServer")
+    public R unbindCidServer(@RequestBody BindCidServerParam param){
+        return companyAiWorkflowServerService.unbindCidServer(param);
+    }
+
+
+}

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java

@@ -73,7 +73,7 @@ public class CompanyInboundCallManageController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         List<CompanyInboundBind> companyInboundBinds = companyInboundBindMapper.selectIdsByCompanyId(loginUser.getUser().getCompanyId());
         if (null == companyInboundBinds || companyInboundBinds.isEmpty()) {
-            return getDataTable(null);
+            return getDataTable(new LinkedList<>());
         }
         List<Long> ids = companyInboundBinds.stream().map(companyInboundBind -> companyInboundBind.getInboundLlmAccountId()).collect(Collectors.toList());
         vo.setVisibleIds(ids);

+ 3 - 3
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java

@@ -73,7 +73,7 @@ public class CompanyVoiceRoboticCallBlacklistController extends BaseController
      * 新增黑名单
      */
     @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:add')")
-    @Log(title = "黑名单", businessType = BusinessType.INSERT)
+    @Log(title = "黑名单", businessType = BusinessType.UPDATE)
     @PostMapping
     public AjaxResult add(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
     {
@@ -87,7 +87,7 @@ public class CompanyVoiceRoboticCallBlacklistController extends BaseController
      * */
     @PutMapping
     @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:update')")
-    @Log(title = "修改黑名单信息", businessType = BusinessType.INSERT)
+    @Log(title = "修改黑名单信息", businessType = BusinessType.UPDATE)
     public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
     {
         return toAjax(companyVoiceRoboticCallBlacklistService.updateCompanyVoiceRoboticCallBlacklist(companyVoiceRoboticCallBlacklist));
@@ -104,7 +104,7 @@ public class CompanyVoiceRoboticCallBlacklistController extends BaseController
         return toAjax(companyVoiceRoboticCallBlacklistService.deleteCompanyVoiceRoboticCallBlacklistByIds(blacklistIds));
     }
 
-    @Log(title = "修改黑名单状态", businessType = BusinessType.UPDATE)
+    @Log(title = "修改黑名单失效或启用状态", businessType = BusinessType.UPDATE)
     @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:update')")
     @PutMapping("/changeStatus")
     public AjaxResult changeStatus(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist) {

+ 1 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -274,6 +274,7 @@ public class CompanyVoiceRoboticController extends BaseController
      * 启动任务
      */
     @GetMapping("/taskRun")
+    @Log(title = "启动机器人外呼任务", businessType = BusinessType.OTHER)
     public R taskRun(Long id){
 
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());

+ 1 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java

@@ -166,6 +166,7 @@ public class CompanyWorkflowController extends BaseController {
      * 修改工作流绑定的销售
      */
     @PostMapping("/updateWorkflowBindCompanyUser")
+    @Log(title = "绑定销售", businessType = BusinessType.GRANT)
     public AjaxResult updateWorkflowBindCompanyUser(@RequestBody CompanyWorkflowUpdateBindWCParam param) {
         return companyWorkflowService.updateWorkflowBindCompanyUser(param);
     }

+ 9 - 2
fs-company/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

@@ -40,6 +40,9 @@ public class DataSourceAspect
     {
         DataSource dataSource = getDataSource(point);
 
+        // 进入前保存当前数据源key,结束后恢复(而不是无脑clear)
+        String previousDs = DynamicDataSourceContextHolder.getDataSourceType();
+
         if (StringUtils.isNotNull(dataSource))
         {
             DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
@@ -51,8 +54,12 @@ public class DataSourceAspect
         }
         finally
         {
-            // 销毁数据源 在执行方法之后
-            DynamicDataSourceContextHolder.clearDataSourceType();
+            // 恢复之前的数据源,而不是直接清空,避免外层租户上下文丢失
+            if (previousDs != null) {
+                DynamicDataSourceContextHolder.setDataSourceType(previousDs);
+            } else {
+                DynamicDataSourceContextHolder.clearDataSourceType();
+            }
         }
     }
 

+ 23 - 0
fs-company/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

@@ -14,6 +14,7 @@ import javax.sql.DataSource;
 import java.lang.reflect.Field;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
 
 @Component
 public class TenantDataSourceManager {
@@ -106,6 +107,28 @@ public class TenantDataSourceManager {
         }
     }
 
+    /**
+     * 在指定租户的数据源上下文下执行业务代码块,业务执行完后强制把 ThreadLocal 的数据源切回该租户。
+     * 主要用于 Controller 写接口:Mapper 的 @DataSource 切面在 finally 中会 clear ThreadLocal,
+     * 导致后续 @Log 切面触发异步日志时拿不到租户 key,回退到主库。
+     * 通过本方法 finally 重新 ensureSwitchByTenantId,可以保证异步日志写入租户库。
+     *
+     * @param tenantId 当前登录用户所属租户ID(可为 null,null 时回退主库)
+     * @param action   业务代码块
+     * @param <T>      业务返回值类型
+     */
+    public <T> T runAsCurrentTenant(Long tenantId, Supplier<T> action) {
+        try {
+            return action.get();
+        } finally {
+            if (tenantId != null) {
+                ensureSwitchByTenantId(tenantId);
+            } else {
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+            }
+        }
+    }
+
     /**
      * 创建租户数据源(MySQL + Druid)
      */

+ 56 - 37
fs-company/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

@@ -10,6 +10,7 @@ import com.fs.company.domain.CompanyLogininfor;
 import com.fs.company.domain.CompanyOperLog;
 import com.fs.company.service.ICompanyLogininforService;
 import com.fs.company.service.ICompanyOperLogService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
 import eu.bitwalker.useragentutils.UserAgent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -40,45 +41,54 @@ public class AsyncFactory
     {
         final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
         final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        // 在主线程捕获当前数据源key(租户标识),避免异步线程ThreadLocal丢失导致写入主库
+        final String dsKey = DynamicDataSourceContextHolder.getDataSourceType();
         return new TimerTask()
         {
             @Override
             public void run()
             {
-                String address = AddressUtils.getRealAddressByIP(ip);
-                StringBuilder s = new StringBuilder();
-                s.append(LogUtils.getBlock(ip));
-                s.append(address);
-                s.append(LogUtils.getBlock(username));
-                s.append(LogUtils.getBlock(status));
-                s.append(LogUtils.getBlock(message));
-                // 打印信息到日志
-                sys_user_logger.info(s.toString(), args);
-                // 获取客户端操作系统
-                String os = userAgent.getOperatingSystem().getName();
-                // 获取客户端浏览器
-                String browser = userAgent.getBrowser().getName();
-                // 封装对象
-                CompanyLogininfor logininfor = new CompanyLogininfor();
-                logininfor.setCompanyId(companyId);
-                logininfor.setUserName(username);
-                logininfor.setIpaddr(ip);
-                logininfor.setLoginLocation(address);
-                logininfor.setBrowser(browser);
-                logininfor.setOs(os);
-                logininfor.setMsg(message);
-                // 日志状态
-                if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
-                {
-                    logininfor.setStatus(Constants.SUCCESS);
+                try {
+                    if (dsKey != null) {
+                        DynamicDataSourceContextHolder.setDataSourceType(dsKey);
+                    }
+                    String address = AddressUtils.getRealAddressByIP(ip);
+                    StringBuilder s = new StringBuilder();
+                    s.append(LogUtils.getBlock(ip));
+                    s.append(address);
+                    s.append(LogUtils.getBlock(username));
+                    s.append(LogUtils.getBlock(status));
+                    s.append(LogUtils.getBlock(message));
+                    // 打印信息到日志
+                    sys_user_logger.info(s.toString(), args);
+                    // 获取客户端操作系统
+                    String os = userAgent.getOperatingSystem().getName();
+                    // 获取客户端浏览器
+                    String browser = userAgent.getBrowser().getName();
+                    // 封装对象
+                    CompanyLogininfor logininfor = new CompanyLogininfor();
+                    logininfor.setCompanyId(companyId);
+                    logininfor.setUserName(username);
+                    logininfor.setIpaddr(ip);
+                    logininfor.setLoginLocation(address);
+                    logininfor.setBrowser(browser);
+                    logininfor.setOs(os);
+                    logininfor.setMsg(message);
+                    // 日志状态
+                    if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
+                    {
+                        logininfor.setStatus(Constants.SUCCESS);
+                    }
+                    else if (Constants.LOGIN_FAIL.equals(status))
+                    {
+                        logininfor.setStatus(Constants.FAIL);
+                    }
+                    logininfor.setLoginTime(new Date());
+                    // 插入数据
+                    SpringUtils.getBean(ICompanyLogininforService.class).insertCompanyLogininfor(logininfor);
+                } finally {
+                    DynamicDataSourceContextHolder.clearDataSourceType();
                 }
-                else if (Constants.LOGIN_FAIL.equals(status))
-                {
-                    logininfor.setStatus(Constants.FAIL);
-                }
-                logininfor.setLoginTime(new Date());
-                // 插入数据
-                SpringUtils.getBean(ICompanyLogininforService.class).insertCompanyLogininfor(logininfor);
             }
         };
     }
@@ -91,15 +101,24 @@ public class AsyncFactory
      */
     public static TimerTask recordOper(final CompanyOperLog operLog)
     {
+        // 在主线程捕获当前数据源key(租户标识),避免异步线程ThreadLocal丢失导致写入主库
+        final String dsKey = DynamicDataSourceContextHolder.getDataSourceType();
         return new TimerTask()
         {
             @Override
             public void run()
             {
-                // 远程查询操作地点
-                operLog.setOperTime(new Date());
-                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
-                SpringUtils.getBean(ICompanyOperLogService.class).insertCompanyOperLog(operLog);
+                try {
+                    if (dsKey != null) {
+                        DynamicDataSourceContextHolder.setDataSourceType(dsKey);
+                    }
+                    // 远程查询操作地点
+                    operLog.setOperTime(new Date());
+                    operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+                    SpringUtils.getBean(ICompanyOperLogService.class).insertCompanyOperLog(operLog);
+                } finally {
+                    DynamicDataSourceContextHolder.clearDataSourceType();
+                }
             }
         };
     }

+ 38 - 0
fs-framework/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

@@ -14,6 +14,7 @@ import javax.sql.DataSource;
 import java.lang.reflect.Field;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
 
 @Component
 public class TenantDataSourceManager {
@@ -110,6 +111,43 @@ public class TenantDataSourceManager {
         }
     }
 
+    /**
+     * 在指定租户的数据源上下文下执行业务代码块,业务执行完后强制把 ThreadLocal 的数据源切回该租户。
+     * 主要用于 Controller 写接口:Mapper 的 @DataSource 切面在 finally 中会 clear ThreadLocal,
+     * 导致后续 @Log 切面触发异步日志时拿不到租户 key,回退到主库。
+     * 通过本方法 finally 重新 ensureSwitchByTenantId,可以保证异步日志写入租户库。
+     *
+     * @param tenantId 当前登录用户所属租户ID(可为 null,null 时回退主库)
+     * @param action   业务代码块
+     * @param <T>      业务返回值类型
+     */
+    public <T> T runAsCurrentTenant(Long tenantId, Supplier<T> action) {
+        try {
+            return action.get();
+        } finally {
+            if (tenantId != null) {
+                ensureSwitchByTenantId(tenantId);
+            } else {
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+            }
+        }
+    }
+
+    /**
+     * {@link #runAsCurrentTenant(Long, Supplier)} 的无返回值重载。
+     */
+    public void runAsCurrentTenant(Long tenantId, Runnable action) {
+        try {
+            action.run();
+        } finally {
+            if (tenantId != null) {
+                ensureSwitchByTenantId(tenantId);
+            } else {
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+            }
+        }
+    }
+
     /**
      * 创建租户数据源(MySQL + Druid)
      */

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -372,4 +372,6 @@ public interface CompanyUserMapper
     CompanyUserAnalyseVO selectCompanyUserNewUserCount(@Param("companyUserId") Long id);
 
     CompanyUserAnalyseVO selectCompanyUserPhoneLogCount(@Param("companyUserId") Long id);
+
+    int unbindCidServer(@Param("companyUserId") Long companyUserId);
 }

+ 18 - 0
fs-service/src/main/java/com/fs/company/param/BindCidServerParam.java

@@ -0,0 +1,18 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/2/26 18:34
+ * @description
+ */
+
+@Data
+public class BindCidServerParam {
+
+    private Long userId;
+
+    private Long companyId;
+
+}

+ 78 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyAiWorkflowServerService.java

@@ -0,0 +1,78 @@
+package com.fs.company.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.company.domain.CompanyAiWorkflowServer;
+import com.fs.company.param.BindCidServerParam;
+
+import java.util.List;
+
+/**
+ * cid服务Service接口
+ * 
+ * @author fs
+ * @date 2026-02-26
+ */
+public interface ICompanyAiWorkflowServerService extends IService<CompanyAiWorkflowServer>{
+    /**
+     * 查询cid服务
+     * 
+     * @param id cid服务主键
+     * @return cid服务
+     */
+    CompanyAiWorkflowServer selectCompanyAiWorkflowServerById(Long id);
+
+    /**
+     * 查询cid服务列表
+     * 
+     * @param companyAiWorkflowServer cid服务
+     * @return cid服务集合
+     */
+    List<CompanyAiWorkflowServer> selectCompanyAiWorkflowServerList(CompanyAiWorkflowServer companyAiWorkflowServer);
+
+    /**
+     * 新增cid服务
+     * 
+     * @param companyAiWorkflowServer cid服务
+     * @return 结果
+     */
+    int insertCompanyAiWorkflowServer(CompanyAiWorkflowServer companyAiWorkflowServer);
+
+    /**
+     * 修改cid服务
+     * 
+     * @param companyAiWorkflowServer cid服务
+     * @return 结果
+     */
+    int updateCompanyAiWorkflowServer(CompanyAiWorkflowServer companyAiWorkflowServer);
+
+    /**
+     * 批量删除cid服务
+     * 
+     * @param ids 需要删除的cid服务主键集合
+     * @return 结果
+     */
+    int deleteCompanyAiWorkflowServerByIds(Long[] ids);
+
+    /**
+     * 删除cid服务信息
+     * 
+     * @param id cid服务主键
+     * @return 结果
+     */
+    int deleteCompanyAiWorkflowServerById(Long id);
+
+    /**
+     * 绑定cid服务
+     * @param param
+     * @return
+     */
+    R bindCidServer( BindCidServerParam param);
+
+    /**
+     * 解绑cid服务
+     * @param param
+     * @return
+     */
+    R unbindCidServer( BindCidServerParam param);
+}

+ 165 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyAiWorkflowServerServiceImpl.java

@@ -0,0 +1,165 @@
+package com.fs.company.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyAiWorkflowServer;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanyAiWorkflowServerMapper;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.param.BindCidServerParam;
+import com.fs.company.service.ICompanyAiWorkflowServerService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Random;
+
+/**
+ * cid服务Service业务层处理
+ *
+ * @author fs
+ * @date 2026-02-26
+ */
+@Service
+@Slf4j
+public class CompanyAiWorkflowServerServiceImpl extends ServiceImpl<CompanyAiWorkflowServerMapper, CompanyAiWorkflowServer> implements ICompanyAiWorkflowServerService {
+
+    @Autowired
+    CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
+    @Autowired
+    CompanyUserMapper companyUserMapper;
+
+    /**
+     * 查询cid服务
+     *
+     * @param id cid服务主键
+     * @return cid服务
+     */
+    @Override
+    public CompanyAiWorkflowServer selectCompanyAiWorkflowServerById(Long id) {
+        return baseMapper.selectCompanyAiWorkflowServerById(id);
+    }
+
+    /**
+     * 查询cid服务列表
+     *
+     * @param companyAiWorkflowServer cid服务
+     * @return cid服务
+     */
+    @Override
+    public List<CompanyAiWorkflowServer> selectCompanyAiWorkflowServerList(CompanyAiWorkflowServer companyAiWorkflowServer) {
+        return baseMapper.selectCompanyAiWorkflowServerList(companyAiWorkflowServer);
+    }
+
+    /**
+     * 新增cid服务
+     *
+     * @param companyAiWorkflowServer cid服务
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyAiWorkflowServer(CompanyAiWorkflowServer companyAiWorkflowServer) {
+        companyAiWorkflowServer.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCompanyAiWorkflowServer(companyAiWorkflowServer);
+    }
+
+    /**
+     * 修改cid服务
+     *
+     * @param companyAiWorkflowServer cid服务
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyAiWorkflowServer(CompanyAiWorkflowServer companyAiWorkflowServer) {
+        return baseMapper.updateCompanyAiWorkflowServer(companyAiWorkflowServer);
+    }
+
+    /**
+     * 批量删除cid服务
+     *
+     * @param ids 需要删除的cid服务主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyAiWorkflowServerByIds(Long[] ids) {
+        return baseMapper.deleteCompanyAiWorkflowServerByIds(ids);
+    }
+
+    /**
+     * 删除cid服务信息
+     *
+     * @param id cid服务主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyAiWorkflowServerById(Long id) {
+        return baseMapper.deleteCompanyAiWorkflowServerById(id);
+    }
+
+    /**
+     * 绑定cid服务
+     *
+     * @param param
+     * @return
+     */
+    @Override
+    @Transactional
+    public R bindCidServer(BindCidServerParam param) {
+        try {
+            List<CompanyAiWorkflowServer> availableServerList = companyAiWorkflowServerMapper.getAvailableServerList();
+            if (null != availableServerList && availableServerList.size() > 0) {
+                Random random = new Random();
+                int randomIndex = random.nextInt(availableServerList.size());
+                CompanyAiWorkflowServer randomServer = availableServerList.get(randomIndex);
+                CompanyAiWorkflowServer server = companyAiWorkflowServerMapper.selectServerForUpdate(randomServer.getId());
+                if (server != null) {
+                    CompanyUser updateUser = new CompanyUser();
+                    updateUser.setUserId(param.getUserId());
+                    updateUser.setCidServerId(server.getId());
+                    companyUserMapper.updateCompanyUser(updateUser);
+                    server.setCount(server.getCount() - 1);
+                    companyAiWorkflowServerMapper.updateCompanyAiWorkflowServer(server);
+                    return R.ok("绑定成功");
+                } else {
+                    return R.error("绑定失败,请稍后重试");
+                }
+            }
+            return R.error("绑定失败,没有空闲服务");
+        } catch (Exception ex) {
+            log.error("bindCidServer内部错误", ex);
+            throw new RuntimeException("内部错误");
+        }
+    }
+
+    /**
+     * 解绑cid服务
+     * @param param
+     * @return
+     */
+    @Override
+    @Transactional
+    public R unbindCidServer(BindCidServerParam param) {
+        try {
+            CompanyUser companyUser = companyUserMapper.selectCompanyUserByCompanyUserId(param.getUserId());
+            if (null != companyUser && companyUser.getCidServerId() != null) {
+                CompanyAiWorkflowServer server = companyAiWorkflowServerMapper.selectServerUnbindForUpdate(companyUser.getCidServerId());
+                if (null != server) {
+                    companyUserMapper.unbindCidServer(companyUser.getUserId());
+                    server.setCount(server.getCount() + 1);
+                    companyAiWorkflowServerMapper.updateCompanyAiWorkflowServer(server);
+                    return R.ok("解绑成功");
+                } else {
+                    return R.error("解除绑定失败,请稍后重试");
+                }
+            } else {
+                return R.error("解除绑定失败,请稍后重试");
+            }
+        } catch (Exception ex) {
+            log.error("bindCidServer内部错误", ex);
+            throw new RuntimeException("内部错误");
+        }
+    }
+}

+ 7 - 0
fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

@@ -326,6 +326,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="doctorId != null">`doctor_id` = #{doctorId},</if>
             <if test="unionId != null">`union_id` = #{unionId},</if>
             <if test="analyseData != null">`analyse_data` = #{analyseData},</if>
+            <if test="cidServerId != null">`cid_server_id` = #{cidServerId},</if>
         </trim>
         where user_id = #{userId}
     </update>
@@ -827,4 +828,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ]]>
     </select>
 
+    <update id="unbindCidServer" parameterType="java.lang.Long" >
+        update company_user
+        set cid_server_id = null
+        where user_id = #{companyUserId}
+    </update>
+
 </mapper>