1. 为什么飞书消息换行这么麻烦第一次用飞书机器人发消息时我也被换行问题坑得不轻。明明在普通文本里加个\n就能搞定的事到了飞书这里偏偏行不通。后来才发现飞书的消息推送机制和其他平台不太一样它把消息分成了几种类型每种类型的处理方式都不同。最常用的普通文本消息text类型确实不支持\n换行这是飞书API的设计决定的。我猜可能是为了防止消息注入攻击或者是为了统一消息格式。不过飞书提供了更强大的富文本消息post类型这才是解决换行问题的正确姿势。post类型的消息本质上是个迷你网页你可以把它想象成一张可以自由排版的电子海报。它支持文字、链接、图片、提醒等各种元素而且每行内容都可以精确控制。代价就是需要构造一个稍微复杂的JSON结构但熟悉之后其实也没那么可怕。2. 解密飞书富文本消息的JSON结构先来看个完整的post消息模板{ msg_type: post, content: { post: { zh_cn: { title: 你的消息标题, content: [ [ {tag: text, text: 第一行内容}, {tag: a, text: 超链接, href: https://example.com} ], [ {tag: text, text: 第二行内容}, {tag: at, user_id: ou_123456789} ] ] } } } }这个结构的关键点在于content数组。数组的每个元素代表一行行内的元素可以是普通文本tag: text超链接tag: a提醒tag: at图片tag: img我刚开始用的时候总搞混层级关系后来发现可以用行→列的思路来理解最外层content数组控制行数每行的子数组控制该行的内容元素每个元素用tag标识类型3. 实战构建多行通知消息假设我们要发送一个项目更新通知包含三部分更新说明详情链接相关责任人用Java代码构建这个JSON的完整示例public String buildFeishuMessage() { StringBuilder builder new StringBuilder(); // 消息头 builder.append({\msg_type\:\post\,\content\:{\post\:{\zh_cn\:{); builder.append(\title\:\项目更新通知\,); builder.append(\content\:[); // 第一行更新说明 builder.append([[{\tag\:\text\,\text\:\项目v2.3.0已发布主要更新\},); builder.append({\tag\:\text\,\text\:\修复登录异常问题\}]],); // 第二行详情链接 builder.append([[{\tag\:\text\,\text\:\详情请查看\},); builder.append({\tag\:\a\,\text\:\发布说明\,\href\:\https://yourdomain.com/release\}]],); // 第三行负责人 builder.append([[{\tag\:\text\,\text\:\负责人\},); builder.append({\tag\:\at\,\user_id\:\ou_18eac8d17ad4f02e8bbbb\}]]); // 消息尾 builder.append(]}}}}); return builder.toString(); }实际发送时建议用JSON库而不是字符串拼接。比如用Jackson库ObjectMapper mapper new ObjectMapper(); ObjectNode contentNode mapper.createObjectNode(); ArrayNode contentArray mapper.createArrayNode(); // 构建第一行 ArrayNode line1 mapper.createArrayNode(); line1.add(mapper.createObjectNode().put(tag, text).put(text, 第一行内容)); contentArray.add(line1); // 构建第二行 ArrayNode line2 mapper.createArrayNode(); line2.add(mapper.createObjectNode().put(tag, text).put(text, 第二行内容)); contentArray.add(line2); // 组装完整消息 ObjectNode message mapper.createObjectNode(); message.put(msg_type, post); message.set(content, mapper.createObjectNode() .set(post, mapper.createObjectNode() .set(zh_cn, mapper.createObjectNode() .put(title, 测试消息) .set(content, contentArray)))); String json mapper.writeValueAsString(message);4. 避坑指南那些年我踩过的JSON坑4.1 特殊字符转义问题最常遇到的问题是文本中包含JSON特殊字符比如引号、反斜杠等。比如要发送这样的内容用户输入Hello\nWorld直接放到JSON里会报错必须转义String text 用户输入\\\Hello\\\\nWorld\\\;建议对所有用户输入的内容都做转义处理可以用StringEscapeUtils.escapeJsonApache Commons Lang或者自己写转义逻辑。4.2 中文字符编码遇到过中文变成unicode编码的问题比如{text:\u4e2d\u6587}解决方法是在创建ObjectMapper时配置ObjectMapper mapper new ObjectMapper(); mapper.configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, false);4.3 数组越界错误content数组必须至少包含一个子数组哪怕空数组否则会报格式错误。这是正确的空消息content: [[]]而这样会报错content: []4.4 消息长度限制飞书post消息有长度限制约20KB超长消息会被截断。解决方法精简内容去掉冗余信息把长文本拆分成多条消息改用飞书文档然后发文档链接5. 高级技巧动态构建复杂消息当消息内容需要动态生成时可以封装一个工具类。这是我常用的FeishuMessageBuilderpublic class FeishuMessageBuilder { private final ObjectMapper mapper new ObjectMapper(); private final ObjectNode message; private final ArrayNode content; public FeishuMessageBuilder(String title) { this.message mapper.createObjectNode(); this.content mapper.createArrayNode(); message.put(msg_type, post); message.set(content, mapper.createObjectNode() .set(post, mapper.createObjectNode() .set(zh_cn, mapper.createObjectNode() .put(title, title) .set(content, content)))); } public FeishuMessageBuilder addLine(String text) { ArrayNode line mapper.createArrayNode(); line.add(mapper.createObjectNode() .put(tag, text) .put(text, text)); content.add(line); return this; } public FeishuMessageBuilder addLineWithLink(String text, String linkText, String url) { ArrayNode line mapper.createArrayNode(); line.add(mapper.createObjectNode() .put(tag, text) .put(text, text)); line.add(mapper.createObjectNode() .put(tag, a) .put(text, linkText) .put(href, url)); content.add(line); return this; } public String build() throws JsonProcessingException { return mapper.writeValueAsString(message); } }使用示例FeishuMessageBuilder builder new FeishuMessageBuilder(每日报告); builder.addLine(今日新增用户42) .addLineWithLink(详情, 点击查看, https://example.com/stats) .addLine(负责人张三); String json builder.build();6. 测试与调试技巧发送飞书消息最头疼的就是调试JSON格式。推荐几个方法6.1 使用JSON格式化工具先把构建好的JSON字符串用在线工具如jsonformatter.org格式化检查结构是否正确。常见的格式问题多余的逗号缺失的引号嵌套层级错误6.2 飞书开放平台调试工具飞书开放平台提供了消息调试工具在开发者后台→消息卡片工具可以实时预览消息效果。6.3 逐步构建法不要一次性写完整个JSON而是逐步添加元素测试先发只有标题的空消息添加一行纯文本添加带链接的行添加提醒6.4 错误处理飞书API返回的错误信息有时候不太直观主要关注code为0表示成功code为19001通常是JSON格式错误code为99991668可能是权限问题建议的异常处理代码try { String response sendToFeishu(json); JsonNode node mapper.readTree(response); if (node.path(code).asInt() ! 0) { log.error(飞书消息发送失败{} - {}, node.path(code).asText(), node.path(msg).asText()); } } catch (Exception e) { log.error(飞书消息发送异常, e); }7. 性能优化建议当需要高频发送消息时比如监控告警有几个优化点7.1 复用ObjectMapperObjectMapper创建成本高应该全局复用private static final ObjectMapper MAPPER new ObjectMapper();7.2 使用消息模板对于固定格式的消息可以准备模板然后替换变量String template {\text\:\${content}\}; String message template.replace(${content}, 实际内容);7.3 异步发送不要阻塞主线程用线程池或消息队列异步发送executor.submit(() - { sendToFeishu(buildMessage()); });7.4 批量发送多条消息合并发送注意飞书可能有频率限制ListString messages getPendingMessages(); if (!messages.isEmpty()) { String batch [ String.join(,, messages) ]; sendToFeishu(batch); }8. 替代方案比较除了post类型飞书还支持其他消息类型类型换行支持适合场景复杂度text不支持简单文本通知低post支持富文本通知中card支持交互式卡片高image不支持纯图片中选择建议纯文本且不需要换行text类型需要换行或简单排版post类型需要按钮交互card类型只发图片image类型9. 真实案例服务器监控告警这是我们生产环境用的服务器监控通知public String buildServerAlert(String server, String metric, String value, String threshold) { return new FeishuMessageBuilder(服务器告警) .addLine(服务器 server) .addLine(监控项 metric) .addLine(当前值 value 阈值 threshold ) .addLineWithLink(, 查看详情, getGrafanaUrl(server)) .addLine(处理人运维团队) .build(); }效果[服务器告警] 服务器web-server-01 监控项CPU使用率 当前值95%阈值80% 查看详情https://grafana.example.com 处理人运维团队10. 最佳实践总结经过多个项目的实践我总结了这些经验始终优先使用post类型发送重要通知用JSON库而不是字符串拼接构建消息对所有动态内容进行转义处理添加适当的空行提升可读性重要消息加上提醒确保被看到在测试环境充分验证消息格式监控消息发送失败的情况刚开始用飞书消息API时我也觉得这个JSON结构有点复杂。但熟悉之后发现这种设计其实提供了很大的灵活性。现在我们的监控系统、CI/CD通知、日报周报都通过这套机制推送效果比原来的纯文本好太多了。