瀏覽代碼

update 飞书看课多账号

ct 5 天之前
父節點
當前提交
acc5b9d575

+ 113 - 0
fs-admin/src/main/java/com/fs/course/controller/FsFeishuConfigController.java

@@ -0,0 +1,113 @@
+package com.fs.course.controller;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+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.bean.BeanUtils;
+import com.fs.feishu.domain.FsFeishuConfig;
+import com.fs.feishu.param.FsFeishuConfigCreateParam;
+import com.fs.feishu.param.FsFeishuConfigEditParam;
+import com.fs.feishu.service.IFsFeishuConfigService;
+import com.github.pagehelper.PageHelper;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.*;
+import java.util.Date;
+
+/**
+ * 飞书应用配置
+ */
+@RestController
+@RequestMapping("/course/feishuConfig")
+@AllArgsConstructor
+public class FsFeishuConfigController extends BaseController {
+
+    private final IFsFeishuConfigService fsFeishuConfigService;
+
+    @PreAuthorize("@ss.hasPermi('course:feishuConfig:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String name,
+                              @RequestParam(required = false) String appId,
+                              @RequestParam(required = false) Integer status,
+                              @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                              @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> params = new HashMap<>();
+        params.put("name", name);
+        params.put("appId", appId);
+        params.put("status", status);
+        PageHelper.startPage(pageNum, pageSize);
+        List<FsFeishuConfig> list = fsFeishuConfigService.selectListByMap(params);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:feishuConfig:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(fsFeishuConfigService.getById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:feishuConfig:add')")
+    @Log(title = "飞书配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Valid @RequestBody FsFeishuConfigCreateParam param) {
+        FsFeishuConfig exist = fsFeishuConfigService.getOne(
+                Wrappers.<FsFeishuConfig>lambdaQuery()
+                        .eq(FsFeishuConfig::getAppId, param.getAppId())
+                        .eq(FsFeishuConfig::getIsDel, 0)
+                        .last("limit 1"));
+        if (exist != null) {
+            return AjaxResult.error("appId已存在");
+        }
+        FsFeishuConfig config = new FsFeishuConfig();
+        BeanUtils.copyProperties(param, config);
+        config.setIsDel(0);
+        config.setCreateTime(new Date());
+        config.setUpdateTime(new Date());
+        fsFeishuConfigService.save(config);
+        fsFeishuConfigService.refreshNormalConfigCache();
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:feishuConfig:edit')")
+    @Log(title = "飞书配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Valid @RequestBody FsFeishuConfigEditParam param) {
+        FsFeishuConfig config = fsFeishuConfigService.getById(param.getId());
+        if (config == null || Objects.equals(config.getIsDel(), 1)) {
+            return AjaxResult.error("飞书配置不存在");
+        }
+        FsFeishuConfig exist = fsFeishuConfigService.getOne(
+                Wrappers.<FsFeishuConfig>lambdaQuery()
+                        .eq(FsFeishuConfig::getAppId, param.getAppId())
+                        .eq(FsFeishuConfig::getIsDel, 0)
+                        .last("limit 1"));
+        if (exist != null && !exist.getId().equals(config.getId())) {
+            return AjaxResult.error("appId已存在");
+        }
+        BeanUtils.copyProperties(param, config);
+        config.setUpdateTime(new Date());
+        fsFeishuConfigService.updateById(config);
+        fsFeishuConfigService.refreshNormalConfigCache();
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:feishuConfig:remove')")
+    @Log(title = "飞书配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        Wrapper<FsFeishuConfig> updateWrapper = Wrappers.<FsFeishuConfig>lambdaUpdate()
+                .set(FsFeishuConfig::getIsDel, 1)
+                .set(FsFeishuConfig::getUpdateTime, new Date())
+                .in(FsFeishuConfig::getId, Arrays.asList(ids));
+        fsFeishuConfigService.update(updateWrapper);
+        fsFeishuConfigService.refreshNormalConfigCache();
+        return AjaxResult.success();
+    }
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/constant/CourseConstant.java

@@ -8,6 +8,9 @@ public class CourseConstant {
     /** 小程序业务域名缓存 key 前缀:course:playSource:businessDomain:{appId} */
     public static final String PLAY_SOURCE_BUSINESS_DOMAIN = "course:playSource:businessDomain:";
 
+    /** 飞书正常状态应用配置列表缓存 */
+    public static final String FEISHU_NORMAL_CONFIG_LIST = "feishu:config:normal:list";
+
     public static String getFeiShuWatchHeartKey(Long userId, Long videoId, Long companyUserId, Long periodId) {
         return String.format(FEI_SHU_WATCH_HEART + "%s:%s:%s:%s", userId, videoId, companyUserId, periodId);
     }

+ 45 - 0
fs-service/src/main/java/com/fs/feishu/domain/FsFeishuConfig.java

@@ -0,0 +1,45 @@
+package com.fs.feishu.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 飞书应用配置
+ */
+@Data
+@TableName("fs_feishu_config")
+public class FsFeishuConfig {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 飞书账号名称 */
+    private String name;
+
+    /** 飞书应用 AppId */
+    private String appId;
+
+    /** 飞书应用 AppSecret */
+    private String appSecret;
+
+    /**
+     * 状态:-1已封禁 0停用 1正常
+     */
+    private Integer status;
+
+    /** 是否删除 0正常 1删除 */
+    private Integer isDel;
+
+    private String remark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/feishu/mapper/FsFeishuConfigMapper.java

@@ -0,0 +1,22 @@
+package com.fs.feishu.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.feishu.domain.FsFeishuConfig;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface FsFeishuConfigMapper extends BaseMapper<FsFeishuConfig> {
+
+    /**
+     * 查询全部正常状态的飞书配置
+     */
+    @Select("select * from fs_feishu_config where is_del = 0 and status = 1")
+    List<FsFeishuConfig> selectNormalList();
+
+    @Select("select * from fs_feishu_config where is_del = 0 and app_id = #{appId} limit 1")
+    FsFeishuConfig selectByAppId(@Param("appId") String appId);
+}

+ 30 - 0
fs-service/src/main/java/com/fs/feishu/param/FsFeishuConfigCreateParam.java

@@ -0,0 +1,30 @@
+package com.fs.feishu.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class FsFeishuConfigCreateParam {
+
+    @NotBlank(message = "飞书账号不能为空")
+    @ApiModelProperty("飞书账号名称")
+    private String name;
+
+    @NotBlank(message = "appId不能为空")
+    @ApiModelProperty("飞书应用 AppId")
+    private String appId;
+
+    @NotBlank(message = "appSecret不能为空")
+    @ApiModelProperty("飞书应用 AppSecret")
+    private String appSecret;
+
+    @NotNull(message = "状态不能为空")
+    @ApiModelProperty("状态:-1已封禁 0停用 1正常")
+    private Integer status;
+
+    @ApiModelProperty("备注")
+    private String remark;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/feishu/param/FsFeishuConfigEditParam.java

@@ -0,0 +1,34 @@
+package com.fs.feishu.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Data
+public class FsFeishuConfigEditParam {
+
+    @NotNull(message = "主键不能为空")
+    @ApiModelProperty("主键")
+    private Long id;
+
+    @NotBlank(message = "飞书账号不能为空")
+    @ApiModelProperty("飞书账号名称")
+    private String name;
+
+    @NotBlank(message = "appId不能为空")
+    @ApiModelProperty("飞书应用 AppId")
+    private String appId;
+
+    @NotBlank(message = "appSecret不能为空")
+    @ApiModelProperty("飞书应用 AppSecret")
+    private String appSecret;
+
+    @NotNull(message = "状态不能为空")
+    @ApiModelProperty("状态:-1已封禁 0停用 1正常")
+    private Integer status;
+
+    @ApiModelProperty("备注")
+    private String remark;
+}

+ 22 - 16
fs-service/src/main/java/com/fs/feishu/service/FeiShuService.java

@@ -1,19 +1,18 @@
 package com.fs.feishu.service;
 
 import cn.hutool.json.JSONUtil;
-import com.fs.feishu.config.FeiShuConfig;
-import com.fs.feishu.util.SecureTokenUtil;
 import com.fs.common.exception.CustomException;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.feishu.domain.FsFeishuConfig;
+import com.fs.feishu.util.SecureTokenUtil;
 import com.fs.system.service.ISysConfigService;
 import com.lark.oapi.Client;
 import com.lark.oapi.service.docx.v1.model.*;
 import com.lark.oapi.service.drive.v2.model.PatchPermissionPublicReq;
 import com.lark.oapi.service.drive.v2.model.PermissionPublic;
 import org.springframework.beans.factory.annotation.Autowired;
-import javax.annotation.PostConstruct;
 import org.springframework.stereotype.Component;
 
 @Component
@@ -22,17 +21,21 @@ public class FeiShuService {
     private final String COURSE_PATH = "/feishu/pages_course/videovip?token=%s";
 
     @Autowired
-    private FeiShuConfig feishuConfig;
+    private IFsFeishuConfigService fsFeishuConfigService;
     @Autowired
     private ISysConfigService configService;
     @Autowired
     private FsUserCourseVideoMapper videoMapper;
 
-    private Client client;
-
-    @PostConstruct
-    public void init() {
-        this.client = Client.newBuilder(feishuConfig.getAppId(), feishuConfig.getAppSecret()).logReqAtDebug(true).build();
+    /**
+     * 随机取一条状态为「正常」的飞书配置并构建 Client
+     */
+    private Client getClient() {
+        FsFeishuConfig config = fsFeishuConfigService.getRandomNormalConfig();
+        if (config == null) {
+            throw new CustomException("无可用的飞书应用配置(需状态为正常)");
+        }
+        return Client.newBuilder(config.getAppId(), config.getAppSecret()).logReqAtDebug(true).build();
     }
 
     /**
@@ -42,27 +45,30 @@ public class FeiShuService {
         if (companyUserId == null || videoId == null) {
             throw new CustomException("用户ID和视频ID不能为空");
         }
-        
+
         FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(videoId);
         if (userCourseVideo == null) {
             throw new CustomException("视频不存在: " + videoId);
         }
 
         try {
+            Client client = getClient();
             // 创建云文档
-            String documentId = createDocument(userCourseVideo.getTitle());
+            String documentId = createDocument(client, userCourseVideo.getTitle());
 
             // 拼接看课url
             String url = buildCourseLink(companyUserId, videoId, periodId);
 
             // 创建iframe块
-            createIframeBlock(documentId, url);
+            createIframeBlock(client, documentId, url);
 
             // 更新文档权限
-            changeDocumentPermissions(documentId);
+            changeDocumentPermissions(client, documentId);
 
             // 返回飞书看课链接
             return "https://www.feishu.cn/docx/" + documentId;
+        } catch (CustomException e) {
+            throw e;
         } catch (Exception e) {
             throw new CustomException("创建飞书课程链接失败: " + e.getMessage(), e);
         }
@@ -71,7 +77,7 @@ public class FeiShuService {
     /**
      * 创建云文档
      */
-    private String createDocument(String title) throws Exception {
+    private String createDocument(Client client, String title) throws Exception {
         CreateDocumentReq req = CreateDocumentReq.newBuilder()
                 .createDocumentReqBody(CreateDocumentReqBody.newBuilder().title(title).build())
                 .build();
@@ -98,7 +104,7 @@ public class FeiShuService {
     /**
      * 创建iframe块
      */
-    private void createIframeBlock(String documentId, String url) throws Exception {
+    private void createIframeBlock(Client client, String documentId, String url) throws Exception {
         CreateDocumentBlockChildrenReq req = CreateDocumentBlockChildrenReq.newBuilder()
                 .documentId(documentId)
                 .blockId(documentId)
@@ -126,7 +132,7 @@ public class FeiShuService {
     /**
      * 修改文档权限
      */
-    private void changeDocumentPermissions(String documentId) throws Exception {
+    private void changeDocumentPermissions(Client client, String documentId) throws Exception {
         PatchPermissionPublicReq req = PatchPermissionPublicReq.newBuilder()
                 .token(documentId)
                 .type("docx")

+ 22 - 0
fs-service/src/main/java/com/fs/feishu/service/IFsFeishuConfigService.java

@@ -0,0 +1,22 @@
+package com.fs.feishu.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.feishu.domain.FsFeishuConfig;
+
+import java.util.List;
+import java.util.Map;
+
+public interface IFsFeishuConfigService extends IService<FsFeishuConfig> {
+
+    List<FsFeishuConfig> selectListByMap(Map<String, Object> params);
+
+    /**
+     * 随机获取一条状态为正常的飞书配置(优先 Redis,未命中再查库并缓存 24 小时)
+     */
+    FsFeishuConfig getRandomNormalConfig();
+
+    /**
+     * 刷新正常状态飞书配置列表缓存
+     */
+    void refreshNormalConfigCache();
+}

+ 86 - 0
fs-service/src/main/java/com/fs/feishu/service/impl/FsFeishuConfigServiceImpl.java

@@ -0,0 +1,86 @@
+package com.fs.feishu.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.course.constant.CourseConstant;
+import com.fs.feishu.domain.FsFeishuConfig;
+import com.fs.feishu.mapper.FsFeishuConfigMapper;
+import com.fs.feishu.service.IFsFeishuConfigService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+public class FsFeishuConfigServiceImpl extends ServiceImpl<FsFeishuConfigMapper, FsFeishuConfig>
+        implements IFsFeishuConfigService {
+
+    /** 状态:正常 */
+    public static final int STATUS_NORMAL = 1;
+
+    private static final int CACHE_HOURS = 24;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Override
+    public List<FsFeishuConfig> selectListByMap(Map<String, Object> params) {
+        LambdaQueryWrapper<FsFeishuConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(FsFeishuConfig::getIsDel, 0);
+        if (params != null) {
+            Object name = params.get("name");
+            if (name != null && StringUtils.isNotBlank(name.toString())) {
+                wrapper.like(FsFeishuConfig::getName, name.toString());
+            }
+            Object appId = params.get("appId");
+            if (appId != null && StringUtils.isNotBlank(appId.toString())) {
+                wrapper.like(FsFeishuConfig::getAppId, appId.toString());
+            }
+            Object status = params.get("status");
+            if (status != null && StringUtils.isNotBlank(status.toString())) {
+                wrapper.eq(FsFeishuConfig::getStatus, Integer.valueOf(status.toString()));
+            }
+        }
+        wrapper.orderByDesc(FsFeishuConfig::getId);
+        return list(wrapper);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public FsFeishuConfig getRandomNormalConfig() {
+        List<FsFeishuConfig> list = redisCache.getCacheObject(CourseConstant.FEISHU_NORMAL_CONFIG_LIST);
+        if (list == null) {
+            list = baseMapper.selectNormalList();
+            if (list == null) {
+                list = Collections.emptyList();
+            }
+            redisCache.setCacheObject(CourseConstant.FEISHU_NORMAL_CONFIG_LIST, new ArrayList<>(list), CACHE_HOURS, TimeUnit.HOURS);
+            log.info("飞书正常配置缓存未命中,已回源入库并缓存{}小时,数量={}", CACHE_HOURS, list.size());
+        }
+        if (CollectionUtils.isEmpty(list)) {
+            return null;
+        }
+        int index = ThreadLocalRandom.current().nextInt(list.size());
+        return list.get(index);
+    }
+
+    @Override
+    public void refreshNormalConfigCache() {
+        List<FsFeishuConfig> list = baseMapper.selectNormalList();
+        if (list == null) {
+            list = Collections.emptyList();
+        }
+        redisCache.setCacheObject(CourseConstant.FEISHU_NORMAL_CONFIG_LIST, new ArrayList<>(list), CACHE_HOURS, TimeUnit.HOURS);
+        log.info("刷新飞书正常配置缓存,数量={}", list.size());
+    }
+}

+ 0 - 4
fs-service/src/main/resources/application-config-dev.yml

@@ -120,7 +120,3 @@ jst:
   authorization_code: 666666
   shop_code: "18461733"
 
-# 飞书
-feishu:
-  appId: "cli_a92ff75ce0785bc0"
-  appSecret: "DCQPW11pTY48zSdwAnqPwhJfihe0kP8G"

+ 4 - 4
fs-service/src/main/resources/application-druid-jnmy-test.yml

@@ -243,8 +243,8 @@ wechat:
 #        redirectUri: http://ta6d97ec.natappfree.cc/callback
 #    isNeedScan: true
 
-# 飞书
-feishu:
-    appId: "cli_aab0956445f89beb"
-    appSecret: "zPonwfW704MLe0YnCfpwzhRjDsFkk0nT"
+# 飞书(已改为库表 fs_feishu_config 动态配置,以下保留兼容注释)
+#feishu:
+#    appId: "cli_aab0956445f89beb"
+#    appSecret: "zPonwfW704MLe0YnCfpwzhRjDsFkk0nT"
 

+ 4 - 3
fs-service/src/main/resources/application-druid-jnmy.yml

@@ -246,8 +246,9 @@ wechat:
         secret: 70d3ed4f8eb68cca0cf525b8ce07405d
         redirectUri: https://admin.jnmyunl.com/prod-api/callback
         isNeedScan: true
-feishu:
-    appId: "cli_aab0956445f89beb"
-    appSecret: "zPonwfW704MLe0YnCfpwzhRjDsFkk0nT"
+# 飞书(已改为库表 fs_feishu_config 动态配置)
+#feishu:
+#    appId: "cli_aab0956445f89beb"
+#    appSecret: "zPonwfW704MLe0YnCfpwzhRjDsFkk0nT"