ソースを参照

修改sop发飞书逻辑

cgp 3 日 前
コミット
0034e5edbd

+ 61 - 0
fs-ipad-task/src/main/java/com/fs/app/service/FeishuAsyncService.java

@@ -0,0 +1,61 @@
+package com.fs.app.service;
+
+import com.fs.common.exception.CustomException;
+import com.fs.feishu.domain.FeishuAccount;
+import com.fs.feishu.service.FeiShuService;
+import com.fs.feishu.service.FeishuDocApiService;
+import com.fs.feishu.util.FeishuErrorCode;
+import com.lark.oapi.Client;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+/**
+ * 飞书异步任务
+ * */
+@Slf4j
+@Service
+public class FeishuAsyncService {
+    @Autowired
+    private FeishuDocApiService docApiService;
+
+    @Autowired
+    private FeiShuService feiShuService;
+    @Async // 开启异步任务
+    public void processFeishuDocAsync(Client client, String docId, String authUrl,
+                                      Long videoId, Long companyId, Long courseId,
+                                      Long companyUserId, String shortLink,
+                                      FeishuAccount account) {
+        try {
+            docApiService.createTextLinkBlock(client, docId, authUrl);
+            docApiService.changeDocumentPermissions(client, docId);
+            // 成功时无需记录错误
+        } catch (CustomException e) {
+            // 调用原Service的方法处理异常
+            Integer code = e.getCode();
+            if (code == null) {
+                feiShuService.recordLinkError(videoId, companyId, courseId,
+                        companyUserId, shortLink,
+                        e.getMessage(), 1);
+                return;
+            }
+            if (FeishuErrorCode.isAccountDisable(code)) {
+                feiShuService.disablePersonalAccount(account, e.getMessage());
+                log.warn("个人账号[{}]被禁用,尝试下一个", account.getAppId());
+            } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                log.warn("个人账号[{}]可重试错误,切换下一个", account.getAppId());
+                feiShuService.recordLinkError(videoId, companyId, courseId,
+                        companyUserId, shortLink,
+                        e.getMessage(), 1);
+            } else {
+                feiShuService.recordLinkError(videoId, companyId, courseId,
+                        companyUserId, shortLink,
+                        e.getMessage(), 1);
+            }
+        } catch (Exception e) {
+            feiShuService.recordLinkError(videoId, companyId, courseId,
+                    companyUserId, shortLink,
+                    e.getMessage(), 1);
+        }
+    }
+}

+ 203 - 4
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -1,11 +1,14 @@
 package com.fs.app.service;
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
 import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.date.DateUtil;
 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.service.ICompanyMiniappService;
 import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsCourseLink;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 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.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.FsSignArticle;
 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.impl.QwSopLogsServiceImpl;
 import com.fs.system.service.ISysConfigService;
+import com.lark.oapi.Client;
 import com.fs.wxwork.dto.*;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -91,6 +107,24 @@ public class IpadSendServer {
     @Autowired
     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;
 
 
@@ -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()
                 .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) {
         FileVo videoVo = FileVo.builder()
                 .url(content.getVideoUrl())
@@ -507,7 +706,7 @@ public class IpadSendServer {
         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();
         vo.setId(Long.parseLong(qwSopLogs.getId()));
         vo.setRoom(qwSopLogs.getSendType() == 12);
@@ -529,7 +728,7 @@ public class IpadSendServer {
                     break;
                 case "3":
                     // 链接
-                    sendLink(vo, content);
+                    sendLink(vo, content, externalUserId);
                     break;
                 case "4":
                 case "10":
@@ -621,7 +820,7 @@ public class IpadSendServer {
                     break;
                 case "18":
                     // 链接
-                    sendLink(vo, content);
+                    sendLink(vo, content,externalUserId);
                     break;
                 case "21":
                     content.setSendStatus(0);

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -246,7 +246,7 @@ public class SendMsg {
                     content.setSendRemarks("发送次数达到上限");
                 } else {
                     // 发送
-                    sendServer.send(content, user, qwSopLogs, miniMap, parentVo);
+                    sendServer.send(content, user, qwSopLogs, miniMap, parentVo,customerId);
                     //判断销售推送成功:保存记录
                     if (content.getSendStatus() != 2) {
                         QwRestrictionPushRecord qrpr = new QwRestrictionPushRecord();

+ 106 - 0
fs-user-app/src/main/java/com/fs/app/controller/FeishuTestController.java

@@ -0,0 +1,106 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.exception.CustomException;
+import com.fs.feishu.service.FeishuDocApiService;
+import com.lark.oapi.Client;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/test/feishu")
+@Slf4j
+public class FeishuTestController {
+
+    @Autowired
+    private FeishuDocApiService docApiService;
+
+    @PostMapping("/batchCreateDocument")
+    public AjaxResult batchCreateDocument(@RequestBody TestRequest request) {
+        String appId = request.getAppId();
+        String appSecret = request.getAppSecret();
+        Integer times = request.getTimes() == null ? 1 : request.getTimes();
+        String titlePrefix = request.getTitlePrefix() == null ? "测试文档222" : request.getTitlePrefix();
+
+        if (times > 100) {
+            return AjaxResult.error("次数不能超过100");
+        }
+
+        Client client = Client.newBuilder(request.getAppId(), request.getAppSecret())
+                .logReqAtDebug(true)
+                .build();
+
+        List<Map<String, Object>> results = new ArrayList<>(times);
+        long totalStart = System.currentTimeMillis();  // 整体开始时间
+
+        for (int i = 1; i <= times; i++) {
+            Map<String, Object> result = new HashMap<>();
+            result.put("index", i);
+            String title = titlePrefix + "-" + System.currentTimeMillis() + "-" + i;
+
+            long startTime = System.currentTimeMillis();  // 单次开始时间
+            try {
+                String docId = docApiService.createDocument(client, title);
+                docApiService.changeDocumentPermissions(client, docId);
+                long endTime = System.currentTimeMillis();
+                long durationMs = endTime - startTime;
+
+                result.put("success", true);
+                result.put("documentId", docId);
+                result.put("url", "https://www.feishu.cn/docx/" + docId);
+                result.put("durationMs", durationMs);
+            } catch (Exception e) {
+                long endTime = System.currentTimeMillis();
+                long durationMs = endTime - startTime;
+
+                result.put("success", false);
+                result.put("error", e.getMessage());
+                if (e instanceof CustomException) {
+                    result.put("errorCode", ((CustomException) e).getCode());
+                }
+                result.put("durationMs", durationMs);
+            }
+            results.add(result);
+        }
+
+        long totalEnd = System.currentTimeMillis();
+        long totalDuration = totalEnd - totalStart;
+
+        // 可选:在返回结果中附加汇总信息
+        Map<String, Object> response = new HashMap<>();
+        response.put("details", results);
+        response.put("totalDurationMs", totalDuration);
+        response.put("totalCount", times);
+        // 统计成功/失败数量
+        long successCount = results.stream().filter(r -> Boolean.TRUE.equals(r.get("success"))).count();
+        response.put("successCount", successCount);
+        response.put("failCount", times - successCount);
+
+        return AjaxResult.success(response);
+    }
+
+    public static class TestRequest {
+        private String appId;
+        private String appSecret;
+        private Integer times;
+        private String titlePrefix;
+
+        public String getAppId() { return appId; }
+        public void setAppId(String appId) { this.appId = appId; }
+        public String getAppSecret() { return appSecret; }
+        public void setAppSecret(String appSecret) { this.appSecret = appSecret; }
+        public Integer getTimes() { return times; }
+        public void setTimes(Integer times) { this.times = times; }
+        public String getTitlePrefix() { return titlePrefix; }
+        public void setTitlePrefix(String titlePrefix) { this.titlePrefix = titlePrefix; }
+    }
+}