Przeglądaj źródła

飞书账户管理+接口优化

cgp 16 godzin temu
rodzic
commit
fadd00e401

+ 120 - 0
fs-company/src/main/java/com/fs/company/controller/qw/FeishuAccountController.java

@@ -0,0 +1,120 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.feishu.domain.FeishuAccount;
+import com.fs.feishu.service.IFeishuAccountService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 飞书应用账号管理 Controller
+ */
+@RestController
+@RequestMapping("/qw/feishuAccount")
+public class FeishuAccountController extends BaseController {
+
+    @Autowired
+    private IFeishuAccountService feishuAccountService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询账号列表(分页)
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(FeishuAccount account) {
+        // 获取当前登录用户
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (account!=null&&account.getMyAccount()!=null&&"true".equals(account.getMyAccount())){
+            //如果用户勾选了"我的用户",则只查询当前用户的账户信息
+            account.setCompanyId(loginUser.getCompany().getCompanyId());
+            account.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+        startPage();
+        List<FeishuAccount> list = feishuAccountService.selectFeishuAccountList(account, loginUser.getCompany().getCompanyId());
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID查询账号详情
+     */
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(feishuAccountService.selectFeishuAccountById(id));
+    }
+
+    /**
+     * 新增账号
+     */
+    @Log(title = "飞书应用账号", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FeishuAccount account) {
+        // 设置所属公司(从当前登录用户获取)
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser != null && loginUser.getCompany() != null) {
+            account.setCompanyId(loginUser.getCompany().getCompanyId());
+            account.setCompanyUserId(loginUser.getCompany().getUserId());
+        }
+        return toAjax(feishuAccountService.insertFeishuAccount(account));
+    }
+
+    /**
+     * 修改账号
+     */
+    @Log(title = "飞书应用账号", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FeishuAccount account) {
+        return toAjax(feishuAccountService.updateFeishuAccount(account));
+    }
+
+    /**
+     * 删除账号(支持批量)
+     */
+    @Log(title = "飞书应用账号", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(feishuAccountService.deleteFeishuAccountByIds(ids));
+    }
+
+    /**
+     * 修改状态(启用/禁用)
+     */
+    @Log(title = "飞书应用账号", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody FeishuAccount account) {
+        if (account.getId() == null || account.getStatus() == null) {
+            return AjaxResult.error("参数缺失");
+        }
+        return toAjax(feishuAccountService.changeStatus(
+                account.getId(), account.getStatus(), account.getErrorMsg()));
+    }
+
+    /**
+     * 导出账号列表(可选)
+     */
+    @Log(title = "飞书应用账号", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, FeishuAccount account) throws IOException {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser != null && loginUser.getCompany() != null) {
+            account.setCompanyId(loginUser.getCompany().getCompanyId());
+        }
+        List<FeishuAccount> list = feishuAccountService.selectFeishuAccountList(account);
+        ExcelUtil<FeishuAccount> util = new ExcelUtil<>(FeishuAccount.class);
+        util.exportExcel(response, list, "飞书应用账号数据");
+    }
+}

+ 23 - 0
fs-service/src/main/java/com/fs/feishu/domain/FeishuAccount.java

@@ -4,15 +4,38 @@ import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.util.Date;
 
+/**
+ * 飞书应用账号实体
+ */
 @Data
 public class FeishuAccount {
     private Long id;
     private String appId;
     private String appSecret;
     private Integer status;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date updateTime;
+
     private String errorMsg;
+    private Long companyId;
+    private Long companyUserId;
+    private String phone;
+
+    /** 调用次数*/
+    private Long numberUse;
+
+    //------------非数据库字段--------------------
+    private Long deptId;
+    private String deptName;
+    private String nickName;
+
+    /** 勾选我的客户 true:已勾选,false:未勾选*/
+    private String myAccount;
+
+    /** 是否是我的账户 true:是,false:不是*/
+    private Boolean isMyAccount= false;
 }

+ 27 - 0
fs-service/src/main/java/com/fs/feishu/domain/FeishuLinkError.java

@@ -0,0 +1,27 @@
+package com.fs.feishu.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 飞书链接错误生成记录
+ */
+@Data
+public class FeishuLinkError {
+    private Integer id;
+    private Integer type;           // 1:授权链接,2:课程链接
+    private String corpId;          // 公司主体
+    private Long companyId;      // 公司id
+    private Long companyUserId;  // 销售人员id
+    private Long videoId;        // 视频id
+    private Long courseId;       // 课程id
+    private String link;            // 课程短链
+    private Long qwUserId;       // qw_user主键id
+    private Long qwExternalId;      // 企微外部联系人
+    private String errorInfo;       // 飞书错误信息
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;        // 生成时间
+}

+ 34 - 3
fs-service/src/main/java/com/fs/feishu/mapper/FeishuAccountMapper.java

@@ -8,13 +8,44 @@ import java.util.List;
 
 @Mapper
 public interface FeishuAccountMapper {
+
+    /**
+     * 分页查询(带条件)
+     */
+    List<FeishuAccount> selectFeishuAccountList(FeishuAccount account);
+
+    /**
+     * 根据ID查询
+     */
+    FeishuAccount selectFeishuAccountById(Long id);
+
+    /**
+     * 新增账号
+     */
+    int insertFeishuAccount(FeishuAccount account);
+
+    /**
+     * 修改账号
+     */
+    int updateFeishuAccount(FeishuAccount account);
+
+    /**
+     * 删除账号(物理删除)
+     */
+    int deleteFeishuAccountById(Long id);
+
+    /**
+     * 批量删除
+     */
+    int deleteFeishuAccountByIds(Long[] ids);
+
     /**
-     * 查询所有启用的账号
+     * 查询所有公共账号
      */
-    List<FeishuAccount> selectEnabledAccounts();
+    List<FeishuAccount> selectAllPublicAccounts();
 
     /**
-     * 更新账号状态和错误信息
+     * 更新状态和错误信息
      */
     int updateStatusAndErrorMsg(@Param("id") Long id,
                                 @Param("status") Integer status,

+ 44 - 0
fs-service/src/main/java/com/fs/feishu/mapper/FeishuLinkErrorMapper.java

@@ -0,0 +1,44 @@
+package com.fs.feishu.mapper;
+
+import com.fs.feishu.domain.FeishuLinkError;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 飞书链接错误生成记录 Mapper 接口
+ */
+@Mapper
+public interface FeishuLinkErrorMapper {
+
+    /**
+     * 分页查询(带条件)
+     */
+    List<FeishuLinkError> selectFeishuLinkErrorList(FeishuLinkError linkError);
+
+    /**
+     * 根据主键查询
+     */
+    FeishuLinkError selectFeishuLinkErrorById(Integer id);
+
+    /**
+     * 新增记录
+     */
+    int insertFeishuLinkError(FeishuLinkError linkError);
+
+    /**
+     * 修改记录
+     */
+    int updateFeishuLinkError(FeishuLinkError linkError);
+
+    /**
+     * 根据主键删除
+     */
+    int deleteFeishuLinkErrorById(Integer id);
+
+    /**
+     * 批量删除
+     */
+    int deleteFeishuLinkErrorByIds(Integer[] ids);
+}

+ 230 - 69
fs-service/src/main/java/com/fs/feishu/service/FeiShuService.java

@@ -3,11 +3,16 @@ package com.fs.feishu.service;
 import cn.hutool.json.JSONUtil;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DateUtils;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCourseLink;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsCourseLinkMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
+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.model.FeishuLinkParam;
 import com.fs.feishu.util.FeishuErrorCode;
 import com.fs.system.service.ISysConfigService;
@@ -20,13 +25,10 @@ import org.springframework.stereotype.Component;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
-/**
- * 飞书业务编排层:
- * 负责课程链接和注册链接的完整业务流程,包括缓存、URL 构建、调用底层 API 服务等。
- */
 @Component
 @Slf4j
 public class FeiShuService {
@@ -38,66 +40,144 @@ public class FeiShuService {
     @Autowired
     private FsCourseLinkMapper courseLinkMapper;
     @Autowired
+    private FeishuAccountMapper feishuAccountMapper;
+    @Autowired
+    private FeishuLinkErrorMapper feishuLinkErrorMapper;
+    @Autowired
     private RedisCache redisCache;
     @Autowired
     private FeishuClientPool clientPool;
     @Autowired
     private FeishuDocApiService docApiService;
 
-    /**
-     * 001 生成飞书初始链接(注册授权链接)
-     * 每次 shortCode 唯一
-     */
+    private static final String FEISHU_COMPANY_USER_KEY = "feishu:account_company_user:";
+
+    // ==================== 获取销售个人账号(带缓存) ====================
+
+    public List<FeishuAccount> getFeishuAccountCompanyUser(Long companyId, Long companyUserId) {
+        String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
+        String cached = redisCache.getCacheObject(cacheKey);
+        if (StringUtils.isNotBlank(cached)) {
+            try {
+                return JSONUtil.parseArray(cached).toList(FeishuAccount.class);
+            } catch (Exception e) {
+                log.warn("缓存数据解析失败,key: {}, 将清除缓存", cacheKey, e);
+                redisCache.deleteObject(cacheKey);
+            }
+        }
+        FeishuAccount account = new FeishuAccount();
+        account.setCompanyId(companyId);
+        account.setCompanyUserId(companyUserId);
+        account.setStatus(1);
+        List<FeishuAccount> feishuAccounts = feishuAccountMapper.selectFeishuAccountList(account);
+        if (feishuAccounts.isEmpty()) {
+            redisCache.setCacheObject(cacheKey, "[]", 1, TimeUnit.MINUTES);
+        } else {
+            redisCache.setCacheObject(cacheKey, JSONUtil.toJsonStr(feishuAccounts), 1, TimeUnit.DAYS);
+        }
+        return feishuAccounts;
+    }
+
+    private void clearCompanyUserCache(Long companyId, Long companyUserId) {
+        if (companyId != null && companyUserId != null) {
+            String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
+            redisCache.deleteObject(cacheKey);
+        }
+    }
+
+    // ==================== 生成飞书注册链接 ====================
+
     public String getFeishuRegisterLink(Long videoId, Long companyId, Long courseId,
                                         Long companyUserId, String shortLink) {
-        // 检查功能开关
         CourseConfig config = getCourseConfig();
         if (!Boolean.TRUE.equals(config.getEnableFeishuNewLink())) {
             throw new CustomException("未开启飞书看课");
         }
-
         FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(videoId);
         if (userCourseVideo == null) {
             throw new CustomException("视频不存在: " + videoId);
         }
 
-        int maxRetries = clientPool.getAvailableCount();
-        for (int i = 0; i < maxRetries; i++) {
+        // 1. 尝试销售个人账号创建文档
+        List<FeishuAccount> personalAccounts = getFeishuAccountCompanyUser(companyId, companyUserId);
+        if (!personalAccounts.isEmpty()) {
+            for (FeishuAccount account : personalAccounts) {
+                if (account.getStatus() != 1) continue;
+                Client client = buildClient(account);
+                try {
+                    String docId = docApiService.createDocument(client, userCourseVideo.getTitle() + "-注册");
+                    String authUrl = buildAuthLink(companyId, companyUserId, courseId, videoId, shortLink);
+                    docApiService.createTextLinkBlock(client, docId, authUrl);
+                    docApiService.changeDocumentPermissions(client, docId);
+                    return "https://www.feishu.cn/docx/" + docId;
+                } catch (CustomException e) {
+                    Integer code = e.getCode();
+                    if (code == null) {
+                        recordLinkError(videoId, companyId, courseId, companyUserId, shortLink, e.getMessage(), 1);
+                        return null;
+                    }
+                    if (FeishuErrorCode.isAccountDisable(code)) {
+                        disablePersonalAccount(account, e.getMessage());
+                        log.warn("个人账号[{}]被禁用,尝试下一个", account.getAppId());
+                    } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                        log.warn("个人账号[{}]可重试错误,切换下一个", account.getAppId());
+                    } else {
+                        // 业务错误或其他,记录失败并返回
+                        recordLinkError(videoId, companyId, courseId, companyUserId, shortLink, e.getMessage(), 1);
+                        return null;
+                    }
+                } catch (Exception e) {
+                    recordLinkError(videoId, companyId, courseId, companyUserId, shortLink, e.getMessage(), 1);
+                    return null;
+                }
+            }
+            log.warn("所有个人账号失败,降级到公共账号池");
+        }
+
+        // 2. 公共账号池
+        int retries = clientPool.getAvailableCount();
+        for (int i = 0; i < retries; i++) {
             Client client = clientPool.getClient();
             try {
-                String documentId = docApiService.createDocument(client, userCourseVideo.getTitle() + "-注册");
+                String docId = docApiService.createDocument(client, userCourseVideo.getTitle() + "-注册");
                 String authUrl = buildAuthLink(companyId, companyUserId, courseId, videoId, shortLink);
-                docApiService.createTextLinkBlock(client, documentId, authUrl);
-                docApiService.changeDocumentPermissions(client, documentId);
-                return "https://www.feishu.cn/docx/" + documentId;
+                docApiService.createTextLinkBlock(client, docId, authUrl);
+                docApiService.changeDocumentPermissions(client, docId);
+                return "https://www.feishu.cn/docx/" + docId;
             } catch (CustomException e) {
-                // 如果是账号禁用异常,继续尝试下一个账号
-                if (e.getMessage().contains("不可用") || e.getMessage().contains("已被自动禁用")) {
-                    log.warn("账号不可用,尝试下一个");
-                    continue;
+                Integer code = e.getCode();
+                if (code == null) {
+                    recordLinkError(videoId, companyId, courseId, companyUserId, shortLink, e.getMessage(), 1);
+                    return null;
+                }
+                if (FeishuErrorCode.isAccountDisable(code)) {
+                    clientPool.disableClient(client, e.getMessage());
+                    log.warn("公共账号被禁用,尝试下一个");
+                } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                    log.warn("公共账号可重试错误,切换下一个");
+                } else {
+                    recordLinkError(videoId, companyId, courseId, companyUserId, shortLink, e.getMessage(), 1);
+                    return null;
                 }
-                throw e;
             } catch (Exception e) {
-                throw new CustomException("创建飞书注册链接失败: " + e.getMessage(), e);
+                recordLinkError(videoId, companyId, courseId, companyUserId, shortLink, e.getMessage(), 1);
+                return null;
             }
         }
-        throw new CustomException("所有飞书账号均不可用,请联系管理员");
+        recordLinkError(videoId, companyId, courseId, companyUserId, shortLink, "所有公共账号不可用", 1);
+        return null;
     }
-    /**
-     * 002 生成飞书课程链接
-     * 相同参数可能重复,使用 Redis 缓存避免重复创建文档
-     */
+
+    // ==================== 生成飞书课程链接 ====================
+
     public String getFeishuCourseNewLink(FeishuLinkParam param) {
         if (param == null || param.getCompanyUserId() == null || param.getCompanyId() == null) {
             throw new CustomException("参数不能为空");
         }
-
         String cacheKey = String.format("feishu:course_link:%d:%d:%d:%d:%d:%s",
                 param.getCompanyId(), param.getCompanyUserId(),
                 param.getCourseId(), param.getVideoId(),
                 param.getUserId(), param.getLink());
-
-        // 缓存命中直接返回
         String cached = redisCache.getCacheObject(cacheKey);
         if (StringUtils.isNotBlank(cached)) {
             return cached;
@@ -107,42 +187,138 @@ public class FeiShuService {
         if (userCourseVideo == null) {
             throw new CustomException("视频不存在: " + param.getVideoId());
         }
-        //获取该视频是否有答题
-        String questionBankId = userCourseVideo.getQuestionBankId();
-        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++) {
+        Integer questionFlag = (StringUtils.isNotBlank(userCourseVideo.getQuestionBankId()) &&
+                !"null".equals(userCourseVideo.getQuestionBankId())) ? 1 : 0;
+
+        // 1. 尝试个人账号
+        List<FeishuAccount> personalAccounts = getFeishuAccountCompanyUser(param.getCompanyId(), param.getCompanyUserId());
+        if (!personalAccounts.isEmpty()) {
+            for (FeishuAccount account : personalAccounts) {
+                if (account.getStatus() != 1) continue;
+                Client client = buildClient(account);
+                try {
+                    String docId = docApiService.createDocument(client, userCourseVideo.getTitle());
+                    String courseUrl = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
+                            param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink(), questionFlag);
+                    docApiService.createIframeBlock(client, docId, courseUrl);
+                    docApiService.changeDocumentPermissions(client, docId);
+                    String result = "https://www.feishu.cn/docx/" + docId;
+                    redisCache.setCacheObject(cacheKey, result, 24, TimeUnit.HOURS);
+                    return result;
+                } catch (CustomException e) {
+                    Integer code = e.getCode();
+                    if (code == null) {
+                        recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                                param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                        return null;
+                    }
+                    if (FeishuErrorCode.isAccountDisable(code)) {
+                        disablePersonalAccount(account, e.getMessage());
+                        log.warn("个人账号[{}]被禁用,尝试下一个", account.getAppId());
+                    } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                        log.warn("个人账号[{}]可重试错误,切换下一个", account.getAppId());
+                    } else {
+                        recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                                param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                        return null;
+                    }
+                } catch (Exception e) {
+                    recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                            param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                    return null;
+                }
+            }
+            log.warn("所有个人账号失败,降级到公共账号池");
+        }
+
+        // 2. 公共账号池
+        int retries = clientPool.getAvailableCount();
+        for (int i = 0; i < retries; 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;
+                String docId = docApiService.createDocument(client, userCourseVideo.getTitle());
+                String courseUrl = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
+                        param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink(), questionFlag);
+                docApiService.createIframeBlock(client, docId, courseUrl);
+                docApiService.changeDocumentPermissions(client, docId);
+                String result = "https://www.feishu.cn/docx/" + docId;
+                redisCache.setCacheObject(cacheKey, result, 24, TimeUnit.HOURS);
+                return result;
             } catch (CustomException e) {
-                if (e.getMessage().contains("不可用") || e.getMessage().contains("已被自动禁用")) {
-                    log.warn("账号不可用,尝试下一个");
-                    continue;
+                Integer code = e.getCode();
+                if (code == null) {
+                    recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                            param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                    return null;
+                }
+                if (FeishuErrorCode.isAccountDisable(code)) {
+                    clientPool.disableClient(client, e.getMessage());
+                    log.warn("公共账号被禁用,尝试下一个");
+                } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                    log.warn("公共账号可重试错误,切换下一个");
+                } else {
+                    recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                            param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                    return null;
                 }
-                throw e;
             } catch (Exception e) {
-                throw new CustomException("创建飞书课程链接失败: " + e.getMessage(), e);
+                recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                        param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                return null;
             }
         }
-        throw new CustomException("所有飞书账号均不可用,请联系管理员");
+        recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                param.getCompanyUserId(), param.getLink(), "所有公共账号不可用", 2);
+        return null;
+    }
+
+    // ==================== 辅助方法 ====================
+
+    private Client buildClient(FeishuAccount account) {
+        return Client.newBuilder(account.getAppId(), account.getAppSecret())
+                .logReqAtDebug(true)
+                .build();
     }
 
-    // ==================== URL 构建(纯业务逻辑,无外部调用) ====================
+    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 CourseConfig getCourseConfig() {
+        String json = configService.selectConfigByKey("course.config");
+        if (StringUtils.isBlank(json)) {
+            throw new CustomException("飞书看课配置为空");
+        }
+        return JSONUtil.toBean(json, CourseConfig.class);
+    }
+
+    // ==================== URL构建 ====================
 
     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();
-
         Map<String, Object> courseParam = new LinkedHashMap<>();
         courseParam.put("companyUserId", companyUserId);
         courseParam.put("videoId", videoId);
@@ -150,7 +326,6 @@ public class FeiShuService {
         courseParam.put("courseId", courseId);
         courseParam.put("link", link);
         courseParam.put("questionFlag", questionFlag);
-
         FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(link);
         if (fsCourseLink == null) {
             throw new CustomException("课程链接已过期");
@@ -159,7 +334,6 @@ public class FeiShuService {
         courseParam.put("linkType", fsCourseLink.getLinkType());
         courseParam.put("qwExternalId", fsCourseLink.getQwExternalId());
         courseParam.put("qwUserId", fsCourseLink.getQwUserId());
-
         try {
             String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(courseParam), "UTF-8");
             String fullUrl = config.getFeishuCourseDomain() +
@@ -173,14 +347,12 @@ public class FeiShuService {
     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() +
@@ -190,15 +362,4 @@ public class FeiShuService {
             throw new CustomException("授权URL拼接失败", e);
         }
     }
-
-    /**
-     * 读取课程配置并检查非空
-     */
-    private CourseConfig getCourseConfig() {
-        String json = configService.selectConfigByKey("course.config");
-        if (StringUtils.isBlank(json)) {
-            throw new CustomException("飞书看课配置为空");
-        }
-        return JSONUtil.toBean(json, CourseConfig.class);
-    }
 }

+ 149 - 0
fs-service/src/main/java/com/fs/feishu/service/FeishuAccountServiceImpl.java

@@ -0,0 +1,149 @@
+package com.fs.feishu.service;
+
+import com.fs.common.core.redis.RedisCache;
+import com.fs.feishu.domain.FeishuAccount;
+import com.fs.feishu.mapper.FeishuAccountMapper;
+import com.fs.feishu.service.IFeishuAccountService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+@Slf4j
+@Service
+public class FeishuAccountServiceImpl implements IFeishuAccountService {
+
+    @Autowired
+    private FeishuAccountMapper feishuAccountMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    // 销售人员飞书账号缓存键
+    private static final String FEISHU_COMPANY_USER_KEY = "feishu:account_company_user:";
+
+    @Override
+    public List<FeishuAccount> selectFeishuAccountList(FeishuAccount account,Long companyUserId) {
+        List<FeishuAccount> feishuAccounts = feishuAccountMapper.selectFeishuAccountList(account);
+        feishuAccounts.forEach(item -> {
+            // 1. 调用脱敏方法处理密钥
+            item.setAppSecret(maskSecret(item.getAppSecret()));
+
+            // 2. 判断是否为当前用户的账号
+            if (Objects.equals(item.getCompanyUserId(), companyUserId)){
+                item.setIsMyAccount(true);
+            }
+        });
+        return feishuAccounts;
+    }
+
+    @Override
+    public List<FeishuAccount> selectFeishuAccountList(FeishuAccount account) {
+        return feishuAccountMapper.selectFeishuAccountList(account);
+    }
+
+    @Override
+    public FeishuAccount selectFeishuAccountById(Long id) {
+        return feishuAccountMapper.selectFeishuAccountById(id);
+    }
+
+    @Override
+    public int insertFeishuAccount(FeishuAccount account) {
+        if (account.getStatus() == null) {
+            account.setStatus(1);
+        }
+        int result = feishuAccountMapper.insertFeishuAccount(account);
+        if (result > 0) {
+            clearFeishuCompanyUserCache(account.getCompanyId(), account.getCompanyUserId());
+        }
+        return result;
+    }
+
+    @Override
+    public int updateFeishuAccount(FeishuAccount account) {
+        int result = feishuAccountMapper.updateFeishuAccount(account);
+        if (result > 0) {
+            clearFeishuCompanyUserCache(account.getCompanyId(), account.getCompanyUserId());
+        }
+        return result;
+    }
+
+    @Override
+    public int deleteFeishuAccountById(Long id) {
+        FeishuAccount account = feishuAccountMapper.selectFeishuAccountById(id);
+        if (account != null) {
+            clearFeishuCompanyUserCache(account.getCompanyId(), account.getCompanyUserId());
+        }
+        return feishuAccountMapper.deleteFeishuAccountById(id);
+    }
+
+    @Override
+    public int deleteFeishuAccountByIds(Long[] ids) {
+        if (ids == null || ids.length == 0) {
+            return 0;
+        }
+
+        // 1. 先查询所有待删除账号,收集需要清除的缓存键
+        Set<String> cacheKeysToClear = new HashSet<>();
+        for (Long id : ids) {
+            FeishuAccount account = feishuAccountMapper.selectFeishuAccountById(id);
+            if (account != null && account.getCompanyId() != null && account.getCompanyUserId() != null) {
+                String cacheKey = FEISHU_COMPANY_USER_KEY + account.getCompanyId() + ":" + account.getCompanyUserId();
+                cacheKeysToClear.add(cacheKey);
+            }
+        }
+
+        // 2. 执行批量删除(物理删除)
+        int result = feishuAccountMapper.deleteFeishuAccountByIds(ids);
+
+        // 3. 删除成功后,批量清除缓存
+        if (result > 0 && !cacheKeysToClear.isEmpty()) {
+            for (String key : cacheKeysToClear) {
+                redisCache.deleteObject(key);
+            }
+            log.info("批量删除飞书账号后,已清除 {} 个缓存键", cacheKeysToClear.size());
+        }
+
+        return result;
+    }
+
+    @Override
+    public int changeStatus(Long id, Integer status, String errorMsg) {
+        return feishuAccountMapper.updateStatusAndErrorMsg(id, status, errorMsg);
+    }
+
+    private void clearFeishuCompanyUserCache(Long companyId, Long companyUserId) {
+        if (companyId != null && companyUserId != null) {
+            String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
+            redisCache.deleteObject(cacheKey);
+        }
+    }
+
+    /**
+     * 对密钥进行脱敏处理,保留前3位和后4位,中间替换为*号
+     * 例如: abcdefghijklmnopqrst -> abc***********qrst
+     */
+    private String maskSecret(String secret) {
+        // 1. 防御性判断:如果为空或长度过短,直接返回原值或返回固定星号
+        if (StringUtils.isBlank(secret)) {
+            return "";
+        }
+        if (secret.length() <= 7) {
+            // 长度太短,全部替换为*号
+            return StringUtils.repeat("*", secret.length());
+        }
+
+        // 2. 截取前3位和后4位,中间用*号填充
+        int prefixLen = 3;
+        int suffixLen = 4;
+        int maskLen = secret.length() - prefixLen - suffixLen;
+
+        return secret.substring(0, prefixLen)
+                + StringUtils.repeat("*", maskLen)
+                + secret.substring(secret.length() - suffixLen);
+    }
+}

+ 4 - 3
fs-service/src/main/java/com/fs/feishu/service/FeishuClientPool.java

@@ -16,7 +16,7 @@ import java.util.List;
 
 /**
  * 飞书账号池管理:
- * 1. 启动时从数据库加载所有启用账号,构建 Client 列表
+ * 1. 启动时从数据库加载所有启用公共账号,构建 Client 列表
  * 2. 使用 Redis 循环队列实现跨实例的轮询负载均衡
  * 3. 提供禁用异常账号的能力,同步更新数据库、内存和 Redis 索引
  */
@@ -41,9 +41,10 @@ public class FeishuClientPool {
     }
 
     public synchronized void refresh() {
-        List<FeishuAccount> accounts = feishuAccountMapper.selectEnabledAccounts();
+        //公共账号
+        List<FeishuAccount> accounts = feishuAccountMapper.selectAllPublicAccounts();
         if (accounts.isEmpty()) {
-            throw new CustomException("没有可用的飞书应用账号");
+            log.error("没有可用的公共飞书应用账号");
         }
 
         List<Client> newClients = new ArrayList<>(accounts.size());

+ 4 - 53
fs-service/src/main/java/com/fs/feishu/service/FeishuDocApiService.java

@@ -14,56 +14,25 @@ import com.lark.oapi.service.drive.v2.model.PatchPermissionPublicResp;
 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.Service;
 
 import java.util.ArrayList;
 import java.util.List;
 
-/**
- * 飞书文档 API 适配层:
- * 封装所有对飞书 OpenAPI 的底层调用,不关心账号分配。
- * 当 API 返回账号级错误时,会通知账号池禁用该 Client,并抛出异常。
- */
 @Service
 @Slf4j
 public class FeishuDocApiService {
 
-    @Autowired
-    private FeishuClientPool clientPool;
-
-    /**
-     * 判断错误码是否属于需要禁用应用账号的严重错误
-     */
-    private boolean isAccountError(int code) {
-        // 99991663: 应用已被管理员停用
-        // 99991664: 应用已被管理员删除
-        // 10012   : 应用未开通所需权限
-        return code == 99991663 || code == 99991664 || code == 10012;
-    }
-
     /**
-     * 检查 API 响应,若为账号级错误则禁用当前 Client 并抛出异常,否则只抛出业务异常
+     * 仅检查响应,任何非0错误码都抛出CustomException(携带错误码),
+     * 由上层业务决定如何处理(重试、禁用账号等)。
      */
     private void checkResponse(Client client, int code, String msg) {
-        if (code == 0) return;
-
-        if (FeishuErrorCode.isAccountDisable(code)) {
-            clientPool.disableClient(client, msg);
-            throw new CustomException("账号不可用: " + msg,code);
+        if (code != 0) {
+            throw new CustomException("飞书API错误: " + msg, code);
         }
-        if (FeishuErrorCode.isRateLimit(code)) {
-            throw new CustomException("频率限制: " + msg,code);
-        }
-        if (FeishuErrorCode.isInternalRetryable(code)) {
-            throw new CustomException("内部错误: " + msg,code);
-        }
-        throw new CustomException("飞书API错误: " + msg,code);
     }
 
-    /**
-     * 创建云文档
-     */
     public String createDocument(Client client, String title) throws Exception {
         CreateDocumentReq req = CreateDocumentReq.newBuilder()
                 .createDocumentReqBody(CreateDocumentReqBody.newBuilder().title(title).build())
@@ -73,9 +42,6 @@ public class FeishuDocApiService {
         return resp.getData().getDocument().getDocumentId();
     }
 
-    /**
-     * 在文档中插入 iframe 块
-     */
     public void createIframeBlock(Client client, String documentId, String url) throws Exception {
         CreateDocumentBlockChildrenReq req = CreateDocumentBlockChildrenReq.newBuilder()
                 .documentId(documentId)
@@ -100,9 +66,6 @@ public class FeishuDocApiService {
         checkResponse(client, resp.getCode(), resp.getMsg());
     }
 
-    /**
-     * 在文档中插入 text + link 块(用于“观看课程”超链接)
-     */
     public void createTextLinkBlock(Client client, String documentId, String url) throws Exception {
         CreateDocumentBlockChildrenReq req = CreateDocumentBlockChildrenReq.newBuilder()
                 .documentId(documentId)
@@ -136,9 +99,6 @@ public class FeishuDocApiService {
         checkResponse(client, resp.getCode(), resp.getMsg());
     }
 
-    /**
-     * 修改文档权限为互联网公开可读
-     */
     public void changeDocumentPermissions(Client client, String documentId) throws Exception {
         PatchPermissionPublicReq req = PatchPermissionPublicReq.newBuilder()
                 .token(documentId)
@@ -157,14 +117,10 @@ public class FeishuDocApiService {
         checkResponse(client, resp.getCode(), resp.getMsg());
     }
 
-    /**
-     * 分页获取所有云文档(按创建时间升序排列,便于清理)
-     */
     public List<FeishuFileDTO> listAllFiles(Client client) throws Exception {
         List<FeishuFileDTO> allFiles = new ArrayList<>();
         String pageToken = null;
         int pageSize = 200;
-
         do {
             ListFileReq.Builder builder = ListFileReq.newBuilder()
                     .pageSize(pageSize)
@@ -175,7 +131,6 @@ public class FeishuDocApiService {
             }
             ListFileResp resp = client.drive().v1().file().list(builder.build());
             checkResponse(client, resp.getCode(), resp.getMsg());
-
             if (resp.getData() != null && resp.getData().getFiles() != null) {
                 for (com.lark.oapi.service.drive.v1.model.File file : resp.getData().getFiles()) {
                     FeishuFileDTO dto = new FeishuFileDTO();
@@ -192,13 +147,9 @@ public class FeishuDocApiService {
             }
             pageToken = resp.getData().getNextPageToken();
         } while (StringUtils.isNotBlank(pageToken));
-
         return allFiles;
     }
 
-    /**
-     * 根据 token 和类型删除指定文件
-     */
     public boolean deleteFile(Client client, String fileToken, String type) throws Exception {
         DeleteFileReq req = DeleteFileReq.newBuilder()
                 .fileToken(fileToken)

+ 47 - 0
fs-service/src/main/java/com/fs/feishu/service/IFeishuAccountService.java

@@ -0,0 +1,47 @@
+package com.fs.feishu.service;
+
+import com.fs.feishu.domain.FeishuAccount;
+import java.util.List;
+
+public interface IFeishuAccountService {
+
+    /**
+     * 分页查询账号列表(含条件)
+     */
+    List<FeishuAccount> selectFeishuAccountList(FeishuAccount account,Long companyUserId);
+
+    /**
+     * 查询账号列表
+     */
+    List<FeishuAccount> selectFeishuAccountList(FeishuAccount account);
+
+    /**
+     * 根据ID查询账号
+     */
+    FeishuAccount selectFeishuAccountById(Long id);
+
+    /**
+     * 新增账号
+     */
+    int insertFeishuAccount(FeishuAccount account);
+
+    /**
+     * 修改账号
+     */
+    int updateFeishuAccount(FeishuAccount account);
+
+    /**
+     * 删除账号
+     */
+    int deleteFeishuAccountById(Long id);
+
+    /**
+     * 批量删除账号
+     */
+    int deleteFeishuAccountByIds(Long[] ids);
+
+    /**
+     * 启用/禁用账号
+     */
+    int changeStatus(Long id, Integer status, String errorMsg);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/feishu/util/FeishuErrorCode.java

@@ -36,6 +36,7 @@ public enum FeishuErrorCode {
     INTERNAL_RETRY_55001(55001, ErrorCategory.INTERNAL_RETRYABLE, "服务内部错误"),
     INTERNAL_RETRY_10500(10500, ErrorCategory.INTERNAL_RETRYABLE, "服务端异常"),
     INTERNAL_RETRY_99991663(99991663, ErrorCategory.INTERNAL_RETRYABLE, "无效token"),
+    INTERNAL_RETRY_99991664(99991664, ErrorCategory.INTERNAL_RETRYABLE, "app_access_token 非法"),
 
     // ========== 常见业务错误(不可重试) ==========
     BUSINESS_ERROR_600(600, ErrorCategory.BUSINESS_ERROR, "资源被删除"),
@@ -92,6 +93,10 @@ public enum FeishuErrorCode {
         return fromCode(code).getCategory() == ErrorCategory.INTERNAL_RETRYABLE;
     }
 
+    public static boolean isBusinessError(int code) {
+        return fromCode(code).getCategory() == ErrorCategory.BUSINESS_ERROR;
+    }
+
     public enum ErrorCategory {
         ACCOUNT_DISABLE,   // 账号级错误,需禁用
         RATE_LIMIT,        // 频率限制,需等待后重试

+ 152 - 4
fs-service/src/main/resources/mapper/his/FeishuAccountMapper.xml

@@ -2,14 +2,162 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.feishu.mapper.FeishuAccountMapper">
-    <select id="selectEnabledAccounts" resultType="com.fs.feishu.domain.FeishuAccount">
-        SELECT id, app_id, app_secret, status, create_time, update_time,error_msg
+
+    <resultMap id="BaseResultMap" type="com.fs.feishu.domain.FeishuAccount">
+        <id property="id" column="id"/>
+        <result property="appId" column="app_id"/>
+        <result property="appSecret" column="app_secret"/>
+        <result property="status" column="status"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="errorMsg" column="error_msg"/>
+        <result property="companyId" column="company_id"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="phone" column="phone"/>
+        <result property="numberUse" column="number_use"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        select id, app_id, app_secret, status, create_time, update_time, error_msg,
+        company_id, company_user_id, phone, number_use FROM feishu_account
+    </sql>
+
+    <!-- 查询条件(不变) -->
+    <sql id="whereClause">
+        <where>
+            <if test="appId != null and appId != ''">
+                AND app_id like concat('%', #{appId}, '%')
+            </if>
+            <if test="status != null">
+                AND status = #{status}
+            </if>
+            <if test="phone != null and phone != ''">
+                AND phone like concat('%', #{phone}, '%')
+            </if>
+            <if test="companyId != null">
+                AND company_id = #{companyId}
+            </if>
+            <if test="companyUserId != null">
+                AND company_user_id = #{companyUserId}
+            </if>
+        </where>
+    </sql>
+
+    <!-- 分页查询(不变) -->
+    <select id="selectFeishuAccountList" resultMap="BaseResultMap">
+        SELECT fa.id, fa.app_id, fa.app_secret, fa.status, fa.create_time, fa.update_time, fa.error_msg,
+        fa.company_id, fa.company_user_id, fa.phone, fa.number_use,cu.nick_name as nickName,cd.dept_name as deptName
+        FROM feishu_account fa
+            left join company_user cu on fa.company_user_id = cu.user_id
+            left join company_dept cd on cd.dept_id = cu.dept_id
+        <where>
+            <if test="appId != null and appId != ''">
+                AND fa.app_id like concat('%', #{appId}, '%')
+            </if>
+            <if test="status != null">
+                AND fa.status = #{status}
+            </if>
+            <if test="phone != null and phone != ''">
+                AND fa.phone like concat('%', #{phone}, '%')
+            </if>
+            <if test="companyId != null">
+                AND fa.company_id = #{companyId}
+            </if>
+            <if test="companyUserId != null">
+                AND fa.company_user_id = #{companyUserId}
+            </if>
+            <if test="deptId != null">
+                AND cd.dept_id =#{deptId}
+            </if>
+            <if test="nickName != null and nickName != ''">
+                AND cu.nick_name like concat('%', #{nickName}, '%')
+            </if>
+        </where>
+        ORDER BY fa.create_time DESC
+    </select>
+
+    <!-- 根据ID查询(不变) -->
+    <select id="selectFeishuAccountById" resultMap="BaseResultMap">
+        <include refid="Base_Column_List"/>
+        WHERE id = #{id}
+    </select>
+
+    <!-- 动态插入(核心修改) -->
+    <insert id="insertFeishuAccount" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO feishu_account
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="appId != null and appId != ''">app_id,</if>
+            <if test="appSecret != null and appSecret != ''">app_secret,</if>
+            <if test="status != null">status,</if>
+            <if test="errorMsg != null">error_msg,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="phone != null and phone != ''">phone,</if>
+            <if test="numberUse != null">number_use,</if>
+        </trim>
+        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
+            <if test="appId != null and appId != ''">#{appId},</if>
+            <if test="appSecret != null and appSecret != ''">#{appSecret},</if>
+            <if test="status != null">#{status},</if>
+            <if test="errorMsg != null">#{errorMsg},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="phone != null and phone != ''">#{phone},</if>
+            <if test="numberUse != null">#{numberUse},</if>
+        </trim>
+    </insert>
+
+    <!-- 动态更新(已包含 number_use) -->
+    <update id="updateFeishuAccount">
+        UPDATE feishu_account
+        <set>
+            <if test="appId != null and appId != ''">app_id = #{appId},</if>
+            <if test="appSecret != null and appSecret != ''">app_secret = #{appSecret},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="errorMsg != null">error_msg = #{errorMsg},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="phone != null and phone != ''">phone = #{phone},</if>
+            <if test="numberUse != null">number_use = #{numberUse},</if>
+        </set>
+        WHERE id = #{id}
+    </update>
+
+    <delete id="deleteFeishuAccountById">
+        DELETE FROM feishu_account WHERE id = #{id}
+    </delete>
+
+    <delete id="deleteFeishuAccountByIds">
+        DELETE FROM feishu_account WHERE id IN
+        <foreach collection="array" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <!-- 查询公共账号 -->
+    <select id="selectAllPublicAccounts" resultMap="BaseResultMap">
+        SELECT id,
+               app_id,
+               app_secret,
+               STATUS,
+               create_time,
+               update_time,
+               error_msg,
+               company_id,
+               company_user_id,
+               phone,
+               number_use
         FROM feishu_account
-        WHERE status = 1
+        WHERE STATUS = 1
+          AND phone IS NULL
+          AND company_id IS NULL
+          AND company_user_id IS NULL
     </select>
 
     <update id="updateStatusAndErrorMsg">
-        UPDATE feishu_account SET status = #{status}, error_msg = #{errorMsg}
+        UPDATE feishu_account
+        SET status = #{status}, error_msg = #{errorMsg}
         WHERE id = #{id}
     </update>
+
 </mapper>

+ 139 - 0
fs-service/src/main/resources/mapper/his/FeishuLinkErrorMapper.xml

@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.feishu.mapper.FeishuLinkErrorMapper">
+
+    <resultMap id="BaseResultMap" type="com.fs.feishu.domain.FeishuLinkError">
+        <id property="id" column="id"/>
+        <result property="type" column="type"/>
+        <result property="corpId" column="corp_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="videoId" column="video_id"/>
+        <result property="courseId" column="course_id"/>
+        <result property="link" column="link"/>
+        <result property="qwUserId" column="qw_user_id"/>
+        <result property="qwExternalId" column="qw_external_id"/>
+        <result property="errorInfo" column="error_info"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        SELECT id, type, corp_id, company_id, company_user_id, video_id, course_id,
+        link, qw_user_id, qw_external_id, error_info, create_time FROM feishu_link_error
+    </sql>
+
+    <!-- 通用查询条件 -->
+    <sql id="whereClause">
+        <where>
+            <if test="type != null">
+                AND type = #{type}
+            </if>
+            <if test="corpId != null and corpId != ''">
+                AND corp_id = #{corpId}
+            </if>
+            <if test="companyId != null">
+                AND company_id = #{companyId}
+            </if>
+            <if test="companyUserId != null">
+                AND company_user_id = #{companyUserId}
+            </if>
+            <if test="videoId != null">
+                AND video_id = #{videoId}
+            </if>
+            <if test="courseId != null">
+                AND course_id = #{courseId}
+            </if>
+            <if test="link != null and link != ''">
+                AND link like concat('%', #{link}, '%')
+            </if>
+            <if test="qwUserId != null">
+                AND qw_user_id = #{qwUserId}
+            </if>
+            <if test="qwExternalId != null">
+                AND qw_external_id = #{qwExternalId}
+            </if>
+            <if test="errorInfo != null and errorInfo != ''">
+                AND error_info like concat('%', #{errorInfo}, '%')
+            </if>
+            <!-- 如果有时间范围查询,可以额外增加参数,此处省略 -->
+        </where>
+    </sql>
+
+    <!-- 分页查询(支持条件) -->
+    <select id="selectFeishuLinkErrorList" resultMap="BaseResultMap">
+        <include refid="Base_Column_List"/>
+        <include refid="whereClause"/>
+        ORDER BY create_time DESC
+    </select>
+
+    <!-- 根据ID查询 -->
+    <select id="selectFeishuLinkErrorById" resultMap="BaseResultMap">
+        <include refid="Base_Column_List"/>
+        WHERE id = #{id}
+    </select>
+
+    <!-- 动态插入(只插入非空字段) -->
+    <insert id="insertFeishuLinkError" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO feishu_link_error
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="type != null">type,</if>
+            <if test="corpId != null and corpId != ''">corp_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="videoId != null">video_id,</if>
+            <if test="courseId != null">course_id,</if>
+            <if test="link != null and link != ''">link,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="qwExternalId != null">qw_external_id,</if>
+            <if test="errorInfo != null and errorInfo != ''">error_info,</if>
+            <if test="createTime != null">create_time,</if>
+        </trim>
+        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
+            <if test="type != null">#{type},</if>
+            <if test="corpId != null and corpId != ''">#{corpId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="videoId != null">#{videoId},</if>
+            <if test="courseId != null">#{courseId},</if>
+            <if test="link != null and link != ''">#{link},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="qwExternalId != null">#{qwExternalId},</if>
+            <if test="errorInfo != null and errorInfo != ''">#{errorInfo},</if>
+            <if test="createTime != null">#{createTime},</if>
+        </trim>
+    </insert>
+
+    <!-- 动态更新 -->
+    <update id="updateFeishuLinkError">
+        UPDATE feishu_link_error
+        <set>
+            <if test="type != null">type = #{type},</if>
+            <if test="corpId != null and corpId != ''">corp_id = #{corpId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="videoId != null">video_id = #{videoId},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="link != null and link != ''">link = #{link},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="qwExternalId != null">qw_external_id = #{qwExternalId},</if>
+            <if test="errorInfo != null and errorInfo != ''">error_info = #{errorInfo},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+        </set>
+        WHERE id = #{id}
+    </update>
+
+    <!-- 单个删除 -->
+    <delete id="deleteFeishuLinkErrorById">
+        DELETE FROM feishu_link_error WHERE id = #{id}
+    </delete>
+
+    <!-- 批量删除 -->
+    <delete id="deleteFeishuLinkErrorByIds">
+        DELETE FROM feishu_link_error WHERE id IN
+        <foreach collection="array" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+</mapper>