Spring Boot 接入飞书自定义机器人:支付成功通知与下游接口故障报警实践
在业务系统里,有些事件既不适合只写日志,也没必要一开始就接入完整的监控平台。 例如:
- 客户购买了声音复刻额度,系统已经给客户发放内部权益,但运营还需要到上游平台采购真实资源。
- 项目依赖的 A-Bogus 下游服务调用失败,需要研发尽快排查。
这类场景非常适合使用飞书自定义机器人:接入成本低、通知及时,也能直接把业务、运营和研发拉到同一个信息闭环里。
首先一定是要在飞书群里面创建一个自定义机器人的,具体在设置 -> 群机器人 -> 自定义机器人复制 Webhook 使用即可
本文以 Spring Boot 3 和 Java 21 为例,完整介绍如何实现一套可复用的飞书告警能力。文中的域名、订单号和 Webhook 均为示例,请替换为自己的配置。
一、需求拆解
这次需要处理两类消息。
1. 声音复刻额度购买通知
客户支付成功并获得声音复刻额度后,机器人需要通知运营及时采购上游资源。消息至少包含:
- 订单号
- 购买账号 ID
- 店铺名或达人账号名称
- 音色数量
- 订单金额
- 支付渠道
- 支付时间
这里有一个重要约束:飞书通知失败不能影响支付结果。
2. A-Bogus 服务失败报警
系统通过自己的 /abogus/generate_abogus 接口调用下游生成服务。当出现以下情况时需要报警:
- HTTP 状态码不是 2xx
- HTTP 成功,但下游业务码不是成功状态
- 返回体缺少关键字段
- 返回体无法解析
- 网络异常或请求超时
下游持续故障时不能每次请求都刷屏,因此还需要增加报警冷却时间。
二、整体设计
整个通知链路分为三层:
FeishuBotProperties:管理 Webhook、超时和冷却时间。FeishuBotClient:负责调用飞书 Webhook。FeishuAlertService:负责消息内容、事务时机和报警限流。
业务代码只负责告诉 FeishuAlertService "发生了什么",不需要关心 HTTP 请求细节。
这种分层有两个直接收益:
- 飞书服务异常不会污染核心业务。
- 后续新增退款、登录异常等告警时可以复用同一个客户端。
三、创建飞书机器人
在目标飞书群中添加"自定义机器人",名称可以写成:
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());
}
}
}
这里有两个设计点:
- 使用
@Async,不让飞书网络耗时阻塞业务线程。 - 捕获所有通知异常,只记录日志,不反向抛给支付或解密接口。
项目需要通过 @EnableAsync 启用异步能力。FeishuBotClient 应作为独立 Spring Bean 被其他服务调用,避免同类内部调用导致 @Async 代理失效。
兼容飞书响应
飞书不同版本的响应可能使用 code 或 StatusCode:
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_type为text- 标题和正文存在
- 飞书成功响应能被识别
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. 购买成功,但机器人没有消息
优先检查:
- 当前运行进程是否真的读取了
FEISHU_BOT_WEBHOOK_URL。 - 修改配置后是否重启了应用。
- 启动日志是否出现"飞书机器人报警已启用"。
- 新机器人是否配置了不匹配的关键词。
- Webhook 是否已经被重置或删除。
- 飞书响应的业务码是否为 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,只需:
- 开发环境替换本地密钥文件中的 Webhook。
- 生产环境替换
FEISHU_BOT_WEBHOOK_URL。 - 重启后端。
- 发送连接测试。
不需要修改数据库、前端或业务代码。
如果新机器人开启了签名校验,则还需要增加类似以下配置:
yaml
feishu:
bot:
webhook-url: ${FEISHU_BOT_WEBHOOK_URL:}
secret: ${FEISHU_BOT_SECRET:}
并按飞书文档计算签名后再发送请求。
十五、可以继续演进的方向
当前方案适合中小规模业务。如果告警逐渐成为关键基础设施,可以继续增强:
- 使用飞书卡片消息提升可读性
- 增加环境标识,明确区分开发和生产
- 使用 Redis 实现多实例报警去重
- 使用数据库 Outbox 保存待发送消息,支持失败重试和审计
- 接入 Micrometer 记录发送成功率和耗时
- 为不同告警配置不同机器人或群聊
- 对敏感字段进行统一脱敏
购买通知如果属于必须送达的运营任务,建议优先增加 Outbox。单纯的异步 HTTP 请求虽然不会影响支付,但进程在提交后立即崩溃时,消息仍有可能丢失。
总结
飞书机器人接入本身并不复杂,真正需要认真处理的是业务边界:
- 支付通知必须在事务提交后发送。
- 飞书和上游资源分配失败不能影响核心支付事务。
- 下游服务的 HTTP、业务、数据和网络异常都要覆盖。
- 高频故障需要限流,避免群消息失控。
- Webhook 必须作为密钥管理,开发和生产环境分开配置。
- 启动状态、响应业务码和自动化测试缺一不可。
做好这些之后,飞书机器人就不只是"发一条消息",而是业务流程中一个可靠、低成本的协作入口。