目标:
-
学习如何使用Java与通联支付API进行交互
-
实现一个简单的支付下单和查询订单状态的示例
所需材料:
- 通联支付API文档
官方文档https://aipboss.allinpay.com/know/devhelp/main.php?pid=38#mid=313
-
通联支付加签代码SybUtil
package com.allinpay.common;
import net.sf.json.JSONObject;
import org.apache.tomcat.util.codec.binary.Base64;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
@SuppressWarnings("all")
public class SybUtil {
/**
* js转化为实体
*
* @param <T>
* @param jsonstr
* @param cls
* @return
*/
public static <T> T json2Obj(String jsonstr, Class<T> cls) {
JSONObject jo = JSONObject.fromObject(jsonstr);
T obj = (T) JSONObject.toBean(jo, cls);
return obj;
}
/**
* md5
*
* @param b
* @return
*/
public static String md5(byte[] b) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.reset();
md.update(b);
byte[] hash = md.digest();
StringBuffer outStrBuf = new StringBuffer(32);
for (int i = 0; i < hash.length; i++) {
int v = hash[i] & 0xFF;
if (v < 16) {
outStrBuf.append('0');
}
outStrBuf.append(Integer.toString(v, 16).toLowerCase());
}
return outStrBuf.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return new String(b);
}
}
/**
* 判断字符串是否为空
*
* @param s
* @return
*/
public static boolean isEmpty(String s) {
if (s == null || "".equals(s.trim()))
return true;
return false;
}
/**
* 生成随机码
*
* @param n
* @return
*/
public static String getValidatecode(int n) {
Random random = new Random();
String sRand = "";
n = n == 0 ? 4 : n;// default 4
for (int i = 0; i < n; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
}
return sRand;
}
public static boolean validSign(TreeMap<String, String> param,
String appkey, String signType) throws Exception {
if (param != null && !param.isEmpty()) {
if (!param.containsKey("sign"))
return false;
String sign = param.remove("sign");
if ("MD5".equals(signType)) {// 如果是md5则需要把md5的key加入到排序
param.put("key", appkey);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : param.entrySet()) {
if (entry.getValue() != null && entry.getValue().length() > 0) {
sb.append(entry.getKey()).append("=")
.append(entry.getValue()).append("&");
}
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
if ("MD5".equals(signType)) {
return sign.toLowerCase().equals(
md5(sb.toString().getBytes("UTF-8")).toLowerCase());
} else {
return rsaVerifyPublickey(sb.toString(), sign, appkey, "UTF-8");
}
}
return false;
}
public static boolean rsaVerifyPublickey(String content, String sign,
String publicKey, String charset) throws Exception {
try {
PublicKey pubKey = getPublicKeyFromX509("RSA",
Base64.decodeBase64(publicKey.getBytes()));
return rsaVerifyPublickey(content, sign, pubKey, charset);
} catch (Exception e) {
e.printStackTrace();
throw new Exception("RSAcontent = " + content + ",sign=" + sign
+ ",charset = " + charset, e);
}
}
public static boolean rsaVerifyPublickey(String content, String sign,
PublicKey pubKey, String charset) throws Exception {
try {
java.security.Signature signature = java.security.Signature
.getInstance("SHA1WithRSA");
signature.initVerify(pubKey);
if (charset == null || "".equals(charset)) {
signature.update(content.getBytes());
} else {
signature.update(content.getBytes(charset));
}
return signature.verify(Base64.decodeBase64(sign.getBytes()));
} catch (Exception e) {
throw e;
}
}
public static String unionSign(TreeMap<String, String> params,String appkey,
String signType) throws Exception {
// TODO Auto-generated method stub
params.remove("sign");
if ("MD5".equals(signType)) {// 如果是md5则需要把md5的key加入到排序
params.put("key", appkey);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (entry.getValue() != null && entry.getValue().length() > 0) {
sb.append(entry.getKey()).append("=").append(entry.getValue())
.append("&");
}
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
String sign = "";
if ("MD5".equals(signType)) {
System.out.println(sb.toString());
sign = md5(sb.toString().getBytes("UTF-8"));// 记得是md5编码的加签
params.remove("key");
} else {
sign = rsaSign(sb.toString(), appkey, "UTF-8");
}
return sign;
}
public static String rsaSign(String content, String privateKey,
String charset) throws Exception {
PrivateKey priKey = getPrivateKeyFromPKCS8("RSA",
Base64.decodeBase64(privateKey.getBytes()));
return rsaSign(content, priKey, charset);
}
public static String rsaSign(String content, byte[] privateKey,
String charset) throws Exception {
PrivateKey priKey = getPrivateKeyFromPKCS8("RSA", privateKey);
return rsaSign(content, priKey, charset);
}
public static String rsaSign(String content, PrivateKey priKey,
String charset) throws Exception {
java.security.Signature signature = java.security.Signature
.getInstance("SHA1WithRSA");
signature.initSign(priKey);
if (charset == null || "".equals(charset)) {
signature.update(content.getBytes());
} else {
signature.update(content.getBytes(charset));
}
byte[] signed = signature.sign();
return new String(Base64.encodeBase64(signed));
}
public static PrivateKey getPrivateKeyFromPKCS8(String algorithm,
byte[] encodedKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
}
public static PublicKey getPublicKeyFromX509(String algorithm,
byte[] encodedKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
}
}
-
IDE(如IntelliJ IDEA或Eclipse)
-
JDK 8 或更高版本
- 需要的maven
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
<!-- SybUtil文件需要用到 -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 辅助类
- 通联支付需要的请求数据格式(Allinpay.java)
package com.allinpay.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Allinpay {
private String cusid;
private String appid;
private int version;
private BigDecimal trxamt;
/*
当指定为F02时,交易仅限分期交易。
分期交易金额必须大于500元。
*/
// ?
private String paytype;
/*
3 花呗分期3期
6 花呗分期6期
12 花呗分期12期
3-cc 支付宝信用卡分期3期
6-cc 支付宝信用卡分期6期
12-cc 支付宝信用卡分期12期
暂只支持支付宝花呗分期,支付宝信用卡分期,仅支持A01/A02
*/
// 可空参数
private String fqnum;
//订单号,商户唯一订单号
private String reqsn;
/*
商户网站使用的编码格式,支持
UTF-8、GBK
跟商户网站的编码一致
*/
//
private String charset;
/*
必须为https协议地址,且不允许带参数
页面跳转同步通知页面路径
*/
private String returl;
// 异步通知地址
// ?
private String notify_url;
// 商品描述,长度最大100
private String body;
//? 通知会原样带上, 订单备注信息
private String remark;
// 随机字符串,自己生成,最大32位
private String randomstr;
//订单有效时间,以分为单位,默认为15
private String validtime;
// ? 支付限制(no_credit--指定不能使用信用卡支付)
private String limit_pay;
// 签名类型,目前支持RSA2和RSA
private String signtype;
// 签名 32位
private String sign;
// 关闭订单的时候需要
private String oldreqsn;
}
2. 由于本案列采用了多商家,所以暂时把配置也建成了一个类(PayConfig.java)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class PayConfig {
private String cusid;
private String appid;
private String privateKey;
}
3.本案列设置了多方支付方式,所以还有一个支付载荷(PayLoad.java)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PayLoad {
private String orderId;
private BigDecimal amount;
private String payConfigKey;
private String remark;
private String title;
private int payType;
private String returnUrl;
private String notifyUrl;
}
-
H5支付下单代码
//这个是PayService的Impl实现层
private final String order = "https://syb.allinpay.com/apiweb/h5unionpay/unionorder";
private final String rsaPrivateKey = "xxx";
@SneakyThrows
@Override
public String h5Pay(PayLoad payLoad) {
// String payConfigKey = payLoad.getPayConfigKey();
PayConfig payConfig = new PayConfig()
.setAppid("xxx")
.setCusid("xxx")
.setPrivateKey(rsaPrivateKey);
Allinpay allinpay = new Allinpay()
.setSigntype("RSA")
.setTrxamt(new BigDecimal(2))
.setReqsn(payLoad.getOrderId())
.setRandomstr(SybUtil.getValidatecode(8))
.setBody(payLoad.getTitle())
.setRemark(payLoad.getRemark())
.setCharset("UTF-8")
.setAppid(payConfig.getAppid())
.setCusid(payConfig.getCusid())
.setReturl(payLoad.getReturnUrl());
allinpay.setSign(URLEncoder.encode(SybUtil.unionSign(objectToTreeMap(allinpay), payConfig.getPrivateKey() , "RSA"), StandardCharsets.UTF_8));
return order + "?" + treeMapToUrlParams(objectToTreeMap(allinpay));
}
//Controller代码
@SneakyThrows
@GetMapping("pay")
public String pay(HttpServletResponse response) {
String paymentUrl = (String) payService.h5Pay(new PayLoad()
.setPayType(2)
.setPayConfigKey("")
.setReturnUrl("https://blog.csdn.net")
.setOrderId(IdUtil.getSnowflakeNextIdStr())
.setAmount(new BigDecimal(2))
.setRemark("测试支付备注")
.setTitle("测试H5支付")
);
String htmlResponse = "<!DOCTYPE html><html><head></head><body>"
+ "<script>window.location.href='" + paymentUrl + "'</script>"
+ "</body></html>";
// 设置响应内容类型为HTML
response.setContentType("text/html;charset=UTF-8");
return htmlResponse;
}
-
关闭订单
// 关闭接口
private final String close = "https://vsp.allinpay.com/apiweb/tranx/close";
@SneakyThrows
@Override
public String closeOrder(PayLoad payLoad) {
PayConfig payConfig = new PayConfig()
.setAppid("xxx")
.setCusid("xxxx")
.setPrivateKey(rsaPrivateKey);
Allinpay allinpay = new Allinpay()
.setCusid(payConfig.getCusid())
.setAppid(payConfig.getAppid())
.setRandomstr(SybUtil.getValidatecode(8))
.setVersion(11)
.setOldreqsn(payLoad.getOrderId())
.setSigntype("RSA");
allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
return HttpUtil.post(close, BeanUtil.beanToMap(allinpay));
}
/**
* 关闭订单
* @param outTradeNo 下单的订单号,也就是你支付下单类Allinpay的reqsn
* @return {@link String}
***/
@SneakyThrows
@GetMapping("closeOrder/{outTradeNo}")
public Map<String,String> closeOrder(@PathVariable Long outTradeNo) {
return handleResult(payService.closeOrder(new PayLoad().setOrderId(outTradeNo.toString())));
}
-
查询订单
// 查询接口
private final String query = "https://vsp.allinpay.com/apiweb/tranx/query";
@SneakyThrows
@Override
public String query(PayLoad payLoad) {
PayConfig payConfig = new PayConfig()
.setAppid("xxx")
.setCusid("xxxx")
.setPrivateKey(rsaPrivateKey);
Allinpay allinpay = new Allinpay()
.setCusid(payConfig.getCusid())
.setAppid(payConfig.getAppid())
.setRandomstr(SybUtil.getValidatecode(8))
.setVersion(11)
.setReqsn(payLoad.getOrderId())
.setSigntype("RSA");
allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
return HttpUtil.post(query, BeanUtil.beanToMap(allinpay));
}
/**
* 查询订单
* @param outTradeNo 下单的订单号
* @return {@link String}
***/
@SneakyThrows
@GetMapping("query/{outTradeNo}")
public Map<String, String> query(@PathVariable Long outTradeNo) {
return handleResult(payService.query(new PayLoad().setOrderId(outTradeNo.toString())));
}
-
退款接口,这个是只能退当天的交易 (全额退实时返回退款结果)
// 取消当天交易退款接口
private final String cancelDayUrl = "https://vsp.allinpay.com/apiweb/tranx/cancel";
@SneakyThrows
@Override
public String cancelDay(PayLoad payLoad) {
PayConfig payConfig = new PayConfig()
.setAppid("xxx")
.setCusid("xxxx")
.setPrivateKey(rsaPrivateKey);
Allinpay allinpay = new Allinpay()
.setCusid(payConfig.getCusid())
.setAppid(payConfig.getAppid())
.setRandomstr(SybUtil.getValidatecode(8))
.setVersion(11)
.setTrxamt(new BigDecimal(2))
.setReqsn(payLoad.getOrderId())
.setOldreqsn(payLoad.getOrderId())
.setSigntype("RSA");
allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
return HttpUtil.post(cancelDayUrl, BeanUtil.beanToMap(allinpay));
}
/**
* 退款接口,只能退今天的,全额退款,实时返回退款结果
* @return {@link String}
*/
@SneakyThrows
@GetMapping("cancelDay/{outTradeNo}")
public Map<String, String> cancelDay(@PathVariable Long outTradeNo) {
return handleResult(payService.cancelDay(new PayLoad().setOrderId(outTradeNo.toString())));
}
-
退款接口 ,可以退部分
// 退款接口
private final String refundUrl = "https://vsp.allinpay.com/apiweb/tranx/refund";
@SneakyThrows
@Override
public String refund(PayLoad payLoad) {
PayConfig payConfig = new PayConfig()
.setAppid("xxx")
.setCusid("xxxx")
.setPrivateKey(rsaPrivateKey);
Allinpay allinpay = new Allinpay()
.setCusid(payConfig.getCusid())
.setAppid(payConfig.getAppid())
.setRandomstr(SybUtil.getValidatecode(8))
.setVersion(11)
.setTrxamt(new BigDecimal(2))
.setReqsn(payLoad.getOrderId())
.setOldreqsn(payLoad.getOrderId())
.setSigntype("RSA");
allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
return HttpUtil.post(refundUrl, BeanUtil.beanToMap(allinpay));
}
// 可以和上面的接口进行整合处理
/**
* 退款接口,可以退部分
* @return {@link String}
*/
@SneakyThrows
@GetMapping("refund/{outTradeNo}")
public Map<String, String> refund(@PathVariable Long outTradeNo) {
return handleResult(payService.refund(new PayLoad().setOrderId(outTradeNo.toString())));
}
提示: 可以和上面的接口进行整合处理
-
其中签名需要用到的转map方法
private TreeMap<String, String> objectToTreeMap(Object obj) {
TreeMap<String, String> treeMap = new TreeMap<>();
Class<?> clazz = obj.getClass();
while (clazz != null) {
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldValue = field.get(obj);
if (fieldValue != null) {
treeMap.put(field.getName(), fieldValue.toString());
}
} catch (IllegalAccessException e) {
log.error("Error accessing field " + field.getName() + ": " + e.getMessage());
}
}
clazz = clazz.getSuperclass();
}
return treeMap;
}
-
其中map转路径参数方法
private static String treeMapToUrlParams(TreeMap<String, String> treeMap) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : treeMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (sb.length() > 0) {
sb.append("&");
}
sb.append(key).append("=").append(value);
}
return sb.toString();
}
-
其中通联返回验签代码
/**
* 验签
* @param result
* @return {@link Map}<{@link String},{@link String}>
* @throws Exception
*/
@SuppressWarnings({ "rawtypes", "all" })
public static Map<String,String> handleResult(String result) throws Exception{
Map map = SybUtil.json2Obj(result, Map.class);
if(map == null){
throw new Exception("返回数据错误");
}
if("SUCCESS".equals(map.get("retcode"))) {
TreeMap tmap = new TreeMap();
tmap.putAll(map);
if(SybUtil.validSign(
tmap,
"xxxxx",
"RSA")
){
return map;
}else{
throw new Exception("验证签名失败");
}
// 验签成功,返回数据
}else{
throw new Exception(map.get("retmsg").toString());
}
}
提示:如果不考虑多商户,多支付通道,可以再方法中Allinpay类直接填参数请求,不用通过PayLoad和PayConfig类
-
完整代码克隆地址:
git clone https://gitee.com/byte1026/allin-pay.git