2021-03-16
虎皮椒 官网:https://www.xunhupay.com/
调用API 之前呢,我们需要获取到自己 appid、appsecret。
获取 方式 如图 商家登录 后 点击 支付渠道管理 再点击 我的支付渠道
我们 获取到我们的 appid、appsecret 后 就可以 模拟Http 请求,调用api 了
官方 API 解释:https://www.xunhupay.com/doc/api/pay.html
不想看人家网站,直接看我提取内容就够了
# | 参数名 | 含义 | 类型 | 说明 |
---|---|---|---|---|
1 | version | API 版本号 | string(24) | 必填。目前为1.1 |
2 | appid | APP ID | string(32) | 必填。支付渠道ID |
3 | trade_order_id | 商户订单号 | string(32) | 必填。请确保在当前网站内是唯一订单号 |
4 | total_fee | 订单金额(元) | decimal(18,2) | 必填。单位为人民币,精确到分 |
5 | title | 订单标题 | string(128) | 必填。商户订单标题 |
6 | time | 当前时间戳 | int(11) | 必填。PHP示例:time() |
7 | notify_url | 通知回调网址 | string(128) | 必填。用户支付成功后,我们服务器会主动发送一个post消息到这个网址(注意:当前接口内,SESSION内容无效) |
8 | return_url | 跳转网址 | string(128) | 可选。用户支付成功后,我们会让用户浏览器自动跳转到这个网址 |
9 | callback_url | 商品网址 | string(128) | 可选。用户取消支付后,我们可能引导用户跳转到这个网址上重新进行支付 |
10 | plugins | 备注 | string(128) | 可选。备注字段,可以传入一些备注数据,回调时原样返回 |
11 | nonce_str | 随机值 | string(32) | 必填。作用:1.避免服务器页面缓存,2.防止安全密钥被猜测出来 |
12 | hash | 签名 | string(32) | 必填。 |
13 | redirect=Y | get请求 | string(32) | GET请求必填 |
# | 参数名 | 含义 | 类型 | 说明 |
---|---|---|---|---|
1 | oderid | 订单id | int | 订单id |
2 | url_qrcode | 二维码地址 | string(156) | 可将该参数生成二维码展示出来进行扫码支付 |
3 | url | 请求url | string(155) | |
4 | errcode | 错误码 | int | |
5 | errmsg | 错误信息 | string(8) | 错误信息具体值 |
6 | hash | 签名 | string(32) | 数据签名,参考下面签名算法 |
我们 从官方获取到 API 请求的地址
https://api.xunhupay.com/payment/do.html
拿到地址 我们 就可以模拟请求了,我们采用 Postman 的方式进行测试。如果不了解 Postman 可以访问 XXX,(这里 Postman 教程 没写,写了会补全)
先看下 成功PostMan 案例。这里泄露我 appid 各位老铁,不要干坏事。
好了 上图 各个参数 都能 只有 time 和 hash 需要我们额外生成,别急,下面有 JAVA 代码。
讲代码之前:我们 先说说 客户端 与 服务端 运行的原理吧
客户端 与 服务端 之间 相互鉴别 通过一个叫 hash 的签名。如果 hash签名一致,就认定请求成功!
hash ,这里不叫哈希了,他叫签名。他的生成原理是:将请求中的所有参数(除本身外),进行 键的Ascll 从小到大进行排序,之后使用 “&” 进行关联。最后直接拼接上 appsecret (注意,appsecret 前 不需要 “&” 关联)。得到一串字符串后,进行MD5 加密。就达到了 hash 值
将来服务端 验证的时候,根据我们请求参数,重复一遍 生成我们客户端生成的Hash 过程。(注意,我们请求参数里面,没有传 appsecret 。服务端是 自己从自己服务器 根据我们传递 appid 取出 appsecret) 进行比对,信息是否正确。
原理说完,我们说一下代码实现
说明,我使用了Hutool 中的 加密工具,就需要我们在 Maven 添加
<!-- Hutool工具 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.5.7</version> </dependency>
首先 我们模拟postman 发送请求的时候,唯一缺少的就是 hash 签名 和 秒的时间戳 了。我们就去生成一下。
在模拟 hash签名之前,我们需要去写一个方法 去获取 秒的时间戳
/** * 获取精确到秒的时间戳 原理 获取毫秒时间戳,因为 1秒 = 100毫秒 去除后三位 就是秒的时间戳 * @return */ public static int getSecondTimestamp(Date date){ if (null == date) { return 0; } String timestamp = String.valueOf(date.getTime()); int length = timestamp.length(); if (length > 3) { return Integer.valueOf(timestamp.substring(0,length-3)); } else { return 0; } }
有了秒的时间戳了,就可以去生成 hash签名了 下面是伪代码
String appid = "你的APPID"; Map<String,Object> options = new HashMap<>(); // 设置版本号 options.put("version","1.1"); // 设置 appid options.put("appid",appid); // 密钥不需要参数 //options.put("appsecret",appsecret); // 订单号 具体内容自己控制 长度 32位 options.put("trade_order_id","1"); // 价格价格 精确到分 options.put("total_fee","0.01"); // 标题 options.put("title","测试使用的title"); // 当前时间戳 调用 刚写的方法 getSecondTimestamp options.put("time", getSecondTimestamp(new Date())); // notify_url 回调地址 options.put("notify_url","49.72.184.28"); // nonce_str 随机值 32位内 作用: 1.避免服务器页面缓存,2.防止安全密钥被猜测出来(md5 密钥越复杂,就越难解密出来) options.put("nonce_str","740969606"); // 定义 sb 为了获取 MD5 加密前的字符串 StringBuilder sb = new StringBuilder(); // 将HashMap 进行 键 的 Ascll 从小到大排序 并 将每个 hashmap元素 以 & 拼接起来 options.entrySet().stream().sorted((e1,e2) -> e1.getKey().compareTo(e2.getKey())).forEach(a ->{ sb.append(a).append("&");}); // 去除 最后一位的 & sb.deleteCharAt(sb.length()-1); // 拼接上密钥 sb.append(appsecret); // 调用 Hutool 的 加密工具 进行 MD5 加密 String s = SecureUtil.md5(sb.toString()); // 输出hash结果 postman 要用 System.out.println("我们生成的Hash 是:"+s); // 输出time结果 postman 要用 System.out.println("我们生成的time 是:"+options.get("time"));
上面 代码 自己创建一下 run 一下 控制台会输出 hash 和 time,有了这些数据,我们就可以postman 模拟了 ,看到 Hashmap集合填写的参数,放到 postman unlencoded 格式中
最终,会看到结果 Success
当然,第一次测试,大部分都不会成功,如图
一定要确保 参数正确。就可以了。
简单的 postman 测试成功了,我们 就准备 制作我们的 支付 – 跳转 功能了。
待更新!!!
好了 完成 Java 调用的测试了,上代码。
package com.zlk; import cn.hutool.crypto.SecureUtil; import cn.hutool.http.HttpUtil; import com.alibaba.fastjson.JSON; import org.junit.Test; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @author : zanglikun * @date : 2021/2/20 13:48 * @Version: 1.0 * @Desc : 费劲,没啥好说的 */ public class pay { // 请求参数的含义 与 值范围,请访问呢: https://www.xunhupay.com/doc/api/pay.html @Test public void pay(){ // appid String appid = "201906134645"; // appsecret String appsecret = "你的密钥"; // 请求路径 String url = "https://api.xunhupay.com/payment/do.html"; // 设置 传递参数的集合,方便 传递数据。 Map<String,Object> options = new HashMap<>(); // 必填 设置版本号 options.put("version","1.1"); // 必填 设置 appid options.put("appid",appid); // 密钥不需要直接传递 //options.put("appsecret",appsecret); // 必填 订单号 具体内容自己控制 长度 32位 官网说 请确保在当前网站内是唯一订单号,具体含义 我测试了 在描述 此备注 options.put("trade_order_id","1"); // 必填 价格 精确到RMB分 options.put("total_fee","0.01"); // 必填 标题 options.put("title","测试使用的title"); // 必填 当前时间戳 调用 刚写的方法 getSecondTimestamp options.put("time", getSecondTimestamp(new Date())); // 必填 通知回调地址 url 什么含义 我们后台需要知道 用户支付了。 options.put("notify_url","https://zanglikun.cn1.utools.club/paycallback"); // 只有这个有用 // 非必填 使用 响应字段中 url 就直接跳到百度了,如果访问,url_qrcode ,不会直接跳转,只有当支付完成后,再次刷新 url_qrcode中的连接,才会跳转。 options.put("return_url","https://www.baidu.com"); // 非必填 用户取消支付,跳转的页面 经过测试,没有触发机制,建议不传递 options.put("callback_url","https://www.sina.com.cn/"); //plugins 非必填 备注信息 options.put("plugins","我是备注信息"); // nonce_str 必填 随机值 32位内 作用: 1.避免服务器页面缓存,2.防止安全密钥被猜测出来(md5 密钥越复杂,就越难解密出来) options.put("nonce_str","740969606"); // 定义 sb 为了获取 MD5 加密前的字符串 StringBuilder sb = new StringBuilder(); // 将HashMap 进行 键 的 Ascll 从小到大排序 并 将每个 hashmap元素 以 & 拼接起来 options.entrySet().stream().sorted((e1,e2) -> e1.getKey().compareTo(e2.getKey())).forEach(a ->{ sb.append(a).append("&");}); // 去除 最后一位的 & sb.deleteCharAt(sb.length()-1); // 拼接上密钥 sb.append(appsecret); // 调用 Hutool 的 加密工具 进行 MD5 加密 String s = SecureUtil.md5(sb.toString()); // 输出hash结果 postman 要用 System.out.println("我们生成的Hash 是:"+s); // 输出time结果 postman 要用 System.out.println("我们生成的time 是: "+options.get("time")); System.out.println(); // 必填 hash 签名 options.put("hash", s); System.out.println("我们传递的参数有:"+options.toString()); System.out.println("开始调 虎皮椒支付 接口..."); // 调用 Hutool 的HttpUtil 发送 post 请求 String post = HttpUtil.post(url, options); System.out.println("结束调 虎皮椒支付 接口...\n"); System.out.println("虎皮椒支付 接口 响应的结果是:"+post+"\n"); // 说明:这里 因为虎皮椒支付 响应结果 不统一,正确是Json;不正确 就是一行String 。没办法 判断是否请求是否有效。所以 只能通过 是由能够解析成json 有无异常判断 是否调用成功 try{ Map map = (Map)JSON.parse(post); map.keySet().stream().forEach(k -> { if (k == "url") { System.out.println("url二维码链接是: "+map.get(k)); } }); }catch (Exception e){ e.printStackTrace(); System.out.println("调 虎皮椒支付 时 出现了问题"); } } /** * 获取精确到秒的时间戳 原理 获取毫秒时间戳,因为 1秒 = 100毫秒 去除后三位 就是秒的时间戳 * @return */ public static int getSecondTimestamp(Date date){ if (null == date) { return 0; } String timestamp = String.valueOf(date.getTime()); int length = timestamp.length(); if (length > 3) { return Integer.valueOf(timestamp.substring(0,length-3)); } else { return 0; } } }
上述 代码中 蓝色 标注的代码是 回调地址。会spring框架的兄弟 都能看懂。
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.Map; /** * @author : zanglikun * @date : 2021/2/22 12:04 * @Version: 1.0 * @Desc : 费劲,没啥好说的 */ @Controller public class PayCallback { @RequestMapping("/paycallback") @ResponseBody public String abc(HttpServletRequest request){ // 记得 map 第二个泛型是数组 要取 第一个元素 即[0] Map<String, String[]> parameterMap = request.getParameterMap(); System.out.println("展示 回调的所有结果:"); // 处理 回调 结果 parameterMap.keySet().stream().forEach((k) ->{ System.out.println(k+":"+parameterMap.get(k)[0]); }); System.out.println("展示 回调的所有结果完成"); System.out.print("\n最终结果是:"); if("OD".equals(parameterMap.get("status")[0])){ System.out.println("用户支付成功了"); }else { System.out.println("用户支付不成功"); } return "ok"; } }
展示 回调的所有结果: trade_order_id:1 total_fee:0.01 transaction_id:2021022222001486491437925203 open_order_id:20207745197 order_title:测试使用的title status:OD plugins:我是备注信息 nonce_str:9375163117 time:1613975317 appid:201906134645 hash:621a90afe443c278f22927a1220f0ee5 展示 回调的所有结果完成 最终结果是:[Ljava.lang.String;@790a1d8e 用户支付成功了