这两天做毕业设计,因为做的是一个电商平台所以需要调用支付宝的支付接口进行支付的操作,于是将整个调用过程用博客形式记录下来,以供以后使用。备注:PC网站调用
前期准备:
本次调用支付宝采用的是电脑支付,官方文档页面如下:
支付宝开放平台网址:
https://open.alipay.com/platform/home.htm
步骤:登陆-》开发者中心-》进入我的控制台-》点网页&移动应用-》研发服务
效果如下图所示
设置上图第三个箭头标注的需要下载支付宝密钥生成器
支付宝密钥生成器下载地址:
下载完使用如图所示:
把上图应用公钥复制出来设置进去。前期准备工作完成。
注意:沙箱环境测试可以不用设置应用网关、回调地址。
要调用支付宝的接口,首先需要下载支付宝的sdk。这里给出官方的下载地址:
支付宝sdk下载地址。下载下来的是一个zip包,里面文件如下(版本不一定和图中一样):
这四个jar包文件如下:
alipay-sdk-java-3.0.0.jar┈┈┈┈┈┈┈支付宝SDK编译文件jar
alipay-sdk-java-3.0.0-source.jar┈┈┈ 支付宝SDK源码文件jar
commons-logging-1.1.1.jar┈┈┈┈┈┈SDK依赖的日志jar
commons-logging-1.1.1-sources.jar┈┈SDK依赖的日志源码jar
项目中需要引入的是alipay-sdk-java-3.0.0.jar和commons-logging-1.1.1.jar。对于commons-logging-1.1.1.jar,可以直接在pom文件中添加依赖:
commons-logging
commons-logging
1.1.1
对于alipay-sdk-java-3.0.0.jar,目前maven仓库没有该依赖,只能手动添加。首先将alipay-sdk-java-3.0.0.jar放入D盘的根目录下,然后运行mvn命令:
方法一:
mvn install:install-file -DgroupId=com.alipay -DartifactId=sdk-java -Dversion=3.0.0 -Dpackaging=jar -Dfile=alipay-sdk-java-3.0.0.jar
此命令会把支付宝的jar包打包到maven本地仓库下,然后在pom文件中引用:
com.alipay
sdk-java
3.0.0
方法二(直接在pom文件引入alipay SDK):
1.alipaySdk引入依赖
选择对应版本导入依赖即可。
我把对支付宝的代码调用都封装到了一个工具类中:
package com.zdk.front.util;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.zdk.front.config.AlipayConfig;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @author zdk
* @date 2020/2/29 22:06
*/
public class PayUtil {
/**
*
* alipay:支付宝的下单接口
* aliRefund:支付宝的退款方法
* checkSign:支付宝的验签方法
*
* @param outTradeNo 商户订单号,商户网站订单系统中唯一订单号,必填 对应缴费记录的orderNo
* @param totalAmount 付款金额,必填
* @param subject 主题
* @param body 商品描述,可空
* @return
*/
public static String alipay(String outTradeNo,String totalAmount,String subject,String body) {
//获得初始化的AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);
//设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(AlipayConfig.return_url);
alipayRequest.setNotifyUrl(AlipayConfig.notify_url);
// 该笔订单允许的最晚付款时间,逾期将关闭交易。取值范围:1m~15d。m-分钟,h-小时,d-天,1c-当天(1c-当天的情况下,无论交易何时创建,都在0点关闭)。 该参数数值不接受小数点, 如 1.5h,可转换为 90m。
String timeout_express = "1c";
try {
alipayRequest.setBizContent("{\"out_trade_no\":\""+ outTradeNo +"\","
+ "\"total_amount\":\""+ totalAmount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\""+ timeout_express +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
//请求
String result;
result = alipayClient.pageExecute(alipayRequest).getBody();
System.out.println("*********************\n返回结果为:"+result);
return result;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
/**
* 支付宝退款接口
* @param outTradeNo
* @param tradeNo
* @param refundAmount
* @param refundReason
* @param out_request_no 标识一次退款请求,同一笔交易多次退款需要保证唯一,如需部分退款,则此参数必传
* @return
*/
public static String aliRefund(String outTradeNo,String tradeNo,String refundAmount,String refundReason,String out_request_no) {
//获得初始化的AlipayClient
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);
//设置请求参数
AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();
alipayRequest.setReturnUrl(AlipayConfig.return_url);
alipayRequest.setNotifyUrl(AlipayConfig.notify_url);
try {
alipayRequest.setBizContent("{\"out_trade_no\":\""+ outTradeNo +"\","
+ "\"trade_no\":\""+ tradeNo +"\","
+ "\"refund_amount\":\""+ refundAmount +"\","
+ "\"refund_reason\":\""+ refundReason +"\","
+ "\"out_request_no\":\""+ out_request_no +"\"}");
//请求
String result;
//请求
result = alipayClient.execute(alipayRequest).getBody();
System.out.println("*********************\n返回结果为:"+result);
return result;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
/**
* 支付宝的验签方法
* @param req
* @return
*/
public static boolean checkSign(HttpServletRequest req) {
Map requestMap = req.getParameterMap();
Map paramsMap = new HashMap<>();
requestMap.forEach((key, values) -> {
String strs = "";
for(String value : values) {
strs = strs + value;
}
System.out.println(("key值为"+key+"value为:"+strs));
paramsMap.put(key, strs);
});
//调用SDK验证签名
try {
return AlipaySignature.rsaCheckV1(paramsMap, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
} catch (AlipayApiException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("*********************验签失败********************");
return false;
}
}
}
这个工具类一共有三个静态方法。
alipay:支付宝的下单接口
aliRefund:支付宝的退款方法
checkSign:支付宝的验签方法
先来讲alipay这个方法,当我们需要下单的时候,需要在业务逻辑代码中调用该工具类的该方法。该方法需要传入的参数如下:
outTradeNo: 商户订单号,商户网站订单系统中唯一订单号,必填 。需要保证商户端唯一。
totalAmount :付款金额,必填
subject:主题
body:商品描述,可空
都是一些商品属性的基本参数,一般都是根据我们具体的业务逻辑去设置。进入方法,第一句话:
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);
这句话主要是初始化一个DefaultAlipayClient,这个类的初始化构造器需要传很多参数,从左往右依次为:支付宝的网关,商户的appid,商户的私钥,传参的格式,传参的字符集,商户的公钥,商户的签名类型。这里都把这些参数封装进了AlipayConfig这个类里面,来看看AlipayConfig这个类:
package com.zdk.front.config;
import java.io.FileWriter;
import java.io.IOException;
/**
* @author zdk
* @date 2020/2/29 22:09
*/
/* *
*类名:AlipayConfig
*功能:基础配置类
*详细:设置帐户有关信息及返回路径
*修改日期:2017-04-05
*说明:
*以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要,按照技术文档编写,并非一定要使用该代码。
*该代码仅供学习和研究支付宝接口使用,只是提供一个参考。
*/
public class AlipayConfig {
// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
public static String app_id = "APPID";
// 商户私钥,您的PKCS8格式RSA2私钥
public static String merchant_private_key = "上一部分你下载工具生成的秘钥中的应 用私钥";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
public static String alipay_public_key = "支付宝公钥";
// 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问(可以使用natapp内容穿透完成外网设置)
public static String notify_url = "http://nnxybi.natappfree.cc/front/alipay/notifyUrl";
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问(可以使用natapp内容穿透完成外网设置第四部分讲解natapp使用配置)
public static String return_url = "http://nnxybi.natappfree.cc/front/alipay/aliReturnPay";
// 签名方式
public static String sign_type = "RSA2";
// 字符编码格式
public static String charset = "utf-8";
// 支付宝网关
public static String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
// 支付宝网关
public static String log_path = "c:\\";
/**
* 写日志,方便测试(看网站需求,也可以改成把记录存入数据库)
* @param sWord 要写入日志里的文本内容
*/
public static void logResult(String sWord) {
FileWriter writer = null;
try {
writer = new FileWriter(log_path + "alipay_log_" + System.currentTimeMillis()+".txt");
writer.write(sWord);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
这个类其实很简单,就是存储了一些支付所需的公钥啊,私钥啊等参数,方便同意管理和使用。代码往下走AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();就是初始化一个阿里封装的request,后面几句就是设置这个request要传入的参数,把回调函数地址,外部订单好,金额都设置到request里面。然后执行alipayClient.pageExecute(alipayRequest).getBody();这个方法,即可向支付宝发起一个下单请求,支付宝返回给我们的是一个字符串,这个字符串实际上是一个form表单。现在,我们写个简单的方法来测试以下,代码如下:
package com.zdk.front.test;
import com.zdk.front.util.PayUtil;
/**
* @author zdk
* @date 2020/1/10 22:24
*/
public class Test {
public static void main(String[] args) {
String str = PayUtil.alipay("2313131", "0.01", "hehe", "haha");
System.out.println(str);
}
}
测试方式非常简单,不需要弄什么运行环境或者一大堆依赖,就写个main方法,调用我的PayUtil的alipay方法,传入自己随便编的参数,右键运行即可。如果基本的参数配置都没有错,那么你的控制台会打印出如下结果:
看到了吗,返回的结果实际上是一个表单加上一个javascript脚本,脚本的目的在于提交表单。所以在实际开发中,你只需要拿到这个返回结果,把结果传给前端,前端把用jquery把这段代码放入一个div即可。表单自动提交,然后你就会跳转到支付宝的支付页面了(第三部分最下面有我的代码,可以参考一下)。
如果测试报错:
java.lang.NoClassDefFoundError: org/bouncycastle/jce/provider/BouncyCastleProvider
解决方法:因为加入了jdk的第三方安全库,需要额外配置
1.下载bcprov-jdkxx-xxx.jar
2.将bcprov-jdkxx-xxx.jar放入 J A V A H O M E / j r e / l i b / e x t 下 3. 打 开 JAVA_HOME/jre/lib/ext下 3.打开 JAVAHOME/jre/lib/ext下3.打开JAVA_HOME/jre/lib/security下的java.security文件,在末尾加上
security.provider.x=org.bouncycastle.jce.provider.BouncyCastleProvider
那么到这里就完了吗?肯定还没有,支付完成后,支付宝会将支付结果推送到我们自己的业务系统中,因此我们需要为支付宝写个异步通知的接口。先来看代码:
package com.zdk.front.controller;
import com.zdk.front.enetity.AliReturnPayBean;
import com.zdk.front.util.JsonView;
import com.zdk.front.util.PayUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.hutool.core.util.IdUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author zdk
* @date 2020/2/29 22:41
*/
@Controller
@RequestMapping("/returnPay")
public class ReturnController {
private static Logger logger = LoggerFactory.getLogger(ReturnController.class);
/**
* 测试接口
*/
@RequestMapping(value = "/testPay", method = RequestMethod.POST)
@ResponseBody
public String test(){
String str = PayUtil.alipay(IdUtil.simpleUUID(), "18", IdUtil.simpleUUID(), "test");
return new JsonView(0,str).toString();
}
/**
* 测试接口
*/
@RequestMapping(value = "/notifyUrl")
@ResponseBody
public String notifyUrl(){
System.out.println("notifyUrl");
return new JsonView(0).toString();
}
/**
* 支付宝回调的接口
*
* @return
* @throws IOException
*/
@RequestMapping(value = "/aliReturnPay")
public void returnPay(HttpServletResponse response, AliReturnPayBean returnPay, HttpServletRequest req)
throws IOException {
response.setContentType("type=text/html;charset=UTF-8");
logger.info("****************************************支付宝的的回调函数被调用******************************");
if (!PayUtil.checkSign(req)) {
logger.info("****************************************验签失败*******************************************");
response.getWriter().write("failture");
return;
}
if (returnPay == null) {
logger.info("支付宝的returnPay返回为空");
response.getWriter().write("success");
return;
}
logger.info("支付宝的returnPay" + returnPay.toString());
if (returnPay.getTrade_status().equals("TRADE_SUCCESS")) {
logger.info("支付宝的支付状态为TRADE_SUCCESS");
//业务逻辑处理
}
response.getWriter().write("success");
}
}
首先需要注意的是,这个类只能被@Controller注释修饰,不能用@RestController,因为@RestController会将该类中所有方法的返回值自动转为json格式,而此处支付宝的返回值不需要json格式。这里因为线上环境不能debug,我打印了大量的日志以监控该方法的运行情况。在支付的回调方法中,首先要做的就是验证签名,所以刚进入方法首先调用了PayUtil.checkSign(req)这个验证签名的方法,我们来看看这个checkSign(req)里面是怎么写的:
/**
* 支付宝的验签方法
* @param req
* @return
*/
public static boolean checkSign(HttpServletRequest req) {
Map requestMap = req.getParameterMap();
Map paramsMap = new HashMap<>();
requestMap.forEach((key, values) -> {
String strs = "";
for(String value : values) {
strs = strs + value;
}
System.out.println(("key值为"+key+"value为:"+strs));
paramsMap.put(key, strs);
});
//调用SDK验证签名
try {
return AlipaySignature.rsaCheckV1(paramsMap, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
} catch (AlipayApiException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("*********************验签失败********************");
return false;
}
}
首先我们先看最下面: AlipaySignature.rsaCheckV1(paramsMap, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);这句话是调用支付宝给我们封装的验证签名的方法,右键指上去,会发现该方法的描述是这个样子的:
boolean com.alipay.api.internal.util.AlipaySignature.rsaCheckV1(Map params, String publicKey, String charset, String signType) throws AlipayApiException
看到Map
package com.zdk.front.enetity;
import java.io.Serializable;
/**
* @author zdk
* @date 2020/2/29 22:43
*/
public class AliReturnPayBean implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* 开发者的app_id
*/
private String app_id;
/**
* 商户订单号
*/
private String out_trade_no;
/**
* 签名
*/
private String sign;
/**
* 交易状态
*/
private String trade_status;
/**
* 支付宝交易号
*/
private String trade_no;
/**
* 交易的金额
*/
private String total_amount;
public String getTotal_amount() {
return total_amount;
}
public void setTotal_amount(String total_amount) {
this.total_amount = total_amount;
}
public String getApp_id() {
return app_id;
}
public void setApp_id(String app_id) {
this.app_id = app_id;
}
public String getOut_trade_no() {
return out_trade_no;
}
public void setOut_trade_no(String out_trade_no) {
this.out_trade_no = out_trade_no;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public String getTrade_status() {
return trade_status;
}
public void setTrade_status(String trade_status) {
this.trade_status = trade_status;
}
public String getTrade_no() {
return trade_no;
}
public void setTrade_no(String trade_no) {
this.trade_no = trade_no;
}
@Override
public String toString() {
return "AliReturnPayBean [app_id=" + app_id + ", out_trade_no=" + out_trade_no + ", sign=" + sign
+ ", trade_status=" + trade_status + ", trade_no=" + trade_no + "]";
}
}
将返回参数封装在实体bean中,这样springmvc会利用反射机制自动将相关参数映射进实体bean里面,省去了我们自己再去解析参数的麻烦。后面那句returnPay.getTrade_status().equals(“TRADE_SUCCESS”)是得到支付宝中trade_status这个参数,在支付宝的接口文档中,若这个参数等于TRADE_SUCCESS,则说明支付是成功的,然后我就会调用tbPaymentRecordsService.aliPaySuccess(returnPay);。这句代码就是具体的业务逻辑代码了,对成功返回的参数进行处理,由于每个人业务逻辑都不一样,我就不再赘述了。你们只需将这局代码替换成你们的业务逻辑代码即可。
这样,支付下单的流程就走通了。当然我还有一个退款的接口没讲。这个接口其实和下单接口大同小异,我也写了相关注释。就请各位自行阅读,我就不做解释了。
还有一个JsonView的返回工具类贴在下面:
package com.zdk.front.util;
import net.sf.json.JSONObject;
public class JsonView {
//错误代码 0-成功
private Integer errcode = 0;
// 消息
private String message;
// 数据
private Object data;
public static String render(Object data){
JsonView tmp = new JsonView(0, "success",data);
return JSONObject.fromObject(tmp).toString();
}
public static String render(Integer errcode){
JsonView tmp = new JsonView(errcode, "");
return JSONObject.fromObject(tmp).toString();
}
public static String render(Integer errcode, String message){
JsonView tmp = new JsonView(errcode, message);
return JSONObject.fromObject(tmp).toString();
}
public static String render(Integer errcode, String message, Object data){
JsonView tmp = new JsonView(errcode, message, data);
return JSONObject.fromObject(tmp).toString();
}
public JsonView(Integer errcode, String message, Object data) {
this.errcode = errcode;
this.message = message;
this.data = data;
}
public JsonView(Integer errcode, String message) {
this.errcode = errcode;
this.message = message;
}
public JsonView(Integer errcode) {
this.errcode = errcode;
}
public JsonView() {
}
public Integer getErrcode() {
return errcode;
}
public void setErrcode(Integer errcode) {
this.errcode = errcode;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String toString(){
return JSONObject.fromObject(this).toString();
}
}
这样东西应该就不少了,支付订单返回的那个表单我是这样写的,可以参考一下:
页面代码:
js代码:
function doComment() {
//付款
$.ajax({
url: "/front/returnPay/testPay",
type: 'POST',
dataType: 'json',
success: function (data) {
$("#test").html(data.message);
}
});
}
natapp内网穿透的使用,
进入natapp主页之后,根据自己的系统下载相应的客户端,我用地WIN7 64位,所以下载了WINDOWS 64的,如下图
下载之后解压,解压之后进入到目录里,会有一个natapp.exe文件。接下来要注册一个账号密码,注册好之后再natapp主页进行注册账号登陆,实名认证成功之后,购买免费隧道。
点击配置,进行简单的配置,将端口80改成8080(本地127.0.0.1或者localhost的默认端口,我自己的是8083端口,你的服务器也要用这个,当然你也可以改成你喜欢的,总之要对应),配置好了之后,可以进行登录了,现在双击natapp.exe运行起来,然后输入natapp -authtoken=你的authtoken(这个在你的隧道信息里有),就可以登录,然后会给你一个网址,这个网址就是你本地对外的网址(这个网址就是我在最上面配置notify_url的网址、return_url的网址)。
最后安利一个GUUID的工具类:
pom文件引入依赖:
cn.hutool
hutool-core
4.5.16
代码中就可以使用**IdUtil.simpleUUID()**方法自动产生UUID作为订单的订单编号了,这个方法在上面代码中有使用。
最后一点了:支付宝沙箱测试APP和账号要用官网提供的这个也是我踩到的坑,只能用给定的账号登陆操作,里面的余额可以自己设置:
那这次支付宝的调用就先写到这里,喜欢的小伙伴可以点波关注哦,有问题可以随时留言问我。