Java 接入示例
本文档提供Java语言接入支付API的完整示例代码。
环境要求
- Java >= 8
- Jackson 2.15+ (JSON处理) 或 使用内置 HttpClient / OkHttp
签名规则说明
- 提取非空参数(sign 除外)
- 按 ASCII 字典序排序参数名(使用 TreeMap 即可)
- 按
key=value&key=value拼接 - 末尾拼接
&key=secretKey - 其中
secretKey = MD5(alliesCode + ":" + alliesSecret) - 对最终字符串做 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" Demo3. 使用 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 签名验证失败
排查:
- 确认
secretKey = MD5(alliesCode + ":" + alliesSecret)是正确的(注意冒号分隔)。 - 确保所有空字符串和
null参数都被过滤掉。 - 确认参数是按 ASCII 升序排列的(使用
TreeMap)。 - 打印待签名的原始字符串,与服务端日志对比。
2. SSL证书错误
生产环境: 导入正确的 CA 证书
bash
keytool -import -alias cacerts -file cacert.pem \
-keystore $JAVA_HOME/lib/security/cacerts3. 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
- 技术支持: 联系客服
