Просмотр исходного кода

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin 3 месяцев назад
Родитель
Сommit
ef54db1720
49 измененных файлов с 4116 добавлено и 55 удалено
  1. 7 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  2. 542 0
      fs-ipad-task/src/main/java/com/fs/app/task/SendSmsMsg.java
  3. 127 11
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  4. 3 0
      fs-service/src/main/java/com/fs/common/service/ISmsService.java
  5. 254 21
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  6. 62 0
      fs-service/src/main/java/com/fs/company/domain/CompanyDetectionPhoneDailyStatistics.java
  7. 78 0
      fs-service/src/main/java/com/fs/company/domain/CompanyDetectionPhoneRecord.java
  8. 58 0
      fs-service/src/main/java/com/fs/company/domain/CompanySmsCommonDailyStatistics.java
  9. 98 0
      fs-service/src/main/java/com/fs/company/domain/CompanySmsCommonLogs.java
  10. 23 0
      fs-service/src/main/java/com/fs/company/domain/CompanySmsLogs.java
  11. 74 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyDetectionPhoneDailyStatisticsMapper.java
  12. 62 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyDetectionPhoneRecordMapper.java
  13. 87 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySmsCommonLogsMapper.java
  14. 31 0
      fs-service/src/main/java/com/fs/company/param/CompanyDetectionPhoneSendLinkParam.java
  15. 34 0
      fs-service/src/main/java/com/fs/company/param/SmsSendFsUserParam.java
  16. 68 0
      fs-service/src/main/java/com/fs/company/service/ICompanyDetectionPhoneDailyStatisticsService.java
  17. 74 0
      fs-service/src/main/java/com/fs/company/service/ICompanyDetectionPhoneRecordService.java
  18. 72 0
      fs-service/src/main/java/com/fs/company/service/ICompanySmsCommonLogsService.java
  19. 180 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyDetectionPhoneDailyStatisticsServiceImpl.java
  20. 303 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyDetectionPhoneRecordServiceImpl.java
  21. 319 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySmsCommonLogsServiceImpl.java
  22. 88 0
      fs-service/src/main/java/com/fs/company/util/DetectionPhoneModelVerifyUtil.java
  23. 3 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  24. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseH5ListParam.java
  25. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseLinkService.java
  26. 15 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java
  27. 44 0
      fs-service/src/main/java/com/fs/course/utils/LinkUtil.java
  28. 14 0
      fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java
  29. 6 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  30. 8 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  31. 5 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  32. 1 0
      fs-service/src/main/java/com/fs/qw/domain/QwIpadServer.java
  33. 83 0
      fs-service/src/main/java/com/fs/qw/domain/QwSopSmsLogs.java
  34. 131 0
      fs-service/src/main/java/com/fs/qw/mapper/QwSopSmsLogsMapper.java
  35. 108 0
      fs-service/src/main/java/com/fs/qw/service/IQwSopSmsLogsService.java
  36. 143 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwSopSmsLogsServiceImpl.java
  37. 6 1
      fs-service/src/main/java/com/fs/sop/domain/QwSopLogs.java
  38. 23 0
      fs-service/src/main/java/com/fs/sop/mapper/QwSopLogsMapper.java
  39. 115 15
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  40. 35 0
      fs-service/src/main/java/com/fs/sop/vo/SopLogsResult.java
  41. 101 0
      fs-service/src/main/resources/mapper/company/CompanyDetectionPhoneDailyStatisticsMapper.xml
  42. 116 0
      fs-service/src/main/resources/mapper/company/CompanyDetectionPhoneRecordMapper.xml
  43. 146 0
      fs-service/src/main/resources/mapper/company/CompanySmsCommonLogsMapper.xml
  44. 10 1
      fs-service/src/main/resources/mapper/company/CompanySmsLogsMapper.xml
  45. 9 0
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  46. 7 0
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  47. 244 0
      fs-service/src/main/resources/mapper/qw/QwSopSmsLogsMapper.xml
  48. 82 4
      fs-service/src/main/resources/mapper/sop/QwSopLogsMapper.xml
  49. 13 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

+ 7 - 2
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -1,5 +1,6 @@
 package com.fs.company.controller.company;
 package com.fs.company.controller.company;
 
 
+import com.alibaba.fastjson.JSONObject;
 import com.fs.aicall.domain.BaseDomain;
 import com.fs.aicall.domain.BaseDomain;
 import com.fs.aicall.domain.TaskInfo;
 import com.fs.aicall.domain.TaskInfo;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
@@ -28,6 +29,7 @@ import com.fs.company.vo.CdrDetailVo;
 import com.fs.company.vo.WorkflowExecRecordVo;
 import com.fs.company.vo.WorkflowExecRecordVo;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.framework.service.TokenService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
@@ -45,6 +47,7 @@ import java.util.stream.Collectors;
  */
  */
 @RestController
 @RestController
 @RequestMapping("/company/companyVoiceRobotic")
 @RequestMapping("/company/companyVoiceRobotic")
+@Slf4j
 public class CompanyVoiceRoboticController extends BaseController
 public class CompanyVoiceRoboticController extends BaseController
 {
 {
     @Autowired
     @Autowired
@@ -225,8 +228,10 @@ public class CompanyVoiceRoboticController extends BaseController
     }
     }
 
 
     @PostMapping("/callerResult4EasyCall")
     @PostMapping("/callerResult4EasyCall")
-    public R callerResult4EasyCall(CdrDetailVo cdr) {
-        companyVoiceRoboticService.callerResult4EasyCall(cdr);
+    public R callerResult4EasyCall(@RequestBody String cdrStr) {
+        log.info(cdrStr);
+        CdrDetailVo cdrDetailVo = JSONObject.parseObject(cdrStr, CdrDetailVo.class);
+        companyVoiceRoboticService.callerResult4EasyCall(cdrDetailVo);
         return R.ok();
         return R.ok();
     }
     }
     /**
     /**

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

@@ -0,0 +1,542 @@
+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);
+            // 判断消息状态是否满足发送条件
+            SendResultDetailDTO checkDto= isSendLogs(qwSopLogs, setting,logRecord);
+            if (!checkDto.isSuccess()) {
+                log.info("销售:{}, 消息发送条件未满足:{}", qwSopLogs.getQwUserKey(), qwSopLogs.getId());
+                failReasonsList.add(checkDto);
+                failedIds.add(logRecord.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 SendResultDetailDTO isSendLogs(QwSopLogs qwSopLogs, QwSopCourseFinishTempSetting setting,QwSopSmsLogs logRecord) {
+        Long qwUserId = qwSopLogs.getQwUserKey();
+        if(qwSopLogs.getSendStatus() != 3){
+            log.info("状态异常不发送:{}, LOGID: {}", qwSopLogs.getQwUserKey(), qwSopLogs.getId());
+            return new SendResultDetailDTO(false, "状态异常不发送", logRecord.getSopLogId());
+        }
+
+        boolean noSop = qwSopLogs.getSendType() != 3 && qwSopLogs.getSendType() != 7;
+
+        if (qwSopLogs.getExpiryTime() == null && noSop) {
+            // 作废消息
+            log.info("SOP_LOG_ID:{}, SOP任务被删除", qwSopLogs.getId());
+            return new SendResultDetailDTO(false, "SOP任务被删除", logRecord.getSopLogId());
+        }
+
+        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());
+            return new SendResultDetailDTO(false, "已过期,不发送", logRecord.getSopLogId());
+        }
+
+        if (setting.getCourseType() == null && noSop && setting.getType() == 2) {
+            log.info("SOP_LOG_ID:{}, 模板未选消息类型,不发送", qwSopLogs.getId());
+            return new SendResultDetailDTO(false, "模板未选消息类型,不发送", logRecord.getSopLogId());
+        }
+        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());
+            return new SendResultDetailDTO(false, "课程暂停,AI不发送", logRecord.getSopLogId());
+        }
+
+        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());
+                        return new SendResultDetailDTO(false, "看课状态未满足,不发送", logRecord.getSopLogId());
+                    }
+                } else {
+                    log.info("SOP_LOG_ID:{}, 无观看记录,不发送", qwSopLogs.getId());
+                    return new SendResultDetailDTO(false, "无观看记录,不发送", logRecord.getSopLogId());
+                }
+            }
+        }
+        return new SendResultDetailDTO(true, "检测通过", logRecord.getSopLogId());
+    }
+
+}

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

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

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

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

+ 254 - 21
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -3,6 +3,8 @@ package com.fs.common.service.impl;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.json.JSONUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCache;
 
 
@@ -11,10 +13,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsSendItemVO;
 import com.fs.common.vo.SmsSendItemVO;
 import com.fs.common.vo.SmsSendVO;
 import com.fs.common.vo.SmsSendVO;
-import com.fs.company.domain.CompanySms;
-import com.fs.company.domain.CompanySmsLogs;
-import com.fs.company.domain.CompanySmsTemp;
-import com.fs.company.domain.CompanyUser;
+import com.fs.company.domain.*;
 import com.fs.company.service.ICompanySmsLogsService;
 import com.fs.company.service.ICompanySmsLogsService;
 import com.fs.company.service.ICompanySmsService;
 import com.fs.company.service.ICompanySmsService;
 import com.fs.company.service.ICompanySmsTempService;
 import com.fs.company.service.ICompanySmsTempService;
@@ -30,8 +29,13 @@ import com.fs.his.domain.FsStoreOrder;
 import com.fs.his.mapper.FsPackageOrderMapper;
 import com.fs.his.mapper.FsPackageOrderMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.vo.FsPackageOrderVO;
 import com.fs.his.vo.FsPackageOrderVO;
+import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.qw.mapper.QwSopSmsLogsMapper;
+import com.fs.qw.service.IQwSopSmsLogsService;
 import com.fs.sms.domain.SendSmsReturn;
 import com.fs.sms.domain.SendSmsReturn;
 import com.fs.sms.service.impl.SmsTServiceImpl;
 import com.fs.sms.service.impl.SmsTServiceImpl;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysConfigMapper;
 import com.google.gson.Gson;
 import com.google.gson.Gson;
@@ -48,6 +52,7 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentHashMap;
+import com.fs.company.service.ICompanySmsCommonLogsService;
 
 
 @Service
 @Service
 @Slf4j
 @Slf4j
@@ -79,6 +84,18 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     @Autowired
     private FsPackageOrderMapper packageOrderMapper;
     private FsPackageOrderMapper packageOrderMapper;
 
 
+    @Autowired
+    private ICompanySmsCommonLogsService smsCommonLogsService;
+
+    @Autowired
+    private IQwSopSmsLogsService qwSopSmsLogsService;
+
+    @Autowired
+    private QwSopLogsMapper qwSopLogsMapper;
+
+    @Autowired
+    private QwSopSmsLogsMapper qwSopSmsLogsMapper;
+
     @Override
     @Override
     public R sendTSms(String mobile, String code) {
     public R sendTSms(String mobile, String code) {
 //        try{
 //        try{
@@ -157,40 +174,80 @@ public class SmsServiceImpl implements ISmsService
 
 
     @Override
     @Override
     public String smsNotify(String json) {
     public String smsNotify(String json) {
+        log.info("短信回调参数:{}", json);
         List<SmsNotifyVO> list = JSONUtil.parseArray(json).toList(SmsNotifyVO.class);
         List<SmsNotifyVO> list = JSONUtil.parseArray(json).toList(SmsNotifyVO.class);
-        if(list!=null){
-            for(SmsNotifyVO vo:list){
-                if(vo.getFlag()!=null&&vo.getFlag()==0){
+        if (list != null) {
+            for (SmsNotifyVO vo : list) {
+                if (vo.getFlag() != null && vo.getFlag() == 0) {
                     //手机用户上行
                     //手机用户上行
-                    CompanySmsLogs logs=smsLogsService.selectCompanySmsLogsByMobile(vo.getMobile());
-                    if(logs!=null){
+                    CompanySmsLogs logs = smsLogsService.selectCompanySmsLogsByMobile(vo.getMobile());
+                    if (logs != null) {
                         logs.setReplyContent(vo.getContent());
                         logs.setReplyContent(vo.getContent());
                         smsLogsService.updateCompanySmsLogs(logs);
                         smsLogsService.updateCompanySmsLogs(logs);
+                        if(logs.getSopSmsLogId() != null){
+                            updateQwSopSmsLogsByUuid(logs.getSopSmsLogId(), null, null, vo.getContent(),logs.getSmsIndex());
+                        }
                     }
                     }
-                }
-                else if(vo.getFlag()!=null&&vo.getFlag()==1){
-                    CompanySmsLogs logs=smsLogsService.selectCompanySmsLogsByMid(vo.getMid());
-                    if(logs!=null&&logs.getStatus().equals(0)){
+                    CompanySmsCommonLogs commonLogs = smsCommonLogsService.selectCompanySmsCommonLogsByMobile(vo.getMobile());
+                    if (commonLogs != null) {
+                        commonLogs.setReplyContent(vo.getContent());
+                        smsCommonLogsService.updateCompanySmsCommonLogs(commonLogs);
+                    }
+                } else if (vo.getFlag() != null && vo.getFlag() == 1) {
+                    CompanySmsLogs logs = smsLogsService.selectCompanySmsLogsByMid(vo.getMid());
+                    if (logs != null && logs.getStatus().equals(0)) {
                         logs.setStat(vo.getStat());
                         logs.setStat(vo.getStat());
-                        if(vo.getTime()!=null){
+                        if (vo.getTime() != null) {
                             try {
                             try {
-                                SimpleDateFormat format = new   SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                                 logs.setSendTime(format.parse(vo.getTime()));
                                 logs.setSendTime(format.parse(vo.getTime()));
-                            }
-                            catch (Exception e){
+                            } catch (Exception e) {
 
 
                             }
                             }
                         }
                         }
                         //状态报告
                         //状态报告
-                        if(vo.getStat().equals("DELIVRD")){
+                        if (vo.getStat().equals("DELIVRD")) {
                             logs.setStatus(1);
                             logs.setStatus(1);
-                        }
-                        else{
+                        } else {
                             logs.setStatus(-1);
                             logs.setStatus(-1);
-                            companySmsService.addCompanySms(logs.getCompanyId(),logs.getNumber());
+                            companySmsService.addCompanySms(logs.getCompanyId(), logs.getNumber());
                         }
                         }
                         smsLogsService.updateCompanySmsLogs(logs);
                         smsLogsService.updateCompanySmsLogs(logs);
                     }
                     }
+
+                    if(logs != null && logs.getSopSmsLogId() != null){
+                        Integer sopStatus = "DELIVRD".equals(vo.getStat()) ? 2 : 3;
+                        Date sopSendTime = null;
+                        if (vo.getTime() != null) {
+                            try {
+                                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                                sopSendTime = format.parse(vo.getTime());
+                            } catch (Exception ignored) { }
+                        }
+                        updateQwSopSmsLogsByUuid(logs.getSopSmsLogId(), sopStatus, sopSendTime, null,logs.getSmsIndex());
+                    }
+
+                    CompanySmsCommonLogs commonLogs = smsCommonLogsService.selectCompanySmsCommonLogsByMid(vo.getMid());
+                    if (commonLogs != null && commonLogs.getStatus().equals(0)) {
+                        commonLogs.setStat(vo.getStat());
+                        if (vo.getTime() != null) {
+                            try {
+                                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                                commonLogs.setSendTime(format.parse(vo.getTime()));
+                            } catch (Exception e) {
+
+                            }
+                        }
+                        //状态报告
+                        if (vo.getStat().equals("DELIVRD")) {
+                            commonLogs.setStatus(1);
+                        } else {
+                            commonLogs.setStatus(-1);
+                            companySmsService.addCompanySms(commonLogs.getCompanyId(), commonLogs.getNumber());
+                        }
+                        smsCommonLogsService.updateCompanySmsCommonLogs(commonLogs);
+
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -611,6 +668,110 @@ public class SmsServiceImpl implements ISmsService
     }
     }
 
 
 
 
+    @Override
+    public R sendUrl(String phone, String content, String code,Long uuid,Integer smsIndex,String deleteKey) {
+        log.info("发送短信:{},链接地址:{},短信模板:{}", phone, content, code);
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(code);
+        if (temp == null) {
+            log.info("{},未找到短信模板:{}", phone, code);
+            return R.error("没有模板");
+        }
+        String urls = null;
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
+        FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
+        if (sms.getType().equals("rf")) {
+            try {
+//                content = content.replace("${sms.sign}",sms.getRfSign2());
+                urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + phone + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+            } catch (UnsupportedEncodingException e) {
+                log.error("{}发送失败", phone, e);
+                return R.error("短信发送失败:" + e.getMessage());
+            }
+            String post = HttpRequest.get(urls)
+                    .execute().body();
+            SmsSendVO vo = JSONUtil.toBean(post, SmsSendVO.class);
+            if (vo.getStatus().equals(0)) {
+                for (SmsSendItemVO itemVO : vo.getList()) {
+                    if (itemVO.getResult().equals("0")) {
+                        CompanySmsLogs logs = new CompanySmsLogs();
+                        logs.setContent(content);
+                        logs.setTempCode(temp.getTempCode());
+                        logs.setTempId(temp.getTempId());
+                        logs.setPhone(phone);
+                        logs.setSendTime(new Date());
+                        logs.setStatus(0);
+                        logs.setType(sms.getType());
+                        logs.setMid(itemVO.getMid());
+                        if (uuid != null) {
+                            logs.setSopSmsLogId(uuid);
+                        }
+                        if (smsIndex != null) {
+                            logs.setSmsIndex(smsIndex);
+                        }
+                        int counts = logs.getContent().length() / 67;
+                        if (logs.getContent().length() % 67 > 0) {
+                            counts = counts + 1;
+                        }
+                        if (counts == 0) {
+                            counts = 1;
+                        }
+                        logs.setNumber(counts);
+                        smsLogsService.insertCompanySmsLogs(logs);
+                    }else{
+                        log.info("{}不发送短信-itemVO.getResult().equals(\"0\"):{}", phone, itemVO);
+                    }
+                }
+            }else{
+                log.info("{}不发送短信-vo.getStatus().equals(0):{}", phone, vo.getStatus());
+
+                return R.error("发送短信失败!");
+            }
+        } else if (sms.getType().equals("dh")) {
+            SendSmsReturn sendSmsReturn = null;
+            content = content.replace("${sms.sign}",sms.getDhSign());
+            sendSmsReturn = smsTService.sendSms(sms.getDhAccount1(), sms.getDhPassword1(), content, phone);
+            if (sendSmsReturn != null) {
+                if (sendSmsReturn.getResult() != null && sendSmsReturn.getResult().equals("0")) {
+                    CompanySmsLogs logs = new CompanySmsLogs();
+                    logs.setContent(content);
+                    logs.setTempCode(temp.getTempCode());
+                    logs.setTempId(temp.getTempId());
+                    logs.setPhone(phone);
+                    logs.setSendTime(new Date());
+                    logs.setStatus(0);
+                    logs.setType(sms.getType());
+                    logs.setMid(sendSmsReturn.getMsgid());
+                    if (uuid != null) {
+                        logs.setSopSmsLogId(uuid);
+                    }
+                    if (smsIndex != null) {
+                        logs.setSmsIndex(smsIndex);
+                    }
+                    int counts = logs.getContent().length() / 67;
+                    if (logs.getContent().length() % 67 > 0) {
+                        counts = counts + 1;
+                    }
+                    if (counts == 0) {
+                        counts = 1;
+                    }
+                    logs.setNumber(counts);
+                    smsLogsService.insertCompanySmsLogs(logs);
+                }else{
+                    log.info("{}不发送短信-sendSmsReturn.getResult() != null && sendSmsReturn.getResult().equals(\"0\"):{}", phone, sendSmsReturn);
+                }
+            }else{
+                log.info("{}不发送短信-sendSmsReturn != null", phone);
+            }
+        }
+        //删除redis缓存
+        if(StringUtils.isNotEmpty(deleteKey)){
+            redisCache.deleteObject(deleteKey);
+        }
+        return R.ok();
+    }
+
+
+
     @Override
     @Override
     @Synchronized
     @Synchronized
     public R sendBatchSms(SmsSendBatchParam param) {
     public R sendBatchSms(SmsSendBatchParam param) {
@@ -875,4 +1036,76 @@ public class SmsServiceImpl implements ISmsService
             }
             }
         }
         }
     }
     }
+
+    /**
+     * 根据 uuid(QwSopSmsLogs.id)和索引更新子记录,并检查主记录完成状态
+     */
+    private void updateQwSopSmsLogsByUuid(Long sopSmsLogId, Integer status, Date sendTime, String replyContent, Integer smsIndex) {
+        if (sopSmsLogId == null) return;
+        QwSopSmsLogs update = new QwSopSmsLogs();
+        update.setSopLogId(sopSmsLogId);
+        update.setSmsIndex(smsIndex);
+        if (status != null) {
+            update.setStatus(status);
+        }
+        if (sendTime != null) {
+            update.setSendTime(sendTime);
+        }
+        if (replyContent != null) {
+            update.setRemark(replyContent);
+        }
+
+        qwSopSmsLogsService.updateSmsNotifyInfo(update);
+        QwSopLogs mainLog = qwSopLogsMapper.selectoneByUid(sopSmsLogId);
+        if (mainLog == null) return;
+        JSONObject jsonObject = JSONObject.parseObject(mainLog.getContentJson());
+        JSONArray jsonArray = jsonObject.getJSONArray("setting");
+        int total = jsonArray.size();
+
+        Integer processed = qwSopSmsLogsMapper.countProcessed(sopSmsLogId);
+
+        if (processed != null && processed >= total) {
+            int successCount = 0;
+            int failCount = 0;
+
+            List<QwSopSmsLogs> list = qwSopSmsLogsMapper.getSmsListBySopLogId(sopSmsLogId);
+            //更新对应content_json信息
+            JSONArray newArray = new JSONArray();
+            for (QwSopSmsLogs sub : list) {
+                JSONObject json = jsonArray.getJSONObject(sub.getSmsIndex());
+                if (sub.getStatus() == 2) {
+                    json.put("sendStatus","1");
+                    successCount++;
+                } else if (sub.getStatus() == 3) {
+                    json.put("sendStatus","0");
+                    failCount++;
+                }
+                newArray.add(json);
+            }
+
+            boolean allSuccess = (successCount == list.size());
+            boolean allFail = (failCount == list.size());
+
+            QwSopLogs updateLog = new QwSopLogs();
+            updateLog.setSmsLogsId(sopSmsLogId);
+            jsonObject.put("setting", newArray);
+            updateLog.setContentJson(jsonObject.toJSONString());
+            if (allSuccess) {
+                updateLog.setSendStatus(1L);
+                log.info("主记录 {} 全部成功", sopSmsLogId);
+            } else if (allFail) {
+                updateLog.setSendStatus(0L);
+                log.info("主记录 {} 全部失败", sopSmsLogId);
+            } else {
+                updateLog.setRemark("部分成功!");
+                updateLog.setSendStatus(0L); // 部分成功
+                log.info("主记录 {} 部分成功(成功{}条,失败{}条)",
+                        sopSmsLogId, successCount, failCount);
+            }
+
+
+            qwSopLogsMapper.updateQwSopLogsBySmsLogsId(updateLog);
+        }
+    }
+
 }
 }

+ 62 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyDetectionPhoneDailyStatistics.java

@@ -0,0 +1,62 @@
+package com.fs.company.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;
+
+/**
+ * 机型检测日统计对象 company_detection_phone_daily_statistics
+ *
+ * @author fs
+ * @date 2025-12-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyDetectionPhoneDailyStatistics extends BaseEntity{
+
+    /** 日统计id */
+    private Long id;
+
+    /** 销售公司id */
+    @Excel(name = "销售公司id")
+    private Long companyId;
+
+    /** 销售公司名称 */
+    @Excel(name = "销售公司名称")
+    private String companyName;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 销售名称 */
+    @Excel(name = "销售名称")
+    private String companyUserName;
+
+    /** 检测时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "检测时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date detectionTime;
+
+    /** 安卓次数 */
+    @Excel(name = "安卓次数")
+    private Integer azCount;
+
+    /** 苹果次数 */
+    @Excel(name = "苹果次数")
+    private Integer iosCount;
+
+    /** 未知次数 */
+    @Excel(name = "未知次数")
+    private Integer unknownCount;
+
+    /** 总计 */
+    @Excel(name = "总计")
+    private Integer totalCount;
+
+
+}

+ 78 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyDetectionPhoneRecord.java

@@ -0,0 +1,78 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.*;
+
+import java.util.Date;
+
+/**
+ * 机型检测记录对象 company_detection_phone_record
+ *
+ * @author fs
+ * @date 2025-12-17
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class CompanyDetectionPhoneRecord extends BaseEntity{
+
+    /** 检测记录id */
+    private Long id;
+
+    /** 企业id */
+    @Excel(name = "企业id")
+    private Long companyId;
+
+    /** 企业名称 */
+    @Excel(name = "企业名称")
+    private String companyName;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 销售名称 */
+    @Excel(name = "销售名称")
+    private String companyUserName;
+    /**
+     * 客户id
+     */
+    @Excel(name = "客户id")
+    private Long userId;
+    /**
+     * 客户昵称
+     */
+    @Excel(name = "客户昵称")
+    private String nickName;
+
+    /** 检测手机 */
+    @Excel(name = "检测手机")
+    private String phoneNumber;
+
+    /** 检测系统 */
+    @Excel(name = "检测系统")
+    private Integer detectionSystem;
+
+    /** 检测机型 */
+    @Excel(name = "检测机型")
+    private String detectionModel;
+
+    /** 检测时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "检测时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date detectionTime;
+
+    /** 检测状态 0:失败;1:成功 */
+    @Excel(name = "检测状态 0:失败;1:成功")
+    private Long status;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
+
+
+}

+ 58 - 0
fs-service/src/main/java/com/fs/company/domain/CompanySmsCommonDailyStatistics.java

@@ -0,0 +1,58 @@
+package com.fs.company.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;
+
+/**
+ * 短信通用日统计对象 company_sms_common_daily_statistics
+ *
+ * @author fs
+ * @date 2025-12-18
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanySmsCommonDailyStatistics extends BaseEntity{
+
+    /** id */
+    private Long id;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 销售ID */
+    @Excel(name = "销售ID")
+    private Long companyUserId;
+
+    /** 销售名称 */
+    @Excel(name = "销售名称")
+    private String companyUserName;
+
+    /** 发送类型 4:会员app下载链接 */
+    @Excel(name = "发送类型 4:会员app下载链接")
+    private Long sendType;
+
+    /** 最后一次发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "最后一次发送时间 ", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date lastSendTime;
+    /** 失败次数 */
+    @Excel(name = "失败次数")
+    private Integer failCount;
+    /** 发送中次数 */
+    @Excel(name = "发送中次数")
+    private Integer pendingCount;
+    /** 成功次数 */
+    @Excel(name = "成功次数")
+    private Integer successCount;
+    /** 总计 */
+    @Excel(name = "总计")
+    private Integer totalCount;
+
+
+}

+ 98 - 0
fs-service/src/main/java/com/fs/company/domain/CompanySmsCommonLogs.java

@@ -0,0 +1,98 @@
+package com.fs.company.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;
+
+/**
+ * 短信通用发送记录对象 company_sms_common_logs
+ *
+ * @author fs
+ * @date 2025-12-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanySmsCommonLogs extends BaseEntity{
+
+    /** ID */
+    private Long logsId;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 销售ID */
+    @Excel(name = "销售ID")
+    private Long companyUserId;
+    /** 销售ID */
+    @Excel(name = "销售昵称")
+    private String companyUserName;
+
+    /** 收信人ID */
+    @Excel(name = "收信人ID")
+    private Long recipientId;
+
+    /** 收信人名称 */
+    @Excel(name = "收信人名称")
+    private String recipientName;
+
+    /** 发送类型 4:会员app下载链接 */
+    @Excel(name = "发送类型 4:会员app下载链接")
+    private Integer sendType;
+
+    /** 模板ID */
+    @Excel(name = "模板ID")
+    private Long tempId;
+
+    /** 模板CODE */
+    @Excel(name = "模板CODE")
+    private String tempCode;
+
+    /** 手机号 */
+    @Excel(name = "手机号")
+    private String phone;
+
+    /** 短信内容 */
+    @Excel(name = "短信内容")
+    private String content;
+
+    /** 发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "发送时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date sendTime;
+
+    /** 状态 */
+    @Excel(name = "状态")
+    private Integer status;
+
+    /** $column.columnComment */
+    @Excel(name = "状态")
+    private String mid;
+
+    /** $column.columnComment */
+    @Excel(name = "状态")
+    private String stat;
+
+    /** $column.columnComment */
+    @Excel(name = "状态")
+    private String replyContent;
+
+    /** 短信数量 */
+    @Excel(name = "短信数量")
+    private Integer number;
+
+    /** 发送短信服务商 */
+    @Excel(name = "发送短信服务商")
+    private String type;
+
+    private Integer isReply;
+
+    /**
+     * sop短信发送记录uuid
+     * **/
+    private Long uuid;
+}

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

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

+ 74 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyDetectionPhoneDailyStatisticsMapper.java

@@ -0,0 +1,74 @@
+package com.fs.company.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyDetectionPhoneDailyStatistics;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 机型检测日统计Mapper接口
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+public interface CompanyDetectionPhoneDailyStatisticsMapper extends BaseMapper<CompanyDetectionPhoneDailyStatistics> {
+    /**
+     * 查询机型检测日统计
+     * 
+     * @param id 机型检测日统计主键
+     * @return 机型检测日统计
+     */
+    CompanyDetectionPhoneDailyStatistics selectCompanyDetectionPhoneDailyStatisticsById(Long id);
+
+    /**
+     * 查询机型检测日统计列表
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 机型检测日统计集合
+     */
+    List<CompanyDetectionPhoneDailyStatistics> selectCompanyDetectionPhoneDailyStatisticsList(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics);
+
+    /**
+     * 新增机型检测日统计
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 结果
+     */
+    int insertCompanyDetectionPhoneDailyStatistics(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics);
+
+    /**
+     * 修改机型检测日统计
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 结果
+     */
+    int updateCompanyDetectionPhoneDailyStatistics(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics);
+
+    /**
+     * 删除机型检测日统计
+     * 
+     * @param id 机型检测日统计主键
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneDailyStatisticsById(Long id);
+
+    /**
+     * 批量删除机型检测日统计
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneDailyStatisticsByIds(Long[] ids);
+    /**
+     * 根据销售id和创建时间查询公司用户
+     * @param companyUserId 销售id
+     * @param startDate 起始时间
+     * @return 结果
+     */
+    @Select("select * from company_detection_phone_daily_statistics where company_user_id=#{companyUserId} and DATE(create_time) =DATE(#{startDate})")
+    CompanyDetectionPhoneDailyStatistics selectByCompanyUserAndDate(@Param("companyUserId")Long companyUserId, @Param("startDate")Date startDate);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyDetectionPhoneRecordMapper.java

@@ -0,0 +1,62 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyDetectionPhoneRecord;
+
+import java.util.List;
+
+/**
+ * 机型检测记录Mapper接口
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+public interface CompanyDetectionPhoneRecordMapper extends BaseMapper<CompanyDetectionPhoneRecord>{
+    /**
+     * 查询机型检测记录
+     * 
+     * @param id 机型检测记录主键
+     * @return 机型检测记录
+     */
+    CompanyDetectionPhoneRecord selectCompanyDetectionPhoneRecordById(Long id);
+
+    /**
+     * 查询机型检测记录列表
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 机型检测记录集合
+     */
+    List<CompanyDetectionPhoneRecord> selectCompanyDetectionPhoneRecordList(CompanyDetectionPhoneRecord companyDetectionPhoneRecord);
+
+    /**
+     * 新增机型检测记录
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 结果
+     */
+    int insertCompanyDetectionPhoneRecord(CompanyDetectionPhoneRecord companyDetectionPhoneRecord);
+
+    /**
+     * 修改机型检测记录
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 结果
+     */
+    int updateCompanyDetectionPhoneRecord(CompanyDetectionPhoneRecord companyDetectionPhoneRecord);
+
+    /**
+     * 删除机型检测记录
+     * 
+     * @param id 机型检测记录主键
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneRecordById(Long id);
+
+    /**
+     * 批量删除机型检测记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneRecordByIds(Long[] ids);
+}

+ 87 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanySmsCommonLogsMapper.java

@@ -0,0 +1,87 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanySmsCommonDailyStatistics;
+import com.fs.company.domain.CompanySmsCommonLogs;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 短信通用发送记录Mapper接口
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+public interface CompanySmsCommonLogsMapper extends BaseMapper<CompanySmsCommonLogs>{
+    /**
+     * 查询短信通用发送记录
+     * 
+     * @param logsId 短信通用发送记录主键
+     * @return 短信通用发送记录
+     */
+    CompanySmsCommonLogs selectCompanySmsCommonLogsByLogsId(Long logsId);
+
+    /**
+     * 查询短信通用发送记录列表
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 短信通用发送记录集合
+     */
+    List<CompanySmsCommonLogs> selectCompanySmsCommonLogsList(CompanySmsCommonLogs companySmsCommonLogs);
+
+    /**
+     * 新增短信通用发送记录
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 结果
+     */
+    int insertCompanySmsCommonLogs(CompanySmsCommonLogs companySmsCommonLogs);
+
+    /**
+     * 修改短信通用发送记录
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 结果
+     */
+    int updateCompanySmsCommonLogs(CompanySmsCommonLogs companySmsCommonLogs);
+
+    /**
+     * 删除短信通用发送记录
+     * 
+     * @param logsId 短信通用发送记录主键
+     * @return 结果
+     */
+    int deleteCompanySmsCommonLogsByLogsId(Long logsId);
+
+    /**
+     * 批量删除短信通用发送记录
+     * 
+     * @param logsIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanySmsCommonLogsByLogsIds(Long[] logsIds);
+
+    @Select("select * from company_sms_common_logs where mid=#{msgid} ")
+    CompanySmsCommonLogs selectCompanySmsCommonLogsByMid(@Param("msgid") String msgid);
+    @Select("select * from company_sms_common_logs where phone=#{phone} order by logs_id desc limit 1")
+    CompanySmsCommonLogs selectCompanySmsCommonLogsByMobile(@Param("phone") String phone);
+
+    @Select("<script>" +
+            "SELECT " +
+            "    company_id, " +
+            "    company_user_id, " +
+            "    company_user_name, " +
+            "    send_type, " +
+            "    MAX(send_time) AS last_send_time, " +
+            "    COUNT(*) AS total_count, " +
+            "    SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) AS pendingCount, " +
+            "    SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS successCount, " +
+            "    SUM(CASE WHEN status = -1 THEN 1 ELSE 0 END) AS failCount " +
+            "FROM company_sms_common_logs " +
+            "WHERE DATE(send_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY) " +
+            "GROUP BY company_user_id, send_type" +
+            "</script>")
+    List<CompanySmsCommonDailyStatistics> yesterdayStatistics();
+}

+ 31 - 0
fs-service/src/main/java/com/fs/company/param/CompanyDetectionPhoneSendLinkParam.java

@@ -0,0 +1,31 @@
+package com.fs.company.param;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @Author:peicj
+ * @Description: 机型检测参数
+ * @Date:2025/12/17 11:18
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class CompanyDetectionPhoneSendLinkParam {
+
+    /**
+     * 会员id
+     */
+    private Long userId;
+    /**
+     * 会员名称
+     */
+    private String nickName;
+    /**
+     * 手机号码
+     */
+    private String mobile;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/company/param/SmsSendFsUserParam.java

@@ -0,0 +1,34 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @Author:peicj
+ * @Description: 发送给会员用户短信入参
+ * @Date:2025/12/17 16:03
+ */
+@Data
+public class SmsSendFsUserParam implements Serializable {
+
+//    @NotNull(message = "会员ID不能为空")
+    private Long  userId;
+//    @NotNull(message = "会员IDS不能为空")
+    private Long[] userIds;
+    @NotNull(message = "模板CODE不能为空")
+    private String tempCode;
+    private Long  companyId;
+    private Long  companyUserId;
+    private Integer smsType;//短信类型 1 CRM客户端信
+    @NotNull(message = "手机号不能为空")
+    private String mobile;
+    @NotNull(message = "短信内容不能为空")
+    private String content;
+
+    private String cardUrl; //名片链接
+    private String workUrl; //名片链接
+    private String downloaAppdUrl;//下载链接
+}
+

+ 68 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyDetectionPhoneDailyStatisticsService.java

@@ -0,0 +1,68 @@
+package com.fs.company.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyDetectionPhoneDailyStatistics;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 机型检测日统计Service接口
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+public interface ICompanyDetectionPhoneDailyStatisticsService extends IService<CompanyDetectionPhoneDailyStatistics>{
+    /**
+     * 查询机型检测日统计
+     * 
+     * @param id 机型检测日统计主键
+     * @return 机型检测日统计
+     */
+    CompanyDetectionPhoneDailyStatistics selectCompanyDetectionPhoneDailyStatisticsById(Long id);
+
+    /**
+     * 查询机型检测日统计列表
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 机型检测日统计集合
+     */
+    List<CompanyDetectionPhoneDailyStatistics> selectCompanyDetectionPhoneDailyStatisticsList(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics);
+
+    /**
+     * 新增机型检测日统计
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 结果
+     */
+    int insertCompanyDetectionPhoneDailyStatistics(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics);
+
+    /**
+     * 修改机型检测日统计
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 结果
+     */
+    int updateCompanyDetectionPhoneDailyStatistics(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics);
+
+    /**
+     * 批量删除机型检测日统计
+     * 
+     * @param ids 需要删除的机型检测日统计主键集合
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneDailyStatisticsByIds(Long[] ids);
+
+    /**
+     * 删除机型检测日统计信息
+     * 
+     * @param id 机型检测日统计主键
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneDailyStatisticsById(Long id);
+    /**
+     * 重新计算统计数据
+     * @param detectionSystemType 1苹果 2安卓 3其他
+     */
+    void recalculateStatistics(Long companyId, String companyName, Long companyUserId, String companyUserName, Date detectionTime, int detectionSystemType);
+}

+ 74 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyDetectionPhoneRecordService.java

@@ -0,0 +1,74 @@
+package com.fs.company.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyDetectionPhoneRecord;
+import com.fs.company.param.CompanyDetectionPhoneSendLinkParam;
+
+import java.util.List;
+
+/**
+ * 机型检测记录Service接口
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+public interface ICompanyDetectionPhoneRecordService extends IService<CompanyDetectionPhoneRecord>{
+    /**
+     * 查询机型检测记录
+     * 
+     * @param id 机型检测记录主键
+     * @return 机型检测记录
+     */
+    CompanyDetectionPhoneRecord selectCompanyDetectionPhoneRecordById(Long id);
+
+    /**
+     * 查询机型检测记录列表
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 机型检测记录集合
+     */
+    List<CompanyDetectionPhoneRecord> selectCompanyDetectionPhoneRecordList(CompanyDetectionPhoneRecord companyDetectionPhoneRecord);
+
+    /**
+     * 新增机型检测记录
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 结果
+     */
+    int insertCompanyDetectionPhoneRecord(CompanyDetectionPhoneRecord companyDetectionPhoneRecord);
+
+    /**
+     * 修改机型检测记录
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 结果
+     */
+    int updateCompanyDetectionPhoneRecord(CompanyDetectionPhoneRecord companyDetectionPhoneRecord);
+
+    /**
+     * 批量删除机型检测记录
+     * 
+     * @param ids 需要删除的机型检测记录主键集合
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneRecordByIds(Long[] ids);
+
+    /**
+     * 删除机型检测记录信息
+     * 
+     * @param id 机型检测记录主键
+     * @return 结果
+     */
+    int deleteCompanyDetectionPhoneRecordById(Long id);
+    /**
+     * 机型检测发送链接
+     *
+     * @param sendLinkParam 机型检测发送链接参数
+     * @param companyId 企业 id
+     * @param companyName 企业名称
+     * @param companyUserId 销售
+     * @param companyUserName 销售名称
+     * @return 结果
+     */
+    String getDownloadAppLink(CompanyDetectionPhoneSendLinkParam sendLinkParam, Long companyId,String companyName,Long companyUserId,String companyUserName);
+}

+ 72 - 0
fs-service/src/main/java/com/fs/company/service/ICompanySmsCommonLogsService.java

@@ -0,0 +1,72 @@
+package com.fs.company.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.company.domain.CompanySmsCommonLogs;
+import com.fs.company.param.SmsSendFsUserParam;
+
+import java.util.List;
+
+/**
+ * 短信通用发送记录Service接口
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+public interface ICompanySmsCommonLogsService extends IService<CompanySmsCommonLogs>{
+    /**
+     * 查询短信通用发送记录
+     * 
+     * @param logsId 短信通用发送记录主键
+     * @return 短信通用发送记录
+     */
+    CompanySmsCommonLogs selectCompanySmsCommonLogsByLogsId(Long logsId);
+
+    /**
+     * 查询短信通用发送记录列表
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 短信通用发送记录集合
+     */
+    List<CompanySmsCommonLogs> selectCompanySmsCommonLogsList(CompanySmsCommonLogs companySmsCommonLogs);
+
+    /**
+     * 新增短信通用发送记录
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 结果
+     */
+    int insertCompanySmsCommonLogs(CompanySmsCommonLogs companySmsCommonLogs);
+
+    /**
+     * 修改短信通用发送记录
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 结果
+     */
+    int updateCompanySmsCommonLogs(CompanySmsCommonLogs companySmsCommonLogs);
+
+    /**
+     * 批量删除短信通用发送记录
+     * 
+     * @param logsIds 需要删除的短信通用发送记录主键集合
+     * @return 结果
+     */
+    int deleteCompanySmsCommonLogsByLogsIds(Long[] logsIds);
+
+    /**
+     * 删除短信通用发送记录信息
+     * 
+     * @param logsId 短信通用发送记录主键
+     * @return 结果
+     */
+    int deleteCompanySmsCommonLogsByLogsId(Long logsId);
+
+    R sendUserDownloadAppMsg(SmsSendFsUserParam param);
+
+    CompanySmsCommonLogs selectCompanySmsCommonLogsByMid(String msgid);
+
+    CompanySmsCommonLogs selectCompanySmsCommonLogsByMobile(String phone);
+
+    R batchPushAppLinks(SmsSendFsUserParam param, Long companyId, String companyName, Long userId, String userName);
+}

+ 180 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyDetectionPhoneDailyStatisticsServiceImpl.java

@@ -0,0 +1,180 @@
+package com.fs.company.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyDetectionPhoneDailyStatistics;
+import com.fs.company.mapper.CompanyDetectionPhoneDailyStatisticsMapper;
+import com.fs.company.service.ICompanyDetectionPhoneDailyStatisticsService;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 机型检测日统计Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+@Service
+public class CompanyDetectionPhoneDailyStatisticsServiceImpl extends ServiceImpl<CompanyDetectionPhoneDailyStatisticsMapper, CompanyDetectionPhoneDailyStatistics> implements ICompanyDetectionPhoneDailyStatisticsService {
+
+    /**
+     * 查询机型检测日统计
+     * 
+     * @param id 机型检测日统计主键
+     * @return 机型检测日统计
+     */
+    @Override
+    public CompanyDetectionPhoneDailyStatistics selectCompanyDetectionPhoneDailyStatisticsById(Long id)
+    {
+        return baseMapper.selectCompanyDetectionPhoneDailyStatisticsById(id);
+    }
+
+    /**
+     * 查询机型检测日统计列表
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 机型检测日统计
+     */
+    @Override
+    public List<CompanyDetectionPhoneDailyStatistics> selectCompanyDetectionPhoneDailyStatisticsList(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics)
+    {
+        return baseMapper.selectCompanyDetectionPhoneDailyStatisticsList(companyDetectionPhoneDailyStatistics);
+    }
+
+    /**
+     * 新增机型检测日统计
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyDetectionPhoneDailyStatistics(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics)
+    {
+        companyDetectionPhoneDailyStatistics.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCompanyDetectionPhoneDailyStatistics(companyDetectionPhoneDailyStatistics);
+    }
+
+    /**
+     * 修改机型检测日统计
+     * 
+     * @param companyDetectionPhoneDailyStatistics 机型检测日统计
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyDetectionPhoneDailyStatistics(CompanyDetectionPhoneDailyStatistics companyDetectionPhoneDailyStatistics)
+    {
+        return baseMapper.updateCompanyDetectionPhoneDailyStatistics(companyDetectionPhoneDailyStatistics);
+    }
+
+    /**
+     * 批量删除机型检测日统计
+     * 
+     * @param ids 需要删除的机型检测日统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyDetectionPhoneDailyStatisticsByIds(Long[] ids)
+    {
+        return baseMapper.deleteCompanyDetectionPhoneDailyStatisticsByIds(ids);
+    }
+
+    /**
+     * 删除机型检测日统计信息
+     * 
+     * @param id 机型检测日统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyDetectionPhoneDailyStatisticsById(Long id)
+    {
+        return baseMapper.deleteCompanyDetectionPhoneDailyStatisticsById(id);
+    }
+
+    /**
+     * 重新计算统计数据
+     * @param detectionSystemType 1苹果 2安卓 3其他
+     */
+    @Override
+    public void recalculateStatistics(Long companyId, String companyName, Long companyUserId, String companyUserName, Date detectionTime, int detectionSystemType) {
+        // 1. 查询当天是否已有统计记录
+        String dateStr = DateUtils.parseDateToStr("yyyy-MM-dd", detectionTime);
+        Date toDay = DateUtils.dateTime(DateUtils.YYYY_MM_DD, dateStr);
+        CompanyDetectionPhoneDailyStatistics existingStatictics = baseMapper.selectByCompanyUserAndDate(
+                companyUserId, toDay);
+        if(existingStatictics != null){
+            //更新操作
+            existingStatictics.setCompanyId(companyId);
+            existingStatictics.setCompanyName(companyName);
+            existingStatictics.setCompanyUserName(companyUserName);
+            updateExistingStatistics(existingStatictics, detectionSystemType);
+            baseMapper.updateCompanyDetectionPhoneDailyStatistics(existingStatictics);
+        }else{
+            //新增
+            CompanyDetectionPhoneDailyStatistics newStats = createNewStatistics(
+                    companyId, companyName, companyUserId, companyUserName, detectionTime, detectionSystemType);
+            baseMapper.insertCompanyDetectionPhoneDailyStatistics(newStats);
+
+        }
+    }
+
+    /**
+     * 创建新的统计记录
+     */
+    private CompanyDetectionPhoneDailyStatistics createNewStatistics(Long companyId, String companyName,
+                                                                     Long companyUserId, String companyUserName,
+                                                                     Date detectionTime, int detectionSystemType) {
+        CompanyDetectionPhoneDailyStatistics statistics = new CompanyDetectionPhoneDailyStatistics();
+        statistics.setCompanyId(companyId);
+        statistics.setCompanyName(companyName);
+        statistics.setCompanyUserId(companyUserId);
+        statistics.setCompanyUserName(companyUserName);
+        statistics.setDetectionTime(detectionTime);
+
+        // 根据检测类型初始化计数
+        int azCount = 0, iosCount = 0, unknownCount = 0;
+        switch (detectionSystemType) {
+            case 1: // 苹果
+                iosCount = 1;
+                break;
+            case 2: // 安卓
+                azCount = 1;
+                break;
+            default: // 其他
+                unknownCount = 1;
+                break;
+        }
+        statistics.setAzCount(azCount);
+        statistics.setIosCount(iosCount);
+        statistics.setUnknownCount(unknownCount);
+        statistics.setTotalCount(azCount + iosCount + unknownCount);
+        statistics.setCreateTime(new Date());
+        return statistics;
+    }
+
+
+    /**
+     * 更新现有统计记录
+     */
+    private void updateExistingStatistics(CompanyDetectionPhoneDailyStatistics statistics, int detectionSystemType) {
+        // 根据检测类型增加对应计数
+        switch (detectionSystemType) {
+            case 1: // 苹果
+                statistics.setIosCount(statistics.getIosCount() + 1);
+                break;
+            case 2: // 安卓
+                statistics.setAzCount(statistics.getAzCount() + 1);
+                break;
+            default: // 其他
+                statistics.setUnknownCount(statistics.getUnknownCount() + 1);
+                break;
+        }
+        // 重新计算总数,保证 az_count + ios_count + unknown_count = total_count
+        int newTotal = statistics.getAzCount() + statistics.getIosCount() + statistics.getUnknownCount();
+        statistics.setTotalCount(newTotal);
+
+        // 更新最后检测时间
+        statistics.setDetectionTime(new Date());
+    }
+}

+ 303 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyDetectionPhoneRecordServiceImpl.java

@@ -0,0 +1,303 @@
+package com.fs.company.service.impl;
+
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyDetectionPhoneRecord;
+import com.fs.company.mapper.CompanyDetectionPhoneRecordMapper;
+import com.fs.company.param.CompanyDetectionPhoneSendLinkParam;
+import com.fs.company.service.ICompanyDetectionPhoneDailyStatisticsService;
+import com.fs.company.service.ICompanyDetectionPhoneRecordService;
+import com.fs.company.util.DetectionPhoneModelVerifyUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 机型检测记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+@Slf4j
+@Service
+public class CompanyDetectionPhoneRecordServiceImpl extends ServiceImpl<CompanyDetectionPhoneRecordMapper, CompanyDetectionPhoneRecord> implements ICompanyDetectionPhoneRecordService {
+
+    @Autowired
+    private ICompanyDetectionPhoneDailyStatisticsService companyDetectionPhoneDailyStatisticsService;
+
+    /**
+     * 查询机型检测记录
+     * 
+     * @param id 机型检测记录主键
+     * @return 机型检测记录
+     */
+    @Override
+    public CompanyDetectionPhoneRecord selectCompanyDetectionPhoneRecordById(Long id)
+    {
+        return baseMapper.selectCompanyDetectionPhoneRecordById(id);
+    }
+
+    /**
+     * 查询机型检测记录列表
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 机型检测记录
+     */
+    @Override
+    public List<CompanyDetectionPhoneRecord> selectCompanyDetectionPhoneRecordList(CompanyDetectionPhoneRecord companyDetectionPhoneRecord)
+    {
+        return baseMapper.selectCompanyDetectionPhoneRecordList(companyDetectionPhoneRecord);
+    }
+
+    /**
+     * 新增机型检测记录
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyDetectionPhoneRecord(CompanyDetectionPhoneRecord companyDetectionPhoneRecord)
+    {
+        companyDetectionPhoneRecord.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCompanyDetectionPhoneRecord(companyDetectionPhoneRecord);
+    }
+
+    /**
+     * 修改机型检测记录
+     * 
+     * @param companyDetectionPhoneRecord 机型检测记录
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyDetectionPhoneRecord(CompanyDetectionPhoneRecord companyDetectionPhoneRecord)
+    {
+        return baseMapper.updateCompanyDetectionPhoneRecord(companyDetectionPhoneRecord);
+    }
+
+    /**
+     * 批量删除机型检测记录
+     * 
+     * @param ids 需要删除的机型检测记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyDetectionPhoneRecordByIds(Long[] ids)
+    {
+        return baseMapper.deleteCompanyDetectionPhoneRecordByIds(ids);
+    }
+
+    /**
+     * 删除机型检测记录信息
+     * 
+     * @param id 机型检测记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyDetectionPhoneRecordById(Long id)
+    {
+        return baseMapper.deleteCompanyDetectionPhoneRecordById(id);
+    }
+
+    /**
+     * 机型检测发送链接
+     *
+     * @param sendLinkParam 机型检测发送链接参数
+     * @param companyId 企业 id
+     * @param companyName 企业名称
+     * @param companyUserId 销售
+     * @param companyUserName 销售名称
+     * @return 检测结果对应的链接
+     */
+    @Override
+    public String getDownloadAppLink(CompanyDetectionPhoneSendLinkParam sendLinkParam, Long companyId, String companyName,
+                          Long companyUserId, String companyUserName) {
+        String requestBody = DetectionPhoneModelVerifyUtil.getRequestBody(sendLinkParam.getMobile());
+        log.info("校验机型发送请求体:" + requestBody);
+        Date now = new Date();
+
+        try {
+            HttpResponse response = HttpUtil.createPost(DetectionPhoneModelVerifyUtil.VERIFY_URL)
+                    .setConnectionTimeout(5000)
+                    .setReadTimeout(10000)
+                    .body(requestBody)
+                    .execute();
+
+            if (!response.isOk()) {
+                String errorMsg = "请求失败,通讯状态码: " + response.getStatus();
+                saveErrorRecord(companyId, companyName, companyUserId, companyUserName, sendLinkParam,
+                              4, null, now, 0L, errorMsg);
+                throw new CustomException(errorMsg);
+            }
+            String responseBody = response.body();
+
+
+            //===========  测试代码begin ==========
+            //ios数据
+//            String iosData = "{\"Code\":200,\"Data\":[{\"IsCharge\":1,\"Mobile\":\"18323465069\",\"State\":\"1\",\"Firm\":\"0\"}]}";
+//            //android数据
+//            String androidData = "{\"Code\":200,\"Data\":[{\"IsCharge\":1,\"Mobile\":\"18580336425\",\"State\":\"3\",\"Firm\":\"3\"}]}";
+//            //库无
+//            String noData = "{\"Code\":200,\"Data\":[{\"IsCharge\":0,\"Mobile\":\"15779601354\",\"State\":\"4\",\"Firm\":\"\"}]}";
+//            String[] data = new String[]{iosData, androidData, noData};
+//            //随机拿个数据处理
+//            int codeIndex = ThreadLocalRandom.current().nextInt(0,data.length);
+//            String responseBody = data[codeIndex];
+            //===========  测试代码end ==========
+
+
+            log.info("机型校验接口返回数据: " + responseBody);
+            JSONObject json = JSONObject.parseObject(responseBody);
+            int code = json.getIntValue("Code");
+            if (code != 200) {
+                log.info("接口返回错误信息:" + responseBody);
+                return null;
+            }
+            JSONArray dataArray = json.getJSONArray("Data");
+            if (dataArray == null || dataArray.isEmpty()) {
+                log.info("解析返回信息为空:" + responseBody);
+                return null;
+            }
+
+            JSONObject firstItem = dataArray.getJSONObject(0);
+            String state = firstItem.getString("State");
+            String firm = firstItem.getString("Firm");
+            Integer myState = getState(state);
+
+            // 保存检测记录
+            int saveResult = saveDetectionPhoneRecord(companyId, companyName, companyUserId, companyUserName,
+                    sendLinkParam.getUserId(), sendLinkParam.getNickName(), sendLinkParam.getMobile(),
+                    myState, getFirm(state, firm), now, 1L, null);
+            if (saveResult <= 0) {
+                String errorMsg = "新增失败插入失败数据";
+                saveErrorRecord(companyId, companyName, companyUserId, companyUserName, sendLinkParam,
+                              myState, null, now, 0L, errorMsg);
+            }
+            // 更新统计
+            companyDetectionPhoneDailyStatisticsService.recalculateStatistics(companyId, companyName,
+                    companyUserId, companyUserName, now, myState);
+
+            // 返回对应链接
+            return getDetectionUrl(myState);
+        } catch (CustomException e) {
+            log.info("系统处理异常:"+e);
+            return null;
+        } catch (Exception e) {
+            log.info("系统逻辑处理异常:" + e);
+            return null;
+        }
+    }
+
+    /**
+     * 根据检测状态获取对应链接
+     */
+    private String getDetectionUrl(Integer state) {
+        switch (state) {
+            case 1: // 苹果
+                return DetectionPhoneModelVerifyUtil.IOS_URL;
+            case 2: // 安卓
+                return DetectionPhoneModelVerifyUtil.ANDROID_URL;
+            default:
+                log.info("无法识别的系统:" + state + ",无法发送链接");
+                return null;
+        }
+    }
+
+    //类型state=1||2 时 0 苹果  State=3时0其他 1其他 2 honor  3 vivo  4 oppo  5 xiaomi  6 huawei
+    private String getFirm(String state, String firm) {
+        // 苹果系列处理
+        if ("1".equals(state) || "2".equals(state)) {
+            return "0".equals(firm) ? "苹果" : "未知机型[State=" + state + ",Firm=" + firm+"]";
+        }
+        // 安卓系列处理
+        if ("3".equals(state)) {
+            switch (firm) {
+                case "2": return "honor";
+                case "3": return "vivo";
+                case "4": return "oppo";
+                case "5": return "xiaomi";
+                case "6": return "huawei";
+                default: return "其他";
+            }
+        }
+        // 其他未知状态
+        return "未知机型[State=" + state + ",Firm=" + firm+"]";
+    }
+    /**
+     * 将外部机型检测状态转换为内部状态
+     * 对方机型状态: 1-苹果90%, 2-疑似苹果75%, 3-不是苹果90%, 4-库无, -1-异常
+     * 我方内部状态: 0-异常, 1-苹果, 2-安卓, 3-库无,4-获取失败
+     * @param state 外部状态码
+     * @return 内部状态码
+     */
+    private Integer getState(String state) {
+        switch (state) {
+            case "1":
+            case "2":
+                return 1;  // 苹果类型
+            case "3":
+                return 2;  // 安卓类型
+            case "4":
+                return 3;  // 库无
+            default:
+                return 0;  // 异常状态
+        }
+    }
+
+    /**
+     * 保存机型检测记录
+     *
+     * @param companyId 企业ID
+     * @param companyName 企业名称
+     * @param companyUserId 销售人员ID
+     * @param companyUserName 销售人员姓名
+     * @param userId 会员ID
+     * @param nickName 会员昵称
+     * @param phoneNumber 手机号码
+     * @param detectionSystem 检测系统类型
+     * @param detectionModel 检测机型
+     * @param detectionTime 检测时间
+     * @param status 状态
+     * @param remark 备注
+     * @return 保存结果
+     */
+    private int saveDetectionPhoneRecord(Long companyId, String companyName, Long companyUserId, String companyUserName,
+                                         Long userId, String nickName,
+                                         String phoneNumber, Integer detectionSystem, String detectionModel,Date detectionTime,
+                                         Long status, String remark) {
+        CompanyDetectionPhoneRecord record = CompanyDetectionPhoneRecord.builder()
+                .companyId(companyId)
+                .companyName(companyName)
+                .companyUserId(companyUserId)
+                .companyUserName(companyUserName)
+                .userId(userId)
+                .nickName(nickName)
+                .phoneNumber(phoneNumber)
+                .detectionSystem(detectionSystem)
+                .detectionModel(detectionModel)
+                .detectionTime(detectionTime)
+                .status(status)
+                .remark(remark)
+                .build();
+        return baseMapper.insertCompanyDetectionPhoneRecord(record);
+    }
+
+    /**
+     * 保存错误记录
+     */
+    private void saveErrorRecord(Long companyId, String companyName, Long companyUserId, String companyUserName,
+                                 CompanyDetectionPhoneSendLinkParam param, Integer systemType, String model,
+                                 Date detectionTime, Long status, String remark) {
+        saveDetectionPhoneRecord(companyId, companyName, companyUserId, companyUserName,
+                param.getUserId(), param.getNickName(), param.getMobile(),
+                systemType, model, detectionTime, status, remark);
+    }
+
+}

+ 319 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanySmsCommonLogsServiceImpl.java

@@ -0,0 +1,319 @@
+package com.fs.company.service.impl;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.vo.SmsSendItemVO;
+import com.fs.common.vo.SmsSendVO;
+import com.fs.company.domain.CompanySms;
+import com.fs.company.domain.CompanySmsCommonLogs;
+import com.fs.company.domain.CompanySmsTemp;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanySmsCommonLogsMapper;
+import com.fs.company.param.CompanyDetectionPhoneSendLinkParam;
+import com.fs.company.param.SmsSendFsUserParam;
+import com.fs.company.service.ICompanySmsCommonLogsService;
+import com.fs.company.service.ICompanySmsService;
+import com.fs.company.service.ICompanySmsTempService;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.his.config.FsSmsConfig;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.sms.domain.SendSmsReturn;
+import com.fs.sms.service.impl.SmsTServiceImpl;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 短信通用发送记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-12-17
+ */
+@Slf4j
+@Service
+public class CompanySmsCommonLogsServiceImpl extends ServiceImpl<CompanySmsCommonLogsMapper, CompanySmsCommonLogs> implements ICompanySmsCommonLogsService {
+
+    @Autowired
+    private ICompanySmsTempService smsTempService;
+    @Autowired
+    private SmsTServiceImpl smsTService;
+    @Autowired
+    private ICompanySmsService companySmsService;
+    @Autowired
+    private ICompanyUserService companyUserService;
+    @Autowired
+    SysConfigMapper sysConfigMapper;
+    @Autowired
+    private IFsUserService fsUserService;
+    @Autowired
+    private CompanyDetectionPhoneRecordServiceImpl companyDetectionPhoneRecordService;
+
+    /**
+     * 查询短信通用发送记录
+     * 
+     * @param logsId 短信通用发送记录主键
+     * @return 短信通用发送记录
+     */
+    @Override
+    public CompanySmsCommonLogs selectCompanySmsCommonLogsByLogsId(Long logsId)
+    {
+        return baseMapper.selectCompanySmsCommonLogsByLogsId(logsId);
+    }
+
+    /**
+     * 查询短信通用发送记录列表
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 短信通用发送记录
+     */
+    @Override
+    public List<CompanySmsCommonLogs> selectCompanySmsCommonLogsList(CompanySmsCommonLogs companySmsCommonLogs)
+    {
+        return baseMapper.selectCompanySmsCommonLogsList(companySmsCommonLogs);
+    }
+
+    /**
+     * 新增短信通用发送记录
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 结果
+     */
+    @Override
+    public int insertCompanySmsCommonLogs(CompanySmsCommonLogs companySmsCommonLogs)
+    {
+        companySmsCommonLogs.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCompanySmsCommonLogs(companySmsCommonLogs);
+    }
+
+    /**
+     * 修改短信通用发送记录
+     * 
+     * @param companySmsCommonLogs 短信通用发送记录
+     * @return 结果
+     */
+    @Override
+    public int updateCompanySmsCommonLogs(CompanySmsCommonLogs companySmsCommonLogs)
+    {
+        return baseMapper.updateCompanySmsCommonLogs(companySmsCommonLogs);
+    }
+
+    /**
+     * 批量删除短信通用发送记录
+     * 
+     * @param logsIds 需要删除的短信通用发送记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanySmsCommonLogsByLogsIds(Long[] logsIds)
+    {
+        return baseMapper.deleteCompanySmsCommonLogsByLogsIds(logsIds);
+    }
+
+    /**
+     * 删除短信通用发送记录信息
+     * 
+     * @param logsId 短信通用发送记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanySmsCommonLogsByLogsId(Long logsId)
+    {
+        return baseMapper.deleteCompanySmsCommonLogsByLogsId(logsId);
+    }
+
+    @Override
+    public R sendUserDownloadAppMsg(SmsSendFsUserParam param) {
+        // 获取模板
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(param.getTempCode());
+        if (temp == null || !temp.getStatus().equals(1) || !temp.getIsAudit().equals(1)) {
+            R.error("模板未审核");
+        }
+        // 获取公司短信配置
+        CompanySms csms = companySmsService.selectCompanySmsByCompanyId(param.getCompanyId());
+        if (csms == null) {
+            R.error("请充值");
+        }
+        if (csms.getRemainSmsCount() <= 0) {
+            R.error("剩余短信数量不足,请充值");
+        }
+        FsUser fsUser = fsUserService.selectFsUserByUserId(param.getUserId());
+        if (fsUser == null) {
+            return R.error("没有此会员");
+        }
+        if (fsUser.getPhone() == null) {
+            return R.error("电话不能为空");
+        }
+        String phone = PhoneUtil.decryptPhone(fsUser.getPhone());
+        String name = fsUser.getNickName();
+        return templateSplicing(phone,name,param,temp,4);
+    }
+
+    @Override
+    public CompanySmsCommonLogs selectCompanySmsCommonLogsByMid(String msgid) {
+        return baseMapper.selectCompanySmsCommonLogsByMid(msgid);
+    }
+
+    @Override
+    public CompanySmsCommonLogs selectCompanySmsCommonLogsByMobile(String phone) {
+        return baseMapper.selectCompanySmsCommonLogsByMobile(phone);
+    }
+
+    @Override
+    public R batchPushAppLinks(SmsSendFsUserParam param, Long companyId, String companyName, Long companyUserId, String userName) {
+        int successCount = 0;
+        int failCount = 0;
+
+        for (Long userId : param.getUserIds()) {
+            try {
+                FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+                if (fsUser == null || fsUser.getPhone() == null) {
+                    failCount++;
+                    continue;
+                }
+
+                String phone = PhoneUtil.decryptPhone(fsUser.getPhone());
+                String nickName = fsUser.getNickName();
+
+                CompanyDetectionPhoneSendLinkParam linkParam = CompanyDetectionPhoneSendLinkParam.builder()
+                        .userId(userId)
+                        .mobile(phone)
+                        .nickName(nickName)
+                        .build();
+
+                String linkUrl = companyDetectionPhoneRecordService.getDownloadAppLink(
+                        linkParam, companyId, companyName, companyUserId, userName);
+
+                if (linkUrl != null && !linkUrl.isEmpty()) {
+                    // 设置当前用户ID并发送短信
+                    param.setUserId(userId);
+                    param.setDownloaAppdUrl(linkUrl);
+                    try {
+                        sendUserDownloadAppMsg(param);
+                        successCount++;
+                    }catch (Exception e){
+                        failCount++;
+                        log.error("为用户 {} 发送短信时发生异常", userName, e);
+                    }
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                failCount++;
+                log.error("为用户 {} 批量推送APP链接时发生异常", userName, e);
+            }
+        }
+        return R.ok(String.format("批量推送完成,成功: %d,失败: %d", successCount, failCount));
+    }
+
+
+
+    /**
+     * 组装模板短信及发送
+     */
+    private R templateSplicing(String phone,String name,SmsSendFsUserParam param,CompanySmsTemp temp,Integer sendType){
+        CompanyUser companyUser = companyUserService.selectCompanyUserById(param.getCompanyUserId());
+        String content;
+        content = param.getContent();
+        if (StringUtils.isNotEmpty(name)) {
+            content = content.replace("${sms.csName}", name);
+        }
+        if (StringUtils.isNotEmpty(param.getCardUrl())) {
+            content = content.replace("${sms.cardUrl}", param.getCardUrl());
+        }
+        if (companyUser != null && StringUtils.isNotEmpty(companyUser.getPhonenumber())) {
+            content = content.replace("${sms.phoneNumber}", companyUser.getPhonenumber());
+        }
+        if(StringUtils.isNotEmpty(param.getDownloaAppdUrl())){
+            content = content.replace("${sms.downloadAppLink}", param.getDownloaAppdUrl());
+        }
+        String urls = null;
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
+        FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
+        if (sms.getType().equals("rf")) {
+            try {
+                if (temp.getTempType().equals(1)) {
+                    urls = sms.getRfUrl1() + "sms?action=send&account=" + sms.getRfAccount1() + "&password=" + sms.getRfPassword1() + "&mobile=" + phone + "&content=" + URLEncoder.encode(sms.getRfSign() + content, "UTF-8") + "&extno=" + sms.getRfCode1() + "&rt=json";
+                } else if (temp.getTempType().equals(2)) {
+                    urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + phone + "&content=" + URLEncoder.encode(sms.getRfSign() + content + "拒收请回复R", "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+                }
+            } catch (UnsupportedEncodingException e) {
+                e.printStackTrace();
+            }
+            String post = HttpRequest.get(urls)
+//                            .body(String.valueOf(jsonObject))
+                    .execute().body();
+            SmsSendVO vo = JSONUtil.toBean(post, SmsSendVO.class);
+            if (vo.getStatus().equals(0)) {
+                for (SmsSendItemVO itemVO : vo.getList()) {
+                    if (itemVO.getResult().equals("0")) {
+                        recordSmsLog(param,temp,content,phone,name,sms,itemVO.getMid(),sendType,companyUser.getUserName());
+                    }
+                }
+            }
+        } else if (sms.getType().equals("dh")) {
+            SendSmsReturn sendSmsReturn = null;
+            if (temp.getTempType().equals(1)) {
+                sendSmsReturn = smsTService.sendSms(sms.getDhAccount1(), sms.getDhPassword1(), content, phone);
+            } else if (temp.getTempType().equals(2)) {
+                sendSmsReturn = smsTService.sendSms(sms.getDhAccount2(), sms.getDhPassword2(), content + "拒收请回复R", phone);
+            }
+            log.info("数据:{}", sendSmsReturn);
+            if (sendSmsReturn != null) {
+                if (sendSmsReturn.getResult() != null && sendSmsReturn.getResult().equals("0")) {
+                    recordSmsLog(param,temp,content,phone,name,sms,sendSmsReturn.getMsgid(),sendType,companyUser.getUserName());
+                }
+            }
+        }
+        return R.ok("短信提交成功,正在发送中...");
+    }
+
+    /**
+     * 记录发送短信日志
+     */
+    private void recordSmsLog(SmsSendFsUserParam param, CompanySmsTemp temp,String content,String phone,String name,FsSmsConfig sms,String mid,Integer sendType,String companyUserName) {
+        CompanySmsCommonLogs logs = new CompanySmsCommonLogs();
+        logs.setCompanyId(param.getCompanyId());
+        logs.setContent(content);
+        logs.setTempCode(temp.getTempCode());
+        logs.setCompanyUserId(param.getCompanyUserId());
+        logs.setCompanyUserName(companyUserName);
+        logs.setTempId(temp.getTempId());
+
+        //=======区别参数
+        logs.setRecipientId(param.getUserId());
+        logs.setRecipientName(name);
+        logs.setSendType(sendType);
+        //=======区别参数
+
+        logs.setPhone(phone);
+        logs.setSendTime(new Date());
+        logs.setStatus(0);
+        logs.setType(sms.getType());
+        logs.setMid(mid);
+        Integer counts = logs.getContent().length() / 67;
+        if (logs.getContent().length() % 67 > 0) {
+            counts = counts + 1;
+        }
+        if (counts == 0) {
+            counts = 1;
+        }
+        logs.setNumber(counts);
+        logs.setCreateTime(new Date());
+        baseMapper.insertCompanySmsCommonLogs(logs);
+        companySmsService.subCompanySms(logs.getCompanyId(), logs.getNumber());
+    }
+}

+ 88 - 0
fs-service/src/main/java/com/fs/company/util/DetectionPhoneModelVerifyUtil.java

@@ -0,0 +1,88 @@
+package com.fs.company.util;
+
+import com.alibaba.fastjson.JSONObject;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @Author:peicj
+ * @Description: 机型检测接口调用工具类
+ * @Date:2025/12/17 11:23
+ */
+public class DetectionPhoneModelVerifyUtil {
+
+    //appid
+    public static final long VERIFY_APPID = 100712;
+    //secretkey
+    public static final String SECRET_KEY = "faGf4dQNG3Tg";
+    //机型校验地址
+    public static final String VERIFY_URL = "http://zhijian.h12321.com:8086/mobile/model/check";
+    //苹果链接
+    public static String IOS_URL = "https://apps.apple.com/cn/app/%E8%8A%B3%E5%8D%8E%E6%9C%AA%E6%9D%A5/id6738688148";
+    //安卓链接  芳华未来App应用商店下载链接 华为/小米/oppo/vivo/荣耀 https://m.malink.cn/s/2UZjMz
+    public static String ANDROID_URL = "https://m.malink.cn/s/2UZjMz";
+
+
+    /**
+     * 获取请求体
+     * @param phone 手机号
+     * @return 请求体
+     */
+    public static String getRequestBody(String phone) {
+            String timestamp = System.currentTimeMillis() + "";
+            Map<String,Object> map = new HashMap<>();
+            map.put("appId", VERIFY_APPID);
+            map.put("timestamp", timestamp);
+            map.put("sign", getSign(VERIFY_APPID, SECRET_KEY, timestamp));
+            map.put("phones", phone);
+        return JSONObject.toJSONString(map);
+    }
+
+    /**
+     * 生成MD5签名字符串
+     * @param input 输入字符串
+     * @return 32位小写MD5字符串
+     */
+    public static String generateMD5(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] digest = md.digest(input.getBytes());
+            return bytesToHex(digest);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("MD5 algorithm not found", e);
+        }
+    }
+
+    /**
+     * 字节数组转十六进制字符串
+     * @param bytes 字节数组
+     * @return 十六进制字符串(小写)
+     */
+    private static String bytesToHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            String hex = Integer.toHexString(0xff & b);
+            if (hex.length() == 1) {
+                sb.append('0');
+            }
+            sb.append(hex);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 获取签名
+     * @param appid appid
+     * @param secretkey secretkey
+     * @param timestamp 时间戳
+     * @Description 签名md5小写32位加密字符串,Md5(appId+secretkey+timestamp)
+     * @return String 签名
+     */
+    private static String getSign(long appid,String secretkey,String timestamp){
+        return generateMD5(appid + secretkey + timestamp);
+    }
+
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -752,4 +752,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     List<FsSopMyCourseH5LinkVO> getSopCourseH5StudyList(@Param("userId") Long userId);
     List<FsSopMyCourseH5LinkVO> getSopCourseH5StudyList(@Param("userId") Long userId);
 
 
     List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact);
     List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact);
+
+    List<FsSopMyCourseH5LinkVO> getSopCourseH5StudyListByQwExId(@Param("qwExternalId") Long qwExternalId);
+
 }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseH5ListParam.java

@@ -17,4 +17,6 @@ public class FsCourseH5ListParam {
     private Long userId;
     private Long userId;
 
 
     private Long logId;
     private Long logId;
+
+    private String link;
 }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseLinkService.java

@@ -112,4 +112,6 @@ public interface IFsCourseLinkService
     List<FsCourseLinkDTO> selectFsCourseLinkListByQwUserId(FsCourseH5ListParam param);
     List<FsCourseLinkDTO> selectFsCourseLinkListByQwUserId(FsCourseH5ListParam param);
 
 
     R getLinkInfo(Long logId);
     R getLinkInfo(Long logId);
+
+    R getSopCourseH5StudyListByMsg(FsCourseH5ListParam param);
 }
 }

+ 15 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java

@@ -30,6 +30,7 @@ import com.fs.course.param.FsCourseLinkRoomParam;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.course.service.IFsUserCourseService;
+import com.fs.course.vo.FsSopMyCourseH5LinkVO;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.HttpUtil;
 import com.fs.his.utils.HttpUtil;
@@ -45,6 +46,8 @@ import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
 import com.fs.voice.utils.StringUtil;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import com.google.common.reflect.TypeToken;
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
 import com.google.gson.Gson;
 import lombok.Synchronized;
 import lombok.Synchronized;
@@ -1032,6 +1035,18 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
         return R.ok().put("data",map);
         return R.ok().put("data",map);
     }
     }
 
 
+    @Override
+    public R getSopCourseH5StudyListByMsg(FsCourseH5ListParam param) {
+        FsCourseLink courseLink = fsCourseLinkMapper.selectFsCourseLinkByLink(param.getLink());
+        if (courseLink != null && courseLink.getQwExternalId()!=null) {
+            PageHelper.startPage(param.getPageNum(), param.getPageSize());
+            List<FsSopMyCourseH5LinkVO> list = fsCourseWatchLogMapper.getSopCourseH5StudyListByQwExId(courseLink.getQwExternalId());
+            return R.ok().put("data",new PageInfo(list));
+        }
+
+        return R.error("链接失效或未绑定销售");
+    }
+
     @Override
     @Override
     public R getLiveWxaCodeGenerateScheme(String linkStr, String appId) {
     public R getLiveWxaCodeGenerateScheme(String linkStr, String appId) {
         CloseableHttpClient client = null;
         CloseableHttpClient client = null;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -364,5 +364,28 @@ public interface QwSopLogsMapper extends BaseMapper<QwSopLogs> {
     @MapKey("id")
     @MapKey("id")
     Map<String, SopUserLogs> queryAllPeriodNew(StatsWatchLogPageListDTO param);
     Map<String, SopUserLogs> queryAllPeriodNew(StatsWatchLogPageListDTO param);
 
 
+    @DataSource(DataSourceType.SOP)
+    QwSopLogs selectoneByUid(@Param("sopSmsLogId") Long sopSmsLogId);
+
+
+    /**
+     * 根据短信执行记录表修改企业微信SOP
+     *
+     * @param qwSopLogs 企业微信SOP  定时任务
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int updateQwSopLogsBySmsLogsId(@Param("data") QwSopLogs qwSopLogs);
+
+    /**
+     * 批量更执行记录表
+     * @param list 更新对象
+     * **/
+    @DataSource(DataSourceType.SOP)
+    void batchUpdateQwSopLogsBySmsLogsId(@Param("list") List<QwSopLogs> list);
+
+
+    @DataSource(DataSourceType.SOP)
+    List<QwSopLogs> getQwSopInfoByUid(@Param("sopSmsLogIds") List<Long> sopSmsLogIds);
 
 
 }
 }

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

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

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

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

+ 101 - 0
fs-service/src/main/resources/mapper/company/CompanyDetectionPhoneDailyStatisticsMapper.xml

@@ -0,0 +1,101 @@
+<?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.company.mapper.CompanyDetectionPhoneDailyStatisticsMapper">
+    
+    <resultMap type="CompanyDetectionPhoneDailyStatistics" id="CompanyDetectionPhoneDailyStatisticsResult">
+        <result property="id"    column="id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyName"    column="company_name"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="companyUserName"    column="company_user_name"    />
+        <result property="detectionTime"    column="detection_time"    />
+        <result property="azCount"    column="az_count"    />
+        <result property="iosCount"    column="ios_count"    />
+        <result property="unknownCount"    column="unknown_count"    />
+        <result property="totalCount"    column="total_count"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectCompanyDetectionPhoneDailyStatisticsVo">
+        select id, company_id, company_name, company_user_id, company_user_name, detection_time, az_count, ios_count, unknown_count, total_count, create_time from company_detection_phone_daily_statistics
+    </sql>
+
+    <select id="selectCompanyDetectionPhoneDailyStatisticsList" parameterType="CompanyDetectionPhoneDailyStatistics" resultMap="CompanyDetectionPhoneDailyStatisticsResult">
+        <include refid="selectCompanyDetectionPhoneDailyStatisticsVo"/>
+        <where>  
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyName != null  and companyName != ''"> and company_name like concat('%', #{companyName}, '%')</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="companyUserName != null  and companyUserName != ''"> and company_user_name like concat('%', #{companyUserName}, '%')</if>
+            <if test="detectionTime != null "> and DATE(detection_time) = DATE(#{detectionTime})</if>
+            <if test="azCount != null "> and az_count = #{azCount}</if>
+            <if test="iosCount != null "> and ios_count = #{iosCount}</if>
+            <if test="unknownCount != null "> and unknown_count = #{unknownCount}</if>
+            <if test="totalCount != null "> and total_count = #{totalCount}</if>
+        </where>
+        order by id desc
+    </select>
+    
+    <select id="selectCompanyDetectionPhoneDailyStatisticsById" parameterType="Long" resultMap="CompanyDetectionPhoneDailyStatisticsResult">
+        <include refid="selectCompanyDetectionPhoneDailyStatisticsVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertCompanyDetectionPhoneDailyStatistics" parameterType="CompanyDetectionPhoneDailyStatistics" useGeneratedKeys="true" keyProperty="id">
+        insert into company_detection_phone_daily_statistics
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="companyUserName != null">company_user_name,</if>
+            <if test="detectionTime != null">detection_time,</if>
+            <if test="azCount != null">az_count,</if>
+            <if test="iosCount != null">ios_count,</if>
+            <if test="unknownCount != null">unknown_count,</if>
+            <if test="totalCount != null">total_count,</if>
+            <if test="createTime != null">create_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="companyUserName != null">#{companyUserName},</if>
+            <if test="detectionTime != null">#{detectionTime},</if>
+            <if test="azCount != null">#{azCount},</if>
+            <if test="iosCount != null">#{iosCount},</if>
+            <if test="unknownCount != null">#{unknownCount},</if>
+            <if test="totalCount != null">#{totalCount},</if>
+            <if test="createTime != null">#{createTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanyDetectionPhoneDailyStatistics" parameterType="CompanyDetectionPhoneDailyStatistics">
+        update company_detection_phone_daily_statistics
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="companyUserName != null">company_user_name = #{companyUserName},</if>
+            <if test="detectionTime != null">detection_time = #{detectionTime},</if>
+            <if test="azCount != null">az_count = #{azCount},</if>
+            <if test="iosCount != null">ios_count = #{iosCount},</if>
+            <if test="unknownCount != null">unknown_count = #{unknownCount},</if>
+            <if test="totalCount != null">total_count = #{totalCount},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCompanyDetectionPhoneDailyStatisticsById" parameterType="Long">
+        delete from company_detection_phone_daily_statistics where id = #{id}
+    </delete>
+
+    <delete id="deleteCompanyDetectionPhoneDailyStatisticsByIds" parameterType="String">
+        delete from company_detection_phone_daily_statistics where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 116 - 0
fs-service/src/main/resources/mapper/company/CompanyDetectionPhoneRecordMapper.xml

@@ -0,0 +1,116 @@
+<?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.company.mapper.CompanyDetectionPhoneRecordMapper">
+    
+    <resultMap type="CompanyDetectionPhoneRecord" id="CompanyDetectionPhoneRecordResult">
+        <result property="id"    column="id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyName"    column="company_name"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="companyUserName"    column="company_user_name"    />
+        <result property="userId"    column="user_id"    />
+        <result property="nickName"    column="nick_name"    />
+        <result property="phoneNumber"    column="phone_number"    />
+        <result property="detectionSystem"    column="detection_system"    />
+        <result property="detectionModel"    column="detection_model"    />
+        <result property="detectionTime"    column="detection_time"    />
+        <result property="status"    column="status"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectCompanyDetectionPhoneRecordVo">
+        select id, company_id, company_name, company_user_id, company_user_name,user_id,nick_name, phone_number, detection_system, detection_model, detection_time, status, create_time,remark from company_detection_phone_record
+    </sql>
+
+    <select id="selectCompanyDetectionPhoneRecordList" parameterType="CompanyDetectionPhoneRecord" resultMap="CompanyDetectionPhoneRecordResult">
+        <include refid="selectCompanyDetectionPhoneRecordVo"/>
+        <where>  
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyName != null  and companyName != ''"> and company_name like concat('%', #{companyName}, '%')</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="companyUserName != null  and companyUserName != ''"> and company_user_name like concat('%', #{companyUserName}, '%')</if>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="nickName != null  and nickName != ''"> and nick_name like concat('%', #{nickName}, '%')</if>
+            <if test="phoneNumber != null  and phoneNumber != ''"> and phone_number like concat('%', #{phoneNumber}, '%')</if>
+            <if test="detectionSystem != null "> and detection_system = #{detectionSystem}</if>
+            <if test="detectionModel != null  and detectionModel != ''"> and detection_model = #{detectionModel}</if>
+            <if test="detectionTime != null "> and DATE(detection_time) = DATE(#{detectionTime})</if>
+            <if test="status != null "> and status = #{status}</if>
+            <if test="remark != null  and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
+        </where>
+        order by id desc
+    </select>
+    
+    <select id="selectCompanyDetectionPhoneRecordById" parameterType="Long" resultMap="CompanyDetectionPhoneRecordResult">
+        <include refid="selectCompanyDetectionPhoneRecordVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertCompanyDetectionPhoneRecord" parameterType="CompanyDetectionPhoneRecord" useGeneratedKeys="true" keyProperty="id">
+        insert into company_detection_phone_record
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="companyUserName != null">company_user_name,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="nickName != null">nick_name,</if>
+            <if test="phoneNumber != null">phone_number,</if>
+            <if test="detectionSystem != null">detection_system,</if>
+            <if test="detectionModel != null">detection_model,</if>
+            <if test="detectionTime != null">detection_time,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="remark != null"> remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="companyUserName != null">#{companyUserName},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="nickName != null">#{nickName},</if>
+            <if test="phoneNumber != null">#{phoneNumber},</if>
+            <if test="detectionSystem != null">#{detectionSystem},</if>
+            <if test="detectionModel != null">#{detectionModel},</if>
+            <if test="detectionTime != null">#{detectionTime},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="remark != null"> #{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanyDetectionPhoneRecord" parameterType="CompanyDetectionPhoneRecord">
+        update company_detection_phone_record
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="companyUserName != null">company_user_name = #{companyUserName},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="nickName != null">nick_name = #{nickName},</if>
+            <if test="phoneNumber != null">phone_number = #{phoneNumber},</if>
+            <if test="detectionSystem != null">detection_system = #{detectionSystem},</if>
+            <if test="detectionModel != null">detection_model = #{detectionModel},</if>
+            <if test="detectionTime != null">detection_time = #{detectionTime},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCompanyDetectionPhoneRecordById" parameterType="Long">
+        delete from company_detection_phone_record where id = #{id}
+    </delete>
+
+    <delete id="deleteCompanyDetectionPhoneRecordByIds" parameterType="String">
+        delete from company_detection_phone_record where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 146 - 0
fs-service/src/main/resources/mapper/company/CompanySmsCommonLogsMapper.xml

@@ -0,0 +1,146 @@
+<?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.company.mapper.CompanySmsCommonLogsMapper">
+
+    <resultMap type="CompanySmsCommonLogs" id="CompanySmsCommonLogsResult">
+        <result property="logsId"    column="logs_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="recipientId"    column="recipient_id"    />
+        <result property="recipientName"    column="recipient_name"    />
+        <result property="sendType"    column="send_type"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="tempCode"    column="temp_code"    />
+        <result property="phone"    column="phone"    />
+        <result property="content"    column="content"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="sendTime"    column="send_time"    />
+        <result property="status"    column="status"    />
+        <result property="mid"    column="mid"    />
+        <result property="stat"    column="stat"    />
+        <result property="replyContent"    column="reply_content"    />
+        <result property="number"    column="number"    />
+        <result property="type"    column="type"    />
+        <result property="companyUserName"    column="company_user_name"    />
+
+    </resultMap>
+
+    <sql id="selectCompanySmsCommonLogsVo">
+        select logs_id, company_id, company_user_id, recipient_id, recipient_name, send_type, temp_id, temp_code, phone, content, create_time, send_time, status, mid, stat, reply_content, number, type,company_user_name from company_sms_common_logs
+    </sql>
+
+    <select id="selectCompanySmsCommonLogsList" parameterType="CompanySmsCommonLogs" resultMap="CompanySmsCommonLogsResult">
+        <include refid="selectCompanySmsCommonLogsVo"/>
+        <where>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="recipientId != null "> and recipient_id = #{recipientId}</if>
+            <if test="recipientName != null "> and recipient_name like concat('%', #{recipientName}, '%')</if>
+            <if test="sendType != null "> and send_type = #{sendType}</if>
+            <if test="tempId != null "> and temp_id = #{tempId}</if>
+            <if test="tempCode != null  and tempCode != ''"> and temp_code = #{tempCode}</if>
+            <if test="phone != null  and phone != ''"> and phone = #{phone}</if>
+            <if test="content != null  and content != ''"> and content = #{content}</if>
+            <if test="sendTime != null "> and send_time = #{sendTime}</if>
+            <if test="status != null "> and status = #{status}</if>
+            <if test="mid != null  and mid != ''"> and mid = #{mid}</if>
+            <if test="stat != null  and stat != ''"> and stat = #{stat}</if>
+            <if test="replyContent != null  and replyContent != ''"> and reply_content = #{replyContent}</if>
+            <if test="number != null "> and number = #{number}</if>
+            <if test="type != null  and type != ''"> and type = #{type}</if>
+            <if test="companyUserName != null  and companyUserName != ''"> and company_user_name = #{companyUserName}</if>
+            <if test="beginTime != null  and beginTime != '' "> and send_time &gt;= #{beginTime}</if>
+            <if test="endTime != null  and endTime != '' "> and send_time &lt; #{endTime}</if>
+            <if test="isReply != null  and isReply == 0 "> and content is not null</if>
+            <if test="isReply != null  and isReply == 1 "> and content is null</if>
+        </where>
+        order by logs_id desc
+    </select>
+
+    <select id="selectCompanySmsCommonLogsByLogsId" parameterType="Long" resultMap="CompanySmsCommonLogsResult">
+        <include refid="selectCompanySmsCommonLogsVo"/>
+        where logs_id = #{logsId}
+    </select>
+
+    <insert id="insertCompanySmsCommonLogs" parameterType="CompanySmsCommonLogs" useGeneratedKeys="true" keyProperty="logsId">
+        insert into company_sms_common_logs
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="recipientId != null">recipient_id,</if>
+            <if test="recipientName != null">recipient_name,</if>
+            <if test="sendType != null">send_type,</if>
+            <if test="tempId != null">temp_id,</if>
+            <if test="tempCode != null">temp_code,</if>
+            <if test="phone != null">phone,</if>
+            <if test="content != null">content,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="sendTime != null">send_time,</if>
+            <if test="status != null">status,</if>
+            <if test="mid != null">mid,</if>
+            <if test="stat != null">stat,</if>
+            <if test="replyContent != null">reply_content,</if>
+            <if test="number != null">number,</if>
+            <if test="type != null">type,</if>
+            <if test="companyUserName != null">company_user_name,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="recipientId != null">#{recipientId},</if>
+            <if test="recipientName != null">#{recipientName},</if>
+            <if test="sendType != null">#{sendType},</if>
+            <if test="tempId != null">#{tempId},</if>
+            <if test="tempCode != null">#{tempCode},</if>
+            <if test="phone != null">#{phone},</if>
+            <if test="content != null">#{content},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="sendTime != null">#{sendTime},</if>
+            <if test="status != null">#{status},</if>
+            <if test="mid != null">#{mid},</if>
+            <if test="stat != null">#{stat},</if>
+            <if test="replyContent != null">#{replyContent},</if>
+            <if test="number != null">#{number},</if>
+            <if test="type != null">#{type},</if>
+            <if test="companyUserName != null">#{companyUserName},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanySmsCommonLogs" parameterType="CompanySmsCommonLogs">
+        update company_sms_common_logs
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="recipientId != null">recipient_id = #{recipientId},</if>
+            <if test="recipientName != null">recipient_name = #{recipientName},</if>
+            <if test="sendType != null">send_type = #{sendType},</if>
+            <if test="tempId != null">temp_id = #{tempId},</if>
+            <if test="tempCode != null">temp_code = #{tempCode},</if>
+            <if test="phone != null">phone = #{phone},</if>
+            <if test="content != null">content = #{content},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="sendTime != null">send_time = #{sendTime},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="mid != null">mid = #{mid},</if>
+            <if test="stat != null">stat = #{stat},</if>
+            <if test="replyContent != null">reply_content = #{replyContent},</if>
+            <if test="number != null">number = #{number},</if>
+            <if test="type != null">type = #{type},</if>
+            <if test="companyUserName != null">company_user_name = #{companyUserName},</if>
+        </trim>
+        where logs_id = #{logsId}
+    </update>
+
+    <delete id="deleteCompanySmsCommonLogsByLogsId" parameterType="Long">
+        delete from company_sms_common_logs where logs_id = #{logsId}
+    </delete>
+
+    <delete id="deleteCompanySmsCommonLogsByLogsIds" parameterType="String">
+        delete from company_sms_common_logs where logs_id in
+        <foreach item="logsId" collection="array" open="(" separator="," close=")">
+            #{logsId}
+        </foreach>
+    </delete>
+</mapper>

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

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

+ 9 - 0
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -1352,4 +1352,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         select log_id logId from fs_course_watch_log where qw_external_contact_id = #{id}
         select log_id logId from fs_course_watch_log where qw_external_contact_id = #{id}
                                                        and qw_user_id = #{qwUserId} and user_id is null
                                                        and qw_user_id = #{qwUserId} and user_id is null
     </select>
     </select>
+    <select id="getSopCourseH5StudyListByQwExId" resultType="com.fs.course.vo.FsSopMyCourseH5LinkVO">
+        select  c.img_url courseUrl,v.title courseName,c.title,l.log_id logId,u.qw_user_name qwUserName from fs_course_watch_log l
+         left join fs_user_course c on c.course_id = l.course_id
+         left join qw_user u on l.qw_user_id = u.id
+         left join fs_user_course_video v on l.video_id =v.video_id
+        where l.qw_external_contact_id = #{qwExternalId}  AND l.create_time &gt;= CURDATE()
+          AND l.create_time &lt; CURDATE() + INTERVAL 1 DAY
+        order by l.create_time desc
+    </select>
 </mapper>
 </mapper>

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

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

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

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

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

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

+ 13 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -174,4 +174,17 @@ public class CourseFsUserController extends AppBaseController {
         logger.error("zyp \n【h5看课中途报错】:{}",msg);
         logger.error("zyp \n【h5看课中途报错】:{}",msg);
     }
     }
 
 
+    @ApiOperation("获取我的sop课程-短信链接")
+    @PostMapping("/getSopCourseH5StudyListByMsg")
+    public R getSopCourseH5StudyListByMsg(@RequestBody FsCourseH5ListParam param ){
+        return  courseLinkService.getSopCourseH5StudyListByMsg(param);
+    }
+
+    @ApiOperation("获取我的sop课程详情")
+    @PostMapping("/getSopCourseH5StudyInfo")
+    public R getSopCourseH5StudyInfo(@RequestBody FsCourseH5ListParam  param ){
+        // 查询看课记录
+        return courseLinkService.getLinkInfo(param.getLogId());
+    }
+
 }
 }