|
|
@@ -13,6 +13,7 @@ import com.fs.feishu.mapper.FeishuAccountMapper;
|
|
|
import com.fs.feishu.service.IFeishuAccountService;
|
|
|
import com.fs.feishu.model.FeishuClientHolder;
|
|
|
import com.fs.feishu.model.FeishuLinkParam;
|
|
|
+import com.fs.feishu.util.SecureTokenUtil;
|
|
|
import com.fs.system.service.ISysConfigService;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
@@ -50,6 +51,97 @@ public class FeiShuService {
|
|
|
@Autowired
|
|
|
private IFeishuAccountService feishuAccountService;
|
|
|
|
|
|
+ private static final String FEISHU_DOC_URL_PREFIX = "https://www.feishu.cn/docx/";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按发课配置生成飞书文档链接:需要授权走注册链,免授权直接生成看课文档。
|
|
|
+ */
|
|
|
+ public String resolveFeishuSendLink(Long videoId, Long companyId, Long courseId,
|
|
|
+ Long companyUserId, String shortLink, Long feishuAccountId,
|
|
|
+ Integer feishuNeedAuth) {
|
|
|
+ if (isAuthRequired(feishuNeedAuth)) {
|
|
|
+ return getFeishuRegisterLink(videoId, companyId, courseId, companyUserId, shortLink, feishuAccountId);
|
|
|
+ }
|
|
|
+ return getFeishuDirectCourseLink(videoId, companyId, courseId, companyUserId, shortLink, feishuAccountId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否需要客户 wxauth 授权后才可看课。null/非0 均视为需要授权(兼容历史数据)。
|
|
|
+ */
|
|
|
+ public static boolean isAuthRequired(Integer feishuNeedAuth) {
|
|
|
+ return feishuNeedAuth == null || feishuNeedAuth != 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 持久化到 fs_course_link / course JSON 的值:1-需要授权,0-免授权。
|
|
|
+ */
|
|
|
+ public static int toStoredFeishuNeedAuth(Integer feishuNeedAuth) {
|
|
|
+ return isAuthRequired(feishuNeedAuth) ? 1 : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否记录看课数据(时长/看课日志等)。免授权模式不记录。
|
|
|
+ */
|
|
|
+ public boolean isWatchDataEnabled(String linkCode) {
|
|
|
+ if (StringUtils.isBlank(linkCode)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ FsCourseLink link = courseLinkMapper.selectFsCourseLinkByLink(linkCode);
|
|
|
+ if (link == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return isAuthRequired(link.getFeishuNeedAuth());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成飞书免授权看课链接(发课时直接嵌入播放页 iframe 文档)。
|
|
|
+ */
|
|
|
+ public String getFeishuDirectCourseLink(Long videoId, Long companyId, Long courseId,
|
|
|
+ Long companyUserId, String shortLink, Long feishuAccountId) {
|
|
|
+ CourseConfig config = getCourseConfig();
|
|
|
+ if (!Boolean.TRUE.equals(config.getEnableFeishuNewLink())) {
|
|
|
+ throw new CustomException("未开启飞书看课");
|
|
|
+ }
|
|
|
+ if (companyUserId == null) {
|
|
|
+ throw new CustomException("销售ID不能为空");
|
|
|
+ }
|
|
|
+ feishuAccountId = feishuAccountService.resolveSendAccountId(companyUserId, feishuAccountId);
|
|
|
+
|
|
|
+ FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(videoId);
|
|
|
+ if (userCourseVideo == null) {
|
|
|
+ throw new CustomException("视频不存在: " + videoId);
|
|
|
+ }
|
|
|
+ int questionFlag = resolveQuestionFlag(userCourseVideo);
|
|
|
+
|
|
|
+ List<FeishuAccount> accounts = clientPool.listEnabledAccounts(companyUserId, feishuAccountId);
|
|
|
+ CustomException lastError = null;
|
|
|
+ for (FeishuAccount account : accounts) {
|
|
|
+ FeishuClientHolder holder = clientPool.getClient(companyUserId, account.getId());
|
|
|
+ try {
|
|
|
+ String documentId = docApiService.createDocument(holder, userCourseVideo.getTitle());
|
|
|
+ String iframeUrl = buildCoursePageLink(companyId, companyUserId, courseId, videoId, 0L, shortLink, questionFlag);
|
|
|
+ docApiService.createIframeBlock(holder, documentId, iframeUrl);
|
|
|
+ docApiService.changeDocumentPermissions(holder, documentId);
|
|
|
+ feishuAccountMapper.incrementNumberUse(account.getId());
|
|
|
+ updateCourseLinkFeishuAccount(shortLink, account.getId());
|
|
|
+ return FEISHU_DOC_URL_PREFIX + documentId;
|
|
|
+ } catch (CustomException e) {
|
|
|
+ lastError = e;
|
|
|
+ if (isAccountUnavailable(e)) {
|
|
|
+ log.warn("销售[{}]飞书账号[{}]不可用,尝试下一个", companyUserId, account.getId());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new CustomException("创建飞书免授权看课链接失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (lastError != null) {
|
|
|
+ throw lastError;
|
|
|
+ }
|
|
|
+ throw new CustomException("当前销售没有可用的飞书账号,请先配置或更换账号");
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成飞书初始链接(注册授权链接)。
|
|
|
*/
|
|
|
@@ -110,6 +202,9 @@ public class FeiShuService {
|
|
|
}
|
|
|
|
|
|
FsCourseLink courseLink = requireCourseLink(param.getLink());
|
|
|
+ if (!isAuthRequired(courseLink.getFeishuNeedAuth())) {
|
|
|
+ throw new CustomException("免授权链接无需调用此接口");
|
|
|
+ }
|
|
|
Long feishuAccountId = courseLink.getFeishuAccountId();
|
|
|
if (feishuAccountId == null) {
|
|
|
feishuAccountId = feishuAccountService.resolveSendAccountId(param.getCompanyUserId(), null);
|
|
|
@@ -131,19 +226,17 @@ public class FeiShuService {
|
|
|
throw new CustomException("视频不存在: " + param.getVideoId());
|
|
|
}
|
|
|
|
|
|
- String questionBankId = userCourseVideo.getQuestionBankId();
|
|
|
- boolean hasQuestion = StringUtils.isNotBlank(questionBankId) && !"null".equals(questionBankId);
|
|
|
- Integer questionFlag = hasQuestion ? 1 : 0;
|
|
|
+ int questionFlag = resolveQuestionFlag(userCourseVideo);
|
|
|
|
|
|
FeishuClientHolder holder = clientPool.getClientByAccountId(feishuAccountId);
|
|
|
try {
|
|
|
String documentId = docApiService.createDocument(holder, userCourseVideo.getTitle());
|
|
|
- String url = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
|
|
|
+ String iframeUrl = buildCoursePageLink(param.getCompanyId(), param.getCompanyUserId(),
|
|
|
param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink(), questionFlag);
|
|
|
- docApiService.createIframeBlock(holder, documentId, url);
|
|
|
+ docApiService.createIframeBlock(holder, documentId, iframeUrl);
|
|
|
docApiService.changeDocumentPermissions(holder, documentId);
|
|
|
|
|
|
- String resultUrl = "https://www.feishu.cn/docx/" + documentId;
|
|
|
+ String resultUrl = FEISHU_DOC_URL_PREFIX + documentId;
|
|
|
redisCache.setCacheObject(cacheKey, resultUrl, 24, TimeUnit.HOURS);
|
|
|
feishuAccountMapper.incrementNumberUse(feishuAccountId);
|
|
|
return resultUrl;
|
|
|
@@ -168,39 +261,52 @@ public class FeiShuService {
|
|
|
return message != null && (message.contains("不可用") || message.contains("已被自动禁用") || message.contains("已禁用"));
|
|
|
}
|
|
|
|
|
|
- private String buildCourseNewLink(Long companyId, Long companyUserId, Long courseId,
|
|
|
- Long videoId, Long userId, String link, Integer questionFlag) {
|
|
|
+ private String buildCoursePageLink(Long companyId, Long companyUserId, Long courseId,
|
|
|
+ Long videoId, Long userId, String link, Integer questionFlag) {
|
|
|
CourseConfig config = getCourseConfig();
|
|
|
+ Map<String, Object> courseParam = buildCourseParam(companyId, companyUserId, courseId, videoId, link, questionFlag);
|
|
|
|
|
|
+ try {
|
|
|
+ String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(courseParam), "UTF-8");
|
|
|
+ String courseToken = SecureTokenUtil.generateCourseAccessToken(companyUserId, videoId, userId);
|
|
|
+ String encodedToken = URLEncoder.encode(courseToken, "UTF-8");
|
|
|
+ String pageUrl = config.getFeishuCourseDomain()
|
|
|
+ + "/feishuCourse/pages_course/video?course=" + encodedJson
|
|
|
+ + "&userId=" + userId
|
|
|
+ + "&token=" + encodedToken;
|
|
|
+ return URLEncoder.encode(pageUrl, "UTF-8");
|
|
|
+ } catch (UnsupportedEncodingException e) {
|
|
|
+ throw new CustomException("看课URL拼接失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildCourseParam(Long companyId, Long companyUserId, Long courseId,
|
|
|
+ Long videoId, String link, Integer questionFlag) {
|
|
|
Map<String, Object> courseParam = new LinkedHashMap<>();
|
|
|
courseParam.put("companyUserId", companyUserId);
|
|
|
courseParam.put("videoId", videoId);
|
|
|
courseParam.put("companyId", companyId);
|
|
|
courseParam.put("courseId", courseId);
|
|
|
courseParam.put("link", link);
|
|
|
- courseParam.put("questionFlag", questionFlag);
|
|
|
+ if (questionFlag != null) {
|
|
|
+ courseParam.put("questionFlag", questionFlag);
|
|
|
+ }
|
|
|
appendLinkMeta(courseParam, requireCourseLink(link));
|
|
|
+ return courseParam;
|
|
|
+ }
|
|
|
|
|
|
- try {
|
|
|
- String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(courseParam), "UTF-8");
|
|
|
- return URLEncoder.encode(config.getFeishuCourseDomain()
|
|
|
- + "/feishuCourse/pages_course/video?course=" + encodedJson + "&userId=" + userId, "UTF-8");
|
|
|
- } catch (UnsupportedEncodingException e) {
|
|
|
- throw new CustomException("看课URL拼接失败", e);
|
|
|
- }
|
|
|
+ private int resolveQuestionFlag(FsUserCourseVideo video) {
|
|
|
+ String questionBankId = video.getQuestionBankId();
|
|
|
+ boolean hasQuestion = StringUtils.isNotBlank(questionBankId) && !"null".equals(questionBankId);
|
|
|
+ return hasQuestion ? 1 : 0;
|
|
|
}
|
|
|
|
|
|
private String buildAuthLink(Long companyId, Long companyUserId, Long courseId,
|
|
|
Long videoId, String shortLink) {
|
|
|
CourseConfig config = getCourseConfig();
|
|
|
|
|
|
- Map<String, Object> authParam = new LinkedHashMap<>();
|
|
|
- authParam.put("companyUserId", companyUserId);
|
|
|
- authParam.put("videoId", videoId);
|
|
|
- authParam.put("companyId", companyId);
|
|
|
- authParam.put("courseId", courseId);
|
|
|
- authParam.put("link", shortLink);
|
|
|
- appendLinkMeta(authParam, requireCourseLink(shortLink));
|
|
|
+ Map<String, Object> authParam = buildCourseParam(companyId, companyUserId, courseId, videoId, shortLink, null);
|
|
|
+ authParam.put("feishuNeedAuth", 1);
|
|
|
|
|
|
try {
|
|
|
String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(authParam), "UTF-8");
|
|
|
@@ -224,6 +330,7 @@ public class FeiShuService {
|
|
|
courseParam.put("linkType", fsCourseLink.getLinkType());
|
|
|
courseParam.put("qwExternalId", fsCourseLink.getQwExternalId());
|
|
|
courseParam.put("qwUserId", fsCourseLink.getQwUserId());
|
|
|
+ courseParam.put("feishuNeedAuth", toStoredFeishuNeedAuth(fsCourseLink.getFeishuNeedAuth()));
|
|
|
if (StringUtils.isNotBlank(fsCourseLink.getProjectCode())) {
|
|
|
courseParam.put("projectCode", fsCourseLink.getProjectCode());
|
|
|
}
|