Ver código fonte

益寿缘-销售端-增加通过短信发送课程链接的功能

cgp 3 dias atrás
pai
commit
2d2f3a2a35
22 arquivos alterados com 1122 adições e 48 exclusões
  1. 2 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java
  2. 74 0
      fs-company/src/main/java/com/fs/company/controller/qw/SmsLinkRemindCourseController.java
  3. 8 1
      fs-service/src/main/java/com/fs/common/service/ISmsService.java
  4. 85 39
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  5. 51 0
      fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionSendMsgLog.java
  6. 52 0
      fs-service/src/main/java/com/fs/qw/domain/QwCourseLinkSendMsgLog.java
  7. 38 0
      fs-service/src/main/java/com/fs/qw/dto/SmsLinkRemindCourseDTO.java
  8. 33 0
      fs-service/src/main/java/com/fs/qw/enums/SmsLogType.java
  9. 61 0
      fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionSendMsgLogMapper.java
  10. 61 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCourseLinkSendMsgLogMapper.java
  11. 15 0
      fs-service/src/main/java/com/fs/qw/service/ISmsLinkRemindCourseService.java
  12. 7 7
      fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java
  13. 197 0
      fs-service/src/main/java/com/fs/qw/service/impl/SmsLinkRemindCourseServiceImpl.java
  14. 19 0
      fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategy.java
  15. 54 0
      fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategyManager.java
  16. 34 0
      fs-service/src/main/java/com/fs/qw/strategy/impl/AcquisitionLinkLogStrategyImpl.java
  17. 34 0
      fs-service/src/main/java/com/fs/qw/strategy/impl/CourseLinkLogStrategyImpl.java
  18. 18 0
      fs-service/src/main/java/com/fs/qw/strategy/impl/NoOpSmsLogStrategy.java
  19. 13 0
      fs-service/src/main/java/com/fs/sop/service/ISopUserLogsInfoService.java
  20. 66 0
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  21. 100 0
      fs-service/src/main/resources/mapper/qw/QwAcquisitionSendMsgLogMapper.xml
  22. 100 0
      fs-service/src/main/resources/mapper/qw/QwCourseLinkSendMsgLogMapper.xml

+ 2 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java

@@ -70,7 +70,8 @@ public class QwAcquisitionAssistantController extends BaseController {
                 return AjaxResult.error(sendResultDetailDTO.getFailReason());
             }
         } catch (Exception e) {
-            return AjaxResult.error("系统异常:" + e.getMessage());
+            log.error("发送失败:" + e.getMessage());
+            return AjaxResult.error("网络异常,请稍后再试");
         }
     }
 

+ 74 - 0
fs-company/src/main/java/com/fs/company/controller/qw/SmsLinkRemindCourseController.java

@@ -0,0 +1,74 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.dto.SmsLinkRemindCourseDTO;
+import com.fs.qw.service.ISmsLinkRemindCourseService;
+import com.fs.voice.utils.StringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+/**
+ * 企业微信客户-短信催课Controller
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@Slf4j
+@RestController
+@RequestMapping("/qw/smsLinkRemindCourse")
+public class SmsLinkRemindCourseController extends BaseController {
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ISmsLinkRemindCourseService smsLinkRemindCourseService;
+
+    @PostMapping("/sendCourseLinkMsg")
+    public AjaxResult sendCourseLinkMsg(@Valid @RequestBody SmsLinkRemindCourseDTO smsLinkRemindCourseDto) {
+        // ========== 1. 参数预处理 ==========
+        log.info("发送课程链接短信请求开始, 请求参数: {}", smsLinkRemindCourseDto);
+
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser == null || loginUser.getCompany() == null) {
+                log.error("获取登录用户信息失败,用户未登录或公司信息为空");
+                return AjaxResult.error("用户未登录或会话已过期");
+            }
+            //校验企业编号
+            if (StringUtil.strIsNullOrEmpty(smsLinkRemindCourseDto.getCorpId())) {
+                throw new CustomException("企业corpId为空,不能发送课程链接短信");
+            }
+            SendResultDetailDTO sendResultDetailDTO = smsLinkRemindCourseService.sendMessageLinkRemindCourse(smsLinkRemindCourseDto);
+
+            if (sendResultDetailDTO.isSuccess()) {
+                log.info("发送课程链接短信成功, fsUserId:{}", smsLinkRemindCourseDto.getFsUserId());
+                return AjaxResult.success("发送成功");
+            } else {
+                log.warn("发送短信课程链失败, fsUserId:{}, 失败原因: {}",
+                        smsLinkRemindCourseDto.getFsUserId(), sendResultDetailDTO.getFailReason());
+                return AjaxResult.error(sendResultDetailDTO.getFailReason());
+            }
+
+        } catch (CustomException e) {
+            log.warn("业务异常: {}", e.getMessage());
+            return AjaxResult.error(e.getMessage());
+
+        } catch (Exception e) {
+            log.error("发送课程链接短信异常, fsUserId:{}", smsLinkRemindCourseDto.getFsUserId(), e);
+            return AjaxResult.error("系统繁忙,请稍后再试");
+        }
+    }
+}

+ 8 - 1
fs-service/src/main/java/com/fs/common/service/ISmsService.java

@@ -5,6 +5,7 @@ import com.fs.company.domain.CompanySmsTemp;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.param.SmsSendParam;
 import com.fs.crm.param.SmsSendUserParam;
+import com.fs.qw.enums.SmsLogType;
 
 
 public interface ISmsService
@@ -27,6 +28,12 @@ public interface ISmsService
 
     R sendUrl(String phone, String content, String code,Long uuid,Integer smsIndex,String deleteKey);
 
-    R sendAcquisitionMessage(String phone, String content, CompanySmsTemp temp);
+    /**
+     *  根据号码、内容、模板发送短信(简洁版)
+     *  @param phone 号码
+     *  @param content 内容
+     *  @param temp 模板
+     * */
+    R simpleSmsSend(String phone, String content, CompanySmsTemp temp, SmsLogType logType, Object contextObject);
 
 }

+ 85 - 39
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -9,6 +9,7 @@ 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.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsSendItemVO;
@@ -26,9 +27,15 @@ 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.QwAcquisitionSendMsgLog;
+import com.fs.qw.domain.QwCourseLinkSendMsgLog;
 import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.mapper.QwAcquisitionSendMsgLogMapper;
+import com.fs.qw.mapper.QwCourseLinkSendMsgLogMapper;
 import com.fs.qw.mapper.QwSopSmsLogsMapper;
 import com.fs.qw.service.IQwSopSmsLogsService;
+import com.fs.qw.strategy.SmsLogStrategyManager;
 import com.fs.sms.domain.SendSmsReturn;
 import com.fs.sms.service.impl.SmsTServiceImpl;
 import com.fs.sop.domain.QwSopLogs;
@@ -91,6 +98,14 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     private FsPackageOrderMapper packageOrderMapper;
 
+    @Autowired
+    private QwAcquisitionSendMsgLogMapper acquisitionSendMsgLogMapper;
+
+    @Autowired
+    private QwCourseLinkSendMsgLogMapper courseLinkSendMsgLogMapper;
+
+    @Autowired
+    private SmsLogStrategyManager smsLogStrategyManager;
     @Override
     public R sendTSms(String mobile, String code) {
 //        try{
@@ -839,51 +854,82 @@ public class SmsServiceImpl implements ISmsService
         return R.ok();
     }
 
+    /**
+     * 发送简单短信
+     * @param phone 接收方手机号
+     * @param content 短信内容
+     * @param temp 短信模板
+     * @param logType 日志记录类型,用于区分调用方
+     * @param contextObject 特定业务的上下文对象,如qwAcquisitionId或externalContactId
+     * @return R 响应结果
+     */
     @Override
-    public R sendAcquisitionMessage(String phone, String content, CompanySmsTemp temp) {
+    public R simpleSmsSend(String phone, String content, CompanySmsTemp temp, SmsLogType logType, Object contextObject) {
         String urls = null;
+        R response; // 存储最终响应
+        Integer number = calculateSmsCount(content);
         SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
         FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
-            try {
-                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());
-                        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("发送获客链接短信失败!");
+        try {
+            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);
+            response = R.error("短信发送失败:" + e.getMessage());
+            // 发送失败也要记录特定业务日志
+            smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, contextObject);
+            return response;
+        }
+
+        String post = HttpRequest.get(urls).execute().body();
+        SmsSendVO vo = JSONUtil.toBean(post, SmsSendVO.class);
+
+        if (vo.getStatus().equals(0)) {
+            boolean anySuccess = false;
+            for (SmsSendItemVO itemVO : vo.getList()) {
+                if (itemVO.getResult().equals("0")) {
+                    anySuccess = true;
+                    // 记录通用日志
+                    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());
+                    logs.setNumber(number);
+                    smsLogsService.insertCompanySmsLogs(logs);
+                }
             }
-        return R.ok();
+            if(anySuccess) {
+                response = R.ok();
+                // 记录特定业务日志
+                smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, contextObject);
+            } else {
+                response = R.error("发送短信失败,服务商返回无成功项!");
+                smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, contextObject);
+            }
+        } else {
+            response = R.error("发送短信失败!状态码: " + vo.getStatus());
+            // 发送失败也要记录特定业务日志
+            smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, contextObject);
+        }
+        return response;
+    }
+
+    // 将计算短信条数的逻辑提取出来,方便复用
+    private int calculateSmsCount(String content) {
+        if (content == null) return 1;
+        int counts = content.length() / 67;
+        if (content.length() % 67 > 0) {
+            counts = counts + 1;
+        }
+        if (counts == 0) {
+            counts = 1;
+        }
+        return counts;
     }
 
     @Override

+ 51 - 0
fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionSendMsgLog.java

@@ -0,0 +1,51 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 企微-获客链接短信发送记录日志对象 qw_acquisition_send_msg_log
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@Data
+public class QwAcquisitionSendMsgLog extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 获客链接管理ID */
+    @Excel(name = "获客链接管理ID")
+    private String qwAcquisitionId;
+
+    /** 客户电话 */
+    @Excel(name = "客户电话")
+    private String phone;
+
+    /** 短信数量,超过67个字符为2条短信 */
+    @Excel(name = "短信数量")
+    private Integer number;
+
+    /** 短信模板id */
+    @Excel(name = "短信模板id")
+    private Long tempId;
+
+    /** 短信服务商类型 */
+    @Excel(name = "短信服务商类型")
+    private String type;
+
+    /** 短信内容 */
+    @Excel(name = "短信内容")
+    private String content;
+
+    /** 发送结果 */
+    @Excel(name = "发送结果")
+    private String result;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
+}

+ 52 - 0
fs-service/src/main/java/com/fs/qw/domain/QwCourseLinkSendMsgLog.java

@@ -0,0 +1,52 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 企微-发送看课链接短信记录日志对象 qw_course_link_send_msg_log
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@Data
+public class QwCourseLinkSendMsgLog extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 短信数量,超过67个字符为2条短信 */
+    @Excel(name = "短信数量")
+    private Integer number;
+
+    /** 短信模板id */
+    @Excel(name = "短信模板id")
+    private Long tempId;
+
+    /** 外部联系人ID */
+    @Excel(name = "外部联系人ID")
+    private Long externalContactId;
+
+    /** 短信服务商类型 */
+    @Excel(name = "短信服务商类型")
+    private String type;
+
+    /** 客户电话 */
+    @Excel(name = "客户电话")
+    private String phone;
+
+    /** 短信内容 */
+    @Excel(name = "短信内容")
+    private String content;
+
+    /** 发送结果 */
+    @Excel(name = "发送结果")
+    private String result;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/qw/dto/SmsLinkRemindCourseDTO.java

@@ -0,0 +1,38 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+import javax.validation.constraints.*;
+import java.io.Serializable;
+/**
+ * 短信发送课程链接DTO类
+ *
+ * */
+@Data
+public class SmsLinkRemindCourseDTO implements Serializable {
+
+    // 用户id
+    @NotNull(message = "用户id不能为空")
+    private Long fsUserId;
+
+    // qwUserId,qw_user表的qw_user_id字段
+    @NotBlank(message = "企业微信用户id不能为空")
+    private String userId;
+
+    // 外部联系人id,qw_external_contact表的id字段
+    @NotNull(message = "外部联系人id不能为空")
+    private Long externalId;
+
+    // 课程id
+    @NotNull(message = "课程id不能为空")
+    @Min(value = 1, message = "课程id必须大于0")
+    private Integer courseId;
+
+    // 视频id
+    @NotNull(message = "视频id不能为空")
+    @Min(value = 1, message = "视频id必须大于0")
+    private Integer videoId;
+
+    // 企业主体id
+    @NotBlank(message = "企业主体id不能为空")
+    private String corpId;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/qw/enums/SmsLogType.java

@@ -0,0 +1,33 @@
+package com.fs.qw.enums;
+
+/**
+ * 短信发送日志类型枚举
+ * 用于在通用发送接口中区分不同的日志记录行为
+ */
+public enum SmsLogType {
+    /**
+     * 获客链接短信
+     */
+    ACQUISITION_LINK("ACQUISITION_LINK", "获客链接短信"),
+
+    /**
+     * 看课链接短信
+     */
+    COURSE_LINK("COURSE_LINK", "看课链接短信");
+
+    private final String code;
+    private final String info;
+
+    SmsLogType(String code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getInfo() {
+        return info;
+    }
+}

+ 61 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionSendMsgLogMapper.java

@@ -0,0 +1,61 @@
+package com.fs.qw.mapper;
+
+import java.util.List;
+import com.fs.qw.domain.QwAcquisitionSendMsgLog;
+
+/**
+ * 企微-获客链接短信发送记录日志Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+public interface QwAcquisitionSendMsgLogMapper
+{
+    /**
+     * 查询企微-获客链接短信发送记录日志
+     *
+     * @param id 企微-获客链接短信发送记录日志主键
+     * @return 企微-获客链接短信发送记录日志
+     */
+    public QwAcquisitionSendMsgLog selectQwAcquisitionSendMsgLogById(Long id);
+
+    /**
+     * 查询企微-获客链接短信发送记录日志列表
+     *
+     * @param qwAcquisitionSendMsgLog 企微-获客链接短信发送记录日志
+     * @return 企微-获客链接短信发送记录日志集合
+     */
+    public List<QwAcquisitionSendMsgLog> selectQwAcquisitionSendMsgLogList(QwAcquisitionSendMsgLog qwAcquisitionSendMsgLog);
+
+    /**
+     * 新增企微-获客链接短信发送记录日志
+     *
+     * @param qwAcquisitionSendMsgLog 企微-获客链接短信发送记录日志
+     * @return 结果
+     */
+    public int insertQwAcquisitionSendMsgLog(QwAcquisitionSendMsgLog qwAcquisitionSendMsgLog);
+
+    /**
+     * 修改企微-获客链接短信发送记录日志
+     *
+     * @param qwAcquisitionSendMsgLog 企微-获客链接短信发送记录日志
+     * @return 结果
+     */
+    public int updateQwAcquisitionSendMsgLog(QwAcquisitionSendMsgLog qwAcquisitionSendMsgLog);
+
+    /**
+     * 删除企微-获客链接短信发送记录日志
+     *
+     * @param id 企微-获客链接短信发送记录日志主键
+     * @return 结果
+     */
+    public int deleteQwAcquisitionSendMsgLogById(Long id);
+
+    /**
+     * 批量删除企微-获客链接短信发送记录日志
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteQwAcquisitionSendMsgLogByIds(Long[] ids);
+}

+ 61 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwCourseLinkSendMsgLogMapper.java

@@ -0,0 +1,61 @@
+package com.fs.qw.mapper;
+
+import java.util.List;
+import com.fs.qw.domain.QwCourseLinkSendMsgLog;
+
+/**
+ * 企微-发送看课链接短信记录日志Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+public interface QwCourseLinkSendMsgLogMapper
+{
+    /**
+     * 查询企微-发送看课链接短信记录日志
+     *
+     * @param id 企微-发送看课链接短信记录日志主键
+     * @return 企微-发送看课链接短信记录日志
+     */
+    public QwCourseLinkSendMsgLog selectQwCourseLinkSendMsgLogById(Long id);
+
+    /**
+     * 查询企微-发送看课链接短信记录日志列表
+     *
+     * @param qwCourseLinkSendMsgLog 企微-发送看课链接短信记录日志
+     * @return 企微-发送看课链接短信记录日志集合
+     */
+    public List<QwCourseLinkSendMsgLog> selectQwCourseLinkSendMsgLogList(QwCourseLinkSendMsgLog qwCourseLinkSendMsgLog);
+
+    /**
+     * 新增企微-发送看课链接短信记录日志
+     *
+     * @param qwCourseLinkSendMsgLog 企微-发送看课链接短信记录日志
+     * @return 结果
+     */
+    public int insertQwCourseLinkSendMsgLog(QwCourseLinkSendMsgLog qwCourseLinkSendMsgLog);
+
+    /**
+     * 修改企微-发送看课链接短信记录日志
+     *
+     * @param qwCourseLinkSendMsgLog 企微-发送看课链接短信记录日志
+     * @return 结果
+     */
+    public int updateQwCourseLinkSendMsgLog(QwCourseLinkSendMsgLog qwCourseLinkSendMsgLog);
+
+    /**
+     * 删除企微-发送看课链接短信记录日志
+     *
+     * @param id 企微-发送看课链接短信记录日志主键
+     * @return 结果
+     */
+    public int deleteQwCourseLinkSendMsgLogById(Long id);
+
+    /**
+     * 批量删除企微-发送看课链接短信记录日志
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteQwCourseLinkSendMsgLogByIds(Long[] ids);
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/service/ISmsLinkRemindCourseService.java

@@ -0,0 +1,15 @@
+package com.fs.qw.service;
+
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.dto.SmsLinkRemindCourseDTO;
+
+public interface ISmsLinkRemindCourseService {
+
+    /**
+     * 发送课程链接短信
+     *
+     * @param smsLinkRemindCourseDto 待发送短信的入参
+     * @return 短信发送结果
+     */
+    public SendResultDetailDTO sendMessageLinkRemindCourse(SmsLinkRemindCourseDTO smsLinkRemindCourseDto);
+}

+ 7 - 7
fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java

@@ -15,6 +15,7 @@ import com.fs.his.dto.SendResultDetailDTO;
 import com.fs.qw.domain.QwAcquisitionAssistant;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.dto.acquisition.*;
+import com.fs.qw.enums.SmsLogType;
 import com.fs.qw.mapper.QwAcquisitionAssistantMapper;
 import com.fs.qw.service.IQwAcquisitionAssistantService;
 import com.fs.qw.service.IQwCompanyService;
@@ -65,7 +66,7 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
     private static final String QW_ACQUISITION_URL_KEY_PREFIX = "qw:acquisition:url:key:";
 
     //获客链接短信模板code
-    private static final String  SOP_SMS_TEMPLATE_CODE = "获客链接短信模板";
+    private static final String  SMS_LINK_TEMPLATE_CODE = "获客链接短信模板";
 
     //访问链接域名
     private static final String  LINK_DOMAIN = "https://c.ysyd.top/";
@@ -260,9 +261,9 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
     @Override
     public SendResultDetailDTO sendMessageAcquisition(String phone,Long qwAcquisitionId) {
         log.info("发送获客链接短信,号码:{}", phone);
-        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SOP_SMS_TEMPLATE_CODE);
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SMS_LINK_TEMPLATE_CODE);
         if (temp == null) {
-            log.info("获客链接-未找到短信模板:{}", SOP_SMS_TEMPLATE_CODE);
+            log.info("获客链接-未找到短信模板:{}", SMS_LINK_TEMPLATE_CODE);
             throw new CustomException("获客链接-未找到短信模板");
         }
         String originalContent = temp.getContent();
@@ -275,18 +276,18 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
         String content = originalContent
                 .replace("${sms.friendLink}", replaceText);
         try {
-            R r = smsService.sendAcquisitionMessage(phone, content, temp);
+            R r = smsService.simpleSmsSend(phone, content, temp, SmsLogType.ACQUISITION_LINK, qwAcquisitionId.toString());
 
             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={}", qwAcquisitionId, phone, msg);
-                return new SendResultDetailDTO(false, msg, qwAcquisitionId);
+                return new SendResultDetailDTO(false, msg, null);
             }
         } catch (Exception e) {
             log.error("发送异常 获客链接id=" + qwAcquisitionId, e);
-            return new SendResultDetailDTO(false, e.getMessage(), qwAcquisitionId);
+            return new SendResultDetailDTO(false, e.getMessage(), null);
         }
     }
 
@@ -343,7 +344,6 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
         log.info(resultMsg);
         return resultMsg;
     }
-
     /**
      * 将企微详情转换为本地实体
      */

+ 197 - 0
fs-service/src/main/java/com/fs/qw/service/impl/SmsLinkRemindCourseServiceImpl.java

@@ -0,0 +1,197 @@
+package com.fs.qw.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import com.fs.common.service.ISmsService;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanySmsTemp;
+import com.fs.company.service.ICompanySmsTempService;
+import com.fs.course.config.CourseConfig;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.dto.SmsLinkRemindCourseDTO;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.his.domain.FsUser;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.ISmsLinkRemindCourseService;
+import com.fs.qw.vo.QwSopCourseFinishTempSetting;
+import com.fs.sop.service.ISopUserLogsInfoService;
+import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+
+
+/**
+ * 企微-短信发送看课链接服务实现类
+ */
+@Slf4j
+@Service
+public class SmsLinkRemindCourseServiceImpl implements ISmsLinkRemindCourseService {
+
+    @Autowired
+    private ISmsService smsService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private ICompanySmsTempService smsTempService;
+
+    @Autowired
+    private ISopUserLogsInfoService sopUserLogsInfoService;
+
+    @Autowired
+    private QwUserMapper qwUserMapper;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    //看课短信模板code
+    private static final String SMS_COURSE_TEMPLATE_CODE = "看课发送短信模板";
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public SendResultDetailDTO sendMessageLinkRemindCourse(SmsLinkRemindCourseDTO smsLinkRemindCourseDto) {
+
+        // ========== 第一阶段:基础参数校验与配置加载 ==========
+        // 1.1 加载课程配置
+        CourseConfig config = loadCourseConfig();
+
+        // ========== 第二阶段:短信模板处理 ==========
+        // 2.1 获取短信模板
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SMS_COURSE_TEMPLATE_CODE);
+        if (temp == null) {
+            log.error("发送课程链接短信-未找到短信模板:{}", SMS_COURSE_TEMPLATE_CODE);
+            throw new CustomException("发送课程链接短信-未找到短信模板");
+        }
+
+        // ========== 第三阶段:准备基础数据 ==========
+        QwUser qwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(smsLinkRemindCourseDto.getCorpId(), smsLinkRemindCourseDto.getUserId());
+        if (qwUser == null) {
+            log.error("发送课程链接短信-未找到企微用户corpId:{},userId:{}", smsLinkRemindCourseDto.getCorpId(), smsLinkRemindCourseDto.getUserId());
+            throw new CustomException("发送课程链接短信-未找到企微用户");
+        }
+        FsUser fsUser = fsUserMapper.selectFsUserById(smsLinkRemindCourseDto.getFsUserId());
+        if (fsUser == null) {
+            log.error("发送课程链接短信-未找到用户fsUserId:{}", smsLinkRemindCourseDto.getFsUserId());
+            throw new CustomException("发送课程链接短信-未找到用户");
+        }
+
+        if (StringUtils.isBlank(fsUser.getPhone())){
+            log.error("发送课程链接短信-未找到用户手机号fsUserId:{}", smsLinkRemindCourseDto.getFsUserId());
+            throw new CustomException("发送课程链接短信-未找到用户手机号");
+        }
+        //处理手机号(存在部分加密、未加密的情况)
+        String phone =decryptSendLinkPhone(fsUser.getPhone());
+        String companyUserId = String.valueOf(qwUser.getCompanyUserId()).trim();
+        String companyId = String.valueOf(qwUser.getCompanyId()).trim();
+        String qwUserTableId = String.valueOf(qwUser.getId());
+        Integer videoId = smsLinkRemindCourseDto.getVideoId();
+        Integer courseId = smsLinkRemindCourseDto.getCourseId();
+        Long externalId = smsLinkRemindCourseDto.getExternalId();
+        Long fsUserId = smsLinkRemindCourseDto.getFsUserId();
+
+        String startTime = DateUtils.getTime();
+        Date createTime = DateUtils.getNowDate();
+
+        // ========== 第四阶段:添加看课记录(未走sop逻辑,无sopId) ==========
+        sopUserLogsInfoService.addWatchLog(
+                null,
+                videoId,
+                courseId,
+                fsUserId,
+                qwUserTableId,
+                companyUserId,
+                companyId,
+                externalId,
+                startTime,
+                createTime
+        );
+
+        // ========== 第五阶段:生成看课短链 ==========
+        QwSopCourseFinishTempSetting.Setting setting = new QwSopCourseFinishTempSetting.Setting();
+        String link = sopUserLogsInfoService.createSmsCourseLink(
+                setting,
+                smsLinkRemindCourseDto.getCorpId(),
+                createTime,
+                courseId,
+                videoId,
+                qwUserTableId,
+                companyUserId,
+                companyId,
+                externalId,
+                config
+        );
+
+        // ========== 第六阶段:校验短链生成结果 ==========
+        if (StringUtils.isBlank(link)) {
+            log.error("生成看课短链失败, phone:{}, link:{}", phone, link);
+            throw new CustomException("生成看课短链失败");
+        }
+
+        // ========== 第七阶段:发送短信 ==========
+        String tempContent = temp.getContent();
+        if (StringUtils.isBlank(tempContent) || !tempContent.contains("${sms.courseUrl}")) {
+            log.error("生成看课短链时检测到短信模板选择错误,跳过设置 URL。");
+            throw new CustomException("短信模板内容异常");
+        }
+
+        // 7.1 构建短信内容
+        String messageContent = tempContent
+                .replaceAll("【(.*?)】", "【" + config.getSmsDomain() + "】")
+                .replace("${sms.courseUrl}", link);
+
+        // 7.2 发送短信
+        try {
+            R result = smsService.simpleSmsSend(phone, messageContent, temp, SmsLogType.COURSE_LINK, externalId);
+            if (result != null && "200".equals(String.valueOf(result.get("code")))) {
+                return new SendResultDetailDTO(true, null, null);
+            } else {
+                String msg = result != null && result.get("msg") != null ? result.get("msg").toString() : "未知错误";
+                log.error("短信发送失败 , phone={}, 发送结果={}", phone, msg);
+                return new SendResultDetailDTO(false, msg, null);
+            }
+        } catch (Exception e) {
+            log.error("发送异常 phone=" + phone, e);
+            return new SendResultDetailDTO(false, e.getMessage(), null);
+        }
+    }
+
+    /**
+     * 加载课程配置
+     */
+    private CourseConfig loadCourseConfig() {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+        if (config == null) {
+            throw new CustomException("课程默认配置为空,请联系管理员");
+        }
+        return config;
+    }
+
+    /**
+     * 解密手机号
+     * */
+    private String decryptSendLinkPhone(String phone) {
+        try {
+            if (phone.length() > 11){
+                return PhoneUtil.decryptPhone(phone);
+            } else if (phone.length()<11) {
+                throw new CustomException("发送课程链接短信-手机号长度异常");
+            }
+            return phone;
+        }catch (Exception e){
+            log.error("发送课程链接短信-解密手机号异常", e);
+            throw new CustomException("发送课程链接短信-解密手机号异常");
+        }
+    }
+
+}

+ 19 - 0
fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategy.java

@@ -0,0 +1,19 @@
+package com.fs.qw.strategy;
+
+import com.fs.common.core.domain.R;
+/**
+ * 短信发送后记录特定业务日志的策略接口
+ */
+public interface SmsLogStrategy {
+    /**
+     * 执行日志记录操作
+     * @param result 发送结果
+     * @param content 短信内容
+     * @param phone 手机号
+     * @param tempId 模板ID
+     * @param type 服务商类型
+     * @param number 短信条数
+     * @param contextObject 特定业务的上下文对象(如qwAcquisitionId, externalContactId等)
+     */
+    void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject);
+}

+ 54 - 0
fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategyManager.java

@@ -0,0 +1,54 @@
+package com.fs.qw.strategy;
+
+import com.fs.common.core.domain.R;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.strategy.impl.AcquisitionLinkLogStrategyImpl;
+import com.fs.qw.strategy.impl.CourseLinkLogStrategyImpl;
+import com.fs.qw.strategy.impl.NoOpSmsLogStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.EnumMap;
+import java.util.Map;
+@Slf4j
+@Component
+public class SmsLogStrategyManager {
+
+    private final Map<SmsLogType, SmsLogStrategy> strategies = new EnumMap<>(SmsLogType.class);
+
+    @Autowired
+    private AcquisitionLinkLogStrategyImpl acquisitionLinkLogStrategy;
+
+    @Autowired
+    private CourseLinkLogStrategyImpl courseLinkLogStrategy;
+
+    //注入空操作策略
+    @Autowired
+    private NoOpSmsLogStrategy noOpSmsLogStrategy;
+
+    @PostConstruct
+    public void init() {
+        strategies.put(SmsLogType.ACQUISITION_LINK, acquisitionLinkLogStrategy);
+        strategies.put(SmsLogType.COURSE_LINK, courseLinkLogStrategy);
+    }
+
+    public void executeLogStrategy(SmsLogType logType, R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+        // 如果 logType 为 null,则使用空操作策略
+        if (logType == null) {
+            noOpSmsLogStrategy.recordLog(result, content, phone, tempId, type, number, contextObject);
+            return;
+        }
+        
+        // 尝试从map中获取策略
+        SmsLogStrategy strategy = strategies.get(logType);
+        if (strategy == null) {
+            // 使用空操作策略,以保证业务流程不中断
+            noOpSmsLogStrategy.recordLog(result, content, phone, tempId, type, number, contextObject);
+             log.warn("未找到处理类型 [{}] 的日志策略,将执行空操作。", logType.getInfo());
+            return;
+        }
+        strategy.recordLog(result, content, phone, tempId, type, number, contextObject);
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/qw/strategy/impl/AcquisitionLinkLogStrategyImpl.java

@@ -0,0 +1,34 @@
+package com.fs.qw.strategy.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.qw.domain.QwAcquisitionSendMsgLog;
+import com.fs.qw.mapper.QwAcquisitionSendMsgLogMapper;
+import com.fs.qw.strategy.SmsLogStrategy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.fs.common.core.domain.R;
+
+@Component
+public class AcquisitionLinkLogStrategyImpl implements SmsLogStrategy {
+
+    @Autowired
+    private QwAcquisitionSendMsgLogMapper acquisitionSendMsgLogMapper;
+
+    @Override
+    public void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+        // contextObject 应该是 String 类型的 qwAcquisitionId
+        String qwAcquisitionId = (String) contextObject;
+
+        QwAcquisitionSendMsgLog log = new QwAcquisitionSendMsgLog();
+        log.setQwAcquisitionId(qwAcquisitionId);
+        log.setNumber(number);
+        log.setType(type);
+        log.setPhone(phone);
+        log.setTempId(tempId);
+        log.setContent(content);
+        log.setCreateTime(DateUtils.getNowDate());
+        log.setResult(result.get("msg").toString());
+        
+        acquisitionSendMsgLogMapper.insertQwAcquisitionSendMsgLog(log);
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/qw/strategy/impl/CourseLinkLogStrategyImpl.java

@@ -0,0 +1,34 @@
+package com.fs.qw.strategy.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.qw.domain.QwCourseLinkSendMsgLog;
+import com.fs.qw.mapper.QwCourseLinkSendMsgLogMapper;
+import com.fs.qw.strategy.SmsLogStrategy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.fs.common.core.domain.R;
+
+@Component
+public class CourseLinkLogStrategyImpl implements SmsLogStrategy {
+
+    @Autowired
+    private QwCourseLinkSendMsgLogMapper courseLinkSendMsgLogMapper;
+
+    @Override
+    public void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+        // contextObject 应该是 Long 类型的 externalContactId
+        Long externalContactId = (Long) contextObject;
+
+        QwCourseLinkSendMsgLog log = new QwCourseLinkSendMsgLog();
+        log.setExternalContactId(externalContactId);
+        log.setNumber(number);
+        log.setType(type);
+        log.setPhone(phone);
+        log.setTempId(tempId);
+        log.setContent(content);
+        log.setCreateTime(DateUtils.getNowDate());
+        log.setResult(result.get("msg").toString());
+        
+        courseLinkSendMsgLogMapper.insertQwCourseLinkSendMsgLog(log);
+    }
+}

+ 18 - 0
fs-service/src/main/java/com/fs/qw/strategy/impl/NoOpSmsLogStrategy.java

@@ -0,0 +1,18 @@
+package com.fs.qw.strategy.impl;
+
+import com.fs.common.core.domain.R;
+import com.fs.qw.strategy.SmsLogStrategy;
+import org.springframework.stereotype.Component;
+
+/**
+ * 空操作日志策略,用于处理不需要记录特定业务日志的场景
+ */
+@Component
+public class NoOpSmsLogStrategy implements SmsLogStrategy {
+
+    @Override
+    public void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+        // 什么都不做
+        // 这样可以保证当logType为null时,simpleSmsSend也能正常运行
+    }
+}

+ 13 - 0
fs-service/src/main/java/com/fs/sop/service/ISopUserLogsInfoService.java

@@ -1,14 +1,17 @@
 package com.fs.sop.service;
 
 import com.fs.common.core.domain.R;
+import com.fs.course.config.CourseConfig;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.param.QwExtCourseSopWatchLog;
+import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.sop.domain.SopUserLogsInfo;
 import com.fs.sop.params.BatchSopUserLogsInfoParam;
 import com.fs.sop.params.SendUserLogsInfoMsgParam;
 import com.fs.sop.vo.ExtCourseSopWatchLogVO;
 import com.fs.sop.vo.SopUserLogsInfoVOE;
 
+import java.util.Date;
 import java.util.List;
 
 public interface ISopUserLogsInfoService {
@@ -84,4 +87,14 @@ public interface ISopUserLogsInfoService {
     List<SopUserLogsInfo> selectRestoreByIsDaysNotStudy(String sopId, String userLogsId);
 
     public List<ExtCourseSopWatchLogVO> getExtCourseSopWatchLog(QwExtCourseSopWatchLog qwExternalContactId);
+
+    /**
+     * 新增观看记录
+     * */
+    public void addWatchLog(String sopId, Integer videoId, Integer courseId, Long fsUserId, String qwUserId, String companyUserId, String companyId, Long externalId, String startTime, Date createTime);
+
+    /**
+     * 生成短信看课链接
+     */
+    public String createSmsCourseLink(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,Integer courseId, Integer videoId, String qwUserId,String companyUserId, String companyId, Long externalId, CourseConfig config);
 }

+ 66 - 0
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
 import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.PubFun;
@@ -1122,6 +1123,71 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return watchLogVOList;
     }
 
+    @Override
+    public void addWatchLog(String sopId, Integer videoId, Integer courseId, Long fsUserId, String qwUserId, String companyUserId, String companyId, Long externalId, String startTime, Date createTime) {
+        try {
+            FsCourseWatchLog watchLog = new FsCourseWatchLog();
+            watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
+            watchLog.setQwExternalContactId(externalId);
+            watchLog.setSendType(2);
+            watchLog.setQwUserId(Long.valueOf(qwUserId));
+            watchLog.setSopId(sopId);
+            watchLog.setDuration(0L);
+            watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
+            watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
+            watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
+            watchLog.setCreateTime(createTime);
+            watchLog.setUpdateTime(createTime);
+            watchLog.setLogType(3);
+            watchLog.setUserId(fsUserId);
+            watchLog.setCampPeriodTime(convertStringToDate(startTime,"yyyy-MM-dd HH:mm:ss"));
+
+            //存看课记录
+            fsCourseWatchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);
+        }catch (Exception e){
+            log.error("短信催课失败-插入观看记录失败:"+e.getMessage());
+            throw new CustomException("新增短信链接看课记录失败: " + e.getMessage());
+        }
+    }
+
+    public String createSmsCourseLink(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
+                                      Integer courseId, Integer videoId, String qwUserId,
+                                      String companyUserId, String companyId, Long externalId, CourseConfig config) {
+
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(corpId);
+        link.setCourseId(courseId.longValue());
+        link.setQwExternalId(externalId);
+        link.setLinkType(0); //正常链接
+        link.setUNo(UUID.randomUUID().toString());
+        String randomString = ShortCodeGeneratorUtils.generate8();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+        link.setCreateTime(sendTime);;
+        Date updateTime = createUpdateTime(setting, sendTime, config);
+        link.setUpdateTime(updateTime);
+
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String realLinkFull = REAL_LINK_PREFIX + JSON.toJSONString(courseMap);
+        link.setRealLink(realLinkFull);
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        if(StringUtils.isEmpty(config.getSmsDomainName())){
+            log.error("检测到未配置看课短信链接域名");
+            return null;
+        }
+        return config.getSmsDomainName() + "/" + link.getLink();
+    }
+
     private R processQwSopLogsBySendMsg(SendUserLogsInfoMsgParam param,Integer draftStrategy) {
 
 

+ 100 - 0
fs-service/src/main/resources/mapper/qw/QwAcquisitionSendMsgLogMapper.xml

@@ -0,0 +1,100 @@
+<?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.QwAcquisitionSendMsgLogMapper">
+
+    <resultMap type="com.fs.qw.domain.QwAcquisitionSendMsgLog" id="QwAcquisitionSendMsgLogResult">
+        <result property="id"    column="id"    />
+        <result property="qwAcquisitionId"    column="qw_acquisition_id"    />
+        <result property="phone"    column="phone"    />
+        <result property="number"    column="number"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="type"    column="type"    />
+        <result property="content"    column="content"    />
+        <result property="result"    column="result"    />
+        <result property="remark"    column="remark"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectQwAcquisitionSendMsgLogVo">
+        select id, qw_acquisition_id, phone, number, temp_id, type, content, result, remark, create_by, create_time from qw_acquisition_send_msg_log
+    </sql>
+
+    <select id="selectQwAcquisitionSendMsgLogList" parameterType="com.fs.qw.domain.QwAcquisitionSendMsgLog" resultMap="QwAcquisitionSendMsgLogResult">
+        <include refid="selectQwAcquisitionSendMsgLogVo"/>
+        <where>
+            <if test="qwAcquisitionId != null  and qwAcquisitionId != ''"> and qw_acquisition_id = #{qwAcquisitionId}</if>
+            <if test="phone != null  and phone != ''"> and phone like concat('%', #{phone}, '%')</if>
+            <if test="number != null "> and number = #{number}</if>
+            <if test="tempId != null "> and temp_id = #{tempId}</if>
+            <if test="type != null  and type != ''"> and type like concat('%', #{type}, '%')</if>
+            <if test="content != null  and content != ''"> and content like concat('%', #{content}, '%')</if>
+            <if test="result != null  and result != ''"> and result like concat('%', #{result}, '%')</if>
+            <if test="remark != null  and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectQwAcquisitionSendMsgLogById" parameterType="Long" resultMap="QwAcquisitionSendMsgLogResult">
+        <include refid="selectQwAcquisitionSendMsgLogVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwAcquisitionSendMsgLog" parameterType="com.fs.qw.domain.QwAcquisitionSendMsgLog" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_acquisition_send_msg_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="qwAcquisitionId != null and qwAcquisitionId != ''">qw_acquisition_id,</if>
+            <if test="phone != null and phone != ''">phone,</if>
+            <if test="number != null">number,</if>
+            <if test="tempId != null">temp_id,</if>
+            <if test="type != null and type != ''">type,</if>
+            <if test="content != null and content != ''">content,</if>
+            <if test="result != null and result != ''">result,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            create_time,
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="qwAcquisitionId != null and qwAcquisitionId != ''">#{qwAcquisitionId},</if>
+            <if test="phone != null and phone != ''">#{phone},</if>
+            <if test="number != null">#{number},</if>
+            <if test="tempId != null">#{tempId},</if>
+            <if test="type != null and type != ''">#{type},</if>
+            <if test="content != null and content != ''">#{content},</if>
+            <if test="result != null and result != ''">#{result},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            sysdate(),
+        </trim>
+    </insert>
+
+    <update id="updateQwAcquisitionSendMsgLog" parameterType="com.fs.qw.domain.QwAcquisitionSendMsgLog">
+        update qw_acquisition_send_msg_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="qwAcquisitionId != null and qwAcquisitionId != ''">qw_acquisition_id = #{qwAcquisitionId},</if>
+            <if test="phone != null and phone != ''">phone = #{phone},</if>
+            <if test="number != null">number = #{number},</if>
+            <if test="tempId != null">temp_id = #{tempId},</if>
+            <if test="type != null and type != ''">type = #{type},</if>
+            <if test="content != null and content != ''">content = #{content},</if>
+            <if test="result != null and result != ''">result = #{result},</if>
+            <if test="remark != null and remark != ''">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate(),
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwAcquisitionSendMsgLogById" parameterType="Long">
+        delete from qw_acquisition_send_msg_log where id = #{id}
+    </delete>
+
+    <delete id="deleteQwAcquisitionSendMsgLogByIds" parameterType="String">
+        delete from qw_acquisition_send_msg_log where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 100 - 0
fs-service/src/main/resources/mapper/qw/QwCourseLinkSendMsgLogMapper.xml

@@ -0,0 +1,100 @@
+<?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.QwCourseLinkSendMsgLogMapper">
+
+    <resultMap type="com.fs.qw.domain.QwCourseLinkSendMsgLog" id="QwCourseLinkSendMsgLogResult">
+        <result property="id"    column="id"    />
+        <result property="number"    column="number"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="externalContactId"    column="external_contact_id"    />
+        <result property="type"    column="type"    />
+        <result property="phone"    column="phone"    />
+        <result property="content"    column="content"    />
+        <result property="result"    column="result"    />
+        <result property="remark"    column="remark"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectQwCourseLinkSendMsgLogVo">
+        select id, number, temp_id, external_contact_id, type, phone, content, result, remark, create_by, create_time from qw_course_link_send_msg_log
+    </sql>
+
+    <select id="selectQwCourseLinkSendMsgLogList" parameterType="com.fs.qw.domain.QwCourseLinkSendMsgLog" resultMap="QwCourseLinkSendMsgLogResult">
+        <include refid="selectQwCourseLinkSendMsgLogVo"/>
+        <where>
+            <if test="number != null "> and number = #{number}</if>
+            <if test="tempId != null "> and temp_id = #{tempId}</if>
+            <if test="externalContactId != null "> and external_contact_id = #{externalContactId}</if>
+            <if test="type != null  and type != ''"> and type like concat('%', #{type}, '%')</if>
+            <if test="phone != null  and phone != ''"> and phone like concat('%', #{phone}, '%')</if>
+            <if test="content != null  and content != ''"> and content like concat('%', #{content}, '%')</if>
+            <if test="result != null  and result != ''"> and result like concat('%', #{result}, '%')</if>
+            <if test="remark != null  and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectQwCourseLinkSendMsgLogById" parameterType="Long" resultMap="QwCourseLinkSendMsgLogResult">
+        <include refid="selectQwCourseLinkSendMsgLogVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwCourseLinkSendMsgLog" parameterType="com.fs.qw.domain.QwCourseLinkSendMsgLog" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_course_link_send_msg_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="number != null">number,</if>
+            <if test="tempId != null">temp_id,</if>
+            <if test="externalContactId != null">external_contact_id,</if>
+            <if test="type != null and type != ''">type,</if>
+            <if test="phone != null and phone != ''">phone,</if>
+            <if test="content != null and content != ''">content,</if>
+            <if test="result != null and result != ''">result,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            create_time,
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="number != null">#{number},</if>
+            <if test="tempId != null">#{tempId},</if>
+            <if test="externalContactId != null">#{externalContactId},</if>
+            <if test="type != null and type != ''">#{type},</if>
+            <if test="phone != null and phone != ''">#{phone},</if>
+            <if test="content != null and content != ''">#{content},</if>
+            <if test="result != null and result != ''">#{result},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            sysdate(),
+        </trim>
+    </insert>
+
+    <update id="updateQwCourseLinkSendMsgLog" parameterType="com.fs.qw.domain.QwCourseLinkSendMsgLog">
+        update qw_course_link_send_msg_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="number != null">number = #{number},</if>
+            <if test="tempId != null">temp_id = #{tempId},</if>
+            <if test="externalContactId != null">external_contact_id = #{externalContactId},</if>
+            <if test="type != null and type != ''">type = #{type},</if>
+            <if test="phone != null and phone != ''">phone = #{phone},</if>
+            <if test="content != null and content != ''">content = #{content},</if>
+            <if test="result != null and result != ''">result = #{result},</if>
+            <if test="remark != null and remark != ''">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate(),
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwCourseLinkSendMsgLogById" parameterType="Long">
+        delete from qw_course_link_send_msg_log where id = #{id}
+    </delete>
+
+    <delete id="deleteQwCourseLinkSendMsgLogByIds" parameterType="String">
+        delete from qw_course_link_send_msg_log where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>