目标:
-
学习如何使用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 4for (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 stubparams.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;// 商品描述,长度最大100private String body;//? 通知会原样带上, 订单备注信息private String remark;// 随机字符串,自己生成,最大32位private String randomstr;//订单有效时间,以分为单位,默认为15private String validtime;// ? 支付限制(no_credit--指定不能使用信用卡支付)private String limit_pay;// 签名类型,目前支持RSA2和RSAprivate 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@Overridepublic 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>";// 设置响应内容类型为HTMLresponse.setContentType("text/html;charset=UTF-8");return htmlResponse;}
-
关闭订单
// 关闭接口private final String close = "https://vsp.allinpay.com/apiweb/tranx/close";@SneakyThrows@Overridepublic 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@Overridepublic 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@Overridepublic 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@Overridepublic 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