package cc.mrbird.febs.mall.controller.dependentStation; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.TypeReference; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; /** * Tokenview 地址监控 Webhook 接收端点 * 接收 Tokenview 推送的链上交易通知,完成充值匹配 * * @author auto-generated */ @Slf4j @RestController @RequestMapping("/api/tokenview") public class TokenviewWebhookController { @Resource private TokenviewWebhookService tokenviewWebhookService; /** * Webhook 签名密钥(HMAC-SHA256) * 需与 Tokenview 后台配置的 Secret Key 保持一致 * TODO: 移至配置文件 */ private static final String WEBHOOK_SECRET = "dd12521274e434115df5c4277755839766349007fb57936d9d5be0a7a4f0e42f"; /** * 接收 Tokenview 地址监控推送 *

* Tokenview 在监测到地址有交易时,会 POST JSON 到此端点。 * 需要在 Tokenview 后台配置此 URL 和 Secret Key。 *

* 配置步骤: * 1. 登录 Tokenview 开发者后台 * 2. 进入「地址监控」模块 * 3. 添加要监控的 TRC20 地址 * 4. 配置 Webhook URL: https://your-domain/api/tokenview/webhook * 5. 设置 Secret Key(与此处 WEBHOOK_SECRET 一致) */ @PostMapping("/webhook") public Map receiveWebhook(HttpServletRequest request) { Map result = new HashMap<>(); result.put("code", 0); result.put("msg", "ok"); try { // 1. 读取原始请求体 String rawBody = readRequestBody(request); if (StrUtil.isBlank(rawBody)) { result.put("code", 1); result.put("msg", "请求体为空"); return result; } // 2. 验证签名 String signature = request.getHeader("X-Tokenview-Signature"); if (StrUtil.isNotBlank(signature)) { if (!verifySignature(rawBody, signature)) { log.error("Webhook 签名验证失败: signature={}", signature); result.put("code", 2); result.put("msg", "签名验证失败"); return result; } } else { // 也尝试兼容其他常见签名头 signature = request.getHeader("Tokenview-Signature"); if (StrUtil.isNotBlank(signature) && !verifySignature(rawBody, signature)) { log.error("Webhook 签名验证失败: signature={}", signature); result.put("code", 2); result.put("msg", "签名验证失败"); return result; } } log.info("收到 Tokenview Webhook 推送: {}", rawBody); // 3. 解析 JSON Map payload = JSON.parseObject( rawBody, new TypeReference>() {} ); if (payload == null || payload.isEmpty()) { result.put("code", 3); result.put("msg", "JSON 解析失败"); return result; } // 4. 处理充值匹配 String processResult = tokenviewWebhookService.processWebhookTransaction(payload); log.info("Webhook 处理结果: {}", processResult); if (processResult != null && processResult.startsWith("SUCCESS")) { result.put("msg", processResult); } else if (processResult != null && !processResult.startsWith("SUCCESS")) { // 非致命错误(如金额未匹配、哈希已使用等),仍返回 200,避免 Tokenview 重复推送 result.put("msg", processResult); } } catch (Exception e) { log.error("Webhook 处理异常", e); result.put("code", 500); result.put("msg", "处理异常: " + e.getMessage()); } return result; } /** * 读取请求体原始字符串 */ private String readRequestBody(HttpServletRequest request) { try { StringBuilder sb = new StringBuilder(); BufferedReader reader = request.getReader(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } return sb.toString(); } catch (Exception e) { log.error("读取请求体失败", e); return null; } } /** * 验证 HMAC-SHA256 签名 * * @param payload 原始请求体 * @param signature 请求头中的签名值 * @return true-验证通过, false-验证失败 */ private boolean verifySignature(String payload, String signature) { try { Mac sha256Hmac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec( WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256" ); sha256Hmac.init(secretKey); byte[] signedBytes = sha256Hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); String expectedSignature = bytesToHex(signedBytes); // 同时尝试 Base64 编码的签名(兼容不同 Tokenview 版本) String expectedBase64 = Base64.getEncoder().encodeToString(signedBytes); boolean hexMatch = expectedSignature.equalsIgnoreCase(signature); boolean base64Match = expectedBase64.equals(signature); if (!hexMatch && !base64Match) { log.warn("签名不匹配: expected_hex={}, expected_base64={}, actual={}", expectedSignature, expectedBase64, signature); return false; } return true; } catch (Exception e) { log.error("签名验证计算异常", e); return false; } } /** * 字节数组转十六进制字符串 */ private String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } }