فهرست منبع

cid服务管理

lmx 3 ساعت پیش
والد
کامیت
26cf1b5286
21فایلهای تغییر یافته به همراه785 افزوده شده و 23 حذف شده
  1. 175 0
      fs-admin/src/main/java/com/fs/admin/controller/CIdServerManageController.java
  2. 7 2
      fs-ai-call-task/src/main/java/com/fs/app/task/TenantTaskRunner.java
  3. 2 0
      fs-ai-call-task/src/main/resources/application-common.yml
  4. 3 12
      fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java
  5. 7 2
      fs-cid-workflow/src/main/java/com/fs/app/task/TenantTaskRunner.java
  6. 2 0
      fs-cid-workflow/src/main/resources/application-common.yml
  7. 13 0
      fs-service/src/main/java/com/fs/admin/param/AdjustCidServerQuotaParam.java
  8. 15 0
      fs-service/src/main/java/com/fs/admin/param/AllocateCidServerQuotaParam.java
  9. 13 0
      fs-service/src/main/java/com/fs/admin/param/TenantServiceConfigQueryParam.java
  10. 16 0
      fs-service/src/main/java/com/fs/admin/service/IAdminCidServerQuotaService.java
  11. 209 0
      fs-service/src/main/java/com/fs/admin/service/impl/AdminCidServerQuotaServiceImpl.java
  12. 31 0
      fs-service/src/main/java/com/fs/admin/vo/TenantCidServerQuotaVO.java
  13. 33 0
      fs-service/src/main/java/com/fs/admin/vo/TenantServiceConfigVO.java
  14. 4 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowServerMapper.java
  15. 13 0
      fs-service/src/main/java/com/fs/wxcid/mapper/TenantServiceConfigMapper.java
  16. 20 0
      fs-service/src/main/java/com/fs/wxcid/service/ITenantServiceConfigService.java
  17. 122 0
      fs-service/src/main/java/com/fs/wxcid/service/impl/TenantServiceConfigServiceImpl.java
  18. 10 0
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowServerMapper.xml
  19. 84 3
      fs-service/src/main/resources/mapper/wxcid/TenantServiceConfigMapper.xml
  20. 4 4
      fs-wx-task/src/main/java/com/fs/app/task/WxTask.java
  21. 2 0
      fs-wx-task/src/main/resources/application-common.yml

+ 175 - 0
fs-admin/src/main/java/com/fs/admin/controller/CIdServerManageController.java

@@ -0,0 +1,175 @@
+package com.fs.admin.controller;
+
+import com.fs.admin.param.AdjustCidServerQuotaParam;
+import com.fs.admin.param.AllocateCidServerQuotaParam;
+import com.fs.admin.param.TenantServiceConfigQueryParam;
+import com.fs.admin.service.IAdminCidServerQuotaService;
+import com.fs.admin.vo.TenantServiceConfigVO;
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.wxcid.domain.TenantServiceConfig;
+import com.fs.wxcid.service.ITenantServiceConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * CID 服务管理(实例调度 + 租户名额)
+ *
+ * @author MixLiu
+ * @date 2026/6/8
+ */
+@RestController
+@RequestMapping("/admin/cidServerManage")
+public class CIdServerManageController extends BaseController {
+
+    @Autowired
+    private ITenantServiceConfigService tenantServiceConfigService;
+
+    @Autowired
+    private IAdminCidServerQuotaService adminCidServerQuotaService;
+
+    /**
+     * 服务实例调度列表
+     */
+    @GetMapping("/serviceConfig/list")
+    public TableDataInfo serviceConfigList(TenantServiceConfigQueryParam param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        startPage();
+        List<TenantServiceConfigVO> list = tenantServiceConfigService.selectList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 服务实例调度详情
+     */
+    @GetMapping("/serviceConfig/{id}")
+    public AjaxResult getServiceConfig(@PathVariable Long id) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        TenantServiceConfigVO data = tenantServiceConfigService.selectById(id);
+        if (data == null) {
+            return AjaxResult.error("记录不存在");
+        }
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 新增服务实例调度
+     */
+    @Log(title = "CID服务实例调度", businessType = BusinessType.INSERT)
+    @PostMapping("/serviceConfig")
+    public AjaxResult addServiceConfig(@RequestBody TenantServiceConfig config) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (StringUtils.isEmpty(config.getServiceMarker())) {
+            return AjaxResult.error("请填写服务标识");
+        }
+        if (config.getServerType() == null) {
+            return AjaxResult.error("请选择服务类型");
+        }
+        if (StringUtils.isEmpty(config.getTenantIds())) {
+            return AjaxResult.error("请选择负责租户");
+        }
+        config.setCreateBy(getUsername());
+        return toAjax(tenantServiceConfigService.insert(config));
+    }
+
+    /**
+     * 修改服务实例调度
+     */
+    @Log(title = "CID服务实例调度", businessType = BusinessType.UPDATE)
+    @PutMapping("/serviceConfig")
+    public AjaxResult editServiceConfig(@RequestBody TenantServiceConfig config) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (config.getId() == null) {
+            return AjaxResult.error("缺少记录ID");
+        }
+        if (StringUtils.isEmpty(config.getServiceMarker())) {
+            return AjaxResult.error("请填写服务标识");
+        }
+        if (config.getServerType() == null) {
+            return AjaxResult.error("请选择服务类型");
+        }
+        if (StringUtils.isEmpty(config.getTenantIds())) {
+            return AjaxResult.error("请选择负责租户");
+        }
+        config.setUpdateBy(getUsername());
+        return toAjax(tenantServiceConfigService.update(config));
+    }
+
+    /**
+     * 删除服务实例调度
+     */
+    @Log(title = "CID服务实例调度", businessType = BusinessType.DELETE)
+    @DeleteMapping("/serviceConfig/{ids}")
+    public AjaxResult removeServiceConfig(@PathVariable Long[] ids) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        return toAjax(tenantServiceConfigService.deleteByIds(ids));
+    }
+
+    /**
+     * 批量查询租户 CID 名额
+     */
+    @PostMapping("/quota/list")
+    public AjaxResult quotaList(@RequestBody Map<String, Object> body) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        List<Long> tenantIds = parseIdList(body.get("tenantIds"));
+        if (tenantIds.isEmpty()) {
+            return AjaxResult.error("请选择租户");
+        }
+        try {
+            return AjaxResult.success(adminCidServerQuotaService.listQuotaByTenantIds(tenantIds));
+        } catch (IllegalArgumentException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 分配租户 CID 名额
+     */
+    @Log(title = "分配租户CID名额", businessType = BusinessType.UPDATE)
+    @PostMapping("/quota/allocate")
+    public AjaxResult allocateQuota(@RequestBody AllocateCidServerQuotaParam param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            return toAjax(adminCidServerQuotaService.allocateQuota(param));
+        } catch (IllegalArgumentException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 调整租户 CID 名额
+     */
+    @Log(title = "调整租户CID名额", businessType = BusinessType.UPDATE)
+    @PutMapping("/quota/adjust")
+    public AjaxResult adjustQuota(@RequestBody AdjustCidServerQuotaParam param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            return toAjax(adminCidServerQuotaService.adjustQuota(param));
+        } catch (IllegalArgumentException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    private List<Long> parseIdList(Object raw) {
+        List<Long> ids = new ArrayList<>();
+        if (raw instanceof List) {
+            for (Object item : (List<?>) raw) {
+                if (item != null) {
+                    ids.add(Long.valueOf(item.toString()));
+                }
+            }
+        }
+        return ids.stream().distinct().collect(Collectors.toList());
+    }
+}

+ 7 - 2
fs-ai-call-task/src/main/java/com/fs/app/task/TenantTaskRunner.java

@@ -190,9 +190,14 @@ public class TenantTaskRunner {
             log.info("[SaaS Task] 定时任务切换数据源和Redis dataSource={}, tenantId={}, tenantCode={}, task={}",
                     dataSourceKey, tenant.getId(), tenant.getTenantCode(), taskName != null ? taskName : "");
 
-            // 加载租户项目配置(安全解析,防止非法JSON导致级联崩溃)
+            // 加载租户项目配置
             SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
-            ProjectConfig.safeLoadTenantConfigFromValue(cfg != null ? cfg.getConfigValue() : null);
+            if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
+                TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
+            } else {
+                TenantConfigContext.set(null);
+            }
+            ProjectConfig.loadTenantConfigsFromContext();
 
             // 执行租户任务
             action.accept(tenant);

+ 2 - 0
fs-ai-call-task/src/main/resources/application-common.yml

@@ -152,4 +152,6 @@ hsy:
   role_access_key: AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM
   role_secret_key: T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
   role_trn: trn:iam::2114522511:role/hylj
+# 配置了服务标记后,请在tenant_service_config 中配置服务应用租户ids信息
+tenant-service-marker: aiCall00
 

+ 3 - 12
fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java

@@ -24,6 +24,9 @@ public class CidTask {
     @Autowired
     private CidWorkflowTaskService cidWorkflowTaskService;
 
+    @Autowired
+    private OutboundRetryTaskService outboundRetryTaskService;
+
     /** SaaS 模式:为 true 时各定时任务按租户遍历执行;为 false 时保持原单库执行(私有化部署) */
     @Value("${saas.task.enabled:true}")
     private boolean saasTaskEnabled;
@@ -31,9 +34,6 @@ public class CidTask {
     @Autowired
     private TenantTaskRunner tenantTaskRunner;
 
-    @Autowired
-    private OutboundRetryTaskService outboundRetryTaskService;
-
     /**
      * 扫描当前分组下就绪任务,并开启执行
      */
@@ -59,15 +59,6 @@ public class CidTask {
 
     }
 
-    /**
-     * 扫描服务定时任务执行
-     */
-//    @Scheduled(cron = "0 0/1 * * * ?")
-//    public void runContinueTask() {
-//        cidWorkflowTaskService.runContinueTask();
-//    }
-
-
     /**
      * 回扫超时认领任务 - 每5分钟执行一次
      * 将卡在认领态(RUNNING)且长时间未流转的任务重置为失败,兜底进程重启/线程池拒绝导致的永久卡死

+ 7 - 2
fs-cid-workflow/src/main/java/com/fs/app/task/TenantTaskRunner.java

@@ -190,9 +190,14 @@ public class TenantTaskRunner {
             log.info("[SaaS Task] 定时任务切换数据源和Redis dataSource={}, tenantId={}, tenantCode={}, task={}",
                     dataSourceKey, tenant.getId(), tenant.getTenantCode(), taskName != null ? taskName : "");
 
-            // 加载租户项目配置(安全解析,防止非法JSON导致级联崩溃)
+            // 加载租户项目配置
             SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
-            ProjectConfig.safeLoadTenantConfigFromValue(cfg != null ? cfg.getConfigValue() : null);
+            if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
+                TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
+            } else {
+                TenantConfigContext.set(null);
+            }
+            ProjectConfig.loadTenantConfigsFromContext();
 
             // 执行租户任务
             action.accept(tenant);

+ 2 - 0
fs-cid-workflow/src/main/resources/application-common.yml

@@ -152,4 +152,6 @@ hsy:
   role_access_key: AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM
   role_secret_key: T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
   role_trn: trn:iam::2114522511:role/hylj
+# 配置了服务标记后,请在tenant_service_config 中配置服务应用租户ids信息
+tenant-service-marker: cidWorkflow00
 

+ 13 - 0
fs-service/src/main/java/com/fs/admin/param/AdjustCidServerQuotaParam.java

@@ -0,0 +1,13 @@
+package com.fs.admin.param;
+
+import lombok.Data;
+
+@Data
+public class AdjustCidServerQuotaParam {
+
+    private Long tenantId;
+
+    private Long serverId;
+
+    private Integer totalCount;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/admin/param/AllocateCidServerQuotaParam.java

@@ -0,0 +1,15 @@
+package com.fs.admin.param;
+
+import lombok.Data;
+
+@Data
+public class AllocateCidServerQuotaParam {
+
+    private Long tenantId;
+
+    private String title;
+
+    private Integer groupNo;
+
+    private Integer allocateCount;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/admin/param/TenantServiceConfigQueryParam.java

@@ -0,0 +1,13 @@
+package com.fs.admin.param;
+
+import lombok.Data;
+
+@Data
+public class TenantServiceConfigQueryParam {
+
+    private String serviceMarker;
+
+    private Integer serverType;
+
+    private Long tenantId;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/admin/service/IAdminCidServerQuotaService.java

@@ -0,0 +1,16 @@
+package com.fs.admin.service;
+
+import com.fs.admin.param.AdjustCidServerQuotaParam;
+import com.fs.admin.param.AllocateCidServerQuotaParam;
+import com.fs.admin.vo.TenantCidServerQuotaVO;
+
+import java.util.List;
+
+public interface IAdminCidServerQuotaService {
+
+    List<TenantCidServerQuotaVO> listQuotaByTenantIds(List<Long> tenantIds);
+
+    int allocateQuota(AllocateCidServerQuotaParam param);
+
+    int adjustQuota(AdjustCidServerQuotaParam param);
+}

+ 209 - 0
fs-service/src/main/java/com/fs/admin/service/impl/AdminCidServerQuotaServiceImpl.java

@@ -0,0 +1,209 @@
+package com.fs.admin.service.impl;
+
+import com.fs.admin.param.AdjustCidServerQuotaParam;
+import com.fs.admin.param.AllocateCidServerQuotaParam;
+import com.fs.admin.service.IAdminCidServerQuotaService;
+import com.fs.admin.vo.TenantCidServerQuotaVO;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyAiWorkflowServer;
+import com.fs.company.mapper.CompanyAiWorkflowServerMapper;
+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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+@Service
+public class AdminCidServerQuotaServiceImpl implements IAdminCidServerQuotaService {
+
+    private static final int MAX_TENANT_QUERY_SIZE = 10;
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
+
+    @Override
+    public List<TenantCidServerQuotaVO> listQuotaByTenantIds(List<Long> tenantIds) {
+        if (CollectionUtils.isEmpty(tenantIds)) {
+            return new ArrayList<>();
+        }
+        if (tenantIds.size() > MAX_TENANT_QUERY_SIZE) {
+            throw new IllegalArgumentException("\u4e00\u6b21\u6700\u591a\u67e5\u8be210\u4e2a\u79df\u6237");
+        }
+        Map<Long, TenantInfo> tenantMap = loadTenantMap(tenantIds);
+        List<TenantCidServerQuotaVO> result = new ArrayList<>();
+        for (Long tenantId : tenantIds) {
+            TenantInfo tenant = tenantMap.get(tenantId);
+            if (tenant == null) {
+                continue;
+            }
+            runInTenant(tenantId, () -> {
+                List<CompanyAiWorkflowServer> servers = companyAiWorkflowServerMapper
+                        .selectCompanyAiWorkflowServerList(new CompanyAiWorkflowServer());
+                if (servers == null) {
+                    return;
+                }
+                for (CompanyAiWorkflowServer server : servers) {
+                    result.add(buildQuotaVo(tenant, server));
+                }
+            });
+        }
+        return result;
+    }
+
+    @Override
+    public int allocateQuota(AllocateCidServerQuotaParam param) {
+        validateAllocateParam(param);
+        return runInTenant(param.getTenantId(), () -> {
+            CompanyAiWorkflowServer existing = companyAiWorkflowServerMapper.selectByGroupNo(param.getGroupNo());
+            if (existing != null) {
+                int addCount = param.getAllocateCount();
+                existing.setTotalCount(safeInt(existing.getTotalCount()) + addCount);
+                existing.setCount(safeInt(existing.getCount()) + addCount);
+                if (StringUtils.isNotEmpty(param.getTitle())) {
+                    existing.setTitle(param.getTitle());
+                }
+                return companyAiWorkflowServerMapper.updateCompanyAiWorkflowServer(existing);
+            }
+            CompanyAiWorkflowServer server = new CompanyAiWorkflowServer();
+            server.setTitle(param.getTitle());
+            server.setGroupNo(param.getGroupNo());
+            server.setTotalCount(param.getAllocateCount());
+            server.setCount(param.getAllocateCount());
+            server.setCreateTime(DateUtils.getNowDate());
+            return companyAiWorkflowServerMapper.insertCompanyAiWorkflowServer(server);
+        });
+    }
+
+    @Override
+    public int adjustQuota(AdjustCidServerQuotaParam param) {
+        if (param == null || param.getTenantId() == null || param.getServerId() == null || param.getTotalCount() == null) {
+            throw new IllegalArgumentException("\u53c2\u6570\u4e0d\u5b8c\u6574");
+        }
+        if (param.getTotalCount() < 0) {
+            throw new IllegalArgumentException("\u603b\u540d\u989d\u4e0d\u80fd\u5c0f\u4e8e0");
+        }
+        return runInTenant(param.getTenantId(), () -> {
+            CompanyAiWorkflowServer server = companyAiWorkflowServerMapper.selectCompanyAiWorkflowServerById(param.getServerId());
+            if (server == null) {
+                throw new IllegalArgumentException("\u670d\u52a1\u8bb0\u5f55\u4e0d\u5b58\u5728");
+            }
+            int usedCount = calcUsedCount(server);
+            if (param.getTotalCount() < usedCount) {
+                throw new IllegalArgumentException("\u603b\u540d\u989d\u4e0d\u80fd\u5c0f\u4e8e\u5df2\u7528\u540d\u989d\uff08" + usedCount + "\uff09");
+            }
+            server.setTotalCount(param.getTotalCount());
+            server.setCount(param.getTotalCount() - usedCount);
+            return companyAiWorkflowServerMapper.updateCompanyAiWorkflowServer(server);
+        });
+    }
+
+    /**
+     * 切到租户库执行(先主库校验租户,再 switchTenant,避免事务绑定主库或静默回退主库)
+     */
+    private <T> T runInTenant(Long tenantId, Supplier<T> action) {
+        TenantInfo tenant = loadActiveTenant(tenantId);
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            tenantDataSourceManager.switchTenant(tenant);
+            return action.get();
+        } finally {
+            tenantDataSourceManager.clear();
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+
+    private void runInTenant(Long tenantId, Runnable action) {
+        runInTenant(tenantId, () -> {
+            action.run();
+            return null;
+        });
+    }
+
+    private TenantInfo loadActiveTenant(Long tenantId) {
+        if (tenantId == null) {
+            throw new IllegalArgumentException("\u79df\u6237ID\u4e0d\u80fd\u4e3a\u7a7a");
+        }
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        TenantInfo tenant = tenantInfoService.getById(tenantId);
+        if (tenant == null) {
+            throw new IllegalArgumentException("\u79df\u6237\u4e0d\u5b58\u5728");
+        }
+        if (tenant.getStatus() == null || tenant.getStatus() != 1) {
+            throw new IllegalArgumentException("\u79df\u6237\u672a\u542f\u7528\uff0c\u6682\u4e0d\u53ef\u64cd\u4f5c");
+        }
+        return tenant;
+    }
+
+    private TenantCidServerQuotaVO buildQuotaVo(TenantInfo tenant, CompanyAiWorkflowServer server) {
+        TenantCidServerQuotaVO vo = new TenantCidServerQuotaVO();
+        vo.setTenantId(tenant.getId());
+        vo.setTenantName(tenant.getTenantName());
+        vo.setTenantCode(tenant.getTenantCode());
+        vo.setServerId(server.getId());
+        vo.setTitle(server.getTitle());
+        vo.setGroupNo(server.getGroupNo());
+        vo.setTotalCount(safeInt(server.getTotalCount()));
+        vo.setRemainCount(safeInt(server.getCount()));
+        vo.setUsedCount(calcUsedCount(server));
+        vo.setCreateTime(server.getCreateTime());
+        return vo;
+    }
+
+    private int calcUsedCount(CompanyAiWorkflowServer server) {
+        int total = safeInt(server.getTotalCount());
+        int remain = safeInt(server.getCount());
+        int used = total - remain;
+        if (used < 0) {
+            used = 0;
+        }
+        int boundCount = companyAiWorkflowServerMapper.countBoundUsersByServerId(server.getId());
+        return Math.max(used, boundCount);
+    }
+
+    private void validateAllocateParam(AllocateCidServerQuotaParam param) {
+        if (param == null || param.getTenantId() == null) {
+            throw new IllegalArgumentException("\u8bf7\u9009\u62e9\u79df\u6237");
+        }
+        if (param.getGroupNo() == null) {
+            throw new IllegalArgumentException("\u8bf7\u586b\u5199\u5206\u7ec4\u53f7");
+        }
+        if (param.getAllocateCount() == null || param.getAllocateCount() <= 0) {
+            throw new IllegalArgumentException("\u5206\u914d\u6570\u91cf\u5fc5\u987b\u5927\u4e8e0");
+        }
+        if (StringUtils.isEmpty(param.getTitle())) {
+            param.setTitle("CID\u670d\u52a1\u7ec4-" + param.getGroupNo());
+        }
+    }
+
+    private Map<Long, TenantInfo> loadTenantMap(List<Long> tenantIds) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        Map<Long, TenantInfo> map = new HashMap<>();
+        for (Long tenantId : tenantIds) {
+            TenantInfo tenant = tenantInfoService.getById(tenantId);
+            if (tenant != null) {
+                map.put(tenantId, tenant);
+            }
+        }
+        return map;
+    }
+
+    private int safeInt(Integer value) {
+        return value == null ? 0 : value;
+    }
+}

+ 31 - 0
fs-service/src/main/java/com/fs/admin/vo/TenantCidServerQuotaVO.java

@@ -0,0 +1,31 @@
+package com.fs.admin.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class TenantCidServerQuotaVO {
+
+    private Long tenantId;
+
+    private String tenantName;
+
+    private String tenantCode;
+
+    private Long serverId;
+
+    private String title;
+
+    private Integer groupNo;
+
+    private Integer totalCount;
+
+    private Integer remainCount;
+
+    private Integer usedCount;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/admin/vo/TenantServiceConfigVO.java

@@ -0,0 +1,33 @@
+package com.fs.admin.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class TenantServiceConfigVO {
+
+    private Long id;
+
+    private String serviceMarker;
+
+    private Integer serverType;
+
+    private String serverTypeName;
+
+    private String tenantIds;
+
+    private String tenantNames;
+
+    private Integer tenantCount;
+
+    private List<Long> tenantIdList;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 4 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowServerMapper.java

@@ -66,4 +66,8 @@ public interface CompanyAiWorkflowServerMapper extends BaseMapper<CompanyAiWorkf
     CompanyAiWorkflowServer selectServerForUpdate(@Param("id") Long  id);
 
     CompanyAiWorkflowServer selectServerUnbindForUpdate(@Param("id") Long  id);
+
+    CompanyAiWorkflowServer selectByGroupNo(@Param("groupNo") Integer groupNo);
+
+    int countBoundUsersByServerId(@Param("serverId") Long serverId);
 }

+ 13 - 0
fs-service/src/main/java/com/fs/wxcid/mapper/TenantServiceConfigMapper.java

@@ -1,8 +1,11 @@
 package com.fs.wxcid.mapper;
 
 import com.fs.wxcid.domain.TenantServiceConfig;
+import com.fs.admin.param.TenantServiceConfigQueryParam;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.List;
+
 /**
  * @author MixLiu
  * @date 2026/4/9 15:13
@@ -11,4 +14,14 @@ import org.apache.ibatis.annotations.Param;
 public interface TenantServiceConfigMapper {
 
     TenantServiceConfig selectTenantServiceConfigByServiceMarkerAndType(@Param("serviceMarker") String serviceMarker, @Param("serverType") Integer serverType);
+
+    List<TenantServiceConfig> selectTenantServiceConfigList(TenantServiceConfigQueryParam param);
+
+    TenantServiceConfig selectTenantServiceConfigById(@Param("id") Long id);
+
+    int insertTenantServiceConfig(TenantServiceConfig config);
+
+    int updateTenantServiceConfig(TenantServiceConfig config);
+
+    int deleteTenantServiceConfigByIds(@Param("ids") Long[] ids);
 }

+ 20 - 0
fs-service/src/main/java/com/fs/wxcid/service/ITenantServiceConfigService.java

@@ -0,0 +1,20 @@
+package com.fs.wxcid.service;
+
+import com.fs.admin.param.TenantServiceConfigQueryParam;
+import com.fs.admin.vo.TenantServiceConfigVO;
+import com.fs.wxcid.domain.TenantServiceConfig;
+
+import java.util.List;
+
+public interface ITenantServiceConfigService {
+
+    List<TenantServiceConfigVO> selectList(TenantServiceConfigQueryParam param);
+
+    TenantServiceConfigVO selectById(Long id);
+
+    int insert(TenantServiceConfig config);
+
+    int update(TenantServiceConfig config);
+
+    int deleteByIds(Long[] ids);
+}

+ 122 - 0
fs-service/src/main/java/com/fs/wxcid/service/impl/TenantServiceConfigServiceImpl.java

@@ -0,0 +1,122 @@
+package com.fs.wxcid.service.impl;
+
+import com.fs.admin.param.TenantServiceConfigQueryParam;
+import com.fs.admin.vo.TenantServiceConfigVO;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.enums.EnumServerType;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.mapper.TenantInfoMapper;
+import com.fs.wxcid.domain.TenantServiceConfig;
+import com.fs.wxcid.mapper.TenantServiceConfigMapper;
+import com.fs.wxcid.service.ITenantServiceConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+public class TenantServiceConfigServiceImpl implements ITenantServiceConfigService {
+
+    @Autowired
+    private TenantServiceConfigMapper tenantServiceConfigMapper;
+
+    @Autowired
+    private TenantInfoMapper tenantInfoMapper;
+
+    @Override
+    public List<TenantServiceConfigVO> selectList(TenantServiceConfigQueryParam param) {
+        List<TenantServiceConfig> list = tenantServiceConfigMapper.selectTenantServiceConfigList(param);
+        Map<Long, String> tenantNameMap = loadTenantNameMap();
+        List<TenantServiceConfigVO> result = new ArrayList<>();
+        if (list == null) {
+            return result;
+        }
+        for (TenantServiceConfig item : list) {
+            result.add(toVo(item, tenantNameMap));
+        }
+        return result;
+    }
+
+    @Override
+    public TenantServiceConfigVO selectById(Long id) {
+        TenantServiceConfig config = tenantServiceConfigMapper.selectTenantServiceConfigById(id);
+        if (config == null) {
+            return null;
+        }
+        Map<Long, String> tenantNameMap = loadTenantNameMap();
+        return toVo(config, tenantNameMap);
+    }
+
+    @Override
+    public int insert(TenantServiceConfig config) {
+        config.setCreateTime(DateUtils.getNowDate());
+        return tenantServiceConfigMapper.insertTenantServiceConfig(config);
+    }
+
+    @Override
+    public int update(TenantServiceConfig config) {
+        config.setUpdateTime(DateUtils.getNowDate());
+        return tenantServiceConfigMapper.updateTenantServiceConfig(config);
+    }
+
+    @Override
+    public int deleteByIds(Long[] ids) {
+        return tenantServiceConfigMapper.deleteTenantServiceConfigByIds(ids);
+    }
+
+    private TenantServiceConfigVO toVo(TenantServiceConfig config, Map<Long, String> tenantNameMap) {
+        TenantServiceConfigVO vo = new TenantServiceConfigVO();
+        vo.setId(config.getId());
+        vo.setServiceMarker(config.getServiceMarker());
+        vo.setServerType(config.getServerType());
+        vo.setTenantIds(config.getTenantIds());
+        vo.setCreateTime(config.getCreateTime());
+        vo.setUpdateTime(config.getUpdateTime());
+        if (config.getServerType() != null) {
+            try {
+                vo.setServerTypeName(EnumServerType.fromValue(config.getServerType()).getDescription());
+            } catch (Exception ignored) {
+                vo.setServerTypeName(String.valueOf(config.getServerType()));
+            }
+        }
+        List<Long> tenantIdList = parseTenantIds(config.getTenantIds());
+        vo.setTenantIdList(tenantIdList);
+        vo.setTenantCount(tenantIdList.size());
+        if (!CollectionUtils.isEmpty(tenantIdList)) {
+            String names = tenantIdList.stream()
+                    .map(id -> tenantNameMap.getOrDefault(id, String.valueOf(id)))
+                    .collect(Collectors.joining(","));
+            vo.setTenantNames(names);
+        }
+        return vo;
+    }
+
+    private List<Long> parseTenantIds(String tenantIds) {
+        if (StringUtils.isEmpty(tenantIds)) {
+            return new ArrayList<>();
+        }
+        return Arrays.stream(tenantIds.split(","))
+                .map(String::trim)
+                .filter(StringUtils::isNotEmpty)
+                .map(Long::valueOf)
+                .collect(Collectors.toList());
+    }
+
+    private Map<Long, String> loadTenantNameMap() {
+        List<TenantInfo> tenants = tenantInfoMapper.selectTenantInfoList(new TenantInfo());
+        Map<Long, String> map = new HashMap<>();
+        if (tenants != null) {
+            for (TenantInfo tenant : tenants) {
+                map.put(tenant.getId(), tenant.getTenantName());
+            }
+        }
+        return map;
+    }
+}

+ 10 - 0
fs-service/src/main/resources/mapper/company/CompanyAiWorkflowServerMapper.xml

@@ -83,4 +83,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         select * from company_ai_workflow_server where id = #{id}  for update
     </select>
 
+    <select id="selectByGroupNo" resultMap="CompanyAiWorkflowServerResult">
+        <include refid="selectCompanyAiWorkflowServerVo"/>
+        where group_no = #{groupNo}
+        limit 1
+    </select>
+
+    <select id="countBoundUsersByServerId" resultType="int">
+        select count(1) from company_user where cid_server_id = #{serverId}
+    </select>
+
 </mapper>

+ 84 - 3
fs-service/src/main/resources/mapper/wxcid/TenantServiceConfigMapper.xml

@@ -4,7 +4,88 @@
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.wxcid.mapper.TenantServiceConfigMapper">
 
-    <select id="selectTenantServiceConfigByServiceMarkerAndType" resultType="com.fs.wxcid.domain.TenantServiceConfig">
-        SELECT * FROM `tenant_service_config` where service_marker = #{serviceMarker} and server_type = #{serverType} order by create_time desc LIMIT 1
+    <resultMap type="com.fs.wxcid.domain.TenantServiceConfig" id="TenantServiceConfigResult">
+        <result property="id" column="id"/>
+        <result property="serviceMarker" column="service_marker"/>
+        <result property="tenantIds" column="tenant_ids"/>
+        <result property="serverType" column="server_type"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectTenantServiceConfigVo">
+        select id, service_marker, tenant_ids, server_type, create_by, create_time, update_by, update_time
+        from tenant_service_config
+    </sql>
+
+    <select id="selectTenantServiceConfigByServiceMarkerAndType" resultMap="TenantServiceConfigResult">
+        <include refid="selectTenantServiceConfigVo"/>
+        where service_marker = #{serviceMarker} and server_type = #{serverType}
+        order by create_time desc
+        limit 1
     </select>
-</mapper>
+
+    <select id="selectTenantServiceConfigList" resultMap="TenantServiceConfigResult">
+        <include refid="selectTenantServiceConfigVo"/>
+        <where>
+            <if test="serviceMarker != null and serviceMarker != ''">
+                and service_marker like concat('%', #{serviceMarker}, '%')
+            </if>
+            <if test="serverType != null">
+                and server_type = #{serverType}
+            </if>
+            <if test="tenantId != null">
+                and find_in_set(#{tenantId}, tenant_ids)
+            </if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectTenantServiceConfigById" resultMap="TenantServiceConfigResult">
+        <include refid="selectTenantServiceConfigVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertTenantServiceConfig" useGeneratedKeys="true" keyProperty="id">
+        insert into tenant_service_config
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="serviceMarker != null">service_marker,</if>
+            <if test="tenantIds != null">tenant_ids,</if>
+            <if test="serverType != null">server_type,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="serviceMarker != null">#{serviceMarker},</if>
+            <if test="tenantIds != null">#{tenantIds},</if>
+            <if test="serverType != null">#{serverType},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updateTenantServiceConfig">
+        update tenant_service_config
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="serviceMarker != null">service_marker = #{serviceMarker},</if>
+            <if test="tenantIds != null">tenant_ids = #{tenantIds},</if>
+            <if test="serverType != null">server_type = #{serverType},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteTenantServiceConfigByIds">
+        delete from tenant_service_config where id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 4 - 4
fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

@@ -31,10 +31,10 @@ public class WxTask {
     @Autowired
     private TenantTaskRunner tenantTaskRunner;
 
-    @Scheduled(cron = "0 0/30 * * * ?")
-    public void addWx() {
-        tenantTaskRunner.runForResponsibleTenant("addWx", () ->   taskService.addWx(null));
-    }
+    //    @Scheduled(cron = "0 0/30 * * * ?")
+//    public void addWx() {
+//        taskService.addWx(null);
+//    }
 //    @Scheduled(cron = "0 0/1 * * * ?")
 //    public void addWx4Workflow() {
 //        if (saasTaskEnabled) {

+ 2 - 0
fs-wx-task/src/main/resources/application-common.yml

@@ -152,4 +152,6 @@ hsy:
   role_access_key: AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM
   role_secret_key: T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
   role_trn: trn:iam::2114522511:role/hylj
+# 配置了服务标记后,请在tenant_service_config 中配置服务应用租户ids信息
+tenant-service-marker: wxTask00