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

Merge remote-tracking branch 'origin/master'

zyy 1 день назад
Родитель
Сommit
5f96f74702
100 измененных файлов с 4151 добавлено и 240 удалено
  1. 28 1
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java
  2. 3 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchCommentController.java
  3. 6 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  4. 22 4
      fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java
  5. 64 0
      fs-admin/src/main/java/com/fs/course/controller/PublicCourseWatchStatisticsController.java
  6. 1 1
      fs-admin/src/main/java/com/fs/his/task/Task.java
  7. 2 0
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  8. 89 0
      fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java
  9. 224 0
      fs-admin/src/main/java/com/fs/live/controller/LiveTrainingCampAdminController.java
  10. 1 1
      fs-company/src/main/java/com/fs/FsCompanyApplication.java
  11. 222 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java
  12. 19 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  13. 58 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  14. 12 7
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  15. 58 58
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  16. 69 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveQuestionLiveController.java
  17. 182 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveTrainingCampController.java
  18. 5 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  19. 2 2
      fs-doctor-app/src/main/resources/application.yml
  20. 94 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  21. 23 4
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  22. 43 0
      fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java
  23. 5 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java
  24. 5 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWxAccount.java
  25. 64 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java
  26. 22 7
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  27. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxAccountMapper.java
  28. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxClientMapper.java
  29. 162 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java
  30. 7 0
      fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java
  31. 69 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  32. 61 0
      fs-service/src/main/java/com/fs/company/service/ICompanySiptaskInfoService.java
  33. 13 7
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java
  34. 99 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  35. 91 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySiptaskInfoServiceImpl.java
  36. 60 13
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  37. 144 26
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  38. 73 9
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  39. 30 22
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java
  40. 21 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java
  41. 73 37
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  42. 15 14
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java
  43. 14 0
      fs-service/src/main/java/com/fs/company/vo/CalleeRoboticCallOutCountVO.java
  44. 14 0
      fs-service/src/main/java/com/fs/company/vo/CustomerRoboticCallOutCountVO.java
  45. 14 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBizGroupVO.java
  46. 75 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java
  47. 14 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallIvrVO.java
  48. 4 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseWatchComment.java
  49. 21 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourse.java
  50. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseCategory.java
  51. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java
  52. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java
  53. 6 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCategoryMapper.java
  54. 9 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java
  55. 18 0
      fs-service/src/main/java/com/fs/course/mapper/PublicCourseWatchStatisticsMapper.java
  56. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchCommentPageParam.java
  57. 3 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchCommentSaveParam.java
  58. 35 0
      fs-service/src/main/java/com/fs/course/param/FsUserCourseCategoryAppQueryParam.java
  59. 38 0
      fs-service/src/main/java/com/fs/course/param/FsUserCoursePublicAppQueryParam.java
  60. 30 0
      fs-service/src/main/java/com/fs/course/param/LiveQuizSubmitUParam.java
  61. 25 0
      fs-service/src/main/java/com/fs/course/param/PublicCourseWatchStatQueryParam.java
  62. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseQuestionBankService.java
  63. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseCategoryService.java
  64. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java
  65. 17 0
      fs-service/src/main/java/com/fs/course/service/IPublicCourseWatchStatisticsService.java
  66. 82 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java
  67. 3 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java
  68. 13 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCategoryServiceImpl.java
  69. 10 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  70. 66 0
      fs-service/src/main/java/com/fs/course/service/impl/PublicCourseWatchStatisticsServiceImpl.java
  71. 3 0
      fs-service/src/main/java/com/fs/course/vo/FsCourseWatchCommentListVO.java
  72. 56 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCoursePublicAppVO.java
  73. 5 0
      fs-service/src/main/java/com/fs/course/vo/FsVideoResourceVO.java
  74. 61 0
      fs-service/src/main/java/com/fs/course/vo/PublicCourseWatchStatCatalogVO.java
  75. 55 0
      fs-service/src/main/java/com/fs/course/vo/PublicCourseWatchStatCourseVO.java
  76. 5 5
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  77. 2 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java
  78. 2 0
      fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java
  79. 2 0
      fs-service/src/main/java/com/fs/crm/vo/CrmMyCustomerListQueryVO.java
  80. 14 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatConversation.java
  81. 3 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatSession.java
  82. 3 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptRole.java
  83. 1 1
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  84. 212 14
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  85. 10 0
      fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java
  86. 48 0
      fs-service/src/main/java/com/fs/his/domain/FsThirdDeviceData.java
  87. 2 0
      fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java
  88. 147 0
      fs-service/src/main/java/com/fs/his/mapper/FsThirdDeviceDataMapper.java
  89. 80 0
      fs-service/src/main/java/com/fs/his/service/IFsThirdDeviceDataService.java
  90. 435 0
      fs-service/src/main/java/com/fs/his/service/impl/FsThirdDeviceDataServiceImpl.java
  91. 2 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  92. 2 0
      fs-service/src/main/java/com/fs/hisStore/vo/StoreOperMainVO.java
  93. 14 1
      fs-service/src/main/java/com/fs/live/domain/Live.java
  94. 22 0
      fs-service/src/main/java/com/fs/live/domain/LiveCourseQuestionRel.java
  95. 37 0
      fs-service/src/main/java/com/fs/live/domain/LiveTrainingCamp.java
  96. 52 0
      fs-service/src/main/java/com/fs/live/domain/LiveTrainingPeriod.java
  97. 28 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCourseQuestionRelMapper.java
  98. 8 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  99. 25 0
      fs-service/src/main/java/com/fs/live/mapper/LiveTrainingCampMapper.java
  100. 27 0
      fs-service/src/main/java/com/fs/live/mapper/LiveTrainingPeriodMapper.java

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

@@ -10,16 +10,21 @@ 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.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyVoiceRoboticWx;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
+import com.fs.framework.web.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -27,11 +32,13 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
  * 机器人外呼任务Controller
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
@@ -47,6 +54,10 @@ public class CompanyVoiceRoboticController extends BaseController
     private AiCallService aiCallService;
     @Autowired
     private ICompanyVoiceRoboticWxService companyVoiceRoboticWxService;
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
     /**
      * 查询机器人外呼任务列表
@@ -71,6 +82,22 @@ public class CompanyVoiceRoboticController extends BaseController
     public TableDataInfo calleesList(Long id){
         startPage();
         List<CompanyVoiceRoboticCallees> list = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesListByRoboticId(id);
+        if (list != null && !list.isEmpty() && id != null) {
+            Long companyId = null;
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser != null && loginUser.getUser() != null) {
+                companyId = loginUser.getUser().getCompanyId();
+            }
+            List<Long> calleeIds = list.stream().map(CompanyVoiceRoboticCallees::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+            if (!calleeIds.isEmpty()) {
+                Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCalleeIds(calleeIds, id, companyId).stream()
+                        .collect(Collectors.toMap(CalleeRoboticCallOutCountVO::getCalleeId, CalleeRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+                for (CompanyVoiceRoboticCallees row : list) {
+                    long n = row.getId() == null ? 0L : countMap.getOrDefault(row.getId(), 0L);
+                    row.setRoboticCallOutCount((int) n);
+                }
+            }
+        }
         return getDataTable(list);
     }
 

+ 3 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchCommentController.java

@@ -96,6 +96,9 @@ public class FsCourseWatchCommentController extends BaseController
     @PostMapping
     public AjaxResult add(@RequestBody FsCourseWatchComment fsCourseWatchComment)
     {
+        if (fsCourseWatchComment.getCateType() == null) {
+            fsCourseWatchComment.setCateType(0);
+        }
         return toAjax(fsCourseWatchCommentService.insertFsCourseWatchComment(fsCourseWatchComment));
     }
 

+ 6 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java

@@ -163,6 +163,9 @@ public class FsUserCourseController extends BaseController {
         if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
+        if (fsUserCourse.getIsPrivate() == null) {
+            fsUserCourse.setIsPrivate(1);
+        }
         fsUserCourseService.insertFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
 
@@ -183,6 +186,9 @@ public class FsUserCourseController extends BaseController {
         if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
+        if (fsUserCourse.getIsPrivate() == null) {
+            fsUserCourse.setIsPrivate(0);
+        }
         fsUserCourseService.insertFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
 

+ 22 - 4
fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java

@@ -3,6 +3,7 @@ package com.fs.course.controller;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.annotation.Log;
@@ -65,6 +66,7 @@ public class FsVideoResourceController extends BaseController {
                               @RequestParam(required = false) String fileName,
                               @RequestParam(required = false) Integer typeId,
                               @RequestParam(required = false) Integer typeSubId,
+                              @RequestParam(required = false) Integer videoType,
                               @RequestParam(required = false, defaultValue = "1") Integer pageNum,
                               @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
         Map<String, Object> params = new HashMap<>();
@@ -72,6 +74,10 @@ public class FsVideoResourceController extends BaseController {
         params.put("fileName", fileName);
         params.put("typeId", typeId);
         params.put("typeSubId", typeSubId);
+        if (videoType == null) {
+            videoType = 0;
+        }
+        params.put("videoType", videoType);
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
@@ -111,6 +117,9 @@ public class FsVideoResourceController extends BaseController {
         if (com.fs.common.utils.StringUtils.isBlank(fsVideoResource.getDisplayType())) {
             fsVideoResource.setDisplayType("landscape");
         }
+        if (fsVideoResource.getVideoType() == null) {
+            fsVideoResource.setVideoType(0);
+        }
 
         fsVideoResource.setCreateTime(LocalDateTime.now());
         boolean save = fsVideoResourceService.save(fsVideoResource);
@@ -172,7 +181,8 @@ public class FsVideoResourceController extends BaseController {
     @PostMapping("/batchUpdateClass")
     public AjaxResult batchUpdateClass(@RequestParam("typeId") Long typeId,
                                        @RequestParam("typeSubId") Long typeSubId,
-                                       @RequestParam("ids") String ids) {
+                                       @RequestParam("ids") String ids,
+                                       @RequestParam(value = "videoType", required = false) Integer videoType) {
         if (typeId == null || typeId <= 0) {
             return AjaxResult.error("请选择分类");
         }
@@ -186,13 +196,18 @@ public class FsVideoResourceController extends BaseController {
         // 将ids字符串分割为ID列表
         List<String> idList = Arrays.asList(ids.split(","));
 
-        // 创建更新条件
-        Wrapper<FsVideoResource> updateWrapper = Wrappers.<FsVideoResource>lambdaUpdate()
+        LambdaUpdateWrapper<FsVideoResource> updateWrapper = Wrappers.<FsVideoResource>lambdaUpdate()
                 .set(FsVideoResource::getTypeId, typeId)
                 .set(FsVideoResource::getTypeSubId, typeSubId)
                 .in(FsVideoResource::getId, idList.stream().map(Long::valueOf).collect(Collectors.toList()));
+        if (videoType != null) {
+            if (videoType == 0) {
+                updateWrapper.apply("(video_type = 0 or video_type is null)");
+            } else {
+                updateWrapper.eq(FsVideoResource::getVideoType, videoType);
+            }
+        }
 
-        // 执行批量更新
         fsVideoResourceService.update(updateWrapper);
         return AjaxResult.success();
     }
@@ -215,6 +230,9 @@ public class FsVideoResourceController extends BaseController {
             if (com.fs.common.utils.StringUtils.isBlank(v.getDisplayType())) {
                 v.setDisplayType("landscape");
             }
+            if (v.getVideoType() == null) {
+                v.setVideoType(0);
+            }
             if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
                 v.setUserId(userId);
             }

+ 64 - 0
fs-admin/src/main/java/com/fs/course/controller/PublicCourseWatchStatisticsController.java

@@ -0,0 +1,64 @@
+package com.fs.course.controller;
+
+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 com.fs.course.param.PublicCourseWatchStatQueryParam;
+import com.fs.course.service.IPublicCourseWatchStatisticsService;
+import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
+import com.fs.course.vo.PublicCourseWatchStatCourseVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 公域看课数据统计(独立 Controller)
+ */
+@RestController
+@RequestMapping("/course/publicCourseWatchStat")
+public class PublicCourseWatchStatisticsController extends BaseController {
+
+    @Autowired
+    private IPublicCourseWatchStatisticsService publicCourseWatchStatisticsService;
+
+    @PreAuthorize("@ss.hasPermi('course:publicCourseWatchStat:list')")
+    @GetMapping("/courseDay/list")
+    public TableDataInfo courseDayList(PublicCourseWatchStatQueryParam param) {
+        startPage();
+        List<PublicCourseWatchStatCourseVO> list = publicCourseWatchStatisticsService.listCourseDayStat(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:publicCourseWatchStat:export')")
+    @Log(title = "公域看课统计-课程数据", businessType = BusinessType.EXPORT)
+    @GetMapping("/courseDay/export")
+    public AjaxResult courseDayExport(PublicCourseWatchStatQueryParam param) {
+        List<PublicCourseWatchStatCourseVO> list = publicCourseWatchStatisticsService.listCourseDayStat(param);
+        ExcelUtil<PublicCourseWatchStatCourseVO> util = new ExcelUtil<>(PublicCourseWatchStatCourseVO.class);
+        return util.exportExcel(list, "公域看课统计-课程数据");
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:publicCourseWatchStat:list')")
+    @GetMapping("/catalog/list")
+    public TableDataInfo catalogList(PublicCourseWatchStatQueryParam param) {
+        startPage();
+        List<PublicCourseWatchStatCatalogVO> list = publicCourseWatchStatisticsService.listCatalogStat(param);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:publicCourseWatchStat:export')")
+    @Log(title = "公域看课统计-目录数据", businessType = BusinessType.EXPORT)
+    @GetMapping("/catalog/export")
+    public AjaxResult catalogExport(PublicCourseWatchStatQueryParam param) {
+        List<PublicCourseWatchStatCatalogVO> list = publicCourseWatchStatisticsService.listCatalogStat(param);
+        ExcelUtil<PublicCourseWatchStatCatalogVO> util = new ExcelUtil<>(PublicCourseWatchStatCatalogVO.class);
+        return util.exportExcel(list, "公域看课统计-目录数据");
+    }
+}

+ 1 - 1
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -265,7 +265,7 @@ public class Task {
                     Long sender = jsonObject.getLong("sender");
                     Integer type = jsonObject.getInteger("type");
 
-                    aiHookService.qwHookNotifyAiReply(qwUserId,sender,content,uid,type);
+                    aiHookService.qwHookNotifyAiReply(qwUserId,sender,content,uid,type,false);
                     //删除已经处理的缓存
                     redisCache.hDel(DELAY_MSG,key);
                 }

+ 2 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveController.java

@@ -61,6 +61,7 @@ public class LiveController extends BaseController {
     @GetMapping("/list")
     public TableDataInfo list(Live live) {
         startPage();
+        live.setExcludeCampLive(true);
         List<Live> list = liveService.selectLiveList(live);
         return getDataTable(list);
     }
@@ -72,6 +73,7 @@ public class LiveController extends BaseController {
     @Log(title = "直播", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(Live live) {
+        live.setExcludeCampLive(true);
         List<Live> list = liveService.selectLiveList(live);
         ExcelUtil<Live> util = new ExcelUtil<Live>(Live.class);
         return util.exportExcel(list, "直播数据");

+ 89 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java

@@ -0,0 +1,89 @@
+package com.fs.live.controller;
+
+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.live.domain.Live;
+import com.fs.live.service.ILiveCourseQuestionRelService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 总后台:直播间答题配置(关联课程题库),与企业端能力一致,按直播间解析所属企业。
+ */
+@RestController
+@RequestMapping("/live/liveQuestionLive")
+public class LiveQuestionLiveController extends BaseController {
+
+    @Autowired
+    private ILiveCourseQuestionRelService liveCourseQuestionRelService;
+    @Autowired
+    private ILiveService liveService;
+
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam Long liveId) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectLinkedByLiveId(liveId, companyId);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/optionList")
+    public TableDataInfo optionList(@RequestParam Long liveId,
+                                    @RequestParam(required = false) String title,
+                                    @RequestParam(required = false) Integer type) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectOptionQuestionBank(
+                liveId, companyId, title, type);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestParam Long liveId, @RequestParam String questionIds) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return AjaxResult.error("直播间不存在");
+        }
+        int rows = liveCourseQuestionRelService.batchAdd(liveId, companyId, questionIds);
+        return rows > 0 ? AjaxResult.success("添加成功") : AjaxResult.success("无新增(可能已全部存在)");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{liveId}")
+    public AjaxResult remove(@PathVariable Long liveId, @RequestParam String ids) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return AjaxResult.error("直播间不存在");
+        }
+        int rows = liveCourseQuestionRelService.deleteByRelIds(liveId, companyId, ids);
+        return toAjax(rows);
+    }
+
+    private Long requireCompanyIdByLiveId(Long liveId) {
+        if (liveId == null) {
+            return null;
+        }
+        Live live = liveService.selectLiveDbByLiveId(liveId);
+        return live != null ? live.getCompanyId() : null;
+    }
+}

+ 224 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveTrainingCampAdminController.java

@@ -0,0 +1,224 @@
+package com.fs.live.controller;
+
+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.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveTrainingCamp;
+import com.fs.live.domain.LiveTrainingPeriod;
+import com.fs.live.param.LiveTrainingLiveAuditBody;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveTrainingCampService;
+import com.fs.live.service.ILiveTrainingPeriodService;
+import com.fs.live.vo.TrainingLiveAuditVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 总后台:直播训练营 / 营期 / 营期直播间(跨企业)及训练营直播间审核
+ */
+@RestController
+@RequestMapping("/live/trainingCamp/admin")
+public class LiveTrainingCampAdminController extends BaseController {
+
+    @Autowired
+    private ILiveTrainingCampService liveTrainingCampService;
+    @Autowired
+    private ILiveTrainingPeriodService liveTrainingPeriodService;
+    @Autowired
+    private ILiveService liveService;
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+
+    // ---------- 训练营 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:list')")
+    @GetMapping("/camp/list")
+    public TableDataInfo campList(LiveTrainingCamp query) {
+        startPage();
+        List<LiveTrainingCamp> list = liveTrainingCampService.selectLiveTrainingCampListAdmin(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:query')")
+    @GetMapping("/camp/{campId}")
+    public AjaxResult getCamp(@PathVariable Long campId, @RequestParam(required = false) Long companyId) {
+        LiveTrainingCamp c = liveTrainingCampService.selectLiveTrainingCampByIdForAdmin(campId, companyId);
+        return c != null ? AjaxResult.success(c) : AjaxResult.error("训练营不存在或无权限");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:add')")
+    @Log(title = "总后台直播训练营", businessType = BusinessType.INSERT)
+    @PostMapping("/camp")
+    public AjaxResult addCamp(@RequestBody LiveTrainingCamp camp) {
+        if (camp.getCompanyId() == null) {
+            return AjaxResult.error("请选择企业");
+        }
+        camp.setCreateBy(getUsername());
+        camp.setCreateTime(new java.util.Date());
+        if (camp.getSortOrder() == null) {
+            camp.setSortOrder(0);
+        }
+        if (camp.getStatus() == null) {
+            camp.setStatus(0);
+        }
+        return toAjax(liveTrainingCampService.insertLiveTrainingCamp(camp));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:edit')")
+    @Log(title = "总后台直播训练营", businessType = BusinessType.UPDATE)
+    @PutMapping("/camp")
+    public AjaxResult editCamp(@RequestBody LiveTrainingCamp camp) {
+        if (camp.getCampId() == null || camp.getCompanyId() == null) {
+            return AjaxResult.error("参数不完整");
+        }
+        camp.setUpdateBy(getUsername());
+        camp.setUpdateTime(new java.util.Date());
+        return toAjax(liveTrainingCampService.updateLiveTrainingCamp(camp));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:remove')")
+    @Log(title = "总后台直播训练营", businessType = BusinessType.DELETE)
+    @DeleteMapping("/camp/{campIds}")
+    public AjaxResult removeCamp(@PathVariable Long[] campIds, @RequestParam Long companyId) {
+        return toAjax(liveTrainingCampService.deleteLiveTrainingCampByIds(campIds, companyId, null));
+    }
+
+    // ---------- 营期 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:list')")
+    @GetMapping("/period/list")
+    public TableDataInfo periodList(LiveTrainingPeriod query) {
+        startPage();
+        List<LiveTrainingPeriod> list = liveTrainingPeriodService.selectLiveTrainingPeriodListAdmin(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:query')")
+    @GetMapping("/period/{periodId}")
+    public AjaxResult getPeriod(@PathVariable Long periodId, @RequestParam(required = false) Long companyId) {
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodByIdForAdmin(periodId, companyId);
+        if (p == null) {
+            return AjaxResult.error("营期不存在或无权限");
+        }
+        LiveTrainingCamp camp = liveTrainingCampService.selectLiveTrainingCampByIdForAdmin(p.getCampId(), null);
+        if (camp != null) {
+            p.setCompanyId(camp.getCompanyId());
+        }
+        return AjaxResult.success(p);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:add')")
+    @Log(title = "总后台直播营期", businessType = BusinessType.INSERT)
+    @PostMapping("/period")
+    public AjaxResult addPeriod(@RequestBody LiveTrainingPeriod period) {
+        if (period.getCampId() == null) {
+            return AjaxResult.error("请选择训练营");
+        }
+        LiveTrainingCamp camp = liveTrainingCampService.selectLiveTrainingCampByIdForAdmin(period.getCampId(), null);
+        if (camp == null) {
+            return AjaxResult.error("训练营不存在");
+        }
+        period.setCompanyId(camp.getCompanyId());
+        period.setCreateBy(camp.getCreateBy());
+        period.setCreateTime(new java.util.Date());
+        if (period.getSortOrder() == null) {
+            period.setSortOrder(0);
+        }
+        if (period.getStatus() == null) {
+            period.setStatus(0);
+        }
+        return toAjax(liveTrainingPeriodService.insertLiveTrainingPeriod(period));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:edit')")
+    @Log(title = "总后台直播营期", businessType = BusinessType.UPDATE)
+    @PutMapping("/period")
+    public AjaxResult editPeriod(@RequestBody LiveTrainingPeriod period) {
+        if (period.getPeriodId() == null || period.getCompanyId() == null) {
+            return AjaxResult.error("参数不完整");
+        }
+        period.setUpdateBy(getUsername());
+        period.setUpdateTime(new java.util.Date());
+        return toAjax(liveTrainingPeriodService.updateLiveTrainingPeriod(period));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:remove')")
+    @Log(title = "总后台直播营期", businessType = BusinessType.DELETE)
+    @DeleteMapping("/period/{periodIds}")
+    public AjaxResult removePeriod(@PathVariable Long[] periodIds, @RequestParam Long companyId) {
+        return toAjax(liveTrainingPeriodService.deleteLiveTrainingPeriodByIds(periodIds, companyId, null));
+    }
+
+    // ---------- 营期下直播间 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:list')")
+    @GetMapping("/live/list")
+    public TableDataInfo trainingLiveList(Live query, @RequestParam Long companyId) {
+        if (query.getTrainingPeriodId() == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodByIdForAdmin(
+                query.getTrainingPeriodId(), companyId);
+        if (p == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        query.setCompanyId(companyId);
+        List<CompanyUser> users = companyUserMapper.selectCompanyUserByCompanyId(companyId);
+        if (users != null && !users.isEmpty()) {
+            query.setCompanyUserId(users.get(0).getUserId());
+        }
+        startPage();
+        List<Live> list = liveService.selectLiveList(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:live:add')")
+    @Log(title = "总后台训练营直播间", businessType = BusinessType.INSERT)
+    @PostMapping("/live")
+    public AjaxResult addTrainingLive(@RequestBody Live live, @RequestParam Long companyId) {
+        if (StringUtils.isEmpty(live.getCreateBy())) {
+            live.setCreateBy(String.valueOf(getUserId()));
+        }
+        return toAjax(liveTrainingPeriodService.insertTrainingLiveForAdmin(live, companyId));
+    }
+
+    // ---------- 训练营直播间审核 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:audit:list')")
+    @GetMapping("/live/auditList")
+    public TableDataInfo auditList(Live query) {
+        startPage();
+        List<TrainingLiveAuditVO> list = liveService.selectTrainingLiveAuditList(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:audit:edit')")
+    @Log(title = "训练营直播间审核", businessType = BusinessType.UPDATE)
+    @PutMapping("/live/audit")
+    public AjaxResult audit(@RequestBody LiveTrainingLiveAuditBody body) {
+        if (body == null || body.getLiveId() == null || body.getPassed() == null) {
+            return AjaxResult.error("参数不完整");
+        }
+        Live db = liveService.selectLiveDbByLiveId(body.getLiveId());
+        if (db == null || db.getTrainingPeriodId() == null) {
+            return AjaxResult.error("仅支持训练营直播间审核");
+        }
+        Live upd = new Live();
+        upd.setLiveId(body.getLiveId());
+        upd.setIsAudit(Boolean.TRUE.equals(body.getPassed()) ? 1 : 2);
+        if (StringUtils.isNotEmpty(body.getRemark())) {
+            upd.setRemark(body.getRemark());
+        }
+        return toAjax(liveService.updateLive(upd));
+    }
+}

+ 1 - 1
fs-company/src/main/java/com/fs/FsCompanyApplication.java

@@ -11,7 +11,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
  */
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
-@EnableAsync
+//@EnableAsync
 public class FsCompanyApplication
 {
     public static void main(String[] args)

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

@@ -0,0 +1,222 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.mapper.EasyCallInboundLlmMapper;
+import com.fs.company.service.ICompanyInboundCallManageService;
+import com.fs.company.vo.easycall.EasyCallBizGroupVO;
+import com.fs.company.vo.easycall.EasyCallGatewayVO;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import com.fs.company.vo.easycall.EasyCallIvrVO;
+import com.fs.company.vo.easycall.EasyCallLlmAccountVO;
+import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 呼入大模型配置 Controller
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/company/inboundCallManage")
+public class CompanyInboundCallManageController extends BaseController {
+
+    @Autowired
+    private ICompanyInboundCallManageService inboundCallManageService;
+
+    @Autowired
+    private EasyCallInboundLlmMapper inboundLlmMapper;
+
+    /**
+     * 查询呼入大模型配置列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(EasyCallInboundLlmVO vo) {
+        startPage();
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmList(vo);
+        TableDataInfo rspData = getDataTable(list);
+        // 填充关联数据
+        @SuppressWarnings("unchecked")
+        List<EasyCallInboundLlmVO> records = (List<EasyCallInboundLlmVO>) rspData.getRows();
+        for (EasyCallInboundLlmVO data : records) {
+            fillRelationData(data);
+        }
+        rspData.setRows(records);
+        return rspData;
+    }
+
+    /**
+     * 获取大模型账户下拉列表
+     */
+    @GetMapping("/llmAccountList")
+    public AjaxResult getLlmAccountList() {
+        List<EasyCallLlmAccountVO> list = inboundLlmMapper.selectLlmAccountList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取呼入大模型配置详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Integer id) {
+        return AjaxResult.success(inboundCallManageService.selectInboundLlmById(id));
+    }
+
+    /**
+     * 新增呼入大模型配置
+     */
+    @Log(title = "呼入大模型配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody EasyCallInboundLlmVO vo) {
+        return toAjax(inboundCallManageService.insertInboundLlm(vo));
+    }
+
+    /**
+     * 修改呼入大模型配置
+     */
+    @Log(title = "呼入大模型配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody EasyCallInboundLlmVO vo) {
+        return toAjax(inboundCallManageService.updateInboundLlm(vo));
+    }
+
+    /**
+     * 删除呼入大模型配置
+     */
+    @Log(title = "呼入大模型配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String ids) {
+        return toAjax(inboundCallManageService.deleteInboundLlmByIds(ids));
+    }
+
+    /**
+     * 校验被叫号码是否唯一
+     */
+    @GetMapping("/checkCallee")
+    public AjaxResult checkCallee(@RequestParam(value = "id", required = false) Integer id,
+                                  @RequestParam("callee") String callee) {
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmByCallee(callee);
+        if (list.size() <= 0) {
+            return AjaxResult.success(true);
+        }
+        if (null != id && list.get(0).getId().equals(id)) {
+            return AjaxResult.success(true);
+        }
+        return AjaxResult.success(false);
+    }
+
+    /**
+     * 获取所有AI配置列表
+     */
+    @GetMapping("/ai/all")
+    public AjaxResult getAllAi() {
+        EasyCallInboundLlmVO query = new EasyCallInboundLlmVO();
+        query.setServiceType("ai");
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmList(query);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取ASR提供商列表
+     */
+    @GetMapping("/asrProviderList")
+    public AjaxResult getAsrProviderList() {
+        List<Map<String, String>> list = inboundLlmMapper.selectAsrProviderList();
+        // 转换为Map格式
+        Map<String, String> result = new HashMap<>();
+        for (Map<String, String> item : list) {
+            String key = item.get("key");
+            String value = item.get("value");
+            if (key != null) {
+                result.put(key, value != null ? value : key);
+            }
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取TTS音色来源列表
+     */
+    @GetMapping("/voiceSourceList")
+    public AjaxResult getVoiceSourceList() {
+        List<Map<String, String>> list = inboundLlmMapper.selectVoiceSourceList();
+        // 转换为Map格式
+        Map<String, String> result = new HashMap<>();
+        for (Map<String, String> item : list) {
+            String key = item.get("key");
+            String value = item.get("value");
+            if (key != null) {
+                result.put(key, value != null ? value : key);
+            }
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 根据音色来源获取音色列表
+     */
+    @GetMapping("/voiceList")
+    public AjaxResult getVoiceList(@RequestParam("voiceSource") String voiceSource) {
+        List<EasyCallVoiceCodeVO> list = inboundLlmMapper.selectVoiceListBySource(voiceSource);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取业务组列表
+     */
+    @GetMapping("/bizGroupList")
+    public AjaxResult getBizGroupList() {
+        List<EasyCallBizGroupVO> list = inboundLlmMapper.selectBizGroupList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取出局网关列表
+     */
+    @GetMapping("/gatewayList")
+    public AjaxResult getGatewayList() {
+        List<EasyCallGatewayVO> list = inboundLlmMapper.selectOutboundGatewayList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取IVR列表
+     */
+    @GetMapping("/ivrList")
+    public AjaxResult getIvrList() {
+        List<EasyCallIvrVO> list = inboundLlmMapper.selectIvrList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 填充关联数据
+     */
+    private void fillRelationData(EasyCallInboundLlmVO data) {
+        // 填充大模型账户名称
+        if (data.getLlmAccountId() != null && data.getLlmAccountId() > 0) {
+            EasyCallLlmAccountVO llmAccount = inboundLlmMapper.selectLlmAccountById(data.getLlmAccountId());
+            if (llmAccount != null) {
+                data.setLlmAccountName(llmAccount.getName());
+            } else {
+                data.setLlmAccountName("");
+            }
+        }
+        // 填充音色名称
+        if (StringUtils.isNotEmpty(data.getVoiceCode())) {
+            EasyCallVoiceCodeVO voiceCode = inboundLlmMapper.selectVoiceCodeByCode(data.getVoiceCode());
+            if (voiceCode != null) {
+                data.setVoiceSource(voiceCode.getVoiceSource());
+                data.setVoiceName(voiceCode.getVoiceName());
+            }
+        }
+    }
+}

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

@@ -21,9 +21,11 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyVoiceRoboticWx;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
 import com.fs.company.vo.CdrBodyVo;
 import com.fs.company.vo.CdrDetailVo;
 import com.fs.company.vo.WorkflowExecRecordVo;
@@ -37,6 +39,8 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
@@ -60,6 +64,8 @@ public class CompanyVoiceRoboticController extends BaseController
     private ICompanyVoiceRoboticWxService companyVoiceRoboticWxService;
     @Autowired
     private TokenService tokenService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
     /**
      * 查询机器人外呼任务列表
@@ -88,6 +94,19 @@ public class CompanyVoiceRoboticController extends BaseController
     public TableDataInfo calleesList(Long id){
         startPage();
         List<CompanyVoiceRoboticCallees> list = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesListByRoboticId(id);
+        if (list != null && !list.isEmpty() && id != null) {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            Long companyId = loginUser.getCompany().getCompanyId();
+            List<Long> calleeIds = list.stream().map(CompanyVoiceRoboticCallees::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+            if (!calleeIds.isEmpty()) {
+                Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCalleeIds(calleeIds, id, companyId).stream()
+                        .collect(Collectors.toMap(CalleeRoboticCallOutCountVO::getCalleeId, CalleeRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+                for (CompanyVoiceRoboticCallees row : list) {
+                    long n = row.getId() == null ? 0L : countMap.getOrDefault(row.getId(), 0L);
+                    row.setRoboticCallOutCount((int) n);
+                }
+            }
+        }
         return getDataTable(list);
     }
 

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

@@ -12,7 +12,9 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.util.OrderUtils;
+import com.fs.company.vo.CustomerRoboticCallOutCountVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.*;
 import com.fs.crm.service.ICrmCustomerPropertyService;
@@ -33,6 +35,9 @@ import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * 客户Controller
@@ -54,6 +59,8 @@ public class CrmCustomerController extends BaseController
     ICrmCustomerUserService crmCustomerUserService;
     @Autowired
     private ICrmCustomerPropertyService crmCustomerPropertyService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
     @ApiOperation("获取线索客户")
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
@@ -71,6 +78,7 @@ public class CrmCustomerController extends BaseController
                 }
 
             }
+            fillLineCustomerListRoboticCallOutCount(list, param.getCompanyId());
         }
         return getDataTable(list);
     }
@@ -178,6 +186,7 @@ public class CrmCustomerController extends BaseController
                     vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
             }
+            fillMyCustomerListRoboticCallOutCount(list, param.getCompanyId());
         }
         return getDataTable(list);
 
@@ -204,6 +213,7 @@ public class CrmCustomerController extends BaseController
                     }
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
                 }
+                fillLineCustomerListRoboticCallOutCount(list1, param.getCompanyId());
             }
             return getDataTable(list1);
         }else {
@@ -216,10 +226,58 @@ public class CrmCustomerController extends BaseController
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
 
                 }
+                fillCustomerListRoboticCallOutCount(list, param.getCompanyId());
             }
             return getDataTable(list);
         }
     }
+    private void fillCustomerListRoboticCallOutCount(List<CrmCustomerListQueryVO> list, Long companyId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<Long> customerIds = list.stream().map(CrmCustomerListQueryVO::getCustomerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (customerIds.isEmpty()) {
+            return;
+        }
+        Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCustomerIds(customerIds, companyId).stream()
+                .collect(Collectors.toMap(CustomerRoboticCallOutCountVO::getCustomerId, CustomerRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+        for (CrmCustomerListQueryVO vo : list) {
+            long n = vo.getCustomerId() == null ? 0L : countMap.getOrDefault(vo.getCustomerId(), 0L);
+            vo.setRoboticCallOutCount((int) n);
+        }
+    }
+
+    private void fillLineCustomerListRoboticCallOutCount(List<CrmLineCustomerListQueryVO> list, Long companyId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<Long> customerIds = list.stream().map(CrmLineCustomerListQueryVO::getCustomerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (customerIds.isEmpty()) {
+            return;
+        }
+        Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCustomerIds(customerIds, companyId).stream()
+                .collect(Collectors.toMap(CustomerRoboticCallOutCountVO::getCustomerId, CustomerRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+        for (CrmLineCustomerListQueryVO vo : list) {
+            long n = vo.getCustomerId() == null ? 0L : countMap.getOrDefault(vo.getCustomerId(), 0L);
+            vo.setRoboticCallOutCount((int) n);
+        }
+    }
+
+    private void fillMyCustomerListRoboticCallOutCount(List<CrmMyCustomerListQueryVO> list, Long companyId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<Long> customerIds = list.stream().map(CrmMyCustomerListQueryVO::getCustomerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (customerIds.isEmpty()) {
+            return;
+        }
+        Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCustomerIds(customerIds, companyId).stream()
+                .collect(Collectors.toMap(CustomerRoboticCallOutCountVO::getCustomerId, CustomerRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+        for (CrmMyCustomerListQueryVO vo : list) {
+            long n = vo.getCustomerId() == null ? 0L : countMap.getOrDefault(vo.getCustomerId(), 0L);
+            vo.setRoboticCallOutCount((int) n);
+        }
+    }
     @ApiOperation("获取客户详情")
     @GetMapping("/getCustomerDetails")
     @PreAuthorize("@ss.hasPermi('crm:customer:query')")

+ 12 - 7
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -89,6 +89,8 @@ public class LiveController extends BaseController
     {
         // 设置企业ID和企业用户ID
         setCompanyId(live);
+        // 普通直播列表不包含「训练营-营期」下的直播间
+        live.setExcludeCampLive(true);
 
         startPage();
         List<Live> list = liveService.selectLiveList(live);
@@ -121,7 +123,7 @@ public class LiveController extends BaseController
     {
         // 设置企业ID和企业用户ID
         setCompanyId(live);
-
+        live.setExcludeCampLive(true);
 
         List<Live> list = liveService.selectLiveList(live);
         ExcelUtil<Live> util = new ExcelUtil<Live>(Live.class);
@@ -202,12 +204,15 @@ public class LiveController extends BaseController
     @PutMapping
     public AjaxResult edit(@RequestBody Live live)
     {
-        return AjaxResult.success();
-//        CompanyUser user = SecurityUtils.getLoginUser().getUser();
-//        live.setCompanyUserId(user.getUserId());
-//        live.setCompanyId(user.getCompanyId());
-//
-//        return toAjax(liveService.updateLive(live));
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        live.setCompanyUserId(user.getUserId());
+        live.setCompanyId(user.getCompanyId());
+        Live db = liveService.selectLiveByLiveIdAndCompanyIdAndCompanyUserId(
+                live.getLiveId(), user.getCompanyId(), user.getUserId());
+        if (db != null && db.getTrainingPeriodId() != null) {
+            live.setIsAudit(0);
+        }
+        return toAjax(liveService.updateLive(live));
     }
 
     /**

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

@@ -49,64 +49,64 @@ 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);
-//    }
+    /**
+     * 直播数据统计-数据概览(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方式)

+ 69 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveQuestionLiveController.java

@@ -0,0 +1,69 @@
+package com.fs.company.controller.live;
+
+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.company.domain.CompanyUser;
+import com.fs.framework.security.SecurityUtils;
+import com.fs.live.service.ILiveCourseQuestionRelService;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 直播间答题配置:关联课程题库 fs_course_question_bank
+ */
+@RestController
+@RequestMapping("/live/liveQuestionLive")
+public class LiveQuestionLiveController extends BaseController {
+
+    @Autowired
+    private ILiveCourseQuestionRelService liveCourseQuestionRelService;
+
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam Long liveId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectLinkedByLiveId(liveId, user.getCompanyId());
+        return getDataTable(list);
+    }
+
+    /**
+     * 待选:课程题库中尚未关联到本直播间的试题(status=1)
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/optionList")
+    public TableDataInfo optionList(@RequestParam Long liveId,
+                                    @RequestParam(required = false) String title,
+                                    @RequestParam(required = false) Integer type) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectOptionQuestionBank(
+                liveId, user.getCompanyId(), title, type);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestParam Long liveId, @RequestParam String questionIds) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        int rows = liveCourseQuestionRelService.batchAdd(liveId, user.getCompanyId(), questionIds);
+        return rows > 0 ? AjaxResult.success("添加成功") : AjaxResult.success("无新增(可能已全部存在)");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{liveId}")
+    public AjaxResult remove(@PathVariable Long liveId, @RequestParam String ids) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        int rows = liveCourseQuestionRelService.deleteByRelIds(liveId, user.getCompanyId(), ids);
+        return toAjax(rows);
+    }
+}

+ 182 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveTrainingCampController.java

@@ -0,0 +1,182 @@
+package com.fs.company.controller.live;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.security.SecurityUtils;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveTrainingCamp;
+import com.fs.live.domain.LiveTrainingPeriod;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveTrainingCampService;
+import com.fs.live.service.ILiveTrainingPeriodService;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 训练营-营期-直播间(与普通直播列表区分,live.training_period_id 非空)
+ */
+@RestController
+@RequestMapping("/live/trainingCamp")
+public class LiveTrainingCampController extends BaseController {
+
+    @Autowired
+    private ILiveTrainingCampService liveTrainingCampService;
+    @Autowired
+    private ILiveTrainingPeriodService liveTrainingPeriodService;
+    @Autowired
+    private ILiveService liveService;
+
+    private static R pageR(List<?> list) {
+        return R.ok().put("rows", list).put("total", new PageInfo(list).getTotal());
+    }
+
+    private static R toR(int rows, String okMsg) {
+        return rows > 0 ? R.ok(okMsg) : R.error("操作失败");
+    }
+
+    // ---------- 训练营 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:list')")
+    @GetMapping("/camp/list")
+    public R campList(LiveTrainingCamp query) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        if (StringUtils.isEmpty(user.getUserName())) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        query.setCompanyId(user.getCompanyId());
+        query.setCreateBy(user.getUserName());
+        startPage();
+        List<LiveTrainingCamp> list = liveTrainingCampService.selectLiveTrainingCampList(query);
+        return pageR(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:query')")
+    @GetMapping("/camp/{campId}")
+    public R getCamp(@PathVariable Long campId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        LiveTrainingCamp c = liveTrainingCampService.selectLiveTrainingCampById(campId, user.getCompanyId(), user.getUserName());
+        return R.ok().put("data", c);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:add')")
+    @Log(title = "直播训练营", businessType = BusinessType.INSERT)
+    @PostMapping("/camp")
+    public R addCamp(@RequestBody LiveTrainingCamp camp) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        camp.setCompanyId(user.getCompanyId());
+        camp.setCreateBy(user.getUserName());
+        return toR(liveTrainingCampService.insertLiveTrainingCamp(camp), "新增成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:edit')")
+    @Log(title = "直播训练营", businessType = BusinessType.UPDATE)
+    @PutMapping("/camp")
+    public R editCamp(@RequestBody LiveTrainingCamp camp) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        camp.setCompanyId(user.getCompanyId());
+        camp.setUpdateBy(user.getUserName());
+        camp.getParams().put("ownerCreateBy", user.getUserName());
+        return toR(liveTrainingCampService.updateLiveTrainingCamp(camp), "修改成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:remove')")
+    @Log(title = "直播训练营", businessType = BusinessType.DELETE)
+    @DeleteMapping("/camp/{campIds}")
+    public R removeCamp(@PathVariable Long[] campIds) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        return toR(liveTrainingCampService.deleteLiveTrainingCampByIds(campIds, user.getCompanyId(), user.getUserName()), "删除成功");
+    }
+
+    // ---------- 营期 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:list')")
+    @GetMapping("/period/list")
+    public R periodList(LiveTrainingPeriod query) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        if (StringUtils.isEmpty(user.getUserName())) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        query.setCompanyId(user.getCompanyId());
+        query.setCreateBy(user.getUserName());
+        startPage();
+        List<LiveTrainingPeriod> list = liveTrainingPeriodService.selectLiveTrainingPeriodList(query);
+        return pageR(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:query')")
+    @GetMapping("/period/{periodId}")
+    public R getPeriod(@PathVariable Long periodId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodById(periodId, user.getCompanyId(), user.getUserName());
+        return R.ok().put("data", p);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:add')")
+    @Log(title = "直播训练营营期", businessType = BusinessType.INSERT)
+    @PostMapping("/period")
+    public R addPeriod(@RequestBody LiveTrainingPeriod period) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        period.setCompanyId(user.getCompanyId());
+        period.setCreateBy(user.getUserName());
+        return toR(liveTrainingPeriodService.insertLiveTrainingPeriod(period), "新增成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:edit')")
+    @Log(title = "直播训练营营期", businessType = BusinessType.UPDATE)
+    @PutMapping("/period")
+    public R editPeriod(@RequestBody LiveTrainingPeriod period) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        period.setCompanyId(user.getCompanyId());
+        period.setUpdateBy(user.getUserName());
+        period.getParams().put("ownerCreateBy", user.getUserName());
+        return toR(liveTrainingPeriodService.updateLiveTrainingPeriod(period), "修改成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:remove')")
+    @Log(title = "直播训练营营期", businessType = BusinessType.DELETE)
+    @DeleteMapping("/period/{periodIds}")
+    public R removePeriod(@PathVariable Long[] periodIds) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        return toR(liveTrainingPeriodService.deleteLiveTrainingPeriodByIds(periodIds, user.getCompanyId(), user.getUserName()), "删除成功");
+    }
+
+    // ---------- 营期下直播间 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:list')")
+    @GetMapping("/live/list")
+    public R trainingLiveList(Live query) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        query.setCompanyId(user.getCompanyId());
+        query.setCompanyUserId(user.getUserId());
+        if (query.getTrainingPeriodId() == null) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodById(
+                query.getTrainingPeriodId(), user.getCompanyId(), user.getUserName());
+        if (p == null) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        startPage();
+        List<Live> list = liveService.selectLiveList(query);
+        return pageR(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:live:add')")
+    @Log(title = "训练营直播间", businessType = BusinessType.INSERT)
+    @PostMapping("/live")
+    public R addTrainingLive(@RequestBody Live live) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        // live.create_by 列为数值类型(创建人用户ID),不能使用昵称/登录名
+        live.setCreateBy(String.valueOf(user.getUserId()));
+        return toR(liveTrainingPeriodService.insertTrainingLive(live, user.getCompanyId(), user.getUserId(), user.getUserName()), "新增成功");
+    }
+}

+ 5 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -553,10 +553,14 @@ public class QwUserController extends BaseController
     public TableDataInfo queryQwList(QwUser qwUser)
     {
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         if(qwUser.getCompanyId() == null){
-            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
             qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
         }
+        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+            qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+            qwUser.setCompanyUserId(loginUser.getUser().getUserId());
+        }
         List<QwUser> list = qwUserService.selectQwUserList(qwUser);
         return getDataTable(list);
     }

+ 2 - 2
fs-doctor-app/src/main/resources/application.yml

@@ -5,5 +5,5 @@ server:
 # Spring配置
 spring:
   profiles:
-#    active: dev
-    active: dev-yjb
+    active: dev
+#    active: dev-yjb

+ 94 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -2,6 +2,7 @@ package com.fs.live.websocket.service;
 
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.redis.RedisCacheT;
@@ -23,7 +24,10 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
+import com.fs.course.domain.FsCourseQuestionBank;
+import com.fs.course.service.IFsCourseQuestionBankService;
 import com.fs.live.domain.*;
+import com.fs.live.mapper.LiveCourseQuestionRelMapper;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.newAdv.service.ILeadService;
@@ -626,12 +630,102 @@ public class WebSocketServer {
                         delAutoTask(liveId, DateUtils.parseDate(msg.getData(),"yyyy-MM-dd'T'HH:mm:ss.SSSZ").getTime());
                     }
                     break;
+                case "liveQuizStart":
+                    if (userType == 1) {
+                        processLiveQuizStart(liveId, msg);
+                    }
+                    break;
+                case "liveQuizClose":
+                    if (userType == 1) {
+                        processLiveQuizClose(liveId, msg);
+                    }
+                    break;
             }
         } catch (Exception e) {
             log.error("webSocket 消息处理失败 msg: {}", e.getMessage(), e);
         }
     }
 
+    /**
+     * 管理端:向直播间观众广播「开始答题」及题目内容(不含正确答案,仅选项文案)
+     */
+    private void processLiveQuizStart(long liveId, SendMsgVo msg) {
+        try {
+            if (StringUtils.isEmpty(msg.getData())) {
+                return;
+            }
+            JSONObject body = JSON.parseObject(msg.getData());
+            Long relId = body.getLong("relId");
+            if (relId == null) {
+                return;
+            }
+            LiveCourseQuestionRelMapper relMapper = SpringUtils.getBean(LiveCourseQuestionRelMapper.class);
+            Long questionBankId = relMapper.selectQuestionBankIdByLiveAndRel(liveId, relId);
+            if (questionBankId == null) {
+                log.warn("liveQuizStart: 未找到关联 liveId={} relId={}", liveId, relId);
+                return;
+            }
+            IFsCourseQuestionBankService bankService = SpringUtils.getBean(IFsCourseQuestionBankService.class);
+            FsCourseQuestionBank bank = bankService.selectFsCourseQuestionBankById(questionBankId);
+            if (bank == null) {
+                return;
+            }
+            JSONArray optionsOut = new JSONArray();
+            String qStr = bank.getQuestion();
+            if (StringUtils.isNotEmpty(qStr)) {
+                JSONArray arr = JSON.parseArray(qStr);
+                String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+                for (int i = 0; i < arr.size(); i++) {
+                    JSONObject opt = arr.getJSONObject(i);
+                    JSONObject row = new JSONObject();
+                    row.put("key", i < alphabet.length() ? String.valueOf(alphabet.charAt(i)) : String.valueOf(i + 1));
+                    row.put("name", opt.getString("name"));
+                    optionsOut.add(row);
+                }
+            }
+            JSONObject data = new JSONObject();
+            data.put("relId", relId);
+            data.put("questionBankId", bank.getId());
+            data.put("title", bank.getTitle());
+            data.put("type", bank.getType());
+            data.put("options", optionsOut);
+            SendMsgVo out = new SendMsgVo();
+            out.setLiveId(liveId);
+            out.setUserType(1L);
+            out.setCmd("liveQuizStart");
+            out.setOn(true);
+            out.setData(data.toJSONString());
+            enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", out)), true);
+        } catch (Exception e) {
+            log.error("liveQuizStart 处理异常 liveId={}", liveId, e);
+        }
+    }
+
+    /**
+     * 管理端:广播「结束答题」
+     */
+    private void processLiveQuizClose(long liveId, SendMsgVo msg) {
+        try {
+            JSONObject data = new JSONObject();
+            if (StringUtils.isNotEmpty(msg.getData())) {
+                JSONObject body = JSON.parseObject(msg.getData());
+                Long relId = body.getLong("relId");
+                if (relId != null) {
+                    data.put("relId", relId);
+                }
+            }
+            SendMsgVo out = new SendMsgVo();
+            out.setLiveId(liveId);
+            out.setUserType(1L);
+            out.setCmd("liveQuizClose");
+            out.setOn(true);
+            out.setData(data.toJSONString());
+            enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", out)), true);
+        } catch (Exception e) {
+            log.error("liveQuizClose 处理异常 liveId={}", liveId, e);
+        }
+    }
+
     private void deleteMsg(long liveId,SendMsgVo msg) {
         SendMsgVo sendMsgVo = new SendMsgVo();
         sendMsgVo.setLiveId(liveId);

+ 23 - 4
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -2,10 +2,14 @@ package com.fs.app.controller;
 
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
+import com.fs.aiSoundReplication.service.AsrService;
+import com.fs.app.msgarchives.QwMsgAuditIngestService;
+import com.fs.app.msgarchives.QwMsgMediaFileService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.uuid.IdUtils;
+import com.fs.enums.MediaMsgTypeEnum;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.AiHookService;
 import com.fs.fastGpt.service.IFastGptRoleService;
@@ -15,10 +19,14 @@ import com.fs.his.dto.TracesDTO;
 import com.fs.his.enums.ShipperCodeEnum;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsStoreOrderService;
+import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.domain.QwUserVideo;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwMsgAuditMessageMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwExternalContactService;
 import com.fs.qw.service.IQwUserService;
@@ -79,6 +87,17 @@ public class QwMsgController {
     private IFsExpressService expressService;
     @Autowired
     private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private QwMsgAuditIngestService ingestService;
+    @Autowired
+    private QwCompanyMapper qwCompanyMapper;
+
+    @Autowired
+    private QwMsgAuditMessageMapper messageMapper;
+    @Autowired
+    private QwMsgMediaFileService mediaFileService;
+    @Autowired
+    private AsrService asrService;
 
     @GetMapping("/sendExpressInfo/{orderId}")
     public R sendExpressInfo(@PathVariable Long orderId){
@@ -196,9 +215,10 @@ public class QwMsgController {
     }
 
 
-    @PostMapping("/callback/{serverId}")
+    @PostMapping("/callback/{serverId}/{isNewVersion}")
     @ResponseBody
-    public Map<String,String> callback(@RequestBody String json,@PathVariable Long serverId ){
+    public Map<String,String> callback(@RequestBody String json, @PathVariable Long serverId, @PathVariable(required = false) Boolean isNewVersion){
+
       //  System.out.println(json);
         WxWorkMsgResp wxWorkMsgResp = JSON.parseObject(json, WxWorkMsgResp.class);
         Integer type = wxWorkMsgResp.getType();
@@ -351,7 +371,7 @@ public class QwMsgController {
 
                     if (2000000000000000L-receiver>0){
                         log.info("id:{}, 客户发送", id);
-                        aiHookService.qwHookNotifyAiReply(id,sender,content,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype());
+                        aiHookService.qwHookNotifyAiReply(id,sender,content,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(), isNewVersion);
                     }else {
                         log.info("销售发送");
                         aiHookService.qwHookNotifyAddMsgNew(id,receiver,content,wxWorkMsgResp.getUuid(),1);
@@ -562,5 +582,4 @@ public class QwMsgController {
             qwUserService.atMsg(qwUser1, "掉线提醒("+msg+")");
         }
     }
-
 }

+ 43 - 0
fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java

@@ -0,0 +1,43 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 任务与外呼sip任务关联关系对象 company_siptask_info
+ *
+ * @author fs
+ * @date 2026-04-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanySiptaskInfo extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** 工作流id */
+    @Excel(name = "工作流id")
+    private Long workflowId;
+
+    /** 任务id */
+    @Excel(name = "任务id")
+    private Long taskId;
+
+    /** 节点key */
+    @Excel(name = "节点key")
+    private String nodeKey;
+
+    /** 对应sip任务id */
+    @Excel(name = "对应sip任务id")
+    private Long batchId;
+
+    /** sip外呼任务 */
+    @Excel(name = "sip外呼任务")
+    private String taskJson;
+
+
+}

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

@@ -8,7 +8,7 @@ import lombok.Data;
 
 /**
  * 任务外呼电话对象 company_voice_robotic_callees
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
@@ -67,4 +67,8 @@ public class CompanyVoiceRoboticCallees{
 
     //是否生成数据(0否,1是)
     private Integer isGenerate;
+
+    /** 本任务下该 callee 的 AI 外呼呼出次数(非表字段,来自 call_log 统计) */
+    @TableField(exist = false)
+    private Integer roboticCallOutCount;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWxAccount.java

@@ -101,4 +101,9 @@ public class CompanyWxAccount extends BaseEntity
 
     @TableField(exist = false)
     private String companyUserName;
+
+    /**
+     * 微信备注(唯一)
+     */
+    private String wxRemark;
 }

+ 64 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java

@@ -0,0 +1,64 @@
+package com.fs.company.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanySiptaskInfo;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 任务与外呼sip任务关联关系Mapper接口
+ * 
+ * @author fs
+ * @date 2026-04-20
+ */
+public interface CompanySiptaskInfoMapper extends BaseMapper<CompanySiptaskInfo>{
+    /**
+     * 查询任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 任务与外呼sip任务关联关系
+     */
+    CompanySiptaskInfo selectCompanySiptaskInfoById(Long id);
+
+    /**
+     * 查询任务与外呼sip任务关联关系列表
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 任务与外呼sip任务关联关系集合
+     */
+    List<CompanySiptaskInfo> selectCompanySiptaskInfoList(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 新增任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int insertCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 修改任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int updateCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 删除任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoById(Long id);
+
+    /**
+     * 批量删除任务与外呼sip任务关联关系
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoByIds(Long[] ids);
+
+    CompanySiptaskInfo selectSipTaskInfoByTaskIdAndNodeKey(@Param("taskId") Long taskId, @Param("nodeKey") String nodeKey);
+}

+ 22 - 7
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -4,20 +4,22 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.CustomerRoboticCallOutCountVO;
 import org.apache.ibatis.annotations.Param;
 
 /**
  * 调用日志_ai打电话Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
 public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<CompanyVoiceRoboticCallLogCallphone>{
     /**
      * 查询调用日志_ai打电话
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 调用日志_ai打电话
      */
@@ -25,7 +27,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 查询调用日志_ai打电话列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 调用日志_ai打电话集合
      */
@@ -33,7 +35,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 新增调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -41,7 +43,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 修改调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -49,7 +51,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 删除调用日志_ai打电话
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 结果
      */
@@ -57,7 +59,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 批量删除调用日志_ai打电话
-     * 
+     *
      * @param logIds 需要删除的数据主键集合
      * @return 结果
      */
@@ -92,4 +94,17 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      * @return 公司ID
      */
     Long selectCompanyIdByBusinessId(@Param("businessId") Long businessId);
+
+    /**
+     * 按客户统计 AI 外呼呼出次数(callphone.caller_id = callees.id,callees.user_id = 客户 customer_id)
+     */
+    List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(@Param("customerIds") List<Long> customerIds,
+                                                                         @Param("companyId") Long companyId);
+
+    /**
+     * 按外呼任务 callee 统计呼出次数(callphone.caller_id = callees.id,且限定当前任务 robotic_id)
+     */
+    List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(@Param("calleeIds") List<Long> calleeIds,
+                                                                     @Param("roboticId") Long roboticId,
+                                                                     @Param("companyId") Long companyId);
 }

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

@@ -67,4 +67,6 @@ public interface CompanyWxAccountMapper extends BaseMapper<CompanyWxAccount> {
     List<CompanyUser> companyListAllCompany(CompanyUser companyUser);
 
     CompanyWxAccount selectByCompanyUserAndWxNo(@Param("userId") Long userId, @Param("wxNo") String wxNo);
+
+    CompanyWxAccount selectCompanyWxAccountByWxRemark(@Param("wxRemark") String wxRemark);
 }

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

@@ -85,4 +85,6 @@ public interface CompanyWxClientMapper extends BaseMapper<CompanyWxClient> {
     List<CompanyWxClient> getQwAddWxList(@Param("accountIdList") List<Long> accountIdList, @Param("isWeCom") Integer isWeCom);
 
     List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(@Param("accountIdList") List<Long> accountIdList, @Param("execStatus") Integer execStatus, @Param("execNodeType") Integer execNodeType, @Param("cidGroupNo") Integer cidGroupNo);
+
+    List<CompanyWxClient> selectWxV2(@Param("id") Long id, @Param("phone") String phone);
 }

+ 162 - 0
fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java

@@ -0,0 +1,162 @@
+package com.fs.company.mapper;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.vo.easycall.EasyCallBizGroupVO;
+import com.fs.company.vo.easycall.EasyCallGatewayVO;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import com.fs.company.vo.easycall.EasyCallIvrVO;
+import com.fs.company.vo.easycall.EasyCallLlmAccountVO;
+import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 呼入大模型配置 Mapper接口
+ *
+ * @author fs
+ */
+@Repository
+public interface EasyCallInboundLlmMapper {
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallInboundLlmVO selectInboundLlmById(Integer id);
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo);
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int insertInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int updateInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteInboundLlmById(Integer id);
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID数组
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteInboundLlmByIds(String[] ids);
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee);
+
+    /**
+     * 查询大模型账户列表
+     *
+     * @return 大模型账户列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallLlmAccountVO> selectLlmAccountList();
+
+    /**
+     * 根据ID查询大模型账户
+     *
+     * @param id 大模型账户ID
+     * @return 大模型账户信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallLlmAccountVO selectLlmAccountById(Integer id);
+
+    /**
+     * 根据voiceCode查询音色信息
+     *
+     * @param voiceCode 音色编号
+     * @return 音色信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallVoiceCodeVO selectVoiceCodeByCode(String voiceCode);
+
+    /**
+     * 查询ASR提供商列表
+     *
+     * @return ASR提供商列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<Map<String, String>> selectAsrProviderList();
+
+    /**
+     * 查询TTS音色来源列表
+     *
+     * @return TTS音色来源列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<Map<String, String>> selectVoiceSourceList();
+
+    /**
+     * 根据音色来源查询音色列表
+     *
+     * @param voiceSource 音色来源
+     * @return 音色列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallVoiceCodeVO> selectVoiceListBySource(String voiceSource);
+
+    /**
+     * 查询业务组列表
+     *
+     * @return 业务组列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallBizGroupVO> selectBizGroupList();
+
+    /**
+     * 查询出局网关列表
+     *
+     * @return 网关列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallGatewayVO> selectOutboundGatewayList();
+
+    /**
+     * 查询IVR列表
+     *
+     * @return IVR列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallIvrVO> selectIvrList();
+}

+ 7 - 0
fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java

@@ -62,4 +62,11 @@ public interface CompanyWorkflowEngine {
      * @param inputData
      */
     void timeDoExecute(String workflowInstanceId, String nodeKey, Map<String, Object> inputData);
+
+    /**
+     * 创建sip任务
+     * @param roboticId
+     * @param workFlowId
+     */
+    Long createSipTask(Long roboticId,Long workFlowId);
 }

+ 69 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java

@@ -0,0 +1,69 @@
+package com.fs.company.service;
+
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+
+import java.util.List;
+
+/**
+ * 呼入大模型配置 Service接口
+ *
+ * @author fs
+ */
+public interface ICompanyInboundCallManageService {
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    EasyCallInboundLlmVO selectInboundLlmById(Integer id);
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo);
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee);
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    int insertInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    int updateInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    int deleteInboundLlmById(Integer id);
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID字符串,逗号分隔
+     * @return 影响行数
+     */
+    int deleteInboundLlmByIds(String ids);
+}

+ 61 - 0
fs-service/src/main/java/com/fs/company/service/ICompanySiptaskInfoService.java

@@ -0,0 +1,61 @@
+package com.fs.company.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanySiptaskInfo;
+
+/**
+ * 任务与外呼sip任务关联关系Service接口
+ * 
+ * @author fs
+ * @date 2026-04-20
+ */
+public interface ICompanySiptaskInfoService extends IService<CompanySiptaskInfo>{
+    /**
+     * 查询任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 任务与外呼sip任务关联关系
+     */
+    CompanySiptaskInfo selectCompanySiptaskInfoById(Long id);
+
+    /**
+     * 查询任务与外呼sip任务关联关系列表
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 任务与外呼sip任务关联关系集合
+     */
+    List<CompanySiptaskInfo> selectCompanySiptaskInfoList(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 新增任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int insertCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 修改任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int updateCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 批量删除任务与外呼sip任务关联关系
+     * 
+     * @param ids 需要删除的任务与外呼sip任务关联关系主键集合
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoByIds(Long[] ids);
+
+    /**
+     * 删除任务与外呼sip任务关联关系信息
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoById(Long id);
+}

+ 13 - 7
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java

@@ -4,19 +4,21 @@ import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.CustomerRoboticCallOutCountVO;
 
 /**
  * 调用日志_ai打电话Service接口
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
 public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<CompanyVoiceRoboticCallLogCallphone>{
     /**
      * 查询调用日志_ai打电话
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 调用日志_ai打电话
      */
@@ -24,7 +26,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 查询调用日志_ai打电话列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 调用日志_ai打电话集合
      */
@@ -32,7 +34,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 新增调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -40,7 +42,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 修改调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -48,7 +50,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 批量删除调用日志_ai打电话
-     * 
+     *
      * @param logIds 需要删除的调用日志_ai打电话主键集合
      * @return 结果
      */
@@ -56,7 +58,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 删除调用日志_ai打电话信息
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 结果
      */
@@ -70,6 +72,10 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
      */
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallLogCallphoneListData(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
+    List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId);
+
+    List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(List<Long> calleeIds, Long roboticId, Long companyId);
+
     /**
      * 查询 calleesIds
      * @param customerId

+ 99 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java

@@ -0,0 +1,99 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.core.text.Convert;
+import com.fs.company.mapper.EasyCallInboundLlmMapper;
+import com.fs.company.service.ICompanyInboundCallManageService;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 呼入大模型配置 Service业务层处理
+ *
+ * @author fs
+ */
+@Service
+public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallManageService {
+
+    @Autowired
+    private EasyCallInboundLlmMapper inboundLlmMapper;
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    @Override
+    public EasyCallInboundLlmVO selectInboundLlmById(Integer id) {
+        return inboundLlmMapper.selectInboundLlmById(id);
+    }
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    @Override
+    public List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo) {
+        return inboundLlmMapper.selectInboundLlmList(vo);
+    }
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    @Override
+    public List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee) {
+        return inboundLlmMapper.selectInboundLlmByCallee(callee);
+    }
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @Override
+    public int insertInboundLlm(EasyCallInboundLlmVO vo) {
+        return inboundLlmMapper.insertInboundLlm(vo);
+    }
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @Override
+    public int updateInboundLlm(EasyCallInboundLlmVO vo) {
+        return inboundLlmMapper.updateInboundLlm(vo);
+    }
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    @Override
+    public int deleteInboundLlmById(Integer id) {
+        return inboundLlmMapper.deleteInboundLlmById(id);
+    }
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID字符串,逗号分隔
+     * @return 影响行数
+     */
+    @Override
+    public int deleteInboundLlmByIds(String ids) {
+        return inboundLlmMapper.deleteInboundLlmByIds(Convert.toStrArray(ids));
+    }
+}

+ 91 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanySiptaskInfoServiceImpl.java

@@ -0,0 +1,91 @@
+package com.fs.company.service.impl;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.company.mapper.CompanySiptaskInfoMapper;
+import com.fs.company.domain.CompanySiptaskInfo;
+import com.fs.company.service.ICompanySiptaskInfoService;
+
+/**
+ * 任务与外呼sip任务关联关系Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-04-20
+ */
+@Service
+public class CompanySiptaskInfoServiceImpl extends ServiceImpl<CompanySiptaskInfoMapper, CompanySiptaskInfo> implements ICompanySiptaskInfoService {
+
+    /**
+     * 查询任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 任务与外呼sip任务关联关系
+     */
+    @Override
+    public CompanySiptaskInfo selectCompanySiptaskInfoById(Long id)
+    {
+        return baseMapper.selectCompanySiptaskInfoById(id);
+    }
+
+    /**
+     * 查询任务与外呼sip任务关联关系列表
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 任务与外呼sip任务关联关系
+     */
+    @Override
+    public List<CompanySiptaskInfo> selectCompanySiptaskInfoList(CompanySiptaskInfo companySiptaskInfo)
+    {
+        return baseMapper.selectCompanySiptaskInfoList(companySiptaskInfo);
+    }
+
+    /**
+     * 新增任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    @Override
+    public int insertCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo)
+    {
+        return baseMapper.insertCompanySiptaskInfo(companySiptaskInfo);
+    }
+
+    /**
+     * 修改任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    @Override
+    public int updateCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo)
+    {
+        return baseMapper.updateCompanySiptaskInfo(companySiptaskInfo);
+    }
+
+    /**
+     * 批量删除任务与外呼sip任务关联关系
+     * 
+     * @param ids 需要删除的任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanySiptaskInfoByIds(Long[] ids)
+    {
+        return baseMapper.deleteCompanySiptaskInfoByIds(ids);
+    }
+
+    /**
+     * 删除任务与外呼sip任务关联关系信息
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanySiptaskInfoById(Long id)
+    {
+        return baseMapper.deleteCompanySiptaskInfoById(id);
+    }
+}

+ 60 - 13
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -5,7 +5,6 @@ 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,7 +15,6 @@ import com.fs.aicall.domain.apiresult.Notify;
 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;
@@ -25,18 +23,14 @@ import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.*;
 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.*;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
 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;
@@ -77,6 +71,8 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Autowired
     QwUserMapper qwUserMapper;
     @Autowired
+    private ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+    @Autowired
     @Qualifier("cidWorkFlowExecutor")
     private Executor cidWorkFlowExecutor;
 
@@ -252,7 +248,7 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
 
                 if (StringUtils.isNotBlank(notify.getUserData())) {
-                    JSONObject userData = JSONObject.parseObject(redisCache2.getCacheObject(WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData()), JSONObject.class);
+                    JSONObject userData = parseRedisCacheToJsonObject(redisCache2.getCacheObject(WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData()));
                     if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
                         Map<String, Object> param = new HashMap<>();
                         param.put("callBackUuid", userData.getString("callBackUuid"));
@@ -313,16 +309,32 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 companyVoiceRoboticCallLog.setCallCreateTime(createTime);
                 Long answerTime = result.getCallEndTime();
                 companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                String intention = result.getIntent();
+                String intention = null;
+                if (StringUtils.isNotBlank(result.getDialogue())) {
+                    try {
+                        intention = crmCustomerAnalyzeService.aiIntentionDegree(
+                                result.getDialogue(),
+                                java.time.LocalTime.now().getLong(java.time.temporal.ChronoField.MILLI_OF_SECOND)
+                        );
+                    } catch (Exception e) {
+                        log.error("easyCall回调日志意向度AI解析失败,uuid={}", result.getUuid(), e);
+                    }
+                }
+                // 历史第三方值(当前不启用,保留用于回滚)
+                // String intention = result.getIntent();
                 String intentf = null;
+                final String intentionLabel = intention;
                 List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
-                if (!isPositiveInteger(intention)) {
-                    Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intention)).findFirst();
+                if (!isPositiveInteger(intentionLabel)) {
+                    Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intentionLabel)).findFirst();
                     if (firstDict.isPresent()) {
                         SysDictData sysDictData = firstDict.get();
                         intentf = sysDictData.getDictValue();
                     }
+                } else {
+                    intentf = intentionLabel;
                 }
+                if (StringUtils.isBlank(intentf)) intentf = "0";
                 companyVoiceRoboticCallLog.setIntention(intentf);
                 companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
                 BigDecimal callCharge = cidConfigVO.getCallCharge();
@@ -340,7 +352,7 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
 
                 if (StringUtils.isNotBlank(result.getBizJson())) {
                     JSONObject bizJson = JSONObject.parseObject(result.getBizJson());
-                    JSONObject userData = JSONObject.parseObject(redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid")), JSONObject.class);
+                    JSONObject userData = parseRedisCacheToJsonObject(redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid")));
                     if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
                         Map<String, Object> param = new HashMap<>();
                         param.put("callBackUuid", userData.getString("callBackUuid"));
@@ -380,6 +392,41 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
         return result;
     }
 
+    @Override
+    public List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return baseMapper.countRoboticCallOutByCustomerIds(customerIds, companyId);
+    }
+
+    @Override
+    public List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(List<Long> calleeIds, Long roboticId, Long companyId) {
+        if (calleeIds == null || calleeIds.isEmpty() || roboticId == null) {
+            return Collections.emptyList();
+        }
+        return baseMapper.countRoboticCallOutByCalleeIds(calleeIds, roboticId, companyId);
+    }
+    /**
+     * Redis 中 workflow 回调缓存可能是 String 或已反序列化的 JSONObject,避免 ClassCastException。
+     */
+    private static JSONObject parseRedisCacheToJsonObject(Object cacheObj) {
+        if (cacheObj == null) {
+            return null;
+        }
+        if (cacheObj instanceof JSONObject) {
+            return (JSONObject) cacheObj;
+        }
+        if (cacheObj instanceof String) {
+            String s = (String) cacheObj;
+            if (StringUtils.isBlank(s)) {
+                return null;
+            }
+            return JSONObject.parseObject(s);
+        }
+        return JSONObject.parseObject(JSONObject.toJSONString(cacheObj));
+    }
+
     @Override
     public List<Long> getCallerIdsByCustomerId(Long customerId) {
         return companyVoiceRoboticCalleesMapper.getCallerIdsByCustomerId(customerId);

+ 144 - 26
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -30,6 +30,7 @@ import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.crm.param.SmsSendBatchParam;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -47,12 +48,15 @@ import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
+
+import java.time.temporal.ChronoField;
 import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static com.fs.company.service.impl.call.node.AbstractWorkflowNode.companyVoiceRoboticCallLogCallphoneMapper;
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
+import static java.time.LocalTime.now;
 
 
 /**
@@ -84,6 +88,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final CompanyVoiceRoboticWxServiceImpl companyVoiceRoboticWxService;
 
     private final CrmCustomerMapper crmCustomerMapper;
+    private final ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
 
     private final CompanyWxClientServiceImpl companyWxClientServiceImpl;
 
@@ -125,6 +130,24 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     /** 每次重试等待时长(毫秒) */
     private static final long EASYCALL_INTENT_RETRY_INTERVAL_MS = 30000L;
 
+    /** EasyCall dialogue 对话内容重试队列 Redis key 前缀,value 为已重试次数 */
+    private static final String EASYCALL_DIALOGUE_RETRY_KEY = "easycall:dialogue:retry:";
+    /** dialogue 对话内容等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int EASYCALL_DIALOGUE_MAX_RETRY = 5;
+    /** dialogue 每次重试等待时长(毫秒) */
+    private static final long EASYCALL_DIALOGUE_RETRY_INTERVAL_MS = 30000L;
+
+    /**
+     * 判断 dialogue 对话内容是否为空(null、空字符串、空数组 "[]" 均视为无对话内容)
+     */
+    private boolean isDialogueEmpty(String dialogue) {
+        if (StringUtils.isBlank(dialogue)) {
+            return true;
+        }
+        String trimmed = dialogue.trim();
+        return "[]".equals(trimmed);
+    }
+
     /**
      * 查询机器人外呼任务
      *
@@ -822,11 +845,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     @Override
     @Async("cidWorkFlowExecutor")
     public void callerResult4EasyCall(CdrDetailVo result) {
-        try {
-            Thread.sleep(20000L);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        }
+//        try {
+//            Thread.sleep(3000L);
+//        } catch (InterruptedException e) {
+//            throw new RuntimeException(e);
+//        }
 //        EASYCALL
         log.info("进入easyCall外呼结果回调:{}", JSON.toJSONString(result));
         if (result == null || StringUtils.isBlank(result.getUuid())) return;
@@ -843,6 +866,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return;
         }
         // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
+        /*
         if (StringUtils.isBlank(callPhoneRes.getIntent())) {
             String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
             Integer retryCount = redisCache2.getCacheObject(retryKey);
@@ -864,6 +888,76 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         // intent 已有值,直接正常处理
         redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
         doHandleEasyCallResult(callPhoneRes);
+        */
+
+        // 当前:根据对话内容同步调用自家 AI 计算意向度,不依赖第三方 intent
+//        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+
+        // dialogue(对话内容)由对方异步写入,回调时可能尚未赋值,进入延迟重试队列等待
+        if (isDialogueEmpty(callPhoneRes.getDialogue())) {
+            String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = 0;
+            }
+            if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall外呼回调dialogue对话内容暂未写入,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+                doRetryDialogue4EasyCall(result, retryCount + 1);
+            } else {
+                // 超过最大重试次数,以 dialogue 为空兜底继续处理
+                log.warn("easyCall外呼回调dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // dialogue 已有值,直接正常处理
+        redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 延迟重试处理 EasyCall 外呼回调(等待 dialogue 对话内容异步写入完成)
+     * 每次重试前等待 {@link #EASYCALL_DIALOGUE_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryDialogue4EasyCall(CdrDetailVo result, int currentRetry) {
+        try {
+            Thread.sleep(EASYCALL_DIALOGUE_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("easyCall dialogue重试等待被中断, uuid={}", result.getUuid());
+            return;
+        }
+        log.info("easyCall dialogue重试第{}次开始, uuid={}", currentRetry, result.getUuid());
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if (null == callPhoneRes) {
+            log.error("easyCall dialogue重试时仍未查询到外呼结果, uuid={}", result.getUuid());
+            return;
+        }
+        if (isDialogueEmpty(callPhoneRes.getDialogue())) {
+            // dialogue 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall dialogue对话内容仍未写入,uuid={},第{}次继续延迟重试", result.getUuid(), retryCount + 1);
+                doRetryDialogue4EasyCall(result, retryCount + 1);
+            } else {
+                log.warn("easyCall dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // dialogue 已写入完成,正常处理
+        log.info("easyCall dialogue重试第{}次成功获取到对话内容,uuid={}", currentRetry, result.getUuid());
+        redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
     }
 
     /**
@@ -916,7 +1010,15 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private void doHandleEasyCallResult(EasyCallCallPhoneVO callPhoneRes) {
         //等待数据信息
         JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
-        String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
+        Object cacheObj = redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
+        String cacheString;
+        if (cacheObj instanceof String) {
+            cacheString = (String) cacheObj;
+        } else if (cacheObj instanceof JSONObject) {
+            cacheString = ((JSONObject) cacheObj).toJSONString();
+        } else {
+            cacheString = cacheObj == null ? null : JSONObject.toJSONString(cacheObj);
+        }
         if (StringUtils.isBlank(cacheString)) {
             log.error("easyCall外呼回调缓存信息缺失, uuid={}", callPhoneRes.getUuid());
             return;
@@ -974,7 +1076,23 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     public void pushDialogContent4EasyCall(JSONObject cacheInfo, EasyCallCallPhoneVO callPhoneRes) {
 
-        String intention = getIntention(callPhoneRes.getIntent());
+        String intention = null;
+        String intentionDegree = null;
+        if (StringUtils.isNotBlank(callPhoneRes.getDialogue())) {
+            log.info("【验证】意向度来源=自家AI, uuid={}, dialogueLength={}", callPhoneRes.getUuid(),
+                    StringUtils.isBlank(callPhoneRes.getDialogue()) ? 0 : callPhoneRes.getDialogue().length());
+            try {
+                intentionDegree = crmCustomerAnalyzeService.aiIntentionDegree(
+                        callPhoneRes.getDialogue(),
+                        now().getLong(ChronoField.MILLI_OF_SECOND)
+                );
+                log.info("【验证】意向度结果={}, uuid={}", intentionDegree, callPhoneRes.getUuid());
+                intention = getIntention(intentionDegree);
+            } catch (Exception e) {
+                log.error("easyCall意向度AI解析失败,uuid={},将使用意向未知兜底", callPhoneRes.getUuid(), e);
+            }
+        }
+        // 2) 最终兜底:意向未知
         if (StringUtils.isEmpty(intention)) {
             intention = "0";
         }
@@ -1547,25 +1665,25 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
 
-        if (!businessIds.isEmpty()) {
-            List<CompanyVoiceRoboticBusiness> businesses = companyVoiceRoboticBusinessMapper.selectList(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()
-                    .in(CompanyVoiceRoboticBusiness::getId, businessIds));
-            if (ObjectUtil.isNotEmpty(businesses)) {
-                Map<Long, CompanyVoiceRoboticBusiness> businessMap = businesses.stream().collect(Collectors.toMap(CompanyVoiceRoboticBusiness::getId, Function.identity()));
-                records.forEach(record -> {
-                    if (record.getBusinessId() != null && businessMap.containsKey(record.getBusinessId())) {
-                        CompanyVoiceRoboticBusiness business = businessMap.get(record.getBusinessId());
-                        CompanyVoiceRoboticCallLogCallphone callLogCallphone = companyVoiceRoboticCallLogCallphoneMapper.selectOne(new LambdaQueryWrapper<CompanyVoiceRoboticCallLogCallphone>()
-                                .eq(CompanyVoiceRoboticCallLogCallphone::getRoboticId, business.getRoboticId())
-                                .eq(CompanyVoiceRoboticCallLogCallphone::getCallerId, business.getCalleeId()));
-                        if (ObjectUtil.isNotEmpty(callLogCallphone)) {
-                            record.setContentList(callLogCallphone.getContentList());
-                            record.setIntention(callLogCallphone.getIntention());
-                        }
-                    }
-                });
-            }
-        }
+//        if (!businessIds.isEmpty()) {
+//            List<CompanyVoiceRoboticBusiness> businesses = companyVoiceRoboticBusinessMapper.selectList(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()
+//                    .in(CompanyVoiceRoboticBusiness::getId, businessIds));
+//            if (ObjectUtil.isNotEmpty(businesses)) {
+//                Map<Long, CompanyVoiceRoboticBusiness> businessMap = businesses.stream().collect(Collectors.toMap(CompanyVoiceRoboticBusiness::getId, Function.identity()));
+//                records.forEach(record -> {
+//                    if (record.getBusinessId() != null && businessMap.containsKey(record.getBusinessId())) {
+//                        CompanyVoiceRoboticBusiness business = businessMap.get(record.getBusinessId());
+//                        CompanyVoiceRoboticCallLogCallphone callLogCallphone = companyVoiceRoboticCallLogCallphoneMapper.selectOne(new LambdaQueryWrapper<CompanyVoiceRoboticCallLogCallphone>()
+//                                .eq(CompanyVoiceRoboticCallLogCallphone::getRoboticId, business.getRoboticId())
+//                                .eq(CompanyVoiceRoboticCallLogCallphone::getCallerId, business.getCalleeId()));
+//                        if (ObjectUtil.isNotEmpty(callLogCallphone)) {
+//                            record.setContentList(callLogCallphone.getContentList());
+//                            record.setIntention(callLogCallphone.getIntention());
+//                        }
+//                    }
+//                });
+//            }
+//        }
 
         if (!instanceIds.isEmpty()) {
             List<CompanyAiWorkflowExecLog> allLogs = companyAiWorkflowExecLogMapper.selectByInstanceIds(instanceIds);

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

@@ -1,23 +1,22 @@
 package com.fs.company.service.impl;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
-import com.fs.company.domain.CompanyAiWorkflowExec;
-import com.fs.company.domain.CompanyAiWorkflowExecLog;
-import com.fs.company.domain.CompanyWorkflow;
-import com.fs.company.domain.CompanyWorkflowNode;
-import com.fs.company.mapper.CompanyAiWorkflowExecLogMapper;
-import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
-import com.fs.company.mapper.CompanyWorkflowMapper;
-import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.domain.*;
+import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.service.IWorkflowNode;
+import com.fs.company.service.easycall.IEasyCallService;
 import com.fs.company.service.impl.call.node.WorkflowNodeFactory;
+import com.fs.company.vo.AiCallConfigVO;
 import com.fs.company.vo.ExecutionResult;
+import com.fs.company.vo.easycall.EasyCallCreateTaskParam;
+import com.fs.company.vo.easycall.EasyCallTaskVO;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import lombok.extern.slf4j.Slf4j;
@@ -60,6 +59,15 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
     @Autowired
     private CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
 
+    @Autowired
+    CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
+
+    @Autowired
+    IEasyCallService easyCallService;
+
+    @Autowired
+    CompanySiptaskInfoMapper companySiptaskInfoMapper;
+
     /**
      * 初始化工作流
      * 创建工作流实例并保存初始状态
@@ -84,7 +92,8 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
                     definition.getStartNodeKey(), context, definition);
 
             log.info("工作流初始化成功: {} -> {}", workflowInstanceId, workflowDefinitionId);
-
+            //为任务创建sip任务并存入表数据
+            createSipTask(Long.parseLong(inputVariables.get("roboticId").toString()),workflowDefinitionId);
             return ExecutionResult.success()
                     .nextNodeKey(definition.getStartNodeKey())
                     .workflowInstanceId(workflowInstanceId).build();
@@ -545,4 +554,59 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
 
     }
 
+    /**
+     * 创建SIP任务
+     * @param roboticId
+     * @param workFlowId
+     */
+    public Long createSipTask(Long roboticId, Long workFlowId) {
+        try {
+            List<String> nodeTypes = Arrays.asList(NodeTypeEnum.AI_CALL_TASK.getCode());
+            CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticId);
+            List<CompanyWorkflowNode> companyWorkflowNodes = companyWorkflowNodeMapper.selectNodesByWorkflowIdAndTypes(workFlowId, nodeTypes);
+            //为所有外呼节点创建任务的对应sip外呼任务
+            for (CompanyWorkflowNode callNode : companyWorkflowNodes) {
+                String nodeConfig = callNode.getNodeConfig();
+                AiCallConfigVO callConfigVo = JSONObject.parseObject(nodeConfig, AiCallConfigVO.class);
+                EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
+                // 任务名称:使用任务名称_工作流id_节点key
+                createParam.setBatchName(robotic.getName() + "_" + workFlowId + "_" + callNode.getNodeKey());
+                if (null != callConfigVo.getMaxConcurrency()) {
+                    createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
+                } else {
+                    createParam.setThreadNum(3L);
+                }
+                // AI 外呼模式
+                createParam.setTaskType(1);
+                // 外呼线路(网关)
+                createParam.setGatewayId(callConfigVo.getGatewayId());
+                // 大模型底座
+                createParam.setLlmAccountId(callConfigVo.getLlmAccountId());
+                // 音色编号
+                createParam.setVoiceCode(callConfigVo.getVoiceCode());
+                // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
+                createParam.setVoiceSource(callConfigVo.getVoiceSource());
+                // 技能组(转人工客服分组,可选)
+                createParam.setGroupId(callConfigVo.getBusiGroupId());
+
+                EasyCallTaskVO task = easyCallService.createTask(createParam, null);
+                if (task == null || task.getBatchId() == null) {
+                    log.error("createSipTask: 创建 EasyCall 任务失败 - workflowInstanceId: {}", workFlowId);
+                    throw new RuntimeException("EasyCallCenter365 创建任务失败");
+                }
+                CompanySiptaskInfo sipTaskInfo = new CompanySiptaskInfo();
+                sipTaskInfo.setTaskId(roboticId);
+                sipTaskInfo.setWorkflowId(workFlowId);
+                sipTaskInfo.setNodeKey(callNode.getNodeKey());
+                sipTaskInfo.setBatchId(task.getBatchId());
+                sipTaskInfo.setTaskJson(JSONObject.toJSONString(task));
+                companySiptaskInfoMapper.insertCompanySiptaskInfo(sipTaskInfo);
+                return task.getBatchId();
+            }
+        } catch (Exception ex) {
+            log.error("创建SIP任务失败:{}", ex);
+        }
+        return null;
+    }
+
 }

+ 30 - 22
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java

@@ -1,5 +1,8 @@
 package com.fs.company.service.impl;
 
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -33,7 +36,10 @@ import com.fs.wxcid.service.*;
 import com.fs.wxcid.service.impl.LoginServiceImpl;
 import com.fs.wxcid.service.impl.UserServiceImpl;
 import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import okhttp3.WebSocketListener;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -101,6 +107,12 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     @Autowired
     CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
 
+    @Value("${wx.websocket.url:ws://localhost:7113/app/webSocket}")
+    private String webSocketUrl;
+
+    @Value("${wx.api.url:http://localhost:7113}")
+    private String wxApiUrl;
+
 
     /**
      * 查询企微账号
@@ -141,6 +153,11 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     @Override
     public int insertCompanyWxAccount(CompanyWxAccount companyWxAccount)
     {
+        //校验添加微信前缀唯一性
+        CompanyWxAccount wxAccount = companyWxAccountMapper.selectCompanyWxAccountByWxRemark(companyWxAccount.getWxRemark());
+        if(null != wxAccount){
+            throw new RuntimeException("微信前缀已存在,请更换后重试");
+        }
         companyWxAccount.setCreateTime(DateUtils.getNowDate());
         return companyWxAccountMapper.insertCompanyWxAccount(companyWxAccount);
     }
@@ -332,37 +349,28 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         account.setSyncFriendTime(LocalDateTime.now());
 
         try {
-            // 3. 清理Redis缓存
-            String key = FRIEND_KEY + accountId;
-            friendListRedis.deleteObject(key);
-
-            // 4. 获取好友列表并过滤
-            GetContactListParam param = new GetContactListParam();
-            param.setAccountId(accountId);
-            ContactListResponse response = friendService.getContactListNotKey(param);
-            List<String> friendList = getFilteredFriendList(response);
-            if (CollectionUtils.isEmpty(friendList)) {
-                log.info("账号暂无需要同步的微信好友/群聊,accountId={}", accountId);
-                updateById(account);
-                return;
+            // 3. 调用WebSocket发送同步指令
+            String wxNo = account.getWxNo();
+            if (wxNo != null && !wxNo.isEmpty()) {
+                sendSyncContactCommand(wxNo);
+            } else {
+                log.warn("微信账号wxNo为空,无法发送同步指令,accountId={}", accountId);
             }
 
-            // 5. 缓存好友列表到Redis
-            friendListRedis.setCacheObject(key, friendList);
-
-            // 6. 同步联系人信息
-            syncContactDetails(accountId, account, friendList);
-
-            // 7. 更新账号信息
+            // 4. 更新账号信息
             updateById(account);
 
-            log.info("微信信息同步完成,accountId={},同步联系人数量={}", accountId, friendList.size());
+//            log.info("微信信息同步完成,accountId={},同步联系人数量={}", accountId, friendList.size());
         } catch (Exception e) {
             log.error("同步微信信息异常,accountId={}", accountId, e);
             throw e;
         }
     }
 
+    private void sendSyncContactCommand(String wxNo) {
+        HttpUtil.get(wxApiUrl + "/app/common/syncWx?wxId=" + wxNo);
+    }
+
     /**
      * 获取过滤后的好友列表
      */
@@ -576,7 +584,7 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
      * 加微成功后触发工作流继续执行
      * @param wxClientId 加微客户ID
      */
-    private void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
+    public void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
         try {
             // 查找等待中的加微工作流实例
             CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(

+ 21 - 0
fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java

@@ -13,6 +13,7 @@ import com.fs.company.service.IWorkflowNode;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
+import com.fs.enums.TaskTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
@@ -426,6 +427,26 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
         companyAiWorkflowExecMapper.updateByWorkflowInstanceId(update);
     }
 
+    /**
+     * 任务完成判定
+     * @param context
+     */
+    public void taskFinish(ExecutionContext context){
+        //判定是否任务完成了更新任务的状态为执行完成
+        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
+        if(null != roboticBusiness){
+            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
+            if(Integer.valueOf(0).equals(i)){
+                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
+                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
+                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+                    robotic.setId(roboticBusiness.getRoboticId());
+                    robotic.setTaskStatus(3);
+                    companyVoiceRoboticMapper.updateById(robotic);
+                }
+            }
+        }
+    }
     public CompanyAiWorkflowExec getWorkflowExec(String workflowInstanceId) {
         CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
         if (null == companyAiWorkflowExec) {

+ 73 - 37
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -6,11 +6,10 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.enums.BusinessTypeEnum;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
-import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyVoiceRoboticCallBlacklistCheckParam;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.service.ICompanyVoiceRoboticCallBlacklistService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.easycall.IEasyCallService;
@@ -27,6 +26,7 @@ import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
+import com.fs.enums.TaskTypeEnum;
 import com.fs.his.config.CidPhoneConfig;
 import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
@@ -45,6 +45,9 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     private static final ICompanyVoiceRoboticService companyVoiceRoboticService = SpringUtils.getBean(ICompanyVoiceRoboticService.class);
     /** EasyCallCenter365 外呼服务 */
     private static final IEasyCallService easyCallService = SpringUtils.getBean(IEasyCallService.class);
+    private static final CompanyConfigMapper companyConfigMapper = SpringUtils.getBean(CompanyConfigMapper.class);
+    private static final CompanySiptaskInfoMapper companySiptaskInfoMapper = SpringUtils.getBean(CompanySiptaskInfoMapper.class);
+    private static final CompanyWorkflowEngine companyWorkflowEngine = SpringUtils.getBean(CompanyWorkflowEngine.class);
     /** 被叫人表 Mapper,用于获取手机号 */
     private static final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper = SpringUtils.getBean(CompanyVoiceRoboticCalleesMapper.class);
     private static final CompanyVoiceRoboticCallLogCallphoneServiceImpl companyVoiceRoboticCallLogCallphoneService = SpringUtils.getBean(CompanyVoiceRoboticCallLogCallphoneServiceImpl.class);
@@ -156,6 +159,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         //如果没有节点可执行通路 条件均不满足 则标记流程为中断
         if(runnableCount < 1){
             super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.INTERRUPT);
+            super.taskFinish(context);
         }
 
         return null;
@@ -260,6 +264,16 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     private void workflowCallPhoneOne4EasyCall(Long roboticId,Long calleeId, ExecutionContext context, AiCallConfigVO callConfigVo) {
 
         CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectById(roboticId);
+        String callBackUrl = "";
+        try{
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(robotic.getCompanyId(), "cId.config");
+            CidPhoneConfig cidConf = JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
+            if(null != cidConf && StringUtils.isNotBlank(cidConf.getCallbackUrl())){
+                callBackUrl = cidConf.getCallbackUrl();
+            }
+        } catch (Exception ex){
+            log.error("获取公司Cid配置失败:{}", ex);
+        }
         // 1. 生成回调唯一标识符,后续回调时通过此 uuid 匹配对应的流程实例
         String callBackUuid = UUID.randomUUID().toString();
         // 将回调信息写入 Redis,保存 1 天
@@ -293,42 +307,44 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         }
 
         // 3. 构建创建任务参数(AI 外呼模式:taskType=1)
-        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
-        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
-        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
-        if (null != callConfigVo.getMaxConcurrency())
-            createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
-        else {
-            createParam.setThreadNum(3L);
-        }
-        // AI 外呼模式
-        createParam.setTaskType(1);
-        // 外呼线路(网关)
-        createParam.setGatewayId(callConfigVo.getGatewayId());
-        // 大模型底座
-        createParam.setLlmAccountId(callConfigVo.getLlmAccountId());
-        // 音色编号
-        createParam.setVoiceCode(callConfigVo.getVoiceCode());
-        // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
-        createParam.setVoiceSource(callConfigVo.getVoiceSource());
-        // 技能组(转人工客服分组,可选)
-        createParam.setGroupId(callConfigVo.getBusiGroupId());
-
-        JSONObject runParam = (JSONObject) JSON.toJSON(createParam);
-        runParam.put("companyId", robotic.getCompanyId());
-        CompanyVoiceRoboticCallLogCallphone addLog = CompanyVoiceRoboticCallLogCallphone.initCallLog(
-                runParam.toJSONString(), calleeId, roboticId, robotic.getCompanyId());
+//        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
+//        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
+//        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
+//        if (null != callConfigVo.getMaxConcurrency())
+//            createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
+//        else {
+//            createParam.setThreadNum(3L);
+//        }
+//        // AI 外呼模式
+//        createParam.setTaskType(1);
+//        // 外呼线路(网关)
+//        createParam.setGatewayId(callConfigVo.getGatewayId());
+//        // 大模型底座
+//        createParam.setLlmAccountId(callConfigVo.getLlmAccountId());
+//        // 音色编号
+//        createParam.setVoiceCode(callConfigVo.getVoiceCode());
+//        // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
+//        createParam.setVoiceSource(callConfigVo.getVoiceSource());
+//        // 技能组(转人工客服分组,可选)
+//        createParam.setGroupId(callConfigVo.getBusiGroupId());
+
+
         // 4. 调用 EasyCallCenter365 创建任务接口
         // companyId 传 null 是因为 EasyCallCenter365 是全局地址,不需要按公司隔离
-        log.info("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务 - workflowInstanceId: {}, calleeId: {}",
-                context.getWorkflowInstanceId(), calleeId);
-        EasyCallTaskVO task = easyCallService.createTask(createParam, null);
-        if (task == null || task.getBatchId() == null) {
-            log.error("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务失败 - workflowInstanceId: {}",
-                    context.getWorkflowInstanceId());
-            throw new RuntimeException("EasyCallCenter365 创建任务失败");
+//        log.info("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务 - workflowInstanceId: {}, calleeId: {}",
+//                context.getWorkflowInstanceId(), calleeId);
+//        EasyCallTaskVO task = easyCallService.createTask(createParam, null);
+//        if (task == null || task.getBatchId() == null) {
+//            log.error("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务失败 - workflowInstanceId: {}",
+//                    context.getWorkflowInstanceId());
+//            throw new RuntimeException("EasyCallCenter365 创建任务失败");
+//        }
+        Long batchId = getTaskBatchId(robotic.getId(), context.getCurrentNodeKey(), context.getWorkflowInstanceId());
+        if(null == batchId ){
+            log.error("workflowCallPhoneOne4EasyCall: 获取 EasyCall 任务批次ID失败 - workflowInstanceId: {}",context.getWorkflowInstanceId());
+            throw new RuntimeException("任务批次ID失败");
         }
-        Long batchId = task.getBatchId();
+
         log.info("workflowCallPhoneOne4EasyCall: EasyCall 任务创建成功 - batchId: {}", batchId);
 
         // 5. 将被叫号码加入任务名单(使用通用追加接口,支持传入业务数据)
@@ -338,7 +354,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         EasyCallPhoneItemVO phoneItem = new EasyCallPhoneItemVO();
         phoneItem.setPhoneNum(callees.getPhone());
         // bizJson 默认传入客户姓名占位,运行时可根据实际业务填充
-        phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid));
+        phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid).fluentPut("callBackUrl",callBackUrl));
         addListParam.setPhoneList(Collections.singletonList(phoneItem));
         easyCallService.addCommonCallList(addListParam, null);
         log.info("workflowCallPhoneOne4EasyCall: 名单追加成功 - batchId: {}, phone: {}",
@@ -347,6 +363,10 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         // 6. 启动外呼任务
         easyCallService.startTask(batchId, null);
         log.info("workflowCallPhoneOne4EasyCall: 任务启动成功 - batchId: {}", batchId);
+        JSONObject runParam = (JSONObject) JSON.toJSON(addListParam);
+        runParam.put("companyId", robotic.getCompanyId());
+        CompanyVoiceRoboticCallLogCallphone addLog = CompanyVoiceRoboticCallLogCallphone.initCallLog(
+                runParam.toJSONString(), calleeId, roboticId, robotic.getCompanyId());
         addLog.setStatus(1);
         addLog.setCallbackUuid(callBackUuid);
         companyVoiceRoboticCallLogCallphoneService.asyncInsertCompanyVoiceRoboticCallLog(addLog);
@@ -374,6 +394,22 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         return false;
     }
 
+    /**
+     * 获取siptaskid
+     * @param roboticId
+     * @param nodeKey
+     * @param workflowInstanceId
+     * @return
+     */
+    public Long getTaskBatchId(Long roboticId, String nodeKey, String workflowInstanceId){
+        CompanySiptaskInfo sipTaskInfo = companySiptaskInfoMapper.selectSipTaskInfoByTaskIdAndNodeKey(roboticId, nodeKey);
+        if(null != sipTaskInfo && null != sipTaskInfo.getBatchId()){
+            return  sipTaskInfo.getBatchId();
+        }
+        //没有的情况下创建任务并返回
+        CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
+        return companyWorkflowEngine.createSipTask(roboticId, companyAiWorkflowExec.getWorkflowId());
+    }
 //    @Override
 //    protected void postExecute(ExecutionContext context, ExecutionResult result) {
 //        super.postExecute(context, result);

+ 15 - 14
fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java

@@ -46,19 +46,20 @@ public class EndNode extends AbstractWorkflowNode {
     protected void postExecute(ExecutionContext context, ExecutionResult result) {
       super.postExecute(context, result);
       super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.SUCCESS);
-      //判定是否任务完成了更新任务的状态为执行完成
-        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
-        if(null != roboticBusiness){
-            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
-            if(Integer.valueOf(0).equals(i)){
-                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
-                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
-                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
-                    robotic.setId(roboticBusiness.getRoboticId());
-                    robotic.setTaskStatus(3);
-                    companyVoiceRoboticMapper.updateById(robotic);
-                }
-            }
-        }
+      super.taskFinish(context);
+//      //判定是否任务完成了更新任务的状态为执行完成
+//        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
+//        if(null != roboticBusiness){
+//            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
+//            if(Integer.valueOf(0).equals(i)){
+//                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
+//                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
+//                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+//                    robotic.setId(roboticBusiness.getRoboticId());
+//                    robotic.setTaskStatus(3);
+//                    companyVoiceRoboticMapper.updateById(robotic);
+//                }
+//            }
+//        }
     }
 }

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

@@ -0,0 +1,14 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * 外呼任务下单个 callee(company_voice_robotic_callees.id)的呼出次数统计
+ */
+@Data
+public class CalleeRoboticCallOutCountVO {
+
+    private Long calleeId;
+
+    private Long callCount;
+}

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

@@ -0,0 +1,14 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * 客户维度 AI 外呼呼出次数统计(按 crm_customer.customer_id,对应 callees.user_id)
+ */
+@Data
+public class CustomerRoboticCallOutCountVO {
+
+    private Long customerId;
+
+    private Long callCount;
+}

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

@@ -0,0 +1,14 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * EasyCallCenter365 业务组VO
+ */
+@Data
+public class EasyCallBizGroupVO {
+    /** 业务组ID */
+    private Long groupId;
+    /** 业务组名称 */
+    private String bizGroupName;
+}

+ 75 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java

@@ -0,0 +1,75 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 呼入大模型配置对象 cc_inbound_llm_account
+ *
+ * @author fs
+ */
+@Data
+@Accessors(chain = true)
+public class EasyCallInboundLlmVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** 呼入别名/名称 */
+    private String inboundAlias;
+
+    /** 大模型底座id */
+    private Integer llmAccountId;
+
+    /** 被叫号码 */
+    private String callee;
+
+    /** TTS voice code */
+    private String voiceCode;
+
+    /** TTS voice source */
+    private String voiceSource;
+
+    /** 服务类型:ai/acd/ivr */
+    private String serviceType;
+
+    /** ASR提供商 */
+    private String asrProvider;
+
+    /** AI转接类型:acd/extension/gateway */
+    private String aiTransferType;
+
+    /** AI转接数据 */
+    private String aiTransferData;
+
+    /** IVR ID */
+    private String ivrId;
+
+    /** 满意度调查IVR ID */
+    private String satisfSurveyIvrId;
+
+    /************ 以下不是表结构字段 ************/
+    /** 大模型底座名称 */
+    private String llmAccountName;
+
+    /** 音色名称 */
+    private String voiceName;
+
+    /** 分组ID */
+    private Integer groupId;
+
+    /** AI转接技能组ID */
+    private String aiTransferGroupId;
+
+    /** AI转接网关ID */
+    private String aiTransferGatewayId;
+
+    /** AI转接网关目标号码 */
+    private String aiTransferGatewayDestNumber;
+
+    /** AI转接分机号 */
+    private String aiTransferExtNumber;
+}

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

@@ -0,0 +1,14 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * EasyCallCenter365 IVR VO
+ */
+@Data
+public class EasyCallIvrVO {
+    /** IVR ID */
+    private Long id;
+    /** IVR节点名称 */
+    private String ivrNodeName;
+}

+ 4 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseWatchComment.java

@@ -67,4 +67,8 @@ public class FsCourseWatchComment extends BaseEntity{
     @Excel(name = "字体颜色")
     private String color;
 
+    /** 分类类型:0-评论(课程),1-公域看课评论 */
+    @ApiModelProperty(value = "分类类型:0-评论,1-公域看课评论")
+    private Integer cateType;
+
 }

+ 21 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCourse.java

@@ -157,4 +157,25 @@ public class FsUserCourse extends BaseEntity
      */
     private String configJson;
 
+    /** 首页课程顶部推荐位:0否 1是 */
+    private Integer recHomeCourseTopEnabled;
+    /** 首页顶部推荐方式:1插入 2替换 */
+    private Integer recHomeCourseTopMode;
+    /** 首页顶部推荐位置序号 */
+    private Integer recHomeCourseTopSort;
+
+    /** 商城首页推荐位:0否 1是 */
+    private Integer recMallHomeEnabled;
+    /** 商城首页推荐方式:1插入 2替换 */
+    private Integer recMallHomeMode;
+    /** 商城首页推荐位置序号 */
+    private Integer recMallHomeSort;
+
+    /** 首页长视频瀑布流:0否 1是 */
+    private Integer recHomeLongVideoEnabled;
+    /** 长视频瀑布流方式:1插入 2替换 */
+    private Integer recHomeLongVideoMode;
+    /** 长视频瀑布流位置序号 */
+    private Integer recHomeLongVideoSort;
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCourseCategory.java

@@ -40,6 +40,9 @@ public class FsUserCourseCategory extends BaseEntity
 
     private String pic;
 
+    /** 分类类型:0-普通课程分类 1-公域课程分类 */
+    @Excel(name = "分类类型")
+    private Integer cateType;
 
     private Long userId;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java

@@ -130,6 +130,9 @@ public class FsUserCourseVideo extends BaseEntity
 
     private String vid;
 
+    /** 看课奖励类型(展示文案,可配置) */
+    private String rewardType;
+
     @TableField(exist = false)
     private Integer showProduct; //1不展示疗法,0展示疗法
 

+ 5 - 0
fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java

@@ -109,4 +109,9 @@ public class FsVideoResource {
      * 视频展示类型:landscape-横屏,portrait-竖屏,默认横屏
      */
     private String displayType;
+
+    /**
+     * 视频资源类型:0-视频资源,1-公域课视频资源
+     */
+    private Integer videoType;
 }

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

@@ -1,6 +1,7 @@
 package com.fs.course.mapper;
 
 import com.fs.course.domain.FsUserCourseCategory;
+import com.fs.course.param.FsUserCourseCategoryAppQueryParam;
 import com.fs.his.vo.OptionsVO;
 import org.apache.ibatis.annotations.MapKey;
 import org.apache.ibatis.annotations.Param;
@@ -33,6 +34,11 @@ public interface FsUserCourseCategoryMapper
      */
     public List<FsUserCourseCategory> selectFsUserCourseCategoryList(FsUserCourseCategory fsUserCourseCategory);
 
+    /**
+     * 小程序端:课程分类分页列表(稳定排序,供 PageHelper 使用)
+     */
+    List<FsUserCourseCategory> selectFsUserCourseCategoryAppList(FsUserCourseCategoryAppQueryParam param);
+
     /**
      * 新增课堂分类
      *

+ 9 - 1
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java

@@ -10,6 +10,7 @@ import com.fs.course.domain.FsUserCourse;
 import com.fs.course.param.FsCourseListBySidebarParam;
 import com.fs.course.param.FsUserCourseAddStudyCourseParam;
 import com.fs.course.param.FsUserCourseListUParam;
+import com.fs.course.param.FsUserCoursePublicAppQueryParam;
 import com.fs.course.param.FsUserCourseParam;
 import com.fs.course.param.newfs.FsUserCourseListParam;
 import com.fs.course.vo.*;
@@ -131,6 +132,10 @@ public interface FsUserCourseMapper
             "</script>"})
     List<FsUserCourseListUVO> selectFsUserCourseListUVO(@Param("maps") FsUserCourseListUParam param);
 
+    /**
+     * 小程序:公域课程分页(联表统计看课人数)
+     */
+    List<FsUserCoursePublicAppVO> selectFsUserCoursePublicAppList(@Param("q") FsUserCoursePublicAppQueryParam param);
 
     @Select({"<script> " +
             "select c.*,cc.cate_name,ucc.cate_name as sub_cate_name from fs_user_course  c " +
@@ -153,7 +158,10 @@ public interface FsUserCourseMapper
             "and c.course_name like concat('%', #{maps.courseName}, '%') " +
             "</if>" +
             "<if test = ' maps.isPrivate !=null '> " +
-            "and c.is_private = #{maps.isPrivate} " +
+            " and <choose>" +
+            "<when test='maps.isPrivate == 1'>(c.is_private = 1 or c.is_private is null)</when>" +
+            "<otherwise>c.is_private = #{maps.isPrivate}</otherwise>" +
+            "</choose> " +
             "</if>" +
             "<if test = ' maps.isShow !=null '> " +
             "and c.is_show = #{maps.isShow} " +

+ 18 - 0
fs-service/src/main/java/com/fs/course/mapper/PublicCourseWatchStatisticsMapper.java

@@ -0,0 +1,18 @@
+package com.fs.course.mapper;
+
+import com.fs.course.param.PublicCourseWatchStatQueryParam;
+import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
+import com.fs.course.vo.PublicCourseWatchStatCourseVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 公域看课统计(独立 Mapper,勿与 fs_course_watch_log 业务混用)
+ */
+public interface PublicCourseWatchStatisticsMapper {
+
+    List<PublicCourseWatchStatCourseVO> selectCourseDayStatList(@Param("q") PublicCourseWatchStatQueryParam q);
+
+    List<PublicCourseWatchStatCatalogVO> selectCatalogStatList(@Param("q") PublicCourseWatchStatQueryParam q);
+}

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchCommentPageParam.java

@@ -39,4 +39,8 @@ public class FsCourseWatchCommentPageParam extends BaseEntity{
     @Excel(name = "视频id")
     private Long videoId;
 
+    /** 分类类型:0-评论(课程),1-公域看课评论 */
+    @ApiModelProperty(value = "分类类型:0-评论,1-公域看课评论")
+    private Integer cateType;
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchCommentSaveParam.java

@@ -41,4 +41,7 @@ public class FsCourseWatchCommentSaveParam{
     @ApiModelProperty(value = "字体颜色")
     private String color;
 
+    @ApiModelProperty(value = "分类类型:0-评论,1-公域看课评论,默认0")
+    private Integer cateType;
+
 }

+ 35 - 0
fs-service/src/main/java/com/fs/course/param/FsUserCourseCategoryAppQueryParam.java

@@ -0,0 +1,35 @@
+package com.fs.course.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 小程序端课程分类分页查询参数(默认公域课分类 cateType=1)
+ */
+@Data
+@ApiModel("小程序-课程分类分页查询参数")
+public class FsUserCourseCategoryAppQueryParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "页码,默认1", example = "1")
+    private Integer pageNum = 1;
+
+    @ApiModelProperty(value = "每页条数,默认10", example = "10")
+    private Integer pageSize = 10;
+
+    @ApiModelProperty(value = "分类名称(模糊)")
+    private String cateName;
+
+    @ApiModelProperty(value = "一级分类ID(父级 pid);只返回该一级下的二级;不传则返回所有「有公域课使用」的二级")
+    private Long pid;
+
+    @ApiModelProperty(value = "是否显示:1显示 0隐藏;不传表示不限")
+    private Integer isShow;
+
+    @ApiModelProperty(value = "分类类型:1=公域课程分类,0=普通;不传时接口默认按1(公域)查询")
+    private Integer cateType;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/course/param/FsUserCoursePublicAppQueryParam.java

@@ -0,0 +1,38 @@
+package com.fs.course.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 小程序端公域课程分页查询(与课程分类联动:传 cateId / subCateId)
+ */
+@Data
+@ApiModel("小程序-公域课程分页查询参数")
+public class FsUserCoursePublicAppQueryParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "页码,默认1", example = "1")
+    private Integer pageNum = 1;
+
+    @ApiModelProperty(value = "每页条数,默认10", example = "10")
+    private Integer pageSize = 10;
+
+    @ApiModelProperty(value = "一级分类ID(与课程分类接口联动)")
+    private Long cateId;
+
+    @ApiModelProperty(value = "二级分类ID")
+    private Long subCateId;
+
+    @ApiModelProperty(value = "关键词(匹配课程标题/名称)")
+    private String keyword;
+
+    /**
+     * 推荐位筛选:不传=全部公域课;1=首页课程顶部推荐位已勾选;2=商城首页推荐位已勾选;3=首页长视频瀑布流已勾选
+     */
+    @ApiModelProperty(value = "推荐位:1首页顶部 2商城首页 3首页长视频瀑布流;不传则不限")
+    private Integer recommendSlot;
+}

+ 30 - 0
fs-service/src/main/java/com/fs/course/param/LiveQuizSubmitUParam.java

@@ -0,0 +1,30 @@
+package com.fs.course.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 直播答题提交(观众端)
+ */
+@Data
+public class LiveQuizSubmitUParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 当前登录用户(由 Controller 写入) */
+    private Long userId;
+
+    private Long liveId;
+
+    private Long relId;
+
+    private Long questionBankId;
+
+    /** 与题库一致:1 单选 2 多选 */
+    private Integer type;
+
+    /** 用户选择的选项 key,如 A、B;多选为多个 */
+    private List<String> answerKeys;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/course/param/PublicCourseWatchStatQueryParam.java

@@ -0,0 +1,25 @@
+package com.fs.course.param;
+
+import lombok.Data;
+
+/**
+ * 公域看课统计查询参数
+ */
+@Data
+public class PublicCourseWatchStatQueryParam {
+
+    /** 课程名称 / 章节名称 模糊 */
+    private String keywords;
+
+    /** 一级分类 */
+    private Long cateId;
+
+    /** 二级分类 */
+    private Long subCateId;
+
+    /** 开始日期 yyyy-MM-dd */
+    private String beginDate;
+
+    /** 结束日期 yyyy-MM-dd(含当天) */
+    private String endDate;
+}

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

@@ -5,6 +5,7 @@ import com.fs.course.domain.FsCourseQuestionBank;
 import com.fs.course.dto.FsCourseQuestionBankImportDTO;
 import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
+import com.fs.course.param.LiveQuizSubmitUParam;
 
 import javax.validation.constraints.Size;
 import java.util.List;
@@ -93,4 +94,9 @@ public interface IFsCourseQuestionBankService
     R courseAnswerByFsUser(FsCourseQuestionAnswerUParam param);
 
     R courseAnswerIsOpen(FsCourseQuestionAnswerUParam param, boolean isH5User);
+
+    /**
+     * 直播答题提交:校验直播间-题目关联、题库是否存在,按 course.config 决定是否跳过对错校验(与课程答题 submit 逻辑一致)。
+     */
+    R submitLiveQuiz(LiveQuizSubmitUParam param);
 }

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

@@ -5,6 +5,7 @@ import java.util.Map;
 
 import com.fs.course.domain.FsUserCourseCategory;
 import com.fs.course.dto.FsCourseCategoryImportDTO;
+import com.fs.course.param.FsUserCourseCategoryAppQueryParam;
 import com.fs.his.vo.OptionsVO;
 
 /**
@@ -31,6 +32,11 @@ public interface IFsUserCourseCategoryService
      */
     public List<FsUserCourseCategory> selectFsUserCourseCategoryList(FsUserCourseCategory fsUserCourseCategory);
 
+    /**
+     * 小程序端:课程分类分页列表(默认公域 cateType=1,仅查未删除)
+     */
+    List<FsUserCourseCategory> selectFsUserCourseCategoryAppList(FsUserCourseCategoryAppQueryParam param);
+
     /**
      * 新增课堂分类
      *

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java

@@ -84,6 +84,11 @@ public interface IFsUserCourseService {
 
     List<FsUserCourseListUVO> selectFsUserCourseListUVO(FsUserCourseListUParam param);
 
+    /**
+     * 小程序:公域课程分页(含看课人数,与分类联动)
+     */
+    List<FsUserCoursePublicAppVO> selectFsUserCoursePublicAppList(FsUserCoursePublicAppQueryParam param);
+
     List<OptionsVO> selectFsUserCourseAllList();
 
     List<FsUserCourseListPVO> selectFsUserCourseListPVO(FsUserCourse param);

+ 17 - 0
fs-service/src/main/java/com/fs/course/service/IPublicCourseWatchStatisticsService.java

@@ -0,0 +1,17 @@
+package com.fs.course.service;
+
+import com.fs.course.param.PublicCourseWatchStatQueryParam;
+import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
+import com.fs.course.vo.PublicCourseWatchStatCourseVO;
+
+import java.util.List;
+
+/**
+ * 公域看课统计
+ */
+public interface IPublicCourseWatchStatisticsService {
+
+    List<PublicCourseWatchStatCourseVO> listCourseDayStat(PublicCourseWatchStatQueryParam param);
+
+    List<PublicCourseWatchStatCatalogVO> listCatalogStat(PublicCourseWatchStatQueryParam param);
+}

+ 82 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java

@@ -14,8 +14,10 @@ import com.fs.course.dto.FsCourseQuestionBankImportDTO;
 import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.mapper.*;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
+import com.fs.course.param.LiveQuizSubmitUParam;
 import com.fs.course.service.IFsCourseQuestionBankService;
 import com.fs.course.util.CourseConfigUserAnswerExpose;
+import com.fs.live.mapper.LiveCourseQuestionRelMapper;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsStorePaymentService;
@@ -59,6 +61,8 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
     @Autowired
     private FsCourseWatchLogMapper courseWatchLogMapper;
     @Autowired
+    private LiveCourseQuestionRelMapper liveCourseQuestionRelMapper;
+    @Autowired
     private FsUserCourseCategoryMapper courseCategoryMapper;
     @Value("${cloud_host.company_name}")
     private String signProjectName;
@@ -506,6 +510,84 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         }
     }
 
+    @Override
+    public R submitLiveQuiz(LiveQuizSubmitUParam param) {
+        if (param == null) {
+            return R.error("参数不能为空");
+        }
+        if (param.getLiveId() == null || param.getRelId() == null || param.getQuestionBankId() == null) {
+            return R.error("参数不完整");
+        }
+        List<String> keys = param.getAnswerKeys();
+        if (keys == null || keys.isEmpty()) {
+            return R.error("请选择答案");
+        }
+        Long questionBankIdFromRel = liveCourseQuestionRelMapper.selectQuestionBankIdByLiveAndRel(
+                param.getLiveId(), param.getRelId());
+        if (questionBankIdFromRel == null) {
+            return R.error("题目与直播间关联无效");
+        }
+        if (!questionBankIdFromRel.equals(param.getQuestionBankId())) {
+            return R.error("题目信息不一致");
+        }
+        FsCourseQuestionBank bank = fsCourseQuestionBankMapper.selectFsCourseQuestionBankById(param.getQuestionBankId());
+        if (bank == null) {
+            return R.error("题目不存在");
+        }
+        if (param.getType() != null && bank.getType() != null
+                && bank.getType().intValue() != param.getType()) {
+            return R.error("题目类型不匹配");
+        }
+        if (bank.getType() == null) {
+            return R.error("题目数据异常");
+        }
+
+        String json = configService.selectConfigByKey("course.config");
+        boolean skipAnswerValidation = CourseConfigUserAnswerExpose.skipValidateAnswerOnSubmit(json);
+
+        boolean correct;
+        if (skipAnswerValidation) {
+            correct = true;
+        } else {
+            correct = matchLiveQuizUserAnswer(bank, keys);
+        }
+
+        if (correct) {
+            return R.ok("回答正确").put("correct", true);
+        }
+        return R.ok("回答错误").put("correct", false);
+    }
+
+    /**
+     * 与 {@link #courseAnswer} 中单选/多选判分规则一致。
+     */
+    private boolean matchLiveQuizUserAnswer(FsCourseQuestionBank bank, List<String> answerKeys) {
+        if (bank.getAnswer() == null) {
+            return false;
+        }
+        long t = bank.getType();
+        if (t == 1L) {
+            if (answerKeys.size() != 1) {
+                return false;
+            }
+            String u = answerKeys.get(0) == null ? "" : answerKeys.get(0).trim();
+            String c = bank.getAnswer().trim();
+            return u.equals(c);
+        }
+        if (t == 2L) {
+            String[] userAnswers = answerKeys.stream()
+                    .filter(Objects::nonNull)
+                    .map(String::trim)
+                    .filter(s -> !s.isEmpty())
+                    .toArray(String[]::new);
+            String[] correctAnswers = convertStringToArray(bank.getAnswer());
+            Arrays.sort(userAnswers);
+            Arrays.sort(correctAnswers);
+            return Arrays.equals(userAnswers, correctAnswers);
+        }
+        return false;
+    }
+
     /**
      * 题目导入
      *

+ 3 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java

@@ -141,6 +141,9 @@ public class FsCourseWatchCommentServiceImpl extends ServiceImpl<FsCourseWatchCo
         }
         FsCourseWatchComment fsCourseWatchComment = new FsCourseWatchComment();
         BeanUtils.copyProperties(param, fsCourseWatchComment);
+        if (fsCourseWatchComment.getCateType() == null) {
+            fsCourseWatchComment.setCateType(0);
+        }
         fsCourseWatchComment.setCreateTime(DateUtils.getNowDate());
         int i = baseMapper.insertFsCourseWatchComment(fsCourseWatchComment);
         if (i > 0) {

+ 13 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCategoryServiceImpl.java

@@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.course.mapper.FsUserCourseCategoryMapper;
 import com.fs.course.domain.FsUserCourseCategory;
+import com.fs.course.param.FsUserCourseCategoryAppQueryParam;
 import com.fs.course.service.IFsUserCourseCategoryService;
 
 /**
@@ -52,6 +53,18 @@ public class FsUserCourseCategoryServiceImpl implements IFsUserCourseCategorySer
         return fsUserCourseCategoryMapper.selectFsUserCourseCategoryList(fsUserCourseCategory);
     }
 
+    @Override
+    public List<FsUserCourseCategory> selectFsUserCourseCategoryAppList(FsUserCourseCategoryAppQueryParam param)
+    {
+        if (param == null) {
+            param = new FsUserCourseCategoryAppQueryParam();
+        }
+        if (param.getCateType() == null) {
+            param.setCateType(1);
+        }
+        return fsUserCourseCategoryMapper.selectFsUserCourseCategoryAppList(param);
+    }
+
     /**
      * 新增课堂分类
      *

+ 10 - 1
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -283,6 +283,11 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         return fsUserCourseMapper.selectFsUserCourseListUVO(param);
     }
 
+    @Override
+    public List<FsUserCoursePublicAppVO> selectFsUserCoursePublicAppList(FsUserCoursePublicAppQueryParam param) {
+        return fsUserCourseMapper.selectFsUserCoursePublicAppList(param);
+    }
+
     @Override
     public List<FsUserCourseListUVO> selectFsUserCourseCommentListUVO(FsUserCourseListUParam param) {
         return fsUserCourseMapper.selectFsUserCourseCommentListUVO(param);
@@ -791,7 +796,11 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         link.setUpdateTime(calendar.getTime());
         int i = fsCourseLinkMapper.insertFsCourseLink(link);
         if (i > 0){
-            if (CloudHostUtils.hasCloudHostName("中康")){
+//            String domainName = getDomainName(param.getCompanyUserId(), config);
+//            String sortLink = domainName + link.getRealLink().replace("/#","");
+//            return R.ok().put("url", sortLink).put("link", random).put("linkId", link.getLinkId());
+            //没人用我先注释了,手动发课 直接用 链接带参数
+            if (CloudHostUtils.hasCloudHostName("中康","蒙牛")){
                 String domainName = getDomainName(param.getCompanyUserId(), config);
                 String sortLink = domainName + link.getRealLink().replace("/#","");
                 return R.ok().put("url", sortLink).put("link", random).put("linkId", link.getLinkId());

+ 66 - 0
fs-service/src/main/java/com/fs/course/service/impl/PublicCourseWatchStatisticsServiceImpl.java

@@ -0,0 +1,66 @@
+package com.fs.course.service.impl;
+
+import com.fs.common.utils.StringUtils;
+import com.fs.course.mapper.PublicCourseWatchStatisticsMapper;
+import com.fs.course.param.PublicCourseWatchStatQueryParam;
+import com.fs.course.service.IPublicCourseWatchStatisticsService;
+import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
+import com.fs.course.vo.PublicCourseWatchStatCourseVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Service
+public class PublicCourseWatchStatisticsServiceImpl implements IPublicCourseWatchStatisticsService {
+
+    private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    @Autowired
+    private PublicCourseWatchStatisticsMapper publicCourseWatchStatisticsMapper;
+
+    @Override
+    public List<PublicCourseWatchStatCourseVO> listCourseDayStat(PublicCourseWatchStatQueryParam param) {
+        fillDefaultDateRange(param);
+        List<PublicCourseWatchStatCourseVO> list = publicCourseWatchStatisticsMapper.selectCourseDayStatList(param);
+        for (PublicCourseWatchStatCourseVO vo : list) {
+            if (vo.getClickRate() == null) {
+                vo.setClickRate(BigDecimal.ZERO);
+            }
+            if (vo.getExposurePv() == null) {
+                vo.setExposurePv(0L);
+            }
+            if (vo.getExposureUv() == null) {
+                vo.setExposureUv(0L);
+            }
+            if (vo.getClickPv() == null) {
+                vo.setClickPv(0L);
+            }
+            if (vo.getClickUv() == null) {
+                vo.setClickUv(0L);
+            }
+        }
+        return list;
+    }
+
+    @Override
+    public List<PublicCourseWatchStatCatalogVO> listCatalogStat(PublicCourseWatchStatQueryParam param) {
+        fillDefaultDateRange(param);
+        return publicCourseWatchStatisticsMapper.selectCatalogStatList(param);
+    }
+
+    private void fillDefaultDateRange(PublicCourseWatchStatQueryParam param) {
+        if (param == null) {
+            return;
+        }
+        if (StringUtils.isEmpty(param.getEndDate())) {
+            param.setEndDate(LocalDate.now().format(DAY));
+        }
+        if (StringUtils.isEmpty(param.getBeginDate())) {
+            param.setBeginDate(LocalDate.now().minusDays(30).format(DAY));
+        }
+    }
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/vo/FsCourseWatchCommentListVO.java

@@ -55,4 +55,7 @@ public class FsCourseWatchCommentListVO {
     @ApiModelProperty(value = "小节名称")
     private String title;
 
+    @ApiModelProperty(value = "分类类型:0-评论,1-公域看课评论")
+    private Integer cateType;
+
 }

+ 56 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCoursePublicAppVO.java

@@ -0,0 +1,56 @@
+package com.fs.course.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 小程序端公域课程列表(含看课人数)
+ */
+@Data
+@ApiModel("小程序-公域课程项")
+public class FsUserCoursePublicAppVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("课程ID")
+    private Long courseId;
+
+    @ApiModelProperty("展示标题(优先 title)")
+    private String courseTitle;
+
+    @ApiModelProperty("课程名称")
+    private String courseName;
+
+    @ApiModelProperty("课程封面")
+    private String imgUrl;
+
+    @ApiModelProperty("小封面")
+    private String secondImg;
+
+    @ApiModelProperty("看课人数(看课记录表 send_type=1 下去重 user_id)")
+    private Long watchUserCount;
+
+    @ApiModelProperty("首页课程顶部推荐位:0否 1是")
+    private Integer recHomeCourseTopEnabled;
+    @ApiModelProperty("首页顶部推荐方式:1插入 2替换")
+    private Integer recHomeCourseTopMode;
+    @ApiModelProperty("首页顶部推荐位置序号")
+    private Integer recHomeCourseTopSort;
+
+    @ApiModelProperty("商城首页推荐位:0否 1是")
+    private Integer recMallHomeEnabled;
+    @ApiModelProperty("商城首页推荐方式:1插入 2替换")
+    private Integer recMallHomeMode;
+    @ApiModelProperty("商城首页推荐位置序号")
+    private Integer recMallHomeSort;
+
+    @ApiModelProperty("首页长视频瀑布流:0否 1是")
+    private Integer recHomeLongVideoEnabled;
+    @ApiModelProperty("长视频瀑布流方式:1插入 2替换")
+    private Integer recHomeLongVideoMode;
+    @ApiModelProperty("长视频瀑布流位置序号")
+    private Integer recHomeLongVideoSort;
+}

+ 5 - 0
fs-service/src/main/java/com/fs/course/vo/FsVideoResourceVO.java

@@ -92,4 +92,9 @@ public class FsVideoResourceVO {
      * 视频展示类型:landscape-横屏,portrait-竖屏
      */
     private String displayType;
+
+    /**
+     * 视频资源类型:0-视频资源,1-公域课视频资源
+     */
+    private Integer videoType;
 }

+ 61 - 0
fs-service/src/main/java/com/fs/course/vo/PublicCourseWatchStatCatalogVO.java

@@ -0,0 +1,61 @@
+package com.fs.course.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 公域看课统计 - 目录(小节)维度
+ */
+@Data
+public class PublicCourseWatchStatCatalogVO {
+
+    @Excel(name = "目录ID")
+    private Long videoId;
+
+    @Excel(name = "目录名称")
+    private String catalogName;
+
+    @Excel(name = "课程名称")
+    private String courseName;
+
+    @Excel(name = "课程分类")
+    private String catePath;
+
+    @Excel(name = "浏览量(PV)")
+    private Long pv;
+
+    @Excel(name = "观看人数(UV)")
+    private Long uv;
+
+    @Excel(name = "完课人数")
+    private Long finishUv;
+
+    @Excel(name = "评论数")
+    private Long commentCount;
+
+    @Excel(name = "完课率%")
+    private BigDecimal finishRate;
+
+    /** 平均观看时长(秒) */
+    private Long avgWatchSeconds;
+
+    @Excel(name = "平均学习时长")
+    private String avgWatchDuration;
+
+    @Excel(name = "看课奖励类型")
+    private String rewardType;
+
+    @Excel(name = "答题人数")
+    private Long answerUv;
+
+    @Excel(name = "领取积分人数")
+    private Long integralReceiveUv;
+
+    @Excel(name = "分享私聊数")
+    private Long sharePrivateCount;
+
+    @Excel(name = "分享朋友圈数")
+    private Long shareTimelineCount;
+}

+ 55 - 0
fs-service/src/main/java/com/fs/course/vo/PublicCourseWatchStatCourseVO.java

@@ -0,0 +1,55 @@
+package com.fs.course.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 公域看课统计 - 按日 + 课程
+ */
+@Data
+public class PublicCourseWatchStatCourseVO {
+
+    @Excel(name = "时间", width = 12, dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date statDate;
+
+    @Excel(name = "课程ID")
+    private Long courseId;
+
+    @Excel(name = "课程名称")
+    private String courseName;
+
+    @Excel(name = "课程分类")
+    private String rootCateName;
+
+    @Excel(name = "子分类")
+    private String subCateName;
+
+    @Excel(name = "曝光位置")
+    private String exposurePositionLabel;
+
+    @Excel(name = "曝光次数")
+    private Long exposurePv;
+
+    @Excel(name = "曝光人数")
+    private Long exposureUv;
+
+    @Excel(name = "点击次数")
+    private Long clickPv;
+
+    @Excel(name = "点击人数")
+    private Long clickUv;
+
+    @Excel(name = "点击率%")
+    private BigDecimal clickRate;
+
+    @Excel(name = "课程观看人数")
+    private Long watchUv;
+
+    @Excel(name = "课程完课人数")
+    private Long finishUv;
+}

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

@@ -399,7 +399,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("isRepository", "");
         requestParam.put("history", "");
         requestParam.put("aiContent", "");
-        requestParam.put("likeRatio", "");
+        requestParam.put("userIntent", "");
         long startTime = System.currentTimeMillis();
         R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, Long.valueOf(param.getChatId()),OTHER_KEY);
         System.out.println(aiResponse);
@@ -545,7 +545,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("isRepository", "");
         requestParam.put("userContent", "");
         requestParam.put("aiContent", "");
-        requestParam.put("likeRatio", "");
+        requestParam.put("userIntent", "");
         requestParam.put("modelType","客户意向度");
 //        log.info("请求参数:{}", requestParam);
 
@@ -780,7 +780,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("isRepository", "");
         requestParam.put("userContent", "");
         requestParam.put("aiContent", "");
-        requestParam.put("likeRatio", "");
+        requestParam.put("userIntent", "");
         requestParam.put("modelType","ai标签");
 
         return requestParam;
@@ -896,7 +896,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("isRepository", "");
         requestParam.put("userContent", "");
         requestParam.put("aiContent", "");
-        requestParam.put("likeRatio", likeRatio);
+        requestParam.put("userIntent", likeRatio);
 
         return requestParam;
     }
@@ -944,7 +944,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("isRepository", "");
         requestParam.put("userContent", "");
         requestParam.put("aiContent", "");
-        requestParam.put("likeRatio", likeRatio);
+        requestParam.put("userIntent", likeRatio);
 
         return requestParam;
     }

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

@@ -18,6 +18,8 @@ public class CrmCustomerListQueryVO implements Serializable
 
     /** 组织机构代码 */
     private String customerCode;
+    /** AI 外呼呼出次数(company_voice_robotic_call_log_callphone,经 callees.user_id=customerId) */
+    private Integer roboticCallOutCount;
 
     /** 客户名称 */
     private String customerName;

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

@@ -17,6 +17,8 @@ public class CrmLineCustomerListQueryVO implements Serializable
 
     /** 组织机构代码 */
     private String customerCode;
+    /** AI 外呼呼出次数(company_voice_robotic_call_log_callphone,经 callees.user_id=customerId) */
+    private Integer roboticCallOutCount;
 
     /** 客户名称 */
     private String customerName;

+ 2 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmMyCustomerListQueryVO.java

@@ -145,4 +145,6 @@ public class CrmMyCustomerListQueryVO implements Serializable
 
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date visitTime;
+    /** AI 外呼呼出次数(company_voice_robotic_call_log_callphone,经 callees.user_id=customerId) */
+    private Integer roboticCallOutCount;
 }

+ 14 - 0
fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatConversation.java

@@ -0,0 +1,14 @@
+package com.fs.fastGpt.domain;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+
+@Data
+public class FastGptChatConversation {
+    private JSONObject userInfo;
+    private JSONObject aiInfo;
+    private JSONObject history;
+    private String isRepository;
+    private String userContent;
+    private String aiContent;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatSession.java

@@ -73,4 +73,7 @@ public class FastGptChatSession extends BaseEntity
 
     private Integer isReply;
 
+    //客户和销售对话中实际收集到的信息
+    private String userInfo;
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/fastGpt/domain/FastGptRole.java

@@ -84,4 +84,7 @@ public class FastGptRole extends BaseEntity
 
     //课程Id
     private Long courseId;
+
+    //需要获取的客户信息
+    private String userInfo;
 }

+ 1 - 1
fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java

@@ -10,7 +10,7 @@ public interface AiHookService {
     R AiRemind();
 
     /** ai回复**/
-    R qwHookNotifyAiReply(Long qwUserID, Long sender,String count,String uid,Integer type);
+    R qwHookNotifyAiReply(Long qwUserID, Long sender,String count,String uid,Integer type, Boolean isNewVersion);
 
     /** 转人工 **/
     void artificial(QwHookVO vo);

+ 212 - 14
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -66,6 +66,7 @@ import com.fs.utils.SensitiveDataUtils;
 import com.fs.voice.utils.StringUtil;
 import com.fs.wxwork.dto.*;
 import com.fs.wxwork.service.WxWorkService;
+import com.google.gson.Gson;
 import com.vdurmont.emoji.EmojiParser;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.ObjectUtils;
@@ -384,7 +385,7 @@ public class AiHookServiceImpl implements AiHookService {
     /** Ai回复 **/
     @Async
     @Override
-    public R qwHookNotifyAiReply(Long qwUserId, Long sender,String qwContent,String uid,Integer type) {
+    public R qwHookNotifyAiReply(Long qwUserId, Long sender,String qwContent,String uid,Integer type, Boolean isNewVersion) {
         if (qwContent==null||qwContent.isEmpty()){
             return R.ok();
         }
@@ -563,7 +564,12 @@ public class AiHookServiceImpl implements AiHookService {
             redisCache.setCacheObject("reply:" + fastGptChatSession.getSessionId(),1,5,TimeUnit.MINUTES);
             redisCache.setCacheObject("msg:" + fastGptChatSession.getSessionId(),contentEmj,5,TimeUnit.MINUTES);
             log.info("等待");
-            R r= sendAiMsg(replyI,fastGptChatSession,role,user,qwExternalContacts.getId(),config.getAPPKey(),qwExternalContacts,sender);
+            R r;
+            if(isNewVersion){
+                r= sendAiMsgNew(replyI,fastGptChatSession,role,user,qwExternalContacts.getId(),config.getAPPKey(),qwExternalContacts,sender);
+            } else {
+                r= sendAiMsg(replyI,fastGptChatSession,role,user,qwExternalContacts.getId(),config.getAPPKey(),qwExternalContacts,sender);
+            }
             EventLogUtils.recordEventLog(sender,1L,1,user);
             EventLogUtils.recordEventLog(sender,1L,2,user);
             log.info("数据:{}", r);
@@ -591,7 +597,15 @@ public class AiHookServiceImpl implements AiHookService {
                 return R.ok();
             }
             String contentKh = result.getChoices().get(0).getMessage().getContent();
-            String content = replace(result.getChoices().get(0).getMessage().getContent()).trim();
+            String content = null;
+            if(isNewVersion){
+                Gson gson = new Gson();
+                FastGptChatConversation fastGptChatConversation = gson.fromJson(contentKh, FastGptChatConversation.class);
+                content = fastGptChatConversation.getAiContent();
+            }else{
+                content = replace(result.getChoices().get(0).getMessage().getContent()).trim();
+            }
+
             //计算token
             List<ChatDetailTStreamFResult.ResponseNode> responseData = result.getResponseData();
             int token=0;
@@ -611,19 +625,29 @@ public class AiHookServiceImpl implements AiHookService {
                     notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 触发关键词",qwExternalContacts.getId(),sender);
                     return R.ok();
                 }
-                //ai回复文字长度大于500就转人工
-                if(content.length() > 500){
-                    log.error("回复长度异常:"+role.getRoleId()+":"+qwExternalContacts.getName());
-                    notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 回复长度异常",qwExternalContacts.getId(),sender);
-                    return R.ok();
+                if(!isNewVersion){
+                    //ai回复文字长度大于500就转人工
+                    if(content.length() > 500){
+                        log.error("回复长度异常:"+role.getRoleId()+":"+qwExternalContacts.getName());
+                        notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 回复长度异常",qwExternalContacts.getId(),sender);
+                        return R.ok();
+                    }
                 }
+
                 if (result.isLongText()){
                     //新增用户信息
                     addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
-                    if (type==16){
-                        sendAiVoiceMsg(content,sender,uid,serverId,user);
-                    }else {
+                    //发送图片消息
+                    sendImgMsg(contentKh,sender,uid,serverId);
+
+                    if(isNewVersion){
                         sendAiMsg(content,sender,uid,serverId);
+                    } else {
+                        if (type==16){
+                            sendAiVoiceMsg(content,sender,uid,serverId,user);
+                        }else {
+                            sendAiMsg(content,sender,uid,serverId);
+                        }
                     }
 
                 }else {
@@ -638,11 +662,17 @@ public class AiHookServiceImpl implements AiHookService {
                     List<String> countList = countString(content);
                     //新增用户信息
                     addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
+                    //发送图片消息
+                    sendImgMsg(contentKh,sender,uid,serverId);
                     for (String msg : countList) {
-                        if (type==16){
-                            sendAiVoiceMsg(msg,sender,uid,serverId,user);
-                        }else {
+                        if(isNewVersion){
                             sendAiMsg(msg,sender,uid,serverId);
+                        } else {
+                            if (type==16){
+                                sendAiVoiceMsg(msg,sender,uid,serverId,user);
+                            }else {
+                                sendAiMsg(msg,sender,uid,serverId);
+                            }
                         }
                         try {
                             Thread.sleep(10000);
@@ -658,6 +688,11 @@ public class AiHookServiceImpl implements AiHookService {
                 }
             }
 
+            //
+            // todo 把当前的内容转成jsonObject,然后取出isRepository,判断是否为知识库,如果不是知识库的,就存储到表中
+            // 什么时候销售回复,销售回复的在哪里可以看到
+//            contentKh
+
             aiEventProcess(sender, uid, role, contentKh, user, fastGptChatSession, serverId,qwExternalContacts);
 
 
@@ -670,6 +705,39 @@ public class AiHookServiceImpl implements AiHookService {
         return R.ok();
     }
 
+    private void sendImgMsg(String contentKh, Long sender, String uuid, Long serverId) {
+        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(contentKh);
+        JSONArray imgUrls = jsonObject.getJSONArray("imgUrl");
+
+
+        if(imgUrls != null && !imgUrls.isEmpty()){
+            WxWorkResponseDTO<WxwUploadCdnLinkImgRespDTO> dto = new WxWorkResponseDTO<>();
+            for (Object imgUrl : imgUrls) {
+                //1.上传cdn网络图片
+                if(imgUrl != null){
+                    String imgUrlString = imgUrl.toString();
+                    WxwUploadCdnLinkImgDTO wxwUploadCdnLinkImgDTO = new WxwUploadCdnLinkImgDTO();
+                    wxwUploadCdnLinkImgDTO.setUuid(uuid);
+                    wxwUploadCdnLinkImgDTO.setUrl(imgUrlString);
+                    dto  = wxWorkService.uploadCdnLinkImg(wxwUploadCdnLinkImgDTO,serverId);
+                }
+                //图片上传成功后再发送图片
+                if("成功".equals(dto.getErrmsg()) && imgUrl != null){
+                    WxwUploadCdnLinkImgRespDTO imgRespDTO = dto.getData();
+                    WxwSendCDNImgMsgDTO wxwSendCDNImgMsgDTO = new WxwSendCDNImgMsgDTO();
+                    wxwSendCDNImgMsgDTO.setSend_userid(sender);
+                    wxwSendCDNImgMsgDTO.setUuid(uuid);
+                    wxwSendCDNImgMsgDTO.setIsRoom(false);
+                    wxwSendCDNImgMsgDTO.setCdnkey(imgRespDTO.getCdn_key());
+                    wxwSendCDNImgMsgDTO.setAeskey(imgRespDTO.getAes_key());
+                    wxwSendCDNImgMsgDTO.setMd5(imgRespDTO.getMd5());
+                    wxwSendCDNImgMsgDTO.setFileSize(imgRespDTO.getSize());
+                    wxWorkService.SendCDNImgMsg(wxwSendCDNImgMsgDTO, serverId);
+                }
+            }
+        }
+    }
+
     /**
      * 根据发送者id设置用户是否为首次回复
      * @param sender 发送者id
@@ -1528,6 +1596,69 @@ public class AiHookServiceImpl implements AiHookService {
         }
 
     }
+
+    /** 发送Ai消息,-- -----从saas移动过来,主要是返回结构和内部逻辑跟之前的有区别 (2026年4月17日16点08分) **/
+    private R  sendAiMsgNew(Integer i,FastGptChatSession fastGptChatSession, FastGptRole role,QwUser user,Long qwExternalContactsId,String appKey,QwExternalContact qwExternalContacts,Long sender){
+        //等待5秒
+        try {
+            Thread.sleep(500); // 5000 毫秒 = 5 秒
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        //获取现在的次数
+        Integer reply = (Integer)redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+        if (reply!=i){
+            //次数变动 重新等待5秒
+            R r = sendAiMsg(reply, fastGptChatSession, role, user, qwExternalContactsId, appKey, qwExternalContacts,sender);
+            return r;
+        }else {
+            System.out.println("开始ai回答");
+            ChatParam param=new ChatParam();
+            param.setChatId(fastGptChatSession.getChatId());
+            param.setStream(false);
+            param.setDetail(true);
+            ChatParam.Variables variables=new ChatParam.Variables();
+            variables.setUid(user.getFastGptRoleId().toString());
+            variables.setName("test");
+            param.setVariables(variables);
+            List<ChatParam.Message> messageList=new ArrayList<ChatParam.Message>();
+            param.setMessages(messageList);
+            //添加看客记录
+            //addCourseWatchLog(qwExternalContactsId);
+            String msgC = (String)redisCache.getCacheObject("msg:" + fastGptChatSession.getSessionId());
+
+            if (("今正科技".equals(cloudHostProper.getCompanyName()))) {
+                //处理名称替换
+                if (role.getReminderWords() != null && !role.getReminderWords().isEmpty() && role.getReminderWords().contains("#销售名称#")) {
+                    CompanyUser companyUser = companyUserMapper.selectCompanyUserByQwUserId(user.getId());
+                    if (companyUser != null) {
+                        role.setReminderWords(role.getReminderWords().replace("#销售名称#", companyUser.getNickName()));
+                    }
+                }
+            }
+
+            //添加关键词
+            addPromptWordNew(messageList,msgC,qwExternalContactsId,role,fastGptChatSession);
+            R r = chatService.initiatingTakeChat(param, "http://129.28.170.206:3000/api/", appKey);
+            Object data1 = r.get("data");
+            if(!(data1 instanceof KnowledgeBaseResult)){
+                ChatDetailTStreamFResult data = (ChatDetailTStreamFResult) r.get("data");
+                EventLogUtils.createEventTokenLog("发起对话",user,sender,data);
+            }
+
+            Integer reply2 = (Integer)redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+            //次数变动 重新等待5秒
+            if (reply2!=i){
+                System.out.println("等待");
+                R r1 = sendAiMsg(reply, fastGptChatSession, role, user, qwExternalContactsId, appKey, qwExternalContacts,sender);
+                return r1;
+            }
+            addSaveAiMsg(2,1,messageList.get(0).getContent(),user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+            return r;
+        }
+
+    }
+
     /** 增加课程信息 **/
     private void addCourseWatchLog(Long id) {
         FsCourseWatchLogVO log = fsCourseWatchLogMapper.selectFsCourseWatchLogByExtId(id);
@@ -1560,6 +1691,73 @@ public class AiHookServiceImpl implements AiHookService {
             }
         }
     }
+
+    /** 组装发送AI内容 **/
+    private void addPromptWordNew(List<ChatParam.Message> messageList,String count,Long extId,FastGptRole role,FastGptChatSession fastGptChatSession){
+
+        FastGptChatConversation conversation = new FastGptChatConversation();
+        conversation.setAiInfo(new com.alibaba.fastjson.JSONObject());
+        conversation.setUserInfo(new com.alibaba.fastjson.JSONObject());
+        conversation.setHistory(new com.alibaba.fastjson.JSONObject());
+
+        //组装客户信息
+        String sessionUserInfo = fastGptChatSession.getUserInfo();
+        String[] split = role.getUserInfo().split(",");
+        com.alibaba.fastjson.JSONObject userInfo = conversation.getUserInfo();
+        if(sessionUserInfo != null){
+            Map<String,Object> map = com.alibaba.fastjson.JSONObject.parseObject(sessionUserInfo, HashMap.class);
+            if(role.getUserInfo() != null){
+                for (String name : split) {
+                    if (name != null) {
+                        Object value = map.get(name);
+                        if(value != null){
+                            userInfo.put(name,value);
+                        }else{
+                            userInfo.put(name,"");
+                        }
+                    }
+                }
+            }
+        }else{
+            for (String name : split) {
+                if (name != null) {
+                    userInfo.put(name,"");
+                }
+            }
+        }
+
+
+        List<FastGptChatMsg> msgs=fastGptChatMsgService.selectFastGptChatMsgByMsgSessionIdAndExtId(fastGptChatSession.getSessionId(),extId);
+        if (!msgs.isEmpty()){
+            com.alibaba.fastjson.JSONObject history = conversation.getHistory();
+            Collections.reverse(msgs);
+            msgs.remove(msgs.size() - 1);
+            for (FastGptChatMsg msg : msgs) {
+                Integer sendType = msg.getSendType();
+                String content = msg.getContent();
+                if(sendType!=1){
+                    if (content!=null&&content.length()>500){
+                        continue;
+                    }
+                }
+                history.put(sendType==1?"user":"ai",content);
+            }
+            conversation.setHistory(history);
+        }
+
+        if (count!=null&& !count.isEmpty()){
+            conversation.setUserContent(count);
+        }
+
+
+        ChatParam.Message message1=new ChatParam.Message();
+        message1.setRole("user");
+        Gson gson = new Gson();
+        String jsonStr = gson.toJson(conversation);
+        message1.setContent(jsonStr);
+        messageList.add(message1);
+    }
+
     /** 组装发送AI内容 **/
     private void addPromptWord(List<ChatParam.Message> messageList,String count,Long extId,String words,String countInfo,Long sessionId){
 

+ 10 - 0
fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java

@@ -43,4 +43,14 @@ public class CidPhoneConfig implements Serializable {
      * 拨打次数
      * **/
     private Long numberCalls;
+
+    /**
+     * 是否允许重复客户
+     */
+    private Boolean allowRepeatCustomer;
+
+    /**
+     * 配置回调地址
+     */
+    private String callbackUrl;
 }

+ 48 - 0
fs-service/src/main/java/com/fs/his/domain/FsThirdDeviceData.java

@@ -0,0 +1,48 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 三方设备健康数据对象 fs_third_device_data
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsThirdDeviceData extends BaseEntity{
+
+    /** 记录id */
+    @TableId
+    private Long id;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 记录类型 0:血压 1:血糖 2:心率  3尿酸 4血氧 */
+    @Excel(name = "记录类型 0:血压 1:血糖 2:心率  3尿酸 4血氧 5步数 6运动 7睡眠")
+    private Integer recordType;
+
+    /** 记录数值 */
+    @Excel(name = "记录数值")
+    private String recordValue;
+
+    /** 记录数值 */
+    @Excel(name = "设备id")
+    private String deviceId;
+
+    /** 设备类型 0小护士设备 1手表 */
+    @Excel(name = "设备类型 0小护士设备 1手表")
+    private Integer deviceType;
+
+    /** 状态 */
+    @Excel(name = "状态")
+    private Integer status;
+
+
+}

+ 2 - 0
fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java

@@ -2,10 +2,12 @@ package com.fs.his.dto;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.io.Serializable;
 
 @Data
+@NoArgsConstructor
 @AllArgsConstructor
 public class SendResultDetailDTO implements Serializable {
     private boolean success;

+ 147 - 0
fs-service/src/main/java/com/fs/his/mapper/FsThirdDeviceDataMapper.java

@@ -0,0 +1,147 @@
+package com.fs.his.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsThirdDeviceData;
+import com.fs.watch.domain.vo.WatchSportRecordVo;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 三方设备健康数据Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface FsThirdDeviceDataMapper extends BaseMapper<FsThirdDeviceData>{
+    /**
+     * 查询三方设备健康数据
+     * 
+     * @param id 三方设备健康数据主键
+     * @return 三方设备健康数据
+     */
+    FsThirdDeviceData selectFsThirdDeviceDataById(Long id);
+
+    /**
+     * 查询三方设备健康数据列表
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 三方设备健康数据集合
+     */
+    List<FsThirdDeviceData> selectFsThirdDeviceDataList(FsThirdDeviceData fsThirdDeviceData);
+
+    /**
+     * 新增三方设备健康数据
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 结果
+     */
+    int insertFsThirdDeviceData(FsThirdDeviceData fsThirdDeviceData);
+
+    /**
+     * 修改三方设备健康数据
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 结果
+     */
+    int updateFsThirdDeviceData(FsThirdDeviceData fsThirdDeviceData);
+
+    /**
+     * 删除三方设备健康数据
+     * 
+     * @param id 三方设备健康数据主键
+     * @return 结果
+     */
+    int deleteFsThirdDeviceDataById(Long id);
+
+    /**
+     * 批量删除三方设备健康数据
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsThirdDeviceDataByIds(Long[] ids);
+
+//    Map<String, Integer> countMaxAndMinBpByDate(String currentStartDate, String currentEndDate, String deviceId);
+
+    void insertFsThirdDeviceDataList(@Param("list") List<FsThirdDeviceData> fsThirdDeviceData);
+
+    List<FsThirdDeviceData> getLastData(@Param("userId")Long userId,@Param("deviceType") Integer deviceType,@Param("deviceId") String deviceId);
+
+    /**
+     * 血压
+     * @param startTime
+     * @param endTime
+     * @param deviceId
+     * @return
+     */
+    Map<String, Integer> countMaxAndMinBpByDate(@Param("startTime") String startTime, @Param("endTime") String endTime,
+                                                @Param("deviceId") String deviceId);
+
+    List<FsThirdDeviceData> queryBgByDate(@Param("startTime")String startTime,@Param("endTime") String endTime,
+                                              @Param("deviceId")String deviceId);
+
+    Map<String, Integer> countMaxAndAvgBpByDate(@Param("startTime")String startTime, @Param("endTime") String endTime,
+                                                @Param("deviceId")String deviceId);
+
+    FsThirdDeviceData getFirstDataByDeviceId(@Param("deviceId")String deviceId);
+
+    List<FsThirdDeviceData> queryByMonth(@Param("deviceId")String deviceId, @Param("monthStr")String monthStr,
+                                           @Param("recordType")Integer recordType);
+
+    FsThirdDeviceData getLatest(@Param("deviceId")String deviceId,
+                                @Param("recordType")Integer recordType);
+
+    List<FsThirdDeviceData> selectListByDateAndDeviceIdAndRecordType(@Param("startTime")Date startTime,
+                                                                     @Param("endTime") Date endTime,
+                                                                     @Param("deviceId")String deviceId,
+                                                                     @Param("recordType")Integer recordType,
+                                                                     @Param("status")Integer status);
+
+    List<Map<String, Object>> countByDateAndDeviceIdAndRecordType(@Param("startTime")Date startTime,
+                                                                  @Param("endTime") Date endTime,
+                                                                  @Param("deviceId")String deviceId,
+                                                                  @Param("recordType")Integer recordType,
+                                                                  @Param("status")Integer status);
+
+    int countPageByDateAndDeviceIdAndRecordType(@Param("startTime")Date startTime,
+                                                @Param("endTime") Date endTime,
+                                                @Param("deviceId")String deviceId,
+                                                @Param("recordType")Integer recordType,
+                                                @Param("status")Integer status);
+
+    List<FsThirdDeviceData> queryPageByDateAndDeviceIdAndRecordType(@Param("startTime")Date startTime,
+                                                                    @Param("endTime") Date endTime,
+                                                                    @Param("deviceId")String deviceId,
+                                                                    @Param("recordType")Integer recordType,
+                                                                    @Param("status")Integer status,
+                                                                    @Param("num")Integer num,
+                                                                    @Param("size")Integer size
+                                                                    );
+
+    Float countAvgBpByDate(@Param("startTime") String startTime, @Param("endTime") String endTime, @Param("deviceId") String deviceId);
+
+    WatchSportRecordVo getOtherData(@Param("startTime")Date startTime,
+                                    @Param("endTime") Date endTime,
+                                    @Param("deviceId")String deviceId,
+                                    @Param("recordType")Integer recordType,
+                                    @Param("status")Integer status);
+
+    List<WatchSportRecordVo> queryActivityRecord(@Param("startTime")Date startTime,
+                                                 @Param("endTime") Date endTime,
+                                                 @Param("deviceId")String deviceId,
+                                                 @Param("recordType")Integer recordType,
+                                                 @Param("status")Integer status);
+
+    WatchSportRecordVo getOtherDataOther(@Param("startTime")Date startTime,
+                                         @Param("endTime") Date endTime,
+                                         @Param("deviceId")String deviceId,
+                                         @Param("recordType")Integer recordType);
+
+    List<FsThirdDeviceData> selectListByDateAndDeviceIdAndRecordTypeOther(@Param("startTime")Date startTime,
+                                                                          @Param("endTime") Date endTime,
+                                                                          @Param("deviceId")String deviceId,
+                                                                          @Param("recordType")Integer recordType);
+}

+ 80 - 0
fs-service/src/main/java/com/fs/his/service/IFsThirdDeviceDataService.java

@@ -0,0 +1,80 @@
+package com.fs.his.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.his.domain.FsThirdDeviceData;
+import com.fs.watch.domain.vo.WatchSportDataByDateVo;
+import com.fs.watch.domain.vo.WatchSportDetailDataVo;
+import com.fs.watch.domain.vo.WatchSportRecordVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 三方设备健康数据Service接口
+ * 
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface IFsThirdDeviceDataService extends IService<FsThirdDeviceData>{
+    /**
+     * 查询三方设备健康数据
+     * 
+     * @param id 三方设备健康数据主键
+     * @return 三方设备健康数据
+     */
+    FsThirdDeviceData selectFsThirdDeviceDataById(Long id);
+
+    /**
+     * 查询三方设备健康数据列表
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 三方设备健康数据集合
+     */
+    List<FsThirdDeviceData> selectFsThirdDeviceDataList(FsThirdDeviceData fsThirdDeviceData);
+
+    /**
+     * 新增三方设备健康数据
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 结果
+     */
+    int insertFsThirdDeviceData(FsThirdDeviceData fsThirdDeviceData);
+
+    /**
+     * 修改三方设备健康数据
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 结果
+     */
+    int updateFsThirdDeviceData(FsThirdDeviceData fsThirdDeviceData);
+
+    /**
+     * 批量删除三方设备健康数据
+     * 
+     * @param ids 需要删除的三方设备健康数据主键集合
+     * @return 结果
+     */
+    int deleteFsThirdDeviceDataByIds(Long[] ids);
+
+    /**
+     * 删除三方设备健康数据信息
+     * 
+     * @param id 三方设备健康数据主键
+     * @return 结果
+     */
+    int deleteFsThirdDeviceDataById(Long id);
+
+    void insertFsThirdDeviceDataList(List<FsThirdDeviceData> fsThirdDeviceData);
+
+    List<FsThirdDeviceData> getLastData(Long userId,Integer deviceType,String deviceId);
+
+    List<WatchSportDataByDateVo> sportDataByDate(Date startTime, Date endTime, String deviceId);
+
+    WatchSportDetailDataVo dataByType(String deviceId, String type);
+
+    public WatchSportRecordVo getOtherData(Date startTime,
+                                           Date endTime,
+                                           String deviceId,
+                                           Integer recordType,
+                                           Integer status);
+}

+ 435 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsThirdDeviceDataServiceImpl.java

@@ -0,0 +1,435 @@
+package com.fs.his.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsThirdDeviceData;
+import com.fs.his.mapper.FsThirdDeviceDataMapper;
+import com.fs.his.service.IFsThirdDeviceDataService;
+import com.fs.watch.domain.vo.WatchSportDataByDateAndThirdDeviceVo;
+import com.fs.watch.domain.vo.WatchSportDataByDateVo;
+import com.fs.watch.domain.vo.WatchSportDetailDataVo;
+import com.fs.watch.domain.vo.WatchSportRecordVo;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAdjusters;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 三方设备健康数据Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-19
+ */
+@Service
+public class FsThirdDeviceDataServiceImpl extends ServiceImpl<FsThirdDeviceDataMapper, FsThirdDeviceData> implements IFsThirdDeviceDataService {
+
+    /**
+     * 查询三方设备健康数据
+     * 
+     * @param id 三方设备健康数据主键
+     * @return 三方设备健康数据
+     */
+    @Override
+    public FsThirdDeviceData selectFsThirdDeviceDataById(Long id)
+    {
+        return baseMapper.selectFsThirdDeviceDataById(id);
+    }
+
+    /**
+     * 查询三方设备健康数据列表
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 三方设备健康数据
+     */
+    @Override
+    public List<FsThirdDeviceData> selectFsThirdDeviceDataList(FsThirdDeviceData fsThirdDeviceData)
+    {
+        return baseMapper.selectFsThirdDeviceDataList(fsThirdDeviceData);
+    }
+
+    /**
+     * 新增三方设备健康数据
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 结果
+     */
+    @Override
+    public int insertFsThirdDeviceData(FsThirdDeviceData fsThirdDeviceData)
+    {
+        fsThirdDeviceData.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertFsThirdDeviceData(fsThirdDeviceData);
+    }
+
+    /**
+     * 修改三方设备健康数据
+     * 
+     * @param fsThirdDeviceData 三方设备健康数据
+     * @return 结果
+     */
+    @Override
+    public int updateFsThirdDeviceData(FsThirdDeviceData fsThirdDeviceData)
+    {
+        return baseMapper.updateFsThirdDeviceData(fsThirdDeviceData);
+    }
+
+    /**
+     * 批量删除三方设备健康数据
+     * 
+     * @param ids 需要删除的三方设备健康数据主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsThirdDeviceDataByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsThirdDeviceDataByIds(ids);
+    }
+
+    /**
+     * 删除三方设备健康数据信息
+     * 
+     * @param id 三方设备健康数据主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsThirdDeviceDataById(Long id)
+    {
+        return baseMapper.deleteFsThirdDeviceDataById(id);
+    }
+
+    @Override
+    public void insertFsThirdDeviceDataList(List<FsThirdDeviceData> fsThirdDeviceData) {
+        baseMapper.insertFsThirdDeviceDataList(fsThirdDeviceData);
+    }
+
+    @Override
+    public List<FsThirdDeviceData> getLastData(Long userId,Integer deviceType,String deviceId) {
+        return baseMapper.getLastData(userId,deviceType,deviceId);
+    }
+
+    /**
+     * 查询运动
+     * @param startTime
+     * @param endTime
+     * @param deviceId
+     * @return
+     */
+    @Override
+    public List<WatchSportDataByDateVo> sportDataByDate(Date startTime, Date endTime, String deviceId) {
+        //活动
+        ArrayList<FsThirdDeviceData> activity = new ArrayList<>();
+        List<FsThirdDeviceData> activity1 = baseMapper.selectListByDateAndDeviceIdAndRecordType(startTime, endTime, deviceId,5,null);
+        activity.addAll(activity1);
+//        activity.addAll(activity2);
+        //解析成活动数据
+        if(!activity.isEmpty()){
+            ArrayList<WatchSportDataByDateAndThirdDeviceVo> vos = new ArrayList<>();
+            for (FsThirdDeviceData fsThirdDeviceData : activity) {
+                String recordValue = fsThirdDeviceData.getRecordValue();
+                if (StringUtils.isNotBlank(recordValue)) {
+                    WatchSportDataByDateAndThirdDeviceVo data = new WatchSportDataByDateAndThirdDeviceVo();
+                    JSONObject jsonObject = JSONObject.parseObject(recordValue);
+                    data.setDeviceId(jsonObject.getString("deviceId"));
+                    data.setStep(Integer.valueOf(jsonObject.get("steps").toString()));
+                    data.setDistance(Float.parseFloat(jsonObject.get("distance").toString())*1000);
+                    data.setCalorie(Float.valueOf(jsonObject.get("calories").toString()));
+//                    data.setType(getActivityType(Integer.valueOf(jsonObject.get("sportType").toString())));
+                    data.setState(0);
+                    data.setCreateTime(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss",fsThirdDeviceData.getCreateTime()));
+                    vos.add(data);
+
+                }
+            }
+            // 计算时间差(以毫秒为单位)
+            long timeDifferenceInMillis = endTime.getTime() - startTime.getTime();
+
+            // 判断是一天还是一周
+            if (timeDifferenceInMillis > 24 * 60 * 60 * 1000) {
+                // 一周的数据(按天统计)
+                return getWeekData(vos, LocalDateTime.ofInstant(startTime.toInstant(), ZoneId.systemDefault()),
+                        LocalDateTime.ofInstant(endTime.toInstant(), ZoneId.systemDefault()));
+            } else {
+                // 一天的数据(按小时统计)
+                return getDayData(vos);
+            }
+        }
+        return null;
+
+    }
+
+    @Override
+    public WatchSportDetailDataVo dataByType(String deviceId, String type) {
+        if (StringUtils.isNotBlank(deviceId)) {
+            //详情
+            WatchSportDetailDataVo vo = new WatchSportDetailDataVo();
+            ArrayList<WatchSportDataByDateVo> list = new ArrayList<>();
+            //1.总计 距离 次数
+            WatchSportRecordVo recordVo = null; //总计只有一条数据
+            List<FsThirdDeviceData> otherDetail = null;
+            if ("其他运动".equals(type)) {
+                //游泳", "登山", "骑行", "跑步", "球类", "健走", "走路"
+                //25,17,1,9,21,5,13
+                recordVo = baseMapper.getOtherDataOther(null, null, deviceId, 6);
+                otherDetail = baseMapper.selectListByDateAndDeviceIdAndRecordTypeOther(null, null, deviceId, 6);
+            } else {
+                Integer sportType = getSportType(type);
+                //步数
+                recordVo = baseMapper.getOtherData(null, null, deviceId, 6, sportType);
+                //2.详情
+                otherDetail = baseMapper.selectListByDateAndDeviceIdAndRecordType(null, null, deviceId, 6, sportType);
+            }
+            long otherTime = 0L;
+            if (otherDetail != null && !otherDetail.isEmpty()) {
+                for (FsThirdDeviceData fsThirdDeviceData : otherDetail) {
+                    String recordValue = fsThirdDeviceData.getRecordValue();
+                    if (StringUtils.isNotBlank(recordValue)) {
+                        JSONObject jsonObject = JSONObject.parseObject(recordValue);
+                        float distance = Float.parseFloat(jsonObject.get("distance").toString());
+                        Float calories = Float.valueOf(jsonObject.get("calories").toString());
+                        String createTime = DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", fsThirdDeviceData.getCreateTime());
+                        float duration = Float.parseFloat(jsonObject.get("duration").toString());
+                        otherTime = otherTime + Long.parseLong(jsonObject.get("duration").toString());
+
+                        //详情
+                        WatchSportDataByDateVo watchSportDataByDateVo = new WatchSportDataByDateVo();
+                        watchSportDataByDateVo.setCreateTime(createTime);
+                        watchSportDataByDateVo.setCalorie(calories);
+                        watchSportDataByDateVo.setDistance(distance);
+                        watchSportDataByDateVo.setTime(duration);
+                        watchSportDataByDateVo.setSpeed((float) Math.round((distance)/(duration/3600)));
+                        list.add(watchSportDataByDateVo);
+                    }
+                }
+            }
+            if (recordVo != null) {
+                vo.setDistanceTotal(recordVo.getDistance()); //换算公里 保留两位
+                vo.setCountTotal(recordVo.getCount());
+                //1.2时长
+                BigDecimal timeInMinutes = new BigDecimal(otherTime)
+                        .divide(new BigDecimal(3600), 2, RoundingMode.HALF_UP);
+                vo.setTimeTotal(Float.valueOf(timeInMinutes+""));
+            }
+            vo.setList(list);
+            return vo;
+        }
+        return null;
+    }
+
+    private String getActivityType(Integer sportType) {
+        if (sportType == null) {
+            return null;
+        }
+        switch (sportType) {
+            case 1:
+                return "骑行";
+            case 5:
+                return "户外跑/健走";
+            case 9:
+                return "室内跑";
+            case 13:
+                return "步行";
+            case 17:
+                return "爬山";
+            case 21:
+                return "篮球";
+            case 25:
+                return "游泳";
+            case 29:
+                return "羽毛球";
+            case 33:
+                return "足球";
+            case 37:
+                return "椭圆机";
+            case 41:
+                return "瑜伽";
+            case 45:
+                return "乒乓球";
+            case 49:
+                return "跳绳";
+            case 53:
+                return "划船机";
+            case 65:
+                return "自由训练";
+            case 69:
+                return "网球";
+            case 85:
+                return "自由运动";
+            case 89:
+                return "力量训练";
+            case 105:
+                return "舞蹈";
+            case 109:
+                return "呼啦圈";
+            case 113:
+                return "高尔夫";
+            default:
+                return null;
+        }
+    }
+
+    private Integer getSportType(String activityType) {
+        //游泳", "登山", "骑行", "跑步", "球类", "健走", "走路" 25,17,1,9,21,5,13
+        if (activityType == null || activityType.isEmpty()) {
+            return null;
+        }
+        switch (activityType) {
+            case "骑行":
+                return 1;
+            case "健步走":
+                return 5;
+            case "跑步":
+                return 9;
+            case "走路":
+                return 13;
+            case "登山":
+                return 17;
+            case "球类":
+                return 21;
+            case "游泳":
+                return 25;
+            case "羽毛球":
+                return 29;
+            case "足球":
+                return 33;
+            case "椭圆机":
+                return 37;
+            case "瑜伽":
+                return 41;
+            case "乒乓球":
+                return 45;
+            case "跳绳":
+                return 49;
+            case "划船机":
+                return 53;
+            case "自由训练":
+                return 65;
+            case "网球":
+                return 69;
+            case "自由运动":
+                return 85;
+            case "力量训练":
+                return 89;
+            case "舞蹈":
+                return 105;
+            case "呼啦圈":
+                return 109;
+            case "高尔夫":
+                return 113;
+            default:
+                return null;
+
+        }
+    }
+
+    private List<WatchSportDataByDateVo> getWeekData(List<WatchSportDataByDateAndThirdDeviceVo> activity, LocalDateTime startTime, LocalDateTime endTime) {
+        // 按天分组
+        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        Map<LocalDate, List<WatchSportDataByDateAndThirdDeviceVo>> groupedByDate = activity.stream()
+                .collect(Collectors.groupingBy(data -> {
+                    LocalDateTime dateTime = LocalDateTime.parse(data.getCreateTime(), dateTimeFormatter);
+                    return dateTime.toLocalDate();
+                }));
+
+        // 确定一周的起始日期(周一)
+        LocalDate startOfWeek = startTime.toLocalDate().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
+        // 初始化结果列表,确保返回7天的数据
+        List<WatchSportDataByDateVo> result = new ArrayList<>();
+        for (int i = 0; i < 7; i++) {
+            LocalDate currentDate = startOfWeek.plusDays(i);
+//            LocalDate currentDate = startDate.plusDays(i);
+            List<WatchSportDataByDateAndThirdDeviceVo> dayData = groupedByDate.getOrDefault(currentDate, Collections.emptyList());
+
+            // 汇总每天数据
+            float totalDistance = (float) dayData.stream()
+                    .mapToDouble(data -> Optional.ofNullable(data.getDistance()).orElse(0f)).sum();
+            float totalCalorie = (float) dayData.stream()
+                    .mapToDouble(data -> Optional.ofNullable(data.getCalorie()).orElse(0f)).sum();
+            float totalTime = (float) dayData.stream()
+                    .mapToDouble(data -> Optional.ofNullable(data.getDuration()).orElse(0f)).sum();
+
+
+            // 创建统计对象
+            WatchSportDataByDateVo vo = new WatchSportDataByDateVo();
+            Float distance = Math.round(totalDistance/1000 * 100) / 100f;
+            vo.setDistance(distance); //换算公里
+            vo.setCalorie(Math.round(totalCalorie * 100) / 100f);
+            Float time = Math.round(totalTime * 100) / 100f;
+            if (time <=0){
+                time = 24*60*60f;
+            }
+            vo.setTime(time); //秒
+            float avgSpeed = time > 0 ? distance / (totalTime/3600) : 0;
+            vo.setSpeed(Math.round(avgSpeed * 100) / 100f);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    private List<WatchSportDataByDateVo> getDayData(List<WatchSportDataByDateAndThirdDeviceVo> activity) {
+        // 提前分配24小时结果数组
+        WatchSportDataByDateVo[] result = new WatchSportDataByDateVo[24];
+        for (int i = 0; i < 24; i++) {
+            result[i] = new WatchSportDataByDateVo(); // 初始化每小时的默认值
+            result[i].setDistance(0f);
+            result[i].setCalorie(0f);
+            result[i].setTime(0f); // 默认总时间为0秒
+            result[i].setSpeed(0f);
+        }
+
+        // 定义时间解析器
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+        // 按小时分组并直接统计
+        for (WatchSportDataByDateAndThirdDeviceVo data : activity) {
+            // 解析 createTime 的小时
+            LocalDateTime dateTime = LocalDateTime.parse(data.getCreateTime(), formatter);
+            int hour = dateTime.getHour();
+
+            // 获取对应小时的统计对象
+            WatchSportDataByDateVo vo = result[hour];
+
+            // 累加距离和卡路里
+            float dis = vo.getDistance() + ((Optional.ofNullable(data.getDistance()).orElse(0f)) / 1000);
+            float cal = vo.getCalorie() + Optional.ofNullable(data.getCalorie()).orElse(0f);
+            float time = vo.getTime() + Optional.ofNullable(data.getDuration()).orElse(0f);
+            vo.setDistance(Math.round(dis * 100) / 100f); //换算公里
+            vo.setCalorie(Math.round(cal * 100) / 100f);
+            // 假设每条记录表示1分钟运动///todo
+//            vo.setTime(vo.getTime() + 1f / 60); // 累加时间,单位为小时
+            if (time <= 0) {
+                time = 3600;
+            }
+            vo.setTime(time); // 累加时间,单位秒
+        }
+
+
+        // 计算每小时的平均速度
+        for (WatchSportDataByDateVo vo : result) {
+            if (vo.getTime() > 0) {
+                vo.setSpeed(Math.round((vo.getDistance() / (vo.getTime()/3600)) * 100) / 100f); // 平均速度 = 距离 / 时间
+            }
+        }
+        return Arrays.asList(result);
+    }
+
+    @Override
+    public WatchSportRecordVo getOtherData(Date startTime,
+                                    Date endTime,
+                                    String deviceId,
+                                    Integer recordType,
+                                    Integer status){
+        return baseMapper.getOtherData(startTime,endTime,deviceId,recordType,status);
+
+    }
+
+
+}

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

@@ -195,7 +195,7 @@ public interface FsStoreOrderScrmMapper
             "select o.*,u.phone,u.register_code,u.register_date,u.source, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber " +
             ", sas.audit_remark as auditRemark, sas.audit_reason_name as auditReasonName, sas.reason_level1_text as reasonValue1, sas.reason_level2_text as reasonValue2 " +
             " from fs_store_order_scrm o left join fs_user u on o.user_id=u.user_id  left join company c on c.company_id=o.company_id left join company_user cu on cu.user_id=o.company_user_id  " +
-            " LEFT JOIN fs_store_after_sales_scrm sas ON sas.order_id = o.id AND sas.status = 4 AND sas.is_del = 0" +
+            " LEFT JOIN fs_store_after_sales_scrm sas ON sas.order_code = o.order_code AND sas.status = 4 AND sas.is_del = 0" +
             "<if test = 'maps.productName != null and  maps.productName !=  \"\" '> " +
             "left join fs_store_order_item_scrm oi on o.id = oi.order_id "+
             "left join fs_store_product_scrm fsp on fsp.product_id = oi.product_id"+
@@ -805,7 +805,7 @@ public interface FsStoreOrderScrmMapper
             ", sas.audit_remark as auditRemark, sas.reason_level1_text as reasonValue1, sas.reason_level2_text as reasonValue2 " +
             " from fs_store_order_scrm o  left JOIN fs_store_product_package_scrm p on o.package_id=p.package_id left join fs_user u on o.user_id=u.user_id  " +
             " left join company c on c.company_id=o.company_id left join company_user cu on cu.user_id=o.company_user_id left join crm_customer cc on cc.customer_id=o.customer_id " +
-            " LEFT JOIN fs_store_after_sales_scrm sas ON sas.order_id = o.id AND sas.status = 4 AND sas.is_del = 0" +
+            " LEFT JOIN fs_store_after_sales_scrm sas ON sas.order_code = o.order_code AND sas.status = 4 AND sas.is_del = 0" +
             "where 1=1 " +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/vo/StoreOperMainVO.java

@@ -2,8 +2,10 @@ package com.fs.hisStore.vo;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 @Data
+@NoArgsConstructor
 @AllArgsConstructor
 public class StoreOperMainVO {
     private String id;

+ 14 - 1
fs-service/src/main/java/com/fs/live/domain/Live.java

@@ -39,6 +39,11 @@ public class   Live extends BaseEntity {
      */
     private Long companyUserId;
 
+    /**
+     * 营期ID(非空表示归属「训练营-营期」直播间,与普通直播列表区分)
+     */
+    private Long trainingPeriodId;
+
     /**
      * 达人ID
      */
@@ -121,7 +126,7 @@ public class   Live extends BaseEntity {
     /** 直播配置 */
     private String configJson;
 
-    /** 直播审核状态,销售端修改后需要总后台审核 0未审核 1已审核*/
+    /** 直播审核状态:0待审核 1已通过 2已驳回(训练营直播间新建/企业修改后需总后台审核;C 端仅展示已通过) */
     private Integer isAudit;
     /** 创建时间 */
     private Date createTime;
@@ -136,4 +141,12 @@ public class   Live extends BaseEntity {
 
     private Integer pageNum;
     private Integer pageSize;
+
+    /** 查询用:为 true 时仅查普通直播(training_period_id 为空),企业端默认排除训练营直播间 */
+    @TableField(exist = false)
+    private Boolean excludeCampLive;
+
+    /** 查询用:为 true 时仅查训练营直播间(training_period_id 非空),总后台审核列表等 */
+    @TableField(exist = false)
+    private Boolean onlyTrainingCampLive;
 }

+ 22 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCourseQuestionRel.java

@@ -0,0 +1,22 @@
+package com.fs.live.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 直播间与课程题库试题关联
+ */
+@Data
+public class LiveCourseQuestionRel {
+
+    private Long relId;
+
+    private Long liveId;
+
+    private Long questionBankId;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 37 - 0
fs-service/src/main/java/com/fs/live/domain/LiveTrainingCamp.java

@@ -0,0 +1,37 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播训练营
+ *
+ * @author fs
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveTrainingCamp extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long campId;
+
+    private Long companyId;
+
+    private String campName;
+
+    private Integer sortOrder;
+
+    /** 0正常 1停用 */
+    private Integer status;
+
+    private Integer isDel;
+
+    private String description;
+
+    /** 关联展示:企业名称(总后台列表) */
+    @TableField(exist = false)
+    private String companyName;
+}

+ 52 - 0
fs-service/src/main/java/com/fs/live/domain/LiveTrainingPeriod.java

@@ -0,0 +1,52 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 直播训练营营期
+ *
+ * @author fs
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveTrainingPeriod extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long periodId;
+
+    private Long campId;
+
+    private String periodName;
+
+    /** 营期封面图 URL */
+    private String periodImgUrl;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String startTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String endTime;
+
+    private Integer sortOrder;
+
+    /** 0正常 1停用 */
+    private Integer status;
+
+    private Integer isDel;
+
+    private String description;
+
+    /** 查询展示:训练营名称 */
+    private String campName;
+
+    /** 列表/更新权限校验:当前企业 */
+    @TableField(exist = false)
+    private Long companyId;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCourseQuestionRelMapper.java

@@ -0,0 +1,28 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCourseQuestionRel;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 直播间-课程题库关联
+ */
+public interface LiveCourseQuestionRelMapper {
+
+    List<LiveQuestionLiveVO> selectLinkedByLiveId(@Param("liveId") Long liveId);
+
+    List<LiveQuestionLiveVO> selectOptionQuestionBank(@Param("liveId") Long liveId,
+                                                      @Param("title") String title,
+                                                      @Param("type") Integer type);
+
+    int insertIgnore(@Param("liveId") Long liveId, @Param("questionBankId") Long questionBankId);
+
+    int deleteByRelIds(@Param("liveId") Long liveId, @Param("relIds") Long[] relIds);
+
+    /**
+     * 校验关联是否属于该直播间,并解析课程题库主键
+     */
+    Long selectQuestionBankIdByLiveAndRel(@Param("liveId") Long liveId, @Param("relId") Long relId);
+}

+ 8 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -6,6 +6,7 @@ import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.Live;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.vo.LiveListVo;
+import com.fs.live.vo.TrainingLiveAuditVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -50,6 +51,8 @@ public interface LiveMapper
      */
     public List<Live> selectLiveList(Live live);
 
+    List<TrainingLiveAuditVO> selectTrainingLiveAuditList(Live live);
+
     /**
      * 新增直播
      *
@@ -235,6 +238,7 @@ public interface LiveMapper
 
     @Select({"<script>" +
             " SELECT * FROM live WHERE is_audit = 1 and is_del = 0 and status in (1,2,4) and live_type in (2,3) " +
+            " and training_period_id is null " +
             "  <if test='live.liveName!=null' > and live_name like concat('%',#{live.liveName},'%') </if> " +
             " order by create_time desc" +
             " </script>"})
@@ -254,8 +258,12 @@ public interface LiveMapper
 
     @Select({"<script>" +
             " SELECT * FROM live WHERE is_audit = 1 and is_del = 0 and status in (1,2,4)" +
+            " and training_period_id is null " +
             "  <if test='live.liveName!=null' > and live_name like concat('%',#{live.liveName},'%') </if> " +
             " order by create_time desc" +
             " </script>"})
     List<Live> listToLiveNoEndNew(@Param("live") Live live);
+
+    @Select("SELECT COUNT(1) FROM live WHERE is_del = 0 AND training_period_id = #{periodId}")
+    int countLiveByTrainingPeriodId(@Param("periodId") Long periodId);
 }

+ 25 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveTrainingCampMapper.java

@@ -0,0 +1,25 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveTrainingCamp;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 直播训练营 Mapper
+ */
+public interface LiveTrainingCampMapper {
+
+    LiveTrainingCamp selectLiveTrainingCampById(@Param("campId") Long campId);
+
+    List<LiveTrainingCamp> selectLiveTrainingCampList(LiveTrainingCamp query);
+
+    int insertLiveTrainingCamp(LiveTrainingCamp camp);
+
+    int updateLiveTrainingCamp(LiveTrainingCamp camp);
+
+    int logicDeleteLiveTrainingCampByIds(@Param("campIds") Long[] campIds, @Param("companyId") Long companyId,
+                                         @Param("createBy") String createBy);
+
+    List<LiveTrainingCamp> selectLiveTrainingCampListAdmin(LiveTrainingCamp query);
+}

+ 27 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveTrainingPeriodMapper.java

@@ -0,0 +1,27 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveTrainingPeriod;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 直播训练营营期 Mapper
+ */
+public interface LiveTrainingPeriodMapper {
+
+    LiveTrainingPeriod selectLiveTrainingPeriodById(@Param("periodId") Long periodId);
+
+    List<LiveTrainingPeriod> selectLiveTrainingPeriodList(LiveTrainingPeriod query);
+
+    int countPeriodByCampId(@Param("campId") Long campId);
+
+    int insertLiveTrainingPeriod(LiveTrainingPeriod period);
+
+    int updateLiveTrainingPeriod(LiveTrainingPeriod period);
+
+    int logicDeleteLiveTrainingPeriodByIds(@Param("periodIds") Long[] periodIds, @Param("companyId") Long companyId,
+                                          @Param("createBy") String createBy);
+
+    List<LiveTrainingPeriod> selectLiveTrainingPeriodListAdmin(LiveTrainingPeriod query);
+}

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