Ver código fonte

Merge remote-tracking branch 'origin/master'

yjwang 1 mês atrás
pai
commit
423c37ce06
37 arquivos alterados com 743 adições e 123 exclusões
  1. 1 1
      fs-admin/src/main/java/com/fs/web/controller/common/CommonController.java
  2. 9 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  3. 1 0
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  4. 7 3
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  5. 2 0
      fs-service/src/main/java/com/fs/aicall/domain/CcTtsAliyun.java
  6. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java
  7. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxClientMapper.java
  8. 26 0
      fs-service/src/main/java/com/fs/company/param/PauseRoboticActiveParam.java
  9. 1 0
      fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java
  10. 4 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  11. 1 1
      fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java
  12. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/AsyncCalleeProcessorServiceImpl.java
  13. 25 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java
  14. 23 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  15. 2 2
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  16. 37 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  17. 5 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  18. 2 2
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java
  19. 105 6
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  20. 9 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  21. 2 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  22. 3 1
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  23. 160 80
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  24. 7 0
      fs-service/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java
  25. 33 0
      fs-service/src/main/java/com/fs/sop/vo/QwSopTempRulesWithDayVO.java
  26. 9 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java
  27. 27 0
      fs-service/src/main/java/com/fs/wx/sop/vo/WxSopUserMsgGenVO.java
  28. 1 0
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecMapper.xml
  29. 1 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceCloneRefMapper.xml
  30. 10 4
      fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml
  31. 0 1
      fs-service/src/main/resources/mapper/company/EasyCallMapper.xml
  32. 33 0
      fs-service/src/main/resources/mapper/sop/QwSopTempRulesMapper.xml
  33. 22 0
      fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml
  34. 7 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseTransferController.java
  35. 7 0
      fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java
  36. 134 12
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java
  37. 22 2
      fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

+ 1 - 1
fs-admin/src/main/java/com/fs/web/controller/common/CommonController.java

@@ -6,7 +6,7 @@ import javax.servlet.http.HttpServletResponse;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.file.OssException;
-import com.fs.course.dto.BatchSendCourseAllDTO;;
+import com.fs.course.dto.BatchSendCourseAllDTO;
 import com.fs.course.service.ITencentCloudCosService;
 import com.fs.framework.config.ServerConfig;
 import com.fs.his.domain.FsExportTask;

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

@@ -9,6 +9,7 @@ import com.fs.aicall.domain.result.EditDialogResult;
 import com.fs.aicall.domain.result.GetairobotResult;
 import com.fs.aicall.domain.result.QueryCallTaskInfoResult;
 import com.fs.aicall.service.AiCallService;
+import com.fs.common.annotation.CallbackIpCheck;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -21,6 +22,7 @@ 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.PauseRoboticActiveParam;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
@@ -322,6 +324,7 @@ public class CompanyVoiceRoboticController extends BaseController
     }
 
     @PostMapping("/callerResult4EasyCall")
+    @CallbackIpCheck
     public String callerResult4EasyCall(@RequestBody String cdrStr) {
         log.info("callerResult4EasyCall:回调结果:{}",cdrStr);
         CdrDetailVo cdrDetailVo = JSONObject.parseObject(cdrStr, CdrDetailVo.class);
@@ -394,4 +397,10 @@ public class CompanyVoiceRoboticController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         return R.ok().put("companyId", loginUser.getCompany().getCompanyId());
     }
+
+    @PostMapping("/pauseRoboticActive")
+    public R pauseRoboticActive(@RequestBody PauseRoboticActiveParam param){
+        return companyVoiceRoboticService.pauseRoboticActive(param);
+    }
+
 }

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

@@ -120,6 +120,7 @@ public class EasyCallController extends BaseController {
                     vo.setVoiceCode(item.getVoiceCode());
                     vo.setVoiceName(item.getVoiceName()+"-"+loginUser.getUser().getUserName());
                     vo.setVoiceSource(item.getVoiceSource());
+                    vo.setTtsModels(item.getTtsModels());
                     return vo;
                 })
                 .collect(Collectors.toList());

+ 7 - 3
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -344,8 +344,9 @@ public class SendMsg {
             // 推送 APP
             boolean hasAppSend = false;
             int successCount = 0;
+            int actualCount = 0;
             if (!setting.getSetting().isEmpty()) {
-                if (!CloudHostUtils.hasCloudHostName("木易华康")) {
+                if (!CloudHostUtils.hasCloudHostName("木易华康", "鸿森堂")) {
                     new Thread(() -> {
                         try {
                             List<QwSopTempSetting.Content.Setting> settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "9".equals(e.getContentType())).collect(Collectors.toList());
@@ -373,6 +374,7 @@ public class SendMsg {
                 } else {
                     List<QwSopCourseFinishTempSetting.Setting> settings = setting.getSetting().stream().filter(e -> "9".equals(e.getContentType())).collect(Collectors.toList());
                     if (!settings.isEmpty()) {
+                        actualCount++;
                         hasAppSend = true;
                         boolean sendFlag = asyncSopTestService.asyncSendMsgBySopAppLinkNormalIM(settings, qwSopLogs.getCorpId(), qwUser.getCompanyUserId(), qwSopLogs.getFsUserId(), qwSopLogs.getId());
                         if (sendFlag) {
@@ -384,6 +386,7 @@ public class SendMsg {
                     log.info("开始发送app文本消息消息开始,消息{},用户{}", JSONObject.toJSONString(settings), user.getQwUserName());
                     settings = setting.getSetting().stream().filter(e -> "15".equals(e.getContentType())).collect(Collectors.toList());
                     if (!settings.isEmpty()) {
+                        actualCount++;
                         hasAppSend = true;
                         boolean sendFlag = asyncSopTestService.asyncSendMsgBySopAppTxtNormalIM(settings, qwUser.getCompanyUserId(), qwSopLogs.getFsUserId(), qwSopLogs.getId());
                         if (sendFlag) {
@@ -395,6 +398,7 @@ public class SendMsg {
                     log.info("开始发送app语音消息消息开始,消息{},用户{}", JSONObject.toJSONString(settings), user.getQwUserName());
                     settings = setting.getSetting().stream().filter(e -> "16".equals(e.getContentType())).collect(Collectors.toList());
                     if (!settings.isEmpty()) {
+                        actualCount++;
                         hasAppSend = true;
                         boolean sendFlag = asyncSopTestService.asyncSendMsgBySopAppMP3NormalIM(settings, qwUser.getCompanyUserId(), qwSopLogs.getFsUserId(), qwSopLogs.getId());
                         if (sendFlag) {
@@ -424,9 +428,9 @@ public class SendMsg {
                 updateQwSop.setRealSendTime(sdf.format(new Date()));
             }
             updateQwSop.setContentJson(JSON.toJSONString(setting));
-
+            log.info("实际需要发送的数量:{}", actualCount);
             if (hasAppSend) {
-                if (successCount == 3) {
+                if (successCount == actualCount) {
                     updateQwSop.setAppSendRemark("APP全部发送成功");
                     updateQwSop.setAppSendStatus(1);
                 } else if (successCount == 0) {

+ 2 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcTtsAliyun.java

@@ -35,4 +35,6 @@ public class CcTtsAliyun implements Serializable {
 
     /** 供应商: aliyun、doubao 等 */
     private String provider;
+
+    private String ttsModels;
 }

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

@@ -76,6 +76,9 @@ public interface CompanyConfigMapper
     @Select("select config_value from company_config where company_id=#{companyId} and config_key='redPacket:config' ")
     String selectRedPacketConfigByKey(Long companyId);
 
+    @Select("select config_value from company_config where company_id=#{companyId} and config_key='his.AppRedPacket' ")
+    String selectRedPacketConfigByKeyApp(Long companyId);
+
     @Select("select \n" +
             "id,\n" +
             "name,\n" +

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

@@ -82,7 +82,7 @@ public interface CompanyWxClientMapper extends BaseMapper<CompanyWxClient> {
 
     List<CompanyWxClient> getWxClientInfoByCustomerId(@Param("customerId") Long customerId);
 
-    List<CompanyWxClient> getQwAddWxList(@Param("accountIdList") List<Long> accountIdList, @Param("isWeCom") Integer isWeCom);
+    List<CompanyWxClient> getQwAddWxList(@Param("accountIdList") List<Long> accountIdList, @Param("isWeCom") Integer isWeCom, @Param("cidGroupNo") Integer cidGroupNo);
 
     List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(@Param("accountIdList") List<Long> accountIdList, @Param("execStatus") Integer execStatus, @Param("execNodeType") Integer execNodeType, @Param("cidGroupNo") Integer cidGroupNo);
 

+ 26 - 0
fs-service/src/main/java/com/fs/company/param/PauseRoboticActiveParam.java

@@ -0,0 +1,26 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/29 14:33
+ * @description
+ */
+
+@Data
+public class PauseRoboticActiveParam {
+
+    /**
+     * 任务id
+     */
+    private Long taskId;
+
+    /**
+     * 操作类型 1、暂停 2、继续
+     */
+    private Integer activeType;
+
+
+
+}

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

@@ -72,6 +72,7 @@ public interface ICompanyConfigService
     CompanyConfig selectCompanyConfigByServerKey(String key);
 
     String selectRedPacketConfigByKey(Long companyId);
+    String selectRedPacketConfigByKeyApp(Long companyId);
 
     /**
      * 获取公司可配置小程序列表

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

@@ -3,8 +3,10 @@ package com.fs.company.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.result.CalltaskcreateaiCustomizeResult;
+import com.fs.common.core.domain.R;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.vo.*;
 
 import java.util.List;
@@ -112,4 +114,6 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
      * @param traceId
      */
     void addNewExec4Task(Long taskId,Long crmCustomerId,String traceId);
+
+    R pauseRoboticActive(PauseRoboticActiveParam param);
 }

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

@@ -75,7 +75,7 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     List<CompanyWxClient4WorkFlowVO> getAddWxList4WorkflowNew(List<Long> accountIdList, Integer cidGroupId);
 
-    List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList,Integer isWeCom);
+    List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList,Integer isWeCom,Integer cidGroupNo);
 
     List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(List<Long> accountIdList,Integer cidGroupNo);
 }

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

@@ -127,7 +127,7 @@ public class AsyncCalleeProcessorServiceImpl implements IAsyncCalleeProcessorSer
 
     private CidPhoneConfig loadPhoneConfig(Long companyId) {
         try {
-            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(companyId, "cid.config");
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(companyId, "cId.config");
             if (companyConfig != null && StringUtils.isNotEmpty(companyConfig.getConfigValue())) {
                 return JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
             }

+ 25 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java

@@ -44,6 +44,7 @@ public class CompanyConfigServiceImpl implements ICompanyConfigService
     @Autowired
     private RedisTemplate<String, String> redisTemplate; // 注入RedisTemplate
     private static final String REDIS_KEY_PREFIX = "red_packet_config:";
+    private static final String REDIS_KEY_PREFIX_APP = "red_packet_config_app:";
     private static final long CACHE_TIMEOUT = 24 * 60 * 60;
 
     @Autowired
@@ -177,6 +178,30 @@ public class CompanyConfigServiceImpl implements ICompanyConfigService
         }
     }
 
+    @Override
+    public String selectRedPacketConfigByKeyApp(Long companyId) {
+        Asserts.notNull(companyId,"公司id不能为空!");
+        String redisKey = REDIS_KEY_PREFIX_APP + companyId;
+        String cachedConfig = redisTemplate.opsForValue().get(redisKey);
+        if (cachedConfig != null) {
+            return cachedConfig;
+        }
+        synchronized (getSynchronizationObject(companyId)) {
+            cachedConfig = redisTemplate.opsForValue().get(redisKey);
+
+            if (cachedConfig != null) {
+                return cachedConfig;
+            }
+            String configFromDb = companyConfigMapper.selectRedPacketConfigByKeyApp(companyId);
+            if (configFromDb != null) {
+                redisTemplate.opsForValue().set(redisKey, configFromDb, CACHE_TIMEOUT, TimeUnit.SECONDS);
+            } else {
+                redisTemplate.opsForValue().set(redisKey, "", 5 * 60, TimeUnit.SECONDS);
+            }
+            return configFromDb;
+        }
+    }
+
     private static final ConcurrentHashMap<Long, Object> LOCKS = new ConcurrentHashMap<>();
     private static Object getSynchronizationObject(Long companyId) {
         return LOCKS.computeIfAbsent(companyId, k -> new Object());

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

@@ -1,5 +1,6 @@
 package com.fs.company.service.impl;
 
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.text.Convert;
 import com.fs.company.domain.CompanyInboundBind;
 import com.fs.company.domain.EasyCallInboundCdrVO;
@@ -7,6 +8,9 @@ import com.fs.company.mapper.CompanyInboundBindMapper;
 import com.fs.company.mapper.EasyCallInboundLlmMapper;
 import com.fs.company.service.ICompanyInboundCallManageService;
 import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -22,6 +26,7 @@ import java.util.stream.Collectors;
  * @author fs
  */
 @Service
+@Slf4j
 public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallManageService {
 
     @Autowired
@@ -29,6 +34,8 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
 
     @Autowired
     CompanyInboundBindMapper companyInboundBindMapper;
+    @Autowired
+    private ISysConfigService sysConfigService;
 
     /**
      * 查询呼入大模型配置
@@ -75,6 +82,22 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
         if(b){
           throw new RuntimeException("被叫号码已存在,不能重复插入");
         }
+        try {
+            //获得总后台配置
+            SysConfig cidConf = sysConfigService.selectConfigByConfigKey("cId.config");
+            if (null != cidConf) {
+                String configValue = cidConf.getConfigValue();
+                if (com.fs.common.utils.StringUtils.isNotBlank(configValue)) {
+                    JSONObject jsonObject = JSONObject.parseObject(configValue);
+                    if (null != jsonObject && jsonObject.containsKey("inboundCallbackUrl")) {
+                        vo.setCallBackUrl(jsonObject.getString("inboundCallbackUrl"));
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("获取总后台配置异常", ex);
+        }
+        
         int i = inboundLlmMapper.insertInboundLlm(vo);
         if(i >0 && vo.getId()!= null) {
             CompanyInboundBind bind = new CompanyInboundBind();

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

@@ -190,7 +190,7 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
 //                updateCallees.setRunTaskFlow(runTaskFlow);
 //                companyVoiceRoboticCalleesMapper.updateById(updateCallees);
 //            }
-            String json = configService.selectConfigByKey("cid.config");
+            String json = configService.selectConfigByKey("cId.config");
             if (StringUtils.isBlank(json)) {
                 log.error("未配置cid.config");
             }
@@ -270,7 +270,7 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Async("callLogExcutor")
     public void asyncHandleCalleeCallBackResult4EasyCall(EasyCallCallPhoneVO result, CompanyVoiceRoboticCallees callees) {
         try {
-            String json = configService.selectConfigByKey("cid.config");
+            String json = configService.selectConfigByKey("cId.config");
             if (StringUtils.isBlank(json)) {
                 log.error("未配置cid.config");
             }

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

@@ -14,6 +14,7 @@ import com.fs.aicall.domain.result.CalltaskcreateaiCustomizeResult;
 import com.fs.aicall.service.AiCallService;
 import com.fs.common.annotation.DataScope;
 import com.fs.common.constant.Constants;
+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.core.redis.RedisCacheT;
@@ -24,6 +25,7 @@ import com.fs.common.utils.*;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.service.*;
 import com.fs.company.vo.*;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
@@ -1794,7 +1796,12 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return new ArrayList<>();
         }
         List<CompanyAiWorkflowExecLog> callLogs = logs.stream().filter(a -> "外呼".equals(a.getNodeName())).collect(Collectors.toList());
-        HashMap<Long,String> callContentMap = selectCallContentByCallLogs(callLogs);
+        HashMap<Long,String> callContentMap;
+        if (null != callLogs && !callLogs.isEmpty()) {
+            callContentMap = selectCallContentByCallLogs(callLogs);
+        } else {
+            callContentMap = new HashMap<>();
+        }
 
         return logs.stream().map(log -> {
             WorkflowExecRecordVo.NodeExecLogVo vo = new WorkflowExecRecordVo.NodeExecLogVo();
@@ -1832,4 +1839,33 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return new HashMap<>();
         }
     }
+
+    //暂停
+    private final Integer ACTIVE_TYPE_PAUSE = 1;
+    //继续
+    private final Integer ACTIVE_TYPE_CONTINUE = 2;
+
+    /**
+     * 任务暂停 & 恢复操作
+     *
+     * @param param
+     * @return
+     */
+    @Override
+    public R pauseRoboticActive(PauseRoboticActiveParam param) {
+        //暂停任务
+        if (ACTIVE_TYPE_PAUSE.equals(param.getActiveType())) {
+
+            // 暂停任务更新
+
+            // 暂停任务创建的三方外呼任务
+
+        }
+        //恢复任务继续进入可运行
+        else if (ACTIVE_TYPE_CONTINUE.equals(param.getActiveType())) {
+
+        }
+
+        return R.ok("操作成功");
+    }
 }

+ 5 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -17,6 +17,7 @@ import com.fs.company.vo.AiCallConfigVO;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.company.vo.easycall.EasyCallCreateTaskParam;
 import com.fs.company.vo.easycall.EasyCallTaskVO;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import lombok.extern.slf4j.Slf4j;
@@ -74,6 +75,9 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
     @Autowired
     CompanyWxClientMapper companyWxClientMapper;
 
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
     /**
      * 初始化工作流
      * 创建工作流实例并保存初始状态
@@ -576,7 +580,7 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
                 AiCallConfigVO callConfigVo = JSONObject.parseObject(nodeConfig, AiCallConfigVO.class);
                 EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
                 // 任务名称:使用任务名称_工作流id_节点key
-                createParam.setBatchName(robotic.getName() + "_" + workFlowId + "_" + callNode.getNodeKey());
+                createParam.setBatchName(cloudHostProper.getCompanyName()+"-"+robotic.getName() + "_" + workFlowId + "_" + callNode.getNodeKey());
                 if (null != callConfigVo.getMaxConcurrency()) {
                     createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
                 } else {

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

@@ -248,8 +248,8 @@ public class CompanyWxClientServiceImpl extends ServiceImpl<CompanyWxClientMappe
     }
 
     @Override
-    public List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList, Integer isWeCom) {
-        return baseMapper.getQwAddWxList(accountIdList,isWeCom);
+    public List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList, Integer isWeCom , Integer cidGroupNo) {
+        return baseMapper.getQwAddWxList(accountIdList,isWeCom,cidGroupNo);
     }
 
     /**

+ 105 - 6
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
@@ -85,6 +86,27 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
     CrmCustomerPropertyServiceImpl crmCustomerPropertyService;
     @Autowired
     EasyCallMapper easyCallMapper;
+    @Autowired
+    private RedisCache redisCache2;
+
+    /** 呼入回调 chatContent(对话内容)重试队列 Redis key 前缀,value 为已重试次数 */
+    private static final String INBOUND_CHAT_CONTENT_RETRY_KEY = "inbound:chat:retry:";
+    /** chatContent 对话内容等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int INBOUND_CHAT_CONTENT_MAX_RETRY = 5;
+    /** chatContent 每次重试等待时长(毫秒) */
+    private static final long INBOUND_CHAT_CONTENT_RETRY_INTERVAL_MS = 30000L;
+
+    /**
+     * 判断 chatContent 对话内容是否为空(null、空字符串、空数组 "[]" 均视为无对话内容)
+     */
+    private boolean isChatContentEmpty(String chatContent) {
+        if (StringUtils.isBlank(chatContent)) {
+            return true;
+        }
+        String trimmed = chatContent.trim();
+        return "[]".equals(trimmed);
+    }
+
     /**
      * 录入客户
      *
@@ -123,7 +145,7 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
     }
 
     private static final String TRADE_TYPE = "trade_type";
-    @Value("${crm.customer.ai.key:mygpt-oPG2ifhnq0ODGioOBMUvMfOZGrtCykqw3oMeYLchdUDK5He6iNiactrhFWA0sID}")
+    @Value("${crm.customer.ai.Key:mygpt-iTUua2CHVd4WGrBbQQGl1HHjyyBAD1KuXARsxHj5eHpLYv5CfnOh8iwVU}")
     private String appKey;
     private List<CrmCustomerAiTagVo> getAiTags(String chatRecord) throws JsonProcessingException {
         Map<String, Object> requestParam = new HashMap<>();
@@ -298,11 +320,13 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
                     property.setCreateTime(new Date());
                     return property;
                 }).collect(Collectors.toList());
-                crmCustomerPropertyService.remove(new LambdaQueryWrapper<CrmCustomerProperty>()
-                        .eq(CrmCustomerProperty::getCustomerId, data.getCustomerId())
-                        .in(CrmCustomerProperty::getPropertyId, ids)
-                );
-                crmCustomerPropertyService.saveBatch(propertyList);
+                if(null != ids && !ids.isEmpty()){
+                    crmCustomerPropertyService.remove(new LambdaQueryWrapper<CrmCustomerProperty>()
+                            .eq(CrmCustomerProperty::getCustomerId, data.getCustomerId())
+                            .in(CrmCustomerProperty::getPropertyId, ids)
+                    );
+                    crmCustomerPropertyService.saveBatch(propertyList);
+                }
             } catch (JsonProcessingException e) {
                 throw new RuntimeException(e);
             }
@@ -415,21 +439,96 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
      * @param param
      */
     @Override
+    @Async("cidWorkFlowExecutor")
     public void inboundCallback(InboundCallbackParam param){
         try {
             Thread.sleep(5000L);
         } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
             throw new RuntimeException(e);
         }
         if (param == null || StringUtils.isBlank(param.getUuid())){
             return;
         }
         InboundCallInfo info  = easyCallMapper.selectInboundCallbackInfoByUuid(param.getUuid());
+        // chatContent(对话内容)由对方异步写入,回调时可能尚未赋值,进入延迟重试队列等待
+        if (info == null || isChatContentEmpty(info.getChatContent())) {
+            String retryKey = INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = 0;
+            }
+            if (retryCount < INBOUND_CHAT_CONTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("呼入回调chatContent对话内容暂未写入,uuid={},第{}次放入延迟重试队列", param.getUuid(), retryCount + 1);
+                doRetryInboundCallback(param, retryCount + 1);
+            } else {
+                // 超过最大重试次数,以 chatContent 为空兜底继续处理
+                log.warn("呼入回调chatContent对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", INBOUND_CHAT_CONTENT_MAX_RETRY, param.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleInboundCallback(param, info);
+            }
+            return;
+        }
+        // chatContent 已有值,直接正常处理
+        redisCache2.deleteObject(INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid());
+        doHandleInboundCallback(param, info);
+    }
+
+    /**
+     * 延迟重试处理呼入回调(等待 chatContent 对话内容异步写入完成)
+     * 每次重试前等待 {@link #INBOUND_CHAT_CONTENT_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryInboundCallback(InboundCallbackParam param, int currentRetry) {
+        try {
+            Thread.sleep(INBOUND_CHAT_CONTENT_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("呼入回调chatContent重试等待被中断, uuid={}", param.getUuid());
+            return;
+        }
+        log.info("呼入回调chatContent重试第{}次开始, uuid={}", currentRetry, param.getUuid());
+        InboundCallInfo info = easyCallMapper.selectInboundCallbackInfoByUuid(param.getUuid());
+        if (info == null || isChatContentEmpty(info.getChatContent())) {
+            // chatContent 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < INBOUND_CHAT_CONTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("呼入回调chatContent对话内容仍未写入,uuid={},第{}次继续延迟重试", param.getUuid(), retryCount + 1);
+                doRetryInboundCallback(param, retryCount + 1);
+            } else {
+                log.error("呼入回调chatContent对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", INBOUND_CHAT_CONTENT_MAX_RETRY, param.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleInboundCallback(param, info);
+            }
+            return;
+        }
+        // chatContent 已写入完成,正常处理
+        log.info("呼入回调chatContent重试第{}次成功获取到对话内容,uuid={}", currentRetry, param.getUuid());
+        redisCache2.deleteObject(INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid());
+        doHandleInboundCallback(param, info);
+    }
+
+    /**
+     * 执行呼入回调核心业务处理(组装客户入参并录入)
+     * 供 {@link #inboundCallback} 和重试逻辑统一调用
+     */
+    private void doHandleInboundCallback(InboundCallbackParam param, InboundCallInfo info) {
+        if (info == null) {
+            log.error("呼入回调信息未查询到结果, uuid={}", param.getUuid());
+            return;
+        }
         EntryCustomerParam entry = new EntryCustomerParam();
         entry.setTraceId(param.getUuid());
         entry.setCompanyId(info.getFsCompanyId());
         entry.setSceneType(info.getFsSceneType());
         entry.setMobile(info.getCaller());
+        entry.setDialogue(info.getChatContent());
         entryCustomer(entry);
     }
 

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

@@ -271,6 +271,14 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
             if(null != cidConf && StringUtils.isNotBlank(cidConf.getCallbackUrl())){
                 callBackUrl = cidConf.getCallbackUrl();
             }
+            //读取总后台配置
+            if(StringUtils.isBlank(callBackUrl)){
+                String s = configService.selectConfigByKey("cId.config");
+                JSONObject obj = JSONObject.parseObject(s);
+                if(null != obj && obj.containsKey("callbackUrl") && StringUtils.isNotBlank(obj.getString("callbackUrl"))){
+                    callBackUrl = obj.getString("callbackUrl");
+                }
+            }
         } catch (Exception ex){
             log.error("获取公司Cid配置失败:{}", ex);
         }
@@ -375,7 +383,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     }
 
     private boolean checkPhoneCallLimit(Long businessId){
-        String json = configService.selectConfigByKey("cid.config");
+        String json = configService.selectConfigByKey("cId.config");
         if(StringUtils.isNotEmpty(json)){//数据存在
             //转换数据
             CidPhoneConfig config =JSONObject.parseObject(json,CidPhoneConfig.class);

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

@@ -5472,7 +5472,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 // 调用第三方接口(锁外操作)
                 R sendRedPacket;
                 try {
-                    sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                    sendRedPacket = paymentService.sendAppRedPacket(packetParam, config);
                 } catch (Exception e) {
                     logger.error("红包发送异常: 异常请求参数{}", packetParam, e);
                     // 异常时回滚余额
@@ -5538,7 +5538,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
                 try{
                     // 发送红包
-                    R sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                    R sendRedPacket = paymentService.sendAppRedPacket(packetParam,config);
                     if (sendRedPacket.get("code").equals(200)) {
                         FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                         TransferBillsResult transferBillsResult;

+ 3 - 1
fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java

@@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.company.param.FsStoreStatisticsParam;
 import com.fs.company.vo.FsStorePaymentStatisticsVO;
+import com.fs.course.config.CourseConfig;
 import com.fs.his.domain.FsStorePayment;
 import com.fs.his.param.FsStorePaymentParam;
 import com.fs.his.param.PayOrderParam;
@@ -120,6 +121,7 @@ public interface IFsStorePaymentService
     String v3TransferNotifyApp(String notifyData, HttpServletRequest request);
 
     String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
+    String v3TransferNotifyWithCompanyIdApp(Long companyId,String notifyData, HttpServletRequest request);
 
 
 
@@ -144,5 +146,5 @@ public interface IFsStorePaymentService
 
     List<FsStorePayment> selectAllPayment();
 
-    R sendAppRedPacket(WxSendRedPacketParam packetParam);
+    R sendAppRedPacket(WxSendRedPacketParam packetParam,CourseConfig config);
 }

+ 160 - 80
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -39,6 +39,7 @@ import com.fs.company.vo.FsStorePaymentStatisticsVO;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.config.CourseConfig;
 import com.fs.course.config.RedPacketConfig;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
@@ -1121,105 +1122,166 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
     @Override
     public String v3TransferNotify(String notifyData, HttpServletRequest request) {
         logger.info("zyp \n【收到转账回调V3】:{}",notifyData);
-        try {
-            String json = configService.selectConfigByKey("redPacket.config");
-            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
-            //创建微信订单
-            WxPayConfig payConfig = new WxPayConfig();
-            BeanUtils.copyProperties(config,payConfig);
-            WxPayService wxPayService = new WxPayServiceImpl();
-            wxPayService.setConfig(payConfig);
-            SignatureHeader signatureHeader = new SignatureHeader();
-            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
-            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
-            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
-            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
-            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
-            logger.info("到零钱回调1:{}",result.getResult());
-            if (result.getResult().getState().equals("SUCCESS")) {
-                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
-                logger.info("result:{}",r);
-                if (r.get("code").equals(200)){
-                    return WxPayNotifyResponse.success("处理成功");
-                }else {
-                    return WxPayNotifyResponse.fail("");
-                }
-            }else {
-                return WxPayNotifyResponse.fail("");
-            }
-        } catch (WxPayException e) {
-            e.printStackTrace();
-            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
-            return WxPayNotifyResponse.fail(e.getMessage());
-        }
+        String json = configService.selectConfigByKey("redPacket.config");
+        return handleTransferV3Notify(json,notifyData,request);
+//        try {
+//            String json = configService.selectConfigByKey("redPacket.config");
+//            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+//            //创建微信订单
+//            WxPayConfig payConfig = new WxPayConfig();
+//            BeanUtils.copyProperties(config,payConfig);
+//            WxPayService wxPayService = new WxPayServiceImpl();
+//            wxPayService.setConfig(payConfig);
+//            SignatureHeader signatureHeader = new SignatureHeader();
+//            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+//            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+//            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+//            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+//            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+//            logger.info("到零钱回调1:{}",result.getResult());
+//            if (result.getResult().getState().equals("SUCCESS")) {
+//                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+//                logger.info("result:{}",r);
+//                if (r.get("code").equals(200)){
+//                    return WxPayNotifyResponse.success("处理成功");
+//                }else {
+//                    return WxPayNotifyResponse.fail("");
+//                }
+//            }else {
+//                return WxPayNotifyResponse.fail("");
+//            }
+//        } catch (WxPayException e) {
+//            e.printStackTrace();
+//            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
+//            return WxPayNotifyResponse.fail(e.getMessage());
+//        }
     }
 
     @Override
     public String v3TransferNotifyApp(String notifyData, HttpServletRequest request) {
         logger.info("zyp \n【app-收到转账回调V3】:{}",notifyData);
-        try {
-            String json = configService.selectConfigByKey("his.AppRedPacket");
-            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
-            //创建微信订单
-            WxPayConfig payConfig = new WxPayConfig();
-            BeanUtils.copyProperties(config,payConfig);
-            WxPayService wxPayService = new WxPayServiceImpl();
-            wxPayService.setConfig(payConfig);
-            SignatureHeader signatureHeader = new SignatureHeader();
-            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
-            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
-            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
-            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
-            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
-            logger.info("app-到零钱回调:{}",result.getResult());
-            if (result.getResult().getState().equals("SUCCESS")) {
-                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
-                logger.info("app,result:{}",r);
-                if (r.get("code").equals(200)){
-                    return WxPayNotifyResponse.success("处理成功");
-                }else {
-                    return WxPayNotifyResponse.fail("");
-                }
-            }else {
-                return WxPayNotifyResponse.fail("");
-            }
-        } catch (WxPayException e) {
-            e.printStackTrace();
-            logger.error("zyp \n【app-转账回调异常】:{}", e.getReturnMsg());
-            return WxPayNotifyResponse.fail(e.getMessage());
-        }
+        String json = configService.selectConfigByKey("his.AppRedPacket");
+        return handleTransferV3Notify(json,notifyData,request);
+//        try {
+//            String json = configService.selectConfigByKey("his.AppRedPacket");
+//            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+//            //创建微信订单
+//            WxPayConfig payConfig = new WxPayConfig();
+//            BeanUtils.copyProperties(config,payConfig);
+//            WxPayService wxPayService = new WxPayServiceImpl();
+//            wxPayService.setConfig(payConfig);
+//            SignatureHeader signatureHeader = new SignatureHeader();
+//            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+//            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+//            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+//            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+//            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+//            logger.info("app-到零钱回调:{}",result.getResult());
+//            if (result.getResult().getState().equals("SUCCESS")) {
+//                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+//                logger.info("app,result:{}",r);
+//                if (r.get("code").equals(200)){
+//                    return WxPayNotifyResponse.success("处理成功");
+//                }else {
+//                    return WxPayNotifyResponse.fail("");
+//                }
+//            }else {
+//                return WxPayNotifyResponse.fail("");
+//            }
+//        } catch (WxPayException e) {
+//            e.printStackTrace();
+//            logger.error("zyp \n【app-转账回调异常】:{}", e.getReturnMsg());
+//            return WxPayNotifyResponse.fail(e.getMessage());
+//        }
     }
 
     @Override
     public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
         logger.info("分公司回调V3::companyId:{}",companyId);
         logger.info("zyp \n【收到转账回调V3::分公司】:{}",notifyData);
+        String json = companyConfigService.selectRedPacketConfigByKey(companyId);
+        return handleTransferV3Notify(json,notifyData,request);
+//        try {
+////            String json = configService.selectConfigByKey("redPacket.config");
+//            String json = companyConfigService.selectRedPacketConfigByKey(companyId);
+//            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+//
+//            //创建微信订单
+//            WxPayConfig payConfig = new WxPayConfig();
+//            BeanUtils.copyProperties(config,payConfig);
+//            WxPayService wxPayService = new WxPayServiceImpl();
+//            wxPayService.setConfig(payConfig);
+//            SignatureHeader signatureHeader = new SignatureHeader();
+//            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+//            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+//            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+//            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+//            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+//            logger.info("到零钱回调1:{}",result.getResult());
+//            if (result.getResult().getState().equals("SUCCESS")) {
+//                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+//                logger.info("result:{}",r);
+//                if (r.get("code").equals(200)){
+//                    return WxPayNotifyResponse.success("处理成功");
+//                }else {
+//                    return WxPayNotifyResponse.fail("");
+//                }
+//            }else {
+//                return WxPayNotifyResponse.fail("");
+//            }
+//        } catch (WxPayException e) {
+//            e.printStackTrace();
+//            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
+//            return WxPayNotifyResponse.fail(e.getMessage());
+//        }
+    }
+
+    @Override
+    public String v3TransferNotifyWithCompanyIdApp(Long companyId, String notifyData, HttpServletRequest request) {
+        logger.info("分公司回调V3app::app的companyId:{}",companyId);
+        logger.info("zyp \n【app收到转账回调V3::分公司】:{}",notifyData);
+
+        String json = companyConfigService.selectRedPacketConfigByKeyApp(companyId);
+
+        return handleTransferV3Notify(json,notifyData,request);
+
+    }
+
+
+    private String handleTransferV3Notify(String json, String notifyData, HttpServletRequest request) {
+        logger.info("zyp \n【收到转账回调V3】:{}", notifyData);
         try {
-//            String json = configService.selectConfigByKey("redPacket.config");
-            String json = companyConfigService.selectRedPacketConfigByKey(companyId);
+
             RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
 
             //创建微信订单
             WxPayConfig payConfig = new WxPayConfig();
-            BeanUtils.copyProperties(config,payConfig);
+            BeanUtils.copyProperties(config, payConfig);
             WxPayService wxPayService = new WxPayServiceImpl();
             wxPayService.setConfig(payConfig);
+
             SignatureHeader signatureHeader = new SignatureHeader();
             signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
             signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
             signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
             signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
-            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
-            logger.info("到零钱回调1:{}",result.getResult());
+
+            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData, signatureHeader);
+            logger.info("到零钱回调1:{}", result.getResult());
+
             if (result.getResult().getState().equals("SUCCESS")) {
-                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
-                logger.info("result:{}",r);
-                if (r.get("code").equals(200)){
+                R r = redPacketLogService.syncRedPacket(
+                        result.getResult().getOutBillNo(),
+                        result.getResult().getTransferBillNo()
+                );
+                logger.info("result:{}", r);
+
+                if (r.get("code").equals(200)) {
                     return WxPayNotifyResponse.success("处理成功");
-                }else {
+                } else {
                     return WxPayNotifyResponse.fail("");
                 }
-            }else {
+            } else {
                 return WxPayNotifyResponse.fail("");
             }
         } catch (WxPayException e) {
@@ -1964,19 +2026,37 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
 
     @Override
     @Transactional
-    public R sendAppRedPacket(WxSendRedPacketParam param) {
+    public R sendAppRedPacket(WxSendRedPacketParam param, CourseConfig config) {
         //组合返回参数
         R result = new R();
-        String json = configService.selectConfigByKey("his.AppRedPacket");
-        AppRedPacketConfig config = JSONUtil.toBean(json, AppRedPacketConfig.class);
-        if (config.getIsNew() != null && config.getIsNew() == 1) {
-            result = sendRedPacketV3(param, config);
+
+        String json;
+        // 根据红包模式获取配置
+        switch (config.getRedPacketMode()){
+            case 1:
+                json = configService.selectConfigByKey("his.AppRedPacket");
+                break;
+            case 2:
+                 json = companyConfigService.selectRedPacketConfigByKeyApp(param.getCompanyId());
+                //如果分公司配置为空就走总后台的配置
+                if (StringUtils.isEmpty(json)){
+                    json = configService.selectConfigByKey("his.AppRedPacket");
+                }
+                break;
+            default:
+                throw new UnsupportedOperationException("当前红包模式不支持!");
+        }
+
+//        String json = configService.selectConfigByKey("his.AppRedPacket");
+        AppRedPacketConfig appRedConfig = JSONUtil.toBean(json, AppRedPacketConfig.class);
+        if (appRedConfig.getIsNew() != null && appRedConfig.getIsNew() == 1) {
+            result = sendRedPacketV3(param, appRedConfig);
         } else {
-            result= sendRedPacketLegacy(param, config);
+            result= sendRedPacketLegacy(param, appRedConfig);
         }
 
-        result.put("mchId", config.getMchId());
-        result.put("isNew",config.getIsNew());
+        result.put("mchId", appRedConfig.getMchId());
+        result.put("isNew",appRedConfig.getIsNew());
         logger.info("App提现返回:{}",result);
         return result;
     }

+ 7 - 0
fs-service/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java

@@ -8,6 +8,8 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.springframework.stereotype.Repository;
 
+import com.fs.sop.vo.QwSopTempRulesWithDayVO;
+
 import java.util.List;
 
 /**
@@ -105,4 +107,9 @@ public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules> {
     List<Long> getTempOfficialIdsForClose(@Param("tempId") String tempId);
 
     int updateTempRulesOfficialBatch(@Param("ids") List<Long> ids,@Param("official") Integer official);
+
+    /**
+     * 查询模板规则并关联 day_num(通过 qw_sop_temp_day)
+     */
+    List<QwSopTempRulesWithDayVO> listByTempIdWithDayNum(@Param("id") String id);
 }

+ 33 - 0
fs-service/src/main/java/com/fs/sop/vo/QwSopTempRulesWithDayVO.java

@@ -0,0 +1,33 @@
+package com.fs.sop.vo;
+
+import lombok.Data;
+
+/**
+ * qw_sop_temp_rules 关联 day_num(content) 
+ * 用于个微消息生成流程
+ */
+@Data
+public class QwSopTempRulesWithDayVO {
+
+    private Long id;
+    private String tempId;
+    private Long dayId;
+    private String name;
+    private String time;
+    private String isOfficial;
+    private Integer contentType;
+    private Integer type;
+    private Integer courseType;
+    private Long courseId;
+    private Long videoId;
+    private String aiTouch;
+    private String addTag;
+    private String delTag;
+    private Integer sorts;
+    private Integer isAtAll;
+    private Long liveId;
+
+    private Integer dayNum;
+
+    private String textContent;
+}

+ 9 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.vo.WxSopUserMsgGenVO;
 
 /**
  * 个微营期Mapper接口
@@ -78,4 +79,12 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
      * @return 营期记录
      */
     WxSopUser selectwxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 查询活跃的个微SOP营期及客户信息(用于消息生成)
+     *
+     * @return 营期客户信息列表
+     */
+    @DataSource(DataSourceType.SOP)
+    List<WxSopUserMsgGenVO> selectActiveWxSopUserForMsgGen();
 }

+ 27 - 0
fs-service/src/main/java/com/fs/wx/sop/vo/WxSopUserMsgGenVO.java

@@ -0,0 +1,27 @@
+package com.fs.wx.sop.vo;
+
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 个微SOP消息生成——营期客户视图对象
+ * 用于查询需要生成消息的活跃营期及客户信息
+ */
+@Data
+public class WxSopUserMsgGenVO {
+
+    private Long sopUserId;
+    private Integer type;
+    private Long sopId;
+    private Long accountId;
+    private LocalDate startTime;
+
+    private String tempId;
+    private Long companyId;
+
+    private Long infoId;
+    private Long wxContactId;
+    private Long customerId;
+    private Long fsUserId;
+}

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

@@ -226,6 +226,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectExecListWithTimeAvailableByStatusAndGroupNo" resultType="CompanyAiWorkflowExec">
         SELECT t1.*
         FROM company_ai_workflow_exec t1
+        inner join company_voice_robotic_business t2 on t1.business_key = t2.id
         where t1.status = #{status}
           and t1.cid_group_no = #{groupNo}
           and NOW() BETWEEN t1.runtime_range_start and t1.runtime_range_end

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

@@ -36,7 +36,7 @@
     </select>
 
     <sql id="selectCcTtsAliyunVo">
-        select id, voice_name, voice_code, voice_enabled, voice_source, priority, provider from cc_tts_aliyun
+        select id, voice_name, voice_code, voice_enabled, voice_source, priority, provider,tts_models from cc_tts_aliyun
     </sql>
 
     <select id="selectCcTtsAliyunList" resultType="com.fs.aicall.domain.CcTtsAliyun">

+ 10 - 4
fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml

@@ -179,14 +179,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         group by account_id
     </select>
     <select id="getQwAddWxList" resultType="com.fs.company.domain.CompanyWxClient">
-        SELECT * FROM company_wx_client where is_add = 2 and account_id is not null
+        SELECT t1.* FROM company_wx_client t1
+          inner join  company_voice_robotic t2 on t1.robotic_id = t2.id
+                    where t1.is_add = 2 and t1.account_id is not null
+                      and t2.task_status = 1 and t2.del_flag != 1
         <if test="accountIdList != null and !accountIdList.isEmpty()">
-            and account_id in <foreach collection="accountIdList" open="(" separator="," close=")" item="item">#{item}</foreach>
+            and t1.account_id in <foreach collection="accountIdList" open="(" separator="," close=")" item="item">#{item}</foreach>
         </if>
         <if test="isWeCom != null">
-            and is_we_com = #{isWeCom}
+            and t1.is_we_com = #{isWeCom}
         </if>
-        group by account_id
+        <if test="cidGroupNo != null">
+            and t2.cid_group_no = #{cidGroupNo}
+        </if>
+--         group by account_id
     </select>
     <select id="getAddWxList4Workflow" resultType="com.fs.company.vo.CompanyWxClient4WorkFlowVO">
 

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

@@ -14,7 +14,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectInboundCallbackInfoByUuid" resultType="com.fs.company.vo.InboundCallInfo">
         select
             t1.uuid,
-            t2.call_back_url,
             t2.fs_company_id,
             t2.fs_scene_type,
             t1.chat_content,

+ 33 - 0
fs-service/src/main/resources/mapper/sop/QwSopTempRulesMapper.xml

@@ -161,4 +161,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </update>
 
+    <resultMap type="com.fs.sop.vo.QwSopTempRulesWithDayVO" id="QwSopTempRulesWithDayResult">
+        <result property="id"    column="id"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="dayId"    column="day_id"    />
+        <result property="name"    column="name"    />
+        <result property="time"    column="time"    />
+        <result property="isOfficial"    column="is_official"    />
+        <result property="contentType"    column="content_type"    />
+        <result property="type"    column="type"    />
+        <result property="courseType"    column="course_type"    />
+        <result property="courseId"    column="course_id"    />
+        <result property="videoId"    column="video_id"    />
+        <result property="aiTouch"    column="ai_touch"    />
+        <result property="addTag"    column="add_tag"    />
+        <result property="delTag"    column="del_tag"    />
+        <result property="sorts"    column="sorts"    />
+        <result property="isAtAll"    column="is_at_all"    />
+        <result property="liveId"    column="live_id"    />
+        <result property="dayNum"    column="day_num"    />
+        <result property="textContent" column="ct_content" />
+    </resultMap>
+
+    <select id="listByTempIdWithDayNum" resultMap="QwSopTempRulesWithDayResult">
+        select tr.*, td.day_num,
+               (select tc.content from qw_sop_temp_content tc 
+                where tc.rules_id = tr.id and tc.content_type = 1 
+                limit 1) as ct_content
+        from qw_sop_temp_rules tr
+        left join qw_sop_temp_day td on tr.day_id = td.id
+        where tr.temp_id = #{id}
+        order by td.day_num, tr.time
+    </select>
+
 </mapper>

+ 22 - 0
fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml

@@ -117,4 +117,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </where>
         LIMIT 1
     </select>
+
+    <select id="selectActiveWxSopUserForMsgGen" resultType="com.fs.wx.sop.vo.WxSopUserMsgGenVO">
+        SELECT
+            wsu.id AS sopUserId,
+            wsu.type,
+            wsu.sop_id AS sopId,
+            wsu.account_id AS accountId,
+            wsu.start_time AS startTime,
+            wsu.chat_id AS chatId,
+            ws.temp_id AS tempId,
+            ws.company_id AS companyId,
+            wsui.id AS infoId,
+            wsui.wx_contact_id AS wxContactId,
+            wsui.customer_id AS customerId,
+            wsui.fs_user_id AS fsUserId
+        FROM wx_sop_user wsu
+        INNER JOIN wx_sop ws ON wsu.sop_id = ws.id AND ws.status IN (1, 2)
+        INNER JOIN wx_sop_user_info wsui ON wsu.id = wsui.sop_user_id AND wsui.status = 0
+        WHERE wsu.start_time &lt;= CURDATE()
+          AND wsu.status = 0
+        ORDER BY wsu.id ASC, wsui.id ASC
+    </select>
 </mapper>

+ 7 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseTransferController.java

@@ -91,6 +91,13 @@ public class CourseTransferController {
         return paymentService.v3TransferNotifyWithCompanyId(companyId,notifyData,request);
     }
 
+    @PostMapping( "/v3TransferNotifyWithCompanyIdApp/{companyId}")
+    public String v3TransferNotifyWithCompanyIdApp(@PathVariable("companyId") Long companyId,@RequestBody String notifyData,HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+        return paymentService.v3TransferNotifyWithCompanyIdApp(companyId,notifyData,request);
+    }
+
+
     @Autowired
     private IFsCourseRedPacketLogService fsCourseRedPacketLogService;
 

+ 7 - 0
fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.app.service.WxTaskService;
+import com.fs.common.utils.date.DateUtil;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyWxAccountService;
@@ -21,6 +22,7 @@ import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -129,4 +131,9 @@ public class CommonController {
         crmCustomerPropertyService.addPropertyByCallLog(byId);
     }
 
+    @GetMapping("/time")
+    public void time(String time) {
+        taskService.generateWxSopMsgByTime(DateUtil.stringToLocalDateTime(time));
+    }
+
 }

+ 134 - 12
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -5,7 +5,6 @@ import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
@@ -17,37 +16,42 @@ import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.*;
 import com.fs.company.service.impl.*;
-import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
 import com.fs.company.service.impl.call.node.AiAddWxTaskNewNode;
+import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
 import com.fs.company.service.impl.call.node.AiQwAddWxTaskNode;
 import com.fs.company.service.impl.call.node.WorkflowNodeFactory;
+import com.fs.company.util.ObjectPlaceholderResolver;
 import com.fs.company.vo.CompanyWxClient4WorkFlowVO;
+import com.fs.company.vo.SendMsgVo;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.config.RedisKeyScanner;
+import com.fs.course.config.WxConfig;
 import com.fs.course.domain.FsCourseLink;
-import com.fs.course.domain.FsCourseRealLink;
 import com.fs.course.mapper.FsCourseLinkMapper;
+import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.SmsSendBatchParam;
+import com.fs.crm.service.ICrmCustomerService;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
-import com.fs.company.util.ObjectPlaceholderResolver;
-import com.fs.company.vo.SendMsgVo;
-import com.fs.course.config.WxConfig;
-import com.fs.crm.domain.CrmCustomer;
-import com.fs.crm.service.ICrmCustomerService;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
-import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qwApi.Result.QwAddContactWayResult;
 import com.fs.qwApi.domain.QwLinkCreateResult;
 import com.fs.qwApi.param.QwAddContactWayParam;
 import com.fs.qwApi.param.QwLinkCreateParam;
 import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.mapper.QwSopTempRulesMapper;
+import com.fs.sop.vo.QwSopTempRulesWithDayVO;
 import com.fs.system.service.ISysConfigService;
 import com.fs.utils.ShortCodeGeneratorUtils;
 import com.fs.voice.utils.StringUtil;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.mapper.WxSopLogsMapper;
+import com.fs.wx.sop.mapper.WxSopMapper;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.vo.WxSopUserMsgGenVO;
 import com.fs.wxcid.dto.friend.AddContactParam;
 import com.fs.wxcid.service.FriendService;
 import com.fs.wxcid.vo.AddContactVo;
@@ -58,7 +62,6 @@ import com.fs.wxwork.dto.WxWorkResponseDTO;
 import com.fs.wxwork.service.WxWorkService;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import lombok.AllArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.redisson.api.RLock;
@@ -66,10 +69,11 @@ import org.redisson.api.RedissonClient;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
@@ -133,6 +137,11 @@ public class WxTaskService {
     private  final FsCourseLinkMapper fsCourseLinkMapper;
     private final ISysConfigService configService;
 
+    private final WxSopUserMapper wxSopUserMapper;
+    private final WxSopMapper wxSopMapper;
+    private final WxSopLogsMapper wxSopLogsMapper;
+    private final QwSopTempRulesMapper qwSopTempRulesMapper;
+
 
     private static final String REAL_CID_LINK_PREFIX = "/pages_cidAddQw/cidAddQw.html?link=";
     private static final String REAL_CID_H5LINK_PREFIX = "/pages_cidAddQwH5/cidAddQw.html?link=";
@@ -1811,7 +1820,7 @@ public class WxTaskService {
         log.info("==========执行企微申请加微结果查询任务开始==========");
         try {
             //is_add = 2,状态为加微中且是企微类型
-            List<CompanyWxClient> clients = companyWxClientService.getQwAddWxList(accountIdList, 2);
+           List<CompanyWxClient> clients = companyWxClientService.getQwAddWxList(accountIdList, 2,cidGroupNo);
            log.info("企微申请加微结果查询任务需要查询的数量:{}", clients.size());
 
             if (clients.isEmpty()) return;
@@ -2097,4 +2106,117 @@ public class WxTaskService {
         }
     }
 
+    /**
+     * 个微SOP消息生成(文本类型)
+     * 每小时触发,查询活跃的WxSOP营期及客户,生成文本消息待发送记录
+     *
+     * @param currentTime 当前整点时间
+     */
+    public void generateWxSopMsgByTime(LocalDateTime currentTime) {
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 个微SOP文本消息生成开始, currentTime: {} ======", currentTime);
+
+        List<WxSopUserMsgGenVO> msgGenList = wxSopUserMapper.selectActiveWxSopUserForMsgGen();
+        if (msgGenList == null || msgGenList.isEmpty()) {
+            log.info("个微SOP消息生成: 没有需要处理的活跃营期。");
+            return;
+        }
+        log.info("个微SOP消息生成: 查询到 {} 条营期客户记录。", msgGenList.size());
+
+        Map<String, List<QwSopTempRulesWithDayVO>> rulesCache = new HashMap<>();
+        List<WxSopLogs> logsToInsert = new ArrayList<>();
+
+        for (WxSopUserMsgGenVO vo : msgGenList) {
+            if (vo.getTempId() == null || vo.getTempId().isEmpty()) {
+                continue;
+            }
+
+            LocalDate startDate = vo.getStartTime();
+            LocalDate currentDate = currentTime.toLocalDate();
+            long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
+            long targetDay = daysBetween + 1;
+
+            List<QwSopTempRulesWithDayVO> rulesList = rulesCache.computeIfAbsent(vo.getTempId(),
+                    qwSopTempRulesMapper::listByTempIdWithDayNum);
+            if (rulesList == null || rulesList.isEmpty()) {
+                continue;
+            }
+
+            List<QwSopTempRulesWithDayVO> dayRules = rulesList.stream()
+                    .filter(r -> r.getDayNum() != null && r.getDayNum() == targetDay)
+                    .filter(r -> r.getContentType() != null && r.getContentType() == 1)
+                    .collect(Collectors.toList());
+
+            if (dayRules.isEmpty()) {
+                continue;
+            }
+
+            for (QwSopTempRulesWithDayVO rule : dayRules) {
+                LocalTime ruleTime;
+                try {
+                    ruleTime = LocalTime.parse(rule.getTime());
+                } catch (Exception e) {
+                    log.warn("个微SOP消息生成: 解析时间失败, ruleId: {}, time: {}", rule.getId(), rule.getTime());
+                    continue;
+                }
+                LocalDateTime ruleDateTime = LocalDateTime.of(currentDate, ruleTime);
+                if (ruleDateTime.isBefore(currentTime) && ruleDateTime.plusHours(1).isBefore(currentTime)) {
+                    continue;
+                }
+
+                LocalDateTime startRange = currentTime.plusMinutes(60);
+                LocalDateTime endRange = startRange.plusMinutes(60);
+                if (ruleDateTime.isBefore(startRange) || !ruleDateTime.isBefore(endRange)) {
+                    continue;
+                }
+
+                if (rule.getTextContent() == null || rule.getTextContent().isEmpty()) {
+                    continue;
+                }
+
+                WxSopLogs sopLogs = new WxSopLogs();
+                sopLogs.setType(vo.getType());
+                sopLogs.setSopId(vo.getSopId());
+                sopLogs.setSopUserId(vo.getSopUserId());
+                sopLogs.setSendType(2);
+                sopLogs.setGenerateType(0);
+                sopLogs.setAccountId(vo.getAccountId());
+                sopLogs.setWxContactId(vo.getWxContactId());
+                sopLogs.setFsUserId(vo.getFsUserId());
+                sopLogs.setSendStatus(0);
+                sopLogs.setSendSort(10000000);
+                sopLogs.setSendTime(ruleDateTime);
+
+                String contentJson = buildTextContentJson(rule.getTextContent());
+                sopLogs.setContentJson(contentJson);
+                sopLogs.setCreateTime(new Date());
+                sopLogs.setUpdateTime(new Date());
+
+                logsToInsert.add(sopLogs);
+            }
+        }
+
+        if (!logsToInsert.isEmpty()) {
+            wxSopLogsMapper.batchInsertWxSopLogs(logsToInsert);
+            log.info("个微SOP消息生成: 批量写入 {} 条文本消息。", logsToInsert.size());
+        }
+
+        long endTimeMillis = System.currentTimeMillis();
+        log.info("====== 个微SOP文本消息生成完成, 耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
+    }
+
+    private String buildTextContentJson(String text) {
+        if (text == null || text.isEmpty()) {
+            return null;
+        }
+        com.alibaba.fastjson.JSONObject item = new com.alibaba.fastjson.JSONObject();
+        item.put("contentType", "1");
+        item.put("value", text);
+        com.alibaba.fastjson.JSONArray settingsArray = new com.alibaba.fastjson.JSONArray();
+        settingsArray.add(item);
+        com.alibaba.fastjson.JSONObject wrapper = new com.alibaba.fastjson.JSONObject();
+        wrapper.put("settings", settingsArray);
+        return com.alibaba.fastjson.JSON.toJSONString(wrapper);
+    }
+
 }

+ 22 - 2
fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

@@ -3,12 +3,15 @@ package com.fs.app.task;
 import com.fs.app.service.WxTaskService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.time.LocalDateTime;
+
 /**
- * 企业微信SOP定时任务管理类
- * 负责处理各种定时任务,包括SOP规则检查、消息发送、数据清理等
+ * 个微定时任务管理类
+ * 负责处理各种定时任务,包括个微SOP消息生成、加微任务处理等
  *
  * @author 系统
  * @version 1.0
@@ -84,4 +87,21 @@ public class WxTask {
     public void cidWorkflowQwAddWxRun(){
         taskService.cidWorkflowQwAddWxRun();
     }
+
+    /**
+     * 个微SOP消息生成任务(文本类型)
+     * 每小时的第5分钟执行,仿照企微selectSopUserLogsListByTime模式
+     * 查询活跃的wx_sop_user及客户,生成仅文本类型(contentType=1)的待发送消息
+     */
+    @Async
+    @Scheduled(cron = "0 5 * * * ?")
+    public void generateWxSopMsgByTime() {
+        LocalDateTime currentTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
+        log.info("个微SOP消息生成任务执行时间: {}", currentTime);
+        try {
+            taskService.generateWxSopMsgByTime(currentTime);
+        } catch (Exception e) {
+            log.error("个微SOP消息生成任务失败: {}", e.getMessage(), e);
+        }
+    }
 }