ソースを参照

Merge remote-tracking branch 'origin/master'

zyy 1 ヶ月 前
コミット
5bef809a8b
100 ファイル変更6330 行追加456 行削除
  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. 4 1
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java
  5. 5 1
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  6. 1 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  7. 25 0
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  8. 60 2
      fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java
  9. 159 0
      fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java
  10. 1 0
      fs-ai-call-task/src/main/resources/logback.xml
  11. 1 0
      fs-cid-workflow/src/main/resources/logback.xml
  12. 333 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentAccountController.java
  13. 134 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentProviderController.java
  14. 153 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmKbCatController.java
  15. 43 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  16. 4 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  17. 22 1
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  18. 2 5
      fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java
  19. 90 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java
  20. 130 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerAnalyzeController.java
  21. 7 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  22. 10 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  23. 62 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  24. 8 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java
  25. 250 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java
  26. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  27. 1 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  28. 4 6
      fs-company/src/test/java/com/mixLiu/test/mixLiuTester.java
  29. 1 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  30. 37 33
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  31. 1 0
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  32. 1 1
      fs-qw-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  33. 1 1
      fs-qwhook-sop/src/main/java/com/fs/FsQwhookSopApplication.java
  34. 6 15
      fs-qwhook-sop/src/main/java/com/fs/app/controller/testController.java
  35. 7 0
      fs-service/src/main/java/com/fs/aiChat/mapper/InterestAiChatSessionMapper.java
  36. 162 0
      fs-service/src/main/java/com/fs/aicall/domain/CcCallTask.java
  37. 68 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmAgentAccount.java
  38. 29 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmAgentProvider.java
  39. 31 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmKb.java
  40. 31 0
      fs-service/src/main/java/com/fs/aicall/domain/CcLlmKbCat.java
  41. 45 0
      fs-service/src/main/java/com/fs/aicall/domain/CcParams.java
  42. 38 0
      fs-service/src/main/java/com/fs/aicall/domain/CompanyBindAiModel.java
  43. 82 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcCallTaskMapper.java
  44. 70 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmAgentAccountMapper.java
  45. 71 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmAgentProviderMapper.java
  46. 70 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmKbCatMapper.java
  47. 79 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcLlmKbMapper.java
  48. 79 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcParamsMapper.java
  49. 97 0
      fs-service/src/main/java/com/fs/aicall/mapper/CompanyBindAiModelMapper.java
  50. 71 0
      fs-service/src/main/java/com/fs/aicall/service/ICcCallTaskService.java
  51. 62 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmAgentAccountService.java
  52. 63 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmAgentProviderService.java
  53. 71 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmKbCatService.java
  54. 70 0
      fs-service/src/main/java/com/fs/aicall/service/ICcLlmKbService.java
  55. 83 0
      fs-service/src/main/java/com/fs/aicall/service/ICcParamsService.java
  56. 114 0
      fs-service/src/main/java/com/fs/aicall/service/ICompanyBindAiModelService.java
  57. 118 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcCallTaskServiceImpl.java
  58. 95 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmAgentAccountServiceImpl.java
  59. 95 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmAgentProviderServiceImpl.java
  60. 113 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmKbCatServiceImpl.java
  61. 154 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcLlmKbServiceImpl.java
  62. 168 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcParamsServiceImpl.java
  63. 179 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CompanyBindAiModelServiceImpl.java
  64. 105 0
      fs-service/src/main/java/com/fs/aicall/utils/CommonUtils.java
  65. 672 0
      fs-service/src/main/java/com/fs/aicall/utils/StringUtils.java
  66. 12 0
      fs-service/src/main/java/com/fs/aicall/utils/XSSFUtils.java
  67. 276 0
      fs-service/src/main/java/com/fs/aicall/utils/http/HttpUtils.java
  68. 1 1
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  69. 4 7
      fs-service/src/main/java/com/fs/company/config/AsyncCalleeConfig.java
  70. 1 1
      fs-service/src/main/java/com/fs/company/domain/CompanyAiWorkflowExec.java
  71. 26 0
      fs-service/src/main/java/com/fs/company/domain/CompanyFsUser.java
  72. 15 0
      fs-service/src/main/java/com/fs/company/domain/CompanyUser.java
  73. 5 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java
  74. 5 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java
  75. 5 1
      fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java
  76. 53 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyFsUserMapper.java
  77. 9 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  78. 9 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java
  79. 2 0
      fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java
  80. 1 1
      fs-service/src/main/java/com/fs/company/param/ExecutionContext.java
  81. 21 0
      fs-service/src/main/java/com/fs/company/service/IAsyncCalleeProcessorService.java
  82. 23 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  83. 12 2
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  84. 473 0
      fs-service/src/main/java/com/fs/company/service/impl/AsyncCalleeProcessorServiceImpl.java
  85. 72 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  86. 32 5
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  87. 116 292
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  88. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  89. 3 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java
  90. 173 24
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  91. 9 5
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  92. 3 3
      fs-service/src/main/java/com/fs/company/util/PhoneNumberUtil.java
  93. 14 0
      fs-service/src/main/java/com/fs/company/vo/CompanyWxClient4WorkFlowVO.java
  94. 1 1
      fs-service/src/main/java/com/fs/core/config/WxMaConfiguration.java
  95. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseLink.java
  96. 16 10
      fs-service/src/main/java/com/fs/course/dto/FsOrderDeliveryNoteDTO.java
  97. 37 2
      fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java
  98. 10 2
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java
  99. 27 7
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  100. 6 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCompanyUserMapper.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;
+        }
+    }
 }

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

@@ -137,7 +137,10 @@ public class CompanyVoiceRoboticController extends BaseController
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
-        return toAjax(companyVoiceRoboticService.deleteCompanyVoiceRoboticByIds(ids));
+        for (Long id : ids) {
+            companyVoiceRoboticService.updateDelFlag(id, 1);
+        }
+        return AjaxResult.success();
     }
     /**
      * 删除机器人外呼任务

+ 5 - 1
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -5,6 +5,7 @@ import java.util.List;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.course.param.FsCourseOverParam;
+import com.fs.course.param.FsCourseSummaryDetailQueryParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
@@ -249,7 +250,10 @@ public class FsCourseWatchLogController extends BaseController
         if (videoId == null || periodId == null) {
             return R.error("视频ID和营期ID不能为空");
         }
-        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(videoId, periodId));
+        FsCourseSummaryDetailQueryParam param = new FsCourseSummaryDetailQueryParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(param));
     }
 
     /**

+ 1 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -679,7 +679,7 @@ public class FsStoreOrderScrmController extends BaseController {
         if (order.getCompanyUserId() != null) {
             CompanyUser companyUser = companyUserService.selectCompanyUserByUserId(order.getCompanyUserId());
             Company company = companyService.selectCompanyById(companyUser.getCompanyId());
-            order.setCompanyUserName(companyUser.getUserName());
+            order.setCompanyUserName(companyUser.getNickName());
             order.setCompanyName(company.getCompanyName());
         } else if (order.getCompanyId() != null) {
             Company company = companyService.selectCompanyById(order.getCompanyId());

+ 25 - 0
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -33,6 +33,7 @@ import com.fs.live.domain.LiveAfterSales;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.domain.LiveOrderItem;
 import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.mapper.LiveMapper;
 import com.fs.live.mapper.LiveOrderItemMapper;
 import com.fs.live.mapper.LiveOrderMapper;
 import com.fs.live.mapper.LiveOrderPaymentMapper;
@@ -170,6 +171,12 @@ public class LiveTask {
     @Autowired
     public RedisBatchHandler redisBatchHandler;
 
+    @Autowired
+    private LiveMapper liveMapper;
+
+    @Autowired
+    private ILiveDataService liveDataService;
+
     /**
      * 查询被拆分的订单,然后查询拆分订单的物流信息
      */
@@ -677,4 +684,22 @@ public class LiveTask {
         redisBatchHandler.consumeBatchData();
     }
 
+    /**
+     * 直播间数据概览缓存定时任务:查询结束时间在7天之内的直播间,执行数据概览统计并缓存到 live_data
+     */
+    public void cacheLiveStatisticsOverview() {
+        try {
+            List<Long> liveIds = liveMapper.selectLiveIdsByFinishTimeWithinDays(7);
+            if (liveIds == null || liveIds.isEmpty()) {
+                log.debug("直播间数据概览缓存任务:7天内无结束的直播间");
+                return;
+            }
+            log.info("直播间数据概览缓存任务:开始处理 {} 个直播间", liveIds.size());
+            liveDataService.calculateAndSaveOverviewForLive(liveIds);
+            log.info("直播间数据概览缓存任务:完成");
+        } catch (Exception e) {
+            log.error("直播间数据概览缓存任务异常", e);
+        }
+    }
+
 }

+ 60 - 2
fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java

@@ -10,8 +10,7 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.live.domain.LiveData;
-import com.fs.live.param.LiveDataCompanyParam;
-import com.fs.live.param.LiveDataParam;
+import com.fs.live.param.*;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.LiveDataCompanyVO;
 import com.fs.live.vo.LiveUserFirstVo;
@@ -190,6 +189,65 @@ public class LiveDataController extends BaseController {
         return util.exportExcel(list, "直播间用户详情数据");
     }
 
+    /**
+     * 直播数据统计-数据概览(12项指标)
+     * @param liveIds 直播间ID列表,前端传入
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/getLiveStatisticsOverview")
+    public R getLiveStatisticsOverview(@RequestBody List<Long> liveIds) {
+
+        return R.ok().put("data",liveDataService.getLiveStatisticsOverview(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 直播趋势-进入人数折线图
+     * 基于 live_user_first_entry 与 live.start_time 计算相对时间,开播前进入的归为"开播前"
+     * @param liveIds 直播间ID列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/getLiveEntryTrend")
+    public R getLiveEntryTrend(@RequestBody List<Long> liveIds) {
+        return R.ok().put("data", liveDataService.getLiveEntryTrend(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 直播间学员列表(分页,基于 live_user_first_entry)
+     * 筛选:直播名称(liveIds)、首次访问时间范围
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listLiveRoomStudents")
+    public R listLiveRoomStudents(@RequestBody LiveRoomStudentParam param) {
+        return liveDataService.listLiveRoomStudents(param);
+    }
+
+    /**
+     * 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listProductCompareStats")
+    public R listProductCompareStats(@RequestBody ProductCompareParam param) {
+        return liveDataService.listProductCompareStats(param);
+    }
+
+    /**
+     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listInviteSalesOptions")
+    public R listInviteSalesOptions(@RequestBody List<Long> liveIds) {
+        return R.ok().put("data", liveDataService.listInviteSalesOptions(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listInviteCompareStats")
+    public R listInviteCompareStats(@RequestBody InviteCompareParam param) {
+        return liveDataService.listInviteCompareStats(param);
+    }
+
     /**
      * 查询分公司直播数据统计列表
      */

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

@@ -0,0 +1,159 @@
+package com.fs.task;
+
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
+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 ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+
+    // 自定义线程池
+    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) {
+                // 获取数据
+                Long customerId = Long.valueOf(data.get("customerId"));
+                String dataJson = data.get("data");
+                Long logId = Long.valueOf(data.get("logId"));
+                //todo 业务!!!!!!2.流失风险等级8.9.客户意向度 //都要异步处理
+                //客户画像
+                crmCustomerAnalyzeService.aiGeneratedCustomerPortrait(customerId,dataJson,logId);
+                //沟通总结
+                crmCustomerAnalyzeService.aiCommunicationSummary(customerId,dataJson,logId);
+                //沟通摘要
+                crmCustomerAnalyzeService.aiCommunicationAbstract(customerId,dataJson,logId);
+//                //流失风险等级 //ai暂时未提供,等ai提供
+                crmCustomerAnalyzeService.aiAttritionLevel(customerId,dataJson,logId);
+                //客户关注点
+                crmCustomerAnalyzeService.aiCustomerFocus(customerId,dataJson,logId);
+                //客户意向度
+                crmCustomerAnalyzeService.aiIntentionDegree(customerId,dataJson,logId);
+
+            }
+
+            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);
+        }
+    }
+}

+ 1 - 0
fs-ai-call-task/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="cidGroupNo" source="cid-group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-ai-call-task/${cidGroupNo}/logs" />
     <!-- 日志输出格式 -->

+ 1 - 0
fs-cid-workflow/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="cidGroupNo" source="cid-group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-cid-workflow/${cidGroupNo}/logs" />
     <!-- 日志输出格式 -->

+ 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());
+        }
+    }
 }

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

@@ -167,7 +167,10 @@ public class CompanyVoiceRoboticController extends BaseController
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
-        return toAjax(companyVoiceRoboticService.deleteCompanyVoiceRoboticByIds(ids));
+        for (Long id : ids) {
+            companyVoiceRoboticService.updateDelFlag(id, 1);
+        }
+        return AjaxResult.success();
     }
     /**
      * 删除机器人外呼任务

+ 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);
     }
 

+ 2 - 5
fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java

@@ -4,10 +4,7 @@ import com.fs.common.core.domain.R;
 import com.fs.company.param.EntryCustomerParam;
 import com.fs.company.service.IGeneralCustomerEntryService;
 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.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 /**
  * @author MixLiu
@@ -22,7 +19,7 @@ public class GeneralCustomerEntryController {
     IGeneralCustomerEntryService iGeneralCustomerEntryService;
 
     @PostMapping("/entryCustomer")
-    public R entryCustomer(EntryCustomerParam param){
+    public R entryCustomer(@RequestBody EntryCustomerParam param){
         iGeneralCustomerEntryService.entryCustomer(param);
        return R.ok("success");
     }

+ 90 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java

@@ -4,8 +4,10 @@ import cn.hutool.core.util.ObjectUtil;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.service.impl.CompanyDeptServiceImpl;
@@ -27,6 +29,8 @@ import com.fs.qw.service.IQwWatchLogService;
 import com.fs.qw.vo.QwWatchLogAllStatisticsListVO;
 import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 import com.fs.sop.mapper.SopUserLogsMapper;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -382,4 +386,90 @@ public class FsCourseWatchLogController extends BaseController
     {
         return toAjax(fsCourseWatchLogService.deleteFsCourseWatchLogByLogIds(logIds));
     }
+
+    /**
+     * 查询课程小结详情总体数据
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 总体统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsDetail")
+    public R getCourseStatisticsDetail(@RequestParam("videoId") Long videoId, @RequestParam("periodId") Long periodId)
+    {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null || loginUser.getCompany() == null || loginUser.getCompany().getCompanyId() == null){
+            return R.error("销售信息不存在!");
+        }
+        FsCourseSummaryDetailQueryParam param = new FsCourseSummaryDetailQueryParam();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(param));
+    }
+
+    /**
+     * 课程小结-用户详情列表(分页)
+     * 根据videoId、periodId查询观看记录,区分首次/第2-n次观看时长,关联订单及销售公司
+     *
+     * @param videoId  视频ID
+     * @param periodId 营期ID
+     * @param pageNum  页码
+     * @param pageSize 每页条数
+     * @return 分页用户详情
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsUserDetail")
+    public R getCourseStatisticsUserDetail(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId,
+            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+            @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null || loginUser.getCompany() == null || loginUser.getCompany().getCompanyId() == null){
+            return R.error("销售信息不存在!");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        param.setPageNum(pageNum);
+        param.setPageSize(pageSize);
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        PageHelper.startPage(pageNum, pageSize);
+        return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.getCourseStatisticsUserDetailList(param)));
+    }
+
+    /**
+     * 课程小结-用户详情导出(按创建时间倒序,最多50000条)
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @Log(title = "课程小结用户详情导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/courseStatisticsUserDetailExport")
+    public AjaxResult courseStatisticsUserDetailExport(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId) {
+        if (videoId == null || periodId == null) {
+            return AjaxResult.error("视频ID和营期ID不能为空");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null || loginUser.getCompany() == null || loginUser.getCompany().getCompanyId() == null){
+            throw new ServiceException("销售信息不存在!");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        List<com.fs.course.vo.CourseStatisticsUserDetailVO> list = fsCourseWatchLogService.getCourseStatisticsUserDetailExportList(param);
+        ExcelUtil<com.fs.course.vo.CourseStatisticsUserDetailVO> util = new ExcelUtil<>(com.fs.course.vo.CourseStatisticsUserDetailVO.class);
+        return util.exportExcel(list, "用户看课数据");
+    }
 }

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

@@ -0,0 +1,130 @@
+package com.fs.company.controller.crm;
+
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.crm.param.PolishingScriptParam;
+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);
+    }
+
+    /**
+     * 查询所有客户(根据客户分组取客户最新一条数据)
+     */
+    @GetMapping("/listAll")
+    public TableDataInfo listAll(CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        startPage();
+        List<CrmCustomerAnalyze> list = crmCustomerAnalyzeService.selectCrmCustomerAnalyzeListAll(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.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));
+    }
+
+    /**
+     * 话术润色
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:polishingScript')")
+    @Log(title = "话术润色", businessType = BusinessType.INSERT)
+    @PostMapping("/polishingScript")
+    public R polishingScript(@RequestBody PolishingScriptParam param)
+    {
+        return R.ok().put("data",crmCustomerAnalyzeService.polishingScript(param));
+    }
+
+}

+ 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);

+ 10 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -5,6 +5,7 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.DateUtils;
@@ -12,7 +13,6 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.http.HttpUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
-import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
 import com.fs.his.domain.FsPayConfig;
@@ -402,4 +402,13 @@ public class LiveController extends BaseController
 
         return R.ok().put("data", exist);
     }
+
+    @ApiOperation("创建App跳转通用链接")
+    @GetMapping("/createAppLink")
+    @PreAuthorize("@ss.hasPermi('live:live:createAppLink')")
+    public R createAppLink(@RequestParam("liveId") Long liveId,@RequestParam("corpId")String corpId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        return liveService.createAppLink(user,liveId,corpId);
+    }
+
 }

+ 62 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java

@@ -15,6 +15,9 @@ import com.fs.framework.service.TokenService;
 import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
+import com.fs.live.param.LiveRoomStudentParam;
+import com.fs.live.param.InviteCompareParam;
+import com.fs.live.param.ProductCompareParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.ColumnsConfigVo;
 import com.fs.live.vo.LiveDataCompanyVO;
@@ -46,6 +49,65 @@ public class LiveDataController extends BaseController
     @Autowired
     private TokenService tokenService;
 
+//    /**
+//     * 直播数据统计-数据概览(12项指标)
+//     * @param liveIds 直播间ID列表,前端传入
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/getLiveStatisticsOverview")
+//    public R getLiveStatisticsOverview(@RequestBody List<Long> liveIds) {
+//
+//        return R.ok().put("data",liveDataService.getLiveStatisticsOverview(liveIds != null ? liveIds : Collections.emptyList()));
+//    }
+//
+//    /**
+//     * 直播趋势-进入人数折线图
+//     * 基于 live_user_first_entry 与 live.start_time 计算相对时间,开播前进入的归为"开播前"
+//     * @param liveIds 直播间ID列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/getLiveEntryTrend")
+//    public R getLiveEntryTrend(@RequestBody List<Long> liveIds) {
+//        return R.ok().put("data", liveDataService.getLiveEntryTrend(liveIds != null ? liveIds : Collections.emptyList()));
+//    }
+//
+//    /**
+//     * 直播间学员列表(分页,基于 live_user_first_entry)
+//     * 筛选:直播名称(liveIds)、首次访问时间范围
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listLiveRoomStudents")
+//    public R listLiveRoomStudents(@RequestBody LiveRoomStudentParam param) {
+//        return liveDataService.listLiveRoomStudents(param);
+//    }
+//
+//    /**
+//     * 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listProductCompareStats")
+//    public R listProductCompareStats(@RequestBody ProductCompareParam param) {
+//        return liveDataService.listProductCompareStats(param);
+//    }
+//
+//    /**
+//     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listInviteSalesOptions")
+//    public R listInviteSalesOptions(@RequestBody List<Long> liveIds) {
+//        return R.ok().put("data", liveDataService.listInviteSalesOptions(liveIds != null ? liveIds : Collections.emptyList()));
+//    }
+//
+//    /**
+//     * 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listInviteCompareStats")
+//    public R listInviteCompareStats(@RequestBody InviteCompareParam param) {
+//        return liveDataService.listInviteCompareStats(param);
+//    }
+
     /**
      * 查询直播间详情数据(SQL方式)
      * @param liveId 直播间ID

+ 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();
     }

+ 250 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java

@@ -0,0 +1,250 @@
+package com.fs.company.controller.qw;
+
+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.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.dto.acquisition.AcquisitionListResponse;
+import com.fs.qw.service.IQwAcquisitionAssistantService;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.AcquisitionAssistantDetailVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 企微-获客链接管理Controller
+ *
+ * @author fs
+ * @date 2026-03-16
+ */
+@Slf4j
+@RestController
+@RequestMapping("/qw/acquisitionAssistant")
+public class QwAcquisitionAssistantController extends BaseController {
+    @Autowired
+    private IQwAcquisitionAssistantService qwAcquisitionAssistantService;
+
+    @Autowired
+    private IQwCompanyService qwCompanyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    /**
+     * 查询企微-获客链接管理列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(QwAcquisitionAssistant qwAcquisitionAssistant) {
+        startPage();
+        List<QwAcquisitionAssistant> list = qwAcquisitionAssistantService.selectQwAcquisitionAssistantList(qwAcquisitionAssistant);
+        return getDataTable(list);
+    }
+
+    /**
+     * 从企微同步获客链接列表(全量)
+     * 手动点击同步按钮时调用
+     */
+    @PostMapping("/syncList")
+    public AjaxResult syncList(@RequestParam String corpId) {
+        try {
+
+            QwCompany qwCompany = getQwCompany(corpId);
+
+            // 调用同步服务
+            String result = qwAcquisitionAssistantService.syncListFromQw(qwCompany.getCorpId(), qwCompany.getOpenSecret());
+
+            return AjaxResult.success(result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 分页获取企微列表(直接调用企微接口)
+     * 用于查看企微原始数据
+     */
+    @GetMapping("/qwList")
+    public AjaxResult getQwList(@RequestParam(required = false) Integer limit,
+                                @RequestParam(required = false) String cursor,
+                                @RequestParam String corpId) {
+        try {
+            QwCompany qwCompany = getQwCompany(corpId);
+            // 调用企微列表接口
+            AcquisitionListResponse response = qwAcquisitionAssistantService.getQwList(
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), limit, cursor);
+
+            return AjaxResult.success(response);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据linkId直接获取详情
+     *
+     * @param linkId 企微链接ID
+     */
+    @GetMapping("/getDetailByLinkId/{linkId}")
+    public AjaxResult getDetailByLinkId(@PathVariable String linkId) {
+        try {
+            // 调用获取详情服务
+            AcquisitionAssistantDetailVO detailVo = qwAcquisitionAssistantService.getDetailWithQw(linkId);
+            return AjaxResult.success("获取成功", detailVo);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 新增企微-获客链接管理
+     */
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody QwAcquisitionAssistant qwAcquisitionAssistant) {
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            qwAcquisitionAssistant.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
+            QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
+            QwAcquisitionAssistant result = qwAcquisitionAssistantService.createWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+            return AjaxResult.success("创建成功", result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取企微用户列表
+     */
+    @PostMapping("/qwUserList")
+    public AjaxResult getQwUserList(@RequestBody QwUser qwUser) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<QwUser> qwUserList = qwUserService.selectQwUserListByAcquisition(qwUser);
+        if (qwUserList != null && !qwUserList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwUserList);
+        } else {
+            return AjaxResult.error("未找到用户");
+        }
+    }
+
+    /**
+     * 获取企微用户主体列表
+     */
+    @PostMapping("/qwUserCompanyList")
+    public AjaxResult getQwUserCompanyList(@RequestBody QwCompany qwCompany) {
+        qwCompany.setStatus(1L);//启用状态的主体
+        List<QwCompany> qwCompanyList = qwCompanyService.selectQwCompanyList(qwCompany);
+        if (qwCompanyList != null && !qwCompanyList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwCompanyList);
+        } else {
+            return AjaxResult.error("未找到主体");
+        }
+    }
+
+    /**
+     * 获取企微用户列表
+     */
+    @PostMapping("/getQwUserListByIds")
+    public AjaxResult getQwUserListByIds(@RequestBody List<Long> qwUserTableIds) {
+        if (qwUserTableIds == null || qwUserTableIds.isEmpty()) {
+            return AjaxResult.success(Collections.emptyList());
+        }
+        // 限制最多500个,避免性能问题
+        if (qwUserTableIds.size() > 500) {
+            qwUserTableIds = qwUserTableIds.subList(0, 500);
+        }
+        List<QwUser> qwUserList = qwUserService.selectQwUserListByIds(qwUserTableIds);
+        if (qwUserList != null && !qwUserList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwUserList);
+        } else {
+            return AjaxResult.error("未找到用户");
+        }
+    }
+
+    /**
+     * 修改企微-获客链接
+     */
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody QwAcquisitionAssistant qwAcquisitionAssistant) {
+        try {
+            // 参数校验
+            if (qwAcquisitionAssistant.getId() == null) {
+                return AjaxResult.error("ID不能为空");
+            }
+            if (StringUtils.isEmpty(qwAcquisitionAssistant.getLinkId())) {
+                return AjaxResult.error("链接ID不能为空");
+            }
+
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+            qwAcquisitionAssistant.setUpdateBy(String.valueOf(loginUser.getUser().getUserId()));
+            QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
+            QwAcquisitionAssistant result = qwAcquisitionAssistantService.updateWithQw(
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+
+            return AjaxResult.success("修改成功", result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 删除企微-获客链接
+     *
+     * @param id 本地记录ID
+     */
+    @GetMapping("/delete/{id}")
+    public AjaxResult delete(@PathVariable Long id) {
+        try {
+            // 先查询本地记录,获取linkId
+            QwAcquisitionAssistant assistant = qwAcquisitionAssistantService.selectQwAcquisitionAssistantById(id);
+            if (assistant == null) {
+                return AjaxResult.error("获客链接不存在");
+            }
+            QwCompany qwCompany = getQwCompany(assistant.getCorpId());
+            // 调用删除服务
+            qwAcquisitionAssistantService.deleteWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), assistant);
+
+            return AjaxResult.success("删除成功");
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    private QwCompany getQwCompany(String corpId) {
+        if (StringUtils.isBlank(corpId)) {
+            log.error("获客链接管理参数异常:{}", corpId);
+            throw new CustomException("未找到企业微信主体");
+        }
+        QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(corpId);
+        if (qwCompany == null) {
+            log.error("获客链接管理-企微主体获取异常:{}", corpId);
+            throw new CustomException("未找到企业微信主体");
+        }
+        return qwCompany;
+    }
+}

+ 1 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -135,6 +135,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/qw/user/selectCloudByCompany").anonymous()
                 .antMatchers("/live/LiveMixLiuTestOpen/**").anonymous()
                 .antMatchers("/app/common/callbackAfterSendSingleMsgCommand").anonymous()
+                .antMatchers("/company/general/customer/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()

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

@@ -258,7 +258,7 @@ public class FsStoreOrderScrmController extends BaseController
         if (order.getCompanyUserId() != null) {
             CompanyUser companyUser = companyUserService.selectCompanyUserByUserId(order.getCompanyUserId());
             Company company = companyService.selectCompanyById(companyUser.getCompanyId());
-            order.setCompanyUserName(companyUser.getUserName());
+            order.setCompanyUserName(companyUser.getNickName());
             order.setCompanyName(company.getCompanyName());
         } else if (order.getCompanyId() != null) {
             Company company = companyService.selectCompanyById(order.getCompanyId());

+ 4 - 6
fs-company/src/test/java/com/mixLiu/test/mixLiuTester.java

@@ -33,12 +33,10 @@ public class mixLiuTester {
 
     @Test
     public void test11(){
-        FsDoctor doctorMap = new FsDoctor();
-        doctorMap.setDoctorId(1L);
-        doctorMap.setJpushId("123");
+        String substring = "18580336425".substring(7, 11);
+        System.out.println(substring);
 
-        if(doctorMap.getStatus() != 1){
-            System.out.println("开始删除处方医生");
-        }
     }
+
+
 }

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -774,7 +774,7 @@ public class IpadSendServer {
         }
         sendShortLink = sendShortLink.replace(".html","");
         String InvitationCode = LinkUtil.encryptLink(sendShortLink);
-        TxtVo txtVo = TxtVo.builder().content(InvitationCode).build();
+        TxtVo txtVo = TxtVo.builder().content("康好健康"+InvitationCode).build();
         txtVo.setBase(vo);
         WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> resp = ipadSendUtils.sendTxt(txtVo);
         if (resp.getErrcode() != 0) {

+ 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();

+ 7 - 0
fs-service/src/main/java/com/fs/aiChat/mapper/InterestAiChatSessionMapper.java

@@ -5,6 +5,7 @@ import com.fs.aiChat.domain.DoctorAiChatLog;
 import com.fs.aiChat.domain.InterestAiChatMsg;
 import com.fs.aiChat.domain.InterestAiSession;
 import com.fs.aiChat.domain.SessionRoleInfo;
+import com.fs.aiChat.param.InterestAiMessage;
 import com.fs.his.domain.FsInterestAiMsg;
 import com.fs.his.domain.FsInterestAiRole;
 import com.fs.his.domain.FsInterestAiSession;
@@ -56,4 +57,10 @@ public interface InterestAiChatSessionMapper extends BaseMapper<DoctorAiChatLog>
 
 
     FsInterestAiRole selectFsInterestAiRoleByRoleId(Long roleId);
+
+    Integer selectSessionIdByUserAndRole(@Param("userId") Long userId, @Param("roleId") Integer roleId);
+
+    FsInterestAiSession selectSessionBySessionId(@Param("sessionId") String sessionId);
+
+    Integer selectAiMsgBySessionIdAndMsg(@Param("param") InterestAiMessage message);
 }

+ 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

@@ -928,7 +928,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());
         CompanyVoiceRoboticCallBlacklistCheckParam companyVoiceRoboticCallBlacklistCheckParam = new CompanyVoiceRoboticCallBlacklistCheckParam();

+ 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 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java

@@ -131,6 +131,11 @@ public class CompanyVoiceRobotic {
     private String qwUserId;
     private Integer taskType;
     private Integer sceneType;
+    @TableField(exist = false)
+    private String sceneTypeName;
     private LocalTime availableStartTime;
     private LocalTime availableEndTime;
+
+    /** 删除标志 0正常 1删除 */
+    private Integer delFlag;
 }

+ 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;
     }
 

+ 5 - 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
  */
@@ -88,4 +88,8 @@ public class CompanyWxClient extends BaseEntityTow
     /** 是否微信 */
     @Excel(name = "加微类型1个微2企微(防止add_type被占用)")
     private Integer isWeCom;
+    /**
+     * 投流来源id
+     */
+    private String traceId;
 }

+ 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);
+}

+ 9 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -74,9 +74,10 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
     /**
      * 根据业务ID查询当天的通话次数
      * @param businessId 业务ID (bes.id)
+     * @param companyId 公司ID
      * @return 当天通话次数,如果没有记录返回0
      */
-    int countTodayCallsByBusinessId(@Param("businessId") Long businessId);
+    int countTodayCallsByBusinessId(@Param("businessId") Long businessId,@Param("companyId") Long companyId);
 
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
@@ -84,4 +85,11 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
 
     List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    /**
+     * 根据业务ID查询公司ID
+     * @param businessId 业务ID (bes.id)
+     * @return 公司ID
+     */
+    Long selectCompanyIdByBusinessId(@Param("businessId") Long businessId);
 }

+ 9 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java

@@ -64,6 +64,15 @@ public interface CompanyVoiceRoboticMapper extends BaseMapper<CompanyVoiceRoboti
      */
     public int deleteCompanyVoiceRoboticByIds(Long[] ids);
 
+    /**
+     * 更新删除标志(逻辑删除)
+     * 
+     * @param id 机器人外呼任务ID
+     * @param delFlag 删除标志 0正常 1删除
+     * @return 结果
+     */
+    public int updateDelFlag(@Param("id") Long id, @Param("delFlag") Integer delFlag);
+
     List<CompanyVoiceRoboticQwUserListVo> qwUserList();
     List<CompanyVoiceRoboticQwUserListVo> qwUserListCompany(CompanyVoiceRobotic companyVoiceRobotic);
 

+ 2 - 0
fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java

@@ -162,5 +162,7 @@ public class EntryCustomerParam {
     private Integer sceneType;
     //对话图
     private String dialogue;
+    //投流id
+    private String traceId;
 
 }

+ 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);
 }

+ 12 - 2
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -72,6 +72,15 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
      */
     public int deleteCompanyVoiceRoboticById(Long id);
 
+    /**
+     * 更新删除标志(逻辑删除)
+     * 
+     * @param id 机器人外呼任务ID
+     * @param delFlag 删除标志 0正常 1删除
+     * @return 结果
+     */
+    public int updateDelFlag(Long id, Integer delFlag);
+
     List<CompanyVoiceRoboticQwUserListVo> qwUserList();
     List<CompanyVoiceRoboticQwUserListVo> qwUserListCompany(CompanyVoiceRobotic companyVoiceRobotic);
 
@@ -97,9 +106,10 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
     void finishAddWxByCallees(Set<Long> roboticIds);
 
     /**
-     *
+     * 入流程执行
      * @param taskId
      * @param crmCustomerId
+     * @param traceId
      */
-    void addNewExec4Task(Long taskId,Long crmCustomerId);
+    void addNewExec4Task(Long taskId,Long crmCustomerId,String traceId);
 }

+ 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;
+    }
+
 }

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

@@ -5,6 +5,7 @@ import java.math.RoundingMode;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
@@ -16,30 +17,31 @@ import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.param.getDialogMapDomain;
 import com.fs.aicall.service.AiCallService;
 import com.fs.common.constant.Constants;
+import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.*;
-import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
-import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.vo.CidConfigVO;
 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;
 import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.impl.SysDictTypeServiceImpl;
 import com.fs.voice.constant.Constant;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
@@ -71,11 +73,15 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Autowired
     CompanyWorkflowEngine companyWorkflowEngine;
     @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+    @Autowired
     QwUserMapper qwUserMapper;
     @Autowired
     @Qualifier("cidWorkFlowExecutor")
     private Executor cidWorkFlowExecutor;
 
+    @Autowired
+    SysDictTypeServiceImpl sysDictTypeService;
     /**
      * 查询调用日志_ai打电话
      *
@@ -307,7 +313,17 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 companyVoiceRoboticCallLog.setCallCreateTime(createTime);
                 Long answerTime = result.getCallEndTime();
                 companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                companyVoiceRoboticCallLog.setIntention(result.getIntent());
+                String intention = result.getIntent();
+                String intentf = null;
+                List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
+                if (!isPositiveInteger(intention)) {
+                    Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intention)).findFirst();
+                    if (firstDict.isPresent()) {
+                        SysDictData sysDictData = firstDict.get();
+                        intentf = sysDictData.getDictValue();
+                    }
+                }
+                companyVoiceRoboticCallLog.setIntention(intentf);
                 companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
                 BigDecimal callCharge = cidConfigVO.getCallCharge();
                 //
@@ -319,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());
@@ -381,4 +399,13 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     public List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
         return baseMapper.listByRoboticId(companyVoiceRoboticCallLogCallphone);
     }
+    /**
+     * 判断整数
+     *
+     * @param str
+     * @return
+     */
+    public boolean isPositiveInteger(String str) {
+        return str != null && str.matches("[1-9]\\d*");
+    }
 }

+ 116 - 292
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,24 @@ 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:";
+    /** intent 意向度等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int EASYCALL_INTENT_MAX_RETRY = 5;
+    /** 每次重试等待时长(毫秒) */
+    private static final long EASYCALL_INTENT_RETRY_INTERVAL_MS = 30000L;
 
     /**
      * 查询机器人外呼任务
@@ -153,10 +144,20 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     public List<CompanyVoiceRobotic> selectCompanyVoiceRoboticList(CompanyVoiceRobotic companyVoiceRobotic){
         return companyVoiceRoboticMapper.selectCompanyVoiceRoboticList(companyVoiceRobotic);
     }
+
     @Override
     @DataScope(deptAlias = "d", userAlias = "u")
-    public List<CompanyVoiceRobotic> selectCompanyVoiceRoboticListCompany(CompanyVoiceRobotic companyVoiceRobotic){
-        return companyVoiceRoboticMapper.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+    public List<CompanyVoiceRobotic> selectCompanyVoiceRoboticListCompany(CompanyVoiceRobotic companyVoiceRobotic) {
+        List<CompanyVoiceRobotic> companyVoiceRobotics = companyVoiceRoboticMapper.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        List<SysDictData> taskSceneType = DictUtils.getDictCache("task_scene_type");
+        companyVoiceRobotics.forEach(a -> {
+            if (null != a.getSceneType()) {
+                taskSceneType.stream().filter(b -> b.getDictValue().equals(a.getSceneType().toString())).findFirst().ifPresent(c -> {
+                    a.setSceneTypeName(c.getDictLabel());
+                });
+            }
+        });
+        return companyVoiceRobotics;
     }
 
     /**
@@ -762,6 +763,18 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         return companyVoiceRoboticMapper.deleteCompanyVoiceRoboticById(id);
     }
 
+    /**
+     * 更新删除标志(逻辑删除)
+     *
+     * @param id 机器人外呼任务ID
+     * @param delFlag 删除标志 0正常 1删除
+     * @return 结果
+     */
+    @Override
+    public int updateDelFlag(Long id, Integer delFlag) {
+        return companyVoiceRoboticMapper.updateDelFlag(id, delFlag);
+    }
+
     @Override
     public List<CompanyVoiceRoboticQwUserListVo> qwUserList() {
         return companyVoiceRoboticMapper.qwUserList();
@@ -819,18 +832,90 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return;
         }
         log.info("进入easyCall外呼结果查询结果callPhoneRes:{}", JSON.toJSONString(callPhoneRes));
+        // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
+        if (StringUtils.isBlank(callPhoneRes.getIntent())) {
+            String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = 0;
+            }
+            if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall外呼回调intent意向度暂未评估完成,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+                doRetryCallerResult4EasyCall(result, retryCount + 1);
+            } else {
+                // 超过最大重试次数,以 intent 为空(意向未知)兜底继续处理
+                log.warn("easyCall外呼回调intent意向度在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // intent 已有值,直接正常处理
+        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 延迟重试处理 EasyCall 外呼回调(等待 intent 意向度异步评估完成)
+     * 每次重试前等待 {@link #EASYCALL_INTENT_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryCallerResult4EasyCall(CdrDetailVo result, int currentRetry) {
+        try {
+            Thread.sleep(EASYCALL_INTENT_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("easyCall intent重试等待被中断, uuid={}", result.getUuid());
+            return;
+        }
+        log.info("easyCall intent重试第{}次开始, uuid={}", currentRetry, result.getUuid());
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if (null == callPhoneRes) {
+            log.error("easyCall intent重试时仍未查询到外呼结果, uuid={}", result.getUuid());
+            return;
+        }
+        if (StringUtils.isBlank(callPhoneRes.getIntent())) {
+            // intent 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall intent仍未评估完成,uuid={},第{}次继续延迟重试", result.getUuid(), retryCount + 1);
+                doRetryCallerResult4EasyCall(result, retryCount + 1);
+            } else {
+                log.warn("easyCall intent在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // intent 已评估完成,正常处理
+        log.info("easyCall intent重试第{}次成功获取到意向度={},uuid={}", currentRetry, callPhoneRes.getIntent(), result.getUuid());
+        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 执行 EasyCall 外呼回调核心业务处理(推送对话内容、更新通话日志)
+     * 供 {@link #callerResult4EasyCall} 和重试逻辑统一调用
+     */
+    private void doHandleEasyCallResult(EasyCallCallPhoneVO callPhoneRes) {
         //等待数据信息
         JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
         String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
         if (StringUtils.isBlank(cacheString)) {
-            log.error("easyCall外呼回调缓存信息缺失:{}", JSON.toJSONString(result));
+            log.error("easyCall外呼回调缓存信息缺失, uuid={}", callPhoneRes.getUuid());
             return;
         }
         JSONObject cacheInfo = JSONObject.parseObject(cacheString);
         pushDialogContent4EasyCall(cacheInfo, callPhoneRes);
         CompanyVoiceRoboticCallees callee = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
         companyVoiceRoboticCallLogCallphoneService.asyncHandleCalleeCallBackResult4EasyCall(callPhoneRes, callee);
-        System.out.println(callPhoneRes);
+        log.info("easyCall外呼回调业务处理完成, uuid={}, intent={}", callPhoneRes.getUuid(), callPhoneRes.getIntent());
     }
 
     public void pushDialogContent(PushIIntentionResult result) {
@@ -1054,6 +1139,9 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         if (robotic.getCompanyAiWorkflowId() == null) {
             throw new RuntimeException("任务未配置工作流: " + id);
         }
+        if(robotic.getDelFlag() == 1){
+            throw new RuntimeException("启动失败,当前任务已删除: " + id);
+        }
         robotic.setTaskStatus(1);
         updateById(robotic);
         // 根据任务加微方式决定是否直接分配微信 平均时 直接分配用户 场景任务不做分配
@@ -1073,7 +1161,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             initAndExecuteWorkflows(robotic, roboticBusinesseList);
         }
     }
-
     /**
      * 初始化场景任务客户流程
      * @param robotic
@@ -1145,7 +1232,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     /**
      * 添加用户到场景任务
      */
-    public void addNewExec4Task(Long taskId, Long crmCustomerId) {
+    public void addNewExec4Task(Long taskId, Long crmCustomerId,String traceId) {
         //保存callees表数据
         CompanyVoiceRobotic companyVoiceRobotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(taskId);
         CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(crmCustomerId);
@@ -1154,6 +1241,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         client.setRoboticId(taskId);
         client.setCustomerId(crmCustomerId);
         client.setIsWeCom(companyVoiceRobotic.getIsWeCom());
+        client.setTraceId(traceId);
         companyWxClientServiceImpl.insertCompanyWxClient(client);
 
         CompanyVoiceRoboticCallees callee = new CompanyVoiceRoboticCallees();
@@ -1203,7 +1291,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         }
         //写入业务表数据
         CompanyVoiceRoboticBusiness companyVoiceRoboticBusiness = buildTaskBussiness4SceneTask(companyVoiceRobotic, callee);
-        //初始化流程表 todo
+        //初始化流程表
         initWorkflows4SceneTask(companyVoiceRobotic,companyVoiceRoboticBusiness);
 
     }
@@ -1337,42 +1425,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());
@@ -1383,13 +1441,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);
 
     }
@@ -1599,231 +1650,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.FAILURE.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);//第一节点
-            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;
     }

+ 3 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java

@@ -179,6 +179,9 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
         workflow.setCreateTime(now);
         workflow.setStartNodeKey(source.getStartNodeKey());
         workflow.setEndNodeKey(source.getEndNodeKey());
+        workflow.setCompanyId(source.getCompanyId());
+        workflow.setCompanyUserId(source.getCompanyUserId());
+
         companyWorkflowMapper.insertCompanyWorkflow(workflow);
         Long newWorkflowId = workflow.getWorkflowId();
 

+ 173 - 24
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -1,27 +1,49 @@
 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.CompanyConfig;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.mapper.CompanyVoiceRoboticMapper;
 import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.service.ICompanyConfigService;
 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 javax.annotation.Resource;
 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,7 +67,11 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
     CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
     @Autowired
     ICompanyVoiceRoboticService companyVoiceRoboticService;
+    @Autowired
+    private SysDictDataMapper sysDictDataMapper;
 
+    @Autowired
+    ICompanyConfigService companyConfigService;
     /**
      * 录入客户
      *
@@ -53,19 +79,19 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
      * @return
      */
     //@Override
-    public R entryCustomer(String param) {
-        try {
-            String decryptParam = CryptoUtil.decrypt(param);
-            if (StringUtils.isBlank(decryptParam)) {
-                return R.error("参数错误");
-            }
-            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
-            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
-        } catch (Exception ex) {
-            log.error("录入客户异常", ex);
-        }
-        return R.ok().put("result", "录入成功");
-    }
+//    public R entryCustomer(String param) {
+//        try {
+//            String decryptParam = CryptoUtil.decrypt(param);
+//            if (StringUtils.isBlank(decryptParam)) {
+//                return R.error("参数错误");
+//            }
+//            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
+//            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
+//        } catch (Exception ex) {
+//            log.error("录入客户异常", ex);
+//        }
+//        return R.ok().put("result", "录入成功");
+//    }
 
     @Override
     @Async("crmCustomerExecutor")
@@ -85,6 +111,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
@@ -98,11 +215,28 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
         }
         //客户数据解析,是否包含对话图 对话图解析标签&意向度标识 todo 庄旭组在研发此功能
         if (StringUtils.isNotBlank(data.getDialogue())) {
-            JSONObject jsonObject = analysisDialogue(data.getDialogue());
-            if (jsonObject != null) {
-                data.setIntention(jsonObject.getString("intention"));
-                data.setTags(jsonObject.getString("tags"));
+            try {
+                List<CrmCustomerAiTagVo> aiTags = getAiTags(data.getDialogue());
+                if(null != aiTags && !aiTags.isEmpty()){
+                    String tags = data.getTags()==null?"":data.getTags();
+                    StringBuilder sb = new StringBuilder(tags);
+                    aiTags.forEach(a->{
+                        sb.append(a.getName()).append(",");
+                    });
+                    if(sb.length() >0 &&  ',' == sb.charAt(sb.length()-1)){
+                        data.setTags(sb.substring(0,sb.length()-1));
+                    }
+                    data.setTags(sb.toString());
+                }
+                //todo 意向度分析
+            } catch (JsonProcessingException e) {
+                log.error("导入用户标签数据解析异常", e);
             }
+//            JSONObject jsonObject = analysisDialogue(data.getDialogue());
+//            if (jsonObject != null) {
+//                data.setIntention(jsonObject.getString("intention"));
+//                data.setTags(jsonObject.getString("tags"));
+//            }
         }
         //客户数据插入
         insertCrmCustomer(data);
@@ -111,7 +245,7 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
             CompanyVoiceRobotic companySceneTasks = getCompanySceneTask(data);
             if(null != companySceneTasks){
                 //场景任务存在 加入场景任务队列
-                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId());
+                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId(),data.getTraceId());
             }
         }
     }
@@ -127,10 +261,18 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
             log.error("手机号格式错误,{}", param.getMobile());
             return false;
         }
-        Long  customerId = crmCustomerMapper.selectCrmCustomerByCrmMobile(param.getMobile());
-        // todo 添加配置是否允许重复客户导入
-        if( null != customerId && true){
-            return false;
+        //根据配置项是否允许重复手机号客户录入
+        CompanyConfig config=companyConfigService.selectCompanyConfigByKey(param.getCompanyId(),"cId.config");
+        if (null != config) {
+            String configValue = config.getConfigValue();
+            JSONObject configObj = JSONObject.parseObject(configValue);
+            if(configObj.containsKey("allowRepeatCustomer") && null != configObj.getBoolean("allowRepeatCustomer") && !configObj.getBoolean("allowRepeatCustomer")){
+                Long  customerId = crmCustomerMapper.selectCrmCustomerByCrmMobileAndCompanyId(param.getCompanyId(),param.getMobile());
+                if( null != customerId && true){
+                    log.error("手机号重复数据,{}", param.getMobile());
+                    return false;
+                }
+            }
         }
         return true;
     }
@@ -153,6 +295,13 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
     public void insertCrmCustomer(EntryCustomerParam data){
         CrmCustomer insertObj = new CrmCustomer();
         BeanUtils.copyProperties(data,insertObj);
+        if(StringUtils.isBlank(insertObj.getCustomerName())){
+            insertObj.setCustomerName("客户"+insertObj.getMobile().substring(7,11));
+        }
+        insertObj.setIsLine(1);
+        if(null == insertObj.getCreateTime()){
+            insertObj.setCreateTime(new Date());
+        }
         crmCustomerMapper.insertCrmCustomer(insertObj);
         data.setCustomerId(insertObj.getCustomerId());
     }

+ 9 - 5
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -356,11 +356,15 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
             //转换数据
             CidPhoneConfig config =JSONObject.parseObject(json,CidPhoneConfig.class);
             if(config.getEnablePhoneLimitConfig() != null && config.getEnablePhoneLimitConfig()){//开启了拨打限制按钮
-                //获取当前外呼业务电话拨打信息
-               int num = companyVoiceRoboticCallLogCallphoneMapper.countTodayCallsByBusinessId(businessId);
-               if(num >= config.getNumberCalls()){
-                   return true;
-               }
+                //获取销售公司
+                Long companyId = companyVoiceRoboticCallLogCallphoneMapper.selectCompanyIdByBusinessId(businessId);
+                if(companyId != null){
+                    //获取当前外呼业务电话拨打信息
+                    int num = companyVoiceRoboticCallLogCallphoneMapper.countTodayCallsByBusinessId(businessId,companyId);
+                    if(num >= config.getNumberCalls()){
+                        return true;
+                    }
+                }
             }
         }
         return false;

+ 3 - 3
fs-service/src/main/java/com/fs/company/util/PhoneNumberUtil.java

@@ -56,20 +56,20 @@ public class PhoneNumberUtil {
     public static boolean isValid(String mobile) {
         // 空值校验
         if (StringUtils.isBlank(mobile)) {
-            log.warn("手机号校验失败:手机号为空");
+            log.error("手机号校验失败:手机号为空");
             return false;
         }
         // 去除首尾空格
         String trimmedMobile = mobile.trim();
         // 长度校验(手机号必须为11位)
         if (trimmedMobile.length() != MOBILE_LENGTH) {
-            log.warn("手机号校验失败:手机号长度不正确,当前长度={}", trimmedMobile.length());
+            log.error("手机号校验失败:手机号长度不正确,当前长度={}", trimmedMobile.length());
             return false;
         }
         // 格式校验(使用正则表达式)
         boolean isValid = MOBILE_PATTERN.matcher(trimmedMobile).matches();
         if (!isValid) {
-            log.warn("手机号校验失败:手机号格式不正确,mobile={}", trimmedMobile);
+            log.error("手机号校验失败:手机号格式不正确,mobile={}", trimmedMobile);
         }
         return isValid;
     }

+ 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;
 }

+ 1 - 1
fs-service/src/main/java/com/fs/core/config/WxMaConfiguration.java

@@ -111,7 +111,7 @@ public class WxMaConfiguration {
             // 查询数据库
             FsCoursePlaySourceConfigMapper configMapper = SpringUtils.getBean(FsCoursePlaySourceConfigMapper.class);
             Wrapper<FsCoursePlaySourceConfig> queryWrapper = Wrappers.<FsCoursePlaySourceConfig>lambdaQuery()
-                    .eq(FsCoursePlaySourceConfig::getAppid, appid)
+                    .eq(FsCoursePlaySourceConfig::getAppid, appid.trim())
                     .eq(FsCoursePlaySourceConfig::getIsDel, 0);
             FsCoursePlaySourceConfig config = configMapper.selectOne(queryWrapper);
             if (config == null) {

+ 2 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseLink.java

@@ -72,4 +72,6 @@ public class FsCourseLink extends BaseEntity
     //@ApiModelProperty(value = "直播id")
     private Long liveId;
 
+    private Long userId;
+
 }

+ 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;
 }

+ 37 - 2
fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java

@@ -119,6 +119,33 @@ public interface FsCourseAnswerLogsMapper
     FsCourseAnswerLogs selectRightLogByCourseVideo(@Param("videoId") Long videoId,@Param("userId") Long userId,@Param("qwUserId") String qwUserId);
 
 
+    @Select({"<script> " +
+            "select * from fs_course_answer_logs where video_id = #{videoId} and user_id = #{userId} and is_right = 1 " +
+            "<if test = 'qwUserId !=null '> " +
+            "and qw_user_id = #{qwUserId} " +
+            "</if>" +
+            "<if test = 'periodId !=null '> " +
+            "and period_id = #{periodId} " +
+            "</if>" +
+            "limit 1 " +
+            "</script>"})
+    FsCourseAnswerLogs selectRightLogByCourseVideoAndPeriodId(@Param("videoId") Long videoId, @Param("userId") Long userId, @Param("qwUserId") String qwUserId, @Param("periodId") Long periodId);
+
+    @Select({"<script> " +
+            "select * from fs_course_answer_logs where video_id = #{videoId} and user_id = #{userId} and is_right = 1 " +
+            "<if test = 'qwUserId !=null '> " +
+            "and qw_user_id = #{qwUserId} " +
+            "</if>" +
+            "<if test = 'periodId !=null '> " +
+            " and period_id = #{periodId} " +
+            "</if>" +
+            "limit 1 " +
+            "</script>"})
+    FsCourseAnswerLogs selectRightLogByCourseVideoWithPeriodId(@Param("videoId") Long videoId,
+                                                               @Param("userId") Long userId,
+                                                               @Param("qwUserId") String qwUserId,
+                                                               @Param("periodId") Long periodId);
+
     @Select({"<script> " +
             "select count(0) from fs_course_answer_logs where video_id = #{videoId} and user_id = #{userId} and is_right = 0 " +
             "<if test = 'qwUserId !=null '> " +
@@ -140,6 +167,14 @@ public interface FsCourseAnswerLogsMapper
     FsCourseAnswerLogs selectRightLogByCourseVideoIsOpen(@Param("videoId") Long videoId,@Param("userId") Long userId);
 
     /** 统计指定视频+营期下去重答题人数 */
-    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_answer_logs WHERE video_id = #{videoId} AND period_id = #{periodId}")
-    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+    @Select("<script>" +
+            "SELECT COUNT(DISTINCT user_id) FROM fs_course_answer_logs " +
+            "WHERE video_id = #{videoId} AND period_id = #{periodId} " +
+            "<if test='companyId != null'>AND company_id = #{companyId}</if>" +
+            "<if test='companyUserId != null'>AND company_user_id = #{companyUserId}</if>" +
+            "</script>")
+    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId,
+                                            @Param("periodId") Long periodId,
+                                            @Param("companyId") Long companyId,
+                                            @Param("companyUserId") Long companyUserId);
 }

+ 10 - 2
fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java

@@ -189,8 +189,16 @@ public interface FsCourseRedPacketLogMapper
     FsCourseRedPacketLog selectUserFsCourseRedPacketLog(@Param("videoId") Long videoId, @Param("userId")Long userId, @Param("periodId")Long periodId);
 
     /** 统计指定视频+营期下去重领红包人数 */
-    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_red_packet_log WHERE video_id = #{videoId} AND period_id = #{periodId}")
-    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+    @Select("<script>" +
+            "SELECT COUNT(DISTINCT user_id) FROM fs_course_red_packet_log " +
+            "WHERE video_id = #{videoId} AND period_id = #{periodId} " +
+            "<if test='companyId != null'>AND company_id = #{companyId}</if>" +
+            "<if test='companyUserId != null'>AND company_user_id = #{companyUserId}</if>" +
+            "</script>")
+    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId,
+                                            @Param("periodId") Long periodId,
+                                            @Param("companyId") Long companyId,
+                                            @Param("companyUserId") Long companyUserId);
 
     @Select("SELECT * FROM fs_course_red_packet_log \n" +
             "WHERE create_time <= DATE_SUB(NOW(), INTERVAL 10 MINUTE)  -- 10 分钟前或更早\n" +

+ 27 - 7
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.dto.WatchLogDTO;
 import com.fs.course.param.*;
+import com.fs.course.param.newfs.FsUserCourseVideoRemainTimeParam;
 import com.fs.course.vo.*;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.param.QwSidebarStatsParam;
@@ -512,6 +513,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     FsCourseWatchLog getWatchCourseVideoByFsUser(@Param("userId") Long userId, @Param("videoId") Long videoId, @Param("companyUserId") Long companyUserId);
 
     FsCourseWatchLog getWatchLogByFsUser(@Param("videoId") Long videoId, @Param("fsUserId") Long fsUserId, @Param("companyUserId") Long companyUserId);
+    FsCourseWatchLog getWatchLogByFsUserAndPeriodId(@Param("videoId") Long videoId, @Param("fsUserId") Long fsUserId, @Param("companyUserId") Long companyUserId, @Param("periodId") Long periodId);
 
     List<FsCourseWatchLogStatisticsListVO> selectFsCourseWatchLogStatisticsListVONew(FsCourseWatchLogStatisticsListParam param);
 
@@ -755,8 +757,8 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
 
     List<FsSopMyCourseH5LinkVO> getSopCourseH5StudyListByQwExId(@Param("qwExternalId") Long qwExternalId);
 
-    @Select("select * from fs_course_watch_log where user_id=#{userId} and company_user_id=#{companyUserId} and course_id=#{courseId} and video_id=#{videoId} limit 1")
-    FsCourseWatchLog selectFsCourseWatchLogWithUCCV(@Param("userId") Long userId,@Param("companyUserId") Long companyUserId,@Param("courseId") Integer courseId,@Param("videoId") Integer videoId);
+    @Select("select * from fs_course_watch_log where user_id=#{param.fsUserId} and company_user_id=#{param.companyUserId} and course_id=#{param.courseId} and video_id=#{param.videoId} and project=#{param.projectId} and period_id=#{param.periodId} limit 1")
+    FsCourseWatchLog selectFsCourseWatchLogWithUCCV(@Param("param") FsUserCourseVideoRemainTimeParam param);
 
     /**
      * 查询视频时长(只返回duration字段)
@@ -772,8 +774,16 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      * @param periodId 营期ID
      * @return 累计观看人数
      */
-    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log WHERE video_id = #{videoId} AND period_id = #{periodId}")
-    Long countDistinctWatchUsers(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+    @Select("<script>" +
+            "SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log " +
+            "WHERE video_id = #{videoId} AND period_id = #{periodId} " +
+            "<if test='companyId != null'>AND company_id = #{companyId}</if>" +
+            "<if test='companyUserId != null'>AND company_user_id = #{companyUserId}</if>" +
+            "</script>")
+    Long countDistinctWatchUsers(@Param("videoId") Long videoId,
+                                 @Param("periodId") Long periodId,
+                                 @Param("companyId") Long companyId,
+                                 @Param("companyUserId") Long companyUserId);
 
     /**
      * 统计累计完课人数(duration >= 1200秒,即20分钟,对userId去重)
@@ -781,8 +791,16 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      * @param periodId 营期ID
      * @return 累计完课人数
      */
-    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log WHERE video_id = #{videoId} AND period_id = #{periodId} AND duration >= 1200")
-    Long countDistinctCompleteUsers(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+    @Select("<script>" +
+            "SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log " +
+            "WHERE video_id = #{videoId} AND period_id = #{periodId} AND duration >= 1200 " +
+            "<if test='companyId != null'>AND company_id = #{companyId}</if>" +
+            "<if test='companyUserId != null'>AND company_user_id = #{companyUserId}</if>" +
+            "</script>")
+    Long countDistinctCompleteUsers(@Param("videoId") Long videoId,
+                                    @Param("periodId") Long periodId,
+                                    @Param("companyId") Long companyId,
+                                    @Param("companyUserId") Long companyUserId);
 
     /**
      * 首次点播数据统计:观看人数、>=20分钟人数、>=30分钟人数
@@ -794,7 +812,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      * @return Map: firstWatchCount, firstWatch20MinCount, firstWatch30MinCount
      */
     Map<String, Object> selectFirstPlaybackStats(@Param("videoId") Long videoId,
-                                                 @Param("periodId") Long periodId);
+                                                 @Param("periodId") Long periodId, @Param("companyId") Long companyId, @Param("companyUserId") Long companyUserId);
 
     /**
      * 第2-n次观看数据统计:view_start不在首次点播窗口内的观看记录
@@ -819,4 +837,6 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      */
     List<com.fs.course.vo.CourseStatisticsUserDetailVO> selectCourseStatisticsUserDetailExportList(
             @Param("param") com.fs.course.param.CourseStatisticsUserDetailParam param);
+
+    FSActualCompletionVO selectActualCompletionList(@Param("periodId") Long periodId, @Param("videoId") Long videoId, @Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
 }

+ 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);
 }

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません