Przeglądaj źródła

Merge remote-tracking branch 'origin/master'

xw 3 dni temu
rodzic
commit
203159f35b
100 zmienionych plików z 3982 dodań i 215 usunięć
  1. 42 24
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  2. 8 2
      fs-ai-call-task/src/main/java/com/fs/app/service/CallTaskService.java
  3. 1 0
      fs-ai-call-task/src/main/resources/application.yml
  4. 1 0
      fs-common/src/main/java/com/fs/common/enums/DataSourceType.java
  5. 14 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  6. 209 0
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  7. 3 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  8. 276 2
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  9. 8 1
      fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java
  10. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  11. 1 0
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  12. 1 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java
  13. 138 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/AddressScrmController.java
  14. 283 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  15. 23 0
      fs-qwhook-sop/src/main/java/com/fs/app/params/vo/CityVO.java
  16. 85 0
      fs-qwhook-sop/src/main/java/com/fs/app/utils/CityTreeUtil.java
  17. 2 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java
  18. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCalleesMapper.java
  19. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java
  20. 1 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxClientMapper.java
  21. 21 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java
  22. 3 4
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  23. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java
  24. 326 0
      fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java
  25. 111 0
      fs-service/src/main/java/com/fs/company/service/easycall/IEasyCallService.java
  26. 93 2
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  27. 87 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  28. 10 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java
  29. 12 7
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java
  30. 15 15
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNode.java
  31. 119 9
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  32. 46 40
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java
  33. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/WorkflowNodeFactory.java
  34. 22 0
      fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java
  35. 124 0
      fs-service/src/main/java/com/fs/company/vo/CdrBodyVo.java
  36. 19 0
      fs-service/src/main/java/com/fs/company/vo/CdrDetailVo.java
  37. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallAddCallListParam.java
  38. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBusiGroupVO.java
  39. 216 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCallPhoneVO.java
  40. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCommonAddCallListParam.java
  41. 36 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCreateTaskParam.java
  42. 28 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallGatewayVO.java
  43. 28 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallLlmAccountVO.java
  44. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPageResult.java
  45. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPhoneItemVO.java
  46. 34 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordQueryParam.java
  47. 39 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordVO.java
  48. 22 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskQueryParam.java
  49. 54 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskVO.java
  50. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallVoiceCodeVO.java
  51. 2 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  52. 3 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  53. 5 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  54. 3 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  55. 1 1
      fs-service/src/main/java/com/fs/his/service/impl/FsPackageOrderServiceImpl.java
  56. 363 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrmSidebarParam.java
  57. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java
  58. 12 4
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  59. 5 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  60. 19 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductCategoryScrmMapper.java
  61. 46 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  62. 2 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartDelParam.java
  63. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartNumParam.java
  64. 2 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartParam.java
  65. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderParam.java
  66. 3 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreCartScrmService.java
  67. 10 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  68. 2 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductCategoryScrmService.java
  69. 3 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  70. 79 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java
  71. 298 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  72. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductCategoryScrmServiceImpl.java
  73. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  74. 1 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreUserEndCategoryScrmServiceImpl.java
  75. 4 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderExportVO.java
  76. 2 2
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java
  77. 87 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderScrmSidebarVO.java
  78. 4 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java
  79. 10 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java
  80. 7 0
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
  81. 5 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  82. 3 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  83. 2 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  84. 5 0
      fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java
  85. 5 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  86. 8 0
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java
  87. 82 0
      fs-service/src/main/java/com/fs/wxcid/FileToBase64Util.java
  88. 0 85
      fs-service/src/main/java/com/fs/wxcid/ImageToBase64Util.java
  89. 74 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/CdnUploadVideoResult.java
  90. 27 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageParam.java
  91. 20 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageRequest.java
  92. 1 1
      fs-service/src/main/java/com/fs/wxcid/service/ICidIpadServerService.java
  93. 1 0
      fs-service/src/main/java/com/fs/wxcid/service/MessageService.java
  94. 2 2
      fs-service/src/main/java/com/fs/wxcid/service/impl/CidIpadServerServiceImpl.java
  95. 28 2
      fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java
  96. 4 0
      fs-service/src/main/resources/application-common.yml
  97. 49 0
      fs-service/src/main/resources/application-dev.yml
  98. 1 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogAddwxMapper.xml
  99. 4 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCalleesMapper.xml
  100. 4 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml

+ 42 - 24
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -277,16 +277,10 @@ public class FsStoreHealthOrderScrmController extends BaseController {
     @Log(title = "商城订单明细导出", businessType = BusinessType.EXPORT)
     @GetMapping("/healthExportItems")
     public AjaxResult exportItems1(FsStoreOrderParam param) {
-        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())){
-            param.setBeginTime(null);
-            param.setEndTime(null);
-        }
+        normalizeExportParam(param);
         if (fsStoreOrderService.isEntityNull(param)){
             return AjaxResult.error("请筛选数据导出");
         }
-        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
-            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
-        }
         if(!StringUtils.isEmpty(param.getPayTimeRange())){
             param.setPayTimeList(param.getPayTimeRange().split("--"));
         }
@@ -390,16 +384,10 @@ public class FsStoreHealthOrderScrmController extends BaseController {
     @Log(title = "商城订单明细导出", businessType = BusinessType.EXPORT)
     @GetMapping("/healthExportItemsDetails")
     public AjaxResult healthExportItemsDetails(FsStoreOrderParam param) {
-        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())){
-            param.setBeginTime(null);
-            param.setEndTime(null);
-        }
+        normalizeExportParam(param);
         if (fsStoreOrderService.isEntityNull(param)){
             return AjaxResult.error("请筛选数据导出");
         }
-        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
-            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
-        }
         if(!StringUtils.isEmpty(param.getPayTimeRange())){
             param.setPayTimeList(param.getPayTimeRange().split("--"));
         }
@@ -428,6 +416,11 @@ public class FsStoreHealthOrderScrmController extends BaseController {
             if (zmvoList != null) {
                     LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
                     for (FsStoreOrderItemExportZMVO vo : zmvoList) {
+                        if ("2".equals(vo.getOrderType())) {
+                            vo.setOrderTypeStr("直播订单");
+                        }else {
+                            vo.setOrderTypeStr("商城订单");
+                        }
                         if (!StringUtils.isEmpty(vo.getJsonInfo())) {
                             try {
                                 StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
@@ -516,6 +509,23 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         return util.importTemplateExcel("订单发货导入模板");
     }
 
+    /** 统一处理导出接口的 GET 参数,与 healthList 的 POST 参数逻辑一致 */
+    private void normalizeExportParam(FsStoreOrderParam param) {
+        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())) {
+            param.setBeginTime(null);
+            param.setEndTime(null);
+        }
+        if (StringUtils.isNotEmpty(param.getCreateTimeRange())) {
+            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
+        }
+        // GET 请求时 orderCodes 可能无法正确绑定,用 orderCodeList(逗号分隔)转 orderCodes
+        if ((param.getOrderCodes() == null || param.getOrderCodes().isEmpty())
+                && StringUtils.isNotBlank(param.getOrderCodeList())) {
+            String[] arr = param.getOrderCodeList().split("[,,\\s]+");
+            param.setOrderCodes(new ArrayList<>(Arrays.asList(arr)));
+        }
+    }
+
     // 检查文件是否为有效的Excel文件
     private boolean isValidExcelFile(String fileName) {
         for (String ext : ALLOWED_EXCEL_EXTENSIONS) {
@@ -588,15 +598,23 @@ public class FsStoreHealthOrderScrmController extends BaseController {
             param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
         }
         param.setIsHealth("1");
-        List<FsStoreOrderDeliveryNoteExportVO> storeOrderDeliveryNoteExportVOList=fsStoreOrderService.getDeliveryNote(param);
-        ExcelUtil<FsStoreOrderDeliveryNoteExportVO> util = new ExcelUtil<>(FsStoreOrderDeliveryNoteExportVO.class);
-        //通过商品ID获取关键字
-        String firstKeyword = storeOrderDeliveryNoteExportVOList.stream()
-                .map(FsStoreOrderDeliveryNoteExportVO::getKeyword)
-                .filter(StringUtils::isNotEmpty)
-                .findFirst()
-                .orElse("无订单");
-        String fileName="077AC"+firstKeyword+new SimpleDateFormat("yyyyMMdd").format(new Date());
-        return util.exportExcel(storeOrderDeliveryNoteExportVOList, fileName);
+        try {
+            List<FsStoreOrderDeliveryNoteExportVO> storeOrderDeliveryNoteExportVOList=fsStoreOrderService.getDeliveryNote(param);
+            if(storeOrderDeliveryNoteExportVOList == null || storeOrderDeliveryNoteExportVOList.isEmpty()){
+                return AjaxResult.error("没有可导出的发货单数据");
+            }
+            ExcelUtil<FsStoreOrderDeliveryNoteExportVO> util = new ExcelUtil<>(FsStoreOrderDeliveryNoteExportVO.class);
+            //通过商品ID获取关键字
+            String firstKeyword = storeOrderDeliveryNoteExportVOList.stream()
+                    .map(FsStoreOrderDeliveryNoteExportVO::getKeyword)
+                    .filter(StringUtils::isNotEmpty)
+                    .findFirst()
+                    .orElse("无订单");
+            String fileName="077AC"+firstKeyword+new SimpleDateFormat("yyyyMMdd").format(new Date());
+            return util.exportExcel(storeOrderDeliveryNoteExportVOList, fileName);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return AjaxResult.error("导出发货单失败:" + e.getMessage());
+        }
     }
 }

+ 8 - 2
fs-ai-call-task/src/main/java/com/fs/app/service/CallTaskService.java

@@ -8,8 +8,10 @@ import com.fs.company.service.*;
 import com.fs.company.service.impl.call.node.AiCallTaskNode;
 import com.fs.course.config.RedisKeyScanner;
 import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
@@ -17,9 +19,13 @@ import java.util.concurrent.*;
 
 @Slf4j
 @Service
-@AllArgsConstructor
+@RequiredArgsConstructor
 public class CallTaskService {
 
+
+    @Value("${cid-group-no}")
+    private Integer cidGroupNo;
+
     private final RedisCacheT<String> redisCache;
 
     private final RedisCache redisCache2;
@@ -40,7 +46,7 @@ public class CallTaskService {
      */
     public void cidWorkflowCallRun() {
         log.info("===========工作流延时任务开始扫描===========");
-        String delayCallKeyPrefix = AiCallTaskNode.getDelayCallKeyPrefix(null) + "*";
+        String delayCallKeyPrefix = AiCallTaskNode.getDelayCallKeyPrefix(cidGroupNo,null) + "*";
         Set<String> keys = redisKeyScanner.scanMatchKey(delayCallKeyPrefix);
         log.info("共扫描到 {} 个待处理键", keys.size());
         keys.parallelStream().forEach(key -> {

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

@@ -14,3 +14,4 @@ spring:
 #    active: druid-sxjz
 #    active: druid-hdt
 #    active: druid-myhk-test
+cid-group-no: 1

+ 1 - 0
fs-common/src/main/java/com/fs/common/enums/DataSourceType.java

@@ -16,6 +16,7 @@ public enum DataSourceType
 
     SOP,
     WX,
+    EASYCALL,
     WX_READ,
     /**
      * 从库

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

@@ -23,6 +23,8 @@ import com.fs.company.domain.CompanyVoiceRoboticWx;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.CdrBodyVo;
+import com.fs.company.vo.CdrDetailVo;
 import com.fs.company.vo.WorkflowExecRecordVo;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -221,6 +223,12 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRoboticService.callerResult(result);
         return R.ok();
     }
+
+    @PostMapping("/callerResult4EasyCall")
+    public R callerResult4EasyCall(CdrDetailVo cdr) {
+        companyVoiceRoboticService.callerResult4EasyCall(cdr);
+        return R.ok();
+    }
     /**
      * 外呼回调
      */
@@ -267,4 +275,10 @@ public class CompanyVoiceRoboticController extends BaseController
         List<WorkflowExecRecordVo> records = companyVoiceRoboticService.getExecRecords(roboticId);
         return R.ok().put("data", records);
     }
+
+    @GetMapping("/getCurrentCompanyId")
+    public R getCurrentCompanyId(){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return R.ok().put("companyId", loginUser.getCompany().getCompanyId());
+    }
 }

+ 209 - 0
fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java

@@ -0,0 +1,209 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.vo.easycall.*;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/6 14:28
+ * @description EasyCallCenter365 外呼管理控制器
+ * <p>
+ * 所有接口均需要登录态,通过 TokenService 获取当前登录用户的 companyId。
+ * 接口地址前缀:/company/easyCall
+ * 对应三方服务器:http://129.28.164.235:8899
+ */
+@Api(tags = "EasyCallCenter365外呼管理")
+@RestController
+@RequestMapping("/company/easyCall")
+public class EasyCallController extends BaseController {
+
+    @Autowired
+    private IEasyCallService easyCallService;
+
+    /** 用于获取当前登录用户信息,从中取出 companyId */
+    @Autowired
+    private TokenService tokenService;
+
+    // =================== 基础数据查询 ===================
+
+    /**
+     * 获取外呼网关列表
+     * 网关是外呼连路的入口,创建任务时需要选择对应的网关 ID
+     */
+    @ApiOperation("获取网关列表")
+    @GetMapping("/gateway/list")
+    public R getGatewayList() {
+        // 获取当前登录用户的公司ID(当前仅用于传递,未涉及多公司隔离)
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取大模型配置列表
+     * 创建 AI 外呼任务时需要选择大模型配置,决定机器人论某能力和话术
+     */
+    @ApiOperation("获取大模型配置列表")
+    @GetMapping("/llmAccount/list")
+    public R getLlmAccountList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallLlmAccountVO> list = easyCallService.getLlmAccountList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取音色列表
+     * AI 外呼任务用于配置语音合成声音风格,如普通话男声、女声等
+     */
+    @ApiOperation("获取音色列表")
+    @GetMapping("/voiceCode/list")
+    public R getVoiceCodeList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallVoiceCodeVO> list = easyCallService.getVoiceCodeList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取技能组列表
+     * 技能组也叫业务组,AI 外呼需要转人工时用于指定转入哪个客服组
+     */
+    @ApiOperation("获取技能组列表")
+    @GetMapping("/busiGroup/list")
+    public R getBusiGroupList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallBusiGroupVO> list = easyCallService.getBusiGroupList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    // =================== 任务管理 ===================
+
+    /**
+     * 分页查询外呼任务列表
+     * 支持按任务ID、任务名称、创建时间范围进行过滤,同时返回拨打统计
+     */
+    @ApiOperation("任务列表查询")
+    @PostMapping("/task/list")
+    public R getTaskList(@RequestBody EasyCallTaskQueryParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallPageResult<EasyCallTaskVO> result = easyCallService.getTaskList(param, companyId);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 分页查询通话记录
+     * 支持按号码、拨打时间、通话时长、通话状态等多条件过滤
+     * callType:01-呼入,02-AI外呼,03-人工外呼,三种类型不支持混合查询
+     */
+    @ApiOperation("通话记录查询")
+    @PostMapping("/record/list")
+    public R getRecordList(@RequestBody EasyCallRecordQueryParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallPageResult<EasyCallRecordVO> result = easyCallService.getRecordList(param, companyId);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 创建外呼任务
+     * 创建成功后返回包含 batchId 的任务对象,后续操作(启动/追加名单)均需要该 batchId
+     */
+    @ApiOperation("创建外呼任务")
+    @PostMapping("/task/create")
+    public R createTask(@RequestBody EasyCallCreateTaskParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallTaskVO task = easyCallService.createTask(param, companyId);
+        return R.ok().put("data", task);
+    }
+
+    /**
+     * 启动外呼任务
+     * 任务创建后默认处于待机状态,调用此接口才会开始对名单中的号码拨打
+     * @param batchId 任务ID,来自创建任务或任务列表查询返回的 batchId
+     */
+    @ApiOperation("启动外呼任务")
+    @GetMapping("/task/start")
+    public R startTask(@RequestParam Long batchId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.startTask(batchId, companyId);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 停止外呼任务
+     * 停止后任务不再拨打,可重新调用启动接口继续运行
+     * @param batchId 任务ID
+     */
+    @ApiOperation("停止外呼任务")
+    @GetMapping("/task/stop")
+    public R stopTask(@RequestParam Long batchId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.stopTask(batchId, companyId);
+        return R.ok("操作成功");
+    }
+
+    // =================== 名单管理 ===================
+
+    /**
+     * AI 外呼专用追加名单
+     * 仅支持 AI 外呼任务,phoneList 为纯手机号字符串列表
+     * 任务启动后也可以继续追加,实现动态补充
+     */
+    @ApiOperation("AI外呼追加名单")
+    @PostMapping("/callList/addAi")
+    public R addAiCallList(@RequestBody EasyCallAddCallListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.addAiCallList(param, companyId);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 通用追加名单
+     * 同时支持 AI 外呼和通知提醒任务
+     * 通知提醒任务必须在每个号码条目中填写 noticeContent(具体提醒话术)
+     * bizJson 可传入需要传递给机器人的业务数据小弹口
+     */
+    @ApiOperation("通用追加名单")
+    @PostMapping("/callList/addCommon")
+    public R addCommonCallList(@RequestBody EasyCallCommonAddCallListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.addCommonCallList(param, companyId);
+        return R.ok("操作成功");
+    }
+
+    // =================== 录音相关 ===================
+
+    /**
+     * 将通话记录中的录音相对路径拼接为可直接访问的完整 URL
+     * 录音查询接口返回的 wavFileUrl 属于相对路径,需要拼接 baseUrl 才能下载
+     * @param wavFileUrl 通话记录中返回的 wavFileUrl 字段
+     */
+    @ApiOperation("获取录音文件完整访问url")
+    @GetMapping("/record/fileUrl")
+    public R getRecordFileUrl(@RequestParam String wavFileUrl) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        String fullUrl = easyCallService.getRecordFileUrl(wavFileUrl, companyId);
+        return R.ok().put("data", fullUrl);
+    }
+}

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

@@ -98,6 +98,9 @@ public class CrmCustomerController extends BaseController
     @PreAuthorize("@ss.hasPermi('crm:customer:list')")
     @GetMapping("/listAll")
     public R listAll(CrmCustomerListQueryParam crmCustomer){
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
         PageHelper.startPage(1, 1000);
         if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
             crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));

+ 276 - 2
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -1,5 +1,6 @@
 package com.fs.company.controller.store;
 
+import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.StrUtil;
 import com.fs.common.annotation.DataScope;
 import com.fs.common.annotation.Log;
@@ -9,6 +10,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.crm.service.ICrmCustomerService;
@@ -31,8 +33,14 @@ import com.fs.his.service.IFsStoreOrderService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.FsStoreOrderListVO;
+import com.fs.his.vo.FsStoreOrderListAndStatisticsVo;
 import com.fs.his.vo.FsStoreOrderVO;
 import com.fs.his.vo.FsStoreProductDeliverExcelVO;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
+import com.fs.hisStore.vo.FsStoreOrderErpExportVO;
+import com.fs.hisStore.vo.FsStoreOrderItemExportVO;
+import com.fs.hisStore.dto.StoreOrderProductDTO;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -40,8 +48,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.util.Date;
-import java.util.List;
+import com.github.pagehelper.PageHelper;
+
+import java.util.*;
 
 import static com.fs.his.utils.PhoneUtil.decryptPhone;
 import static com.fs.his.utils.PhoneUtil.encryptPhone;
@@ -67,6 +76,271 @@ public class FsStoreOrderController extends BaseController
     @Autowired
     @Qualifier("erpOrderServiceImpl")
     IErpOrderService erpOrderService;
+
+    @Autowired
+    private IFsStoreOrderScrmService fsStoreOrderScrmService;
+    @Autowired
+    private IFsStoreOrderItemScrmService orderItemScrmService;
+
+    /**
+     * 查询直播订单列表(仅 fs_store_order_scrm 中 order_type=2)
+     * 分公司负责人(userType=00)可查公司下所有直播订单,否则仅能查自己的直播订单
+     */
+    @PostMapping("/healthLiveList")
+    public FsStoreOrderListAndStatisticsVo healthLiveList(@RequestBody com.fs.hisStore.param.FsStoreOrderParam param) {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setOrderType(2);
+        if (!"00".equals(loginUser.getUser().getUserType())) {
+            param.setCompanyUserId(loginUser.getUser().getUserId());
+        } else {
+            param.setCompanyUserId(null);
+        }
+        if (StringUtils.isNotEmpty(param.getCreateTimeRange())) {
+            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
+        }
+        if (StringUtils.isNotEmpty(param.getPayTimeRange())) {
+            param.setPayTimeList(param.getPayTimeRange().split("--"));
+        }
+        if (StringUtils.isNotEmpty(param.getDeliveryImportTimeRange())) {
+            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
+        }
+        if (StringUtils.isNotEmpty(param.getDeliverySendTimeRange())) {
+            param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
+        }
+        Map<String, java.math.BigDecimal> statistics = fsStoreOrderScrmService.selectFsStoreOrderStatistics(param);
+        String productInfoStr = fsStoreOrderScrmService.selectFsStoreOrderProductStatistics(param);
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<com.fs.hisStore.vo.FsStoreOrderVO> list = fsStoreOrderScrmService.selectFsStoreOrderListVO(param);
+        if (list != null) {
+            for (com.fs.hisStore.vo.FsStoreOrderVO vo : list) {
+                if (StringUtils.isNotEmpty(vo.getPhone())) {
+                    vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+                if (StringUtils.isNotEmpty(vo.getUserPhone())) {
+                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+            }
+        }
+        FsStoreOrderListAndStatisticsVo vo = new FsStoreOrderListAndStatisticsVo();
+        vo.setRows(list);
+        vo.setTotal(new com.github.pagehelper.PageInfo<>(list).getTotal());
+        vo.setCode(200);
+        vo.setMsg("查询成功");
+        if (list != null && !list.isEmpty()) {
+            if (statistics != null && statistics.size() >= 3) {
+                vo.setPayPriceTotal(statistics.get("pay_price") != null ? statistics.get("pay_price").toString() : "0");
+                vo.setPayMoneyTotal(statistics.get("pay_money") != null ? statistics.get("pay_money").toString() : "0");
+                vo.setPayRemainTotal(statistics.get("pay_remain") != null ? statistics.get("pay_remain").toString() : "0");
+            } else {
+                vo.setPayPriceTotal("0");
+                vo.setPayMoneyTotal("0");
+                vo.setPayRemainTotal("0");
+            }
+            vo.setProductInfo(StringUtils.isNotBlank(productInfoStr) ? productInfoStr : "");
+        } else {
+            vo.setPayPriceTotal("0");
+            vo.setPayMoneyTotal("0");
+            vo.setPayRemainTotal("0");
+            vo.setProductInfo("");
+        }
+        return vo;
+    }
+
+    /** 直播订单导出:筛选条件与 healthLiveList 一致(orderType=2 + 公司/负责人权限) */
+    private void applyHealthLiveFilter(com.fs.hisStore.param.FsStoreOrderParam param) {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setOrderType(2);
+        if (!"00".equals(loginUser.getUser().getUserType())) {
+            param.setCompanyUserId(loginUser.getUser().getUserId());
+        } else {
+            param.setCompanyUserId(null);
+        }
+        if (StringUtils.isNotEmpty(param.getCreateTimeRange())) {
+            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
+        }
+        if (StringUtils.isNotEmpty(param.getPayTimeRange())) {
+            param.setPayTimeList(param.getPayTimeRange().split("--"));
+        }
+        if (StringUtils.isNotEmpty(param.getDeliveryImportTimeRange())) {
+            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
+        }
+        if (StringUtils.isNotEmpty(param.getDeliverySendTimeRange())) {
+            param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
+        }
+    }
+
+    private void normalizeExportParam(com.fs.hisStore.param.FsStoreOrderParam param) {
+        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())) {
+            param.setBeginTime(null);
+            param.setEndTime(null);
+        }
+        if (StringUtils.isNotEmpty(param.getCreateTimeRange())) {
+            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
+        }
+        if ((param.getOrderCodes() == null || param.getOrderCodes().isEmpty())
+                && StringUtils.isNotBlank(param.getOrderCodeList())) {
+            String[] arr = param.getOrderCodeList().split("[,,\\s]+");
+            param.setOrderCodes(new ArrayList<>(Arrays.asList(arr)));
+        }
+    }
+
+    /**
+     * 导出直播订单列表(脱敏)
+     */
+    @Log(title = "直播订单导出", businessType = BusinessType.EXPORT)
+    @PostMapping("/healthExport")
+    public AjaxResult healthExport(@RequestBody com.fs.hisStore.param.FsStoreOrderParam param) {
+        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())) {
+            param.setBeginTime(null);
+            param.setEndTime(null);
+        }
+        applyHealthLiveFilter(param);
+        if (fsStoreOrderScrmService.isEntityNull(param)) {
+            return AjaxResult.error("请筛选数据导出");
+        }
+        List<FsStoreOrderErpExportVO> list = fsStoreOrderScrmService.selectFsStoreOrderListVOByExport(param);
+        if (list != null) {
+            for (FsStoreOrderErpExportVO vo : list) {
+                if (vo.getPhone() != null) {
+                    vo.setPhone(vo.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+                if (vo.getUserPhone() != null) {
+                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+                if (vo.getUserAddress() != null) {
+                    vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+                }
+            }
+        }
+        String filter = param.getFilter();
+        ArrayList<String> filterList = new ArrayList<>();
+        if (StringUtils.isNotBlank(filter)) {
+            String[] filterArr = filter.split("\\s*,\\s*");
+            filterList.addAll(Arrays.asList(filterArr));
+        }
+        ExcelUtil<FsStoreOrderErpExportVO> util = new ExcelUtil<>(FsStoreOrderErpExportVO.class);
+        if (filter != null && !filter.isEmpty()) {
+            return util.exportExcelSelectedColumns(list, "订单数据", filterList);
+        } else {
+            return util.exportExcel(list, "订单数据");
+        }
+    }
+
+    /**
+     * 导出直播订单列表(明文)
+     */
+    @Log(title = "直播订单导出(明文)", businessType = BusinessType.EXPORT)
+    @PostMapping("/healthExportDetails")
+    public AjaxResult healthExportDetails(@RequestBody com.fs.hisStore.param.FsStoreOrderParam param) {
+        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())) {
+            param.setBeginTime(null);
+            param.setEndTime(null);
+        }
+        applyHealthLiveFilter(param);
+        if (fsStoreOrderScrmService.isEntityNull(param)) {
+            return AjaxResult.error("请筛选数据导出");
+        }
+        List<FsStoreOrderErpExportVO> list = fsStoreOrderScrmService.selectFsStoreOrderListVOByExport(param);
+        String filter = param.getFilter();
+        ArrayList<String> filterList = new ArrayList<>();
+        if (StringUtils.isNotBlank(filter)) {
+            String[] filterArr = filter.split("\\s*,\\s*");
+            filterList.addAll(Arrays.asList(filterArr));
+        }
+        ExcelUtil<FsStoreOrderErpExportVO> util = new ExcelUtil<>(FsStoreOrderErpExportVO.class);
+        if (filter != null && !filter.isEmpty()) {
+            return util.exportExcelSelectedColumns(list, "订单数据", filterList);
+        } else {
+            return util.exportExcel(list, "订单数据");
+        }
+    }
+
+    /**
+     * 导出直播订单明细(脱敏)
+     */
+    @Log(title = "直播订单明细导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/healthExportItems")
+    public AjaxResult healthExportItems(com.fs.hisStore.param.FsStoreOrderParam param) {
+        normalizeExportParam(param);
+        applyHealthLiveFilter(param);
+        if (fsStoreOrderScrmService.isEntityNull(param)) {
+            return AjaxResult.error("请筛选数据导出");
+        }
+        List<FsStoreOrderItemExportVO> list = orderItemScrmService.selectFsStoreOrderItemListExportVO(param);
+        if (list != null) {
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+            for (FsStoreOrderItemExportVO vo : list) {
+                if (vo.getUserPhone() != null) {
+                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{1})", "$1****$2"));
+                }
+                if (StringUtils.isNotEmpty(vo.getJsonInfo())) {
+                    try {
+                        StoreOrderProductDTO dto = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                        BeanUtil.copyProperties(dto, vo);
+                    } catch (Exception e) {
+                        // ignore
+                    }
+                }
+                if (vo.getUserAddress() != null) {
+                    vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+                }
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) && vo.getCost() != null) {
+                    vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getTotalNum())));
+                } else {
+                    vo.setPayPostage(java.math.BigDecimal.ZERO);
+                    vo.setCost(java.math.BigDecimal.ZERO);
+                    vo.setFPrice(java.math.BigDecimal.ZERO);
+                    vo.setBarCode("");
+                    vo.setCateName("");
+                    vo.setBankTransactionId("");
+                }
+            }
+        }
+        ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);
+        return util.exportExcel(list, "订单明细数据");
+    }
+
+    /**
+     * 导出直播订单明细(明文)
+     */
+    @Log(title = "直播订单明细导出(明文)", businessType = BusinessType.EXPORT)
+    @GetMapping("/healthExportItemsDetails")
+    public AjaxResult healthExportItemsDetails(com.fs.hisStore.param.FsStoreOrderParam param) {
+        normalizeExportParam(param);
+        applyHealthLiveFilter(param);
+        if (fsStoreOrderScrmService.isEntityNull(param)) {
+            return AjaxResult.error("请筛选数据导出");
+        }
+        List<FsStoreOrderItemExportVO> list = orderItemScrmService.selectFsStoreOrderItemListExportVO(param);
+        if (list != null) {
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+            for (FsStoreOrderItemExportVO vo : list) {
+                if (StringUtils.isNotEmpty(vo.getJsonInfo())) {
+                    try {
+                        StoreOrderProductDTO dto = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                        BeanUtil.copyProperties(dto, vo);
+                    } catch (Exception e) {
+                        // ignore
+                    }
+                }
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) && vo.getCost() != null) {
+                    vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getTotalNum())));
+                } else {
+                    vo.setPayPostage(java.math.BigDecimal.ZERO);
+                    vo.setCost(java.math.BigDecimal.ZERO);
+                    vo.setFPrice(java.math.BigDecimal.ZERO);
+                    vo.setBarCode("");
+                    vo.setCateName("");
+                    vo.setBankTransactionId("");
+                }
+            }
+        }
+        ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);
+        return util.exportExcel(list, "订单明细数据");
+    }
+
     /**
      * 查询订单列表
      */

+ 8 - 1
fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -46,17 +46,24 @@ public class DataSourceConfig {
         return new DruidDataSource();
     }
 
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.easycall.druid.master")
+    public DataSource easyCallSource() {
+        return new DruidDataSource();
+    }
+
 
 
     @Bean
     @Primary
     public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource,
-                                        @Qualifier("slaveDataSource") DataSource slaveDataSource , @Qualifier("wxDataSource") DataSource wxDataSource) {
+                                        @Qualifier("slaveDataSource") DataSource slaveDataSource , @Qualifier("wxDataSource") DataSource wxDataSource, @Qualifier("easyCallSource") DataSource easyCallSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
         targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SLAVE.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         targetDataSources.put(DataSourceType.WX, wxDataSource);
+        targetDataSources.put(DataSourceType.EASYCALL.name(), easyCallSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }
 

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

@@ -112,6 +112,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 ).permitAll()
                 .antMatchers("/test").anonymous()
                 .antMatchers("/company/companyVoiceRobotic/callerResult").anonymous()
+                .antMatchers("/company/companyVoiceRobotic/callerResult4EasyCall").anonymous()
                 .antMatchers("/qw/getJsapiTicket/**").anonymous()
                 .antMatchers("/msg/**").anonymous()
                 .antMatchers("/baiduBack/**").anonymous()

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

@@ -722,6 +722,7 @@ public class IpadSendServer {
                 case "99":
                     // 群发
                     sendTxtAtMsg(vo);
+                    break;
                 default:
                     // 未知类型,记录警告
                     log.error("SOP_LOG_ID:{}错误的发送类型: {}", qwSopLogs.getId(), content.getContentType());

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

@@ -114,6 +114,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg/**").anonymous()
                 .antMatchers("/msg/**/**").anonymous()
                 .antMatchers("/msg").anonymous()
+                .antMatchers("/app/common/**").anonymous()
                 .antMatchers("/testSop").anonymous()
                 .antMatchers("/common/getId**").anonymous()
                 .antMatchers("/common/uploadOSS**").anonymous()

+ 138 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/AddressScrmController.java

@@ -0,0 +1,138 @@
+package com.fs.app.controller;
+
+
+import cn.hutool.core.bean.BeanUtil;
+
+import com.fs.app.params.vo.CityVO;
+import com.fs.app.utils.CityTreeUtil;
+import com.fs.common.core.domain.R;
+import com.fs.hisStore.domain.FsCityScrm;
+import com.fs.hisStore.domain.FsUserAddressScrm;
+import com.fs.hisStore.param.FsAddressParseParam;
+import com.fs.hisStore.param.FsUserAddressAddEditParam;
+import com.fs.hisStore.service.IFsCityScrmService;
+import com.fs.hisStore.service.IFsExpressScrmService;
+import com.fs.hisStore.service.IFsUserAddressScrmService;
+import com.google.common.collect.Lists;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import springfox.documentation.annotations.Cacheable;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+import java.util.List;
+
+
+
+@Api("地址接口")
+@RestController
+@RequestMapping(value="/store/app/address")
+public class AddressScrmController {
+
+    @Autowired
+    private IFsExpressScrmService expressService;
+    @Autowired
+    private IFsUserAddressScrmService addressService;
+
+    @Autowired
+    private IFsCityScrmService cityService;
+
+
+    /*@ApiOperation("获取城市数据")
+    @GetMapping("/getCity")
+    @Cacheable("citys")
+    public R getCity(HttpServletRequest request){
+        List<FsCityScrm> list=cityService.selectFsCitys();
+        return R.ok().put("data", list);
+
+    }*/
+
+    @ApiOperation("获取城市数据")
+    @GetMapping("/getCitys")
+    @Cacheable("cityData")
+    public R getCitys(HttpServletRequest request){
+        List<FsCityScrm> list=cityService.selectFsCitys();
+        List<CityVO> cityVOS = Lists.newArrayList();
+        for (FsCityScrm city : list){
+            CityVO cityVO = new CityVO();
+            cityVO.setV(city.getCityId());
+            cityVO.setN(city.getCityName());
+            cityVO.setPid(city.getParentId());
+            cityVOS.add(cityVO);
+        }
+        return R.ok().put("data", CityTreeUtil.list2TreeConverter(cityVOS, "0"));
+
+    }
+
+    @GetMapping("/getAddressList")
+    public R getAddressList(FsUserAddressScrm fsUserAddress){
+        fsUserAddress.setIsDel(0);
+        List<FsUserAddressScrm> list = addressService.selectFsUserAddressList(fsUserAddress);
+        return R.ok().put("data", list);
+    }
+
+    @GetMapping("/getAddressById")
+    public R getAddressById(@RequestParam("id")Long id){
+        FsUserAddressScrm address=addressService.selectFsUserAddressById(id);
+        return R.ok().put("data",address);
+    }
+    @ApiOperation("添加地址")
+    @PostMapping("/addAddress")
+    public R addAddress(@Valid @RequestBody FsUserAddressAddEditParam address, HttpServletRequest request){
+        Long userId = address.getUserId();
+        //处理地址数量 最大20个
+        Integer count=addressService.selectFsUserAddressCountsByUserId(userId);
+        if(count>20){
+            return R.error("最多可创建20个地址");
+        }
+        if(address.getIsDefault()==1){
+            //处理默认地址
+            addressService.clearIsDefalut(userId);
+        }
+        address.setPhone(address.getPhone().trim());
+        FsUserAddressScrm userAddress=new FsUserAddressScrm();
+        BeanUtil.copyProperties(address, userAddress);
+        addressService.insertFsUserAddress(userAddress);
+        return R.ok("操作成功").put("addressId", userAddress.getId());
+    }
+
+    @ApiOperation("编辑地址")
+    @PostMapping("/editAddress")
+    public R editAddress(@Valid @RequestBody FsUserAddressAddEditParam address, HttpServletRequest request){
+        Long userId = address.getUserId();
+        if(address.getIsDefault()==1){
+            //处理默认地址
+            addressService.clearIsDefalut(userId);
+        }
+        FsUserAddressScrm userAddress=new FsUserAddressScrm();
+        BeanUtil.copyProperties(address, userAddress);
+        addressService.updateFsUserAddress(userAddress);
+        return R.ok("操作成功");
+    }
+
+    @ApiOperation("删除地址")
+    @PostMapping("/delAddress")
+    public R delAddress(@RequestBody FsUserAddressAddEditParam address, HttpServletRequest request){
+        addressService.deleteFsUserAddressById(address.getId());
+        return R.ok("操作成功");
+    }
+
+    @ApiOperation("地址解析")
+    @PostMapping("/parseAddress")
+    public R parseAddress(@RequestBody FsAddressParseParam param){
+        return expressService.parseAddress(param.getContent());
+    }
+
+
+    @ApiOperation("删除所有地址")
+    @PostMapping("/delAllAddress")
+    public R delAllAddress(@RequestBody FsUserAddressAddEditParam address){
+        Long userId = address.getUserId();
+        addressService.delAllAddress(userId);
+        return R.ok("操作成功");
+    }
+
+
+}

+ 283 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -1,11 +1,17 @@
 package com.fs.app.controller;
 
+import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.app.annotation.Login;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.common.utils.CloudHostUtils;
+import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyUserService;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsUserCourse;
 import com.fs.course.param.FsCourseLinkCreateParam;
@@ -24,10 +30,17 @@ import com.fs.course.vo.newfs.FsUserCourseVideoDetailsVO;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
 import com.fs.course.vo.newfs.FsUserVideoListVO;
 import com.fs.his.utils.ConfigUtil;
+import com.fs.hisStore.domain.*;
+import com.fs.hisStore.enums.OrderInfoEnum;
 import com.fs.hisStore.enums.SysConfigEnum;
+import com.fs.hisStore.param.*;
+import com.fs.hisStore.service.*;
+import com.fs.hisStore.vo.*;
+import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.service.IQwExternalContactService;
 import com.fs.qw.service.IQwUserService;
+import com.fs.store.config.StoreConfig;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
 import com.github.pagehelper.PageHelper;
@@ -36,8 +49,15 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
 import java.util.List;
 
 
@@ -63,6 +83,30 @@ public class FsUserCourseVideoController {
 
     @Autowired
     private ISysConfigService configService;
+    @Autowired
+    private IFsStoreProductScrmService productService;
+    @Autowired
+    private ICompanyUserService companyUserService;
+    @Autowired
+    private IFsStoreOrderScrmService orderService;
+    @Autowired
+    private IFsStoreCartScrmService cartService;
+    @Autowired
+    private IFsStoreProductCategoryScrmService categoryService;
+    @Autowired
+    private IFsStoreScrmService storeScrmService;
+    @Autowired
+    private IFsStoreProductAttrScrmService attrService;
+    @Autowired
+    private IFsStoreProductAttrValueScrmService attrValueService;
+    @Autowired
+    private IFsStoreProductRelationScrmService productRelationService;
+    @Autowired
+    private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
+    @Autowired
+    private IFsPrescribeScrmService prescribeService;
+    @Autowired
+    private IFsStoreOrderItemScrmService itemService;
 
 
     @ApiOperation("课程视频详情")
@@ -71,6 +115,245 @@ public class FsUserCourseVideoController {
         return fsUserCourseVideoService.getVideoDetails(videoId);
     }
 
+    @ApiOperation("获取侧边栏商品列表")
+    @PostMapping("/getSalesProductsBySidebar")
+    public R getSalesProductsBySidebar(@RequestBody FsStoreOrderScrmSidebarVO param){
+        //
+        QwUser qwUser = qwExternalContactService.getQwUserByRedis(param.getCorpId().trim(),param.getQwUserId().trim());
+
+        if (qwUser == null || qwUser.getCompanyId() == null) {
+            return R.error("员工未绑定 销售公司 或 未获取到员工信息,请重试!");
+        }
+        String externalUserId = param.getExternalUserId();
+        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactByExternalUserIdSidebar(externalUserId, param.getCorpId());
+
+        if(qwExternalContact == null || qwExternalContact.getFsUserId() == null){
+            return R.error("用户未绑定,暂时无法下单");
+        }
+        param.setCompanyId(qwUser.getCompanyId());
+
+        PageHelper.startPage(param.getPage(), param.getPageSize());
+        param.setIsDisplay(0);
+        List<FsStoreProductListQueryVO> productList = productService.selectFsStoreProductSidebarListQuery(param);
+        for (FsStoreProductListQueryVO fsStoreProductListQueryVO : productList) {
+            fsStoreProductListQueryVO.setCompanyId(qwUser.getCompanyId());
+            fsStoreProductListQueryVO.setCompanyUserId(qwUser.getCompanyUserId());
+        }
+        PageInfo<FsStoreProductListQueryVO> listPageInfo=new PageInfo<>(productList);
+        return R.ok().put("data",listPageInfo).put("qwUser",qwUser).put("extUser",qwExternalContact);
+    }
+    @ApiOperation("获取商品详情")
+    @GetMapping("/getProductDetails")
+    public R getProductDetails(@RequestParam(value="productId") Long productId,@RequestParam(value="storeId")String storeId,@RequestParam(value="userId") Long userId){
+        FsStoreProductQueryVO product=productService.selectFsStoreProductByIdQuery(productId, storeId);
+        if(product==null){
+            return R.error("商品不存在或已下架");
+        }
+        //增加店铺信息
+        FsStoreScrm fsStoreScrm = null;
+        if(StringUtils.isNotEmpty(product.getStoreId())){
+            fsStoreScrm = storeScrmService.selectFsStoreByStoreId(Long.parseLong(product.getStoreId()));
+        }
+        List<FsStoreProductAttrScrm> productAttr=attrService.selectFsStoreProductAttrByProductId(product.getProductId());
+        List<FsStoreProductAttrValueScrm> productValues=attrValueService.selectFsStoreProductAttrValueByProductId(product.getProductId());
+
+        //获取用户的TOKEN写入足迹
+        if(userId!=null){
+            FsStoreProductRelationScrm productRelation=new FsStoreProductRelationScrm();
+            productRelation.setIsDel(0);
+            productRelation.setUserId(userId);
+            productRelation.setProductId(product.getProductId());
+            productRelation.setType("foot");
+            List<FsStoreProductRelationScrm> productRelations=productRelationService.selectFsStoreProductRelationList(productRelation);
+            if(productRelations!=null&&productRelations.size()>0){
+                FsStoreProductRelationScrm relation=productRelations.get(0);
+                relation.setUpdateTime(new Date());
+                productRelationService.updateFsStoreProductRelation(relation);
+            }
+            else{
+                FsStoreProductRelationScrm relation=new FsStoreProductRelationScrm();
+                relation.setUserId(userId);
+                relation.setIsDel(0);
+                relation.setProductId(product.getProductId());
+                relation.setUpdateTime(new Date());
+                relation.setType("foot");
+                relation.setCreateTime(new Date());
+                relation.setUpdateTime(new Date());
+                productRelationService.insertFsStoreProductRelation(relation);
+            }
+        }
+
+        // 查询限购信息
+        Integer remainingPurchaseLimit = null; // 剩余可购买数量
+        Integer purchasedNum = 0; // 已购买数量
+        if (product.getPurchaseLimit() != null && product.getPurchaseLimit() > 0) {
+            // 商品有限购,查询用户是否购买过
+            if (userId != null) {
+                FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(
+                        product.getProductId(), userId);
+                if (purchaseLimit != null) {
+                    purchasedNum = purchaseLimit.getNum();
+                }
+                // 计算剩余可购买数量
+                remainingPurchaseLimit = product.getPurchaseLimit() - purchasedNum;
+                if (remainingPurchaseLimit < 0) {
+                    remainingPurchaseLimit = 0;
+                }
+            } else {
+                // 未登录用户,剩余可购买数量等于限购数量
+                remainingPurchaseLimit = product.getPurchaseLimit();
+            }
+        }
+
+        return R.ok().put("product",product)
+                .put("productAttr",productAttr)
+                .put("productValues",productValues)
+                .put("store",fsStoreScrm)
+                .put("remainingPurchaseLimit", remainingPurchaseLimit) // 剩余可购买数量
+                .put("purchasedNum", purchasedNum); // 已购买数量
+    }
+
+    @ApiOperation("侧边栏制单")
+    @PostMapping("/createSalesOrderBySidebar")
+    public R createSalesOrderBySidebar(@RequestBody FsStoreOrderScrmSidebarVO param){
+        String cartIds = param.getCartIds();
+
+        CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+        if(companyUser==null||companyUser.getDelFlag().equals("1")){
+            return R.error("用户不存在");
+        }
+        if(!companyUser.getStatus().equals("0")){
+            return R.error("用户已禁用");
+        }
+        return orderService.createSalesOrderBySidebar(companyUser,cartIds);
+    }
+
+    @ApiOperation("获取我的订单详情")
+    @GetMapping("/getMyStoreOrderById")
+    public R getMyStoreOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
+        FsStoreOrderScrm order=orderService.selectFsStoreOrderById(orderId);
+        order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
+        order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
+        List<FsStoreOrderItemVO> list=itemService.selectFsStoreOrderItemListByOrderId(orderId);
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(order.getCreateTime());
+        String json=configService.selectConfigByKey("his.store");
+        StoreConfig config= JSONUtil.toBean(json,StoreConfig.class);
+        calendar.add(Calendar.MINUTE,config.getUnPayTime());
+        SimpleDateFormat format = new   SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        String payLimitTime = format.format(calendar.getTime() );
+        FsPrescribeScrm prescribe=null;
+        if(order.getPrescribeId()!=null){
+            prescribe=prescribeService.selectFsPrescribeById(order.getPrescribeId());
+        }
+        //处理是否可以申请售后
+        Integer isAfterSales=0;
+        if(order.getStatus().equals(OrderInfoEnum.STATUS_3.getValue())) {
+            //已完成订单
+            isAfterSales=1;
+            if (order.getFinishTime() != null) {
+                if (config.getStoreAfterSalesDay() != null && config.getStoreAfterSalesDay() > 0) {
+                    //判断完成时间是否超过指定时间
+                    Calendar calendarAfterSales = new GregorianCalendar();
+                    calendarAfterSales.setTime(order.getFinishTime());
+                    calendarAfterSales.add(calendarAfterSales.DATE, config.getStoreAfterSalesDay()); //把日期往后增加一天,整数  往后推,负数往前移动
+                    if (calendarAfterSales.getTime().getTime() < new Date().getTime()) {
+                        isAfterSales = 0;
+                    }
+                }
+            }
+        }
+        else if(order.getStatus()==1||order.getStatus()==2){
+            isAfterSales=1;
+        }
+        return R.ok().put("isAfterSales",isAfterSales).put("order",order).put("items",list).put("payLimitTime",payLimitTime).put("prescribe",prescribe);
+    }
+    @ApiOperation("创建订单 侧边栏")
+    @PostMapping("/createBySidebar")
+    public R createBySidebar(@Validated @RequestBody FsStoreOrderScrmSidebarVO param){
+        log.info("开始创建订单,客户id:{}", param.getUserId());
+        return orderService.createOrderBySidebar(param);
+    }
+
+    @ApiOperation("获取制单信息 侧边栏")
+    @GetMapping("/getOrderInfoBySidebar")
+    public R getOrderInfoBySidebar(@RequestParam("orderKey") String orderKey,@RequestParam("userId")Long userId){
+        return orderService.getOrderInfoBySidebar(orderKey,userId);
+    }
+
+
+    @ApiOperation("改订单价格  侧边栏")
+    @PostMapping("/editOrderMoneyBySidebar")
+    @Transactional
+    public R editOrderMoneyBySidebar(@RequestBody FsStoreOrderScrmSidebarVO param){
+        return orderService.editOrderMoneyBySidebar(param);
+    }
+
+    @ApiOperation("获取客户订单列表 侧边栏")
+    @GetMapping("/getStoreOrderListBySidebar")
+    public R getStoreOrderListBySidebar(FsStoreOrderScrmSidebarVO param){
+
+        String externalUserId = param.getExternalUserId();
+        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactByExternalUserIdSidebar(externalUserId, param.getCorpId());
+
+        if(qwExternalContact == null || qwExternalContact.getFsUserId() == null){
+            return R.error("用户未绑定,无法查询订单");
+        }
+        PageHelper.startPage(param.getPage(), param.getPageSize());
+
+        param.setUserId(qwExternalContact.getFsUserId());
+        param.setCompanyUserId(param.getCompanyUserId());
+
+        List<FsMyStoreOrderListQueryVO> list=orderService.selectFsStoreOrderListBySidebarVO(param);
+        PageInfo<FsMyStoreOrderListQueryVO> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+    @ApiOperation("获取分类 侧边栏")
+    @GetMapping("/getProductCateBySidebar")
+    public R getProductCateBySidebar(@RequestParam(value="pid") Long pid, @RequestParam(value="storeId",required = false) Long storeId){
+        try {
+            FsStoreProductCategoryScrm param=new FsStoreProductCategoryScrm();
+            param.setIsShow(1);
+            param.setIsDel(0);
+            param.setPid(pid);
+            param.setStoreId(storeId);
+            List<FsStoreProductCategoryScrm> list=categoryService.selectFsStoreProductCategoryListQueryBySidebar(param);
+            return R.ok().put("data",list);
+        } catch (Exception e){
+            return R.error("操作异常");
+        }
+    }
+
+    @ApiOperation("添加购物车 侧边栏")
+    @PostMapping("/addCartBySidebar")
+    public R addCartBySidebar(@Validated @RequestBody FsStoreCartParam cartParam){
+        return cartService.addCartBySidebar(cartParam.getUserId(),cartParam);
+    }
+    @ApiOperation("获取购物车列表 侧边栏")
+    @GetMapping("/getCartsBySidebar")
+    public R getCartsBySidebar(@RequestParam("userId") Long userId){
+        List<FsStoreCartVO> carts= cartService.selectFsStoreCartListByUid(userId);
+        return R.ok().put("carts",carts);
+    }
+
+    @ApiOperation("计算购物车商品总价 侧边栏")
+    @GetMapping("/getCartsTotalBySidebar")
+    public R getCartsTotalBySidebar(@RequestParam("cartIds") String cartIds){
+        return cartService.selectFsStoreCartTotalByCarts(cartIds);
+    }
+
+    @ApiOperation("改变购物车数量 侧边栏")
+    @PostMapping("/cartNumBySidebar")
+    public R cartNumBySidebar(@Validated @RequestBody FsStoreCartNumParam cartParam){
+        return cartService.changeNum(cartParam.getUserId(),cartParam);
+    }
+    @ApiOperation("删除购物车 侧边栏")
+    @PostMapping("/delCartBySidebar")
+    public R delCartBySidebar(@Validated @RequestBody FsStoreCartDelParam cartParam){
+        return cartService.delCart(cartParam.getUserId(),cartParam);
+    }
+
+
     @PostMapping("/getFsCourseListBySidebar")
     @ApiOperation("获取视频课程下拉列表 侧边栏")
     public R getFsCourseListBySidebar(@RequestBody FsCourseListBySidebarParam param) {

+ 23 - 0
fs-qwhook-sop/src/main/java/com/fs/app/params/vo/CityVO.java

@@ -0,0 +1,23 @@
+package com.fs.app.params.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Getter
+@Setter
+@ToString
+public class CityVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String v; //id
+
+    private String n; //名称
+
+    private String pid;
+
+    private List<CityVO> c; //子集
+}

+ 85 - 0
fs-qwhook-sop/src/main/java/com/fs/app/utils/CityTreeUtil.java

@@ -0,0 +1,85 @@
+package com.fs.app.utils;
+import com.fs.app.params.vo.CityVO;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @ClassName 树形工具类
+ **/
+public class CityTreeUtil {
+    /**
+     * 获得指定节点下所有归档
+     *
+     * @param list
+     * @param parentId
+     * @return
+     */
+    public static List<CityVO> list2TreeConverter(List<CityVO> list, String parentId) {
+        List<CityVO> returnList = new ArrayList<>();
+
+        for (CityVO res : list) {
+            //判断对象是否为根节点
+
+            if (res.getPid().equals(parentId)) {
+                //该节点为根节点,开始递归
+
+                //通过递归为节点设置childList
+                recursionFn(list, res);
+
+                returnList.add(res);
+            }
+        }
+
+        return returnList;
+    }
+
+    /**
+     * 递归列表
+     * 通过递归,给指定t节点设置childList
+     *
+     * @param list
+     * @param t
+     */
+    public static void recursionFn(List<CityVO> list, CityVO t) {
+        //只能获取当前t节点的子节点集,并不是所有子节点集
+        List<CityVO> childsList = getChildList(list, t);
+
+        //设置他的子集对象集
+        t.setC(childsList);
+
+        //迭代子集对象集
+
+        //遍历完,则退出递归
+        for (CityVO nextChild : childsList) {
+
+            //判断子集对象是否还有子节点
+            if (!CollectionUtils.isEmpty(childsList)) {
+                //有下一个子节点,继续递归
+                recursionFn(list, nextChild);
+            }
+        }
+    }
+
+    /**
+     * 获得指定节点下的所有子节点
+     *
+     * @param list
+     * @param t
+     * @return
+     */
+    public static List<CityVO> getChildList(List<CityVO> list, CityVO t) {
+        List<CityVO> childsList = new ArrayList<>();
+        //遍历集合元素,如果元素的Parentid==指定元素的id,则说明是该元素的子节点
+        for (CityVO t1 : list) {
+            if (t1.getPid().equals(t.getV())) {
+                childsList.add(t1);
+            }
+        }
+
+        return childsList;
+    }
+
+
+}

+ 2 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java

@@ -62,4 +62,6 @@ public class CompanyVoiceRoboticCallees{
 
     @TableField(exist = false)
     private String idToString;
+
+    private Integer isWeCom;
 }

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

@@ -84,6 +84,6 @@ public interface CompanyVoiceRoboticCalleesMapper extends BaseMapper<CompanyVoic
 
     List<SendMsgByTaskVO> getSendMsgTaskListByRoboticId(@Param("roboticId") Long roboticId);
 
-    List<CompanyVoiceRoboticCallees> selectExcludeList(@Param("list")List<CompanyWxClient> list);
+    List<CompanyVoiceRoboticCallees> selectExcludeList(@Param("list")List<CompanyWxClient> list,@Param("isWeCom") Integer isWeCom);
     List<Long> getNotFinishAddWxRobotic(@Param("roboticIds") Set<Long> roboticIds);
 }

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

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.vo.CompanyVoiceRoboticQwUserListVo;
+import com.fs.company.vo.DictVO;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -69,4 +70,6 @@ public interface CompanyVoiceRoboticMapper extends BaseMapper<CompanyVoiceRoboti
     void finishRobotic(@Param("id") Long id);
 
     int finishAddWxRobotic(@Param("collect") List<Long> collect);
+
+    List<DictVO> getDictDataList(@Param("dictType") String dictType);
 }

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

@@ -84,4 +84,5 @@ public interface CompanyWxClientMapper extends BaseMapper<CompanyWxClient> {
 
     List<CompanyWxClient> getQwAddWxList(@Param("accountIdList") List<Long> accountIdList, @Param("isWeCom") Integer isWeCom);
 
+    List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(@Param("accountIdList") List<Long> accountIdList, @Param("execStatus") Integer execStatus, @Param("execNodeType") Integer execNodeType);
 }

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

@@ -0,0 +1,21 @@
+package com.fs.company.mapper;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/7 10:43
+ * @description
+ */
+
+@Repository
+public interface EasyCallMapper {
+
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallCallPhoneVO getCallPhoneInfoByUuid(@Param("uuid") String uuid);
+
+}

+ 3 - 4
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -5,10 +5,7 @@ import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.result.CalltaskcreateaiCustomizeResult;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.param.ExecutionContext;
-import com.fs.company.vo.AddWxClientVo;
-import com.fs.company.vo.AiCallConfigVO;
-import com.fs.company.vo.CompanyVoiceRoboticQwUserListVo;
-import com.fs.company.vo.WorkflowExecRecordVo;
+import com.fs.company.vo.*;
 
 import java.util.List;
 import java.util.Set;
@@ -79,6 +76,8 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     void callerResult(PushIIntentionResult result);
 
+    void callerResult4EasyCall(CdrDetailVo result);
+
     void dispenseWx(Long id);
 
     void addCompany(AddWxClientVo vo);

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

@@ -74,4 +74,6 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
     List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList);
 
     List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList,Integer isWeCom);
+
+    List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(List<Long> accountIdList);
 }

+ 326 - 0
fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java

@@ -0,0 +1,326 @@
+package com.fs.company.service.easycall;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.vo.easycall.*;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * EasyCallCenter365 外呼服务实现类
+ * <p>
+ * 对接 EasyCallCenter365 外呼系统的所有 HTTP 接口,包括:
+ * 网关、大模型、音色、技能组的基础数据查询,
+ * 外呼任务的创建/启动/停止,名单追加,以及通话记录查询。
+ * <p>
+ * 服务地址通过配置项 easycall.base-url 注入,默认值:http://129.28.164.235:8899
+ */
+@Service
+@Slf4j
+public class EasyCallServiceImpl implements IEasyCallService {
+
+    /** EasyCallCenter365 服务器基础地址,从配置文件 easycall.base-url 读取 */
+    @Value("${easycall.base-url:http://129.28.164.235:8899}")
+    private String baseUrl;
+
+    // =================== EasyCallCenter365 接口路径常量 ===================
+    /** 获取外呼网关列表 */
+    private static final String API_GATEWAY_LIST     = "/aicall/api/gateway/list";
+    /** 获取大模型配置列表 */
+    private static final String API_LLMACCOUNT_LIST  = "/aicall/api/llmacount/list";
+    /** 获取音色列表 */
+    private static final String API_VOICECODE_LIST   = "/aicall/api/voicecode/list";
+    /** 获取技能组(业务组)列表 */
+    private static final String API_BUSIGROUP_LIST   = "/aicall/api/busigroup/list";
+    /** 分页查询任务列表 */
+    private static final String API_CALLTASK_LIST    = "/aicall/api/calltask/list";
+    /** 分页查询通话记录 */
+    private static final String API_RECORDS_LIST     = "/aicall/api/records/list";
+    /** 创建外呼任务 */
+    private static final String API_CREATE_TASK      = "/aicall/api/ai/createTask";
+    /** 启动外呼任务(GET,携带 batchId 参数) */
+    private static final String API_START_TASK       = "/aicall/api/ai/startTask";
+    /** 停止外呼任务(GET,携带 batchId 参数) */
+    private static final String API_STOP_TASK        = "/aicall/api/ai/stopTask";
+    /** AI外呼专用追加名单(仅支持 AI 外呼任务,phoneList 为纯号码列表) */
+    private static final String API_AI_ADD_CALL_LIST = "/aicall/api/ai/addCallList";
+    /** 通用追加名单(同时支持 AI 外呼和通知提醒任务,支持传入提醒内容和业务数据) */
+    private static final String API_COMMON_ADD_LIST  = "/aicall/api/common/addCallList";
+
+    // =================== 基础数据查询接口 ===================
+
+    /**
+     * 查询外呼网关列表
+     * 网关用于创建任务时选择线路,purpose=2 表示仅限AI外呼,purpose=3 表示不限制
+     */
+    @Override
+    public List<EasyCallGatewayVO> getGatewayList(Long companyId) {
+        // 拼接完整请求地址并发起 GET 请求
+        String url = buildUrl(API_GATEWAY_LIST);
+        JSONObject result = doGet(url);
+        // 从响应的 data 字段中取出网关数组并转为 Java 对象列表
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallGatewayVO.class);
+    }
+
+    /**
+     * 查询大模型配置列表
+     * 大模型配置用于创建任务时绑定 AI 话术,支持 DeepSeek、Coze、MaxKB、Dify 等
+     */
+    @Override
+    public List<EasyCallLlmAccountVO> getLlmAccountList(Long companyId) {
+        String url = buildUrl(API_LLMACCOUNT_LIST);
+        JSONObject result = doGet(url);
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallLlmAccountVO.class);
+    }
+
+    /**
+     * 查询音色列表
+     * 音色用于 AI 外呼任务(taskType=1)时配置语音合成的声音风格
+     */
+    @Override
+    public List<EasyCallVoiceCodeVO> getVoiceCodeList(Long companyId) {
+        String url = buildUrl(API_VOICECODE_LIST);
+        JSONObject result = doGet(url);
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallVoiceCodeVO.class);
+    }
+
+    /**
+     * 查询技能组(业务组)列表
+     * 技能组用于 AI 外呼需要转人工时,指定接入的人工客服分组
+     */
+    @Override
+    public List<EasyCallBusiGroupVO> getBusiGroupList(Long companyId) {
+        String url = buildUrl(API_BUSIGROUP_LIST);
+        JSONObject result = doGet(url);
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallBusiGroupVO.class);
+    }
+
+    // =================== 任务管理接口 ===================
+
+    /**
+     * 分页查询外呼任务列表
+     * 返回任务基本信息以及各任务的拨打统计数据(总量、接通量、接通率等)
+     */
+    @Override
+    public EasyCallPageResult<EasyCallTaskVO> getTaskList(EasyCallTaskQueryParam param, Long companyId) {
+        String url = buildUrl(API_CALLTASK_LIST);
+        // 将查询参数序列化为 JSON 后 POST 发送
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        // 响应为分页格式:{ total, rows },统一用 parsePageResult 解析
+        return parsePageResult(result, EasyCallTaskVO.class);
+    }
+
+    /**
+     * 分页查询通话记录
+     * 支持按号码、拨打时间、通话时长、通话状态等多条件过滤
+     * callType:01-呼入,02-AI外呼,03-人工外呼,三种类型不支持混合查询
+     * pageNum 和 pageSize 均为空时返回全部数据
+     */
+    @Override
+    public EasyCallPageResult<EasyCallRecordVO> getRecordList(EasyCallRecordQueryParam param, Long companyId) {
+        String url = buildUrl(API_RECORDS_LIST);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        return parsePageResult(result, EasyCallRecordVO.class);
+    }
+
+    /**
+     * 创建外呼任务
+     * taskType=1 为 AI 外呼(需传 voiceCode、voiceSource)
+     * taskType=2 为通知提醒(需传 playTimes)
+     * 创建成功后返回包含 batchId 的任务信息,后续启动/停止/追加名单均依赖 batchId
+     */
+    @Override
+    public EasyCallTaskVO createTask(EasyCallCreateTaskParam param, Long companyId) {
+        String url = buildUrl(API_CREATE_TASK);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        // 响应的 data 字段即为创建成功的任务对象
+        JSONObject data = result.getJSONObject("data");
+        return data == null ? null : data.toJavaObject(EasyCallTaskVO.class);
+    }
+
+    /**
+     * 启动外呼任务
+     * 任务创建后默认处于待机状态,调用此接口后开始拨打名单中的号码
+     */
+    @Override
+    public void startTask(Long batchId, Long companyId) {
+        // batchId 以 Query 参数形式拼接到 URL
+        String url = buildUrl(API_START_TASK) + "?batchId=" + batchId;
+        JSONObject result = doGet(url);
+        checkSuccess(result);
+    }
+
+    /**
+     * 停止外呼任务
+     * 停止后任务不再继续拨打,可重新启动
+     */
+    @Override
+    public void stopTask(Long batchId, Long companyId) {
+        String url = buildUrl(API_STOP_TASK) + "?batchId=" + batchId;
+        JSONObject result = doGet(url);
+        checkSuccess(result);
+    }
+
+    // =================== 名单管理接口 ===================
+
+    /**
+     * AI 外呼专用追加名单
+     * 仅支持 AI 外呼任务(taskType=1),phoneList 传入纯手机号列表
+     * 注意:任务启动后也可以继续追加名单
+     */
+    @Override
+    public void addAiCallList(EasyCallAddCallListParam param, Long companyId) {
+        String url = buildUrl(API_AI_ADD_CALL_LIST);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        checkSuccess(result);
+    }
+
+    /**
+     * 通用追加名单
+     * 同时支持 AI 外呼(taskType=1)和通知提醒(taskType=2)任务
+     * 通知提醒任务必须在 phoneList 的每条记录中填写 noticeContent(播报内容)
+     * bizJson 字段可传入需要传递给机器人的业务数据(如客户姓名、订单号等)
+     */
+    @Override
+    public void addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId) {
+        String url = buildUrl(API_COMMON_ADD_LIST);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        checkSuccess(result);
+    }
+
+    // =================== 录音相关接口 ===================
+
+    /**
+     * 拼接录音文件完整访问 URL
+     * 通话记录查询返回的 wavFileUrl 是相对路径(如 /recordings/files?filename=xxx.wav)
+     * 需要拼接系统 baseUrl 才能直接在浏览器访问或下载
+     */
+    @Override
+    public String getRecordFileUrl(String wavFileUrl, Long companyId) {
+        // 录音路径为空时直接返回空字符串,避免拼出错误地址
+        if (StringUtils.isEmpty(wavFileUrl)) {
+            return "";
+        }
+        String base = trimTrailingSlash(baseUrl);
+        return base + wavFileUrl;
+    }
+
+    // =================== 私有工具方法 ===================
+
+    /**
+     * 拼接完整请求地址
+     * 将 baseUrl(如 http://129.28.164.235:8899)与接口路径(如 /aicall/api/gateway/list)拼接
+     */
+    private String buildUrl(String path) {
+        return trimTrailingSlash(baseUrl) + path;
+    }
+
+    /**
+     * 去除 URL 末尾多余的斜杠
+     * 防止拼接后出现 http://xxx//path 的双斜杠问题
+     */
+    private String trimTrailingSlash(String url) {
+        if (url != null && url.endsWith("/")) {
+            return url.substring(0, url.length() - 1);
+        }
+        return url;
+    }
+
+    /**
+     * 发送 GET 请求
+     * 用于不需要请求体的查询类接口(如网关列表、启动/停止任务等)
+     */
+    private JSONObject doGet(String url) {
+        log.info("EasyCall GET 请求: {}", url);
+        try {
+            String body = HttpUtil.createGet(url)
+                    .header("Accept", "application/json")
+                    .execute()
+                    .body();
+            log.info("EasyCall GET 响应: {}", body);
+            // 解析响应 JSON 并校验 code 字段,非 0 则抛异常
+            return parseAndCheck(body);
+        } catch (RuntimeException e) {
+            // RuntimeException 直接向上抛(已经是业务异常,不重新包装)
+            throw e;
+        } catch (Exception e) {
+            log.error("EasyCall GET 请求异常, url: {}", url, e);
+            throw new RuntimeException("请求EasyCallCenter365失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 发送 POST 请求
+     * 用于需要传入 JSON 请求体的接口(如创建任务、查询列表、追加名单等)
+     */
+    private JSONObject doPost(String url, String jsonBody) {
+        log.info("EasyCall POST 请求: {}, body: {}", url, jsonBody);
+        try {
+            HttpRequest request = HttpUtil.createPost(url)
+                    .header("Accept", "application/json")
+                    .header("Content-Type", "application/json")
+                    .body(jsonBody);
+            String body = request.execute().body();
+            log.info("EasyCall POST 响应: {}", body);
+            return parseAndCheck(body);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("EasyCall POST 请求异常, url: {}", url, e);
+            throw new RuntimeException("请求EasyCallCenter365失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 解析响应 JSON 并校验业务状态码
+     * EasyCallCenter365 的接口规范:code=0 表示成功,非 0 表示失败
+     * 失败时将 msg 字段内容包装为 RuntimeException 向上抛出
+     */
+    private JSONObject parseAndCheck(String responseBody) {
+        JSONObject json = JSON.parseObject(responseBody);
+        Integer code = json.getInteger("code");
+        if (code == null || code != 0) {
+            String msg = json.getString("msg");
+            log.error("EasyCallCenter365 接口返回错误, code: {}, msg: {}", code, msg);
+            throw new RuntimeException("EasyCallCenter365 接口错误: " + msg);
+        }
+        return json;
+    }
+
+    /**
+     * 操作类接口成功校验(占位方法)
+     * 启动、停止、追加名单等操作类接口无 data 返回,
+     * 成功与否已由 parseAndCheck 中的 code 判断保证,此处无需额外处理
+     */
+    private void checkSuccess(JSONObject result) {
+        // parseAndCheck 中 code != 0 时已抛异常,走到这里即代表操作成功
+    }
+
+    /**
+     * 解析分页响应结果
+     * EasyCallCenter365 分页接口格式:{ code, msg, total, rows }
+     * 将 total(总数)和 rows(当前页数据)封装到 EasyCallPageResult 中返回
+     */
+    private <T> EasyCallPageResult<T> parsePageResult(JSONObject result, Class<T> clazz) {
+        EasyCallPageResult<T> pageResult = new EasyCallPageResult<>();
+        // total 可能为 null(如无数据时),默认为 0
+        Long total = result.getLong("total");
+        pageResult.setTotal(total == null ? 0L : total);
+        // rows 为当前页的数据数组,反序列化为指定 VO 类型列表
+        JSONArray rows = result.getJSONArray("rows");
+        pageResult.setRows(rows == null ? new ArrayList<>() : rows.toJavaList(clazz));
+        return pageResult;
+    }
+}

+ 111 - 0
fs-service/src/main/java/com/fs/company/service/easycall/IEasyCallService.java

@@ -0,0 +1,111 @@
+package com.fs.company.service.easycall;
+
+import com.fs.company.vo.easycall.*;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 外呼服务接口
+ */
+public interface IEasyCallService {
+
+    /**
+     * 获取网关列表
+     *
+     * @param companyId 公司id
+     * @return 网关列表
+     */
+    List<EasyCallGatewayVO> getGatewayList(Long companyId);
+
+    /**
+     * 获取大模型配置列表
+     *
+     * @param companyId 公司id
+     * @return 大模型配置列表
+     */
+    List<EasyCallLlmAccountVO> getLlmAccountList(Long companyId);
+
+    /**
+     * 获取音色列表
+     *
+     * @param companyId 公司id
+     * @return 音色列表
+     */
+    List<EasyCallVoiceCodeVO> getVoiceCodeList(Long companyId);
+
+    /**
+     * 获取技能组列表
+     *
+     * @param companyId 公司id
+     * @return 技能组列表
+     */
+    List<EasyCallBusiGroupVO> getBusiGroupList(Long companyId);
+
+    /**
+     * 任务列表查询
+     *
+     * @param param     查询参数
+     * @param companyId 公司id
+     * @return 分页任务列表
+     */
+    EasyCallPageResult<EasyCallTaskVO> getTaskList(EasyCallTaskQueryParam param, Long companyId);
+
+    /**
+     * 通话记录查询
+     *
+     * @param param     查询参数
+     * @param companyId 公司id
+     * @return 分页通话记录
+     */
+    EasyCallPageResult<EasyCallRecordVO> getRecordList(EasyCallRecordQueryParam param, Long companyId);
+
+    /**
+     * 创建外呼任务
+     *
+     * @param param     任务参数
+     * @param companyId 公司id
+     * @return 创建结果
+     */
+    EasyCallTaskVO createTask(EasyCallCreateTaskParam param, Long companyId);
+
+    /**
+     * 启动外呼任务
+     *
+     * @param batchId   任务id
+     * @param companyId 公司id
+     */
+    void startTask(Long batchId, Long companyId);
+
+    /**
+     * 停止外呼任务
+     *
+     * @param batchId   任务id
+     * @param companyId 公司id
+     */
+    void stopTask(Long batchId, Long companyId);
+
+    /**
+     * AI外呼追加名单
+     *
+     * @param param     追加参数
+     * @param companyId 公司id
+     */
+    void addAiCallList(EasyCallAddCallListParam param, Long companyId);
+
+    /**
+     * 通用追加名单(支持AI外呼和通知提醒任务)
+     *
+     * @param param     追加参数
+     * @param companyId 公司id
+     */
+    void addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId);
+
+    /**
+     * 获取录音文件访问URL
+     *
+     * @param wavFileUrl 录音文件相对路径
+     * @param companyId  公司id
+     * @return 完整的录音文件URL
+     */
+    String getRecordFileUrl(String wavFileUrl, Long companyId);
+}

+ 93 - 2
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -29,6 +29,9 @@ import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
 import com.fs.company.mapper.CompanyWxAccountMapper;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.vo.CidConfigVO;
+import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+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.voice.constant.Constant;
@@ -40,6 +43,8 @@ import org.springframework.stereotype.Service;
 import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 
+import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
+
 /**
  * 调用日志_ai打电话Service业务层处理
  *
@@ -67,6 +72,8 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Autowired
     CompanyWorkflowEngine companyWorkflowEngine;
     @Autowired
+    QwUserMapper qwUserMapper;
+    @Autowired
     @Qualifier("cidWorkFlowExecutor")
     private Executor cidWorkFlowExecutor;
 
@@ -203,8 +210,16 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
 
                 CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
                 CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxServiceImpl.getById(companyWxClient.getRoboticWxId());
-                CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
-                companyVoiceRoboticCallLog.setCompanyUserId(companyWxAccount.getCompanyUserId());
+                Long setCompanyUserId = null;
+                if(Integer.valueOf(1).equals(companyWxClient.getIsWeCom())){
+                    CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
+                    setCompanyUserId =  companyWxAccount.getCompanyUserId();
+                }else if(Integer.valueOf(2).equals(companyWxClient.getIsWeCom())){
+                    QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
+                    setCompanyUserId = qwUser.getCompanyUserId();
+                }
+
+                companyVoiceRoboticCallLog.setCompanyUserId(setCompanyUserId);
                 // 调用接口查询通话其他信息
                 TaskInfo dialogMap = aiCallService.getDialogMapNew(getDialogMap, companyVoiceRoboticCallLog.getCompanyId());
                 // 写入其他记录
@@ -251,6 +266,82 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
             log.error("处理回调结果异常:{}", result, ex);
         }
     }
+    @Async("callLogExcutor")
+    public void asyncHandleCalleeCallBackResult4EasyCall(EasyCallCallPhoneVO result, CompanyVoiceRoboticCallees callees) {
+        try {
+            String json = configService.selectConfigByKey("cid.config");
+            if (StringUtils.isBlank(json)) {
+                log.error("未配置cid.config");
+            }
+            CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
+            if (null != result) {
+//                getDialogMapDomain getDialogMap = getDialogMapDomain.builder()
+//                        .uuid(uuid)
+//                        .build();
+                CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog = companyVoiceRoboticCallLogCallphoneMapper.selectNoResultLogByCallees(callees);
+
+                companyVoiceRoboticCallLog.setStatus(2);
+                companyVoiceRoboticCallLog.setResult(JSON.toJSONString(result));
+
+                CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
+                CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxServiceImpl.getById(companyWxClient.getRoboticWxId());
+                Long setCompanyUserId = null;
+                if(Integer.valueOf(1).equals(companyWxClient.getIsWeCom())){
+                    CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
+                    setCompanyUserId =  companyWxAccount.getCompanyUserId();
+                }else if(Integer.valueOf(2).equals(companyWxClient.getIsWeCom())){
+                    QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
+                    setCompanyUserId = qwUser.getCompanyUserId();
+                }
+
+                companyVoiceRoboticCallLog.setCompanyUserId(setCompanyUserId);
+                // 调用接口查询通话其他信息
+//                TaskInfo dialogMap = aiCallService.getDialogMapNew(getDialogMap, companyVoiceRoboticCallLog.getCompanyId());
+                // 写入其他记录
+//                JSONObject telData = dialogMap.getTelData();
+                companyVoiceRoboticCallLog.setRecordPath(result.getWavfile());
+                companyVoiceRoboticCallLog.setContentList(result.getDialogue());
+                companyVoiceRoboticCallLog.setCallerNum(result.getTelephone());
+                companyVoiceRoboticCallLog.setCalleeNum(result.getCallerNumber());
+                companyVoiceRoboticCallLog.setUuid(result.getUuid());
+                Long createTime = result.getCalloutTime();
+                companyVoiceRoboticCallLog.setCallCreateTime(createTime);
+                Long answerTime = result.getCallEndTime();
+                companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
+                companyVoiceRoboticCallLog.setIntention(result.getIntent());
+                companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
+                BigDecimal callCharge = cidConfigVO.getCallCharge();
+                //
+                if (null == callCharge) {
+                    callCharge = DEFAULT_CALL_CHARGE;
+                }
+                //向上取整分钟数
+                BigDecimal divide = new BigDecimal(companyVoiceRoboticCallLog.getCallTime()).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
+                BigDecimal multiply = divide.multiply(callCharge);
+                companyVoiceRoboticCallLog.setCost(multiply);
+                baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
+
+                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);
+                    if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
+                        Map<String, Object> param = new HashMap<>();
+                        param.put("callBackUuid", userData.getString("callBackUuid"));
+                        param.put("callSource", "callBack");
+                        CompletableFuture.runAsync(() -> {
+                            companyWorkflowEngine.resumeFromBlockingNode(userData.getString("workflowInstanceId"), userData.getString("nodeKey"), param);
+                        }, cidWorkFlowExecutor).thenRun(() -> {
+                            redisCache2.deleteObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid"));
+                        });
+                    }
+                    redisCache2.deleteObject(bizJson.getString("callBackUuid"));
+                }
+            }
+
+        } catch (Exception ex) {
+            log.error("处理回调结果异常:{}", result, ex);
+        }
+    }
 
     @Async("callLogExcutor")
     public void asyncInsertCompanyVoiceRoboticCallLogBatch(List<CompanyVoiceRoboticCallLogCallphone> list) {

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

@@ -23,6 +23,7 @@ import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.*;
 import com.fs.company.vo.*;
+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;
@@ -44,6 +45,8 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
+import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
+
 
 /**
  * 机器人外呼任务Service业务层处理
@@ -102,6 +105,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final RedisCache redisCache2;
     private final CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
     private final QwUserMapper qwUserMapper;
+    private final EasyCallMapper easyCallMapper;
     /**
      * 查询机器人外呼任务
      *
@@ -185,6 +189,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             callees.setResult(0);
             callees.setTaskFlow(companyVoiceRobotic.getTaskFlow());
             callees.setRunTaskFlow(companyVoiceRobotic.getRunTaskFlow());
+            callees.setIsWeCom(isWeCom);
             return callees;
         }).collect(Collectors.toList());
         companyVoiceRoboticCalleesService.saveBatch(callesList);
@@ -759,6 +764,34 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         }
     }
 
+    /**
+     * 外呼结果回调
+     * @param result
+     */
+    @Override
+    public void callerResult4EasyCall(CdrDetailVo result){
+//        EASYCALL
+        log.info("进入easyCall外呼结果回调:{}", JSON.toJSONString(result));
+        if(result == null || StringUtils.isBlank(result.getUuid())) return;
+        //调用查询查找外呼结果
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if(null == callPhoneRes){
+            log.error("easyCall外呼回调信息未查询到结果:{}", JSON.toJSONString(result));
+            return;
+        }
+        JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
+        String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid"));
+        if(StringUtils.isBlank(cacheString)){
+            log.error("easyCall外呼回调缓存信息缺失:{}", JSON.toJSONString(result));
+            return;
+        }
+        JSONObject cacheInfo = JSONObject.parseObject(cacheString);
+        pushDialogContent4EasyCall(cacheInfo,callPhoneRes);
+        CompanyVoiceRoboticCallees callee =  companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
+        companyVoiceRoboticCallLogCallphoneService.asyncHandleCalleeCallBackResult4EasyCall(callPhoneRes,callee);
+        System.out.println(callPhoneRes);
+    }
+
     public void pushDialogContent(PushIIntentionResult result){
         Notify notify = result.getNotify();
         String intention = notify.getIntention();
@@ -802,12 +835,64 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         bindCompany(companyWxClient, roboticWxList);
         companyWxClientServiceImpl.saveOrUpdate(companyWxClient);
     }
+    public void pushDialogContent4EasyCall(JSONObject cacheInfo,EasyCallCallPhoneVO callPhoneRes){
+
+        String intention = getIntention(callPhoneRes.getIntent());
+        if(StringUtils.isEmpty(intention)){
+            intention = "0";
+        }
+        CompanyVoiceRoboticCallees callee =  companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
+        callee.setUuid(callPhoneRes.getUuid());
+        callee.setIntention(intention);
+        callee.setJson(JSON.toJSONString(callPhoneRes.getDialogue()));
+        callee.setResult(1);
+        companyVoiceRoboticCalleesMapper.updateById(callee);
+        CrmCustomer crmCustomer = crmCustomerMapper.selectCrmCustomerById(callee.getUserId());
+        crmCustomer.setIntention(intention);
+        crmCustomerMapper.updateById(crmCustomer);
+        CompanyVoiceRobotic companyVoiceRobotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(callee.getRoboticId());
+        //平均分配时 已经完成了分配 不需要走下面的分配动作
+        if(Integer.valueOf(0).equals(companyVoiceRobotic.getAddType())){
+            return;
+        }
+        List<CompanyVoiceRoboticWx> roboticWxList = companyVoiceRoboticWxMapper.selectByRoboticId(callee.getRoboticId(), intention);
+        List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(PubFun.listToNewList(roboticWxList, CompanyVoiceRoboticWx::getAccountId)));
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(accountList, CompanyWxAccount::getId);
+        roboticWxList.forEach(e -> e.setAccount(accountMap.get(e.getAccountId())));
+        CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callee.getRoboticId()).eq("customer_id", callee.getUserId()));
+        if(companyWxClient == null){
+            companyWxClient = new CompanyWxClient();
+        }
+        // 任务ID
+        companyWxClient.setRoboticId(callee.getRoboticId());
+        // 客户名称
+        companyWxClient.setNickName(callee.getUserName());
+        // 手机号
+        companyWxClient.setPhone(callee.getPhone());
+        // 微信号
+        companyWxClient.setCustomerId(callee.getUserId());
+        // 意向
+        companyWxClient.setIntention(intention);
+        bindCompany(companyWxClient, roboticWxList);
+        companyWxClientServiceImpl.saveOrUpdate(companyWxClient);
+    }
+
+    public String getIntention(String intent){
+        List<DictVO> datas =  companyVoiceRoboticMapper.getDictDataList("customer_intention_level");
+        List<DictVO> collect = datas.stream().filter(e -> e.getDictLabel().equals(intent)).collect(Collectors.toList());
+        return collect.isEmpty() ? null : collect.get(0).getDictValue();
+    }
     public void pushBilling(PushIIntentionResult result){
         Notify notify = result.getNotify();
         CompanyVoiceRoboticCallees callee = getResultCalleeInfo(notify);
         callee.setResult(1);
         companyVoiceRoboticCalleesMapper.updateById(callee);
     }
+//    public void pushBilling4EasyCall(Long callId){
+//        CompanyVoiceRoboticCallees callee = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(callId);
+//        callee.setResult(1);
+//        companyVoiceRoboticCalleesMapper.updateById(callee);
+//    }
     public CompanyVoiceRoboticCallees getResultCalleeInfo(Notify notify){
         String cacheString = (String) redisCache2.getCacheObject(companyVoiceRoboticCallLogCallphoneService.WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData());
         if(StringUtils.isNotBlank(cacheString)){
@@ -1093,7 +1178,8 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     // 绑定销售
     private void bindCompany(CompanyWxClient client, List<CompanyVoiceRoboticWx> wxList) {
-        List<CompanyVoiceRoboticWx> wx = wxList.stream().filter(f -> f.getAccount() != null && f.getAccount().getAllocateNum() < f.getAccount().getAddNum()).collect(Collectors.toList());
+//         取消再分配时 最大加微限制判定 && f.getAccount().getAllocateNum() < f.getAccount().getAddNum()
+        List<CompanyVoiceRoboticWx> wx = wxList.stream().filter(f -> f.getAccount() != null).collect(Collectors.toList());
         // 绑定销售,添加值达到阈值后设置为空,等待下次绑定
         if (!wx.isEmpty()) {
             CompanyVoiceRoboticWx companyVoiceRoboticWx = wx.get(0);

+ 10 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java

@@ -246,4 +246,14 @@ public class CompanyWxClientServiceImpl extends ServiceImpl<CompanyWxClientMappe
     public List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList, Integer isWeCom) {
         return baseMapper.getQwAddWxList(accountIdList,isWeCom);
     }
+
+    /**
+     * 获取添加微信列表 工作流用
+     * @param accountIdList
+     * @return
+     */
+    @Override
+    public  List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(List<Long> accountIdList){
+        return baseMapper.getQwAddWxList4Workflow(accountIdList, ExecutionStatusEnum.WAITING.getValue(), NodeTypeEnum.AI_QW_ADD_WX_TASK.getValue());
+    }
 }

+ 12 - 7
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java

@@ -8,10 +8,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.*;
-import com.fs.company.mapper.CompanyAiWorkflowExecLogMapper;
-import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
-import com.fs.company.mapper.CompanyWxAccountMapper;
-import com.fs.company.mapper.CompanyWxClientMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.service.*;
 import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
 import com.fs.enums.ExecutionStatusEnum;
@@ -101,6 +98,8 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     @Autowired
     private CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
 
+    @Autowired
+    CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
 
 
     /**
@@ -256,7 +255,13 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         if (addressId==null || addressId.isEmpty()){
             return R.error("请先绑定地址");
         }
-        Long serverId = cidIpadServerService.selectQwIpadServerByAddressId(addressId);
+        Long cidServerId = companyUser.getCidServerId();
+        if ( cidServerId==null ){
+            return R.error("请先绑定cid服务");
+        }
+        CompanyAiWorkflowServer cidServer = companyAiWorkflowServerMapper.selectCompanyAiWorkflowServerById(companyUser.getCidServerId());
+
+        Long serverId = cidIpadServerService.selectQwIpadServerByAddressId(addressId,cidServer.getGroupNo());
         if (serverId==null){
             return  R.error(501,"该地区服务器剩余数量不足");
         }
@@ -264,7 +269,7 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         account.setServerStatus(1);
         updateById(account);
 
-        cidIpadServerService.subtractServer(serverId);
+//        cidIpadServerService.subtractServer(serverId);
         CidIpadServerUser qwIpadServerUser = new CidIpadServerUser();
         qwIpadServerUser.setCompanyUserId(companyUser.getUserId());
         qwIpadServerUser.setCompanyId(companyUser.getCompanyId());
@@ -608,7 +613,7 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
             }
 
             // 清除超时检测Key(回调成功了,不需要超时检测了)
-            AiAddWxTaskNode.clearTimeoutKey(workflowInstanceId, wxClientId);
+//            AiAddWxTaskNode.clearTimeoutKey(workflowInstanceId, wxClientId);
 
             // 触发工作流继续执行
             Map<String, Object> inputData = new HashMap<>();

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

@@ -35,7 +35,7 @@ public class AiAddWxTaskNode extends AbstractWorkflowNode {
     private static final CompanyWorkflowNodeMapper companyWorkflowNodeMapper = SpringUtils.getBean(CompanyWorkflowNodeMapper.class);
     @SuppressWarnings("unchecked")
     private static final RedisCacheT<String> redisCache = SpringUtils.getBean(RedisCacheT.class);
-    public static final String DELAY_ADD_WX_KEY = "addWxTask:delay:%s:%s:";
+    public static final String DELAY_ADD_WX_KEY = "addWxTask:delay:%s:%s:%s:";
     /**
      * 默认加微超时时间(分钟)
      */
@@ -225,17 +225,17 @@ public class AiAddWxTaskNode extends AbstractWorkflowNode {
         return true;
     }
 
-    /**
-     * 清除超时检测 Key
-     *
-     * @param workflowInstanceId 工作流实例ID
-     * @param wxClientId         加微客户ID
-     */
-    public static void clearTimeoutKey(String workflowInstanceId, Long wxClientId) {
-        String timeoutKey = Constants.WORKFLOW_ADD_WX_TIMEOUT + workflowInstanceId + ":" + wxClientId;
-        redisCache.deleteObject(timeoutKey);
-        log.info("清除加微超时检测 Key: {}", timeoutKey);
-    }
+//    /**
+//     * 清除超时检测 Key
+//     *
+//     * @param workflowInstanceId 工作流实例ID
+//     * @param wxClientId         加微客户ID
+//     */
+//    public static void clearTimeoutKey(String workflowInstanceId, Long wxClientId) {
+//        String timeoutKey = Constants.WORKFLOW_ADD_WX_TIMEOUT + workflowInstanceId + ":" + wxClientId;
+//        redisCache.deleteObject(timeoutKey);
+//        log.info("清除加微超时检测 Key: {}", timeoutKey);
+//    }
 
     /**
      * getRedisCacheKey
@@ -243,12 +243,12 @@ public class AiAddWxTaskNode extends AbstractWorkflowNode {
      * @param time
      * @return
      */
-    public static String getDelayAddWxKeyPrefix(Long time) {
+    public static String getDelayAddWxKeyPrefix(Integer cidGroupNo,Long time) {
         Date nowDay = new Date();
         if (null != time) {
             nowDay = new Date(time);
         }
-        return String.format(DELAY_ADD_WX_KEY, nowDay.getHours(), nowDay.getMinutes());
+        return String.format(DELAY_ADD_WX_KEY,cidGroupNo, nowDay.getHours(), nowDay.getMinutes());
     }
 
     /**
@@ -287,7 +287,7 @@ public class AiAddWxTaskNode extends AbstractWorkflowNode {
                 //节点包含延时条件
                 if (null != condition.getAddTime() && !condition.isAdd()) {
                     long l = System.currentTimeMillis() + condition.getAddTime() * 60 * 1000;
-                    String redisKey = getDelayAddWxKeyPrefix(l) + workflowInstanceId;
+                    String redisKey = getDelayAddWxKeyPrefix(exec.getCidGroupNo(),l) + workflowInstanceId;
                     ExecutionContext nextContext = context.clone();
                     nextContext.setCurrentNodeKey(edge.getTargetNodeKey());
                     super.redisCache.setCacheObject(redisKey, nextContext);

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

@@ -1,23 +1,29 @@
 package com.fs.company.service.impl.call.node;
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
+import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
 import com.fs.company.mapper.CompanyWorkflowNodeMapper;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.IWorkflowNode;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.service.impl.CompanyVoiceRoboticCallLogCallphoneServiceImpl;
 import com.fs.company.vo.AiCallConfigVO;
 import com.fs.company.vo.AiCallWorkflowConditionVo;
 import com.fs.company.vo.ExecutionResult;
+import com.fs.company.vo.easycall.EasyCallCommonAddCallListParam;
+import com.fs.company.vo.easycall.EasyCallCreateTaskParam;
+import com.fs.company.vo.easycall.EasyCallPhoneItemVO;
+import com.fs.company.vo.easycall.EasyCallTaskVO;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -29,7 +35,14 @@ import java.util.concurrent.TimeUnit;
 public class AiCallTaskNode extends AbstractWorkflowNode {
     private static final CompanyWorkflowNodeMapper companyWorkflowNodeMapper = SpringUtils.getBean(CompanyWorkflowNodeMapper.class);
     private static final ICompanyVoiceRoboticService companyVoiceRoboticService = SpringUtils.getBean(ICompanyVoiceRoboticService.class);
-    public static final String DELAY_CALL_KEY = "aiCallTask:delay:%s:%s:";
+    /** EasyCallCenter365 外呼服务 */
+    private static final IEasyCallService easyCallService = SpringUtils.getBean(IEasyCallService.class);
+    /** 被叫人表 Mapper,用于获取手机号 */
+    private static final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper = SpringUtils.getBean(CompanyVoiceRoboticCalleesMapper.class);
+    private static final CompanyVoiceRoboticCallLogCallphoneServiceImpl companyVoiceRoboticCallLogCallphoneService = SpringUtils.getBean(CompanyVoiceRoboticCallLogCallphoneServiceImpl.class);
+    /** EasyCall 外呼回调信息存放到 Redis 的 key 前缀 */
+    public static final String EASYCALL_WORKFLOW_REDIS_KEY = "easycall:workflow:callback:";
+    public static final String DELAY_CALL_KEY = "aiCallTask:delay:%s:%s:%s:";
     private final String CALL_FROM_CALLBACK = "callBack";
     private final String CALL_FROM_TIMER = "timer";
 
@@ -110,7 +123,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                         ExecutionContext nextContext = context.clone();
                         nextContext.setCurrentNodeKey(edge.getTargetNodeKey());
                         //添加到延时扫描redis
-                        super.redisCache.setCacheObject(this.getDelayCallKeyPrefix(l) + exec.getWorkflowInstanceId(), nextContext, 1, TimeUnit.DAYS);
+                        super.redisCache.setCacheObject(this.getDelayCallKeyPrefix(exec.getCidGroupNo(),l) + exec.getWorkflowInstanceId(), nextContext, 1, TimeUnit.DAYS);
                         super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.WAITING);
                         updateLogStatusIfExist(context, ExecutionStatusEnum.PAUSED, ExecutionStatusEnum.WAITING);
                         runnableCount++;
@@ -158,7 +171,9 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                 if (bus == null) {
                     return ExecutionResult.failure().errorMessage("未找到业务数据").build();
                 }
-                companyVoiceRoboticService.workflowCallPhoneOne(bus.getRoboticId(), bus.getCalleeId(), context, callConfigVo);
+//                companyVoiceRoboticService.workflowCallPhoneOne(bus.getRoboticId(), bus.getCalleeId(), context, callConfigVo);
+                // EasyCallCenter365 外呼
+                 workflowCallPhoneOne4EasyCall(bus.getRoboticId(),bus.getCalleeId(), context, callConfigVo);
                 super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.PAUSED);
                 return ExecutionResult.paused()
                         .outputData(context.getVariables())
@@ -187,16 +202,111 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
 
     /**
      * getRedisCacheKey
-     *
+     * @param cidGroupNo
      * @param time
      * @return
      */
-    public static String getDelayCallKeyPrefix(Long time) {
+    public static String getDelayCallKeyPrefix(Integer cidGroupNo,Long time) {
         Date nowDay = new Date();
         if (null != time) {
             nowDay = new Date(time);
         }
-        return String.format(DELAY_CALL_KEY, nowDay.getHours(), nowDay.getMinutes());
+        return String.format(DELAY_CALL_KEY, cidGroupNo,nowDay.getHours(), nowDay.getMinutes());
+    }
+
+    /**
+     * EasyCallCenter365 外呼节点核心方法(替代旧天天外呼的 workflowCallPhoneOne)
+     * <p>
+     * 执行流程:
+     * 1. 生成 callBackUuid 存入 Redis(用于后续回调匹配)
+     * 2. 查询被叫人手机号
+     * 3. 调用 EasyCallCenter365 创建 AI 外呼任务
+     * 4. 将被叫号码加入任务名单
+     * 5. 启动外呼任务
+     *
+     * @param calleeId    被叫人记录 ID(对应 company_voice_robotic_callees 表)
+     * @param context     工作流执行上下文,含 workflowInstanceId、currentNodeKey 等
+     * @param callConfigVo 节点配置,包含外呼线路 ID、大模型底座 ID、音色、技能组等参数
+     */
+    private void workflowCallPhoneOne4EasyCall(Long roboticId,Long calleeId, ExecutionContext context, AiCallConfigVO callConfigVo) {
+
+        CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectById(roboticId);
+        // 1. 生成回调唯一标识符,后续回调时通过此 uuid 匹配对应的流程实例
+        String callBackUuid = UUID.randomUUID().toString();
+        // 将回调信息写入 Redis,保存 1 天
+        JSONObject callbackInfo = new JSONObject();
+        callbackInfo.put("callBackUuid", callBackUuid);
+        callbackInfo.put("nodeKey", context.getCurrentNodeKey());
+        callbackInfo.put("workflowInstanceId", context.getWorkflowInstanceId());
+        callbackInfo.put("calleeId", calleeId);
+        super.redisCache.setCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + callBackUuid,
+                callbackInfo.toJSONString(), 1, TimeUnit.DAYS);
+        // 将 callBackUuid 写入 context,供后续回调时从 context 取用
+        context.setVariable("callBackUuid", callBackUuid);
+
+        // 2. 获取被叫人手机号
+        CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectById(calleeId);
+        if (callees == null || StringUtils.isBlank(callees.getPhone())) {
+            log.error("workflowCallPhoneOne4EasyCall: 被叫人不存在或手机号为空 - calleeId: {}", calleeId);
+            throw new RuntimeException("被叫人信息异常,calleeId: " + calleeId);
+        }
+
+        // 3. 构建创建任务参数(AI 外呼模式:taskType=1)
+        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
+        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
+        createParam.setBatchName("workflow_" + context.getWorkflowInstanceId() + "_" + calleeId);
+        createParam.setThreadNum(100L);
+        // 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 创建任务失败");
+        }
+        Long batchId = task.getBatchId();
+        log.info("workflowCallPhoneOne4EasyCall: EasyCall 任务创建成功 - batchId: {}", batchId);
+
+        // 5. 将被叫号码加入任务名单(使用通用追加接口,支持传入业务数据)
+        EasyCallCommonAddCallListParam addListParam = new EasyCallCommonAddCallListParam();
+        addListParam.setBatchId(batchId);
+        // 构建号码条目,bizJson 传入默认客户信息占位符
+        EasyCallPhoneItemVO phoneItem = new EasyCallPhoneItemVO();
+        phoneItem.setPhoneNum(callees.getPhone());
+        // bizJson 默认传入客户姓名占位,运行时可根据实际业务填充
+        phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid));
+        addListParam.setPhoneList(Collections.singletonList(phoneItem));
+        easyCallService.addCommonCallList(addListParam, null);
+        log.info("workflowCallPhoneOne4EasyCall: 名单追加成功 - batchId: {}, phone: {}",
+                batchId, callees.getPhone());
+
+        // 6. 启动外呼任务
+        easyCallService.startTask(batchId, null);
+        log.info("workflowCallPhoneOne4EasyCall: 任务启动成功 - batchId: {}", batchId);
+        addLog.setStatus(1);
+        addLog.setCallbackUuid(callBackUuid);
+        companyVoiceRoboticCallLogCallphoneService.asyncInsertCompanyVoiceRoboticCallLog(addLog);
+        // 7. 将 batchId 写入 context,方便后续节点使用
+        context.setVariable("easyCallBatchId", batchId);
     }
 
 //    @Override

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

@@ -1,5 +1,6 @@
 package com.fs.company.service.impl.call.node;
 
+import cn.hutool.core.collection.CollectionUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.redis.RedisCacheT;
@@ -65,10 +66,14 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
         boolean addSuccess = wxClient != null && Integer.valueOf(1).equals(wxClient.getIsAdd());
         //回调加微成功
         if (addSuccess) {
-            List<CompanyWorkflowEdge> cList = edges.stream().filter(a ->
-                            StringUtils.isNotBlank(a.getConditionExpr()) && JSONObject.parseArray(a.getConditionExpr(), AiCallWorkflowConditionVo.class).get(0).isAdd())
-                    .collect(Collectors.toList());
-            if(!cList.isEmpty() && nodeKey.equals(exec.getCurrentNodeKey())){
+            List<CompanyWorkflowEdge> cList = edges.stream().filter(a -> {
+                if (StringUtils.isBlank(a.getConditionExpr())) {
+                    return false;
+                }
+                List<AiCallWorkflowConditionVo> list = JSONObject.parseArray(a.getConditionExpr(), AiCallWorkflowConditionVo.class);
+                return list != null && !list.isEmpty() && list.get(0).isAdd();
+            }).collect(Collectors.toList());
+            if (!cList.isEmpty() && nodeKey.equals(exec.getCurrentNodeKey())) {
                 super.runNextNode(context, cList.get(0));
             }
         }
@@ -78,7 +83,8 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
                     .collect(Collectors.toList());
             // 加微失败,根据条件判断走哪条边
             CompanyWorkflowEdge edge = cList.get(0);
-                AiCallWorkflowConditionVo condition = JSONObject.parseObject(edge.getConditionExpr(), AiCallWorkflowConditionVo.class);
+            List<AiCallWorkflowConditionVo> conditions = JSONObject.parseArray(edge.getConditionExpr(), AiCallWorkflowConditionVo.class);
+             AiCallWorkflowConditionVo condition = conditions.get(0);
                 // 匹配失败条件
                 if (!condition.isAdd()) {
                     log.info("加微失败,执行失败分支 - workflowInstanceId: {}", context.getWorkflowInstanceId());
@@ -104,8 +110,8 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
             return ExecutionResult.failure().nextNodeKey(null).build();
         }
         try {
-            super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.PAUSED);
-            return ExecutionResult.paused()
+            super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.WAITING);
+            return ExecutionResult.waiting()
                     .outputData(context.getVariables())
                     .nextNodeKey("").build();
         } catch (Exception e) {
@@ -184,49 +190,49 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
      * getRedisCacheKey
      *
      */
-    public static String getDelayAddWxKeyPrefix(Long time) {
+    public static String getDelayAddWxKeyPrefix(Integer cidGroupNo,Long time) {
         Date nowDay;
         if (null != time) {
             nowDay = new Date(time);
         }else{
             nowDay = new Date();
         }
-        return String.format(DELAY_QW_ADD_WX_KEY, nowDay.getHours(), nowDay.getMinutes());
+        return String.format(DELAY_QW_ADD_WX_KEY,cidGroupNo, nowDay.getHours(), nowDay.getMinutes());
     }
 
     /**
      * 完成加微动作
      *
      */
-//    public void doneQwAddWx(String workflowInstanceId) {
-//        ExecutionContext context = createExecutionContext(workflowInstanceId, nodeKey);
-//        context.setVariable("lastNodeKey", nodeKey);
-//        //启动定时节点倒计时
-//        CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
-//        if (!exec.getCurrentNodeKey().equals(nodeKey)) {
-//            //当前节点已流转
-//            log.error("当前节点已流转 ,目标:{},实际:{}", nodeKey, exec.getCurrentNodeKey());
-//            return;
-//        }
-//        //更新加微日志执行状态
-//        super.updateLogStatusIfExist(context, ExecutionStatusEnum.PAUSED, ExecutionStatusEnum.WAITING);
-//        super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), nodeKey, context, ExecutionStatusEnum.WAITING);
-//        List<CompanyWorkflowEdge> edges = companyWorkflowEdgeMapper.selectListByWorkflowIdAndNodeKey(exec.getWorkflowId(), nodeKey);
-//        edges.forEach(edge -> {
-//            List<AiCallWorkflowConditionVo> conditions = JSONObject.parseArray(edge.getConditionExpr(), AiCallWorkflowConditionVo.class);
-//            if (null == conditions || conditions.isEmpty()) {
-//                super.runNextNode(context, edge);
-//            } else {
-//                AiCallWorkflowConditionVo condition = conditions.get(0);
-//                //节点包含延时条件
-//                if (null != condition.getAddTime() && !condition.isAdd()) {
-//                    long l = System.currentTimeMillis() + condition.getAddTime() * 60 * 1000;
-//                    String redisKey = getDelayAddWxKeyPrefix(l) + workflowInstanceId;
-//                    ExecutionContext nextContext = context.clone();
-//                    nextContext.setCurrentNodeKey(edge.getTargetNodeKey());
-//                    super.redisCache.setCacheObject(redisKey, nextContext);
-//                }
-//            }
-//        });
-//    }
+    public void doneQwAddWx(String workflowInstanceId) {
+        ExecutionContext context = createExecutionContext(workflowInstanceId, nodeKey);
+        context.setVariable("lastNodeKey", nodeKey);
+        //启动定时节点倒计时
+        CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
+        if (!exec.getCurrentNodeKey().equals(nodeKey)) {
+            //当前节点已流转
+            log.error("当前节点已流转 ,目标:{},实际:{}", nodeKey, exec.getCurrentNodeKey());
+            return;
+        }
+        //更新加微日志执行状态
+        super.updateLogStatusIfExist(context, ExecutionStatusEnum.WAITING, ExecutionStatusEnum.WAITING);
+        super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), nodeKey, context, ExecutionStatusEnum.WAITING);
+        List<CompanyWorkflowEdge> edges = companyWorkflowEdgeMapper.selectListByWorkflowIdAndNodeKey(exec.getWorkflowId(), nodeKey);
+        edges.forEach(edge -> {
+            List<AiCallWorkflowConditionVo> conditions = JSONObject.parseArray(edge.getConditionExpr(), AiCallWorkflowConditionVo.class);
+            if (CollectionUtil.isEmpty(conditions)) {
+                super.runNextNode(context, edge);
+            } else {
+                AiCallWorkflowConditionVo condition = conditions.get(0);
+                //节点包含延时条件
+                if (null != condition.getAddTime() && !condition.isAdd()) {
+                    long l = System.currentTimeMillis() + condition.getAddTime() * 60 * 1000;
+                    String redisKey = getDelayAddWxKeyPrefix(exec.getCidGroupNo(),l) + workflowInstanceId;
+                    ExecutionContext nextContext = context.clone();
+                    nextContext.setCurrentNodeKey(edge.getTargetNodeKey());
+                    super.redisCache.setCacheObject(redisKey, nextContext);
+                }
+            }
+        });
+    }
 }

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

@@ -3,6 +3,7 @@ package com.fs.company.service.impl.call.node;
 import com.fs.company.service.IWorkflowNode;
 import com.fs.company.service.IWorkflowNodeFactory;
 import com.fs.enums.NodeTypeEnum;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 import java.util.Map;
@@ -15,7 +16,6 @@ import java.util.Map;
 @Component
 public class WorkflowNodeFactory implements IWorkflowNodeFactory {
 
-
     @Override
     public IWorkflowNode createNode(String nodeKey, NodeTypeEnum type, String nodeName,
                                     Map<String, Object> properties) {

+ 22 - 0
fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java

@@ -38,4 +38,26 @@ public class AiCallConfigVO {
      */
     private String cidGroupId;
 
+    // ===== EasyCallCenter365 外呼新字段 =====
+    /**
+     * 外呼线路(网关列表接口返回的 id)
+     */
+    private Long gatewayId;
+    /**
+     * 大模型底座(大模型配置列表接口返回的 id)
+     */
+    private Long llmAccountId;
+    /**
+     * 音色编号(音色列表接口返回的 voiceCode)
+     */
+    private String voiceCode;
+    /**
+     * 音色来源(音色列表接口返回的 voiceSource,如 aliyun_tts)
+     */
+    private String voiceSource;
+    /**
+     * tts厂商 / 技能组 id(技能组列表接口返回的 groupId)
+     */
+    private String busiGroupId;
+
 }

+ 124 - 0
fs-service/src/main/java/com/fs/company/vo/CdrBodyVo.java

@@ -0,0 +1,124 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CdrBodyVo {
+
+    private String uuid;
+
+    private String cdrType;
+
+    private String cdrBody;
+
+    private Long answeredTime;
+    private Long answeredTimeLen;
+    private String callee;
+    private String caller;
+    private List<ChatContent> chatContent;
+    private String extnum;
+    private String groupId;
+    private String hangupCause;
+    private Long hangupTime;
+    private String id;
+    private Long inboundTime;
+    private String ivrDtmfDigits;
+    private Long manualAnsweredTime;
+    private Long manualAnsweredTimeLen;
+    private String opnum;
+    private OutboundPhoneInfo outboundPhoneInfo;
+    private Integer remoteVideoPort;
+    private Boolean startDtmfExecuted;
+    private Long timeLen;
+    private Boolean transferredSucceed;
+    private String wavFile;
+
+    @Data
+    public static class ChatContent {
+        private String content;
+        private String content_type;
+        private String role;
+        private Integer completionTokens;
+        private Integer promptTokens;
+        private String bizJson;
+    }
+
+    @Data
+    public static class OutboundPhoneInfo {
+        private String acdOpnum;
+        private Long acdQueueTime;
+        private Long acdWaitTime;
+        private Long answeredTime;
+        private Long batchId;
+        private String bizJson;
+        private Long callEndTime;
+        private Integer callcount;
+        private Long calloutTime;
+        private Integer callstatus;
+        private Long connectedTime;
+        private Long createtime;
+        private String custName;
+        private List<Dialogue> dialogue;
+        private Integer dialogueCount;
+        private String emptyNumberDetectionText;
+        private Boolean hangup;
+        private String hangupCause;
+        private String id;
+        private String ivrDtmfDigits;
+        private Long manualAnsweredTime;
+        private Long manualAnsweredTimeLen;
+        private String recordServerUrl;
+        private TaskInfo taskInfo;
+        private String telephone;
+        private Long timeLen;
+        private Boolean transferred;
+        private String ttsText;
+        private String uuid;
+        private Long validTimeLen;
+        private String wavfile;
+    }
+
+    @Data
+    public static class Dialogue {
+        private String $ref;
+    }
+
+    @Data
+    public static class TaskInfo {
+        private String aiTransferData;
+        private String aiTransferType;
+        private String asrProvider;
+        private String authUsername;
+        private Integer autoStop;
+        private Long avgCallEndProcessTimeLen;
+        private Long avgCallTalkTimeLen;
+        private Long avgRingTimeLen;
+        private Long batchId;
+        private String batchName;
+        private String callNodeNo;
+        private String calleePrefix;
+        private String caller;
+        private String codec;
+        private Long createtime;
+        private Integer executing;
+        private Long gatewayId;
+        private String groupId;
+        private String gwAddr;
+        private String gwName;
+        private Integer ifcall;
+        private String ivrId;
+        private Long llmAccountId;
+        private Integer playTimes;
+        private String profileName;
+        private Integer rate;
+        private Integer register;
+        private Long stopTime;
+        private Integer taskType;
+        private Integer threadNum;
+        private String userid;
+        private String voiceCode;
+        private String voiceSource;
+    }
+}

+ 19 - 0
fs-service/src/main/java/com/fs/company/vo/CdrDetailVo.java

@@ -0,0 +1,19 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+@Data
+public class CdrDetailVo {
+
+    private String uuid;
+
+    /**
+     *  话单类型: inbound、outbound
+     */
+    private String cdrType;
+
+    /**
+     * 话单消息体;
+     */
+    private String cdrBody;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallAddCallListParam.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 AI外呼追加名单请求参数
+ */
+@Data
+public class EasyCallAddCallListParam {
+    /** 任务id */
+    private Long batchId;
+    /** 号码列表 */
+    private List<String> phoneList;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBusiGroupVO.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 技能组VO
+ */
+@Data
+public class EasyCallBusiGroupVO {
+    /** 技能组id */
+    private Long groupId;
+    /** 技能组名称 */
+    private String bizGroupName;
+    /** 备注 */
+    private String notes;
+}

+ 216 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCallPhoneVO.java

@@ -0,0 +1,216 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/7 11:21
+ * @description
+ */
+@Data
+public class EasyCallCallPhoneVO {
+    /**
+     * 主键ID
+     */
+    private String id;
+
+    /**
+     * 任务批次ID
+     */
+    private Integer batchId;
+
+    /**
+     * 电话号码
+     */
+    private String telephone;
+
+    /**
+     * 客户称呼
+     */
+    private String custName;
+
+    /**
+     * 任务创建时间
+     */
+    private Long createtime;
+
+    /**
+     * 呼叫状态
+     * 0. Not dialed
+     * 1. Entered call queue
+     * 2. Dialing (in progress)
+     * 3. Not connected (If the empty number detection feature is turned off)
+     * 4. Connected
+     * 5. Call dropped (hung up before transferring to agent)
+     * 6. Successfully transferred to agent or AI
+     * 7. Line failure
+     * 30. Not connected
+     * 31. Customer is on another call
+     * 32. Phone is powered off
+     * 33. Invalid number
+     * 34. No answer
+     * 35. Suspended service
+     * 36. Network busy
+     * 37. Voice assistant
+     * 38. Temporarily unavailable
+     * 39. Call restriction
+     */
+    private Short callstatus;
+
+    /**
+     * 外呼时间
+     */
+    private Long calloutTime;
+
+    /**
+     * 呼叫次数
+     */
+    private Short callcount;
+
+    /**
+     * 呼叫结束时间
+     */
+    private Long callEndTime;
+
+    /**
+     * 通话时长; 秒;
+     */
+    private Integer timeLen;
+
+    /**
+     * 人工接听的通话时长; 秒
+     */
+    private Integer validTimeLen;
+
+    /**
+     * 通话唯一标志
+     */
+    private String uuid;
+
+    /**
+     * 通话接通时间
+     */
+    private Long connectedTime;
+
+    /**
+     * 挂机原因
+     */
+    private String hangupCause;
+
+    /**
+     * 人工坐席应答时间
+     */
+    private Long answeredTime;
+
+    /**
+     * 对话内容
+     */
+    private String dialogue;
+
+    /**
+     * 全程通话录音文件名
+     */
+    private String wavfile;
+
+    /**
+     * 录音文件路径前缀
+     */
+    private String recordServerUrl;
+
+    /**
+     * 业务json数据
+     */
+    private String bizJson;
+
+    /**
+     * 交互轮次(一问一答算一轮交互)
+     */
+    private Integer dialogueCount;
+
+    /**
+     * 人工坐席工号
+     */
+    private String acdOpnum;
+
+    /**
+     * 加入转人工排队的时间;
+     */
+    private Long acdQueueTime;
+
+    /**
+     * 人工排队等待时长,秒
+     */
+    private Integer acdWaitTime;
+
+    /**
+     * tts text for voice call notification.
+     */
+    private String ttsText;
+
+    /**
+     * 空号检测文本
+     */
+    private String emptyNumberDetectionText;
+
+    /**
+     * 客户意向
+     */
+    private String intent;
+
+    /**
+     * asr时长(秒)
+     */
+    private Integer asrSeconds;
+
+    /**
+     * tts调用次数(次)
+     */
+    private Integer ttsTimes;
+
+    /**
+     * 大模型tts的字符数(字符)
+     */
+    private Integer ttsFlowTokens;
+
+    /**
+     * 总输入token数
+     */
+    private Integer inputTokens;
+
+    /**
+     * 总输出token数
+     */
+    private Integer outputTokens;
+
+    /**
+     * 总调用费用(asr+tts+大模型)
+     */
+    private BigDecimal totalCost;
+
+    /**
+     * 计费状态(1:已计费、0:未计费)
+     */
+    private Integer billingStatus;
+
+    /**
+     * 主叫号码
+     */
+    private String callerNumber;
+
+    /**
+     * customer dtmf input digits
+     */
+    private String ivrDtmfDigits;
+
+    /**
+     * manual agent answered time.
+     */
+    private Long manualAnsweredTime;
+
+    /**
+     * The duration of the manual agent service time.
+     */
+    private Long manualAnsweredTimeLen;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCommonAddCallListParam.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 通用追加名单请求参数
+ */
+@Data
+public class EasyCallCommonAddCallListParam {
+    /** 任务id */
+    private Long batchId;
+    /** 号码列表 */
+    private List<EasyCallPhoneItemVO> phoneList;
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCreateTaskParam.java

@@ -0,0 +1,36 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 创建任务请求参数
+ */
+@Data
+public class EasyCallCreateTaskParam {
+    /** 任务名称 */
+    private String batchName;
+    /** 最大外呼并发 */
+    private Long threadNum;
+    /** 任务类型:1-AI外呼,2-通知提醒 */
+    private Integer taskType;
+    /** 线路id(来自网关列表接口) */
+    private Long gatewayId;
+    /** 大模型配置id(来自大模型配置列表接口) */
+    private Long llmAccountId;
+    /** 音色编号(taskType=1时必填) */
+    private String voiceCode;
+    /** 音色来源(taskType=1时必填) */
+    private String voiceSource;
+    /** 播放次数(taskType=2时必填) */
+    private Integer playTimes;
+    /** 业务组id(AI外呼需要转人工时必填) */
+    private String groupId;
+    /** 预估接通率(taskType=0时必填,如接通率30%则传30) */
+    private Integer conntectRate;
+    /** 平均振铃时长(taskType=0时必填,单位秒) */
+    private Double avgRingTimeLen;
+    /** 平均通话时长(taskType=0时必填,单位秒) */
+    private Double avgCallTalkTimeLen;
+    /** 平均事后处理时长(taskType=0时必填,单位秒) */
+    private Double avgCallEndProcessTimeLen;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallGatewayVO.java

@@ -0,0 +1,28 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 网关信息VO
+ */
+@Data
+public class EasyCallGatewayVO {
+    /** 网关id */
+    private Long id;
+    /** 网关名称 */
+    private String gwName;
+    /** profile名称 */
+    private String profileName;
+    /** 外呼的主叫号码 */
+    private String caller;
+    /** 被叫前缀 */
+    private String calleePrefix;
+    /** 网关地址 */
+    private String gwAddr;
+    /** 语音编码 */
+    private String codec;
+    /** 网关描述 */
+    private String gwDesc;
+    /** 网关用途:2-AI外呼,3-不限制 */
+    private Integer purpose;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallLlmAccountVO.java

@@ -0,0 +1,28 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 大模型配置VO
+ */
+@Data
+public class EasyCallLlmAccountVO {
+    /** 大模型配置id */
+    private Integer id;
+    /** 大模型配置名称 */
+    private String name;
+    /** 配置详细信息 */
+    private String accountJson;
+    /** 大模型类型:LlmAccount / CozeAccount */
+    private String accountEntity;
+    /** 实现类:DeepSeekChat / Coze / MaxKB / Dify */
+    private String providerClassName;
+    /** 是否支持打断 */
+    private Integer interruptFlag;
+    /** 打断关键词列表 */
+    private String interruptKeywords;
+    /** 打断忽略关键字列表 */
+    private String interruptIgnoreKeywords;
+    /** 客户意向提示词 */
+    private String intentionTips;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPageResult.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 分页结果VO
+ */
+@Data
+public class EasyCallPageResult<T> {
+    /** 总数 */
+    private Long total;
+    /** 当前页数据 */
+    private List<T> rows;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPhoneItemVO.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 通用追加名单中的号码条目VO
+ */
+@Data
+public class EasyCallPhoneItemVO {
+    /** 号码 */
+    private String phoneNum;
+    /** 提醒内容(提醒类任务必填) */
+    private String noticeContent;
+    /** 业务数据(需要传给机器人的业务数据,如客户信息) */
+    private Object bizJson;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordQueryParam.java

@@ -0,0 +1,34 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 通话记录查询请求参数
+ */
+@Data
+public class EasyCallRecordQueryParam {
+    /** 页号(传空则返回全部) */
+    private Integer pageNum;
+    /** 每页数量(传空则返回全部) */
+    private Integer pageSize;
+    /** 通话唯一标识 */
+    private String uuid;
+    /** 类型:01-呼入,02-AI外呼,03-人工外呼 */
+    private String callType;
+    /** 客户号码(外呼即被叫号码) */
+    private String telephone;
+    /** 通话状态:3-未接通,6-成功转接,7-线路故障 */
+    private Integer callstatus;
+    /** 主叫号码 */
+    private String callerNumber;
+    /** 拨打时间起(格式:yyyy-MM-dd HH:mm:ss) */
+    private String calloutTimeStart;
+    /** 拨打外呼时间止(格式:yyyy-MM-dd HH:mm:ss) */
+    private String calloutTimeEnd;
+    /** 通话时长起(单位秒) */
+    private Integer timeLenStart;
+    /** 通话时长止(单位秒) */
+    private Integer timeLenEnd;
+    /** 分机号 */
+    private String extnum;
+}

+ 39 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordVO.java

@@ -0,0 +1,39 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description EasyCallCenter365 通话记录VO
+ */
+@Data
+public class EasyCallRecordVO {
+    /** 通话唯一标识 */
+    private String uuid;
+    /** 号码 */
+    private String telephone;
+    /** 通话状态:3-未接通,6-成功转接,7-线路故障 */
+    private Integer callstatus;
+    /** 外呼记录id(AI外呼) */
+    private String sessionId;
+    /** 主叫号码 */
+    private String callerNumber;
+    /** 外呼/呼入时间 */
+    private String calloutTime;
+    /** 接通时间 */
+    private String answeredTime;
+    /** 挂机时间 */
+    private String callEndTime;
+    /** 挂机原因 */
+    private String hangupCause;
+    /** 录音文件url(需拼接系统baseUrl) */
+    private String wavFileUrl;
+    /** 对话内容 */
+    private List<Map<String, String>> dialogue;
+    /** 分机号 */
+    private String extnum;
+    /** 通话时长(秒) */
+    private Integer timeLen;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskQueryParam.java

@@ -0,0 +1,22 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 任务列表查询请求参数
+ */
+@Data
+public class EasyCallTaskQueryParam {
+    /** 页号 */
+    private Integer pageNum = 1;
+    /** 每页数量 */
+    private Integer pageSize = 10;
+    /** 任务id */
+    private Long batchId;
+    /** 任务名称(支持模糊搜索) */
+    private String batchName;
+    /** 任务创建时间起(格式:yyyy-MM-dd HH:mm:ss) */
+    private String createTimeStart;
+    /** 任务创建时间止(格式:yyyy-MM-dd HH:mm:ss) */
+    private String createTimeEnd;
+}

+ 54 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskVO.java

@@ -0,0 +1,54 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 任务信息VO(查询列表/创建任务通用)
+ */
+@Data
+public class EasyCallTaskVO {
+    /** 任务id */
+    private Long batchId;
+    /** 任务名称 */
+    private String batchName;
+    /** 最大外呼并发 */
+    private Long threadNum;
+    /** 任务类型:0-人工预测外呼,1-AI外呼,2-通知提醒 */
+    private Integer taskType;
+    /** 线路id */
+    private Long gatewayId;
+    /** 大模型配置id */
+    private Long llmAccountId;
+    /** 音色编号 */
+    private String voiceCode;
+    /** 音色来源 */
+    private String voiceSource;
+    /** 播放次数(taskType=2时必填) */
+    private Integer playTimes;
+    /** 业务组id */
+    private String groupId;
+    /** 预估接通率(taskType=0时必填) */
+    private Integer conntectRate;
+    /** 平均振铃时长(taskType=0时必填,单位秒) */
+    private Double avgRingTimeLen;
+    /** 平均通话时长(taskType=0时必填,单位秒) */
+    private Double avgCallTalkTimeLen;
+    /** 平均事后处理时长(taskType=0时必填,单位秒) */
+    private Double avgCallEndProcessTimeLen;
+    /** 创建时间(时间戳) */
+    private Long createtime;
+    /** 结束时间(时间戳,0代表没有结束) */
+    private Long stopTime;
+    /** 总电话量 */
+    private Integer phoneCount;
+    /** 未拨打电话量 */
+    private Integer noCallCount;
+    /** 已拨打电话量 */
+    private Integer callCount;
+    /** 接通量 */
+    private Integer connectCount;
+    /** 未接通量 */
+    private Integer noConnectCount;
+    /** 实际接通率 */
+    private Double realConnectRate;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallVoiceCodeVO.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 音色VO
+ */
+@Data
+public class EasyCallVoiceCodeVO {
+    /** 音色编号 */
+    private String voiceCode;
+    /** 音色名称 */
+    private String voiceName;
+    /** 声音源:aliyun_tts */
+    private String voiceSource;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -750,4 +750,6 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     List<FsCourseReportVO> selectWatchStatistics(FsCourseWatchLogStatisticsListParam param);
 
     List<FsSopMyCourseH5LinkVO> getSopCourseH5StudyList(@Param("userId") Long userId);
+
+    List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact);
 }

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

@@ -5,6 +5,7 @@ import com.fs.common.core.domain.R;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.param.*;
 import com.fs.course.vo.*;
+import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.param.QwSidebarStatsParam;
 import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 
@@ -166,4 +167,6 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
     R encryptLink(String url);
 
     R decryptLink(String url);
+
+    List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact);
 }

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

@@ -1719,4 +1719,9 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return R.ok().put("data", data);
     }
 
+    @Override
+    public List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact) {
+        return fsCourseWatchLogMapper.selectFsUserWatchLogByExtId(qwExternalContact);
+    }
+
 }

+ 3 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -4549,10 +4549,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
      * 小黄车商品和展示
      */
     private void getGoodsAndShow(Long videoId, FsUserCourseVideoDetailsVO vo) {
-        String show = fsDepVideoShowMapper.selectFsDepVideoShowByVideoId(videoId, null);
-        vo.setShowProduct(show);
+//        String show = fsDepVideoShowMapper.selectFsDepVideoShowByVideoId(videoId, null);
+//        vo.setShowProduct(show);
 
         FsUserCourseVideo courseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
+        vo.setShowProduct(courseVideo.getIsProduct() == 1 ? "0" : "1");
         String packageJson = courseVideo.getPackageJson();
         if (StringUtils.isNotEmpty(packageJson)) {
             ObjectMapper objectMapper = new ObjectMapper();

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

@@ -516,7 +516,7 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
         FsPatient patient=null;
         FsDoctor doctor=null;
         FsPackage fsPackage=fsPackageMapper.selectFsPackageByPackageId(param.getPackageId());
-        if(fsPackage.getProductType()!= null &&(fsPackage.getProductType()==1 || fsPackage.getProductType()==2)){
+        if(fsPackage.getProductType()!= null &&(fsPackage.getProductType()==1 || fsPackage.getProductType()==2) && fsPackage.getIsHealthProductType() == 0){
             if(param.getPatientId()!=null){
                 patient=fsPatientMapper.selectFsPatientByPatientId(param.getPatientId());
                 if (patient==null){

+ 363 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrmSidebarParam.java

@@ -0,0 +1,363 @@
+package com.fs.hisStore.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+
+@Data
+public class FsStoreOrderScrmSidebarParam extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 订单ID */
+    private Long id;
+
+    /** 订单号 */
+    @Excel(name = "订单号")
+    private String orderCode;
+
+    /** 额外订单号 */
+    private String extendOrderId;
+
+    private String payOrderId;
+
+    private String bankOrderId;
+
+    /** 用户id */
+    private Long userId;
+
+    /** 用户姓名 */
+    @Excel(name = "收货人")
+    private String realName;
+
+    /** 用户电话 */
+    @Excel(name = "手机号")
+    private String userPhone;
+
+    /** 详细地址 */
+    @Excel(name = "详细地址")
+    private String userAddress;
+
+    /** 购物车id */
+    private String cartId;
+
+    /** 运费金额 */
+    private BigDecimal freightPrice;
+
+    /** 订单商品总数 */
+    @Excel(name = "订单商品总数")
+    private Long totalNum;
+
+    /** 订单总价 */
+    @Excel(name = "订单总价")
+    private BigDecimal totalPrice;
+
+    /** 邮费 */
+    private BigDecimal totalPostage;
+
+    /** 实际支付金额 */
+    @Excel(name = "应付金额")
+    private BigDecimal payPrice;
+
+    /** 支付邮费 */
+    private BigDecimal payPostage;
+
+    @Excel(name = "物流代收金额")
+    private BigDecimal payDelivery;
+
+    @Excel(name = "支付金额")
+    private BigDecimal payMoney;
+
+    /** 抵扣金额 */
+    private BigDecimal deductionPrice;
+
+    /** 优惠券id */
+    private Long couponId;
+
+    /** 优惠券金额 */
+    private BigDecimal couponPrice;
+
+    /** 支付状态 */
+    @Excel(name = "支付状态 1已支付 0未支付 2支付中")
+    private Integer paid;
+
+    /** 支付时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    /** 支付方式 */
+    @Excel(name = "支付方式 1线上 2物流代收")
+    private String payType;
+
+    private Integer status;
+
+    /** 0 未退款 1 申请中 2 已退款 */
+    private Integer refundStatus;
+
+    /** 退款图片 */
+    @Excel(name = "退款图片")
+    private String refundReasonWapImg;
+
+    /** 退款用户说明 */
+    @Excel(name = "退款用户说明")
+    private String refundReasonWapExplain;
+
+    /** 退款时间 */
+
+    private Date refundReasonTime;
+
+    /** 前台退款原因 */
+    private String refundReasonWap;
+
+    /** 不退款的理由 */
+    private String refundReason;
+
+    /** 退款金额 */
+    private BigDecimal refundPrice;
+
+    /** 快递公司编号 */
+    @Excel(name = "快递公司编号")
+    private String deliverySn;
+
+    /** 快递名称/送货人姓名 */
+    @Excel(name = "快递名称")
+    private String deliveryName;
+
+    /** 发货类型 */
+    private String deliveryType;
+
+    /** 快递单号/手机号 */
+    @Excel(name = "快递单号")
+    private String deliveryId;
+
+    /** 消费赚取积分 */
+    private BigDecimal gainIntegral;
+
+    /** 使用积分 */
+    @Excel(name = "使用积分")
+    private BigDecimal useIntegral;
+
+    /** 实际支付积分 */
+    @Excel(name = "实际支付积分")
+    private BigDecimal payIntegral;
+
+    /** 给用户退了多少积分 */
+    private BigDecimal backIntegral;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String mark;
+
+
+    /** 是否删除 */
+    private Integer isDel;
+
+    /** 成本价 */
+    private BigDecimal cost;
+
+    /** 核销码 */
+    private String verifyCode;
+
+    /** 门店id */
+    private Long storeId;
+
+    /** 配送方式 1=快递 ,2=门店自提 */
+    private Integer shippingType;
+
+    /** 支付渠道(0微信公众号1微信小程序) */
+    private Integer isChannel;
+
+    /** 是否提醒 */
+    private Integer isRemind;
+
+    /** 是否系统删除 */
+    private Integer isSysDel;
+
+    private Integer isPrescribe;
+
+    private Long prescribeId;
+
+    private Long companyId;
+
+    private Long companyUserId;
+
+    private Integer isPackage;
+
+    private String packageJson;
+
+    private String itemJson;
+
+    // 直播订单类型:2
+    private Integer orderType;
+
+    private Long packageId;
+
+    private Integer orderCreateType;
+
+    private String storeHouseCode;
+
+
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "完成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date finishTime;
+
+    private Integer deliveryStatus;
+
+    private Integer deliveryPayStatus;
+
+    @Excel(name = "快递帐单日期")
+    private String deliveryTime;
+
+    @Excel(name = "快递结算日期")
+    private String deliveryPayTime;
+
+    private BigDecimal deliveryPayMoney;
+
+    private BigDecimal tuiMoney;
+
+    private Integer tuiMoneyStatus;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "回单导入时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date deliveryImportTime;
+
+    private Long tuiUserId;
+
+    private Integer tuiUserMoneyStatus;
+
+    private Long deptId;
+
+    private Integer isEditMoney;
+
+    private Long customerId;
+
+    private Integer isPayRemain;
+
+
+    @Excel(name = "剩余金额")
+    @TableField(exist = false)
+    private BigDecimal payRemain;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "发货时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date deliverySendTime;
+
+    //凭证
+    private String certificates;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date uploadTime;
+
+    private Long scheduleId; //归属档期id
+
+    private String orderVisit; //订单跟随阶段
+
+    private BigDecimal serviceFee;
+
+    private Integer orderMedium; //媒体来源
+
+
+    @Excel(name = "医生ID")
+    private Long doctorId;
+
+    /** 支付状态 待支付 1已支付 */
+    @Excel(name = "支付状态 待支付 1已支付")
+    private Integer isPay;
+
+    /** 退款图片 */
+    @Excel(name = "退款图片")
+    private String refundImg;
+
+    /** 退款用户说明 */
+    @Excel(name = "退款用户说明")
+    private String refundExplain;
+
+    /** 退款时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "退款时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date refundTime;
+
+    /** 退款金额 */
+    @Excel(name = "退款金额")
+    private BigDecimal refundMoney;
+
+    /** 快递公司编号 */
+    @Excel(name = "快递公司编号")
+    private String deliveryCode;
+
+    /** 成本价 */
+    @Excel(name = "成本价")
+    private BigDecimal costPrice;
+
+    private BigDecimal discountMoney;
+
+    private Long userCouponId;
+
+    private Long inquiryOrderId;
+
+    private Long packageOrderId;
+
+    private String remark;
+
+    private Integer isAfterSales;
+
+    private BigDecimal prescribePrice;
+
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date followTime;
+
+    private Long followDoctorId;
+    private Integer cycle;
+    private Integer followNum;
+
+
+    private Long orderBuyType;//购买类型,同药品商城的订单订单类型
+    private String channel;//公众号/渠道
+
+    private String orderChannel;//订单渠道
+
+    private String qwSubject;
+
+    private String packageName;
+
+    private String packageSecondName;
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date followEndTime;
+
+    private Integer followStatus;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date deliveryUpdateTime;
+
+    private Integer isFirst;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date tuiMoneyTime;
+
+    private Integer source;
+    private BigDecimal billPrice;
+    private String erpPhone;
+
+    @TableField(exist = false)
+    private String bankTransactionId;
+
+    // 是否审核,1-是,0-否
+    private Integer isAudit;
+
+    // 订单总后台备注
+    private String orderRemark;
+
+    // 后台修改商品类型,0-未修改过;1-总后台;2-销售后台
+    private Integer backendEditProductType;
+
+    // 线下支付金额
+    private BigDecimal offlinePayAmount;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java

@@ -112,4 +112,7 @@ public interface FsStoreCartScrmMapper
             "</if>" +
             "</script>"})
     Integer selectFsStoreCartCount(@Param("maps") FsStoreCartCountParam param);
+
+    @Select("select * from fs_store_cart_scrm c inner join fs_store_product_scrm p on p.product_id=c.product_id where c.is_pay=0 and c.is_del=0 and c.is_buy=0 and p.is_show=1 and p.is_del=0 and c.user_id = #{userId}")
+    List<FsStoreCartVO> selectFsStoreCartByUserId(Long userId);
 }

+ 12 - 4
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java

@@ -75,12 +75,11 @@ public interface FsStoreOrderItemScrmMapper
     List<FsStoreOrderItemVO> selectFsStoreOrderItemListAndProductByOrderId(Long id);
 
     @Select({"<script> " +
-            "select i.*,o.user_id,psps.cost,o.pay_postage,o.total_num,o.status,fspcs.cate_name, o.real_name,o.user_phone,o.user_address,o.create_time,o.pay_time,o.delivery_sn,o.delivery_name,o.delivery_id, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber,o.upload_time ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload,p.title as package_name,cts.name as scheduleName,os.pay_money, os.bank_transaction_id as bankTransactionId, o.delivery_send_time," +
+            "select i.*,o.user_id,psps.cost,o.pay_postage,o.total_num,o.status,fspcs.cate_name, o.real_name,o.user_phone,o.user_address,o.create_time,o.pay_time,o.delivery_sn,o.delivery_name,o.delivery_id, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber,o.upload_time ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload,p.title as package_name,cts.name as scheduleName,sp_latest.pay_money, sp_latest.bank_transaction_id as bankTransactionId, o.delivery_send_time," +
             " o.order_code, o.pay_price, o.pay_money, o.deduction_price,o.pay_delivery, o.order_type,psps.price " +
             ", CASE o.is_audit WHEN 1 THEN '是' ELSE '否' END AS isAudit " +
             " from fs_store_order_item_scrm i " +
             " left join fs_store_order_scrm o on o.id=i.order_id" +
-            " left join fs_store_payment_scrm os on os.business_order_id = o.id " +
             " left join fs_user u on o.user_id=u.user_id  " +
             " left join fs_store_product_package_scrm p on o.package_id=p.package_id " +
             " left join company c on c.company_id=o.company_id " +
@@ -99,6 +98,12 @@ public interface FsStoreOrderItemScrmMapper
             "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
             " and sp_latest.bank_transaction_id = #{maps.bankTransactionId} " +
             "</if>" +
+            "<if test=\"maps.orderCodes != null  and maps.orderCodes.size > 0\">" +
+            " and o.order_code in" +
+            " <foreach collection=\"maps.orderCodes\" item=\"orderCode\" open=\"(\" close=\")\" separator=\",\">" +
+            "     #{orderCode}" +
+            " </foreach>" +
+            "</if>" +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +
@@ -114,14 +119,17 @@ public interface FsStoreOrderItemScrmMapper
             "<if test = 'maps.userPhone != null and  maps.userPhone !=\"\"     '> " +
             "and o.user_phone like CONCAT('%',#{maps.userPhone},'%') " +
             "</if>" +
-            "<if test = 'maps.status != null    '> " +
+            "<if test = 'maps.status != null and maps.status != 6    '> " +
             "and o.status =#{maps.status} " +
             "</if>" +
+            "<if test = 'maps.status != null and maps.status == 6    '> " +
+            "and o.`status`= 1 and (o.extend_order_id is null or o.extend_order_id like '') " +
+            "</if>" +
             "<if test = 'maps.companyId != null    '> " +
             "and o.company_id =#{maps.companyId} " +
             "</if>" +
             "<if test = 'maps.isHealth != null and maps.isHealth !=  \"\"  '> " +
-            "and o.company_id is null " +
+            "and (o.company_id is null or o.order_type = 2 ) " +
             "</if>" +
             "<if test = 'maps.notHealth != null and maps.notHealth !=  \"\"  '> " +
             "and o.company_id is not null " +

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

@@ -657,7 +657,7 @@ public interface FsStoreOrderScrmMapper
     List<FsStoreOrderScrm> selectFsStoreOrderListByFinish7Day();
 
     @Select({"<script> " +
-            "select o.*,cts.name as scheduleName,u.nickname,u.phone,cc.push_code,cc.create_time as customer_create_time,cc.source,cc.customer_code, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber ,p.title as package_title ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload  " +
+            "select o.*,cts.name as scheduleName,sp_latest.pay_code as hfshh,u.nickname,u.phone,cc.push_code,cc.create_time as customer_create_time,cc.source,cc.customer_code, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber ,p.title as package_title ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload  " +
             ", CASE o.is_audit WHEN 1 THEN '是' ELSE '否' END AS isAudit " +
             " from fs_store_order_scrm o  " +
             " left JOIN fs_store_product_package_scrm p on o.package_id=p.package_id " +
@@ -1485,4 +1485,8 @@ public interface FsStoreOrderScrmMapper
     List<FsStoreOrderScrm> selectFsStoreOrderByUserIDexcludeCurrentOrderId(@Param("userId") Long userId, @Param("notOrderId") Long notOrderId,@Param("offset") Integer offset, @Param("limit") Integer limit);
     @Select("SELECT count(*) FROM fs_store_order_scrm WHERE user_id = #{userId} AND id != #{notOrderId}")
     int selectFsStoreOrderByUserIDexcludeCurrentOrderIdCount(@Param("userId") Long userId, @Param("notOrderId") Long notOrderId);
+
+
+    List<FsMyStoreOrderListQueryVO> selectFsStoreOrderListBySidebarVO(@Param("maps") FsStoreOrderScrmSidebarVO param);
+
 }

+ 19 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductCategoryScrmMapper.java

@@ -116,4 +116,23 @@ public interface FsStoreProductCategoryScrmMapper
     List<OptionsVO> selectFsStoreProductPidList();
 
     List<Long> selectCateIdsByName(FsStoreProductCategoryScrm fsStoreProductCategoryScrm);
+
+    @Select({"<script> " +
+            "select c.* from fs_store_product_category_scrm c " +
+            "where 1=1 " +
+            "<if test = 'maps.isDel != null    '> " +
+            "and c.is_del=#{maps.isDel} " +
+            "</if>" +
+            "<if test = 'maps.isShow != null    '> " +
+            "and c.is_show =#{maps.isShow} " +
+            "</if>" +
+            "<if test = 'maps.pid != null    '> " +
+            "and c.pid =#{maps.pid} " +
+            "</if>" +
+            "<if test = 'maps.storeId != null    '> " +
+            "and c.store_id =#{maps.storeId} " +
+            "</if>" +
+            " order by c.sort asc "+
+            "</script>"})
+    List<FsStoreProductCategoryScrm> selectFsStoreProductCategoryListQueryBySidebar(@Param("maps") FsStoreProductCategoryScrm param);
 }

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

@@ -107,6 +107,9 @@ public interface FsStoreProductScrmMapper
             "<if test = 'maps.isAudit != null '> " +
             "and p.is_audit = #{maps.isAudit} " +
             "</if>" +
+            "<if test = 'maps.isDel != null '> " +
+            "and p.is_del = #{maps.isDel} " +
+            "</if>" +
             "<if test='maps.drugRegCertNo != null and maps.drugRegCertNo != \"\"'>" +
             "    AND p.drug_reg_cert_no LIKE CONCAT('%', #{maps.drugRegCertNo}, '%')" +
             "</if>" +
@@ -184,6 +187,12 @@ public interface FsStoreProductScrmMapper
             "<if test = 'maps.isShow != null    '> " +
             "and p.is_show =#{maps.isShow} " +
             "</if>" +
+            "<if test = 'maps.isAudit != null    '> " +
+            "and p.is_audit =#{maps.isAudit} " +
+            "</if>" +
+            "<if test = 'maps.isDel != null    '> " +
+            "and p.is_del =#{maps.isDel} " +
+            "</if>" +
             "<if test = 'maps.excludeProductIds != null '>" +
             "and p.product_id not in " +
             "<foreach collection='maps.excludeProductIds'  item='item' index='index'  open='(' separator=',' close=')'> " +
@@ -439,4 +448,41 @@ public interface FsStoreProductScrmMapper
     List<FsStoreProductListVO> liveList(@Param("maps") LiveGoods maps);
 
     List<FsStoreProductScrm> selectFsStoreProductByProductIds(@Param("productIds") List<Long> productIdsLong);
+
+    @Select({"<script> " +
+            "select distinct p.* from fs_store_product_scrm p  " +
+            "where p.is_del=0 and p.is_show=1  and p.is_audit = '1'" +
+            "<if test = 'maps.productName != null and  maps.productName !=\"\"    '> " +
+            "and (p.product_name like CONCAT('%',#{maps.productName},'%')  or p.keyword like concat('%',#{maps.productName},'%') ) " +
+            "</if>" +
+            "<if test = 'maps.isDisplay != null and maps.isDisplay == 1   '> " +
+            "and p.is_display =#{maps.isDisplay}  " +
+            "</if>" +
+            "<if test = 'maps.companyId != null'> " +
+            "and find_in_set(#{maps.companyId}, p.company_ids) " +
+            "</if>" +
+            "<if test = 'maps.cateId != null    '> " +
+            "and p.cate_id =#{maps.cateId}  " +
+            "</if>" +
+            "<if test = 'maps.defaultOrder != null and maps.defaultOrder==\"desc\"  '> " +
+            "order by p.sort asc,product_id desc" +
+            "</if>" +
+            "<if test = 'maps.priceOrder != null and maps.priceOrder==\"desc\"   '> " +
+            "order by p.price desc " +
+            "</if>" +
+            "<if test = 'maps.priceOrder != null and maps.priceOrder==\"asc\"   '> " +
+            "order by p.price asc " +
+            "</if>" +
+            "<if test = 'maps.salesOrder != null  and maps.salesOrder==\"desc\" '> " +
+            "order by p.sales desc " +
+            "</if>" +
+            "<if test = 'maps.salesOrder != null  and maps.salesOrder==\"asc\" '> " +
+            "order by p.sales asc " +
+            "</if>" +
+            "<if test = 'maps.newOrder != null and maps.newOrder==\"desc\" '> " +
+            "and p.is_new =1 order by p.create_time desc  " +
+            "</if>" +
+            "</script>"})
+    List<FsStoreProductListQueryVO> selectFsStoreProductSidebarListQuery(@Param("maps")FsStoreOrderScrmSidebarVO param);
+
 }

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartDelParam.java

@@ -14,4 +14,6 @@ public class FsStoreCartDelParam {
     @ApiModelProperty(value = "购物车ID,多个用,分隔开")
     Long[] ids;
 
+    //侧边栏需要
+    private Long userId;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartNumParam.java

@@ -21,5 +21,8 @@ public class FsStoreCartNumParam {
     @ApiModelProperty(value = "购物车ID")
     private Long id;
 
+    //侧边栏需要
+    private Long userId;
+
 
 }

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreCartParam.java

@@ -28,5 +28,7 @@ public class FsStoreCartParam {
     @ApiModelProperty(value = "是否购买")
     private Integer isBuy=0;
 
+    //侧边栏需要
+    private Long userId;
 
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderParam.java

@@ -16,6 +16,9 @@ public class FsStoreOrderParam extends BaseEntity implements Serializable
     //多个订单号搜索
     private List<String> orderCodes;
 
+    /** 多个订单号搜索(逗号分隔字符串,用于GET请求) */
+    private String orderCodeList;
+
     private String nickname;
 
     private String phone;

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

@@ -81,4 +81,7 @@ public interface IFsStoreCartScrmService
 
     Integer selectFsStoreCartCount(FsStoreCartCountParam param);
 
+    R selectFsStoreCartTotalByCarts(String carts);
+
+    R addCartBySidebar(Long userId, FsStoreCartParam cartParam);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -180,6 +180,8 @@ public interface IFsStoreOrderScrmService
 
     R createSalesOrder(CompanyUser companyUser, String cateIds,Integer orderType,Integer orderMedium);
 
+    R createSalesOrderBySidebar(CompanyUser companyUser, String cateIds);
+
     R getSalesOrder(String createOrderKey);
 
     R addUserCart(long userId, String createOrderKey);
@@ -377,4 +379,12 @@ public interface IFsStoreOrderScrmService
     List<FsStoreOrderScrm> selectFsStoreOrderByUserIDexcludeCurrentOrderId(Long userId, Long notOrderId,Integer offset, Integer limit);
 
     int selectFsStoreOrderByUserIDexcludeCurrentOrderIdCount(Long userId, Long notOrderId);
+
+    List<FsMyStoreOrderListQueryVO> selectFsStoreOrderListBySidebarVO(FsStoreOrderScrmSidebarVO param);
+
+    R createOrderBySidebar(FsStoreOrderScrmSidebarVO param);
+
+    R getOrderInfoBySidebar(String orderId,Long userId);
+
+    R editOrderMoneyBySidebar(FsStoreOrderScrmSidebarVO param);
 }

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

@@ -78,4 +78,6 @@ public interface IFsStoreProductCategoryScrmService
     List<FsStoreProductCategorysVO> selectFsStoreProductCategorysVOList(Long storeId);
 
     List<FsStoreProductCategoryVO> getPidList(Long storeId);
+
+    List<FsStoreProductCategoryScrm> selectFsStoreProductCategoryListQueryBySidebar(FsStoreProductCategoryScrm param);
 }

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

@@ -90,6 +90,9 @@ public interface IFsStoreProductScrmService
 
     List<FsStoreProductListQueryVO> selectFsStoreProductListQuery(FsStoreProductQueryParam param);
 
+    List<FsStoreProductListQueryVO> selectFsStoreProductSidebarListQuery(FsStoreOrderScrmSidebarVO param);
+
+
     FsStoreProductQueryVO selectFsStoreProductByIdQuery(Long productId,String storeId);
 
     void decProductStock(Long productId, Long productAttrValueId, Integer cartNum);

+ 79 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java

@@ -29,12 +29,14 @@ import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
 import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
 import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.vo.FsStoreCartQueryVO;
 import com.fs.hisStore.vo.FsStoreCartVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
 import java.util.Date;
 import java.util.List;
 
@@ -78,6 +80,8 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
 
     @Autowired
     private IFsStoreProductScrmService productService;
+    @Autowired
+    private FsStoreCartScrmMapper cartMapper;
 
 
 
@@ -413,6 +417,81 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
         return  fsStoreCartMapper.selectFsStoreCartCount(param);
     }
 
+    @Override
+    public R selectFsStoreCartTotalByCarts(String cartIds) {
+        List<FsStoreCartQueryVO> carts = cartMapper.selectFsStoreCartListByIds(cartIds);
+
+        BigDecimal totalMoney = BigDecimal.ZERO;
+        for (FsStoreCartQueryVO vo : carts) {
+            totalMoney = totalMoney.add(vo.getPrice().multiply(new BigDecimal(vo.getCartNum().toString())));
+        }
+        return R.ok().put("totalMoney", totalMoney);
+    }
+
+    @Override
+    public R addCartBySidebar(Long uid, FsStoreCartParam cartParam) {
+        // 检查并调整限购数量
+        Integer adjustedNum = adjustPurchaseLimit(uid, cartParam.getProductId(), cartParam.getCartNum());
+        cartParam.setCartNum(adjustedNum);
+
+        //如果是直接购买,直接写入记录
+        if(cartParam.getIsBuy()==1){
+            FsStoreCartScrm storeCart = FsStoreCartScrm.builder()
+                    .cartNum(cartParam.getCartNum())
+                    .productAttrValueId(cartParam.getAttrValueId())
+                    .productId(cartParam.getProductId())
+                    .userId(uid)
+                    .cartNum(cartParam.getCartNum())
+                    .isPay(0)
+                    .isDel(0)
+                    .isBuy(cartParam.getIsBuy())
+                    .build();
+            storeCart.setCreateTime(new Date());
+            checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId());
+            fsStoreCartMapper.insertFsStoreCart(storeCart);
+            return R.ok().put("id",storeCart.getId());
+
+        }
+        else{
+            FsStoreCartScrm cartMap=new FsStoreCartScrm();
+            cartMap.setUserId(uid);
+            cartMap.setIsPay(0);
+            cartMap.setIsBuy(0);
+            cartMap.setProductId(cartParam.getProductId());
+            cartMap.setProductAttrValueId(cartParam.getAttrValueId());
+            List<FsStoreCartScrm> cart = fsStoreCartMapper.selectFsStoreCartList(cartMap);
+            FsStoreCartScrm storeCart;
+            if(cart==null||cart.size()==0){
+                storeCart = FsStoreCartScrm.builder()
+                        .cartNum(cartParam.getCartNum())
+                        .productAttrValueId(cartMap.getProductAttrValueId())
+                        .productId(cartMap.getProductId())
+                        .userId(uid)
+                        .cartNum(cartParam.getCartNum())
+                        .isPay(0)
+                        .isDel(0)
+                        .isBuy(0)
+                        .build();
+                storeCart.setCreateTime(new Date());
+                checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId());
+                fsStoreCartMapper.insertFsStoreCart(storeCart);
+                return R.ok().put("id",storeCart.getId());
+            }
+            else{
+                storeCart=cart.get(0);
+                int newCartNum = cartParam.getCartNum() + cart.get(0).getCartNum();
+                // 检查并调整限购数量(需要检查新的总数量)
+                Integer adjustedNewNum = adjustPurchaseLimit(uid, cartParam.getProductId(), newCartNum);
+                storeCart.setCartNum(adjustedNewNum);
+                storeCart.setUpdateTime(new Date());
+                checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId());
+                fsStoreCartMapper.updateFsStoreCart(storeCart);
+                return R.ok().put("id",storeCart.getId());
+            }
+
+        }
+    }
+
     private IErpGoodsService getErpService(){
         //判断是否开启erp
         IErpGoodsService goodsService = null;

+ 298 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -2827,6 +2827,22 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return R.ok().put("orderKey", uuid).put("carts", carts);
     }
 
+    @Override
+    public R createSalesOrderBySidebar(CompanyUser companyUser, String cartIds) {
+        List<FsStoreCartQueryVO> carts = cartMapper.selectFsStoreCartListByIds(cartIds);
+        String uuid = IdUtil.randomUUID();
+        redisCache.setCacheObject("createOrderKey:" + uuid, companyUser.getCompanyId() + "-" + companyUser.getUserId(), 24, TimeUnit.HOURS);
+        redisCache.setCacheObject("orderCarts:" + uuid, carts, 24, TimeUnit.HOURS);
+        //计算总价
+        BigDecimal totalMoney = BigDecimal.ZERO;
+        for (FsStoreCartQueryVO vo : carts) {
+            totalMoney = totalMoney.add(vo.getPrice().multiply(new BigDecimal(vo.getCartNum().toString())));
+        }
+        redisCache.setCacheObject("orderKey:" + uuid, cartIds, 300, TimeUnit.SECONDS);
+        redisCache.setCacheObject("createOrderMoney:" + uuid, totalMoney, 24, TimeUnit.HOURS);
+        return R.ok().put("orderKey", uuid).put("carts", carts);
+    }
+
     @Override
     public R getSalesOrder(String createOrderKey) {
         String key = redisCache.getCacheObject("createOrderKey:" + createOrderKey);
@@ -5665,6 +5681,288 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return fsStoreOrderMapper.selectFsStoreOrderByUserIDexcludeCurrentOrderIdCount(userId,notOrderId);
     }
 
+    @Override
+    public List<FsMyStoreOrderListQueryVO> selectFsStoreOrderListBySidebarVO(FsStoreOrderScrmSidebarVO param) {
+        List<FsMyStoreOrderListQueryVO> list = fsStoreOrderMapper.selectFsStoreOrderListBySidebarVO(param);
+        for (FsMyStoreOrderListQueryVO vo : list) {
+            if (StringUtils.isNotEmpty(vo.getItemJson())) {
+                JSONArray jsonArray = JSONUtil.parseArray(vo.getItemJson());
+                List<FsStoreOrderItemVO> items = JSONUtil.toList(jsonArray, FsStoreOrderItemVO.class);
+                if (!items.isEmpty()) {
+                    vo.setItems(items);
+                }
+            }
+        }
+        return list;
+    }
+
+    @Override
+    public R createOrderBySidebar(FsStoreOrderScrmSidebarVO param) {
+        Long userId = param.getUserId();
+        log.error("进入到数据");
+        if (ObjectUtil.isEmpty(param.getAddressId())){
+            return R.error("地址不能为空!");
+        }
+        FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
+        BeanUtils.copyProperties(param, computedParam);
+        //计算金额
+        FsStoreOrderComputeDTO dto;
+        try {
+            dto = this.computedOrder(userId, computedParam);
+        } catch (ServiceException e) {
+            // 捕获运费模板检查异常,直接返回错误
+            if ("偏远地区暂不可购买".equals(e.getMessage())) {
+                return R.error("偏远地区暂不可购买");
+            }
+            throw e;
+        }
+        String cartIds = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
+        Integer payType = redisCache.getCacheObject("createOrderPayType:" + param.getCreateOrderKey());
+        if (payType != null) {
+            param.setPayType(payType.toString());
+        }
+//        Integer totalNum = 0;
+        BigDecimal integral = BigDecimal.ZERO;
+        if (cartIds != null) {
+            //获取购物车列表
+            List<FsStoreCartQueryVO> carts = redisCache.getCacheObject("orderCarts:" + param.getOrderKey());
+            //获取地址
+            FsUserAddressScrm address = userAddressMapper.selectFsUserAddressById(param.getAddressId());
+            //生成分布式唯一值
+
+            String orderSn = SnowflakeUtil.nextIdStr();
+            //是否使用积分
+            Boolean isIntegral = false;
+            //组合数据
+            FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+            storeOrder.setStoreHouseCode("CK01");
+            storeOrder.setCompanyId(param.getCompanyId());
+            storeOrder.setCompanyUserId(param.getCompanyUserId());
+
+            String json = configService.selectConfigByKey("store.config");
+            StoreConfig config= JSONUtil.toBean(json, StoreConfig.class);
+
+            CompanyUserUser map=new CompanyUserUser();
+            map.setCompanyUserId(param.getCompanyUserId());
+            map.setUserId(userId);
+            List<CompanyUserUser> list= companyUserUserMapper.selectCompanyUserUserList(map);
+            if(list==null||list.size()==0){
+                CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+                if(companyUser!=null&&companyUser.getStatus().equals("0")){
+                    map.setCompanyId(companyUser.getCompanyId());
+                    companyUserUserMapper.insertCompanyUserUser(map);
+                }
+            }
+
+            storeOrder.setUserId(userId);
+            storeOrder.setOrderCode(orderSn);
+            if (ObjectUtil.isNotEmpty(address)){
+                storeOrder.setRealName(address.getRealName());
+                storeOrder.setUserPhone(address.getPhone());
+                storeOrder.setUserAddress(address.getProvince() + " " + address.getCity() +
+                        " " + address.getDistrict() + " " + address.getDetail().trim());
+            }
+            storeOrder.setCartId(cartIds);
+            storeOrder.setTotalNum(Long.parseLong(String.valueOf(carts.size())));
+            storeOrder.setTotalPrice(dto.getTotalPrice());
+            storeOrder.setTotalPostage(dto.getPayPostage());
+
+            storeOrder.setPayPostage(dto.getPayPostage());
+            storeOrder.setDeductionPrice(dto.getDeductionPrice());
+            storeOrder.setPaid(0);
+            storeOrder.setPayType(param.getPayType());
+            if (isIntegral) {
+                storeOrder.setPayIntegral(integral);
+            }
+            storeOrder.setUseIntegral(BigDecimal.valueOf(dto.getUsedIntegral()));
+            storeOrder.setBackIntegral(BigDecimal.ZERO);
+            storeOrder.setGainIntegral(BigDecimal.ZERO);
+            storeOrder.setMark(param.getMark());
+            //todo 获取成本价
+            BigDecimal costPrice = this.getOrderSumPrice(carts, "costPrice");
+            storeOrder.setCost(costPrice);
+            storeOrder.setIsChannel(1);
+            storeOrder.setShippingType(1);
+            storeOrder.setCreateTime(new Date());
+
+//            String json = configService.selectConfigByKey("store.config");
+//            StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+            if (config.getServiceFee() != null) {
+                storeOrder.setServiceFee(config.getServiceFee());
+            }
+
+            //后台制单处理
+            if (param.getPayPrice() != null && param.getPayPrice().compareTo(BigDecimal.ZERO) > 0) {
+                if (param.getPayPrice().compareTo(dto.getTotalPrice()) > 0) {
+                    return R.error("改价价格不能大于商品总价");
+                }
+                storeOrder.setPayPrice(param.getPayPrice());
+                storeOrder.setPayMoney(param.getPayPrice());
+            } else {
+                storeOrder.setPayPrice(dto.getPayPrice());
+                storeOrder.setPayMoney(dto.getPayPrice());
+            }
+
+            storeOrder.setStatus(1);
+            storeOrder.setPaid(1);
+            storeOrder.setPayTime(new Date());
+            Boolean isPay = false;
+
+            storeOrder.setOrderCreateType(param.getOrderCreateType());
+            Long prescribe = carts.stream().filter(item -> item.getProductType() != null && item.getProductType() == 2).count();
+            if (prescribe > 0) {
+                storeOrder.setIsPrescribe(1);
+            } else {
+                storeOrder.setIsPrescribe(0);
+            }
+
+            FsStoreOrderScrm tempOrder = redisCache.getCacheObject("orderInfo:" + param.getCreateOrderKey());
+            if (tempOrder != null) {
+                storeOrder.setOrderType(tempOrder.getOrderType());
+                storeOrder.setOrderMedium(tempOrder.getOrderMedium());
+                redisCache.deleteObject("orderInfo:" + param.getCreateOrderKey());
+            }
+            Integer flag = fsStoreOrderMapper.insertFsStoreOrder(storeOrder);
+            if (flag == 0) {
+                return R.error("订单创建失败");
+            }
+            if (!isPay && storeOrder.getCompanyId() != null) {
+                // 添加订单审核
+                addOrderAudit(storeOrder);
+            }
+            //使用了积分扣积分
+            if (dto.getUsedIntegral() > 0) {
+                this.decIntegral(userId, dto.getUsedIntegral(), dto.getDeductionPrice(), storeOrder.getId().toString());
+            }
+            //减库存加销量
+            this.deStockIncSale(carts);
+            //保存OrderItem
+            List<FsStoreOrderItemScrm> listOrderItem = new ArrayList<>();
+            //保存购物车商品信息
+            for (FsStoreCartQueryVO vo : carts) {
+                // 检查限购
+                checkAndRecordPurchaseLimit(userId, vo.getProductId(), vo.getCartNum());
+
+                FsStoreCartDTO fsStoreCartDTO = new FsStoreCartDTO();
+                fsStoreCartDTO.setProductId(vo.getProductId());
+                fsStoreCartDTO.setPrice(vo.getPrice());
+                fsStoreCartDTO.setSku(vo.getProductAttrName());
+                fsStoreCartDTO.setProductName(vo.getProductName());
+                fsStoreCartDTO.setNum(vo.getCartNum());
+                fsStoreCartDTO.setBarCode(vo.getBarCode());
+                fsStoreCartDTO.setGroupBarCode(vo.getGroupBarCode());
+                fsStoreCartDTO.setBrokerage(vo.getBrokerage());
+                fsStoreCartDTO.setBrokerageTwo(vo.getBrokerageTwo());
+                fsStoreCartDTO.setBrokerageThree(vo.getBrokerageThree());
+                if (StringUtils.isEmpty(vo.getProductAttrImage())) {
+                    fsStoreCartDTO.setImage(vo.getProductImage());
+                } else {
+                    fsStoreCartDTO.setImage(vo.getProductAttrImage());
+                }
+
+                FsStoreOrderItemScrm item = new FsStoreOrderItemScrm();
+                item.setOrderId(storeOrder.getId());
+                item.setOrderCode(orderSn);
+                item.setCartId(vo.getId());
+                item.setProductId(vo.getProductId());
+                item.setJsonInfo(JSONUtil.toJsonStr(fsStoreCartDTO));
+                item.setNum(vo.getCartNum());
+                item.setIsAfterSales(0);
+                //处方药
+                if (vo.getProductType().equals(2)) {
+                    item.setIsPrescribe(1);
+                }
+                fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
+                listOrderItem.add(item);
+
+            }
+            if (listOrderItem.size() > 0) {
+                String itemJson = JSONUtil.toJsonStr(listOrderItem);
+                storeOrder.setItemJson(itemJson);
+                fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
+            }
+            //购物车状态修改
+            cartMapper.updateIsPay(cartIds);
+
+            //删除缓存
+            redisCache.deleteObject("orderKey:" + param.getOrderKey());
+            if (config.getIsBrushOrders() == null || !(config.getIsBrushOrders() && param.getCompanyUserId() != null)) {//未开启刷单
+                redisCache.deleteObject("orderCarts:" + param.getOrderKey());
+            }
+
+            //添加记录
+            orderStatusService.create(storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(),
+                    OrderLogEnum.CREATE_ORDER.getDesc());
+
+            //加入redis,24小时自动取消
+            String redisKey = String.valueOf(StrUtil.format("{}{}",
+                    StoreConstants.REDIS_ORDER_OUTTIME_UNPAY, storeOrder.getId()));
+
+            if (config.getUnPayTime() != null && config.getUnPayTime() > 0) {
+                redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+            } else {
+                redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+            }
+            //添加支付到期时间
+            Calendar calendar = Calendar.getInstance();
+            calendar.setTime(storeOrder.getCreateTime());
+            if (config.getUnPayTime() != null) {
+                calendar.add(Calendar.MINUTE, config.getUnPayTime());
+            }
+            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            String payLimitTime = format.format(calendar.getTime());
+            redisCache.setCacheObject("orderAmount:" + storeOrder.getId(), storeOrder.getPayMoney(), 24, TimeUnit.HOURS);//物流代收自定义金额
+            //删除推荐订单KEY
+            String createOrderKey = param.getCreateOrderKey();
+
+            if (StringUtils.isNotEmpty(createOrderKey)) {
+                if (config.getIsBrushOrders() == null || !(config.getIsBrushOrders() && param.getCompanyUserId() != null)) {//未开启刷单
+                    redisCache.deleteObject("createOrderKey:" + createOrderKey);
+                    redisCache.deleteObject("orderCarts:" + createOrderKey);
+                    redisCache.deleteObject("createOrderMoney:" + createOrderKey);
+                }
+
+                //货到付款自定义金额 key改为id存储
+                BigDecimal amount = redisCache.getCacheObject("createOrderAmount:" + createOrderKey);
+                redisCache.deleteObject("createOrderAmount:" + createOrderKey);
+            }
+            return R.ok().put("order", storeOrder).put("payLimitTime", payLimitTime);
+        } else {
+            return R.error("订单已过期");
+        }
+    }
+
+    @Override
+    public R getOrderInfoBySidebar(String orderKey,Long userId) {
+        List<FsStoreCartQueryVO> carts = redisCache.getCacheObject("orderCarts:" + orderKey);
+        BigDecimal money = redisCache.getCacheObject("createOrderMoney:" + orderKey);
+        if(carts == null || money == null){
+            return R.error("订单信息已过期,请重新制单");
+        }
+        FsUserAddressScrm address = userAddressMapper.selectFsUserAddressByDefaultAddress(userId);
+        return R.ok().put("money", money).put("carts", carts).put("address",address);
+    }
+
+    @Override
+    public R editOrderMoneyBySidebar(FsStoreOrderScrmSidebarVO param) {
+        String orderKey = param.getOrderKey();
+        BigDecimal moneyOld = redisCache.getCacheObject("createOrderMoney:" + orderKey);
+        BigDecimal money = param.getMoney();
+        if(moneyOld != null){
+            if(money.compareTo(moneyOld)>0){
+                return R.error("金额不能超过订单金额");
+            }
+            if(money.compareTo(BigDecimal.ZERO)<0){
+                return R.error("金额必须大于等于0");
+            }
+            redisCache.setCacheObject("createOrderMoney:" + orderKey, money);
+            return R.ok("改价成功");
+        }
+        else{
+            return R.error("非法操作");
+        }
+    }
+
     private static final DateTimeFormatter CST_FORMATTER = DateTimeFormatter
             .ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US)
             .withZone(ZoneId.of("Asia/Shanghai"));

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductCategoryScrmServiceImpl.java

@@ -188,4 +188,9 @@ public class FsStoreProductCategoryScrmServiceImpl implements IFsStoreProductCat
         list.add(vo);
         return list;
     }
+
+    @Override
+    public List<FsStoreProductCategoryScrm> selectFsStoreProductCategoryListQueryBySidebar(FsStoreProductCategoryScrm param) {
+        return fsStoreProductCategoryMapper.selectFsStoreProductCategoryListQueryBySidebar(param);
+    }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -1092,6 +1092,11 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         return fsStoreProductMapper.selectFsStoreProductListQuery(param);
     }
 
+    @Override
+    public List<FsStoreProductListQueryVO> selectFsStoreProductSidebarListQuery(FsStoreOrderScrmSidebarVO param) {
+        return fsStoreProductMapper.selectFsStoreProductSidebarListQuery(param);
+    }
+
     @Override
     public FsStoreProductQueryVO selectFsStoreProductByIdQuery(Long productId,String storeId) {
         return fsStoreProductMapper.selectFsStoreProductByIdQuery(productId,storeId,medicalMallConfig);

+ 1 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreUserEndCategoryScrmServiceImpl.java

@@ -103,6 +103,7 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
             vo.setOtPrice(p.getOtPrice());
             vo.setSales(p.getSales());
             vo.setTagList(tagMap.getOrDefault(pid, new ArrayList<>()));
+            vo.setPositiveRating();
             result.add(vo);
         }
         out.put("list", result);

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderExportVO.java

@@ -271,6 +271,10 @@ public class FsStoreOrderExportVO implements Serializable
     @Excel(name = "归属档期")
     private String scheduleName;
 
+    /** 汇付商户订单号 */
+    @Excel(name = "汇付商户订单号")
+    private String hfshh;
+
     @Excel(name = "是否审核")
     private String isAudit;
 

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

@@ -58,7 +58,7 @@ public class FsStoreOrderItemExportZMVO implements Serializable {
     private BigDecimal FPrice;
 
     @Excel(name = "额外运费")
-    private BigDecimal payDelivery;
+    private BigDecimal payPostage;
 
     @Excel(name = "商品分类")
     private String cateName;
@@ -108,7 +108,7 @@ public class FsStoreOrderItemExportZMVO implements Serializable {
     /** 商品分类(与 MergedOrderVO 一致,不导出) */
 
     // 以下字段供内部使用,不参与导出
-    private BigDecimal payPostage;
+    private BigDecimal payDelivery;
     private Integer totalNum;
     private String jsonInfo;
     private String packageName;

+ 87 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderScrmSidebarVO.java

@@ -0,0 +1,87 @@
+package com.fs.hisStore.vo;
+
+
+import com.fs.common.param.BaseQueryParam;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class FsStoreOrderScrmSidebarVO extends BaseQueryParam {
+
+    @ApiModelProperty(value = "分类ID")
+    Long cateId;
+    @ApiModelProperty(value = "商品名")
+    String productName;
+    @ApiModelProperty(value = "默认")
+    private String defaultOrder;
+    @ApiModelProperty(value = "是否新品")
+    private String newOrder;
+    @ApiModelProperty(value = "价格排序")
+    private String priceOrder;
+    @ApiModelProperty(value = "销量排序")
+    private String salesOrder;
+    @ApiModelProperty(value = "1 只显示商城展示的商品 0展示所有")
+    private Integer isDisplay=1;
+
+    // 用户企业ID
+    private Long companyId;
+
+    private Long companyUserId;
+
+    private Long userId;
+
+    /**
+     * 客户信息的长字符串id
+     */
+    private String externalUserId;
+
+    private String corpId;
+
+    private String qwUserId;
+
+    private String cartId;
+
+    private String cartIds;
+
+    private Long orderId;
+
+    private Long productId;
+
+    //订单价格
+    private BigDecimal totalPrice;
+
+    //实际支付价格
+    private BigDecimal payPrice;
+
+    //需要修改的价格
+    private BigDecimal money;
+
+    @ApiModelProperty(value = "地址ID")
+    private Long addressId;
+
+    @ApiModelProperty(value = "orderKey")
+    @NotNull(message = "orderKey不能为空")
+    private String orderKey;
+
+    @Size(max = 200,message = "长度超过了限制")
+    @ApiModelProperty(value = "备注")
+    private String mark;
+
+    @NotBlank(message="请选择支付方式")
+    @ApiModelProperty(value = "支付方式")
+    private String payType;
+
+    private String createOrderKey;
+
+    private Integer orderCreateType;
+
+    private Integer status;
+
+    private String keyword;
+}

+ 4 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java

@@ -1,10 +1,13 @@
 package com.fs.hisStore.vo;
 
 import com.fs.common.annotation.Excel;
+import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
 import lombok.Data;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 @Data
@@ -63,7 +66,7 @@ public class FsStoreProductListQueryVO implements Serializable
     private Long companyId;
     private Long companyUserId;
 
-
+    private List<FsStoreProductAttrValueScrm> attrValueList = new ArrayList<>();
 
 
 

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java

@@ -4,7 +4,9 @@ import lombok.Data;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
 
 /**
  * 用户端分类下的商品项:商品ID、名称、售价、原价、销量、产品标签列表
@@ -13,6 +15,8 @@ import java.util.List;
 public class FsStoreUserEndCategoryProductVO implements Serializable {
 
     private static final long serialVersionUID = 1L;
+    private static final double min = 95.0;
+    private static final double max = 99.9;
 
     private Long productId;
     private String productName;
@@ -22,8 +26,14 @@ public class FsStoreUserEndCategoryProductVO implements Serializable {
     private BigDecimal price;
     /** 原价 */
     private BigDecimal otPrice;
+    /** 好评率 */
+    private BigDecimal positiveRating;
     /** 销量 */
     private Long sales;
     /** 产品标签名称列表 */
     private List<String> tagList;
+    public void setPositiveRating() {
+        double rating = ThreadLocalRandom.current().nextDouble(min, max);
+        this.positiveRating = new BigDecimal(rating).setScale(1, RoundingMode.HALF_UP);
+    }
 }

+ 7 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java

@@ -133,6 +133,13 @@ public interface LiveOrderMapper {
     @Select("select * from live_order where order_code=#{orderCode} limit 1")
     LiveOrder selectLiveOrderByOrderCode(@Param("orderCode") String orderCode);
 
+    /**
+     * 根据订单号批量查询直播订单
+     * @param orderCodes 订单号列表
+     * @return 订单列表
+     */
+    List<LiveOrder> selectLiveOrderByOrderCodes(@Param("orderCodes") List<String> orderCodes);
+
     /**
      * 查询状态为6(被拆分)的订单
      */

+ 5 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -577,9 +577,14 @@ public class LiveDataServiceImpl implements ILiveDataService {
     @Override
     public List<LiveAppSimpleVO> listLivingLivesForApp() {
         List<LiveAppSimpleVO> list = liveDataMapper.selectLivingLivesForApp();
+
         if (list == null || list.isEmpty()) {
             return list;
         }
+        // 如果查询结果超过 3 条,进行截取
+        if (list.size() > 3) {
+            list = list.subList(0, 3);
+        }
         for (LiveAppSimpleVO vo : list) {
             if (vo.getLiveType() != null && vo.getLiveType() == 2 && vo.getLiveId() != null) {
                 try {

+ 3 - 3
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -4062,7 +4062,8 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 注意:bizOrderType 字段需要在 FsStoreOrderScrm 实体类中添加
         // storeOrder.setBizOrderType(1); // 设置为直播订单
 
-        BigDecimal payPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        BigDecimal totalPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
+        BigDecimal payPrice = totalPrice;
         if (attrValue != null) {
             payPrice = attrValue.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
         }
@@ -4097,7 +4098,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 设置商城订单字段(按照 createOrder 的逻辑)
         storeOrder.setUserId(Long.parseLong(liveOrder.getUserId()));
         storeOrder.setTotalNum(Long.parseLong(liveOrder.getTotalNum()));
-        storeOrder.setTotalPrice(payPrice);
+        storeOrder.setTotalPrice(totalPrice);
         storeOrder.setTotalPostage(deliveryMoney);
         storeOrder.setPayPostage(deliveryMoney);
         storeOrder.setPayDelivery(deliveryMoney);
@@ -4124,7 +4125,6 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
 
         // 设置支付金额
         storeOrder.setPayPrice(payPrice.subtract(discountMoney));
-        storeOrder.setPayMoney(storeOrder.getPayPrice());
 
         // 设置订单状态
         storeOrder.setStatus(0); // 待支付

+ 2 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -649,6 +649,8 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
             "</script>")
     public int batchUpdateQwExternalContactMandatoryRegistration(@Param("map") List<QwMandatoryRegistrParam> batchList);
 
+    QwExternalContact selectQwExternalContactByExternalUserIdSidebar(@Param("externalUserId") String externalUserId, @Param("corpId") String corpId);
+
     @Select("SELECT " +
             "id " +
             "FROM " +

+ 5 - 0
fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java

@@ -265,4 +265,9 @@ public interface IQwExternalContactService extends IService<QwExternalContact> {
     List<QwMandatoryRegistrParam> selectQwExternalContactMandatoryRegistrationByIds(String corpId);
 
     int batchUpdateQwExternalContactMandatoryRegistration(List<QwMandatoryRegistrParam> batchList);
+
+    /**
+     * 企微用户-查询外部联系人信息
+     */
+    QwExternalContact selectQwExternalContactByExternalUserIdSidebar(String externalUserId, String corpId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -6010,6 +6010,11 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         return qwExternalContactMapper.batchUpdateQwExternalContactMandatoryRegistration(batchList);
     }
 
+    @Override
+    public QwExternalContact selectQwExternalContactByExternalUserIdSidebar(String externalUserId, String corpId) {
+        return qwExternalContactMapper.selectQwExternalContactByExternalUserIdSidebar(externalUserId,corpId);
+    }
+
     @Override
     public R getRepeat(RepeatParam param) {
         List<QwExternalContact> list = qwExternalContactMapper.selectList(new QueryWrapper<QwExternalContact>().eq("external_user_id", param.getExternalUserId()));

+ 8 - 0
fs-service/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java

@@ -13,6 +13,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.vo.CompanyQwUserByIdsVo;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.course.service.IFsCourseLinkService;
@@ -125,6 +126,9 @@ public class QwSopServiceImpl implements IQwSopService {
     @Autowired
     private ConfigUtil configUtil;
 
+    @Autowired
+    CloudHostProper cloudHostProper;
+
     @Autowired
     public void setIQwSopTempVoiceService(@Lazy IQwSopTempVoiceService qwSopTempVoiceService) {
         this.qwSopTempVoiceService = qwSopTempVoiceService;
@@ -537,6 +541,10 @@ public class QwSopServiceImpl implements IQwSopService {
     @Override
     public void checkSopRuleTime() {
 
+        if("恒春来".equals(cloudHostProper.getCompanyName())){
+            return;
+        }
+
         List<QwSopRuleTimeVO> qwSopList = qwSopMapper.checkSopRuleTime();
 
         CompanyWxUserSopParam wxUserSopParam=new CompanyWxUserSopParam();

+ 82 - 0
fs-service/src/main/java/com/fs/wxcid/FileToBase64Util.java

@@ -0,0 +1,82 @@
+package com.fs.wxcid;
+
+import javax.imageio.stream.FileImageInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Base64;
+
+/**
+ * 网络图片转Base64工具类
+ */
+public class FileToBase64Util {
+
+    /**
+     * 网络文件 URL 转 byte[]
+     *
+     * @param fileUrl 文件的网络URL
+     * @return 文件字节数组
+     */
+    public static byte[] downloadToBytes(String fileUrl) throws Exception {
+        if (fileUrl == null || fileUrl.trim().isEmpty()) {
+            throw new IllegalArgumentException("文件URL不能为空");
+        }
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        ByteArrayOutputStream outputStream = null;
+        try {
+            URL url = new URL(fileUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(5000);
+            connection.setReadTimeout(10000);
+            connection.setInstanceFollowRedirects(true);
+
+            int responseCode = connection.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                throw new Exception("文件URL访问失败,响应码:" + responseCode);
+            }
+
+            inputStream = connection.getInputStream();
+            outputStream = new ByteArrayOutputStream();
+            byte[] buffer = new byte[4096];
+            int len;
+            while ((len = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, len);
+            }
+            return outputStream.toByteArray();
+        } finally {
+            if (outputStream != null) outputStream.close();
+            if (inputStream != null) inputStream.close();
+            if (connection != null) connection.disconnect();
+        }
+    }
+
+    /**
+     * 网络文件 URL 转 Base64 编码字符串
+     *
+     * @param imageUrl 文件的网络URL
+     * @return Base64编码字符串(不带data:image/xxx;base64,前缀)
+     */
+    public static String convertImageUrlToBase64(String imageUrl) throws Exception {
+        byte[] bytes = downloadToBytes(imageUrl);
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    public static void main(String[] args) throws Exception {
+        FileInputStream inputStream = new FileInputStream("F:\\google下载\\a71990bf-4d22-4e9e-9d58-05bf99b6784b.mp3");
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        byte[] buffer = new byte[4096];
+        int len;
+        while ((len = inputStream.read(buffer)) != -1) {
+            outputStream.write(buffer, 0, len);
+        }
+        byte[] bytes = outputStream.toByteArray();
+        System.out.printf(Base64.getEncoder().encodeToString(bytes));
+//        String s = convertImageUrlToBase64("https://cdn.his.cdwjyyh.com/fs/20250627/ec72b8ea378340b2b804f38983f1e5e8.wav");
+//        System.out.println(s);
+    }
+}

+ 0 - 85
fs-service/src/main/java/com/fs/wxcid/ImageToBase64Util.java

@@ -1,85 +0,0 @@
-package com.fs.wxcid;
-
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Base64;
-
-/**
- * 网络图片转Base64工具类
- */
-public class ImageToBase64Util {
-
-    /**
-     * 网络图片转Base64编码字符串
-     * @param imageUrl 图片的网络URL(如https://xxx.com/xxx.png)
-     * @return Base64编码字符串(不带data:image/xxx;base64,前缀)
-     * @throws Exception 异常(网络错误、图片读取失败等)
-     */
-    public static String convertImageUrlToBase64(String imageUrl) throws Exception {
-        // 1. 校验URL参数
-        if (imageUrl == null || imageUrl.trim().isEmpty()) {
-            throw new IllegalArgumentException("图片URL不能为空");
-        }
-
-        HttpURLConnection connection = null;
-        InputStream inputStream = null;
-        ByteArrayOutputStream outputStream = null;
-
-        try {
-            // 2. 打开URL连接
-            URL url = new URL(imageUrl);
-            connection = (HttpURLConnection) url.openConnection();
-            // 设置连接参数(防超时、防重定向问题)
-            connection.setRequestMethod("GET");
-            connection.setConnectTimeout(5000); // 连接超时5秒
-            connection.setReadTimeout(5000);    // 读取超时5秒
-            connection.setInstanceFollowRedirects(true); // 允许重定向
-
-            // 3. 校验响应码(200表示成功)
-            int responseCode = connection.getResponseCode();
-            if (responseCode != HttpURLConnection.HTTP_OK) {
-                throw new Exception("图片URL访问失败,响应码:" + responseCode);
-            }
-
-            // 4. 读取图片流到字节数组
-            inputStream = connection.getInputStream();
-            outputStream = new ByteArrayOutputStream();
-            byte[] buffer = new byte[1024]; // 缓冲区,每次读1KB
-            int len;
-            while ((len = inputStream.read(buffer)) != -1) {
-                outputStream.write(buffer, 0, len);
-            }
-
-            // 5. 将字节数组编码为Base64
-            byte[] imageBytes = outputStream.toByteArray();
-            return Base64.getEncoder().encodeToString(imageBytes);
-
-        } finally {
-            // 6. 关闭所有资源(避免内存泄漏)
-            if (outputStream != null) {
-                outputStream.close();
-            }
-            if (inputStream != null) {
-                inputStream.close();
-            }
-            if (connection != null) {
-                connection.disconnect();
-            }
-        }
-    }
-
-    // 测试示例
-    public static void main(String[] args) {
-        try {
-            // 替换为你的网络图片URL
-            String imageUrl = "https://czwh.obs.cn-southwest-2.myhuaweicloud.com/fs/20260225/1771999986358.png";
-            String base64Str = convertImageUrlToBase64(imageUrl);
-            System.out.println(base64Str);
-        } catch (Exception e) {
-            e.printStackTrace();
-            System.out.println("转换失败:" + e.getMessage());
-        }
-    }
-}

+ 74 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/CdnUploadVideoResult.java

@@ -0,0 +1,74 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * /message/CdnUploadVideo 接口返回的 Data 结构
+ */
+@Data
+public class CdnUploadVideoResult {
+
+    @JSONField(name = "FileKey")
+    private String fileKey;
+
+    @JSONField(name = "Ver")
+    private Integer ver;
+
+    @JSONField(name = "ThumbDataSize")
+    private Integer thumbDataSize;
+
+    @JSONField(name = "ThumbURL")
+    private String thumbURL;
+
+    @JSONField(name = "FileAesKey")
+    private String fileAesKey;
+
+    @JSONField(name = "Mp4identify")
+    private String mp4identify;
+
+    @JSONField(name = "EnableQuic")
+    private Integer enableQuic;
+
+    @JSONField(name = "IsRetry")
+    private Integer isRetry;
+
+    @JSONField(name = "IsOverLoad")
+    private Integer isOverLoad;
+
+    @JSONField(name = "RecvLen")
+    private Integer recvLen;
+
+    @JSONField(name = "IsGetCDN")
+    private Integer isGetCDN;
+
+    @JSONField(name = "RetrySec")
+    private Integer retrySec;
+
+    @JSONField(name = "XClientIP")
+    private String xClientIP;
+
+    @JSONField(name = "FileURL")
+    private String fileURL;
+
+    @JSONField(name = "VideoDataMD5")
+    private String videoDataMD5;
+
+    @JSONField(name = "RetCode")
+    private Integer retCode;
+
+    @JSONField(name = "FileID")
+    private String fileID;
+
+    @JSONField(name = "ThumbHeight")
+    private Integer thumbHeight;
+
+    @JSONField(name = "ThumbWidth")
+    private Integer thumbWidth;
+
+    @JSONField(name = "Seq")
+    private Integer seq;
+
+    @JSONField(name = "VideoDataSize")
+    private Integer videoDataSize;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageParam.java

@@ -0,0 +1,27 @@
+package com.fs.wxcid.dto.message;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 发送视频消息业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SendVideoMessageParam extends BaseAccountRequest {
+    /**
+     * 视频封面文件 URL
+     */
+    private String thumbUrl;
+
+    /**
+     * 视频文件 URL
+     */
+    private String videoUrl;
+
+    /**
+     * 接收方 wxid
+     */
+    private String toUser;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageRequest.java

@@ -0,0 +1,20 @@
+package com.fs.wxcid.dto.message;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * /message/CdnUploadVideo 请求体
+ */
+@Data
+public class SendVideoMessageRequest {
+
+    @JsonProperty("ThumbData")
+    private byte[] thumbData;
+
+    @JsonProperty("ToUserName")
+    private String toUserName;
+
+    @JsonProperty("VideoData")
+    private byte[] videoData;
+}

+ 1 - 1
fs-service/src/main/java/com/fs/wxcid/service/ICidIpadServerService.java

@@ -60,7 +60,7 @@ public interface ICidIpadServerService extends IService<CidIpadServer>{
      */
     int deleteCidIpadServerById(Long id);
 
-    Long selectQwIpadServerByAddressId(String addressId);
+    Long selectQwIpadServerByAddressId(String addressId,Integer cidGroupNo);
 
     void subtractServer(Long serverId);
 }

+ 1 - 0
fs-service/src/main/java/com/fs/wxcid/service/MessageService.java

@@ -8,6 +8,7 @@ import java.util.List;
 public interface MessageService {
     List<SendMessageResult> sendTextMessage(SendTextMessageParam param);
     List<SendImageMessageResult> sendImageMessage(SendImageMessageParam param);
+    CdnUploadVideoResult sendVideoMessage(SendVideoMessageParam param);
     RevokeMsgResult revokeMessage(RevokeMsgRequest request);
 
 }

+ 2 - 2
fs-service/src/main/java/com/fs/wxcid/service/impl/CidIpadServerServiceImpl.java

@@ -96,8 +96,8 @@ public class CidIpadServerServiceImpl extends ServiceImpl<CidIpadServerMapper, C
     }
 
     @Override
-    public Long selectQwIpadServerByAddressId(String addressId) {
-        CidIpadServer ipadServer = getOne(new QueryWrapper<CidIpadServer>().eq("address_id", addressId).last("limit 1"));
+    public Long selectQwIpadServerByAddressId(String addressId,Integer cidGroupNo) {
+        CidIpadServer ipadServer = getOne(new QueryWrapper<CidIpadServer>().eq("address_id", addressId).eq("group_no", cidGroupNo).last("limit 1"));
         if(ipadServer == null){
             throw new CustomException("地区PAD不足");
         }

+ 28 - 2
fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java

@@ -2,18 +2,21 @@ package com.fs.wxcid.service.impl;
 
 import com.alibaba.fastjson.TypeReference;
 import com.fs.common.exception.CustomException;
-import com.fs.wxcid.ImageToBase64Util;
+import com.fs.wxcid.FileToBase64Util;
 import com.fs.wxcid.ServiceUtils;
 import com.fs.wxcid.dto.common.ApiResponseCommon;
 import com.fs.wxcid.dto.login.RequestBaseVo;
+import com.fs.wxcid.dto.message.CdnUploadVideoResult;
 import com.fs.wxcid.dto.message.MsgItem;
 import com.fs.wxcid.dto.message.RevokeMsgRequest;
 import com.fs.wxcid.dto.message.SendImageMessageParam;
 import com.fs.wxcid.dto.message.SendTextMessageParam;
+import com.fs.wxcid.dto.message.SendVideoMessageParam;
 import com.fs.wxcid.dto.message.RevokeMsgResult;
 import com.fs.wxcid.dto.message.SendImageMessageResult;
 import com.fs.wxcid.dto.message.SendMessageResult;
 import com.fs.wxcid.dto.message.SendTextMessageRequest;
+import com.fs.wxcid.dto.message.SendVideoMessageRequest;
 import com.fs.wxcid.service.MessageService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -59,7 +62,7 @@ public class MessageServiceImpl implements MessageService {
         MsgItem msgItem = new MsgItem();
         try {
             msgItem.setMsgType(0);
-            msgItem.setImageContent(ImageToBase64Util.convertImageUrlToBase64(param.getImgUrl()));
+            msgItem.setImageContent(FileToBase64Util.convertImageUrlToBase64(param.getImgUrl()));
             msgItem.setToUserName(param.getToUser());
         }catch (Exception e){
             log.error("发送消息时,图片转换base64错误", e);
@@ -93,4 +96,27 @@ public class MessageServiceImpl implements MessageService {
         }
         return response.getData();
     }
+
+    @Override
+    public CdnUploadVideoResult sendVideoMessage(SendVideoMessageParam param) {
+        SendVideoMessageRequest request = new SendVideoMessageRequest();
+        request.setToUserName(param.getToUser());
+        try {
+            request.setThumbData(FileToBase64Util.downloadToBytes(param.getThumbUrl()));
+            request.setVideoData(FileToBase64Util.downloadToBytes(param.getVideoUrl()));
+        } catch (Exception e) {
+            log.error("发送视频消息时,文件下载失败", e);
+            throw new CustomException("视频消息发送失败");
+        }
+        ApiResponseCommon<CdnUploadVideoResult> response = serviceUtils.sendPost(
+                BASE_URL + "CdnUploadVideo",
+                RequestBaseVo.builder().accountId(param.getAccountId()).data(request).build(),
+                new TypeReference<ApiResponseCommon<CdnUploadVideoResult>>() {}
+        );
+        CdnUploadVideoResult result = response.getData();
+        if (result != null && result.getRetCode() != null && result.getRetCode() != 0) {
+            throw new CustomException("视频上传失败,RetCode=" + result.getRetCode());
+        }
+        return result;
+    }
 }

+ 4 - 0
fs-service/src/main/resources/application-common.yml

@@ -151,3 +151,7 @@ hsy:
   role_secret_key: T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
   role_trn: trn:iam::2114522511:role/hylj
 
+# EasyCallCenter365 外呼系统配置
+easycall:
+  base-url: http://129.28.164.235:8899
+

+ 49 - 0
fs-service/src/main/resources/application-dev.yml

@@ -44,6 +44,7 @@ spring:
                 # 主库数据源
                 master:
                     url: jdbc:mysql://139.186.77.83:3306/ylrz_his_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+#                    url: jdbc:mysql://139.186.77.83:3306/ylrz_his_scrm_hetai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
                     username: Rtroot
                     password: Rtroot
                 # 主库数据源
@@ -138,6 +139,54 @@ spring:
                     wall:
                         config:
                             multi-statement-allow: true
+        easycall:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: easycallcenter365
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
 
 #rocketmq:
 #    name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址

+ 1 - 1
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogAddwxMapper.xml

@@ -66,7 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">#{createTime},</if>
             <if test="companyId != null">#{companyId},</if>
             <if test="wxAccountId != null">#{wxAccountId},</if>
-            <if test="isWeCom != null">#{is_we_com},</if>
+            <if test="isWeCom != null">#{isWeCom},</if>
          </trim>
     </insert>
 

+ 4 - 1
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCalleesMapper.xml

@@ -175,7 +175,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectExcludeList" resultType="com.fs.company.domain.CompanyVoiceRoboticCallees" >
         SELECT * FROM  company_voice_robotic_callees where 1=1
-        <if test="list != null">
+        <if test="isWeCom != null and isWeCom != ''">
+            and is_we_com = #{isWeCom}
+        </if>
+        <if test="list != null and list.size() > 0">
             and
             <foreach item="item" collection="list" separator=" or " open="(" close=")">
                 ( user_id = #{item.customerId} and robotic_id = #{item.roboticId} )

+ 4 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml

@@ -208,4 +208,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </update>
+
+    <select id="getDictDataList" resultType="com.fs.company.vo.DictVO">
+        SELECT dict_type,dict_label,dict_value FROM `sys_dict_data` where  dict_type = #{dictType}
+    </select>
 </mapper>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików