Преглед изворни кода

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin пре 3 недеља
родитељ
комит
f8c46d8c2f
100 измењених фајлова са 4058 додато и 361 уклоњено
  1. 7 9
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java
  2. 72 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  3. 2 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  4. 1 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java
  5. 159 7
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  6. 2 2
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  7. 149 3
      fs-ai-call-task/src/main/java/com/fs/app/service/CallTaskService.java
  8. 23 0
      fs-ai-call-task/src/main/java/com/fs/app/task/Task.java
  9. 106 3
      fs-cid-workflow/src/main/java/com/fs/app/service/CidWorkflowTaskService.java
  10. 114 0
      fs-cid-workflow/src/main/java/com/fs/app/service/OutboundRetryTaskService.java
  11. 25 0
      fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java
  12. 26 0
      fs-common/src/main/java/com/fs/common/constant/Constants.java
  13. 14 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  14. 7 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  15. 6 0
      fs-company-app/src/main/java/com/fs/app/controller/LiveController.java
  16. 523 0
      fs-company/src/main/java/com/fs/company/controller/aiAddwxSop/AiAddwxSopController.java
  17. 276 0
      fs-company/src/main/java/com/fs/company/controller/common/RecordingProxyController.java
  18. 9 16
      fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java
  19. 2 4
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java
  20. 5 6
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java
  21. 167 21
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java
  22. 32 19
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java
  23. 2 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCalleesController.java
  24. 94 48
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  25. 2 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticWxController.java
  26. 7 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java
  27. 84 5
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java
  28. 3 4
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxClientController.java
  29. 4 18
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxDialogController.java
  30. 7 7
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxUserController.java
  31. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxUserGroupController.java
  32. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  33. 170 35
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  34. 40 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java
  35. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  36. 4 2
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  37. 86 9
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  38. 129 11
      fs-ipad-task/src/main/java/com/fs/app/task/SendSmsMsg.java
  39. 78 0
      fs-live-ws/src/main/resources/logback-spring.xml
  40. 2 0
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  41. 24 1
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  42. 3 2
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java
  43. 3 3
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  44. 2 1
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java
  45. 6 12
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  46. 13 12
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  47. 4 3
      fs-service/src/main/java/com/fs/app/service/impl/AppPayServiceImpl.java
  48. 9 1
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  49. 77 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopExecLog.java
  50. 47 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTagTemplateBind.java
  51. 48 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTemplate.java
  52. 65 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTemplateNode.java
  53. 56 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTemplateNodeRule.java
  54. 93 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopUserNode.java
  55. 71 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopUserNodeRule.java
  56. 95 0
      fs-service/src/main/java/com/fs/company/domain/AiAddwxSopUserWorkflow.java
  57. 1 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java
  58. 1 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticBusiness.java
  59. 8 6
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java
  60. 70 11
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java
  61. 6 6
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java
  62. 1 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticWx.java
  63. 47 0
      fs-service/src/main/java/com/fs/company/domain/OutboundLineLimitLog.java
  64. 26 0
      fs-service/src/main/java/com/fs/company/dto/OutboundLimitResultVO.java
  65. 103 0
      fs-service/src/main/java/com/fs/company/enums/AiAddWechatSopNodeEnum.java
  66. 31 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopExecLogMapper.java
  67. 34 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTagTemplateBindMapper.java
  68. 27 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTemplateMapper.java
  69. 32 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTemplateNodeMapper.java
  70. 32 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTemplateNodeRuleMapper.java
  71. 32 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopUserNodeMapper.java
  72. 32 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopUserNodeRuleMapper.java
  73. 28 0
      fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopUserWorkflowMapper.java
  74. 36 8
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java
  75. 10 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceCloneRefMapper.java
  76. 17 5
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  77. 9 8
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.java
  78. 7 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java
  79. 18 0
      fs-service/src/main/java/com/fs/company/mapper/OutboundLineLimitLogMapper.java
  80. 25 0
      fs-service/src/main/java/com/fs/company/param/AppendCustomersParam.java
  81. 14 0
      fs-service/src/main/java/com/fs/company/param/InitAddWxSopTestParam.java
  82. 5 3
      fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java
  83. 30 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopExecLogService.java
  84. 30 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTagTemplateBindService.java
  85. 30 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTemplateNodeRuleService.java
  86. 30 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTemplateNodeService.java
  87. 34 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTemplateService.java
  88. 30 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopUserNodeRuleService.java
  89. 37 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopUserNodeService.java
  90. 39 0
      fs-service/src/main/java/com/fs/company/service/IAiAddwxSopUserWorkflowService.java
  91. 15 0
      fs-service/src/main/java/com/fs/company/service/ICompanyAiCallDataSyncService.java
  92. 9 8
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java
  93. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  94. 10 9
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceApiService.java
  95. 10 9
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceBlacklistService.java
  96. 2 1
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCallerService.java
  97. 0 2
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneRefService.java
  98. 27 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneService.java
  99. 10 9
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceConfigService.java
  100. 3 3
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceLogsService.java

+ 7 - 9
fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java

@@ -164,10 +164,7 @@ public class CompanyVoiceRoboticController extends BaseController
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
-        for (Long id : ids) {
-            companyVoiceRoboticService.updateDelFlag(id, 1);
-        }
-        return AjaxResult.success();
+        return toAjax(companyVoiceRoboticService.deleteCompanyVoiceRoboticByIds(ids));
     }
     /**
      * 删除机器人外呼任务
@@ -182,16 +179,16 @@ public class CompanyVoiceRoboticController extends BaseController
      * 查询状态
      */
     @GetMapping("/statusList")
-    public R statusList(String ids, Long companyId){
+    public R statusList(String ids,Long companyId){
         List<String> list = Arrays.stream(ids.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
-        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build(), companyId)).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
+        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build(),companyId)).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
     }
     /**
      * 启动任务
      */
 	@GetMapping("/startRobotic")
     public R startRobotic(String taskId, Long companyId){
-        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build(), companyId);
+        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build(),companyId);
         return R.ok();
     }
     /**
@@ -199,7 +196,7 @@ public class CompanyVoiceRoboticController extends BaseController
      */
 	@GetMapping("/stopRobotic")
     public R stopRobotic(String taskId, Long companyId){
-        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build(), companyId);
+        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build(),companyId);
         return R.ok();
     }
     /**
@@ -227,10 +224,11 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRoboticService.dispenseWx(id);
         return R.ok();
     }
+
     /**
      * 启动任务
      */
-	@GetMapping("/taskRun")
+    @GetMapping("/taskRun")
     public R taskRun(Long id){
         companyVoiceRoboticService.taskRun(id);
         return R.ok();

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

@@ -2,6 +2,8 @@ package com.fs.his.task;
 
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.date.DateTime;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
@@ -56,8 +58,10 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.FsSubOrderResultVO;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
+import com.fs.hisStore.enums.ShipperCodeEnum;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
+import com.fs.hisStore.service.IFsExpressScrmService;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStorePaymentScrmService;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
@@ -266,6 +270,73 @@ public class Task {
     @Autowired
     private ConversationSyncService syncService;
 
+    @Autowired
+    private IFsExpressScrmService expressService;
+
+    @Autowired
+    private IFsStoreOrderScrmService  fsStoreOrderScrmService;
+
+
+    /**
+     * 快递信息同步
+     */
+    public void expressInfoSync(){
+        FsStoreOrderScrm storeOrderScrm = new FsStoreOrderScrm();
+        storeOrderScrm.setIsAudit(1);
+        storeOrderScrm.setStatus(2);
+        storeOrderScrm.setDeliveryExceptionStatus(null);
+        List<FsStoreOrderScrm> orderList = fsStoreOrderScrmService.selectFsStoreOrderList(storeOrderScrm);
+        com.fs.hisStore.dto.ExpressInfoDTO expressInfoDTO = null;
+        String lastFourNumber = "";
+        for (FsStoreOrderScrm order : orderList) {
+            try {
+                if (com.fs.common.utils.StringUtils.isNotEmpty(order.getDeliveryId()) &&ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())) {
+                    if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliverySn().equals(ShipperCodeEnum.ZTO.getValue())) {
+                        if(order.getDeliveryExceptionStatus() !=null){
+                            continue;
+                        }
+                        if (lastFourNumber.contains("-")) {
+                            String beforeDash = lastFourNumber.split("-")[0];
+                            lastFourNumber = beforeDash.length() >= 4 ? beforeDash.substring(beforeDash.length() - 4) : beforeDash;
+                        }else{
+                            lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+                        }
+                        expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                        if (lastFourNumber.contains("-")) {
+                            if (expressInfoDTO != null && !expressInfoDTO.isSuccess()) {
+                                lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
+                                expressInfoDTO = expressService.getExpressInfo(order.getOrderCode(), order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
+                            }
+                        }
+                        if(expressInfoDTO!=null){
+                            FsStoreOrderScrm map = new FsStoreOrderScrm();
+                            map.setId(order.getId());
+                            if(expressInfoDTO.isSuccess()){
+                                map.setDeliveryType(expressInfoDTO.getStateEx());
+                                map.setDeliveryStatus(Integer.parseInt(expressInfoDTO.getState()));
+                            }else{
+                                map.setDeliveryExceptionRemark(expressInfoDTO.getReason());
+                                map.setDeliveryExceptionStatus(1);
+                            }
+                            fsStoreOrderScrmMapper.updateFsStoreOrder(map);
+                            //如果是正常签收,更新订单状态
+                            if (expressInfoDTO.getState().equals("3") && (expressInfoDTO.getStateEx().equals("301") || expressInfoDTO.getStateEx().equals("302") || expressInfoDTO.getStateEx().equals("304") || expressInfoDTO.getStateEx().equals("311"))) {
+                                fsStoreOrderScrmService.finishOrder(order.getId());
+                            }
+                            continue;
+                        }
+                    }else if ((lastFourNumber = order.getUserPhone()).length() == 11) {
+                        lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+                    }
+                    expressInfoDTO = expressService.getExpressInfo(order.getOrderCode(), order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
+                }
+            } catch (Exception e) {
+                System.out.println(e);
+                continue;
+            }
+        }
+    }
+
     /**
      * 定时任务,处理ai禁止回复之后的消息
      */
@@ -2153,6 +2224,7 @@ public class Task {
                         shareAmountDetail.setMerchantId(fsSubMerchant.getId());
                         shareAmountDetail.setOrderCode(order.getOrderSn());
                         shareAmountDetail.setOrderId(order.getOrderId());
+                        shareAmountDetail.setOrderType(1);//互医订单
                         //添加分账明细
                         shareAmountDetailService.insertFsShareAmountDetail(shareAmountDetail);
                     }

+ 2 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -103,6 +103,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         if ("北京卓美".equals(signProjectName)) {
           param.setIsHealth("1");
         }
+        fsStoreOrderService.applyHealthStoreQueryFlags(param);
         List<FsStoreOrderVO> list = fsStoreOrderService.selectFsStoreOrderListVO(param);
         //金牛需求 区别其他项目 status = 6 (金牛代服管家) ,其他项目请避免使用订单状态status = 6
         TableDataInfo dataTable = getDataTable(list);
@@ -519,6 +520,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
             String[] arr = param.getOrderCodeList().split("[,,\\s]+");
             param.setOrderCodes(new ArrayList<>(Arrays.asList(arr)));
         }
+        fsStoreOrderService.applyHealthStoreQueryFlags(param);
     }
 
     // 检查文件是否为有效的Excel文件

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

@@ -168,6 +168,7 @@ public class FsStorePaymentScrmController extends BaseController
                     String[] order=o.getOrg_req_seq_id().split("-");
                     switch (order[0]) {
                         case "store":
+                        case "live":
                             orderService.payConfirm(1,null,order[1], o.getOrg_hf_seq_id(),o.getOut_trans_id(),o.getParty_order_id());
                             break;
                         case "store_remain":

+ 159 - 7
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -1,13 +1,18 @@
 package com.fs.hisStore.task;
 
 
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.vo.RedPacketMoneyVO;
+import com.fs.core.utils.OrderCodeUtils;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
 import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpGoods;
@@ -19,26 +24,27 @@ import com.fs.erp.dto.ErpOrderQueryResponse;
 import com.fs.erp.service.IErpGoodsService;
 import com.fs.erp.service.IErpOrderService;
 import com.fs.his.config.FsSysConfig;
-import com.fs.his.domain.FsStoreProductAttrValue;
+import com.fs.his.domain.*;
 import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.his.mapper.FsStoreProductAttrValueMapper;
+import com.fs.his.mapper.FsSubMerchantMapper;
 import com.fs.his.service.IFsExpressService;
+import com.fs.his.service.IFsShareAmountDetailService;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.hisStore.domain.*;
 import com.fs.hisStore.dto.DateComparisonConfigDTO;
 import com.fs.hisStore.enums.ShipperCodeEnum;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
-import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
-import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
-import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
-import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
-import com.fs.hisStore.mapper.FsUserScrmMapper;
-import com.fs.hisStore.mapper.FsUserIntegralLogsScrmMapper;
+import com.fs.hisStore.mapper.*;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
+import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.domain.HuifuOrderConfirm;
+import com.fs.huifuPay.domain.HuifuOrderConfirmResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.sdk.opps.core.utils.DateTools;
 import com.fs.huifuPay.service.HuiFuService;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.domain.LiveOrderPayment;
@@ -184,6 +190,21 @@ public class MallStoreTask
     @Autowired
     private HuiFuService huiFuService;
 
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+
+    @Autowired
+    CompanyMapper companyMapper;
+
+    @Autowired
+    private FsSubMerchantMapper fsSubMerchantMapper;
+
+    @Autowired
+    private IFsShareAmountDetailService shareAmountDetailService;
+
+    @Autowired
+    private FsStoreOrderItemScrmMapper fsStoreOrderItemScrmMapper;
+
     // 订单银行回调数据丢失补偿
     public void recoveryBankOrder() {
         // 查询出来最近30分钟的订单 待支付 未退款
@@ -721,6 +742,7 @@ public class MallStoreTask
                 if(orderResult.getStatus().equals("100")){
                     switch (order[0]) {
                         case "store":
+                        case "live":
                             orderService.payConfirm(1,null,order[1], o.getUpOrderId(),orderResult.getBankTrxId(),orderResult.getBankOrderId());
                         case "store_remain":
                             orderService.payRemainConfirm( order[1], o.getUpOrderId(),orderResult.getBankTrxId(),orderResult.getBankOrderId());
@@ -989,4 +1011,134 @@ public class MallStoreTask
         log.info("购物积分发放完成,本次发放{}笔订单", count);
     }
 
+    public void shareAmount(){
+        List<FsStorePaymentScrm> payments = fsStorePaymentMapper.selectSharePaymentList();
+        if (CollectionUtil.isEmpty(payments)) return;
+        log.info("开始处理分享金额");
+        for (FsStorePaymentScrm storePayment : payments) {
+            try {
+                Thread.sleep(1000);
+                //查询订单确认金额
+                V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+                request.setOrgReqDate(DateTools.formatYYYYMMDD(storePayment.getPayTime()));
+                request.setOrgReqSeqId(storePayment.getShareCode()+"-"+storePayment.getPayCode());
+                request.setAppId(storePayment.getAppId());
+                HuiFuQueryOrderResult queryOrderResult = huiFuService.queryOrder(request);
+                log.info("汇付订单查询返回:{}" , queryOrderResult);
+                if (queryOrderResult != null && "00000000".equals(queryOrderResult.getResp_code())) {
+                    //未开启延迟支付的跳出当前循环 执行下次循环
+                    if (queryOrderResult.getDelay_acct_flag().equals("N")) {
+                        FsStorePaymentScrm mapPayment = new FsStorePaymentScrm();
+                        mapPayment.setPaymentId(storePayment.getPaymentId());
+                        mapPayment.setIsShare(0);
+                        fsStorePaymentService.updateFsStorePayment(mapPayment);
+                        continue;
+                    }
+                    log.info("直播分账订单号:{}",storePayment.getOrderId());
+
+                    //待确认金额
+                    BigDecimal unConfirmAmt = new BigDecimal(queryOrderResult.getUnconfirm_amt());
+                    //分账金额
+                    BigDecimal divAmount = null;
+                    //默认全部进入收款主账户
+                    BigDecimal mainAmount = unConfirmAmt;
+
+
+                    FsSubMerchant fsSubMerchant = null;
+                    FsStoreOrderScrm order = null;
+
+                    //是否分账到合作人
+                    boolean isDiv = false;
+                    HuifuOrderConfirm confirm = new HuifuOrderConfirm();
+                    confirm.setAppId(storePayment.getAppId());
+
+                    //支付金额大于等于0.1的进行分账 最低分账金额为0.1
+                    if (storePayment.getPayMoney().compareTo(new BigDecimal("0.1")) > -1) {
+                        //分账订单
+                        order = fsStoreOrderMapper.selectFsStoreOrderById(storePayment.getOrderId());
+                        if (order != null &&  order.getCompanyId() != null) {
+                            List<FsStoreOrderItemVO> itemVOS = fsStoreOrderItemScrmMapper.selectFsStoreOrderItemListByOrderId(order.getId());
+                            if (CollectionUtil.isNotEmpty(itemVOS) && itemVOS.size() == 1) {
+                                FsStoreProductScrm productScrm = fsStoreProductScrmMapper.selectFsStoreProductById(itemVOS.get(0).getProductId());
+                                //公司信息
+                                Company company = companyMapper.selectCompanyById(order.getCompanyId());
+                                //分账比列大于0且公司有分账商户
+                                if (productScrm != null && (productScrm.getShareRate() != null && productScrm.getShareRate() > 0)
+                                        && company != null && company.getSubMerchantId() != null) {
+                                    //分账商户
+                                    fsSubMerchant = fsSubMerchantMapper.selectFsSubMerchantById(company.getSubMerchantId());
+                                    if (fsSubMerchant != null && fsSubMerchant.getMerchantNo() != null) {
+                                        //金额全部进入分账商户 分账比列为100
+                                        if (productScrm.getShareRate() == 100) {
+                                            divAmount = unConfirmAmt;
+                                            mainAmount = BigDecimal.ZERO;
+                                        } else {
+                                            //金额进行分账
+                                            divAmount = unConfirmAmt.multiply(BigDecimal.valueOf(productScrm.getShareRate())).divide(new BigDecimal(100));
+                                            mainAmount = unConfirmAmt.subtract(divAmount);
+                                        }
+                                        isDiv = true;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    log.info("开始确认订单分账..........");
+                    String orderCode = OrderCodeUtils.getOrderSn();
+                    if (StringUtils.isEmpty(orderCode)) {
+                        throw new CustomException("订单生成失败,请重试");
+                    }
+                    confirm.setReqSeqId(orderCode);
+                    if (isDiv) {
+                        //合作人分账金额和商户号
+                        confirm.setDivAmt(divAmount);
+                        confirm.setDivHfId(fsSubMerchant.getMerchantNo());
+                        //分账明细数据
+                        FsShareAmountDetail shareAmountDetail = new FsShareAmountDetail();
+                        shareAmountDetail.setShareAmount(divAmount);
+                        shareAmountDetail.setMerchantId(fsSubMerchant.getId());
+                        shareAmountDetail.setOrderCode(order.getOrderCode());
+                        shareAmountDetail.setOrderId(order.getId());
+                        shareAmountDetail.setOrderType(2);
+                        //添加分账明细
+                        shareAmountDetailService.insertFsShareAmountDetail(shareAmountDetail);
+                    }
+                    confirm.setMainDivAmt(mainAmount);
+                    confirm.setOrgReqDate(queryOrderResult.getOrg_req_date());
+                    confirm.setOrgReqSeqId(queryOrderResult.getOrg_req_seq_id());
+                    confirm.setMainHfId(queryOrderResult.getHuifu_id());
+                    confirm.setTotalAmount(storePayment.getPayMoney());
+
+                    //执行分账
+                    HuifuOrderConfirmResult result = huiFuService.confirmOrder(confirm);
+                    if (result != null && result.getResp_code().equals("00000000") && (result.getTrans_stat().equals("S") || result.getTrans_stat().equals("P"))) {
+                        log.info("分账成功,订单号:{}",storePayment.getOrderId());
+                        FsStorePaymentScrm mapPayment = new FsStorePaymentScrm();
+                        //分账标识
+                        mapPayment.setPaymentId(storePayment.getPaymentId());
+                        mapPayment.setShareStatus(1);
+                        mapPayment.setShareDate(result.getReq_date());
+                        mapPayment.setShareCode(result.getReq_seq_id());
+                        log.info("更新支付记录");
+                        fsStorePaymentMapper.updateFsStorePayment(mapPayment);
+                    } else {
+                        throw new CustomException("直播订单分账失败");
+                    }
+                } else {
+                    if (queryOrderResult != null) {
+                        throw new CustomException(queryOrderResult.getResp_desc());
+                    }
+                }
+            } catch (Exception e) {
+                FsStorePaymentScrm mapPayment = new FsStorePaymentScrm();
+                //分账标识
+                mapPayment.setPaymentId(storePayment.getPaymentId());
+                mapPayment.setShareStatus(1);
+                mapPayment.setShareCode(e.getMessage());
+                fsStorePaymentMapper.updateFsStorePayment(mapPayment);
+                log.info("分账失败,订单号:{},原因:{}", storePayment.getBusinessCode(),e.getMessage());
+            }
+        }
+    }
+
 }

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

@@ -11,8 +11,8 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.vo.CompanyVO;
 import com.fs.framework.web.service.TokenService;
-import com.fs.hisStore.task.LiveTask;
-import com.fs.hisStore.task.MallStoreTask;
+//import com.fs.hisStore.task.LiveTask;
+//import com.fs.hisStore.task.MallStoreTask;
 import com.fs.live.domain.Live;
 import com.fs.live.service.ILiveService;
 import com.fs.live.vo.LiveListVo;

+ 149 - 3
fs-ai-call-task/src/main/java/com/fs/app/service/CallTaskService.java

@@ -1,9 +1,14 @@
 package com.fs.app.service;
 
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCacheT;
+import com.fs.company.domain.CompanyVoiceRobotic;
+import com.fs.company.mapper.CompanyVoiceRoboticMapper;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.service.*;
 import com.fs.company.service.impl.call.node.AiCallTaskNode;
 import com.fs.course.config.RedisKeyScanner;
@@ -11,9 +16,12 @@ import lombok.AllArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalTime;
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -30,6 +38,8 @@ public class CallTaskService {
 
     private final RedisCache redisCache2;
     private final CompanyWorkflowEngine companyWorkflowEngine;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
+    private final CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
     private final ExecutorService cidExcutor = new ThreadPoolExecutor(
             32,
             64,
@@ -39,6 +49,11 @@ public class CallTaskService {
             r -> new Thread(r, "callPool-" + System.currentTimeMillis()),
             new ThreadPoolExecutor.CallerRunsPolicy()
     );
+
+    @Autowired
+    @Qualifier("cidWorkFlowExecutor")
+    private Executor cidWorkFlowExecutor;
+
     private final RedisKeyScanner redisKeyScanner;
 
     /**
@@ -49,25 +64,156 @@ public class CallTaskService {
         String delayCallKeyPrefix = AiCallTaskNode.getDelayCallKeyPrefix(cidGroupNo,null) + "*";
         Set<String> keys = redisKeyScanner.scanMatchKey(delayCallKeyPrefix);
         log.info("共扫描到 {} 个待处理键", keys.size());
+
+        // 本地缓存已查询的任务暂停状态,避免同一批次重复查询
+        Map<Long, Boolean> pausedCache = new ConcurrentHashMap<>();
         keys.parallelStream().forEach(key -> {
             try {
                 //doExec
                 CompletableFuture.runAsync(() -> {
                     try {
                         ExecutionContext context = redisCache2.getCacheObject(key);
+                        if (context == null) {
+                            log.warn("工作流延时任务context为空,跳过 - key: {}", key);
+                            redisCache2.deleteObject(key);
+                            return;
+                        }
+                        // 任务暂停守卫检查(roboticId即CompanyVoiceRobotic.id,是实际暂停操作的目标)
+                        Long taskId = context.getVariable("roboticId", Long.class);
+                        if (taskId != null && pausedCache.computeIfAbsent(taskId, id -> companyVoiceRoboticService.isTaskPaused(id))) {
+                            // 延时key是时间分片前缀,下一分钟就不会再扫到,直接删除
+                            // 同步context信息到DB exec,供恢复时resumePausedInstances使用
+                            context.setVariable("callSource", "callTaskTimer");
+                            context.setVariable("_delayTargetNodeKey", context.getCurrentNodeKey());
+                            companyWorkflowEngine.updateExecVariables(context.getWorkflowInstanceId(), context.getVariables());
+                            log.info("任务已暂停,删除延时key并同步exec,等待恢复时从DB重建 - taskId: {}, key: {}", taskId, key);
+                            redisCache2.deleteObject(key);
+                            return;
+                        }
                         context.setVariable("callRedisKey", key);
                         context.setVariable("callSource", "callTaskTimer");
                         companyWorkflowEngine.timeDoExecute(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context.getVariables());
+                        redisCache2.deleteObject(key);
                     } catch (Exception e) {
                         log.error("处理工作流延时任务异常 - key: {}", key, e);
                     }
-                }, cidExcutor).thenRun(()->{
-                    redisCache2.deleteObject(key);
-                });
+                }, cidWorkFlowExecutor);
             } catch (Exception ex) {
                 log.error("处理工作流延时任务异常 - key: {}", key, ex);
             }
         });
         log.info("===========工作流延时任务扫描结束===========");
     }
+
+
+    /**
+     * 检查任务状态来暂停外呼任务的外呼运行
+     */
+    public void taskStatusChange(){
+        LocalTime now = LocalTime.now();
+        log.info("===========任务状态变更扫描开始, 当前时间: {}, 分组: {}===========", now, cidGroupNo);
+        pauseOutOfRangeTasks(now);
+        resumeInRangeTasks(now);
+        log.info("===========任务状态变更扫描结束===========");
+    }
+
+    private void pauseOutOfRangeTasks(LocalTime now) {
+        List<CompanyVoiceRobotic> activeTasks = companyVoiceRoboticMapper.selectList(
+                new QueryWrapper<CompanyVoiceRobotic>()
+                        .eq("task_status", 1)
+                        .eq("task_type", 1)
+                        .eq("cid_group_no", cidGroupNo)
+                        .eq("del_flag", 0)
+        );
+        log.info("扫描到执行中的普通任务 {} 个", activeTasks.size());
+        if (activeTasks.isEmpty()) {
+            return;
+        }
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (CompanyVoiceRobotic task : activeTasks) {
+            if (isOutOfRuntimeRange(task, now)) {
+                log.info("任务[{}]运行时间范围[{},{}),当前{}超出范围,准备自动暂停", task.getId(), task.getRuntimeRangeStart(), task.getRuntimeRangeEnd(), now);
+                futures.add(CompletableFuture.runAsync(() -> {
+                    try {
+                        PauseRoboticActiveParam param = new PauseRoboticActiveParam();
+                        param.setTaskId(task.getId());
+                        param.setActiveType(1);
+                        companyVoiceRoboticService.pauseRoboticActive(param);
+                        companyVoiceRoboticMapper.update(null,
+                                new LambdaUpdateWrapper<CompanyVoiceRobotic>()
+                                        .eq(CompanyVoiceRobotic::getId, task.getId())
+                                        .set(CompanyVoiceRobotic::getPauseSource, "auto")
+                        );
+                        log.info("任务[{}]超出运行时间范围,已自动暂停", task.getId());
+                    } catch (Exception e) {
+                        log.error("自动暂停任务[{}]异常", task.getId(), e);
+                    }
+                }, cidWorkFlowExecutor));
+            }
+        }
+        if (!futures.isEmpty()) {
+            log.info("本次需自动暂停 {} 个任务,等待并行执行完成", futures.size());
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+    }
+
+    private void resumeInRangeTasks(LocalTime now) {
+        List<CompanyVoiceRobotic> pausedTasks = companyVoiceRoboticMapper.selectList(
+                new QueryWrapper<CompanyVoiceRobotic>()
+                        .eq("task_status", 2)
+                        .eq("task_type", 1)
+                        .eq("pause_source", "auto")
+                        .eq("cid_group_no", cidGroupNo)
+                        .eq("del_flag", 0)
+        );
+        log.info("扫描到自动暂停中的普通任务 {} 个", pausedTasks.size());
+        if (pausedTasks.isEmpty()) {
+            return;
+        }
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (CompanyVoiceRobotic task : pausedTasks) {
+            if (isWithinRuntimeRange(task, now)) {
+                log.info("任务[{}]运行时间范围[{},{}),当前{}在范围内,准备自动恢复", task.getId(), task.getRuntimeRangeStart(), task.getRuntimeRangeEnd(), now);
+                futures.add(CompletableFuture.runAsync(() -> {
+                    try {
+                        PauseRoboticActiveParam param = new PauseRoboticActiveParam();
+                        param.setTaskId(task.getId());
+                        param.setActiveType(2);
+                        companyVoiceRoboticService.pauseRoboticActive(param);
+                        companyVoiceRoboticMapper.update(null,
+                                new LambdaUpdateWrapper<CompanyVoiceRobotic>()
+                                        .eq(CompanyVoiceRobotic::getId, task.getId())
+                                        .set(CompanyVoiceRobotic::getPauseSource, null)
+                        );
+                        log.info("任务[{}]回到运行时间范围,已自动恢复", task.getId());
+                    } catch (Exception e) {
+                        log.error("自动恢复任务[{}]异常", task.getId(), e);
+                    }
+                }, cidWorkFlowExecutor));
+            }
+        }
+        if (!futures.isEmpty()) {
+            log.info("本次需自动恢复 {} 个任务,等待并行执行完成", futures.size());
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+    }
+
+    private boolean isOutOfRuntimeRange(CompanyVoiceRobotic task, LocalTime now) {
+        LocalTime start = task.getRuntimeRangeStart();
+        LocalTime end = task.getRuntimeRangeEnd();
+        if (start == null || end == null) {
+            return false;
+        }
+        if (start.isBefore(end)) {
+            return now.isBefore(start) || now.isAfter(end);
+        } else {
+            return now.isAfter(end) && now.isBefore(start);
+        }
+    }
+
+    private boolean isWithinRuntimeRange(CompanyVoiceRobotic task, LocalTime now) {
+        return !isOutOfRuntimeRange(task, now);
+    }
+
+
 }

+ 23 - 0
fs-ai-call-task/src/main/java/com/fs/app/task/Task.java

@@ -38,8 +38,31 @@ public class Task {
 //    }
 
 
+//    @Scheduled(cron = "0/30 * * * * ?")
+//    public void cidWorkflowRun(){
+//        taskService.cidWorkflowCallRun();
+//    }
+
     @Scheduled(cron = "0/30 * * * * ?")
     public void cidWorkflowRun(){
+
+        if (taskService == null) {
+            log.error("[Task] taskService 未注入,请检查 Spring 配置");
+            return;
+        }
         taskService.cidWorkflowCallRun();
     }
+
+
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void taskStatusChange(){
+
+        if (taskService == null) {
+            log.error("[Task] taskService 未注入,请检查 Spring 配置");
+            return;
+        }
+
+            taskService.taskStatusChange();
+        }
+
 }

+ 106 - 3
fs-cid-workflow/src/main/java/com/fs/app/service/CidWorkflowTaskService.java

@@ -1,5 +1,7 @@
 package com.fs.app.service;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
@@ -29,6 +31,7 @@ public class CidWorkflowTaskService {
     Integer cidGroupNo;
     private final CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
     private final CompanyWorkflowEngine companyWorkflowEngine;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
 
     private final RedisKeyScanner redisKeyScanner;
 
@@ -41,15 +44,51 @@ public class CidWorkflowTaskService {
     @Autowired
     private WorkflowNodeFactory nodeFactory;
 
+    /** 认领态(RUNNING)超时分钟数,超过该时间未流转的任务视为卡死并回扫重置为失败 */
+    @Value("${cid-workflow.running-timeout-minutes:30}")
+    Integer runningTimeoutMinutes;
+
     /**
      * 扫描当前分组下就绪任务,并开启执行
      */
     public void runCidWorkflow() {
+//        List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.READY.getValue(), cidGroupNo);
+//        System.out.println(companyAiWorkflowExecs);
+//        if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
+//            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+//                try {
+//                    companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
+//                } catch (Exception e) {
+//                    log.error("处理就绪任务异常 - exec: {}", exec, e);
+//                }
+//            });
+//        }
+
         List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.READY.getValue(), cidGroupNo);
-        System.out.println(companyAiWorkflowExecs);
         if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
-            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+            // 本地缓存已查询的任务暂停状态,避免同一批次重复查询
+            Map<Long, Boolean> pausedCache = new HashMap<>();
+            companyAiWorkflowExecs.forEach(exec -> {
                 try {
+                    // 任务暂停守卫检查(从 variables JSON 中提取 roboticId,即 CompanyVoiceRobotic.id)
+                    Long taskId = extractRoboticIdFromExec(exec);
+                    if (taskId != null) {
+                        boolean paused = pausedCache.computeIfAbsent(taskId, id -> companyVoiceRoboticService.isTaskPaused(id));
+                        if (paused) {
+                            log.debug("任务已暂停,跳过执行 - taskId: {}, execId: {}", taskId, exec.getId());
+                            return;
+                        }
+                    }
+                    // 原子认领:READY -> RUNNING,仅认领成功(影响行数=1)才提交执行,
+                    // 防止上一轮异步任务尚未完成时下一次定时扫描重复调度同一条记录
+                    int claimed = companyAiWorkflowExecMapper.claimExecForRun(
+                            exec.getId(),
+                            ExecutionStatusEnum.READY.getValue(),
+                            ExecutionStatusEnum.RUNNING.getValue());
+                    if (claimed == 0) {
+                        log.debug("任务已被认领,跳过重复执行 - execId: {}", exec.getId());
+                        return;
+                    }
                     companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
                 } catch (Exception e) {
                     log.error("处理就绪任务异常 - exec: {}", exec, e);
@@ -62,10 +101,41 @@ public class CidWorkflowTaskService {
      * 扫描可执行任务,并激活执行
      */
     public void activateTimeAvailableTask() {
+//        List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.PENDING.getValue(), cidGroupNo);
+//        if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
+//            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+//                try {
+//                    companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
+//                } catch (Exception e) {
+//                    log.error("处理就绪任务异常 - exec: {}", exec, e);
+//                }
+//            });
+//        }
         List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.PENDING.getValue(), cidGroupNo);
         if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
-            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+            // 本地缓存已查询的任务暂停状态,避免同一批次重复查询
+            Map<Long, Boolean> pausedCache = new HashMap<>();
+            companyAiWorkflowExecs.forEach(exec -> {
                 try {
+                    // 任务暂停守卫检查(从 variables JSON 中提取 roboticId,即 CompanyVoiceRobotic.id)
+                    Long taskId = extractRoboticIdFromExec(exec);
+                    if (taskId != null) {
+                        boolean paused = pausedCache.computeIfAbsent(taskId, id -> companyVoiceRoboticService.isTaskPaused(id));
+                        if (paused) {
+                            log.debug("任务已暂停,跳过执行 - taskId: {}, execId: {}", taskId, exec.getId());
+                            return;
+                        }
+                    }
+                    // 原子认领:PENDING -> RUNNING,仅认领成功(影响行数=1)才提交执行,
+                    // 防止上一轮异步任务尚未完成时下一次定时扫描重复激活同一条记录
+                    int claimed = companyAiWorkflowExecMapper.claimExecForRun(
+                            exec.getId(),
+                            ExecutionStatusEnum.PENDING.getValue(),
+                            ExecutionStatusEnum.RUNNING.getValue());
+                    if (claimed == 0) {
+                        log.debug("任务已被认领,跳过重复执行 - execId: {}", exec.getId());
+                        return;
+                    }
                     companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
                 } catch (Exception e) {
                     log.error("处理就绪任务异常 - exec: {}", exec, e);
@@ -108,4 +178,37 @@ public class CidWorkflowTaskService {
         });
     }
 
+    /**
+     * 从 exec 记录的 variables JSON 中提取 roboticId(CompanyVoiceRobotic.id)
+     * 用于暂停守卫检查,fallback 到 exec.getBusinessKey()
+     */
+    private Long extractRoboticIdFromExec(CompanyAiWorkflowExec exec) {
+        if (StringUtils.isNotBlank(exec.getVariables())) {
+            try {
+                JSONObject vars = JSONObject.parseObject(exec.getVariables());
+                if (vars != null && vars.containsKey("roboticId")) {
+                    return vars.getLong("roboticId");
+                }
+            } catch (Exception e) {
+                log.warn("extractRoboticIdFromExec解析variables失败 - execId: {}", exec.getId(), e);
+            }
+        }
+        return exec.getBusinessKey();
+    }
+
+    /**
+     * 回扫超时认领任务:将处于 RUNNING 态且超过 runningTimeoutMinutes 分钟未更新的记录重置为 FAILURE。
+     * 用于兜底进程重启 / 线程池拒绝导致任务永久卡在认领态(RUNNING)无法被再次扫描的情况。
+     */
+    public void resetTimeoutRunningTask() {
+        java.time.LocalDateTime timeoutTime = java.time.LocalDateTime.now().minusMinutes(runningTimeoutMinutes);
+        int reset = companyAiWorkflowExecMapper.resetTimeoutRunningExec(
+                cidGroupNo,
+                ExecutionStatusEnum.RUNNING.getValue(),
+                ExecutionStatusEnum.FAILURE.getValue(),
+                timeoutTime);
+        if (reset > 0) {
+            log.warn("回扫超时认领任务,重置为失败状态 - groupNo: {}, count: {}, timeoutMinutes: {}", cidGroupNo, reset, runningTimeoutMinutes);
+        }
+    }
 }

+ 114 - 0
fs-cid-workflow/src/main/java/com/fs/app/service/OutboundRetryTaskService.java

@@ -0,0 +1,114 @@
+package com.fs.app.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.vo.easycall.EasyCallCommonAddCallListParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Set;
+
+/**
+ * 外呼限制重试任务服务(ZSET 方案)
+ * <p>
+ * 使用 Redis Sorted Set 存储待重试任务,score 为 nextAvailableTime 毫秒时间戳。
+ * 定时任务通过 ZRANGEBYSCORE 按批次查询已到期任务,支撑百万级数据量。
+ */
+@Service
+@Slf4j
+public class OutboundRetryTaskService {
+
+    private static final String ZSET_KEY = "outbound:limit:retry:zset";
+    private static final int BATCH_SIZE = 1000;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private IEasyCallService easyCallService;
+
+    /**
+     * 处理外呼重试任务(ZSET 方案)
+     * <p>
+     * 1. 用 ZRANGEBYSCORE 查询 score <= 当前时间 的任务(即已到期的)
+     * 2. 每次取一批(100 条),循环处理直到没有到期任务为止
+     * 3. 处理完后 ZREM 移除
+     */
+    public void processRetryTasks() {
+        int totalProcessed = 0;
+
+        try {
+            while (true) {
+                long now = System.currentTimeMillis();
+                // 使用 ZRANGEBYSCORE 查询已到期的任务(score <= now),每批最多 BATCH_SIZE 条
+                Set<String> readyTasks = redisCache.zRangeByScore(ZSET_KEY, 0, now, 0, BATCH_SIZE);
+                if (readyTasks == null || readyTasks.isEmpty()) {
+                    break;
+                }
+
+                for (String taskJson : readyTasks) {
+                    try {
+                        processOneRetryTask(taskJson);
+                    } catch (Exception e) {
+                        log.error("processRetryTasks: 处理重试任务异常", e);
+                        // 处理失败也要移除,避免死循环
+                        redisCache.zSetRemove(ZSET_KEY, taskJson);
+                    }
+                    totalProcessed++;
+                }
+            }
+        } catch (Exception e) {
+            log.error("processRetryTasks: 扫描重试任务异常", e);
+        }
+
+        if (totalProcessed > 0) {
+            log.info("processRetryTasks: 本次共处理 {} 个重试任务", totalProcessed);
+        }
+    }
+
+    /**
+     * 处理单个重试任务
+     */
+    private void processOneRetryTask(String taskJson) {
+        JSONObject retryData = JSON.parseObject(taskJson);
+        if (retryData == null) {
+            redisCache.zSetRemove(ZSET_KEY, taskJson);
+            return;
+        }
+
+        Long companyId = retryData.getLong("companyId");
+        Long gatewayId = retryData.getLong("gatewayId");
+        EasyCallCommonAddCallListParam param = retryData.getObject("param", EasyCallCommonAddCallListParam.class);
+
+        if (companyId == null || gatewayId == null || param == null) {
+            log.warn("processOneRetryTask: 参数不完整,移除无效任务");
+            redisCache.zSetRemove(ZSET_KEY, taskJson);
+            return;
+        }
+
+        // 从 param 中获取 batchId(正常流程中 addCommonCallList 前已通过 setBatchId 设置)
+        Long batchId = param.getBatchId();
+
+        log.info("processOneRetryTask: 开始重试外呼 - companyId: {}, gatewayId: {}", companyId, gatewayId);
+
+        // 重新调用 addCommonCallList(内部会再次检查限制,如果仍被限制会再次存入 ZSET)
+        boolean success = easyCallService.addCommonCallList(param, companyId, gatewayId);
+        if (success) {
+            log.info("processOneRetryTask: 重试成功 - companyId: {}, gatewayId: {}", companyId, gatewayId);
+            // 启动外呼任务
+            if (batchId != null) {
+                easyCallService.startTask(batchId, null);
+            } else {
+                log.warn("processOneRetryTask: batchId 为空,跳过启动任务 - companyId: {}, gatewayId: {}", companyId, gatewayId);
+            }
+        } else {
+            log.warn("processOneRetryTask: 重试未成功(可能仍被限制,已重新入队)- companyId: {}, gatewayId: {}", companyId, gatewayId);
+        }
+
+        // 无论成功失败都移除当前记录(如果仍被限制,addCommonCallList 内部会重新 saveRetryTask)
+        redisCache.zSetRemove(ZSET_KEY, taskJson);
+    }
+}

+ 25 - 0
fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java

@@ -1,6 +1,7 @@
 package com.fs.app.task;
 
 import com.fs.app.service.CidWorkflowTaskService;
+import com.fs.app.service.OutboundRetryTaskService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -23,6 +24,9 @@ public class CidTask {
     @Autowired
     private CidWorkflowTaskService cidWorkflowTaskService;
 
+    @Autowired
+    private OutboundRetryTaskService outboundRetryTaskService;
+
 
     /**
      * 扫描当前分组下就绪任务,并开启执行
@@ -48,7 +52,28 @@ public class CidTask {
 //        cidWorkflowTaskService.runContinueTask();
 //    }
 
+    /**
+     * 回扫超时认领任务 - 每5分钟执行一次
+     * 将卡在认领态(RUNNING)且长时间未流转的任务重置为失败,兜底进程重启/线程池拒绝导致的永久卡死
+     */
+    @Scheduled(cron = "0 0/5 * * * ?")
+    public void resetTimeoutRunningTask() {
+
+            cidWorkflowTaskService.resetTimeoutRunningTask();
+
+    }
+
+    /**
+     * 外呼重试任务 - 每30分钟执行一次
+     * 扫描 Redis 中被外呼限制拦截的待重试呼叫,到达 nextAvailableTime 后重新执行
+     */
+    @Scheduled(cron = "0 0/30 * * * ?")
+    public void retryOutboundCallTask() {
+        log.info("执行外呼限制重试机制");
+
+        outboundRetryTaskService.processRetryTasks();
 
+    }
 
 
 }

+ 26 - 0
fs-common/src/main/java/com/fs/common/constant/Constants.java

@@ -64,6 +64,11 @@ public class Constants
      */
     public static final String CAPTCHA_CODE_KEY = "captcha_codes:";
 
+    /**
+     * App登录图形验证码 redis key
+     */
+    public static final String APP_LOGIN_CAPTCHA_CODE_KEY = "app_login:captcha:";
+
     /**
      * 登录用户 redis key
      */
@@ -84,6 +89,11 @@ public class Constants
      */
     public static final Integer CAPTCHA_EXPIRATION = 2;
 
+    /**
+     * App登录图形验证码有效期(分钟)
+     */
+    public static final Integer APP_LOGIN_CAPTCHA_EXPIRATION = 3;
+
     /**
      * 令牌
      */
@@ -220,4 +230,20 @@ public class Constants
      * Value: 超时时间戳
      */
     public static final String WORKFLOW_QW_ADD_WX_TIMEOUT = "workflow:qwaddwx:timeout:";
+
+
+    /**
+     * CID 企微加个微 searchContact user_id 待匹配 Key 前缀(需拼接 tenantId 与 userId,避免跨租户冲突)
+     * 完整业务 Key:cid:qwAddWx:searchUserId:{tenantId}:{userId}
+     */
+    public static final String CID_QW_ADD_WX_SEARCH_USER_ID = "cid:qwAddWx:searchUserId:";
+    /**
+     * 构建 CID 企微加微 searchUserId 的 Redis 业务 Key(不含 Redis 序列化层的 tenantid 前缀)
+     */
+    public static String cidQwAddWxSearchUserIdKey(Long userId) {
+        if ( userId == null) {
+            return null;
+        }
+        return CID_QW_ADD_WX_SEARCH_USER_ID  + ":" + userId;
+    }
 }

+ 14 - 0
fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java

@@ -344,6 +344,20 @@ public class RedisCache
         return redisTemplate.opsForZSet().reverseRange(key, start, end);
     }
 
+    /**
+     * 按 score 范围查询有序集合元素(带分页)
+     *
+     * @param key    有序集合键
+     * @param min    最小 score(包含)
+     * @param max    最大 score(包含)
+     * @param offset 偏移量
+     * @param count  数量限制
+     * @return 元素集合
+     */
+    public Set<String> zRangeByScore(String key, double min, double max, long offset, long count) {
+        return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count);
+    }
+
     /**
      * 获取有序集合元素数量
      *

+ 7 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -400,6 +400,13 @@ public class FsUserCourseVideoController extends AppBaseController {
         return openImResponseDTO;
     }
 
+
+    @GetMapping("/realLinkByLink")
+    @ApiOperation("根据短链link查询真实链接")
+    public R getRealLinkByLink(@RequestParam String link) {
+        return courseLinkService.getRealLink(link);
+    }
+
     @Login
     @ApiOperation("app-看课记录(包含今日完课、今日催课)")
     @GetMapping("/courseWatchLog")

+ 6 - 0
fs-company-app/src/main/java/com/fs/app/controller/LiveController.java

@@ -80,4 +80,10 @@ public class LiveController extends AppBaseController {
         return ResponseResult.ok(liveService.getGotoWxAppLiveLink(linkStr, appid));
     }
 
+    @GetMapping("/realLinkByLink")
+    @ApiOperation("根据短链link查询真实链接")
+    public R getRealLinkByLink(@RequestParam String link) {
+        return liveSortLinkService.getRealLinkByLink(link);
+    }
+
 }

+ 523 - 0
fs-company/src/main/java/com/fs/company/controller/aiAddwxSop/AiAddwxSopController.java

@@ -0,0 +1,523 @@
+package com.fs.company.controller.aiAddwxSop;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.*;
+import com.fs.company.param.InitAddWxSopTestParam;
+import com.fs.company.service.*;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.qw.domain.QwExternalContact;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI添加微信SOP工作流Controller
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@RestController
+@RequestMapping("/aiAddwxSop")
+public class AiAddwxSopController extends BaseController {
+
+    @Autowired
+    private IAiAddwxSopTemplateService templateService;
+
+    @Autowired
+    private IAiAddwxSopTemplateNodeService templateNodeService;
+
+    @Autowired
+    private IAiAddwxSopUserWorkflowService userWorkflowService;
+
+    @Autowired
+    private IAiAddwxSopUserNodeService userNodeService;
+
+    @Autowired
+    private IAiAddwxSopExecLogService execLogService;
+
+    @Autowired
+    private IAiAddwxSopTemplateNodeRuleService templateNodeRuleService;
+
+    @Autowired
+    private IAiAddwxSopUserNodeRuleService userNodeRuleService;
+
+    @Autowired
+    private IAiAddwxSopTagTemplateBindService tagTemplateBindService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    // ==================== 模板管理 ====================
+
+    /**
+     * 查询模板列表
+     */
+    @GetMapping("/template/list")
+    public TableDataInfo templateList(AiAddwxSopTemplate template) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        template.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<AiAddwxSopTemplate> list = templateService.selectAiAddwxSopTemplateList(template);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取模板详情(含节点列表)
+     */
+    @GetMapping("/template/{templateId}")
+    public AjaxResult templateDetail(@PathVariable Long templateId) {
+        AiAddwxSopTemplate template = templateService.selectAiAddwxSopTemplateById(templateId);
+        if (template == null) {
+            return AjaxResult.error("模板不存在");
+        }
+        List<AiAddwxSopTemplateNode> nodes = templateNodeService.selectAiAddwxSopTemplateNodeByTemplateId(templateId);
+        AjaxResult result = AjaxResult.success(template);
+        result.put("nodes", nodes);
+        return result;
+    }
+
+    /**
+     * 新增模板
+     */
+    @Log(title = "AI添加微信SOP模板", businessType = BusinessType.INSERT)
+    @PostMapping("/template")
+    public AjaxResult addTemplate(@RequestBody AiAddwxSopTemplate template) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        template.setCompanyId(loginUser.getCompany().getCompanyId());
+        template.setDelFlag(0);
+        template.setVersion(1);
+        Long templateId = templateService.insertAiAddwxSopTemplate(template);
+        return AjaxResult.success(templateId);
+    }
+
+    /**
+     * 修改模板
+     */
+    @Log(title = "AI添加微信SOP模板", businessType = BusinessType.UPDATE)
+    @PutMapping("/template")
+    public AjaxResult editTemplate(@RequestBody AiAddwxSopTemplate template) {
+        templateService.updateAiAddwxSopTemplate(template);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除模板
+     */
+    @Log(title = "AI添加微信SOP模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/template/{templateIds}")
+    public AjaxResult removeTemplate(@PathVariable Long[] templateIds) {
+        templateService.deleteAiAddwxSopTemplateByIds(templateIds);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 保存模板及节点(含节点增删改)
+     */
+    @Log(title = "AI添加微信SOP模板", businessType = BusinessType.UPDATE)
+    @PostMapping("/template/saveWithNodes")
+    public AjaxResult saveTemplateWithNodes(@RequestBody AiAddwxSopTemplate template) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        template.setCompanyId(loginUser.getCompany().getCompanyId());
+        // nodes通过请求体中的额外字段传入,此处简化处理
+        Long templateId = templateService.saveTemplateWithNodes(template, null);
+        return AjaxResult.success(templateId);
+    }
+
+    // ==================== 模板节点管理 ====================
+
+    /**
+     * 查询模板节点列表
+     */
+    @GetMapping("/templateNode/list/{templateId}")
+    public AjaxResult templateNodeList(@PathVariable Long templateId) {
+        List<AiAddwxSopTemplateNode> list = templateNodeService.selectAiAddwxSopTemplateNodeByTemplateId(templateId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取模板节点详情
+     */
+    @GetMapping("/templateNode/{nodeId}")
+    public AjaxResult templateNodeDetail(@PathVariable Long nodeId) {
+        return AjaxResult.success(templateNodeService.selectAiAddwxSopTemplateNodeById(nodeId));
+    }
+
+    /**
+     * 新增模板节点
+     */
+    @Log(title = "AI添加微信SOP模板节点", businessType = BusinessType.INSERT)
+    @PostMapping("/templateNode")
+    public AjaxResult addTemplateNode(@RequestBody AiAddwxSopTemplateNode node) {
+        templateNodeService.insertAiAddwxSopTemplateNode(node);
+        return AjaxResult.success(node.getNodeId());
+    }
+
+    /**
+     * 修改模板节点
+     */
+    @Log(title = "AI添加微信SOP模板节点", businessType = BusinessType.UPDATE)
+    @PutMapping("/templateNode")
+    public AjaxResult editTemplateNode(@RequestBody AiAddwxSopTemplateNode node) {
+        templateNodeService.updateAiAddwxSopTemplateNode(node);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除模板节点
+     */
+    @Log(title = "AI添加微信SOP模板节点", businessType = BusinessType.DELETE)
+    @DeleteMapping("/templateNode/{nodeId}")
+    public AjaxResult removeTemplateNode(@PathVariable Long nodeId) {
+        templateNodeService.deleteAiAddwxSopTemplateNodeById(nodeId);
+        return AjaxResult.success();
+    }
+
+    // ==================== 模板节点规则管理 ====================
+
+    /**
+     * 查询节点规则列表
+     */
+    @GetMapping("/templateNodeRule/list/{nodeId}")
+    public AjaxResult templateNodeRuleList(@PathVariable Long nodeId) {
+        List<AiAddwxSopTemplateNodeRule> list = templateNodeRuleService.selectAiAddwxSopTemplateNodeRuleByNodeId(nodeId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取节点规则详情
+     */
+    @GetMapping("/templateNodeRule/{ruleId}")
+    public AjaxResult templateNodeRuleDetail(@PathVariable Long ruleId) {
+        return AjaxResult.success(templateNodeRuleService.selectAiAddwxSopTemplateNodeRuleById(ruleId));
+    }
+
+    /**
+     * 新增节点规则
+     */
+    @Log(title = "AI添加微信SOP模板节点规则", businessType = BusinessType.INSERT)
+    @PostMapping("/templateNodeRule")
+    public AjaxResult addTemplateNodeRule(@RequestBody AiAddwxSopTemplateNodeRule rule) {
+        templateNodeRuleService.insertAiAddwxSopTemplateNodeRule(rule);
+        return AjaxResult.success(rule.getRuleId());
+    }
+
+    /**
+     * 修改节点规则
+     */
+    @Log(title = "AI添加微信SOP模板节点规则", businessType = BusinessType.UPDATE)
+    @PutMapping("/templateNodeRule")
+    public AjaxResult editTemplateNodeRule(@RequestBody AiAddwxSopTemplateNodeRule rule) {
+        templateNodeRuleService.updateAiAddwxSopTemplateNodeRule(rule);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除节点规则
+     */
+    @Log(title = "AI添加微信SOP模板节点规则", businessType = BusinessType.DELETE)
+    @DeleteMapping("/templateNodeRule/{ruleId}")
+    public AjaxResult removeTemplateNodeRule(@PathVariable Long ruleId) {
+        templateNodeRuleService.deleteAiAddwxSopTemplateNodeRuleById(ruleId);
+        return AjaxResult.success();
+    }
+
+    // ==================== 个人工作流管理 ====================
+
+    /**
+     * 查询个人工作流列表
+     */
+    @GetMapping("/userWorkflow/list")
+    public TableDataInfo userWorkflowList(AiAddwxSopUserWorkflow userWorkflow) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        userWorkflow.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<AiAddwxSopUserWorkflow> list = userWorkflowService.selectAiAddwxSopUserWorkflowList(userWorkflow);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取个人工作流详情(含节点树)
+     */
+    @GetMapping("/userWorkflow/{userWorkflowId}")
+    public AjaxResult userWorkflowDetail(@PathVariable Long userWorkflowId) {
+        AiAddwxSopUserWorkflow workflow = userWorkflowService.selectAiAddwxSopUserWorkflowById(userWorkflowId);
+        if (workflow == null) {
+            return AjaxResult.error("工作流不存在");
+        }
+        List<AiAddwxSopUserNode> nodes = userWorkflowService.selectUserNodesByWorkflowId(userWorkflowId);
+        AjaxResult result = AjaxResult.success(workflow);
+        result.put("nodes", nodes);
+        return result;
+    }
+
+    /**
+     * 根据模板创建个人工作流
+     */
+    @Log(title = "AI添加微信SOP个人工作流", businessType = BusinessType.INSERT)
+    @PostMapping("/userWorkflow/createFromTemplate")
+    public AjaxResult createFromTemplate(@RequestParam Long templateId
+//            , @RequestParam Long businessKey
+    ) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyUserId = loginUser.getUser().getUserId();
+        Long companyId = loginUser.getCompany().getCompanyId();
+        Long userWorkflowId = userWorkflowService.createUserWorkflowFromTemplate(templateId, companyUserId, companyId
+//                , businessKey
+        );
+        return AjaxResult.success(userWorkflowId);
+    }
+
+    /**
+     * 推进工作流到下一节点
+     */
+    @Log(title = "AI添加微信SOP个人工作流", businessType = BusinessType.UPDATE)
+    @PostMapping("/userWorkflow/advance")
+    public AjaxResult advanceWorkflow(@RequestParam Long userWorkflowId, @RequestParam Long currentNodeId) {
+        userWorkflowService.advanceToNextNode(userWorkflowId, currentNodeId);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 修改个人工作流
+     */
+    @Log(title = "AI添加微信SOP个人工作流", businessType = BusinessType.UPDATE)
+    @PutMapping("/userWorkflow")
+    public AjaxResult editUserWorkflow(@RequestBody AiAddwxSopUserWorkflow userWorkflow) {
+        userWorkflowService.updateAiAddwxSopUserWorkflow(userWorkflow);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除个人工作流
+     */
+    @Log(title = "AI添加微信SOP个人工作流", businessType = BusinessType.DELETE)
+    @DeleteMapping("/userWorkflow/{userWorkflowIds}")
+    public AjaxResult removeUserWorkflow(@PathVariable Long[] userWorkflowIds) {
+        userWorkflowService.deleteAiAddwxSopUserWorkflowByIds(userWorkflowIds);
+        return AjaxResult.success();
+    }
+
+    // ==================== 个人节点管理 ====================
+
+    /**
+     * 查询个人节点列表
+     */
+    @GetMapping("/userNode/list/{userWorkflowId}")
+    public AjaxResult userNodeList(@PathVariable Long userWorkflowId) {
+        List<AiAddwxSopUserNode> list = userNodeService.selectAiAddwxSopUserNodeByWorkflowId(userWorkflowId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取个人节点详情
+     */
+    @GetMapping("/userNode/{userNodeId}")
+    public AjaxResult userNodeDetail(@PathVariable Long userNodeId) {
+        return AjaxResult.success(userNodeService.selectAiAddwxSopUserNodeById(userNodeId));
+    }
+
+    /**
+     * 修改个人节点(含提示词、条件等)
+     */
+    @Log(title = "AI添加微信SOP个人节点", businessType = BusinessType.UPDATE)
+    @PutMapping("/userNode")
+    public AjaxResult editUserNode(@RequestBody AiAddwxSopUserNode userNode) {
+        userNodeService.updateAiAddwxSopUserNode(userNode);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 更新节点状态
+     */
+    @Log(title = "AI添加微信SOP个人节点", businessType = BusinessType.UPDATE)
+    @PutMapping("/userNode/status")
+    public AjaxResult updateNodeStatus(@RequestParam Long userNodeId, @RequestParam Integer status) {
+        userNodeService.updateNodeStatus(userNodeId, status);
+        return AjaxResult.success();
+    }
+
+    // ==================== 个人节点规则管理 ====================
+
+    /**
+     * 查询个人节点规则列表
+     */
+    @GetMapping("/userNodeRule/list/{userNodeId}")
+    public AjaxResult userNodeRuleList(@PathVariable Long userNodeId) {
+        List<AiAddwxSopUserNodeRule> list = userNodeRuleService.selectAiAddwxSopUserNodeRuleByUserNodeId(userNodeId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取个人节点规则详情
+     */
+    @GetMapping("/userNodeRule/{userNodeRuleId}")
+    public AjaxResult userNodeRuleDetail(@PathVariable Long userNodeRuleId) {
+        return AjaxResult.success(userNodeRuleService.selectAiAddwxSopUserNodeRuleById(userNodeRuleId));
+    }
+
+    /**
+     * 新增个人节点规则
+     */
+    @Log(title = "AI添加微信SOP个人节点规则", businessType = BusinessType.INSERT)
+    @PostMapping("/userNodeRule")
+    public AjaxResult addUserNodeRule(@RequestBody AiAddwxSopUserNodeRule rule) {
+        userNodeRuleService.insertAiAddwxSopUserNodeRule(rule);
+        return AjaxResult.success(rule.getUserNodeRuleId());
+    }
+
+    /**
+     * 修改个人节点规则
+     */
+    @Log(title = "AI添加微信SOP个人节点规则", businessType = BusinessType.UPDATE)
+    @PutMapping("/userNodeRule")
+    public AjaxResult editUserNodeRule(@RequestBody AiAddwxSopUserNodeRule rule) {
+        userNodeRuleService.updateAiAddwxSopUserNodeRule(rule);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除个人节点规则
+     */
+    @Log(title = "AI添加微信SOP个人节点规则", businessType = BusinessType.DELETE)
+    @DeleteMapping("/userNodeRule/{userNodeRuleId}")
+    public AjaxResult removeUserNodeRule(@PathVariable Long userNodeRuleId) {
+        userNodeRuleService.deleteAiAddwxSopUserNodeRuleById(userNodeRuleId);
+        return AjaxResult.success();
+    }
+
+    // ==================== 执行日志管理 ====================
+
+    /**
+     * 查询执行日志列表
+     */
+    @GetMapping("/execLog/list")
+    public TableDataInfo execLogList(AiAddwxSopExecLog execLog) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        execLog.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<AiAddwxSopExecLog> list = execLogService.selectAiAddwxSopExecLogList(execLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据工作流ID查询执行日志
+     */
+    @GetMapping("/execLog/workflow/{userWorkflowId}")
+    public AjaxResult execLogByWorkflow(@PathVariable Long userWorkflowId) {
+        List<AiAddwxSopExecLog> list = execLogService.selectAiAddwxSopExecLogByWorkflowId(userWorkflowId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 根据节点ID查询执行日志
+     */
+    @GetMapping("/execLog/node/{userNodeId}")
+    public AjaxResult execLogByNode(@PathVariable Long userNodeId) {
+        List<AiAddwxSopExecLog> list = execLogService.selectAiAddwxSopExecLogByNodeId(userNodeId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取执行日志详情
+     */
+    @GetMapping("/execLog/{logId}")
+    public AjaxResult execLogDetail(@PathVariable Long logId) {
+        return AjaxResult.success(execLogService.selectAiAddwxSopExecLogById(logId));
+    }
+
+    /**
+     * 导出执行日志
+     */
+    @Log(title = "AI添加微信SOP执行日志", businessType = BusinessType.EXPORT)
+    @GetMapping("/execLog/export")
+    public AjaxResult exportExecLog(AiAddwxSopExecLog execLog) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        execLog.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AiAddwxSopExecLog> list = execLogService.selectAiAddwxSopExecLogList(execLog);
+        ExcelUtil<AiAddwxSopExecLog> util = new ExcelUtil<>(AiAddwxSopExecLog.class);
+        return util.exportExcel(list, "执行日志数据");
+    }
+
+    // ==================== 标签模板绑定管理 ====================
+
+    /**
+     * 查询标签模板绑定列表
+     */
+    @GetMapping("/tagTemplateBind/list")
+    public TableDataInfo tagTemplateBindList(AiAddwxSopTagTemplateBind bind) {
+        startPage();
+        List<AiAddwxSopTagTemplateBind> list = tagTemplateBindService.selectAiAddwxSopTagTemplateBindList(bind);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据标签ID查询绑定
+     */
+    @GetMapping("/tagTemplateBind/byTag/{qwTagId}")
+    public AjaxResult tagTemplateBindByTag(@PathVariable Long qwTagId) {
+        AiAddwxSopTagTemplateBind bind = tagTemplateBindService.selectAiAddwxSopTagTemplateBindByTagId(qwTagId);
+        return AjaxResult.success(bind);
+    }
+
+    /**
+     * 新增标签模板绑定(一个标签只能绑定一个模板,如已存在则更新)
+     */
+    @Log(title = "标签模板绑定", businessType = BusinessType.INSERT)
+    @PostMapping("/tagTemplateBind")
+    public AjaxResult addTagTemplateBind(@RequestBody AiAddwxSopTagTemplateBind bind) {
+        // 检查该标签是否已绑定
+        AiAddwxSopTagTemplateBind exist = tagTemplateBindService.selectTagBindBytagIdAndTemplateId(bind);
+//        AiAddwxSopTagTemplateBind exist = tagTemplateBindService.selectAiAddwxSopTagTemplateBindByTagId(bind.getQwTagId());
+        if (exist != null) {
+//            // 已存在则更新
+//            bind.setBindId(exist.getBindId());
+//            tagTemplateBindService.updateAiAddwxSopTagTemplateBind(bind);
+            return AjaxResult.error("已存在该标签与该模板绑定关系");
+        }
+        tagTemplateBindService.insertAiAddwxSopTagTemplateBind(bind);
+        return AjaxResult.success(bind.getBindId());
+    }
+
+    /**
+     * 修改标签模板绑定
+     */
+    @Log(title = "标签模板绑定", businessType = BusinessType.UPDATE)
+    @PutMapping("/tagTemplateBind")
+    public AjaxResult editTagTemplateBind(@RequestBody AiAddwxSopTagTemplateBind bind) {
+        tagTemplateBindService.updateAiAddwxSopTagTemplateBind(bind);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除标签模板绑定
+     */
+    @Log(title = "标签模板绑定", businessType = BusinessType.DELETE)
+    @DeleteMapping("/tagTemplateBind/{bindIds}")
+    public AjaxResult removeTagTemplateBind(@PathVariable Long[] bindIds) {
+        for (Long bindId : bindIds) {
+            tagTemplateBindService.deleteAiAddwxSopTagTemplateBindById(bindId);
+        }
+        return AjaxResult.success();
+    }
+
+    /**
+     * 测试
+
+     * @return
+     */
+    @PostMapping("/initAddWxSop")
+    public AjaxResult initAddWxSop(@RequestBody InitAddWxSopTestParam params){
+        templateService.initAddWxSop(params.getContacts(),params.getTagIds());
+
+        return AjaxResult.success();
+    }
+}

+ 276 - 0
fs-company/src/main/java/com/fs/company/controller/common/RecordingProxyController.java

@@ -0,0 +1,276 @@
+package com.fs.company.controller.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 录音文件代理接口
+ * 解决 HTTPS 页面下无法直接访问 HTTP 录音资源的问题
+ */
+@RestController
+public class RecordingProxyController {
+
+    private static final Logger log = LoggerFactory.getLogger(RecordingProxyController.class);
+
+    /**
+     * 允许代理的录音服务器地址白名单
+     */
+    private static final List<String> ALLOWED_HOSTS = Arrays.asList(
+            "129.28.164.235:8899"
+    );
+
+    /**
+     * 允许代理的路径前缀
+     */
+    private static final String ALLOWED_PATH_PREFIX = "/recordings/";
+
+    /**
+     * 流式缓冲区大小
+     */
+    private static final int BUFFER_SIZE = 4096;
+
+    /**
+     * 连接超时时间(毫秒)
+     */
+    private static final int CONNECT_TIMEOUT = 5000;
+
+    /**
+     * 读取超时时间(毫秒)
+     */
+    private static final int READ_TIMEOUT = 30000;
+
+    /**
+     * 录音文件代理接口
+     * 前端通过此接口请求录音文件,后端转发到录音服务器获取文件流
+     *
+     * @param url      录音文件的原始 HTTP 地址
+     * @param request  HTTP 请求
+     * @param response HTTP 响应
+     */
+    @GetMapping("/common/proxy/recording")
+    public void proxyRecording(@RequestParam("url") String url,
+                               HttpServletRequest request,
+                               HttpServletResponse response) {
+        // 1. 安全校验
+        if (!isUrlAllowed(url)) {
+            log.warn("录音代理请求被拒绝,非法地址: {}", url);
+            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+            try {
+                response.getWriter().write("Access denied: URL not allowed");
+            } catch (IOException e) {
+                log.error("写入错误响应失败", e);
+            }
+            return;
+        }
+
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // 2. 建立到录音服务器的连接
+            URL targetUrl = new URL(url);
+            connection = (HttpURLConnection) targetUrl.openConnection();
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(CONNECT_TIMEOUT);
+            connection.setReadTimeout(READ_TIMEOUT);
+            connection.setInstanceFollowRedirects(true);
+
+            // 3. 转发 Range 请求头(支持音频播放器拖动进度条)
+            String rangeHeader = request.getHeader("Range");
+            if (rangeHeader != null && !rangeHeader.isEmpty()) {
+                connection.setRequestProperty("Range", rangeHeader);
+            }
+
+            // 4. 发起请求
+            int responseCode = connection.getResponseCode();
+
+            // 5. 处理错误响应
+            if (responseCode >= 400) {
+                log.error("录音服务器返回错误状态码: {}, url: {}", responseCode, url);
+                response.setStatus(responseCode);
+                return;
+            }
+
+            // 6. 设置响应状态码(200 或 206)
+            response.setStatus(responseCode);
+
+            // 7. 设置 Content-Type
+            String contentType = connection.getContentType();
+            if (contentType != null && !contentType.isEmpty()) {
+                response.setContentType(contentType);
+            } else {
+                // 根据文件扩展名推断 Content-Type
+                response.setContentType(guessContentType(url));
+            }
+
+            // 7.1 设置 Content-Disposition,使浏览器下载时保存为正确的文件名
+            String fileName = extractFileName(url);
+            if (fileName != null) {
+                response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+            }
+
+            // 8. 转发关键响应头
+            String contentLength = connection.getHeaderField("Content-Length");
+            if (contentLength != null) {
+                response.setHeader("Content-Length", contentLength);
+            }
+
+            String contentRange = connection.getHeaderField("Content-Range");
+            if (contentRange != null) {
+                response.setHeader("Content-Range", contentRange);
+            }
+
+            String acceptRanges = connection.getHeaderField("Accept-Ranges");
+            if (acceptRanges != null) {
+                response.setHeader("Accept-Ranges", acceptRanges);
+            } else {
+                response.setHeader("Accept-Ranges", "bytes");
+            }
+
+            // 9. 流式传输文件内容
+            inputStream = connection.getInputStream();
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[BUFFER_SIZE];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+            outputStream.flush();
+
+        } catch (IOException e) {
+            log.error("录音文件代理请求失败, url: {}", url, e);
+            if (!response.isCommitted()) {
+                response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
+            }
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭输入流失败", e);
+                }
+            }
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭输出流失败", e);
+                }
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 校验 URL 是否在允许的白名单范围内
+     *
+     * @param url 待校验的 URL
+     * @return 是否允许代理
+     */
+    private boolean isUrlAllowed(String url) {
+        if (url == null || url.isEmpty()) {
+            return false;
+        }
+
+        // 必须是 http 协议
+        if (!url.startsWith("http://") && !url.startsWith("https://")) {
+            return false;
+        }
+
+        try {
+            URL parsedUrl = new URL(url);
+            String host = parsedUrl.getHost();
+            int port = parsedUrl.getPort();
+            String hostWithPort = port > 0 ? host + ":" + port : host;
+            String path = parsedUrl.getPath();
+
+            // 校验主机地址是否在白名单内
+            boolean hostAllowed = ALLOWED_HOSTS.contains(hostWithPort);
+
+            // 校验路径是否包含允许的前缀
+            boolean pathAllowed = path != null && path.startsWith(ALLOWED_PATH_PREFIX);
+
+            return hostAllowed && pathAllowed;
+        } catch (Exception e) {
+            log.warn("URL 解析失败: {}", url, e);
+            return false;
+        }
+    }
+
+    /**
+     * 根据文件扩展名推断 Content-Type
+     *
+     * @param url 文件 URL
+     * @return Content-Type
+     */
+    private String guessContentType(String url) {
+        if (url == null) {
+            return "application/octet-stream";
+        }
+        String lowerUrl = url.toLowerCase();
+        if (lowerUrl.contains(".wav")) {
+            return "audio/wav";
+        } else if (lowerUrl.contains(".mp3")) {
+            return "audio/mpeg";
+        } else if (lowerUrl.contains(".ogg")) {
+            return "audio/ogg";
+        } else if (lowerUrl.contains(".flac")) {
+            return "audio/flac";
+        } else if (lowerUrl.contains(".m4a") || lowerUrl.contains(".aac")) {
+            return "audio/aac";
+        }
+        return "application/octet-stream";
+    }
+
+    /**
+     * 从录音文件 URL 中提取文件名
+     * 支持直接路径 /recordings/xxx.wav 和 query 参数 ?filename=xxx.wav
+     *
+     * @param url 录音文件 URL
+     * @return 文件名,无法提取时返回 null
+     */
+    private String extractFileName(String url) {
+        if (url == null || url.isEmpty()) {
+            return null;
+        }
+        try {
+            // 先尝试从 query 参数 filename 获取
+            URL parsedUrl = new URL(url);
+            String query = parsedUrl.getQuery();
+            if (query != null && query.contains("filename=")) {
+                for (String param : query.split("&")) {
+                    if (param.startsWith("filename=")) {
+                        return param.substring("filename=".length());
+                    }
+                }
+            }
+            // 从路径中提取文件名
+            String path = parsedUrl.getPath();
+            if (path != null && path.contains("/")) {
+                String name = path.substring(path.lastIndexOf('/') + 1);
+                if (!name.isEmpty()) {
+                    return name;
+                }
+            }
+        } catch (Exception e) {
+            log.debug("提取文件名失败: {}", url);
+        }
+        return null;
+    }
+}

+ 9 - 16
fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java

@@ -1,27 +1,20 @@
 package com.fs.company.controller.company;
 
-import java.util.List;
-
-import com.fs.common.core.domain.R;
-import com.fs.company.param.BindCidServerParam;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyAiWorkflowServer;
+import com.fs.company.param.BindCidServerParam;
 import com.fs.company.service.ICompanyAiWorkflowServerService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
 
 /**
  * cid服务Controller

+ 2 - 4
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java

@@ -7,9 +7,7 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.company.domain.CompanyVoiceBlacklist;
 import com.fs.company.domain.CompanyVoiceRoboticCallBlacklist;
-import com.fs.company.service.ICompanyVoiceBlacklistService;
 import com.fs.company.service.ICompanyVoiceRoboticCallBlacklistService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -89,7 +87,7 @@ public class CompanyVoiceRoboticCallBlacklistController extends BaseController
      * */
     @PutMapping
     @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:update')")
-    @Log(title = "修改黑名单信息", businessType = BusinessType.INSERT)
+    @Log(title = "修改黑名单信息", businessType = BusinessType.UPDATE)
     public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
     {
         return toAjax(companyVoiceRoboticCallBlacklistService.updateCompanyVoiceRoboticCallBlacklist(companyVoiceRoboticCallBlacklist));
@@ -106,7 +104,7 @@ public class CompanyVoiceRoboticCallBlacklistController extends BaseController
         return toAjax(companyVoiceRoboticCallBlacklistService.deleteCompanyVoiceRoboticCallBlacklistByIds(blacklistIds));
     }
 
-    @Log(title = "修改黑名单状态", businessType = BusinessType.UPDATE)
+    @Log(title = "修改黑名单失效或启用状态", businessType = BusinessType.UPDATE)
     @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:update')")
     @PutMapping("/changeStatus")
     public AjaxResult changeStatus(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist) {

+ 5 - 6
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java

@@ -10,7 +10,6 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
-import com.fs.company.service.impl.CompanyUserServiceImpl;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddWxExportVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
@@ -36,13 +35,8 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
 {
     @Autowired
     private ICompanyVoiceRoboticCallLogAddwxService companyVoiceRoboticCallLogAddwxService;
-
     @Autowired
     private TokenService tokenService;
-
-    @Autowired
-    private CompanyUserServiceImpl companyUserService;
-
     /**
      * 查询调用日志_加微信列表
      */
@@ -86,6 +80,10 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
         startPage();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         companyVoiceRoboticCallLogAddwx.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyVoiceRoboticCallLogAddwx.setCompanyUserId(loginUser.getUser().getUserId());
+        }
         List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listAll(companyVoiceRoboticCallLogAddwx);
         TableDataInfo dataTable = getDataTable(list);
         Map<String, Long> countMap = companyVoiceRoboticCallLogAddwxService.countListAll(companyVoiceRoboticCallLogAddwx);
@@ -161,6 +159,7 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
         return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 
+
 //    /**
 //     * 导出调用日志_加微信列表
 //     */

+ 167 - 21
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java

@@ -1,34 +1,32 @@
 package com.fs.company.controller.company;
 
-import java.util.ArrayList;
-import java.util.List;
-
-import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.service.ICompanyAiCallDataSyncService;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * 调用日志_ai打电话Controller
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
@@ -39,6 +37,30 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
+    @Autowired
+    private ICrmCustomerService crmCustomerService;
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ICompanyAiCallDataSyncService companyAiCallDataSyncService;
+
+    /**
+     * 异步同步 AI 外呼数据(补偿 EasyCall 已完成但未回调的记录)
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @Log(title = "同步AI外呼数据", businessType = BusinessType.OTHER)
+    @PostMapping("/syncAiCallData")
+    public AjaxResult syncAiCallData() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+
+        if (!companyAiCallDataSyncService.tryStartSync(companyId)) {
+            return AjaxResult.error("正在同步中,请勿重复操作");
+        }
+        return AjaxResult.success("正在同步中");
+    }
+
     /**
      * 查询调用日志_ai打电话列表
      */
@@ -54,6 +76,7 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
                 companyVoiceRoboticCallLogCallphone.setCallerIds(calleeIds);
                 startPage();
                 List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallLogCallphoneListData(companyVoiceRoboticCallLogCallphone);
+
                 return getDataTable(list);
             } else {
                 return getDataTable(new ArrayList<>());
@@ -74,6 +97,30 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
 
     }
 
+    /**
+     * AI外呼记录明细列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @GetMapping("/detailList")
+    public TableDataInfo detailList(CompanyVoiceRoboticCallLogCallphone param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphoneService.prepareDetailListQuery(param, loginUser.getCompany().getCompanyId());
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.selectDetailList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * AI外呼记录明细统计
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @GetMapping("/detailSummary")
+    public AjaxResult detailSummary(CompanyVoiceRoboticCallLogCallphone param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphoneService.prepareDetailListQuery(param, loginUser.getCompany().getCompanyId());
+        return AjaxResult.success(companyVoiceRoboticCallLogCallphoneService.selectDetailSummary(param));
+    }
+
 
     /**
      * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
@@ -83,6 +130,14 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
     public TableDataInfo groupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
     {
         startPage();
+        if(null == companyVoiceRoboticCallLogCallphone.getCompanyId()){
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            companyVoiceRoboticCallLogCallphone.setCompanyId(loginUser.getUser().getCompanyId());
+            if (!"00".equals(loginUser.getUser().getUserType())){
+                companyVoiceRoboticCallLogCallphone.setCompanyUserId(loginUser.getUser().getUserId());
+            }
+        }
+
         List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
         return getDataTable(list);
     }
@@ -94,10 +149,72 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
     @GetMapping("/count")
     public AjaxResult selectCompanyVoiceRoboticCallPhoneLogCount()
     {
-        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        Long companyUserId=null;
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyUserId=loginUser.getUser().getUserId();
+        }
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount(companyId,companyUserId);
         return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 
+    /**
+     * 查询转人工接听记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:query')")
+    @GetMapping("/manualAnsweredList")
+    public TableDataInfo manualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphone.setCompanyId(loginUser.getCompany().getCompanyId());
+        companyVoiceRoboticCallLogCallphone.setManualAnswered(1);
+        if(!loginUser.getUser().getUserType().equals("00")){
+            companyVoiceRoboticCallLogCallphone.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectManualAnsweredList(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据外呼记录ID查询CRM客户信息(处理数据弹窗用)
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:handle')")
+    @GetMapping("/getCrmCustomerByLogId")
+    public AjaxResult getCrmCustomerByLogId(@RequestParam("callphoneLogId") Long callphoneLogId)
+    {
+        CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerByCallphoneLogId(callphoneLogId);
+        return AjaxResult.success(crmCustomer);
+    }
+
+    /**
+     * 确认信息(仅标记外呼记录为已处理,不更新客户信息)
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:handle')")
+    @Log(title = "转人工接听确认信息", businessType = BusinessType.UPDATE)
+    @PostMapping("/confirmManualAnswered")
+    public AjaxResult confirmManualAnswered(@RequestBody CompanyVoiceRoboticCallLogCallphone param)
+    {
+        return toAjax(companyVoiceRoboticCallLogCallphoneService.markHandleFlag(param.getLogId()));
+    }
+
+    /**
+     * 处理数据(标记外呼记录为已处理 + 更新CRM客户信息)
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:handle')")
+    @Log(title = "转人工接听处理数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/submitManualAnswered")
+    public AjaxResult submitManualAnswered(@RequestBody CrmCustomer param)
+    {
+        // 1. 标记外呼记录为已处理
+        if (param.getLastEffectiveCallphoneLogId() != null) {
+            companyVoiceRoboticCallLogCallphoneService.markHandleFlag(param.getLastEffectiveCallphoneLogId());
+        }
+        // 2. 更新CRM客户信息
+        return toAjax(crmCustomerService.updateCrmCustomer(param));
+    }
+
     /**
      * 导出调用日志_ai打电话列表
      */
@@ -111,6 +228,35 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
         return util.exportExcel(list, "调用日志_ai打电话数据");
     }
 
+    /**
+     * 导出详情外呼记录(任务名称、客户名称、解密手机号)
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphonelog:exportPhone')")
+    @Log(title = "外呼记录详情手机号导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportDetailPhone")
+    public AjaxResult exportDetailPhone(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphoneService.prepareDetailListQuery(companyVoiceRoboticCallLogCallphone, loginUser.getCompany().getCompanyId());
+        List<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> list =
+                companyVoiceRoboticCallLogCallphoneService.listDecryptPhoneExport(companyVoiceRoboticCallLogCallphone);
+        ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> util =
+                new ExcelUtil<>(CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO.class);
+        return util.exportExcel(list, "外呼记录详情手机号");
+    }
+
+    /**
+     * 查看外呼记录解密手机号(无CRM客户时按记录解密)
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:queryPhone')")
+    @Log(title = "查看外呼记录手机号", businessType = BusinessType.GRANT)
+    @GetMapping("/queryPhone/{logId}")
+    public AjaxResult queryCallLogPhone(@PathVariable("logId") Long logId)
+    {
+        String mobile = companyVoiceRoboticCallLogCallphoneService.getDecryptPhoneByLogId(logId);
+        return (AjaxResult) AjaxResult.success().put("mobile", mobile);
+    }
+
 //    /**
 //     * 导出调用日志_ai打电话列表
 //     */

+ 32 - 19
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java

@@ -1,33 +1,31 @@
 package com.fs.company.controller.company;
 
-import java.util.ArrayList;
-import java.util.List;
-
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogSendmsgService;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
-import org.springframework.security.access.prepost.PreAuthorize;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
-import com.fs.common.annotation.Log;
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.enums.BusinessType;
-import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
-import com.fs.company.service.ICompanyVoiceRoboticCallLogSendmsgService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * 调用日志_发送短信Controller
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
@@ -40,6 +38,10 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
 
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+
+    @Autowired
+    private TokenService tokenService;
+
     /**
      * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
      */
@@ -48,6 +50,11 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
     public TableDataInfo list(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
     {
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogSendmsg.setCompanyId(loginUser.getCompany().getCompanyId());
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyVoiceRoboticCallLogSendmsg.setCompanyUserId(loginUser.getUser().getUserId());
+        }
         List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgGroupList(companyVoiceRoboticCallLogSendmsg);
         return getDataTable(list);
     }
@@ -85,7 +92,13 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
     @GetMapping("/count")
     public AjaxResult selectCompanyVoiceRoboticCallLogSendMsgCount()
     {
-        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendMsgCount();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId=loginUser.getCompany().getCompanyId();
+        Long companyUserId=null;
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyUserId=loginUser.getUser().getUserId();
+        }
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendMsgCount(companyId,companyUserId);
         return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 

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

@@ -16,7 +16,7 @@ import java.util.List;
 
 /**
  * 任务外呼电话Controller
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
@@ -89,7 +89,7 @@ public class CompanyVoiceRoboticCalleesController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRoboticCallees:remove')")
     @Log(title = "任务外呼电话", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         return toAjax(companyVoiceRoboticCalleesService.deleteCompanyVoiceRoboticCalleesByIds(ids));

+ 94 - 48
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -1,7 +1,6 @@
 package com.fs.company.controller.company;
 
 import com.alibaba.fastjson.JSONObject;
-import com.fs.aicall.domain.BaseDomain;
 import com.fs.aicall.domain.TaskInfo;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.result.CIDGroupListResult;
@@ -16,25 +15,27 @@ 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.SecurityUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyVoiceRoboticWx;
+import com.fs.company.param.AppendCustomersParam;
 import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
 import com.fs.company.service.impl.CompanyUserServiceImpl;
-import com.fs.company.vo.*;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
+import com.fs.company.vo.CdrDetailVo;
+import com.fs.company.vo.DocCompanyUserVO;
 import com.fs.crm.domain.CrmCustomerProperty;
 import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
-import com.fs.sop.domain.QwSopTemp;
-import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -88,51 +89,20 @@ public class CompanyVoiceRoboticController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询我的机器人外呼任务列表(所有人只看自己的)
+     */
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:myList')")
     @GetMapping("/myList")
-    public TableDataInfo myList(CompanyVoiceRobotic companyVoiceRobotic){
+    public TableDataInfo myList(CompanyVoiceRobotic companyVoiceRobotic) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
-        companyVoiceRobotic.setCreateUser(loginUser.getUser().getUserId());
+        companyVoiceRobotic.setCompanyUserId(loginUser.getUser().getUserId());
         startPage();
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
         fillUserInfo(list);
         return getDataTable(list);
     }
-
-    /**
-     * 填充创建人信息
-     */
-    private void fillUserInfo(List<CompanyVoiceRobotic> list) {
-        if (list == null || list.isEmpty()) {
-            return;
-        }
-
-        Set<Long> userIds = list.stream()
-                .map(CompanyVoiceRobotic::getCreateUser)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
-
-        if (userIds.isEmpty()) {
-            return;
-        }
-
-        Map<Long, DocCompanyUserVO> userMap = companyUserService
-                .selectDocCompanyUserListByUserIds(userIds)
-                .stream()
-                .collect(Collectors.toMap(DocCompanyUserVO::getUserId, Function.identity()));
-
-        list.forEach(item -> {
-            if (item.getCreateUser() != null) {
-                DocCompanyUserVO user = userMap.get(item.getCreateUser());
-                if (user != null) {
-                    item.setCreateByName(user.getNickName());
-                    item.setCreateByDeptName(user.getDeptName());
-                }
-            }
-        });
-    }
-
     /**
      * 查询机器人外呼任务列表
      */
@@ -140,9 +110,26 @@ public class CompanyVoiceRoboticController extends BaseController
     @GetMapping("/listAll")
     public R listAll(CompanyVoiceRobotic companyVoiceRobotic){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyVoiceRobotic.setCompanyUserId(loginUser.getUser().getUserId());
+        }
         companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         return R.ok().put("data", companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic));
     }
+
+    /**
+     * 外呼任务下拉选项(分页 + 名称模糊搜索,按当前登录公司过滤)
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @GetMapping("/selectOptions")
+    public TableDataInfo selectOptions(CompanyVoiceRobotic companyVoiceRobotic) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        return getDataTable(list);
+    }
+
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
     @GetMapping("/calleesList")
     public TableDataInfo calleesList(Long id){
@@ -192,7 +179,7 @@ public class CompanyVoiceRoboticController extends BaseController
         return getDataTable(list);
     }
 
-//    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:wxListQw')")
+    //    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:wxListQw')")
     @GetMapping("/wxListQw")
     public TableDataInfo wxListQw(Long id){
         startPage();
@@ -215,6 +202,21 @@ public class CompanyVoiceRoboticController extends BaseController
         return util.exportExcel(list, "robotic");
     }
 
+    /**
+     * 导出我的机器人外呼任务列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:export')")
+    @Log(title = "我的机器人外呼任务", businessType = BusinessType.EXPORT)
+    @GetMapping("/myExport")
+    public AjaxResult myExport(CompanyVoiceRobotic companyVoiceRobotic) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
+        companyVoiceRobotic.setCompanyUserId(loginUser.getUser().getUserId());
+        List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        ExcelUtil<CompanyVoiceRobotic> util = new ExcelUtil<CompanyVoiceRobotic>(CompanyVoiceRobotic.class);
+        return util.exportExcel(list, "myRobotic");
+    }
+
     /**
      * 获取机器人外呼任务详细信息
      */
@@ -257,7 +259,7 @@ public class CompanyVoiceRoboticController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:remove')")
     @Log(title = "机器人外呼任务", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         for (Long id : ids) {
@@ -268,7 +270,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 删除机器人外呼任务
      */
-	@GetMapping("/getTypes")
+    @GetMapping("/getTypes")
     public R getTypes(){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -289,7 +291,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 启动任务
      */
-	@GetMapping("/startRobotic")
+    @GetMapping("/startRobotic")
     public R startRobotic(String taskId){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -299,7 +301,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 停止任务
      */
-	@GetMapping("/stopRobotic")
+    @GetMapping("/stopRobotic")
     public R stopRobotic(String taskId){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -309,7 +311,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 员工列表
      */
-	@GetMapping("/companyUserList")
+    @GetMapping("/companyUserList")
     public R qwUserList(){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -323,7 +325,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 外呼回调
      */
-	@PostMapping("/callerResult")
+    @PostMapping("/callerResult")
     public R callerResult(@RequestBody PushIIntentionResult result){
         companyVoiceRoboticService.callerResult(result);
         return R.ok();
@@ -359,6 +361,7 @@ public class CompanyVoiceRoboticController extends BaseController
      * 启动任务
      */
     @GetMapping("/taskRun")
+    @Log(title = "启动机器人外呼任务", businessType = BusinessType.OTHER)
     public R taskRun(Long id){
         companyVoiceRoboticService.taskRun(id);
         return R.ok();
@@ -390,12 +393,13 @@ public class CompanyVoiceRoboticController extends BaseController
                             @RequestParam(defaultValue = "10") Integer pageSize,
                             @RequestParam(required = false) String customerName,
                             @RequestParam(required = false) String customerPhone,
+                            @RequestParam(required = false) String encryptPhone,
                             @RequestParam Boolean onlyCallNode) {
         if (roboticId == null) {
             return R.error("任务ID不能为空");
         }
         return R.ok(companyVoiceRoboticService.getExecRecords(roboticId, pageNum, pageSize, customerName,
-                 customerPhone,onlyCallNode));
+                customerPhone,onlyCallNode,encryptPhone));
     }
 
     @GetMapping("/getCurrentCompanyId")
@@ -403,10 +407,52 @@ public class CompanyVoiceRoboticController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         return R.ok().put("companyId", loginUser.getCompany().getCompanyId());
     }
+    /**
+     * 填充创建人信息
+     */
+    private void fillUserInfo(List<CompanyVoiceRobotic> list) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
 
+        Set<Long> userIds = list.stream()
+                .map(CompanyVoiceRobotic::getCreateUser)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        if (userIds.isEmpty()) {
+            return;
+        }
+
+        Map<Long, DocCompanyUserVO> userMap = companyUserService
+                .selectDocCompanyUserListByUserIds(userIds)
+                .stream()
+                .collect(Collectors.toMap(DocCompanyUserVO::getUserId, Function.identity()));
+
+        list.forEach(item -> {
+            if (item.getCreateUser() != null) {
+                DocCompanyUserVO user = userMap.get(item.getCreateUser());
+                if (user != null) {
+                    item.setCreateByName(user.getNickName());
+                    item.setCreateByDeptName(user.getDeptName());
+                }
+            }
+        });
+    }
+
+    @Log(title = "外呼任务暂停操作", businessType = BusinessType.UPDATE)
     @PostMapping("/pauseRoboticActive")
     public R pauseRoboticActive(@RequestBody PauseRoboticActiveParam param){
         return companyVoiceRoboticService.pauseRoboticActive(param);
     }
 
+    /**
+     * 追加客户到运行中的普通任务
+     */
+    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:edit')")
+    @Log(title = "追加客户到运行中任务", businessType = BusinessType.INSERT)
+    @PostMapping("/appendCustomers")
+    public R appendCustomers(@RequestBody AppendCustomersParam param){
+        return companyVoiceRoboticService.appendCustomersToRunningTask(param.getTaskId(), param.getCustomerIds());
+    }
 }

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

@@ -16,7 +16,7 @@ import java.util.List;
 
 /**
  * AI外呼任务加微方式Controller
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */
@@ -89,7 +89,7 @@ public class CompanyVoiceRoboticWxController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:VoiceRoboticWx:remove')")
     @Log(title = "AI外呼任务加微方式", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         return toAjax(companyVoiceRoboticWxService.deleteCompanyVoiceRoboticWxByIds(ids));

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

@@ -239,6 +239,13 @@ public class CompanyWorkflowController extends BaseController {
         return R.ok().put("data",optionVOS);
     }
 
+    @GetMapping("/nodeTypeCodes/{workflowId}")
+    public R getNodeTypeCodes(@PathVariable("workflowId") Long workflowId) {
+        List<String> typeCodes = companyWorkflowService.selectNodeTypeCodesByWorkflowId(workflowId);
+        return R.ok().put("data", typeCodes);
+    }
+
+
     /**
      * 查询某个工作流的版本列表
      */

+ 84 - 5
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java

@@ -21,7 +21,7 @@ import java.util.List;
 
 /**
  * 企微账号Controller
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */
@@ -42,11 +42,23 @@ public class CompanyWxAccountController extends BaseController
     public TableDataInfo list(CompanyWxAccount companyWxEnterpriseAccount)
     {
         startPage();
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxEnterpriseAccount.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxEnterpriseAccount);
         return getDataTable(list);
     }
+
+    /**
+     * 查询我的企微账号列表(仅当前登录人)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myList')")
+    @GetMapping("/myList")
+    public TableDataInfo myList(CompanyWxAccount companyWxAccount)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxAccount.setCompanyUserId(loginUser.getUser().getUserId());
+        startPage();
+        List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxAccount);
+        return getDataTable(list);
+    }
     /**
      * 查询企微账号列表
      */
@@ -74,6 +86,21 @@ public class CompanyWxAccountController extends BaseController
         return util.exportExcel(list, "companyAccount");
     }
 
+    /**
+     * 导出我的企微账号列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myExport')")
+    @Log(title = "我的企微账号", businessType = BusinessType.EXPORT)
+    @GetMapping("/myExport")
+    public AjaxResult myExport(CompanyWxAccount companyWxAccount)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxAccount.setCompanyUserId(loginUser.getUser().getUserId());
+        List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxAccount);
+        ExcelUtil<CompanyWxAccount> util = new ExcelUtil<CompanyWxAccount>(CompanyWxAccount.class);
+        return util.exportExcel(list, "myCompanyAccount");
+    }
+
     /**
      * 获取企微账号详细信息
      */
@@ -91,6 +118,19 @@ public class CompanyWxAccountController extends BaseController
     @Log(title = "企微账号", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody CompanyWxAccount account){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        account.setCompanyId(loginUser.getCompany().getCompanyId());
+        account.setCreateUser(loginUser.getUser().getUserId());
+        return toAjax(companyWxAccountService.insertCompanyWxAccount(account));
+    }
+
+    /**
+     * 新增我的企微账号(仅绑定当前登录人)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myAdd')")
+    @Log(title = "我的企微账号", businessType = BusinessType.INSERT)
+    @PostMapping("/myAdd")
+    public AjaxResult myAdd(@RequestBody CompanyWxAccount account){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         account.setCompanyId(loginUser.getCompany().getCompanyId());
         account.setCompanyUserId(loginUser.getUser().getUserId());
@@ -108,6 +148,25 @@ public class CompanyWxAccountController extends BaseController
         return toAjax(companyWxAccountService.updateCompanyWxAccount(companyWxEnterpriseAccount));
     }
 
+    /**
+     * 修改我的企微账号(仅允许修改当前登录人的数据)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myEdit')")
+    @Log(title = "我的企微账号", businessType = BusinessType.UPDATE)
+    @PutMapping("/myEdit")
+    public AjaxResult myEdit(@RequestBody CompanyWxAccount account)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyUserId = loginUser.getUser().getUserId();
+        CompanyWxAccount existAccount = companyWxAccountService.selectCompanyWxAccountById(account.getId());
+        if (existAccount == null || !companyUserId.equals(existAccount.getCompanyUserId())) {
+            return AjaxResult.error("无权修改该微信账号");
+        }
+        account.setCompanyUserId(companyUserId);
+        account.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(companyWxAccountService.updateCompanyWxAccount(account));
+    }
+
     /**
      * 删除企微账号
      */
@@ -118,15 +177,35 @@ public class CompanyWxAccountController extends BaseController
     {
         return toAjax(companyWxAccountService.deleteCompanyWxAccountByIds(ids));
     }
+
+    /**
+     * 删除我的企微账号(仅允许删除当前登录人的数据)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myRemove')")
+    @Log(title = "我的企微账号", businessType = BusinessType.DELETE)
+    @DeleteMapping("/my/{ids}")
+    public AjaxResult myRemove(@PathVariable Long[] ids)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyUserId = loginUser.getUser().getUserId();
+        for (Long id : ids) {
+            CompanyWxAccount account = companyWxAccountService.selectCompanyWxAccountById(id);
+            if (account == null || !companyUserId.equals(account.getCompanyUserId())) {
+                return AjaxResult.error("无权删除该微信账号");
+            }
+        }
+        return toAjax(companyWxAccountService.deleteCompanyWxAccountByIds(ids));
+    }
     /**
      * 删除企微账号
      */
     @PreAuthorize("@ss.hasPermi('company:companyWx:list')")
 	@GetMapping("/companyListAll")
     public R companyListAll(){
-        CompanyUser companyUser = new CompanyUser();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        CompanyUser companyUser = new CompanyUser();
+        companyUser.setCompanyId(companyId);
         return R.ok().put("data", companyWxAccountService.companyListAllCompany(companyUser));
     }
     /**

+ 3 - 4
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxClientController.java

@@ -12,19 +12,18 @@ import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyWxClientService;
 import com.fs.company.vo.AddWxClientVo;
-import com.fs.framework.security.LoginUser;
-import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
-
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import java.time.LocalDate;
 import java.util.List;
 
 /**
  * 添加个微信账号Controller
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */

+ 4 - 18
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxDialogController.java

@@ -6,12 +6,9 @@ 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.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyWxDialog;
 import com.fs.company.service.ICompanyWxDialogService;
-import com.fs.framework.security.LoginUser;
-import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -20,7 +17,7 @@ import java.util.List;
 
 /**
  * 添加微信话术Controller
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */
@@ -30,8 +27,6 @@ public class CompanyWxDialogController extends BaseController
 {
     @Autowired
     private ICompanyWxDialogService companyWxDialogService;
-    @Autowired
-    private TokenService tokenService;
 
     /**
      * 查询添加微信话术列表
@@ -40,9 +35,6 @@ public class CompanyWxDialogController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(CompanyWxDialog companyWxDialog)
     {
-
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyWxDialog> list = companyWxDialogService.selectCompanyWxDialogList(companyWxDialog);
         return getDataTable(list);
@@ -51,8 +43,6 @@ public class CompanyWxDialogController extends BaseController
     @PreAuthorize("@ss.hasPermi('company:wxDialog:list')")
     @GetMapping("/listAll")
     public R listAll(CompanyWxDialog companyWxDialog){
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         return R.ok().put("data", companyWxDialogService.selectCompanyWxDialogList(companyWxDialog));
     }
 
@@ -64,8 +54,6 @@ public class CompanyWxDialogController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyWxDialog companyWxDialog)
     {
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxDialog> list = companyWxDialogService.selectCompanyWxDialogList(companyWxDialog);
         ExcelUtil<CompanyWxDialog> util = new ExcelUtil<CompanyWxDialog>(CompanyWxDialog.class);
         return util.exportExcel(list, "wxDialog");
@@ -87,10 +75,8 @@ public class CompanyWxDialogController extends BaseController
     @PreAuthorize("@ss.hasPermi('company:wxDialog:add')")
     @Log(title = "添加微信话术", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody CompanyWxDialog companyWxDialog){
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
-        companyWxDialog.setCompanyUserId(loginUser.getUser().getUserId());
+    public AjaxResult add(@RequestBody CompanyWxDialog companyWxDialog)
+    {
         return toAjax(companyWxDialogService.insertCompanyWxDialog(companyWxDialog));
     }
 
@@ -110,7 +96,7 @@ public class CompanyWxDialogController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:wxDialog:remove')")
     @Log(title = "添加微信话术", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         return toAjax(companyWxDialogService.deleteCompanyWxDialogByIds(ids));

+ 7 - 7
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxUserController.java

@@ -111,15 +111,15 @@ public class CompanyWxUserController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:wxUser:remove')")
     @Log(title = "个微用户", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{userIds}")
+    @DeleteMapping("/{userIds}")
     public AjaxResult remove(@PathVariable Long[] userIds)
     {
         return toAjax(companyWxUserService.deleteCompanyWxUserByUserIds(userIds));
     }
 
     /**
-    *  绑定小程序id
-    */
+     *  绑定小程序id
+     */
     @PutMapping("/bindMiniUserId")
     public R bindUserId(@RequestBody CompanyWxUserBindFsUserParam param)
     {
@@ -127,8 +127,8 @@ public class CompanyWxUserController extends BaseController
     }
 
     /**
-    * 批量添加分组
-    */
+     * 批量添加分组
+     */
 
     @PreAuthorize("@ss.hasPermi('company:wxUser:addGroup')")
     @Log(title = "批量添加分组", businessType = BusinessType.INSERT)
@@ -140,8 +140,8 @@ public class CompanyWxUserController extends BaseController
     }
 
     /**
-    * 批量移除分组
-    */
+     * 批量移除分组
+     */
     @PreAuthorize("@ss.hasPermi('company:wxUser:delGroup')")
     @Log(title = "批量移除分组", businessType = BusinessType.INSERT)
     @PostMapping("/delGroup")

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

@@ -99,7 +99,7 @@ public class CompanyWxUserGroupController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:wxUserGroup:remove')")
     @Log(title = "个微 分组", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{groupIds}")
+    @DeleteMapping("/{groupIds}")
     public AjaxResult remove(@PathVariable Long[] groupIds)
     {
         return toAjax(companyWxUserGroupService.deleteCompanyWxUserGroupByGroupIds(groupIds));

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

@@ -239,7 +239,7 @@ public class EasyCallController extends BaseController {
     public R addCommonCallList(@RequestBody EasyCallCommonAddCallListParam param) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getUser().getCompanyId();
-        easyCallService.addCommonCallList(param, companyId);
+        easyCallService.addCommonCallList(param, companyId,null);
         return R.ok("操作成功");
     }
 

+ 170 - 35
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -5,12 +5,15 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.util.OrderUtils;
@@ -22,7 +25,11 @@ import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerUserService;
 import com.fs.crm.vo.*;
 import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.PermissionService;
 import com.fs.framework.service.TokenService;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.ISysDictTypeService;
 import com.github.pagehelper.PageHelper;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -34,6 +41,7 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -62,6 +70,16 @@ public class CrmCustomerController extends BaseController
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @Autowired
+    private PermissionService permissionService;
     @ApiOperation("获取线索客户")
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
     @GetMapping("/getLineCustomerList")
@@ -70,6 +88,10 @@ public class CrmCustomerController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if(!StringUtils.isEmpty(param.getEncryptedMobile())){
+            param.setMobile(PhoneUtil.encryptPhone(param.getEncryptedMobile()));
+            param.setEncryptedMobile(null);
+        }
         List<CrmLineCustomerListQueryVO> list = crmCustomerService.selectCrmLineCustomerListQuery(param);
         if (list != null) {
             for (CrmLineCustomerListQueryVO vo : list) {
@@ -106,37 +128,65 @@ public class CrmCustomerController extends BaseController
         }
         return getDataTable(list);
     }
-    @PreAuthorize("@ss.hasPermi('crm:customer:list')")
-    @GetMapping("/listAll")
-    public R listAll(CrmCustomerListQueryParam crmCustomer){
-
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
-
-        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
-            crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
-        }
+//    @PreAuthorize("@ss.hasPermi('crm:customer:list')")
+//    @GetMapping("/listAll")
+//    public R listAll(CrmCustomerListQueryParam crmCustomer){
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+//
+//        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+//            crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
+//        }
+//
+//        PageHelper.startPage(1, 1000);
+//        if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
+//            crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
+//        }
+//        List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
+//        if (list != null) {
+//            for (CrmCustomerListVO vo : list) {
+//                if(vo.getMobile()!=null){
+//                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+//                }
+//            }
+//        }
+//        return R.ok().put("rows", list);
+//    }
+@PreAuthorize("@ss.hasPermi('crm:customer:list')")
+@GetMapping("/listAll")
+public TableDataInfo listAll(CrmCustomerListQueryParam crmCustomer){
+    LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+    crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+
+    if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+        crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
+    }
+    PageHelper.startPage(crmCustomer.getPageNum(), crmCustomer.getPageSize());
+    if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
+        crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
+    }
+    List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
 
-        PageHelper.startPage(1, 1000);
-        if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
-            crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
-        }
-        List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
-        if (list != null) {
-            for (CrmCustomerListVO vo : list) {
-                if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
-                }
+    if (list != null) {
+        for (CrmCustomerListVO vo : list) {
+            if(vo.getMobile()!=null){
+                vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
             }
         }
-        return R.ok().put("rows", list);
     }
+    return getDataTable(list);
+}
     @PreAuthorize("@ss.hasPermi('crm:customer:list')")
     @GetMapping("/listNoPage")
     public R listNoPage(CrmCustomerListQueryParam crmCustomer){
 
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+            crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
+        }
         if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
             crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
         }
@@ -144,7 +194,7 @@ public class CrmCustomerController extends BaseController
         if (list != null) {
             for (CrmCustomerListVO vo : list) {
                 if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                 }
             }
         }
@@ -159,6 +209,7 @@ public class CrmCustomerController extends BaseController
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         param.setCompanyUserId(loginUser.getUser().getUserId());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
+
         if(!StringUtils.isEmpty(param.getCreateTimeRange())){
             param.setCustomerCreateTime(param.getCreateTimeRange().split("--"));
         }
@@ -166,7 +217,7 @@ public class CrmCustomerController extends BaseController
         if (list != null) {
             for (CrmMyCustomerListQueryVO vo : list) {
                 if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                 }
             }
         }
@@ -188,7 +239,7 @@ public class CrmCustomerController extends BaseController
         if (list != null) {
             for (CrmMyCustomerListQueryVO vo : list) {
                 if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                 }
             }
             fillMyCustomerListRoboticCallOutCount(list, param.getCompanyId());
@@ -214,7 +265,7 @@ public class CrmCustomerController extends BaseController
             if (list1 != null) {
                 for (CrmLineCustomerListQueryVO vo : list1) {
                     if(vo.getMobile()!=null){
-                        vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                        vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                     }
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
                 }
@@ -226,7 +277,7 @@ public class CrmCustomerController extends BaseController
             if (list != null) {
                 for (CrmCustomerListQueryVO vo : list) {
                     if (vo.getMobile() != null) {
-                        vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                        vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                     }
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
 
@@ -311,7 +362,7 @@ public class CrmCustomerController extends BaseController
         CrmCustomer customer=crmCustomerService.selectCrmCustomerById(customerId);
         Boolean isReceive=false;
         if (customer != null){
-            customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+            customer.setMobile(PhoneUtil.maskMobileForList(customer.getMobile()));
             if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
                 isReceive=true;
             }
@@ -343,6 +394,29 @@ public class CrmCustomerController extends BaseController
 
     }
 
+    //条件分配-统计数量
+    @PreAuthorize("@ss.hasPermi('crm:customer:assignToUser')")
+    @PostMapping("/countByCondition")
+    public R countByCondition(@RequestBody CrmCustomerConditionAssignParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.getQueryParams().put("companyId", loginUser.getCompany().getCompanyId());
+        int count = crmCustomerService.countByCondition(param.getQueryType(), param.getQueryParams());
+        return R.ok().put("count", count);
+    }
+
+    //条件分配-执行分配
+    @PreAuthorize("@ss.hasPermi('crm:customer:assignToUser')")
+    @PostMapping("/assignByCondition")
+    public R assignByCondition(@RequestBody CrmCustomerConditionAssignParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.getQueryParams().put("companyId", loginUser.getCompany().getCompanyId());
+        return crmCustomerService.assignByCondition(loginUser.getUsername(), loginUser.getUser().getUserId(), param);
+    }
+
+
 
     @PreAuthorize("@ss.hasPermi('crm:customer:add')")
     @Log(title = "创建客户", businessType = BusinessType.INSERT)
@@ -484,15 +558,19 @@ public class CrmCustomerController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
-
-
-
-
         List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListQuery(param);
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerExportVO customer:list){
             if(StringUtils.isNotEmpty(customer.getMobile())){
-
-                    customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                customer.setMobile(PhoneUtil.maskMobileForList(customer.getMobile()));
+            }
+            if(hasQueryPhonePermission && StringUtils.isNotEmpty(customer.getMobile())){
+                String originalMobile = customer.getMobile().replaceAll("\\*+", "");
+                if(originalMobile.length() >= 11){
+                    customer.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                }else{
+                    customer.setMobilePlain(originalMobile);
+                }
             }
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
@@ -504,14 +582,21 @@ public class CrmCustomerController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListByIds(customerIds);
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerExportVO customer:list){
             if(StringUtils.isNotEmpty(customer.getMobile())){
                 if(loginUser.getUser().getUserType().equals("00")){
+                    if(hasQueryPhonePermission){
+                        if(customer.getMobile().length() > 11){
+                            customer.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                        }else{
+                            customer.setMobilePlain(customer.getMobile());
+                        }
+                    }
                 }
                 else{
-                    customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    customer.setMobile(PhoneUtil.maskMobileForList(customer.getMobile()));
                 }
-
             }
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
@@ -544,4 +629,54 @@ public class CrmCustomerController extends BaseController
         return R.ok().put("data",list);
     }
 
+    /**
+     * 查看手机号
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:queryPhone')")
+    @Log(title = "查看手机号", businessType = BusinessType.GRANT)
+    @GetMapping(value = "/queryPhone/{customerId}")
+    public R queryPhone(@PathVariable("customerId") Long customerId)
+    {
+        CrmCustomer customer = crmCustomerService.selectCrmCustomerById(customerId);
+        if(null != customer){
+            String mobile = customer.getMobile();
+            if (mobile != null && mobile.length() > 11) {
+                mobile = PhoneUtil.decryptPhone(mobile);
+            }
+
+            return  R.ok().put("mobile", mobile);
+        }
+
+        return R.error("数据缺失,请联系管理员");
+    }
+
+    @ApiOperation("动态数据字典查询")
+    @GetMapping("/tradeDicts")
+    public AjaxResult getTradeDicts() {
+
+        String[] dictTypes = {"crm_customer_source", "crm_customer_user_status",
+                "crm_customer_type", "crm_customer_tag"};
+        Map<String, List<SysDictData>> result = new HashMap<>();
+
+        for (String dictType : dictTypes) {
+            List<SysDictData> data = null;
+            // 保底逻辑:前缀查不到数据则使用原始字典
+            data = dictTypeService.selectDictDataByType(dictType);
+            result.put(dictType, data);
+        }
+
+        return AjaxResult.success(result);
+    }
+
+    private String getDictRemarkByValue(String dictType, String dictValue) {
+        List<SysDictData> dictDataList = dictTypeService.selectDictDataByType(dictType);
+        if (dictDataList != null) {
+            for (SysDictData data : dictDataList) {
+                if (dictValue.equals(data.getDictValue())) {
+                    return StringUtils.isNotEmpty(data.getRemark()) ? data.getRemark() : "";
+                }
+            }
+        }
+        return "";
+    }
 }

+ 40 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java

@@ -602,6 +602,46 @@ public class QwExternalContactController extends BaseController
         return toAjax(qwExternalContactService.updateQwExternalContact(qwExternalContact));
     }
 
+    /**
+     * 修改外部联系人联系手机号码(仅更新数据库 contact_mobiles 字段)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:externalContact:edit') or @ss.hasPermi('qw:externalContact:deptEdit') or @ss.hasPermi('qw:externalContact:myEdit')")
+    @Log(title = "企业微信客户", businessType = BusinessType.UPDATE)
+    @PutMapping("/editRemarkMobiles")
+    public R editRemarkMobiles(@RequestBody QwExternalContact qwExternalContact)
+    {
+        if (qwExternalContact.getId() == null) {
+            return R.error("客户ID不能为空");
+        }
+        String contactMobiles = qwExternalContact.getContactMobiles();
+        if (contactMobiles == null) {
+            contactMobiles = "";
+        }
+        contactMobiles = contactMobiles.trim();
+        if (StringUtils.isNotEmpty(contactMobiles)) {
+            if (contactMobiles.contains(",")) {
+                return R.error("请使用英文逗号分隔手机号码");
+            }
+            if (contactMobiles.contains(",,") || contactMobiles.startsWith(",") || contactMobiles.endsWith(",")) {
+                return R.error("手机号码格式不正确,请使用英文逗号分隔");
+            }
+            String[] parts = contactMobiles.split(",");
+            LinkedHashSet<String> uniqueMobiles = new LinkedHashSet<>();
+            for (String part : parts) {
+                String mobile = part.trim();
+                if (StringUtils.isEmpty(mobile)) {
+                    return R.error("手机号码格式不正确,请使用英文逗号分隔");
+                }
+                if (!uniqueMobiles.add(mobile)) {
+                    return R.error("手机号码重复:" + mobile);
+                }
+            }
+            contactMobiles = String.join(",", uniqueMobiles);
+        }
+        int rows = qwExternalContactService.updateContactMobilesById(qwExternalContact.getId(), contactMobiles);
+        return rows > 0 ? R.ok() : R.error("修改失败");
+    }
+
     @PreAuthorize("@ss.hasPermi('qw:externalContact:edit')")
     @Log(title = "企业微信客户", businessType = BusinessType.UPDATE)
     @PutMapping("/editStatus")

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

@@ -120,6 +120,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/common/getId**").anonymous()
                 .antMatchers("/common/uploadOSS**").anonymous()
+                .antMatchers("/common/proxy/recording").anonymous()
                 .antMatchers("/company/user/common/uploadOSS").anonymous()
                 .antMatchers("/pay/wxPay/payNotify**").anonymous()
                 .antMatchers("/common/uploadWang**").anonymous()

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

@@ -674,8 +674,10 @@ public class FsStoreOrderScrmController extends BaseController
      */
     private void applyCompanyDataScope(FsStoreOrderParam param, LoginUser loginUser) {
         param.setCompanyId(loginUser.getCompany().getCompanyId());
-        if (!"00".equals(loginUser.getUser().getUserType()) && !hasRoleKey(loginUser, "finance_order_goods")) {
-            param.setCompanyUserId(loginUser.getUser().getUserId());
+        if(!"恒春来".equals(cloudHostProper.getCompanyName())) {
+            if (!"00".equals(loginUser.getUser().getUserType()) && !hasRoleKey(loginUser, "finance_order_goods")) {
+                param.setCompanyUserId(loginUser.getUser().getUserId());
+            }
         }
     }
 

+ 86 - 9
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -3,25 +3,26 @@ package com.fs.app.task;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.app.service.IpadSendServer;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.PubFun;
+import com.fs.company.domain.AiAddwxSopUserNodeRule;
+import com.fs.company.domain.AiAddwxSopUserWorkflow;
+import com.fs.company.enums.AiAddWechatSopNodeEnum;
+import com.fs.company.mapper.AiAddwxSopUserNodeRuleMapper;
+import com.fs.company.mapper.AiAddwxSopUserWorkflowMapper;
+import com.fs.company.service.IAiAddwxSopUserNodeService;
 import com.fs.company.service.ICompanyMiniappService;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.ipad.vo.BaseVo;
-import com.fs.qw.domain.QwIpadServer;
-import com.fs.qw.domain.QwPushCount;
-import com.fs.qw.domain.QwRestrictionPushRecord;
-import com.fs.qw.domain.QwUser;
-import com.fs.qw.mapper.QwIpadServerMapper;
-import com.fs.qw.mapper.QwPushCountMapper;
-import com.fs.qw.mapper.QwRestrictionPushRecordMapper;
-import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.domain.*;
+import com.fs.qw.mapper.*;
 import com.fs.qw.service.impl.AsyncSopTestService;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopTempSetting;
@@ -41,6 +42,7 @@ import org.springframework.util.StringUtils;
 
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -62,6 +64,10 @@ public class SendMsg {
     private final IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
     private final QwPushCountMapper qwPushCountMapper;
     private final QwRestrictionPushRecordMapper qwRestrictionPushRecordMapper;
+    private final IAiAddwxSopUserNodeService aiAddwxSopUserNodeService;
+    private final AiAddwxSopUserWorkflowMapper aiAddwxSopUserWorkflowMapper;
+    private final AiAddwxSopUserNodeRuleMapper aiAddwxSopUserNodeRuleMapper;
+    private final QwExternalContactMapper qwExternalContactMapper;
 
     @Value("${group-no}")
     private String groupNo;
@@ -95,7 +101,7 @@ public class SendMsg {
     @Qualifier("customThreadPool")
     private ThreadPoolTaskExecutor customThreadPool;
 
-    public SendMsg(QwUserMapper qwUserMapper, QwSopLogsMapper qwSopLogsMapper, IpadSendServer sendServer, SysConfigMapper sysConfigMapper, IQwSopLogsService qwSopLogsService, QwIpadServerMapper qwIpadServerMapper, RedisCacheT<Long> redisCache, AsyncSopTestService asyncSopTestService, ICompanyMiniappService companyMiniappService, IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService, QwPushCountMapper qwPushCountMapper, QwRestrictionPushRecordMapper qwRestrictionPushRecordMapper) {
+    public SendMsg(QwUserMapper qwUserMapper, QwSopLogsMapper qwSopLogsMapper, IpadSendServer sendServer, SysConfigMapper sysConfigMapper, IQwSopLogsService qwSopLogsService, QwIpadServerMapper qwIpadServerMapper, RedisCacheT<Long> redisCache, AsyncSopTestService asyncSopTestService, ICompanyMiniappService companyMiniappService, IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService, QwPushCountMapper qwPushCountMapper, QwRestrictionPushRecordMapper qwRestrictionPushRecordMapper, IAiAddwxSopUserNodeService aiAddwxSopUserNodeService, AiAddwxSopUserWorkflowMapper aiAddwxSopUserWorkflowMapper, AiAddwxSopUserNodeRuleMapper aiAddwxSopUserNodeRuleMapper, QwExternalContactMapper qwExternalContactMapper) {
         this.qwUserMapper = qwUserMapper;
         this.qwSopLogsMapper = qwSopLogsMapper;
         this.sendServer = sendServer;
@@ -108,6 +114,10 @@ public class SendMsg {
         this.fsCoursePlaySourceConfigService = fsCoursePlaySourceConfigService;
         this.qwPushCountMapper = qwPushCountMapper;
         this.qwRestrictionPushRecordMapper = qwRestrictionPushRecordMapper;
+        this.aiAddwxSopUserNodeService = aiAddwxSopUserNodeService;
+        this.aiAddwxSopUserWorkflowMapper = aiAddwxSopUserWorkflowMapper;
+        this.aiAddwxSopUserNodeRuleMapper = aiAddwxSopUserNodeRuleMapper;
+        this.qwExternalContactMapper = qwExternalContactMapper;
     }
     private List<QwUser> getQwUserList() {
         if (qwUserList.isEmpty()) {
@@ -477,4 +487,71 @@ public class SendMsg {
         long end3 = System.currentTimeMillis();
         log.info("销售执行完成:{}, 耗时:{}", user.getQwUserName(), end3 - start3);
     }
+    /**
+     * 今正cid加微后定时发消息
+     */
+    @Scheduled(cron = "0 * 9-18 * * ?") // 每分钟
+    public void sendAiAddWxWorkflowMsg(){
+        if(!CloudHostUtils.hasCloudHostName("今正科技")){
+            log.info("非今正科技,不执行cid加微后工作流发企微消息");
+            return;
+        }
+
+        log.info("开始执行cid加微后sop任务,现在时间{}",LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        List<AiAddwxSopUserWorkflow> aiAddwxSopUserWorkflows = aiAddwxSopUserWorkflowMapper.selectList(new LambdaQueryWrapper<AiAddwxSopUserWorkflow>()
+                .eq(AiAddwxSopUserWorkflow::getDelFlag, 0).ne(AiAddwxSopUserWorkflow::getCurrentNodeType, AiAddWechatSopNodeEnum.NOT_DEAL.getCode())
+                .ne(AiAddwxSopUserWorkflow::getCurrentNodeType, AiAddWechatSopNodeEnum.DEAL.getCode()));
+        Map<String, List<AiAddwxSopUserWorkflow>> workflowsMap = aiAddwxSopUserWorkflows
+                .stream()
+                .collect(Collectors.groupingBy(AiAddwxSopUserWorkflow::getCurrentNodeType));
+        //        Set<AiAddWechatSopNodeEnum> skipNodes = EnumSet.of(
+//                AiAddWechatSopNodeEnum.DEAL,
+//                AiAddWechatSopNodeEnum.NOT_DEAL
+//        );
+        for (Map.Entry<String, List<AiAddwxSopUserWorkflow>> next : workflowsMap.entrySet()) {
+//                        AiAddWechatSopNodeEnum nodeEnum = AiAddWechatSopNodeEnum.fromCode(next.getKey());
+//            if (skipNodes.contains(nodeEnum)) {
+//                continue;
+//            }
+            aiAddwxSopUserNodeService.nextNode(next);
+        }
+        //执行后再发送user_rule的消息
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime minutesAgo = now.minusMinutes(1);
+        List<AiAddwxSopUserNodeRule> sendRules = aiAddwxSopUserNodeRuleMapper.selectList(new LambdaQueryWrapper<AiAddwxSopUserNodeRule>()
+                .eq(AiAddwxSopUserNodeRule::getIsSent, 0).isNotNull(AiAddwxSopUserNodeRule::getRealSendTime)
+                .between(AiAddwxSopUserNodeRule::getRealSendTime, minutesAgo, now));
+        sendRules.forEach( rule ->{
+            try {
+                cidAddwxSopSendQwMsg(rule);
+            }catch (Exception e){
+                log.error("nodeRule{}发送消息错误:{}",rule.getUserNodeRuleId(), e.getMessage());
+            }
+            rule.setIsSent(1);
+            aiAddwxSopUserNodeRuleMapper.updateById( rule);
+        });
+    }
+        private void cidAddwxSopSendQwMsg(AiAddwxSopUserNodeRule rule ){
+        QwUser qwUser = qwUserMapper.selectById(rule.getQwUserId());
+        QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(rule.getQwExternalId());
+            //只拼一个发消息的逻辑
+        QwSopCourseFinishTempSetting.Setting content = new QwSopCourseFinishTempSetting.Setting();
+        content.setValue(rule.getContentJson());
+        content.setContentType("1");
+
+        QwSopLogs qwSopLogs = new QwSopLogs();
+        qwSopLogs.setId(String.valueOf(rule.getUserNodeRuleId()));
+        qwSopLogs.setSendType(1);
+        qwSopLogs.setExternalUserId(externalContact.getExternalUserId());
+
+        BaseVo vo = new BaseVo();
+        vo.setCorpCode(qwUser.getCorpId());
+        vo.setCorpId(qwUser.getCorpId());
+        // 判断这个企微是否需要发送
+        if (!sendServer.isSend(qwUser, vo)) {
+            log.info(" 当前这个企微不需要发送 数据{}", qwUser);
+        }
+
+        sendServer.send(content, qwUser, qwSopLogs, null, vo);
+    }
 }

+ 129 - 11
fs-ipad-task/src/main/java/com/fs/app/task/SendSmsMsg.java

@@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.service.ISmsService;
 import com.fs.company.domain.CompanySmsLogs;
@@ -13,10 +14,13 @@ import com.fs.his.domain.FsUser;
 import com.fs.his.dto.SendResultDetailDTO;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.PhoneUtil;
+import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwIpadServer;
 import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwIpadServerMapper;
 import com.fs.qw.service.IQwSopSmsLogsService;
+import com.fs.system.service.ISysDictTypeService;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.service.IQwSopLogsService;
@@ -47,6 +51,10 @@ public class SendSmsMsg {
     @Value("${group-no}")
     private String groupNo;
 
+    private static final String DICT_TYPE_QW_EXTERNAL_CONTACT_FUNC = "sys_qw_external_contact_func";
+    /** 字典 dict_sort=2:是否开启根据外部联系人手机号码发送短信 */
+    private static final long DICT_SORT_SEND_SMS_BY_CONTACT_MOBILE = 2L;
+
     private final RedisCache redisCache;
 
     private final ISmsService smsService;
@@ -65,6 +73,10 @@ public class SendSmsMsg {
 
     private final IFsCourseWatchLogService watchLogService;
 
+    private final ISysDictTypeService sysDictTypeService;
+
+    private final QwExternalContactMapper qwExternalContactMapper;
+
     // 线程池配置
     private static final int CORE_POOL_SIZE = 50;
     private static final int MAX_POOL_SIZE = 150;
@@ -74,12 +86,18 @@ public class SendSmsMsg {
     // 分页大小
     private static final int PAGE_SIZE = 3000;
 
-    // 手机号缓存
+    // fs_user 手机号缓存
     private final Cache<Long, String> phoneCache = CacheBuilder.newBuilder()
             .expireAfterWrite(1, TimeUnit.HOURS)
             .maximumSize(100000)
             .build();
 
+    // 外部联系人 contact_mobiles 缓存(取逗号分隔的第一个号码)
+    private final Cache<Long, String> contactMobileCache = CacheBuilder.newBuilder()
+            .expireAfterWrite(1, TimeUnit.HOURS)
+            .maximumSize(100000)
+            .build();
+
     // 限流器:控制全局发送速率
     private final RateLimiter rateLimiter = RateLimiter.create(500); // 每秒500条
 
@@ -105,7 +123,9 @@ public class SendSmsMsg {
                       QwSopLogsMapper qwSopLogsMapper,
                       IQwSopLogsService qwSopLogsService,
                       IFsCourseWatchLogService watchLogService,
-                      CompanySmsLogsMapper companySmsLogsMapper) {
+                      CompanySmsLogsMapper companySmsLogsMapper,
+                      ISysDictTypeService sysDictTypeService,
+                      QwExternalContactMapper qwExternalContactMapper) {
         this.qwIpadServerMapper = qwIpadServerMapper;
         this.qwSopSmsLogsService = qwSopSmsLogsService;
         this.smsService = smsService;
@@ -115,6 +135,8 @@ public class SendSmsMsg {
         this.qwSopLogsService = qwSopLogsService;
         this.watchLogService = watchLogService;
         this.companySmsLogsMapper = companySmsLogsMapper;
+        this.sysDictTypeService = sysDictTypeService;
+        this.qwExternalContactMapper = qwExternalContactMapper;
     }
 
     @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次
@@ -171,6 +193,8 @@ public class SendSmsMsg {
 
         // 计算时间范围
         TimeRange timeRange = calculateTimeRange();
+        boolean sendByContactMobile = isSendSmsByContactMobileEnabled();
+        log.info("sendSms: 外部联系人手机号发送开关={}", sendByContactMobile);
 
         long lastId = 0L;
         AtomicLong totalProcessed = new AtomicLong(0);
@@ -204,7 +228,7 @@ public class SendSmsMsg {
                 List<QwSopSmsLogs> serverBatch = entry.getValue();
                 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                     try {
-                        SendResult result = processServerBatch(serverBatch,sopLogsMap);
+                        SendResult result = processServerBatch(serverBatch, sopLogsMap, sendByContactMobile);
                         totalProcessed.addAndGet(result.success);
                         totalFailed.addAndGet(result.failed);
                     } catch (Exception e) {
@@ -261,15 +285,29 @@ public class SendSmsMsg {
         return new TimeRange(startTime, endTime);
     }
 
+    /**
+     * 是否开启根据外部联系人手机号码发送短信(字典 sys_qw_external_contact_func,dict_sort=2,dict_value=1)
+     */
+    private boolean isSendSmsByContactMobileEnabled() {
+        List<SysDictData> dictList = sysDictTypeService.selectDictDataByType(DICT_TYPE_QW_EXTERNAL_CONTACT_FUNC);
+        if (dictList == null || dictList.isEmpty()) {
+            return false;
+        }
+        return dictList.stream()
+                .filter(item -> "0".equals(item.getStatus()))
+                .filter(item -> item.getDictSort() != null && DICT_SORT_SEND_SMS_BY_CONTACT_MOBILE == item.getDictSort())
+                .anyMatch(item -> "1".equals(item.getDictValue() != null ? item.getDictValue().trim() : ""));
+    }
+
     /**
      * 处理单个server的一批短信(同步发送,避免线程爆炸)
      */
-    private SendResult processServerBatch(List<QwSopSmsLogs> batch,Map<Long, QwSopLogs> sopLogsMap) {
+    private SendResult processServerBatch(List<QwSopSmsLogs> batch, Map<Long, QwSopLogs> sopLogsMap, boolean sendByContactMobile) {
         int success = 0;
         List<SendResultDetailDTO> failReasonsList = Collections.synchronizedList(new ArrayList<>());
         List<Long> failedIds = Collections.synchronizedList(new ArrayList<>());
         // 批量获取手机号
-        Map<Long, String> userPhoneMap = getPhoneNumbers(batch);
+        Map<Long, String> phoneMap = resolvePhones(batch, sendByContactMobile);
 
         for (QwSopSmsLogs logRecord : batch) {
             rateLimiter.acquire();
@@ -301,7 +339,7 @@ public class SendSmsMsg {
                     continue;
                 }
 
-                SendResultDetailDTO detail = sendSingleSms(logRecord, userPhoneMap, redisKey);
+                SendResultDetailDTO detail = sendSingleSms(logRecord, phoneMap, redisKey);
                 if (detail.isSuccess()) {
                     success++;
                 } else {
@@ -333,9 +371,40 @@ public class SendSmsMsg {
     }
 
     /**
-     * 获取手机号映射(缓存+批量查询)
+     * 解析待发送记录的手机号(key 为 qw_sop_sms_logs.id)
+     */
+    private Map<Long, String> resolvePhones(List<QwSopSmsLogs> batch, boolean sendByContactMobile) {
+        Map<Long, String> result = new HashMap<>();
+        if (sendByContactMobile) {
+            Map<Long, String> contactPhoneMap = getContactMobiles(batch);
+            for (QwSopSmsLogs logRecord : batch) {
+                if (logRecord.getContactId() == null) {
+                    continue;
+                }
+                String phone = contactPhoneMap.get(logRecord.getContactId());
+                if (phone != null) {
+                    result.put(logRecord.getId(), phone);
+                }
+            }
+            return result;
+        }
+        Map<Long, String> userPhoneMap = getFsUserPhones(batch);
+        for (QwSopSmsLogs logRecord : batch) {
+            if (logRecord.getFsUserId() == null) {
+                continue;
+            }
+            String phone = userPhoneMap.get(logRecord.getFsUserId());
+            if (phone != null) {
+                result.put(logRecord.getId(), phone);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 获取 fs_user 手机号映射(缓存+批量查询)
      */
-    private Map<Long, String> getPhoneNumbers(List<QwSopSmsLogs> batch) {
+    private Map<Long, String> getFsUserPhones(List<QwSopSmsLogs> batch) {
         // 收集所有用户ID
         Set<Long> userIds = batch.stream()
                 .map(QwSopSmsLogs::getFsUserId)
@@ -369,11 +438,57 @@ public class SendSmsMsg {
         return result;
     }
 
+    /**
+     * 获取外部联系人 contact_mobiles 映射(取逗号分隔的第一个号码)
+     */
+    private Map<Long, String> getContactMobiles(List<QwSopSmsLogs> batch) {
+        Set<Long> contactIds = batch.stream()
+                .map(QwSopSmsLogs::getContactId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        Map<Long, String> result = new HashMap<>();
+        Set<Long> missingContactIds = new HashSet<>();
+        for (Long contactId : contactIds) {
+            String phone = contactMobileCache.getIfPresent(contactId);
+            if (phone != null) {
+                result.put(contactId, phone);
+            } else {
+                missingContactIds.add(contactId);
+            }
+        }
+
+        if (!missingContactIds.isEmpty()) {
+            List<QwExternalContact> contacts = qwExternalContactMapper.selectQwExternalContactByIds(new ArrayList<>(missingContactIds));
+            for (QwExternalContact contact : contacts) {
+                String phone = pickFirstContactMobile(contact.getContactMobiles());
+                if (phone != null) {
+                    result.put(contact.getId(), phone);
+                    contactMobileCache.put(contact.getId(), phone);
+                }
+            }
+        }
+        return result;
+    }
+
+    private String pickFirstContactMobile(String contactMobiles) {
+        if (StringUtils.isEmpty(contactMobiles)) {
+            return null;
+        }
+        for (String part : contactMobiles.split(",")) {
+            String mobile = part.trim();
+            if (!StringUtils.isEmpty(mobile)) {
+                return mobile;
+            }
+        }
+        return null;
+    }
+
     /**
      * 发送单条短信,返回是否成功
      */
-    private SendResultDetailDTO sendSingleSms(QwSopSmsLogs logRecord, Map<Long, String> userPhoneMap, String redisKey) {
-        String phone = userPhoneMap.get(logRecord.getFsUserId());
+    private SendResultDetailDTO sendSingleSms(QwSopSmsLogs logRecord, Map<Long, String> phoneMap, String redisKey) {
+        String phone = phoneMap.get(logRecord.getId());
         String content = logRecord.getContent();
 
         if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(content)) {
@@ -576,6 +691,9 @@ public class SendSmsMsg {
             return;
         }
 
+        boolean sendByContactMobile = isSendSmsByContactMobileEnabled();
+        log.info("processSendLogs: 外部联系人手机号发送开关={}", sendByContactMobile);
+
         List<Long> sopLogIds = needCheckList.stream()
                 .map(QwSopSmsLogs::getSopLogId)
                 .collect(Collectors.toList());
@@ -595,7 +713,7 @@ public class SendSmsMsg {
             List<QwSopSmsLogs> serverBatch = entry.getValue();
             CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                 try {
-                    SendResult result = processServerBatch(serverBatch, sopLogsMap);
+                    SendResult result = processServerBatch(serverBatch, sopLogsMap, sendByContactMobile);
                     totalProcessed.addAndGet(result.success);
                     totalFailed.addAndGet(result.failed);
                 } catch (Exception e) {

+ 78 - 0
fs-live-ws/src/main/resources/logback-spring.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds">
+
+    <!-- 日志目录,可通过 logging.file.path 或环境变量 LOG_PATH 覆盖 -->
+    <springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="/home/fs-live-ws/logs"/>
+    <!-- 单文件大小上限:超过后自动切割 -->
+    <springProperty scope="context" name="MAX_FILE_SIZE" source="logging.logback.rollingpolicy.max-file-size" defaultValue="200MB"/>
+    <!-- 归档日志保留天数,超过自动删除 -->
+    <springProperty scope="context" name="MAX_HISTORY" source="logging.logback.rollingpolicy.max-history" defaultValue="15"/>
+    <!-- 所有归档日志总大小上限(兜底) -->
+    <springProperty scope="context" name="TOTAL_SIZE_CAP" source="logging.logback.rollingpolicy.total-size-cap" defaultValue="10GB"/>
+
+    <property name="APP_NAME" value="fs-live-ws"/>
+    <property name="LOG_CHARSET" value="UTF-8"/>
+    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
+
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>${LOG_CHARSET}</charset>
+            <immediateFlush>true</immediateFlush>
+        </encoder>
+    </appender>
+
+    <!--
+        按「日期 + 大小」滚动,归档 gzip 压缩:
+        - 每天跨日自动切割
+        - 单文件超过 200MB 时在同一天内继续切割(.%i)
+        - 超过 15 天的归档自动删除
+    -->
+    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_PATH}/${APP_NAME}.log</file>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>${LOG_CHARSET}</charset>
+            <immediateFlush>true</immediateFlush>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_PATH}/archive/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
+            <cleanHistoryOnStart>false</cleanHistoryOnStart>
+        </rollingPolicy>
+    </appender>
+
+    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_PATH}/${APP_NAME}-error.log</file>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>${LOG_PATTERN}</pattern>
+            <charset>${LOG_CHARSET}</charset>
+            <immediateFlush>true</immediateFlush>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_PATH}/archive/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
+            <cleanHistoryOnStart>false</cleanHistoryOnStart>
+        </rollingPolicy>
+    </appender>
+
+    <root level="INFO">
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="FILE_ALL"/>
+        <appender-ref ref="FILE_ERROR"/>
+    </root>
+
+    <logger name="com.fs" level="INFO"/>
+    <logger name="org.springframework" level="WARN"/>
+    <logger name="io.netty" level="WARN"/>
+
+</configuration>

+ 2 - 0
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -211,6 +211,8 @@ public class CommonController {
 
     @Autowired
     private ILiveRedPacketLogService redPacketLogService;
+    @Autowired
+    private com.fs.app.task.qwTask qwTask;
 
     @GetMapping("/test")
     public R test(){

+ 24 - 1
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -58,6 +58,7 @@ import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.retry.annotation.Backoff;
 import org.springframework.retry.annotation.Retryable;
 import org.springframework.scheduling.annotation.Async;
@@ -223,6 +224,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Autowired
     private ConfigUtil configUtil;
 
+    @Value("${cloud_host.company_name}")
+    private String signProjectName;
+
     @PostConstruct
     public void init() {
         loadCourseConfig();
@@ -1924,6 +1928,21 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     setting.setContentType("14");
                     break;
                 case "15": //app文本
+                    String appTxt = StringUtil.strIsNullOrEmpty(welcomeText) ? "" : welcomeText;
+                    String defaultCustomerName = "同学";
+                    if (contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())) {
+                        defaultCustomerName = contact.getName();
+                    }
+                    String customerName = contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus())
+                            ? defaultCustomerName : contact.getStageStatus();
+                    if (StringUtils.isNotEmpty(setting.getValue())) {
+                        setting.setValue(setting.getValue()
+                                .replaceAll("#客服称呼#", appTxt)
+                                .replaceAll("#销售称呼#", appTxt)
+                                .replaceAll("#客户称呼#", customerName));
+                    }
+                    sopLogs.setAppSendStatus(0);
+                    break;
                 case "16": //app语音
                     sopLogs.setAppSendStatus(0);
                     break;
@@ -1972,7 +1991,11 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     createLiveWatchLogAndEnQueue(companyId,companyUserId,externalId, setting.getLiveId(),sysConfig.getAppId(),1,qwUserId,logVo.getCorpId());
                     break;
                 case "21"://短信看课
-                    if (sopLogs.getFsUserId() != null && !Long.valueOf(0L).equals(sopLogs.getFsUserId())) {
+                    boolean check = !Long.valueOf(0L).equals(sopLogs.getFsUserId());
+                    if ("济南联志健康".equals(signProjectName)) {
+                        check = true;
+                    }
+                    if (sopLogs.getFsUserId() != null && check) {
                         addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId, logVo,2);
                         sortLink = generateSmsShortLink(setting, logVo, sendTime, courseId, videoId,
                                 qwUserId, companyUserId, companyId, externalId, isOfficial, sopLogs.getFsUserId());

+ 3 - 2
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java

@@ -1,12 +1,13 @@
 package com.fs.aiSipCall.domain;
 
-import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.util.Date;
+
 /**
  * sip用户信息对象 ai_sip_call_user
  *

+ 3 - 3
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java

@@ -1,12 +1,12 @@
 package com.fs.aiSipCall.service;
 
-import java.util.List;
-import java.util.Map;
-
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.aiSipCall.domain.AiSipCallUser;
 import com.fs.common.core.domain.AjaxResult;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * sip用户信息Service接口
  *

+ 2 - 1
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java

@@ -1,9 +1,10 @@
 package com.fs.aiSipCall.service;
 
-import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.aiSipCall.domain.AiSipCallVoiceTtsAliyun;
 
+import java.util.List;
+
 /**
  * aiSIP外呼阿里云音色Service接口
  *

+ 6 - 12
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java

@@ -15,7 +15,6 @@ import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
 import com.fs.aiSipCall.param.ApiCallRecordQueryParams;
 import com.fs.aiSipCall.service.IAiSipCallOutboundCdrService;
 import com.fs.aiSipCall.utils.DateUtils;
-import com.fs.aiSipCall.vo.ApiCallRecordQueryVo;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.company.domain.CompanyAiWorkflowExec;
@@ -25,11 +24,8 @@ import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
 import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
 import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.mapper.EasyCallMapper;
-import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.CompanyWorkflowEngine;
-import com.fs.company.service.impl.call.node.AiCallTaskNode;
 import com.fs.company.vo.CidConfigVO;
-import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
@@ -40,8 +36,6 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.net.URLEncoder;
-import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -51,7 +45,7 @@ import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORK
 
 /**
  * aiSIP手动外呼通话记录Service业务层处理
- * 
+ *
  * @author fs
  * @date 2026-03-19
  */
@@ -91,7 +85,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
     }
     /**
      * 查询aiSIP手动外呼通话记录列表
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return aiSIP手动外呼通话记录
      */
@@ -128,7 +122,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 新增aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -140,7 +134,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 修改aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -152,7 +146,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 批量删除aiSIP手动外呼通话记录
-     * 
+     *
      * @param ids 需要删除的aiSIP手动外呼通话记录主键
      * @return 结果
      */
@@ -164,7 +158,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 删除aiSIP手动外呼通话记录信息
-     * 
+     *
      * @param id aiSIP手动外呼通话记录主键
      * @return 结果
      */

+ 13 - 12
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java

@@ -1,19 +1,20 @@
 package com.fs.aiSipCall.service.impl;
-import java.util.List;
-import java.util.Map;
 
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallUser;
+import com.fs.aiSipCall.mapper.AiSipCallUserMapper;
+import com.fs.aiSipCall.service.IAiSipCallUserService;
 import com.fs.common.core.domain.AjaxResult;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.company.mapper.CompanyUserMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import com.fs.aiSipCall.mapper.AiSipCallUserMapper;
-import com.fs.aiSipCall.domain.AiSipCallUser;
-import com.fs.aiSipCall.service.IAiSipCallUserService;
+
+import java.util.List;
+import java.util.Map;
 
 /**
  * sip用户信息Service业务层处理
@@ -30,7 +31,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 查询sip用户信息
-     * 
+     *
      * @param userId sip用户信息主键
      * @return sip用户信息
      */
@@ -42,7 +43,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 查询sip用户信息列表
-     * 
+     *
      * @param aiSipCallUser sip用户信息
      * @return sip用户信息
      */
@@ -54,7 +55,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 新增sip用户信息
-     * 
+     *
      * @param aiSipCallUser sip用户信息
      * @return 结果
      */
@@ -102,7 +103,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 修改sip用户信息
-     * 
+     *
      * @param aiSipCallUser sip用户信息
      * @return 结果
      */
@@ -126,7 +127,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 批量删除sip用户信息
-     * 
+     *
      * @param userIds 需要删除的sip用户信息主键
      * @return 结果
      */
@@ -138,7 +139,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 删除sip用户信息信息
-     * 
+     *
      * @param userId sip用户信息主键
      * @return 结果
      */

+ 4 - 3
fs-service/src/main/java/com/fs/app/service/impl/AppPayServiceImpl.java

@@ -118,15 +118,16 @@ public class AppPayServiceImpl implements AppPayService {
                 inquiryOrderService.payConfirm("", tradeNoArr[1],"","",1,result.getTransactionId(),"");
                 break;
             case "store":
+            case "live":
                 storeOrderService.payConfirm(1, null,tradeNoArr[1],"",result.getTransactionId(),"");
                 break;
                 // 表示app的处方订单
             case "storeOrder":
                 fsStoreOrderService.payConfirm("", tradeNoArr[1],"","",1,result.getTransactionId(),"");
                 break;
-            case "live":
-                liveOrderService.payConfirm(1, null,tradeNoArr[1],"",result.getTransactionId(),"");
-                break;
+//            case "live":
+//                liveOrderService.payConfirm(1, null,tradeNoArr[1],"",result.getTransactionId(),"");
+//                break;
             case "package":
                 packageOrderService.payConfirm("", tradeNoArr[1],"","",1,result.getTransactionId(),"");
                 break;

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

@@ -640,9 +640,17 @@ public class SmsServiceImpl implements ISmsService
         SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
         FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
         if (sms.getType().equals("rf")) {
+
             try {
                 content = content.replace("${sms.sign}",sms.getRfSign());
-                urls = sms.getRfUrl1() + "sms?action=send&account=" + sms.getRfAccount1() + "&password=" + sms.getRfPassword1() + "&mobile=" + phone + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode1() + "&rt=json";
+                if (temp.getTempType().equals(1)) {
+                    urls = sms.getRfUrl1() + "sms?action=send&account=" + sms.getRfAccount1() + "&password=" + sms.getRfPassword1() + "&mobile=" + phone + "&content=" + URLEncoder.encode(sms.getRfSign() + content, "UTF-8") + "&extno=" + sms.getRfCode1() + "&rt=json";
+                } else if (temp.getTempType().equals(2)) {
+                    urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + phone + "&content=" + URLEncoder.encode(sms.getRfSign() + content + "拒收请回复R", "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+                }
+                if(StringUtils.isEmpty(urls)){
+                    urls = sms.getRfUrl1() + "sms?action=send&account=" + sms.getRfAccount1() + "&password=" + sms.getRfPassword1() + "&mobile=" + phone + "&content=" + URLEncoder.encode(sms.getRfSign() + content, "UTF-8") + "&extno=" + sms.getRfCode1() + "&rt=json";
+                }
             } catch (UnsupportedEncodingException e) {
                 e.printStackTrace();
             }

+ 77 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopExecLog.java

@@ -0,0 +1,77 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI添加微信SOP执行日志对象 ai_addwx_sop_exec_log
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+public class AiAddwxSopExecLog implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 日志ID */
+    @TableId(type = IdType.AUTO)
+    private Long logId;
+
+    /** 个人工作流ID */
+    private Long userWorkflowId;
+
+    /** 个人节点ID */
+    private Long userNodeId;
+
+    /** 节点Key */
+    private String nodeKey;
+
+    /** 节点名称 */
+    private String nodeName;
+
+    /** 节点类型 */
+    private String nodeType;
+
+    /** 输入数据(JSON) */
+    private String inputData;
+
+    /** 输出数据(JSON) */
+    private String outputData;
+
+    /** 执行状态: 0-待执行, 1-执行中, 2-成功, 3-失败, 4-跳过 */
+    @Excel(name = "执行状态", readConverterExp = "0=待执行,1=执行中,2=成功,3=失败,4=跳过")
+    private Integer status;
+
+    /** 开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 执行时长(毫秒) */
+    private Long duration;
+
+    /** 错误信息 */
+    private String errorMessage;
+
+    /** 重试次数 */
+    private Integer retryCount;
+
+    /** 企业用户ID */
+    private Long companyUserId;
+
+    /** 企业ID */
+    private Long companyId;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 47 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTagTemplateBind.java

@@ -0,0 +1,47 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 企微标签与工作流模板绑定关系对象 ai_addwx_sop_tag_template_bind
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+public class AiAddwxSopTagTemplateBind implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 绑定ID */
+    @TableId(type = IdType.AUTO)
+    private Long bindId;
+
+    /** 企微标签ID */
+    private Long qwTagId;
+
+    /** 企微标签名称 */
+    private String qwTagName;
+
+    /** 模板ID */
+    private Long templateId;
+
+    /** 模板名称 */
+    private String templateName;
+
+    /** 优先级 0-100,评分越高优先级越高 */
+    private Integer priority;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 48 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTemplate.java

@@ -0,0 +1,48 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * AI添加微信SOP工作流模板对象 ai_addwx_sop_template
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiAddwxSopTemplate extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 模板ID */
+    @TableId(type = IdType.AUTO)
+    private Long templateId;
+
+    /** 模板名称 */
+    @Excel(name = "模板名称")
+    private String templateName;
+
+    /** 模板描述 */
+    @Excel(name = "模板描述")
+    private String templateDesc;
+
+    /** 模板状态 0禁用 1启用 */
+    @Excel(name = "模板状态", readConverterExp = "0=禁用,1=启用")
+    private Integer status;
+
+    /** 版本号 */
+    private Integer version;
+
+    /** 企业ID */
+    private Long companyId;
+
+    /** 删除标志 0正常 1删除 */
+    private Integer delFlag;
+}

+ 65 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTemplateNode.java

@@ -0,0 +1,65 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI添加微信SOP模板节点对象 ai_addwx_sop_template_node
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+public class AiAddwxSopTemplateNode implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 节点ID */
+    @TableId(type = IdType.AUTO)
+    private Long nodeId;
+
+    /** 模板ID */
+    private Long templateId;
+
+    /** 节点唯一标识 */
+    private String nodeKey;
+
+    /** 节点名称 */
+    private String nodeName;
+
+    /** 节点类型: receive-收货节点, medication-用药节点, effect_feedback-效果反馈节点, transfer_ai-转AI节点 */
+    private String nodeType;
+
+    /** 上一个节点ID */
+    private Long prevNodeId;
+
+    /** 下一个节点ID */
+    private Long nextNodeId;
+
+    /** 节点图标 */
+    private String nodeIcon;
+
+    /** 节点颜色 */
+    private String nodeColor;
+
+    /** 节点配置JSON(包含提示词、条件等配置) */
+    private String nodeConfig;
+
+    /** 是否有效回复 */
+    private Boolean isValidReply;
+
+    /** 排序 */
+    private Integer sortOrder;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 56 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopTemplateNodeRule.java

@@ -0,0 +1,56 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI添加微信SOP模板节点规则对象 ai_addwx_sop_template_node_rule
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+public class AiAddwxSopTemplateNodeRule implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 规则ID */
+    @TableId(type = IdType.AUTO)
+    private Long ruleId;
+
+    /** 节点ID */
+    private Long nodeId;
+
+    /** 规则名称(内部可见) */
+    private String ruleName;
+
+    /** 发送时间 HH:mm */
+    private String sendTime;
+
+    /** 第几天 */
+    private Integer sendDay;
+
+    /** 消息类别: 1-普通, 2-课程, 4-AI触达, 5-打标签, 20-直播间 */
+    private Integer msgType;
+
+    /** 内容类别 */
+    private String contentType;
+
+    /** 内容JSON */
+    private String contentJson;
+
+    /** 排序 */
+    private Integer sortOrder;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 93 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopUserNode.java

@@ -0,0 +1,93 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI添加微信SOP个人节点对象 ai_addwx_sop_user_node
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+public class AiAddwxSopUserNode implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 个人节点ID */
+    @TableId(type = IdType.AUTO)
+    private Long userNodeId;
+
+    /** 个人工作流ID */
+    private Long userWorkflowId;
+
+    /** 企微外部联系人ID */
+    private Long qwExternalId;
+
+    /** 企微用户ID */
+    private Long qwUserId;
+
+    /** 关联模板节点ID */
+    private Long templateNodeId;
+
+    /** 节点唯一标识 */
+    private String nodeKey;
+
+    /** 节点名称 */
+    private String nodeName;
+
+    /** 节点类型: receive-收货节点, medication-用药节点, effect_feedback-效果反馈节点, transfer_ai-转AI节点 */
+    private String nodeType;
+
+    /** 上一个节点ID */
+    private Long prevNodeId;
+
+    /** 下一个节点ID */
+    private Long nextNodeId;
+
+    /** 进入下一节点条件(JSON格式,如 {"type":"timeout","value":86400} 或 {"type":"manual"}) */
+    private String nextCondition;
+
+    /** 子节点ID列表(JSON数组,如 [101,102,103]) */
+    private String childNodes;
+
+    /** 提示词 */
+    private String prompt;
+
+    /** 节点配置JSON(扩展配置) */
+    private String nodeConfig;
+
+    /** 是否有效回复: 0-未有效回复, 1-已有效回复 */
+    private Integer isValidReply;
+
+    /** 节点状态: 0-待执行, 1-执行中, 2-已完成, 3-已跳过, 4-执行失败 */
+    private Integer status;
+
+    /** 排序 */
+    private Integer sortOrder;
+
+    /** 开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /** 子节点列表(非数据库字段) */
+    @TableField(exist = false)
+    private java.util.List<AiAddwxSopUserNode> childNodeList;
+}

+ 71 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopUserNodeRule.java

@@ -0,0 +1,71 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI添加微信SOP个人节点规则对象 ai_addwx_sop_user_node_rule
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+@Accessors(chain = true)
+public class AiAddwxSopUserNodeRule implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 个人节点规则ID */
+    @TableId(type = IdType.AUTO)
+    private Long userNodeRuleId;
+
+    /** 个人节点ID */
+    private Long userNodeId;
+
+    /** 企微外部联系人ID */
+    private Long qwExternalId;
+
+    /** 企微用户ID */
+    private Long qwUserId;
+
+    /** 规则名称(内部可见) */
+    private String ruleName;
+
+    /** 发送时间 HH:mm */
+    private String sendTime;
+
+    /** 第几天 */
+    private Integer sendDay;
+
+    /** 消息类别: 1-普通, 2-课程, 4-AI触达, 5-打标签, 20-直播间 */
+    private Integer msgType;
+
+    /** 内容类别 */
+    private String contentType;
+
+    /** 内容JSON */
+    private String contentJson;
+
+    /** 排序 */
+    private Integer sortOrder;
+
+    /** 是否发送 0-未发送 1-已发送 */
+    private Integer isSent;
+
+    /** 真实需要发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date realSendTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 95 - 0
fs-service/src/main/java/com/fs/company/domain/AiAddwxSopUserWorkflow.java

@@ -0,0 +1,95 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * AI添加微信SOP个人工作流对象 ai_addwx_sop_user_workflow
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+@Data
+@Accessors(chain = true)
+public class AiAddwxSopUserWorkflow  {
+    private static final long serialVersionUID = 1L;
+
+    /** 个人工作流ID */
+    @TableId(type = IdType.AUTO)
+    private Long userWorkflowId;
+
+    /** 关联模板ID */
+    private Long templateId;
+
+    /** 企业用户ID(一人一个) */
+    private Long companyUserId;
+
+    /** 企微外部联系人ID */
+    private Long qwExternalId;
+
+    /** 企微用户ID */
+    private Long qwUserId;
+
+    /** 企业ID */
+    private Long companyId;
+
+    /** 工作流状态: 0-未开始, 1-进行中, 2-已完成, 3-已终止 */
+    @Excel(name = "工作流状态", readConverterExp = "0=未开始,1=进行中,2=已完成,3=已终止")
+    private Integer status;
+
+    /** 当前节点ID */
+    private Long currentNodeId;
+
+    /** 当前节点Key */
+    private String currentNodeKey;
+
+    /** 当前节点类型 */
+    private String currentNodeType;
+
+//    /** 关联业务ID(如订单ID、客户ID等) */
+//    private Long businessKey;
+
+    /** 开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 删除标志 0正常 1删除 */
+    private Integer delFlag;
+
+    /** 创建者 */
+    @TableField(fill = FieldFill.INSERT)
+    private String createBy;
+
+    /** 创建时间 */
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /** 更新者 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /** 备注 */
+    @TableField(exist = false)
+    private String remark;
+}

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

@@ -104,6 +104,7 @@ public class CompanyVoiceRobotic {
     private String runTaskFlow;
     // 任务状态0待执行1执行中2执行中断3执行完成
     private Integer taskStatus;
+    private String pauseSource;
     private Integer addWxTime;
     /** 创建人 */
     @Excel(name = "创建时间")

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

@@ -3,8 +3,8 @@ package com.fs.company.domain;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 /**

+ 8 - 6
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java

@@ -1,18 +1,17 @@
 package com.fs.company.domain;
 
-import java.util.Date;
-import java.util.List;
-
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import com.fs.common.constant.Constants;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.util.Date;
+import java.util.List;
+
 /**
  * 调用日志_加微信对象 company_voice_robotic_call_log_addwx
  *
@@ -57,6 +56,9 @@ public class CompanyVoiceRoboticCallLogAddwx extends BaseEntity{
     @Excel(name = "公司id")
     private Long companyId;
 
+    @TableField(exist = false)
+    private Long companyUserId;
+
     /** 个微账号id */
     @Excel(name = "个微账号id")
     private Long wxAccountId;

+ 70 - 11
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java

@@ -1,19 +1,18 @@
 package com.fs.company.domain;
 
-import java.math.BigDecimal;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
-
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
 /**
  * 调用日志_ai打电话对象 company_voice_robotic_call_log_callphone
  *
@@ -106,9 +105,38 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @Excel(name = "外呼类型")
     private Integer callType;
 
-    /** 回调重试次数 */
-    @Excel(name = "回调重试次数")
-    private Integer retryCount;
+    /** 未拨通类型 */
+    @Excel(name = "未拨通类型")
+    private String hangupType;
+
+    /** 是否警告(0否 1是)用于敏感词 */
+    @Excel(name = "是否警告(0否 1是)用于敏感词")
+    private Integer isWarning;
+
+    /**
+     * 违规次数
+     * **/
+    private Integer violationNum;
+    /**
+     * 转人工接听:1:是,0或无:否
+     */
+    private Integer manualAnswered;
+    /**
+     * 是否被处理:0:未被处理,1:已处理
+     */
+    private Integer handleFlag;
+    /**
+     * 接听分机号码
+     */
+    private String answeredExtNum;
+    /**
+     * 人工接听时间
+     */
+    private Long manualAnsweredTime;
+    /**
+     * 人工接听时长
+     */
+    private Long manualAnsweredTimeLen;
 
     @TableField(exist = false)
     private String companyName;
@@ -122,6 +150,23 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private List<Long> callerIds;
 
+    @TableField(exist = false)
+    private Integer isConnected;
+
+    @TableField(exist = false)
+    private Long minCallTime;
+
+    @TableField(exist = false)
+    private Long maxCallTime;
+
+    /** 详情筛选-手机号(明文) */
+    @TableField(exist = false)
+    private String phone;
+
+    /** 详情筛选-加密手机号(匹配 callees.phone) */
+    @TableField(exist = false)
+    private String encryptedPhone;
+
     @TableField(exist = false)
     private String roboticName;
 
@@ -137,8 +182,22 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private Integer runningCount;
 
+    /** 详情筛选-运行时间起(yyyy-MM-dd) */
+    @TableField(exist = false)
+    private String beginRunTime;
+
+    /** 详情筛选-运行时间止(yyyy-MM-dd) */
     @TableField(exist = false)
-    private String successRate;
+    private String endRunTime;
+
+    /** 详情筛选-客户类型为「无」 */
+    @TableField(exist = false)
+    private Boolean intentionEmpty;
+
+    /**
+     * 重试次数
+     */
+    private Integer retryCount;
 
     public static CompanyVoiceRoboticCallLogCallphone initCallLog( String runParam, Long keyId, Long taskId,Long companyId) {
         CompanyVoiceRoboticCallLogCallphone log = new CompanyVoiceRoboticCallLogCallphone();

+ 6 - 6
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java

@@ -1,18 +1,18 @@
 package com.fs.company.domain;
 
-import java.math.BigDecimal;
-import java.util.Date;
-import java.util.List;
-
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
 /**
  * 调用日志_发送短信对象 company_voice_robotic_call_log_sendmsg
  *

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

@@ -7,7 +7,7 @@ import lombok.Data;
 
 /**
  * AI外呼任务加微方式对象 company_voice_robotic_wx
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */

+ 47 - 0
fs-service/src/main/java/com/fs/company/domain/OutboundLineLimitLog.java

@@ -0,0 +1,47 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("outbound_line_limit_log")
+public class OutboundLineLimitLog {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    private Long gatewayId;
+
+    /** 时间粒度(分钟): 30/60/120/360/1440 */
+    private Integer timeWindow;
+
+    /** 时间窗口起始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime windowStart;
+
+    /** 该窗口最大允许呼出次数 */
+    private Integer maxCalls;
+
+    /** 当前窗口已呼叫次数 */
+    private Integer currentCount;
+
+    /** 是否触发限制: 0否 1是 */
+    private Integer isLimited;
+
+    /** 触发时的调用参数JSON */
+    private String callParam;
+
+    /** 下一个可用时间点 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime nextAvailableTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/company/dto/OutboundLimitResultVO.java

@@ -0,0 +1,26 @@
+package com.fs.company.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class OutboundLimitResultVO {
+    private boolean passed;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date nextAvailableTime;
+
+    public static OutboundLimitResultVO pass() {
+        OutboundLimitResultVO r = new OutboundLimitResultVO();
+        r.passed = true;
+        return r;
+    }
+
+    public static OutboundLimitResultVO limited(Date nextAvailableTime) {
+        OutboundLimitResultVO r = new OutboundLimitResultVO();
+        r.passed = false;
+        r.nextAvailableTime = nextAvailableTime;
+        return r;
+    }
+}

+ 103 - 0
fs-service/src/main/java/com/fs/company/enums/AiAddWechatSopNodeEnum.java

@@ -0,0 +1,103 @@
+package com.fs.company.enums;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * AI添加微信SOP节点类型枚举
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public enum AiAddWechatSopNodeEnum {
+
+    /** 加微 */
+    ADD_WECHAT("1", "用户加微"),
+
+    /** 到货 */
+    ARRIVED("2", "到货"),
+
+    /** 未到货 */
+    NOT_ARRIVED("3", "未到货"),
+
+    /** 使用 */
+    USED("4", "使用"),
+
+    /** 未使用 */
+    NOT_USED("5", "未使用"),
+
+    /** 断药 */
+    DRUG_INTERRUPTION("6", "断药"),
+
+    /** 反馈结果1 */
+    FEEDBACK_1("7", "反馈结果1"),
+
+    /** 反馈结果2 */
+    FEEDBACK_2("8", "反馈结果2"),
+
+    /** 攻单/推荐转人工 */
+    TRANSFER_MANUAL("9", "攻单/推荐转人工"),
+
+    /** 成交 */
+    DEAL("10", "成交"),
+
+    /** 未成交 */
+    NOT_DEAL("11", "未成交");
+
+    private final String code;
+    private final String desc;
+
+    AiAddWechatSopNodeEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    /**
+     * 根据 code 获取枚举
+     */
+    public static AiAddWechatSopNodeEnum fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (AiAddWechatSopNodeEnum item : values()) {
+            if (item.code.equals(code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 校验是否为合法节点类型
+     */
+    public static boolean isValid(String code) {
+        return fromCode(code) != null;
+    }
+
+    /**
+     * 获取所有 code 列表
+     */
+    public static List<String> getCodes() {
+        return Arrays.stream(values())
+                .map(AiAddWechatSopNodeEnum::getCode)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 判断是否相等
+     */
+    public boolean equalsCode(String code) {
+        return this.code.equals(code);
+    }
+
+
+}

+ 31 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopExecLogMapper.java

@@ -0,0 +1,31 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopExecLog;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP执行日志Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopExecLogMapper extends BaseMapper<AiAddwxSopExecLog> {
+
+    AiAddwxSopExecLog selectAiAddwxSopExecLogById(Long logId);
+
+    List<AiAddwxSopExecLog> selectAiAddwxSopExecLogList(AiAddwxSopExecLog execLog);
+
+    List<AiAddwxSopExecLog> selectAiAddwxSopExecLogByWorkflowId(Long userWorkflowId);
+
+    List<AiAddwxSopExecLog> selectAiAddwxSopExecLogByNodeId(Long userNodeId);
+
+    int insertAiAddwxSopExecLog(AiAddwxSopExecLog execLog);
+
+    int updateAiAddwxSopExecLog(AiAddwxSopExecLog execLog);
+
+    int deleteAiAddwxSopExecLogById(Long logId);
+
+    int deleteAiAddwxSopExecLogByWorkflowId(Long userWorkflowId);
+}

+ 34 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTagTemplateBindMapper.java

@@ -0,0 +1,34 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopTagTemplateBind;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 企微标签与工作流模板绑定关系Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopTagTemplateBindMapper extends BaseMapper<AiAddwxSopTagTemplateBind> {
+
+    AiAddwxSopTagTemplateBind selectAiAddwxSopTagTemplateBindById(Long bindId);
+
+    AiAddwxSopTagTemplateBind selectAiAddwxSopTagTemplateBindByTagId(Long qwTagId);
+
+    List<AiAddwxSopTagTemplateBind> selectAiAddwxSopTagTemplateBindList(AiAddwxSopTagTemplateBind bind);
+
+    int insertAiAddwxSopTagTemplateBind(AiAddwxSopTagTemplateBind bind);
+
+    int updateAiAddwxSopTagTemplateBind(AiAddwxSopTagTemplateBind bind);
+
+    int deleteAiAddwxSopTagTemplateBindById(Long bindId);
+
+    int deleteAiAddwxSopTagTemplateBindByTagId(String qwTagId);
+
+    Long selectOneAiAddwxSopTagTemplateBindByTagIds(@Param("tagIds") List<Long> tagIds);
+
+    AiAddwxSopTagTemplateBind selectTagBindBytagIdAndTemplateId(AiAddwxSopTagTemplateBind bind);
+}

+ 27 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTemplateMapper.java

@@ -0,0 +1,27 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopTemplate;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP工作流模板Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopTemplateMapper extends BaseMapper<AiAddwxSopTemplate> {
+
+    AiAddwxSopTemplate selectAiAddwxSopTemplateById(Long templateId);
+
+    List<AiAddwxSopTemplate> selectAiAddwxSopTemplateList(AiAddwxSopTemplate template);
+
+    int insertAiAddwxSopTemplate(AiAddwxSopTemplate template);
+
+    int updateAiAddwxSopTemplate(AiAddwxSopTemplate template);
+
+    int deleteAiAddwxSopTemplateById(Long templateId);
+
+    int deleteAiAddwxSopTemplateByIds(Long[] templateIds);
+}

+ 32 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTemplateNodeMapper.java

@@ -0,0 +1,32 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopTemplateNode;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP模板节点Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopTemplateNodeMapper extends BaseMapper<AiAddwxSopTemplateNode> {
+
+    AiAddwxSopTemplateNode selectAiAddwxSopTemplateNodeById(Long nodeId);
+
+    List<AiAddwxSopTemplateNode> selectAiAddwxSopTemplateNodeByTemplateId(Long templateId);
+
+    List<AiAddwxSopTemplateNode> selectAiAddwxSopTemplateNodeList(AiAddwxSopTemplateNode node);
+
+    int insertAiAddwxSopTemplateNode(AiAddwxSopTemplateNode node);
+
+    int updateAiAddwxSopTemplateNode(AiAddwxSopTemplateNode node);
+
+    int deleteAiAddwxSopTemplateNodeById(Long nodeId);
+
+    int deleteAiAddwxSopTemplateNodeByTemplateId(Long templateId);
+
+    int batchInsertAiAddwxSopTemplateNode(@Param("list") List<AiAddwxSopTemplateNode> list);
+}

+ 32 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopTemplateNodeRuleMapper.java

@@ -0,0 +1,32 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopTemplateNodeRule;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP模板节点规则Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopTemplateNodeRuleMapper extends BaseMapper<AiAddwxSopTemplateNodeRule> {
+
+    AiAddwxSopTemplateNodeRule selectAiAddwxSopTemplateNodeRuleById(Long ruleId);
+
+    List<AiAddwxSopTemplateNodeRule> selectAiAddwxSopTemplateNodeRuleByNodeId(Long nodeId);
+
+    List<AiAddwxSopTemplateNodeRule> selectAiAddwxSopTemplateNodeRuleList(AiAddwxSopTemplateNodeRule rule);
+
+    int insertAiAddwxSopTemplateNodeRule(AiAddwxSopTemplateNodeRule rule);
+
+    int updateAiAddwxSopTemplateNodeRule(AiAddwxSopTemplateNodeRule rule);
+
+    int deleteAiAddwxSopTemplateNodeRuleById(Long ruleId);
+
+    int deleteAiAddwxSopTemplateNodeRuleByNodeId(Long nodeId);
+
+    int batchInsertAiAddwxSopTemplateNodeRule(@Param("list") List<AiAddwxSopTemplateNodeRule> list);
+}

+ 32 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopUserNodeMapper.java

@@ -0,0 +1,32 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopUserNode;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP个人节点Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopUserNodeMapper extends BaseMapper<AiAddwxSopUserNode> {
+
+    AiAddwxSopUserNode selectAiAddwxSopUserNodeById(Long userNodeId);
+
+    List<AiAddwxSopUserNode> selectAiAddwxSopUserNodeByWorkflowId(Long userWorkflowId);
+
+    List<AiAddwxSopUserNode> selectAiAddwxSopUserNodeList(AiAddwxSopUserNode userNode);
+
+    int insertAiAddwxSopUserNode(AiAddwxSopUserNode userNode);
+
+    int updateAiAddwxSopUserNode(AiAddwxSopUserNode userNode);
+
+    int deleteAiAddwxSopUserNodeById(Long userNodeId);
+
+    int deleteAiAddwxSopUserNodeByWorkflowId(Long userWorkflowId);
+
+    int batchInsertAiAddwxSopUserNode(@Param("list") List<AiAddwxSopUserNode> list);
+}

+ 32 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopUserNodeRuleMapper.java

@@ -0,0 +1,32 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopUserNodeRule;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP个人节点规则Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopUserNodeRuleMapper extends BaseMapper<AiAddwxSopUserNodeRule> {
+
+    AiAddwxSopUserNodeRule selectAiAddwxSopUserNodeRuleById(Long userNodeRuleId);
+
+    List<AiAddwxSopUserNodeRule> selectAiAddwxSopUserNodeRuleByUserNodeId(Long userNodeId);
+
+    List<AiAddwxSopUserNodeRule> selectAiAddwxSopUserNodeRuleList(AiAddwxSopUserNodeRule rule);
+
+    int insertAiAddwxSopUserNodeRule(AiAddwxSopUserNodeRule rule);
+
+    int updateAiAddwxSopUserNodeRule(AiAddwxSopUserNodeRule rule);
+
+    int deleteAiAddwxSopUserNodeRuleById(Long userNodeRuleId);
+
+    int deleteAiAddwxSopUserNodeRuleByUserNodeId(Long userNodeId);
+
+    int batchInsertAiAddwxSopUserNodeRule(@Param("list") List<AiAddwxSopUserNodeRule> list);
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/mapper/AiAddwxSopUserWorkflowMapper.java

@@ -0,0 +1,28 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiAddwxSopUserWorkflow;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP个人工作流Mapper接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface AiAddwxSopUserWorkflowMapper extends BaseMapper<AiAddwxSopUserWorkflow> {
+
+    AiAddwxSopUserWorkflow selectAiAddwxSopUserWorkflowById(Long userWorkflowId);
+
+    List<AiAddwxSopUserWorkflow> selectAiAddwxSopUserWorkflowList(AiAddwxSopUserWorkflow userWorkflow);
+
+    int insertAiAddwxSopUserWorkflow(AiAddwxSopUserWorkflow userWorkflow);
+
+    int updateAiAddwxSopUserWorkflow(AiAddwxSopUserWorkflow userWorkflow);
+
+    int deleteAiAddwxSopUserWorkflowById(Long userWorkflowId);
+
+    int deleteAiAddwxSopUserWorkflowByIds(@Param("userWorkflowIds") Long[] userWorkflowIds);
+}

+ 36 - 8
fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java

@@ -11,14 +11,14 @@ import java.util.List;
 
 /**
  * AI外呼工作流执行Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-01-28
  */
 public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflowExec>{
     /**
      * 查询AI外呼工作流执行
-     * 
+     *
      * @param id AI外呼工作流执行主键
      * @return AI外呼工作流执行
      */
@@ -26,7 +26,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 查询AI外呼工作流执行列表
-     * 
+     *
      * @param companyAiWorkflowExec AI外呼工作流执行
      * @return AI外呼工作流执行集合
      */
@@ -34,7 +34,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 新增AI外呼工作流执行
-     * 
+     *
      * @param companyAiWorkflowExec AI外呼工作流执行
      * @return 结果
      */
@@ -42,7 +42,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 修改AI外呼工作流执行
-     * 
+     *
      * @param companyAiWorkflowExec AI外呼工作流执行
      * @return 结果
      */
@@ -52,7 +52,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 删除AI外呼工作流执行
-     * 
+     *
      * @param id AI外呼工作流执行主键
      * @return 结果
      */
@@ -60,7 +60,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 批量删除AI外呼工作流执行
-     * 
+     *
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
@@ -106,14 +106,42 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
      * **/
     int insertBatchInfo(@Param("list") List<CompanyAiWorkflowExec> list);
 //    List<WorkflowExecRecordVo> selectExecRecordsByRoboticId(@Param("roboticId") Long roboticId);
+
     List<WorkflowExecRecordVo> selectExecRecordsByRoboticId(
             @Param("roboticId") Long roboticId,
             @Param("customerName") String customerName,
             @Param("customerPhone") String customerPhone,
-            @Param(("onlyCallNode")) Boolean onlyCallNode
+            @Param(("onlyCallNode")) Boolean onlyCallNode,
+            @Param("encryptPhone") String encryptPhone
     );
 
     WxContact selectWxContectByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
 
     Long selectWxClientIdByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
+
+    /**
+     * 原子认领待执行任务:仅当当前状态等于 expectStatus 时,才将状态更新为 targetStatus。
+     * 用于防止定时扫描在上一轮任务尚未完成时重复调度同一条记录。
+     * @param id 执行记录主键
+     * @param expectStatus 期望的原状态(如 READY=9)
+     * @param targetStatus 认领后的目标状态(如 RUNNING=3)
+     * @return 影响行数,1 表示认领成功,0 表示已被其它线程/上一轮认领
+     */
+    int claimExecForRun(@Param("id") Long id,
+                        @Param("expectStatus") Integer expectStatus,
+                        @Param("targetStatus") Integer targetStatus);
+
+    /**
+     * 回扫超时认领任务:将处于 runningStatus 且 last_update_time 早于 timeoutTime 的记录重置为 failStatus,
+     * 防止进程重启 / 线程池拒绝导致任务永久卡在认领态。
+     * @param groupNo cid 分组号
+     * @param runningStatus 认领态(如 RUNNING=3)
+     * @param failStatus 重置目标态(如 FAILURE=2)
+     * @param timeoutTime 超时时间界限,last_update_time 早于该时间的视为卡死
+     * @return 影响行数
+     */
+    int resetTimeoutRunningExec(@Param("groupNo") Integer groupNo,
+                                @Param("runningStatus") Integer runningStatus,
+                                @Param("failStatus") Integer failStatus,
+                                @Param("timeoutTime") java.time.LocalDateTime timeoutTime);
 }

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

@@ -26,8 +26,17 @@ public interface CompanyVoiceCloneRefMapper {
     List<CcTtsAliyun> selectCcTtsAliyunList();
 
 
+    /**
+     * 分页查询公司音色关联列表
+     * @param ref 查询条件(companyId必填,companyUserId可选用于子账号过滤)
+     * @return 音色关联列表
+     */
+    List<CompanyVoiceCloneRef> selectCompanyVoiceCloneRefList(CompanyVoiceCloneRef ref);
 
+    int deleteById(@Param("id") Long id);
 
+    int updateStatusById(CompanyVoiceCloneRef ref);
 
 
-}
+
+}

+ 17 - 5
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -4,10 +4,7 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
-import com.fs.company.vo.CalleeRoboticCallOutCountVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
-import com.fs.company.vo.CustomerRoboticCallOutCountVO;
+import com.fs.company.vo.*;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -83,11 +80,18 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount(@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
 
 
     List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
+    List<CompanyVoiceRoboticCallLogCallPhoneVO> selectDetailList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    CompanyVoiceRoboticCallLogDetailSummary selectDetailSummary(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    List<CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO> listDecryptPhoneExport(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+
     /**
      * 根据业务ID查询公司ID
      * @param businessId 业务ID (bes.id)
@@ -95,6 +99,9 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      */
     Long selectCompanyIdByBusinessId(@Param("businessId") Long businessId);
 
+    List<CompanyVoiceRoboticCallLogCallphone> selectManualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+
     /**
      * 按客户统计 AI 外呼呼出次数(callphone.caller_id = callees.id,callees.user_id = 客户 customer_id)
      */
@@ -117,4 +124,9 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      * 更新重试次数和状态
      */
     int updateRetryCountAndStatus(CompanyVoiceRoboticCallLogCallphone record);
+
+    /**
+     * 查询执行中任务下、外呼记录仍为执行中的 callbackUuid 列表
+     */
+    List<String> selectRunningCallbackUuidsByCompanyId(@Param("companyId") Long companyId);
 }

+ 9 - 8
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.java

@@ -5,17 +5,18 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 调用日志_发送短信Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
 public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<CompanyVoiceRoboticCallLogSendmsg>{
     /**
      * 查询调用日志_发送短信
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 调用日志_发送短信
      */
@@ -23,7 +24,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 查询调用日志_发送短信列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 调用日志_发送短信集合
      */
@@ -33,7 +34,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 新增调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -41,7 +42,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 修改调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -49,7 +50,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 删除调用日志_发送短信
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 结果
      */
@@ -57,7 +58,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 批量删除调用日志_发送短信
-     * 
+     *
      * @param logIds 需要删除的数据主键集合
      * @return 结果
      */
@@ -65,5 +66,5 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount(@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
 }

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

@@ -8,6 +8,8 @@ import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 import org.apache.ibatis.annotations.Param;
 import org.springframework.stereotype.Repository;
 
+import java.util.List;
+
 /**
  * @author MixLiu
  * @date 2026/3/7 10:43
@@ -29,4 +31,9 @@ public interface EasyCallMapper {
     @DataSource(DataSourceType.EASYCALL)
     EasyCallCallPhoneVO getCallPhoneInfoByCallBackUuid(@Param("callBackUuid") String callBackUuid);
 
+    /**
+     * 根据 callbackUuid 批量查询 EasyCall 已外呼完成的话单 uuid
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<String> selectCompletedUuidsByCallbackUuids(@Param("callbackUuids") List<String> callbackUuids);
 }

+ 18 - 0
fs-service/src/main/java/com/fs/company/mapper/OutboundLineLimitLogMapper.java

@@ -0,0 +1,18 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.OutboundLineLimitLog;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+
+public interface OutboundLineLimitLogMapper extends BaseMapper<OutboundLineLimitLog> {
+
+    int asyncInsert(OutboundLineLimitLog log);
+
+    int countByWindow(@Param("companyId") Long companyId,
+                      @Param("gatewayId") Long gatewayId,
+                      @Param("timeWindow") Integer timeWindow,
+                      @Param("windowStart") LocalDateTime windowStart,
+                      @Param("windowEnd") LocalDateTime windowEnd);
+}

+ 25 - 0
fs-service/src/main/java/com/fs/company/param/AppendCustomersParam.java

@@ -0,0 +1,25 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2026/5/26
+ * @description 追加客户到运行中任务的请求参数
+ */
+@Data
+public class AppendCustomersParam {
+
+    /**
+     * 任务id
+     */
+    private Long taskId;
+
+    /**
+     * 要追加的CRM客户ID列表
+     */
+    private List<Long> customerIds;
+
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/param/InitAddWxSopTestParam.java

@@ -0,0 +1,14 @@
+package com.fs.company.param;
+
+import com.fs.qw.domain.QwExternalContact;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@Accessors
+public class InitAddWxSopTestParam {
+    private List<QwExternalContact> contacts;
+    private List<String> tagIds;
+}

+ 5 - 3
fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java

@@ -70,9 +70,11 @@ public interface CompanyWorkflowEngine {
      * @param workFlowId
      */
     Long createSipTask(Long roboticId,Long workFlowId);
+
     /**
-     * 添加微信成功
-     * @param object
+     * 更新exec记录的variables字段(用于暂停时同步延时上下文到DB)
+     * @param workflowInstanceId
+     * @param variables 最新的variables map
      */
-    void addWxSuccess(JSONObject object);
+    void updateExecVariables(String workflowInstanceId, Map<String, Object> variables);
 }

+ 30 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopExecLogService.java

@@ -0,0 +1,30 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopExecLog;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP执行日志Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopExecLogService {
+
+    AiAddwxSopExecLog selectAiAddwxSopExecLogById(Long logId);
+
+    List<AiAddwxSopExecLog> selectAiAddwxSopExecLogList(AiAddwxSopExecLog execLog);
+
+    List<AiAddwxSopExecLog> selectAiAddwxSopExecLogByWorkflowId(Long userWorkflowId);
+
+    List<AiAddwxSopExecLog> selectAiAddwxSopExecLogByNodeId(Long userNodeId);
+
+    int insertAiAddwxSopExecLog(AiAddwxSopExecLog execLog);
+
+    int updateAiAddwxSopExecLog(AiAddwxSopExecLog execLog);
+
+    int deleteAiAddwxSopExecLogById(Long logId);
+
+    int deleteAiAddwxSopExecLogByWorkflowId(Long userWorkflowId);
+}

+ 30 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTagTemplateBindService.java

@@ -0,0 +1,30 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopTagTemplateBind;
+
+import java.util.List;
+
+/**
+ * 企微标签与工作流模板绑定关系Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopTagTemplateBindService {
+
+    AiAddwxSopTagTemplateBind selectAiAddwxSopTagTemplateBindById(Long bindId);
+
+    AiAddwxSopTagTemplateBind selectAiAddwxSopTagTemplateBindByTagId(Long qwTagId);
+
+    List<AiAddwxSopTagTemplateBind> selectAiAddwxSopTagTemplateBindList(AiAddwxSopTagTemplateBind bind);
+
+    int insertAiAddwxSopTagTemplateBind(AiAddwxSopTagTemplateBind bind);
+
+    int updateAiAddwxSopTagTemplateBind(AiAddwxSopTagTemplateBind bind);
+
+    int deleteAiAddwxSopTagTemplateBindById(Long bindId);
+
+    int deleteAiAddwxSopTagTemplateBindByTagId(String qwTagId);
+
+    AiAddwxSopTagTemplateBind selectTagBindBytagIdAndTemplateId(AiAddwxSopTagTemplateBind bind);
+}

+ 30 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTemplateNodeRuleService.java

@@ -0,0 +1,30 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopTemplateNodeRule;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP模板节点规则Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopTemplateNodeRuleService {
+
+    AiAddwxSopTemplateNodeRule selectAiAddwxSopTemplateNodeRuleById(Long ruleId);
+
+    List<AiAddwxSopTemplateNodeRule> selectAiAddwxSopTemplateNodeRuleByNodeId(Long nodeId);
+
+    List<AiAddwxSopTemplateNodeRule> selectAiAddwxSopTemplateNodeRuleList(AiAddwxSopTemplateNodeRule rule);
+
+    int insertAiAddwxSopTemplateNodeRule(AiAddwxSopTemplateNodeRule rule);
+
+    int updateAiAddwxSopTemplateNodeRule(AiAddwxSopTemplateNodeRule rule);
+
+    int deleteAiAddwxSopTemplateNodeRuleById(Long ruleId);
+
+    int deleteAiAddwxSopTemplateNodeRuleByNodeId(Long nodeId);
+
+    int batchInsertAiAddwxSopTemplateNodeRule(List<AiAddwxSopTemplateNodeRule> list);
+}

+ 30 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTemplateNodeService.java

@@ -0,0 +1,30 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopTemplateNode;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP模板节点Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopTemplateNodeService {
+
+    AiAddwxSopTemplateNode selectAiAddwxSopTemplateNodeById(Long nodeId);
+
+    List<AiAddwxSopTemplateNode> selectAiAddwxSopTemplateNodeByTemplateId(Long templateId);
+
+    List<AiAddwxSopTemplateNode> selectAiAddwxSopTemplateNodeList(AiAddwxSopTemplateNode node);
+
+    int insertAiAddwxSopTemplateNode(AiAddwxSopTemplateNode node);
+
+    int updateAiAddwxSopTemplateNode(AiAddwxSopTemplateNode node);
+
+    int deleteAiAddwxSopTemplateNodeById(Long nodeId);
+
+    int deleteAiAddwxSopTemplateNodeByTemplateId(Long templateId);
+
+    int batchInsertAiAddwxSopTemplateNode(List<AiAddwxSopTemplateNode> list);
+}

+ 34 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopTemplateService.java

@@ -0,0 +1,34 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopTemplate;
+import com.fs.company.domain.AiAddwxSopTemplateNode;
+import com.fs.qw.domain.QwExternalContact;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP模板Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopTemplateService {
+
+    AiAddwxSopTemplate selectAiAddwxSopTemplateById(Long templateId);
+
+    List<AiAddwxSopTemplate> selectAiAddwxSopTemplateList(AiAddwxSopTemplate template);
+
+    Long insertAiAddwxSopTemplate(AiAddwxSopTemplate template);
+
+    int updateAiAddwxSopTemplate(AiAddwxSopTemplate template);
+
+    int deleteAiAddwxSopTemplateByIds(Long[] templateIds);
+
+    /** 查询模板详情(含节点列表) */
+    AiAddwxSopTemplate selectTemplateDetailById(Long templateId);
+
+    /** 保存模板及节点 */
+    Long saveTemplateWithNodes(AiAddwxSopTemplate template, List<AiAddwxSopTemplateNode> nodes);
+
+    void initAddWxSop(List<QwExternalContact> externalUsers, List<String> tagIds);
+}

+ 30 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopUserNodeRuleService.java

@@ -0,0 +1,30 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopUserNodeRule;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP个人节点规则Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopUserNodeRuleService {
+
+    AiAddwxSopUserNodeRule selectAiAddwxSopUserNodeRuleById(Long userNodeRuleId);
+
+    List<AiAddwxSopUserNodeRule> selectAiAddwxSopUserNodeRuleByUserNodeId(Long userNodeId);
+
+    List<AiAddwxSopUserNodeRule> selectAiAddwxSopUserNodeRuleList(AiAddwxSopUserNodeRule rule);
+
+    int insertAiAddwxSopUserNodeRule(AiAddwxSopUserNodeRule rule);
+
+    int updateAiAddwxSopUserNodeRule(AiAddwxSopUserNodeRule rule);
+
+    int deleteAiAddwxSopUserNodeRuleById(Long userNodeRuleId);
+
+    int deleteAiAddwxSopUserNodeRuleByUserNodeId(Long userNodeId);
+
+    int batchInsertAiAddwxSopUserNodeRule(List<AiAddwxSopUserNodeRule> list);
+}

+ 37 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopUserNodeService.java

@@ -0,0 +1,37 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopUserNode;
+import com.fs.company.domain.AiAddwxSopUserWorkflow;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI添加微信SOP个人节点Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopUserNodeService {
+
+    AiAddwxSopUserNode selectAiAddwxSopUserNodeById(Long userNodeId);
+
+    List<AiAddwxSopUserNode> selectAiAddwxSopUserNodeByWorkflowId(Long userWorkflowId);
+
+    List<AiAddwxSopUserNode> selectAiAddwxSopUserNodeList(AiAddwxSopUserNode userNode);
+
+    int insertAiAddwxSopUserNode(AiAddwxSopUserNode userNode);
+
+    int updateAiAddwxSopUserNode(AiAddwxSopUserNode userNode);
+
+    int deleteAiAddwxSopUserNodeById(Long userNodeId);
+
+    int deleteAiAddwxSopUserNodeByWorkflowId(Long userWorkflowId);
+
+    int batchInsertAiAddwxSopUserNode(List<AiAddwxSopUserNode> list);
+
+    /** 更新节点状态 */
+    int updateNodeStatus(Long userNodeId, Integer status);
+
+    void nextNode(Map.Entry<String, List<AiAddwxSopUserWorkflow>> next);
+}

+ 39 - 0
fs-service/src/main/java/com/fs/company/service/IAiAddwxSopUserWorkflowService.java

@@ -0,0 +1,39 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.AiAddwxSopUserWorkflow;
+import com.fs.company.domain.AiAddwxSopUserNode;
+
+import java.util.List;
+
+/**
+ * AI添加微信SOP个人工作流Service接口
+ *
+ * @author fs
+ * @date 2026-06-24
+ */
+public interface IAiAddwxSopUserWorkflowService {
+
+    AiAddwxSopUserWorkflow selectAiAddwxSopUserWorkflowById(Long userWorkflowId);
+
+    List<AiAddwxSopUserWorkflow> selectAiAddwxSopUserWorkflowList(AiAddwxSopUserWorkflow userWorkflow);
+
+    Long insertAiAddwxSopUserWorkflow(AiAddwxSopUserWorkflow userWorkflow);
+
+    int updateAiAddwxSopUserWorkflow(AiAddwxSopUserWorkflow userWorkflow);
+
+    int deleteAiAddwxSopUserWorkflowByIds(Long[] userWorkflowIds);
+
+    /** 查询个人工作流详情(含节点列表) */
+    AiAddwxSopUserWorkflow selectUserWorkflowDetailById(Long userWorkflowId);
+
+    /** 根据模板创建个人工作流(含节点) */
+    Long createUserWorkflowFromTemplate(Long templateId, Long companyUserId, Long companyId
+//            , Long businessKey
+    );
+
+    /** 推进工作流到下一节点 */
+    int advanceToNextNode(Long userWorkflowId, Long currentNodeId);
+
+    /** 查询用户的工作流节点列表 */
+    List<AiAddwxSopUserNode> selectUserNodesByWorkflowId(Long userWorkflowId);
+}

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

@@ -0,0 +1,15 @@
+package com.fs.company.service;
+
+/**
+ * AI外呼数据同步服务
+ */
+public interface ICompanyAiCallDataSyncService {
+
+    /**
+     * 尝试启动异步同步任务(同公司防重复)
+     *
+     * @param companyId 公司ID
+     * @return true-已启动;false-已有任务在运行
+     */
+    boolean tryStartSync(Long companyId);
+}

+ 9 - 8
fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java

@@ -1,19 +1,20 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyInboundBind;
 
+import java.util.List;
+
 /**
  * 呼入线路模型绑定关系Service接口
- * 
+ *
  * @author fs
  * @date 2026-04-27
  */
 public interface ICompanyInboundBindService extends IService<CompanyInboundBind>{
     /**
      * 查询呼入线路模型绑定关系
-     * 
+     *
      * @param id 呼入线路模型绑定关系主键
      * @return 呼入线路模型绑定关系
      */
@@ -21,7 +22,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 查询呼入线路模型绑定关系列表
-     * 
+     *
      * @param companyInboundBind 呼入线路模型绑定关系
      * @return 呼入线路模型绑定关系集合
      */
@@ -29,7 +30,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 新增呼入线路模型绑定关系
-     * 
+     *
      * @param companyInboundBind 呼入线路模型绑定关系
      * @return 结果
      */
@@ -37,7 +38,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 修改呼入线路模型绑定关系
-     * 
+     *
      * @param companyInboundBind 呼入线路模型绑定关系
      * @return 结果
      */
@@ -45,7 +46,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 批量删除呼入线路模型绑定关系
-     * 
+     *
      * @param ids 需要删除的呼入线路模型绑定关系主键集合
      * @return 结果
      */
@@ -53,7 +54,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 删除呼入线路模型绑定关系信息
-     * 
+     *
      * @param id 呼入线路模型绑定关系主键
      * @return 结果
      */

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

@@ -75,4 +75,6 @@ public interface ICompanyInboundCallManageService {
      * @return 通话记录列表
      */
     List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo);
+
+    Boolean checkCalleeInboundLlm(String callee);
 }

+ 10 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceApiService.java

@@ -1,19 +1,20 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceApi;
 
+import java.util.List;
+
 /**
  * 呼叫接口Service接口
- * 
+ *
  * @author fs
  * @date 2021-10-04
  */
-public interface ICompanyVoiceApiService 
+public interface ICompanyVoiceApiService
 {
     /**
      * 查询呼叫接口
-     * 
+     *
      * @param apiId 呼叫接口ID
      * @return 呼叫接口
      */
@@ -21,7 +22,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 查询呼叫接口列表
-     * 
+     *
      * @param companyVoiceApi 呼叫接口
      * @return 呼叫接口集合
      */
@@ -29,7 +30,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 新增呼叫接口
-     * 
+     *
      * @param companyVoiceApi 呼叫接口
      * @return 结果
      */
@@ -37,7 +38,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 修改呼叫接口
-     * 
+     *
      * @param companyVoiceApi 呼叫接口
      * @return 结果
      */
@@ -45,7 +46,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 批量删除呼叫接口
-     * 
+     *
      * @param apiIds 需要删除的呼叫接口ID
      * @return 结果
      */
@@ -53,7 +54,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 删除呼叫接口信息
-     * 
+     *
      * @param apiId 呼叫接口ID
      * @return 结果
      */

+ 10 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceBlacklistService.java

@@ -1,19 +1,20 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceBlacklist;
 
+import java.util.List;
+
 /**
  * 黑名单Service接口
- * 
+ *
  * @author fs
  * @date 2023-02-23
  */
-public interface ICompanyVoiceBlacklistService 
+public interface ICompanyVoiceBlacklistService
 {
     /**
      * 查询黑名单
-     * 
+     *
      * @param blacklistId 黑名单ID
      * @return 黑名单
      */
@@ -21,7 +22,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 查询黑名单列表
-     * 
+     *
      * @param companyVoiceBlacklist 黑名单
      * @return 黑名单集合
      */
@@ -29,7 +30,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 新增黑名单
-     * 
+     *
      * @param companyVoiceBlacklist 黑名单
      * @return 结果
      */
@@ -37,7 +38,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 修改黑名单
-     * 
+     *
      * @param companyVoiceBlacklist 黑名单
      * @return 结果
      */
@@ -45,7 +46,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 批量删除黑名单
-     * 
+     *
      * @param blacklistIds 需要删除的黑名单ID
      * @return 结果
      */
@@ -53,7 +54,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 删除黑名单信息
-     * 
+     *
      * @param blacklistId 黑名单ID
      * @return 结果
      */

+ 2 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCallerService.java

@@ -1,11 +1,12 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceCaller;
 import com.fs.company.param.CompanyVoiceCallerImportParam;
 import com.fs.company.param.CompanyVoiceCallerParam;
 import com.fs.company.vo.CompanyVoiceCallerListVO;
 
+import java.util.List;
+
 /**
  * 企业用户坐席Service接口
  *

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

@@ -1,8 +1,6 @@
 package com.fs.company.service;
 
-import com.fs.common.core.domain.AjaxResult;
 import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
-import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 

+ 27 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneService.java

@@ -1,8 +1,11 @@
 package com.fs.company.service;
 
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyVoiceCloneRef;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.List;
+
 /**
  * 豆包声音克隆 Service 接口
  *
@@ -33,4 +36,28 @@ public interface ICompanyVoiceCloneService {
      * @return 操作结果(含 base64 音频数据)
      */
     AjaxResult doubaoTtsTest(String speakerId, Integer language, String text);
+
+    /**
+     * 分页查询音色关联列表
+     *
+     * @param ref 查询条件(companyId、companyUserId、voiceName、status)
+     * @return 音色关联列表
+     */
+    List<CompanyVoiceCloneRef> selectRefList(CompanyVoiceCloneRef ref);
+
+    /**
+     * 根据ID删除音色关联
+     *
+     * @param id 主键ID
+     * @return 删除行数
+     */
+    int deleteRefById(Long id);
+
+    /**
+     * 修改音色关联启用/禁用状态
+     *
+     * @param ref 包含 id 和 status
+     * @return 更新行数
+     */
+    int changeStatus(CompanyVoiceCloneRef ref);
 }

+ 10 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceConfigService.java

@@ -1,20 +1,21 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceConfig;
 import com.fs.company.vo.CompanyVoiceConfigListVO;
 
+import java.util.List;
+
 /**
  * 呼叫频率配置Service接口
- * 
+ *
  * @author fs
  * @date 2021-11-13
  */
-public interface ICompanyVoiceConfigService 
+public interface ICompanyVoiceConfigService
 {
     /**
      * 查询呼叫频率配置
-     * 
+     *
      * @param configId 呼叫频率配置ID
      * @return 呼叫频率配置
      */
@@ -22,7 +23,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 查询呼叫频率配置列表
-     * 
+     *
      * @param companyVoiceConfig 呼叫频率配置
      * @return 呼叫频率配置集合
      */
@@ -30,7 +31,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 新增呼叫频率配置
-     * 
+     *
      * @param companyVoiceConfig 呼叫频率配置
      * @return 结果
      */
@@ -38,7 +39,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 修改呼叫频率配置
-     * 
+     *
      * @param companyVoiceConfig 呼叫频率配置
      * @return 结果
      */
@@ -46,7 +47,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 批量删除呼叫频率配置
-     * 
+     *
      * @param configIds 需要删除的呼叫频率配置ID
      * @return 结果
      */
@@ -54,7 +55,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 删除呼叫频率配置信息
-     * 
+     *
      * @param configId 呼叫频率配置ID
      * @return 结果
      */

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

@@ -1,8 +1,5 @@
 package com.fs.company.service;
 
-import java.util.List;
-import java.util.Map;
-
 import com.alibaba.fastjson.JSONObject;
 import com.fs.company.domain.CompanyVoiceLogs;
 import com.fs.company.param.CompanyStatisticsParam;
@@ -13,6 +10,9 @@ import com.fs.company.vo.CompanyVoiceLogsListVO;
 import com.fs.company.vo.CompanyVoiceLogsStatisticsVO;
 import com.fs.company.vo.CompanyVoiceLogsVO;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * 通话记录Service接口
  *

Неке датотеке нису приказане због велике количине промена