|
@@ -1,11 +1,14 @@
|
|
|
package com.fs.app.service;
|
|
package com.fs.app.service;
|
|
|
|
|
|
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
import com.alibaba.fastjson.JSON;
|
|
import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.fs.common.core.redis.RedisCache;
|
|
import com.fs.common.core.redis.RedisCache;
|
|
|
|
|
+import com.fs.common.exception.CustomException;
|
|
|
import com.fs.common.exception.base.BaseException;
|
|
import com.fs.common.exception.base.BaseException;
|
|
|
|
|
+import com.fs.common.utils.DateUtils;
|
|
|
import com.fs.common.utils.StringUtils;
|
|
import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.common.utils.date.DateUtil;
|
|
import com.fs.common.utils.date.DateUtil;
|
|
|
import com.fs.company.domain.Company;
|
|
import com.fs.company.domain.Company;
|
|
@@ -13,9 +16,17 @@ import com.fs.company.domain.CompanyMiniapp;
|
|
|
import com.fs.company.mapper.CompanyMapper;
|
|
import com.fs.company.mapper.CompanyMapper;
|
|
|
import com.fs.company.service.ICompanyMiniappService;
|
|
import com.fs.company.service.ICompanyMiniappService;
|
|
|
import com.fs.course.config.CourseConfig;
|
|
import com.fs.course.config.CourseConfig;
|
|
|
|
|
+import com.fs.course.domain.FsCourseLink;
|
|
|
import com.fs.course.domain.FsCoursePlaySourceConfig;
|
|
import com.fs.course.domain.FsCoursePlaySourceConfig;
|
|
|
import com.fs.course.domain.FsCourseWatchLog;
|
|
import com.fs.course.domain.FsCourseWatchLog;
|
|
|
|
|
+import com.fs.course.mapper.FsCourseLinkMapper;
|
|
|
|
|
+import com.fs.course.mapper.FsUserCourseVideoMapper;
|
|
|
import com.fs.course.service.IFsCourseWatchLogService;
|
|
import com.fs.course.service.IFsCourseWatchLogService;
|
|
|
|
|
+import com.fs.feishu.domain.FeishuAccount;
|
|
|
|
|
+import com.fs.feishu.domain.FeishuLinkError;
|
|
|
|
|
+import com.fs.feishu.mapper.FeishuAccountMapper;
|
|
|
|
|
+import com.fs.feishu.mapper.FeishuLinkErrorMapper;
|
|
|
|
|
+import com.fs.feishu.service.FeiShuService;
|
|
|
import com.fs.his.domain.FsShareMaterial;
|
|
import com.fs.his.domain.FsShareMaterial;
|
|
|
import com.fs.his.domain.FsSignArticle;
|
|
import com.fs.his.domain.FsSignArticle;
|
|
|
import com.fs.his.domain.FsUser;
|
|
import com.fs.his.domain.FsUser;
|
|
@@ -40,15 +51,20 @@ import com.fs.sop.domain.QwSopLogs;
|
|
|
import com.fs.sop.service.IQwSopLogsService;
|
|
import com.fs.sop.service.IQwSopLogsService;
|
|
|
import com.fs.sop.service.impl.QwSopLogsServiceImpl;
|
|
import com.fs.sop.service.impl.QwSopLogsServiceImpl;
|
|
|
import com.fs.system.service.ISysConfigService;
|
|
import com.fs.system.service.ISysConfigService;
|
|
|
|
|
+import com.lark.oapi.Client;
|
|
|
import com.fs.wxwork.dto.*;
|
|
import com.fs.wxwork.dto.*;
|
|
|
import lombok.AllArgsConstructor;
|
|
import lombok.AllArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
+import java.io.UnsupportedEncodingException;
|
|
|
|
|
+import java.net.URI;
|
|
|
|
|
+import java.net.URLEncoder;
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.ZoneId;
|
|
import java.time.ZoneId;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeUnit;
|
|
@@ -91,6 +107,24 @@ public class IpadSendServer {
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private IFsShareMaterialService shareMaterialService;
|
|
private IFsShareMaterialService shareMaterialService;
|
|
|
|
|
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FeishuAsyncService asyncService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FsCourseLinkMapper courseLinkMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FeishuAccountMapper feishuAccountMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FeishuLinkErrorMapper feishuLinkErrorMapper;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FeiShuService feiShuService;
|
|
|
|
|
+
|
|
|
|
|
+ //销售的个人飞书应用账户缓存的key
|
|
|
|
|
+ private static final String FEISHU_COMPANY_USER_KEY = "feishu:account_company_user:";
|
|
|
|
|
+
|
|
|
|
|
+ private static final String FEISHU_DOC_ID = "fei_shu_doc_id:";
|
|
|
|
|
+
|
|
|
|
|
+ private static final String FEISHU_CLIENT = "fei_shu_client:";
|
|
|
|
|
+
|
|
|
//private final LiveWatchLogMapper liveWatchLogMapper;
|
|
//private final LiveWatchLogMapper liveWatchLogMapper;
|
|
|
|
|
|
|
|
|
|
|
|
@@ -295,7 +329,11 @@ public class IpadSendServer {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void sendLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
|
|
|
|
|
|
|
+ public void sendLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Long externalId) {
|
|
|
|
|
+ if ("18".equals(content.getContentType())){
|
|
|
|
|
+ // 完善content里面的飞书注册链接
|
|
|
|
|
+ completeFeishuRegisterLink(vo, content, externalId);
|
|
|
|
|
+ }
|
|
|
// 文本
|
|
// 文本
|
|
|
LinkVo linkVo = LinkVo.builder()
|
|
LinkVo linkVo = LinkVo.builder()
|
|
|
.title(content.getLinkTitle())
|
|
.title(content.getLinkTitle())
|
|
@@ -312,6 +350,167 @@ public class IpadSendServer {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 完善飞书注册链接
|
|
|
|
|
+ * */
|
|
|
|
|
+ public QwSopCourseFinishTempSetting.Setting completeFeishuRegisterLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Long externalId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ //初始链接
|
|
|
|
|
+ String initUrl = content.getLinkUrl();
|
|
|
|
|
+ // 1. 从初始链接提取携带的短链
|
|
|
|
|
+ String shortLink = extractLastPathSegment(initUrl);
|
|
|
|
|
+ FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(shortLink);
|
|
|
|
|
+ Long companyId = fsCourseLink.getCompanyId();
|
|
|
|
|
+ Long companyUserId = fsCourseLink.getCompanyUserId();
|
|
|
|
|
+ Long courseId = fsCourseLink.getCourseId();
|
|
|
|
|
+ Long videoId = fsCourseLink.getVideoId();
|
|
|
|
|
+ //从缓存取出docId;account
|
|
|
|
|
+ String docIdKey=FEISHU_DOC_ID + externalId+ "_" + videoId;
|
|
|
|
|
+ String feiShuDocId = redisCache.getCacheObject(docIdKey);
|
|
|
|
|
+ if (StringUtils.isBlank(feiShuDocId)) {
|
|
|
|
|
+ //调用飞书API创建docId
|
|
|
|
|
+ String feishuRegisterLink = feiShuService.getFeishuRegisterLink(videoId, companyId, courseId, companyUserId, shortLink);
|
|
|
|
|
+ content.setLinkUrl(feishuRegisterLink);
|
|
|
|
|
+ String docId = extractLastPathSegment(initUrl);
|
|
|
|
|
+ //缓存docId
|
|
|
|
|
+ redisCache.setCacheObject(docIdKey, docId);
|
|
|
|
|
+ return content;
|
|
|
|
|
+ }
|
|
|
|
|
+ String clientKey = FEISHU_CLIENT+feiShuDocId;
|
|
|
|
|
+ String accountStr = redisCache.getCacheObject(clientKey);
|
|
|
|
|
+ FeishuAccount account = JSONUtil.toBean(accountStr, FeishuAccount.class);
|
|
|
|
|
+
|
|
|
|
|
+ //如果缓存中的docId为空,则从feishu接口创建新的docId
|
|
|
|
|
+ Client client = buildClient(account);
|
|
|
|
|
+ //根据初始链接组装真实链接
|
|
|
|
|
+ String completeFeiShuLinkUrl = replaceShortLinkInUrl(initUrl, feiShuDocId);
|
|
|
|
|
+ content.setLinkUrl(completeFeiShuLinkUrl);
|
|
|
|
|
+ String authUrl = buildAuthLink(companyId, companyUserId, courseId, videoId, shortLink);
|
|
|
|
|
+ // ============ 异步调用飞书API完善文档内容与文档权限 ============
|
|
|
|
|
+ asyncService.processFeishuDocAsync(
|
|
|
|
|
+ client, feiShuDocId, authUrl,
|
|
|
|
|
+ videoId, companyId, courseId,
|
|
|
|
|
+ companyUserId, shortLink,
|
|
|
|
|
+ account
|
|
|
|
|
+ );
|
|
|
|
|
+ return content;
|
|
|
|
|
+ }catch (Exception e){
|
|
|
|
|
+ log.error("ID:{}-完善飞书注册链接异常:{}", vo.getId(), e.getMessage());
|
|
|
|
|
+ throw new BaseException("完善飞书注册链接异常");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //构造发送的飞书授权链接
|
|
|
|
|
+ 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);
|
|
|
|
|
+ try {
|
|
|
|
|
+ String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(authParam), "UTF-8");
|
|
|
|
|
+ String fullUrl = config.getFeishuAuthDomain() +
|
|
|
|
|
+ "/feishuAuth/pages/wxauth?course=" + encodedJson;
|
|
|
|
|
+ return URLEncoder.encode(fullUrl, "UTF-8");
|
|
|
|
|
+ } catch (UnsupportedEncodingException e) {
|
|
|
|
|
+ throw new CustomException("授权URL拼接失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private CourseConfig getCourseConfig() {
|
|
|
|
|
+ String json = configService.selectConfigByKey("course.config");
|
|
|
|
|
+ if (org.apache.commons.lang3.StringUtils.isBlank(json)) {
|
|
|
|
|
+ throw new CustomException("飞书看课配置为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ return JSONUtil.toBean(json, CourseConfig.class);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void disablePersonalAccount(FeishuAccount account, String errorMsg) {
|
|
|
|
|
+ feishuAccountMapper.updateStatusAndErrorMsg(account.getId(), 0, errorMsg);
|
|
|
|
|
+ clearCompanyUserCache(account.getCompanyId(), account.getCompanyUserId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void recordLinkError(Long videoId, Long companyId, Long courseId,
|
|
|
|
|
+ Long companyUserId, String link, String errorInfo, Integer type) {
|
|
|
|
|
+ FeishuLinkError error = new FeishuLinkError();
|
|
|
|
|
+ error.setVideoId(videoId);
|
|
|
|
|
+ error.setCompanyId(companyId);
|
|
|
|
|
+ error.setCourseId(courseId);
|
|
|
|
|
+ error.setCompanyUserId(companyUserId);
|
|
|
|
|
+ error.setLink(link);
|
|
|
|
|
+ error.setErrorInfo(errorInfo);
|
|
|
|
|
+ error.setCreateTime(DateUtils.getNowDate());
|
|
|
|
|
+ error.setType(type);
|
|
|
|
|
+ FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(link);
|
|
|
|
|
+ if (fsCourseLink != null) {
|
|
|
|
|
+ error.setQwUserId(fsCourseLink.getQwUserId());
|
|
|
|
|
+ error.setQwExternalId(fsCourseLink.getQwExternalId());
|
|
|
|
|
+ error.setCorpId(fsCourseLink.getCorpId());
|
|
|
|
|
+ }
|
|
|
|
|
+ feishuLinkErrorMapper.insertFeishuLinkError(error);
|
|
|
|
|
+ log.info("飞书链接创建失败已记录,videoId={}, link={}, error={}", videoId, link, errorInfo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 清理销售个人飞书账号缓存
|
|
|
|
|
+ * */
|
|
|
|
|
+ private void clearCompanyUserCache(Long companyId, Long companyUserId) {
|
|
|
|
|
+ if (companyId != null && companyUserId != null) {
|
|
|
|
|
+ String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
|
|
|
|
|
+ redisCache.deleteObject(cacheKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 工具方法
|
|
|
|
|
+ private String extractLastPathSegment(String url) throws Exception {
|
|
|
|
|
+ URI uri = new URI(url);
|
|
|
|
|
+ String path = uri.getPath(); // /docx/xxx
|
|
|
|
|
+ // 防御性判断
|
|
|
|
|
+ if (path == null || !path.contains("/")) return null;
|
|
|
|
|
+ return path.substring(path.lastIndexOf("/") + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 替换 URL 中最后一段路径为新短链
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String replaceShortLinkInUrl(String oldUrl, String newShortLink) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ URI uri = new URI(oldUrl);
|
|
|
|
|
+ String path = uri.getPath();
|
|
|
|
|
+ if (path == null || path.isEmpty()) {
|
|
|
|
|
+ return oldUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ int lastSlashIndex = path.lastIndexOf("/");
|
|
|
|
|
+ String newPath;
|
|
|
|
|
+ if (lastSlashIndex == -1) {
|
|
|
|
|
+ newPath = "/" + newShortLink; // 原本没斜杠则加一个
|
|
|
|
|
+ } else {
|
|
|
|
|
+ newPath = path.substring(0, lastSlashIndex + 1) + newShortLink;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重建完整 URI
|
|
|
|
|
+ return new URI(
|
|
|
|
|
+ uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
|
|
|
|
|
+ newPath, uri.getQuery(), uri.getFragment()
|
|
|
|
|
+ ).toString();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // 降级:纯字符串替换
|
|
|
|
|
+ String oldValue = oldUrl.substring(oldUrl.lastIndexOf("/") + 1);
|
|
|
|
|
+ return oldUrl.replace(oldValue, newShortLink);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // ==================== 辅助方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private Client buildClient(FeishuAccount account) {
|
|
|
|
|
+ return Client.newBuilder(account.getAppId(), account.getAppSecret())
|
|
|
|
|
+ .logReqAtDebug(true)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public void sendVideo(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
|
|
public void sendVideo(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
|
|
|
FileVo videoVo = FileVo.builder()
|
|
FileVo videoVo = FileVo.builder()
|
|
|
.url(content.getVideoUrl())
|
|
.url(content.getVideoUrl())
|
|
@@ -507,7 +706,7 @@ public class IpadSendServer {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public void send(QwSopCourseFinishTempSetting.Setting content, QwUser qwUser, QwSopLogs qwSopLogs, Map<String, FsCoursePlaySourceConfig> miniMap, BaseVo parentVo) {
|
|
|
|
|
|
|
+ public void send(QwSopCourseFinishTempSetting.Setting content, QwUser qwUser, QwSopLogs qwSopLogs, Map<String, FsCoursePlaySourceConfig> miniMap, BaseVo parentVo,Long externalUserId) {
|
|
|
BaseVo vo = new BaseVo();
|
|
BaseVo vo = new BaseVo();
|
|
|
vo.setId(Long.parseLong(qwSopLogs.getId()));
|
|
vo.setId(Long.parseLong(qwSopLogs.getId()));
|
|
|
vo.setRoom(qwSopLogs.getSendType() == 12);
|
|
vo.setRoom(qwSopLogs.getSendType() == 12);
|
|
@@ -529,7 +728,7 @@ public class IpadSendServer {
|
|
|
break;
|
|
break;
|
|
|
case "3":
|
|
case "3":
|
|
|
// 链接
|
|
// 链接
|
|
|
- sendLink(vo, content);
|
|
|
|
|
|
|
+ sendLink(vo, content, externalUserId);
|
|
|
break;
|
|
break;
|
|
|
case "4":
|
|
case "4":
|
|
|
case "10":
|
|
case "10":
|
|
@@ -621,7 +820,7 @@ public class IpadSendServer {
|
|
|
break;
|
|
break;
|
|
|
case "18":
|
|
case "18":
|
|
|
// 链接
|
|
// 链接
|
|
|
- sendLink(vo, content);
|
|
|
|
|
|
|
+ sendLink(vo, content,externalUserId);
|
|
|
break;
|
|
break;
|
|
|
case "21":
|
|
case "21":
|
|
|
content.setSendStatus(0);
|
|
content.setSendStatus(0);
|