支付宝对接学习笔记:
功能介绍:
- 支付宝对接
- 支付宝回调
- 查询支付状态(略过不讲)
要求:
- 熟悉支付宝对接核心文档,调通支付宝官方Demo
- 解析支付宝SDK对接源码
- RSA1和RSA2验证签名及加解密
- 避免支付宝的重复通知而加数据校验(略)
技巧:
- ngrok 外网穿透
- 生成二维码并持久化到图片服务器
调试完demo后,集合到开发项目。
把支付宝依赖的jar宝按照提供版本要求导入,sdk则放在web下lib文件夹下。然后在module的依赖中导入lib下的本地jar包(坑!!不然会报红)
那么为什么不统一使用pom导入呢?原因就是阿里没有提供该jar包的线上导入,只能本地导入。为了统一jar地址,所以必须先配置sdk的jar包的位置。(在这之前还要配置一个maven插件以加载本地jar包).
接下来简单梳理一遍流程:
一、登录进入蚂蚁金服
本次使用沙箱环境下进行整合,沙箱环境开发上线流程差别不大,和正式几乎是一致的,只是切换不同的APPID和支付宝网关。
二、下载官方的demo
这里选中java版的demo
选中idea导入。先在本地调通再集成到系统中去。
右键运行主函数会发现运行不了,那是因为我们还没有修改配置文件中设置。
对应配置如下。
那么问题来了怎么生成这些公钥私钥呢?前往这里根据系统下载对应的工具。
接着:
配置好配置文件后,运行一下:
运行没有问题,证明已经调通。下载沙箱版的支付宝,登录沙箱提供的买家账户,复制当面付二维码找一个二维码生成工具扫描支付看能不能成功。
扫描支付后:
到此为止本地支付宝已经调通,这个还是相对来说比较简单的。从demo的项目结构来看,这是一个web项目,可以自行配置运行环境再运行,测试会更加方便一点,如果没有出错的话就会出现下图:
三、系统对接支付宝支付接口
虽然官网已经写得很清楚了,但是第一次对接还是很吃力,这里写一下思路:
1、先把demo中的aplipay那个包及配置文件复制放到需要集成项目的类路径下:
2、把支付宝依赖的jar宝按照提供版本要求导入,sdk则放在web下lib文件夹下。然后在module的依赖中导入lib下的本地jar包(坑!!不然会报红)
那么为什么不统一使用pom导入呢?原因就是阿里没有提供该jar包的线上导入,只能本地导入。为了统一jar地址,所以必须先配置sdk的jar包的位置。
还要配置一个maven插件以加载本地jar包.
3、运行下主函数没有报错就是初步导入成功。
四、对接支付宝支付接口
1、这里是整个过程中最难的部分。
从下订单到支付到支付完成,省去下订单的接口,支付过程需要用到两个接口,一个是支付接口,一个是给支付宝授权回调接口。订单这里采用模拟数据。
2、首先支付接口
扫码支付调用流程:
官方文档参数描述:
因此先查询数据组装支付宝要求的参数值:
Map<String, String> resultMap = Maps.newHashMap();
Order order = orderMapper.selectByUserAndOrderNo(userId, orderNo);
if (order == null) {
return ServerRespond.createByErrorMessage("用户没有该订单");
}
resultMap.put("orderNo", String.valueOf(order.getOrderNo()));
// (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线,
// 需保证商户系统端不能重复,建议通过数据库sequence生成,
String outTradeNo = order.getOrderNo().toString();
// (必填) 订单标题,粗略描述用户的支付目的。如“xxx品牌xxx门店当面付扫码消费”
String subject = new StringBuilder().append("寸金在线商城,订单号:").append(outTradeNo).toString();
// (必填) 订单总金额,单位为元,不能超过1亿元
// 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】
String totalAmount = order.getPayment().toString();
// (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段
// 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】
String undiscountableAmount = "0";
// 卖家支付宝账号ID,用于支持一个签约账号下支持打款到不同的收款账号,(打款到sellerId对应的支付宝账号)
// 如果该字段为空,则默认为与支付宝签约的商户的PID,也就是appid对应的PID
String sellerId = "";
// 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元"
String body = new StringBuilder().append("订单").append(outTradeNo).append("购买商品共").append(totalAmount).append("元").toString();
// 商户操作员编号,添加此参数可以为商户操作员做销售统计
String operatorId = "test_operator_id";
// (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持
String storeId = "test_store_id";
// 业务扩展参数,目前可添加由支付宝分配的系统商编号(通过setSysServiceProviderId方法),详情请咨询支付宝技术支持
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId("2088100200300400500");
// 支付超时,定义为120分钟
String timeoutExpress = "120m";
// 商品明细列表,需填写购买商品详细信息,
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();
List<OrderItem> orderItemList = orderItemMapper.getByOrderNoUserId(orderNo, userId);
System.out.println(orderItemList.get(0));
for (OrderItem orderItem : orderItemList) {
GoodsDetail goods = GoodsDetail.newInstance(orderItem.getProductId().toString(), orderItem.getProductName().toString(),
BigDecimalUtil.mul(orderItem.getCurrentUnitPrice().doubleValue(), new Double(100).doubleValue()).longValue(), orderItem.getQuantity());
goodsDetailList.add(goods);
}
// // 创建一个商品信息,参数含义分别为商品id(使用国标)、名称、单价(单位为分)、数量,如果需要添加商品类别,详见GoodsDetail
// GoodsDetail goods1 = GoodsDetail.newInstance("goods_id001", "xxx小面包", 1000, 1);
// // 创建好一个商品后添加至商品明细列表
// goodsDetailList.add(goods1);
//
// // 继续创建并添加第一条商品信息,用户购买的产品为“黑人牙刷”,单价为5.00元,购买了两件
// GoodsDetail goods2 = GoodsDetail.newInstance("goods_id002", "xxx牙刷", 500, 2);
// goodsDetailList.add(goods2);
// 创建扫码支付请求builder,设置请求参数
AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
.setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
.setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
.setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
.setTimeoutExpress(timeoutExpress)
.setNotifyUrl(PropertiesUtil.getProperty("alipay.callback.url"))//支付宝服务器主动通知商户服务器里指定的页面http路径,根据需要设置
.setGoodsDetailList(goodsDetailList);
/** 一定要在创建AlipayTradeService之前调用Configs.init()设置默认参数
* Configs会读取classpath下的zfbinfo.properties文件配置信息,如果找不到该文件则确认该文件是否在classpath目录
*/
Configs.init("zfbinfo.properties");
/** 使用Configs提供的默认参数
* AlipayTradeService可以使用单例或者为静态成员对象,不需要反复new
*/
AlipayTradeService tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();
AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);
接着就是出参,二维码的生成,并展示给用户支付。图片展示通过上传到图片服务器的方式。所以前提得已经有一个ftp服务器和连接服务器的ftp工具类。
switch (result.getTradeStatus()) {
case SUCCESS:
log.info("支付宝预下单成功: )");
AlipayTradePrecreateResponse response = result.getResponse();
dumpResponse(response);
// 关键部分,把生成二维码上传到图片服务器
File folder = new File(path);
if (!folder.exists()) {
folder.setWritable(true);
folder.mkdirs();
}
// 需要修改为运行机器上的路径
//替换s占位符
String QRPath = String.format(path + "/qr-%s.png",
response.getOutTradeNo());
String qrFileName = String.format("qr-%s.png", response.getOutTradeNo());
//支付宝调用guava生成二维码
ZxingUtils.getQRCodeImge(response.getQrCode(), 256, QRPath);
File targetFile = new File(path, qrFileName);
try {
FTPUtil.uploadFile(Lists.newArrayList(targetFile));
} catch (IOException e) {
log.error("上传二维码异常", e);
}
log.info("QRPath:" + QRPath);
String qrUrl = PropertiesUtil.getProperty("ftp.server.http.prefix") + targetFile.getName();
resultMap.put("qrUrl", qrUrl);
return ServerRespond.createBySuccess(resultMap);
case FAILED:
log.error("支付宝预下单失败!!!");
return ServerRespond.createByErrorMessage("支付宝预下单失败");
case UNKNOWN:
log.error("系统异常,预下单状态未知!!!");
return ServerRespond.createByErrorMessage("系统异常,预下单状态未知!!!");
default:
log.error("不支持的交易状态,交易返回异常!!!");
return ServerRespond.createByErrorMessage("不支持的交易状态,交易返回异常!!!");
}
3、支付宝回调接口
这个授权支付宝调用的接口,所以不能是本地ip,必须得有一个外网ip,最直接的方式是服务器上操作,但显然现在是没办法这样做的,于是采用了内网穿透的办法,内网穿透工具我采用ngrok,缺点是不能绑定固定域名。
授权回调接口:
public ServerRespond aliCallback(Map<String, String> params) {
//处理回调数据
Long orderNo = Long.parseLong(params.get("out_trade_no"));
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
return ServerRespond.createByErrorMessage("寸金商场订单,回调忽略");
}
if (order.getStatus() >= Const.OrderStatus.PAID.getCode()) {
return ServerRespond.createBySuccess("支付宝重复调用");
}
if (Const.alipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)) {
order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
order.setStatus(Const.OrderStatus.PAID.getCode());
orderMapper.updateByPrimaryKeySelective(order);
}
PayInfo payInfo = new PayInfo();
payInfo.setUserId(order.getUserId());
payInfo.setOrderNo(order.getOrderNo());
payInfo.setPayPlatform(Const.PayPlatFormEnum.ALIPAY.getCode());
payInfo.setPlatformNumber(tradeNo);
payInfo.setPlatformStatus(tradeStatus);
payInfoMapper.insert(payInfo);
return ServerRespond.createBySuccess();
}
关于回调接口可以看看文档。
第一步: 在通知返回参数列表中,除去sign、sign_type两个参数外,凡是通知返回回来的参数皆是待验签的参数。
这一步很重要,不然没办法验签,看源码,便可知sdk已经做了,接着组装StringBuffer,因为StringBuffer是线程安全的,可以以应付高并发操作。
第三步: 将签名参数(sign)使用base64解码为字节码串。
这一步sdk也做了。
第四步: 使用RSA的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名。
然而需要注意的是上面这个方法实际上是不ok的,因为它的算法请求类型跟配置中的不一致。我们的请求算法类型是RSA2不是SHA1WithRSA。
然而还有一个函数重载允许多了一个可以选择加密类型的参数。
点击rsaCheck,当signType的值equal不同的值调用不同的方法,很明显第二个就是我们要的。
于是在控制器中就得这样写:
五、对接测试
完成接口编写后就是接口测试
从数据库中提一个未付款的订单号做测试
成功的话会返回一个付款二维码
打开该二维码:
如图则已经对接成功:
以上就是支付宝集成的主要过程,代码只是贴了一部分具体可以看这里。
六、总结
虽然官方的说明已经够详细了,但是真正入手去做还是有很多坑,此次对接过程中学习很多,其中尤其要注意的是因为官方的关系,sdk必须放在lib下,为了打包时能够同其他依赖包一起打包,还需配置好对sdk的打包插件,其他支付宝需要的依赖包如果用maven引入的话尽量保持版本一致或者一起跟支付宝sdk一同从Demo中复制过来放在lib包下。