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 = "your-webhook-secret-key-here";
|
|
/**
|
* 接收 Tokenview 地址监控推送
|
* <p>
|
* Tokenview 在监测到地址有交易时,会 POST JSON 到此端点。
|
* 需要在 Tokenview 后台配置此 URL 和 Secret Key。
|
* <p>
|
* 配置步骤:
|
* 1. 登录 Tokenview 开发者后台
|
* 2. 进入「地址监控」模块
|
* 3. 添加要监控的 TRC20 地址
|
* 4. 配置 Webhook URL: https://your-domain/api/tokenview/webhook
|
* 5. 设置 Secret Key(与此处 WEBHOOK_SECRET 一致)
|
*/
|
@PostMapping("/webhook")
|
public Map<String, Object> receiveWebhook(HttpServletRequest request) {
|
Map<String, Object> 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<String, Object> payload = JSON.parseObject(
|
rawBody,
|
new TypeReference<Map<String, Object>>() {}
|
);
|
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();
|
}
|
}
|