wjj 4 дней назад
Родитель
Сommit
e85e2c1c4e

+ 134 - 0
fs-admin/src/main/java/com/fs/his/controller/FsCourseReissueConfigController.java

@@ -0,0 +1,134 @@
+package com.fs.his.controller;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.common.core.domain.R;
+import com.fs.course.domain.FsCoursePlaySourceConfig;
+import com.fs.course.service.IFsCoursePlaySourceConfigService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.his.domain.FsCourseReissueConfig;
+import com.fs.his.service.IFsCourseReissueConfigService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 补发SOP配置Controller
+ * 
+ * @author fs
+ * @date 2026-07-16
+ */
+@RestController
+@RequestMapping("/his/reissueConfig")
+public class FsCourseReissueConfigController extends BaseController
+{
+    @Autowired
+    private IFsCourseReissueConfigService fsCourseReissueConfigService;
+
+    @Autowired
+    private IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
+
+    /**
+     * 查询补发SOP配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:reissueConfig:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsCourseReissueConfig fsCourseReissueConfig)
+    {
+        startPage();
+        List<FsCourseReissueConfig> list = fsCourseReissueConfigService.selectFsCourseReissueConfigList(fsCourseReissueConfig);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出补发SOP配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:reissueConfig:export')")
+    @Log(title = "补发SOP配置", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsCourseReissueConfig fsCourseReissueConfig)
+    {
+        List<FsCourseReissueConfig> list = fsCourseReissueConfigService.selectFsCourseReissueConfigList(fsCourseReissueConfig);
+        ExcelUtil<FsCourseReissueConfig> util = new ExcelUtil<FsCourseReissueConfig>(FsCourseReissueConfig.class);
+        return util.exportExcel(list, "补发SOP配置数据");
+    }
+
+    /**
+     * 获取补发SOP配置详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:reissueConfig:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsCourseReissueConfigService.selectFsCourseReissueConfigById(id));
+    }
+
+    @GetMapping("/getConfigInfo")
+    public AjaxResult getConfigInfo()
+    {
+        return AjaxResult.success(fsCourseReissueConfigService.getInfo());
+    }
+
+    /**
+     * 新增补发SOP配置
+     */
+    @PreAuthorize("@ss.hasPermi('his:reissueConfig:add')")
+    @Log(title = "补发SOP配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsCourseReissueConfig fsCourseReissueConfig)
+    {
+        return toAjax(fsCourseReissueConfigService.insertFsCourseReissueConfig(fsCourseReissueConfig));
+    }
+
+    /**
+     * 修改补发SOP配置
+     */
+    @PreAuthorize("@ss.hasPermi('his:reissueConfig:edit')")
+    @Log(title = "补发SOP配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsCourseReissueConfig fsCourseReissueConfig)
+    {
+        return toAjax(fsCourseReissueConfigService.updateFsCourseReissueConfig(fsCourseReissueConfig));
+    }
+
+    /**
+     * 删除补发SOP配置
+     */
+    @PreAuthorize("@ss.hasPermi('his:reissueConfig:remove')")
+    @Log(title = "补发SOP配置", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsCourseReissueConfigService.deleteFsCourseReissueConfigByIds(ids));
+    }
+
+    @GetMapping("/getAppIdList")
+    public AjaxResult getAppIdList(){
+        LambdaQueryWrapper<FsCoursePlaySourceConfig> queryWrapper = Wrappers.<FsCoursePlaySourceConfig>lambdaQuery()
+                .eq(FsCoursePlaySourceConfig::getStatus, 0)
+                .eq(FsCoursePlaySourceConfig::getReplaceStatus, 1)
+                .eq(FsCoursePlaySourceConfig::getIsDel, 0)
+                .eq(FsCoursePlaySourceConfig::getType, 1);
+        List<FsCoursePlaySourceConfig> configs = fsCoursePlaySourceConfigService.list(queryWrapper);
+        return AjaxResult.success(configs);
+    }
+
+    @PostMapping("/reissueCourseSop")
+    public R reissueCourseSop(@RequestBody FsCourseReissueConfig config){
+        return fsCourseReissueConfigService.reissueCourseSop(config.getId());
+    }
+}

+ 16 - 0
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -385,6 +385,9 @@ public class Task {
     @Autowired
     private FsUserCourseVideoMapper courseVideoMapper;
 
+    @Autowired
+    private IFsCourseReissueConfigService fsCourseReissueConfigService;
+
     public void syncExpressToWx() {
         List<FsWxExpressTask> fsWxExpressTasks = fsWxExpressTaskMapper.selectPendingData();
         if (CollectionUtils.isEmpty(fsWxExpressTasks)) {
@@ -2679,6 +2682,16 @@ public class Task {
      * 发送答题SOP
      */
     public void sendSopLogs(){
+        FsCourseReissueConfig configInfoRedis = fsCourseReissueConfigService.getConfigInfoRedis();
+        if (configInfoRedis == null) {
+            return;
+        }
+        if (configInfoRedis.getStatus() != 1) {
+            return;
+        }
+        if (configInfoRedis.getType() != 2) {
+            return;
+        }
         String lockKey = "sendSopLogs:lock";
         Boolean acquired = redisCache.setIfAbsent(lockKey, "1", 10, TimeUnit.MINUTES);
         if (acquired == null || !acquired) {
@@ -2884,6 +2897,9 @@ public class Task {
                     banConfigs.add(config);
                 }
             }
+        } else {
+            log.warn("未找到可用小程序配置");
+            return;
         }
         List<QwCompany> qwCompanies = qwCompanyMapper.selectQwCompanyList(new QwCompany());
         if (CollectionUtils.isNotEmpty(qwCompanies)) {

+ 30 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -27,6 +27,8 @@ import com.fs.course.service.IFsCourseFinishTempService;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.feishu.service.FeiShuService;
+import com.fs.his.domain.FsCourseReissueConfig;
+import com.fs.his.service.IFsCourseReissueConfigService;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
@@ -221,6 +223,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Autowired
     private FeiShuService feiShuService;
 
+    @Autowired
+    private IFsCourseReissueConfigService fsCourseReissueConfigService;
+
 
 
     @PostConstruct
@@ -1012,6 +1017,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         for (QwSopTempSetting.Content.Setting setting : settings) {
 
             Integer currentIndex = index.getAndIncrement();
+            //后台SOP发课配置
+            createSetting(setting);
 
             switch (setting.getContentType()) {
                 //文字和短链一起
@@ -2609,4 +2616,27 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         //默认值
         return DEFAULT_APP_ID;
     }
+
+    private void createSetting(QwSopTempSetting.Content.Setting st) {
+        FsCourseReissueConfig configInfoRedis = fsCourseReissueConfigService.getConfigInfoRedis();
+        if (configInfoRedis != null && configInfoRedis.getStatus() == 1) {
+
+            if (configInfoRedis.getType() == 1) {
+                if ("18".equals(st.getContentType())) {
+                    st.setMiniprogramPicUrl(st.getLinkImageUrl());
+                    st.setMiniprogramTitle(st.getLinkDescribe());
+                    st.setContentType("4");
+                }
+            }
+            if (configInfoRedis.getType() == 2) {
+                if ("4".equals(st.getContentType())) {
+                    st.setContentType("18");
+                    st.setLinkDescribe(st.getMiniprogramTitle());
+                    st.setLinkImageUrl(st.getMiniprogramPicUrl());
+                    st.setLinkTitle(st.getMiniprogramTitle());
+                    st.setIsBindUrl("1");
+                }
+            }
+        }
+    }
 }

+ 10 - 0
fs-service/src/main/java/com/fs/course/vo/FsCoursePlaySourceConfigVO.java

@@ -54,4 +54,14 @@ public class FsCoursePlaySourceConfigVO {
      * 是否是互医/商城小程序
      */
     private Integer isMall;
+
+    /**
+     * 状态 0-未封禁 1-已封禁
+     */
+    private Integer status;
+
+    /**
+     * 是否替换 0-否 1-是
+     */
+    private Integer replaceStatus;
 }

+ 35 - 0
fs-service/src/main/java/com/fs/his/domain/FsCourseReissueConfig.java

@@ -0,0 +1,35 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 补发SOP配置对象 fs_course_reissue_config
+ *
+ * @author fs
+ * @date 2026-07-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsCourseReissueConfig extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 类型 1-小程序 2-飞书 */
+    @Excel(name = "类型 1-小程序 2-飞书")
+    private Integer type;
+
+    /** 小程序id */
+    @Excel(name = "小程序id")
+    private String appId;
+
+    /** 可用状态 1-正常 2-禁用 */
+    @Excel(name = "可用状态 1-正常 2-禁用")
+    private Integer status;
+
+
+}

+ 61 - 0
fs-service/src/main/java/com/fs/his/mapper/FsCourseReissueConfigMapper.java

@@ -0,0 +1,61 @@
+package com.fs.his.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsCourseReissueConfig;
+
+/**
+ * 补发SOP配置Mapper接口
+ * 
+ * @author fs
+ * @date 2026-07-16
+ */
+public interface FsCourseReissueConfigMapper extends BaseMapper<FsCourseReissueConfig>{
+    /**
+     * 查询补发SOP配置
+     * 
+     * @param id 补发SOP配置主键
+     * @return 补发SOP配置
+     */
+    FsCourseReissueConfig selectFsCourseReissueConfigById(Long id);
+
+    /**
+     * 查询补发SOP配置列表
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 补发SOP配置集合
+     */
+    List<FsCourseReissueConfig> selectFsCourseReissueConfigList(FsCourseReissueConfig fsCourseReissueConfig);
+
+    /**
+     * 新增补发SOP配置
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 结果
+     */
+    int insertFsCourseReissueConfig(FsCourseReissueConfig fsCourseReissueConfig);
+
+    /**
+     * 修改补发SOP配置
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 结果
+     */
+    int updateFsCourseReissueConfig(FsCourseReissueConfig fsCourseReissueConfig);
+
+    /**
+     * 删除补发SOP配置
+     * 
+     * @param id 补发SOP配置主键
+     * @return 结果
+     */
+    int deleteFsCourseReissueConfigById(Long id);
+
+    /**
+     * 批量删除补发SOP配置
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseReissueConfigByIds(Long[] ids);
+}

+ 73 - 0
fs-service/src/main/java/com/fs/his/service/IFsCourseReissueConfigService.java

@@ -0,0 +1,73 @@
+package com.fs.his.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.FsCourseReissueConfig;
+
+/**
+ * 补发SOP配置Service接口
+ * 
+ * @author fs
+ * @date 2026-07-16
+ */
+public interface IFsCourseReissueConfigService extends IService<FsCourseReissueConfig>{
+    /**
+     * 查询补发SOP配置
+     * 
+     * @param id 补发SOP配置主键
+     * @return 补发SOP配置
+     */
+    FsCourseReissueConfig selectFsCourseReissueConfigById(Long id);
+
+    FsCourseReissueConfig getInfo();
+
+    /**
+     * 查询补发SOP配置列表
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 补发SOP配置集合
+     */
+    List<FsCourseReissueConfig> selectFsCourseReissueConfigList(FsCourseReissueConfig fsCourseReissueConfig);
+
+    /**
+     * 新增补发SOP配置
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 结果
+     */
+    int insertFsCourseReissueConfig(FsCourseReissueConfig fsCourseReissueConfig);
+
+    /**
+     * 修改补发SOP配置
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 结果
+     */
+    int updateFsCourseReissueConfig(FsCourseReissueConfig fsCourseReissueConfig);
+
+    /**
+     * 批量删除补发SOP配置
+     * 
+     * @param ids 需要删除的补发SOP配置主键集合
+     * @return 结果
+     */
+    int deleteFsCourseReissueConfigByIds(Long[] ids);
+
+    /**
+     * 删除补发SOP配置信息
+     * 
+     * @param id 补发SOP配置主键
+     * @return 结果
+     */
+    int deleteFsCourseReissueConfigById(Long id);
+
+    /**
+     * 补发SOP
+     * @param configId 补发SOP配置id
+     * @return 结果
+     */
+    R reissueCourseSop(Long configId);
+
+    FsCourseReissueConfig getConfigInfoRedis();
+}

+ 531 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsCourseReissueConfigServiceImpl.java

@@ -0,0 +1,531 @@
+package com.fs.his.service.impl;
+
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.StringUtils;
+import com.fs.config.cloud.CloudHostProper;
+import com.fs.core.config.WxMaConfiguration;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.*;
+import com.fs.course.mapper.FsCourseLinkMapper;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.FsUserCourseMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.course.service.IFsCoursePlaySourceConfigService;
+import com.fs.feishu.service.FeiShuService;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.QwSopCourseFinishTempSetting;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.his.mapper.FsCourseReissueConfigMapper;
+import com.fs.his.domain.FsCourseReissueConfig;
+import com.fs.his.service.IFsCourseReissueConfigService;
+
+import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
+
+/**
+ * 补发SOP配置Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-07-16
+ */
+@Service
+public class FsCourseReissueConfigServiceImpl extends ServiceImpl<FsCourseReissueConfigMapper, FsCourseReissueConfig> implements IFsCourseReissueConfigService {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+
+    @Autowired
+    private FsCourseReissueConfigMapper fsCourseReissueConfigMapper;
+
+    @Autowired
+    private FsCourseLinkMapper fsCourseLinkMapper;
+
+
+
+    @Autowired
+    private QwSopLogsMapper qwSopLogsMapper;
+
+    @Autowired
+    private FsCourseWatchLogMapper watchLogMapper;
+
+    @Autowired
+    private IQwExternalContactService qwExternalContactService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    @Autowired
+    private FsUserCourseVideoMapper courseVideoMapper;
+
+    @Autowired
+    private FsUserCourseMapper userCourseMapper;
+
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private FeiShuService feiShuService;
+
+    @Autowired
+    RedisCache redisCache;
+
+
+    private static final String courseRealLink = "/pages_course/video.html?course=";
+    private static final String feiShuMiniAppLink = "/pages_course/video?course=";
+    /**
+     * 查询补发SOP配置
+     * 
+     * @param id 补发SOP配置主键
+     * @return 补发SOP配置
+     */
+    @Override
+    public FsCourseReissueConfig selectFsCourseReissueConfigById(Long id)
+    {
+        return baseMapper.selectFsCourseReissueConfigById(id);
+    }
+
+    @Override
+    public FsCourseReissueConfig getInfo() {
+        List<FsCourseReissueConfig> fsCourseReissueConfigs = fsCourseReissueConfigMapper.selectFsCourseReissueConfigList(new FsCourseReissueConfig());
+        if (CollectionUtils.isEmpty(fsCourseReissueConfigs)) {
+            return new FsCourseReissueConfig();
+        } else {
+            return fsCourseReissueConfigs.get(0);
+        }
+    }
+
+    /**
+     * 查询补发SOP配置列表
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 补发SOP配置
+     */
+    @Override
+    public List<FsCourseReissueConfig> selectFsCourseReissueConfigList(FsCourseReissueConfig fsCourseReissueConfig)
+    {
+        return baseMapper.selectFsCourseReissueConfigList(fsCourseReissueConfig);
+    }
+
+    /**
+     * 新增补发SOP配置
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 结果
+     */
+    @Override
+    public int insertFsCourseReissueConfig(FsCourseReissueConfig fsCourseReissueConfig)
+    {
+        fsCourseReissueConfig.setCreateTime(DateUtils.getNowDate());
+        int i = baseMapper.insertFsCourseReissueConfig(fsCourseReissueConfig);
+        if (i > 0) {
+            redisCache.setCacheObject("fs_course_reissue_config:", fsCourseReissueConfig);
+        }
+        return i;
+    }
+
+    /**
+     * 修改补发SOP配置
+     * 
+     * @param fsCourseReissueConfig 补发SOP配置
+     * @return 结果
+     */
+    @Override
+    public int updateFsCourseReissueConfig(FsCourseReissueConfig fsCourseReissueConfig)
+    {
+        fsCourseReissueConfig.setUpdateTime(DateUtils.getNowDate());
+        int i = baseMapper.updateFsCourseReissueConfig(fsCourseReissueConfig);
+        if (i > 0) {
+            redisCache.setCacheObject("fs_course_reissue_config:", fsCourseReissueConfig);
+        }
+        return i;
+    }
+
+    /**
+     * 批量删除补发SOP配置
+     * 
+     * @param ids 需要删除的补发SOP配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseReissueConfigByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsCourseReissueConfigByIds(ids);
+    }
+
+    /**
+     * 删除补发SOP配置信息
+     * 
+     * @param id 补发SOP配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseReissueConfigById(Long id)
+    {
+        return baseMapper.deleteFsCourseReissueConfigById(id);
+    }
+
+    @Override
+    public R reissueCourseSop(Long configId) {
+        // 1.查询补发SOP配置
+        FsCourseReissueConfig fsCourseReissueConfig = fsCourseReissueConfigMapper.selectFsCourseReissueConfigById(configId);
+
+        if (fsCourseReissueConfig == null) {
+            return R.error("补发SOP配置不存在");
+        }
+        if (fsCourseReissueConfig.getStatus() == 2) {
+            return R.error("补发SOP配置已禁用");
+        }
+        if (fsCourseReissueConfig.getType() == 1) {
+            // 小程序
+            if ( fsCourseReissueConfig.getAppId() == null) {
+                return R.error("小程序APPID不能为空");
+            }
+            if (!checkAppId(fsCourseReissueConfig.getAppId())) {
+                return R.error("小程序已封禁");
+            }
+            //作废sopLos
+            qwSopLogsMapper.wasteQwSopLogs();
+            //小程序补发
+            List<FsCourseWatchLog> watchLogs = watchLogMapper.selectTodayWatchLogs(4);
+            if (CollectionUtils.isNotEmpty(watchLogs)) {
+                reissueCourse(fsCourseReissueConfig.getAppId(), fsCourseReissueConfig.getType(), watchLogs, null);
+            }
+            List<FsCourseWatchLog> waitWatchLogs = watchLogMapper.selectTodayWatchLogs(3);
+
+            if (CollectionUtils.isNotEmpty(waitWatchLogs)) {
+                reissueCourse(fsCourseReissueConfig.getAppId(), fsCourseReissueConfig.getType(), waitWatchLogs, null);
+            }
+            return R.ok("补发成功");
+        } else if (fsCourseReissueConfig.getType() == 2) {
+            // 飞书
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+            List<FsCourseWatchLog> watchLogs = watchLogMapper.selectTodayWatchLogs(4);
+            if (CollectionUtils.isNotEmpty(watchLogs)) {
+                reissueCourse(null, fsCourseReissueConfig.getType(), watchLogs, config);
+            }
+            List<FsCourseWatchLog> waitWatchLogs = watchLogMapper.selectTodayWatchLogs(3);
+            if (CollectionUtils.isNotEmpty(waitWatchLogs)) {
+                reissueCourse(null, fsCourseReissueConfig.getType(), waitWatchLogs, config);
+            }
+            return R.ok("补发成功");
+        } else {
+            return R.error("补发SOP配置类型错误");
+        }
+
+        //return R.error("补发失败");
+    }
+
+    @Override
+    public FsCourseReissueConfig getConfigInfoRedis() {
+        FsCourseReissueConfig config = redisCache.getCacheObject("fs_course_reissue_config:");
+        if (config != null) {
+            return config;
+        }
+        config = fsCourseReissueConfigMapper.selectOne(Wrappers.<FsCourseReissueConfig>lambdaQuery().eq(FsCourseReissueConfig::getStatus, 1));
+        if (config != null) {
+            redisCache.setCacheObject("fs_course_reissue_config:", config);
+            return config;
+        }
+        return null;
+    }
+
+
+    private boolean checkAppId(String appId) {
+        boolean result = false;
+        final WxMaService wxMaService = WxMaConfiguration.getMaService(appId);
+        String scene="collectionId";
+        try {
+            wxMaService.getQrcodeService().createWxaCodeUnlimitBytes(
+                    scene,
+                    "pages_course/customerStatistics",
+                    true,
+                    "release",//trial - 体验版  release - 正式版  develop - 开发版
+                    430,
+                    true,
+                    null,
+                    false);
+            result = true;
+        } catch (WxErrorException e) {
+            int errorCode = e.getError().getErrorCode();
+            if (errorCode == 50002) {
+                logger.info("生成二维码失败,mainAppId: {}, 错误码: {}, 错误信息: {}", appId, errorCode, e.getMessage());
+            }
+        } catch (Exception e) {
+            result = true;
+            logger.info("生成二维码失败,mainAppId: {}, 错误信息: {}", appId, e.getMessage());
+        }
+        return result;
+    }
+
+    private String createCourseLinkByMiniApp(Date sendTime, Long courseId, Long videoId,
+                                             QwUser qwUser, Long externalId) {
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(qwUser.getCompanyId());
+        link.setQwUserId(qwUser.getId());
+        link.setCompanyUserId(qwUser.getCompanyUserId());
+        link.setVideoId(videoId);
+        link.setCorpId(qwUser.getCorpId());
+        link.setCourseId(courseId);
+        link.setQwExternalId(externalId);
+        link.setIsRoom(1);
+        link.setUNo(UUID.randomUUID().toString());
+        link.setLinkType(3);
+
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)){
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        }else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link,courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+
+        String realLinkFull = courseRealLink + courseJson;
+
+        link.setRealLink(realLinkFull);
+
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(0);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+
+
+        return link.getRealLink();
+    }
+
+
+
+    private void reissueCourse(String appId,Integer type,List<FsCourseWatchLog> watchLogs,CourseConfig config) {
+        List<QwSopLogs> sopLogsList = new ArrayList<>();
+        for (FsCourseWatchLog watchLog : watchLogs) {
+            try {
+                FsUserCourse userCourse = userCourseMapper.selectFsUserCourseByCourseId(watchLog.getCourseId());
+                if (userCourse==null) {
+                    throw new CustomException("课程不存在,请检查");
+                }
+
+                FsUserCourseVideo courseVideo = courseVideoMapper.selectFsUserCourseVideoByVideoId(watchLog.getVideoId());
+                if (courseVideo==null) {
+                    throw new CustomException("视频不存在,请检查");
+                }
+
+                QwUser qwUser = qwUserService.selectQwUserById(watchLog.getQwUserId());
+                if (qwUser==null||qwUser.getCompanyId()==null||qwUser.getCompanyUserId()==null){
+                    throw new CustomException("用户不存在,请检查");
+                }
+
+                QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactById(watchLog.getQwExternalContactId());
+                if (qwExternalContact==null) {
+                    throw new CustomException("客户不存在");
+                }
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                if (type == 1) {
+                    String linkByMiniApp = createCourseLinkByMiniApp(new Date(), watchLog.getCourseId(), watchLog.getVideoId(), qwUser, watchLog.getQwExternalContactId());
+
+                    QwSopLogs sopLogs = new QwSopLogs();
+                    sopLogs.setQwUserid(qwUser.getQwUserId());
+                    sopLogs.setExternalUserId(qwExternalContact.getExternalUserId());
+                    sopLogs.setExternalId(qwExternalContact.getId());
+                    sopLogs.setLogType(2);
+                    sopLogs.setSendStatus(3L);
+                    sopLogs.setSendTime(sdf.format(new Date()));
+                    sopLogs.setCompanyId(qwUser.getCompanyId());
+                    sopLogs.setReceivingStatus(0L);
+                    sopLogs.setSopId("miniSidebar");
+                    sopLogs.setCorpId(qwUser.getCorpId());
+                    sopLogs.setFsUserId(watchLog.getUserId());
+                    sopLogs.setSort(99999999);
+                    sopLogs.setSendType(3);
+                    sopLogs.setExternalUserName(qwExternalContact.getName());
+                    sopLogs.setQwUserKey(qwUser.getId());
+
+                    QwSopCourseFinishTempSetting setting = new QwSopCourseFinishTempSetting();
+                    List<QwSopCourseFinishTempSetting.Setting> list = new ArrayList<>();
+                    QwSopCourseFinishTempSetting.Setting st = new QwSopCourseFinishTempSetting.Setting();
+                    st.setContentType("4");
+                    st.setVideoId(watchLog.getVideoId());
+                    st.setMiniprogramAppid(appId);
+                    st.setMiniprogramTitle(courseVideo.getTitle());
+                    st.setMiniprogramPicUrl(userCourse.getImgUrl());
+                    st.setMiniprogramPage(linkByMiniApp);
+
+                    list.add(st);
+                    setting.setSetting(list);
+                    setting.setType(2);
+                    setting.setCourseType(0);
+                    setting.setCourseId(watchLog.getCourseId().intValue());
+                    setting.setVideoId(watchLog.getVideoId().intValue());
+
+                    sopLogs.setSendMiniAppId(appId);
+                    sopLogs.setContentJson(JSON.toJSONString(setting));
+                    sopLogsList.add(sopLogs);
+                } else if (type == 2) {
+                    //创建课程短链
+                    Map<String, String> feiShuH5Link = createFeiShuH5Link(qwUser.getCorpId(), new Date()
+                            , watchLog.getCourseId().intValue(), watchLog.getVideoId().intValue()
+                            , qwUser.getId(), String.valueOf(watchLog.getCompanyUserId())
+                            , String.valueOf(watchLog.getCompanyId()), qwExternalContact.getId()
+                            , config);
+                    //获取生成的课程短链码
+                    String shortCode = feiShuH5Link.get("link");
+                    //调用生成飞书注册授权链接
+                    String feiShuLink = feiShuService.getFeishuRegisterLink(watchLog.getVideoId(), watchLog.getCompanyId(), watchLog.getCourseId(), watchLog.getCompanyUserId(),shortCode);
+
+                    QwSopLogs sopLogs = new QwSopLogs();
+                    sopLogs.setQwUserid(qwUser.getQwUserId());
+                    sopLogs.setExternalUserId(qwExternalContact.getExternalUserId());
+                    sopLogs.setExternalId(qwExternalContact.getId());
+                    sopLogs.setLogType(2);
+                    sopLogs.setSendStatus(3L);
+                    sopLogs.setSendTime(sdf.format(new Date()));
+                    sopLogs.setCompanyId(qwUser.getCompanyId());
+                    sopLogs.setReceivingStatus(0L);
+                    sopLogs.setSopId("miniSidebar");
+                    sopLogs.setCorpId(qwUser.getCorpId());
+                    sopLogs.setFsUserId(watchLog.getUserId());
+                    sopLogs.setSort(99999999);
+                    sopLogs.setSendType(3);
+                    sopLogs.setExternalUserName(qwExternalContact.getName());
+                    sopLogs.setQwUserKey(qwUser.getId());
+
+                    QwSopCourseFinishTempSetting setting = new QwSopCourseFinishTempSetting();
+                    List<QwSopCourseFinishTempSetting.Setting> list = new ArrayList<>();
+                    QwSopCourseFinishTempSetting.Setting st = new QwSopCourseFinishTempSetting.Setting();
+
+                    st.setContentType("18");
+                    st.setIsBindUrl("1");
+                    st.setLinkDescribe(courseVideo.getTitle());
+                    st.setLinkTitle(userCourse.getCourseName());
+                    st.setLinkImageUrl(userCourse.getImgUrl());
+                    st.setLinkUrl(feiShuLink);
+
+                    list.add(st);
+                    setting.setSetting(list);
+                    setting.setType(2);
+                    setting.setCourseType(0);
+                    setting.setCourseId(watchLog.getCourseId().intValue());
+                    setting.setVideoId(watchLog.getVideoId().intValue());
+
+                    sopLogs.setContentJson(JSON.toJSONString(setting));
+                    sopLogsList.add(sopLogs);
+                }
+            } catch (CustomException e) {
+                logger.error("生成SOP失败,原因:{}", e.getMessage());
+            }
+        }
+        if (CollectionUtils.isNotEmpty(sopLogsList)) {
+            qwSopLogsMapper.batchInsertQwSopLogs(sopLogsList);
+        }
+    }
+
+    /**
+     * 生成课程链接
+     * */
+    private Map<String, String> createFeiShuH5Link(String corpId, Date sendTime,
+                                                   Integer courseId, Integer videoId, Long qwUserId,
+                                                   String companyUserId, String companyId, Long externalId, CourseConfig config) {
+        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
+                companyUserId, companyId, externalId, 3, null);
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = feiShuMiniAppLink + courseJson;
+        if (StringUtils.isNotEmpty(config.getRealLinkGjDomainName())) {
+            realLinkFull = config.getRealLinkGjDomainName() + realLinkFull;
+        }
+        link.setRealLink(realLinkFull);
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(0);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        Map<String, String> result = new HashMap<>();
+        result.put("url", link.getRealLink());//真实课程链接
+        result.put("link", link.getLink());//课程短链
+        return result;
+    }
+
+    public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
+                                           String companyUserId, String companyId, Long externalId, Integer type, String chatId) {
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(qwUserId);
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(corpId);
+        link.setCourseId(courseId.longValue());
+        link.setChatId(chatId);
+        link.setQwExternalId(externalId);
+        link.setLinkType(type); //小程序
+        link.setUNo(UUID.randomUUID().toString());
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+        link.setProjectCode(cloudHostProper.getProjectCode());
+
+        return link;
+    }
+
+    private Date createUpdateTime(QwSopCourseFinishTempSetting.Setting setting,Date sendTime,CourseConfig config){
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+//         使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays-1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+        return updateTime;
+    }
+}

+ 37 - 2
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -32,6 +32,8 @@ import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.fastGpt.domain.FastGptChatReplaceWords;
 import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
 import com.fs.feishu.service.FeiShuService;
+import com.fs.his.domain.FsCourseReissueConfig;
+import com.fs.his.service.IFsCourseReissueConfigService;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.*;
 import com.fs.qw.param.QwExtCourseSopWatchLog;
@@ -201,6 +203,9 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     @Autowired
     private FeiShuService feiShuService;
 
+    @Autowired
+    private IFsCourseReissueConfigService fsCourseReissueConfigService;
+
 
     @Override
     public void save(SopUserLogsInfo sopUserLogsInfo) {
@@ -602,6 +607,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
                         }
 
+                        //后台SOP发课配置
+                        createSetting(st);
                         switch (st.getContentType()) {
                             //文字和短链一起
                             case "1":
@@ -741,6 +748,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             replaceContent(st.getContentType(), st.getLinkTitle(), st::setLinkTitle, words); // 替换 linkTitle
                             replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
                         }
+                        //后台SOP发课配置
+                        createSetting(st);
                         switch (st.getContentType()) {
                             //文字和短链一起
                             case "1":
@@ -938,7 +947,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         replaceContent(st.getContentType(), st.getLinkTitle(), st::setLinkTitle, words); // 替换 linkTitle
                         replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
                     }
-
+                    //后台SOP发课配置
+                    createSetting(st);
                     switch (st.getContentType()){
                         //文字和短链一起
                         case "1":
@@ -1626,7 +1636,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 replaceContent(st.getContentType(), st.getLinkTitle(), st::setLinkTitle, words); // 替换 linkTitle
                 replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
             }
-
+            //后台SOP发课配置
+            createSetting(st);
             switch (st.getContentType()){
                 //文字和短链一起
                 case "1":
@@ -2410,4 +2421,28 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         //默认值
         return DEFAULT_APP_ID;
     }
+
+    private void createSetting(QwSopCourseFinishTempSetting.Setting st) {
+        FsCourseReissueConfig configInfoRedis = fsCourseReissueConfigService.getConfigInfoRedis();
+        if (configInfoRedis != null && configInfoRedis.getStatus() == 1) {
+
+            if (configInfoRedis.getType() == 1) {
+                if ("18".equals(st.getContentType())) {
+                    st.setMiniprogramPicUrl(st.getLinkImageUrl());
+                    st.setMiniprogramTitle(st.getLinkDescribe());
+                    st.setContentType("4");
+                }
+            }
+            if (configInfoRedis.getType() == 2) {
+                if ("4".equals(st.getContentType())) {
+                    st.setContentType("18");
+                    st.setLinkDescribe(st.getMiniprogramTitle());
+                    st.setLinkImageUrl(st.getMiniprogramPicUrl());
+                    st.setLinkTitle(st.getMiniprogramTitle());
+                    st.setIsBindUrl("1");
+                }
+            }
+        }
+    }
+
 }

+ 74 - 0
fs-service/src/main/resources/mapper/his/FsCourseReissueConfigMapper.xml

@@ -0,0 +1,74 @@
+<?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.his.mapper.FsCourseReissueConfigMapper">
+    
+    <resultMap type="FsCourseReissueConfig" id="FsCourseReissueConfigResult">
+        <result property="id"    column="id"    />
+        <result property="type"    column="type"    />
+        <result property="appId"    column="app_id"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="status"    column="status"    />
+    </resultMap>
+
+    <sql id="selectFsCourseReissueConfigVo">
+        select id, type, app_id, create_time, update_time, status from fs_course_reissue_config
+    </sql>
+
+    <select id="selectFsCourseReissueConfigList" parameterType="FsCourseReissueConfig" resultMap="FsCourseReissueConfigResult">
+        <include refid="selectFsCourseReissueConfigVo"/>
+        <where>  
+            <if test="type != null "> and type = #{type}</if>
+            <if test="appId != null  and appId != ''"> and app_id = #{appId}</if>
+            <if test="status != null "> and status = #{status}</if>
+        </where>
+    </select>
+    
+    <select id="selectFsCourseReissueConfigById" parameterType="Long" resultMap="FsCourseReissueConfigResult">
+        <include refid="selectFsCourseReissueConfigVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertFsCourseReissueConfig" parameterType="FsCourseReissueConfig" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_course_reissue_config
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="type != null">type,</if>
+            <if test="appId != null">app_id,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="status != null">status,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="type != null">#{type},</if>
+            <if test="appId != null">#{appId},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="status != null">#{status},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsCourseReissueConfig" parameterType="FsCourseReissueConfig">
+        update fs_course_reissue_config
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="type != null">type = #{type},</if>
+            <if test="appId != null">app_id = #{appId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="status != null">status = #{status},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsCourseReissueConfigById" parameterType="Long">
+        delete from fs_course_reissue_config where id = #{id}
+    </delete>
+
+    <delete id="deleteFsCourseReissueConfigByIds" parameterType="String">
+        delete from fs_course_reissue_config where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>