当前位置: 首页 > news >正文

冲刺博客9

昨天的成就
工单系统 CRUD 已经打通,维修工单、巡检工单、保养工单、检测工单四类工单各自拥有独立的 Servlet 和数据表。工单编号自动生成功能正常(如 WX20260615001 ),状态流转通过时间戳字段记录,模糊搜索 + 状态筛选 + 分页的组合查询测试通过。

遇到的困难
困难一:飞书开放平台 API 版本与认证机制 。飞书的接口需要先获取 tenant_access_token ,而且 token 有有效期,每次请求都重新获取会触发频率限制。需要设计一个缓存机制,在 token 过期前复用。
困难二:工程师姓名与飞书 open_id 的映射问题 。系统中存储的是"赵工程师"这种中文姓名,但飞书消息推送必须用 ou_xxxx 格式的 open_id 。需要维护一个映射表,而且在工单指派场景下要能双向解析(姓名 → id / id → 姓名)。
困难三:交互式卡片(Card)的 JSON 格式嵌套太深 。飞书卡片支持富文本、按钮交互,但手工拼接 JSON 很容易漏掉引号或逗号,调试时需要反复对照飞书卡片搭建工具的示例。
困难四:长连接模式 vs 回调模式 。飞书 SDK 同时支持 WebSocket 长连接和 HTTP 事件回调两种接收消息的方式,初期只打算实现回调模式,但 WebSocket 模式调试更方便(不需要公网回调地址),决定两种模式都支持。

今天的任务

  1. 实现 FeishuConfig 配置类 :集中管理 App ID、App Secret、接口 URL
  2. 实现 FeishuService 核心服务类 :token 缓存、文本消息发送、交互式卡片发送、工程师姓名 ↔ open_id 双向映射
  3. 实现 notifyAssigneeByCard :工单创建时自动向被指派工程师推送卡片
  4. 搭建 FeishuWebSocketService :通过飞书 SDK 建立长连接,接收用户消息

代码嵌入
FeishuConfig.java — 飞书应用配置
package com.yunwei.util;

public class FeishuConfig {
public static final String APP_ID = "cli_aa96f065b038dcd9";
public static final String APP_SECRET = "wSFGQk6XvqLiRBvBYd2FOcGvPiHNVS6O";
public static final String TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
public static final String MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
}
FeishuService.java — Token 缓存 & open_id 映射
public class FeishuService {
private static String tenantAccessToken = null;
private static long tokenExpireTime = 0;

// ========= 工程师 ↔ 飞书 open_id 映射 =========
// 新增工程师时在这里加一行;后续可迁移到数据库表
private static final Map<String, String> ENGINEER_OPEN_ID = new HashMap<String, String>() {{
put("赵工程师", "ou_f9e92b592e650988738707ddbe717c8e");
put("我", null); // "派给我" 会通过发送者 open_id 回填
}};

public static String getTenantAccessToken() {
// 缓存未过期直接返回,避免频繁请求
if (tenantAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
return tenantAccessToken;
}
try {
URL url = new URL(FeishuConfig.TOKEN_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);

String requestJson = "{"app_id":"" + FeishuConfig.APP_ID
+ "","app_secret":"" + FeishuConfig.APP_SECRET + ""}";

try (OutputStream os = conn.getOutputStream()) {
os.write(requestJson.getBytes(StandardCharsets.UTF_8));
}

// 解析响应中的 tenant_access_token 和 expire(秒)
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) sb.append(line);
reader.close();

// 简易 JSON 字段提取
String token = extractField(sb.toString(), "tenant_access_token");
int expire = Integer.parseInt(extractField(sb.toString(), "expire"));
tenantAccessToken = token;
// 提前 5 分钟过期,避免临界时间窗失效
tokenExpireTime = System.currentTimeMillis() + (expire - 300) * 1000L;
return token;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
姓名 → open_id 解析方法(支持模糊匹配):
/** 根据中文姓名获取飞书 open_id;找不到返回 null */
public static String resolveAssignToOpenId(String assignTo, String senderOpenId) {
if (assignTo == null || assignTo.isEmpty()) return null;
String key = assignTo.trim();
// "我 / 本人" → 回退到发送者自己的 open_id
if ("我".equals(key) || "本人".equals(key)) return senderOpenId;
// 精确匹配
if (ENGINEER_OPEN_ID.containsKey(key)) return ENGINEER_OPEN_ID.get(key);
// 模糊包含匹配
for (Map.Entry<String, String> e : ENGINEER_OPEN_ID.entrySet()) {
if (e.getValue() == null) continue;
if (e.getKey().equals(key) || e.getKey().contains(key) || key.contains(e.getKey())) {
return e.getValue();
}
}
return null;
}

/** 根据 open_id 反向查找工程师姓名(用于"我的工单"场景) /
public static String resolveEngineerNameFromOpenId(String openId) {
if (openId == null || openId.isEmpty()) return null;
for (Map.Entry<String, String> e : ENGINEER_OPEN_ID.entrySet()) {
if (openId.equals(e.getValue())) return e.getKey();
}
return null;
}
发送交互式卡片通知 — notifyAssigneeByCard:
/
* 向被指派工程师推送"新工单到了"的卡片 */
public static void notifyAssigneeByCard(String assignName, String typeName,
String orderNo, String deviceName, String location,
String priority, String description, String createBy) {

String openId = resolveAssignToOpenId(assignName, null);
if (openId == null || openId.isEmpty()) {
System.out.println("[飞书] 跳过通知:找不到 " + assignName + " 对应的 open_id");
return;
}

// 构建卡片内容(简化版:展示关键信息 + 操作按钮)
String card = buildSimpleCard(
"✅ 你收到一条新的" + typeName,
"blue",
new String[][]{
{"工单/计划编号", orderNo == null || orderNo.isEmpty() ? "—" : orderNo},
{"类型", typeName},
{"设备", deviceName == null ? "—" : deviceName},
{"位置", location == null ? "—" : location},
{"优先级", priority == null || priority.isEmpty() ? "中" : priority},
{"描述", description == null ? "—" : description},
{"创建人", createBy == null ? "系统用户" : createBy}
}
);

boolean ok = sendInteractiveCard(openId, "open_id", card);
System.out.println("[飞书] 向 " + assignName + "(" + openId + ") 发送"
+ (ok ? "✅ 成功" : "❌ 失败") + " 卡片通知");
}
sendInteractiveCard — 通过 HTTP POST 调飞书接口发送卡片:
public static boolean sendInteractiveCard(String receiveId, String receiveIdType, String cardContentJson) {
String token = getTenantAccessToken();
if (token == null) { System.out.println("获取飞书 token 失败!"); return false; }
try {
URL url = new URL(FeishuConfig.MESSAGE_URL + "?receive_id_type=" + receiveIdType);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "Bearer " + token);
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);

String requestJson = "{"receive_id":"" + receiveId
+ "","msg_type":"interactive","content":"" + escapeJson(cardContentJson) + ""}";

try (OutputStream os = conn.getOutputStream()) {
os.write(requestJson.getBytes(StandardCharsets.UTF_8));
}
int responseCode = conn.getResponseCode();
return responseCode >= 200 && responseCode < 300;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

http://www.jsqmd.com/news/1037992/

相关文章:

  • 用 MLflow 系统化评估大语言模型:新手入门与工程实践
  • 告别手动标注:用Semi_Utils智能水印提升摄影作品专业度
  • Gemini原生多模态原理与工程实践指南
  • 2026年 全自动加袋机/FFS吨袋上袋机厂家推荐榜:智能精准与高效稳定的优质品牌解析及选购指南 - 品牌发掘
  • 洛雪音乐音源配置全攻略:3分钟解锁全网无损音乐的正确姿势
  • MiniMax M2 Agent:开箱即用的AI协作者如何重塑前端开发范式
  • pandas多维聚合实战:银行风控中的生产级聚合模式
  • MC92600 Quad DDR SERDES系统设计:启动、待机、中继模式与电源完整性详解
  • 5.19冲刺
  • 西安未央学车怎么选?未央湖快马优驾自有训练场驾校深度实地测评 联系电话:17792657403 地址:陕西省未央区未央湖街道花辰路 - 资讯纵览
  • 闲置京东E卡怎么回收划算?2026年6月折扣计算方式与三家主流平台到账速度实测 - 信息热点
  • Java IO 文件复制
  • 2026 年招标智能清标工具客观测试与高合规使用指南 - 资讯纵览
  • 兰花油选购指南|哪个牌子性价比高?适合什么肤质?怎么用? - 信息热点
  • 多模态大语言模型融合技术:ES-Merging方法解析与应用
  • 探寻优质汽车传感器厂家?这里有可靠的联系方式! - 资讯纵览
  • 2026年PEEK注塑厂家实力解析:模具开发/精密注塑/非标定制/工程塑料加工 - 资讯纵览
  • 2026 石家庄高端婚恋推荐榜 TOP1|将爱婚恋:燕赵纸媒背书,本地精英本硕博专属严选平台 - 星际AI
  • AI落地实战:从迷人趋势到可拆解、可验证、可迭代的工程化路径
  • 5个突破性技巧:彻底解决Amlogic S905L3B设备Armbian部署实战难题
  • 上班族在职备考法考:四大热门APP实测,哪款能帮你充分利用碎片时间 - 信息热点
  • 7+ Taskbar Tweaker:如何彻底掌控Windows任务栏的5个核心维度?
  • Pandas多维聚合五大生产级模式:跨列异构、自定义函数、滚动窗口、扩展计算与语义重塑
  • 2026年 上海工程监理服务/工程造价咨询/全过程项目管理公司推荐:专业严谨与高效透明的最新口碑之选 - 品牌发掘
  • 固安睛睿眼镜深耕视光二十载 全品类配镜一站式门店深度解读 联系电话:183336301983 地址:河北省廊坊市固安县固安镇新昌街凤凰城小区37号楼一单元1601 - 资讯纵览
  • 2026年TikTok Shop大促全攻略:从新手到大卖的11个核心知识点 - 信息热点
  • Qwen3.6-Plus实战指南:视觉编程、多模态推理与Agentic任务落地
  • 不小心弄丢文件?9种电脑数据恢复方法,新手高手通用
  • 手把手复现RLHF摘要模型:从奖励建模到PPO调优的工程实践
  • 2026年国内电池盒总成检具厂家推荐:新能源汽车核心检测装备实力解析 - 资讯纵览