|
|
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.fs.app.taskService.SopLogsTaskService;
|
|
|
import com.fs.common.core.domain.R;
|
|
|
import com.fs.common.core.redis.RedisCache;
|
|
|
+import com.fs.common.exception.CustomException;
|
|
|
import com.fs.common.exception.base.BaseException;
|
|
|
import com.fs.common.utils.DateUtils;
|
|
|
import com.fs.common.utils.PubFun;
|
|
|
@@ -26,7 +27,14 @@ import com.fs.course.param.FsCourseLinkCreateParam;
|
|
|
import com.fs.course.service.IFsCourseFinishTempService;
|
|
|
import com.fs.course.service.IFsCourseLinkService;
|
|
|
import com.fs.course.service.IFsUserCourseService;
|
|
|
+import com.fs.feishu.domain.FeishuAccount;
|
|
|
+import com.fs.feishu.domain.FeishuLinkError;
|
|
|
+import com.fs.feishu.mapper.FeishuAccountMapper;
|
|
|
+import com.fs.feishu.mapper.FeishuLinkErrorMapper;
|
|
|
import com.fs.feishu.service.FeiShuService;
|
|
|
+import com.fs.feishu.service.FeishuClientPool;
|
|
|
+import com.fs.feishu.service.FeishuDocApiService;
|
|
|
+import com.fs.feishu.util.FeishuErrorCode;
|
|
|
import com.fs.his.domain.FsCourseReissueConfig;
|
|
|
import com.fs.his.service.IFsCourseReissueConfigService;
|
|
|
import com.fs.qw.domain.*;
|
|
|
@@ -53,6 +61,7 @@ import com.fs.system.service.ISysConfigService;
|
|
|
import com.fs.utils.ShortCodeGeneratorUtils;
|
|
|
import com.fs.voice.utils.StringUtil;
|
|
|
import com.fs.xiaoshouyi.service.XiaoShouYiTrackLinkCacheService;
|
|
|
+import com.lark.oapi.Client;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
@@ -226,6 +235,18 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
@Autowired
|
|
|
private IFsCourseReissueConfigService fsCourseReissueConfigService;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private FeishuLinkErrorMapper feishuLinkErrorMapper;
|
|
|
+
|
|
|
+ private static final String FEISHU_COMPANY_USER_KEY = "feishu:account_company_user:";
|
|
|
+ @Autowired
|
|
|
+ private FeishuAccountMapper feishuAccountMapper;
|
|
|
+ @Autowired
|
|
|
+ private FeishuDocApiService docApiService;
|
|
|
+ @Autowired
|
|
|
+ private FeishuClientPool clientPool;
|
|
|
+ @Autowired
|
|
|
+ private FsUserCourseVideoMapper fsUserCourseVideoMapper;
|
|
|
|
|
|
|
|
|
@PostConstruct
|
|
|
@@ -410,6 +431,56 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
log.info("====== SOP 用户日志处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void selectNewSopUserLogsListByTime(LocalDateTime currentTime) throws Exception {
|
|
|
+ long startTimeMillis = System.currentTimeMillis();
|
|
|
+ log.info("====== 开始选择和处理 SOP 用户日志 ======");
|
|
|
+
|
|
|
+ // 获取缓存的配置
|
|
|
+ CourseConfig config;
|
|
|
+ synchronized(configLock) {
|
|
|
+ config = cachedCourseConfig;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<SopUserLogsVo> sopUserLogsVos = sopUserLogsMapper.selectNewSopUserLogsListByTime();
|
|
|
+ if (sopUserLogsVos.isEmpty()) {
|
|
|
+ log.info("没有需要处理的 SOP 用户日志。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, List<SopUserLogsVo>> sopLogsGroupedById = sopUserLogsVos.stream()
|
|
|
+ .collect(Collectors.groupingBy(SopUserLogsVo::getSopId));
|
|
|
+
|
|
|
+ // 查询公司关联小程序数据
|
|
|
+ List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
|
|
|
+
|
|
|
+ Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
|
|
|
+
|
|
|
+
|
|
|
+ List<Company> companies = companyMapper.selectCompanyAllList();
|
|
|
+
|
|
|
+ log.info("共分组 {} 个 SOP ID 进行处理。", sopLogsGroupedById.size());
|
|
|
+
|
|
|
+ CountDownLatch sopGroupLatch = new CountDownLatch(sopLogsGroupedById.size());
|
|
|
+
|
|
|
+ for (Map.Entry<String, List<SopUserLogsVo>> entry : sopLogsGroupedById.entrySet()) {
|
|
|
+ String sopId = entry.getKey();
|
|
|
+ List<SopUserLogsVo> userLogsVos = entry.getValue();
|
|
|
+ processSopGroupAsyncNew(sopId, userLogsVos, sopGroupLatch,currentTime, new HashMap<>(),config,miniMap,companies);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待所有 SOP 分组处理完成
|
|
|
+ sopGroupLatch.await();
|
|
|
+
|
|
|
+ // 触发批量插入(可选,如果需要立即插入队列中的数据)
|
|
|
+ // batchInsertQwSopLogs();
|
|
|
+ // batchInsertFsCourseWatchLogs();
|
|
|
+
|
|
|
+ long endTimeMillis = System.currentTimeMillis();
|
|
|
+ log.info("====== SOP 用户日志处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
|
|
|
+ }
|
|
|
+
|
|
|
@Async("sopTaskExecutor")
|
|
|
@Retryable(
|
|
|
value = { Exception.class },
|
|
|
@@ -428,6 +499,24 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @Async("sopTaskExecutor")
|
|
|
+ @Retryable(
|
|
|
+ value = { Exception.class },
|
|
|
+ maxAttempts = 3,
|
|
|
+ backoff = @Backoff(delay = 2000)
|
|
|
+ )
|
|
|
+ public void processSopGroupAsyncNew(String sopId, List<SopUserLogsVo> userLogsVos, CountDownLatch latch ,LocalDateTime currentTime,
|
|
|
+ Map<String, QwGroupChat> groupChatMap,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
+ List<Company> companies) {
|
|
|
+ try {
|
|
|
+ processSopGroupNew(sopId, userLogsVos,currentTime, groupChatMap, config,miniMap,companies);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ latch.countDown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
private void processSopGroup(String sopId, List<SopUserLogsVo> userLogsVos,LocalDateTime currentTime, Map<String,
|
|
|
QwGroupChat> groupChatMap,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
@@ -487,6 +576,64 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
log.info("SOP ID {} 的所有用户日志已处理完毕。", sopId);
|
|
|
}
|
|
|
|
|
|
+ private void processSopGroupNew(String sopId, List<SopUserLogsVo> userLogsVos,LocalDateTime currentTime, Map<String,
|
|
|
+ QwGroupChat> groupChatMap,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
+ List<Company> companies) throws Exception {
|
|
|
+ QwSopRuleTimeVO ruleTimeVO = sopMapper.selectQwSopByClickHouseId(sopId);
|
|
|
+
|
|
|
+ if (ruleTimeVO == null) {
|
|
|
+// sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
|
|
|
+ log.error("SOP ID {} 已删除或不存在,相关日志已清除。", sopId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(ruleTimeVO.getTempId());
|
|
|
+ if (qwSopTemp == null) {
|
|
|
+// sopUserLogsMapper.deleteSopUserLogsBySopId(sopId);
|
|
|
+ log.error("SOP ID {} 模板不存在,相关日志已清除。", sopId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ ruleTimeVO.setTempStatus(qwSopTemp.getStatus());
|
|
|
+ ruleTimeVO.setTempGap(qwSopTemp.getGap());
|
|
|
+
|
|
|
+ if (ruleTimeVO.getStatus() == 0 || "0".equals(ruleTimeVO.getTempStatus())) {
|
|
|
+// SopUserLogs sopUserLogs = new SopUserLogs();
|
|
|
+// sopUserLogs.setSopId(sopId);
|
|
|
+// sopUserLogs.setStatus(2);
|
|
|
+// sopUserLogsMapper.updateSopUserLogsByStatus(sopUserLogs);
|
|
|
+ log.error("SOP ID {} 的状态为停用,相关日志状态已更新。", sopId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<QwSopTempRules> rulesList = qwSopTempRulesService.listByTempId(ruleTimeVO.getTempId());
|
|
|
+ if (rulesList.isEmpty()) {
|
|
|
+ log.error("SOP ID {} 的 TempSetting 为空,跳过处理。", sopId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(ruleTimeVO.getCorpId());
|
|
|
+
|
|
|
+ if (qwCompany == null ) {
|
|
|
+ log.error("SOP ID {} 的 公司信息为空 为空,跳过处理。", sopId);
|
|
|
+ return ;
|
|
|
+ }
|
|
|
+
|
|
|
+ CountDownLatch userLogsLatch = new CountDownLatch(userLogsVos.size());
|
|
|
+ for (SopUserLogsVo logVo : userLogsVos) {
|
|
|
+ processUserLogAsyncNew(logVo, ruleTimeVO, rulesList, userLogsLatch, currentTime, groupChatMap,qwCompany.getMiniAppId(),
|
|
|
+ config,miniMap,companies);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待所有用户日志处理完成
|
|
|
+ try {
|
|
|
+ userLogsLatch.await();
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ log.error("等待用户日志处理完成时被中断: {}", e.getMessage(), e);
|
|
|
+ }
|
|
|
+ log.info("SOP ID {} 的所有用户日志已处理完毕。", sopId);
|
|
|
+ }
|
|
|
+
|
|
|
@Async("sopTaskExecutor")
|
|
|
@Retryable(
|
|
|
value = { Exception.class },
|
|
|
@@ -506,6 +653,25 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @Async("sopTaskExecutor")
|
|
|
+ @Retryable(
|
|
|
+ value = { Exception.class },
|
|
|
+ maxAttempts = 3,
|
|
|
+ backoff = @Backoff(delay = 2000)
|
|
|
+ )
|
|
|
+ public void processUserLogAsyncNew(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
|
|
|
+ CountDownLatch latch, LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap,
|
|
|
+ String miniAppId,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
+ List<Company> companies) {
|
|
|
+ try {
|
|
|
+ processUserLogNew(logVo, ruleTimeVO, tempSettings,currentTime, groupChatMap, miniAppId, config,miniMap,companies);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ latch.countDown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
private void processUserLog(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
|
|
|
LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap,String miniAppId,
|
|
|
@@ -692,6 +858,181 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private void processUserLogNew(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
|
|
|
+ LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap,String miniAppId,
|
|
|
+ CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
+ List<Company> companies) {
|
|
|
+ try {
|
|
|
+
|
|
|
+ LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
|
|
|
+ LocalDate currentDate = currentTime.toLocalDate();
|
|
|
+
|
|
|
+ long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
|
|
|
+ int tempGap = ruleTimeVO.getTempGap();
|
|
|
+
|
|
|
+ if (tempGap <= 0) {
|
|
|
+ log.error("SOP ID {} 的 TempGap {} 无效,跳过处理。", logVo.getSopId(), tempGap);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int intervalDay = (int) (daysBetween / tempGap);
|
|
|
+ if (intervalDay < 0 || intervalDay >= tempSettings.size()) {
|
|
|
+ log.info("用户日志 {} 的 intervalDay {} 超出 TempSettings 范围,跳过处理。", logVo.getId(), intervalDay);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ long day = daysBetween;
|
|
|
+ if(day == 0 && ruleTimeVO.getIsAutoSop() == 1){
|
|
|
+ day = 1;
|
|
|
+ }else{
|
|
|
+ day++;
|
|
|
+ }
|
|
|
+ List<QwSopTempSetting.Content> contents = getDay(tempSettings, day);
|
|
|
+ if (contents == null || contents.isEmpty()) {
|
|
|
+ log.error("SOP ID {} 的 TempSetting 内容为空,跳过处理。天数 {}", logVo.getSopId(),day);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //获取企业微信员工的称呼//从redis里或者从库里取
|
|
|
+ QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedis(logVo.getCorpId(),logVo.getQwUserId());
|
|
|
+ if (qwUserByRedis==null){
|
|
|
+ log.error("无企微员工信息 {} 跳过处理。:{}", logVo.getUserId(),logVo.getCorpId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String qwUserId = String.valueOf(qwUserByRedis.getId()).trim();
|
|
|
+ String companyUserId = String.valueOf(qwUserByRedis.getCompanyUserId()).trim();
|
|
|
+ String companyId = String.valueOf(qwUserByRedis.getCompanyId()).trim();
|
|
|
+ Integer sendMsgType = qwUserByRedis.getSendMsgType();
|
|
|
+ Long serverId = qwUserByRedis.getServerId();
|
|
|
+
|
|
|
+ if (StringUtil.strIsNullOrEmpty(companyUserId) || StringUtil.strIsNullOrEmpty(companyId) || "null".equals(companyUserId)) {
|
|
|
+ log.error("员工未绑定销售账号或公司,跳过处理:"+qwUserId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ CompanyUser companyUser = companyUserService.selectCompanyUserByIdForRedis(Long.valueOf(companyUserId));
|
|
|
+ if (Objects.nonNull(companyUser)) {
|
|
|
+ if (!StringUtil.strIsNullOrEmpty(companyUser.getDomain())) {
|
|
|
+ logVo.setDomain(companyUser.getDomain().trim());
|
|
|
+ } else {
|
|
|
+ logVo.setDomain(config.getRealLinkDomainName().trim());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ logVo.setDomain(config.getRealLinkDomainName().trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ //寻找时间
|
|
|
+// LocalDateTime currentTime = LocalDateTime.of(2024, 12, 25,23 , 40);
|
|
|
+
|
|
|
+ // 先算好 60分钟后 ~ 再60分钟后的时间段
|
|
|
+ LocalDateTime startRangeFirst = currentTime.plusMinutes(60);
|
|
|
+
|
|
|
+ // 如果发现已经跨天
|
|
|
+ if (!startRangeFirst.toLocalDate().equals(currentDate)) {
|
|
|
+ // 更新 currentDate
|
|
|
+ currentDate = startRangeFirst.toLocalDate();
|
|
|
+
|
|
|
+ // 重新计算 daysBetween
|
|
|
+ daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
|
|
|
+ intervalDay = (int) (daysBetween / tempGap);
|
|
|
+ day = daysBetween;
|
|
|
+ if(day == 0 && ruleTimeVO.getIsAutoSop() == 1){
|
|
|
+ day = 1;
|
|
|
+ }else{
|
|
|
+ day++;
|
|
|
+ }
|
|
|
+//
|
|
|
+// // 再次验证 intervalDay 是否在范围内
|
|
|
+// if (intervalDay < 0 || intervalDay >= tempSettings.size()) {
|
|
|
+// log.info("跨天后,intervalDay={} 超出 TempSettings 范围,跳过。", intervalDay);
|
|
|
+// return;
|
|
|
+// }
|
|
|
+//
|
|
|
+// if (daysBetween % tempGap != 0) {
|
|
|
+// log.error("天数差 {} 不是 tempGap {} 的整数倍,跳过操作,SopId {} ", daysBetween, tempGap,logVo.getSopId());
|
|
|
+// return;
|
|
|
+// }
|
|
|
+
|
|
|
+ // 重新拿新的 “天” 的 Setting
|
|
|
+ contents = getDay(tempSettings, day);
|
|
|
+ if (contents == null || contents.isEmpty()) {
|
|
|
+ log.error("跨天-SOP ID {} 的 TempSetting 内容为空,跳过处理。", logVo.getSopId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 只有整倍数才做事
|
|
|
+ if (daysBetween % tempGap != 0) {
|
|
|
+ log.error("天数差 {} 不是 tempGap {} 的整数倍,跳过操作,SopId {} ", daysBetween, tempGap,logVo.getSopId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ for (QwSopTempSetting.Content content : contents) {
|
|
|
+ try {
|
|
|
+
|
|
|
+ LocalTime elementLocalTime = LocalTime.parse(content.getTime());
|
|
|
+ LocalDateTime elementDateTime = LocalDateTime.of(currentTime.toLocalDate(), elementLocalTime);
|
|
|
+
|
|
|
+ // 动态调整 elementDateTime 的日期
|
|
|
+ if (elementLocalTime.isBefore(currentTime.toLocalTime())) {
|
|
|
+ elementDateTime = elementDateTime.plusDays(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDateTime startRange = currentTime.plusMinutes(60);
|
|
|
+ LocalDateTime endRange = startRange.plusMinutes(60);
|
|
|
+
|
|
|
+ // 跨天逻辑修正:仅当 startRange 的时间晚于 endRange 的时间时调整
|
|
|
+ if (startRange.toLocalTime().isAfter(endRange.toLocalTime())
|
|
|
+ && startRange.toLocalDate().equals(endRange.toLocalDate())) {
|
|
|
+ endRange = endRange.plusDays(1); // 将 endRange 调整为第二天
|
|
|
+ }
|
|
|
+ if (!elementDateTime.isBefore(startRange) && !elementDateTime.isAfter(endRange.minusMinutes(1))) {
|
|
|
+
|
|
|
+ // 如果时间差在目标范围内,更新记录
|
|
|
+ // 组合年月日和element的时间
|
|
|
+ LocalDate targetDate = startDate.plusDays(intervalDay * tempGap);
|
|
|
+
|
|
|
+ // 将 targetDate 和 elementTime 组合成 LocalDateTime
|
|
|
+ LocalDateTime dateTime = LocalDateTime.of(targetDate, elementLocalTime);
|
|
|
+
|
|
|
+ // 将 LocalDateTime 转换为 Date
|
|
|
+ Date sendTime = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
|
|
+
|
|
|
+ SopUserLogsInfo userLogsInfo=new SopUserLogsInfo();
|
|
|
+ userLogsInfo.setSopId(logVo.getSopId());
|
|
|
+ userLogsInfo.setUserLogsId(logVo.getId());
|
|
|
+
|
|
|
+ List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoList(userLogsInfo);
|
|
|
+ if (logVo.getIsRegister() == 1) {
|
|
|
+ List<Long> externalContactIdList = PubFun.listToNewList(sopUserLogsInfos, SopUserLogsInfo::getExternalId);
|
|
|
+ if (!externalContactIdList.isEmpty()) {
|
|
|
+ List<QwExternalContact> list = qwExternalContactService.list(new QueryWrapper<QwExternalContact>().isNotNull("fs_user_id").in("id", externalContactIdList));
|
|
|
+ Map<Long, QwExternalContact> map = PubFun.listToMapByGroupObject(list, QwExternalContact::getId);
|
|
|
+ sopUserLogsInfos = sopUserLogsInfos.stream().filter(e -> map.containsKey(e.getExternalId())).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ insertSopUserLogsNew(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, qwUserId,
|
|
|
+ companyUserId, companyId, qwUserByRedis.getWelcomeText(),qwUserByRedis.getQwUserName(),
|
|
|
+ groupChatMap, miniAppId,config,miniMap, sendMsgType,companies,day, serverId);
|
|
|
+
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("解析模板内容 {} 失败: {}", content.getTime(), e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("解析解析模板 {} 失败: {}", logVo.getStartTime(), e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
private List<QwSopTempSetting.Content> getDay(List<QwSopTempRules> tempSettings, long days){
|
|
|
List<QwSopTempRules> collect = tempSettings.stream().filter(e -> e.getDayNum() == days && e.getTime() != null).collect(Collectors.toList());
|
|
|
@@ -834,6 +1175,83 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
// });
|
|
|
}
|
|
|
|
|
|
+ //消息处理
|
|
|
+ private void insertSopUserLogsNew(List<SopUserLogsInfo> sopUserLogsInfos, SopUserLogsVo logVo, Date sendTime,
|
|
|
+ QwSopRuleTimeVO ruleTimeVO, QwSopTempSetting.Content content,
|
|
|
+ String qwUserId,String companyUserId,String companyId,String welcomeText,String qwUserName,
|
|
|
+ Map<String, QwGroupChat> groupChatMap,String miniAppId,CourseConfig config,
|
|
|
+ Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
|
|
|
+ List<Company> companies,Long day,Long serverId) {
|
|
|
+ String formattedSendTime = sendTime.toInstant()
|
|
|
+ .atZone(ZoneId.systemDefault())
|
|
|
+ .format(DATE_TIME_FORMATTER);
|
|
|
+ int type = content.getType();
|
|
|
+ Long courseId = content.getCourseId();
|
|
|
+ Long videoId = content.getVideoId();
|
|
|
+ Integer isOfficial = content.getIsOfficial() != null ? Integer.valueOf(content.getIsOfficial()) : 0;
|
|
|
+
|
|
|
+
|
|
|
+ // 发送语音 start
|
|
|
+ if(content.getSetting() == null){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ List<QwSopTempSetting.Content.Setting> setting = content.getSetting().stream().filter(e -> "7".equals(e.getContentType())).collect(Collectors.toList());
|
|
|
+ if (!setting.isEmpty()) {
|
|
|
+ List<String> valuesList = PubFun.listToNewList(setting, QwSopTempSetting.Content.Setting::getValue);
|
|
|
+ if (valuesList != null && !valuesList.isEmpty()) {
|
|
|
+ try {
|
|
|
+ List<QwSopTempVoice> voiceList = qwSopTempVoiceService.getVoiceByText(Long.parseLong(companyUserId), valuesList);
|
|
|
+ if (voiceList != null && !voiceList.isEmpty()) {
|
|
|
+ Map<String, QwSopTempVoice> collect = voiceList.stream().collect(Collectors.toMap(QwSopTempVoice::getVoiceTxt, e -> e));
|
|
|
+ setting.parallelStream().filter(e -> "7".equals(e.getContentType())).forEach(st -> {
|
|
|
+ QwSopTempVoice voice = collect.get(st.getValue());
|
|
|
+ if (voice.getVoiceUrl() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ st.setVoiceUrl(voice.getVoiceUrl());
|
|
|
+ st.setVoiceDuration(voice.getDuration() + "");
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+// // 发送语音 end
|
|
|
+ if (content.getType()==5){
|
|
|
+ sopAddTag(logVo,content,sendTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ //发送销售医生任务
|
|
|
+// if (content.getType()==6){
|
|
|
+// addSopOrderLog(logVo,content,sendTime);
|
|
|
+// }
|
|
|
+
|
|
|
+ //当语音模板的qw_sop_temp_voice中无对应语音,就不生成qw_sop_logs记录
|
|
|
+ if (content.getType() == 7 && content.getSetting() != null && !content.getSetting().isEmpty()) {
|
|
|
+ if (content.getSetting().get(0).getVoiceUrl() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 处理每个 externalContactId
|
|
|
+ sopUserLogsInfos.forEach(contactId -> {
|
|
|
+ try {
|
|
|
+ String externalId = contactId.getExternalId().toString();
|
|
|
+ String externalUserName = contactId.getExternalUserName();
|
|
|
+ Long fsUserId = contactId.getFsUserId();
|
|
|
+ Integer grade = contactId.getGrade();
|
|
|
+ QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId, isOfficial, contactId.getExternalId(),contactId.getIsDaysNotStudy());
|
|
|
+ handleLogBasedOnTypeNew(sopLogs, content, logVo, sendTime, courseId, videoId,
|
|
|
+ type, qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId, false, miniAppId,
|
|
|
+ null,config, miniMap, grade, sendMsgType,companies,day,serverId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
private void sopAddTag(SopUserLogsVo logVo, QwSopTempSetting.Content content, Date sendTime) {
|
|
|
String id = logVo.getId();
|
|
|
String addTag = content.getAddTag();
|
|
|
@@ -970,6 +1388,23 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+
|
|
|
+ private void handleLogBasedOnTypeNew(QwSopLogs sopLogs, QwSopTempSetting.Content content,
|
|
|
+ SopUserLogsVo logVo, Date sendTime, Long courseId, Long videoId, int type, String qwUserId,
|
|
|
+ String companyUserId, String companyId, String externalId, String welcomeText,
|
|
|
+ String qwUserName, Long fsUserId, boolean isGroupChat, String miniAppId,
|
|
|
+ QwGroupChat groupChat,CourseConfig config,
|
|
|
+ Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
+ Integer grade, Integer sendMsgType ,List<Company> companies,Long day,Long serverId) {
|
|
|
+ if (type == 2) {
|
|
|
+ handleCourseMessageNew(sopLogs, content, logVo, sendTime, courseId, videoId,
|
|
|
+ qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId,
|
|
|
+ isGroupChat, miniAppId, groupChat, config, miniMap, grade, sendMsgType, companies, day, serverId);
|
|
|
+ } else {
|
|
|
+ log.error("未知的消息类型 {},跳过处理。", type);
|
|
|
+ }
|
|
|
+ }
|
|
|
private void handleVoiceMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content, String companyUserId) {
|
|
|
sopLogs.setContentJson(JSON.toJSONString(content));
|
|
|
enqueueQwSopLogs(sopLogs);
|
|
|
@@ -1289,6 +1724,208 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
enqueueQwSopLogs(sopLogs);
|
|
|
}
|
|
|
|
|
|
+ private void handleCourseMessageNew(QwSopLogs sopLogs, QwSopTempSetting.Content content,
|
|
|
+ SopUserLogsVo logVo, Date sendTime, Long courseId, Long videoId, String qwUserId, String companyUserId,
|
|
|
+ String companyId, String externalId, String welcomeText, String qwUserName,
|
|
|
+ Long fsUserId, boolean isGroupChat, String miniAppId, QwGroupChat groupChat,CourseConfig config,Map<Long,
|
|
|
+ Map<Integer, List<CompanyMiniapp>>> miniMap,Integer grade, Integer sendMsgType,
|
|
|
+ List<Company> companies,Long day,Long serverId) {
|
|
|
+ // 深拷贝 Content 对象,避免使用 JSON
|
|
|
+ QwSopTempSetting.Content clonedContent = deepCopyContent(content);
|
|
|
+ if (clonedContent == null) {
|
|
|
+ log.error("Failed to clone content, skipping handleCourseMessage.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Long msgNum = Long.valueOf(generateRandomNumberWithLock());
|
|
|
+ sopLogs.setSmsLogsId(msgNum);
|
|
|
+
|
|
|
+ AtomicInteger index = new AtomicInteger(0);
|
|
|
+
|
|
|
+ String isOfficial = clonedContent.getIsOfficial();
|
|
|
+
|
|
|
+ List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
|
|
|
+ if (settings == null || settings.isEmpty()) {
|
|
|
+ log.error("Cloned content settings are empty, skipping.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
|
|
|
+
|
|
|
+ // 顺序处理每个 Setting,避免过多的并行导致线程开销
|
|
|
+ for (QwSopTempSetting.Content.Setting setting : settings) {
|
|
|
+
|
|
|
+ Integer currentIndex = index.getAndIncrement();
|
|
|
+ //后台SOP发课配置
|
|
|
+ //createSetting(setting);
|
|
|
+
|
|
|
+ switch (setting.getContentType()) {
|
|
|
+
|
|
|
+ case "18" :
|
|
|
+ //飞书看课链接
|
|
|
+ Long companyIdLong = Long.parseLong(companyId);
|
|
|
+ Long companyUserIdLong = Long.parseLong(companyUserId);
|
|
|
+ long externalIdLong = Long.parseLong(externalId);
|
|
|
+
|
|
|
+
|
|
|
+ String key = "fei_shu_doc_id:" + externalIdLong + "_" + videoId;
|
|
|
+ String feiShuDocId = redisCache.getCacheObject(key);
|
|
|
+ if (org.apache.commons.lang3.StringUtils.isBlank(feiShuDocId)) {
|
|
|
+ feiShuDocId = getFeiShuDocId(video, companyIdLong, companyUserIdLong);
|
|
|
+ if (org.apache.commons.lang3.StringUtils.isNotBlank(feiShuDocId)) {
|
|
|
+ redisCache.setCacheObject("fei_shu_doc_id:" + externalIdLong + "_" + video.getVideoId(), feiShuDocId,1,TimeUnit.DAYS);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case "19" :
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ sopLogs.setContentJson(JSON.toJSONString(clonedContent));
|
|
|
+ enqueueQwSopLogs(sopLogs);
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<FeishuAccount> getFeiShuAccountCompanyUser(Long companyId, Long companyUserId) {
|
|
|
+ String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
|
|
|
+ String cached = redisCache.getCacheObject(cacheKey);
|
|
|
+ if (org.apache.commons.lang3.StringUtils.isNotBlank(cached)) {
|
|
|
+ try {
|
|
|
+ return JSONUtil.parseArray(cached).toList(FeishuAccount.class);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("缓存数据解析失败,key: {}, 将清除缓存", cacheKey, e);
|
|
|
+ redisCache.deleteObject(cacheKey);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ FeishuAccount account = new FeishuAccount();
|
|
|
+ account.setCompanyId(companyId);
|
|
|
+ account.setCompanyUserId(companyUserId);
|
|
|
+ account.setStatus(1);
|
|
|
+ List<FeishuAccount> feiShuAccounts = feishuAccountMapper.selectFeishuAccountList(account);
|
|
|
+ if (feiShuAccounts.isEmpty()) {
|
|
|
+ redisCache.setCacheObject(cacheKey, "[]", 1, TimeUnit.MINUTES);
|
|
|
+ } else {
|
|
|
+ redisCache.setCacheObject(cacheKey, JSONUtil.toJsonStr(feiShuAccounts), 1, TimeUnit.DAYS);
|
|
|
+ }
|
|
|
+ return feiShuAccounts;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void disablePersonalAccount(FeishuAccount account, String errorMsg) {
|
|
|
+ feishuAccountMapper.updateStatusAndErrorMsg(account.getId(), 0, errorMsg);
|
|
|
+ clearCompanyUserCache(account.getCompanyId(), account.getCompanyUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void clearCompanyUserCache(Long companyId, Long companyUserId) {
|
|
|
+ if (companyId != null && companyUserId != null) {
|
|
|
+ String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
|
|
|
+ redisCache.deleteObject(cacheKey);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private CourseConfig getCourseConfig() {
|
|
|
+ String json = sysConfigService.selectConfigByKey("course.config");
|
|
|
+ if (org.apache.commons.lang3.StringUtils.isBlank(json)) {
|
|
|
+ throw new CustomException("飞书看课配置为空");
|
|
|
+ }
|
|
|
+ return JSONUtil.toBean(json, CourseConfig.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Client buildClient(FeishuAccount account) {
|
|
|
+ return Client.newBuilder(account.getAppId(), account.getAppSecret())
|
|
|
+ .logReqAtDebug(true)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getFeiShuDocId(FsUserCourseVideo video, Long companyId,
|
|
|
+ Long companyUserId) {
|
|
|
+
|
|
|
+
|
|
|
+ // 1. 尝试销售个人账号创建文档
|
|
|
+ List<FeishuAccount> personalAccounts = getFeiShuAccountCompanyUser(companyId, companyUserId);
|
|
|
+ if (!personalAccounts.isEmpty()) {
|
|
|
+ for (FeishuAccount account : personalAccounts) {
|
|
|
+ if (account.getStatus() != 1) continue;
|
|
|
+ Client client = buildClient(account);
|
|
|
+ try {
|
|
|
+ String docId = docApiService.createDocument(client, video.getTitle() + "-注册");
|
|
|
+ redisCache.setCacheObject("fei_shu_client:" + docId, account,1,TimeUnit.DAYS);
|
|
|
+ return docId;
|
|
|
+
|
|
|
+ } catch (CustomException e) {
|
|
|
+ Integer code = e.getCode();
|
|
|
+ if (code == null) {
|
|
|
+ recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (FeishuErrorCode.isAccountDisable(code)) {
|
|
|
+ disablePersonalAccount(account, e.getMessage());
|
|
|
+ log.warn("个人账号[{}]被禁用,尝试下一个", account.getAppId());
|
|
|
+ } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
|
|
|
+ log.warn("个人账号[{}]可重试错误,切换下一个", account.getAppId());
|
|
|
+ } else {
|
|
|
+ // 业务错误或其他,记录失败并返回
|
|
|
+ recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.warn("所有个人账号失败,降级到公共账号池");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 公共账号池
|
|
|
+ int retries = clientPool.getAvailableCount();
|
|
|
+ for (int i = 0; i < retries; i++) {
|
|
|
+ FeishuClientPool.ClientWrapper wrapper = clientPool.getClientWrapper();
|
|
|
+ Client client = wrapper.getClient();
|
|
|
+ FeishuAccount account = wrapper.getAccount();
|
|
|
+ try {
|
|
|
+ String docId = docApiService.createDocument(client, video.getTitle() + "-注册");
|
|
|
+ redisCache.setCacheObject("fei_shu_client:" + docId, account);
|
|
|
+ return docId;
|
|
|
+
|
|
|
+ } catch (CustomException e) {
|
|
|
+ Integer code = e.getCode();
|
|
|
+ if (code == null) {
|
|
|
+ recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (FeishuErrorCode.isAccountDisable(code)) {
|
|
|
+ clientPool.disableAccount(account, e.getMessage());
|
|
|
+ log.warn("公共账号被禁用,尝试下一个");
|
|
|
+ } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
|
|
|
+ log.warn("公共账号可重试错误,切换下一个");
|
|
|
+ } else {
|
|
|
+ recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", "所有公共账号不可用", 1);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void recordLinkError(Long videoId, Long companyId, Long courseId,
|
|
|
+ Long companyUserId, String link, String errorInfo, Integer type) {
|
|
|
+ FeishuLinkError error = new FeishuLinkError();
|
|
|
+ error.setVideoId(videoId);
|
|
|
+ error.setCompanyId(companyId);
|
|
|
+ error.setCourseId(courseId);
|
|
|
+ error.setCompanyUserId(companyUserId);
|
|
|
+ error.setLink(link);
|
|
|
+ error.setErrorInfo(errorInfo);
|
|
|
+ error.setCreateTime(DateUtils.getNowDate());
|
|
|
+ error.setType(type);
|
|
|
+ feishuLinkErrorMapper.insertFeishuLinkError(error);
|
|
|
+ log.info("飞书链接创建失败已记录,videoId={}, link={}, error={}", videoId, link, errorInfo);
|
|
|
+ }
|
|
|
+
|
|
|
private Long getCourseProjectType(Long courseId){
|
|
|
|
|
|
String courseKey="h5user:course:project:" + courseId;
|