当前位置:   article > 正文

通联支付API集成(适用于SpringBoot)_通联支付java

通联支付java

目标:

  • 学习如何使用Java与通联支付API进行交互

  • 实现一个简单的支付下单和查询订单状态的示例

所需材料:

  • 通联支付API文档

官方文档icon-default.png?t=N7T8https://aipboss.allinpay.com/know/devhelp/main.php?pid=38#mid=313

  • 通联支付加签代码SybUtil

  1. package com.allinpay.common;
  2. import net.sf.json.JSONObject;
  3. import org.apache.tomcat.util.codec.binary.Base64;
  4. import java.security.*;
  5. import java.security.spec.PKCS8EncodedKeySpec;
  6. import java.security.spec.X509EncodedKeySpec;
  7. import java.util.Map;
  8. import java.util.Random;
  9. import java.util.TreeMap;
  10. @SuppressWarnings("all")
  11. public class SybUtil {
  12. /**
  13. * js转化为实体
  14. *
  15. * @param <T>
  16. * @param jsonstr
  17. * @param cls
  18. * @return
  19. */
  20. public static <T> T json2Obj(String jsonstr, Class<T> cls) {
  21. JSONObject jo = JSONObject.fromObject(jsonstr);
  22. T obj = (T) JSONObject.toBean(jo, cls);
  23. return obj;
  24. }
  25. /**
  26. * md5
  27. *
  28. * @param b
  29. * @return
  30. */
  31. public static String md5(byte[] b) {
  32. try {
  33. MessageDigest md = MessageDigest.getInstance("MD5");
  34. md.reset();
  35. md.update(b);
  36. byte[] hash = md.digest();
  37. StringBuffer outStrBuf = new StringBuffer(32);
  38. for (int i = 0; i < hash.length; i++) {
  39. int v = hash[i] & 0xFF;
  40. if (v < 16) {
  41. outStrBuf.append('0');
  42. }
  43. outStrBuf.append(Integer.toString(v, 16).toLowerCase());
  44. }
  45. return outStrBuf.toString();
  46. } catch (NoSuchAlgorithmException e) {
  47. e.printStackTrace();
  48. return new String(b);
  49. }
  50. }
  51. /**
  52. * 判断字符串是否为空
  53. *
  54. * @param s
  55. * @return
  56. */
  57. public static boolean isEmpty(String s) {
  58. if (s == null || "".equals(s.trim()))
  59. return true;
  60. return false;
  61. }
  62. /**
  63. * 生成随机码
  64. *
  65. * @param n
  66. * @return
  67. */
  68. public static String getValidatecode(int n) {
  69. Random random = new Random();
  70. String sRand = "";
  71. n = n == 0 ? 4 : n;// default 4
  72. for (int i = 0; i < n; i++) {
  73. String rand = String.valueOf(random.nextInt(10));
  74. sRand += rand;
  75. }
  76. return sRand;
  77. }
  78. public static boolean validSign(TreeMap<String, String> param,
  79. String appkey, String signType) throws Exception {
  80. if (param != null && !param.isEmpty()) {
  81. if (!param.containsKey("sign"))
  82. return false;
  83. String sign = param.remove("sign");
  84. if ("MD5".equals(signType)) {// 如果是md5则需要把md5的key加入到排序
  85. param.put("key", appkey);
  86. }
  87. StringBuilder sb = new StringBuilder();
  88. for (Map.Entry<String, String> entry : param.entrySet()) {
  89. if (entry.getValue() != null && entry.getValue().length() > 0) {
  90. sb.append(entry.getKey()).append("=")
  91. .append(entry.getValue()).append("&");
  92. }
  93. }
  94. if (sb.length() > 0) {
  95. sb.deleteCharAt(sb.length() - 1);
  96. }
  97. if ("MD5".equals(signType)) {
  98. return sign.toLowerCase().equals(
  99. md5(sb.toString().getBytes("UTF-8")).toLowerCase());
  100. } else {
  101. return rsaVerifyPublickey(sb.toString(), sign, appkey, "UTF-8");
  102. }
  103. }
  104. return false;
  105. }
  106. public static boolean rsaVerifyPublickey(String content, String sign,
  107. String publicKey, String charset) throws Exception {
  108. try {
  109. PublicKey pubKey = getPublicKeyFromX509("RSA",
  110. Base64.decodeBase64(publicKey.getBytes()));
  111. return rsaVerifyPublickey(content, sign, pubKey, charset);
  112. } catch (Exception e) {
  113. e.printStackTrace();
  114. throw new Exception("RSAcontent = " + content + ",sign=" + sign
  115. + ",charset = " + charset, e);
  116. }
  117. }
  118. public static boolean rsaVerifyPublickey(String content, String sign,
  119. PublicKey pubKey, String charset) throws Exception {
  120. try {
  121. java.security.Signature signature = java.security.Signature
  122. .getInstance("SHA1WithRSA");
  123. signature.initVerify(pubKey);
  124. if (charset == null || "".equals(charset)) {
  125. signature.update(content.getBytes());
  126. } else {
  127. signature.update(content.getBytes(charset));
  128. }
  129. return signature.verify(Base64.decodeBase64(sign.getBytes()));
  130. } catch (Exception e) {
  131. throw e;
  132. }
  133. }
  134. public static String unionSign(TreeMap<String, String> params,String appkey,
  135. String signType) throws Exception {
  136. // TODO Auto-generated method stub
  137. params.remove("sign");
  138. if ("MD5".equals(signType)) {// 如果是md5则需要把md5的key加入到排序
  139. params.put("key", appkey);
  140. }
  141. StringBuilder sb = new StringBuilder();
  142. for (Map.Entry<String, String> entry : params.entrySet()) {
  143. if (entry.getValue() != null && entry.getValue().length() > 0) {
  144. sb.append(entry.getKey()).append("=").append(entry.getValue())
  145. .append("&");
  146. }
  147. }
  148. if (sb.length() > 0) {
  149. sb.deleteCharAt(sb.length() - 1);
  150. }
  151. String sign = "";
  152. if ("MD5".equals(signType)) {
  153. System.out.println(sb.toString());
  154. sign = md5(sb.toString().getBytes("UTF-8"));// 记得是md5编码的加签
  155. params.remove("key");
  156. } else {
  157. sign = rsaSign(sb.toString(), appkey, "UTF-8");
  158. }
  159. return sign;
  160. }
  161. public static String rsaSign(String content, String privateKey,
  162. String charset) throws Exception {
  163. PrivateKey priKey = getPrivateKeyFromPKCS8("RSA",
  164. Base64.decodeBase64(privateKey.getBytes()));
  165. return rsaSign(content, priKey, charset);
  166. }
  167. public static String rsaSign(String content, byte[] privateKey,
  168. String charset) throws Exception {
  169. PrivateKey priKey = getPrivateKeyFromPKCS8("RSA", privateKey);
  170. return rsaSign(content, priKey, charset);
  171. }
  172. public static String rsaSign(String content, PrivateKey priKey,
  173. String charset) throws Exception {
  174. java.security.Signature signature = java.security.Signature
  175. .getInstance("SHA1WithRSA");
  176. signature.initSign(priKey);
  177. if (charset == null || "".equals(charset)) {
  178. signature.update(content.getBytes());
  179. } else {
  180. signature.update(content.getBytes(charset));
  181. }
  182. byte[] signed = signature.sign();
  183. return new String(Base64.encodeBase64(signed));
  184. }
  185. public static PrivateKey getPrivateKeyFromPKCS8(String algorithm,
  186. byte[] encodedKey) throws Exception {
  187. KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
  188. return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
  189. }
  190. public static PublicKey getPublicKeyFromX509(String algorithm,
  191. byte[] encodedKey) throws Exception {
  192. KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
  193. return keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
  194. }
  195. }
  • IDE(如IntelliJ IDEA或Eclipse)

  • JDK 8 或更高版本

  • 需要的maven
  1. <dependency>
  2. <groupId>com.alibaba.fastjson2</groupId>
  3. <artifactId>fastjson2</artifactId>
  4. <version>2.0.43</version>
  5. </dependency>
  6. <!-- SybUtil文件需要用到 -->
  7. <dependency>
  8. <groupId>net.sf.json-lib</groupId>
  9. <artifactId>json-lib</artifactId>
  10. <version>2.4</version>
  11. <classifier>jdk15</classifier>
  12. </dependency>
  13. <dependency>
  14. <groupId>cn.hutool</groupId>
  15. <artifactId>hutool-all</artifactId>
  16. <version>5.8.20</version>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.projectlombok</groupId>
  20. <artifactId>lombok</artifactId>
  21. <optional>true</optional>
  22. </dependency>
  •  辅助类
  1. 通联支付需要的请求数据格式(Allinpay.java)
  1. package com.allinpay.pojo;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import lombok.experimental.Accessors;
  6. import java.math.BigDecimal;
  7. @Data
  8. @NoArgsConstructor
  9. @AllArgsConstructor
  10. @Accessors(chain = true)
  11. public class Allinpay {
  12. private String cusid;
  13. private String appid;
  14. private int version;
  15. private BigDecimal trxamt;
  16. /*
  17. 当指定为F02时,交易仅限分期交易。
  18. 分期交易金额必须大于500元。
  19. */
  20. // ?
  21. private String paytype;
  22. /*
  23. 3 花呗分期3期
  24. 6 花呗分期6期
  25. 12 花呗分期12期
  26. 3-cc 支付宝信用卡分期3期
  27. 6-cc 支付宝信用卡分期6期
  28. 12-cc 支付宝信用卡分期12期
  29. 暂只支持支付宝花呗分期,支付宝信用卡分期,仅支持A01/A02
  30. */
  31. // 可空参数
  32. private String fqnum;
  33. //订单号,商户唯一订单号
  34. private String reqsn;
  35. /*
  36. 商户网站使用的编码格式,支持
  37. UTF-8、GBK
  38. 跟商户网站的编码一致
  39. */
  40. //
  41. private String charset;
  42. /*
  43. 必须为https协议地址,且不允许带参数
  44. 页面跳转同步通知页面路径
  45. */
  46. private String returl;
  47. // 异步通知地址
  48. // ?
  49. private String notify_url;
  50. // 商品描述,长度最大100
  51. private String body;
  52. //? 通知会原样带上, 订单备注信息
  53. private String remark;
  54. // 随机字符串,自己生成,最大32位
  55. private String randomstr;
  56. //订单有效时间,以分为单位,默认为15
  57. private String validtime;
  58. // ? 支付限制(no_credit--指定不能使用信用卡支付)
  59. private String limit_pay;
  60. // 签名类型,目前支持RSA2和RSA
  61. private String signtype;
  62. // 签名 32位
  63. private String sign;
  64. // 关闭订单的时候需要
  65. private String oldreqsn;
  66. }

 2. 由于本案列采用了多商家,所以暂时把配置也建成了一个类(PayConfig.java)

  1. import lombok.AllArgsConstructor;
  2. import lombok.Data;
  3. import lombok.NoArgsConstructor;
  4. import lombok.experimental.Accessors;
  5. @AllArgsConstructor
  6. @NoArgsConstructor
  7. @Data
  8. @Accessors(chain = true)
  9. public class PayConfig {
  10. private String cusid;
  11. private String appid;
  12. private String privateKey;
  13. }

 3.本案列设置了多方支付方式,所以还有一个支付载荷(PayLoad.java)

  1. import lombok.AllArgsConstructor;
  2. import lombok.Data;
  3. import lombok.NoArgsConstructor;
  4. import lombok.experimental.Accessors;
  5. import java.math.BigDecimal;
  6. @Data
  7. @NoArgsConstructor
  8. @AllArgsConstructor
  9. @Accessors(chain = true)
  10. public class PayLoad {
  11. private String orderId;
  12. private BigDecimal amount;
  13. private String payConfigKey;
  14. private String remark;
  15. private String title;
  16. private int payType;
  17. private String returnUrl;
  18. private String notifyUrl;
  19. }
  •  H5支付下单代码

  1. //这个是PayService的Impl实现层
  2. private final String order = "https://syb.allinpay.com/apiweb/h5unionpay/unionorder";
  3. private final String rsaPrivateKey = "xxx";
  4. @SneakyThrows
  5. @Override
  6. public String h5Pay(PayLoad payLoad) {
  7. // String payConfigKey = payLoad.getPayConfigKey();
  8. PayConfig payConfig = new PayConfig()
  9. .setAppid("xxx")
  10. .setCusid("xxx")
  11. .setPrivateKey(rsaPrivateKey);
  12. Allinpay allinpay = new Allinpay()
  13. .setSigntype("RSA")
  14. .setTrxamt(new BigDecimal(2))
  15. .setReqsn(payLoad.getOrderId())
  16. .setRandomstr(SybUtil.getValidatecode(8))
  17. .setBody(payLoad.getTitle())
  18. .setRemark(payLoad.getRemark())
  19. .setCharset("UTF-8")
  20. .setAppid(payConfig.getAppid())
  21. .setCusid(payConfig.getCusid())
  22. .setReturl(payLoad.getReturnUrl());
  23. allinpay.setSign(URLEncoder.encode(SybUtil.unionSign(objectToTreeMap(allinpay), payConfig.getPrivateKey() , "RSA"), StandardCharsets.UTF_8));
  24. return order + "?" + treeMapToUrlParams(objectToTreeMap(allinpay));
  25. }
  26. //Controller代码
  27. @SneakyThrows
  28. @GetMapping("pay")
  29. public String pay(HttpServletResponse response) {
  30. String paymentUrl = (String) payService.h5Pay(new PayLoad()
  31. .setPayType(2)
  32. .setPayConfigKey("")
  33. .setReturnUrl("https://blog.csdn.net")
  34. .setOrderId(IdUtil.getSnowflakeNextIdStr())
  35. .setAmount(new BigDecimal(2))
  36. .setRemark("测试支付备注")
  37. .setTitle("测试H5支付")
  38. );
  39. String htmlResponse = "<!DOCTYPE html><html><head></head><body>"
  40. + "<script>window.location.href='" + paymentUrl + "'</script>"
  41. + "</body></html>";
  42. // 设置响应内容类型为HTML
  43. response.setContentType("text/html;charset=UTF-8");
  44. return htmlResponse;
  45. }

  • 关闭订单 

  1. // 关闭接口
  2. private final String close = "https://vsp.allinpay.com/apiweb/tranx/close";
  3. @SneakyThrows
  4. @Override
  5. public String closeOrder(PayLoad payLoad) {
  6. PayConfig payConfig = new PayConfig()
  7. .setAppid("xxx")
  8. .setCusid("xxxx")
  9. .setPrivateKey(rsaPrivateKey);
  10. Allinpay allinpay = new Allinpay()
  11. .setCusid(payConfig.getCusid())
  12. .setAppid(payConfig.getAppid())
  13. .setRandomstr(SybUtil.getValidatecode(8))
  14. .setVersion(11)
  15. .setOldreqsn(payLoad.getOrderId())
  16. .setSigntype("RSA");
  17. allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
  18. return HttpUtil.post(close, BeanUtil.beanToMap(allinpay));
  19. }
  20. /**
  21. * 关闭订单
  22. * @param outTradeNo 下单的订单号,也就是你支付下单类Allinpay的reqsn
  23. * @return {@link String}
  24. ***/
  25. @SneakyThrows
  26. @GetMapping("closeOrder/{outTradeNo}")
  27. public Map<String,String> closeOrder(@PathVariable Long outTradeNo) {
  28. return handleResult(payService.closeOrder(new PayLoad().setOrderId(outTradeNo.toString())));
  29. }
  •  查询订单

  1. // 查询接口
  2. private final String query = "https://vsp.allinpay.com/apiweb/tranx/query";
  3. @SneakyThrows
  4. @Override
  5. public String query(PayLoad payLoad) {
  6. PayConfig payConfig = new PayConfig()
  7. .setAppid("xxx")
  8. .setCusid("xxxx")
  9. .setPrivateKey(rsaPrivateKey);
  10. Allinpay allinpay = new Allinpay()
  11. .setCusid(payConfig.getCusid())
  12. .setAppid(payConfig.getAppid())
  13. .setRandomstr(SybUtil.getValidatecode(8))
  14. .setVersion(11)
  15. .setReqsn(payLoad.getOrderId())
  16. .setSigntype("RSA");
  17. allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
  18. return HttpUtil.post(query, BeanUtil.beanToMap(allinpay));
  19. }
  20. /**
  21. * 查询订单
  22. * @param outTradeNo 下单的订单号
  23. * @return {@link String}
  24. ***/
  25. @SneakyThrows
  26. @GetMapping("query/{outTradeNo}")
  27. public Map<String, String> query(@PathVariable Long outTradeNo) {
  28. return handleResult(payService.query(new PayLoad().setOrderId(outTradeNo.toString())));
  29. }
  • 退款接口,这个是只能退当天的交易 (全额退实时返回退款结果)

  1. // 取消当天交易退款接口
  2. private final String cancelDayUrl = "https://vsp.allinpay.com/apiweb/tranx/cancel";
  3. @SneakyThrows
  4. @Override
  5. public String cancelDay(PayLoad payLoad) {
  6. PayConfig payConfig = new PayConfig()
  7. .setAppid("xxx")
  8. .setCusid("xxxx")
  9. .setPrivateKey(rsaPrivateKey);
  10. Allinpay allinpay = new Allinpay()
  11. .setCusid(payConfig.getCusid())
  12. .setAppid(payConfig.getAppid())
  13. .setRandomstr(SybUtil.getValidatecode(8))
  14. .setVersion(11)
  15. .setTrxamt(new BigDecimal(2))
  16. .setReqsn(payLoad.getOrderId())
  17. .setOldreqsn(payLoad.getOrderId())
  18. .setSigntype("RSA");
  19. allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
  20. return HttpUtil.post(cancelDayUrl, BeanUtil.beanToMap(allinpay));
  21. }
  22. /**
  23. * 退款接口,只能退今天的,全额退款,实时返回退款结果
  24. * @return {@link String}
  25. */
  26. @SneakyThrows
  27. @GetMapping("cancelDay/{outTradeNo}")
  28. public Map<String, String> cancelDay(@PathVariable Long outTradeNo) {
  29. return handleResult(payService.cancelDay(new PayLoad().setOrderId(outTradeNo.toString())));
  30. }
  • 退款接口 ,可以退部分

  1. // 退款接口
  2. private final String refundUrl = "https://vsp.allinpay.com/apiweb/tranx/refund";
  3. @SneakyThrows
  4. @Override
  5. public String refund(PayLoad payLoad) {
  6. PayConfig payConfig = new PayConfig()
  7. .setAppid("xxx")
  8. .setCusid("xxxx")
  9. .setPrivateKey(rsaPrivateKey);
  10. Allinpay allinpay = new Allinpay()
  11. .setCusid(payConfig.getCusid())
  12. .setAppid(payConfig.getAppid())
  13. .setRandomstr(SybUtil.getValidatecode(8))
  14. .setVersion(11)
  15. .setTrxamt(new BigDecimal(2))
  16. .setReqsn(payLoad.getOrderId())
  17. .setOldreqsn(payLoad.getOrderId())
  18. .setSigntype("RSA");
  19. allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
  20. return HttpUtil.post(refundUrl, BeanUtil.beanToMap(allinpay));
  21. }
  22. // 可以和上面的接口进行整合处理
  23. /**
  24. * 退款接口,可以退部分
  25. * @return {@link String}
  26. */
  27. @SneakyThrows
  28. @GetMapping("refund/{outTradeNo}")
  29. public Map<String, String> refund(@PathVariable Long outTradeNo) {
  30. return handleResult(payService.refund(new PayLoad().setOrderId(outTradeNo.toString())));
  31. }

提示: 可以和上面的接口进行整合处理

  • 其中签名需要用到的转map方法 

  1. private TreeMap<String, String> objectToTreeMap(Object obj) {
  2. TreeMap<String, String> treeMap = new TreeMap<>();
  3. Class<?> clazz = obj.getClass();
  4. while (clazz != null) {
  5. for (Field field : clazz.getDeclaredFields()) {
  6. field.setAccessible(true);
  7. try {
  8. Object fieldValue = field.get(obj);
  9. if (fieldValue != null) {
  10. treeMap.put(field.getName(), fieldValue.toString());
  11. }
  12. } catch (IllegalAccessException e) {
  13. log.error("Error accessing field " + field.getName() + ": " + e.getMessage());
  14. }
  15. }
  16. clazz = clazz.getSuperclass();
  17. }
  18. return treeMap;
  19. }
  •  其中map转路径参数方法

  1. private static String treeMapToUrlParams(TreeMap<String, String> treeMap) {
  2. StringBuilder sb = new StringBuilder();
  3. for (Map.Entry<String, String> entry : treeMap.entrySet()) {
  4. String key = entry.getKey();
  5. String value = entry.getValue();
  6. if (sb.length() > 0) {
  7. sb.append("&");
  8. }
  9. sb.append(key).append("=").append(value);
  10. }
  11. return sb.toString();
  12. }
  • 其中通联返回验签代码

  1. /**
  2. * 验签
  3. * @param result
  4. * @return {@link Map}<{@link String},{@link String}>
  5. * @throws Exception
  6. */
  7. @SuppressWarnings({ "rawtypes", "all" })
  8. public static Map<String,String> handleResult(String result) throws Exception{
  9. Map map = SybUtil.json2Obj(result, Map.class);
  10. if(map == null){
  11. throw new Exception("返回数据错误");
  12. }
  13. if("SUCCESS".equals(map.get("retcode"))) {
  14. TreeMap tmap = new TreeMap();
  15. tmap.putAll(map);
  16. if(SybUtil.validSign(
  17. tmap,
  18. "xxxxx",
  19. "RSA")
  20. ){
  21. return map;
  22. }else{
  23. throw new Exception("验证签名失败");
  24. }
  25. // 验签成功,返回数据
  26. }else{
  27. throw new Exception(map.get("retmsg").toString());
  28. }
  29. }

提示:如果不考虑多商户,多支付通道,可以再方法中Allinpay类直接填参数请求,不用通过PayLoad和PayConfig类  

  • 完整代码克隆地址:

git clone https://gitee.com/byte1026/allin-pay.git
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/615332
推荐阅读
相关标签
  

闽ICP备14008679号