package org.jeecg.modules.feishu.service; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.util.PasswordUtil; import org.jeecg.common.util.RestUtil; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.andon.dto.AndonOrdeDto; import org.jeecg.modules.mes.entity.FeishuUser; import org.jeecg.modules.system.entity.SysUser; import org.jeecg.modules.system.service.ISysUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Service @Slf4j public class FeishuUserService { // 类中定义模板常量(便于维护) private static final String FIRST_LEVEL_TEMPLATE = "【工艺安灯】\n%s · %s 发起了工艺安灯,请%d分钟内在PDA上进行响应处理!!!"; private static final String SECOND_LEVEL_TEMPLATE = "【工艺安灯】\n%s · %s 发起的工艺安灯,%s未在%d分钟内响应,请尽快督促进行响应处理!!!"; @Resource private ISysUserService sysUserService; @Resource private RestTemplate restTemplate; @Value("${feishu.appId:}") private String appId; @Value("${feishu.appSecret:}") private String appSecret; @Value("${feishu.url:}") private String feishuUrl; // 定时任务线程池(支持多级响应调度) private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); private final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); /** * 同步飞书部门用户到系统用户表 */ public void syncFeishuDepartmentUsers(String departmentId) { try { log.info("开始同步飞书部门用户,部门ID: {}", departmentId); // 1. 获取飞书访问令牌 String accessToken = getFeishuAccessToken(); if (oConvertUtils.isEmpty(accessToken)) { log.error("获取飞书访问令牌失败,终止同步流程"); return; } // 2. 获取部门下的用户列表 List feishuUsers = getDepartmentUsers(accessToken, departmentId); log.info("获取到飞书部门用户数量: {}", feishuUsers.size()); // 3. 同步到系统用户表 int successCount = 0; int updateCount = 0; int addCount = 0; for (FeishuUser feishuUser : feishuUsers) { try { boolean isUpdated = syncFeishuUserToSystem(feishuUser); successCount++; if (isUpdated) { updateCount++; } else { addCount++; } } catch (Exception e) { log.error("同步飞书用户失败,用户ID: {}", feishuUser.getUserId(), e); } } log.info("飞书用户同步完成,总处理: {},新增: {},更新: {}", successCount, addCount, updateCount); } catch (Exception e) { log.error("同步飞书部门用户失败,部门ID: {}", departmentId, e); throw new RuntimeException("同步飞书用户失败: " + e.getMessage()); } } public String getFeishuAccessToken() { try { String url = feishuUrl + "open-apis/auth/v3/tenant_access_token/internal"; log.info("开始获取飞书访问令牌,AppId: {}", appId != null ? appId.substring(0, Math.min(appId.length(), 10)) + "..." : "null"); JSONObject params = new JSONObject(); params.put("app_id", appId); params.put("app_secret", appSecret); log.debug("获取令牌请求参数: {}", params.toJSONString()); JSONObject result = RestUtil.post(url, params); log.info("获取飞书访问令牌响应: {}", result != null ? result.toJSONString() : "null"); if (result != null && result.getInteger("code") == 0) { String accessToken = result.getString("tenant_access_token"); log.info("成功获取飞书访问令牌,令牌长度: {}", accessToken != null ? accessToken.length() : 0); return accessToken; } else { log.error("获取飞书访问令牌失败,响应: {}", result != null ? result.toJSONString() : "null"); if (result != null) { log.error("错误码: {}, 错误信息: {}", result.getInteger("code"), result.getString("msg")); } return null; } } catch (Exception e) { log.error("获取飞书访问令牌异常", e); return null; } } /** * 发送安灯通知消息(支持一、二、三级响应) * 流程: * 1. APP点击发送一级响应(初级响应) * 2. 一级响应时长后检查状态,未处理则发送二级 * 3. 二级响应时长后检查状态,未处理则发送三级 */ public boolean sendAndonNotification(String accessToken, AndonOrdeDto andonOrde) { try { String currentTime = sdf.format(new Date()); log.info("【{}】触发一级响应(初级响应),安灯ID: {}", currentTime, andonOrde.getId()); // 1. 参数验证 if (!validateNotificationParams(andonOrde)) { return false; } if (accessToken == null || accessToken.isEmpty()) { log.error("访问令牌为空,无法发送安灯通知"); return false; } // 2. 发送一级响应通知(即时发送) boolean firstLevelResult = sendLevelNotification(accessToken, andonOrde, 1); // 3. 调度二级响应通知(延迟时间=一级响应时长) if (firstLevelResult) { int secondDelay = andonOrde.getUpgradeResponseDuration(); String secondTime = sdf.format(new Date(System.currentTimeMillis() + secondDelay * 60 * 1000L)); log.info("【{}】一级响应调度二级通知,延迟{}分钟,预计发送时间: {}", currentTime, secondDelay, secondTime); scheduleNextLevelNotification(accessToken, andonOrde, 1, secondDelay); } else { log.warn("一级通知发送失败,终止后续通知流程,安灯ID: {}", andonOrde.getId()); } return firstLevelResult; } catch (Exception e) { log.error("处理安灯通知异常,安灯ID: {}", andonOrde != null ? andonOrde.getId() : "未知", e); return false; } } /** * 发送指定级别的通知 * @param level 1-一级,2-二级,3-三级 */ private boolean sendLevelNotification(String accessToken, AndonOrdeDto andonOrde, int level) { try { String levelDesc = getLevelDesc(level); String openId = getResponderOpenId(andonOrde, level); String currentTime = sdf.format(new Date()); log.info("【{}】开始发送{}级安灯通知,接收人openId: {}, 安灯ID: {}", currentTime, levelDesc, openId, andonOrde.getId()); // 构建请求URL String url = feishuUrl + "open-apis/im/v1/messages?receive_id_type=open_id"; log.info("{}级通知请求URL: {}", levelDesc, url); // 设置请求头 HttpHeaders headers = new HttpHeaders(); String authHeader = "Bearer " + accessToken; headers.add("Authorization", authHeader); headers.add("Content-Type", "application/json; charset=utf-8"); log.info("设置{}级通知请求头: Authorization = {}", levelDesc, authHeader.substring(0, Math.min(30, authHeader.length())) + "..."); // 构建消息内容 JSONObject content = new JSONObject(); String notificationContent = buildNotificationContent(andonOrde, level); content.put("text", notificationContent); log.debug("【{}】{}级通知内容: {}", currentTime, levelDesc, notificationContent); // 构建请求体 JSONObject requestBody = new JSONObject(); requestBody.put("receive_id", openId); requestBody.put("msg_type", "text"); requestBody.put("content", content.toJSONString()); requestBody.put("uuid", "andon_" + andonOrde.getId() + "_level_" + level + "_" + System.currentTimeMillis()); // 发送请求 HttpEntity requestEntity = new HttpEntity<>(requestBody.toJSONString(), headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, requestEntity, String.class ); // 解析响应 JSONObject result = JSONObject.parseObject(response.getBody()); log.debug("{}级通知API响应: {}", levelDesc, result != null ? result.toJSONString() : "null"); if (result != null && result.getInteger("code") == 0) { JSONObject data = result.getJSONObject("data"); if (data != null) { log.info("【{}】{}级安灯通知发送成功,消息ID: {}, 安灯ID: {}", currentTime, levelDesc, data.getString("message_id"), andonOrde.getId()); return true; } else { log.warn("{}级通知响应data为空,安灯ID: {}", levelDesc, andonOrde.getId()); return false; } } else { log.error("发送{}级安灯通知失败,安灯ID: {}", levelDesc, andonOrde.getId()); if (result != null) { log.error("错误码: {}, 错误信息: {}", result.getInteger("code"), result.getString("msg")); } return false; } } catch (Exception e) { log.error("发送{}级安灯通知异常,安灯ID: {}", getLevelDesc(level), andonOrde.getId(), e); return false; } } /** * 调度下一级通知(1→2→3) * @param currentLevel 当前级别 * @param delayMinutes 延迟分钟数(由当前级别响应时长决定) */ private void scheduleNextLevelNotification(String accessToken, AndonOrdeDto andonOrde, int currentLevel, int delayMinutes) { int nextLevel = currentLevel + 1; if (nextLevel > 3) { log.info("已到达最高级别通知,无需继续调度,安灯ID: {}", andonOrde.getId()); return; } String currentLevelDesc = getLevelDesc(currentLevel); String nextLevelDesc = getLevelDesc(nextLevel); String scheduleTime = sdf.format(new Date()); String executeTime = sdf.format(new Date(System.currentTimeMillis() + delayMinutes * 60 * 1000L)); log.info("【{}】调度{}通知,延迟{}分钟(当前级别{}时长),预计执行时间: {}", scheduleTime, nextLevelDesc, delayMinutes, currentLevelDesc, executeTime); scheduler.schedule(() -> { try { String currentExecuteTime = sdf.format(new Date()); log.info("【{}】开始执行{}通知检查,安灯ID: {}", currentExecuteTime, nextLevelDesc, andonOrde.getId()); // 检查订单状态:3表示已响应,无需发送下一级 if (andonOrde.getOrderStatus() != null && "3".equals(andonOrde.getOrderStatus())) { log.info("【{}】订单已响应(status=3),取消{}通知发送,安灯ID: {}", currentExecuteTime, nextLevelDesc, andonOrde.getId()); return; } // 检查下一级响应人是否有效 if (!hasValidResponder(andonOrde, nextLevel)) { log.warn("【{}】{}响应人信息无效,取消通知发送,安灯ID: {}", currentExecuteTime, nextLevelDesc, andonOrde.getId()); return; } // 发送下一级通知 boolean nextLevelResult = sendLevelNotification(accessToken, andonOrde, nextLevel); if (nextLevelResult && nextLevel < 3) { int nextDelay = getResponseDuration(andonOrde, nextLevel); String nextExecuteTime = sdf.format(new Date(System.currentTimeMillis() + nextDelay * 60 * 1000L)); log.info("【{}】{}通知调度三级通知,延迟{}分钟,预计发送时间: {}", currentExecuteTime, nextLevelDesc, nextDelay, nextExecuteTime); scheduleNextLevelNotification(accessToken, andonOrde, nextLevel, nextDelay); } else if (!nextLevelResult) { log.warn("【{}】{}通知发送失败,终止后续调度,安灯ID: {}", currentExecuteTime, nextLevelDesc, andonOrde.getId()); } } catch (Exception e) { log.error("执行{}调度任务异常,安灯ID: {}", nextLevelDesc, andonOrde.getId(), e); } }, delayMinutes, TimeUnit.MINUTES); } /** * 构建通知内容(优化分隔符,提升可读性) */ private String buildNotificationContent(AndonOrdeDto andonOrde, int level) { // 获取工厂名称和产线名称,做空值保护 String parentFactoryName = andonOrde.getParentFactoryName() != null ? andonOrde.getParentFactoryName() : "未知工厂"; String factoryName = andonOrde.getFactoryName() != null ? andonOrde.getFactoryName() : "未知产线"; String content; if (level == 1) { // 一级模板:使用"·"分隔工厂和产线 int firstDuration = andonOrde.getUpgradeResponseDuration() != null ? andonOrde.getUpgradeResponseDuration() : 0; content = String.format(FIRST_LEVEL_TEMPLATE, parentFactoryName, factoryName, firstDuration); } else { // 二级模板:保持一致的分隔风格 String prevResponder = getResponderName(andonOrde, level - 1); int prevDuration = getResponseDuration(andonOrde, level - 1); prevResponder = oConvertUtils.isEmpty(prevResponder) ? "未知人员" : prevResponder; prevDuration = Math.max(prevDuration, 0); content = String.format(SECOND_LEVEL_TEMPLATE, parentFactoryName, factoryName, prevResponder, prevDuration); } log.debug("构建{}级通知内容: {}", level, content); return content; } // /** // * 构建通知内容(按级别使用不同模板) // */ // private String buildNotificationContent(AndonButtonDTO andonOrde, int level) { // String factoryName = andonOrde.getFactoryName() != null ? andonOrde.getFactoryName() : "未知产线"; // // if (level == 1) { // // 一级模板:【工艺安灯】XXX产线发起了工艺安灯,请xx分钟内在PDA上进行响应处理!!! // return String.format("【工艺安灯】\n%s产线发起了工艺安灯,请%d分钟内在PDA上进行响应处理!!!", // factoryName, // andonOrde.getUpgradeResponseDuration()); // } else { // // 二/三级模板:【工艺安灯】XXX(产线)发起的工艺安灯,xxx(上一级响应人)未在xx分钟内响应,请尽快督促进行响应处理!!! // String prevLevelDesc = getLevelDesc(level - 1); // String prevResponder = getResponderName(andonOrde, level - 1); // int prevDuration = getResponseDuration(andonOrde, level - 1); // // return String.format("【工艺安灯】\n%s(产线)发起的工艺安灯,%s(%s响应人)未在%d分钟内响应,请尽快督促进行响应处理!!!", // factoryName, // prevResponder, // prevLevelDesc, // prevDuration); // } // } /** * 验证通知相关参数 */ private boolean validateNotificationParams(AndonOrdeDto andonOrde) { if (andonOrde == null) { log.error("安灯订单信息为空"); return false; } if (andonOrde.getId() == null) { log.error("安灯ID为空"); return false; } // 一级响应校验(必选) if (andonOrde.getResponderOpenId() == null || andonOrde.getResponderOpenId().isEmpty() || andonOrde.getUpgradeResponseDuration() == null || andonOrde.getUpgradeResponseDuration() <= 0) { log.error("一级响应人信息无效(openId或时长缺失),安灯ID: {}", andonOrde.getId()); return false; } // 二级响应校验(若存在则必选完整) if (andonOrde.getSecondResponderOpenId() != null && !andonOrde.getSecondResponderOpenId().isEmpty()) { if (andonOrde.getSecondUpgradeResponseDuration() == null || andonOrde.getSecondUpgradeResponseDuration() <= 0) { log.error("二级响应人存在但时长无效,安灯ID: {}", andonOrde.getId()); return false; } } // 三级响应校验(若存在则必选完整) if (andonOrde.getThirdResponderOpenId() != null && !andonOrde.getThirdResponderOpenId().isEmpty()) { if (andonOrde.getSecondUpgradeResponseDuration() == null || andonOrde.getSecondUpgradeResponseDuration() <= 0) { log.error("三级响应人存在但二级时长无效(三级依赖二级时长),安灯ID: {}", andonOrde.getId()); return false; } } return true; } // 工具方法:获取级别描述 private String getLevelDesc(int level) { switch (level) { case 1: return "一级"; case 2: return "二级"; case 3: return "三级"; default: return "未知级别"; } } // 工具方法:获取响应人openId private String getResponderOpenId(AndonOrdeDto andonOrde, int level) { switch (level) { case 1: return andonOrde.getResponderOpenId(); case 2: return andonOrde.getSecondResponderOpenId(); case 3: return andonOrde.getThirdResponderOpenId(); default: return null; } } // 工具方法:获取响应人名称 private String getResponderName(AndonOrdeDto andonOrde, int level) { String name = null; switch (level) { case 1: name = andonOrde.getResponder(); break; case 2: name = andonOrde.getSecondResponder(); break; case 3: name = andonOrde.getThirdResponder(); break; default: name = null; } return name != null ? name : "未知响应人"; } // 工具方法:获取响应时长(一级=二级延迟,二级=三级延迟) private int getResponseDuration(AndonOrdeDto andonOrde, int level) { int duration = 0; switch (level) { case 1: duration = andonOrde.getUpgradeResponseDuration() != null ? andonOrde.getUpgradeResponseDuration() : 0; break; case 2: duration = andonOrde.getSecondUpgradeResponseDuration() != null ? andonOrde.getSecondUpgradeResponseDuration() : 0; break; case 3: duration = 0; // 三级是最后一级,无需延迟 break; default: duration = 0; } return duration; } // 工具方法:判断指定级别响应人是否有效 private boolean hasValidResponder(AndonOrdeDto andonOrde, int level) { String openId = getResponderOpenId(andonOrde, level); return openId != null && !openId.isEmpty(); } /** * 获取部门用户列表(直属员工) */ private List getDepartmentUsers(String accessToken, String departmentId) { List userList = new ArrayList<>(); try { log.info("开始获取飞书部门用户列表,部门ID: {}", departmentId); if (accessToken == null || accessToken.isEmpty()) { log.error("访问令牌为空,无法继续执行"); return userList; } log.info("使用的访问令牌前缀: Bearer {}", accessToken.substring(0, Math.min(20, accessToken.length())) + "..."); String url = feishuUrl + "open-apis/contact/v3/users/find_by_department"; String pageToken = null; int pageNumber = 1; do { StringBuilder urlBuilder = new StringBuilder(url); urlBuilder.append("?department_id=").append(departmentId); urlBuilder.append("&department_id_type=open_department_id"); urlBuilder.append("&page_size=50"); urlBuilder.append("&user_id_type=open_id"); if (pageToken != null && !pageToken.isEmpty()) { urlBuilder.append("&page_token=").append(pageToken); } log.info("请求第{}页数据,URL: {}", pageNumber, urlBuilder.toString()); HttpHeaders headers = new HttpHeaders(); String authHeader = "Bearer " + accessToken; headers.add("Authorization", authHeader); headers.add("Content-Type", "application/json; charset=utf-8"); log.info("设置请求头: Authorization = {}", authHeader.substring(0, Math.min(30, authHeader.length())) + "..."); HttpEntity requestEntity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( urlBuilder.toString(), HttpMethod.GET, requestEntity, String.class ); JSONObject result = JSONObject.parseObject(response.getBody()); log.debug("第{}页API响应: {}", pageNumber, result != null ? result.toJSONString() : "null"); if (result != null && result.getInteger("code") == 0) { JSONObject data = result.getJSONObject("data"); if (data != null) { Object items = data.get("items"); log.info("第{}页原始用户数据数量: {}", pageNumber, items instanceof List ? ((List) items).size() : 0); if (items != null) { List pageUsers = parseUsers(items); userList.addAll(pageUsers); log.info("第{}页解析后用户数量: {}", pageNumber, pageUsers.size()); } pageToken = data.getString("page_token"); boolean hasMore = data.getBooleanValue("has_more"); log.info("第{}页has_more: {}, page_token: {}", pageNumber, hasMore, pageToken); pageNumber++; } else { log.warn("第{}页data为空", pageNumber); break; } } else { log.error("获取飞书部门用户列表失败,响应: {}", result != null ? result.toJSONString() : "null"); if (result != null) { log.error("错误码: {}, 错误信息: {}", result.getInteger("code"), result.getString("msg")); } break; } } while (pageToken != null && !pageToken.isEmpty()); log.info("获取飞书部门用户完成,总用户数量: {}", userList.size()); } catch (Exception e) { log.error("获取飞书部门用户列表异常", e); } return userList; } /** * 解析用户数据 */ @SuppressWarnings("unchecked") private List parseUsers(Object items) { Logger log = LoggerFactory.getLogger(FeishuUserService.class); List userList = new ArrayList<>(); if (!(items instanceof List)) { log.warn("解析用户数据失败,items不是列表类型: {}", items); return userList; } List userItems = (List) items; log.info("开始解析飞书用户列表,共{}条记录", userItems.size()); for (Object item : userItems) { if (item == null || !(item instanceof Map)) { log.warn("跳过无效用户数据项,类型不匹配: {}", item != null ? item.getClass() : "null"); continue; } Map userMap = (Map) item; FeishuUser user = new FeishuUser(); user.setOpenId((String) userMap.getOrDefault("open_id", "")); String name = (String) userMap.getOrDefault("name", ""); if (name.contains(" ")) { String[] parts = name.split(" "); if (parts.length == 2) { user.setUsername(parts[0].trim()); user.setRealname(parts[1].trim()); user.setWorkNo(parts[0].trim()); } else { user.setUsername(name); user.setRealname(name); user.setWorkNo(""); log.warn("飞书用户name字段格式异常,无法拆分,原始值:{}", name); } } else { user.setUsername(name); user.setRealname(name); user.setWorkNo(""); log.warn("飞书用户name字段无空格分隔,无法拆分,原始值:{}", name); } user.setEnName((String) userMap.getOrDefault("en_name", "")); user.setDescription((String) userMap.getOrDefault("description", "")); Object mobileVisibleObj = userMap.get("mobile_visible"); user.setMobileVisible(mobileVisibleObj instanceof Boolean ? (Boolean) mobileVisibleObj : false); Object avatarObj = userMap.get("avatar"); if (avatarObj instanceof Map) { Map avatarMap = (Map) avatarObj; user.setAvatar240((String) avatarMap.getOrDefault("avatar_240", "")); user.setAvatar640((String) avatarMap.getOrDefault("avatar_640", "")); user.setAvatar72((String) avatarMap.getOrDefault("avatar_72", "")); user.setAvatarOrigin((String) avatarMap.getOrDefault("avatar_origin", "")); } else { log.info("用户[{}]未设置头像或头像格式异常", user.getRealname()); } user.setPassword("123456"); user.setOpenId((String) userMap.getOrDefault("open_id", "")); userList.add(user); } return userList; } /** * 同步单个飞书用户到系统 */ private boolean syncFeishuUserToSystem(FeishuUser feishuUser) { String username = feishuUser.getUsername(); SysUser existUser = sysUserService.getUserByName(username); SysUser sysUser = new SysUser(); sysUser.setUsername(username); sysUser.setStatus(1); sysUser.setDelFlag(0); sysUser.setAvatar(feishuUser.getAvatar640()); sysUser.setRealname(feishuUser.getRealname()); sysUser.setWorkNo(feishuUser.getWorkNo()); String password = "123456", salt = oConvertUtils.randomGen(8); String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(), password, salt); sysUser.setSalt(salt); sysUser.setPassword(passwordEncode); sysUser.setOpenId(feishuUser.getOpenId()); if (existUser == null) { sysUserService.addUserWithRole(sysUser, "5"); log.info("新增用户: {}", username); return false; } else { sysUser.setId(existUser.getId()); sysUserService.editUser(sysUser); log.info("更新用户: {}", username); return true; } } }