wangxy преди 3 дни
родител
ревизия
5473b73a0a

+ 32 - 0
fs-admin/src/main/java/com/fs/task/FeiShuFileCleanTask.java

@@ -0,0 +1,32 @@
+package com.fs.task;
+
+import com.fs.feishu.service.FeiShuService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 飞书云文档清理定时任务
+ * 每天凌晨2点删除今天0点之前创建的飞书云文档
+ */
+@Component
+@Slf4j
+public class FeiShuFileCleanTask {
+
+    @Autowired
+    private FeiShuService feiShuService;
+
+    /**
+     * 每天凌晨2点执行,清理今天之前创建的飞书云文档
+     */
+//    @Scheduled(cron = "0 0 2 * * ?")
+    public void deleteFilesBeforeToday() {
+        try {
+            log.info("飞书云文档清理 - 定时任务开始");
+            feiShuService.deleteFilesBeforeToday();
+        } catch (Exception e) {
+            log.error("飞书云文档清理 - 定时任务执行失败", e);
+        }
+    }
+}

+ 37 - 2
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.course.dto.BatchUrgeCourseTaskDTO;
 import com.fs.course.utils.WechatErrorUtil;
+import com.fs.feishu.model.FeishuLinkParam;
 import com.fs.feishu.service.FeiShuService;
 import com.fs.app.annotation.Login;
 import com.fs.app.config.ImageStorageConfig;
@@ -483,14 +484,48 @@ public class FsUserCourseVideoController extends AppBaseController {
 
     @Autowired
     private FeiShuService feiShuService;
+    @Autowired
+    private ISysConfigService configService;
 
     @Login
     @ApiOperation("获取飞书看课链接")
     @GetMapping("/getFeiShuCourseLink")
     public R getFeiShuCourseLink(@RequestParam Long companyUserId,
                                  @RequestParam Long videoId,
-                                 @RequestParam Long periodId) {
-        return R.ok().put("data", feiShuService.getFeishuCourseLink(companyUserId, videoId, periodId));
+                                 @RequestParam Long periodId,
+                                 @RequestParam(required = false) Long companyId,
+                                 @RequestParam(required = false) Long courseId,
+                                 @RequestParam(required = false) Long projectId,
+                                 @RequestParam(required = false) Long id) {
+        // 读取配置判断是否启用飞书新跳转
+        Boolean enableFeishuNewLink = null;
+        try {
+            String json = configService.selectConfigByKey("course.config");
+            if (StringUtils.isNotBlank(json)) {
+                com.fs.course.config.CourseConfig config = cn.hutool.json.JSONUtil.toBean(json, com.fs.course.config.CourseConfig.class);
+                enableFeishuNewLink = config.getEnableFeishuNewLink();
+            }
+        } catch (Exception e) {
+            log.warn("读取飞书新跳转配置失败,使用老方法", e);
+        }
+
+        if (Boolean.TRUE.equals(enableFeishuNewLink)) {
+            FeishuLinkParam param = new FeishuLinkParam();
+            param.setCompanyId(companyId);
+            param.setCompanyUserId(companyUserId);
+            param.setCourseId(courseId);
+            param.setVideoId(videoId);
+            param.setPeriodId(periodId);
+            param.setProjectId(projectId);
+            param.setId(id);
+            String link = feiShuService.getFeishuRegisterLink(param);
+            log.info("链接{}", link);
+            return R.ok().put("data", link);
+        }
+        // 默认使用老方法
+        String oldLink = feiShuService.getFeishuCourseLink(companyUserId, videoId, periodId);
+        log.info("老链接{}", oldLink);
+        return R.ok().put("data", oldLink);
     }
 
     @ApiOperation("会员批量发送课程消息")

+ 7 - 0
fs-company-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -26,6 +26,7 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
     @Autowired
     RedisCache redisCache;
     public static final String USER_KEY = "userId";
+    private static final String SKIP_AUTH_HEADER = "X-Skip-Auth";
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
@@ -40,6 +41,12 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
             return true;
         }
 
+        // 请求头存在跳过认证标识,直接放行
+        String skipAuth = request.getHeader(SKIP_AUTH_HEADER);
+        if (StringUtils.isNotEmpty(skipAuth)) {
+            return true;
+        }
+
         //获取用户凭证
         String token = request.getHeader(jwtUtils.getHeader());
         if(StringUtils.isBlank(token)){

+ 2 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -269,7 +269,8 @@ public interface CompanyMapper
     @Select({"<script> " +
             "SELECT company_id as companyId, company_name as companyName " +
             "FROM company " +
-            "WHERE status = 1 " +
+            "WHERE status =1 " +
+            "AND is_del = 0 " +
             "<if test=\"companyId != null\"> " +
             "  AND company_id = #{companyId} " +
             "</if> " +

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

@@ -20,6 +20,8 @@ public class CourseConfig implements Serializable {
     private Integer defaultLine;//默认看课线路
     private String realLinkDomainName;//真链域名
     private String feishuLinkDomainName;//飞书看课域名
+    private String feishuNewLinkDomainName;//新飞书看课域名
+    private Boolean enableFeishuNewLink;//是否启用飞书新跳转
     private String authDomainName;//网页授权域名
     private String mpAppId;//看课公众号APPID
     private String registerDomainName;//注册域名

+ 5 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java

@@ -42,5 +42,10 @@ public class FsCourseSendRewardUParam implements Serializable
 
     private Long  activityId;
 
+    /**
+     * 是否是飞书
+     */
+    private Boolean isFeiShu=false;
+
 
 }

+ 8 - 7
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -1470,13 +1470,13 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
         if (log.getLogType() != 2) {
             return R.error("未完课");
         }
-
-        FsCourseAnswerLogs rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideo(param.getVideoId(), param.getUserId(), param.getQwUserId());
-        if (rightLog == null) {
-            logger.error("未答题:{}", param.getUserId());
-            return R.error("未答题");
+        if(!param.getIsFeiShu()){
+            FsCourseAnswerLogs rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideo(param.getVideoId(), param.getUserId(), param.getQwUserId());
+            if (rightLog == null) {
+                logger.error("未答题:{}", param.getUserId());
+                return R.error("未答题");
+            }
         }
-
         if (log.getRewardType() != null) {
             if (log.getRewardType() == 1) {
                 FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
@@ -1881,7 +1881,8 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             // 更新观看记录的奖励类
 //            log.setRewardType(config.getRewardType());
 //            courseWatchLogMapper.updateFsCourseWatchLog(log);
-            return R.ok("答题成功,请联系群主");
+            String msg=param.getIsFeiShu()?"奖励发放成功,请联系客服":"答题成功,请联系群主";
+            return R.ok(msg);
         }
 
     }

+ 16 - 0
fs-service/src/main/java/com/fs/feishu/model/FeishuFileDTO.java

@@ -0,0 +1,16 @@
+package com.fs.feishu.model;
+
+import lombok.Data;
+
+@Data
+public class FeishuFileDTO {
+
+    private String name;
+    private String token;
+    private String type;
+    private String url;
+    private String ownerId;
+    private String parentToken;
+    private String createdTime;
+    private String modifiedTime;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/feishu/model/FeishuLinkParam.java

@@ -0,0 +1,38 @@
+package com.fs.feishu.model;
+
+import lombok.Data;
+
+
+import java.io.Serializable;
+
+/**
+ * 飞书链接参数
+ */
+@Data
+public class FeishuLinkParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 公司用户ID */
+    private Long companyUserId;
+
+    /** 课程ID */
+    private Long courseId;
+
+    /** 视频ID */
+    private Long videoId;
+
+    /** 期数ID */
+    private Long periodId;
+
+    /** 项目ID */
+    private Long projectId;
+
+    private  Long userId;
+
+    private  Long id;
+
+}

+ 284 - 0
fs-service/src/main/java/com/fs/feishu/service/FeiShuService.java

@@ -2,6 +2,8 @@ package com.fs.feishu.service;
 
 import cn.hutool.json.JSONUtil;
 import com.fs.feishu.config.FeiShuConfig;
+import com.fs.feishu.model.FeishuFileDTO;
+import com.fs.feishu.model.FeishuLinkParam;
 import com.fs.feishu.util.SecureTokenUtil;
 import com.fs.common.exception.CustomException;
 import com.fs.course.config.CourseConfig;
@@ -10,13 +12,23 @@ import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.system.service.ISysConfigService;
 import com.lark.oapi.Client;
 import com.lark.oapi.service.docx.v1.model.*;
+import com.lark.oapi.service.drive.v1.model.DeleteFileReq;
+import com.lark.oapi.service.drive.v1.model.DeleteFileResp;
+import com.lark.oapi.service.drive.v1.model.ListFileReq;
+import com.lark.oapi.service.drive.v1.model.ListFileResp;
 import com.lark.oapi.service.drive.v2.model.PatchPermissionPublicReq;
 import com.lark.oapi.service.drive.v2.model.PermissionPublic;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import javax.annotation.PostConstruct;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.*;
+
 import org.springframework.stereotype.Component;
 
 @Component
+@Slf4j
 public class FeiShuService {
 
     private final String COURSE_PATH = "/feishu/pages_course/videovip?token=%s";
@@ -68,6 +80,73 @@ public class FeiShuService {
         }
     }
 
+    /**
+     * 复制飞书看课新链接
+     */
+    public String getFeishuCourseNewLink(FeishuLinkParam param) {
+        if (param == null || param.getCompanyUserId() == null || param.getCompanyId() == null) {
+            throw new CustomException("参数不能为空");
+        }
+
+        FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+        if (userCourseVideo == null) {
+            throw new CustomException("视频不存在: " + param.getVideoId());
+        }
+        try {
+            // 创建云文档
+            String documentId = createDocument(userCourseVideo.getTitle());
+
+            // 拼接新看课url
+            String url = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(), param.getCourseId(), param.getVideoId(), param.getPeriodId(), param.getProjectId(),param.getUserId(),param.getId());
+
+            // 创建iframe块
+            createIframeBlock(documentId, url);
+
+            // 更新文档权限
+            changeDocumentPermissions(documentId);
+
+            // 返回飞书看课链接
+            return "https://www.feishu.cn/docx/" + documentId;
+        } catch (Exception e) {
+            throw new CustomException("创建飞书课程链接失败: " + e.getMessage(), e);
+        }
+    }
+
+
+    /**
+     * 复制飞书注册链接
+     */
+    public String getFeishuRegisterLink(FeishuLinkParam param) {
+        if (param == null || param.getCompanyUserId() == null || param.getCompanyId() == null) {
+            throw new CustomException("参数不能为空");
+        }
+
+        FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+        if (userCourseVideo == null) {
+            throw new CustomException("视频不存在: " + param.getVideoId());
+        }
+
+        try {
+            // 创建云文档
+            String documentId = createDocument(userCourseVideo.getTitle() + "-注册");
+
+            // 拼接授权url
+            String authUrl = buildAuthLink(param.getCompanyId(), param.getCompanyUserId(),
+                    param.getCourseId(), param.getVideoId(), param.getPeriodId(), param.getProjectId(),param.getId());
+
+            // 创建关注公众号 text + link block
+            createTextLinkBlock(documentId, authUrl);
+            // 更新文档权限
+            changeDocumentPermissions(documentId);
+
+            // 返回飞书看课链接
+            return "https://www.feishu.cn/docx/" + documentId;
+        } catch (Exception e) {
+            throw new CustomException("创建飞书注册链接失败: " + e.getMessage(), e);
+        }
+    }
+
+
     /**
      * 创建云文档
      */
@@ -95,6 +174,63 @@ public class FeiShuService {
         return config.getFeishuLinkDomainName() + String.format(COURSE_PATH, token);
     }
 
+    /**
+     * 拼接看课url(课程参数JSON方式,适配飞书iframe,双重URL编码)
+     */
+    private String buildCourseNewLink(Long companyId, Long companyUserId, Long courseId, Long videoId, Long periodId, Long projectId,Long userId,Long id) {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+        // 构造课程参数 JSON
+        Map<String, Object> courseParam = new LinkedHashMap<>();
+        courseParam.put("periodId", periodId);
+        courseParam.put("companyUserId", companyUserId);
+        courseParam.put("videoId", videoId);
+        courseParam.put("projectId", projectId);
+        courseParam.put("companyId", companyId);
+        courseParam.put("courseId", courseId);
+        courseParam.put("id", id);
+        String courseJson = JSONUtil.toJsonStr(courseParam);
+        try {
+            // 第一次编码:对JSON进行URL编码
+            String encodedJson = URLEncoder.encode(courseJson, "UTF-8");
+            // 拼接完整URL(userId与course参数同级)
+            String baseUrl = config.getFeishuNewLinkDomainName();
+            String fullUrl = baseUrl + "/feishuCourse/pages_course/videovip?course=" + encodedJson + "&userId=" +userId;
+            // 第二次编码:对整个URL进行URL编码(适配飞书iframe创建要求)
+            return URLEncoder.encode(fullUrl, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new CustomException("看课URL拼接失败", e);
+        }
+    }
+
+    /**
+     * 拼接授权url(课程参数JSON方式,适配飞书iframe,双重URL编码)
+     */
+    private String buildAuthLink(Long companyId, Long companyUserId, Long courseId, Long videoId, Long periodId, Long projectId,Long id) {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+        Map<String, Object> authParam = new LinkedHashMap<>();
+        authParam.put("periodId", periodId);
+        authParam.put("companyUserId", companyUserId);
+        authParam.put("videoId", videoId);
+        authParam.put("projectId", projectId);
+        authParam.put("companyId", companyId);
+        authParam.put("courseId", courseId);
+        authParam.put("id", id);
+        String authJson = JSONUtil.toJsonStr(authParam);
+
+        try {
+            String encodedJson = URLEncoder.encode(authJson, "UTF-8");
+            String baseUrl =config.getFeishuNewLinkDomainName();
+            String fullUrl = baseUrl + "/feishuCourse/pages_course/wxauth?course=" + encodedJson;
+            return URLEncoder.encode(fullUrl, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new CustomException("授权URL拼接失败", e);
+        }
+    }
+
     /**
      * 创建iframe块
      */
@@ -123,6 +259,86 @@ public class FeiShuService {
         client.docx().v1().documentBlockChildren().create(req);
     }
 
+    /**
+     * 创建 text + link block
+     */
+    private void createTextLinkBlock(String documentId, String url) throws Exception {
+
+        CreateDocumentBlockChildrenReq req = CreateDocumentBlockChildrenReq.newBuilder()
+                .documentId(documentId)
+                .blockId(documentId)
+                .documentRevisionId(-1)
+                .createDocumentBlockChildrenReqBody(
+                    CreateDocumentBlockChildrenReqBody.newBuilder()
+                        .index(0)
+                        .children(new Block[] {
+                            Block.newBuilder()
+                                .blockType(2)
+                                .text(Text.newBuilder()
+                                    .elements(new TextElement[] {
+                                        TextElement.newBuilder()
+                                            .textRun(TextRun.newBuilder()
+                                                .content("观看课程")
+                                                .textElementStyle(TextElementStyle.newBuilder()
+                                                    .link(Link.newBuilder()
+                                                        .url(url)
+                                                        .build())
+                                                    .build())
+                                                .build())
+                                            .build()
+                                    })
+                                    .build())
+                                .build()
+                        })
+                        .build())
+                .build();
+
+        client.docx().v1().documentBlockChildren().create(req);
+    }
+
+    /**
+     * 获取飞书云文档列表
+     */
+    public List<FeishuFileDTO> listFiles(int pageSize) throws Exception {
+
+        ListFileReq req = ListFileReq.newBuilder()
+                .pageSize(pageSize)
+                .orderBy("EditedTime")
+                .direction("DESC")
+                .build();
+
+        ListFileResp resp = client.drive().v1().file().list(req);
+
+        if (!resp.success()) {
+            throw new RuntimeException(
+                    "Feishu error code:" + resp.getCode()
+                            + ", msg:" + resp.getMsg()
+                            + ", reqId:" + resp.getRequestId()
+            );
+        }
+
+        List<FeishuFileDTO> result = new ArrayList<>();
+
+        if (resp.getData() != null && resp.getData().getFiles() != null) {
+            com.lark.oapi.service.drive.v1.model.File[] files = resp.getData().getFiles();
+            Arrays.stream(files).forEach(file -> {
+                FeishuFileDTO dto = new FeishuFileDTO();
+                dto.setName(file.getName());
+                dto.setToken(file.getToken());
+                dto.setType(file.getType());
+                dto.setUrl(file.getUrl());
+                dto.setOwnerId(file.getOwnerId());
+                dto.setParentToken(file.getParentToken());
+                dto.setCreatedTime(file.getCreatedTime());
+                dto.setModifiedTime(file.getModifiedTime());
+
+                result.add(dto);
+            });
+        }
+
+        return result;
+    }
+
     /**
      * 修改文档权限
      */
@@ -144,4 +360,72 @@ public class FeiShuService {
         // 发起请求
         client.drive().v2().permissionPublic().patch(req);
     }
+
+    /**
+     * 根据 token 删除飞书文档
+     *
+     * @param fileToken 文档token
+     * @param type 文档类型(docx / sheet / bitable / file 等)
+     */
+    public boolean deleteFile(String fileToken, String type) throws Exception {
+
+        DeleteFileReq req = DeleteFileReq.newBuilder()
+                .fileToken(fileToken)
+                .type(type)
+                .build();
+
+        DeleteFileResp resp = client.drive().v1().file().delete(req);
+
+        if (!resp.success()) {
+            throw new RuntimeException(
+                    "Feishu delete failed, code=" + resp.getCode()
+                            + ", msg=" + resp.getMsg()
+                            + ", reqId=" + resp.getRequestId()
+            );
+        }
+
+        return true;
+    }
+
+    /**
+     * 删除今天之前创建的飞书云文档
+     * 1. 查询云文档列表
+     * 2. 遍历比较创建时间(飞书返回秒级时间戳)
+     * 3. 创建时间在今天 00:00:00 之前的执行删除
+     */
+    public void deleteFilesBeforeToday() throws Exception {
+        // 今天 00:00:00 的时间戳(秒)
+        Calendar today = Calendar.getInstance();
+        today.set(Calendar.HOUR_OF_DAY, 0);
+        today.set(Calendar.MINUTE, 0);
+        today.set(Calendar.SECOND, 0);
+        today.set(Calendar.MILLISECOND, 0);
+        long todayStartSec = today.getTimeInMillis() / 1000;
+
+        List<FeishuFileDTO> files = listFiles(200);
+        int total = files.size();
+        int deleted = 0;
+        for (FeishuFileDTO file : files) {
+            try {
+                // 飞书返回的 createdTime 为秒级时间戳
+                long createdTime = Long.parseLong(file.getCreatedTime());
+                if (createdTime < todayStartSec) {
+                    String type = file.getType();
+                    // wiki 类型无法直接删除,跳过
+                    if ("wiki".equalsIgnoreCase(type)) {
+                        continue;
+                    }
+                    boolean ok = deleteFile(file.getToken(), type);
+                    if (ok) {
+                        deleted++;
+                        log.info("已删除: {} | type={} | createdTime={}", file.getName(), type, file.getCreatedTime());
+                    }
+                }
+            } catch (Exception e) {
+                log.error("删除失败: {} | token={} | err={}", file.getName(), file.getToken(), e.getMessage());
+            }
+        }
+        log.info("飞书云文档清理完成 | 列表总数={} | 删除成功={}", total, deleted);
+    }
+
 }

+ 19 - 2
fs-service/src/main/java/com/fs/feishu/util/SecureTokenUtil.java

@@ -27,6 +27,18 @@ public class SecureTokenUtil {
         }
     }
 
+    /**
+     * 生成安全令牌
+     */
+    public static String generateTokenUser(Long companyUserId, Long videoId, Long periodId, Long timestamp,Long userId) {
+        try {
+            String dataBuilder = companyUserId + ":" + videoId + ":" + periodId + ":"+userId+":" + timestamp;
+            return encryptData(dataBuilder);
+        } catch (Exception e) {
+            throw new RuntimeException("飞书令牌生成失败", e);
+        }
+    }
+
     /**
      * 加密数据
      */
@@ -39,7 +51,7 @@ public class SecureTokenUtil {
     }
     
     /**
-     * 解析安全令牌
+     * 解析安全令牌(兼容4字段和5字段)
      */
     public static Map<String, Object> parseToken(String token) {
         try {
@@ -50,7 +62,12 @@ public class SecureTokenUtil {
             result.put("companyUserId", Long.parseLong(parts[0]));
             result.put("videoId", Long.parseLong(parts[1]));
             result.put("periodId", Long.parseLong(parts[2]));
-            result.put("timestamp", Long.parseLong(parts[3]));
+            if (parts.length == 5) {
+                result.put("userId", Long.parseLong(parts[3]));
+                result.put("timestamp", Long.parseLong(parts[4]));
+            } else {
+                result.put("timestamp", Long.parseLong(parts[3]));
+            }
 
             return result;
         } catch (Exception e) {

+ 4 - 3
fs-service/src/main/resources/application-config-druid-hdt.yml

@@ -104,9 +104,10 @@ wx_miniapp_temp:
 
 # 飞书
 feishu:
-  appId: "cli_aaa27d4aed395bee"
-  appSecret: "DYh8JVjwnoGT4p4ykHjK7AiA6AzH1x6e"
-
+#  appId: "cli_aaa27d4aed395bee"
+#  appSecret: "DYh8JVjwnoGT4p4ykHjK7AiA6AzH1x6e"
+  appId: "cli_aac988920f785bd0"
+  appSecret: "B5SqOpNzJ5jsMdcZkwwSPgkON1ZGK7R3"
 # sip外呼配置
 sip:
   call:

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

@@ -1562,6 +1562,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         c.company_name AS companyName
         FROM company c
         <where>
+            status=1 and is_del=0
             <if test="companyId != null and companyId != ''">
                 AND c.company_id = #{companyId}
             </if>

+ 23 - 4
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -20,6 +20,8 @@ import com.fs.course.vo.CourseCheckinResultVO;
 import com.fs.course.vo.FsUserCourseVideoH5VO;
 import com.fs.course.vo.TodayUnfinishedCourseVO;
 import com.fs.course.vo.newfs.FsUserCourseVideoLinkDetailsVO;
+import com.fs.feishu.model.FeishuLinkParam;
+import com.fs.feishu.service.FeiShuService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.dto.FsUserBindSalesParamDTO;
 import com.fs.his.enums.FsUserOperationEnum;
@@ -75,6 +77,9 @@ public class CourseFsUserController extends AppBaseController {
     @Autowired
     private OpenIMService openIMService;
 
+    @Autowired
+    private FeiShuService feiShuService;
+
 
     @ApiOperation("短信H5看课-判断是否关联销售")
     @PostMapping("/sms/isAddKf")
@@ -148,7 +153,9 @@ public class CourseFsUserController extends AppBaseController {
     @GetMapping("/videoDetails")
     @UserOperationLog(operationType = FsUserOperationEnum.STUDY)
     public ResponseResult<FsUserCourseVideoLinkDetailsVO>   getCourseVideoDetails(FsUserCourseVideoLinkParam param) {
-        param.setFsUserId(Long.parseLong(getUserId()));
+        if(param.getFsUserId()==null){
+            param.setFsUserId(Long.parseLong(getUserId()));
+        }
         return courseVideoService.getLinkCourseVideoDetails(param);
     }
 
@@ -168,7 +175,9 @@ public class CourseFsUserController extends AppBaseController {
     @PostMapping("/updateWatchDuration")
     @Login
     public R updateWatchDuration(@RequestBody FsUserCourseVideoUParam param) {
-        param.setUserId(Long.parseLong(getUserId()));
+        if(param.getUserId()==null){
+            param.setUserId(Long.parseLong(getUserId()));
+        }
         return courseVideoService.updateWatchDurationWx(param);
     }
 
@@ -177,7 +186,9 @@ public class CourseFsUserController extends AppBaseController {
     @PostMapping("/getInternetTraffic")
     @Login
     public R getInternetTraffic(@RequestBody FsUserCourseVideoFinishUParam param) {
-        param.setUserId(Long.parseLong(getUserId()));
+        if(param.getUserId()==null){
+            param.setUserId(Long.parseLong(getUserId()));
+        }
         return courseVideoService.getInternetTraffic(param);
     }
 
@@ -195,7 +206,9 @@ public class CourseFsUserController extends AppBaseController {
     @PostMapping("/getPublicCourseInternetTraffic")
     @Login
     public R getPublicCourseInternetTraffic(@RequestBody FsUserCourseVideoFinishUParam param ){
-        param.setUserId(Long.parseLong(getUserId()));
+        if(param.getUserId()==null){
+            param.setUserId(Long.parseLong(getUserId()));
+        }
         return courseVideoService.getPublicCourseInternetTraffic(param);
     }
 
@@ -321,4 +334,10 @@ public class CourseFsUserController extends AppBaseController {
         return openIMService.getSmsShortLinkDetail(code);
     }
 
+
+    @ApiOperation("获取飞书看课新链接")
+    @PostMapping("/getFeiShuCourseNewLink")
+    public R getFeiShuCourseNewLink(@RequestBody FeishuLinkParam param) {
+        return R.ok().put("data", feiShuService.getFeishuCourseNewLink(param));
+    }
 }

+ 7 - 0
fs-user-app/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -30,6 +30,7 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
     @Autowired
     FsUserMapper fsUserMapper;
     public static final String USER_KEY = "userId";
+    private static final String SKIP_AUTH_HEADER = "X-Skip-Auth";
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
@@ -44,6 +45,12 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
             return true;
         }
 
+        // 请求头存在跳过认证标识,直接放行
+        String skipAuth = request.getHeader(SKIP_AUTH_HEADER);
+        if (StringUtils.isNotEmpty(skipAuth)) {
+            return true;
+        }
+
         //获取用户凭证
         String token = request.getHeader(jwtUtils.getHeader());
         if(StringUtils.isBlank(token)){

+ 16 - 5
fs-user-app/src/main/java/com/fs/framework/aspectj/UserOperationLogAspect.java

@@ -51,6 +51,7 @@ public class UserOperationLogAspect {
 
     private final ObjectMapper objectMapper = new ObjectMapper();
     private static final ThreadLocal<FsUserOperationLog> LOG_HOLDER = new ThreadLocal<>();
+    private static final String SKIP_AUTH_HEADER = "X-Skip-Auth";
 
     @Pointcut("@annotation(com.fs.app.annotation.UserOperationLog)")
     public void logPointcut() {}
@@ -115,11 +116,21 @@ public class UserOperationLogAspect {
             operationLog.setOperationType(annotation.operationType().getLabel());
 
             //用户
-            Long userId =null;
-            try {
-                userId = Long.valueOf(jwtUtils.getClaimByToken(ServletUtils.getRequest().getHeader("APPToken")).getSubject().toString());
-            } catch (Exception ie){
-                log.info("获取用户id失败");
+            Long userId = null;
+            // 优先从 X-Skip-Auth 请求头取 userId(跳过token校验场景)
+            String skipAuth = ServletUtils.getRequest().getHeader(SKIP_AUTH_HEADER);
+            if (StringUtils.isNotEmpty(skipAuth)) {
+                try {
+                    userId = Long.valueOf(skipAuth);
+                } catch (NumberFormatException ne) {
+                    log.info("X-Skip-Auth 请求头不是有效的userId: {}", skipAuth);
+                }
+            } else {
+                try {
+                    userId = Long.valueOf(jwtUtils.getClaimByToken(ServletUtils.getRequest().getHeader("APPToken")).getSubject().toString());
+                } catch (Exception ie) {
+                    log.info("获取用户id失败");
+                }
             }
             if (userId == null) {
                 LOG_HOLDER.set(operationLog);