|
|
@@ -1,14 +1,13 @@
|
|
|
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;
|
|
|
import com.fs.course.domain.FsUserCourseVideo;
|
|
|
import com.fs.course.mapper.FsUserCourseVideoMapper;
|
|
|
+import com.fs.feishu.config.FeiShuConfig;
|
|
|
+import com.fs.feishu.model.FeishuFileDTO;
|
|
|
+import com.fs.feishu.model.FeishuLinkParam;
|
|
|
import com.fs.system.service.ISysConfigService;
|
|
|
import com.lark.oapi.Client;
|
|
|
import com.lark.oapi.service.docx.v1.model.*;
|
|
|
@@ -21,23 +20,27 @@ import com.lark.oapi.service.drive.v2.model.PermissionPublic;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
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/video?token=%s";
|
|
|
-
|
|
|
@Autowired
|
|
|
private FeiShuConfig feishuConfig;
|
|
|
+
|
|
|
@Autowired
|
|
|
private ISysConfigService configService;
|
|
|
+
|
|
|
@Autowired
|
|
|
private FsUserCourseVideoMapper videoMapper;
|
|
|
|
|
|
@@ -45,221 +48,197 @@ public class FeiShuService {
|
|
|
|
|
|
@PostConstruct
|
|
|
public void init() {
|
|
|
- this.client = Client.newBuilder(feishuConfig.getAppId(), feishuConfig.getAppSecret()).logReqAtDebug(true).build();
|
|
|
+ this.client = Client.newBuilder(feishuConfig.getAppId(), feishuConfig.getAppSecret())
|
|
|
+ .logReqAtDebug(true)
|
|
|
+ .build();
|
|
|
}
|
|
|
|
|
|
+ // ==================== 对外业务方法 ====================
|
|
|
+
|
|
|
/**
|
|
|
- * 001 生成飞书初始链接
|
|
|
+ * 生成注册授权页飞书文档(第一阶段)
|
|
|
*/
|
|
|
- public String getFeishuRegisterLink(Long videoId, Long companyId, Long courseId,Long companyUserId) {
|
|
|
- // 读取配置判断是否启用飞书新跳转
|
|
|
- String json = configService.selectConfigByKey("course.config");
|
|
|
- if (StringUtils.isBlank(json)) {
|
|
|
- throw new CustomException("飞书看课配置为空");
|
|
|
- }
|
|
|
- CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
|
|
|
- Boolean enableFeishuNewLink = config.getEnableFeishuNewLink();
|
|
|
- if (!Boolean.TRUE.equals(enableFeishuNewLink)) {
|
|
|
- throw new CustomException("未开启飞书看课");
|
|
|
- }
|
|
|
-
|
|
|
- // 构建参数
|
|
|
- FeishuLinkParam param = new FeishuLinkParam();
|
|
|
- param.setCompanyId(companyId);
|
|
|
- param.setCompanyUserId(companyUserId);
|
|
|
- param.setCourseId(courseId);
|
|
|
- param.setVideoId(videoId);
|
|
|
+ public String getFeishuRegisterLink(Long videoId, Long companyId, Long courseId,
|
|
|
+ Long companyUserId, String shortCode) {
|
|
|
+ CourseConfig config = getCourseConfig();
|
|
|
+ FsUserCourseVideo video = getVideoEntity(videoId);
|
|
|
|
|
|
- FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
|
|
|
- if (userCourseVideo == null) {
|
|
|
- throw new CustomException("视频不存在: " + param.getVideoId());
|
|
|
- }
|
|
|
+ // 1. 构建授权URL
|
|
|
+ String authUrl = buildAuthUrl(config, companyId, companyUserId, courseId, videoId, shortCode);
|
|
|
|
|
|
- try {
|
|
|
- // 创建云文档
|
|
|
- String documentId = createDocument(userCourseVideo.getTitle() + "-注册");
|
|
|
-
|
|
|
- // 拼接授权url
|
|
|
- String authUrl = buildAuthLink(param.getCompanyId(), param.getCompanyUserId(),
|
|
|
- param.getCourseId(), param.getVideoId());
|
|
|
-
|
|
|
- // 创建关注公众号 text + link block
|
|
|
- createTextLinkBlock(documentId, authUrl);
|
|
|
- // 更新文档权限
|
|
|
- changeDocumentPermissions(documentId);
|
|
|
+ // 2. 构造文档内容
|
|
|
+ Block linkBlock = buildTextLinkBlock(authUrl, "观看课程");
|
|
|
|
|
|
- // 返回飞书看课链接
|
|
|
- return "https://www.feishu.cn/docx/" + documentId;
|
|
|
- } catch (Exception e) {
|
|
|
- throw new CustomException("创建飞书注册链接失败: " + e.getMessage(), e);
|
|
|
- }
|
|
|
+ // 3. 创建文档并返回链接
|
|
|
+ String documentId = createFeishuDocument(video.getTitle() + "-注册", new Block[]{linkBlock});
|
|
|
+ return "https://www.feishu.cn/docx/" + documentId;
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
/**
|
|
|
- * 002生成飞书课程链接
|
|
|
+ * 生成课程播放页飞书文档(第二阶段)
|
|
|
*/
|
|
|
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());
|
|
|
+ CourseConfig config = getCourseConfig();
|
|
|
+ FsUserCourseVideo video = getVideoEntity(param.getVideoId());
|
|
|
|
|
|
- // 拼接新看课url
|
|
|
- String url = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(), param.getCourseId(), param.getVideoId(),param.getUserId());
|
|
|
+ // 1. 构建课程播放URL(已双重编码)
|
|
|
+ String courseUrl = buildCourseUrl(config, param);
|
|
|
|
|
|
- // 创建iframe块
|
|
|
- createIframeBlock(documentId, url);
|
|
|
+ // 2. 构造文档内容(一个iframe块)
|
|
|
+ Block iframeBlock = buildIframeBlock(courseUrl);
|
|
|
|
|
|
- // 更新文档权限
|
|
|
- changeDocumentPermissions(documentId);
|
|
|
+ // 3. 创建文档并返回链接
|
|
|
+ String documentId = createFeishuDocument(video.getTitle(), new Block[]{iframeBlock});
|
|
|
+ return "https://www.feishu.cn/docx/" + documentId;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 核心文档创建流程 ====================
|
|
|
|
|
|
- // 返回飞书看课链接
|
|
|
- return "https://www.feishu.cn/docx/" + documentId;
|
|
|
+ /**
|
|
|
+ * 通用文档创建流程:创建文档 → 添加子块 → 设置权限
|
|
|
+ */
|
|
|
+ private String createFeishuDocument(String title, Block[] children) {
|
|
|
+ try {
|
|
|
+ String documentId = createDocument(title);
|
|
|
+ addDocumentChildren(documentId, children);
|
|
|
+ changeDocumentPermissions(documentId);
|
|
|
+ return documentId;
|
|
|
} catch (Exception e) {
|
|
|
- throw new CustomException("创建飞书课程链接失败: " + e.getMessage(), e);
|
|
|
+ throw new CustomException("创建飞书文档失败: " + e.getMessage(), e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ==================== 文档内容块构建 ====================
|
|
|
+
|
|
|
+ private Block buildTextLinkBlock(String url, String text) {
|
|
|
+ return Block.newBuilder()
|
|
|
+ .blockType(2) // 文本块
|
|
|
+ .text(Text.newBuilder()
|
|
|
+ .elements(new TextElement[]{
|
|
|
+ TextElement.newBuilder()
|
|
|
+ .textRun(TextRun.newBuilder()
|
|
|
+ .content(text)
|
|
|
+ .textElementStyle(TextElementStyle.newBuilder()
|
|
|
+ .link(Link.newBuilder().url(url).build())
|
|
|
+ .build())
|
|
|
+ .build())
|
|
|
+ .build()
|
|
|
+ })
|
|
|
+ .build())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Block buildIframeBlock(String url) {
|
|
|
+ return Block.newBuilder()
|
|
|
+ .blockType(26) // iframe块
|
|
|
+ .iframe(Iframe.newBuilder()
|
|
|
+ .component(IframeComponent.newBuilder()
|
|
|
+ .iframeType(1)
|
|
|
+ .url(url)
|
|
|
+ .build())
|
|
|
+ .build())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 飞书API操作封装 ====================
|
|
|
|
|
|
- /**
|
|
|
- * 创建云文档
|
|
|
- */
|
|
|
private String createDocument(String title) throws Exception {
|
|
|
CreateDocumentReq req = CreateDocumentReq.newBuilder()
|
|
|
.createDocumentReqBody(CreateDocumentReqBody.newBuilder().title(title).build())
|
|
|
.build();
|
|
|
- CreateDocumentResp resp = client.docx().v1().document().create(req);
|
|
|
+ return client.docx().v1().document().create(req)
|
|
|
+ .getData().getDocument().getDocumentId();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addDocumentChildren(String documentId, Block[] children) throws Exception {
|
|
|
+ CreateDocumentBlockChildrenReq req = CreateDocumentBlockChildrenReq.newBuilder()
|
|
|
+ .documentId(documentId)
|
|
|
+ .blockId(documentId)
|
|
|
+ .documentRevisionId(-1)
|
|
|
+ .createDocumentBlockChildrenReqBody(
|
|
|
+ CreateDocumentBlockChildrenReqBody.newBuilder()
|
|
|
+ .index(0)
|
|
|
+ .children(children)
|
|
|
+ .build()
|
|
|
+ )
|
|
|
+ .build();
|
|
|
+ client.docx().v1().documentBlockChildren().create(req);
|
|
|
+ }
|
|
|
|
|
|
- CreateDocumentRespBody data = resp.getData();
|
|
|
- return data.getDocument().getDocumentId();
|
|
|
+ // ==================== URL构建工具 ====================
|
|
|
+
|
|
|
+ private String buildAuthUrl(CourseConfig config, Long companyId, Long companyUserId,
|
|
|
+ Long courseId, Long videoId, String shortCode) {
|
|
|
+ Map<String, Object> params = new LinkedHashMap<>();
|
|
|
+ params.put("companyUserId", companyUserId);
|
|
|
+ params.put("videoId", videoId);
|
|
|
+ params.put("companyId", companyId);
|
|
|
+ params.put("courseId", courseId);
|
|
|
+ params.put("link", shortCode);
|
|
|
+ String fullUrl = config.getFeishuAuthDomain()
|
|
|
+ + "/feishuCourse/pages_course/wxauth?course=" + encodeJsonParam(params);
|
|
|
+ return encodeUrl(fullUrl);
|
|
|
}
|
|
|
|
|
|
+ private String buildCourseUrl(CourseConfig config, FeishuLinkParam param) {
|
|
|
+ Map<String, Object> params = new LinkedHashMap<>();
|
|
|
+ params.put("companyUserId", param.getCompanyUserId());
|
|
|
+ params.put("videoId", param.getVideoId());
|
|
|
+ params.put("companyId", param.getCompanyId());
|
|
|
+ params.put("courseId", param.getCourseId());
|
|
|
+ String fullUrl = config.getFeishuCourseDomain()
|
|
|
+ + "/feishuCourse/pages_course/video?course=" + encodeJsonParam(params)
|
|
|
+ + "&userId=" + param.getUserId();
|
|
|
+ return encodeUrl(fullUrl);
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
- * 拼接看课url(课程参数JSON方式,适配飞书iframe,双重URL编码)
|
|
|
+ * 将参数Map编码为JSON并URL编码(第一次编码)
|
|
|
*/
|
|
|
- private String buildCourseNewLink(Long companyId, Long companyUserId, Long courseId, Long videoId,Long userId) {
|
|
|
- String json = configService.selectConfigByKey("course.config");
|
|
|
- CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
|
|
|
-
|
|
|
- // 构造课程参数 JSON
|
|
|
- Map<String, Object> courseParam = new LinkedHashMap<>();
|
|
|
- courseParam.put("companyUserId", companyUserId);
|
|
|
- courseParam.put("videoId", videoId);
|
|
|
- courseParam.put("companyId", companyId);
|
|
|
- courseParam.put("courseId", courseId);
|
|
|
- //TODO: 查询这个用户对应的sop课程短链
|
|
|
- courseParam.put("link", courseId);
|
|
|
- String courseJson = JSONUtil.toJsonStr(courseParam);
|
|
|
+ private String encodeJsonParam(Map<String, Object> params) {
|
|
|
try {
|
|
|
- // 第一次编码:对JSON进行URL编码
|
|
|
- String encodedJson = URLEncoder.encode(courseJson, "UTF-8");
|
|
|
- // 拼接完整URL(userId与course参数同级)
|
|
|
- String baseUrl = config.getFeishuNewLinkDomainName();
|
|
|
- String fullUrl = baseUrl + "/feishuCourse/pages_course/video?course=" + encodedJson + "&userId=" +userId;
|
|
|
- // 第二次编码:对整个URL进行URL编码(适配飞书iframe创建要求)
|
|
|
- return URLEncoder.encode(fullUrl, "UTF-8");
|
|
|
+ String json = JSONUtil.toJsonStr(params);
|
|
|
+ return URLEncoder.encode(json, "UTF-8");
|
|
|
} catch (UnsupportedEncodingException e) {
|
|
|
- throw new CustomException("看课URL拼接失败", e);
|
|
|
+ throw new CustomException("参数JSON编码失败", e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 拼接授权url(课程参数JSON方式,适配飞书iframe,双重URL编码)
|
|
|
+ * 对整个URL进行URL编码(第二次编码,用于iframe)
|
|
|
*/
|
|
|
- private String buildAuthLink(Long companyId, Long companyUserId, Long courseId, Long videoId) {
|
|
|
- String json = configService.selectConfigByKey("course.config");
|
|
|
- CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
|
|
|
-
|
|
|
- Map<String, Object> authParam = new LinkedHashMap<>();
|
|
|
- authParam.put("companyUserId", companyUserId);
|
|
|
- authParam.put("videoId", videoId);
|
|
|
- authParam.put("companyId", companyId);
|
|
|
- authParam.put("courseId", courseId);
|
|
|
- String authJson = JSONUtil.toJsonStr(authParam);
|
|
|
-
|
|
|
+ private String encodeUrl(String url) {
|
|
|
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");
|
|
|
+ return URLEncoder.encode(url, "UTF-8");
|
|
|
} catch (UnsupportedEncodingException e) {
|
|
|
- throw new CustomException("授权URL拼接失败", e);
|
|
|
+ throw new CustomException("URL编码失败", e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 创建iframe块
|
|
|
- */
|
|
|
- private void createIframeBlock(String documentId, String url) throws Exception {
|
|
|
- CreateDocumentBlockChildrenReq req = CreateDocumentBlockChildrenReq.newBuilder()
|
|
|
- .documentId(documentId)
|
|
|
- .blockId(documentId)
|
|
|
- .documentRevisionId(-1)
|
|
|
- .createDocumentBlockChildrenReqBody(CreateDocumentBlockChildrenReqBody.newBuilder()
|
|
|
- .children(new Block[] {
|
|
|
- Block.newBuilder()
|
|
|
- .blockType(26)
|
|
|
- .iframe(Iframe.newBuilder()
|
|
|
- .component(IframeComponent.newBuilder()
|
|
|
- .iframeType(1)
|
|
|
- .url(url).build()
|
|
|
- ).build()
|
|
|
- )
|
|
|
- .build()
|
|
|
- })
|
|
|
- .index(0)
|
|
|
- .build())
|
|
|
- .build();
|
|
|
+ // ==================== 配置与数据获取 ====================
|
|
|
|
|
|
- // 发起请求
|
|
|
- client.docx().v1().documentBlockChildren().create(req);
|
|
|
+ private CourseConfig getCourseConfig() {
|
|
|
+ String json = configService.selectConfigByKey("course.config");
|
|
|
+ if (StringUtils.isBlank(json)) {
|
|
|
+ throw new CustomException("飞书看课配置为空");
|
|
|
+ }
|
|
|
+ CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
|
|
|
+ if (!Boolean.TRUE.equals(config.getEnableFeishuNewLink())) {
|
|
|
+ throw new CustomException("未开启飞书看课");
|
|
|
+ }
|
|
|
+ return config;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 创建 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);
|
|
|
+ private FsUserCourseVideo getVideoEntity(Long videoId) {
|
|
|
+ FsUserCourseVideo video = videoMapper.selectFsUserCourseVideoByVideoId(videoId);
|
|
|
+ if (video == null) {
|
|
|
+ throw new CustomException("视频不存在: " + videoId);
|
|
|
+ }
|
|
|
+ return video;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -391,7 +370,7 @@ public class FeiShuService {
|
|
|
log.error("删除失败: {} | token={} | err={}", file.getName(), file.getToken(), e.getMessage());
|
|
|
}
|
|
|
}
|
|
|
- log.info("飞书云文档清理完成 | 列表总数={} | 删除成功={}", total, deleted);
|
|
|
+ log.info("飞书看课文档清理完成 | 列表总数={} | 删除成功={}", total, deleted);
|
|
|
}
|
|
|
|
|
|
-}
|
|
|
+}
|