Skip to content

Java — 託管支付

請替換示例憑證

運行前請替換所有佔位符:

  • "your-api-key" → 商戶後台的實際 API Key
  • "your-api-secret" → 商戶後台的實際 API Secret
  • "1000" → 您的實際商戶號

依賴(Maven)

xml
<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
</dependencies>

<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
</properties>

簽名工具類

所有託管支付請求需要 X-Api-KeyX-TimestampX-Signature

  • POST 請求: Base64(HMAC-SHA256(timestamp\nPOST\npath\nBase64(SHA256(body)), apiSecret))
  • GET 請求(無查詢字符串): Base64(HMAC-SHA256(timestamp\nGET\npath, apiSecret))
  • GET 請求(有查詢字符串): Base64(HMAC-SHA256(timestamp\nGET\npath\nBase64(SHA256(queryString)), apiSecret))
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

public class SignatureHelper {

    private final String apiSecret;

    public SignatureHelper(String apiSecret) {
        this.apiSecret = apiSecret;
    }

    /** 為 POST 請求生成簽名。 */
    public String signPost(String timestamp, String path, String body) throws Exception {
        byte[] bodyHash = MessageDigest.getInstance("SHA-256")
            .digest(body.getBytes(StandardCharsets.UTF_8));
        String bodyHashB64 = Base64.getEncoder().encodeToString(bodyHash);
        return hmacBase64(timestamp + "\n" + "POST" + "\n" + path + "\n" + bodyHashB64);
    }

    /** 為無查詢字符串的 GET 請求生成簽名。 */
    public String signGet(String timestamp, String path) throws Exception {
        return hmacBase64(timestamp + "\n" + "GET" + "\n" + path);
    }

    /** 為帶查詢字符串的 GET 請求生成簽名(如退款查詢)。 */
    public String signGetWithQuery(String timestamp, String path, String queryString) throws Exception {
        byte[] qsHash = MessageDigest.getInstance("SHA-256")
            .digest(queryString.getBytes(StandardCharsets.UTF_8));
        String qsHashB64 = Base64.getEncoder().encodeToString(qsHash);
        return hmacBase64(timestamp + "\n" + "GET" + "\n" + path + "\n" + qsHashB64);
    }

    private String hmacBase64(String data) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
    }

    /** 驗證 Webhook 簽名。 */
    public boolean verifyWebhook(String timestamp, String signature, String rawBody) throws Exception {
        String expected = hmacBase64(timestamp + "\n" + rawBody);
        return expected.equals(signature);
    }
}

創建支付訂單

java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class CreateOrderExample {

    private static final String BASE_URL    = "https://api.paystablecoin.global";
    private static final String MERCHANT_ID = "1000";
    private static final String API_KEY     = "your-api-key";
    private static final String API_SECRET  = "your-api-secret";

    public static void main(String[] args) throws Exception {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String path      = "/api/v1/merchants/" + MERCHANT_ID + "/orders";

        // 請求體必須是緊湊格式(無多餘空白)
        String body = String.format(
            "{\"merchantOrderId\":\"ORDER_%s\",\"orderAmount\":{\"value\":\"99.99\",\"currency\":\"USD\"}," +
            "\"expiresAt\":\"2026-12-31T23:59:59Z\"," +
            "\"returnUrl\":\"https://yoursite.com/payment/success\"," +
            "\"callbackUrl\":\"https://yoursite.com/webhook/payment\"}",
            timestamp
        );

        SignatureHelper signer = new SignatureHelper(API_SECRET);
        String signature = signer.signPost(timestamp, path, body);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE_URL + path))
            .header("X-Api-Key",    API_KEY)
            .header("X-Timestamp",  timestamp)
            .header("X-Signature",  signature)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
        // 成功時:{"code":"00000","data":{"status":"INIT","checkoutUrl":"https://..."}}
        // 將客戶重定向至 checkoutUrl
    }
}

查詢訂單

java
public class QueryOrderExample {

    private static final String BASE_URL    = "https://api.paystablecoin.global";
    private static final String MERCHANT_ID = "1000";
    private static final String API_KEY     = "your-api-key";
    private static final String API_SECRET  = "your-api-secret";

    public static void main(String[] args) throws Exception {
        String merchantOrderId = "ORDER_1738112345000";  // 替換為實際訂單號
        String timestamp = String.valueOf(System.currentTimeMillis());
        String path = "/api/v1/merchants/" + MERCHANT_ID + "/orders/" + merchantOrderId;

        SignatureHelper signer = new SignatureHelper(API_SECRET);
        String signature = signer.signGet(timestamp, path);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE_URL + path))
            .header("X-Api-Key",   API_KEY)
            .header("X-Timestamp", timestamp)
            .header("X-Signature", signature)
            .GET()
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
        // 查看 data.status:INIT / PROCESSING / SUCCEEDED / FAILED / CLOSED
    }
}

驗證 Webhook 簽名

java
import com.sun.net.httpserver.HttpExchange;

public class WebhookHandler {

    private static final String API_SECRET = "your-api-secret";

    public void handle(HttpExchange exchange) throws Exception {
        String timestamp = exchange.getRequestHeaders().getFirst("X-Timestamp");
        String signature = exchange.getRequestHeaders().getFirst("X-Signature");
        String rawBody   = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);

        // 第一步:處理業務邏輯前先驗證簽名
        SignatureHelper signer = new SignatureHelper(API_SECRET);
        if (!signer.verifyWebhook(timestamp, signature, rawBody)) {
            exchange.sendResponseHeaders(401, 0);
            exchange.close();
            return;
        }

        // 第二步:驗證時間戳在 5 分鐘內
        long webhookTime = Long.parseLong(timestamp);
        if (Math.abs(System.currentTimeMillis() - webhookTime) > 300_000) {
            exchange.sendResponseHeaders(401, 0);
            exchange.close();
            return;
        }

        // 第三步:立即返回 HTTP 200,業務邏輯異步處理
        exchange.sendResponseHeaders(200, 0);
        exchange.close();

        // 第四步:異步解析並處理事件
        // 按 status 分發:PROCESSING / SUCCEEDED
        // 使用 acquiringOrderId 或 merchantOrderId 進行去重
    }
}

創建退款

java
public class CreateRefundExample {

    private static final String BASE_URL    = "https://api.paystablecoin.global";
    private static final String MERCHANT_ID = "1000";
    private static final String API_KEY     = "your-api-key";
    private static final String API_SECRET  = "your-api-secret";

    public static void main(String[] args) throws Exception {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String path      = "/api/v1/merchants/" + MERCHANT_ID + "/refunds";

        String body = "{\"merchantRefundOrderId\":\"REFUND_" + timestamp + "\"," +
                      "\"acquiringOrderId\":\"ORDER_1738112345000\"," +
                      "\"refundReason\":\"Customer request full refund\"}";

        SignatureHelper signer = new SignatureHelper(API_SECRET);
        String signature = signer.signPost(timestamp, path, body);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE_URL + path))
            .header("X-Api-Key",    API_KEY)
            .header("X-Timestamp",  timestamp)
            .header("X-Signature",  signature)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
        // 成功時:{"code":"00000","data":{"refundOrderId":"REF_...","status":"PROCESSING",...}}
    }
}

查詢退款

java
public class QueryRefundExample {

    private static final String BASE_URL    = "https://api.paystablecoin.global";
    private static final String MERCHANT_ID = "1000";
    private static final String API_KEY     = "your-api-key";
    private static final String API_SECRET  = "your-api-secret";

    public static void main(String[] args) throws Exception {
        String merchantRefundOrderId = "REFUND_1738112345000";  // 替換為實際退款單號
        String timestamp    = String.valueOf(System.currentTimeMillis());
        String path         = "/api/v1/merchants/" + MERCHANT_ID + "/refunds";
        String queryString  = "merchantRefundOrderId=" + merchantRefundOrderId;

        SignatureHelper signer = new SignatureHelper(API_SECRET);
        String signature = signer.signGetWithQuery(timestamp, path, queryString);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE_URL + path + "?" + queryString))
            .header("X-Api-Key",   API_KEY)
            .header("X-Timestamp", timestamp)
            .header("X-Signature", signature)
            .GET()
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
        // 查看 data.status:PROCESSING / SUCCEEDED / FAILED / CLOSED
    }
}

相關文檔

Released under the MIT License.