|
@@ -8,10 +8,12 @@ import com.fs.course.domain.FsCourseLink;
|
|
|
import com.fs.course.domain.FsUserCourseVideo;
|
|
import com.fs.course.domain.FsUserCourseVideo;
|
|
|
import com.fs.course.mapper.FsCourseLinkMapper;
|
|
import com.fs.course.mapper.FsCourseLinkMapper;
|
|
|
import com.fs.course.mapper.FsUserCourseVideoMapper;
|
|
import com.fs.course.mapper.FsUserCourseVideoMapper;
|
|
|
|
|
+import com.fs.feishu.domain.FeishuAccount;
|
|
|
|
|
+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.model.FeishuLinkParam;
|
|
|
-import com.fs.feishu.util.FeishuErrorCode;
|
|
|
|
|
import com.fs.system.service.ISysConfigService;
|
|
import com.fs.system.service.ISysConfigService;
|
|
|
-import com.lark.oapi.Client;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
@@ -20,12 +22,12 @@ import org.springframework.stereotype.Component;
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.io.UnsupportedEncodingException;
|
|
|
import java.net.URLEncoder;
|
|
import java.net.URLEncoder;
|
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 飞书业务编排层:
|
|
|
|
|
- * 负责课程链接和注册链接的完整业务流程,包括缓存、URL 构建、调用底层 API 服务等。
|
|
|
|
|
|
|
+ * 飞书业务编排层:按销售绑定的飞书账号创建文档链接。
|
|
|
*/
|
|
*/
|
|
|
@Component
|
|
@Component
|
|
|
@Slf4j
|
|
@Slf4j
|
|
@@ -43,37 +45,46 @@ public class FeiShuService {
|
|
|
private FeishuClientPool clientPool;
|
|
private FeishuClientPool clientPool;
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private FeishuDocApiService docApiService;
|
|
private FeishuDocApiService docApiService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FeishuAccountMapper feishuAccountMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private IFeishuAccountService feishuAccountService;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 001 生成飞书初始链接(注册授权链接)
|
|
|
|
|
- * 每次 shortCode 唯一
|
|
|
|
|
|
|
+ * 生成飞书初始链接(注册授权链接)。
|
|
|
*/
|
|
*/
|
|
|
public String getFeishuRegisterLink(Long videoId, Long companyId, Long courseId,
|
|
public String getFeishuRegisterLink(Long videoId, Long companyId, Long courseId,
|
|
|
- Long companyUserId, String shortLink) {
|
|
|
|
|
- // 检查功能开关
|
|
|
|
|
|
|
+ Long companyUserId, String shortLink, Long feishuAccountId) {
|
|
|
CourseConfig config = getCourseConfig();
|
|
CourseConfig config = getCourseConfig();
|
|
|
if (!Boolean.TRUE.equals(config.getEnableFeishuNewLink())) {
|
|
if (!Boolean.TRUE.equals(config.getEnableFeishuNewLink())) {
|
|
|
throw new CustomException("未开启飞书看课");
|
|
throw new CustomException("未开启飞书看课");
|
|
|
}
|
|
}
|
|
|
|
|
+ if (companyUserId == null) {
|
|
|
|
|
+ throw new CustomException("销售ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ feishuAccountId = feishuAccountService.resolveSendAccountId(companyUserId, feishuAccountId);
|
|
|
|
|
|
|
|
FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(videoId);
|
|
FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(videoId);
|
|
|
if (userCourseVideo == null) {
|
|
if (userCourseVideo == null) {
|
|
|
throw new CustomException("视频不存在: " + videoId);
|
|
throw new CustomException("视频不存在: " + videoId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- int maxRetries = clientPool.getAvailableCount();
|
|
|
|
|
- for (int i = 0; i < maxRetries; i++) {
|
|
|
|
|
- Client client = clientPool.getClient();
|
|
|
|
|
|
|
+ List<FeishuAccount> accounts = clientPool.listEnabledAccounts(companyUserId, feishuAccountId);
|
|
|
|
|
+ CustomException lastError = null;
|
|
|
|
|
+ for (FeishuAccount account : accounts) {
|
|
|
|
|
+ FeishuClientHolder holder = clientPool.getClient(companyUserId, account.getId());
|
|
|
try {
|
|
try {
|
|
|
- String documentId = docApiService.createDocument(client, userCourseVideo.getTitle() + "-注册");
|
|
|
|
|
|
|
+ String documentId = docApiService.createDocument(holder, userCourseVideo.getTitle() + "-注册");
|
|
|
String authUrl = buildAuthLink(companyId, companyUserId, courseId, videoId, shortLink);
|
|
String authUrl = buildAuthLink(companyId, companyUserId, courseId, videoId, shortLink);
|
|
|
- docApiService.createTextLinkBlock(client, documentId, authUrl);
|
|
|
|
|
- docApiService.changeDocumentPermissions(client, documentId);
|
|
|
|
|
|
|
+ docApiService.createTextLinkBlock(holder, documentId, authUrl);
|
|
|
|
|
+ docApiService.changeDocumentPermissions(holder, documentId);
|
|
|
|
|
+ feishuAccountMapper.incrementNumberUse(account.getId());
|
|
|
|
|
+ updateCourseLinkFeishuAccount(shortLink, account.getId());
|
|
|
return "https://www.feishu.cn/docx/" + documentId;
|
|
return "https://www.feishu.cn/docx/" + documentId;
|
|
|
} catch (CustomException e) {
|
|
} catch (CustomException e) {
|
|
|
- // 如果是账号禁用异常,继续尝试下一个账号
|
|
|
|
|
- if (e.getMessage().contains("不可用") || e.getMessage().contains("已被自动禁用")) {
|
|
|
|
|
- log.warn("账号不可用,尝试下一个");
|
|
|
|
|
|
|
+ lastError = e;
|
|
|
|
|
+ if (isAccountUnavailable(e)) {
|
|
|
|
|
+ log.warn("销售[{}]飞书账号[{}]不可用,尝试下一个", companyUserId, account.getId());
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
throw e;
|
|
throw e;
|
|
@@ -81,23 +92,35 @@ public class FeiShuService {
|
|
|
throw new CustomException("创建飞书注册链接失败: " + e.getMessage(), e);
|
|
throw new CustomException("创建飞书注册链接失败: " + e.getMessage(), e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- throw new CustomException("所有飞书账号均不可用,请联系管理员");
|
|
|
|
|
|
|
+ if (lastError != null) {
|
|
|
|
|
+ throw lastError;
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new CustomException("当前销售没有可用的飞书账号,请先配置或更换账号");
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * 002 生成飞书课程链接
|
|
|
|
|
- * 相同参数可能重复,使用 Redis 缓存避免重复创建文档
|
|
|
|
|
|
|
+ * 生成飞书课程链接(授权后看课文档)。
|
|
|
*/
|
|
*/
|
|
|
public String getFeishuCourseNewLink(FeishuLinkParam param) {
|
|
public String getFeishuCourseNewLink(FeishuLinkParam param) {
|
|
|
if (param == null || param.getCompanyUserId() == null || param.getCompanyId() == null) {
|
|
if (param == null || param.getCompanyUserId() == null || param.getCompanyId() == null) {
|
|
|
throw new CustomException("参数不能为空");
|
|
throw new CustomException("参数不能为空");
|
|
|
}
|
|
}
|
|
|
|
|
+ if (StringUtils.isBlank(param.getLink())) {
|
|
|
|
|
+ throw new CustomException("飞书课程短链不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- String cacheKey = String.format("feishu:course_link:%d:%d:%d:%d:%d:%s",
|
|
|
|
|
|
|
+ FsCourseLink courseLink = requireCourseLink(param.getLink());
|
|
|
|
|
+ Long feishuAccountId = courseLink.getFeishuAccountId();
|
|
|
|
|
+ if (feishuAccountId == null) {
|
|
|
|
|
+ feishuAccountId = feishuAccountService.resolveSendAccountId(param.getCompanyUserId(), null);
|
|
|
|
|
+ updateCourseLinkFeishuAccount(param.getLink(), feishuAccountId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String cacheKey = String.format("feishu:course_link:%d:%d:%d:%d:%d:%s:%d",
|
|
|
param.getCompanyId(), param.getCompanyUserId(),
|
|
param.getCompanyId(), param.getCompanyUserId(),
|
|
|
param.getCourseId(), param.getVideoId(),
|
|
param.getCourseId(), param.getVideoId(),
|
|
|
- param.getUserId(), param.getLink());
|
|
|
|
|
|
|
+ param.getUserId(), param.getLink(), feishuAccountId);
|
|
|
|
|
|
|
|
- // 缓存命中直接返回
|
|
|
|
|
String cached = redisCache.getCacheObject(cacheKey);
|
|
String cached = redisCache.getCacheObject(cacheKey);
|
|
|
if (StringUtils.isNotBlank(cached)) {
|
|
if (StringUtils.isNotBlank(cached)) {
|
|
|
return cached;
|
|
return cached;
|
|
@@ -107,40 +130,46 @@ public class FeiShuService {
|
|
|
if (userCourseVideo == null) {
|
|
if (userCourseVideo == null) {
|
|
|
throw new CustomException("视频不存在: " + param.getVideoId());
|
|
throw new CustomException("视频不存在: " + param.getVideoId());
|
|
|
}
|
|
}
|
|
|
- //获取该视频是否有答题
|
|
|
|
|
|
|
+
|
|
|
String questionBankId = userCourseVideo.getQuestionBankId();
|
|
String questionBankId = userCourseVideo.getQuestionBankId();
|
|
|
boolean hasQuestion = StringUtils.isNotBlank(questionBankId) && !"null".equals(questionBankId);
|
|
boolean hasQuestion = StringUtils.isNotBlank(questionBankId) && !"null".equals(questionBankId);
|
|
|
- Integer questionFlag = hasQuestion ? 1 : 0;//0:无题 1:有题
|
|
|
|
|
- int maxRetries = clientPool.getAvailableCount();
|
|
|
|
|
- for (int i = 0; i < maxRetries; i++) {
|
|
|
|
|
- Client client = clientPool.getClient();
|
|
|
|
|
- try {
|
|
|
|
|
- String documentId = docApiService.createDocument(client, userCourseVideo.getTitle());
|
|
|
|
|
- String url = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
|
|
|
|
|
- param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink(),questionFlag);
|
|
|
|
|
- docApiService.createIframeBlock(client, documentId, url);
|
|
|
|
|
- docApiService.changeDocumentPermissions(client, documentId);
|
|
|
|
|
-
|
|
|
|
|
- String resultUrl = "https://www.feishu.cn/docx/" + documentId;
|
|
|
|
|
- redisCache.setCacheObject(cacheKey, resultUrl, 24, TimeUnit.HOURS);
|
|
|
|
|
- return resultUrl;
|
|
|
|
|
- } catch (CustomException e) {
|
|
|
|
|
- if (e.getMessage().contains("不可用") || e.getMessage().contains("已被自动禁用")) {
|
|
|
|
|
- log.warn("账号不可用,尝试下一个");
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- throw e;
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- throw new CustomException("创建飞书课程链接失败: " + e.getMessage(), e);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Integer questionFlag = hasQuestion ? 1 : 0;
|
|
|
|
|
+
|
|
|
|
|
+ FeishuClientHolder holder = clientPool.getClientByAccountId(feishuAccountId);
|
|
|
|
|
+ try {
|
|
|
|
|
+ String documentId = docApiService.createDocument(holder, userCourseVideo.getTitle());
|
|
|
|
|
+ String url = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
|
|
|
|
|
+ param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink(), questionFlag);
|
|
|
|
|
+ docApiService.createIframeBlock(holder, documentId, url);
|
|
|
|
|
+ docApiService.changeDocumentPermissions(holder, documentId);
|
|
|
|
|
+
|
|
|
|
|
+ String resultUrl = "https://www.feishu.cn/docx/" + documentId;
|
|
|
|
|
+ redisCache.setCacheObject(cacheKey, resultUrl, 24, TimeUnit.HOURS);
|
|
|
|
|
+ feishuAccountMapper.incrementNumberUse(feishuAccountId);
|
|
|
|
|
+ return resultUrl;
|
|
|
|
|
+ } catch (CustomException e) {
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new CustomException("创建飞书课程链接失败: " + e.getMessage(), e);
|
|
|
}
|
|
}
|
|
|
- throw new CustomException("所有飞书账号均不可用,请联系管理员");
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ==================== URL 构建(纯业务逻辑,无外部调用) ====================
|
|
|
|
|
|
|
+ private void updateCourseLinkFeishuAccount(String shortLink, Long feishuAccountId) {
|
|
|
|
|
+ FsCourseLink link = courseLinkMapper.selectFsCourseLinkByLink(shortLink);
|
|
|
|
|
+ if (link == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ link.setFeishuAccountId(feishuAccountId);
|
|
|
|
|
+ courseLinkMapper.updateFsCourseLink(link);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isAccountUnavailable(CustomException e) {
|
|
|
|
|
+ String message = e.getMessage();
|
|
|
|
|
+ return message != null && (message.contains("不可用") || message.contains("已被自动禁用") || message.contains("已禁用"));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
private String buildCourseNewLink(Long companyId, Long companyUserId, Long courseId,
|
|
private String buildCourseNewLink(Long companyId, Long companyUserId, Long courseId,
|
|
|
- Long videoId, Long userId, String link,Integer questionFlag) {
|
|
|
|
|
|
|
+ Long videoId, Long userId, String link, Integer questionFlag) {
|
|
|
CourseConfig config = getCourseConfig();
|
|
CourseConfig config = getCourseConfig();
|
|
|
|
|
|
|
|
Map<String, Object> courseParam = new LinkedHashMap<>();
|
|
Map<String, Object> courseParam = new LinkedHashMap<>();
|
|
@@ -154,9 +183,8 @@ public class FeiShuService {
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(courseParam), "UTF-8");
|
|
String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(courseParam), "UTF-8");
|
|
|
- String fullUrl = config.getFeishuCourseDomain() +
|
|
|
|
|
- "/feishuCourse/pages_course/video?course=" + encodedJson + "&userId=" + userId;
|
|
|
|
|
- return URLEncoder.encode(fullUrl, "UTF-8");
|
|
|
|
|
|
|
+ return URLEncoder.encode(config.getFeishuCourseDomain()
|
|
|
|
|
+ + "/feishuCourse/pages_course/video?course=" + encodedJson + "&userId=" + userId, "UTF-8");
|
|
|
} catch (UnsupportedEncodingException e) {
|
|
} catch (UnsupportedEncodingException e) {
|
|
|
throw new CustomException("看课URL拼接失败", e);
|
|
throw new CustomException("看课URL拼接失败", e);
|
|
|
}
|
|
}
|
|
@@ -176,9 +204,8 @@ public class FeiShuService {
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(authParam), "UTF-8");
|
|
String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(authParam), "UTF-8");
|
|
|
- String fullUrl = config.getFeishuAuthDomain() +
|
|
|
|
|
- "/feishuCourse/pages/wxauth?course=" + encodedJson;
|
|
|
|
|
- return URLEncoder.encode(fullUrl, "UTF-8");
|
|
|
|
|
|
|
+ return URLEncoder.encode(config.getFeishuAuthDomain()
|
|
|
|
|
+ + "/feishuCourse/pages/wxauth?course=" + encodedJson, "UTF-8");
|
|
|
} catch (UnsupportedEncodingException e) {
|
|
} catch (UnsupportedEncodingException e) {
|
|
|
throw new CustomException("授权URL拼接失败", e);
|
|
throw new CustomException("授权URL拼接失败", e);
|
|
|
}
|
|
}
|
|
@@ -202,9 +229,6 @@ public class FeiShuService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 读取课程配置并检查非空
|
|
|
|
|
- */
|
|
|
|
|
private CourseConfig getCourseConfig() {
|
|
private CourseConfig getCourseConfig() {
|
|
|
String json = configService.selectConfigByKey("course.config");
|
|
String json = configService.selectConfigByKey("course.config");
|
|
|
if (StringUtils.isBlank(json)) {
|
|
if (StringUtils.isBlank(json)) {
|
|
@@ -212,4 +236,4 @@ public class FeiShuService {
|
|
|
}
|
|
}
|
|
|
return JSONUtil.toBean(json, CourseConfig.class);
|
|
return JSONUtil.toBean(json, CourseConfig.class);
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+}
|