Selaa lähdekoodia

从润天迁移 短信发送

三七 2 päivää sitten
vanhempi
commit
43c4d9640b
23 muutettua tiedostoa jossa 1840 lisäystä ja 32 poistoa
  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.IQwGroupChatService;
 import com.fs.qw.service.IQwGroupChatUserService;
+import com.fs.qw.service.IQwSopSmsLogsService;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.vo.GroupUserExternalVo;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
@@ -74,6 +75,7 @@ import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
+import static com.fs.course.utils.LinkUtil.generateRandomNumberWithLock;
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 
 @Service
@@ -132,6 +134,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Autowired
     private IQwSopLogsService qwSopLogsService;
 
+    @Autowired
+    private IQwSopSmsLogsService qwSopSmsLogsService;
+
     @Autowired
     private QwSopLogsMapper qwSopLogsMapper;
 
@@ -177,6 +182,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
     // Blocking queues with bounded capacity to implement backpressure
     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<FsCourseLink> linkQueue = 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 companyId = String.valueOf(qwUserByRedis.getCompanyId()).trim();
             Integer sendMsgType = qwUserByRedis.getSendMsgType();
+            Long serverId = qwUserByRedis.getServerId();
 
             if (StringUtil.strIsNullOrEmpty(companyUserId) || StringUtil.strIsNullOrEmpty(companyId) || "null".equals(companyUserId)) {
                 log.error("员工未绑定销售账号或公司,跳过处理:" + qwUserId);
@@ -668,7 +675,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
                         insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, qwUserId,
                                 companyUserId, companyId, qwUserByRedis.getWelcomeText(), qwUserByRedis.getQwUserName(),
-                                groupChatMap, miniAppId, config, miniMap, sendMsgType, companies);
+                                groupChatMap, miniAppId, config, miniMap, sendMsgType, companies, serverId);
 
                     }
                 } catch (Exception e) {
@@ -714,7 +721,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                    String qwUserId, String companyUserId, String companyId, String welcomeText, String qwUserName,
                                    Map<String, QwGroupChat> groupChatMap, String miniAppId, CourseConfig config,
                                    Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
-                                   List<Company> companies) {
+                                   List<Company> companies,Long serverId) {
         String formattedSendTime = sendTime.toInstant()
                 .atZone(ZoneId.systemDefault())
                 .format(DATE_TIME_FORMATTER);
@@ -787,7 +794,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 ruleTimeVO.setType(2);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                         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) {
 //                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());
                     handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                             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) {
                     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,
                                       String companyUserId, String companyId, String externalId, String welcomeText,
                                       String qwUserName, Long fsUserId, boolean isGroupChat, String miniAppId,
-                                      QwGroupChat groupChat, CourseConfig config,
-                                      Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                      Integer grade, Integer sendMsgType, List<Company> companies, Long 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) {
             case 1:
                 handleNormalMessage(sopLogs, content, companyUserId, companyId, isGroupChat, qwUserId, groupChat, externalId, logVo,sendTime);
@@ -939,7 +946,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             case 2:
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
                         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;
             case 3:
                 handleOrderMessage(sopLogs, content);
@@ -1603,7 +1610,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                      String companyId, String externalId, String welcomeText, String qwUserName,
                                      Long fsUserId, boolean isGroupChat, String miniAppId, QwGroupChat groupChat, CourseConfig config, Map<Long,
             Map<Integer, List<CompanyMiniapp>>> miniMap, Integer grade, Integer sendMsgType,
-                                     List<Company> companies) {
+                                     List<Company> companies,Long serverId) {
         QwExternalContact contact = null;
         if (logVo.getExternalId() != null) {
             contact = qwExternalContactMapper.selectById(logVo.getExternalId());
@@ -1615,11 +1622,16 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             return;
         }
 
-//
-//        Integer courseType = clonedContent.getCourseType();
 
         String isOfficial = clonedContent.getIsOfficial();
 
+
+        Long msgNum = Long.valueOf(generateRandomNumberWithLock());
+        sopLogs.setSmsLogsId(msgNum);
+
+
+        AtomicInteger index = new AtomicInteger(0);
+
         List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
         if (settings == null || settings.isEmpty()) {
 //            log.error("Cloned content settings are empty, skipping.");
@@ -1633,6 +1645,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         for (QwSopTempSetting.Content.Setting setting : settings) {
+
+            Integer currentIndex = index.getAndIncrement();
+
             switch (setting.getContentType()) {
                 //文字和短链一起
                 case "1":
@@ -1902,6 +1917,26 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         } else {
                             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;
                 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 放入队列
      */
@@ -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 队列并进行批量插入
      */
@@ -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
      */

+ 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 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
     @Synchronized
     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 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() {
         return type;

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

@@ -51,4 +51,48 @@ public class LinkUtil {
         // 示例:使用UUID(牺牲有序性,但保证唯一性)
         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}")
     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);
 
     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();
     }
 
+
+    @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 = "剩余数量")
     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不发送
      */
     private Integer appSendStatus;
-    
+
     /**
      * app发送备注
      */
     private String appSendRemark;
 
+    /**
+     * 短信执行记录表
+     */
+    private Long smsLogsId;
+
     // 构造函数
 //    public QwSopLogs() {
 //        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")
     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.ISopUserLogsInfoService;
 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.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
@@ -83,9 +80,11 @@ import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
+import static com.fs.course.utils.LinkUtil.generateRandomNumberWithLock;
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 
 @Service
@@ -138,6 +137,9 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     @Autowired
     private QwSopLogsMapper qwSopLogsMapper;
 
+    @Autowired
+    private QwSopSmsLogsMapper qwSopSmsLogsMapper;
+
     @Autowired
     private ISopUserLogsService sopUserLogsService;
 
@@ -522,6 +524,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             return  R.error().put("msg","企业不存在,请联系管理员");
         }
         List<QwSopLogs> sopLogsList;
+
+        //短信的
+        List<QwSopSmsLogs> sopSmsLogsList = new ArrayList<>();
+
         if(param.getFilterMode() != null && param.getFilterMode() == 2 && param.getChatIds() != null && param.getChatIds().length > 0){
 
             if (config.getRoomLinkAllow() != null && config.getRoomLinkAllow()) {
@@ -1191,13 +1197,21 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                        sopLogs.setSendType(20);
                    }
 
+                Long msgNum = Long.valueOf(generateRandomNumberWithLock());
+                sopLogs.setSmsLogsId(msgNum);
+
                 QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());
 
                 QwSopCourseFinishTempSetting setting=new    QwSopCourseFinishTempSetting();
 
                 List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
+
+                AtomicInteger index = new AtomicInteger(0);
+
                 for (QwSopCourseFinishTempSetting.Setting st : list) {
 
+                    Integer currentIndex = index.getAndIncrement();
+
                     //过滤违禁词
                     if ("1".equals(st.getContentType())){
                         replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
@@ -1564,6 +1578,26 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 } else {
                                     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;
                         //群公告(仅用于一键群发,个人不应该有群公告)
@@ -1602,6 +1636,11 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             processAndInsertQwSopLogsBySendMsg(sopLogsList);
         }
 
+        //批量插入短信的发送记录
+        if (!sopSmsLogsList.isEmpty()) {
+            processAndInsertQwSopSmsLogsBySendMsg(sopSmsLogsList);
+        }
+
         return R.ok();
     }
 
@@ -1755,14 +1794,23 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
             }else {
 
                 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 {
                     log.error("员工未绑定销售账号 请先绑定 :" + qwUser.getQwUserName()+",账号id:"+qwUser.getQwUserId());
                 }
@@ -1776,10 +1824,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         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");
 
@@ -1794,6 +1842,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         }
         List<QwSopLogs> sopLogsList=new ArrayList<>();
 
+        List<QwSopSmsLogs> sopSmsLogsList = new ArrayList<>();
+
         //域名
 //        String domainName = companyUserMapper.selectDomainByUserId(qwUser.getCompanyUserId());
 //        if (StringUtils.isEmpty(domainName)){
@@ -1823,6 +1873,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
             sopLogs.setExternalUserName(item.getExternalUserName());
 
+
+            Long msgNum = Long.valueOf(generateRandomNumberWithLock());
+            sopLogs.setSmsLogsId(msgNum);
+
             QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());
 
             QwSopCourseFinishTempSetting setting=new   QwSopCourseFinishTempSetting();
@@ -1835,7 +1889,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 case 5:
                 case 20:
                     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);
                     break;
                 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,
                                                                       QwExternalContact contact,Date dataTime,String domainName,
                                                                       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);
 
+        AtomicInteger index = new AtomicInteger(0);
+
         for (QwSopCourseFinishTempSetting.Setting st : list) {
 
+            Integer currentIndex = index.getAndIncrement();
+
             //过滤违禁词
             if ("1".equals(st.getContentType())){
                 replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
@@ -2278,6 +2336,27 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         } else {
                             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;
                 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,
                                      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="number"    column="number"    />
         <result property="type"    column="type"    />
+        <result property="sopSmsLogId"    column="sop_sms_log_id"    />
+        <result property="smsIndex"    column="sms_index"    />
     </resultMap>
 
     <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>
 
 
@@ -52,6 +56,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="replyContent != null">reply_content,</if>
             <if test="number != null">number,</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 prefix="values (" suffix=")" suffixOverrides=",">
             <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="number != null">#{number},</if>
             <if test="type != null">#{type},</if>
+            <if test="sopSmsLogId != null">#{sopSmsLogId},</if>
+            <if test="smsIndex != null">#{smsIndex},</if>
          </trim>
     </insert>
 
@@ -90,6 +98,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="replyContent != null">reply_content = #{replyContent},</if>
             <if test="number != null">number = #{number},</if>
             <if test="type != null">type = #{type},</if>
+            <if test="sopSmsLogId != null">sop_sms_log_id = #{sopSmsLogId},</if>
         </trim>
         where logs_id = #{logsId}
     </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
     </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>

+ 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,
         content_json, send_status, send_time, real_send_time, send_type,
         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
         <foreach collection="qwSopLogs" item="log" separator=",">
@@ -625,7 +625,8 @@
             #{log.fsUserId},
             #{log.sort},
             #{log.qwUserKey},
-            #{log.appSendStatus}
+            #{log.appSendStatus},
+            #{log.smsLogsId}
             )
         </foreach>
     </insert>
@@ -636,7 +637,8 @@
         qw_userid, external_user_id,external_id, external_user_name, log_type,
         content_json, send_status, send_time, real_send_time, send_type,
         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
         <foreach collection="qwSopLogs" item="log" separator=",">
@@ -665,7 +667,8 @@
             #{log.takeRecords},
             #{log.qwUserKey},
             #{log.appSendStatus},
-            #{log.appSendRemark}
+            #{log.appSendRemark},
+            #{log.smsLogsId}
             )
         </foreach>
     </insert>
@@ -894,4 +897,79 @@
         ]]>
         order by ql.sort DESC ,ql.send_time asc limit 30
     </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>