Spring Boot 接入飞书自定义机器人

Spring Boot 接入飞书自定义机器人:支付成功通知与下游接口故障报警实践

在业务系统里,有些事件既不适合只写日志,也没必要一开始就接入完整的监控平台。 例如:

  1. 客户购买了声音复刻额度,系统已经给客户发放内部权益,但运营还需要到上游平台采购真实资源。
  2. 项目依赖的 A-Bogus 下游服务调用失败,需要研发尽快排查。

这类场景非常适合使用飞书自定义机器人:接入成本低、通知及时,也能直接把业务、运营和研发拉到同一个信息闭环里。

首先一定是要在飞书群里面创建一个自定义机器人的,具体在设置 -> 群机器人 -> 自定义机器人复制 Webhook 使用即可

本文以 Spring Boot 3 和 Java 21 为例,完整介绍如何实现一套可复用的飞书告警能力。文中的域名、订单号和 Webhook 均为示例,请替换为自己的配置。

一、需求拆解

这次需要处理两类消息。

1. 声音复刻额度购买通知

客户支付成功并获得声音复刻额度后,机器人需要通知运营及时采购上游资源。消息至少包含:

  • 订单号
  • 购买账号 ID
  • 店铺名或达人账号名称
  • 音色数量
  • 订单金额
  • 支付渠道
  • 支付时间

这里有一个重要约束:飞书通知失败不能影响支付结果。

2. A-Bogus 服务失败报警

系统通过自己的 /abogus/generate_abogus 接口调用下游生成服务。当出现以下情况时需要报警:

  • HTTP 状态码不是 2xx
  • HTTP 成功,但下游业务码不是成功状态
  • 返回体缺少关键字段
  • 返回体无法解析
  • 网络异常或请求超时

下游持续故障时不能每次请求都刷屏,因此还需要增加报警冷却时间。

二、整体设计

整个通知链路分为三层:

  1. FeishuBotProperties:管理 Webhook、超时和冷却时间。
  2. FeishuBotClient:负责调用飞书 Webhook。
  3. FeishuAlertService:负责消息内容、事务时机和报警限流。

业务代码只负责告诉 FeishuAlertService "发生了什么",不需要关心 HTTP 请求细节。

flowchart LR A["支付成功"] --> B["提交订单和权益事务"] B --> C["事务提交后发送购买通知"] C --> D["FeishuAlertService"] E["调用 A-Bogus 下游"] --> F{"调用结果"} F -->|成功| G["返回业务数据"] F -->|失败| H["冷却时间判断"] H --> D D --> I["FeishuBotClient 异步发送"] I --> J["飞书群机器人"]

这种分层有两个直接收益:

  • 飞书服务异常不会污染核心业务。
  • 后续新增退款、登录异常等告警时可以复用同一个客户端。

三、创建飞书机器人

在目标飞书群中添加"自定义机器人",名称可以写成:

text 复制代码
LumiQ 业务告警助手

描述可以写成:

text 复制代码
用于监控关键业务事件:客户购买声音复刻额度后提醒运营补充上游资源;
A-Bogus 服务调用失败时通知研发及时排查。

创建后会得到一个 Webhook:

text 复制代码
https://open.feishu.cn/open-apis/bot/v2/hook/REPLACE_WITH_YOUR_TOKEN

Webhook 具备直接向群里发送消息的权限,不能提交到 Git,也不要打印到日志。

如果机器人启用了关键词校验,可以配置"声音复刻"和"A-Bogus"两个关键词。若启用签名校验,还需要额外实现时间戳和签名计算,不能只发送普通 Webhook 请求。

四、配置开发和生产环境

1. Spring Boot 配置

application.yml 中只引用环境变量:

yaml 复制代码
feishu:
  bot:
    webhook-url: ${FEISHU_BOT_WEBHOOK_URL:}
    request-timeout-seconds: ${FEISHU_BOT_REQUEST_TIMEOUT_SECONDS:8}
    abogus-alert-cooldown-seconds: ${FEISHU_BOT_ABOGUS_ALERT_COOLDOWN_SECONDS:300}

Webhook 为空时关闭通知,应用仍然可以正常启动。

2. 开发环境使用独立机器人

开发环境可以导入一个不进 Git 的本地密钥文件:

yaml 复制代码
spring:
  config:
    import: optional:file:./config/application-local-secrets.yml

本地文件内容如下:

yaml 复制代码
feishu:
  bot:
    webhook-url: https://open.feishu.cn/open-apis/bot/v2/hook/DEV_TOKEN

并在 .gitignore 中忽略它:

gitignore 复制代码
/config/application-local-secrets.yml

3. 生产环境使用环境变量

Docker Compose 可以透传生产机器人的 Webhook:

yaml 复制代码
services:
  app:
    environment:
      FEISHU_BOT_WEBHOOK_URL: ${FEISHU_BOT_WEBHOOK_URL:-}

部署机器上配置:

bash 复制代码
export FEISHU_BOT_WEBHOOK_URL='https://open.feishu.cn/open-apis/bot/v2/hook/PROD_TOKEN'

这样开发和生产使用不同机器人,测试消息不会进入生产群。

五、封装配置类

配置类负责判断机器人是否启用,并在启动时输出明确状态。日志中只记录"已启用/未启用",不打印 Webhook。

java 复制代码
@Data
@Slf4j
@Component
@ConfigurationProperties(prefix = "feishu.bot")
public class FeishuBotProperties {

    private String webhookUrl;
    private int requestTimeoutSeconds = 8;
    private long abogusAlertCooldownSeconds = 300L;

    @PostConstruct
    public void logConfigurationStatus() {
        if (isEnabled()) {
            log.info("飞书机器人报警已启用");
        } else {
            log.warn("飞书机器人报警未启用:请配置 Webhook");
        }
    }

    public boolean isEnabled() {
        return StrUtil.isNotBlank(webhookUrl);
    }
}

启动状态日志很重要。实际排查中,最常见的"代码执行了但群里没消息",往往只是运行进程没有读取到环境变量,或者修改配置后没有重启应用。

六、封装飞书客户端

飞书自定义机器人接收 JSON 格式的 POST 请求。纯文本消息结构如下:

json 复制代码
{
  "msg_type": "text",
  "content": {
    "text": "消息内容"
  }
}

使用 Java 21 自带的 HttpClient 封装客户端:

java 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
public class FeishuBotClient {

    private static final String CONTENT_TYPE = "application/json; charset=utf-8";

    private final FeishuBotProperties properties;
    private final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(8))
            .build();

    @Async
    public void sendTextAsync(String title, String content) {
        if (!properties.isEnabled()) {
            log.debug("飞书机器人 Webhook 未配置,跳过通知:{}", title);
            return;
        }

        try {
            JSONObject payload = new JSONObject();
            payload.set("msg_type", "text");
            payload.set("content", new JSONObject().set("text", title + "\n" + content));

            int timeoutSeconds = Math.max(3, properties.getRequestTimeoutSeconds());
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(properties.getWebhookUrl().trim()))
                    .timeout(Duration.ofSeconds(timeoutSeconds))
                    .header("Content-Type", CONTENT_TYPE)
                    .POST(HttpRequest.BodyPublishers.ofString(
                            JSONUtil.toJsonStr(payload), StandardCharsets.UTF_8))
                    .build();

            HttpResponse<String> response = httpClient.send(
                    request,
                    HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

            if (response.statusCode() < 200 || response.statusCode() >= 300) {
                log.warn("飞书机器人通知失败,title={}, httpStatus={}",
                        title, response.statusCode());
                return;
            }

            validateResponse(title, response.body());
        } catch (InterruptedException error) {
            Thread.currentThread().interrupt();
            log.warn("飞书机器人通知被中断,title={}", title);
        } catch (Exception error) {
            log.warn("飞书机器人通知异常,title={}, message={}",
                    title, error.getMessage());
        }
    }
}

这里有两个设计点:

  1. 使用 @Async,不让飞书网络耗时阻塞业务线程。
  2. 捕获所有通知异常,只记录日志,不反向抛给支付或解密接口。

项目需要通过 @EnableAsync 启用异步能力。FeishuBotClient 应作为独立 Spring Bean 被其他服务调用,避免同类内部调用导致 @Async 代理失效。

兼容飞书响应

飞书不同版本的响应可能使用 codeStatusCode

json 复制代码
{
  "code": 0,
  "msg": "success"
}

也可能返回:

json 复制代码
{
  "StatusCode": 0,
  "StatusMessage": "success"
}

因此不能只判断 HTTP 200,还需要解析业务状态:

java 复制代码
private void validateResponse(String title, String responseBody) {
    try {
        JSONObject response = JSONUtil.parseObj(responseBody);
        Integer code = response.getInt("code");
        Integer statusCode = response.getInt("StatusCode");

        if ((code != null && code != 0)
                || (statusCode != null && statusCode != 0)) {
            String message = response.getStr(
                    "msg",
                    response.getStr("StatusMessage", "未知错误"));
            log.warn("飞书机器人拒绝通知,title={}, code={}, message={}",
                    title, code == null ? statusCode : code, message);
        }
    } catch (Exception error) {
        log.warn("飞书机器人响应无法解析,title={}", title);
    }
}

七、统一管理消息内容

不要在 Controller 或支付 Service 里直接拼飞书 JSON。可以增加一个 FeishuAlertService,统一管理:

  • 消息模板
  • 金额和时间格式化
  • 支付事务提交时机
  • A-Bogus 报警冷却
  • 错误详情截断
java 复制代码
@Service
@RequiredArgsConstructor
public class FeishuAlertService {

    private final FeishuBotProperties properties;
    private final FeishuBotClient botClient;

    private final ConcurrentMap<String, Long> lastAlertTimes =
            new ConcurrentHashMap<>();
}

八、购买通知必须在事务提交后发送

支付成功逻辑通常包含多次数据库写入:

  • 更新订单状态
  • 写入支付流水号和支付时间
  • 发放客户权益
  • 同步兼容额度

如果在事务中间发送消息,后面的数据库操作一旦失败回滚,飞书群却已经收到"购买成功",就会出现假通知。

正确做法是注册事务同步回调,只在提交成功后发送:

java 复制代码
public void notifyVoiceClonePurchaseAfterCommit(VoiceClonePurchaseAlert alert) {
    if (alert == null || !properties.isEnabled()) {
        return;
    }

    Runnable notification = () -> botClient.sendTextAsync(
            "【声音复刻额度购买通知】",
            formatVoiceClonePurchase(alert));

    if (!TransactionSynchronizationManager.isSynchronizationActive()) {
        notification.run();
        return;
    }

    TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    notification.run();
                }
            });
}

资源分配也要与支付事务隔离

声音复刻购买成功后,系统可能还要查询资源池或触发上游采购。如果资源池表缺失、上游不可用或分配代码抛异常,不应该把已成功的支付事务回滚。

可以在支付事务内发布事件,在事务提交后异步处理资源分配:

java 复制代码
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(VoiceCloneProvisioningRequestedEvent event) {
    try {
        resourceSyncService.requestProvisioning(
                event.scope(),
                entitlement(event.entitlementId()));
    } catch (Exception error) {
        log.error("声音复刻权益已入账,但资源分配失败,entitlementId={}",
                event.entitlementId(), error);
    }
}

这里需要明确区分两件事:

  • 客户支付和系统权益到账,是核心事务。
  • 上游资源分配和飞书消息,是提交后的附加动作。

附加动作失败可以重试或人工处理,但不能改变已经确认的支付事实。

九、构造购买提醒

只有订单包含声音复刻商品时才通知:

java 复制代码
List<OrderItem> voiceItems = items.stream()
        .filter(this::isVoicePackage)
        .toList();

if (voiceItems.isEmpty()) {
    return;
}

店铺账号和达人账号都走同一个支付成功入口,因此都可以触发机器人通知。展示名称时做区分:

  • 店铺账号显示 店铺名:xxx
  • 达人账号显示 达人账号:xxx
  • 混合或多目标订单显示 购买对象:xxx、yyy

通知效果示例:

text 复制代码
【声音复刻额度购买通知】
系统声音复刻额度已到账,请及时到上游平台补充真实资源。
订单号:WXP202608100001
购买账号ID:10086
店铺名:示例旗舰店
音色数量:2 个
权益范围:1 个账号范围
音色金额:¥39.80
订单总额:¥39.80
支付渠道:微信支付
支付时间:2026-08-10 17:30:00

消息中不要出现支付密钥、用户手机号、访问令牌等敏感信息。

十、A-Bogus 失败报警

项目对外暴露一个统一解密入口,再由后端调用真正的 A-Bogus 服务:

text 复制代码
POST https://api.example.com/abogus/generate_abogus

Controller 需要覆盖不同失败阶段。

1. HTTP 请求失败

java 复制代码
if (!response.isOk()) {
    notifyAbogusFailure(
            endpoint,
            "HTTP请求失败",
            response.getStatus(),
            response.body());
    return R.fail("调用外部接口失败: " + response.getStatus());
}

2. 下游业务失败

java 复制代码
Integer code = responseJson.getInt("code");
if (code == null || code != 200) {
    String message = responseJson.getStr("msg", "未知错误");
    notifyAbogusFailure(
            endpoint,
            "下游业务失败",
            response.getStatus(),
            "code=" + code + ", msg=" + message);
    return R.fail("生成 A-Bogus 失败: " + message);
}

3. 数据异常

java 复制代码
JSONObject data = responseJson.getJSONObject("data");
if (data == null) {
    notifyAbogusFailure(
            endpoint,
            "响应数据异常",
            response.getStatus(),
            "成功响应缺少 data 字段");
    return R.fail("生成 A-Bogus 失败: 响应数据异常");
}

4. 超时、网络和解析异常

java 复制代码
} catch (Exception error) {
    notifyAbogusFailure(
            endpoint,
            "调用异常",
            null,
            error.getClass().getSimpleName() + ": " + error.getMessage());
    return R.fail("生成 A-Bogus 失败: " + error.getMessage());
}

报警链路本身也必须保护:

java 复制代码
private void notifyAbogusFailure(...) {
    try {
        feishuAlertService.notifyAbogusFailure(...);
    } catch (Exception error) {
        log.warn("提交 A-Bogus 飞书报警失败: {}", error.getMessage());
    }
}

即使飞书不可用,原接口仍然按照原有协议返回,不应该被二次异常覆盖。

十一、增加报警冷却时间

解密接口可能被高频调用。如果下游持续故障,每个失败请求都发送飞书会很快刷屏。

可以设置 5 分钟冷却时间:

java 复制代码
private boolean acquireAlertWindow() {
    long cooldownMillis = Math.max(
            0L,
            properties.getAbogusAlertCooldownSeconds()) * 1000L;

    if (cooldownMillis == 0L) {
        return true;
    }

    long now = System.currentTimeMillis();
    AtomicBoolean acquired = new AtomicBoolean(false);

    lastAlertTimes.compute("ABOGUS", (key, previous) -> {
        if (previous == null || now - previous >= cooldownMillis) {
            acquired.set(true);
            return now;
        }
        return previous;
    });

    return acquired.get();
}

这里使用的是进程内缓存,适合单实例部署。如果应用有多个实例,每个实例都会独立报警,建议将冷却状态放到 Redis,并使用 SET NX EX 实现跨实例去重。

十二、测试方案

这类通知不能只靠人工点支付验证。建议至少覆盖以下测试。

1. 飞书客户端请求格式

使用 JDK 自带的 HttpServer 启动本地临时服务,验证:

  • 请求方法为 POST
  • Content-Type 正确
  • msg_typetext
  • 标题和正文存在
  • 飞书成功响应能被识别

2. 支付通知

验证:

  • 非声音复刻商品不通知
  • 店铺购买包含店铺名
  • 达人购买包含达人账号名称
  • 多目标数量和金额计算正确
  • 事务提交前不发送
  • 事务提交后只发送一次
  • 资源分配异常不会回滚支付

3. A-Bogus 报警

分别模拟:

  • HTTP 503
  • HTTP 200,但业务码失败
  • 返回非法 JSON
  • 缺少 data
  • 网络超时
  • 冷却期内重复失败

4. Webhook 连通性测试

部署前可以发送一条明确标注为测试的消息:

bash 复制代码
curl --request POST "$FEISHU_BOT_WEBHOOK_URL" \
  --header 'Content-Type: application/json; charset=utf-8' \
  --data '{
    "msg_type": "text",
    "content": {
      "text": "【机器人连接测试】\nWebhook 通道正常。"
    }
  }'

成功响应通常包含:

json 复制代码
{
  "code": 0,
  "msg": "success"
}

十三、常见问题排查

1. 购买成功,但机器人没有消息

优先检查:

  1. 当前运行进程是否真的读取了 FEISHU_BOT_WEBHOOK_URL
  2. 修改配置后是否重启了应用。
  3. 启动日志是否出现"飞书机器人报警已启用"。
  4. 新机器人是否配置了不匹配的关键词。
  5. Webhook 是否已经被重置或删除。
  6. 飞书响应的业务码是否为 0。

不要把 Webhook 为空设计成静默状态,启动时至少应有一条明确日志。

2. 日志一直打印 SqlSession was not registered for synchronization

这类日志通常表示定时查询没有运行在 Spring 事务中,并不等于数据库报错。只读定时任务不一定需要事务。

如果同一段 SQL 每几秒出现一次,还要检查 @Scheduled 的执行周期。例如资源分配任务和退款处理中任务会定期查询待处理记录,查询结果为 0 说明当前没有待处理数据。

真正需要关注的是后面是否出现 SQL 异常、连接异常或任务执行失败堆栈。

3. 飞书返回 HTTP 200,但群里没有消息

继续检查响应体。HTTP 200 只代表请求到达飞书,关键词校验失败、签名失败等情况可能通过业务码返回。

4. 开发机器人能收到,生产机器人收不到

检查生产容器中是否透传了环境变量:

bash 复制代码
docker compose config

不要在命令输出、CI 日志或截图中暴露完整 Webhook。

十四、如何更换机器人

如果新机器人仍然使用普通 Webhook,只需:

  1. 开发环境替换本地密钥文件中的 Webhook。
  2. 生产环境替换 FEISHU_BOT_WEBHOOK_URL
  3. 重启后端。
  4. 发送连接测试。

不需要修改数据库、前端或业务代码。

如果新机器人开启了签名校验,则还需要增加类似以下配置:

yaml 复制代码
feishu:
  bot:
    webhook-url: ${FEISHU_BOT_WEBHOOK_URL:}
    secret: ${FEISHU_BOT_SECRET:}

并按飞书文档计算签名后再发送请求。

十五、可以继续演进的方向

当前方案适合中小规模业务。如果告警逐渐成为关键基础设施,可以继续增强:

  • 使用飞书卡片消息提升可读性
  • 增加环境标识,明确区分开发和生产
  • 使用 Redis 实现多实例报警去重
  • 使用数据库 Outbox 保存待发送消息,支持失败重试和审计
  • 接入 Micrometer 记录发送成功率和耗时
  • 为不同告警配置不同机器人或群聊
  • 对敏感字段进行统一脱敏

购买通知如果属于必须送达的运营任务,建议优先增加 Outbox。单纯的异步 HTTP 请求虽然不会影响支付,但进程在提交后立即崩溃时,消息仍有可能丢失。

总结

飞书机器人接入本身并不复杂,真正需要认真处理的是业务边界:

  1. 支付通知必须在事务提交后发送。
  2. 飞书和上游资源分配失败不能影响核心支付事务。
  3. 下游服务的 HTTP、业务、数据和网络异常都要覆盖。
  4. 高频故障需要限流,避免群消息失控。
  5. Webhook 必须作为密钥管理,开发和生产环境分开配置。
  6. 启动状态、响应业务码和自动化测试缺一不可。

做好这些之后,飞书机器人就不只是"发一条消息",而是业务流程中一个可靠、低成本的协作入口。

相关推荐
2401_894915531 小时前
部署 GEO 优化源码常见报错排查:端口、伪静态、缓存问题解决
java·运维·服务器·后端·缓存·开源
卷无止境1 小时前
聊聊Web开发里的流式数据 从原理到FastAPI实战
后端·python·fastapi
Scene2161 小时前
AgentScope 2.0:4. Message & Event —— 消息模型与事件流深度解析
后端
元界metalite1 小时前
MyBatis 字段改名为何查询不报错?MetaLite ORM 如何做到类型安全?
后端
七牛开发者1 小时前
Codex 实践系列 Vol.04:用 Goal 和 Plan 管住一个长任务
java·数据库·人工智能·github·copilot
我命由我123451 小时前
Android Drawable - gradient
android·java·java-ee·kotlin·android studio·android-studio·android runtime
Csvn1 小时前
🐍 Day 2 :Python 变量与数据类型 — 一切皆对象
后端
Csvn2 小时前
📊 SQL 入门 Day 17:数据更新与删除
后端·sql
秋天的一阵风2 小时前
🔥 Network 里那坨 "data:" 我真看吐了,自制开源 Chrome 插件,AI 流式调试直接开挂
前端·人工智能·后端