Browse Source

Merge remote-tracking branch 'origin/master' into matser

吴树波 2 ngày trước cách đây
mục cha
commit
ef959c886f
100 tập tin đã thay đổi với 3902 bổ sung228 xóa
  1. 28 1
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java
  2. 114 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyWithdrawDetailAdminController.java
  3. 3 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchCommentController.java
  4. 6 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  5. 22 4
      fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java
  6. 64 0
      fs-admin/src/main/java/com/fs/course/controller/PublicCourseWatchStatisticsController.java
  7. 1 1
      fs-admin/src/main/java/com/fs/his/task/Task.java
  8. 2 0
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  9. 89 0
      fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java
  10. 224 0
      fs-admin/src/main/java/com/fs/live/controller/LiveTrainingCampAdminController.java
  11. 2 1
      fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  12. 1 1
      fs-company/src/main/java/com/fs/FsCompanyApplication.java
  13. 222 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java
  14. 19 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  15. 88 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWithdrawDetailController.java
  16. 58 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  17. 12 7
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  18. 58 58
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  19. 69 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveQuestionLiveController.java
  20. 182 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveTrainingCampController.java
  21. 5 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  22. 2 2
      fs-doctor-app/src/main/resources/application.yml
  23. 94 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  24. 23 4
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  25. 43 0
      fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java
  26. 5 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java
  27. 64 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java
  28. 22 7
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  29. 17 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWithdrawDetailMapper.java
  30. 162 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java
  31. 20 0
      fs-service/src/main/java/com/fs/company/param/CompanyWithdrawDetailAdminParam.java
  32. 7 0
      fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java
  33. 69 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  34. 61 0
      fs-service/src/main/java/com/fs/company/service/ICompanySiptaskInfoService.java
  35. 1 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  36. 13 7
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java
  37. 24 0
      fs-service/src/main/java/com/fs/company/service/ICompanyWithdrawDetailService.java
  38. 99 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  39. 58 13
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  40. 91 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySiptaskInfoServiceImpl.java
  41. 5 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  42. 60 13
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  43. 144 26
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  44. 64 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWithdrawDetailServiceImpl.java
  45. 73 9
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  46. 21 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java
  47. 71 35
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  48. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java
  49. 15 14
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java
  50. 90 0
      fs-service/src/main/java/com/fs/company/util/CompanyTuiMoneyCalc.java
  51. 14 0
      fs-service/src/main/java/com/fs/company/vo/CalleeRoboticCallOutCountVO.java
  52. 57 0
      fs-service/src/main/java/com/fs/company/vo/CompanyWithdrawDetailVO.java
  53. 14 0
      fs-service/src/main/java/com/fs/company/vo/CustomerRoboticCallOutCountVO.java
  54. 14 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBizGroupVO.java
  55. 75 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java
  56. 14 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallIvrVO.java
  57. 4 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseWatchComment.java
  58. 29 0
      fs-service/src/main/java/com/fs/course/domain/FsPublicCourseTrafficLog.java
  59. 21 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourse.java
  60. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseCategory.java
  61. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java
  62. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java
  63. 42 0
      fs-service/src/main/java/com/fs/course/mapper/FsPublicCourseTrafficLogMapper.java
  64. 6 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCategoryMapper.java
  65. 9 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java
  66. 18 0
      fs-service/src/main/java/com/fs/course/mapper/PublicCourseWatchStatisticsMapper.java
  67. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchCommentPageParam.java
  68. 3 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchCommentSaveParam.java
  69. 35 0
      fs-service/src/main/java/com/fs/course/param/FsUserCourseCategoryAppQueryParam.java
  70. 38 0
      fs-service/src/main/java/com/fs/course/param/FsUserCoursePublicAppQueryParam.java
  71. 30 0
      fs-service/src/main/java/com/fs/course/param/LiveQuizSubmitUParam.java
  72. 25 0
      fs-service/src/main/java/com/fs/course/param/PublicCourseWatchStatQueryParam.java
  73. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseQuestionBankService.java
  74. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseCategoryService.java
  75. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java
  76. 3 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  77. 17 0
      fs-service/src/main/java/com/fs/course/service/IPublicCourseWatchStatisticsService.java
  78. 82 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java
  79. 3 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java
  80. 13 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCategoryServiceImpl.java
  81. 5 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  82. 62 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  83. 66 0
      fs-service/src/main/java/com/fs/course/service/impl/PublicCourseWatchStatisticsServiceImpl.java
  84. 3 0
      fs-service/src/main/java/com/fs/course/vo/FsCourseWatchCommentListVO.java
  85. 56 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCoursePublicAppVO.java
  86. 5 0
      fs-service/src/main/java/com/fs/course/vo/FsVideoResourceVO.java
  87. 61 0
      fs-service/src/main/java/com/fs/course/vo/PublicCourseWatchStatCatalogVO.java
  88. 55 0
      fs-service/src/main/java/com/fs/course/vo/PublicCourseWatchStatCourseVO.java
  89. 5 5
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  90. 2 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java
  91. 2 0
      fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java
  92. 2 0
      fs-service/src/main/java/com/fs/crm/vo/CrmMyCustomerListQueryVO.java
  93. 14 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatConversation.java
  94. 3 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatSession.java
  95. 3 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptRole.java
  96. 1 1
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  97. 212 14
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  98. 10 0
      fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java
  99. 1 1
      fs-service/src/main/java/com/fs/his/config/StoreConfig.java
  100. 48 0
      fs-service/src/main/java/com/fs/his/domain/FsThirdDeviceData.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);
     }
 

+ 114 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyWithdrawDetailAdminController.java

@@ -0,0 +1,114 @@
+package com.fs.company.controller;
+
+import cn.hutool.json.JSONUtil;
+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.model.LoginUser;
+import com.fs.common.core.page.PageDomain;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.page.TableSupport;
+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.param.CompanyParam;
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyWithdrawDetailService;
+import com.fs.company.vo.CompanyNameVO;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import com.fs.course.config.CourseConfig;
+import com.fs.framework.web.service.TokenService;
+import com.fs.system.service.ISysConfigService;
+import com.github.pagehelper.PageHelper;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 管理后台-提现明细(多公司、类型筛选;与销售端数据源一致)
+ */
+@RestController
+@RequestMapping("/company/withdrawDetailAdmin")
+public class CompanyWithdrawDetailAdminController extends BaseController {
+
+    @Autowired
+    private ICompanyWithdrawDetailService companyWithdrawDetailService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 分公司下拉:关键词模糊匹配公司名称(最多 500 条)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/companyOptions")
+    public AjaxResult companyOptions(@RequestParam(value = "keyword", required = false) String keyword) {
+        CompanyParam param = new CompanyParam();
+        if (StringUtils.isNotEmpty(keyword)) {
+            param.setCompanyName(keyword.trim());
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (!loginUser.isAdmin() && config.getDept() != null && config.getDept()) {
+            param.setDeptId(loginUser.getDeptId());
+        }
+        PageHelper.startPage(1, 500);
+        List<CompanyNameVO> list = companyService.selectCompanyNameVOList(param);
+        return AjaxResult.success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(
+            @RequestParam(required = false) List<Long> companyIds,
+            @RequestParam(required = false) List<Integer> detailTypes) {
+        PageDomain page = TableSupport.buildPageRequest();
+        int pageNum = page.getPageNum() != null && page.getPageNum() > 0 ? page.getPageNum() : 1;
+        int pageSize = page.getPageSize() != null && page.getPageSize() > 0 ? page.getPageSize() : 10;
+        PageHelper.startPage(pageNum, pageSize);
+
+        CompanyWithdrawDetailAdminParam p = new CompanyWithdrawDetailAdminParam();
+        p.setCompanyIds(companyIds);
+        p.setDetailTypes(detailTypes);
+
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailListAdmin(p);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        long start = (long) (pageNum - 1) * pageSize;
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo((int) (start + i + 1));
+        }
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:export')")
+    @Log(title = "提现明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(
+            @RequestParam(required = false) List<Long> companyIds,
+            @RequestParam(required = false) List<Integer> detailTypes) {
+        CompanyWithdrawDetailAdminParam p = new CompanyWithdrawDetailAdminParam();
+        p.setCompanyIds(companyIds);
+        p.setDetailTypes(detailTypes);
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailListAdmin(p);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo(i + 1);
+        }
+        ExcelUtil<CompanyWithdrawDetailVO> util = new ExcelUtil<>(CompanyWithdrawDetailVO.class);
+        return util.exportExcel(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));
+    }
+}

+ 2 - 1
fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -255,7 +255,8 @@ public class CompanyUserController extends AppBaseController {
         // 查询公司下销售
         CompanyUser companyUser = new CompanyUser();
         companyUser.setCompanyId(getCompanyId());
-        List<CompanyUser> companyUsers = companyUserService.selectCompanyUserList(companyUser);
+//        List<CompanyUser> companyUsers = companyUserService.selectCompanyUserList(companyUser);
+        List<CompanyUser> companyUsers = companyUserService.selectCompanyUserListByCompanyId(companyUser);
         return R.ok().put("data",companyUsers);
     }
 

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

+ 88 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWithdrawDetailController.java

@@ -0,0 +1,88 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.PageDomain;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.page.TableSupport;
+import com.github.pagehelper.PageHelper;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.service.ICompanyWithdrawDetailService;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 销售端-提现明细(新接口,数据自 2026-03-27 起)
+ */
+@RestController
+@RequestMapping("/company/withdrawDetail")
+public class CompanyWithdrawDetailController extends BaseController {
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ICompanyWithdrawDetailService companyWithdrawDetailService;
+
+    /**
+     * 分公司名称 + 可提现金额(实时,取 company.money)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/summary")
+    public R summary() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        return R.ok(companyWithdrawDetailService.summary(companyId));
+    }
+
+    /**
+     * 分页列表:订单记录时间倒序,默认每页 10 条(由前端传 pageSize)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/list")
+    public TableDataInfo list() {
+        PageDomain page = TableSupport.buildPageRequest();
+        int pageNum = page.getPageNum() != null && page.getPageNum() > 0 ? page.getPageNum() : 1;
+        int pageSize = page.getPageSize() != null && page.getPageSize() > 0 ? page.getPageSize() : 10;
+        PageHelper.startPage(pageNum, pageSize);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailList(companyId);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        long start = (long) (pageNum - 1) * pageSize;
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo((int) (start + i + 1));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出全部字段(同列表,不受分页限制)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:export')")
+    @Log(title = "提现明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailList(companyId);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo(i + 1);
+        }
+        ExcelUtil<CompanyWithdrawDetailVO> util = new ExcelUtil<>(CompanyWithdrawDetailVO.class);
+        return util.exportExcel(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;
 }

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

+ 17 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWithdrawDetailMapper.java

@@ -0,0 +1,17 @@
+package com.fs.company.mapper;
+
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 提现明细(基于 company_money_logs 聚合展示)
+ */
+public interface CompanyWithdrawDetailMapper {
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailList(@Param("companyId") Long companyId);
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailListAdmin(@Param("p") CompanyWithdrawDetailAdminParam p);
+}

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

+ 20 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWithdrawDetailAdminParam.java

@@ -0,0 +1,20 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 管理端-提现明细查询(多公司、多明细类型)
+ */
+@Data
+public class CompanyWithdrawDetailAdminParam {
+
+    /** 分公司 ID,空表示不限 */
+    private List<Long> companyIds;
+
+    /**
+     * 明细类型:1订单金额入账 2订单金额扣减 3总公司充值 4总公司扣款 5分公司提现 6总公司驳回;空表示全部
+     */
+    private List<Integer> detailTypes;
+}

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

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

@@ -48,6 +48,7 @@ public interface ICompanyUserService {
      * @return 物业公司管理员信息集合
      */
     public List<CompanyUser> selectCompanyUserList(CompanyUser companyUser);
+    public List<CompanyUser> selectCompanyUserListByCompanyId(CompanyUser companyUser);
 
     /**
      * 新增物业公司管理员信息

+ 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

+ 24 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyWithdrawDetailService.java

@@ -0,0 +1,24 @@
+package com.fs.company.service;
+
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 销售端提现明细
+ */
+public interface ICompanyWithdrawDetailService {
+
+    Map<String, Object> summary(Long companyId);
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailList(Long companyId);
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailListAdmin(CompanyWithdrawDetailAdminParam param);
+
+    /**
+     * PRD:订单金额扣减类中 logs_type=6 在库中为成本返还入账,展示为与「扣减」一致的符号口径
+     */
+    void normalizeDisplayAmounts(List<CompanyWithdrawDetailVO> list);
+}

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

+ 58 - 13
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.company.service.impl;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
@@ -20,6 +21,7 @@ import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyLiveShowParam;
 import com.fs.company.param.CompanyParam;
 import com.fs.company.service.*;
+import com.fs.company.util.CompanyTuiMoneyCalc;
 import com.fs.company.vo.*;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.config.RedPacketConfig;
@@ -33,8 +35,10 @@ import com.fs.his.domain.FsStorePayment;
 import com.fs.his.dto.InquiryConfigDTO;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.vo.OptionsVO;
+import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
+import com.fs.hisStore.mapper.FsStoreAfterSalesScrmMapper;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.mapper.LiveOrderMapper;
@@ -60,6 +64,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
@@ -143,6 +148,15 @@ public class CompanyServiceImpl implements ICompanyService
     @Autowired
     CompanyBindGatewayMapper companyBindGatewayMapper;
 
+    @Value("${cloud_host.company_name}")
+    private String companyName;
+
+
+    private static final BigDecimal BJZM_PARTIAL_REFUND_COMMISSION_RATE = new BigDecimal("0.20");
+
+    @Autowired
+    private FsStoreAfterSalesScrmMapper fsStoreAfterSalesScrmMapper;
+
     @Override
     public List<CompanyVO> liveShowList(CompanyParam param) {
         return companyMapper.liveShowList(param);
@@ -162,9 +176,12 @@ public class CompanyServiceImpl implements ICompanyService
             if(company!=null){
                 String json =configService.selectConfigByKey("store.config");
                 com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率
                 Double rate=config.getTuiMoneyRate()/100d;
-                BigDecimal tuiMoney=order.getPayPrice().subtract(order.getTotalPrice().multiply(new BigDecimal(rate)));
+                BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                        CompanyTuiMoneyCalc.paidAmount(order),
+                        CompanyTuiMoneyCalc.freight(order));
+                BigDecimal tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 logger.info("写入公司推广佣金:"+tuiMoney);
                 company.setTuiMoney(company.getTuiMoney().add(tuiMoney));
                 companyMapper.updateCompany(company);
@@ -626,9 +643,12 @@ public class CompanyServiceImpl implements ICompanyService
             if(company!=null){
                 String json =configService.selectConfigByKey("his.store");
                 StoreConfig config= JSONUtil.toBean(json,StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率
                 Double rate=config.getTuiMoneyRate()/100d;
-                BigDecimal tuiMoney=order.getPayPrice().subtract(order.getTotalPrice().multiply(new BigDecimal(rate)));
+                BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                        CompanyTuiMoneyCalc.paidAmount(order),
+                        CompanyTuiMoneyCalc.freight(order));
+                BigDecimal tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 logger.info("写入公司推广佣金:"+tuiMoney);
                 company.setTuiMoney(company.getTuiMoney().add(tuiMoney));
                 companyMapper.updateCompany(company);
@@ -648,10 +668,13 @@ public class CompanyServiceImpl implements ICompanyService
             if(company!=null){
                 String json =configService.selectConfigByKey("his.store");
                 StoreConfig config= JSONUtil.toBean(json,StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率
                 if (config.getTuiMoneyRate()!=null){
                     Double rate=config.getTuiMoneyRate()/100d;
-                    BigDecimal tuiMoney=order.getPayPrice().subtract(order.getTotalPrice().multiply(new BigDecimal(rate)));
+                    BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                            CompanyTuiMoneyCalc.paidAmount(order),
+                            CompanyTuiMoneyCalc.freight(order));
+                    BigDecimal tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                     logger.info("写入公司推广佣金:"+tuiMoney);
                     company.setTuiMoney(company.getTuiMoney().add(tuiMoney));
                     companyMapper.updateCompany(company);
@@ -708,11 +731,14 @@ public class CompanyServiceImpl implements ICompanyService
                 // 卓美,按照润天进行百分比进行分佣
                 String json =configService.selectConfigByKey("store.config");
                 com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率;未配置费率时仍为应付 payPrice
                 BigDecimal tuiMoney = BigDecimal.ZERO;
                 if (config != null && config.getTuiMoneyRate() != null) {
                     Double rate = config.getTuiMoneyRate() / 100d;
-                    tuiMoney = order.getTotalPrice().multiply(new BigDecimal(rate));
+                    BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                            CompanyTuiMoneyCalc.paidAmount(order),
+                            CompanyTuiMoneyCalc.freight(order));
+                    tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 } else {
                     tuiMoney = order.getPayPrice();
                 }
@@ -731,6 +757,7 @@ public class CompanyServiceImpl implements ICompanyService
                 FsStoreOrderScrm fsStoreOrder = new FsStoreOrderScrm();
                 fsStoreOrder.setId(order.getId());
                 fsStoreOrder.setTuiMoneyStatus(1);
+                fsStoreOrder.setTuiMoney(tuiMoney);
                 storeOrderScrmMapper.updateFsStoreOrder(fsStoreOrder);
             }
         }
@@ -744,11 +771,14 @@ public class CompanyServiceImpl implements ICompanyService
                 // 卓美,按照润天进行百分比进行分佣
                 String json =configService.selectConfigByKey("store.config");
                 com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率;未配置费率时仍为 payPrice
                 BigDecimal tuiMoney = BigDecimal.ZERO;
                 if (config != null && config.getTuiMoneyRate() != null) {
                     double rate = config.getTuiMoneyRate() / 100d;
-                    tuiMoney = order.getTotalPrice().multiply(new BigDecimal(rate));
+                    BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                            CompanyTuiMoneyCalc.paidAmount(order),
+                            CompanyTuiMoneyCalc.freight(order));
+                    tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 } else {
                     tuiMoney = order.getPayPrice();
                 }
@@ -806,14 +836,29 @@ public class CompanyServiceImpl implements ICompanyService
         if(order.getCompanyId()>0){
             Company company=companyMapper.selectCompanyByIdForUpdate(order.getCompanyId());
             if(company!=null){
-                company.setMoney(company.getMoney().subtract(order.getTuiMoney()));
-                company.setTuiMoney(company.getTuiMoney().subtract(order.getTuiMoney()));
+                BigDecimal clawback = order.getTuiMoney();
+                BigDecimal logMoney = order.getTuiMoney().multiply(new BigDecimal(-1));
+
+                // 260415 卓美财务需求,希望部分退款:部分退款金额 * 20%
+                if ("北京卓美".equals(companyName)) {
+                    FsStoreAfterSalesScrm after = fsStoreAfterSalesScrmMapper.selectFsStoreAfterSalesByOrderCode(order.getOrderCode());
+                    BigDecimal payMoney = order.getPayMoney() != null ? order.getPayMoney() : BigDecimal.ZERO;
+                    BigDecimal refundAmount = after != null && after.getRefundAmount() != null ? after.getRefundAmount() : null;
+                    if (refundAmount != null && payMoney.compareTo(refundAmount) != 0) {
+                        // 部分退款:公司扣回金额 / 日志金额 = -(退款金额 × 20%)
+                        clawback = refundAmount.multiply(BJZM_PARTIAL_REFUND_COMMISSION_RATE).setScale(2, RoundingMode.HALF_UP);
+                        logMoney = clawback.negate();
+                    }
+                }
+
+                company.setMoney(company.getMoney().subtract(clawback));
+                company.setTuiMoney(company.getTuiMoney().subtract(clawback));
                 companyMapper.updateCompany(company);
                 //写入日志
                 CompanyMoneyLogs log=new CompanyMoneyLogs();
                 log.setCompanyId(order.getCompanyId());
                 log.setRemark("订单佣金退款");
-                log.setMoney(order.getTuiMoney().multiply(new BigDecimal(-1)));
+                log.setMoney(logMoney);
                 log.setLogsType(4);
                 log.setBalance(company.getMoney());
                 log.setCreateTime(new Date());

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

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

@@ -179,6 +179,11 @@ public class CompanyUserServiceImpl implements ICompanyUserService
         return companyUserMapper.selectCompanyUserList(companyUser);
     }
 
+    @Override
+    public List<CompanyUser> selectCompanyUserListByCompanyId(CompanyUser companyUser) {
+        return companyUserMapper.selectCompanyUserList(companyUser);
+    }
+
     /**
      * 新增物业公司管理员信息
      *

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

+ 64 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWithdrawDetailServiceImpl.java

@@ -0,0 +1,64 @@
+package com.fs.company.service.impl;
+
+import com.fs.company.mapper.CompanyWithdrawDetailMapper;
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyWithdrawDetailService;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import com.fs.company.domain.Company;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class CompanyWithdrawDetailServiceImpl implements ICompanyWithdrawDetailService {
+
+    @Autowired
+    private CompanyWithdrawDetailMapper companyWithdrawDetailMapper;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    @Override
+    public Map<String, Object> summary(Long companyId) {
+        Company company = companyService.selectCompanyById(companyId);
+        Map<String, Object> m = new HashMap<>(4);
+        if (company != null) {
+            m.put("companyName", company.getCompanyName());
+            m.put("withdrawableMoney", company.getMoney() != null ? company.getMoney() : BigDecimal.ZERO);
+        } else {
+            m.put("companyName", "");
+            m.put("withdrawableMoney", BigDecimal.ZERO);
+        }
+        return m;
+    }
+
+    @Override
+    public List<CompanyWithdrawDetailVO> selectWithdrawDetailList(Long companyId) {
+        return companyWithdrawDetailMapper.selectWithdrawDetailList(companyId);
+    }
+
+    @Override
+    public List<CompanyWithdrawDetailVO> selectWithdrawDetailListAdmin(CompanyWithdrawDetailAdminParam param) {
+        if (param == null) {
+            param = new CompanyWithdrawDetailAdminParam();
+        }
+        return companyWithdrawDetailMapper.selectWithdrawDetailListAdmin(param);
+    }
+
+    @Override
+    public void normalizeDisplayAmounts(List<CompanyWithdrawDetailVO> list) {
+        if (list == null) {
+            return;
+        }
+        for (CompanyWithdrawDetailVO vo : list) {
+            if (vo.getLogsType() != null && vo.getLogsType() == 6 && vo.getAmount() != null) {
+                vo.setAmount(vo.getAmount().negate());
+            }
+        }
+    }
+}

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

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

+ 71 - 35
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());
-        // 技能组(转人工客服分组,可选)
+//        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());
+
         // 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);

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java

@@ -31,7 +31,7 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
     private static final CompanyWxClientMapper companyWxClientMapper = SpringUtils.getBean(CompanyWxClientMapper.class);
     @SuppressWarnings("unchecked")
     private static final RedisCacheT<String> redisCache = SpringUtils.getBean(RedisCacheT.class);
-    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:";
+    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:%s:";
     /**
      * 默认加微超时时间(分钟)
      */

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

+ 90 - 0
fs-service/src/main/java/com/fs/company/util/CompanyTuiMoneyCalc.java

@@ -0,0 +1,90 @@
+package com.fs.company.util;
+
+import com.fs.his.domain.FsStoreOrder;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.live.domain.LiveOrder;
+
+import java.math.BigDecimal;
+
+/**
+ * 分公司佣金/入账基数:(实付金额 - 运费)× 费率
+ */
+public final class CompanyTuiMoneyCalc {
+
+    private CompanyTuiMoneyCalc() {
+    }
+
+    /** 实付金额:优先取 payMoney,否则 payPrice */
+    public static BigDecimal paidAmount(FsStoreOrder order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayMoney() != null) {
+            return order.getPayMoney();
+        }
+        return order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+    }
+
+    /** 运费:his 订单取 freightPrice */
+    public static BigDecimal freight(FsStoreOrder order) {
+        if (order == null || order.getFreightPrice() == null) {
+            return BigDecimal.ZERO;
+        }
+        return order.getFreightPrice();
+    }
+
+    public static BigDecimal paidAmount(FsStoreOrderScrm order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayPrice() != null) {
+            return order.getPayPrice();
+        }
+        return order.getPayMoney() != null ? order.getPayMoney() : BigDecimal.ZERO;
+    }
+
+    /** 运费:商城 SCRM 订单取用户实付邮费 payPostage */
+    public static BigDecimal freight(FsStoreOrderScrm order) {
+        if (order == null || order.getPayPostage() == null) {
+            return BigDecimal.ZERO;
+        }
+
+        return order.getPayPostage();
+    }
+
+    public static BigDecimal paidAmount(LiveOrder order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayMoney() != null) {
+            return order.getPayMoney();
+        }
+        return order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+    }
+
+    /** 运费:直播单优先 payPostage,否则 totalPostage */
+    public static BigDecimal freight(LiveOrder order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayPostage() != null) {
+            return order.getPayPostage();
+        }
+        if (order.getTotalPostage() != null) {
+            return order.getTotalPostage();
+        }
+        return BigDecimal.ZERO;
+    }
+
+    /** 计佣基数:max(0, 实付 - 运费) */
+    public static BigDecimal commissionBase(BigDecimal paid, BigDecimal freightAmt) {
+        BigDecimal f = freightAmt != null ? freightAmt : BigDecimal.ZERO;
+        BigDecimal p = paid != null ? paid : BigDecimal.ZERO;
+        BigDecimal b = p.subtract(f);
+        return b.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : b;
+    }
+
+    public static BigDecimal multiplyRate(BigDecimal base, double rateFraction) {
+        return base.multiply(BigDecimal.valueOf(rateFraction));
+    }
+}

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

+ 57 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyWithdrawDetailVO.java

@@ -0,0 +1,57 @@
+package com.fs.company.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 销售端-提现明细分页/导出
+ */
+@Data
+public class CompanyWithdrawDetailVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 内部用:原始 logs_type */
+    private Integer logsType;
+
+    private Long logsId;
+
+    @Excel(name = "序号", sort = 1)
+    private Integer serialNo;
+
+    @Excel(name = "公司名称", sort = 2)
+    private String companyName;
+
+    @Excel(name = "所属销售", sort = 3)
+    private String salesName;
+
+    @Excel(name = "订单号", sort = 4)
+    private String orderCode;
+
+    @Excel(name = "交易单号", sort = 5)
+    private String tradeNo;
+
+    @Excel(name = "订单状态", sort = 6)
+    private String orderStatusText;
+
+    /** 订单原始状态码,与商城订单列表字典 store_order_status 一致;非订单行无此值 */
+    private Integer orderStatus;
+
+    @Excel(name = "售后状态", sort = 7)
+    private String afterSalesStatusText;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "订单记录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 8)
+    private Date recordTime;
+
+    @Excel(name = "明细类型", sort = 9)
+    private String detailTypeText;
+
+    @Excel(name = "金额", sort = 10)
+    private BigDecimal amount;
+}

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

+ 29 - 0
fs-service/src/main/java/com/fs/course/domain/FsPublicCourseTrafficLog.java

@@ -0,0 +1,29 @@
+package com.fs.course.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 公开课流量记录对象 fs_public_course_traffic_log
+ */
+@Data
+public class FsPublicCourseTrafficLog extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    private Long logId;
+
+    private String uuId;
+
+    private Long userId;
+
+    private Long courseId;
+
+    private Long videoId;
+
+    private Long internetTraffic;
+
+    private Integer status;
+
+    private  Long projectId;
+}

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

+ 42 - 0
fs-service/src/main/java/com/fs/course/mapper/FsPublicCourseTrafficLogMapper.java

@@ -0,0 +1,42 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.FsPublicCourseTrafficLog;
+import com.fs.course.param.FsCourseTrafficLogParam;
+import com.fs.course.vo.FsCourseTrafficLogListVO;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 公开课流量记录Mapper接口
+ */
+@Repository
+public interface FsPublicCourseTrafficLogMapper {
+
+    FsPublicCourseTrafficLog selectFsPublicCourseTrafficLogByLogId(Long logId);
+
+    int insertFsPublicCourseTrafficLog(FsPublicCourseTrafficLog fsPublicCourseTrafficLog);
+
+    int updateFsPublicCourseTrafficLog(FsPublicCourseTrafficLog fsPublicCourseTrafficLog);
+
+    @Select("select * from fs_public_course_traffic_log where uu_id = #{uuId}")
+    FsPublicCourseTrafficLog selectFsPublicCourseTrafficLogByUuId(@Param("uuId") String uuId);
+
+    void insertOrUpdateTrafficLog(FsPublicCourseTrafficLog trafficLog);
+
+    @Select("SELECT IFNULL(SUM(internet_traffic), 0) FROM fs_public_course_traffic_log " +
+            "WHERE DATE(create_time) = DATE(CURDATE()) AND company_id = #{companyId}")
+    Long getTodayTrafficLogCompanyId(@Param("companyId") Long companyId);
+
+    @Select("SELECT IFNULL(SUM(internet_traffic), 0) FROM fs_public_course_traffic_log " +
+            "WHERE DATE(create_time) = DATE(CURDATE() - INTERVAL 1 DAY) AND company_id = #{companyId}")
+    Long getYesterdayTrafficLogCompanyId(@Param("companyId") Long companyId);
+
+    @Select("SELECT IFNULL(SUM(internet_traffic), 0) FROM fs_public_course_traffic_log " +
+            "WHERE YEAR(create_time) = YEAR(CURDATE()) AND MONTH(create_time) = MONTH(CURDATE()) AND company_id = #{companyId}")
+    Long getMonthTrafficLogCompanyId(@Param("companyId") Long companyId);
+
+    List<FsCourseTrafficLogListVO> selectTrafficNew(FsCourseTrafficLogParam param);
+}

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

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

@@ -99,6 +99,9 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
 
     R getInternetTraffic(FsUserCourseVideoFinishUParam param);
 
+    //公开课流量统计
+    R getPublicCourseInternetTraffic(FsUserCourseVideoFinishUParam param);
+
     R getIntegralByH5Video(FsUserCourseVideoFinishUParam param);
 
     R sendReward(FsCourseSendRewardUParam 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);
+    }
+
     /**
      * 新增课堂分类
      *

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

+ 62 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -194,6 +194,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private FsCourseTrafficLogMapper fsCourseTrafficLogMapper;
     @Autowired
+    private FsPublicCourseTrafficLogMapper fsPublicCourseTrafficLogMapper;
+    @Autowired
     private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
     @Autowired
     private FsUserMapper fsUserMapper;
@@ -1329,6 +1331,66 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         return R.ok();
     }
 
+    @Override
+    public R getPublicCourseInternetTraffic(FsUserCourseVideoFinishUParam param) {
+        try {
+            if (param.getBufferRate() == null) {
+                logger.error("【公开课缓冲值空】参数: {}", param);
+                return R.error("缓冲值空");
+            }
+            FsPublicCourseTrafficLog trafficLog = new FsPublicCourseTrafficLog();
+            trafficLog.setCreateTime(new Date());
+            BeanUtils.copyProperties(param, trafficLog);
+
+            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+            if (video == null) {
+                return R.error("视频不存在");
+            }
+            FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
+            if (fsUserCourse != null) {
+                trafficLog.setProjectId(fsUserCourse.getProject());
+            }
+            BigDecimal result = param.getBufferRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
+            BigDecimal longAsBigDecimal = BigDecimal.valueOf(video.getFileSize());
+            long roundedResult = result.multiply(longAsBigDecimal).setScale(0, RoundingMode.HALF_UP).longValue();
+            trafficLog.setInternetTraffic(roundedResult);
+
+            if (StringUtils.isNotEmpty(trafficLog.getUuId())) {
+                fsPublicCourseTrafficLogMapper.insertOrUpdateTrafficLog(trafficLog);
+//                asyncDeductPublicCourseTraffic(company, trafficLog);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            logger.error("【公开课插入或更新流量失败】参数: {}, 错误信息:{}", param, e.getMessage(), e);
+            return R.error();
+        }
+        return R.ok();
+    }
+
+//    public void asyncDeductPublicCourseTraffic(Company company, FsPublicCourseTrafficLog trafficLog) {
+//        try {
+//            FsPublicCourseTrafficLog existingLog = fsPublicCourseTrafficLogMapper.selectFsPublicCourseTrafficLogByUuId(trafficLog.getUuId());
+//            long recordedTraffic;
+//            if (existingLog != null) {
+//                recordedTraffic = trafficLog.getInternetTraffic() - existingLog.getInternetTraffic();
+//            } else {
+//                recordedTraffic = trafficLog.getInternetTraffic();
+//            }
+//            if (recordedTraffic <= 0) {
+//                return;
+//            }
+//            Long remainingTraffic = updateRedisCache(company, recordedTraffic / 1024);
+//
+//            if ("1".equals(configUtil.generateConfigByKey("watch.course.config").getString("doNotPlay")) && remainingTraffic <= 0) {
+//                logger.warn("公开课公司ID: {} 流量不足,当前剩余: {}", company.getCompanyId(), remainingTraffic);
+//                throw new Exception("流量不足");
+//            }
+//        } catch (Exception e) {
+//            logger.error("公开课异步扣除流量失败 - 公司ID: {}, 错误信息: {}",
+//                    company.getCompanyId(), e.getMessage(), e);
+//        }
+//    }
+
     public void asyncDeductTraffic(Company company, FsCourseTrafficLog trafficLog) {
         try {
             //根据uuid查询

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

+ 1 - 1
fs-service/src/main/java/com/fs/his/config/StoreConfig.java

@@ -11,7 +11,7 @@ public class StoreConfig implements Serializable {
 
     private Integer payRate;//支付比例
     private Integer unPayTime;//未支付订单自动取消时间
-    private Integer tuiMoneyRate;//公司佣金比例 支付金额-(订单金额*rate%)
+    private Integer tuiMoneyRate;// 公司佣金比例%:(实付金额-运费)× rate%
     private Integer deductMoneyRate;
     private Integer createMoneyRate;//公司制单金额最低比例
     private Integer storeAfterSalesDay;//已完成订单售后有效天数

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

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác