فهرست منبع

从润天迁移 短信发送

三七 1 روز پیش
والد
کامیت
43c4d9640b
23فایلهای تغییر یافته به همراه1840 افزوده شده و 32 حذف شده
  1. 545 0
      fs-ipad-task/src/main/java/com/fs/app/task/SendSmsMsg.java
  2. 113 11
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  3. 3 0
      fs-service/src/main/java/com/fs/common/service/ISmsService.java
  4. 104 0
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  5. 23 0
      fs-service/src/main/java/com/fs/company/domain/CompanySmsLogs.java
  6. 44 0
      fs-service/src/main/java/com/fs/course/utils/LinkUtil.java
  7. 14 0
      fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java
  8. 6 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  9. 8 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  10. 5 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  11. 1 0
      fs-service/src/main/java/com/fs/qw/domain/QwIpadServer.java
  12. 83 0
      fs-service/src/main/java/com/fs/qw/domain/QwSopSmsLogs.java
  13. 131 0
      fs-service/src/main/java/com/fs/qw/mapper/QwSopSmsLogsMapper.java
  14. 108 0
      fs-service/src/main/java/com/fs/qw/service/IQwSopSmsLogsService.java
  15. 143 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwSopSmsLogsServiceImpl.java
  16. 6 1
      fs-service/src/main/java/com/fs/sop/domain/QwSopLogs.java
  17. 10 0
      fs-service/src/main/java/com/fs/sop/mapper/QwSopLogsMapper.java
  18. 115 15
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  19. 35 0
      fs-service/src/main/java/com/fs/sop/vo/SopLogsResult.java
  20. 10 1
      fs-service/src/main/resources/mapper/company/CompanySmsLogsMapper.xml
  21. 7 0
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  22. 244 0
      fs-service/src/main/resources/mapper/qw/QwSopSmsLogsMapper.xml
  23. 82 4
      fs-service/src/main/resources/mapper/sop/QwSopLogsMapper.xml

+ 545 - 0
fs-ipad-task/src/main/java/com/fs/app/task/SendSmsMsg.java

@@ -0,0 +1,545 @@
+package com.fs.app.task;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.service.ISmsService;
+import com.fs.common.utils.date.DateUtil;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.his.domain.FsUser;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.qw.domain.QwIpadServer;
+import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.qw.mapper.QwIpadServerMapper;
+import com.fs.qw.service.IQwSopSmsLogsService;
+import com.fs.qw.vo.QwSopCourseFinishTempSetting;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.sop.service.IQwSopLogsService;
+import com.fs.sop.service.impl.QwSopLogsServiceImpl;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.util.concurrent.RateLimiter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.PreDestroy;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+/**
+ * 短信发送服务
+ */
+@Component
+@Slf4j
+public class SendSmsMsg {
+
+    @Value("${group-no}")
+    private String groupNo;
+
+    private final RedisCache redisCache;
+
+    private final ISmsService smsService;
+
+    private final IFsUserService fsUserService;
+
+    private final QwSopLogsMapper qwSopLogsMapper;
+
+    private final QwIpadServerMapper qwIpadServerMapper;
+
+    private final IQwSopSmsLogsService qwSopSmsLogsService;
+
+    private final IQwSopLogsService qwSopLogsService;
+
+    private final IFsCourseWatchLogService watchLogService;
+
+    // 线程池配置
+    private static final int CORE_POOL_SIZE = 50;
+    private static final int MAX_POOL_SIZE = 200;
+    private static final int QUEUE_CAPACITY = 1000;
+    private static final long KEEP_ALIVE_TIME = 60L;
+
+    // 分页大小
+    private static final int PAGE_SIZE = 5000;
+
+    // 手机号缓存
+    private final Cache<Long, String> phoneCache = CacheBuilder.newBuilder()
+            .expireAfterWrite(1, TimeUnit.HOURS)
+            .maximumSize(100000)
+            .build();
+
+    // 限流器:控制全局发送速率
+    private final RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒1000条
+
+    // 短信发送线程池
+    private final ThreadPoolExecutor smsExecutor = new ThreadPoolExecutor(
+            CORE_POOL_SIZE,
+            MAX_POOL_SIZE,
+            KEEP_ALIVE_TIME,
+            TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(QUEUE_CAPACITY),
+            new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行,避免任务丢失
+    );
+
+    private final ExecutorService statusExecutor = Executors.newSingleThreadExecutor();
+
+    // 批量失败状态更新阈值
+    private static final int BATCH_UPDATE_SIZE = 500;
+
+    public SendSmsMsg(QwIpadServerMapper qwIpadServerMapper,
+                      IQwSopSmsLogsService qwSopSmsLogsService,
+                      @Lazy ISmsService smsService,
+                      IFsUserService fsUserService, RedisCache redisCache,
+                      QwSopLogsMapper qwSopLogsMapper,
+                      IQwSopLogsService qwSopLogsService,
+                      IFsCourseWatchLogService watchLogService) {
+        this.qwIpadServerMapper = qwIpadServerMapper;
+        this.qwSopSmsLogsService = qwSopSmsLogsService;
+        this.smsService = smsService;
+        this.fsUserService = fsUserService;
+        this.redisCache = redisCache;
+        this.qwSopLogsMapper = qwSopLogsMapper;
+        this.qwSopLogsService = qwSopLogsService;
+        this.watchLogService = watchLogService;
+    }
+
+    @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次
+    public synchronized void sendSms() {
+        sendSms(null);
+    }
+
+
+    public void sendSms(String groupNum) {
+        if (groupNum != null) {
+            groupNo = groupNum;
+        }
+        long startTime = System.currentTimeMillis();
+        log.info("sendSms: 开始执行,groupNo={}", groupNo);
+
+        if (StringUtils.isEmpty(groupNo)) {
+            log.warn("sendSms: groupNo 为空,跳过执行");
+            return;
+        }
+
+        long groupOn;
+        try {
+            groupOn = Long.parseLong(groupNo.trim());
+        } catch (NumberFormatException e) {
+            log.warn("sendSms: groupNo 无法转为数字, groupNo={}", groupNo);
+            return;
+        }
+
+        // 获取server_ids
+        List<Long> serverIds = getServerIds(groupOn);
+        if (serverIds.isEmpty()) {
+            log.info("sendSms: 分组 groupNo={} 无 server,跳过", groupNo);
+            return;
+        }
+
+        // 计算时间范围
+        TimeRange timeRange = calculateTimeRange();
+
+        long lastId = 0L;
+        AtomicLong totalProcessed = new AtomicLong(0);
+        AtomicLong totalFailed = new AtomicLong(0);
+
+        // 收集所有任务的Future
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+
+        while (true) {
+            // 分页查询待发送短信
+            List<QwSopSmsLogs> page = qwSopSmsLogsService.selectPendingSmsByGroupAndTimePage(
+                    serverIds, "0", timeRange.endTime, lastId, PAGE_SIZE);
+
+            if (page == null || page.isEmpty()) {
+                break;
+            }
+
+            // 批量更新状态为“发送中”
+            List<Long> batchIds = page.stream().map(QwSopSmsLogs::getId).collect(Collectors.toList());
+            qwSopSmsLogsService.updateStatusByIds(batchIds, "1");
+
+            //获取执行记录数据
+            List<QwSopLogs> sopLogs = qwSopLogsMapper.getQwSopInfoByUid(page.stream().map(QwSopSmsLogs::getSopLogId).collect(Collectors.toList()));
+            Map<Long, QwSopLogs> sopLogsMap = sopLogs.stream().collect(Collectors.toMap(QwSopLogs::getSmsLogsId,s->s));
+            // 按server_id分组
+            Map<Long, List<QwSopSmsLogs>> groupByServer = page.stream()
+                    .collect(Collectors.groupingBy(QwSopSmsLogs::getServerId));
+
+            // 为每个server的数据创建独立任务
+            for (Map.Entry<Long, List<QwSopSmsLogs>> entry : groupByServer.entrySet()) {
+                List<QwSopSmsLogs> serverBatch = entry.getValue();
+                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+                    try {
+                        SendResult result = processServerBatch(serverBatch,sopLogsMap);
+                        totalProcessed.addAndGet(result.success);
+                        totalFailed.addAndGet(result.failed);
+                    } catch (Exception e) {
+                        log.error("处理server {} 批次失败", entry.getKey(), e);
+                    }
+                }, smsExecutor);
+                futures.add(future);
+            }
+
+            lastId = page.get(page.size() - 1).getId();
+
+            if (page.size() < PAGE_SIZE) {
+                break;
+            }
+
+            // 每处理10页,等待当前任务完成,避免内存堆积
+            if (futures.size() >= 10) {
+                CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+                futures.clear();
+                log.info("sendSms: 已处理 {} 条,失败 {} 条,继续...", totalProcessed.get(), totalFailed.get());
+            }
+        }
+
+        // 等待所有剩余任务完成
+        if (!futures.isEmpty()) {
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+
+        long cost = System.currentTimeMillis() - startTime;
+        log.info("sendSms: 分组 groupNo={} 处理完成, 成功: {}, 失败: {}, 耗时: {}ms",
+                groupNo, totalProcessed.get(), totalFailed.get(), cost);
+    }
+
+    /**
+     * 获取分组下的所有server ID
+     */
+    private List<Long> getServerIds(long groupOn) {
+        List<QwIpadServer> serverList = qwIpadServerMapper.selectList(
+                new LambdaQueryWrapper<QwIpadServer>()
+                        .select(QwIpadServer::getId)
+                        .eq(QwIpadServer::getGroupNo, groupOn));
+        return serverList.stream().map(QwIpadServer::getId).collect(Collectors.toList());
+    }
+
+    /**
+     * 计算查询时间范围
+     */
+    private TimeRange calculateTimeRange() {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime nextHourStart = now.withMinute(0).withSecond(0).withNano(0).plusHours(1);
+        LocalDateTime startOfDay = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
+        Date startTime = Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
+        Date endTime = Date.from(nextHourStart.atZone(ZoneId.systemDefault()).toInstant());
+        return new TimeRange(startTime, endTime);
+    }
+
+    /**
+     * 处理单个server的一批短信(同步发送,避免线程爆炸)
+     */
+    private SendResult processServerBatch(List<QwSopSmsLogs> batch,Map<Long, QwSopLogs> sopLogsMap) {
+        int success = 0;
+        List<SendResultDetailDTO> failReasonsList = Collections.synchronizedList(new ArrayList<>());
+        List<Long> failedIds = Collections.synchronizedList(new ArrayList<>());
+        // 批量获取手机号
+        Map<Long, String> userPhoneMap = getPhoneNumbers(batch);
+
+        for (QwSopSmsLogs logRecord : batch) {
+            rateLimiter.acquire();
+            //数据走数据校验
+            if(!sopLogsMap.containsKey(logRecord.getSopLogId())){
+                log.error("处理sopLogId {} 失败,sopLogsMap 中不存在执行记录", logRecord.getSopLogId());
+                continue;
+            }
+            QwSopLogs qwSopLogs = sopLogsMap.get(logRecord.getSopLogId());
+            QwSopCourseFinishTempSetting setting = JSON.parseObject(qwSopLogs.getContentJson(), QwSopCourseFinishTempSetting.class);
+            // 判断消息状态是否满足发送条件
+            if (!isSendLogs(qwSopLogs, setting)) {
+                log.info("销售:{}, 消息发送条件未满足:{}", qwSopLogs.getQwUserKey(), qwSopLogs.getId());
+                continue;
+            }
+
+            String redisKey = groupNo + ":" + logRecord.getId();
+            try {
+                redisCache.setCacheObject(redisKey, logRecord.getId(), 2, TimeUnit.HOURS);
+                SendResultDetailDTO detail = sendSingleSms(logRecord, userPhoneMap, redisKey);
+                if (detail.isSuccess()) {
+                    success++;
+                } else {
+                    redisCache.deleteObject(redisKey);
+                    failReasonsList.add(detail);
+                    failedIds.add(logRecord.getId());
+                }
+            } catch (Exception e) {
+                log.error("发送异常 id={}", logRecord.getId(), e);
+                failReasonsList.add(new SendResultDetailDTO(false, e.getMessage(), logRecord.getSopLogId()));
+                failedIds.add(logRecord.getId());
+                redisCache.deleteObject(redisKey);
+            }
+
+            //批量阈值
+            if (failReasonsList.size() >= BATCH_UPDATE_SIZE) {
+                submitFailedUpdate(failedIds, failReasonsList, sopLogsMap);
+                failReasonsList.clear();
+                failedIds.clear();
+            }
+        }
+
+        // 提交剩余的失败记录
+        if (!failReasonsList.isEmpty()) {
+            submitFailedUpdate(failedIds, failReasonsList,sopLogsMap);
+        }
+
+        return new SendResult(success, failedIds.size(), failedIds);
+    }
+
+    /**
+     * 获取手机号映射(缓存+批量查询)
+     */
+    private Map<Long, String> getPhoneNumbers(List<QwSopSmsLogs> batch) {
+        // 收集所有用户ID
+        Set<Long> userIds = batch.stream()
+                .map(QwSopSmsLogs::getFsUserId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        Map<Long, String> result = new HashMap<>();
+
+        // 先从缓存获取
+        Set<Long> missingUserIds = new HashSet<>();
+        for (Long userId : userIds) {
+            String phone = phoneCache.getIfPresent(userId);
+            if (phone != null) {
+                result.put(userId, phone);
+            } else {
+                missingUserIds.add(userId);
+            }
+        }
+
+        // 批量查询缺失的手机号
+        if (!missingUserIds.isEmpty()) {
+            List<FsUser> userList = fsUserService.selectUserListByUserIds(new ArrayList<>(missingUserIds));
+            for (FsUser user : userList) {
+                if (user.getPhone() != null) {
+                    result.put(user.getUserId(), user.getPhone());
+                    phoneCache.put(user.getUserId(), user.getPhone()); // 放入缓存
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 发送单条短信,返回是否成功
+     */
+    private SendResultDetailDTO sendSingleSms(QwSopSmsLogs logRecord, Map<Long, String> userPhoneMap, String redisKey) {
+        String phone = userPhoneMap.get(logRecord.getFsUserId());
+        String content = logRecord.getContent();
+
+        if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(content)) {
+            String reason = StringUtils.isEmpty(phone) ? "手机号为空" : "内容为空";
+            log.warn("记录 id={} {},跳过", logRecord.getId(), reason);
+            return new SendResultDetailDTO(false, reason, logRecord.getSopLogId());
+        }
+
+        try {
+            R r = smsService.sendUrl(
+                    PhoneUtil.decryptPhone(phone),
+                    content,
+                    logRecord.getSmsTemplateCode(),
+                    logRecord.getSopLogId(),
+                    logRecord.getSmsIndex(),
+                    redisKey
+            );
+
+            if (r != null && "200".equals(String.valueOf(r.get("code")))) {
+                return new SendResultDetailDTO(true, null, null);
+            } else {
+                String msg = r != null && r.get("msg") != null ? r.get("msg").toString() : "未知错误";
+                log.warn("短信发送失败 id={}, phone={}, msg={}", logRecord.getId(), phone, msg);
+                return new SendResultDetailDTO(false, msg, logRecord.getSopLogId());
+            }
+        } catch (Exception e) {
+            log.error("发送异常 id=" + logRecord.getId(), e);
+            return new SendResultDetailDTO(false, e.getMessage(), logRecord.getSopLogId());
+        }
+    }
+
+    /**
+     * 异步批量更新失败状态
+     */
+    private void submitFailedUpdate(List<Long> failedIds, List<SendResultDetailDTO> failReasonsList,Map<Long, QwSopLogs> sopLogsMap) {
+        statusExecutor.submit(() -> {
+            try {
+                qwSopSmsLogsService.updateStatusByIds(failedIds, "3"); // 失败状态
+                if (!failReasonsList.isEmpty()) {
+                    //组装数据
+                    List<QwSopLogs> updateList = failReasonsList.stream().map(f -> {
+                        QwSopLogs update = new QwSopLogs();
+                        update.setSmsLogsId(f.getSopLogId());
+                        update.setRemark(f.getFailReason());
+                        //同步json发送状态
+                        if (sopLogsMap != null && sopLogsMap.containsKey(f.getSopLogId())) {
+                            try {
+                                String contentJson = sopLogsMap.get(f.getSopLogId()).getContentJson();
+                                JSONObject jsonObject = JSONObject.parseObject(contentJson);
+                                JSONArray settingArray = jsonObject.getJSONArray("setting");
+                                if (settingArray != null && !settingArray.isEmpty()) {
+                                    for (int i = 0; i < settingArray.size(); i++) {
+                                        JSONObject item = settingArray.getJSONObject(i);
+                                        item.put("sendStatus", "0");
+                                    }
+                                    update.setContentJson(jsonObject.toJSONString());
+                                }
+                            } catch (Exception e) {
+                                log.warn("解析ContentJson失败,sopLogId:{}", f.getSopLogId(), e);
+                                update.setContentJson(sopLogsMap.get(f.getSopLogId()).getContentJson());
+                            }
+                        }
+                        return update;
+                    }).collect(Collectors.toList());
+
+                    //批量同步执行记录表
+                    qwSopLogsMapper.batchUpdateQwSopLogsBySmsLogsId(updateList);
+                }
+                log.debug("批量更新失败状态完成,条数:{}", failedIds.size());
+            } catch (Exception e) {
+                log.error("批量更新失败状态异常", e);
+            }
+        });
+    }
+
+    @PreDestroy
+    public void destroy() {
+        log.info("SmsSendService 正在关闭...");
+        smsExecutor.shutdown();
+        statusExecutor.shutdown();
+        try {
+            if (!smsExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                smsExecutor.shutdownNow();
+            }
+            if (!statusExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+                statusExecutor.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            smsExecutor.shutdownNow();
+            statusExecutor.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+        log.info("SmsSendService 已关闭");
+    }
+
+    /**
+     * 时间范围内部类
+     */
+    private static class TimeRange {
+        Date endTime;
+
+        TimeRange(Date startTime, Date endTime) {
+            this.endTime = endTime;
+        }
+    }
+
+    /**
+     * 发送结果内部类
+     */
+    @lombok.Data
+    @lombok.AllArgsConstructor
+    private static class SendResult {
+        private int success;
+        private int failed;
+        private List<Long> failedIds;
+    }
+
+
+    //处理特定场景,电脑被重启后,发送短信任务会丢失
+
+
+
+    public boolean isSendLogs(QwSopLogs qwSopLogs, QwSopCourseFinishTempSetting setting) {
+        Long qwUserId = qwSopLogs.getQwUserKey();
+        if(qwSopLogs.getSendStatus() != 3){
+            log.info("状态异常不发送:{}, LOGID: {}", qwSopLogs.getQwUserKey(), qwSopLogs.getId());
+            return false;
+        }
+
+        boolean noSop = qwSopLogs.getSendType() != 3 && qwSopLogs.getSendType() != 7;
+
+        if (qwSopLogs.getExpiryTime() == null && noSop) {
+            // 作废消息
+            log.info("SOP_LOG_ID:{}, SOP任务被删除", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "SOP任务被删除");
+            return false;
+        }
+
+        LocalDateTime sendTime = DateUtil.stringToLocalDateTime(qwSopLogs.getSendTime());
+        LocalDateTime expiryDateTime;
+
+        // 判断是否过期
+        if(qwSopLogs.getSendType() == 3 || qwSopLogs.getSendType() == 7){
+            expiryDateTime = sendTime.plusHours(12);
+        }else{
+            expiryDateTime = sendTime.plusHours(qwSopLogs.getExpiryTime());
+        }
+
+        if (LocalDateTime.now().isAfter(expiryDateTime)  ) {
+            // 作废消息
+            log.info("SOP_LOG_ID:{}, 已过期,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "已过期,不发送");
+            return false;
+        }
+
+        if (setting.getCourseType() == null && noSop && setting.getType() == 2) {
+            log.info("SOP_LOG_ID:{}, 模板未选消息类型,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "模板未选消息类型,不发送");
+            return false;
+        }
+        Integer cacheValue = redisCache.getCacheObject("sopCourse:video:isPause:" + setting.getVideoId());
+        int isPause = (cacheValue != null) ? cacheValue : 0;
+        log.info("SOP_LOG_ID:{},判断课程({})当前状态:{}", qwSopLogs.getId(), setting.getVideoId(), isPause);
+        if (isPause == 1){
+            log.info("SOP_LOG_ID:{}, 课程暂停,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "课程暂停,AI不发送");
+            return false;
+        }
+
+        if (qwSopLogs.getSendType() != 12 && noSop) {
+            // 客户的信息
+            Integer courseType = setting.getCourseType();
+            if (setting.getType() == 2 && courseType != 0) {// 课程消息,进行复杂的条件判断
+                FsCourseWatchLog watchLog = watchLogService.getWatchCourseLogVideoBySop(
+                        setting.getVideoId().longValue(),
+                        String.valueOf(qwUserId),
+                        qwSopLogs.getExternalId()
+                );
+                log.debug("ID:{}-看课记录参数:videoID:{}, qwUserID:{}, extID:{}", qwSopLogs.getId(), setting.getVideoId().longValue(), qwUserId, qwSopLogs.getExternalId());
+                log.debug("ID:{}-看课记录:{}", qwSopLogs.getId(), watchLog);
+                String logId = qwSopLogs.getId();
+                if (watchLog != null) {
+                    //新逻辑
+                    if (!QwSopLogsServiceImpl.isCourseTypeValid(courseType, watchLog.getLogType())) {
+                        // 作废消息
+                        log.info("SOP_LOG_ID:{}, 看课状态未满足,不发送", qwSopLogs.getId());
+                        qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "看课状态未满足,不发送");
+                        return false;
+                    }
+                } else {
+                    log.info("SOP_LOG_ID:{}, 无观看记录,不发送", qwSopLogs.getId());
+                    qwSopLogsService.updateQwSopLogsByWatchLogType(logId, "无观看记录,不发送");
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+}

+ 113 - 11
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -36,6 +36,7 @@ import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwGroupChatService;
 import com.fs.qw.service.IQwGroupChatService;
 import com.fs.qw.service.IQwGroupChatUserService;
 import com.fs.qw.service.IQwGroupChatUserService;
+import com.fs.qw.service.IQwSopSmsLogsService;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.vo.GroupUserExternalVo;
 import com.fs.qw.vo.GroupUserExternalVo;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
@@ -74,6 +75,7 @@ import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
+import static com.fs.course.utils.LinkUtil.generateRandomNumberWithLock;
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 
 
 @Service
 @Service
@@ -132,6 +134,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Autowired
     @Autowired
     private IQwSopLogsService qwSopLogsService;
     private IQwSopLogsService qwSopLogsService;
 
 
+    @Autowired
+    private IQwSopSmsLogsService qwSopSmsLogsService;
+
     @Autowired
     @Autowired
     private QwSopLogsMapper qwSopLogsMapper;
     private QwSopLogsMapper qwSopLogsMapper;
 
 
@@ -177,6 +182,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
     // Blocking queues with bounded capacity to implement backpressure
     // Blocking queues with bounded capacity to implement backpressure
     private final BlockingQueue<QwSopLogs> qwSopLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<QwSopLogs> qwSopLogsQueue = new LinkedBlockingQueue<>(20000);
+    private final BlockingQueue<QwSopSmsLogs> qwSopSmsLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseWatchLog> watchLogsQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseLink> linkQueue = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseSopAppLink> sopAppLinks = new LinkedBlockingQueue<>(20000);
     private final BlockingQueue<FsCourseSopAppLink> sopAppLinks = new LinkedBlockingQueue<>(20000);
@@ -543,6 +549,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             String companyUserId = String.valueOf(qwUserByRedis.getCompanyUserId()).trim();
             String companyUserId = String.valueOf(qwUserByRedis.getCompanyUserId()).trim();
             String companyId = String.valueOf(qwUserByRedis.getCompanyId()).trim();
             String companyId = String.valueOf(qwUserByRedis.getCompanyId()).trim();
             Integer sendMsgType = qwUserByRedis.getSendMsgType();
             Integer sendMsgType = qwUserByRedis.getSendMsgType();
+            Long serverId = qwUserByRedis.getServerId();
 
 
             if (StringUtil.strIsNullOrEmpty(companyUserId) || StringUtil.strIsNullOrEmpty(companyId) || "null".equals(companyUserId)) {
             if (StringUtil.strIsNullOrEmpty(companyUserId) || StringUtil.strIsNullOrEmpty(companyId) || "null".equals(companyUserId)) {
                 log.error("员工未绑定销售账号或公司,跳过处理:" + qwUserId);
                 log.error("员工未绑定销售账号或公司,跳过处理:" + qwUserId);
@@ -668,7 +675,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
 
                         insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, qwUserId,
                         insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, qwUserId,
                                 companyUserId, companyId, qwUserByRedis.getWelcomeText(), qwUserByRedis.getQwUserName(),
                                 companyUserId, companyId, qwUserByRedis.getWelcomeText(), qwUserByRedis.getQwUserName(),
-                                groupChatMap, miniAppId, config, miniMap, sendMsgType, companies);
+                                groupChatMap, miniAppId, config, miniMap, sendMsgType, companies, serverId);
 
 
                     }
                     }
                 } catch (Exception e) {
                 } catch (Exception e) {
@@ -714,7 +721,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                    String qwUserId, String companyUserId, String companyId, String welcomeText, String qwUserName,
                                    String qwUserId, String companyUserId, String companyId, String welcomeText, String qwUserName,
                                    Map<String, QwGroupChat> groupChatMap, String miniAppId, CourseConfig config,
                                    Map<String, QwGroupChat> groupChatMap, String miniAppId, CourseConfig config,
                                    Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
                                    Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
-                                   List<Company> companies) {
+                                   List<Company> companies,Long serverId) {
         String formattedSendTime = sendTime.toInstant()
         String formattedSendTime = sendTime.toInstant()
                 .atZone(ZoneId.systemDefault())
                 .atZone(ZoneId.systemDefault())
                 .format(DATE_TIME_FORMATTER);
                 .format(DATE_TIME_FORMATTER);
@@ -787,7 +794,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 ruleTimeVO.setType(2);
                 ruleTimeVO.setType(2);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                         type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
                         type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
-                        null, true, miniAppId, groupChat, config, miniMap, null, sendMsgType, companies, liveId);
+                        null, true, miniAppId, groupChat, config, miniMap, null, sendMsgType,
+                        companies, liveId,serverId);
             }
             }
 //            if (content.getIndex() == 0) {
 //            if (content.getIndex() == 0) {
 //                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
 //                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
@@ -817,7 +825,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId, isOfficial, contactId.getExternalId(), contactId.getIsDaysNotStudy());
                     QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId, isOfficial, contactId.getExternalId(), contactId.getIsDaysNotStudy());
                     handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                     handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                             type, qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId, false, miniAppId,
                             type, qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId, false, miniAppId,
-                            null, config, miniMap, grade, sendMsgType, companies, liveId);
+                            null, config, miniMap, grade, sendMsgType, companies, liveId,serverId);
                 } catch (Exception e) {
                 } catch (Exception e) {
                     log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
                     log.error("处理 externalContactId {} 时发生异常: {}", contactId, e.getMessage(), e);
                 }
                 }
@@ -929,9 +937,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                       SopUserLogsVo logVo, Date sendTime, Long courseId, Long videoId, int type, String qwUserId,
                                       SopUserLogsVo logVo, Date sendTime, Long courseId, Long videoId, int type, String qwUserId,
                                       String companyUserId, String companyId, String externalId, String welcomeText,
                                       String companyUserId, String companyId, String externalId, String welcomeText,
                                       String qwUserName, Long fsUserId, boolean isGroupChat, String miniAppId,
                                       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 liveId) {
+                                      QwGroupChat groupChat, CourseConfig config, Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
+                                      Integer grade, Integer sendMsgType, List<Company> companies, Long liveId,Long serverId) {
         switch (type) {
         switch (type) {
             case 1:
             case 1:
                 handleNormalMessage(sopLogs, content, companyUserId, companyId, isGroupChat, qwUserId, groupChat, externalId, logVo,sendTime);
                 handleNormalMessage(sopLogs, content, companyUserId, companyId, isGroupChat, qwUserId, groupChat, externalId, logVo,sendTime);
@@ -939,7 +946,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             case 2:
             case 2:
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
                         qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId,
                         qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId,
-                        isGroupChat, miniAppId, groupChat, config, miniMap, grade, sendMsgType, companies);
+                        isGroupChat, miniAppId, groupChat, config, miniMap, grade, sendMsgType, companies,serverId);
                 break;
                 break;
             case 3:
             case 3:
                 handleOrderMessage(sopLogs, content);
                 handleOrderMessage(sopLogs, content);
@@ -1603,7 +1610,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                      String companyId, String externalId, String welcomeText, String qwUserName,
                                      String companyId, String externalId, String welcomeText, String qwUserName,
                                      Long fsUserId, boolean isGroupChat, String miniAppId, QwGroupChat groupChat, CourseConfig config, Map<Long,
                                      Long fsUserId, boolean isGroupChat, String miniAppId, QwGroupChat groupChat, CourseConfig config, Map<Long,
             Map<Integer, List<CompanyMiniapp>>> miniMap, Integer grade, Integer sendMsgType,
             Map<Integer, List<CompanyMiniapp>>> miniMap, Integer grade, Integer sendMsgType,
-                                     List<Company> companies) {
+                                     List<Company> companies,Long serverId) {
         QwExternalContact contact = null;
         QwExternalContact contact = null;
         if (logVo.getExternalId() != null) {
         if (logVo.getExternalId() != null) {
             contact = qwExternalContactMapper.selectById(logVo.getExternalId());
             contact = qwExternalContactMapper.selectById(logVo.getExternalId());
@@ -1615,11 +1622,16 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             return;
             return;
         }
         }
 
 
-//
-//        Integer courseType = clonedContent.getCourseType();
 
 
         String isOfficial = clonedContent.getIsOfficial();
         String isOfficial = clonedContent.getIsOfficial();
 
 
+
+        Long msgNum = Long.valueOf(generateRandomNumberWithLock());
+        sopLogs.setSmsLogsId(msgNum);
+
+
+        AtomicInteger index = new AtomicInteger(0);
+
         List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
         List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
         if (settings == null || settings.isEmpty()) {
         if (settings == null || settings.isEmpty()) {
 //            log.error("Cloned content settings are empty, skipping.");
 //            log.error("Cloned content settings are empty, skipping.");
@@ -1633,6 +1645,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         }
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         for (QwSopTempSetting.Content.Setting setting : settings) {
         for (QwSopTempSetting.Content.Setting setting : settings) {
+
+            Integer currentIndex = index.getAndIncrement();
+
             switch (setting.getContentType()) {
             switch (setting.getContentType()) {
                 //文字和短链一起
                 //文字和短链一起
                 case "1":
                 case "1":
@@ -1902,6 +1917,26 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         } else {
                         } else {
                             log.error("生成看课短链失败,跳过设置 URL。");
                             log.error("生成看课短链失败,跳过设置 URL。");
                         }
                         }
+
+                        QwSopSmsLogs sopSmsLogs=new QwSopSmsLogs();
+                        sopSmsLogs.setSopId(sopLogs.getSopId());
+                        sopSmsLogs.setQwUserId(Long.valueOf(qwUserId));
+                        sopSmsLogs.setSopLogId(msgNum);
+                        sopSmsLogs.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
+                        sopSmsLogs.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
+                        sopSmsLogs.setContactId(externalId != null ? Long.valueOf(externalId) : null);
+                        sopSmsLogs.setServerId(serverId);
+                        sopSmsLogs.setStatus(0);
+                        sopSmsLogs.setSendTime(new Date());
+                        sopSmsLogs.setUpdateTime(new Date());
+                        sopSmsLogs.setCreateTime(new Date());
+
+                        sopSmsLogs.setFsUserId(sopLogs.getFsUserId());
+                        sopSmsLogs.setSmsIndex(currentIndex);
+                        sopSmsLogs.setContent(setting.getValue());
+                        sopSmsLogs.setSmsTemplateCode(setting.getSmsTemplateCode());
+
+                        enqueueQwSopSmsLogs(sopSmsLogs);
                     }
                     }
                     break;
                     break;
                 default:
                 default:
@@ -2743,6 +2778,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         }
     }
     }
 
 
+    /**
+     * 将 QwSopSmsLogs 放入队列
+     */
+    private void enqueueQwSopSmsLogs(QwSopSmsLogs smsLogs) {
+        try {
+            boolean offered = qwSopSmsLogsQueue.offer(smsLogs, 5, TimeUnit.SECONDS);
+            if (!offered) {
+                log.error("QwSopSmsLogs 队列已满,无法添加日志: {}", JSON.toJSONString(smsLogs));
+                // 处理队列已满的情况,例如记录到失败队列或持久化存储
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("插入 QwSopLogs 队列时被中断: {}", e.getMessage(), e);
+        }
+    }
+
     /**
     /**
      * 将 FsCourseWatchLog 放入队列
      * 将 FsCourseWatchLog 放入队列
      */
      */
@@ -2804,6 +2855,38 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         }
     }
     }
 
 
+
+
+    /**
+     * 消费 QwSopLogs 队列并进行批量插入
+     */
+    private void consumeQwSopSmsLogs() {
+        List<QwSopSmsLogs> batch = new ArrayList<>(BATCH_SIZE);
+        while (running || !qwSopSmsLogsQueue.isEmpty()) {
+            try {
+                QwSopSmsLogs log = qwSopSmsLogsQueue.poll(1, TimeUnit.SECONDS);
+                if (log != null) {
+                    batch.add(log);
+                }
+                if (batch.size() >= BATCH_SIZE || (!batch.isEmpty() && log == null)) {
+                    if (!batch.isEmpty()) {
+                        batchInsertQwSopSmsLogs(new ArrayList<>(batch));
+                        batch.clear();
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("QwSopLogs 消费线程被中断: {}", e.getMessage(), e);
+            }
+        }
+
+        // 处理剩余的数据
+        if (!batch.isEmpty()) {
+            batchInsertQwSopSmsLogs(batch);
+        }
+    }
+
+
     /**
     /**
      * 消费 FsCourseWatchLog 队列并进行批量插入
      * 消费 FsCourseWatchLog 队列并进行批量插入
      */
      */
@@ -2939,6 +3022,25 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         }
     }
     }
 
 
+    /**
+     * 批量插入 QwSopSmsLogs
+     */
+    @Transactional
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 2000)
+    )
+    public void batchInsertQwSopSmsLogs(List<QwSopSmsLogs> logsToInsert) {
+        try {
+            qwSopSmsLogsService.batchInsertQwSopSmsLogsOneTouch(logsToInsert);
+//            log.info("批量插入 QwSopSmsLogs 完成,共插入 {} 条记录。", logsToInsert.size());
+        } catch (Exception e) {
+            log.error("批量插入 QwSopSmsLogs 失败: {}", e.getMessage(), e);
+            // 可选:将失败的数据记录到失败队列或持久化存储以便后续重试
+        }
+    }
+
     /**
     /**
      * 批量插入 FsCourseWatchLog
      * 批量插入 FsCourseWatchLog
      */
      */

+ 3 - 0
fs-service/src/main/java/com/fs/common/service/ISmsService.java

@@ -23,4 +23,7 @@ public interface ISmsService
     R sendPackageOrderMsg(SmsSendUserParam param);
     R sendPackageOrderMsg(SmsSendUserParam param);
 
 
     R sendCaptcha(String phone, String captcha, String code);
     R sendCaptcha(String phone, String captcha, String code);
+
+    R sendUrl(String phone, String content, String code,Long uuid,Integer smsIndex,String deleteKey);
+
 }
 }

+ 104 - 0
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -611,6 +611,110 @@ public class SmsServiceImpl implements ISmsService
     }
     }
 
 
 
 
+    @Override
+    public R sendUrl(String phone, String content, String code,Long uuid,Integer smsIndex,String deleteKey) {
+        log.info("发送短信:{},链接地址:{},短信模板:{}", phone, content, code);
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(code);
+        if (temp == null) {
+            log.info("{},未找到短信模板:{}", phone, code);
+            return R.error("没有模板");
+        }
+        String urls = null;
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
+        FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
+        if (sms.getType().equals("rf")) {
+            try {
+//                content = content.replace("${sms.sign}",sms.getRfSign2());
+                urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + phone + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+            } catch (UnsupportedEncodingException e) {
+                log.error("{}发送失败", phone, e);
+                return R.error("短信发送失败:" + e.getMessage());
+            }
+            String post = HttpRequest.get(urls)
+                    .execute().body();
+            SmsSendVO vo = JSONUtil.toBean(post, SmsSendVO.class);
+            if (vo.getStatus().equals(0)) {
+                for (SmsSendItemVO itemVO : vo.getList()) {
+                    if (itemVO.getResult().equals("0")) {
+                        CompanySmsLogs logs = new CompanySmsLogs();
+                        logs.setContent(content);
+                        logs.setTempCode(temp.getTempCode());
+                        logs.setTempId(temp.getTempId());
+                        logs.setPhone(phone);
+                        logs.setSendTime(new Date());
+                        logs.setStatus(0);
+                        logs.setType(sms.getType());
+                        logs.setMid(itemVO.getMid());
+                        if (uuid != null) {
+                            logs.setSopSmsLogId(uuid);
+                        }
+                        if (smsIndex != null) {
+                            logs.setSmsIndex(smsIndex);
+                        }
+                        int counts = logs.getContent().length() / 67;
+                        if (logs.getContent().length() % 67 > 0) {
+                            counts = counts + 1;
+                        }
+                        if (counts == 0) {
+                            counts = 1;
+                        }
+                        logs.setNumber(counts);
+                        smsLogsService.insertCompanySmsLogs(logs);
+                    }else{
+                        log.info("{}不发送短信-itemVO.getResult().equals(\"0\"):{}", phone, itemVO);
+                    }
+                }
+            }else{
+                log.info("{}不发送短信-vo.getStatus().equals(0):{}", phone, vo.getStatus());
+
+                return R.error("发送短信失败!");
+            }
+        } else if (sms.getType().equals("dh")) {
+            SendSmsReturn sendSmsReturn = null;
+            content = content.replace("${sms.sign}",sms.getDhSign());
+            sendSmsReturn = smsTService.sendSms(sms.getDhAccount1(), sms.getDhPassword1(), content, phone);
+            if (sendSmsReturn != null) {
+                if (sendSmsReturn.getResult() != null && sendSmsReturn.getResult().equals("0")) {
+                    CompanySmsLogs logs = new CompanySmsLogs();
+                    logs.setContent(content);
+                    logs.setTempCode(temp.getTempCode());
+                    logs.setTempId(temp.getTempId());
+                    logs.setPhone(phone);
+                    logs.setSendTime(new Date());
+                    logs.setStatus(0);
+                    logs.setType(sms.getType());
+                    logs.setMid(sendSmsReturn.getMsgid());
+                    if (uuid != null) {
+                        logs.setSopSmsLogId(uuid);
+                    }
+                    if (smsIndex != null) {
+                        logs.setSmsIndex(smsIndex);
+                    }
+                    int counts = logs.getContent().length() / 67;
+                    if (logs.getContent().length() % 67 > 0) {
+                        counts = counts + 1;
+                    }
+                    if (counts == 0) {
+                        counts = 1;
+                    }
+                    logs.setNumber(counts);
+                    smsLogsService.insertCompanySmsLogs(logs);
+                }else{
+                    log.info("{}不发送短信-sendSmsReturn.getResult() != null && sendSmsReturn.getResult().equals(\"0\"):{}", phone, sendSmsReturn);
+                }
+            }else{
+                log.info("{}不发送短信-sendSmsReturn != null", phone);
+            }
+        }
+        //删除redis缓存
+        if(StringUtils.isNotEmpty(deleteKey)){
+            redisCache.deleteObject(deleteKey);
+        }
+        return R.ok();
+    }
+
+
+
     @Override
     @Override
     @Synchronized
     @Synchronized
     public R sendBatchSms(SmsSendBatchParam param) {
     public R sendBatchSms(SmsSendBatchParam param) {

+ 23 - 0
fs-service/src/main/java/com/fs/company/domain/CompanySmsLogs.java

@@ -67,6 +67,29 @@ public class CompanySmsLogs extends BaseEntity
 
 
     private String type;
     private String type;
 
 
+    /**
+     * 下标
+     * **/
+    private Integer smsIndex;
+
+    /** 关联 SOP 短信日志 id(QwSopSmsLogs.id),用于回调时更新该记录 */
+    private Long sopSmsLogId;
+
+    public Long getSopSmsLogId() {
+        return sopSmsLogId;
+    }
+
+    public void setSopSmsLogId(Long sopSmsLogId) {
+        this.sopSmsLogId = sopSmsLogId;
+    }
+
+    public Integer getSmsIndex() {
+        return smsIndex;
+    }
+
+    public void setSmsIndex(Integer smsIndex) {
+        this.smsIndex = smsIndex;
+    }
 
 
     public String getType() {
     public String getType() {
         return type;
         return type;

+ 44 - 0
fs-service/src/main/java/com/fs/course/utils/LinkUtil.java

@@ -51,4 +51,48 @@ public class LinkUtil {
         // 示例:使用UUID(牺牲有序性,但保证唯一性)
         // 示例:使用UUID(牺牲有序性,但保证唯一性)
         return UUID.randomUUID().toString().replace("-", "");
         return UUID.randomUUID().toString().replace("-", "");
     }
     }
+
+    public static String generateRandomNumberWithLock() {
+
+        RedissonClient redissonClient = SpringUtils.getBean(RedissonClient.class);
+        RLock lock = redissonClient.getLock("generateNumLinkLock");
+
+        int retryCount = 3;  // 设置重试次数
+        int waitTime = 100;  // 等待锁的最大时间(单位:毫秒)
+        int leaseTime = 10;  // 锁的持有时间(单位:秒)
+
+        while (retryCount > 0) {
+            try {
+                // 尝试获取锁,最多等待waitTime毫秒,锁定之后最多持有锁leaseTime秒
+                boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
+                if (isLocked) {
+                    // 成功获取锁,生成唯一的随机字符串
+                    return IdUtil.getSnowflake(0, 0).nextIdStr();
+                } else {
+                    // 如果锁未能获取到,重试
+                    retryCount--;
+                    if (retryCount == 0) {
+                        log.warn("Failed to acquire lock after multiple retries.");
+                        return generateFallbackIdByNum(); // 返回失败标识
+                    }
+                    // 可以在这里设置重试等待时间
+                    Thread.sleep(200); // 等待一段时间后重试
+                }
+            } catch (Exception e) {
+                log.error("Error while acquiring lock", e);
+                Thread.currentThread().interrupt(); // 恢复中断状态
+                return generateFallbackIdByNum();  // 异常情况下返回失败
+            }
+        }
+        return generateFallbackIdByNum();  // 如果所有重试都失败,返回失败
+    }
+
+    private static String generateFallbackIdByNum() {
+        // 使用纳秒级时间戳(但实际精度可能不高)
+        long nanoTime = System.nanoTime();
+        long milliTime = System.currentTimeMillis();
+
+        // 组合毫秒和纳秒,增加唯一性
+        return String.format("%d%d", milliTime, Math.abs(nanoTime % 1000000));
+    }
 }
 }

+ 14 - 0
fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java

@@ -0,0 +1,14 @@
+package com.fs.his.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@AllArgsConstructor
+public class SendResultDetailDTO implements Serializable {
+    private boolean success;
+    private String failReason;
+    private Long sopLogId;
+}

+ 6 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java

@@ -481,4 +481,10 @@ public interface FsUserMapper
 
 
     @Select("select * from fs_user where apple_key = #{appleKey}")
     @Select("select * from fs_user where apple_key = #{appleKey}")
     FsUser findUserByAppleKey(String appleKey);
     FsUser findUserByAppleKey(String appleKey);
+
+    /**
+     * 获取用户id
+     * @param userIds
+     * **/
+    List<FsUser> selectUserListByUserIds(@Param("userIds") List<Long> userIds);
 }
 }

+ 8 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserService.java

@@ -237,4 +237,12 @@ public interface IFsUserService
     List<FsUser> selectFsUserListByPhone(String phone);
     List<FsUser> selectFsUserListByPhone(String phone);
 
 
     R updatePasswordByPhone(String password, String encryptPhone);
     R updatePasswordByPhone(String password, String encryptPhone);
+
+
+    /**
+     * 批量查询用户信息
+     * @param userIds
+     * @return List<FsUser>
+     * **/
+    List<FsUser> selectUserListByUserIds(List<Long> userIds);
 }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -1621,4 +1621,9 @@ public class FsUserServiceImpl implements IFsUserService {
         return R.ok();
         return R.ok();
     }
     }
 
 
+
+    @Override
+    public List<FsUser> selectUserListByUserIds(List<Long> userIds) {
+        return fsUserMapper.selectUserListByUserIds(userIds);
+    }
 }
 }

+ 1 - 0
fs-service/src/main/java/com/fs/qw/domain/QwIpadServer.java

@@ -46,5 +46,6 @@ public class QwIpadServer extends BaseEntity{
     @Excel(name = "剩余数量")
     @Excel(name = "剩余数量")
     private Long count;
     private Long count;
 
 
+    private String groupNo;
 
 
 }
 }

+ 83 - 0
fs-service/src/main/java/com/fs/qw/domain/QwSopSmsLogs.java

@@ -0,0 +1,83 @@
+package com.fs.qw.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 润天发送短信日志对象 qw_sop_sms_logs
+ *
+ * @author fs
+ * @date 2026-03-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class QwSopSmsLogs extends BaseEntity{
+
+    /** 自增主键 */
+
+    private Long id;
+
+    /** 关联sopId */
+    @Excel(name = "关联sopId")
+    private String sopId;
+
+    /** 执行记录Id */
+    @Excel(name = "执行记录Id")
+    private Long sopLogId;
+
+    /** 企微Id */
+    @Excel(name = "企微Id")
+    private Long qwUserId;
+
+    /** 销售公司id */
+    @Excel(name = "销售公司id")
+    private Long companyId;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 外部联系人ID */
+    @Excel(name = "外部联系人ID")
+    private Long contactId;
+
+    /** 接收用户ID */
+    @Excel(name = "接收用户ID")
+    private Long fsUserId;
+
+    /** 接收手机号 */
+    @Excel(name = "接收手机号")
+    private String phoneNumber;
+
+    /** 短信内容 */
+    @Excel(name = "短信内容")
+    private String content;
+
+    /** 服务端ID */
+    @Excel(name = "服务端ID")
+    private Long serverId;
+
+    /** 状态(0未发送,1已经发送,2成功发送,3发送失败) */
+    @Excel(name = "状态", readConverterExp = "0=未发送,1已经发送,2成功发送,3发送失败")
+    private Integer status;
+
+    /** 发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "发送时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date sendTime;
+
+    /**
+     * 下标
+     * **/
+    private Integer smsIndex;
+
+    /** 短信模板编码 */
+    @Excel(name = "短信模板编码")
+    private String smsTemplateCode;
+
+}

+ 131 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwSopSmsLogsMapper.java

@@ -0,0 +1,131 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.qw.domain.QwSopSmsLogs;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 润天发送短信日志Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-02
+ */
+public interface QwSopSmsLogsMapper extends BaseMapper<QwSopSmsLogs>{
+    /**
+     * 查询润天发送短信日志
+     *
+     * @param id 润天发送短信日志主键
+     * @return 润天发送短信日志
+     */
+    @DataSource(DataSourceType.SOP)
+    QwSopSmsLogs selectQwSopSmsLogsById(Long id);
+
+    /**
+     * 查询润天发送短信日志列表
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 润天发送短信日志集合
+     */
+    @DataSource(DataSourceType.SOP)
+    List<QwSopSmsLogs> selectQwSopSmsLogsList(QwSopSmsLogs qwSopSmsLogs);
+
+    /**
+     * 新增润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int insertQwSopSmsLogs(QwSopSmsLogs qwSopSmsLogs);
+
+    /**
+     * 修改润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int updateQwSopSmsLogs(QwSopSmsLogs qwSopSmsLogs);
+
+    /**
+     * 删除润天发送短信日志
+     *
+     * @param id 润天发送短信日志主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int deleteQwSopSmsLogsById(Long id);
+
+    /**
+     * 批量删除润天发送短信日志
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int deleteQwSopSmsLogsByIds(Long[] ids);
+
+    /**
+     * 根据服务端ID、状态、创建时间范围查询待发送短信列表
+     *
+     * @param serverIds  服务端ID
+     * @param status    状态(0未发送)
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     * @return 短信记录列表
+     */
+    @DataSource(DataSourceType.SOP)
+    List<QwSopSmsLogs> selectPendingSmsByServerAndTime(@Param("serverIds") List<Long> serverIds, @Param("status") String status,@Param("startTime") Date startTime,@Param("endTime") Date endTime);
+
+    /**
+     * 分页查询待发送短信(游标分页,避免大结果集 OOM),用于海量数据
+     *
+     * @param serverIds  服务端ID
+     * @param status
+     * @param endTime   结束时间
+     * @param lastId    上一页最后一条 id,首次传 0
+     * @param pageSize  每页条数
+     * @return 本页记录(按 id 升序)
+     */
+    @DataSource(DataSourceType.SOP)
+    List<QwSopSmsLogs> selectPendingSmsByServerAndTimePage(@Param("serverIds") List<Long> serverIds, @Param("status") String status, @Param("endTime") Date endTime, @Param("lastId") Long lastId, @Param("pageSize") int pageSize);
+
+    /**
+     * 批量更新状态
+     *
+     * @param ids    主键集合
+     * @param status 状态(1发送中 2成功 3失败)
+     * @return 更新行数
+     */
+    @DataSource(DataSourceType.SOP)
+    int updateStatusByIds(@Param("ids") List<Long> ids, @Param("status") String status);
+
+    @DataSource(DataSourceType.SOP)
+    List<QwSopSmsLogs> selectQwSopSmsLogsByIdAndIndex(@Param("id") Long id, @Param("index") Long index);
+
+    /**
+     * 修改润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int updateSmsNotifyInfo(@Param("qwSopSmsLogs") QwSopSmsLogs qwSopSmsLogs);
+
+    @Select("SELECT COUNT(*) FROM qw_sop_sms_logs WHERE sop_log_id = #{sopLogId} AND status IN (2,3)")
+    @DataSource(DataSourceType.SOP)
+    Integer countProcessed(@Param("sopLogId") Long sopLogId);
+
+    @Select("SELECT status,sms_index FROM qw_sop_sms_logs WHERE sop_log_id = #{sopLogId}")
+    @DataSource(DataSourceType.SOP)
+    List<QwSopSmsLogs> getSmsListBySopLogId(@Param("sopLogId") Long sopLogId);
+
+    @DataSource(DataSourceType.SOP)
+    void batchInsertQwSopSmsLogsOneTouch(@Param("qwSopSmsLogs") List<QwSopSmsLogs> logsToInsert);
+}

+ 108 - 0
fs-service/src/main/java/com/fs/qw/service/IQwSopSmsLogsService.java

@@ -0,0 +1,108 @@
+package com.fs.qw.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.qw.domain.QwSopSmsLogs;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 润天发送短信日志Service接口
+ *
+ * @author fs
+ * @date 2026-03-02
+ */
+public interface IQwSopSmsLogsService extends IService<QwSopSmsLogs>{
+    /**
+     * 查询润天发送短信日志
+     *
+     * @param id 润天发送短信日志主键
+     * @return 润天发送短信日志
+     *
+     */
+    QwSopSmsLogs selectQwSopSmsLogsById(Long id);
+
+    /**
+     * 查询润天发送短信日志列表
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 润天发送短信日志集合
+     *
+     */
+    List<QwSopSmsLogs> selectQwSopSmsLogsList(QwSopSmsLogs qwSopSmsLogs);
+
+    /**
+     * 新增润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    int insertQwSopSmsLogs(QwSopSmsLogs qwSopSmsLogs);
+
+    /**
+     * 修改润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    int updateQwSopSmsLogs(QwSopSmsLogs qwSopSmsLogs);
+
+    /**
+     * 批量删除润天发送短信日志
+     *
+     * @param ids 需要删除的润天发送短信日志主键集合
+     * @return 结果
+     */
+    int deleteQwSopSmsLogsByIds(Long[] ids);
+
+    /**
+     * 删除润天发送短信日志信息
+     *
+     * @param id 润天发送短信日志主键
+     * @return 结果
+     */
+    int deleteQwSopSmsLogsById(Long id);
+
+    /**
+     * 分页查询待发送短信(游标分页),用于海量数据避免 OOM
+     *
+     * @param serverIds   ipad信息
+     * @param status    状态(0未发送)
+     * @param endTime   结束时间
+     * @param lastId    上一页最后一条 id,首次传 0
+     * @param pageSize  每页条数
+     * @return 本页记录
+     */
+    List<QwSopSmsLogs> selectPendingSmsByGroupAndTimePage(List<Long> serverIds, String status,Date endTime, Long lastId, int pageSize);
+
+    /**
+     * 批量更新状态(内部分批 IN,支持海量 id)
+     *
+     * @param ids    主键集合
+     * @param status 状态(1发送中 2成功 3失败)
+     * @return 更新行数
+     */
+    int updateStatusByIds(List<Long> ids, String status);
+
+    /**
+     * 根据主键 id 和 index 查询一条记录
+     *
+     * @param id    主键
+     * @param index 下标
+     * @return 润天发送短信日志,不存在返回 null
+     */
+    List<QwSopSmsLogs> selectQwSopSmsLogsByIdAndIndex(Long id, Long index);
+
+
+    /**
+     * 修改润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    int updateSmsNotifyInfo(QwSopSmsLogs qwSopSmsLogs);
+
+
+
+    void batchInsertQwSopSmsLogsOneTouch(List<QwSopSmsLogs> logsToInsert);
+}

+ 143 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwSopSmsLogsServiceImpl.java

@@ -0,0 +1,143 @@
+package com.fs.qw.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.qw.mapper.QwSopSmsLogsMapper;
+import com.fs.qw.service.IQwSopSmsLogsService;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 润天发送短信日志Service业务层处理
+ *
+ * @author fs
+ * @date 2026-03-02
+ */
+@Service
+public class QwSopSmsLogsServiceImpl extends ServiceImpl<QwSopSmsLogsMapper, QwSopSmsLogs> implements IQwSopSmsLogsService {
+
+    /**
+     * 查询润天发送短信日志
+     *
+     * @param id 润天发送短信日志主键
+     * @return 润天发送短信日志
+     */
+    @Override
+    public QwSopSmsLogs selectQwSopSmsLogsById(Long id)
+    {
+        return baseMapper.selectQwSopSmsLogsById(id);
+    }
+
+    /**
+     * 查询润天发送短信日志列表
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 润天发送短信日志
+     */
+    @Override
+    public List<QwSopSmsLogs> selectQwSopSmsLogsList(QwSopSmsLogs qwSopSmsLogs)
+    {
+        return baseMapper.selectQwSopSmsLogsList(qwSopSmsLogs);
+    }
+
+    /**
+     * 新增润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    @Override
+    public int insertQwSopSmsLogs(QwSopSmsLogs qwSopSmsLogs)
+    {
+        qwSopSmsLogs.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertQwSopSmsLogs(qwSopSmsLogs);
+    }
+
+    /**
+     * 修改润天发送短信日志
+     *
+     * @param qwSopSmsLogs 润天发送短信日志
+     * @return 结果
+     */
+    @Override
+    public int updateQwSopSmsLogs(QwSopSmsLogs qwSopSmsLogs)
+    {
+        qwSopSmsLogs.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateQwSopSmsLogs(qwSopSmsLogs);
+    }
+
+    /**
+     * 批量删除润天发送短信日志
+     *
+     * @param ids 需要删除的润天发送短信日志主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwSopSmsLogsByIds(Long[] ids)
+    {
+        return baseMapper.deleteQwSopSmsLogsByIds(ids);
+    }
+
+    /**
+     * 删除润天发送短信日志信息
+     *
+     * @param id 润天发送短信日志主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwSopSmsLogsById(Long id)
+    {
+        return baseMapper.deleteQwSopSmsLogsById(id);
+    }
+
+
+    /**
+     * 分页查询待发送短信(游标分页)
+     */
+    @Override
+    public List<QwSopSmsLogs> selectPendingSmsByGroupAndTimePage(List<Long> serverIds, String status, Date endTime, Long lastId, int pageSize) {
+        return baseMapper.selectPendingSmsByServerAndTimePage(serverIds, status, endTime, lastId, pageSize);
+    }
+
+    /** 批量更新时每批 IN 的最大条数,防止 50w 一次 */
+    private static final int UPDATE_STATUS_BATCH_SIZE = 2000;
+
+    /**
+     * 批量更新状态(内部分批 IN,支持海量 id 不撑爆 SQL)
+     */
+    @Override
+    public int updateStatusByIds(List<Long> ids, String status) {
+        if (ids == null || ids.isEmpty()) {
+            return 0;
+        }
+        int total = 0;
+        for (int i = 0; i < ids.size(); i += UPDATE_STATUS_BATCH_SIZE) {
+            int to = Math.min(i + UPDATE_STATUS_BATCH_SIZE, ids.size());
+            List<Long> chunk = ids.subList(i, to);
+            total += baseMapper.updateStatusByIds(new ArrayList<>(chunk), status);
+        }
+        return total;
+    }
+
+    /**
+     * 根据主键 id 和 index 查询一条记录
+     */
+    @Override
+    public List<QwSopSmsLogs> selectQwSopSmsLogsByIdAndIndex(Long id, Long index) {
+        return baseMapper.selectQwSopSmsLogsByIdAndIndex(id, index);
+    }
+
+    @Override
+    public int updateSmsNotifyInfo(QwSopSmsLogs qwSopSmsLogs) {
+        return baseMapper.updateSmsNotifyInfo(qwSopSmsLogs);
+    }
+
+    @Override
+    public void batchInsertQwSopSmsLogsOneTouch(List<QwSopSmsLogs> logsToInsert) {
+         baseMapper.batchInsertQwSopSmsLogsOneTouch(logsToInsert);
+    }
+}

+ 6 - 1
fs-service/src/main/java/com/fs/sop/domain/QwSopLogs.java

@@ -106,12 +106,17 @@ public class QwSopLogs implements Serializable {
      * app发送状态 0未发送 1成功 2失败 3不发送
      * app发送状态 0未发送 1成功 2失败 3不发送
      */
      */
     private Integer appSendStatus;
     private Integer appSendStatus;
-    
+
     /**
     /**
      * app发送备注
      * app发送备注
      */
      */
     private String appSendRemark;
     private String appSendRemark;
 
 
+    /**
+     * 短信执行记录表
+     */
+    private Long smsLogsId;
+
     // 构造函数
     // 构造函数
 //    public QwSopLogs() {
 //    public QwSopLogs() {
 //        this.id = UUID.randomUUID().toString();
 //        this.id = UUID.randomUUID().toString();

+ 10 - 0
fs-service/src/main/java/com/fs/sop/mapper/QwSopLogsMapper.java

@@ -364,5 +364,15 @@ public interface QwSopLogsMapper extends BaseMapper<QwSopLogs> {
     @MapKey("id")
     @MapKey("id")
     Map<String, SopUserLogs> queryAllPeriodNew(StatsWatchLogPageListDTO param);
     Map<String, SopUserLogs> queryAllPeriodNew(StatsWatchLogPageListDTO param);
 
 
+    /**
+     * 批量更执行记录表
+     * @param list 更新对象
+     * **/
+    @DataSource(DataSourceType.SOP)
+    void batchUpdateQwSopLogsBySmsLogsId(@Param("list") List<QwSopLogs> list);
+
+
+    @DataSource(DataSourceType.SOP)
+    List<QwSopLogs> getQwSopInfoByUid(@Param("sopSmsLogIds") List<Long> sopSmsLogIds);
 
 
 }
 }

+ 115 - 15
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -58,10 +58,7 @@ import com.fs.sop.service.IQwSopService;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.sop.service.ISopUserLogsInfoService;
 import com.fs.sop.service.ISopUserLogsInfoService;
 import com.fs.sop.service.ISopUserLogsService;
 import com.fs.sop.service.ISopUserLogsService;
-import com.fs.sop.vo.ExtCourseSopWatchLogVO;
-import com.fs.sop.vo.QwCreateLinkByAppVO;
-import com.fs.sop.vo.SopUserLogsInfoVOE;
-import com.fs.sop.vo.SopUserLogsVo;
+import com.fs.sop.vo.*;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
@@ -83,9 +80,11 @@ import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.*;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
+import static com.fs.course.utils.LinkUtil.generateRandomNumberWithLock;
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 
 
 @Service
 @Service
@@ -138,6 +137,9 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     @Autowired
     @Autowired
     private QwSopLogsMapper qwSopLogsMapper;
     private QwSopLogsMapper qwSopLogsMapper;
 
 
+    @Autowired
+    private QwSopSmsLogsMapper qwSopSmsLogsMapper;
+
     @Autowired
     @Autowired
     private ISopUserLogsService sopUserLogsService;
     private ISopUserLogsService sopUserLogsService;
 
 
@@ -522,6 +524,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             return  R.error().put("msg","企业不存在,请联系管理员");
             return  R.error().put("msg","企业不存在,请联系管理员");
         }
         }
         List<QwSopLogs> sopLogsList;
         List<QwSopLogs> sopLogsList;
+
+        //短信的
+        List<QwSopSmsLogs> sopSmsLogsList = new ArrayList<>();
+
         if(param.getFilterMode() != null && param.getFilterMode() == 2 && param.getChatIds() != null && param.getChatIds().length > 0){
         if(param.getFilterMode() != null && param.getFilterMode() == 2 && param.getChatIds() != null && param.getChatIds().length > 0){
 
 
             if (config.getRoomLinkAllow() != null && config.getRoomLinkAllow()) {
             if (config.getRoomLinkAllow() != null && config.getRoomLinkAllow()) {
@@ -1191,13 +1197,21 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                        sopLogs.setSendType(20);
                        sopLogs.setSendType(20);
                    }
                    }
 
 
+                Long msgNum = Long.valueOf(generateRandomNumberWithLock());
+                sopLogs.setSmsLogsId(msgNum);
+
                 QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());
                 QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());
 
 
                 QwSopCourseFinishTempSetting setting=new    QwSopCourseFinishTempSetting();
                 QwSopCourseFinishTempSetting setting=new    QwSopCourseFinishTempSetting();
 
 
                 List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
                 List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
+
+                AtomicInteger index = new AtomicInteger(0);
+
                 for (QwSopCourseFinishTempSetting.Setting st : list) {
                 for (QwSopCourseFinishTempSetting.Setting st : list) {
 
 
+                    Integer currentIndex = index.getAndIncrement();
+
                     //过滤违禁词
                     //过滤违禁词
                     if ("1".equals(st.getContentType())){
                     if ("1".equals(st.getContentType())){
                         replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
                         replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
@@ -1564,6 +1578,26 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 } else {
                                 } else {
                                     log.error("生成看课短链失败,跳过设置 URL。");
                                     log.error("生成看课短链失败,跳过设置 URL。");
                                 }
                                 }
+
+                                QwSopSmsLogs sopSmsLogs=new QwSopSmsLogs();
+                                sopSmsLogs.setSopId(item.getSopId());
+                                sopSmsLogs.setQwUserId(qwUser.getId());
+                                sopSmsLogs.setSopLogId(msgNum);
+                                sopSmsLogs.setCompanyId(qwUser.getCompanyId());
+                                sopSmsLogs.setCompanyUserId(qwUser.getCompanyUserId());
+                                sopSmsLogs.setContactId(item.getExternalId());
+                                sopSmsLogs.setServerId(qwUser.getServerId());
+                                sopSmsLogs.setStatus(0);
+                                sopSmsLogs.setSendTime(new Date());
+                                sopSmsLogs.setUpdateTime(new Date());
+                                sopSmsLogs.setCreateTime(new Date());
+
+                                sopSmsLogs.setFsUserId(sopLogs.getFsUserId());
+                                sopSmsLogs.setSmsIndex(currentIndex);
+                                sopSmsLogs.setContent(st.getValue());
+                                sopSmsLogs.setSmsTemplateCode(st.getSmsTemplateCode());
+
+                                sopSmsLogsList.add(sopSmsLogs);
                             }
                             }
                             break;
                             break;
                         //群公告(仅用于一键群发,个人不应该有群公告)
                         //群公告(仅用于一键群发,个人不应该有群公告)
@@ -1602,6 +1636,11 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             processAndInsertQwSopLogsBySendMsg(sopLogsList);
             processAndInsertQwSopLogsBySendMsg(sopLogsList);
         }
         }
 
 
+        //批量插入短信的发送记录
+        if (!sopSmsLogsList.isEmpty()) {
+            processAndInsertQwSopSmsLogsBySendMsg(sopSmsLogsList);
+        }
+
         return R.ok();
         return R.ok();
     }
     }
 
 
@@ -1755,14 +1794,23 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             }else {
             }else {
 
 
                 if (qwUser.getCompanyUserId()!=null && qwUser.getCompanyId()!=null){
                 if (qwUser.getCompanyUserId()!=null && qwUser.getCompanyId()!=null){
-                    List<QwSopLogs> sopLogsList = processInsertSopUserLogsInfo(logs, qwUser, param, words, config, qwCompany, finalSort,
-                            finalSendType,miniMap,companies);
+
+//                    List<QwSopLogs> sopLogsList = processInsertSopUserLogsInfo(logs, qwUser, param, words, config, qwCompany, finalSort,
+//                            finalSendType,miniMap,companies);
+
+                    SopLogsResult result = processInsertSopUserLogsInfo(logs, qwUser, param, words, config, qwCompany, finalSort,
+                            finalSendType, miniMap,companies);
 
 
                     //批量插入 发送记录
                     //批量插入 发送记录
-                    if (!sopLogsList.isEmpty()) {
-                        processAndInsertQwSopLogsBySendMsg(sopLogsList);
+                    if (!result.getSopLogsList().isEmpty()) {
+                        processAndInsertQwSopLogsBySendMsg(result.getSopLogsList());
+                    }
+
+                    if (!result.getSopSmsLogsList().isEmpty()) {
+                        processAndInsertQwSopSmsLogsBySendMsg(result.getSopSmsLogsList());
                     }
                     }
 
 
+
                 }else {
                 }else {
                     log.error("员工未绑定销售账号 请先绑定 :" + qwUser.getQwUserName()+",账号id:"+qwUser.getQwUserId());
                     log.error("员工未绑定销售账号 请先绑定 :" + qwUser.getQwUserName()+",账号id:"+qwUser.getQwUserId());
                 }
                 }
@@ -1776,10 +1824,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return  R.ok();
         return  R.ok();
     }
     }
 
 
-    private List<QwSopLogs> processInsertSopUserLogsInfo(List<SopUserLogsInfo> sopUserLogsInfos,QwUser qwUser,
-                                                         SendUserLogsInfoMsgParam param,List<FastGptChatReplaceWords> words,
-                                                         CourseConfig config,QwCompany qwCompany,int finalSort,int finalSendType,
-                                                         Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,List<Company> companies ){
+    private SopLogsResult processInsertSopUserLogsInfo(List<SopUserLogsInfo> sopUserLogsInfos, QwUser qwUser,
+                                                       SendUserLogsInfoMsgParam param, List<FastGptChatReplaceWords> words,
+                                                       CourseConfig config, QwCompany qwCompany, int finalSort, int finalSendType,
+                                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, List<Company> companies ){
 
 
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
 
@@ -1794,6 +1842,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         }
         }
         List<QwSopLogs> sopLogsList=new ArrayList<>();
         List<QwSopLogs> sopLogsList=new ArrayList<>();
 
 
+        List<QwSopSmsLogs> sopSmsLogsList = new ArrayList<>();
+
         //域名
         //域名
 //        String domainName = companyUserMapper.selectDomainByUserId(qwUser.getCompanyUserId());
 //        String domainName = companyUserMapper.selectDomainByUserId(qwUser.getCompanyUserId());
 //        if (StringUtils.isEmpty(domainName)){
 //        if (StringUtils.isEmpty(domainName)){
@@ -1823,6 +1873,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
 
             sopLogs.setExternalUserName(item.getExternalUserName());
             sopLogs.setExternalUserName(item.getExternalUserName());
 
 
+
+            Long msgNum = Long.valueOf(generateRandomNumberWithLock());
+            sopLogs.setSmsLogsId(msgNum);
+
             QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());
             QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());
 
 
             QwSopCourseFinishTempSetting setting=new   QwSopCourseFinishTempSetting();
             QwSopCourseFinishTempSetting setting=new   QwSopCourseFinishTempSetting();
@@ -1835,7 +1889,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 case 5:
                 case 5:
                 case 20:
                 case 20:
                     List<QwSopCourseFinishTempSetting.Setting> list = processSetting(item,qwUser, param, words, config, qwCompany,companyUserId,companyId,
                     List<QwSopCourseFinishTempSetting.Setting> list = processSetting(item,qwUser, param, words, config, qwCompany,companyUserId,companyId,
-                            contact,dataTime, finalDomainName,miniMap,companies,sopLogs);
+                            contact,dataTime, finalDomainName,miniMap,companies,sopLogs,sopSmsLogsList);
                     setting.setSetting(list);
                     setting.setSetting(list);
                     break;
                     break;
                 case 9:
                 case 9:
@@ -1875,7 +1929,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
 
 
 
 
 
-        return sopLogsList;
+        return new SopLogsResult(sopLogsList, sopSmsLogsList);
 
 
     }
     }
 
 
@@ -1900,11 +1954,15 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                                                       CourseConfig config,QwCompany qwCompany,String companyUserId, String companyId,
                                                                       CourseConfig config,QwCompany qwCompany,String companyUserId, String companyId,
                                                                       QwExternalContact contact,Date dataTime,String domainName,
                                                                       QwExternalContact contact,Date dataTime,String domainName,
                                                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                                                      List<Company> companies,QwSopLogs sopLogs ){
+                                                                      List<Company> companies,QwSopLogs sopLogs,List<QwSopSmsLogs> sopSmsLogsList ){
         List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
         List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
 
 
+        AtomicInteger index = new AtomicInteger(0);
+
         for (QwSopCourseFinishTempSetting.Setting st : list) {
         for (QwSopCourseFinishTempSetting.Setting st : list) {
 
 
+            Integer currentIndex = index.getAndIncrement();
+
             //过滤违禁词
             //过滤违禁词
             if ("1".equals(st.getContentType())){
             if ("1".equals(st.getContentType())){
                 replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
                 replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
@@ -2278,6 +2336,27 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         } else {
                         } else {
                             log.error("生成看课短链失败,跳过设置 URL。");
                             log.error("生成看课短链失败,跳过设置 URL。");
                         }
                         }
+
+                        QwSopSmsLogs sopSmsLogs=new QwSopSmsLogs();
+
+                        sopSmsLogs.setSopId(item.getSopId());
+                        sopSmsLogs.setQwUserId(qwUser.getId());
+                        sopSmsLogs.setSopLogId(sopLogs.getSmsLogsId());
+                        sopSmsLogs.setCompanyId(qwUser.getCompanyId());
+                        sopSmsLogs.setCompanyUserId(qwUser.getCompanyUserId());
+                        sopSmsLogs.setContactId(item.getExternalId());
+                        sopSmsLogs.setServerId(qwUser.getServerId());
+                        sopSmsLogs.setStatus(0);
+                        sopSmsLogs.setSendTime(new Date());
+                        sopSmsLogs.setUpdateTime(new Date());
+                        sopSmsLogs.setCreateTime(new Date());
+
+                        sopSmsLogs.setFsUserId(sopLogs.getFsUserId());
+                        sopSmsLogs.setSmsIndex(currentIndex);
+                        sopSmsLogs.setContent(st.getValue());
+                        sopSmsLogs.setSmsTemplateCode(st.getSmsTemplateCode());
+
+                        sopSmsLogsList.add(sopSmsLogs);
                     }
                     }
                     break;
                     break;
                 default:
                 default:
@@ -2541,6 +2620,27 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         }
         }
     }
     }
 
 
+    private void processAndInsertQwSopSmsLogsBySendMsg(List<QwSopSmsLogs> sopSmsLogsList) {
+        // 定义批量插入的大小
+        int batchSize = 500;
+
+        // 循环处理外部用户 ID,每次处理批量大小的子集
+        for (int i = 0; i < sopSmsLogsList.size(); i += batchSize) {
+
+            int endIndex = Math.min(i + batchSize, sopSmsLogsList.size());
+            List<QwSopSmsLogs> batchList = sopSmsLogsList.subList(i, endIndex);  // 获取当前批次的子集
+
+            // 直接使用批次数据进行批量更新,不需要额外的 List
+            try {
+                qwSopSmsLogsMapper.batchInsertQwSopSmsLogsOneTouch(batchList);
+            } catch (Exception e) {
+                // 记录异常日志,方便后续排查问题
+                log.error("批量执行一键群发时发生异常,处理的批次起始索引为: " + i, e);
+            }
+        }
+    }
+
+
     //插入观看记录
     //插入观看记录
     public void addWatchLogIfNeeded(String sopId, Integer videoId, Integer courseId,
     public void addWatchLogIfNeeded(String sopId, Integer videoId, Integer courseId,
                                      Long fsUserId, String qwUserId, String companyUserId,
                                      Long fsUserId, String qwUserId, String companyUserId,

+ 35 - 0
fs-service/src/main/java/com/fs/sop/vo/SopLogsResult.java

@@ -0,0 +1,35 @@
+package com.fs.sop.vo;
+
+import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.sop.domain.QwSopLogs;
+
+import java.util.List;
+
+public class SopLogsResult {
+
+    private List<QwSopLogs> sopLogsList;
+    private List<QwSopSmsLogs> sopSmsLogsList;
+
+    // 构造方法
+    public SopLogsResult(List<QwSopLogs> sopLogsList, List<QwSopSmsLogs> sopSmsLogsList) {
+        this.sopLogsList = sopLogsList;
+        this.sopSmsLogsList = sopSmsLogsList;
+    }
+
+    // getter和setter
+    public List<QwSopLogs> getSopLogsList() {
+        return sopLogsList;
+    }
+
+    public void setSopLogsList(List<QwSopLogs> sopLogsList) {
+        this.sopLogsList = sopLogsList;
+    }
+
+    public List<QwSopSmsLogs> getSopSmsLogsList() {
+        return sopSmsLogsList;
+    }
+
+    public void setSopSmsLogsList(List<QwSopSmsLogs> sopSmsLogsList) {
+        this.sopSmsLogsList = sopSmsLogsList;
+    }
+}

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

@@ -21,10 +21,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="replyContent"    column="reply_content"    />
         <result property="replyContent"    column="reply_content"    />
         <result property="number"    column="number"    />
         <result property="number"    column="number"    />
         <result property="type"    column="type"    />
         <result property="type"    column="type"    />
+        <result property="sopSmsLogId"    column="sop_sms_log_id"    />
+        <result property="smsIndex"    column="sms_index"    />
     </resultMap>
     </resultMap>
 
 
     <sql id="selectCompanySmsLogsVo">
     <sql id="selectCompanySmsLogsVo">
-        select logs_id, company_id,type, company_user_id, customer_id, temp_id, temp_code, phone, content, create_time, send_time, status,mid,stat,reply_content,number from company_sms_logs
+        select logs_id, company_id,type, company_user_id, customer_id, temp_id, temp_code, phone, content,
+               create_time, send_time, status,mid,stat,reply_content,number,sop_sms_log_id,sms_index
+        from company_sms_logs
     </sql>
     </sql>
 
 
 
 
@@ -52,6 +56,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="replyContent != null">reply_content,</if>
             <if test="replyContent != null">reply_content,</if>
             <if test="number != null">number,</if>
             <if test="number != null">number,</if>
             <if test="type != null">type,</if>
             <if test="type != null">type,</if>
+            <if test="sopSmsLogId != null">sop_sms_log_id,</if>
+            <if test="smsIndex != null">sms_index,</if>
          </trim>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
             <if test="companyId != null">#{companyId},</if>
@@ -69,6 +75,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="replyContent != null">#{replyContent},</if>
             <if test="replyContent != null">#{replyContent},</if>
             <if test="number != null">#{number},</if>
             <if test="number != null">#{number},</if>
             <if test="type != null">#{type},</if>
             <if test="type != null">#{type},</if>
+            <if test="sopSmsLogId != null">#{sopSmsLogId},</if>
+            <if test="smsIndex != null">#{smsIndex},</if>
          </trim>
          </trim>
     </insert>
     </insert>
 
 
@@ -90,6 +98,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="replyContent != null">reply_content = #{replyContent},</if>
             <if test="replyContent != null">reply_content = #{replyContent},</if>
             <if test="number != null">number = #{number},</if>
             <if test="number != null">number = #{number},</if>
             <if test="type != null">type = #{type},</if>
             <if test="type != null">type = #{type},</if>
+            <if test="sopSmsLogId != null">sop_sms_log_id = #{sopSmsLogId},</if>
         </trim>
         </trim>
         where logs_id = #{logsId}
         where logs_id = #{logsId}
     </update>
     </update>

+ 7 - 0
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -2548,4 +2548,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY f1.user_id DESC
         ORDER BY f1.user_id DESC
     </select>
     </select>
 
 
+    <select id="selectUserListByUserIds" resultType="com.fs.his.domain.FsUser">
+        SELECT user_id,phone FROM fs_user WHERE user_id IN
+        <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </select>
+
 </mapper>
 </mapper>

+ 244 - 0
fs-service/src/main/resources/mapper/qw/QwSopSmsLogsMapper.xml

@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwSopSmsLogsMapper">
+
+    <resultMap type="QwSopSmsLogs" id="QwSopSmsLogsResult">
+        <result property="id"    column="id"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="sopLogId"    column="sop_log_id"    />
+        <result property="qwUserId"    column="qw_user_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="contactId"    column="contact_id"    />
+        <result property="fsUserId"    column="fs_user_id"    />
+        <result property="phoneNumber"    column="phone_number"    />
+        <result property="remark"    column="remark"    />
+        <result property="content"    column="content"    />
+        <result property="serverId"    column="server_id"    />
+        <result property="status"    column="status"    />
+        <result property="sendTime"    column="send_time"    />
+        <result property="smsTemplateCode"    column="sms_template_code"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="smsIndex" column="sms_index"></result>
+    </resultMap>
+
+    <sql id="selectQwSopSmsLogsVo">
+        select id, sop_id, sop_log_id, qw_user_id, company_id, company_user_id, contact_id, fs_user_id, phone_number, remark, content, server_id, status, send_time, sms_template_code, create_time, update_time,sms_index from qw_sop_sms_logs
+    </sql>
+
+    <select id="selectQwSopSmsLogsList" parameterType="QwSopSmsLogs" resultMap="QwSopSmsLogsResult">
+        <include refid="selectQwSopSmsLogsVo"/>
+        <where>
+            <if test="sopId != null  and sopId != ''"> and sop_id = #{sopId}</if>
+            <if test="sopLogId != null "> and sop_log_id = #{sopLogId}</if>
+            <if test="qwUserId != null "> and qw_user_id = #{qwUserId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="contactId != null "> and contact_id = #{contactId}</if>
+            <if test="fsUserId != null  and fsUserId != ''"> and fs_user_id = #{fsUserId}</if>
+            <if test="phoneNumber != null  and phoneNumber != ''"> and phone_number = #{phoneNumber}</if>
+            <if test="content != null  and content != ''"> and content = #{content}</if>
+            <if test="serverId != null "> and server_id = #{serverId}</if>
+            <if test="status != null  and status != ''"> and status = #{status}</if>
+            <if test="sendTime != null "> and send_time = #{sendTime}</if>
+            <if test="smsTemplateCode != null  and smsTemplateCode != ''"> and sms_template_code = #{smsTemplateCode}</if>
+        </where>
+    </select>
+
+    <select id="selectQwSopSmsLogsById" parameterType="Long" resultMap="QwSopSmsLogsResult">
+        <include refid="selectQwSopSmsLogsVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwSopSmsLogs" parameterType="QwSopSmsLogs" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_sop_sms_logs
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="sopId != null">sop_id,</if>
+            <if test="sopLogId != null">sop_log_id,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="contactId != null">contact_id,</if>
+            <if test="fsUserId != null">fs_user_id,</if>
+            <if test="phoneNumber != null">phone_number,</if>
+            <if test="remark != null">remark,</if>
+            <if test="content != null">content,</if>
+            <if test="serverId != null">server_id,</if>
+            <if test="status != null">status,</if>
+            <if test="sendTime != null">send_time,</if>
+            <if test="smsTemplateCode != null">sms_template_code,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="smsIndex != null">sms_index,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="sopId != null">#{sopId},</if>
+            <if test="sopLogId != null">#{sopLogId},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="contactId != null">#{contactId},</if>
+            <if test="fsUserId != null">#{fsUserId},</if>
+            <if test="phoneNumber != null">#{phoneNumber},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="content != null">#{content},</if>
+            <if test="serverId != null">#{serverId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="sendTime != null">#{sendTime},</if>
+            <if test="smsTemplateCode != null">#{smsTemplateCode},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="smsIndex != null">#{smsIndex},</if>
+         </trim>
+    </insert>
+
+
+    <update id="updateQwSopSmsLogs" parameterType="QwSopSmsLogs">
+        update qw_sop_sms_logs
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="sopId != null">sop_id = #{sopId},</if>
+            <if test="sopLogId != null">sop_log_id = #{sopLogId},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="contactId != null">contact_id = #{contactId},</if>
+            <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
+            <if test="phoneNumber != null">phone_number = #{phoneNumber},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="content != null">content = #{content},</if>
+            <if test="serverId != null">server_id = #{serverId},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="sendTime != null">send_time = #{sendTime},</if>
+            <if test="smsTemplateCode != null">sms_template_code = #{smsTemplateCode},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="smsIndex != null">sms_index = #{smsIndex},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwSopSmsLogsById" parameterType="Long">
+        delete from qw_sop_sms_logs where id = #{id}
+    </delete>
+
+    <delete id="deleteQwSopSmsLogsByIds" parameterType="String">
+        delete from qw_sop_sms_logs where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <!-- 查询待发送:例如 10点执行则查 当天 0 点 ~ 11 点之前。serverIds 为空时返回空结果。建议索引:(server_id, status, create_time) -->
+    <select id="selectPendingSmsByServerAndTime" resultType="com.fs.qw.domain.QwSopSmsLogs">
+        SELECT
+        id,
+        fs_user_id,
+        content,
+        sms_template_code
+        FROM
+        qw_sop_sms_logs
+        <where>
+            <choose>
+                <when test="serverIds != null and serverIds.size() > 0">
+                    server_id IN
+                    <foreach collection="serverIds" item="serverId" open="(" separator="," close=")">
+                        #{serverId}
+                    </foreach>
+                    AND status = #{status}
+                    AND create_time &gt;= #{startTime}
+                    AND create_time &lt; #{endTime}
+                </when>
+                <otherwise>
+                    1 = 0
+                </otherwise>
+            </choose>
+        </where>
+    </select>
+
+    <!-- 分页查询待发送 OOM -->
+    <select id="selectPendingSmsByServerAndTimePage" resultType="com.fs.qw.domain.QwSopSmsLogs">
+        SELECT
+        id,
+        fs_user_id,
+        content,
+        sms_template_code,
+        sop_log_id,
+        server_id,
+        sms_index
+        FROM
+        qw_sop_sms_logs
+        <where>
+            <choose>
+                <when test="serverIds != null and serverIds.size() > 0">
+                    server_id IN
+                    <foreach collection="serverIds" item="serverId" open="(" separator="," close=")">
+                        #{serverId}
+                    </foreach>
+                    AND status = #{status}
+                    AND create_time &lt; #{endTime}
+                </when>
+                <otherwise>
+                    1 = 0
+                </otherwise>
+            </choose>
+        </where>
+        LIMIT #{pageSize}
+    </select>
+
+    <update id="updateStatusByIds">
+        UPDATE qw_sop_sms_logs SET status = #{status}, update_time = NOW() WHERE id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
+    <select id="selectQwSopSmsLogsByIdAndIndex"  resultType="com.fs.qw.domain.QwSopSmsLogs">
+        SELECT
+            id,
+            status,
+            sms_index
+        FROM
+            qw_sop_sms_logs
+        where id = #{id} and `index` = #{index}
+    </select>
+
+    <update id="updateSmsNotifyInfo" parameterType="QwSopSmsLogs">
+        update qw_sop_sms_logs
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="qwSopSmsLogs.remark != null">remark = #{qwSopSmsLogs.remark},</if>
+            <if test="qwSopSmsLogs.status != null">status = #{qwSopSmsLogs.status},</if>
+            <if test="qwSopSmsLogs.sendTime != null">send_time = #{qwSopSmsLogs.sendTime},</if>
+        </trim>
+        where sop_log_id = #{qwSopSmsLogs.sopLogId} AND sms_index = #{qwSopSmsLogs.smsIndex}
+    </update>
+
+    <insert id="batchInsertQwSopSmsLogsOneTouch" parameterType="java.util.List" useGeneratedKeys="false">
+        INSERT INTO qw_sop_sms_logs
+        (
+         sop_id, sop_log_id, qw_user_id, company_id, company_user_id, contact_id, fs_user_id,
+         content, server_id, status, send_time, sms_template_code, create_time,sms_index
+        )
+        VALUES
+        <foreach collection="qwSopSmsLogs" item="log" separator=",">
+            (
+            #{log.sopId},
+            #{log.sopLogId},
+            #{log.qwUserId},
+            #{log.companyId},
+            #{log.companyUserId},
+            #{log.contactId},
+            #{log.fsUserId},
+            #{log.content},
+            #{log.serverId},
+            #{log.status},
+            #{log.sendTime},
+            #{log.smsTemplateCode},
+            #{log.createTime},
+            #{log.smsIndex}
+            )
+        </foreach>
+    </insert>
+</mapper>

+ 82 - 4
fs-service/src/main/resources/mapper/sop/QwSopLogsMapper.xml

@@ -600,7 +600,7 @@
         qw_userid, external_user_id,external_id, external_user_name, log_type,
         qw_userid, external_user_id,external_id, external_user_name, log_type,
         content_json, send_status, send_time, real_send_time, send_type,
         content_json, send_status, send_time, real_send_time, send_type,
         company_id, receiving_status, msg_id, sop_id, remark,
         company_id, receiving_status, msg_id, sop_id, remark,
-        corp_id, customer_id,fs_user_id,sort,qw_user_key,app_send_status
+        corp_id, customer_id,fs_user_id,sort,qw_user_key,app_send_status,sms_logs_id
         )
         )
         VALUES
         VALUES
         <foreach collection="qwSopLogs" item="log" separator=",">
         <foreach collection="qwSopLogs" item="log" separator=",">
@@ -625,7 +625,8 @@
             #{log.fsUserId},
             #{log.fsUserId},
             #{log.sort},
             #{log.sort},
             #{log.qwUserKey},
             #{log.qwUserKey},
-            #{log.appSendStatus}
+            #{log.appSendStatus},
+            #{log.smsLogsId}
             )
             )
         </foreach>
         </foreach>
     </insert>
     </insert>
@@ -636,7 +637,8 @@
         qw_userid, external_user_id,external_id, external_user_name, log_type,
         qw_userid, external_user_id,external_id, external_user_name, log_type,
         content_json, send_status, send_time, real_send_time, send_type,
         content_json, send_status, send_time, real_send_time, send_type,
         company_id, receiving_status, msg_id, sop_id, remark,
         company_id, receiving_status, msg_id, sop_id, remark,
-        corp_id, customer_id, fs_user_id, expiration_time,sort,user_logs_id,take_records,qw_user_key,app_send_status,app_send_remark
+        corp_id, customer_id, fs_user_id, expiration_time,sort,user_logs_id,take_records,
+         qw_user_key,app_send_status,app_send_remark,sms_logs_id
         )
         )
         VALUES
         VALUES
         <foreach collection="qwSopLogs" item="log" separator=",">
         <foreach collection="qwSopLogs" item="log" separator=",">
@@ -665,7 +667,8 @@
             #{log.takeRecords},
             #{log.takeRecords},
             #{log.qwUserKey},
             #{log.qwUserKey},
             #{log.appSendStatus},
             #{log.appSendStatus},
-            #{log.appSendRemark}
+            #{log.appSendRemark},
+            #{log.smsLogsId}
             )
             )
         </foreach>
         </foreach>
     </insert>
     </insert>
@@ -894,4 +897,79 @@
         ]]>
         ]]>
         order by ql.sort DESC ,ql.send_time asc limit 30
         order by ql.sort DESC ,ql.send_time asc limit 30
     </select>
     </select>
+
+
+    <select id="selectSendTypeOneNoSend" resultType="com.fs.sop.domain.QwSopLogs">
+        select * from qw_sop_logs where send_type = 1 and send_status = 3
+    </select>
+    <select id="selectAllAppEByQwUserId" resultType="com.fs.sop.domain.QwSopLogs">
+        SELECT
+            ql.*,
+            qs.name,
+            qs.expiry_time AS expiryTime
+        FROM qw_sop_logs ql FORCE INDEX (idx_app_send_force)
+        LEFT JOIN qw_sop qs ON qs.id = ql.sop_id
+        WHERE ql.qw_user_key = #{id}
+          AND ql.log_type = 2
+          AND ql.send_type > 1
+          AND ql.is_have_app = 1
+          AND ql.app_send_status = 0
+        <![CDATA[
+          AND ql.send_time <= now()
+        ]]>
+        ORDER BY ql.sort DESC, ql.send_time ASC
+            LIMIT 800;
+    </select>
+
+    <select id="selectoneByUid" resultType="com.fs.sop.domain.QwSopLogs">
+        select * from  qw_sop_logs where sms_logs_id = #{sopSmsLogId}
+    </select>
+
+    <update id="updateQwSopLogsBySmsLogsId">
+        UPDATE qw_sop_logs SET send_status = #{data.sendStatus},real_send_time = NOW(),content_json = #{data.contentJson}
+        <if test="data.remark != null and data.remark != ''">
+            ,remark = #{data.remark}
+        </if>
+        WHERE sms_logs_id = #{data.smsLogsId}
+    </update>
+
+    <update id="batchUpdateQwSopLogsBySmsLogsId" parameterType="list">
+        UPDATE qw_sop_logs
+        <set>
+            send_status = 0,
+            remark = CASE sms_logs_id
+            <foreach collection="list" item="item" separator=" ">
+                WHEN #{item.smsLogsId} THEN #{item.remark}
+            </foreach>
+            END,
+            content_json = COALESCE(
+            CASE sms_logs_id
+            <foreach collection="list" item="item" separator=" ">
+                WHEN #{item.smsLogsId} THEN
+                <choose>
+                    <when test="item.contentJson != null and item.contentJson != ''">
+                        #{item.contentJson}
+                    </when>
+                    <otherwise>
+                        NULL
+                    </otherwise>
+                </choose>
+            </foreach>
+            END,
+            content_json
+            )
+        </set>
+        WHERE sms_logs_id IN
+        <foreach collection="list" item="item" open="(" separator="," close=")">
+            #{item.smsLogsId}
+        </foreach>
+    </update>
+
+    <select id="getQwSopInfoByUid" resultType="com.fs.sop.domain.QwSopLogs">
+        select id,send_status,expiration_time,send_time,external_id,send_type,sms_logs_id,content_json,qw_user_key from qw_sop_logs where sms_logs_id IN
+        <foreach item="item" collection="sopSmsLogIds" separator="," open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+
 </mapper>
 </mapper>