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

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

caoliqin 2 дней назад
Родитель
С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;
 
+import com.alibaba.fastjson.JSONObject;
 import com.fs.aicall.domain.BaseDomain;
 import com.fs.aicall.domain.TaskInfo;
 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.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -45,6 +47,7 @@ import java.util.stream.Collectors;
  */
 @RestController
 @RequestMapping("/company/companyVoiceRobotic")
+@Slf4j
 public class CompanyVoiceRoboticController extends BaseController
 {
     @Autowired
@@ -225,8 +228,10 @@ public class CompanyVoiceRoboticController extends BaseController
     }
 
     @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();
     }
     /**

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

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

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

+ 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.json.JSONUtil;
 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.redis.RedisCache;
 
@@ -11,10 +13,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsSendItemVO;
 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.ICompanySmsService;
 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.FsStoreOrderMapper;
 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.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.mapper.SysConfigMapper;
 import com.google.gson.Gson;
@@ -48,6 +52,7 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
+import com.fs.company.service.ICompanySmsCommonLogsService;
 
 @Service
 @Slf4j
@@ -79,6 +84,18 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     private FsPackageOrderMapper packageOrderMapper;
 
+    @Autowired
+    private ICompanySmsCommonLogsService smsCommonLogsService;
+
+    @Autowired
+    private IQwSopSmsLogsService qwSopSmsLogsService;
+
+    @Autowired
+    private QwSopLogsMapper qwSopLogsMapper;
+
+    @Autowired
+    private QwSopSmsLogsMapper qwSopSmsLogsMapper;
+
     @Override
     public R sendTSms(String mobile, String code) {
 //        try{
@@ -157,40 +174,80 @@ public class SmsServiceImpl implements ISmsService
 
     @Override
     public String smsNotify(String json) {
+        log.info("短信回调参数:{}", json);
         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());
                         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());
-                        if(vo.getTime()!=null){
+                        if (vo.getTime() != null) {
                             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()));
-                            }
-                            catch (Exception e){
+                            } catch (Exception e) {
 
                             }
                         }
                         //状态报告
-                        if(vo.getStat().equals("DELIVRD")){
+                        if (vo.getStat().equals("DELIVRD")) {
                             logs.setStatus(1);
-                        }
-                        else{
+                        } else {
                             logs.setStatus(-1);
-                            companySmsService.addCompanySms(logs.getCompanyId(),logs.getNumber());
+                            companySmsService.addCompanySms(logs.getCompanyId(), logs.getNumber());
                         }
                         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
     @Synchronized
     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 Integer smsIndex;
+
+    /** 关联 SOP 短信日志 id(QwSopSmsLogs.id),用于回调时更新该记录 */
+    private Long sopSmsLogId;
+
+    public Long getSopSmsLogId() {
+        return sopSmsLogId;
+    }
+
+    public void setSopSmsLogId(Long sopSmsLogId) {
+        this.sopSmsLogId = sopSmsLogId;
+    }
+
+    public Integer getSmsIndex() {
+        return smsIndex;
+    }
+
+    public void setSmsIndex(Integer smsIndex) {
+        this.smsIndex = smsIndex;
+    }
 
     public String getType() {
         return type;

+ 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<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 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);
 
     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.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsUserCourseService;
+import com.fs.course.vo.FsSopMyCourseH5LinkVO;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.utils.ConfigUtil;
 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.service.ISysConfigService;
 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.gson.Gson;
 import lombok.Synchronized;
@@ -1032,6 +1035,18 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
         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
     public R getLiveWxaCodeGenerateScheme(String linkStr, String appId) {
         CloseableHttpClient client = null;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 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}
                                                        and qw_user_id = #{qwUserId} and user_id is null
     </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>

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

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

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

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

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

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

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