Lark(飞书)群机器人提醒 OpenFeign+异步+动态配置

前言

定时任务执行失败时期望发送Lark(飞书)群消息提醒

最下方免费获取源码

前置条件

1、创建群聊

2、自定义群机器人

3、获取 Webhook 地址和 签名校验 secret

代码

1、依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>4.1.4</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

其他基础依赖...

2、配置文件(建议配置到nacos中)

enabled:配置是否生效

webapi:机器人webhook地址

secret:机器人密钥

yaml 复制代码
server:
  port: 8099

lark:
  robot:
    enabled: false
    webapi: your Webhook
    secret: your Secret

3、配置属性

java 复制代码
@ConfigurationProperties(prefix = "lark.robot")
@Data
public class LarkRobotProperties {

    private Boolean enabled = false;

    private String webapi;

    private String secret;
}

4、FeignClient接口调用

java 复制代码
@FeignClient(name = "lesson-client", url = "${lark.robot.webapi}")
public interface LarkRobotClient {

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    String sendMessage(String request);

}

5、配置拦截器

创建此拦截器的目的:

​ @FeignClient(name = "lesson-client", url = "${lark.robot.webapi}") 如果将webapi放入nacos动态配置时,修改配置无法生效,目前采用拦截器修改,如果有大佬知道如何解决希望评论区讨论。

java 复制代码
@Configuration(proxyBeanMethods = false)
@EnableFeignClients(clients = {LarkRobotClient.class})
@EnableConfigurationProperties(LarkRobotProperties.class)
public class RpcConfiguration {

    @Resource
    private LarkRobotProperties larkRobotProperties;
    
    /**
     * 此拦截器会全局生效,一定要加控制好范围,否则会影响到其他模块的调用
     */
    @Bean
    public RequestInterceptor urlInterceptor() {

        // 动态修改 lark机器人的webapi
        return template -> {
            if ("lark-robot-client".equals(template.feignTarget().name())) {
                String newUrl = larkRobotProperties.getWebapi();
                template.target(newUrl);
            }
        };
    }
}

6、配置线程池(可选)

java 复制代码
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("asyncCommonExecutor")
    public Executor handleDeviceRecordExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数:线程池创建时候初始化的线程数
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
        // 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 5);
        // 队列最大值
        executor.setQueueCapacity(500);
        // 允许线程的空闲时间60秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        // 等待所有任务结束后再关闭线程池
         executor.setWaitForTasksToCompleteOnShutdown(true);
        // 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("async-common-");
        // 设置拒绝策略,超限的不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
        executor.setAwaitTerminationSeconds(600);
        executor.initialize();
        return executor;
    }

}

7、LarkRobotUtils

genSign:根据密钥获取签名

getPostContent:封装富文本实体类

java 复制代码
@Component
public class LarkRobotUtils {


    public static String genSign(String secret, int timestamp) throws NoSuchAlgorithmException, InvalidKeyException {
        //把timestamp+"\n"+密钥当做签名字符串
        String stringToSign = timestamp + "\n" + secret;

        //使用HmacSHA256算法计算签名
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(new byte[]{});
        return new String(Base64.encodeBase64(signData));
    }


    /**
     * 富文本 post类型消息
     */
    public static PostContent getPostContent(AlarmContentRequest request) {

        // 富文本内容
        PostContent postContent = new PostContent();
        // 创建内容并封装
        List<List<PostElement>> postElements = new ArrayList<>();
        // 处理内容
        handleContentList(postElements, request.getContentList());
        // 拼装结果内容
        setPostContent(request, postElements, postContent);

        return postContent;
    }

    private static void setPostContent(AlarmContentRequest request, List<List<PostElement>> postElements, PostContent postContent) {
        Content cnContent = new Content();
        cnContent.setTitle(request.getTitle());
        cnContent.setPostElements(postElements);

        Post post = new Post();
        post.setCnContent(cnContent);

        postContent.setPost(post);
    }

    @SuppressWarnings("all")
    private static void handleContentList(List<List<PostElement>> contentList, List<ContentRequest> list) {
        // @所有人 + 标题
        contentList.add(List.of(createPostElement(PostTagEnum.AT, "all", null, null)));
        if (CollUtil.isEmpty(list)) return;
        // 遍历内容
        for (ContentRequest contentRequest : list) {
            // 创建元素列表
            List<PostElement> elements = new ArrayList<>();
            // 文本内容
            String text = contentRequest.getText();
            if (StringUtils.isNotBlank(text)) {
                elements.add(createPostElement(PostTagEnum.TEXT, null, text, null));
            }
            // 链接
            String href = contentRequest.getHref();
            if (StringUtils.isNotBlank(href)) {
                elements.add(createPostElement(PostTagEnum.LINK, null, contentRequest.getHrefDesc(), href));
            }
            contentList.add(elements);
        }
    }

    /**
     * 创建 PostElement
     */
    @SuppressWarnings("all")
    private static PostElement createPostElement(PostTagEnum tag, String userId, String text, String href) {
        PostElement element = new PostElement();
        element.setTag(tag.getTag());
        element.setUserId(userId);
        element.setText(text);
        element.setHref(href);
        return element;
    }

}

8、service层 发送消息服务类

sendMessage 同步发送Lark消息

sendMessageAsync 异步发送Lark消息

java 复制代码
@Slf4j
@Service
public class SendMessageServiceImpl implements SendMessageService {

    @Resource
    private LarkRobotClient larkRobotClient;

    @Resource
    private LarkRobotProperties larkRobotProperties;

    @Override
    public MessageResponse sendMessage(AlarmContentRequest request) {
        if (!larkRobotProperties.getEnabled()) {
            MessageResponse messageResponse = new MessageResponse();
            messageResponse.setCode(-1).setMessage("Lark Robot config is not enabled");
            log.info("排课状态字段更新失败,发送lark结果: {}", JSON.toJSON(messageResponse));
            return messageResponse;
        }

        // 生成时间戳和签名
        int timestamp = (int) (System.currentTimeMillis() / 1000);
        String sign;
        try {
            sign = LarkRobotUtils.genSign(larkRobotProperties.getSecret(), timestamp);
        } catch (Exception e) {
            log.error("加密失败: {}", e.getMessage(), e);
            throw new RuntimeException("加密失败");
        }
        // 构造消息内容
        PostContent postContent = LarkRobotUtils.getPostContent(request);
        // 构造消息请求对象
        String requestJson = this.getRequestJson(timestamp, sign, postContent);
        // 使用 Feign 调用接口
        String response;
        try {
            response = larkRobotClient.sendMessage(requestJson);
        } catch (Exception e) {
            log.error("发送lark异常: {}", e.getMessage(), e);
            throw e;
        }
        MessageResponse messageResponse = JSON.parseObject(response, MessageResponse.class);
        log.info("发送lark结果: {}", messageResponse);
        return messageResponse;
    }

    @Override
    @Async("asyncCommonExecutor")
    public void sendMessageAsync(AlarmContentRequest request) {
        sendMessage(request);
    }


    private String getRequestJson(int timestamp, String sign, PostContent postContent) {
        LarkMessageRequest larkMessageRequest = new LarkMessageRequest();
        larkMessageRequest.setTimestamp(timestamp);
        larkMessageRequest.setSign(sign);
        larkMessageRequest.setMsgType(MessageTypeEnum.POST.getType());
        larkMessageRequest.setContent(postContent);
        return JSON.toJSONString(larkMessageRequest);
    }
}

9、Controller层

java 复制代码
@RestController
@RequestMapping("/lark")
public class SendMessageController {

    @Resource
    private SendMessageService sendMessageService;


    @GetMapping("/send")
    public MessageResponse send() {
        AlarmContentRequest request = setAlarmContentRequest(1, 1, 1, new Exception("test"));
        return sendMessageService.sendMessage(request);
    }

    @GetMapping("/sendAsync")
    public Boolean sendAsync() {
        AlarmContentRequest request = setAlarmContentRequest(2, 2, 2, new Exception("test"));
        sendMessageService.sendMessageAsync(request);
        return true;
    }


    /**
     * 拼装发送lark请求对象
     */
    private AlarmContentRequest setAlarmContentRequest(Integer studentId, int size, int count, Exception e) {
        List<ContentRequest> contentList = Arrays.asList(
                new ContentRequest().setText("** 总共【" + size + "】条数据"),
                new ContentRequest().setText("** 更新了【" + count + "】条数据"),
                new ContentRequest().setText("** 错误studentId:【" + studentId + "】"),
                new ContentRequest().setText("** 错误信息:【" + e.getMessage() + "】")
        );
        AlarmContentRequest request = new AlarmContentRequest();
        request.setTitle("更新失败").setContentList(contentList);
        return request;
    }
}

10、调用接口

localhost:8099/lark/sendAsync

localhost:8099/lark/send

效果如下:

完结。如有疑问麻烦留言或者私信。

subscribe vvv: H先生出品

相关推荐
红尘散仙4 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记6 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪6 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6167 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364577 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao7 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒8 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰9 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox9 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全