isTokenValid(request)

本文介绍Struts如何通过令牌机制在Action处理过程中有效防止表单重复提交,详细解释了Struts提供的相关方法及其实现原理。

Struts中的isTokenValid(request,boolean)是用来防止表单重复提交的!  
Struts在你每次访问Action的时候,根据用户会话ID和当前系统时间对于每个会话生成一个唯一令牌,保存在你的Session里面,如果你在Action里的函数里面,使用了saveToken(request),那么这个令牌也会保存在这个Action所Forward到的jsp所生成的静态页面里。 
如果你在你Action的方法里使用了isTokenValid,那么Struts将会从你的request里面去获取这个令牌值,然后和Session里的令牌值做比较,如果两者相等,就不是重复提交,如果不相等,就是重复提交了。

一,首先介绍一下struts提供的有关令牌环的相关方法

请求有效性处理,使用令牌可以有效的防止重复提交。
protected String generateToken(HttpServletRequest request) 创建一个令牌.
protected boolean isTokenValid(HttpServletRequest request) 检查令牌是否有效
protected boolean isTokenValid(HttpServletRequest request,Boolean reset) 检查令牌是否有效,并且重置令牌(如果reset 是true)
protected void resetToken(HttpServletRequest request) 重置令牌
protected void saveToken(HttpServletRequest request) 添加令牌

二,利用struts的同步令牌机制

利用同步令牌(Token)机制来解决Web应用中重复提交的问题,Struts也给出了一个参考实现。

基本原理:

服务器端在处理到达的请求之前,会将请求中包含的令牌值与保存在当前用户会话中的令牌值进行比较,
看是否匹配。在处理完该请求后,且在答复发送给客户端之前,将会产生一个新的令牌,该令牌除传给
客户端以外,也会将用户会话中保存的旧的令牌进行替换。这样如果用户回退到刚才的提交页面并再次
提交的话,客户端传过来的令牌就和服务器端的令牌不一致,从而有效地防止了重复提交的发生。

if (isTokenValid(request, true)) {
    // your code here
    return mapping.findForward("success");
} else {
    saveToken(request);
    return mapping.findForward("submitagain");
}

Struts根据用户会话ID和当前系统时间来生成一个唯一(对于每个会话)令牌的,具体实现可以参考
TokenProcessor类中的generateToken()方法。

1. //验证事务控制令牌,<html:form >会自动根据session中标识生成一个隐含input代表令牌,防止两次提交
2. 在action中:

       //<input type="hidden" name="org.apache.struts.taglib.html.TOKEN"
       // value="6aa35341f25184fd996c4c918255c3ae">
       if (!isTokenValid(request))
           errors.add(ActionErrors.GLOBAL_ERROR,
                      new ActionError("error.transaction.token"));
       resetToken(request); //删除session中的令牌

3. action有这样的一个方法生成令牌

   protected String generateToken(HttpServletRequest request) {
       HttpSession session = request.getSession();
       try {
           byte id[] = session.getId().getBytes();
           byte now[] =
               new Long(System.currentTimeMillis()).toString().getBytes();
           MessageDigest md = MessageDigest.getInstance("MD5");
           md.update(id);
           md.update(now);
           return (toHex(md.digest()));
       } catch (IllegalStateException e) {
           return (null);
       } catch (NoSuchAlgorithmException e) {
           return (null);
       }
   }


package org.example; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; /** * 飞书公司圈帖子数据抓取并同步至多维表格 */ @RestController @RequestMapping("/feishu/events") public class MomentsBitableIntegration { private static final Logger logger = LoggerFactory.getLogger(MomentsBitableIntegration.class); private static final String APP_ID = "cli_a77e623b63fbd00d"; private static final String APP_SECRET = "p1y9z84vBOxSClmqn4y0CcVJPrdKeF3Y"; private static final String ENCRYPT_KEY = "eGXBxvckaDb7NdN8gC4ZW4YQishxILFi"; // 飞书事件加密密钥 private static final String TENANT_ACCESS_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"; private static final String BITABLE_APP_TOKEN = "NQyxb7bxma1BNDszeFqcHLrbnje"; private static final String BITABLE_TABLE_ID = "tblycyMgokQQDoAd"; private static final String MOMENTS_POST_API = "https://open.feishu.cn/open-apis/moments/v1/posts/"; private static final String USER_INFO_API = "https://open.feishu.cn/open-apis/contact/v3/users/"; // 日期时间格式化器,用于解析多种格式的日期 private static final DateTimeFormatter[] DATE_TIME_FORMATTERS = { DateTimeFormatter.ISO_OFFSET_DATE_TIME, DateTimeFormatter.ISO_LOCAL_DATE_TIME, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") }; private final OkHttpClient httpClient = new OkHttpClient(); private final Gson gson = new Gson(); private final JsonParser jsonParser = new JsonParser(); private String tenantAccessToken; private long lastTokenRefreshTime; // 记录上次令牌刷新时间 private ScheduledExecutorService scheduler; private Map<String, PostData> postDataMap = new ConcurrentHashMap<>(); private final Map<String, String> userCache = new ConcurrentHashMap<>(); // 用户信息缓存 private final EventReceiver eventReceiver; // 帖子数据结构 static class PostData { String postId; String author; // 作者姓名 String authorId; // 作者ID(open_id等) List<String> mentionedUsers = new ArrayList<>(); int likeCount; int dislikeCount; double popularityScore; String content; long publishTimeUnix = 0; // 存储毫秒级Unix时间戳 public PostData(String postId) { this.postId = postId; } } public MomentsBitableIntegration() { this.eventReceiver = new EventReceiver(this); init(); scheduleWeeklyReport(); } // 初始化飞书认证 public void init() { try { refreshAccessToken(); scheduler = Executors.newScheduledThreadPool(2); // 添加定时刷新token任务(每90分钟刷新一次) scheduler.scheduleAtFixedRate(() -> { try { refreshAccessToken(); } catch (Exception e) { logger.error("刷新访问令牌失败", e); } }, 0, 90, TimeUnit.MINUTES); logger.info("飞书集成服务初始化完成"); } catch (Exception e) { logger.error("初始化失败", e); System.exit(1); } } // 刷新访问令牌 private void refreshAccessToken() throws IOException { MediaType JSON = MediaType.get("application/json; charset=utf-8"); String json = "{\"app_id\":\"" + APP_ID + "\",\"app_secret\":\"" + APP_SECRET + "\"}"; okhttp3.RequestBody body = okhttp3.RequestBody.create(json, JSON); Request request = new Request.Builder() .url(TENANT_ACCESS_TOKEN_URL) .post(body) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { logger.error("刷新令牌请求失败: HTTP {}", response.code()); throw new IOException("刷新令牌失败: HTTP " + response.code()); } String responseData = response.body().string(); Type type = new TypeToken<Map<String, Object>>(){}.getType(); Map<String, Object> result = gson.fromJson(responseData, type); if (result.containsKey("tenant_access_token")) { tenantAccessToken = (String) result.get("tenant_access_token"); lastTokenRefreshTime = System.currentTimeMillis(); logger.info("访问令牌刷新成功"); } else { logger.error("获取访问令牌失败: {}", responseData); throw new IOException("获取访问令牌失败: " + result.get("msg")); } } } // 检查令牌是否有效 private boolean isTokenValid() { // 令牌有效期为2小时,提前10分钟刷新 return System.currentTimeMillis() - lastTokenRefreshTime < 110 * 60 * 1000; } // 处理飞书事件推送 @PostMapping("/callback") public Map<String, Object> handleFeishuEvent(HttpServletRequest request) { String eventJson = null; try { // 读取请求体 StringBuilder requestBody = new StringBuilder(); try (BufferedReader reader = request.getReader()) { String line; while ((line = reader.readLine()) != null) { requestBody.append(line); } } eventJson = requestBody.toString(); logger.info("收到飞书事件: {}", eventJson); // 用于传递给EventReceiver的事件数据 String eventJsonForReceiver = null; Map<String, Object> eventData; // 判断是否为加密数据 if (eventJson.contains("\"encrypt\"")) { Map<String, Object> encryptedMap = gson.fromJson( eventJson, new TypeToken<Map<String, Object>>() {}.getType() ); String encryptedData = (String) encryptedMap.get("encrypt"); if (encryptedData == null || encryptedData.isEmpty()) { logger.warn("加密数据为空: {}", eventJson); return createErrorResponse(400, "Missing encrypt data"); } // 解密获取原始事件数据 String decryptedJson = FeishuEncryptUtils.decrypt(ENCRYPT_KEY, encryptedData); logger.info("解密后事件数据: {}", decryptedJson); eventJsonForReceiver = decryptedJson; eventData = gson.fromJson(decryptedJson, new TypeToken<Map<String, Object>>() {}.getType()); } else { eventJsonForReceiver = eventJson; eventData = gson.fromJson(eventJson, new TypeToken<Map<String, Object>>() {}.getType()); } if (eventData == null) { logger.warn("事件数据为空: {}", eventJson); return createErrorResponse(400, "Invalid event data"); } // 1. 优先处理URL验证事件 if (isUrlVerificationEvent(eventData)) { return handleUrlVerification(eventData); } // 2. 获取header Map<String, Object> header = getEventHeader(eventData); if (header == null) { logger.warn("事件header为空: {}", eventJson); return createErrorResponse(400, "Missing event header"); } // 3. 获取事件类型 String eventType = (String) header.get("event_type"); if (eventType == null) { logger.warn("事件类型为空: {}", eventJson); return createErrorResponse(400, "Missing event_type in event data"); } // 4. 验证签名 if (!verifySignature(request, eventJson)) { logger.warn("事件签名验证失败"); return createErrorResponse(401, "Unauthorized"); } // 传递事件数据 eventReceiver.handleEvent(eventJsonForReceiver); return createSuccessResponse(); } catch (Exception e) { logger.error("处理飞书事件失败: {}", eventJson, e); return createErrorResponse(500, "Internal Server Error"); } } // ===== 辅助方法 ===== private boolean isUrlVerificationEvent(Map<String, Object> eventData) { return "url_verification".equals(eventData.get("type")); } private Map<String, Object> getEventHeader(Map<String, Object> eventData) { Object headerObj = eventData.get("header"); if (headerObj instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> header = (Map<String, Object>) headerObj; return header; } return null; } private Map<String, Object> handleUrlVerification(Map<String, Object> eventData) { String challenge = (String) eventData.get("challenge"); if (challenge == null) { logger.warn("URL验证请求缺少challenge参数"); return createErrorResponse(400, "Missing challenge parameter"); } logger.info("完成URL验证,challenge: {}", challenge); return Collections.singletonMap("challenge", challenge); } // 验证事件签名 private boolean verifySignature(HttpServletRequest request, String eventJson) { if (ENCRYPT_KEY == null || ENCRYPT_KEY.isEmpty()) { return true; } String timestamp = request.getHeader("X-Lark-Request-Timestamp"); String nonce = request.getHeader("X-Lark-Request-Nonce"); String signature = request.getHeader("X-Lark-Signature"); if (timestamp == null || nonce == null || signature == null) { logger.warn("缺少签名参数:timestamp={}, nonce={}, signature={}", timestamp, nonce, signature); return false; } String signStr = timestamp + nonce + ENCRYPT_KEY + eventJson; try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hashBytes = md.digest(signStr.getBytes(StandardCharsets.UTF_8)); String calculatedSignature = bytesToHex(hashBytes); return calculatedSignature.equals(signature); } catch (NoSuchAlgorithmException e) { logger.error("签名计算失败", e); return false; } } // 字节数组转十六进制字符串 private String bytesToHex(byte[] bytes) { StringBuilder hexString = new StringBuilder(); for (byte b : bytes) { String hex = String.format("%02x", b); hexString.append(hex); } return hexString.toString(); } // 处理表情互动创建事件 public void handleReactionCreatedEvent(Map<String, Object> eventData) { try { Map<String, Object> event = (Map<String, Object>) eventData.get("event"); if (event == null) { logger.warn("无效的表情互动事件: {}", eventData); return; } String postId = (String) event.get("entity_id"); String reactionType = (String) event.get("type"); if (postId == null || reactionType == null) { logger.warn("表情互动事件缺少必要字段: postId={}, reactionType={}", postId, reactionType); return; } PostData postData = postDataMap.computeIfAbsent(postId, PostData::new); // 核心增强:通过帖子ID获取作者信息 if (postData.author == null || postData.authorId == null) { try { // 第一步:获取帖子详情(包含作者ID) fetchPostDetails(postData); // 第二步:如果获取到了作者ID但还没有姓名,则调用用户接口 if (postData.authorId != null && (postData.author == null || postData.author.equals("未知作者"))) { logger.info("通过表情互动事件获取作者信息: 帖子ID={} → 作者ID={}", postId, postData.authorId); postData.author = getUserName(postData.authorId); logger.info("获取作者姓名成功: {} → {}", postData.authorId, postData.author); } } catch (Exception e) { logger.error("通过表情互动事件获取作者信息失败", e); } } if ("THUMBSUP".equals(reactionType)) { postData.likeCount++; logger.info("帖子 {} 收到新点赞,当前点赞数: {}", postId, postData.likeCount); } updatePopularityScore(postData); } catch (Exception e) { logger.error("处理表情互动事件失败", e); } } // 处理表情互动删除事件 public void handleReactionDeletedEvent(Map<String, Object> eventData) { try { Map<String, Object> event = (Map<String, Object>) eventData.get("event"); if (event == null) { logger.warn("无效的表情互动删除事件: {}", eventData); return; } String postId = (String) event.get("entity_id"); String reactionType = (String) event.get("type"); if (postId == null || reactionType == null) { logger.warn("表情互动删除事件缺少必要字段: postId={}, reactionType={}", postId, reactionType); return; } PostData postData = postDataMap.computeIfAbsent(postId, PostData::new); // 通过帖子ID获取作者信息 if (postData.author == null || postData.authorId == null) { try { fetchPostDetails(postData); if (postData.authorId != null && (postData.author == null || postData.author.equals("未知作者"))) { postData.author = getUserName(postData.authorId); } } catch (Exception e) { logger.error("获取帖子详情失败,但将继续处理事件", e); } } if ("THUMBSUP".equals(reactionType)) { if (postData.likeCount > 0) { postData.likeCount--; logger.info("帖子 {} 取消点赞,当前点赞数: {}", postId, postData.likeCount); } else { logger.warn("帖子 {} 点赞数为0,但收到取消点赞事件", postId); } } updatePopularityScore(postData); } catch (Exception e) { logger.error("处理表情互动删除事件失败", e); } } // 处理点踩创建事件 public void handleDislikeCreatedEvent(Map<String, Object> eventData) { try { Map<String, Object> event = (Map<String, Object>) eventData.get("event"); if (event == null) { logger.warn("无效的点踩事件: {}", eventData); return; } String postId = (String) event.get("entity_id"); if (postId == null) { logger.warn("点踩事件缺少必要字段: {}", eventData); return; } PostData postData = postDataMap.computeIfAbsent(postId, PostData::new); // 通过帖子ID获取作者信息 if (postData.author == null || postData.authorId == null) { try { fetchPostDetails(postData); if (postData.authorId != null && (postData.author == null || postData.author.equals("未知作者"))) { postData.author = getUserName(postData.authorId); } } catch (Exception e) { logger.error("获取帖子详情失败,但将继续处理事件", e); } } postData.dislikeCount++; logger.info("帖子 {} 收到新点踩,当前点踩数: {}", postId, postData.dislikeCount); updatePopularityScore(postData); } catch (Exception e) { logger.error("处理点踩事件失败", e); } } // 处理点踩删除事件 public void handleDislikeDeletedEvent(Map<String, Object> eventData) { try { Map<String, Object> event = (Map<String, Object>) eventData.get("event"); if (event == null) { logger.warn("无效的点踩删除事件: {}", eventData); return; } String postId = (String) event.get("entity_id"); if (postId == null) { logger.warn("点踩删除事件缺少必要字段: {}", eventData); return; } PostData postData = postDataMap.computeIfAbsent(postId, PostData::new); // 通过帖子ID获取作者信息 if (postData.author == null || postData.authorId == null) { try { fetchPostDetails(postData); if (postData.authorId != null && (postData.author == null || postData.author.equals("未知作者"))) { postData.author = getUserName(postData.authorId); } } catch (Exception e) { logger.error("获取帖子详情失败,但将继续处理事件", e); } } if (postData.dislikeCount > 0) { postData.dislikeCount--; logger.info("帖子 {} 取消点踩,当前点踩数: {}", postId, postData.dislikeCount); } else { logger.warn("帖子 {} 点踩数为0,但收到取消点踩事件", postId); } updatePopularityScore(postData); } catch (Exception e) { logger.error("处理点踩删除事件失败", e); } } public void handlePostCreatedEvent(Map<String, Object> eventData) { try { Map<String, Object> event = (Map<String, Object>) eventData.get("event"); if (event == null) { logger.warn("无效的帖子创建事件: {}", eventData); return; } String postId = (String) event.get("id"); if (postId == null) { logger.warn("帖子创建事件缺少必要字段: {}", eventData); return; } PostData postData = postDataMap.computeIfAbsent(postId, PostData::new); try { fetchPostDetails(postData); } catch (Exception e) { logger.error("获取帖子详情失败", e); } Map<String, Object> userIdentity = (Map<String, Object>) event.get("user_id"); String eventUserId = null; if (userIdentity != null) { eventUserId = (String) userIdentity.get("open_id"); if (eventUserId == null) { eventUserId = (String) userIdentity.get("union_id"); } } if (postData.author == null && eventUserId != null) { try { postData.author = getUserName(eventUserId); } catch (Exception e) { logger.error("获取用户姓名失败", e); postData.author = eventUserId; } } logger.info("新帖子创建: {}, 作者ID: {}, 发布时间: {}", postId, eventUserId, postData.publishTimeUnix); logger.info("作者姓名: {}", postData.author); } catch (Exception e) { logger.error("处理帖子创建事件失败", e); } } // 判断用户ID类型 private String determineUserIdType(String userId) { if (userId.startsWith("ou_")) return "open_id"; if (userId.startsWith("u_")) return "user_id"; if (userId.startsWith("on_")) return "union_id"; return null; } // 获取用户姓名(带缓存和重试机制) private String getUserName(String userId) throws IOException { // 检查缓存 if (userCache.containsKey(userId)) { String cachedName = userCache.get(userId); logger.info("用户信息命中缓存: {} → {}", userId, cachedName); // 调试日志:检查缓存中的名称是否是中文 if (!isValidChineseName(cachedName)) { logger.warn("缓存中的名称不是中文: {} → {}", userId, cachedName); } return cachedName; } if (userId == null || userId.isEmpty()) { logger.warn("用户ID为空"); return "无效ID"; } String userType = determineUserIdType(userId); if (userType == null) { logger.warn("不支持的用户ID类型: {}", userId); return "未知用户"; } int retries = 3; while (retries-- > 0) { try { String url = USER_INFO_API + userId + "?department_id_type=open_department_id&user_id_type=" + userType; logger.debug("调用用户信息API: {}", url); Request request = new Request.Builder() .url(url) .header("Authorization", "Bearer " + tenantAccessToken) .get() .build(); try (Response response = httpClient.newCall(request).execute()) { logger.debug("用户信息接口响应: HTTP {}", response.code()); if (!response.isSuccessful()) { logger.error("用户信息接口错误: HTTP {} - {}", response.code(), response.message()); if (response.code() == 429) { Thread.sleep(1000); continue; } else if (response.code() == 404) { logger.warn("用户不存在: {}", userId); return "已离职用户"; } throw new IOException("获取用户信息失败: HTTP " + response.code()); } String responseData = response.body().string(); logger.debug("用户信息API原始响应: {}", responseData); // 添加原始响应日志 JsonObject result = jsonParser.parse(responseData).getAsJsonObject(); if (result.has("code") && result.get("code").getAsInt() != 0) { logger.error("飞书API返回错误: {}", responseData); continue; } JsonObject data = result.getAsJsonObject("data"); if (data != null && data.has("user")) { JsonObject user = data.getAsJsonObject("user"); // === 详细调试日志开始 === logger.info("完整用户对象: {}", user.toString()); // 记录完整用户对象 String userName = null; String source = "未知来源"; // 1. 优先使用name字段 if (user.has("name")) { JsonElement nameElement = user.get("name"); if (nameElement.isJsonPrimitive()) { userName = nameElement.getAsString().trim(); source = "name字段"; logger.info("从{}获取用户名: {}", source, userName); // 检查是否是有效中文名 if (isValidChineseName(userName)) { logger.debug("name字段是有效中文名"); } else { logger.warn("name字段不是中文名: {}", userName); } } } // 2. 尝试custom_attrs获取中文名 if ((userName == null || !isValidChineseName(userName)) && user.has("custom_attrs")) { JsonObject customAttrs = user.getAsJsonObject("custom_attrs"); logger.debug("检查custom_attrs: {}", customAttrs); for (String key : customAttrs.keySet()) { // 查找包含"中文"或"姓名"的自定义字段 if (key.contains("中文") || key.contains("姓名")) { JsonElement attrElement = customAttrs.get(key); if (attrElement.isJsonPrimitive()) { String candidate = attrElement.getAsString().trim(); logger.debug("找到可能的中文名字段: {} = {}", key, candidate); if (isValidChineseName(candidate)) { userName = candidate; source = "custom_attrs." + key; logger.info("从{}获取中文名: {}", source, userName); break; } } } } } // 3. 尝试en_name字段 if ((userName == null || userName.isEmpty()) && user.has("en_name")) { JsonElement enNameElement = user.get("en_name"); if (enNameElement.isJsonPrimitive()) { userName = enNameElement.getAsString().trim(); source = "en_name字段"; logger.info("从{}获取用户名: {}", source, userName); } } // 4. 回退到user_id if ((userName == null || userName.isEmpty()) && user.has("user_id")) { JsonElement userIdElement = user.get("user_id"); if (userIdElement.isJsonPrimitive()) { userName = userIdElement.getAsString().trim(); source = "user_id字段"; logger.info("从{}获取用户名: {}", source, userName); } } // 5. 最终回退 if (userName == null || userName.isEmpty()) { userName = "未知用户"; source = "默认值"; logger.warn("无法获取有效用户名,使用默认值"); } // 检查最终用户名是否是中文 if (!isValidChineseName(userName)) { logger.warn("最终用户名不是中文: {} → {} (来源: {})", userId, userName, source); logger.warn("完整用户对象: {}", user.toString()); } else { logger.info("获取到中文名: {} → {}", userId, userName); } // === 详细调试日志结束 === // 缓存结果 logger.info("缓存用户信息: {} → {}", userId, userName); userCache.put(userId, userName); logger.info("最终确定的用户名: {} → {}", userId, userName); return userName; } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("用户信息请求被中断", e); } catch (Exception e) { logger.error("获取用户信息异常", e); if (retries == 0) { logger.error("最终获取用户信息失败: {}", userId, e); return "未知用户"; } try { Thread.sleep(2000); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } return "未知用户"; } // 检查是否是有效中文名 private boolean isValidChineseName(String name) { if (name == null || name.isEmpty()) return false; // 简单检查:包含中文字符即为有效 return name.matches(".*[\\u4e00-\\u9fa5]+.*"); } // 获取帖子详情(严格使用user_id字段) private void fetchPostDetails(PostData postData) throws IOException { Request request = new Request.Builder() .url(MOMENTS_POST_API + postData.postId) .header("Authorization", "Bearer " + tenantAccessToken) .get() .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("获取帖子详情失败: HTTP " + response.code()); } String responseData = response.body().string(); logger.debug("帖子详情API响应: {}", responseData); JsonObject result = jsonParser.parse(responseData).getAsJsonObject(); JsonObject data = result.getAsJsonObject("data"); if (data != null) { JsonObject post = data.getAsJsonObject("post"); if (post != null) { // ===== 严格使用user_id字段 ===== String authorId = null; String authorSource = "unknown"; // 只使用user_id字段获取作者ID if (post.has("user_id")) { JsonElement userIdElem = post.get("user_id"); // 处理字符串格式的user_id if (userIdElem.isJsonPrimitive()) { authorId = userIdElem.getAsString(); authorSource = "user_id"; logger.info("从user_id字段获取作者ID: {}", authorId); // 标准化ID格式(确保以ou_开头) if (!authorId.startsWith("ou_") && authorId.length() == 32) { authorId = "ou_" + authorId; logger.info("标准化作者ID格式: {} → {}", authorId, authorId); } } // 处理对象格式的user_id else if (userIdElem.isJsonObject()) { JsonObject userIdObj = userIdElem.getAsJsonObject(); if (userIdObj.has("open_id")) { authorId = userIdObj.get("open_id").getAsString(); authorSource = "user_id.open_id"; logger.info("从user_id.open_id获取作者ID: {}", authorId); } } } // 存储作者ID if (authorId != null) { postData.authorId = authorId; logger.info("获取到帖子作者ID: {} (来源: {})", authorId, authorSource); // 获取作者姓名 if (postData.author == null) { try { postData.author = getUserName(authorId); logger.info("通过作者ID获取姓名: {} → {}", authorId, postData.author); } catch (Exception e) { logger.error("获取作者姓名失败", e); postData.author = "未知作者"; } } } else { logger.warn("无法确定帖子作者ID: {}", postData.postId); } // ===== 其他字段处理 ===== // 提及用户处理 if (post.has("mentions")) { for (JsonElement mention : post.getAsJsonArray("mentions")) { JsonObject mentionObj = mention.getAsJsonObject(); String mentionedUserId = null; if (mentionObj.has("open_id")) { mentionedUserId = mentionObj.get("open_id").getAsString(); } else if (mentionObj.has("user_id")) { mentionedUserId = mentionObj.get("user_id").getAsString(); } else if (mentionObj.has("id")) { mentionedUserId = mentionObj.get("id").getAsString(); } if (mentionedUserId != null) { postData.mentionedUsers.add(getUserName(mentionedUserId)); } else if (mentionObj.has("name")) { postData.mentionedUsers.add(mentionObj.get("name").getAsString()); } } } // 内容处理 if (post.has("content")) { // 解析富文本内容为纯文本 String contentJson = post.get("content").getAsString(); postData.content = parseRichTextToPlainText(contentJson); } // 时间处理逻辑 if (post.has("create_time")) { try { JsonElement elem = post.get("create_time"); if (elem.isJsonPrimitive()) { if (elem.getAsJsonPrimitive().isString()) { String timeStr = elem.getAsString(); // 尝试多种格式解析 postData.publishTimeUnix = parseTimeToUnixMillis(timeStr); logger.info("解析发布时间: {} → {}", timeStr, postData.publishTimeUnix); } else if (elem.getAsJsonPrimitive().isNumber()) { postData.publishTimeUnix = elem.getAsLong() * 1000; logger.info("解析发布时间戳: {}", postData.publishTimeUnix); } } } catch (Exception e) { logger.error("解析create_time失败", e); } } } } } catch (Exception e) { logger.error("解析帖子详情失败", e); throw new IOException("解析帖子详情失败: " + e.getMessage()); } } // 发布时间解析方法 private long parseTimeToUnixMillis(String timeStr) { if (timeStr == null || timeStr.isEmpty()) { return 0; } // 1. 尝试解析为数字(秒级时间戳) try { long seconds = Long.parseLong(timeStr); return seconds * 1000; } catch (NumberFormatException e) { // 不是数字,继续尝试其他格式 } // 2. 尝试多种日期格式 for (DateTimeFormatter formatter : DATE_TIME_FORMATTERS) { try { // 尝试带时区解析 if (formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME) { ZonedDateTime zdt = ZonedDateTime.parse(timeStr, formatter); return zdt.toInstant().toEpochMilli(); } // 尝试本地时间解析(默认时区) LocalDateTime ldt = LocalDateTime.parse(timeStr, formatter); return ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); } catch (DateTimeParseException e) { } } // 3. 尝试其他可能的格式 try { // 尝试解析为毫秒级时间戳 return Long.parseLong(timeStr); } catch (NumberFormatException e) { // 解析失败 } logger.warn("无法解析时间字符串: {}", timeStr); return 0; } // 解析飞书富文本格式为纯文本 private String parseRichTextToPlainText(String richTextJson) { try { if (richTextJson == null || richTextJson.isEmpty()) { return ""; } JsonArray blocks = jsonParser.parse(richTextJson).getAsJsonArray(); StringBuilder plainText = new StringBuilder(); for (JsonElement block : blocks) { if (block.isJsonArray()) { JsonArray elements = block.getAsJsonArray(); for (JsonElement element : elements) { if (element.isJsonObject()) { JsonObject obj = element.getAsJsonObject(); if (obj.has("tag") && obj.has("text")) { String tag = obj.get("tag").getAsString(); String text = obj.get("text").getAsString(); // 处理不同类型的标签 if ("text".equals(tag)) { plainText.append(text); } else if ("hashtag".equals(tag)) { // 保留话题标签的文本内容 plainText.append("#").append(text).append(" "); } // 可以根据需要添加更多标签类型的处理 } } } // 块之间添加换行 plainText.append("\n"); } } return plainText.toString().trim(); } catch (Exception e) { logger.error("解析富文本失败: {}", richTextJson, e); // 如果解析失败,返回原始内容 return richTextJson; } } // 更新帖子热度值 private void updatePopularityScore(PostData postData) { double newScore = postData.likeCount * 1.0 - postData.dislikeCount * 0.5; double oldScore = postData.popularityScore; postData.popularityScore = newScore; logger.info("帖子 {} 热度更新: {} → {}", postData.postId, oldScore, newScore); } // 安排报告任务(每10分钟生成一次报告并清理缓存) public void scheduleWeeklyReport() { // 立即执行第一次报告生成和缓存清理 long initialDelay = 0; long period = 10; // 10分钟间隔 // 每10分钟生成报告 scheduler.scheduleAtFixedRate(() -> { try { logger.info("开始执行定期报告生成"); generateWeeklyReport(); } catch (Exception e) { logger.error("生成报告失败", e); } }, initialDelay, period, TimeUnit.MINUTES); // 每10分钟清理用户缓存 scheduler.scheduleAtFixedRate(() -> { logger.info("开始清理用户缓存"); int cacheSize = userCache.size(); userCache.clear(); logger.info("用户缓存已清除,共清理 {} 条记录", cacheSize); }, initialDelay, period, TimeUnit.MINUTES); LocalDateTime firstExecution = LocalDateTime.now().plusMinutes(initialDelay); logger.info("任务已安排:报告生成和缓存清理将于 {} 开始执行,每 {} 分钟运行一次", firstExecution.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")), period); } // 生成周报并同步至多维表格 private void generateWeeklyReport() throws IOException { logger.info("开始生成周报..."); long insertionTime = System.currentTimeMillis(); // 获取统一的插入时间 List<PostData> topPosts = postDataMap.values().stream() .sorted(Comparator.comparingDouble(p -> -p.popularityScore)) .limit(3) .collect(Collectors.toList()); if (topPosts.isEmpty()) { logger.info("本周没有帖子数据"); return; } List<Map<String, Object>> records = new ArrayList<>(); for (int i = 0; i < topPosts.size(); i++) { PostData post = topPosts.get(i); Map<String, Object> record = new HashMap<>(); Map<String, Object> fields = new HashMap<>(); fields.put("板块信息", "夸一夸"); fields.put("帖子正文", post.content); fields.put("被@的人", String.join(", ", post.mentionedUsers)); fields.put("排名", i + 1); fields.put("综合热度值", post.popularityScore); fields.put("点赞数量", post.likeCount); fields.put("作者", post.author); // 使用帖子自身的发布时间 fields.put("发布时间", post.publishTimeUnix); record.put("fields", fields); records.add(record); logger.info("添加记录: 帖子ID={}, 作者={}, 发布时间={}", post.postId, post.author, post.publishTimeUnix); } insertRecordsToBitable(records); postDataMap.clear(); logger.info("周报生成完成,已同步至多维表格"); } // 插入记录到多维表格(增强错误处理) private void insertRecordsToBitable(List<Map<String, Object>> records) throws IOException { // 检查令牌有效性 if (!isTokenValid()) { logger.warn("访问令牌即将过期,正在刷新..."); refreshAccessToken(); } String url = "https://open.feishu.cn/open-apis/bitable/v1/apps/" + BITABLE_APP_TOKEN + "/tables/" + BITABLE_TABLE_ID + "/records/batch_create"; Map<String, Object> requestBody = new HashMap<>(); requestBody.put("records", records); MediaType JSON = MediaType.get("application/json; charset=utf-8"); okhttp3.RequestBody body = okhttp3.RequestBody.create(gson.toJson(requestBody), JSON); Request request = new Request.Builder() .url(url) .header("Authorization", "Bearer " + tenantAccessToken) .post(body) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { String errorBody = response.body().string(); logger.error("多维表格插入失败: HTTP {} - {}", response.code(), errorBody); // 处理403错误:尝试刷新令牌并重试 if (response.code() == 403) { logger.warn("访问令牌可能失效,尝试刷新并重试..."); refreshAccessToken(); Request newRequest = request.newBuilder() .header("Authorization", "Bearer " + tenantAccessToken) .build(); try (Response retryResponse = httpClient.newCall(newRequest).execute()) { if (!retryResponse.isSuccessful()) { String retryErrorBody = retryResponse.body().string(); logger.error("重试插入失败: HTTP {} - {}", retryResponse.code(), retryErrorBody); throw new IOException("重试插入失败: HTTP " + retryResponse.code()); } logger.info("重试插入成功"); return; } } throw new IOException("插入多维表格失败: HTTP " + response.code()); } String responseData = response.body().string(); logger.info("插入多维表格成功: {}", responseData); } } // 错误响应 private Map<String, Object> createErrorResponse(int code, String msg) { Map<String, Object> result = new HashMap<>(); result.put("code", code); result.put("msg", msg); return result; } // 成功响应 private Map<String, Object> createSuccessResponse() { Map<String, Object> result = new HashMap<>(); result.put("code", 0); result.put("msg", "success"); return result; } } 目前接口返回200
06-18
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值