package com.bpaygate.sdk; import com.google.gson.Gson; import com.google.gson.JsonObject; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Map; /** * BPayGate Java SDK(Java 8+,依赖 Gson)。 * *
* BPayGateClient gw = new BPayGateClient("https://pay.example.com", "你的API_AUTH_KEY");
* JsonObject order = gw.createOrder("SHOP-1001", "USDT", "10",
* "https://shop.example.com/bpg/notify", "https://shop.example.com/orders/1001", 0);
* String payUrl = order.get("pay_url").getAsString(); // 跳转用户
*
* // 回调处理:
* JsonObject payload = BPayGateClient.verifyCallback(tsHeader, nonceHeader, sigHeader, rawBody, SECRET, 300000);
* if ("paid".equals(payload.get("event").getAsString())) { ... } // 按 merchant_order_id 幂等发货
*
*/
public class BPayGateClient {
public static class BPayGateException extends RuntimeException {
public final String code;
public final int httpStatus;
public BPayGateException(String code, String message, int httpStatus) {
super(code + ": " + message);
this.code = code;
this.httpStatus = httpStatus;
}
}
private static final Gson GSON = new Gson();
private static final SecureRandom RND = new SecureRandom();
private final String baseUrl;
private final String secret;
private final int timeoutMs;
public BPayGateClient(String baseUrl, String secret) {
this(baseUrl, secret, 10000);
}
public BPayGateClient(String baseUrl, String secret, int timeoutMs) {
String b = baseUrl;
while (b.endsWith("/")) b = b.substring(0, b.length() - 1);
this.baseUrl = b;
this.secret = secret;
this.timeoutMs = timeoutMs;
}
/* ---------- API ---------- */
public JsonObject createOrder(String merchantOrderId, String currency, String amount,
String callbackUrl, String returnUrl, long timeoutSec) {
JsonObject p = new JsonObject();
p.addProperty("merchant_order_id", merchantOrderId);
p.addProperty("currency", currency);
p.addProperty("amount", amount);
if (callbackUrl != null && !callbackUrl.isEmpty()) p.addProperty("callback_url", callbackUrl);
if (returnUrl != null && !returnUrl.isEmpty()) p.addProperty("return_url", returnUrl);
if (timeoutSec > 0) p.addProperty("timeout", timeoutSec);
return request("POST", "/api/v1/orders", GSON.toJson(p));
}
public JsonObject getOrder(String orderId) {
return request("GET", "/api/v1/orders/" + orderId, "");
}
public JsonObject getOrderByMerchantId(String merchantOrderId) {
return request("GET", "/api/v1/orders/by-merchant/" + merchantOrderId, "");
}
public JsonObject closeOrder(String orderId) {
return request("POST", "/api/v1/orders/" + orderId + "/close", "{}");
}
/* ---------- 签名 ---------- */
public static String signRequest(String secret, String ts, String nonce, String method, String path, String body) {
String sts = ts + "\n" + nonce + "\n" + method + "\n" + path + "\n" + sha256Hex(body.getBytes(StandardCharsets.UTF_8));
return hmacHex(secret, sts);
}
public static String signCallback(String secret, String ts, String nonce, String body) {
String sts = ts + "\n" + nonce + "\n" + sha256Hex(body.getBytes(StandardCharsets.UTF_8));
return hmacHex(secret, sts);
}
/**
* 校验网关回调,通过返回回调 JSON,失败抛 BPayGateException。
* 注意:回调可能重复投递,商户须按 merchant_order_id 幂等处理。
*/
public static JsonObject verifyCallback(String ts, String nonce, String sig, String body,
String secret, long maxSkewMs) {
if (ts == null || nonce == null || sig == null || ts.isEmpty() || nonce.isEmpty() || sig.isEmpty()) {
throw new BPayGateException("ERR_AUTH", "缺少签名头", 0);
}
long tsv;
try {
tsv = Long.parseLong(ts);
} catch (NumberFormatException e) {
throw new BPayGateException("ERR_AUTH", "时间戳非法", 0);
}
if (Math.abs(System.currentTimeMillis() - tsv) > maxSkewMs) {
throw new BPayGateException("ERR_AUTH", "时间戳偏差过大", 0);
}
if (!constantTimeEquals(signCallback(secret, ts, nonce, body), sig)) {
throw new BPayGateException("ERR_AUTH", "签名错误", 0);
}
return GSON.fromJson(body, JsonObject.class);
}
/** 便捷重载:直接传 header map(键不区分大小写)。 */
public static JsonObject verifyCallback(Map