Skip to content

Java 接入示例

本文档提供Java语言接入支付API的完整示例代码。

环境要求

  • Java >= 8
  • Jackson 2.15+ (JSON处理) 或 使用内置 HttpClient / OkHttp

签名规则说明

  1. 提取非空参数(sign 除外)
  2. 按 ASCII 字典序排序参数名(使用 TreeMap 即可)
  3. key=value&key=value 拼接
  4. 末尾拼接 &key=secretKey
  5. 其中 secretKey = MD5(alliesCode + ":" + alliesSecret)
  6. 对最终字符串做 MD5 并转大写
sign = MD5("k1=v1&k2=v2&key=secretKey").toUpperCase()

Maven 依赖

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

    <!-- HTTP请求(Java 11+ 内置 HttpClient,无需依赖) -->

    <!-- 日志(可选) -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.9</version>
    </dependency>

    <!-- Spring Boot(如使用 NotifyController) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.2.0</version>
    </dependency>
</dependencies>

快速开始

1. 配置参数

编辑示例文件中的配置区域:

java
// API地址
private static final String API_URL = "https://pay.example.com/api/alliesPay";

// 商户编号
private static final String ALLIES_CODE = "IG20250911001";

// 商户密钥
private static final String ALLIES_SECRET = "your-secret-key";

2. 编译运行

bash
javac -cp ".:jackson-databind-2.15.2.jar" SignUtil.java PaymentClient.java Demo.java
java -cp ".:jackson-databind-2.15.2.jar" Demo

3. 使用 Maven/Gradle

bash
# Maven
mvn compile exec:java -Dexec.mainClass="com.payment.api.Demo"

# Gradle
gradle run

代码结构

1. 签名工具类(SignUtil.java)

SignUtil - MD5 签名生成与校验

java
package com.payment.api;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.TreeMap;

/**
 * 签名工具类 - MD5签名
 */
public final class SignUtil {

    private SignUtil() {}

    public static String calcSecretKey(String alliesCode, String alliesSecret) {
        return md5Hex(alliesCode + ":" + alliesSecret).toUpperCase();
    }

    public static String buildSign(Map<String, ?> params, String secretKey) {
        TreeMap<String, String> sorted = new TreeMap<>();
        for (Map.Entry<String, ?> entry : params.entrySet()) {
            String key = entry.getKey();
            if ("sign".equalsIgnoreCase(key)) continue;
            Object value = entry.getValue();
            if (value == null) continue;
            String str = String.valueOf(value);
            if (str.isEmpty()) continue;
            sorted.put(key, str);
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sorted.entrySet()) {
            if (sb.length() > 0) sb.append('&');
            sb.append(entry.getKey()).append('=').append(entry.getValue());
        }
        sb.append("&key=").append(secretKey);
        return md5Hex(sb.toString()).toUpperCase();
    }

    public static boolean verifySign(Map<String, ?> response, String secretKey) {
        Object signObj = response.get("sign");
        if (signObj == null || String.valueOf(signObj).isEmpty()) {
            return false;
        }
        String calculated = buildSign(response, secretKey);
        return String.valueOf(signObj).equalsIgnoreCase(calculated);
    }

    private static String md5Hex(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder(digest.length * 2);
            for (byte b : digest) {
                sb.append(String.format("%02x", b & 0xff));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("MD5 not available", e);
        }
    }
}

2. API 客户端(PaymentClient.java)

使用 Java 11+ 内置 HttpClient 发送明文 JSON 请求,自动附加签名并验证响应签名。

java
package com.payment.api;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class PaymentClient {

    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();

    private final String apiUrl;
    private final String alliesCode;
    private final String secretKey;

    public PaymentClient(String apiUrl, String alliesCode, String alliesSecret) {
        this.apiUrl = apiUrl.endsWith("/") ? apiUrl.substring(0, apiUrl.length() - 1) : apiUrl;
        this.alliesCode = alliesCode;
        this.secretKey = SignUtil.calcSecretKey(alliesCode, alliesSecret);
    }

    private Map<String, Object> post(String path, Map<String, Object> payload) throws IOException, InterruptedException {
        payload = new HashMap<>(payload);
        payload.put("sign", SignUtil.buildSign(payload, secretKey));
        String body = MAPPER.writeValueAsString(payload);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(apiUrl + "/" + path))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .timeout(Duration.ofSeconds(30))
                .build();

        HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
        }

        Map<String, Object> parsed = MAPPER.readValue(response.body(),
                new TypeReference<Map<String, Object>>() {});

        Object data = parsed.get("data");
        if (data instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> dataMap = (Map<String, Object>) data;
            if (!SignUtil.verifySign(dataMap, secretKey)) {
                throw new IOException("响应签名验证失败");
            }
        } else if (!SignUtil.verifySign(parsed, secretKey)) {
            throw new IOException("响应签名验证失败");
        }

        return parsed;
    }

    public Map<String, Object> createOrder(Map<String, Object> params) throws IOException, InterruptedException {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("userCode", alliesCode);
        payload.putAll(params);
        return post("order", payload);
    }

    public Map<String, Object> queryOrder(String outTradeNo) throws IOException, InterruptedException {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("userCode", alliesCode);
        payload.put("outTradeNo", outTradeNo);
        return post("query", payload);
    }

    public Map<String, Object> queryBalance(int tradeType) throws IOException, InterruptedException {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("userCode", alliesCode);
        payload.put("tradeType", tradeType);
        return post("queryBalance", payload);
    }
}

3. 使用示例(Demo.java)

java
package com.payment.api;

import java.util.LinkedHashMap;
import java.util.Map;

public class Demo {

    private static final String API_URL = "https://pay.example.com/api/alliesPay";
    private static final String ALLIES_CODE = "IG20250911001";
    private static final String ALLIES_SECRET = "your-secret-key";

    public static void main(String[] args) throws Exception {
        PaymentClient client = new PaymentClient(API_URL, ALLIES_CODE, ALLIES_SECRET);

        Map<String, Object> order = new LinkedHashMap<>();
        order.put("orderNo", "TEST" + System.currentTimeMillis());
        order.put("amount", "100.00");
        order.put("paymentMethod", "GCASH");
        order.put("productId", "0");
        order.put("returnUrl", "https://example.com/return");
        order.put("notifyUrl", "https://example.com/notify");
        order.put("clientIp", "127.0.0.1");

        Map<String, Object> result = client.createOrder(order);
        System.out.println("响应码: " + result.get("code"));
        System.out.println("响应信息: " + result.get("msg"));
        if ("0000".equals(String.valueOf(result.get("code")))) {
            @SuppressWarnings("unchecked")
            Map<String, Object> data = (Map<String, Object>) result.get("data");
            System.out.println("支付链接: " + data.get("payUrl"));
            System.out.println("平台订单号: " + data.get("outTradeNo"));
        }

        Map<String, Object> queryResult = client.queryOrder("PO20250213001");
        System.out.println("查询响应: " + queryResult);
    }
}

4. 使用 OkHttp 发送请求(可选)

xml
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>
java
package com.payment.api;

import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class OkHttpPaymentClient {

    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
    private static final OkHttpClient HTTP = new OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build();

    private final String apiUrl;
    private final String alliesCode;
    private final String secretKey;

    public OkHttpPaymentClient(String apiUrl, String alliesCode, String alliesSecret) {
        this.apiUrl = apiUrl;
        this.alliesCode = alliesCode;
        this.secretKey = SignUtil.calcSecretKey(alliesCode, alliesSecret);
    }

    public Map<String, Object> createOrder(Map<String, Object> params) throws IOException {
        Map<String, Object> payload = new HashMap<>();
        payload.put("userCode", alliesCode);
        payload.putAll(params);
        return post("order", payload);
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> post(String path, Map<String, Object> payload) throws IOException {
        payload = new HashMap<>(payload);
        payload.put("sign", SignUtil.buildSign(payload, secretKey));
        String body = MAPPER.writeValueAsString(payload);

        Request request = new Request.Builder()
                .url(apiUrl + "/" + path)
                .post(RequestBody.create(body, JSON))
                .build();

        try (Response response = HTTP.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("HTTP " + response.code());
            String responseBody = response.body().string();
            return MAPPER.readValue(responseBody, Map.class);
        }
    }
}

Spring Boot 集成

1. 配置类

java
package com.payment.api.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "payment")
public class PaymentConfig {
    private String apiUrl;
    private String alliesCode;
    private String alliesSecret;

    public String getApiUrl()       { return apiUrl; }
    public void setApiUrl(String v) { this.apiUrl = v; }
    public String getAlliesCode()   { return alliesCode; }
    public void setAlliesCode(String v) { this.alliesCode = v; }
    public String getAlliesSecret() { return alliesSecret; }
    public void setAlliesSecret(String v) { this.alliesSecret = v; }
}

2. 服务类

java
package com.payment.api.service;

import com.payment.api.PaymentClient;
import com.payment.api.config.PaymentConfig;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Map;

@Service
public class PaymentService {

    private final PaymentClient client;

    public PaymentService(PaymentConfig config) {
        this.client = new PaymentClient(
                config.getApiUrl(),
                config.getAlliesCode(),
                config.getAlliesSecret()
        );
    }

    public Map<String, Object> createOrder(Map<String, Object> params) throws IOException, InterruptedException {
        return client.createOrder(params);
    }
}

3. 控制器

java
package com.payment.api.controller;

import com.payment.api.service.PaymentService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/payment")
public class PaymentController {

    private final PaymentService paymentService;

    public PaymentController(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @PostMapping("/create")
    public Map<String, Object> createOrder(@RequestBody Map<String, Object> request) throws Exception {
        return paymentService.createOrder(request);
    }
}

application.yml 配置

yaml
payment:
  api-url: https://pay.example.com/api/alliesPay
  allies-code: IG20250911001
  allies-secret: your-secret-key

回调处理

java
package com.payment.api.controller;

import com.payment.api.SignUtil;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/payment")
public class NotifyController {

    private static final String ALLIES_CODE = "IG20250911001";
    private static final String ALLIES_SECRET = "your-secret-key";

    @PostMapping("/notify")
    public Map<String, Object> notify(@RequestBody Map<String, Object> body) {
        String secretKey = SignUtil.calcSecretKey(ALLIES_CODE, ALLIES_SECRET);
        if (!SignUtil.verifySign(body, secretKey)) {
            return Map.of("code", "FAIL", "msg", "Invalid Sign");
        }
        return Map.of("code", "SUCCESS", "msg", "OK");
    }

    @PostMapping("/payout-notify")
    public Map<String, Object> payoutNotify(@RequestBody Map<String, Object> body) {
        return notify(body);
    }
}

故障排除

1. 签名计算不一致

问题: 服务端返回 1012 签名验证失败

排查:

  1. 确认 secretKey = MD5(alliesCode + ":" + alliesSecret) 是正确的(注意冒号分隔)。
  2. 确保所有空字符串null 参数都被过滤掉。
  3. 确认参数是按 ASCII 升序排列的(使用 TreeMap)。
  4. 打印待签名的原始字符串,与服务端日志对比。

2. SSL证书错误

生产环境: 导入正确的 CA 证书

bash
keytool -import -alias cacerts -file cacert.pem \
    -keystore $JAVA_HOME/lib/security/cacerts

3. JSON 序列化问题

确保使用 UTF-8 编码:

java
byte[] data = jsonString.getBytes(StandardCharsets.UTF_8);

4. 依赖冲突

Spring Boot 可能自带不同的 Jackson 版本,建议统一管理。

测试

单元测试

java
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;

public class SignUtilTest {

    @Test
    public void testBuildSign() {
        String secretKey = SignUtil.calcSecretKey("IG20250911001", "your-secret-key");
        Map<String, Object> params = Map.of(
                "userCode", "IG20250911001",
                "amount", "100.00",
                "orderNo", "TEST001"
        );
        String sign = SignUtil.buildSign(params, secretKey);
        assertNotNull(sign);
        assertTrue(SignUtil.verifySign(Map.of(
                "userCode", "IG20250911001",
                "amount", "100.00",
                "orderNo", "TEST001",
                "sign", sign
        ), secretKey));
    }
}

获取帮助

  • 文档: 参考当前目录的 README 与示例源码
  • 问题反馈: 提交 Issue 到 GitHub
  • 技术支持: 联系客服