Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

yuhongqi 1 месяц назад
Родитель
Сommit
738cce8055
100 измененных файлов с 6640 добавлено и 395 удалено
  1. 43 5
      fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java
  2. 4 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java
  3. 59 13
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  4. 148 0
      fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java
  5. 333 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentAccountController.java
  6. 134 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentProviderController.java
  7. 153 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmKbCatController.java
  8. 43 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  9. 22 1
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  10. 103 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerAnalyzeController.java
  11. 7 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  12. 8 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java
  13. 37 33
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  14. 1 0
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  15. 1 1
      fs-qw-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  16. 1 1
      fs-qwhook-sop/src/main/java/com/fs/FsQwhookSopApplication.java
  17. 6 15
      fs-qwhook-sop/src/main/java/com/fs/app/controller/testController.java
  18. 162 0
      fs-service/src/main/java/com/fs/aicall/domain/CcCallTask.java
  19. 68 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmAgentAccount.java
  20. 29 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmAgentProvider.java
  21. 31 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmKb.java
  22. 31 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmKbCat.java
  23. 45 0
      fs-service/src/main/java/com/fs/aicall/domain/CcParams.java
  24. 38 0
      fs-service/src/main/java/com/fs/aicall/domain/CompanyBindAiModel.java
  25. 82 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcCallTaskMapper.java
  26. 70 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmAgentAccountMapper.java
  27. 71 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmAgentProviderMapper.java
  28. 70 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmKbCatMapper.java
  29. 79 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmKbMapper.java
  30. 79 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcParamsMapper.java
  31. 97 0
      fs-service/src/main/java/com/fs/aicall/mapper/CompanyBindAiModelMapper.java
  32. 71 0
      fs-service/src/main/java/com/fs/aicall/service/ICcCallTaskService.java
  33. 62 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmAgentAccountService.java
  34. 63 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmAgentProviderService.java
  35. 71 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmKbCatService.java
  36. 70 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmKbService.java
  37. 83 0
      fs-service/src/main/java/com/fs/aicall/service/ICcParamsService.java
  38. 114 0
      fs-service/src/main/java/com/fs/aicall/service/ICompanyBindAiModelService.java
  39. 118 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcCallTaskServiceImpl.java
  40. 95 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmAgentAccountServiceImpl.java
  41. 95 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmAgentProviderServiceImpl.java
  42. 113 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmKbCatServiceImpl.java
  43. 154 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmKbServiceImpl.java
  44. 168 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcParamsServiceImpl.java
  45. 179 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CompanyBindAiModelServiceImpl.java
  46. 105 0
      fs-service/src/main/java/com/fs/aicall/utils/CommonUtils.java
  47. 672 0
      fs-service/src/main/java/com/fs/aicall/utils/StringUtils.java
  48. 12 0
      fs-service/src/main/java/com/fs/aicall/utils/XSSFUtils.java
  49. 276 0
      fs-service/src/main/java/com/fs/aicall/utils/http/HttpUtils.java
  50. 1 1
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  51. 4 7
      fs-service/src/main/java/com/fs/company/config/AsyncCalleeConfig.java
  52. 1 1
      fs-service/src/main/java/com/fs/company/domain/CompanyAiWorkflowExec.java
  53. 26 0
      fs-service/src/main/java/com/fs/company/domain/CompanyFsUser.java
  54. 15 0
      fs-service/src/main/java/com/fs/company/domain/CompanyUser.java
  55. 5 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java
  56. 1 1
      fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java
  57. 53 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyFsUserMapper.java
  58. 1 1
      fs-service/src/main/java/com/fs/company/param/ExecutionContext.java
  59. 21 0
      fs-service/src/main/java/com/fs/company/service/IAsyncCalleeProcessorService.java
  60. 23 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  61. 473 0
      fs-service/src/main/java/com/fs/company/service/impl/AsyncCalleeProcessorServiceImpl.java
  62. 72 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  63. 5 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  64. 5 287
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  65. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  66. 114 2
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  67. 14 0
      fs-service/src/main/java/com/fs/company/vo/CompanyWxClient4WorkFlowVO.java
  68. 16 10
      fs-service/src/main/java/com/fs/course/dto/FsOrderDeliveryNoteDTO.java
  69. 6 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCompanyUserMapper.java
  70. 70 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerAnalyze.java
  71. 258 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerInfo.java
  72. 3 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerPropertyTemplate.java
  73. 12 0
      fs-service/src/main/java/com/fs/crm/dto/CrmCustomerAiAutoTagVo.java
  74. 61 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerAnalyzeMapper.java
  75. 37 2
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  76. 25 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerAiTagParam.java
  77. 7 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java
  78. 7 0
      fs-service/src/main/java/com/fs/crm/param/CrmLineCustomerListQueryParam.java
  79. 61 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerAnalyzeService.java
  80. 3 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java
  81. 92 0
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  82. 56 3
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java
  83. 424 0
      fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java
  84. 16 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerAiTagVo.java
  85. 7 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java
  86. 7 0
      fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java
  87. 2 1
      fs-service/src/main/java/com/fs/erp/domain/WeizouApiPushOrderParam.java
  88. 7 1
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java
  89. 10 0
      fs-service/src/main/java/com/fs/his/mapper/MerchantAppConfigMapper.java
  90. 2 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java
  91. 1 1
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  92. 15 0
      fs-service/src/main/java/com/fs/his/vo/CompanyUserBindUserVO.java
  93. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  94. 5 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsIntegralGoodsScrmMapper.java
  95. 7 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueScrmMapper.java
  96. 1 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  97. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePaymentScrmMapper.java
  98. 4 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductAttrValueScrmMapper.java
  99. 2 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  100. 1 0
      fs-service/src/main/java/com/fs/hisStore/param/FsCouponIssueParam.java

+ 43 - 5
fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java

@@ -1,16 +1,22 @@
 package com.fs.app.controller;
 
 import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.base.BusinessException;
 import com.fs.common.result.Result;
+import com.fs.common.service.ISmsService;
+import com.fs.newAdv.dto.req.FormSubmitReq;
 import com.fs.newAdv.dto.req.LandingIndexReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 落地页控制器
@@ -26,6 +32,10 @@ public class LandingPageController {
 
     @Autowired
     private CallbackProcessingFacadeService facadeService;
+    @Autowired
+    private ISmsService smsService;
+    @Autowired
+    private RedisCache redisCache;
 
     /**
      * 落地页访问
@@ -34,7 +44,7 @@ public class LandingPageController {
     public Result<LandingIndexRes> h5Home(
             @RequestBody LandingIndexReq req) {
         // 查询落地页模板
-        return Result.success(facadeService.getLandingIndexBySiteId(req.getViewUrl(),req.getAllParams()));
+        return Result.success(facadeService.getLandingIndexBySiteId(req.getViewUrl(), req.getAllParams()));
     }
 
     /**
@@ -45,6 +55,34 @@ public class LandingPageController {
         return Result.success(facadeService.getWxLandingIndexBySiteId(req));
     }
 
+    /**
+     * 获取验证码
+     *
+     * @return
+     */
+    @GetMapping(value = "/sendSmsCode/{phone}")
+    public Result<String> sendSmsCode(@PathVariable String phone) {
+        String captcha = redisCache.getCacheObject("smsCode"+phone);
+        if (StringUtils.isNotEmpty(captcha)) {
+            throw new BusinessException("短信已发送,1分钟以内请勿重新发送验证码:");
+        }
+        int code = (int) (Math.random() * (9999 - 1000 + 1)) + 1000;// 产生1000-9999的随机数
+        redisCache.setCacheObject("smsCode"+phone,code, 2, TimeUnit.MINUTES);
+        R r = smsService.sendCaptcha(phone, code + "","验证码");
+        return Result.success(String.valueOf(r.get("msg")));
+    }
 
+    /**
+     * @return
+     */
+    @PostMapping(value = "/submit")
+    public Result<String> formSubmit(@RequestBody @Valid FormSubmitReq req) {
+        String captcha = redisCache.getCacheObject(req.getPhone());
+        if (StringUtils.isEmpty(captcha) || !captcha.equals(req.getSmsCode())) {
+            throw new BusinessException("短信验证码有误:" + req.getSmsCode());
+        }
+        facadeService.updateFromByTraceId(req);
+        return Result.success();
+    }
 }
 

+ 4 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java

@@ -1,10 +1,12 @@
 package com.fs.app.facade;
 
+import com.fs.newAdv.dto.req.FormSubmitReq;
 import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.req.updateNickNameReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 
+import javax.validation.Valid;
 import java.util.Map;
 
 public interface CallbackProcessingFacadeService {
@@ -33,4 +35,6 @@ public interface CallbackProcessingFacadeService {
     void qwExternalIdBindTrack(QwExternalIdBindTrackReq req);
 
     void updateNickName(updateNickNameReq req);
+
+    void updateFromByTraceId(@Valid FormSubmitReq req);
 }

+ 59 - 13
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java

@@ -1,5 +1,6 @@
 package com.fs.app.facade;
 
+import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
@@ -9,9 +10,12 @@ import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.fs.common.utils.RedisUtil;
 import com.fs.common.utils.SnowflakeUtil;
+import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.service.IGeneralCustomerEntryService;
 import com.fs.newAdv.domain.LandingPageTemplate;
 import com.fs.newAdv.domain.Lead;
 import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.dto.req.FormSubmitReq;
 import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.req.updateNickNameReq;
@@ -68,6 +72,10 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     @Autowired
     private RedisUtil redisUtil;
 
+    @Autowired
+    IGeneralCustomerEntryService iGeneralCustomerEntryService;
+
+
     private static final String TEMPLATE_DATA = "new-adv:template-data:";
 
     @Override
@@ -146,16 +154,9 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
             lead.setAdvertiserId(advertiserId);
             lead.setSiteId(siteId);
             lead.setClickId(clickId);
+            lead.setTraceId(SnowflakeUtil.randomUUID());
             // 设置站点和落地页的关联
             setSiteByIdeaId(siteId, lead.getIdeaId());
-        } else {
-            // 检查站点和广告商信息是否异常
-            if (!Objects.equals(lead.getSiteId(), siteId)) {
-                log.info("落地页站点信息异常:{}---{}", lead.getSiteId(), siteId);
-            }
-            if (!Objects.equals(lead.getAdvertiserId(), advertiserId)) {
-                log.info("落地页广告商信息异常:{}---{}", lead.getAdvertiserId(), advertiserId);
-            }
         }
         // 模板缓存
 /*        Object ca = redisUtil.get(TEMPLATE_DATA + traceId);
@@ -186,7 +187,6 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         lead.setUpdateTime(now);
         lead.setViewUrl(viewUrl);
         if (isNewLead) {
-            lead.setTraceId(SnowflakeUtil.randomUUID());
             leadService.save(lead);
         } else {
             leadService.updateById(lead);
@@ -221,12 +221,29 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
                 .filter(ObjectUtil::isNotEmpty)
                 .flatMap(Collection::stream)
                 .map(module -> (JSONObject) module)
-                .filter(module -> "h5-qrcode".equals(module.getStr("type")))
                 .forEach(module -> {
-                    if (StrUtil.isEmpty(qrCode.get())) {
-                        qrCode.set(getQrCodeByAllocationRuleId(site.getLaunchType(), site.getAllocationRule(), site.getAllocationRuleId(), lead));
+                    String type = module.getStr("type");
+
+                    switch (type) {
+                        case "h5-qrcode":
+                            if (StrUtil.isEmpty(qrCode.get())) {
+                                qrCode.set(getQrCodeByAllocationRuleId(
+                                        site.getLaunchType(),
+                                        site.getAllocationRule(),
+                                        site.getAllocationRuleId(),
+                                        lead));
+                            }
+                            module.set("workUrl", qrCode.get());
+                            break;
+
+                        case "h5-customer-link-button":
+                            module.set("workUrl", module.getStr("workUrl") + "?customer_channel=" + lead.getTraceId());
+                            log.info("更新获客链接参数: {}", qrCode.get());
+                            break;
+
+                        default:
+                            break;
                     }
-                    module.set("workUrl", qrCode.get());
                 });
     }
 
@@ -316,4 +333,33 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         update.setWeiChatName(req.getNickName());
         leadService.updateById(update);
     }
+
+    @Override
+    public void updateFromByTraceId(FormSubmitReq req) {
+        leadService.update(new LambdaUpdateWrapper<Lead>()
+                .eq(Lead::getTraceId, req.getTraceId())
+                .set(Lead::getPhone, req.getPhone()));
+        ThreadUtil.execute(() -> {
+            Lead byTraceId = leadService.getByTraceId(req.getTraceId());
+            EntryCustomerParam param = new EntryCustomerParam();
+            param.setMobile(req.getPhone());
+            param.setCustomerName(req.getName());
+            param.setSceneType(getSceneType(byTraceId.getAdvertiserId()));
+            Site byId = siteService.getById(byTraceId.getSiteId());
+            param.setCompanyId(byId.getCompanyId());
+            iGeneralCustomerEntryService.entryCustomer(param);
+            log.info("广告数据发送到ai电话: {}", param);
+        });
+    }
+
+    private Integer getSceneType(Long advertiserId) {
+        switch (AdvertiserTypeEnum.getByCode(advertiserId)) {
+            case BAIDU:
+                return 2;
+            case OCEANENGINE:
+                return 3;
+            default:
+                return 1;
+        }
+    }
 }

+ 148 - 0
fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java

@@ -0,0 +1,148 @@
+package com.fs.task;
+
+import com.google.common.collect.Lists;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Component("CrmCustomerAiProcessingTask")
+@RequiredArgsConstructor
+@Slf4j
+public class CrmCustomerAiProcessingTask {
+
+    private final RedisTemplate redisTemplate;
+
+    private static final String CRM_AI_REDIS_KEY = "crm:AI:data:processing";
+
+    // 自定义线程池
+    private final ExecutorService executorService = new ThreadPoolExecutor(
+            5,  // 核心线程数
+            10, // 最大线程数
+            60, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(200),
+            r -> {
+                Thread thread = new Thread(r);
+                thread.setName("crm-ai-processor-" + thread.getId());
+                return thread;
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程处理
+    );
+
+    public void process() {
+        List<Map<String,String>> range = (List<Map<String, String>>) redisTemplate.opsForList().range(CRM_AI_REDIS_KEY, 0, -1);
+        if (range == null || range.isEmpty()) {
+            log.info("CrmCustomerAiProcessingTask没有待处理的数据");
+            return;
+        }
+        log.info("CrmCustomerAiProcessingTask开始处理数据,条数"+range.size());
+        // 2. 每100条分成一批
+        List<List<Map<String, String>>> partitions = Lists.partition(range, 10);//ai沟通很慢,批量处理10条每批
+        int totalBatches = partitions.size();
+        log.info("共分为 {} 批, 每批"+(partitions.size()>1?"10":range.size())+"条", totalBatches);
+
+        // 3. 统计计数器
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger failCount = new AtomicInteger(0);
+
+        long startTime = System.currentTimeMillis();
+
+        // 4. 多线程处理
+        List<CompletableFuture<Void>> futures = partitions.stream()
+                .map(batch -> CompletableFuture.runAsync(() -> {
+                    processBatch(batch, successCount, failCount);
+                }, executorService))
+                .collect(Collectors.toList());
+
+        // 5. 等待所有任务完成
+        try {
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+            long costTime = System.currentTimeMillis() - startTime;
+            log.info("CrmCustomerAiProcessingTask处理完成, 总条数: {}, 成功: {}, 失败: {}, 耗时: {}ms",
+                    range.size(), successCount.get(), failCount.get(), costTime);
+
+            // 6. 处理完成后,从Redis中删除已处理的数据
+            if (failCount.get() == 0) {
+                // 全部成功,删除整个key
+                redisTemplate.delete(CRM_AI_REDIS_KEY);
+                log.info("全部处理成功,已删除Redis数据");
+            } else {
+                // 有失败的数据,保留或移到失败队列
+                handleFailedData(partitions, successCount.get());
+            }
+
+        } catch (Exception e) {
+            log.error("多线程处理异常", e);
+        }
+
+    }
+    /**
+     * 处理单个批次
+     */
+    private void processBatch(List<Map<String, String>> batch,
+                              AtomicInteger successCount,
+                              AtomicInteger failCount) {
+        String threadName = Thread.currentThread().getName();
+        long batchStartTime = System.currentTimeMillis();
+
+        try {
+            log.info("线程 {} 开始处理批次, 数据量: {}", threadName, batch.size());
+
+            // 示例:处理每条数据
+            for (Map<String, String> data : batch) {
+                // 获取数据
+                String customerId = data.get("customerId");
+                String dataJson = data.get("data");
+                //todo 业务!!!!!!1.ai沟通总结2.流失风险等级3.沟通摘要4.客户画像8.客户关注点9.客户意向度
+
+
+
+                // 模拟业务处理
+                Thread.sleep(10);
+            }
+
+            long costTime = System.currentTimeMillis() - batchStartTime;
+            successCount.addAndGet(batch.size());
+            log.info("线程 {} 批次处理完成, 数据量: {}, 耗时: {}ms",
+                    threadName, batch.size(), costTime);
+
+        } catch (Exception e) {
+            failCount.addAndGet(batch.size());
+            log.error("线程 {} 批次处理失败, 数据量: {}", threadName, batch.size(), e);
+            throw new RuntimeException("批次处理失败", e);
+        }
+    }
+    /**
+     * 处理失败的数据
+     */
+    private void handleFailedData(List<List<Map<String, String>>> partitions, int successCount) {
+        try {
+            // 找出未成功处理的数据
+            List<Map<String, String>> failedData = partitions.stream()
+                    .flatMap(List::stream)
+                    .skip(successCount)
+                    .collect(Collectors.toList());
+
+            if (!failedData.isEmpty()) {
+                String failedKey = CRM_AI_REDIS_KEY + ":failed";
+                for (Map<String, String> data : failedData) {
+                    redisTemplate.opsForList().rightPush(failedKey, data);
+                }
+                log.info("失败数据已移至失败队列: {}, 数量: {}", failedKey, failedData.size());
+            }
+
+            // 清理已处理的数据(可选:根据业务需求决定是否删除)
+            // cleanProcessedData(partitions, successCount);
+
+        } catch (Exception e) {
+            log.error("处理失败数据异常", e);
+        }
+    }
+}

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

@@ -0,0 +1,333 @@
+package com.fs.company.controller.aicall;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.aicall.domain.CcCallTask;
+import com.fs.aicall.domain.CcLlmAgentAccount;
+import com.fs.aicall.service.ICcLlmAgentAccountService;
+import com.fs.aicall.service.ICcParamsService;
+import com.fs.aicall.service.ICcCallTaskService;
+import com.fs.aicall.service.ICompanyBindAiModelService;
+import com.fs.aicall.utils.CommonUtils;
+import com.fs.aicall.utils.StringUtils;
+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.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 机器人参数配置Controller
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Controller
+@RequestMapping("/aicall/account")
+public class CcLlmAgentAccountController extends BaseController
+{
+    private String prefix = "aicall/account";
+
+    @Autowired
+    private ICcLlmAgentAccountService ccLlmAgentAccountService;
+    @Autowired
+    private ICcParamsService ccParamsService;
+    @Autowired
+    private ICcCallTaskService ccCallTaskService;
+    @Autowired
+    private ICompanyBindAiModelService companyBindAiModelService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    private static List<String> hideKeys = Arrays.asList("apiKey", "oauthPrivateKey", "oauthPublicKeyId", "patToken");
+
+
+    @PreAuthorize("@ss.hasPermi('aicall:account:view')")
+    @GetMapping()
+    public String account()
+    {
+        return prefix + "/account";
+    }
+
+    /**
+     * 查询机器人参数配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:list')")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(@RequestBody CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        // 获取当前登录的公司ID
+        Long companyId = getCurrentCompanyId();
+        if (companyId != null) {
+            List<Long> modelIds = companyBindAiModelService.selectModelIdsByCompanyId(companyId);
+            if (!modelIds.isEmpty()) {
+                ccLlmAgentAccount.setModelIds(modelIds);
+            } else {
+                return getDataTable(new ArrayList<>());
+            }
+        }
+        
+        startPage();
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+        TableDataInfo tableDataInfo = getDataTable(list);
+        List<CcLlmAgentAccount> records = (List<CcLlmAgentAccount>) tableDataInfo.getRows();
+        for (CcLlmAgentAccount data: records) {
+            JSONObject accountJson = JSONObject.parseObject(data.getAccountJson());
+            for (String key: accountJson.keySet()) {
+                if (hideKeys.contains(key)) {
+                    accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+                }
+            }
+            data.setAccountJson(JSONObject.toJSONString(accountJson));
+        }
+        tableDataInfo.setRows(records);
+        return tableDataInfo;
+    }
+
+    /**
+     * 获取当前登录的公司ID
+     * @return 公司ID
+     */
+    private Long getCurrentCompanyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getUser().getCompanyId();
+    }
+
+    /**
+     * 导出机器人参数配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:export')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+        ExcelUtil<CcLlmAgentAccount> util = new ExcelUtil<CcLlmAgentAccount>(CcLlmAgentAccount.class);
+        return util.exportExcel(list, "机器人参数配置数据");
+    }
+
+    /**
+     * 新增机器人参数配置
+     */
+    @GetMapping("/add")
+    public String add(ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = new CcLlmAgentAccount();
+        ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        ccLlmAgentAccount.setInterruptFlag(0);
+        ccLlmAgentAccount.setAccountJson("{}");
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:add')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(@RequestBody CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        if ("Coze".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("LocalNlpChat".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("JiutianWorkflow".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())
+                || "JiutianAgent".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("JiutianAccount");
+        } else {
+            ccLlmAgentAccount.setAccountEntity("LlmAccount");
+        }
+        if (null == ccLlmAgentAccount.getKbCatId()) {
+            ccLlmAgentAccount.setKbCatId(-1);
+        }
+        
+        // 新增模型
+        int result = ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount);
+        
+        // 获取当前登录的公司ID
+        Long companyId = getCurrentCompanyId();
+        if (result > 0 && companyId != null) {
+            companyBindAiModelService.bindCompanyToModel(ccLlmAgentAccount.getId().longValue(), companyId);
+        }
+        
+        return toAjax(result);
+    }
+
+    /**
+     * 修改机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:edit')")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        if (StringUtils.isBlank(ccLlmAgentAccount.getInterruptIgnoreKeywords())) {
+            ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        }
+        JSONObject accountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: accountJson.keySet()) {
+            if (hideKeys.contains(key)) {
+                accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(accountJson));
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+
+        String errMsg = checkEdit(ccLlmAgentAccount.getId());
+        mmap.put("errorMsg", errMsg);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 复制机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:edit')")
+    @GetMapping("/copy/{id}")
+    public String copy(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        if (StringUtils.isBlank(ccLlmAgentAccount.getInterruptIgnoreKeywords())) {
+            ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        }
+        ccLlmAgentAccount.setId(-1*ccLlmAgentAccount.getId());
+        ccLlmAgentAccount.setName(ccLlmAgentAccount.getName() + "-副本");
+        JSONObject accountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: accountJson.keySet()) {
+            if (hideKeys.contains(key)) {
+                accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(accountJson));
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:edit')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(@RequestBody CcLlmAgentAccount ccLlmAgentAccount)
+    {
+
+        if (ccLlmAgentAccount.getId() > 0) {
+            String errMsg = checkEdit(ccLlmAgentAccount.getId());
+            if (StringUtils.isNotEmpty(errMsg)) {
+                return AjaxResult.error(errMsg);
+            }
+        }
+
+        if ("Coze".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("LocalNlpChat".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("JiutianWorkflow".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())
+                || "JiutianAgent".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("JiutianAccount");
+        } else {
+            ccLlmAgentAccount.setAccountEntity("LlmAccount");
+        }
+
+        Integer orignId = ccLlmAgentAccount.getId();
+        if (orignId < 0) {
+            orignId = orignId * -1;
+        }
+        CcLlmAgentAccount oldCcLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(orignId);
+        JSONObject oldAccountJson = JSONObject.parseObject(oldCcLlmAgentAccount.getAccountJson());
+        JSONObject newAccountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: newAccountJson.keySet()) {
+            // 是需要脱敏的key,且值包含星号,则值不更新
+            if (hideKeys.contains(key) && newAccountJson.getString(key).contains("****")) {
+                newAccountJson.put(key, oldAccountJson.getString(key));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(newAccountJson));
+        if (null == ccLlmAgentAccount.getKbCatId()) {
+            ccLlmAgentAccount.setKbCatId(-1);
+        }
+
+        if (ccLlmAgentAccount.getId() > 0) {
+            return toAjax(ccLlmAgentAccountService.updateCcLlmAgentAccount(ccLlmAgentAccount));
+        } else {
+            ccLlmAgentAccount.setId(null);
+            return toAjax(ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount));
+        }
+    }
+
+    /**
+     * 删除机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:remove')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        //获取公司id
+        Long companyId = getCurrentCompanyId();
+        //删除公司绑定的AI模型
+        companyBindAiModelService.deleteBindAiModelByCompanyIdAndModelIds(companyId,ids);
+
+        return toAjax(ccLlmAgentAccountService.deleteCcLlmAgentAccountByIds(ids));
+    }
+
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(new CcLlmAgentAccount());
+        for (CcLlmAgentAccount data: list) {
+            JSONObject accountJson = JSONObject.parseObject(data.getAccountJson());
+            for (String key: accountJson.keySet()) {
+                if (hideKeys.contains(key)) {
+                    accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+                }
+            }
+            data.setAccountJson(JSONObject.toJSONString(accountJson));
+        }
+        return AjaxResult.success(list);
+    }
+
+
+
+    private String checkEdit(Integer id)
+    {
+        CcLlmAgentAccount llmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        List<CcCallTask> ccCallTaskList = ccCallTaskService.selectCcCallTaskList(new CcCallTask().setLlmAccountId(id));
+        List<String> ids = new ArrayList<>();
+        for (CcCallTask ccCallTask: ccCallTaskList) {
+            // AI外呼,且不自动停止,且状态为正在拨打的任务,需要手动停止任务后再修改大模型配置再手动启动任务
+            if (ccCallTask.getTaskType() == 1
+                    && ccCallTask.getIfcall() == 1
+                    && ccCallTask.getAutoStop() == 0) {
+                ids.add(ccCallTask.getBatchName());
+            }
+        }
+        if (ids.size() > 0) {
+            return String.format("%s%s%s", "请先暂停任务:", StringUtils.join(ids, ","), ",再修改该配置,修改完成后再启动任务");
+        } else {
+            return "";
+        }
+    }
+
+}

+ 134 - 0
fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentProviderController.java

@@ -0,0 +1,134 @@
+package com.fs.company.controller.aicall;
+
+
+import com.fs.aicall.domain.CcLlmAgentProvider;
+import com.fs.aicall.service.ICcLlmAgentProviderService;
+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.utils.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 大模型实现类列表Controller
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Controller
+@RequestMapping("/aicall/provider")
+public class CcLlmAgentProviderController extends BaseController
+{
+    private String prefix = "aicall/provider";
+
+    @Autowired
+    private ICcLlmAgentProviderService ccLlmAgentProviderService;
+
+    @PreAuthorize("@ss.hasPermi('aicall:provider:view')")
+    @GetMapping()
+    public String provider()
+    {
+        return prefix + "/provider";
+    }
+
+    /**
+     * 查询大模型实现类列表列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:list')")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        startPage();
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(ccLlmAgentProvider);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出大模型实现类列表列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:export')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(ccLlmAgentProvider);
+        ExcelUtil<CcLlmAgentProvider> util = new ExcelUtil<CcLlmAgentProvider>(CcLlmAgentProvider.class);
+        return util.exportExcel(list, "大模型实现类列表数据");
+    }
+
+    /**
+     * 新增大模型实现类列表
+     */
+    @GetMapping("/add")
+    public String add()
+    {
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:add')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return toAjax(ccLlmAgentProviderService.insertCcLlmAgentProvider(ccLlmAgentProvider));
+    }
+
+    /**
+     * 修改大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:edit')")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentProvider ccLlmAgentProvider = ccLlmAgentProviderService.selectCcLlmAgentProviderById(id);
+        mmap.put("ccLlmAgentProvider", ccLlmAgentProvider);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:edit')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return toAjax(ccLlmAgentProviderService.updateCcLlmAgentProvider(ccLlmAgentProvider));
+    }
+
+    /**
+     * 删除大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:remove')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccLlmAgentProviderService.deleteCcLlmAgentProviderByIds(ids));
+    }
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(new CcLlmAgentProvider());
+        return AjaxResult.success(list);
+    }
+
+}

+ 153 - 0
fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmKbCatController.java

@@ -0,0 +1,153 @@
+package com.fs.company.controller.aicall;
+
+import com.fs.aicall.domain.CcLlmKbCat;
+import com.fs.aicall.service.ICcLlmKbCatService;
+import com.fs.aicall.service.ICcLlmKbService;
+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.utils.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 知识库Controller
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+@Controller
+@RequestMapping("/aicall/kbcat")
+public class CcLlmKbCatController extends BaseController
+{
+    private String prefix = "aicall/kbcat";
+
+    @Autowired
+    private ICcLlmKbCatService ccLlmKbCatService;
+    @Autowired
+    private ICcLlmKbService ccLlmKbService;
+
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:view')")
+    @GetMapping()
+    public String kbcat()
+    {
+        return prefix + "/kbcat";
+    }
+
+    /**
+     * 查询知识库列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:list')")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcLlmKbCat ccLlmKbCat)
+    {
+        startPage();
+        List<CcLlmKbCat> list = ccLlmKbCatService.selectCcLlmKbCatList(ccLlmKbCat);
+        for (CcLlmKbCat data: list) {
+            data.setContentCount(ccLlmKbService.selectCountByCatId(data.getId()));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出知识库列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:export')")
+    @Log(title = "知识库", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmKbCat ccLlmKbCat)
+    {
+        List<CcLlmKbCat> list = ccLlmKbCatService.selectCcLlmKbCatList(ccLlmKbCat);
+        ExcelUtil<CcLlmKbCat> util = new ExcelUtil<CcLlmKbCat>(CcLlmKbCat.class);
+        return util.exportExcel(list, "知识库数据");
+    }
+
+    /**
+     * 新增知识库
+     */
+    @GetMapping("/add")
+    public String add(ModelMap mmap)
+    {
+        mmap.put("ccLlmKbCat", new CcLlmKbCat());
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:add')")
+    @Log(title = "知识库", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcLlmKbCat ccLlmKbCat)
+    {
+        CcLlmKbCat checkCcLlmKbCat = ccLlmKbCatService.selectCcLlmKbCatByCat(null, ccLlmKbCat.getCat());
+        if (null != checkCcLlmKbCat) {
+            return AjaxResult.error("知识库分类不能重复,请修改");
+        }
+        return toAjax(ccLlmKbCatService.insertCcLlmKbCat(ccLlmKbCat));
+    }
+
+    /**
+     * 修改知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:edit')")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Long id, ModelMap mmap)
+    {
+        CcLlmKbCat ccLlmKbCat = ccLlmKbCatService.selectCcLlmKbCatById(id);
+        mmap.put("ccLlmKbCat", ccLlmKbCat);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:edit')")
+    @Log(title = "知识库", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcLlmKbCat ccLlmKbCat)
+    {
+        CcLlmKbCat checkCcLlmKbCat = ccLlmKbCatService.selectCcLlmKbCatByCat(ccLlmKbCat.getId(), ccLlmKbCat.getCat());
+        if (null != checkCcLlmKbCat) {
+            return AjaxResult.error("知识库分类不能重复,请修改");
+        }
+        return toAjax(ccLlmKbCatService.updateCcLlmKbCat(ccLlmKbCat));
+    }
+
+    /**
+     * 删除知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:remove')")
+    @Log(title = "知识库", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccLlmKbCatService.deleteCcLlmKbCatByIds(ids));
+    }
+
+
+
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmKbCat> list = ccLlmKbCatService.selectCcLlmKbCatList(new CcLlmKbCat());
+        return AjaxResult.success(list);
+    }
+
+
+
+}

+ 43 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java

@@ -30,10 +30,13 @@ import com.fs.company.vo.CompanyUserQwListVO;
 import com.fs.company.vo.CompanyUserVO;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
+import com.fs.his.domain.FsUser;
 import com.fs.his.utils.qrcode.QRCodeUtils;
+import com.fs.his.vo.CompanyUserBindUserVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.hisStore.vo.FsStoreProductExportVO;
@@ -912,4 +915,44 @@ public class CompanyUserController extends BaseController {
         List<com.fs.hisStore.domain.FsUserScrm> userList = companyUserService.selectBoundFsUsersByCompanyUserId(companyUserId);
         return R.ok().put("data", userList);
     }
+
+    /**
+     * 获取销售绑定的fs_user
+     */
+
+    @GetMapping("/getFsUserBySaleId")
+    public  TableDataInfo getFsUserBySaleId(FsUser fsUser){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<CompanyUserBindUserVO> list = companyUserService.getFsUserByCompanyUserId(fsUser);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 绑定销售和fs_user 的关系(该销售绑定fs_user)
+     */
+    @ApiOperation("绑定销售和fs_user 的关系(该销售绑定fs_user)")
+//    @PreAuthorize("@ss.hasPermi('company:user:bindUser')")
+    @PostMapping("/bindSaleAndFsUser")
+    public R bindSaleAndFsUser(@RequestBody FsUserCompanyUser companyUser){
+        int i = companyUserService.bindCompanyUserAndFsUser(companyUser.getCompanyUserId(), companyUser.getUserId());
+        return i>0?R.ok():R.error("绑定失败");
+    }
+
+    /**
+     * 解绑销售和 fs_user 的关系
+     */
+    @ApiOperation("解绑销售和 fs_user 的关系")
+    @PostMapping("/unbindSaleAndFsUser")
+    public R unbindSaleAndFsUser(@RequestBody FsUserCompanyUser companyUser) {
+        try {
+            boolean result = companyUserService.unbindCompanyUserAndFsUser(companyUser.getCompanyUserId(), companyUser.getUserId());
+            return result ? R.ok("解绑成功") : R.error("解绑失败");
+        } catch (Exception e) {
+            logger.error("解绑销售和用户关系异常", e);
+            return R.error("操作失败:" + e.getMessage());
+        }
+    }
 }

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

@@ -1,5 +1,8 @@
 package com.fs.company.controller.company;
 
+import com.fs.aicall.domain.CcLlmAgentAccount;
+import com.fs.aicall.service.ICcLlmAgentAccountService;
+import com.fs.aicall.service.ICompanyBindAiModelService;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.ServletUtils;
@@ -12,6 +15,8 @@ import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.ArrayList;
+import java.util.LinkedList;
 import java.util.List;
 
 /**
@@ -35,6 +40,12 @@ public class EasyCallController extends BaseController {
     @Autowired
     private TokenService tokenService;
 
+    @Autowired
+    private ICompanyBindAiModelService companyBindAiModelService;
+
+    @Autowired
+    private ICcLlmAgentAccountService ccLlmAgentAccountService;
+
     // =================== 基础数据查询 ===================
 
     /**
@@ -60,7 +71,17 @@ public class EasyCallController extends BaseController {
     public R getLlmAccountList() {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getUser().getCompanyId();
-        List<EasyCallLlmAccountVO> list = easyCallService.getLlmAccountList(companyId);
+        CcLlmAgentAccount ccLlmAgentAccount = new CcLlmAgentAccount();
+        if (companyId != null) {
+            List<Long> modelIds = companyBindAiModelService.selectModelIdsByCompanyId(companyId);
+            if (!modelIds.isEmpty()) {
+                ccLlmAgentAccount.setModelIds(modelIds);
+            } else {
+                return R.ok().put("data", new ArrayList<>());
+            }
+        }
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+//        List<EasyCallLlmAccountVO> list = easyCallService.getLlmAccountList(companyId);
         return R.ok().put("data", list);
     }
 

+ 103 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerAnalyzeController.java

@@ -0,0 +1,103 @@
+package com.fs.company.controller.crm;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.crm.domain.CrmCustomerAnalyze;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 客户聊天记录分析Controller
+ * 
+ * @author fs
+ * @date 2026-03-24
+ */
+@RestController
+@RequestMapping("/crm/analyze")
+public class CrmCustomerAnalyzeController extends BaseController
+{
+    @Autowired
+    private ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+
+    /**
+     * 查询客户聊天记录分析列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        startPage();
+        List<CrmCustomerAnalyze> list = crmCustomerAnalyzeService.selectCrmCustomerAnalyzeList(crmCustomerAnalyze);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出客户聊天记录分析列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:export')")
+    @Log(title = "客户聊天记录分析", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        List<CrmCustomerAnalyze> list = crmCustomerAnalyzeService.selectCrmCustomerAnalyzeList(crmCustomerAnalyze);
+        ExcelUtil<CrmCustomerAnalyze> util = new ExcelUtil<CrmCustomerAnalyze>(CrmCustomerAnalyze.class);
+        return util.exportExcel(list, "客户聊天记录分析数据");
+    }
+
+    /**
+     * 获取客户聊天记录分析详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(crmCustomerAnalyzeService.selectCrmCustomerAnalyzeById(id));
+    }
+
+    /**
+     * 新增客户聊天记录分析
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:add')")
+    @Log(title = "客户聊天记录分析", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        return toAjax(crmCustomerAnalyzeService.insertCrmCustomerAnalyze(crmCustomerAnalyze));
+    }
+
+    /**
+     * 修改客户聊天记录分析
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:edit')")
+    @Log(title = "客户聊天记录分析", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        return toAjax(crmCustomerAnalyzeService.updateCrmCustomerAnalyze(crmCustomerAnalyze));
+    }
+
+    /**
+     * 删除客户聊天记录分析
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:remove')")
+    @Log(title = "客户聊天记录分析", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(crmCustomerAnalyzeService.deleteCrmCustomerAnalyzeByIds(ids));
+    }
+}

+ 7 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -190,6 +190,13 @@ public class CrmCustomerController extends BaseController
 //        if(loginUser.getCompany().getCompanyId()==116){   // 河北湘银信息咨询服务有限公司(JZ-1)客户假删除不显示
 //            param.setCompanyId(0L);
 //        }
+        //默认值处理
+        if(param.getIntentionDegreeGt() != null && param.getIntentionDegreeGt() == 0){
+            param.setIntentionDegreeGt(null);
+        }
+        if(param.getIntentionDegreelt() != null && param.getIntentionDegreelt() == 0){
+            param.setIntentionDegreelt(null);
+        }
         if (param.getIsReceive() != null && param.getIsReceive() == 0){
             CrmLineCustomerListQueryParam param1 = new CrmLineCustomerListQueryParam();
             BeanUtils.copyProperties(param,param1);

+ 8 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java

@@ -5,6 +5,9 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.fs.common.result.Result;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import com.fs.newAdv.domain.Site;
 import com.fs.newAdv.service.ISiteService;
 import lombok.extern.slf4j.Slf4j;
@@ -27,6 +30,9 @@ public class SiteController {
     @Autowired
     private ISiteService siteService;
 
+    @Autowired
+    private TokenService tokenService;
+
     @GetMapping("/page")
     public Result<IPage<Site>> pageSiteStatistics(
             @RequestParam(defaultValue = "1") Long pageNum,
@@ -66,6 +72,8 @@ public class SiteController {
      */
     @PostMapping
     public Result<Void> create(@RequestBody Site site) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        site.setCompanyId(loginUser.getCompany().getCompanyId());
         siteService.createSite(site);
         return Result.success();
     }

+ 37 - 33
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -25,6 +25,7 @@ import com.fs.common.utils.spring.SpringUtils;
 import com.fs.live.domain.*;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
+import com.fs.newAdv.service.ILeadService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.DateUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -71,7 +72,7 @@ public class WebSocketServer {
     private final static long HEARTBEAT_TIMEOUT = 2 * 60 * 1000;
     // admin房间消息发送线程池(单线程,保证串行化)
     private final static ConcurrentHashMap<Long, ExecutorService> adminExecutors = new ConcurrentHashMap<>();
-    
+
     // 消息队列系统
     // 每个直播间的消息队列,使用优先级队列支持管理员消息插队
     private final static ConcurrentHashMap<Long, PriorityBlockingQueue<QueueMessage>> messageQueues = new ConcurrentHashMap<>();
@@ -104,6 +105,7 @@ public class WebSocketServer {
     private final ILiveWatchLogService liveWatchLogService = SpringUtils.getBean(ILiveWatchLogService.class);
     private final ILiveVideoService liveVideoService = SpringUtils.getBean(ILiveVideoService.class);
     private final ILiveCompletionPointsRecordService completionPointsRecordService = SpringUtils.getBean(ILiveCompletionPointsRecordService.class);
+    private final ILeadService leadService = SpringUtils.getBean(ILeadService.class);
     private static Random random = new Random();
 
     // Redis key 前缀:用户进入直播间时间
@@ -292,6 +294,8 @@ public class WebSocketServer {
                     liveUserFirstEntry.setExternalContactId(externalContactId);
                 }
                 liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
+                // 第一次进入直播间 发送广告线索
+                leadService.enterLive(userId, liveId);
             }
             redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry, 4, TimeUnit.HOURS);
 
@@ -309,7 +313,7 @@ public class WebSocketServer {
         sessionLocks.putIfAbsent(session.getId(), new ReentrantLock());
         // 初始化心跳时间
         heartbeatCache.put(session.getId(), System.currentTimeMillis());
-        
+
         // 如果有session,启动消费者线程
         ConcurrentHashMap<Long, Session> tempRoom = getRoom(liveId);
         List<Session> tempAdminRoom = getAdminRoom(liveId);
@@ -400,7 +404,7 @@ public class WebSocketServer {
         // 清理Session相关资源
         heartbeatCache.remove(session.getId());
         sessionLocks.remove(session.getId());
-        
+
         // 检查并清理空的直播间资源
         cleanupEmptyRoom(liveId);
     }
@@ -1621,7 +1625,7 @@ public class WebSocketServer {
     private void startConsumerThread(Long liveId) {
         consumerRunningFlags.computeIfAbsent(liveId, k -> new AtomicBoolean(false));
         AtomicBoolean runningFlag = consumerRunningFlags.get(liveId);
-        
+
         // 如果线程已经在运行,直接返回
         if (runningFlag.get()) {
             return;
@@ -1633,16 +1637,16 @@ public class WebSocketServer {
                 Thread consumerThread = new Thread(() -> {
                     PriorityBlockingQueue<QueueMessage> queue = getMessageQueue(liveId);
                     log.info("[消息队列] 启动消费者线程, liveId={}", liveId);
-                    
+
                     while (runningFlag.get()) {
                         try {
                             // 检查是否还有session,如果没有则退出
                             ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
                             List<Session> adminRoom = adminRooms.get(liveId);
-                            
-                            boolean hasSession = (room != null && !room.isEmpty()) || 
+
+                            boolean hasSession = (room != null && !room.isEmpty()) ||
                                                 (adminRoom != null && !adminRoom.isEmpty());
-                            
+
                             if (!hasSession) {
                                 log.info("[消息队列] 直播间无session,停止消费者线程, liveId={}", liveId);
                                 break;
@@ -1667,13 +1671,13 @@ public class WebSocketServer {
                             log.error("[消息队列] 消费消息异常, liveId={}", liveId, e);
                         }
                     }
-                    
+
                     // 清理资源
                     runningFlag.set(false);
                     consumerThreads.remove(liveId);
                     log.info("[消息队列] 消费者线程已停止, liveId={}", liveId);
                 }, "MessageConsumer-" + liveId);
-                
+
                 consumerThread.setDaemon(true);
                 consumerThread.start();
                 consumerThreads.put(liveId, consumerThread);
@@ -1705,22 +1709,22 @@ public class WebSocketServer {
     private boolean enqueueMessage(Long liveId, String message, boolean isAdmin) {
         PriorityBlockingQueue<QueueMessage> queue = getMessageQueue(liveId);
         AtomicLong currentSize = queueSizes.computeIfAbsent(liveId, k -> new AtomicLong(0));
-        
+
         // 计算新消息的大小
         long messageSize = message != null ? message.getBytes(StandardCharsets.UTF_8).length : 0;
-        
+
         // 检查队列条数限制
         if (!isAdmin && queue.size() >= MAX_QUEUE_SIZE) {
             log.warn("[消息队列] 队列条数已满,丢弃消息, liveId={}, queueSize={}", liveId, queue.size());
             return false;
         }
-        
+
         // 检查队列大小限制(200MB)
         long newTotalSize = currentSize.get() + messageSize;
         if (newTotalSize > MAX_QUEUE_SIZE_BYTES) {
             if (!isAdmin) {
                 // 普通消息超过大小限制,直接丢弃
-                log.warn("[消息队列] 队列大小超过限制,丢弃普通消息, liveId={}, currentSize={}MB, messageSize={}KB", 
+                log.warn("[消息队列] 队列大小超过限制,丢弃普通消息, liveId={}, currentSize={}MB, messageSize={}KB",
                         liveId, currentSize.get() / (1024.0 * 1024.0), messageSize / 1024.0);
                 return false;
             } else {
@@ -1728,13 +1732,13 @@ public class WebSocketServer {
                 long needToFree = newTotalSize - MAX_QUEUE_SIZE_BYTES;
                 long freedSize = removeMessagesToFreeSpace(queue, currentSize, needToFree, true);
                 if (freedSize < needToFree) {
-                    log.warn("[消息队列] 无法释放足够空间,管理员消息可能无法入队, liveId={}, needToFree={}KB, freed={}KB", 
+                    log.warn("[消息队列] 无法释放足够空间,管理员消息可能无法入队, liveId={}, needToFree={}KB, freed={}KB",
                             liveId, needToFree / 1024.0, freedSize / 1024.0);
                     // 即使空间不足,也尝试入队(可能会超过限制,但管理员消息优先级高)
                 }
             }
         }
-        
+
         // 如果是管理员消息且队列条数已满,移除一个普通消息
         if (isAdmin && queue.size() >= MAX_QUEUE_SIZE) {
             // 由于是优先级队列,普通消息(priority=0)会在队列末尾
@@ -1758,21 +1762,21 @@ public class WebSocketServer {
                 log.warn("[消息队列] 队列条数已满且无普通消息可移除, liveId={}", liveId);
             }
         }
-        
+
         QueueMessage queueMessage = new QueueMessage(message, isAdmin);
         queue.offer(queueMessage);
         currentSize.addAndGet(messageSize);
-        
+
         // 如果有session,确保消费者线程在运行
         ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
         List<Session> adminRoom = adminRooms.get(liveId);
-        boolean hasSession = (room != null && !room.isEmpty()) || 
+        boolean hasSession = (room != null && !room.isEmpty()) ||
                             (adminRoom != null && !adminRoom.isEmpty());
-        
+
         if (hasSession) {
             startConsumerThread(liveId);
         }
-        
+
         return true;
     }
 
@@ -1784,13 +1788,13 @@ public class WebSocketServer {
      * @param onlyRemoveNormal 是否只移除普通消息(true=只移除普通消息,false=可以移除任何消息)
      * @return 实际释放的空间(字节数)
      */
-    private long removeMessagesToFreeSpace(PriorityBlockingQueue<QueueMessage> queue, 
-                                          AtomicLong currentSize, 
-                                          long needToFree, 
+    private long removeMessagesToFreeSpace(PriorityBlockingQueue<QueueMessage> queue,
+                                          AtomicLong currentSize,
+                                          long needToFree,
                                           boolean onlyRemoveNormal) {
         long freedSize = 0;
         List<QueueMessage> toRemove = new ArrayList<>();
-        
+
         // 收集需要移除的消息(优先移除普通消息)
         Iterator<QueueMessage> iterator = queue.iterator();
         while (iterator.hasNext() && freedSize < needToFree) {
@@ -1800,7 +1804,7 @@ public class WebSocketServer {
                 freedSize += msg.getSizeBytes();
             }
         }
-        
+
         // 如果只移除普通消息但空间还不够,可以移除管理员消息
         if (onlyRemoveNormal && freedSize < needToFree) {
             iterator = queue.iterator();
@@ -1812,19 +1816,19 @@ public class WebSocketServer {
                 }
             }
         }
-        
+
         // 移除消息并更新大小
         for (QueueMessage msg : toRemove) {
             if (queue.remove(msg)) {
                 currentSize.addAndGet(-msg.getSizeBytes());
             }
         }
-        
+
         if (freedSize > 0) {
-            log.info("[消息队列] 释放队列空间, removedCount={}, freedSize={}KB", 
+            log.info("[消息队列] 释放队列空间, removedCount={}, freedSize={}KB",
                     toRemove.size(), freedSize / 1024.0);
         }
-        
+
         return freedSize;
     }
 
@@ -1841,10 +1845,10 @@ public class WebSocketServer {
     private void cleanupEmptyRoom(Long liveId) {
         ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
         List<Session> adminRoom = adminRooms.get(liveId);
-        
-        boolean hasSession = (room != null && !room.isEmpty()) || 
+
+        boolean hasSession = (room != null && !room.isEmpty()) ||
                             (adminRoom != null && !adminRoom.isEmpty());
-        
+
         if (!hasSession) {
             // 停止消费者线程
             stopConsumerThread(liveId);

+ 1 - 0
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -206,6 +206,7 @@ public class QwDataCallbackService {
                             if(StateList.getLength() > 0) {
                                 State = StateList.item(0).getTextContent();
                             }
+                            log.info("添加企微State参数: {}",State);
                             String WelcomeCode =null;
                             NodeList WelcomeCodeList = root.getElementsByTagName("WelcomeCode");
                             if(WelcomeCodeList.getLength() > 0) {

+ 1 - 1
fs-qw-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -108,7 +108,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                         "/**/*.js",
                         "/profile/**"
                 ).permitAll()
-
+                .antMatchers("/zentao/**").denyAll() // 拒绝访问
                 .antMatchers("/**").anonymous()
                 .antMatchers("/msg/**").anonymous()
                 .antMatchers("/msg/**/**").anonymous()

+ 1 - 1
fs-qwhook-sop/src/main/java/com/fs/FsQwhookSopApplication.java

@@ -11,7 +11,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableAsync
-@EnableScheduling
+//@EnableScheduling
 public class FsQwhookSopApplication {
 
 

+ 6 - 15
fs-qwhook-sop/src/main/java/com/fs/app/controller/testController.java

@@ -2,8 +2,10 @@ package com.fs.app.controller;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwUserService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -21,25 +23,14 @@ public class testController {
     testService testService;
     @Autowired
     private RedisCache redisCache;
+    @Autowired
+    private  IQwUserService qwUserService;
 
     @GetMapping("/qwHookNotify")
     public R qwHookNotify() {
 
-//        redisCache.setCacheObject("qwUserRd:"+12313+":"+12313 ,JSON.toJSONString(null),1, TimeUnit.HOURS);
-
-//
-//        List<QwUser> qwUserAllKey = qwUserMapper.getQwUserAllKey();
-//
-//
-//        int i=1;
-//
-//        for (QwUser qwUser : qwUserAllKey) {
-//            System.out.println(qwUser);
-//            i++;
-//            System.out.println("执行到第:"+i);
-//            testService.add(qwUser);
-//
-//        }
+        QwUser user = qwUserMapper.selectById(34205);
+        qwUserService.atMsg(user, "掉线提醒(登录异常)");
 
 
         return  R.ok();

+ 162 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcCallTask.java

@@ -0,0 +1,162 @@
+package com.fs.aicall.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 外呼任务对象 cc_call_task
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Data
+@Accessors(chain = true)
+public class CcCallTask implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**  */
+    private Long batchId;
+
+    /** 外呼任务的业务组 */
+    @Excel(name = "外呼任务的业务组")
+    private String groupId;
+
+    /**  */
+    @Excel(name = "")
+    private String batchName;
+
+    /** 是否启动任务, 1 启动, 0 停止 */
+    @Excel(name = "是否启动任务, 1 启动, 0 停止")
+    private Integer ifcall;
+
+    /** 外呼速率 */
+    @Excel(name = "外呼速率")
+    private Double rate;
+
+    /** 当前任务最大可用外线数 */
+    @Excel(name = "当前任务最大可用外线数")
+    private Long threadNum;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间")
+    private Long createtime;
+
+    /** 任务是否正在执行; */
+    @Excel(name = "任务是否正在执行;")
+    private Long executing;
+
+    /** 任务停止时间 */
+    @Excel(name = "任务停止时间")
+    private Long stopTime;
+
+    /** 任务创建者用户id */
+    @Excel(name = "任务创建者用户id")
+    private String userid;
+
+    /** 0 Pure manual outbound call; 1 AI outbound calling; 2 voice call notification. */
+    @Excel(name = "0 Pure manual outbound call; 1 AI outbound calling; 2 voice call notification.")
+    private Integer taskType;
+
+    /** 使用哪条线路外呼 */
+    @Excel(name = "使用哪条线路外呼")
+    private Long gatewayId;
+
+    /** 音色 */
+    @Excel(name = "音色")
+    private String voiceCode;
+
+    /** 音源 */
+    @Excel(name = "音源")
+    private String voiceSource;
+
+    /** The average ringing duration of the call; seconds */
+    @Excel(name = "The average ringing duration of the call; seconds")
+    private Double avgRingTimeLen;
+
+    /** The average pure call duration per call; seconds */
+    @Excel(name = "The average pure call duration per call; seconds")
+    private Double avgCallTalkTimeLen;
+
+    /** The duration of form filling after the call ends; seconds */
+    @Excel(name = "The duration of form filling after the call ends; seconds")
+    private Double avgCallEndProcessTimeLen;
+
+    /** 外呼节点 */
+    @Excel(name = "外呼节点")
+    private String callNodeNo;
+
+    /** 大模型底座账号的Id */
+    @Excel(name = "大模型底座账号的Id")
+    private Integer llmAccountId;
+
+    /** 播放次数 */
+    @Excel(name = "播放次数")
+    private Integer playTimes;
+
+    /** 请求参数 */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    private Map<String, Object> params = new HashMap<>();
+
+    /** 总名单量 */
+    private Integer phoneCount;
+
+    /** 未拨打名单量 */
+    private Integer noCallCount;
+
+    /** 已拨打名单量 */
+    private Integer callCount;
+
+    /** 接通名单量 */
+    private Integer connectCount;
+
+    /** 未接通名单量 */
+    private Integer noConnectCount;
+
+    /** 实际接通率 */
+    private Double realConnectRate;
+
+    /** 预估接通率 (百分数格式)*/
+    @Excel(name = "预估接通率")
+    private Integer conntectRate;
+
+//    /** tts provider */
+//    private String provider;
+
+    /** tts provider */
+    private String asrProvider;
+
+    /** aiTransferType */
+    private String aiTransferType;
+
+    /** aiTransferData */
+    private String aiTransferData;
+
+    /** aiTransferGroupId */
+    private String aiTransferGroupId;
+
+    /** aiTransferGatewayId */
+    private String aiTransferGatewayId;
+
+    /** aiTransferGatewayDestNumber */
+    private String aiTransferGatewayDestNumber;
+
+    /** aiTransferExtNumber */
+    private String aiTransferExtNumber;
+
+    /** 播放次数 */
+    @Excel(name = "播放次数")
+    private Integer autoStop;
+
+    /** ivrId */
+    private String ivrId;
+
+    /** 是否允许删除 */
+    private Integer allowDel;
+
+}

+ 68 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcLlmAgentAccount.java

@@ -0,0 +1,68 @@
+package com.fs.aicall.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 机器人参数配置对象 cc_llm_agent_account
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Data
+public class CcLlmAgentAccount implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** 机器人名称 */
+    @Excel(name = "机器人名称")
+    private String name;
+
+    /** 机器人配置信息 */
+    @Excel(name = "机器人配置信息")
+    private String accountJson;
+
+    /** Entity class for storing account config info. */
+    @Excel(name = "Entity class for storing account config info.")
+    private String accountEntity;
+
+    /** 实现类 */
+    @Excel(name = "实现类")
+    private String providerClassName;
+
+    /** 是否打断(1:是,0:否) */
+    @Excel(name = "是否打断(1:是,0:否)")
+    private Integer interruptFlag;
+
+    /** 打断关键词列表 */
+    @Excel(name = "打断关键词列表")
+    private String interruptKeywords;
+
+    /** 打断忽略关键字列表 */
+    @Excel(name = "打断忽略关键字列表")
+    private String interruptIgnoreKeywords;
+
+    /** 客户意向提示词 */
+    @Excel(name = "客户意向提示词")
+    private String intentionTips;
+
+    /** 模型并发数 */
+    @Excel(name = "模型并发数")
+    private Integer concurrentNum;
+
+    /** 转人工的按键支持 */
+    @Excel(name = "转人工的按键支持")
+    private String transferManualDigit;
+
+    /** 知识库id */
+    private Integer kbCatId;
+
+    /** 模型ID列表,用于IN查询 */
+    private List<Long> modelIds;
+
+}

+ 29 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcLlmAgentProvider.java

@@ -0,0 +1,29 @@
+package com.fs.aicall.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 大模型实现类列表对象 cc_llm_agent_provider
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Data
+public class CcLlmAgentProvider implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** 实现类 */
+    @Excel(name = "实现类")
+    private String providerClassName;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String note;
+
+}

+ 31 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcLlmKb.java

@@ -0,0 +1,31 @@
+package com.fs.aicall.domain;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 知识库内容对象 cc_llm_kb
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+@Data
+@Accessors(chain = true)
+public class CcLlmKb implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Long id;
+
+    /** kb title */
+    private String title;
+
+    /** 答案 */
+    private String content;
+
+    /** kb cat id. */
+    private Long catId;
+
+}

+ 31 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcLlmKbCat.java

@@ -0,0 +1,31 @@
+package com.fs.aicall.domain;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 知识库对象 cc_llm_kb_cat
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+@Data
+@Accessors(chain = true)
+public class CcLlmKbCat implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Long id;
+
+    /** 分类 */
+    private String cat;
+
+    /** 描述 */
+    private String description;
+
+    /** 内容数量 */
+    private Integer contentCount;
+
+}

+ 45 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcParams.java

@@ -0,0 +1,45 @@
+package com.fs.aicall.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * callcenter参数配置对象 cc_params
+ * 
+ * @author ruoyi
+ * @date 2025-04-21
+ */
+@Data
+@Accessors(chain = true)
+public class CcParams implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /** 主键id */
+    private Long id;
+
+    /** 参数名 */
+    @Excel(name = "参数名")
+    private String paramName;
+
+    /** 参数编码 */
+    @Excel(name = "参数编码")
+    private String paramCode;
+
+    /** 参数值 */
+    @Excel(name = "参数值")
+    private String paramValue;
+
+    /** 参数类型 */
+    @Excel(name = "参数类型")
+    private String paramType;
+
+    /** 是否在网页上隐藏参数值 */
+    @Excel(name = "隐藏参数值")
+    private int hideValue;
+
+    /** 是否允许编辑 */
+    @Excel(name = "是否允许编辑")
+    private int allowEdit;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/aicall/domain/CompanyBindAiModel.java

@@ -0,0 +1,38 @@
+package com.fs.aicall.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 销售公司与AI模型绑定表
+ * 
+ * @author ruoyi
+ * @date 2026-03-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyBindAiModel extends BaseDomain
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 销售公司ID */
+    private Long companyId;
+
+    /** 模型ID */
+    private Long modelId;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+}

+ 82 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcCallTaskMapper.java

@@ -0,0 +1,82 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CcCallTask;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 外呼任务Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+public interface CcCallTaskMapper 
+{
+    /**
+     * 查询外呼任务
+     * 
+     * @param batchId 外呼任务主键
+     * @return 外呼任务
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public CcCallTask selectCcCallTaskByBatchId(Long batchId);
+
+    /**
+     * 查询外呼任务列表
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 外呼任务集合
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public List<CcCallTask> selectCcCallTaskList(CcCallTask ccCallTask);
+
+    /**
+     * 新增外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int insertCcCallTask(CcCallTask ccCallTask);
+
+    /**
+     * 修改外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int updateCcCallTask(CcCallTask ccCallTask);
+
+    /**
+     * 删除外呼任务
+     * 
+     * @param batchId 外呼任务主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcCallTaskByBatchId(Long batchId);
+
+    /**
+     * 批量删除外呼任务
+     * 
+     * @param batchIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcCallTaskByBatchIds(String[] batchIds);
+
+    /**
+     * 根据任务名称获取任务
+     * @param batchName
+     * @return
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    CcCallTask selectCcCallTaskByBatchName(@Param("batchName") String batchName, @Param("taskType") Integer taskType);
+
+    @DataSource(DataSourceType.EASYCALL)
+    void bakCallTaskByBatchId(Long batchId);
+}

+ 70 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcLlmAgentAccountMapper.java

@@ -0,0 +1,70 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CcLlmAgentAccount;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+
+import java.util.List;
+
+/**
+ * 机器人参数配置Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+public interface CcLlmAgentAccountMapper 
+{
+    /**
+     * 查询机器人参数配置
+     * 
+     * @param id 机器人参数配置主键
+     * @return 机器人参数配置
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public CcLlmAgentAccount selectCcLlmAgentAccountById(Integer id);
+
+    /**
+     * 查询机器人参数配置列表
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 机器人参数配置集合
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public List<CcLlmAgentAccount> selectCcLlmAgentAccountList(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 新增机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int insertCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 修改机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int updateCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 删除机器人参数配置
+     * 
+     * @param id 机器人参数配置主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmAgentAccountById(Integer id);
+
+    /**
+     * 批量删除机器人参数配置
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmAgentAccountByIds(String[] ids);
+}

+ 71 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcLlmAgentProviderMapper.java

@@ -0,0 +1,71 @@
+package com.fs.aicall.mapper;
+
+
+import com.fs.aicall.domain.CcLlmAgentProvider;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+
+import java.util.List;
+
+/**
+ * 大模型实现类列表Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+public interface CcLlmAgentProviderMapper 
+{
+    /**
+     * 查询大模型实现类列表
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 大模型实现类列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public CcLlmAgentProvider selectCcLlmAgentProviderById(Integer id);
+
+    /**
+     * 查询大模型实现类列表列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 大模型实现类列表集合
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public List<CcLlmAgentProvider> selectCcLlmAgentProviderList(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 新增大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int insertCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 修改大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int updateCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 删除大模型实现类列表
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmAgentProviderById(Integer id);
+
+    /**
+     * 批量删除大模型实现类列表
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmAgentProviderByIds(String[] ids);
+}

+ 70 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcLlmKbCatMapper.java

@@ -0,0 +1,70 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CcLlmKbCat;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+
+import java.util.List;
+
+/**
+ * 知识库Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+public interface CcLlmKbCatMapper 
+{
+    /**
+     * 查询知识库
+     * 
+     * @param id 知识库主键
+     * @return 知识库
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public CcLlmKbCat selectCcLlmKbCatById(Long id);
+
+    /**
+     * 查询知识库列表
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 知识库集合
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public List<CcLlmKbCat> selectCcLlmKbCatList(CcLlmKbCat ccLlmKbCat);
+
+    /**
+     * 新增知识库
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int insertCcLlmKbCat(CcLlmKbCat ccLlmKbCat);
+
+    /**
+     * 修改知识库
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int updateCcLlmKbCat(CcLlmKbCat ccLlmKbCat);
+
+    /**
+     * 删除知识库
+     * 
+     * @param id 知识库主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmKbCatById(Long id);
+
+    /**
+     * 批量删除知识库
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmKbCatByIds(String[] ids);
+}

+ 79 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcLlmKbMapper.java

@@ -0,0 +1,79 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CcLlmKb;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+
+import java.util.List;
+
+/**
+ * 知识库内容Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+public interface CcLlmKbMapper 
+{
+    /**
+     * 查询知识库内容
+     * 
+     * @param id 知识库内容主键
+     * @return 知识库内容
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public CcLlmKb selectCcLlmKbById(Long id);
+
+    /**
+     * 查询知识库内容列表
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 知识库内容集合
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public List<CcLlmKb> selectCcLlmKbList(CcLlmKb ccLlmKb);
+
+    /**
+     * 新增知识库内容
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int insertCcLlmKb(CcLlmKb ccLlmKb);
+
+    /**
+     * 修改知识库内容
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int updateCcLlmKb(CcLlmKb ccLlmKb);
+
+    /**
+     * 删除知识库内容
+     * 
+     * @param id 知识库内容主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmKbById(Long id);
+
+    /**
+     * 批量删除知识库内容
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcLlmKbByIds(String[] ids);
+
+    @DataSource(DataSourceType.EASYCALL)
+    Integer selectCountByCatId(Long catId);
+
+    @DataSource(DataSourceType.EASYCALL)
+    void deleteCcLlmKbByCatId(Long catId);
+
+    @DataSource(DataSourceType.EASYCALL)
+    void insertBatch(List<CcLlmKb> ccLlmKbList);
+}

+ 79 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcParamsMapper.java

@@ -0,0 +1,79 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CcParams;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * callcenter参数配置Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2025-04-21
+ */
+public interface CcParamsMapper 
+{
+    /**
+     * 查询callcenter参数配置
+     * 
+     * @param id callcenter参数配置主键
+     * @return callcenter参数配置
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public CcParams selectCcParamsById(Long id);
+
+    /**
+     * 查询callcenter参数配置列表
+     * 
+     * @param ccParams callcenter参数配置
+     * @return callcenter参数配置集合
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public List<CcParams> selectCcParamsList(CcParams ccParams);
+
+    /**
+     * 新增callcenter参数配置
+     * 
+     * @param ccParams callcenter参数配置
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int insertCcParams(CcParams ccParams);
+
+    /**
+     * 修改callcenter参数配置
+     * 
+     * @param ccParams callcenter参数配置
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int updateCcParams(CcParams ccParams);
+
+    /**
+     * 删除callcenter参数配置
+     * 
+     * @param id callcenter参数配置主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcParamsById(Long id);
+
+    /**
+     * 批量删除callcenter参数配置
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    public int deleteCcParamsByIds(String[] ids);
+
+    /**
+     *
+     * @param paramCode
+     * @param paramValue
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    void updateParamsValue(@Param("paramCode") String paramCode, @Param("paramValue") String paramValue);
+}

+ 97 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CompanyBindAiModelMapper.java

@@ -0,0 +1,97 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CompanyBindAiModel;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 销售公司与AI模型绑定表Mapper接口
+ * 
+ * @author ruoyi
+ * @date 2026-03-20
+ */
+@Mapper
+public interface CompanyBindAiModelMapper
+{
+    /**
+     * 查询销售公司与AI模型绑定表
+     * 
+     * @param id 销售公司与AI模型绑定表主键
+     * @return 销售公司与AI模型绑定表
+     */
+    public CompanyBindAiModel selectCompanyBindAiModelById(Long id);
+
+    /**
+     * 查询销售公司与AI模型绑定表列表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 销售公司与AI模型绑定表集合
+     */
+    public List<CompanyBindAiModel> selectCompanyBindAiModelList(CompanyBindAiModel companyBindAiModel);
+
+    /**
+     * 新增销售公司与AI模型绑定表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 结果
+     */
+    public int insertCompanyBindAiModel(CompanyBindAiModel companyBindAiModel);
+
+    /**
+     * 修改销售公司与AI模型绑定表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 结果
+     */
+    public int updateCompanyBindAiModel(CompanyBindAiModel companyBindAiModel);
+
+    /**
+     * 删除销售公司与AI模型绑定表
+     * 
+     * @param id 销售公司与AI模型绑定表主键
+     * @return 结果
+     */
+    public int deleteCompanyBindAiModelById(Long id);
+
+    /**
+     * 批量删除销售公司与AI模型绑定表
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCompanyBindAiModelByIds(Long[] ids);
+
+    /**
+     * 根据公司ID查询绑定的模型ID列表
+     * 
+     * @param companyId 公司ID
+     * @return 模型ID列表
+     */
+    public List<Long> selectModelIdsByCompanyId(Long companyId);
+
+    /**
+     * 根据模型ID删除绑定关系
+     * 
+     * @param modelId 模型ID
+     * @return 结果
+     */
+    public int deleteCompanyBindAiModelByModelId(Long modelId);
+
+    /**
+     * 批量插入绑定关系
+     * 
+     * @param companyId 公司ID
+     * @param modelIds 模型ID列表
+     * @return 结果
+     */
+    public int batchInsertCompanyBindAiModel(@Param("companyId") Long companyId, @Param("modelIds") List<Long> modelIds);
+
+    /**
+     * 批量删除·1、公司ID、模型ID
+     * @param companyId
+     * @param modelIds 模型ID列表
+     * **/
+    void deleteBindAiModelByCompanyIdAndModelIds(@Param("companyId") Long companyId, @Param("modelIds") String[] modelIds);
+}

+ 71 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcCallTaskService.java

@@ -0,0 +1,71 @@
+package com.fs.aicall.service;
+
+import com.fs.aicall.domain.CcCallTask;
+
+import java.util.List;
+
+/**
+ * 外呼任务Service接口
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+public interface ICcCallTaskService 
+{
+    /**
+     * 查询外呼任务
+     * 
+     * @param batchId 外呼任务主键
+     * @return 外呼任务
+     */
+    public CcCallTask selectCcCallTaskByBatchId(Long batchId);
+
+    /**
+     * 查询外呼任务列表
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 外呼任务集合
+     */
+    public List<CcCallTask> selectCcCallTaskList(CcCallTask ccCallTask);
+
+    /**
+     * 新增外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    public int insertCcCallTask(CcCallTask ccCallTask);
+
+    /**
+     * 修改外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    public int updateCcCallTask(CcCallTask ccCallTask);
+
+    /**
+     * 批量删除外呼任务
+     * 
+     * @param batchIds 需要删除的外呼任务主键集合
+     * @return 结果
+     */
+    public int deleteCcCallTaskByBatchIds(String batchIds);
+
+    /**
+     * 删除外呼任务信息
+     * 
+     * @param batchId 外呼任务主键
+     * @return 结果
+     */
+    public int deleteCcCallTaskByBatchId(Long batchId);
+
+    /**
+     * 根据任务名称获取任务
+     * @param batchName
+     * @return
+     */
+    CcCallTask selectCcCallTaskByBatchName(String batchName, Integer taskType);
+
+    void bakCallTaskByBatchId(Long batchId);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcLlmAgentAccountService.java

@@ -0,0 +1,62 @@
+package com.fs.aicall.service;
+
+import com.fs.aicall.domain.CcLlmAgentAccount;
+
+import java.util.List;
+
+/**
+ * 机器人参数配置Service接口
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+public interface ICcLlmAgentAccountService 
+{
+    /**
+     * 查询机器人参数配置
+     * 
+     * @param id 机器人参数配置主键
+     * @return 机器人参数配置
+     */
+    public CcLlmAgentAccount selectCcLlmAgentAccountById(Integer id);
+
+    /**
+     * 查询机器人参数配置列表
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 机器人参数配置集合
+     */
+    public List<CcLlmAgentAccount> selectCcLlmAgentAccountList(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 新增机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    public int insertCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 修改机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    public int updateCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount);
+
+    /**
+     * 批量删除机器人参数配置
+     * 
+     * @param ids 需要删除的机器人参数配置主键集合
+     * @return 结果
+     */
+    public int deleteCcLlmAgentAccountByIds(String ids);
+
+    /**
+     * 删除机器人参数配置信息
+     * 
+     * @param id 机器人参数配置主键
+     * @return 结果
+     */
+    public int deleteCcLlmAgentAccountById(Integer id);
+}

+ 63 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcLlmAgentProviderService.java

@@ -0,0 +1,63 @@
+package com.fs.aicall.service;
+
+
+import com.fs.aicall.domain.CcLlmAgentProvider;
+
+import java.util.List;
+
+/**
+ * 大模型实现类列表Service接口
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+public interface ICcLlmAgentProviderService 
+{
+    /**
+     * 查询大模型实现类列表
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 大模型实现类列表
+     */
+    public CcLlmAgentProvider selectCcLlmAgentProviderById(Integer id);
+
+    /**
+     * 查询大模型实现类列表列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 大模型实现类列表集合
+     */
+    public List<CcLlmAgentProvider> selectCcLlmAgentProviderList(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 新增大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    public int insertCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 修改大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    public int updateCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider);
+
+    /**
+     * 批量删除大模型实现类列表
+     * 
+     * @param ids 需要删除的大模型实现类列表主键集合
+     * @return 结果
+     */
+    public int deleteCcLlmAgentProviderByIds(String ids);
+
+    /**
+     * 删除大模型实现类列表信息
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 结果
+     */
+    public int deleteCcLlmAgentProviderById(Integer id);
+}

+ 71 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcLlmKbCatService.java

@@ -0,0 +1,71 @@
+package com.fs.aicall.service;
+
+
+import com.fs.aicall.domain.CcLlmKbCat;
+
+import java.util.List;
+
+/**
+ * 知识库Service接口
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+public interface ICcLlmKbCatService 
+{
+    /**
+     * 查询知识库
+     * 
+     * @param id 知识库主键
+     * @return 知识库
+     */
+    public CcLlmKbCat selectCcLlmKbCatById(Long id);
+
+    /**
+     * 查询知识库列表
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 知识库集合
+     */
+    public List<CcLlmKbCat> selectCcLlmKbCatList(CcLlmKbCat ccLlmKbCat);
+
+    /**
+     * 新增知识库
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 结果
+     */
+    public int insertCcLlmKbCat(CcLlmKbCat ccLlmKbCat);
+
+    /**
+     * 修改知识库
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 结果
+     */
+    public int updateCcLlmKbCat(CcLlmKbCat ccLlmKbCat);
+
+    /**
+     * 批量删除知识库
+     * 
+     * @param ids 需要删除的知识库主键集合
+     * @return 结果
+     */
+    public int deleteCcLlmKbCatByIds(String ids);
+
+    /**
+     * 删除知识库信息
+     * 
+     * @param id 知识库主键
+     * @return 结果
+     */
+    public int deleteCcLlmKbCatById(Long id);
+
+    /**
+     *
+     * @param id
+     * @param cat
+     * @return
+     */
+    CcLlmKbCat selectCcLlmKbCatByCat(Long id, String cat);
+}

+ 70 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcLlmKbService.java

@@ -0,0 +1,70 @@
+package com.fs.aicall.service;
+
+import com.fs.aicall.domain.CcLlmKb;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * 知识库内容Service接口
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+public interface ICcLlmKbService 
+{
+    /**
+     * 查询知识库内容
+     * 
+     * @param id 知识库内容主键
+     * @return 知识库内容
+     */
+    public CcLlmKb selectCcLlmKbById(Long id);
+
+    /**
+     * 查询知识库内容列表
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 知识库内容集合
+     */
+    public List<CcLlmKb> selectCcLlmKbList(CcLlmKb ccLlmKb);
+
+    /**
+     * 新增知识库内容
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 结果
+     */
+    public int insertCcLlmKb(CcLlmKb ccLlmKb);
+
+    /**
+     * 修改知识库内容
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 结果
+     */
+    public int updateCcLlmKb(CcLlmKb ccLlmKb);
+
+    /**
+     * 批量删除知识库内容
+     * 
+     * @param ids 需要删除的知识库内容主键集合
+     * @return 结果
+     */
+    public int deleteCcLlmKbByIds(String ids);
+
+    /**
+     * 删除知识库内容信息
+     * 
+     * @param id 知识库内容主键
+     * @return 结果
+     */
+    public int deleteCcLlmKbById(Long id);
+
+    Integer selectCountByCatId(Long catId);
+
+    String importData(MultipartFile file,  Long fixedCatId) throws Exception;
+
+    CcLlmKb selectCcLlmKbContentByTitle(Long id, String title, Long catId);
+    
+}

+ 83 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcParamsService.java

@@ -0,0 +1,83 @@
+package com.fs.aicall.service;
+
+
+import com.fs.aicall.domain.CcParams;
+
+import java.util.List;
+
+/**
+ * callcenter参数配置Service接口
+ * 
+ * @author ruoyi
+ * @date 2025-04-21
+ */
+public interface ICcParamsService 
+{
+    /**
+     * 查询callcenter参数配置
+     * 
+     * @param id callcenter参数配置主键
+     * @return callcenter参数配置
+     */
+    public CcParams selectCcParamsById(Long id);
+
+    /**
+     * 查询callcenter参数配置列表
+     * 
+     * @param ccParams callcenter参数配置
+     * @return callcenter参数配置集合
+     */
+    public List<CcParams> selectCcParamsList(CcParams ccParams);
+
+    /**
+     * 新增callcenter参数配置
+     * 
+     * @param ccParams callcenter参数配置
+     * @return 结果
+     */
+    public int insertCcParams(CcParams ccParams);
+
+    /**
+     * 修改callcenter参数配置
+     * 
+     * @param ccParams callcenter参数配置
+     * @return 结果
+     */
+    public int updateCcParams(CcParams ccParams);
+
+    /**
+     * 批量删除callcenter参数配置
+     * 
+     * @param ids 需要删除的callcenter参数配置主键集合
+     * @return 结果
+     */
+    public int deleteCcParamsByIds(String ids);
+
+    /**
+     * 删除callcenter参数配置信息
+     * 
+     * @param id callcenter参数配置主键
+     * @return 结果
+     */
+    public int deleteCcParamsById(Long id);
+
+    /**
+     * 根据参数编号获取参数值
+     * @param paramCode
+     * @return
+     */
+    String getParamValueByCode(String paramCode, String defaultValue);
+
+    /**
+     * 更新参数值
+     * @param paramCode
+     * @param paramValue
+     */
+    void updateParamsValue(String paramCode, String paramValue);
+
+    /**
+     *
+     * @return
+     */
+    String reloadParams();
+}

+ 114 - 0
fs-service/src/main/java/com/fs/aicall/service/ICompanyBindAiModelService.java

@@ -0,0 +1,114 @@
+package com.fs.aicall.service;
+
+import com.fs.aicall.domain.CompanyBindAiModel;
+
+import java.util.List;
+
+/**
+ * 销售公司与AI模型绑定表Service接口
+ * 
+ * @author ruoyi
+ * @date 2026-03-20
+ */
+public interface ICompanyBindAiModelService
+{
+    /**
+     * 查询销售公司与AI模型绑定表
+     * 
+     * @param id 销售公司与AI模型绑定表主键
+     * @return 销售公司与AI模型绑定表
+     */
+    public CompanyBindAiModel selectCompanyBindAiModelById(Long id);
+
+    /**
+     * 查询销售公司与AI模型绑定表列表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 销售公司与AI模型绑定表集合
+     */
+    public List<CompanyBindAiModel> selectCompanyBindAiModelList(CompanyBindAiModel companyBindAiModel);
+
+    /**
+     * 新增销售公司与AI模型绑定表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 结果
+     */
+    public int insertCompanyBindAiModel(CompanyBindAiModel companyBindAiModel);
+
+    /**
+     * 修改销售公司与AI模型绑定表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 结果
+     */
+    public int updateCompanyBindAiModel(CompanyBindAiModel companyBindAiModel);
+
+    /**
+     * 删除销售公司与AI模型绑定表
+     * 
+     * @param id 销售公司与AI模型绑定表主键
+     * @return 结果
+     */
+    public int deleteCompanyBindAiModelById(Long id);
+
+    /**
+     * 批量删除销售公司与AI模型绑定表
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteCompanyBindAiModelByIds(Long[] ids);
+
+    /**
+     * 根据公司ID查询绑定的模型ID列表
+     * 
+     * @param companyId 公司ID
+     * @return 模型ID列表
+     */
+    public List<Long> selectModelIdsByCompanyId(Long companyId);
+
+    /**
+     * 根据模型ID删除绑定关系
+     * 
+     * @param modelId 模型ID
+     * @return 结果
+     */
+    public int deleteCompanyBindAiModelByModelId(Long modelId);
+
+    /**
+     * 批量插入绑定关系
+     * 
+     * @param companyId 公司ID
+     * @param modelIds 模型ID列表
+     * @return 结果
+     */
+    public int batchInsertCompanyBindAiModel(Long companyId, List<Long> modelIds);
+
+    /**
+     * 为模型添加公司绑定
+     * 
+     * @param modelId 模型ID
+     * @param companyId 公司ID
+     * @return 结果
+     */
+    public int bindCompanyToModel(Long modelId, Long companyId);
+
+    /**
+     * 为模型批量添加公司绑定
+     * 
+     * @param modelId 模型ID
+     * @param companyIds 公司ID列表
+     * @return 结果
+     */
+    public int batchBindCompaniesToModel(Long modelId, List<Long> companyIds);
+
+    /**
+     * 批量删除对应公司模型关系表
+     *
+     * @param companyId 公司ID
+     * @param modelIds 模型ID列表
+     * @return 结果
+     */
+    void deleteBindAiModelByCompanyIdAndModelIds(Long companyId, String modelIds);
+}

+ 118 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcCallTaskServiceImpl.java

@@ -0,0 +1,118 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcCallTask;
+import com.fs.aicall.mapper.CcCallTaskMapper;
+import com.fs.aicall.service.ICcCallTaskService;
+import com.fs.common.core.text.Convert;
+import com.fs.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 外呼任务Service业务层处理
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Service
+public class CcCallTaskServiceImpl implements ICcCallTaskService
+{
+    @Autowired
+    private CcCallTaskMapper ccCallTaskMapper;
+
+    /**
+     * 查询外呼任务
+     * 
+     * @param batchId 外呼任务主键
+     * @return 外呼任务
+     */
+    @Override
+    public CcCallTask selectCcCallTaskByBatchId(Long batchId)
+    {
+        return ccCallTaskMapper.selectCcCallTaskByBatchId(batchId);
+    }
+
+    /**
+     * 查询外呼任务列表
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 外呼任务
+     */
+    @Override
+    public List<CcCallTask> selectCcCallTaskList(CcCallTask ccCallTask)
+    {
+
+        Map<String, Object> params = ccCallTask.getParams();
+        if (null != params.get("createTimeStart")
+                && !"".equals(params.get("createTimeStart"))) {
+            params.put("createTimeStart", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("createTimeStart")).getTime());
+        }
+        if (null != params.get("createTimeEnd")
+                && !"".equals(params.get("createTimeEnd"))) {
+            params.put("createTimeEnd", DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", (String)params.get("createTimeEnd")).getTime());
+        }
+        ccCallTask.setParams(params);
+        return ccCallTaskMapper.selectCcCallTaskList(ccCallTask);
+    }
+
+    /**
+     * 新增外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    @Override
+    public int insertCcCallTask(CcCallTask ccCallTask)
+    {
+        return ccCallTaskMapper.insertCcCallTask(ccCallTask);
+    }
+
+    /**
+     * 修改外呼任务
+     * 
+     * @param ccCallTask 外呼任务
+     * @return 结果
+     */
+    @Override
+    public int updateCcCallTask(CcCallTask ccCallTask)
+    {
+        return ccCallTaskMapper.updateCcCallTask(ccCallTask);
+    }
+
+    /**
+     * 批量删除外呼任务
+     * 
+     * @param batchIds 需要删除的外呼任务主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcCallTaskByBatchIds(String batchIds)
+    {
+        return ccCallTaskMapper.deleteCcCallTaskByBatchIds(Convert.toStrArray(batchIds));
+    }
+
+    /**
+     * 删除外呼任务信息
+     * 
+     * @param batchId 外呼任务主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcCallTaskByBatchId(Long batchId)
+    {
+        return ccCallTaskMapper.deleteCcCallTaskByBatchId(batchId);
+    }
+
+    @Override
+    public CcCallTask selectCcCallTaskByBatchName(String batchName, Integer taskType) {
+        return ccCallTaskMapper.selectCcCallTaskByBatchName(batchName, taskType);
+    }
+
+    @Override
+    public void bakCallTaskByBatchId(Long batchId) {
+        ccCallTaskMapper.bakCallTaskByBatchId(batchId);
+    }
+}

+ 95 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmAgentAccountServiceImpl.java

@@ -0,0 +1,95 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcLlmAgentAccount;
+import com.fs.aicall.mapper.CcLlmAgentAccountMapper;
+import com.fs.aicall.service.ICcLlmAgentAccountService;
+import com.fs.common.core.text.Convert;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 机器人参数配置Service业务层处理
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Service
+public class CcLlmAgentAccountServiceImpl implements ICcLlmAgentAccountService
+{
+    @Autowired
+    private CcLlmAgentAccountMapper ccLlmAgentAccountMapper;
+
+    /**
+     * 查询机器人参数配置
+     * 
+     * @param id 机器人参数配置主键
+     * @return 机器人参数配置
+     */
+    @Override
+    public CcLlmAgentAccount selectCcLlmAgentAccountById(Integer id)
+    {
+        return ccLlmAgentAccountMapper.selectCcLlmAgentAccountById(id);
+    }
+
+    /**
+     * 查询机器人参数配置列表
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 机器人参数配置
+     */
+    @Override
+    public List<CcLlmAgentAccount> selectCcLlmAgentAccountList(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        return ccLlmAgentAccountMapper.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+    }
+
+    /**
+     * 新增机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    @Override
+    public int insertCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        return ccLlmAgentAccountMapper.insertCcLlmAgentAccount(ccLlmAgentAccount);
+    }
+
+    /**
+     * 修改机器人参数配置
+     * 
+     * @param ccLlmAgentAccount 机器人参数配置
+     * @return 结果
+     */
+    @Override
+    public int updateCcLlmAgentAccount(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        return ccLlmAgentAccountMapper.updateCcLlmAgentAccount(ccLlmAgentAccount);
+    }
+
+    /**
+     * 批量删除机器人参数配置
+     * 
+     * @param ids 需要删除的机器人参数配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmAgentAccountByIds(String ids)
+    {
+        return ccLlmAgentAccountMapper.deleteCcLlmAgentAccountByIds(Convert.toStrArray(ids));
+    }
+
+    /**
+     * 删除机器人参数配置信息
+     * 
+     * @param id 机器人参数配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmAgentAccountById(Integer id)
+    {
+        return ccLlmAgentAccountMapper.deleteCcLlmAgentAccountById(id);
+    }
+}

+ 95 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmAgentProviderServiceImpl.java

@@ -0,0 +1,95 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcLlmAgentProvider;
+import com.fs.aicall.mapper.CcLlmAgentProviderMapper;
+import com.fs.aicall.service.ICcLlmAgentProviderService;
+import com.fs.common.core.text.Convert;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 大模型实现类列表Service业务层处理
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Service
+public class CcLlmAgentProviderServiceImpl implements ICcLlmAgentProviderService
+{
+    @Autowired
+    private CcLlmAgentProviderMapper ccLlmAgentProviderMapper;
+
+    /**
+     * 查询大模型实现类列表
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 大模型实现类列表
+     */
+    @Override
+    public CcLlmAgentProvider selectCcLlmAgentProviderById(Integer id)
+    {
+        return ccLlmAgentProviderMapper.selectCcLlmAgentProviderById(id);
+    }
+
+    /**
+     * 查询大模型实现类列表列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 大模型实现类列表
+     */
+    @Override
+    public List<CcLlmAgentProvider> selectCcLlmAgentProviderList(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return ccLlmAgentProviderMapper.selectCcLlmAgentProviderList(ccLlmAgentProvider);
+    }
+
+    /**
+     * 新增大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    @Override
+    public int insertCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return ccLlmAgentProviderMapper.insertCcLlmAgentProvider(ccLlmAgentProvider);
+    }
+
+    /**
+     * 修改大模型实现类列表
+     * 
+     * @param ccLlmAgentProvider 大模型实现类列表
+     * @return 结果
+     */
+    @Override
+    public int updateCcLlmAgentProvider(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return ccLlmAgentProviderMapper.updateCcLlmAgentProvider(ccLlmAgentProvider);
+    }
+
+    /**
+     * 批量删除大模型实现类列表
+     * 
+     * @param ids 需要删除的大模型实现类列表主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmAgentProviderByIds(String ids)
+    {
+        return ccLlmAgentProviderMapper.deleteCcLlmAgentProviderByIds(Convert.toStrArray(ids));
+    }
+
+    /**
+     * 删除大模型实现类列表信息
+     * 
+     * @param id 大模型实现类列表主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmAgentProviderById(Integer id)
+    {
+        return ccLlmAgentProviderMapper.deleteCcLlmAgentProviderById(id);
+    }
+}

+ 113 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmKbCatServiceImpl.java

@@ -0,0 +1,113 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcLlmKbCat;
+import com.fs.aicall.mapper.CcLlmKbCatMapper;
+import com.fs.aicall.service.ICcLlmKbCatService;
+import com.fs.common.core.text.Convert;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * 知识库Service业务层处理
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+@Service
+public class CcLlmKbCatServiceImpl implements ICcLlmKbCatService
+{
+    @Autowired
+    private CcLlmKbCatMapper ccLlmKbCatMapper;
+
+    /**
+     * 查询知识库
+     * 
+     * @param id 知识库主键
+     * @return 知识库
+     */
+    @Override
+    public CcLlmKbCat selectCcLlmKbCatById(Long id)
+    {
+        return ccLlmKbCatMapper.selectCcLlmKbCatById(id);
+    }
+
+    /**
+     * 查询知识库列表
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 知识库
+     */
+    @Override
+    public List<CcLlmKbCat> selectCcLlmKbCatList(CcLlmKbCat ccLlmKbCat)
+    {
+        return ccLlmKbCatMapper.selectCcLlmKbCatList(ccLlmKbCat);
+    }
+
+    /**
+     * 新增知识库
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 结果
+     */
+    @Override
+    public int insertCcLlmKbCat(CcLlmKbCat ccLlmKbCat)
+    {
+        return ccLlmKbCatMapper.insertCcLlmKbCat(ccLlmKbCat);
+    }
+
+    /**
+     * 修改知识库
+     * 
+     * @param ccLlmKbCat 知识库
+     * @return 结果
+     */
+    @Override
+    public int updateCcLlmKbCat(CcLlmKbCat ccLlmKbCat)
+    {
+        return ccLlmKbCatMapper.updateCcLlmKbCat(ccLlmKbCat);
+    }
+
+    /**
+     * 批量删除知识库
+     * 
+     * @param ids 需要删除的知识库主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmKbCatByIds(String ids)
+    {
+        return ccLlmKbCatMapper.deleteCcLlmKbCatByIds(Convert.toStrArray(ids));
+    }
+
+    /**
+     * 删除知识库信息
+     * 
+     * @param id 知识库主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmKbCatById(Long id)
+    {
+        return ccLlmKbCatMapper.deleteCcLlmKbCatById(id);
+    }
+
+    @Override
+    public CcLlmKbCat selectCcLlmKbCatByCat(Long id, String cat) {
+        List<CcLlmKbCat> list = ccLlmKbCatMapper.selectCcLlmKbCatList(new CcLlmKbCat().setCat(cat));
+        if (!CollectionUtils.isEmpty(list)) {
+            if (null == id) {
+                return list.get(0);
+            } else {
+                for (CcLlmKbCat data: list) {
+                    if (data.getId() != id) {
+                        return data;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+}

+ 154 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmKbServiceImpl.java

@@ -0,0 +1,154 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcLlmKb;
+import com.fs.aicall.mapper.CcLlmKbMapper;
+import com.fs.aicall.service.ICcLlmKbService;
+import com.fs.aicall.utils.StringUtils;
+import com.fs.aicall.utils.XSSFUtils;
+import com.fs.common.core.text.Convert;
+import com.fs.common.exception.ServiceException;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 知识库内容Service业务层处理
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+@Service
+public class CcLlmKbServiceImpl implements ICcLlmKbService
+{
+    @Autowired
+    private CcLlmKbMapper ccLlmKbMapper;
+
+    /**
+     * 查询知识库内容
+     * 
+     * @param id 知识库内容主键
+     * @return 知识库内容
+     */
+    @Override
+    public CcLlmKb selectCcLlmKbById(Long id)
+    {
+        return ccLlmKbMapper.selectCcLlmKbById(id);
+    }
+
+    /**
+     * 查询知识库内容列表
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 知识库内容
+     */
+    @Override
+    public List<CcLlmKb> selectCcLlmKbList(CcLlmKb ccLlmKb)
+    {
+        return ccLlmKbMapper.selectCcLlmKbList(ccLlmKb);
+    }
+
+    /**
+     * 新增知识库内容
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 结果
+     */
+    @Override
+    public int insertCcLlmKb(CcLlmKb ccLlmKb)
+    {
+        return ccLlmKbMapper.insertCcLlmKb(ccLlmKb);
+    }
+
+    /**
+     * 修改知识库内容
+     * 
+     * @param ccLlmKb 知识库内容
+     * @return 结果
+     */
+    @Override
+    public int updateCcLlmKb(CcLlmKb ccLlmKb)
+    {
+        return ccLlmKbMapper.updateCcLlmKb(ccLlmKb);
+    }
+
+    /**
+     * 批量删除知识库内容
+     * 
+     * @param ids 需要删除的知识库内容主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmKbByIds(String ids)
+    {
+        return ccLlmKbMapper.deleteCcLlmKbByIds(Convert.toStrArray(ids));
+    }
+
+    /**
+     * 删除知识库内容信息
+     * 
+     * @param id 知识库内容主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcLlmKbById(Long id)
+    {
+        return ccLlmKbMapper.deleteCcLlmKbById(id);
+    }
+
+    @Override
+    public Integer selectCountByCatId(Long catId) {
+        return ccLlmKbMapper.selectCountByCatId(catId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String importData(MultipartFile file, Long fixedCatId) throws Exception {
+        Workbook wb = WorkbookFactory.create(file.getInputStream());
+        Sheet sheet = wb.getSheetAt(0);
+        if (sheet == null) throw new ServiceException("Excel 为空");
+        List<CcLlmKb> ccLlmKbList = new ArrayList<>();
+        // 跳过表头
+        for (int i = 1; i <= sheet.getLastRowNum(); i++) {
+            Row row = sheet.getRow(i);
+            if (row == null) continue;
+            CcLlmKb e = new CcLlmKb();
+            String title = XSSFUtils.getCellString(row.getCell(0));
+            e.setTitle(title);
+            e.setContent(XSSFUtils.getCellString(row.getCell(1)));
+            if (StringUtils.isBlank(e.getTitle())) continue;
+            e.setCatId(fixedCatId);
+            ccLlmKbList.add(e);
+        }
+        wb.close();
+        ccLlmKbMapper.deleteCcLlmKbByCatId(fixedCatId);
+        ccLlmKbMapper.insertBatch(ccLlmKbList);
+        return "导入成功";
+    }
+
+
+    @Override
+    public CcLlmKb selectCcLlmKbContentByTitle(Long id, String title, Long catId) {
+        List<CcLlmKb> list = ccLlmKbMapper.selectCcLlmKbList(new CcLlmKb().setTitle(title).setCatId(catId));
+        if (!CollectionUtils.isEmpty(list)) {
+            if (null == id) {
+                return list.get(0);
+            } else {
+                for (CcLlmKb data: list) {
+                    if (data.getId() != id) {
+                        return data;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+}

+ 168 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcParamsServiceImpl.java

@@ -0,0 +1,168 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcParams;
+import com.fs.aicall.mapper.CcParamsMapper;
+import com.fs.aicall.service.ICcParamsService;
+import com.fs.aicall.utils.CommonUtils;
+import com.fs.aicall.utils.StringUtils;
+import com.fs.aicall.utils.http.HttpUtils;
+import com.fs.common.core.text.Convert;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * callcenter参数配置Service业务层处理
+ * 
+ * @author ruoyi
+ * @date 2025-04-21
+ */
+@Service
+@Slf4j
+public class CcParamsServiceImpl implements ICcParamsService
+{
+    @Autowired
+    private CcParamsMapper ccParamsMapper;
+
+
+    @Value("${sysconfig.hide-secret:}")//默认为空,不至于报错
+    private String sysConfigHideSecret;
+
+
+    /**
+     * 查询callcenter参数配置
+     * 
+     * @param id callcenter参数配置主键
+     * @return callcenter参数配置
+     */
+    @Override
+    public CcParams selectCcParamsById(Long id)
+    {
+        CcParams params =  ccParamsMapper.selectCcParamsById(id);
+        boolean hideSecret = Boolean.parseBoolean(sysConfigHideSecret);
+        if(params.getHideValue() == 1 && hideSecret){
+            String hideString = params.getParamValue();
+            params.setParamValue(CommonUtils.maskStringUtil(hideString));
+        }
+        return params;
+    }
+
+    /**
+     * 查询callcenter参数配置列表
+     * 
+     * @param ccParams callcenter参数配置
+     * @return callcenter参数配置
+     */
+    @Override
+    public List<CcParams> selectCcParamsList(CcParams ccParams)
+    {
+        List<CcParams> origList = ccParamsMapper.selectCcParamsList(ccParams);
+        boolean hideSecret = Boolean.parseBoolean(sysConfigHideSecret);
+        if(hideSecret) {
+            for (CcParams params : origList) {
+                if (params.getHideValue() == 1) {
+                    String hideString = params.getParamValue();
+                    params.setParamValue(CommonUtils.maskStringUtil(hideString));
+                }
+            }
+        }
+        return origList;
+    }
+
+    /**
+     * 新增callcenter参数配置
+     * 
+     * @param ccParams callcenter参数配置
+     * @return 结果
+     */
+    @Override
+    public int insertCcParams(CcParams ccParams)
+    {
+        return ccParamsMapper.insertCcParams(ccParams);
+    }
+
+    /**
+     * 修改callcenter参数配置
+     * 
+     * @param ccParams callcenter参数配置
+     * @return 结果
+     */
+    @Override
+    public int updateCcParams(CcParams ccParams)
+    {
+        // 禁止修改参数的 param_code 和 param_type,否则可能引起混乱;
+        CcParams ccParamsOld = selectCcParamsById(ccParams.getId());
+        ccParams.setParamName(ccParamsOld.getParamName());
+        ccParams.setParamCode(ccParamsOld.getParamCode());
+        ccParams.setParamType(ccParamsOld.getParamType());
+
+        boolean hideSecret = Boolean.parseBoolean(sysConfigHideSecret);
+        boolean containsMaskStr =  ccParams.getParamValue().contains("**");
+        if(!hideSecret || !containsMaskStr) {
+            return ccParamsMapper.updateCcParams(ccParams);
+        }
+        return 1;
+    }
+
+    /**
+     * 批量删除callcenter参数配置
+     * 
+     * @param ids 需要删除的callcenter参数配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcParamsByIds(String ids)
+    {
+        return ccParamsMapper.deleteCcParamsByIds(Convert.toStrArray(ids));
+    }
+
+    /**
+     * 删除callcenter参数配置信息
+     * 
+     * @param id callcenter参数配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCcParamsById(Long id)
+    {
+        return ccParamsMapper.deleteCcParamsById(id);
+    }
+
+    @Override
+    public String getParamValueByCode(String paramCode, String defaultValue) {
+        Locale locale = LocaleContextHolder.getLocale();
+        // 先找对应语言包的配置,没有的话再找通用配置
+        List<CcParams> list = ccParamsMapper.selectCcParamsList(new CcParams().setParamCode(paramCode + "_" + locale.toString()));
+        if (list.size() > 0) {
+            return list.get(0).getParamValue();
+        } else {
+            list = ccParamsMapper.selectCcParamsList(new CcParams().setParamCode(paramCode));
+            if (list.size() > 0) {
+                return list.get(0).getParamValue();
+            }
+        }
+        return defaultValue;
+    }
+
+    @Override
+    public void updateParamsValue(String paramCode, String paramValue) {
+        ccParamsMapper.updateParamsValue(paramCode, paramValue);
+    }
+
+    @Override
+    public String reloadParams() {
+        // Access the 'reloadParams' webapi interface to make the parameters take effect;
+        String serverPort = getParamValueByCode("call-center-server-port", "");
+        if(!StringUtils.isEmpty(serverPort)){
+            String reloadParamsUrl = String.format("http://127.0.0.1:%s/call-center/reloadParams", serverPort);
+            String response = HttpUtils.sendGet(reloadParamsUrl);
+            return response;
+        }
+        return "";
+    }
+}

+ 179 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CompanyBindAiModelServiceImpl.java

@@ -0,0 +1,179 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CompanyBindAiModel;
+import com.fs.aicall.mapper.CompanyBindAiModelMapper;
+import com.fs.aicall.service.ICompanyBindAiModelService;
+import com.fs.aicall.utils.StringUtils;
+import com.fs.common.core.text.Convert;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 销售公司与AI模型绑定表Service实现类
+ * 
+ * @author ruoyi
+ * @date 2026-03-20
+ */
+@Service
+public class CompanyBindAiModelServiceImpl implements ICompanyBindAiModelService
+{
+    @Autowired
+    private CompanyBindAiModelMapper companyBindAiModelMapper;
+
+    /**
+     * 查询销售公司与AI模型绑定表
+     * 
+     * @param id 销售公司与AI模型绑定表主键
+     * @return 销售公司与AI模型绑定表
+     */
+    @Override
+    public CompanyBindAiModel selectCompanyBindAiModelById(Long id)
+    {
+        return companyBindAiModelMapper.selectCompanyBindAiModelById(id);
+    }
+
+    /**
+     * 查询销售公司与AI模型绑定表列表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 销售公司与AI模型绑定表集合
+     */
+    @Override
+    public List<CompanyBindAiModel> selectCompanyBindAiModelList(CompanyBindAiModel companyBindAiModel)
+    {
+        return companyBindAiModelMapper.selectCompanyBindAiModelList(companyBindAiModel);
+    }
+
+    /**
+     * 新增销售公司与AI模型绑定表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyBindAiModel(CompanyBindAiModel companyBindAiModel)
+    {
+        companyBindAiModel.setCreateTime(new Date());
+        companyBindAiModel.setUpdateTime(new Date());
+        return companyBindAiModelMapper.insertCompanyBindAiModel(companyBindAiModel);
+    }
+
+    /**
+     * 修改销售公司与AI模型绑定表
+     * 
+     * @param companyBindAiModel 销售公司与AI模型绑定表
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyBindAiModel(CompanyBindAiModel companyBindAiModel)
+    {
+        companyBindAiModel.setUpdateTime(new Date());
+        return companyBindAiModelMapper.updateCompanyBindAiModel(companyBindAiModel);
+    }
+
+    /**
+     * 删除销售公司与AI模型绑定表
+     * 
+     * @param id 销售公司与AI模型绑定表主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyBindAiModelById(Long id)
+    {
+        return companyBindAiModelMapper.deleteCompanyBindAiModelById(id);
+    }
+
+    /**
+     * 批量删除销售公司与AI模型绑定表
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyBindAiModelByIds(Long[] ids)
+    {
+        return companyBindAiModelMapper.deleteCompanyBindAiModelByIds(ids);
+    }
+
+    /**
+     * 根据公司ID查询绑定的模型ID列表
+     * 
+     * @param companyId 公司ID
+     * @return 模型ID列表
+     */
+    @Override
+    public List<Long> selectModelIdsByCompanyId(Long companyId)
+    {
+        return companyBindAiModelMapper.selectModelIdsByCompanyId(companyId);
+    }
+
+    /**
+     * 根据模型ID删除绑定关系
+     * 
+     * @param modelId 模型ID
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyBindAiModelByModelId(Long modelId)
+    {
+        return companyBindAiModelMapper.deleteCompanyBindAiModelByModelId(modelId);
+    }
+
+    /**
+     * 批量插入绑定关系
+     * 
+     * @param companyId 公司ID
+     * @param modelIds 模型ID列表
+     * @return 结果
+     */
+    @Override
+    public int batchInsertCompanyBindAiModel(Long companyId, List<Long> modelIds)
+    {
+        return companyBindAiModelMapper.batchInsertCompanyBindAiModel(companyId, modelIds);
+    }
+
+    /**
+     * 为模型添加公司绑定
+     * 
+     * @param modelId 模型ID
+     * @param companyId 公司ID
+     * @return 结果
+     */
+    @Override
+    public int bindCompanyToModel(Long modelId, Long companyId)
+    {
+        CompanyBindAiModel companyBindAiModel = new CompanyBindAiModel();
+        companyBindAiModel.setModelId(modelId);
+        companyBindAiModel.setCompanyId(companyId);
+        companyBindAiModel.setCreateTime(new Date());
+        companyBindAiModel.setUpdateTime(new Date());
+        return companyBindAiModelMapper.insertCompanyBindAiModel(companyBindAiModel);
+    }
+
+    /**
+     * 为模型批量添加公司绑定
+     * 
+     * @param modelId 模型ID
+     * @param companyIds 公司ID列表
+     * @return 结果
+     */
+    @Override
+    public int batchBindCompaniesToModel(Long modelId, List<Long> companyIds)
+    {
+        int result = 0;
+        for (Long companyId : companyIds) {
+            result += bindCompanyToModel(modelId, companyId);
+        }
+        return result;
+    }
+
+    @Override
+    public void deleteBindAiModelByCompanyIdAndModelIds(Long companyId, String modelIds) {
+        companyBindAiModelMapper.deleteBindAiModelByCompanyIdAndModelIds(companyId, Convert.toStrArray(modelIds));
+    }
+}

+ 105 - 0
fs-service/src/main/java/com/fs/aicall/utils/CommonUtils.java

@@ -0,0 +1,105 @@
+package com.fs.aicall.utils;
+
+import java.util.List;
+
+public class CommonUtils {
+    public static String getStackTraceString(StackTraceElement[] stackTraceElements){
+        StringBuilder stringBuilder = new StringBuilder();
+        for (int i = 0; i < stackTraceElements.length; i++) {
+            stringBuilder.append("ClassName:");
+            stringBuilder.append(stackTraceElements[i].getClassName());
+            stringBuilder.append("\n FileName:");
+            stringBuilder.append(stackTraceElements[i].getFileName());
+            stringBuilder.append("\n LineNumber:");
+            stringBuilder.append(stackTraceElements[i].getLineNumber());
+            stringBuilder.append("\n MethodName:");
+            stringBuilder.append(stackTraceElements[i].getMethodName());
+        }
+        return stringBuilder.toString();
+    }
+
+    public static String ListToString(List<?> objectList) {
+        if(objectList.size() == 0) {
+            return "";
+        }
+        return ListToString(objectList, true);
+    }
+
+    /**
+     * ListToString,是否使用逗号分隔符
+     ***/
+    public static String ListToString(List<?> objectList, boolean useSpe) {
+        StringBuilder sb = new StringBuilder("");
+        for (Object ele : objectList) {
+            sb.append(ele);
+            if (useSpe) {
+                sb.append(",");
+            }
+        }
+        String result = sb.toString();
+        if (useSpe) {
+            result = result.substring(0, result.length() - 1);
+        }
+        return result;
+    }
+
+    /**
+     * ListToString, 使用分隔符
+     ***/
+    public static String ListToString(List<?> objectList, char delimiter) {
+        StringBuilder sb = new StringBuilder("");
+        for (Object ele : objectList) {
+            sb.append(ele);
+            sb.append(delimiter);
+        }
+        String result = sb.toString();
+        result = result.substring(0, result.length() - 1);
+        return result;
+    }
+
+    /**
+     *  隐藏敏感字符串
+     * @param input
+     * @return
+     */
+    public static String maskStringUtil(String input) {
+        if (input == null || input.isEmpty()) {
+            return input;
+        }
+
+        int length = input.length();
+        if (length <= 10) {
+            // 场景1:长度≤10,隐藏第4-8位(索引3-7)
+            if (length < 10) {
+                return repeatString("*", length); // 不足3位直接返回
+            }
+            int startIdx = 3;
+            int endIdx = Math.min(8, length); // 防止越界
+            String stars = repeatString("*", endIdx - startIdx);
+            return input.substring(0, startIdx) + stars + input.substring(endIdx);
+        } else {
+            // 场景2:长度>10,仅保留前3位和后3位
+            String prefix = input.substring(0, 3);
+            String suffix = input.substring(length - 3);
+            String stars = repeatString("*", length - 6);
+            return prefix + stars + suffix;
+        }
+    }
+
+    /**
+     * 手动实现字符串重复方法(兼容Java 8以下)
+     * @param str
+     * @param times
+     * @return
+     */
+    private static String repeatString(String str, int times) {
+        if (times <= 0) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < times; i++) {
+            sb.append(str);
+        }
+        return sb.toString();
+    }
+}

+ 672 - 0
fs-service/src/main/java/com/fs/aicall/utils/StringUtils.java

@@ -0,0 +1,672 @@
+package com.fs.aicall.utils;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.core.text.StrFormatter;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+
+/**
+ * 字符串工具类
+ * 
+ * @author ruoyi
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils
+{
+    /** 空字符串 */
+    private static final String NULLSTR = "";
+
+    /** 下划线 */
+    private static final char SEPARATOR = '_';
+
+    /** 星号 */
+    private static final char ASTERISK = '*';
+
+    /**
+     * 获取参数不为空值
+     * 
+     * @param value defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static <T> T nvl(T value, T defaultValue)
+    {
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * * 判断一个Collection是否为空, 包含List,Set,Queue
+     * 
+     * @param coll 要判断的Collection
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Collection<?> coll)
+    {
+        return isNull(coll) || coll.isEmpty();
+    }
+
+    /**
+     * * 判断一个Collection是否非空,包含List,Set,Queue
+     * 
+     * @param coll 要判断的Collection
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Collection<?> coll)
+    {
+        return !isEmpty(coll);
+    }
+
+    /**
+     * * 判断一个对象数组是否为空
+     * 
+     * @param objects 要判断的对象数组
+     ** @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Object[] objects)
+    {
+        return isNull(objects) || (objects.length == 0);
+    }
+
+    /**
+     * * 判断一个对象数组是否非空
+     * 
+     * @param objects 要判断的对象数组
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Object[] objects)
+    {
+        return !isEmpty(objects);
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     * 
+     * @param map 要判断的Map
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Map<?, ?> map)
+    {
+        return isNull(map) || map.isEmpty();
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     * 
+     * @param map 要判断的Map
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Map<?, ?> map)
+    {
+        return !isEmpty(map);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     * 
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str)
+    {
+        return isNull(str) || NULLSTR.equals(str.trim());
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     * 
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str)
+    {
+        return !isEmpty(str);
+    }
+
+    /**
+     * * 判断一个对象是否为空
+     * 
+     * @param object Object
+     * @return true:为空 false:非空
+     */
+    public static boolean isNull(Object object)
+    {
+        return object == null;
+    }
+
+    /**
+     * * 判断一个对象是否非空
+     * 
+     * @param object Object
+     * @return true:非空 false:空
+     */
+    public static boolean isNotNull(Object object)
+    {
+        return !isNull(object);
+    }
+
+    /**
+     * * 判断一个对象是否是数组类型(Java基本型别的数组)
+     * 
+     * @param object 对象
+     * @return true:是数组 false:不是数组
+     */
+    public static boolean isArray(Object object)
+    {
+        return isNotNull(object) && object.getClass().isArray();
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str)
+    {
+        return (str == null ? "" : str.trim());
+    }
+
+    /**
+     * 替换指定字符串的指定区间内字符为"*"
+     *
+     * @param str 字符串
+     * @param startInclude 开始位置(包含)
+     * @param endExclude 结束位置(不包含)
+     * @return 替换后的字符串
+     */
+    public static String hide(CharSequence str, int startInclude, int endExclude)
+    {
+        if (isEmpty(str))
+        {
+            return NULLSTR;
+        }
+        final int strLength = str.length();
+        if (startInclude > strLength)
+        {
+            return NULLSTR;
+        }
+        if (endExclude > strLength)
+        {
+            endExclude = strLength;
+        }
+        if (startInclude > endExclude)
+        {
+            // 如果起始位置大于结束位置,不替换
+            return NULLSTR;
+        }
+        final char[] chars = new char[strLength];
+        for (int i = 0; i < strLength; i++)
+        {
+            if (i >= startInclude && i < endExclude)
+            {
+                chars[i] = ASTERISK;
+            }
+            else
+            {
+                chars[i] = str.charAt(i);
+            }
+        }
+        return new String(chars);
+    }
+
+    /**
+     * 截取字符串
+     * 
+     * @param str 字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (start > str.length())
+        {
+            return NULLSTR;
+        }
+
+        return str.substring(start);
+    }
+
+    /**
+     * 截取字符串
+     * 
+     * @param str 字符串
+     * @param start 开始
+     * @param end 结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (end < 0)
+        {
+            end = str.length() + end;
+        }
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (end > str.length())
+        {
+            end = str.length();
+        }
+
+        if (start > end)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (end < 0)
+        {
+            end = 0;
+        }
+
+        return str.substring(start, end);
+    }
+
+    /**
+     * 格式化文本, {} 表示占位符<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     * 
+     * @param template 文本模板,被替换的部分用 {} 表示
+     * @param params 参数值
+     * @return 格式化后的文本
+     */
+    public static String format(String template, Object... params)
+    {
+        if (isEmpty(params) || isEmpty(template))
+        {
+            return template;
+        }
+        return StrFormatter.format(template, params);
+    }
+
+    /**
+     * 是否为http(s)://开头
+     * 
+     * @param link 链接
+     * @return 结果
+     */
+    public static boolean ishttp(String link)
+    {
+        return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
+    }
+
+    /**
+     * 字符串转set
+     * 
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static final Set<String> str2Set(String str, String sep)
+    {
+        return new HashSet<String>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     * 
+     * @param str 字符串
+     * @param sep 分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim 去掉首尾空白
+     * @return list集合
+     */
+    public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim)
+    {
+        List<String> list = new ArrayList<String>();
+        if (StringUtils.isEmpty(str))
+        {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && StringUtils.isBlank(str))
+        {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split)
+        {
+            if (filterBlank && StringUtils.isBlank(string))
+            {
+                continue;
+            }
+            if (trim)
+            {
+                string = string.trim();
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
+     *
+     * @param collection 给定的集合
+     * @param array 给定的数组
+     * @return boolean 结果
+     */
+    public static boolean containsAny(Collection<String> collection, String... array)
+    {
+        if (isEmpty(collection) || isEmpty(array))
+        {
+            return false;
+        }
+        else
+        {
+            for (String str : array)
+            {
+                if (collection.contains(str))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs 指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
+    {
+        if (isEmpty(cs) || isEmpty(searchCharSequences))
+        {
+            return false;
+        }
+        for (CharSequence testStr : searchCharSequences)
+        {
+            if (containsIgnoreCase(cs, testStr))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str)
+    {
+        if (str == null)
+        {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        // 前置字符是否大写
+        boolean preCharIsUpperCase = true;
+        // 当前字符是否大写
+        boolean curreCharIsUpperCase = true;
+        // 下一字符是否大写
+        boolean nexteCharIsUpperCase = true;
+        for (int i = 0; i < str.length(); i++)
+        {
+            char c = str.charAt(i);
+            if (i > 0)
+            {
+                preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
+            }
+            else
+            {
+                preCharIsUpperCase = false;
+            }
+
+            curreCharIsUpperCase = Character.isUpperCase(c);
+
+            if (i < (str.length() - 1))
+            {
+                nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
+            }
+
+            if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            sb.append(Character.toLowerCase(c));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 是否包含字符串
+     * 
+     * @param str 验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs)
+    {
+        if (str != null && strs != null)
+        {
+            for (String s : strs)
+            {
+                if (str.equalsIgnoreCase(trim(s)))
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 删除最后一个字符串
+     *
+     * @param str 输入字符串
+     * @param spit 以什么类型结尾的
+     * @return 截取后的字符串
+     */
+    public static String lastStringDel(String str, String spit)
+    {
+        if (!StringUtils.isEmpty(str) && str.endsWith(spit))
+        {
+            return str.subSequence(0, str.length() - 1).toString();
+        }
+        return str;
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     * 
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name)
+    {
+        StringBuilder result = new StringBuilder();
+        // 快速检查
+        if (name == null || name.isEmpty())
+        {
+            // 没必要转换
+            return "";
+        }
+        else if (!name.contains("_"))
+        {
+            // 不含下划线,仅将首字母大写
+            return name.substring(0, 1).toUpperCase() + name.substring(1);
+        }
+        // 用下划线将原始字符串分割
+        String[] camels = name.split("_");
+        for (String camel : camels)
+        {
+            // 跳过原始字符串中开头、结尾的下换线或双重下划线
+            if (camel.isEmpty())
+            {
+                continue;
+            }
+            // 首字母大写
+            result.append(camel.substring(0, 1).toUpperCase());
+            result.append(camel.substring(1).toLowerCase());
+        }
+        return result.toString();
+    }
+
+    /**
+     * 驼峰式命名法
+     * 例如:user_name->userName
+     */
+    public static String toCamelCase(String s)
+    {
+        if (s == null)
+        {
+            return null;
+        }
+        if (s.indexOf(SEPARATOR) == -1)
+        {
+            return s;
+        }
+        s = s.toLowerCase();
+        StringBuilder sb = new StringBuilder(s.length());
+        boolean upperCase = false;
+        for (int i = 0; i < s.length(); i++)
+        {
+            char c = s.charAt(i);
+
+            if (c == SEPARATOR)
+            {
+                upperCase = true;
+            }
+            else if (upperCase)
+            {
+                sb.append(Character.toUpperCase(c));
+                upperCase = false;
+            }
+            else
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     * 
+     * @param str 指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs)
+    {
+        if (isEmpty(str) || isEmpty(strs))
+        {
+            return false;
+        }
+        for (String pattern : strs)
+        {
+            if (isMatch(pattern, str))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置: 
+     * ? 表示单个字符; 
+     * * 表示一层路径内的任意字符串,不可跨层级; 
+     * ** 表示任意层路径;
+     * 
+     * @param pattern 匹配规则
+     * @param url 需要匹配的url
+     * @return
+     */
+    public static boolean isMatch(String pattern, String url)
+    {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T cast(Object obj)
+    {
+        return (T) obj;
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     * 
+     * @param num 数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static final String padl(final Number num, final int size)
+    {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     * 
+     * @param s 原始字符串
+     * @param size 字符串指定长度
+     * @param c 用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static final String padl(final String s, final int size, final char c)
+    {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null)
+        {
+            final int len = s.length();
+            if (s.length() <= size)
+            {
+                for (int i = size - len; i > 0; i--)
+                {
+                    sb.append(c);
+                }
+                sb.append(s);
+            }
+            else
+            {
+                return s.substring(len - size, len);
+            }
+        }
+        else
+        {
+            for (int i = size; i > 0; i--)
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/aicall/utils/XSSFUtils.java

@@ -0,0 +1,12 @@
+package com.fs.aicall.utils;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+
+public class XSSFUtils {
+    public static String getCellString(Cell cell) {
+        if (cell == null) return "";
+        cell.setCellType(CellType.STRING);
+        return cell.getStringCellValue() == null ? "" : cell.getStringCellValue().trim();
+    }
+}

+ 276 - 0
fs-service/src/main/java/com/fs/aicall/utils/http/HttpUtils.java

@@ -0,0 +1,276 @@
+package com.fs.aicall.utils.http;
+
+import com.fs.aicall.utils.StringUtils;
+import com.fs.common.constant.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.*;
+import java.io.*;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 通用http发送方法
+ * 
+ * @author ruoyi
+ */
+public class HttpUtils
+{
+    private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
+
+    /**
+     * 向指定 URL 发送GET方法的请求
+     *
+     * @param url 发送请求的 URL
+     * @return 所代表远程资源的响应结果
+     */
+    public static String sendGet(String url)
+    {
+        return sendGet(url, StringUtils.EMPTY);
+    }
+
+    /**
+     * 向指定 URL 发送GET方法的请求
+     *
+     * @param url 发送请求的 URL
+     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+     * @return 所代表远程资源的响应结果
+     */
+    public static String sendGet(String url, String param)
+    {
+        return sendGet(url, param, Constants.UTF8);
+    }
+
+    public static String sendGet(String url, String param, String contentType) {
+        return sendGet(url, param, contentType, new HashMap<>());
+    }
+
+    /**
+     * 向指定 URL 发送GET方法的请求
+     *
+     * @param url 发送请求的 URL
+     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+     * @param contentType 编码类型
+     * @return 所代表远程资源的响应结果
+     */
+    public static String sendGet(String url, String param, String contentType, Map<String, String> headers )
+    {
+        StringBuilder result = new StringBuilder();
+        BufferedReader in = null;
+        try
+        {
+            String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url;
+            log.info("sendGet - {}", urlNameString);
+            URL realUrl = new URL(urlNameString);
+            URLConnection connection = realUrl.openConnection();
+            connection.setRequestProperty("accept", "*/*");
+            connection.setRequestProperty("connection", "Keep-Alive");
+            connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+            /* ===== 动态头 ===== */
+            if (headers != null) {
+                headers.forEach(connection::setRequestProperty);
+            }
+            connection.connect();
+            in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
+            String line;
+            while ((line = in.readLine()) != null)
+            {
+                result.append(line);
+            }
+            log.info("recv - {}", result);
+        }
+        catch (ConnectException e)
+        {
+            log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
+        }
+        catch (SocketTimeoutException e)
+        {
+            log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
+        }
+        catch (IOException e)
+        {
+            log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
+        }
+        catch (Exception e)
+        {
+            log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
+        }
+        finally
+        {
+            try
+            {
+                if (in != null)
+                {
+                    in.close();
+                }
+            }
+            catch (Exception ex)
+            {
+                log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * 向指定 URL 发送POST方法的请求
+     *
+     * @param url 发送请求的 URL
+     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+     * @return 所代表远程资源的响应结果
+     */
+    public static String sendPost(String url, String param)
+    {
+        PrintWriter out = null;
+        BufferedReader in = null;
+        StringBuilder result = new StringBuilder();
+        try
+        {
+            log.info("sendPost - {}", url);
+            URL realUrl = new URL(url);
+            URLConnection conn = realUrl.openConnection();
+            conn.setRequestProperty("accept", "*/*");
+            conn.setRequestProperty("connection", "Keep-Alive");
+            conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+            conn.setRequestProperty("Accept-Charset", "utf-8");
+            conn.setRequestProperty("contentType", "utf-8");
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            out = new PrintWriter(conn.getOutputStream());
+            out.print(param);
+            out.flush();
+            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
+            String line;
+            while ((line = in.readLine()) != null)
+            {
+                result.append(line);
+            }
+            log.info("recv - {}", result);
+        }
+        catch (ConnectException e)
+        {
+            log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
+        }
+        catch (SocketTimeoutException e)
+        {
+            log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
+        }
+        catch (IOException e)
+        {
+            log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
+        }
+        catch (Exception e)
+        {
+            log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
+        }
+        finally
+        {
+            try
+            {
+                if (out != null)
+                {
+                    out.close();
+                }
+                if (in != null)
+                {
+                    in.close();
+                }
+            }
+            catch (IOException ex)
+            {
+                log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
+            }
+        }
+        return result.toString();
+    }
+
+    public static String sendSSLPost(String url, String param)
+    {
+        StringBuilder result = new StringBuilder();
+        String urlNameString = url + "?" + param;
+        try
+        {
+            log.info("sendSSLPost - {}", urlNameString);
+            SSLContext sc = SSLContext.getInstance("SSL");
+            sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
+            URL console = new URL(urlNameString);
+            HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
+            conn.setRequestProperty("accept", "*/*");
+            conn.setRequestProperty("connection", "Keep-Alive");
+            conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+            conn.setRequestProperty("Accept-Charset", "utf-8");
+            conn.setRequestProperty("contentType", "utf-8");
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+
+            conn.setSSLSocketFactory(sc.getSocketFactory());
+            conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
+            conn.connect();
+            InputStream is = conn.getInputStream();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is));
+            String ret = "";
+            while ((ret = br.readLine()) != null)
+            {
+                if (ret != null && !ret.trim().equals(""))
+                {
+                    result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
+                }
+            }
+            log.info("recv - {}", result);
+            conn.disconnect();
+            br.close();
+        }
+        catch (ConnectException e)
+        {
+            log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e);
+        }
+        catch (SocketTimeoutException e)
+        {
+            log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e);
+        }
+        catch (IOException e)
+        {
+            log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e);
+        }
+        catch (Exception e)
+        {
+            log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e);
+        }
+        return result.toString();
+    }
+
+    private static class TrustAnyTrustManager implements X509TrustManager
+    {
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String authType)
+        {
+        }
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String authType)
+        {
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers()
+        {
+            return new X509Certificate[] {};
+        }
+    }
+
+    private static class TrustAnyHostnameVerifier implements HostnameVerifier
+    {
+        @Override
+        public boolean verify(String hostname, SSLSession session)
+        {
+            return true;
+        }
+    }
+}

+ 1 - 1
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -921,7 +921,7 @@ public class SmsServiceImpl implements ISmsService
      * @param temp
      * @param param
      */
-    @Async
+//    @Async
     public void batchSmsOp4AiSend(CompanySmsTemp temp, SmsSendBatchParam param){
         CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
         for(Long id:param.getCustomerIds()){

+ 4 - 7
fs-service/src/main/java/com/fs/company/config/AsyncConfig.java → fs-service/src/main/java/com/fs/company/config/AsyncCalleeConfig.java

@@ -10,20 +10,17 @@ import java.util.concurrent.ThreadPoolExecutor;
 
 @Configuration
 @EnableAsync
-public class AsyncConfig {
+public class AsyncCalleeConfig {
     @Bean(name = "calleeTaskExecutor")
     public Executor calleeTaskExecutor() {
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
-        int cpuCores = Runtime.getRuntime().availableProcessors();
-        executor.setCorePoolSize(cpuCores);
-        executor.setMaxPoolSize(20);
-        executor.setQueueCapacity(1000);
+        executor.setCorePoolSize(10);
+        executor.setMaxPoolSize(50);
+        executor.setQueueCapacity(500);
         executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
         executor.setKeepAliveSeconds(60);
-        executor.setAllowCoreThreadTimeOut(false);
         executor.setWaitForTasksToCompleteOnShutdown(true);
         executor.setAwaitTerminationSeconds(60);
-
         executor.initialize();
         return executor;
     }

+ 1 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyAiWorkflowExec.java

@@ -69,7 +69,7 @@ public class CompanyAiWorkflowExec {
 
     /** 业务键值 */
     @Excel(name = "业务键值")
-    private String businessKey;
+    private Long businessKey;
 
     /**
      * 开始节点key

+ 26 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyFsUser.java

@@ -0,0 +1,26 @@
+package com.fs.company.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+@Data
+public class CompanyFsUser extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** ID */
+    private Long id;
+
+    /** fs_user 用户id */
+    @Excel(name = "fs_user 用户id")
+    private Long fsUserId;
+
+    /** company_user 销售id */
+    @Excel(name = "company_user 销售id")
+    private Long companyUserId;
+
+    /** 绑定状态 */
+    @Excel(name = "绑定状态")
+    private Integer status;
+
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyUser.java

@@ -195,11 +195,26 @@ public class CompanyUser extends BaseEntity
     @TableField(exist = false)
     private List<Long> deptList;
 
+
+
+    /**
+     * 多个用户ID,逗号分隔
+     */
+    private  String  fsUserId;
+
     /**
      * cid服务id
      */
     private Long cidServerId;
 
+    public String getFsUserId() {
+        return fsUserId;
+    }
+
+    public void setFsUserId(String fsUserId) {
+        this.fsUserId = fsUserId;
+    }
+
     public String getMaOpenId() {
         return maOpenId;
     }

+ 5 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java

@@ -69,7 +69,10 @@ public class CompanyVoiceRoboticCallLogAddwx extends BaseEntity{
 
     @TableField(exist = false)
     private List<Long> wxclientIds;
-    public static CompanyVoiceRoboticCallLogAddwx initCallLog( String runParam, Long keyId, Long taskId,Long wxAccountId,Long companyId) {
+
+    private Integer qwWxAddWayId;
+
+    public static CompanyVoiceRoboticCallLogAddwx initCallLog( String runParam, Long keyId, Long taskId,Long wxAccountId,Long companyId,int qwWxAddWayId) {
         CompanyVoiceRoboticCallLogAddwx log = new CompanyVoiceRoboticCallLogAddwx();
         log.wxClientId = keyId;
         log.runParam = runParam;
@@ -77,6 +80,7 @@ public class CompanyVoiceRoboticCallLogAddwx extends BaseEntity{
         log.runTime = new Date();
         log.wxAccountId = wxAccountId;
         log.companyId = companyId;
+        log.qwWxAddWayId=qwWxAddWayId;
         return log;
     }
 

+ 1 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java

@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
 
 /**
  * 添加个微信账号对象 company_wx_client
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */

+ 53 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyFsUserMapper.java

@@ -0,0 +1,53 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyFsUser;
+import com.fs.his.domain.FsUser;
+import com.fs.his.vo.CompanyUserBindUserVO;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+public interface CompanyFsUserMapper {
+
+    /**
+     * 新增公司用户绑定关系
+     *
+     * @param companyFsUser 公司用户绑定关系
+     * @return 结果
+     */
+    public int insertCompanyFsUser(CompanyFsUser companyFsUser);
+
+    /**
+     * 根据FS用户ID查询绑定关系
+     *
+     * @param fsUserId FS用户ID
+     * @return 公司用户绑定关系
+     */
+    public CompanyFsUser selectByFsUserId(Long fsUserId);
+
+    /**
+     * 更新公司用户绑定关系
+     *
+     * @param companyFsUser 公司用户绑定关系
+     * @return 结果
+     */
+    public int updateCompanyFsUser(CompanyFsUser companyFsUser);
+
+    /**
+     * 销售公司用户列表
+     */
+    List<CompanyUserBindUserVO> selecUserList(FsUser fsUser);
+
+    /**
+     * 根据销售id 查询绑定的用户
+     */
+    String selectUserListBySalesId(Long companyUserId);
+
+    @Select("select * from company_fs_user where company_user_id=#{companyUserId} and fs_user_id=#{userId}")
+    CompanyFsUser selectByCompanyUserIdAndUserId(@Param("companyUserId") Long companyUserId, @Param("userId") Long userId);
+
+    @Delete("delete from company_fs_user WHERE id=#{id}")
+    int deleteById(Long id);
+}

+ 1 - 1
fs-service/src/main/java/com/fs/company/param/ExecutionContext.java

@@ -53,7 +53,7 @@ public class ExecutionContext {
     /**
      * 业务关键id
      */
-    private String businessId;
+    private Long businessId;
     /**
      * 设置本地变量
      */

+ 21 - 0
fs-service/src/main/java/com/fs/company/service/IAsyncCalleeProcessorService.java

@@ -0,0 +1,21 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.CompanyVoiceRobotic;
+import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.domain.CompanyWxClient;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 异步调用服务加入日志
+ * **/
+public interface IAsyncCalleeProcessorService {
+    /**
+     * 生成客户信息日志
+     * @param calleesList 客户列表对象
+     * @param clientMp 添加个微信账号对象
+     * @param robotic 任务
+     * **/
+    void generateCustomerInfo(List<CompanyVoiceRoboticCallees> calleesList, Map<String, CompanyWxClient> clientMp, CompanyVoiceRobotic robotic);
+}

+ 23 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java

@@ -7,7 +7,9 @@ import com.fs.company.param.CompanyUserAreaParam;
 import com.fs.company.param.CompanyUserCodeParam;
 import com.fs.company.param.CompanyUserQwParam;
 import com.fs.company.vo.*;
+import com.fs.his.domain.FsUser;
 import com.fs.his.vo.CitysAreaVO;
+import com.fs.his.vo.CompanyUserBindUserVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.vo.FsStoreProductExportVO;
 import com.fs.qw.dto.UserProjectDTO;
@@ -276,4 +278,25 @@ public interface ICompanyUserService {
     List<CompanyUser> getDataScopeCompanyUser(Long companyUserId);
 
     List<Company> getCompanyList(String corpId);
+
+
+
+    /**
+     * 绑定销售和fs_user 的关系(该销售绑定fs_user)
+     */
+    int  bindCompanyUserAndFsUser(Long companyUserId, Long fsUserId);
+
+    /**
+     * 解绑销售和 fs_user 的关系
+     * @param companyUserId 销售 ID
+     * @param userId 用户 ID
+     * @return 是否删除成功
+     */
+    boolean unbindCompanyUserAndFsUser(Long companyUserId, Long userId);
+
+
+    /**
+     * 获取销售绑定的fs_user
+     */
+    List<CompanyUserBindUserVO> getFsUserByCompanyUserId(FsUser fsUser);
 }

+ 473 - 0
fs-service/src/main/java/com/fs/company/service/impl/AsyncCalleeProcessorServiceImpl.java

@@ -0,0 +1,473 @@
+package com.fs.company.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.*;
+import com.fs.company.mapper.*;
+import com.fs.company.service.IAsyncCalleeProcessorService;
+import com.fs.company.util.RandomNameGeneratorUtil;
+import com.fs.company.vo.CompanyNodeInfoVo;
+import com.fs.enums.ExecutionStatusEnum;
+import com.fs.enums.NodeTypeEnum;
+import com.fs.his.config.CidPhoneConfig;
+import com.fs.system.service.ISysConfigService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+
+@Service
+public class AsyncCalleeProcessorServiceImpl implements IAsyncCalleeProcessorService {
+
+    private static final Logger log = LoggerFactory.getLogger(AsyncCalleeProcessorServiceImpl.class);
+
+    private static final int BATCH_SIZE = 1500;
+    private static final int CPU_YIELD_INTERVAL = 500;
+    private static final int SLEEP_INTERVAL = 2000;
+    private static final int SLEEP_MILLIS = 10;
+    private static final int PHONE_LENGTH = 11;
+    private static final char DIGIT_ZERO = '0';
+    private static final int RADIX_TEN = 10;
+
+    private final ISysConfigService configService;
+    private final CompanyWorkflowEdgeMapper edgeMapper;
+    private final CompanyConfigMapper companyConfigMapper;
+    private final CompanyWorkflowMapper companyWorkflowMapper;
+    private final CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
+    private final CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
+    private final CompanyVoiceRoboticBusinessMapper companyVoiceRoboticBusinessMapper;
+    private final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper;
+
+    AsyncCalleeProcessorServiceImpl(CompanyConfigMapper companyConfigMapper,
+                                    ISysConfigService configService,
+                                    CompanyWorkflowEdgeMapper edgeMapper,
+                                    CompanyWorkflowMapper companyWorkflowMapper,
+                                    CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper,
+                                    CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper,
+                                    CompanyVoiceRoboticBusinessMapper companyVoiceRoboticBusinessMapper,
+                                    CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper) {
+        this.companyConfigMapper = companyConfigMapper;
+        this.configService = configService;
+        this.edgeMapper = edgeMapper;
+        this.companyWorkflowMapper = companyWorkflowMapper;
+        this.companyAiWorkflowExecMapper = companyAiWorkflowExecMapper;
+        this.companyAiWorkflowExecLogMapper = companyAiWorkflowExecLogMapper;
+        this.companyVoiceRoboticBusinessMapper = companyVoiceRoboticBusinessMapper;
+        this.companyVoiceRoboticCalleesMapper = companyVoiceRoboticCalleesMapper;
+    }
+
+    @Override
+    @Async("calleeTaskExecutor")
+    public void generateCustomerInfo(List<CompanyVoiceRoboticCallees> calleesList,
+                                     Map<String, CompanyWxClient> clientMp,
+                                     CompanyVoiceRobotic robotic) {
+        if (calleesList == null || calleesList.isEmpty() || robotic == null) {
+            return;
+        }
+
+        Thread currentThread = Thread.currentThread();
+        if (currentThread.isInterrupted()) {
+            log.info("任务被中断,退出处理");
+            return;
+        }
+
+        CidPhoneConfig phoneConfig = loadPhoneConfig(robotic.getCompanyId());
+        if (phoneConfig == null || !Boolean.TRUE.equals(phoneConfig.getEnablePhoneConfig())) {
+            log.warn("电话配置未启用或为空,companyId: {}", robotic.getCompanyId());
+            return;
+        }
+
+        CompanyWorkflow workflow = loadWorkflow(robotic.getCompanyAiWorkflowId());
+        if (workflow == null) {
+            log.warn("工作流不存在,workflowId: {}", robotic.getCompanyAiWorkflowId());
+            return;
+        }
+
+        CompanyNodeInfoVo nodeInfoVo = loadNodeInfo(workflow.getWorkflowId(), workflow.getStartNodeKey());
+        if (nodeInfoVo == null) {
+            log.warn("节点信息不存在,workflowId: {}, startNodeKey: {}", workflow.getWorkflowId(), workflow.getStartNodeKey());
+            return;
+        }
+
+        List<CompanyVoiceRoboticCallees> batchToInsert = new ArrayList<>(BATCH_SIZE);
+        int processedCount = 0;
+
+        for (CompanyVoiceRoboticCallees callees : calleesList) {
+            if (currentThread.isInterrupted()) {
+                log.info("任务被中断,已处理 {} 条", processedCount);
+                break;
+            }
+
+            try {
+                generatePhoneNumberInBatch(phoneConfig, callees, batchToInsert, clientMp, robotic, workflow, nodeInfoVo);
+                processedCount++;
+
+                if (processedCount % CPU_YIELD_INTERVAL == 0) {
+                    Thread.yield();
+                }
+
+                if (processedCount % SLEEP_INTERVAL == 0) {
+                    Thread.sleep(SLEEP_MILLIS);
+                }
+            } catch (InterruptedException e) {
+                log.info("任务被中断,已处理 {} 条", processedCount);
+                Thread.currentThread().interrupt();
+                break;
+            } catch (Exception e) {
+                log.error("处理被叫信息异常,phone: {}", callees.getPhone(), e);
+            }
+        }
+
+        log.info("generateCustomerInfo处理完成,共处理 {} 条", processedCount);
+    }
+
+    private CidPhoneConfig loadPhoneConfig(Long companyId) {
+        try {
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(companyId, "cid.config");
+            if (companyConfig != null && StringUtils.isNotEmpty(companyConfig.getConfigValue())) {
+                return JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
+            }
+            String json = configService.selectConfigByKey("his.store");
+            if (StringUtils.isNotEmpty(json)) {
+                return JSONObject.parseObject(json, CidPhoneConfig.class);
+            }
+        } catch (Exception e) {
+            log.error("加载电话配置异常,companyId: {}", companyId, e);
+        }
+        return null;
+    }
+
+    private CompanyWorkflow loadWorkflow(Long workflowId) {
+        try {
+            return companyWorkflowMapper.selectCompanyWorkflowById(workflowId);
+        } catch (Exception e) {
+            log.error("加载工作流异常,workflowId: {}", workflowId, e);
+            return null;
+        }
+    }
+
+    private CompanyNodeInfoVo loadNodeInfo(Long workflowId, String startNodeKey) {
+        try {
+            return edgeMapper.slectNodeInfoByWorkflowId(workflowId, startNodeKey);
+        } catch (Exception e) {
+            log.error("加载节点信息异常,workflowId: {}, startNodeKey: {}", workflowId, startNodeKey, e);
+            return null;
+        }
+    }
+
+    private void generatePhoneNumberInBatch(CidPhoneConfig config,
+                                            CompanyVoiceRoboticCallees callees,
+                                            List<CompanyVoiceRoboticCallees> batchToInsert,
+                                            Map<String, CompanyWxClient> clientMp,
+                                            CompanyVoiceRobotic robotic,
+                                            CompanyWorkflow workflow,
+                                            CompanyNodeInfoVo nodeInfoVo) {
+        String basePhone = callees.getPhone();
+        if (basePhone == null || basePhone.length() != PHONE_LENGTH) {
+            return;
+        }
+
+        int start = config.getStartIndex();
+        int end = config.getEndIndex();
+        int totalCount = config.getGenerateCount();
+
+        if (!isValidRange(start, end)) {
+            return;
+        }
+
+        int startIdx = start - 1;
+        int endIdx = end - 1;
+        char[] baseChars = basePhone.toCharArray();
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+        int nameIndex = 0;
+
+        while (nameIndex < totalCount) {
+            if (Thread.currentThread().isInterrupted()) {
+                break;
+            }
+
+            int currentBatchSize = Math.min(BATCH_SIZE, totalCount - nameIndex);
+
+            for (int i = 0; i < currentBatchSize; i++) {
+                CompanyVoiceRoboticCallees roboticCallees = createCalleesWithPhone(
+                        baseChars, startIdx, endIdx, random, callees);
+                batchToInsert.add(roboticCallees);
+            }
+
+            nameIndex += currentBatchSize;
+
+            if (batchToInsert.size() >= BATCH_SIZE || nameIndex >= totalCount) {
+                flushGeneratedCalleesBatch(batchToInsert, clientMp, robotic, workflow, nodeInfoVo);
+            }
+
+            if (nameIndex % SLEEP_INTERVAL == 0) {
+                try {
+                    Thread.sleep(SLEEP_MILLIS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    break;
+                }
+            }
+        }
+    }
+
+    private boolean isValidRange(int start, int end) {
+        return start >= 1 && start <= PHONE_LENGTH
+                && end >= 1 && end <= PHONE_LENGTH
+                && start <= end;
+    }
+
+    private CompanyVoiceRoboticCallees createCalleesWithPhone(char[] baseChars, int startIdx, int endIdx,
+                                                              ThreadLocalRandom random,
+                                                              CompanyVoiceRoboticCallees template) {
+        char[] newChars = baseChars.clone();
+        for (int j = startIdx; j <= endIdx; j++) {
+            newChars[j] = (char) (DIGIT_ZERO + random.nextInt(RADIX_TEN));
+        }
+
+        CompanyVoiceRoboticCallees roboticCallees = new CompanyVoiceRoboticCallees();
+        roboticCallees.setPhone(new String(newChars));
+        roboticCallees.setRoboticId(template.getRoboticId());
+        roboticCallees.setTaskFlow(template.getTaskFlow());
+        roboticCallees.setRunTaskFlow(template.getRunTaskFlow());
+        roboticCallees.setIsWeCom(template.getIsWeCom());
+        roboticCallees.setUserId(0L);
+        roboticCallees.setUserName(RandomNameGeneratorUtil.generateOne());
+        roboticCallees.setIsGenerate(1);
+
+        return roboticCallees;
+    }
+
+    private void flushGeneratedCalleesBatch(List<CompanyVoiceRoboticCallees> batchToInsert,
+                                            Map<String, CompanyWxClient> clientMap,
+                                            CompanyVoiceRobotic robotic,
+                                            CompanyWorkflow workflow,
+                                            CompanyNodeInfoVo nodeInfoVo) {
+        if (batchToInsert == null || batchToInsert.isEmpty()) {
+            return;
+        }
+
+        try {
+            int rows = companyVoiceRoboticCalleesMapper.batchInsertGenerateInfo(batchToInsert);
+            if (rows > 0) {
+                generateVoiceRoboticBusiness(batchToInsert, clientMap, robotic, workflow, nodeInfoVo);
+            }
+        } catch (Exception e) {
+            log.error("批量插入被叫数据异常,size: {}", batchToInsert.size(), e);
+        } finally {
+            batchToInsert.clear();
+        }
+    }
+
+    private void generateVoiceRoboticBusiness(List<CompanyVoiceRoboticCallees> calleesList,
+                                              Map<String, CompanyWxClient> clientMp,
+                                              CompanyVoiceRobotic robotic,
+                                              CompanyWorkflow workflow,
+                                              CompanyNodeInfoVo nodeInfoVo) {
+        if (calleesList == null || calleesList.isEmpty()) {
+            return;
+        }
+
+        List<CompanyVoiceRoboticBusiness> addList = new ArrayList<>(calleesList.size());
+        Date date = new Date();
+
+        for (CompanyVoiceRoboticCallees callees : calleesList) {
+            CompanyVoiceRoboticBusiness business = createBusiness(callees, clientMp, date);
+            addList.add(business);
+        }
+
+        if (!addList.isEmpty()) {
+            flushGeneratedBusinessBatch(addList, robotic, workflow, nodeInfoVo);
+        }
+    }
+
+    private CompanyVoiceRoboticBusiness createBusiness(CompanyVoiceRoboticCallees callees,
+                                                       Map<String, CompanyWxClient> clientMp,
+                                                       Date date) {
+        CompanyVoiceRoboticBusiness business = new CompanyVoiceRoboticBusiness();
+        business.setRoboticId(callees.getRoboticId());
+        business.setCalleeId(callees.getId());
+
+        String clientKey = callees.getRoboticId() + "-" + callees.getUserId();
+        CompanyWxClient client = clientMp.get(clientKey);
+        business.setWxClientId(client != null ? client.getId() : null);
+
+        business.setAddWxDone(0);
+        business.setCallPhoneDone(0);
+        business.setSendMsgDone(0);
+        business.setCreateTime(date);
+        business.setIsGenerate(1);
+
+        return business;
+    }
+
+    private void flushGeneratedBusinessBatch(List<CompanyVoiceRoboticBusiness> batchToInsert,
+                                             CompanyVoiceRobotic robotic,
+                                             CompanyWorkflow workflow,
+                                             CompanyNodeInfoVo nodeInfoVo) {
+        if (batchToInsert == null || batchToInsert.isEmpty()) {
+            return;
+        }
+
+        try {
+            int rows = companyVoiceRoboticBusinessMapper.insertBatchGenerateInfo(batchToInsert);
+            if (rows > 0) {
+                generateWorkflowExecRecords(batchToInsert, robotic, workflow, nodeInfoVo);
+            }
+        } catch (Exception e) {
+            log.error("批量插入业务数据异常,size: {}", batchToInsert.size(), e);
+        } finally {
+            batchToInsert.clear();
+        }
+    }
+
+    private void generateWorkflowExecRecords(List<CompanyVoiceRoboticBusiness> businessList,
+                                             CompanyVoiceRobotic robotic,
+                                             CompanyWorkflow workflow,
+                                             CompanyNodeInfoVo nodeInfoVo) {
+        if (businessList == null || businessList.isEmpty()) {
+            return;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        List<CompanyAiWorkflowExec> startExecList = new ArrayList<>(businessList.size());
+        List<CompanyAiWorkflowExec> targetExecList = new ArrayList<>(businessList.size());
+
+        for (CompanyVoiceRoboticBusiness business : businessList) {
+            if (Thread.currentThread().isInterrupted()) {
+                break;
+            }
+
+            CompanyAiWorkflowExec startExec = createStartExec(business, robotic, workflow, now);
+            startExecList.add(startExec);
+
+            CompanyAiWorkflowExec targetExec = createTargetExec(startExec, nodeInfoVo, now);
+            targetExecList.add(targetExec);
+
+            if (targetExecList.size() >= BATCH_SIZE) {
+                persistExecLogs(targetExecList, startExecList);
+            }
+        }
+
+        if (!targetExecList.isEmpty()) {
+            persistExecLogs(targetExecList, startExecList);
+        }
+    }
+
+    private CompanyAiWorkflowExec createStartExec(CompanyVoiceRoboticBusiness business,
+                                                  CompanyVoiceRobotic robotic,
+                                                  CompanyWorkflow workflow,
+                                                  LocalDateTime now) {
+        CompanyAiWorkflowExec exec = new CompanyAiWorkflowExec();
+        exec.setWorkflowInstanceId(generateInstanceId());
+        exec.setWorkflowId(robotic.getCompanyAiWorkflowId());
+        exec.setCurrentNodeKey(workflow.getStartNodeKey());
+        exec.setCurrentNodeType(NodeTypeEnum.START.getValue());
+        exec.setCurrentNodeName(NodeTypeEnum.START.getDescription());
+        exec.setStatus(ExecutionStatusEnum.SUCCESS.getValue());
+        exec.setStartTime(now);
+        exec.setVariables(buildVariables(robotic, business));
+        exec.setBusinessKey(business.getId());
+        exec.setStartNodeKey(workflow.getStartNodeKey());
+        exec.setEndNodeKey(workflow.getEndNodeKey());
+        exec.setCidGroupNo(robotic.getCidGroupNo());
+        exec.setRuntimeRangeStart(robotic.getRuntimeRangeStart());
+        exec.setRuntimeRangeEnd(robotic.getRuntimeRangeEnd());
+        exec.setIsGenerate(1);
+        return exec;
+    }
+
+    private CompanyAiWorkflowExec createTargetExec(CompanyAiWorkflowExec startExec,
+                                                   CompanyNodeInfoVo nodeInfoVo,
+                                                   LocalDateTime now) {
+        CompanyAiWorkflowExec exec = new CompanyAiWorkflowExec();
+        exec.setWorkflowInstanceId(startExec.getWorkflowInstanceId());
+        exec.setWorkflowId(startExec.getWorkflowId());
+        exec.setCurrentNodeKey(nodeInfoVo.getTargetNodeKey());
+        exec.setCurrentNodeName(nodeInfoVo.getNodeName());
+        exec.setCurrentNodeType(NodeTypeEnum.fromCode(nodeInfoVo.getNodeType()).getValue());
+        exec.setStatus(ExecutionStatusEnum.INTERRUPT.getValue());
+        exec.setStartTime(now);
+        exec.setVariables(startExec.getVariables());
+        exec.setBusinessKey(startExec.getBusinessKey());
+        exec.setStartNodeKey(startExec.getStartNodeKey());
+        exec.setEndNodeKey(startExec.getEndNodeKey());
+        exec.setCidGroupNo(startExec.getCidGroupNo());
+        exec.setRuntimeRangeStart(startExec.getRuntimeRangeStart());
+        exec.setRuntimeRangeEnd(startExec.getRuntimeRangeEnd());
+        exec.setIsGenerate(1);
+        return exec;
+    }
+
+    private String buildVariables(CompanyVoiceRobotic robotic, CompanyVoiceRoboticBusiness business) {
+        JSONObject variables = new JSONObject(5);
+        variables.put("roboticId", robotic.getId());
+        variables.put("businessId", business.getId());
+        variables.put("cidGroupNo", robotic.getCidGroupNo());
+        variables.put("runtimeRangeStart", robotic.getRuntimeRangeStart());
+        variables.put("runtimeRangeEnd", robotic.getRuntimeRangeEnd());
+        return variables.toJSONString();
+    }
+
+    private void persistExecLogs(List<CompanyAiWorkflowExec> workflowExecs,
+                                 List<CompanyAiWorkflowExec> startExecList) {
+        if (workflowExecs == null || workflowExecs.isEmpty()) {
+            return;
+        }
+
+        try {
+            int rows = companyAiWorkflowExecMapper.insertBatchInfo(workflowExecs);
+            if (rows > 0) {
+                workflowExecLogBatchInsert(startExecList);
+                workflowExecs.forEach(exec -> exec.setStatus(ExecutionStatusEnum.FAILURE.getValue()));
+                workflowExecLogBatchInsert(workflowExecs);
+            }
+        } catch (Exception e) {
+            log.error("持久化执行日志异常,size: {}", workflowExecs.size(), e);
+        } finally {
+            workflowExecs.clear();
+            startExecList.clear();
+        }
+    }
+
+    private void workflowExecLogBatchInsert(List<CompanyAiWorkflowExec> workflowExecs) {
+        if (workflowExecs == null || workflowExecs.isEmpty()) {
+            return;
+        }
+
+        List<CompanyAiWorkflowExecLog> batch = new ArrayList<>(workflowExecs.size());
+        Date date = new Date();
+
+        for (CompanyAiWorkflowExec w : workflowExecs) {
+            CompanyAiWorkflowExecLog execLog = new CompanyAiWorkflowExecLog();
+            execLog.setWorkflowInstanceId(w.getWorkflowInstanceId());
+            execLog.setNodeKey(w.getCurrentNodeKey());
+            execLog.setNodeName(w.getCurrentNodeName());
+            execLog.setNodeType(w.getCurrentNodeType());
+            execLog.setInputData(w.getVariables());
+            execLog.setStatus(w.getStatus());
+            execLog.setOutputData("null");
+            execLog.setStartTime(date);
+            execLog.setEndTime(date);
+            execLog.setIsGenerate(1);
+            batch.add(execLog);
+        }
+
+        try {
+            companyAiWorkflowExecLogMapper.batchInsert(batch);
+        } catch (Exception e) {
+            log.error("批量插入执行日志异常,size: {}", batch.size(), e);
+        } finally {
+            batch.clear();
+        }
+    }
+
+    private String generateInstanceId() {
+        return "wf_" + System.currentTimeMillis() + "_" +
+                UUID.randomUUID().toString().replace("-", "");
+    }
+}

+ 72 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java

@@ -23,9 +23,12 @@ import com.fs.company.param.CompanyUserQwParam;
 import com.fs.company.service.*;
 import com.fs.company.vo.*;
 import com.fs.course.service.IFsUserCompanyUserService;
+import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsCityService;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.CitysAreaVO;
+import com.fs.his.vo.CompanyUserBindUserVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.domain.FsStoreProductAttrScrm;
 import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
@@ -130,6 +133,9 @@ public class CompanyUserServiceImpl implements ICompanyUserService
     @Autowired
     private CompanyMapper companyMapper;
 
+    @Autowired
+    private  CompanyFsUserMapper companyFsUserMapper;
+
 
     /**
      * 查询物业公司管理员信息
@@ -1128,4 +1134,70 @@ public class CompanyUserServiceImpl implements ICompanyUserService
         return companyMapper.getCompanyList(corpId);
 
     }
+
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int bindCompanyUserAndFsUser(Long companyUserId, Long fsUserId) {
+        // 检查该用户是否已经与其他销售绑定
+        CompanyFsUser existingBinding = companyFsUserMapper.selectByFsUserId(fsUserId);
+
+        if (existingBinding != null) {
+            // 如果已存在绑定关系,检查是否是同一个销售
+            if (existingBinding.getCompanyUserId().equals(companyUserId)) {
+                // 已经是同一个销售,更新状态并返回成功
+                existingBinding.setStatus(1);
+                existingBinding.setCreateTime(DateUtils.getNowDate());
+                return companyFsUserMapper.updateCompanyFsUser(existingBinding);
+            } else {
+                // 已绑定其他销售,需要先解绑
+                throw new CustomException("该用户已绑定销售:" +
+                        companyUserService.selectCompanyUserNameUserById(existingBinding.getCompanyUserId()) +
+                        ",需先解绑后才能重新绑定");
+            }
+        }
+
+        // 没有绑定记录,创建新的绑定关系
+        CompanyFsUser companyFsUser = new CompanyFsUser();
+        companyFsUser.setCompanyUserId(companyUserId);
+        companyFsUser.setFsUserId(fsUserId);
+        companyFsUser.setStatus(1);
+        companyFsUser.setCreateTime(DateUtils.getNowDate());
+        return companyFsUserMapper.insertCompanyFsUser(companyFsUser);
+    }
+
+    @Override
+    public boolean unbindCompanyUserAndFsUser(Long companyUserId, Long userId) {
+        // 校验参数
+        if (companyUserId == null || userId == null) {
+            throw new IllegalArgumentException("销售 ID 或用户 ID 不能为空");
+        }
+
+        // 查询是否存在绑定关系
+        CompanyFsUser relation = companyFsUserMapper.selectByCompanyUserIdAndUserId(companyUserId, userId);
+        if (relation == null) {
+            throw new RuntimeException("未找到销售与用户之间的绑定关系");
+        }
+
+        // 删除绑定关系
+        int rows = companyFsUserMapper.deleteById(relation.getId());
+        return rows > 0;
+    }
+
+
+    @Override
+    public List<CompanyUserBindUserVO> getFsUserByCompanyUserId(FsUser fsUser) {
+        //加密手机号
+        if (StringUtils.isNotBlank(fsUser.getPhone())) {
+            fsUser.setPhone(PhoneUtil.encryptPhone(fsUser.getPhone()));
+        }
+        List<CompanyUserBindUserVO> fsUsers = companyFsUserMapper.selecUserList(fsUser);
+        for (CompanyUserBindUserVO user : fsUsers) {
+            if (StringUtils.isNotBlank(user.getPhone())) {
+                user.setPhone(PhoneUtil.decryptPhone(user.getPhone()));
+            }
+        }
+        return fsUsers;
+    }
+
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -30,6 +30,7 @@ import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.DictVO;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.store.config.StoreConfig;
@@ -72,6 +73,8 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Autowired
     CompanyWorkflowEngine companyWorkflowEngine;
     @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+    @Autowired
     QwUserMapper qwUserMapper;
     @Autowired
     @Qualifier("cidWorkFlowExecutor")
@@ -332,6 +335,8 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 BigDecimal multiply = divide.multiply(callCharge);
                 companyVoiceRoboticCallLog.setCost(multiply);
                 baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
+                // 更新用户标签
+                crmCustomerPropertyService.addPropertyByCallLog(companyVoiceRoboticCallLog);
 
                 if (StringUtils.isNotBlank(result.getBizJson())) {
                     JSONObject bizJson = JSONObject.parseObject(result.getBizJson());

+ 5 - 287
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -23,7 +23,6 @@ import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.*;
-import com.fs.company.util.RandomNameGeneratorUtil;
 import com.fs.company.vo.*;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.crm.domain.CrmCustomer;
@@ -33,14 +32,11 @@ import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import com.fs.enums.TaskTypeEnum;
-import com.fs.his.config.CidPhoneConfig;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
-import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysDictDataMapper;
-import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.impl.SysDictTypeServiceImpl;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -49,11 +45,8 @@ import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-
-import java.time.LocalDateTime;
 import java.util.*;
 import java.util.stream.Collectors;
-
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
 
 
@@ -75,7 +68,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
     private final AiCallService aiCallService;
     private final CrmCustomerServiceImpl crmCustomerService;
-    private final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper;
 
     private final CompanyVoiceRoboticCalleesServiceImpl companyVoiceRoboticCalleesService;
     private final ICompanyWxAccountService companyWxAccountService;
@@ -90,8 +82,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     private final CompanyWxClientServiceImpl companyWxClientServiceImpl;
 
-    private final ISysConfigService configService;
-
     private final SysDictDataMapper sysDictDataMapper;
 
     private final SmsServiceImpl smsService;
@@ -111,23 +101,17 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final CompanyWorkflowEngine companyWorkflowEngine;
     private final CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
     private final CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
+
+    private final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper;
     private final RedisCache redisCache2;
     private final CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
     private final QwUserMapper qwUserMapper;
     private final EasyCallMapper easyCallMapper;
-
-    private final SysConfigMapper sysConfigMapper;
-    private final CompanyConfigMapper companyConfigMapper;
-
-    private final CompanyWorkflowMapper companyWorkflowMapper;
-
-    private final CompanyWorkflowEdgeMapper edgeMapper;
-
     private final QwExternalContactServiceImpl qwExternalContactService;
 
     private final SysDictTypeServiceImpl sysDictTypeService;
 
-    final int BATCH_SIZE = 1500;
+    private final IAsyncCalleeProcessorService asyncCalleeProcessorService;
 
     /** EasyCall intent 意向度重试队列 Redis key 前缀,value 为已重试次数 */
     private static final String EASYCALL_INTENT_RETRY_KEY = "easycall:intent:retry:";
@@ -1152,7 +1136,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             initAndExecuteWorkflows(robotic, roboticBusinesseList);
         }
     }
-
     /**
      * 初始化场景任务客户流程
      * @param robotic
@@ -1417,42 +1400,12 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     }
     public void buildTaskBussiness(CompanyVoiceRobotic robotic) {
         List<CompanyVoiceRoboticCallees> calleesList = companyVoiceRoboticCalleesMapper.selectByRoboticId(robotic.getId());
-        //获取电话生成配置
-        CidPhoneConfig phoneConfig = null;
-        //获取销售公司手机配置
-        CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(robotic.getCompanyId(), "cid.config");
-        //如果配置为空就获取总后台配置
-        if(companyConfig == null){
-            String json = configService.selectConfigByKey("his.store");
-            if(StringUtils.isNotEmpty(json)){
-                phoneConfig = JSONObject.parseObject(json,CidPhoneConfig.class);
-            }
-        } else {
-            phoneConfig = JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
-        }
-
-        //获取工作流主表信息
-        CompanyWorkflow workflow = companyWorkflowMapper.selectCompanyWorkflowById(robotic.getCompanyAiWorkflowId());
-
-        //获取相关节点信息
-        CompanyNodeInfoVo nodeInfoVo = edgeMapper.slectNodeInfoByWorkflowId(workflow.getWorkflowId(), workflow.getStartNodeKey());
-
         List<CompanyWxClient> companyWxClients = companyWxClientMapper.selectListByRoboticId(robotic.getId());
         Map<String, CompanyWxClient> clientMp = companyWxClients.stream().collect(Collectors.toMap(e -> e.getRoboticId() + "-" + e.getCustomerId(), e -> e));
         List<CompanyVoiceRoboticBusiness> addList = new ArrayList<>();
-        List<CompanyVoiceRoboticCallees> batchToInsert = new LinkedList<>();
+        //异步生成列表日志
+        asyncCalleeProcessorService.generateCustomerInfo(calleesList,clientMp,robotic);
         for (CompanyVoiceRoboticCallees callees : calleesList) {
-            //根据配置随机生成电话号
-            if (phoneConfig != null && phoneConfig.getEnablePhoneConfig()) {//配置不为空并且开启了
-                List<CompanyVoiceRoboticCallees> roboticCallees = generatePhoneNumber(phoneConfig, callees);
-                if (!roboticCallees.isEmpty()) {
-                    batchToInsert.addAll(roboticCallees);
-                    if (batchToInsert.size() >= BATCH_SIZE) {
-                        flushGeneratedCalleesBatch(new LinkedList<>(batchToInsert), clientMp, robotic, workflow, nodeInfoVo);
-                        batchToInsert.clear();
-                    }
-                }
-            }
             CompanyVoiceRoboticBusiness companyVoiceRoboticBusiness = new CompanyVoiceRoboticBusiness();
             companyVoiceRoboticBusiness.setRoboticId(robotic.getId());
             companyVoiceRoboticBusiness.setCalleeId(callees.getId());
@@ -1463,13 +1416,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             companyVoiceRoboticBusiness.setCreateTime(new Date());
             addList.add(companyVoiceRoboticBusiness);
         }
-
-        //处理剩余数据
-        if (!batchToInsert.isEmpty()) {
-            flushGeneratedCalleesBatch(new LinkedList<>(batchToInsert), clientMp, robotic, workflow, nodeInfoVo);
-            batchToInsert.clear();
-        }
-
         companyVoiceRoboticBusinessMapper.insertBatch(addList);
 
     }
@@ -1679,232 +1625,4 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return vo;
         }).collect(Collectors.toList());
     }
-
-    /**
-     * 根据配置生成手机号
-     *
-     * @param config  配置对象(包含是否启用、生成数量、起始位置、结束位置)
-     * @param callees 任务外呼电话对象
-     * @return 生成的手机号列表
-     */
-    public static List<CompanyVoiceRoboticCallees> generatePhoneNumber(CidPhoneConfig config, CompanyVoiceRoboticCallees callees) {
-        String basePhone = callees.getPhone();
-        if (basePhone == null || basePhone.length() != 11) {
-            return null;
-        }
-
-        int start = config.getStartIndex();
-        int end = config.getEndIndex();
-        int count = config.getGenerateCount();
-
-        // 校验索引范围
-        if (start < 1 || start > 11 || end < 1 || end > 11 || start > end) {
-            return null;
-        }
-
-        int startIdx = start;
-        int endIdx = end - 1;
-
-        char[] baseChars = basePhone.toCharArray();
-        //预加载随机
-        List<String> nameList = RandomNameGeneratorUtil.generateBatch(count);
-        Random random = new Random();
-        List<CompanyVoiceRoboticCallees> result = new ArrayList<>(count);
-        for (int i = 0; i < count; i++) {
-            CompanyVoiceRoboticCallees roboticCallees = new CompanyVoiceRoboticCallees();
-            // 克隆基础数组
-            char[] newChars = baseChars.clone();
-            for (int j = startIdx; j <= endIdx; j++) {
-                // 生成0-9的随机数字字符
-                newChars[j] = (char) ('0' + random.nextInt(10));
-            }
-            String phone = String.valueOf(newChars);
-            roboticCallees.setPhone(phone);//电话
-            roboticCallees.setRoboticId(callees.getRoboticId());//任务ID
-            roboticCallees.setTaskFlow(callees.getTaskFlow());//任务流程
-            roboticCallees.setRunTaskFlow(callees.getRunTaskFlow());//已执行流程
-            roboticCallees.setIsWeCom(callees.getIsWeCom());
-            roboticCallees.setUserId(0L);//用户ID
-            roboticCallees.setUserName(nameList.get(i));//姓名
-            roboticCallees.setIsGenerate(1);
-            result.add(roboticCallees);
-        }
-        return result;
-    }
-
-    /**
-     * 生成外呼任务表
-     *
-     **/
-    public void generateVoiceRoboticBusiness(List<CompanyVoiceRoboticCallees> calleesList,
-                                             Map<String, CompanyWxClient> clientMp
-            , CompanyVoiceRobotic robotic
-            , CompanyWorkflow workflow
-            , CompanyNodeInfoVo nodeInfoVo) {
-        if (!calleesList.isEmpty()) {
-            List<CompanyVoiceRoboticBusiness> addList = new LinkedList<>();
-            Date date = new Date();
-            for (CompanyVoiceRoboticCallees callees : calleesList) {
-                CompanyVoiceRoboticBusiness companyVoiceRoboticBusiness = new CompanyVoiceRoboticBusiness();
-                companyVoiceRoboticBusiness.setRoboticId(callees.getRoboticId());
-                companyVoiceRoboticBusiness.setCalleeId(callees.getId());
-                companyVoiceRoboticBusiness.setWxClientId(clientMp.getOrDefault(callees.getRoboticId() + "-" + callees.getUserId(), new CompanyWxClient()).getId());
-                companyVoiceRoboticBusiness.setAddWxDone(0);
-                companyVoiceRoboticBusiness.setCallPhoneDone(0);
-                companyVoiceRoboticBusiness.setSendMsgDone(0);
-                companyVoiceRoboticBusiness.setCreateTime(date);
-                companyVoiceRoboticBusiness.setIsGenerate(1);
-                addList.add(companyVoiceRoboticBusiness);
-            }
-
-            //批量插入ai外呼业务对象
-            if (!addList.isEmpty()) {
-                flushGeneratedBusinessBatch(addList, robotic, workflow, nodeInfoVo);
-            }
-        }
-    }
-
-    /**
-     * 刷新并批量插入生成的被叫数据,并生成对应的业务对象
-     *
-     * @param batchToInsert 待插入的被叫列表
-     * @param clientMap     微信客户映射,用于关联客户ID
-     */
-    @Async("calleeTaskExecutor")
-    public void flushGeneratedCalleesBatch(List<CompanyVoiceRoboticCallees> batchToInsert,
-                                           Map<String, CompanyWxClient> clientMap,
-                                           CompanyVoiceRobotic robotic,
-                                           CompanyWorkflow workflow,
-                                           CompanyNodeInfoVo nodeInfoVo) {
-        if (batchToInsert.isEmpty()) {
-            return;
-        }
-        int rows = companyVoiceRoboticCalleesMapper.batchInsertGenerateInfo(batchToInsert);
-        if (rows > 0) {
-            generateVoiceRoboticBusiness(batchToInsert, clientMap, robotic, workflow, nodeInfoVo);
-        }
-        batchToInsert.clear();
-    }
-
-    /**
-     * 批量插入业务对象,并创建工作流执行记录
-     */
-    private void flushGeneratedBusinessBatch(List<CompanyVoiceRoboticBusiness> batchToInsert,CompanyVoiceRobotic robotic,CompanyWorkflow workflow,CompanyNodeInfoVo nodeInfoVo){
-        int rows = companyVoiceRoboticBusinessMapper.insertBatchGenerateInfo(batchToInsert);
-        if(rows > 0){
-            LocalDateTime now = LocalDateTime.now();
-            List<CompanyAiWorkflowExec> startExecList = new LinkedList<>();//第一节点
-            List<CompanyAiWorkflowExec> targetExecList = new LinkedList<>();//第二节点
-            //插入执行流程代码
-            for (CompanyVoiceRoboticBusiness business : batchToInsert){
-                // 第一个节点(开始节点)
-                CompanyAiWorkflowExec startExec = new CompanyAiWorkflowExec();
-                startExec.setWorkflowInstanceId(generateInstanceId());
-                startExec.setWorkflowId(robotic.getCompanyAiWorkflowId());
-                startExec.setCurrentNodeKey(workflow.getStartNodeKey());
-                startExec.setCurrentNodeType(NodeTypeEnum.START.getValue());
-                startExec.setCurrentNodeName(NodeTypeEnum.START.getDescription());
-                startExec.setStatus(ExecutionStatusEnum.SUCCESS.getValue()); // 开始节点执行成功
-                startExec.setStartTime(now);
-
-                JSONObject variables = new JSONObject();
-                variables.put("roboticId", robotic.getId());
-                variables.put("businessId", business.getId());
-                variables.put("cidGroupNo", robotic.getCidGroupNo());
-                variables.put("runtimeRangeStart", robotic.getRuntimeRangeStart());
-                variables.put("runtimeRangeEnd", robotic.getRuntimeRangeEnd());
-                startExec.setVariables(variables.toJSONString());
-                startExec.setBusinessKey(business.getId().toString());
-                startExec.setStartNodeKey(workflow.getStartNodeKey());
-                startExec.setEndNodeKey(workflow.getEndNodeKey());
-                startExec.setCidGroupNo(robotic.getCidGroupNo());
-                startExec.setRuntimeRangeStart(robotic.getRuntimeRangeStart());
-                startExec.setRuntimeRangeEnd(robotic.getRuntimeRangeEnd());
-                startExec.setIsGenerate(1);
-                startExecList.add(startExec);
-
-                CompanyAiWorkflowExec targetExec = new CompanyAiWorkflowExec();
-                // 复制公共字段
-                targetExec.setWorkflowInstanceId(startExec.getWorkflowInstanceId());
-                targetExec.setWorkflowId(startExec.getWorkflowId());
-                targetExec.setCurrentNodeKey(nodeInfoVo.getTargetNodeKey());
-                targetExec.setCurrentNodeName(nodeInfoVo.getNodeName());
-                targetExec.setCurrentNodeType(NodeTypeEnum.fromCode(nodeInfoVo.getNodeType()).getValue());
-                targetExec.setStatus(ExecutionStatusEnum.INTERRUPT.getValue());
-                targetExec.setStartTime(now);
-                targetExec.setVariables(variables.toJSONString());
-                targetExec.setBusinessKey(startExec.getBusinessKey());
-                targetExec.setStartNodeKey(startExec.getStartNodeKey());
-                targetExec.setEndNodeKey(startExec.getEndNodeKey());
-                targetExec.setCidGroupNo(startExec.getCidGroupNo());
-                targetExec.setRuntimeRangeStart(startExec.getRuntimeRangeStart());
-                targetExec.setRuntimeRangeEnd(startExec.getRuntimeRangeEnd());
-                targetExec.setIsGenerate(1);
-                targetExecList.add(targetExec);
-
-                if(targetExecList.size() >= BATCH_SIZE){
-                    generateVoiceRoboticCurrentExecLogs(targetExecList, startExecList);
-                }
-            }
-
-            if(!targetExecList.isEmpty()){
-                generateVoiceRoboticCurrentExecLogs(targetExecList, startExecList);
-            }
-        }
-        batchToInsert.clear();
-    }
-
-    public void generateVoiceRoboticCurrentExecLogs(List<CompanyAiWorkflowExec> workflowExecs, List<CompanyAiWorkflowExec> startExecList){
-        if (workflowExecs.isEmpty()) {
-            return;
-        }
-
-        int rows = companyAiWorkflowExecMapper.insertBatchInfo(workflowExecs);
-        if(rows > 0){
-            workflowExecLogBatchInsert(startExecList);//第一节点
-            workflowExecs.stream().forEach(a->a.setStatus(ExecutionStatusEnum.FAILURE.getValue()));
-            workflowExecLogBatchInsert(workflowExecs);//第二节点
-        }
-        workflowExecs.clear();
-        startExecList.clear();
-    }
-
-    /**
-     * AI外呼流程执行记录对象批量插入
-     * **/
-    private void workflowExecLogBatchInsert(List<CompanyAiWorkflowExec> workflowExecs){
-        //插入日志记录表
-        Date date =new Date();
-        List<CompanyAiWorkflowExecLog> batch = new LinkedList<>();
-        workflowExecs.forEach(w->{
-            CompanyAiWorkflowExecLog execLog = new CompanyAiWorkflowExecLog();
-            execLog.setWorkflowInstanceId(w.getWorkflowInstanceId());
-            execLog.setNodeKey(w.getCurrentNodeKey());
-            execLog.setNodeName(w.getCurrentNodeName());
-            execLog.setNodeType(w.getCurrentNodeType());
-            execLog.setInputData(w.getVariables());
-            execLog.setStatus(w.getStatus());
-            execLog.setOutputData("null");
-            execLog.setStartTime(date);
-            execLog.setEndTime(date);
-            execLog.setIsGenerate(1);
-            batch.add(execLog);
-        });
-        if(!batch.isEmpty()){
-            companyAiWorkflowExecLogMapper.batchInsert(batch);
-            batch.clear();
-        }
-        workflowExecs.clear();
-    }
-
-
-    /**
-     * 生成工作流实例ID
-     */
-    private String generateInstanceId() {
-        return "wf_" + System.currentTimeMillis() + "_" +
-                UUID.randomUUID().toString().replace("-", "");
-    }
-
-
 }

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -368,7 +368,7 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
         context.setVariables(inputVariables != null ? inputVariables : new HashMap<>());
         context.setStartTime(LocalDateTime.now());
         context.setCurrentTime(LocalDateTime.now());
-        context.setBusinessId(inputVariables.containsKey("businessId") ? inputVariables.get("businessId").toString() : null);
+        context.setBusinessId(inputVariables.containsKey("businessId") ? Long.parseLong(inputVariables.get("businessId").toString()) : null);
 
         return context;
     }

+ 114 - 2
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -1,8 +1,16 @@
 package com.fs.company.service.impl;
 
+import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.mapper.CompanyVoiceRoboticMapper;
 import com.fs.company.param.EntryCustomerParam;
@@ -10,18 +18,29 @@ import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.IGeneralCustomerEntryService;
 import com.fs.company.util.CryptoUtil;
 import com.fs.company.util.PhoneNumberUtil;
+import com.fs.config.ai.AiHostProper;
 import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
 import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import com.fs.crm.utils.CrmCustomerAiTagUtil;
+import com.fs.crm.vo.CrmCustomerAiTagVo;
+import com.fs.fastgptApi.param.ChatParam;
+import com.fs.fastgptApi.service.ChatService;
+import com.fs.hisapi.util.MapUtil;
+import com.fs.system.mapper.SysDictDataMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.logging.log4j.core.util.UuidUtil;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalTime;
-import java.util.HashMap;
-import java.util.List;
+import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
@@ -45,6 +64,8 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
     CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
     @Autowired
     ICompanyVoiceRoboticService companyVoiceRoboticService;
+    @Autowired
+    private SysDictDataMapper sysDictDataMapper;
 
     /**
      * 录入客户
@@ -85,6 +106,97 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
         });
     }
 
+    private static final String TRADE_TYPE = "trade_type";
+    @Value("${crm.customer.ai.key:mygpt-tbQfq4ejR162mGJBCTTDUH9ecP1XCVuUfaOGTipnLjb1hP8x5prg}")
+    private String appKey;
+    private List<CrmCustomerAiTagVo> getAiTags(String chatRecord) throws JsonProcessingException {
+        Map<String, Object> requestParam = new HashMap<>();
+        List<CrmCustomerPropertyTemplate> crmCustomerPropertyTemplates = SpringUtils.getBean(ICrmCustomerPropertyTemplateService.class).getBaseMapper().selectList(new LambdaQueryWrapper<CrmCustomerPropertyTemplate>());
+        List<SysDictData> sysDictData = sysDictDataMapper.selectDictDataByType(TRADE_TYPE);
+        ArrayList<Map<String, String>> tags = new ArrayList<>();//标签模板及提示词
+        crmCustomerPropertyTemplates.forEach(o -> {
+            Map<String, String> tag = MapUtil.convertToMap(new CrmCustomerAiAutoTagVo(String.valueOf(o.getId()), o.getName(), o.getAiHint()));
+            tags.add(tag);
+        });
+        StringBuilder history = new StringBuilder();
+        List<Map<String, Object>> communication;
+        ObjectMapper mapper = new ObjectMapper();
+        try {
+            communication =  mapper.readValue(chatRecord,
+                    new TypeReference<List<Map<String, Object>>>() {});
+        } catch (Exception e) {
+            throw new RuntimeException("数据格式错误", e);
+        }
+        //对话内容删掉第一条(提示词)
+        if (!communication.isEmpty())communication.remove(0);
+        history.append("{");
+        for (Map<String, Object> o :
+                communication) {
+            String role = (String) o.get("role");
+            String content = (String) o.get("content");
+            String roleTag = "user".equals(role) ? "user" : "ai";
+            history.append(String.format("\"%s\":\"%s\",", roleTag, content));
+        }
+        history.deleteCharAt(history.length() - 1).append("}");
+        HashMap<String, String> userInfo = new HashMap<String, String>();
+        HashMap<String, String> aiInfo = new HashMap<>();
+        aiInfo.put("name", "");
+        aiInfo.put("sex", "");
+        aiInfo.put("age", "");
+        aiInfo.put("city", "");
+        aiInfo.put("habits", "");
+        aiInfo.put("describe", "");
+        userInfo.put("name", "");
+        userInfo.put("sex", "");
+        userInfo.put("age", "");
+        userInfo.put("city", "");
+        userInfo.put("habits", "");
+        userInfo.put("describe", "");
+        requestParam.put("aiInfo", aiInfo);
+        requestParam.put("likeRatio", "");
+        requestParam.put("userInfo", userInfo);
+        requestParam.put("history", history);
+        requestParam.put("tags", tags);
+        requestParam.put("tradeName", sysDictData.stream().map(SysDictData::getDictLabel).collect(Collectors.toList()));
+        requestParam.put("tradeType", sysDictData.stream().map(SysDictData::getDictValue).collect(Collectors.toList()));
+        requestParam.put("tagInfos", Collections.emptyList());
+        requestParam.put("isRepository", "");
+        requestParam.put("userContent", "");
+        requestParam.put("aiContent", "");
+        requestParam.put("likeRatio", "");
+
+        String requestJson = mapper.writeValueAsString(requestParam);
+        ChatParam param = new ChatParam();
+        param.setChatId(UuidUtil.getTimeBasedUuid().toString());
+        param.setStream(false);
+        param.setDetail(true);
+        ChatParam.Message message = new ChatParam.Message();
+        List<ChatParam.Message> messageList = new ArrayList<ChatParam.Message>();
+        message.setContent(requestJson);
+        message.setRole("user");
+        messageList.add(message);
+        param.setMessages(messageList);
+        ChatService chatService = SpringUtils.getBean(ChatService.class);
+        AiHostProper aiHost = SpringUtils.getBean(AiHostProper.class);
+        R aiResponse = chatService.initiatingTakeChat(param, aiHost.getAiApi(), appKey);
+        if (aiResponse == null || !Integer.valueOf(200).equals(aiResponse.get("code"))) {
+            throw new RuntimeException("AI响应异常: " +
+                    (aiResponse != null ? aiResponse.get("msg") : "响应为空"));
+        }
+
+        List<Map<String, String>> tagInfos = CrmCustomerAiTagUtil.extractTagInfos(JSONUtil.toJsonStr(aiResponse));
+        if (CollectionUtils.isEmpty(tagInfos)) {
+            return Collections.emptyList();
+        }
+
+        List<CrmCustomerAiTagVo> collect = tagInfos.stream()
+                .map(tag -> {
+                    return new CrmCustomerAiTagVo()
+                            .setValue(tag.get("value")).setName(tag.get("name"));
+                })
+                .collect(Collectors.toList());
+        return collect;
+    }
     /**
      * 处理单条数据
      * @param data

+ 14 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyWxClient4WorkFlowVO.java

@@ -80,6 +80,20 @@ public class CompanyWxClient4WorkFlowVO extends BaseEntityTow {
     private String memo;
     private String workflowInstanceId;
     private String currentNodeKey;
+    /**
+    *  加微方式配置
+    */
+    private String nodeConfig;
     private String currentNodeName;
     private Integer currentNodeType;
+
+    /**
+    * 外呼id
+    */
+    private Long calleeId;
+
+    /**
+    * 投流 id
+    */
+    private String traceId;
 }

+ 16 - 10
fs-service/src/main/java/com/fs/course/dto/FsOrderDeliveryNoteDTO.java

@@ -13,36 +13,36 @@ public class FsOrderDeliveryNoteDTO {
     /**
      * 系统订单号
      * **/
-    @Excel(name = "系统订单号(必填)",width = 20,sort = 1)
+    @Excel(name = "系统订单号(必填)",width = 20,sort = 10)
     private String orderNumber;
 
-    @Excel(name = "物流公司编号(必填)(SF:顺丰、EMS:邮政、ZTO:中通、JD:京东、DBL:德邦、YTO:圆通)",width = 30,sort = 2)
+    @Excel(name = "物流公司编号(必填)(SF:顺丰、EMS:邮政、ZTO:中通、JD:京东、DBL:德邦、YTO:圆通)",width = 30,sort = 20)
     private String deliverySn;
 
-    @Excel(name = "物流公司",width = 10,sort = 10)
+    @Excel(name = "物流公司",width = 10,sort = 100)
     private String logisticsCompany;
 
     private String deliveryName;
 
-    @Excel(name = "快递单号(必填)",width = 20,sort = 3)
+    @Excel(name = "快递单号(必填)",width = 20,sort = 30)
     private String deliveryId;
 
-    @Excel(name = "物流状态(0:暂无信息、1:已揽收、2:在途中、3:签收、4:问题件)",width = 40,sort = 4)
+    @Excel(name = "物流状态(0:暂无信息、1:已揽收、2:在途中、3:签收、4:问题件)",width = 40,sort = 40)
     private Integer deliveryStatus;
 
-    @Excel(name = "物流结算费用",width = 20,sort = 5)
+    @Excel(name = "物流结算费用",width = 20,sort = 50)
     private BigDecimal deliveryPayMoney;
 
-    @Excel(name = "物流跟踪状态(311:快递柜或驿站签收、304:派件异常后最终签收、301:正常签收、211:已放入快递柜或驿站、202:派件中、201:到达派件城市、401:发货无信息、412:快递柜或驿站超时未取、407:退货未签收)",width = 40,sort = 6)
+    @Excel(name = "物流跟踪状态(311:快递柜或驿站签收、304:派件异常后最终签收、301:正常签收、211:已放入快递柜或驿站、202:派件中、201:到达派件城市、401:发货无信息、412:快递柜或驿站超时未取、407:退货未签收)",width = 40,sort = 60)
     private Integer deliveryType;
 
-    @Excel(name = "物流结算状态(1:已结算、2:冻结、3:解冻、4:退回运费、5.调账)",width = 20,sort = 7)
+    @Excel(name = "物流结算状态(1:已结算、2:冻结、3:解冻、4:退回运费、5.调账)",width = 20,sort = 70)
     private Integer deliveryPayStatus;
 
-    @Excel(name = "快递账单日期",width = 20,sort = 8)
+    @Excel(name = "快递账单日期",width = 20,sort = 80)
     private String deliveryTime;
 
-    @Excel(name = "快递结算日期",width = 20,sort = 9)
+    @Excel(name = "快递结算日期",width = 20,sort = 90)
     private String deliveryPayTime;
 
 //    /**
@@ -52,4 +52,10 @@ public class FsOrderDeliveryNoteDTO {
 //    private Integer deliveryNoteStatus;
 
     private Integer deliveryNoteStatus;
+
+    /**
+     * 虚拟手机号
+     */
+    @Excel(name = "虚拟手机号", width = 30, sort = 45)
+    private String virtualPhone;
 }

+ 6 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCompanyUserMapper.java

@@ -104,4 +104,10 @@ public interface FsUserCompanyUserMapper extends BaseMapper<FsUserCompanyUser>{
      */
     int batchDeleteByIds(@Param("userIds") List<Long> userIds);
 
+    /**
+     * 根据userId查询最开始的数据
+     * @param userId
+     * @return
+     */
+    FsUserCompanyUser selectFsUserCompanyUserByUserId(@Param("userId") Long userId);
 }

+ 70 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerAnalyze.java

@@ -0,0 +1,70 @@
+package com.fs.crm.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 客户聊天记录分析对象 crm_customer_analyze
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CrmCustomerAnalyze extends BaseEntity{
+
+    /** 主键 */
+    private Long id;
+
+    /** 客户id */
+    @Excel(name = "客户id")
+    private Long customerId;
+
+    /** 客户姓名 */
+    @Excel(name = "客户姓名")
+    private String customerName;
+
+    /** 客户画像 */
+    @Excel(name = "客户画像")
+    private String customerPortraitJson;
+
+    /** 沟通摘要 */
+    @Excel(name = "沟通摘要")
+    private String communicationAbstract;
+
+    /** 沟通总结 */
+    @Excel(name = "沟通总结")
+    private String communicationSummary;
+
+    /** 流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险 */
+    @Excel(name = "流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险")
+    private Long attritionLevel;
+
+    /** 流失风险等级提示 */
+    @Excel(name = "流失风险等级提示")
+    private String attritionLevelPrompt;
+
+    /** 客户关注点 */
+    @Excel(name = "客户关注点")
+    private String customerFocusJson;
+
+    /** 意向度 */
+    @Excel(name = "意向度")
+    private Long intentionDegree;
+
+    /** ai通话聊天记录 */
+    @Excel(name = "ai通话聊天记录")
+    private String aiChatRecord;
+
+    /** 预留数字型字段 */
+    @Excel(name = "预留数字型字段")
+    private Long reserveInt;
+
+    /** 预留字符串型字段 */
+    @Excel(name = "预留字符串型字段")
+    private String reserveStr;
+
+
+}

+ 258 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerInfo.java

@@ -0,0 +1,258 @@
+package com.fs.crm.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * 客户信息表实体类
+ * @author lk
+ * @since 2026-3-19
+ */
+@Data
+@TableName("crm_customer_info")
+@Accessors(chain = true)
+public class CrmCustomerInfo {
+
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * crm_customer_id
+     */
+    @TableField("customer_id")
+    private Long customerId;
+
+    /**
+     * 姓名
+     */
+    @TableField("name")
+    private String name;
+
+    /**
+     * 性别
+     */
+    @TableField("sex")
+    private String sex;
+
+    /**
+     * 年龄
+     */
+    @TableField("age")
+    private String age;
+
+    /**
+     * 地区
+     */
+    @TableField("address")
+    private String address;
+
+    /**
+     * 行为习惯
+     */
+    @TableField("habits")
+    private String habits;
+
+    /**
+     * 患病时间
+     */
+    @TableField("illness_time")
+    private String illnessTime;
+
+    /**
+     * 身体状态
+     */
+    @TableField("body")
+    private String body;
+
+    /**
+     * 学习到的章节
+     */
+    @TableField("study")
+    private String study;
+
+    /**
+     * 今日课程完成情况
+     */
+    @TableField("course_status")
+    private String courseStatus;
+
+    /**
+     * 学习课程
+     */
+    @TableField("course")
+    private String course;
+
+    /**
+     * 提及的家人
+     */
+    @TableField("family")
+    private String family;
+
+    /**
+     * 家人的疾病
+     */
+    @TableField("family_disease")
+    private String familyDisease;
+
+    /**
+     * 疾病
+     */
+    @TableField("disease")
+    private String disease;
+
+    /**
+     * 是否线下就诊
+     */
+    @TableField("is_line")
+    private String isLine;
+
+    /**
+     * 交流状态
+     */
+    @TableField("talk")
+    private String talk;
+
+    /**
+     * 用户分类
+     */
+    @TableField("user_type")
+    private String userType;
+
+    /**
+     * 是否本人会诊
+     */
+    @TableField("is_self")
+    private String isSelf;
+
+    /**
+     * 什么情况加重或缓解
+     */
+    @TableField("intensify")
+    private String intensify;
+
+    /**
+     * 是否怕热或者怕冷
+     */
+    @TableField("is_cold")
+    private String isCold;
+
+    /**
+     * 怕冷或怕热的部位
+     */
+    @TableField("cold_body")
+    private String coldBody;
+
+    /**
+     * 出汗情况
+     */
+    @TableField("sweat")
+    private String sweat;
+
+    /**
+     * 其他情况
+     */
+    @TableField("other")
+    private String other;
+
+    /**
+     * 大小便情况
+     */
+    @TableField("toilet")
+    private String toilet;
+
+    /**
+     * 饮食情况
+     */
+    @TableField("eat")
+    private String eat;
+
+    /**
+     * 经期如何 女 55岁以下
+     */
+    @TableField("menses")
+    private String menses;
+
+    /**
+     * 用药
+     */
+    @TableField("medicine")
+    private String medicine;
+
+    /**
+     * 体质
+     */
+    @TableField("constitution")
+    private String constitution;
+
+    /**
+     * 推荐用药
+     */
+    @TableField("recommend_medicine")
+    private String recommendMedicine;
+
+    /**
+     * 咨询产品
+     */
+    @TableField("consult_product")
+    private String consultProduct;
+
+    /**
+     * 是否已经购买产品
+     */
+    @TableField("is_buy")
+    private String isBuy;
+
+    /**
+     * 已经购买的产品
+     */
+    @TableField("buy_product")
+    private String buyProduct;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /**
+     * 最后回复时间
+     */
+    @TableField("reply_time")
+    private Date replyTime;
+
+    /**
+     * 产品交流
+     */
+    @TableField("product_talk")
+    private String productTalk;
+
+    /**
+     * 疾病交流
+     */
+    @TableField("disease_talk")
+    private String diseaseTalk;
+
+    /**
+     * 渠道类型
+     */
+    @TableField("channel_type")
+    private String channelType;
+
+    /**
+     * ai通话聊天记录
+     */
+    @TableField("ai_chat_record")
+    private String aiChatRecord;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerPropertyTemplate.java

@@ -22,4 +22,7 @@ public class CrmCustomerPropertyTemplate extends BaseEntityTow {
 
     @Excel(name = "行业类型")
     private String tradeType;
+
+    @Excel(name = "是否有意向度0否1是")
+    private Integer intention;
 }

+ 12 - 0
fs-service/src/main/java/com/fs/crm/dto/CrmCustomerAiAutoTagVo.java

@@ -0,0 +1,12 @@
+package com.fs.crm.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class CrmCustomerAiAutoTagVo {
+    private String id;
+    private String name;
+    private String aiHint;
+}

+ 61 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerAnalyzeMapper.java

@@ -0,0 +1,61 @@
+package com.fs.crm.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.crm.domain.CrmCustomerAnalyze;
+
+/**
+ * 客户聊天记录分析Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-24
+ */
+public interface CrmCustomerAnalyzeMapper extends BaseMapper<CrmCustomerAnalyze>{
+    /**
+     * 查询客户聊天记录分析
+     * 
+     * @param id 客户聊天记录分析主键
+     * @return 客户聊天记录分析
+     */
+    CrmCustomerAnalyze selectCrmCustomerAnalyzeById(Long id);
+
+    /**
+     * 查询客户聊天记录分析列表
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 客户聊天记录分析集合
+     */
+    List<CrmCustomerAnalyze> selectCrmCustomerAnalyzeList(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    /**
+     * 新增客户聊天记录分析
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 结果
+     */
+    int insertCrmCustomerAnalyze(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    /**
+     * 修改客户聊天记录分析
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 结果
+     */
+    int updateCrmCustomerAnalyze(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    /**
+     * 删除客户聊天记录分析
+     * 
+     * @param id 客户聊天记录分析主键
+     * @return 结果
+     */
+    int deleteCrmCustomerAnalyzeById(Long id);
+
+    /**
+     * 批量删除客户聊天记录分析
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCrmCustomerAnalyzeByIds(Long[] ids);
+}

+ 37 - 2
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -4,6 +4,7 @@ package com.fs.crm.mapper;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.domain.CrmCustomerInfo;
 import com.fs.crm.param.*;
 import com.fs.crm.vo.*;
 import com.fs.qwApi.param.QwCustomerDetailParam;
@@ -344,9 +345,24 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "</script>"})
     List<CrmMyCustomerListQueryVO> selectCrmMyCustomerListQuery(@Param("maps") CrmMyCustomerListQueryParam param);
     @Select({"<script> " +
-            "select c.*,u.nick_name as company_user_nick_name,ccu.start_time as startTime  from  crm_customer c left join company_user u on u.user_id=c.receive_user_id left join crm_customer_user ccu on c.customer_user_id = ccu.customer_user_id " +
+            "select c.*,u.nick_name as company_user_nick_name,ccu.start_time as startTime,ca.attrition_level,ca.intention_degree  from  crm_customer c " +
+            "left join company_user u on u.user_id=c.receive_user_id " +
+            "left join crm_customer_user ccu on c.customer_user_id = ccu.customer_user_id " +
+            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
+            "   ORDER BY create_time DESC " +
+            "   LIMIT 1 " +
+            ") ca ON TRUE " +
 //            "where is_line=0 " +
             "where 1=1 " +
+            "<if test = 'maps.attritionLevel != null and maps.attritionLevel != \"\" '> " +
+            "and ca.attrition_level = #{maps.attritionLevel} " +
+            "</if>" +
+            "<if test = 'maps.intentionDegreeGt != null'> " +
+            "and ca.intention_degree &gt;= #{maps.intentionDegreeGt} " +
+            "</if>" +
+            "<if test = 'maps.intentionDegreelt != null'> " +
+            "and ca.intention_degree &lt;= #{maps.intentionDegreelt} " +
+            "</if>" +
             "<if test = 'maps.companyId != null     '> " +
             "and c.company_id =#{maps.companyId} " +
             "</if>" +
@@ -426,7 +442,12 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "</script>"})
     List<CrmCustomerListQueryVO> selectCrmCustomerListQuery(@Param("maps") CrmCustomerListQueryParam param);
     @Select({"<script> " +
-            "select c.*,u.nick_name as company_user_nick_name  from  crm_customer c left join company_user u on u.user_id=c.receive_user_id   " +
+            "select c.*,u.nick_name as company_user_nick_name,ca.attrition_level,ca.intention_degree  from  crm_customer c " +
+            "left join company_user u on u.user_id=c.receive_user_id " +
+            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
+            "   ORDER BY create_time DESC " +
+            "   LIMIT 1 " +
+            ") ca ON TRUE " +
             "where is_line=1 " +
 //            " select  c.*  " +
 ////            "<if test = 'maps.isDuplicate != null '> " +
@@ -479,6 +500,15 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "<if test = 'maps.endTime != null and maps.endTime != \"\" '> " +
             "and date_format(c.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d') " +
             "</if>" +
+            "<if test = 'maps.attritionLevel != null and maps.attritionLevel != \"\" '> " +
+            "and ca.attrition_level = #{maps.attritionLevel} " +
+            "</if>" +
+            "<if test = 'maps.intentionDegreeGt != null'> " +
+            "and ca.intention_degree &gt;= #{maps.intentionDegreeGt} " +
+            "</if>" +
+            "<if test = 'maps.intentionDegreelt != null'> " +
+            "and ca.intention_degree &lt;= #{maps.intentionDegreelt} " +
+            "</if>" +
             " order by c.customer_id desc "+
             "</script>"})
     List<CrmLineCustomerListQueryVO> selectCrmLineCustomerListQuery(@Param("maps") CrmLineCustomerListQueryParam param);
@@ -966,4 +996,9 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
      */
     List<Long> selectCustomerIdByCompanyUserId(@Param("companyUserId") Long companyUserId);
 
+    CrmCustomerInfo selectCrmCustomerInfoById(@Param("customerId") Long customerId);
+
+    void insertCrmCustomerInfo(CrmCustomerInfo crmCustomerInfo);
+
+    int updateCrmCustomerInfo(CrmCustomerInfo crmCustomerInfo);
 }

+ 25 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerAiTagParam.java

@@ -0,0 +1,25 @@
+package com.fs.crm.param;
+
+import com.baidu.dev2.thirdparty.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * @Description: 客户ai标签参数
+ */
+@Data
+@Accessors(chain = true)
+public class CrmCustomerAiTagParam {
+
+    //company_voice_robotic_call_log_callphone 的log_id
+    @ApiModelProperty("日志id")
+    private Long logId;
+    @ApiModelProperty("客户id")
+    private Long customerId;
+    //crm_customer_property_template的 trade_type
+    @ApiModelProperty("行业")
+    private String tradeType;
+    @ApiModelProperty("对话内容")
+    private String json;
+}

+ 7 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java

@@ -103,5 +103,12 @@ public class CrmCustomerListQueryParam extends BaseQueryParam
     /** 结束时间 */
     private String endTime;
 
+    /** 流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险 */
+    private Long attritionLevel;
+    /** 意向度>= */
+    private Long intentionDegreeGt;
+    /** 意向度<= */
+    private Long intentionDegreelt;
+
 
 }

+ 7 - 0
fs-service/src/main/java/com/fs/crm/param/CrmLineCustomerListQueryParam.java

@@ -85,4 +85,11 @@ public class CrmLineCustomerListQueryParam extends BaseQueryParam
     @Excel(name = "标签" )
     private String tags;
 
+    /** 流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险 */
+    private Long attritionLevel;
+    /** 意向度>= */
+    private Long intentionDegreeGt;
+    /** 意向度<= */
+    private Long intentionDegreelt;
+
 }

+ 61 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerAnalyzeService.java

@@ -0,0 +1,61 @@
+package com.fs.crm.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.crm.domain.CrmCustomerAnalyze;
+
+/**
+ * 客户聊天记录分析Service接口
+ * 
+ * @author fs
+ * @date 2026-03-24
+ */
+public interface ICrmCustomerAnalyzeService extends IService<CrmCustomerAnalyze>{
+    /**
+     * 查询客户聊天记录分析
+     * 
+     * @param id 客户聊天记录分析主键
+     * @return 客户聊天记录分析
+     */
+    CrmCustomerAnalyze selectCrmCustomerAnalyzeById(Long id);
+
+    /**
+     * 查询客户聊天记录分析列表
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 客户聊天记录分析集合
+     */
+    List<CrmCustomerAnalyze> selectCrmCustomerAnalyzeList(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    /**
+     * 新增客户聊天记录分析
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 结果
+     */
+    int insertCrmCustomerAnalyze(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    /**
+     * 修改客户聊天记录分析
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 结果
+     */
+    int updateCrmCustomerAnalyze(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    /**
+     * 批量删除客户聊天记录分析
+     * 
+     * @param ids 需要删除的客户聊天记录分析主键集合
+     * @return 结果
+     */
+    int deleteCrmCustomerAnalyzeByIds(Long[] ids);
+
+    /**
+     * 删除客户聊天记录分析信息
+     * 
+     * @param id 客户聊天记录分析主键
+     * @return 结果
+     */
+    int deleteCrmCustomerAnalyzeById(Long id);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java

@@ -1,6 +1,7 @@
 package com.fs.crm.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.crm.domain.CrmCustomerProperty;
 
 import java.util.List;
@@ -139,4 +140,6 @@ public interface ICrmCustomerPropertyService extends IService<CrmCustomerPropert
      * @return 添加结果(添加的记录数)
      */
     int batchAddPropertiesByTemplateIds(Long customerId, Map<Long, String> propertyMap, String createBy);
+
+    void addPropertyByCallLog(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog);
 }

+ 92 - 0
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java

@@ -0,0 +1,92 @@
+package com.fs.crm.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import com.fs.crm.mapper.CrmCustomerAnalyzeMapper;
+import com.fs.crm.domain.CrmCustomerAnalyze;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
+
+/**
+ * 客户聊天记录分析Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-24
+ */
+@Service
+public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyzeMapper, CrmCustomerAnalyze> implements ICrmCustomerAnalyzeService {
+
+    /**
+     * 查询客户聊天记录分析
+     * 
+     * @param id 客户聊天记录分析主键
+     * @return 客户聊天记录分析
+     */
+    @Override
+    public CrmCustomerAnalyze selectCrmCustomerAnalyzeById(Long id)
+    {
+        return baseMapper.selectCrmCustomerAnalyzeById(id);
+    }
+
+    /**
+     * 查询客户聊天记录分析列表
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 客户聊天记录分析
+     */
+    @Override
+    public List<CrmCustomerAnalyze> selectCrmCustomerAnalyzeList(CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        return baseMapper.selectCrmCustomerAnalyzeList(crmCustomerAnalyze);
+    }
+
+    /**
+     * 新增客户聊天记录分析
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 结果
+     */
+    @Override
+    public int insertCrmCustomerAnalyze(CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        crmCustomerAnalyze.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCrmCustomerAnalyze(crmCustomerAnalyze);
+    }
+
+    /**
+     * 修改客户聊天记录分析
+     * 
+     * @param crmCustomerAnalyze 客户聊天记录分析
+     * @return 结果
+     */
+    @Override
+    public int updateCrmCustomerAnalyze(CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        return baseMapper.updateCrmCustomerAnalyze(crmCustomerAnalyze);
+    }
+
+    /**
+     * 批量删除客户聊天记录分析
+     * 
+     * @param ids 需要删除的客户聊天记录分析主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCrmCustomerAnalyzeByIds(Long[] ids)
+    {
+        return baseMapper.deleteCrmCustomerAnalyzeByIds(ids);
+    }
+
+    /**
+     * 删除客户聊天记录分析信息
+     * 
+     * @param id 客户聊天记录分析主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCrmCustomerAnalyzeById(Long id)
+    {
+        return baseMapper.deleteCrmCustomerAnalyzeById(id);
+    }
+}

+ 56 - 3
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java

@@ -1,24 +1,40 @@
 package com.fs.crm.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PubFun;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.crm.domain.CrmCustomerProperty;
 import com.fs.crm.domain.CrmCustomerPropertyTemplate;
 import com.fs.crm.mapper.CrmCustomerPropertyMapper;
+import com.fs.crm.param.CrmCustomerAiTagParam;
 import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import com.fs.crm.utils.CrmCustomerAiTagUtil;
+import com.fs.crm.vo.CrmCustomerAiTagVo;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
+@Slf4j
 @Service
 public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPropertyMapper, CrmCustomerProperty> implements ICrmCustomerPropertyService {
 
     @Autowired
     private ICrmCustomerPropertyTemplateService propertyTemplateService;
+    @Autowired
+    private ICompanyVoiceRoboticCalleesService companyVoiceRoboticCalleesService;
 
     @Override
     public CrmCustomerProperty selectCrmCustomerPropertyById(Long id) {
@@ -89,7 +105,7 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
 
     public int addOrUpdateCustomerPropertyWithExtra(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String intention, Integer likeRatio, String createBy) {
         String autoIntention = calculateIntentionByLikeRatio(likeRatio, intention);
-        
+
         CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, propertyId);
         if (existProperty != null) {
             existProperty.setPropertyValue(propertyValue);
@@ -116,12 +132,12 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
             return baseMapper.insertCrmCustomerProperty(property);
         }
     }
-    
+
     private String calculateIntentionByLikeRatio(Integer likeRatio, String intention) {
         if (likeRatio == null) {
             return intention;
         }
-        
+
         if (likeRatio >= 80) {
             return "high";
         } else if (likeRatio >= 50) {
@@ -188,4 +204,41 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
         }
         return count;
     }
+
+    @Override
+    @Async
+    public void addPropertyByCallLog(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        CompanyVoiceRoboticCallees companyVoiceRoboticCallees = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesById(companyVoiceRoboticCallLogCallphone.getCallerId());
+        log.info("获取电话记录:{}", companyVoiceRoboticCallees);
+        CrmCustomerAiTagParam param = new CrmCustomerAiTagParam();
+        param.setLogId(companyVoiceRoboticCallLogCallphone.getLogId());
+        param.setCustomerId(companyVoiceRoboticCallees.getUserId());
+        param.setTradeType("3");
+        param.setJson(companyVoiceRoboticCallLogCallphone.getContentList());
+        log.info("传输数据:{}", param);
+        try {
+            List<CrmCustomerAiTagVo> tag = CrmCustomerAiTagUtil.getCrmCustomerAiTag(param);
+            log.info("解析数据:{}", tag);
+            List<Long> ids = PubFun.listToNewList(tag, e -> Long.parseLong(e.getId()));
+            List<CrmCustomerProperty> propertyList = tag.stream().map(e -> {
+                CrmCustomerProperty property = new CrmCustomerProperty();
+                property.setCustomerId(e.getCustomerId());
+                property.setPropertyId(Long.parseLong(e.getId()));
+                property.setPropertyName(e.getName());
+                property.setPropertyValue(e.getValue());
+                property.setPropertyValueType("String");
+                property.setTradeType("3");
+                property.setIntention("medium");
+                property.setLikeRatio(50);
+                return property;
+            }).collect(Collectors.toList());
+            remove(new LambdaQueryWrapper<CrmCustomerProperty>()
+                    .eq(CrmCustomerProperty::getCustomerId, companyVoiceRoboticCallees.getUserId())
+                    .in(CrmCustomerProperty::getPropertyId, ids)
+            );
+            saveBatch(propertyList);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }

+ 424 - 0
fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java

@@ -0,0 +1,424 @@
+package com.fs.crm.utils;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DictUtils;
+import com.fs.common.utils.date.DateUtil;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.config.ai.AiHostProper;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.domain.CrmCustomerAnalyze;
+import com.fs.crm.domain.CrmCustomerInfo;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
+import com.fs.crm.mapper.CrmCustomerAnalyzeMapper;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.crm.param.CrmCustomerAiTagParam;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import com.fs.crm.vo.CrmCustomerAiTagVo;
+import com.fs.fastgptApi.param.ChatParam;
+import com.fs.fastgptApi.service.ChatService;
+import com.fs.hisapi.util.MapUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Component
+public class CrmCustomerAiTagUtil {
+
+    //行业字典名称
+    private static final String TRADE_TYPE = "trade_type";
+    @Value("${crm.customer.ai.key:mygpt-tbQfq4ejR162mGJBCTTDUH9ecP1XCVuUfaOGTipnLjb1hP8x5prg}")
+    private String appKey;
+    private static final String CRM_AI_REDIS_KEY = "crm:AI:data:processing";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private static String APP_KEY;
+
+    @PostConstruct
+    public void initStatic() {
+        APP_KEY = this.appKey;
+    }
+
+    public static List<CrmCustomerAiTagVo> getCrmCustomerAiTag(CrmCustomerAiTagParam content) throws JsonProcessingException {
+        // 1. 参数校验
+        validateParams(content);
+        Long customerId = content.getCustomerId();
+        Long logId = content.getLogId();
+        String tradeType = content.getTradeType();
+        List<Map<String, Object>> communication = parseCommunicationJson(content.getJson());
+
+        // 2. 构建请求参数
+        Map<String, Object> requestParam = buildRequestParam(customerId, tradeType, communication);
+
+        // 3. 调用AI服务
+        R aiResponse = callAiService(requestParam, logId);
+
+        // 4. 解析响应并保存
+        List<CrmCustomerAiTagVo> results = parseAiResponse(aiResponse, customerId);
+
+        // 5. 异步保存到Redis,后续调用ai分析其他数据
+        saveToRedisAsync(customerId, aiResponse);
+        return results;
+    }
+    private static void saveToRedisAsync(Long customerId, R aiResponse) {
+        // 使用线程池异步保存,避免影响主流程
+        CompletableFuture.runAsync(() -> {
+            try {
+                Map<String, Object> dataMap = new HashMap<>();
+                dataMap.put("customerId", customerId);
+                dataMap.put("data", aiResponse);
+                dataMap.put("timestamp", System.currentTimeMillis());
+
+                RedisTemplate<String, Object> redisTemplate = SpringUtils.getBean(RedisTemplate.class);
+
+                // 存储队列索引
+                redisTemplate.opsForList().rightPush(CRM_AI_REDIS_KEY, dataMap);
+
+            } catch (Exception e) {
+            }
+        });
+    }
+    private static CrmCustomerAiTagVo buildTagVo(Map<String, String> tag, Long customerId) {
+        CrmCustomerAiTagVo vo = new CrmCustomerAiTagVo();
+        vo.setCustomerId(customerId);
+        vo.setId(tag.get("id"));
+        vo.setName(tag.get("name"));
+        vo.setValue(tag.get("value"));
+        return vo;
+    }
+    private static List<CrmCustomerAiTagVo> parseAiResponse(R aiResponse, Long customerId) {
+        if (aiResponse == null || !Integer.valueOf(200).equals(aiResponse.get("code"))) {
+            throw new RuntimeException("AI响应异常: " +
+                    (aiResponse != null ? aiResponse.get("msg") : "响应为空"));
+        }
+
+        List<Map<String, String>> tagInfos = extractTagInfos(JSONUtil.toJsonStr(aiResponse));
+        if (CollectionUtils.isEmpty(tagInfos)) {
+            return Collections.emptyList();
+        }
+
+        return tagInfos.stream()
+                .map(tag -> buildTagVo(tag, customerId))
+                .collect(Collectors.toList());
+    }
+    private static R callAiService(Map<String, Object> requestParam, Long logId) {
+        try {
+            String requestJson = mapper.writeValueAsString(requestParam);
+
+            ChatParam param = new ChatParam();
+            param.setChatId(logId.toString());
+            param.setStream(false);
+            param.setDetail(true);
+            ChatParam.Message message = new ChatParam.Message();
+            List<ChatParam.Message> messageList = new ArrayList<ChatParam.Message>();
+            message.setContent(requestJson);
+            message.setRole("user");
+            messageList.add(message);
+            param.setMessages(messageList);
+            ChatService chatService = SpringUtils.getBean(ChatService.class);
+            AiHostProper aiHost = SpringUtils.getBean(AiHostProper.class);
+
+            return chatService.initiatingTakeChat(param, aiHost.getAiApi(), APP_KEY);
+        } catch (Exception e) {
+            throw new RuntimeException("AI服务调用失败", e);
+        }
+    }
+    private static Map<String, Object> buildRequestParam(Long customerId,
+                                                         String tradeType,
+                                                         List<Map<String, Object>> communication) {
+        Map<String, Object> requestParam = new HashMap<>();
+
+        // 获取各类数据
+        Map<String, Object> remove = communication.remove(0);
+        String tradeName = getDictLabel(tradeType);
+        Map<String, Object> tags = getTags(tradeType);
+        Map<String, Object> history = getHistory(communication,customerId.toString());
+        Map<String, Object> userInfo = getUserInfo(customerId);
+        Map<String, Object> aiInfo = getAiInfo(remove);
+        String likeRatio ="";
+        if (!userInfo.isEmpty()){
+            likeRatio = (String) userInfo.remove("likeRatio");
+        }
+        // 合并数据
+        Stream.of(tags, history, userInfo, aiInfo)
+                .filter(Objects::nonNull)
+                .forEach(requestParam::putAll);
+
+        // 设置其他参数
+        requestParam.put("tradeName", tradeName);
+        requestParam.put("tradeType", tradeType);
+        requestParam.put("tagInfos", Collections.emptyList());
+        requestParam.put("isRepository", "");
+        requestParam.put("userContent", "");
+        requestParam.put("aiContent", "");
+        requestParam.put("likeRatio", likeRatio);
+
+        return requestParam;
+    }
+    private static List<Map<String, Object>> parseCommunicationJson(String jsonStr) {
+        try {
+            return mapper.readValue(jsonStr,
+                    new TypeReference<List<Map<String, Object>>>() {});
+        } catch (Exception e) {
+            throw new RuntimeException("数据格式错误", e);
+        }
+    }
+    private static void validateParams(CrmCustomerAiTagParam content) {
+        if (ObjectUtil.isEmpty(content.getTradeType())
+                || ObjectUtil.isEmpty(content.getCustomerId())
+                || ObjectUtil.isEmpty(content.getLogId())
+                || ObjectUtil.isEmpty(content.getJson())) {
+            throw new IllegalArgumentException("参数不能为空");
+        }
+    }
+    /**
+     * 提取 tagInfos 数据
+     */
+    public static List<Map<String, String>> extractTagInfos(String jsonStr) {
+        try {
+            JsonNode root = mapper.readTree(jsonStr);
+
+            // 获取 responseData 数组
+            JsonNode responseData = root.path("data").path("responseData");
+
+            // 查找 AI 对话节点
+            for (JsonNode node : responseData) {
+                String moduleName = node.path("moduleName").asText();
+                if ("AI 对话".equals(moduleName)) {
+                    // 获取 historyPreview 数组
+                    JsonNode historyPreview = node.path("historyPreview");
+
+                    // 查找 AI 节点的 historyPreview
+                    for (JsonNode preview : historyPreview) {
+                        String objType = preview.path("obj").asText();
+                        if ("AI".equals(objType)) {
+                            JsonNode valueNode = preview.path("value");
+
+                            // 如果 value 是字符串,需要再次解析
+                            if (valueNode.isTextual()) {
+                                String valueStr = valueNode.asText();
+                                JsonNode tagInfosNode = mapper.readTree(valueStr).path("tagInfos");
+
+                                if (tagInfosNode.isArray()) {
+                                    return mapper.convertValue(tagInfosNode,
+                                            new TypeReference<List<Map<String, String>>>() {
+                                            });
+                                }
+                            } else if (valueNode.isObject()) {
+                                JsonNode tagInfosNode = valueNode.path("tagInfos");
+                                if (tagInfosNode.isArray()) {
+                                    return mapper.convertValue(tagInfosNode,
+                                            new TypeReference<List<Map<String, String>>>() {
+                                            });
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return new ArrayList<>();
+    }
+
+    /**
+     * 获取完整的 AI 响应数据
+     */
+    public static Map<String, Object> getAIResponseData(String jsonStr) {
+        try {
+            JsonNode root = mapper.readTree(jsonStr);
+            JsonNode responseData = root.path("data").path("responseData");
+
+            for (JsonNode node : responseData) {
+                if ("AI 对话".equals(node.path("moduleName").asText())) {
+                    JsonNode historyPreview = node.path("historyPreview");
+
+                    for (JsonNode preview : historyPreview) {
+                        if ("AI".equals(preview.path("obj").asText())) {
+                            JsonNode valueNode = preview.path("value");
+
+                            if (valueNode.isTextual()) {
+                                return mapper.readValue(valueNode.asText(),
+                                        new TypeReference<Map<String, Object>>() {
+                                        });
+                            } else if (valueNode.isObject()) {
+                                return mapper.convertValue(valueNode,
+                                        new TypeReference<Map<String, Object>>() {
+                                        });
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return new HashMap<>();
+    }
+
+    /**
+     * 提取所有标签(包括 tagInfos 和 tags 定义)
+     */
+    public static Map<String, Object> getAllTagInfo(String jsonStr) {
+        Map<String, Object> result = new HashMap<>();
+
+        try {
+            // 获取 AI 响应数据
+            Map<String, Object> aiResponse = getAIResponseData(jsonStr);
+
+            // tagInfos - 实际提取的标签值
+            List<Map<String, String>> tagInfos = (List<Map<String, String>>) aiResponse.get("tagInfos");
+            result.put("tagInfos", tagInfos != null ? tagInfos : new ArrayList<>());
+
+            // tags - 标签定义(从原始 query 中获取)
+            JsonNode root = mapper.readTree(jsonStr);
+            JsonNode responseData = root.path("data").path("responseData");
+
+            for (JsonNode node : responseData) {
+                if ("AI 对话".equals(node.path("moduleName").asText())) {
+                    JsonNode query = node.path("query");
+                    JsonNode tagsNode = query.path("tags");
+
+                    if (tagsNode.isArray()) {
+                        List<Map<String, String>> tags = mapper.convertValue(tagsNode,
+                                new TypeReference<List<Map<String, String>>>() {
+                                });
+                        result.put("tags", tags);
+                    }
+                    break;
+                }
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return result;
+    }
+
+    private static Map<String, Object> getAiInfo(Map<String, Object> remove) {
+        HashMap<String, String> aiInfo = new HashMap<>();
+        aiInfo.put("name", "");
+        aiInfo.put("sex", "");
+        aiInfo.put("age", "");
+        aiInfo.put("city", "");
+        aiInfo.put("habits", "");
+        aiInfo.put("describe", "");
+        HashMap<String, Object> result = new HashMap<>();
+        result.put("aiInfo", aiInfo);
+        return result;
+    }
+
+    private static Map<String, Object> getUserInfo(Long customerId) {
+        CrmCustomerMapper customerMapper = SpringUtils.getBean(CrmCustomerMapper.class);
+        CrmCustomer crmCustomer = customerMapper.selectCrmCustomerById(customerId);
+        if (ObjectUtil.isEmpty(crmCustomer)) throw new RuntimeException("客户不存在");
+        CrmCustomerInfo crmCustomerInfo = customerMapper.selectCrmCustomerInfoById(customerId);
+        if (ObjectUtil.isEmpty(crmCustomerInfo)) {
+            crmCustomerInfo = new CrmCustomerInfo();
+            crmCustomerInfo.setCustomerId(crmCustomer.getCustomerId()).setName(crmCustomer.getCustomerName()).setSex(crmCustomer.getSex().toString())
+                    .setTalk("首次交流");
+            customerMapper.insertCrmCustomerInfo(crmCustomerInfo);
+        }
+        HashMap<String, String> userInfo = new HashMap<String, String>();
+        userInfo.put("name", crmCustomerInfo.getName());
+        userInfo.put("sex", crmCustomerInfo.getSex());
+        userInfo.put("age", "");
+        userInfo.put("city", "");
+        userInfo.put("habits", "");
+        userInfo.put("describe", "");
+        HashMap<String, Object> result = new HashMap<>();
+        result.put("userInfo", userInfo);
+        result.put("likeRatio", ObjectUtil.isNotEmpty(crmCustomer.getIntention()) ? crmCustomer.getIntention() : "");
+        return result;
+    }
+
+    private static Map<String, Object> getHistory(List<Map<String, Object>> communication,String customerId) {
+        StringBuilder history = new StringBuilder();
+        history.append("{");
+        for (Map<String, Object> o :
+                communication) {
+            String role = (String) o.get("role");
+            String content = (String) o.get("content");
+            String roleTag = "user".equals(role) ? "user" : "ai";
+            history.append(String.format("\"%s\":\"%s\",", roleTag, content));
+        }
+        history.deleteCharAt(history.length() - 1).append("}");
+        Map<String, Object> result = new HashMap<String, Object>();
+        result.put("history", history);
+        ArrayList<Map> maps = new ArrayList<>();
+        //存入crm_customer_info
+        HashMap<String, Object> time = new HashMap<>();
+        maps.add(time);
+        communication.forEach(o->{
+            String role = (String) o.get("role");
+            String content = (String) o.get("content");
+            if (content != null && !content.trim().isEmpty()) { // 过滤空内容
+                String roleTag = "user".equals(role) ? "user" : "ai";
+                Map<String, String> message = new HashMap<>();
+                message.put(roleTag, content);
+                maps.add(message);
+            }
+        });
+        if (!maps.isEmpty()){
+            CrmCustomerAnalyze crmCustomerInfo = new CrmCustomerAnalyze();
+            crmCustomerInfo.setCustomerId(Long.valueOf(customerId));
+            crmCustomerInfo.setAiChatRecord(JSONUtil.toJsonStr(maps));
+            SpringUtils.getBean(CrmCustomerAnalyzeMapper.class).insertCrmCustomerAnalyze(crmCustomerInfo);
+        }
+
+        return result;
+    }
+
+    private static Map<String, Object> getTags(String tradeType) {
+        List<CrmCustomerPropertyTemplate> templates = SpringUtils.getBean(ICrmCustomerPropertyTemplateService.class).getBaseMapper().selectList(new LambdaQueryWrapper<CrmCustomerPropertyTemplate>().eq(
+                CrmCustomerPropertyTemplate::getTradeType, tradeType
+        ));
+        if (ObjectUtil.isEmpty(templates)) throw new RuntimeException("该行业无标签模板");
+        ArrayList<Map<String, String>> tags = new ArrayList<>();//标签及提示词
+        templates.forEach(o -> {
+            Map<String, String> tag = MapUtil.convertToMap(new CrmCustomerAiAutoTagVo(String.valueOf(o.getId()), o.getName(), o.getAiHint()));
+            tags.add(tag);
+        });
+        HashMap<String, Object> resultMap = new HashMap<>();
+        resultMap.put("tags", tags);
+        return resultMap;
+    }
+
+    private static String getDictLabel(String tradeType) {
+        List<SysDictData> tradeTypeDict = DictUtils.getDictCache(TRADE_TYPE);
+        String dictLabel;
+        if (ObjectUtil.isEmpty(tradeTypeDict)) {
+            dictLabel = DictUtils.getDictLabel(TRADE_TYPE, tradeType);
+        } else {
+            Map<String, String> collect = tradeTypeDict.stream().collect(Collectors.toMap(SysDictData::getDictValue,
+                    SysDictData::getDictLabel, (v1, v2) -> v1)
+            );
+            dictLabel = collect.get(tradeType);
+        }
+        if (ObjectUtil.isEmpty(dictLabel)) {
+            throw new RuntimeException("字典中不存在该行业");
+        } else return dictLabel;
+    }
+}

+ 16 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmCustomerAiTagVo.java

@@ -0,0 +1,16 @@
+package com.fs.crm.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 调用ai获取标签的响应类
+ */
+@Data
+@Accessors(chain = true)
+public class CrmCustomerAiTagVo {
+    private String id; // 标签id
+    private String name; // 标签名称
+    private String value; // 标签值
+    private Long customerId; // 客户id
+}

+ 7 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java

@@ -113,4 +113,11 @@ public class CrmCustomerListQueryVO implements Serializable
     /** 认领开始时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date startTime;
+
+    /** 流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险 */
+    @Excel(name = "流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险")
+    private Long attritionLevel;
+    /** 意向度 */
+    @Excel(name = "意向度")
+    private Long intentionDegree;
 }

+ 7 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java

@@ -106,5 +106,12 @@ public class CrmLineCustomerListQueryVO implements Serializable
     /** 非重客户Id(重客户最早录入手机号码的客户id) */
     private Long dCustomerId;
 
+    /** 流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险 */
+    @Excel(name = "流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险")
+    private Long attritionLevel;
+    /** 意向度 */
+    @Excel(name = "意向度")
+    private Long intentionDegree;
+
 
 }

+ 2 - 1
fs-service/src/main/java/com/fs/erp/domain/WeizouApiPushOrderParam.java

@@ -82,7 +82,8 @@ public class WeizouApiPushOrderParam {
     /**
      * 制单人 (正式环境待提供)
      */
-    private String createName = "wz_5dd9126f39b84ae096a0e18ea313559a";
+
+    private String createName;
 
     /**
      * 收货人所在省

+ 7 - 1
fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java

@@ -86,9 +86,15 @@ public interface FsIntegralGoodsMapper
     @Select({"<script> " +
             "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.cash,g.sort  from fs_integral_goods g  " +
             "where g.status=1  " +
-            "<if test = 'maps.goodsType != null and maps.goodsType != 0     '> " +
+            "<if test = 'maps.goodsType != null and maps.goodsType != 0 '> " +
             "and g.goods_type = #{maps.goodsType}  " +
             "</if>" +
+            "<if test = 'maps.keyword != null and maps.keyword != \" \" '> " +
+            "and g.goods_name like concat('%', #{maps.keyword}, '%') " +
+            "</if>" +
+            "<if test = 'maps.appId != null and maps.appId != \" \" '> " +
+            " and ((FIND_IN_SET(#{maps.appId}, g.app_ids) > 0)) " +
+            "</if>"+
             " order by g.goods_id desc "+
             "</script>"})
     List<FsIntegralGoodsListUVO> selectFsIntegralGoodsListUVO(@Param("maps")FsIntegralGoodsListUParam param);

+ 10 - 0
fs-service/src/main/java/com/fs/his/mapper/MerchantAppConfigMapper.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.his.domain.MerchantAppConfig;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 商户应用配置Mapper接口
@@ -74,4 +75,13 @@ public interface MerchantAppConfigMapper extends BaseMapper<MerchantAppConfig>{
      * @return 结果
      */
     int deleteMerchantAppConfigByIds(Long[] ids);
+
+    /**
+     * 根据appId和支付类型查询商户信息
+     *
+     * @param appId
+     * @param payType
+     * @return
+     */
+    MerchantAppConfig selectMerchantAppConfigByAppId(@Param("appId") String appId, @Param("payType") String payType);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java

@@ -28,4 +28,6 @@ public class FsIntegralGoodsListUParam  implements Serializable {
      * 商品状态(1:上架 2:下架)
      */
     private Integer status;
+
+    private String appId;
 }

+ 1 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -1602,7 +1602,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         storePayment.setStoreId(payOrderParam.getStoreId());
         storePayment.setUserId(user.getUserId());
         storePayment.setBusinessId(payOrderParam.getOrderId().toString());
-
+        storePayment.setAppId(payOrderParam.getAppId());
         // 设置openId(如果是微信支付)
         if (isWechatPayment(payOrderParam.getPaymentMethod())) {
             storePayment.setOpenId(getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig));

+ 15 - 0
fs-service/src/main/java/com/fs/his/vo/CompanyUserBindUserVO.java

@@ -0,0 +1,15 @@
+package com.fs.his.vo;
+
+import lombok.Data;
+
+@Data
+public class CompanyUserBindUserVO {
+
+    private Long userId;
+
+    private String phone;
+
+    private  String nickName;
+
+    private  Integer status;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java

@@ -386,4 +386,7 @@ public class FsStoreOrderScrm extends BaseEntity
     private Integer projectId;
     // 营期ID
     private Integer periodId;
+
+    //虚拟手机号
+    private String virtualPhone;
 }

+ 5 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsIntegralGoodsScrmMapper.java

@@ -75,11 +75,14 @@ public interface FsIntegralGoodsScrmMapper
             "</script>"})
     List<FsIntegralGoodsListVO> selectFsIntegralGoodsListVO(FsIntegralGoodsScrm fsIntegralGoods);
     @Select({"<script> " +
-            "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.sort  from fs_integral_goods g  " +
+            "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.cash,g.sort  from fs_integral_goods g  " +
             "where g.status=1  " +
-            "<if test = 'maps.goodsType != null and maps.goodsType != 0     '> " +
+            "<if test = 'maps.goodsType != null and maps.goodsType != 0 '> " +
             "and g.goods_type = #{maps.goodsType}  " +
             "</if>" +
+            "<if test = 'maps.keyword != null and maps.keyword != \" \"  '> " +
+            "and g.goods_name like concat('%', #{maps.keyword}, '%') " +
+            "</if>" +
             "<if test = 'maps.appId != null and maps.appId != \" \" '> " +
             " and ((FIND_IN_SET(#{maps.appId}, g.app_ids) > 0)) " +
             "</if>"+

+ 7 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueScrmMapper.java

@@ -83,9 +83,10 @@ public interface FsStoreCouponIssueScrmMapper
             "update fs_store_coupon_issue_scrm  set remain_count=(select ifnull(count(1),0) from fs_store_coupon_issue_user_scrm where issue_id=#{id}) where id=#{id}  " +
             "</script>"})
     void updateFsStoreCouponIssueCount(Long id);
+
     @Select({"<script> " +
             "select i.*,c.coupon_price,c.use_min_price,c.coupon_time   from fs_store_coupon_issue_scrm i left join fs_store_coupon_scrm c on c.coupon_id=i.coupon_id  " +
-            "where i.limit_time &gt; now() and i.is_del=0 and i.status=1   " +
+            "where i.limit_time &gt; now() and i.is_del=0 and i.status=1  and (i.is_permanent = 1 or (i.is_permanent = 0 and i.remain_count &lt; i.total_count)) and c.coupon_id is not null " +
             "<if test = 'maps.cateId != null and maps.cateId!=0     '> " +
             "and find_in_set(#{maps.cateId},c.package_cate_ids) " +
             "</if>" +
@@ -95,9 +96,13 @@ public interface FsStoreCouponIssueScrmMapper
             "<if test = 'maps.couponPrice != null     '> " +
             "and c.coupon_price =  #{maps.couponPrice} " +
             "</if>" +
-            " order by c.coupon_price desc "+
+            "<if test='maps.couponName != null and maps.couponName != \"\"'> " +
+            "and i.coupon_name like concat(#{maps.couponName},'%') " +
+            "</if>" +
+            " order by c.coupon_price desc " +
             "</script>"})
     List<FsStoreCouponIssueVO> getCompanyCouponIssueList(@Param("maps") FsCouponIssueParam map);
+
     @Select({"<script> " +
             "select i.*,c.coupon_price,c.use_min_price,c.coupon_time  from fs_store_coupon_issue_scrm i left join fs_store_coupon_scrm c on c.coupon_id=i.coupon_id  " +
             "where i.id=#{id}  order by c.coupon_price desc " +

+ 1 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java

@@ -69,6 +69,7 @@ public interface FsStoreOrderItemScrmMapper
 
     @Select("select * from fs_store_order_item_scrm where order_id=#{orderId}")
     List<FsStoreOrderItemVO> selectFsStoreOrderItemListByOrderId(Long orderId);
+
     @Select("select * from fs_store_order_item_scrm where order_id=#{orderId}")
     List<FsStoreOrderItemVO> selectMyFsStoreOrderItemListByOrderId(Long id);
 

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePaymentScrmMapper.java

@@ -293,7 +293,7 @@ public interface FsStorePaymentScrmMapper
             "</script>"})
     List<FsStorePaymentVO> selectFsMyStorePaymentListQueryVO(@Param("maps") FsStorePaymentParam fsStorePayment);
 
-    @Select("select * from fs_store_payment_scrm where business_type=2 and order_id=#{orderId} and status=1   ")
+    @Select("select * from fs_store_payment_scrm where (business_type=2 or business_type=8) and order_id=#{orderId} and status=1   ")
     List<FsStorePaymentScrm> selectFsStorePaymentByOrderId(Long orderId);
 
 

+ 4 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductAttrValueScrmMapper.java

@@ -80,7 +80,7 @@ public interface FsStoreProductAttrValueScrmMapper
     List<FsStoreProductAttrValueScrm> selectFsStoreProductAttrValueByProductId(Long productId);
     @Select("select ifnull(stock,0) from fs_store_product_attr_value_scrm where  id=#{productAttrValueId}")
     int selectFsStoreProductStockById(Long productAttrValueId);
-    
+
     /**
      * 使用行锁查询规格库存
      */
@@ -91,9 +91,11 @@ public interface FsStoreProductAttrValueScrmMapper
     @Update("update fs_store_product_attr_value_scrm set stock=stock-#{num},sales=sales+#{num}" +
             " where id=#{productAttrValueId} and stock >= #{num} ")
     int decProductAttrStock(@Param("productAttrValueId")Long productAttrValueId, @Param("num") Integer cartNum);
+
     @Update("update fs_store_product_attr_value_scrm set stock=stock+#{num}, sales=sales-#{num}" +
             " where product_id=#{productId} and id=#{productAttrValueId}")
     int incProductAttrStock(@Param("num")Long num,@Param("productId") Long productId,@Param("productAttrValueId") Long productAttrValueId);
+
     @Select({"<script> " +
             "select v.*,p.product_name,p.product_type,c.cate_name  from fs_store_product_attr_value_scrm v inner join fs_store_product_scrm p on p.product_id=v.product_id left join fs_store_product_category_scrm c on c.cate_id=p.cate_id   " +
             "where 1=1 and v.bar_code is not null " +
@@ -118,6 +120,7 @@ public interface FsStoreProductAttrValueScrmMapper
             " order by v.id desc "+
             "</script>"})
     List<FsStoreProductAttrValueVO> selectFsStoreProductAttrValueListVO(@Param("maps")FsProductAttrValueParam param);
+
     @Select({"<script> " +
             "select v.*,p.product_name from fs_store_product_attr_value_scrm v " +
             "inner join fs_store_product_scrm p on p.product_id=v.product_id    " +

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java

@@ -277,6 +277,7 @@ public interface FsStoreProductScrmMapper
             " and p.product_id=#{productId} " +
             "</script>"})
     FsStoreProductQueryVO selectFsStoreProductByIdQuery(@Param("productId") Long productId, @Param("storeId") String storeId, @Param("config") MedicalMallConfig config);
+
     @Update("update fs_store_product_scrm set stock=stock-#{num}, sales=sales+#{num}" +
             " where product_id=#{productId} and stock >= #{num}")
     int decProductAttrStock(@Param("productId")Long productId, @Param("num")Integer cartNum);
@@ -287,6 +288,7 @@ public interface FsStoreProductScrmMapper
     @Select("select stock from fs_store_product_scrm where product_id=#{productId} for update")
     @DataSource(DataSourceType.SLAVE)
     Integer selectProductStockForUpdate(@Param("productId") Long productId);
+
     @Update("update fs_store_product_scrm set stock=stock+#{num}, sales=sales-#{num}" +
             " where product_id=#{productId}")
     int incStockDecSales( @Param("num")Long num, @Param("productId")Long productId);

+ 1 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsCouponIssueParam.java

@@ -13,4 +13,5 @@ public class FsCouponIssueParam extends BaseQueryParam implements Serializable
     private Integer cateId;
     private Integer couponType;
     private BigDecimal couponPrice;
+    private String couponName;
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов