Selaa lähdekoodia

个微SOP定时任务

吴树波 1 päivä sitten
vanhempi
commit
9bfaaabe51

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

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

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

+ 133 - 11
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=";
@@ -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);
+        }
+    }
 }