支付宝当面付功能实现
1、调用支付宝的api的技巧
当我们在集成第三方接口的时候,往往有很多的api和文档和demo。此时我们如何从这么多资料中获取我们所需要的集成的接口。个人认为有以下几方面:
(1)我们根据web项目对接数据库对接思路来思考:连接数据库-->获取数据库的数据(请求参数)-->数据库数据返回到前端,封装数据展示给前端。
(2)我们分清楚两块:谁调用谁(谁向谁传递参数):这一块主要是对三方平台的业务到底是如何实现的要清楚。
a:商户调用平台:己方(代码的编写者,分清自己的角色,这里比如我们的商城网站,我们是商户方),平台方(即我们需要调用的api的方)
b:平台调用商户。
(3)通常第三方平台会提供一些api的demo给我们,此时这是一个很好的参考例子,我们可以根据例子来进行代码的coding。
(4)将SDK集成到我们的项目,此时我们通过查阅第三方的SDK,我们看下第三方的SDK中给我们封装了哪些东西,有哪些工具类我们可以直接使用,有哪些内容这里面都已经做好了,我们只需调用即可。
2、支付宝当面付的功能
结合上面的描述,我们分析下当面付的功能实现:
(1)首先我们要对当面付的业务要熟悉:当面付即扫码支付,指用户打开支付宝钱包中的“扫一扫”功能,扫描商家展示在某收银场景下的二维码并进行支付的模式。该模式适用于线下实体店支付、面对面支付等场景。结合实际场景,我们在网站上购物,生成订单后,需要支付订单,此时网站上需要展示该订单的二维码信息,用户扫描二维码后付款,付款后网站的订单的一些状态进行改变。(三个角色:商户、用户、支付宝)
(2)流程如下:
(3)熟悉了业务之后,我们需要进行查看第三方平台的api文档。SDK和demo
我们先运行下支付宝的的demo看下是如何运作的,需要的参数是什么,返回信息是什么。这样子我们根据这些参数和返回信息我们再看API文档,这样子就比较容易了。
其实这里面主要就是;第三方的公共参数SDK中有没有封装;业务参数有哪些,我们需要哪些必须的参数;返回的信息是什么;
连接第三方(有没有验签)--》发送参数的到第三方后,第三方的返回信息是什么--》解析第三方的响应然后封装展示给前端
3、支付宝回调的理解
知乎上面有个有意思的解答:
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,
店员就打了你的电话,然后你接到电话后就到店里去取了货。
在这个例子里,你的电话号码就叫回调函数,
你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,
店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
其实简单理解:就是A调用了B,然后B又把信息返回给A,A使用了B的返回信息修改了自己的内容。就是回调。
其实就是A与B通信的一个中间函数,这样子来确保双方信息保持一致。
这一块自己琢磨了很久,一直都对于回调难以理解,通过这次项目,对这块有了深入的理解:
用户付款成功后,其实这块支付宝第三方已经封装了很多东西,这些支付信息第三方平台都收到了这些内容,但是我们作为商户并不知道用户是否付款成功,所以需要支付宝返回这些支付的信息,我们商户来改变订单状态库存等等。这一步最重要的是如何获取到支付宝返回的信息,如何验证它返回的参数到底是正确的,即非常重要的一步:验签,即验证支付宝返回的信息是不是我们这个订单的 支付信息。
所以简单来说:回调就是A调用了B,然后B返回信息给A,A需要验证B的信息,然后修改A的一些内容。这就是支付宝的回调。为什么叫回调呢,个人理解,就是我们调用了三方,三方那边的信息需要回传给我们使用,即三方回传了这些信息供我们调用。即回调。
4、代码实现
这里主要实现:创建二维码(支付宝使用的是google的zxing生成二维码),查询订单状态,支付宝回调功能。
主要写controller和service层:
package com.imooc.project.alipayController; import com.alipay.api.AlipayApiException; import com.alipay.api.internal.util.AlipaySignature; import com.alipay.demo.trade.config.Configs; import com.imooc.project.alipayService.IOrderService; import com.imooc.project.common.ServerResponse; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * 支付宝当面付功能模块 */ @RestController @Api(value = "订单支付功能模块编码",tags = {"imoocProject项目订单支付功能模块编码"}) public class OrderController { private static final Logger logger= LoggerFactory.getLogger(OrderController.class); @Autowired private IOrderService orderService; @RequestMapping(value = "pay.do",method = RequestMethod.GET) @ApiOperation("支付接口,获取生成的二维码链接") @ApiImplicitParam(name = "orderNo",value = "订单号",dataType = "Long",paramType = "query") public ServerResponse paydo(Long orderNo,HttpServletRequest request){ Integer userId=21; //这里是我们在项目中暂时生成的文件夹,名称为upload String path=request.getSession().getServletContext().getRealPath("upload"); ServerResponse response=orderService.paydo(userId,orderNo,path); return response; } @RequestMapping(value = "query_order_pay_status.do",method = RequestMethod.GET) @ApiOperation("查看订单支付状态") @ApiImplicitParam(name = "orderNo",value = "订单号",dataType = "Long",paramType = "query") public ServerResponse<Boolean> queryOrderStatus(Long orderNo){ Integer userId=21; ServerResponse response=orderService.payStatus(userId,orderNo); if(response.isSuccess()){ return ServerResponse.createBySuccess(true); } return ServerResponse.createBySuccess(false); } @RequestMapping(value = "alipay_callback.do",method = RequestMethod.GET) @ApiOperation("支付宝回调") public Object callback(HttpServletRequest request){ /*当收银台调用预下单请求API生成二维码展示给用户后,用户通过手机扫描二维码进行支付,支付宝会将该笔订单的变更信息,沿着商户调用预下单请求时所传入的通知地址主动推送给商户。即我们称为支付宝回调,回调是为了商户收到用户的支付信息,然后更新库存,更新订单状态*/ Map<String ,String[]> requestParams=request.getParameterMap(); Map<String ,String> params=new HashMap<>(); /*对Map进行遍历,获取key和value.遍历map集合的三种方式:* (1)map.keySet()获取key,然后根据key遍历value.也可以使用迭代器来遍历。 (2)通过Map.entrySet使用iterator遍历key和value(1、2种方式都可以通过迭代器来进行) (3)第三种:通过Map.entrySet遍历key和value(for (Map.Entry<Integer, String> entry : map.entrySet()), entry.getKey(),entry.getValue()) */ String valueStr=""; for(Iterator<String> iter=requestParams.keySet().iterator();iter.hasNext();){ String name=iter.next(); String[] values=requestParams.get(name); for(int i=0;i<=values.length;i++){ //这里是为了在构成的valueStr中间有逗号隔开,且最后一个末尾没有逗号 valueStr=(i==values.length-1? valueStr+values[i]:valueStr+values[i]+","); } params.put(name,valueStr); } logger.info("支付宝回调,sign:{},trade_status:{},参数:{}",params.get("sign"),params.get("trade_status"),params.toString()); logger.info("这时候从支付宝获取到了一些信息,收到支付宝异步回调:"); //这个时候回调已经拿到了即支付宝回传过来的信息以及到达商户这边,我们需要验证,此时要验证回调的正确性是否是支付宝发的,并且避免重复通知 /* 第一步: 在通知返回参数列表中,除去sign、sign_type两个参数外,凡是通知返回回来的参数皆是待验签的参数。 第二步: 将剩下参数进行url_decode, 然后进行字典排序,组成字符串,得到待签名字符串: 第三步: 将签名参数(sign)使用base64解码为字节码串。(有的支付宝的SDK已经封装了) 第四步: 使用RSA的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名。 第五步:需要严格按照如下描述校验通知数据的正确性。 * */ params.remove("sign_type"); try { Boolean alipayRsacheckv2=AlipaySignature.rsaCheckV2(params, Configs.getPublicKey(),"utf-8",Configs.getSignType()); if (!alipayRsacheckv2){ //这里取个反,当返回false的时候,直接返回 return ServerResponse.createByErrorMessage("非法请求验证不通过"); } //下面开始写业务逻辑:处理alipay回调的逻辑,这里判断订单是否已经支付了,更新订单状态这些逻辑 } catch (AlipayApiException e) { logger.error("支付宝验证异常",e); e.printStackTrace(); } //todo 验证各种数据 return orderService.aliCallback(params); } }
package com.imooc.project.alipayServiceImpl; import com.alipay.api.AlipayResponse; import com.alipay.api.response.AlipayTradePrecreateResponse; import com.alipay.demo.trade.model.ExtendParams; import com.alipay.demo.trade.model.GoodsDetail; import com.alipay.demo.trade.model.builder.AlipayTradePrecreateRequestBuilder; import com.alipay.demo.trade.model.result.AlipayF2FPrecreateResult; import com.alipay.demo.trade.service.AlipayTradeService; import com.alipay.demo.trade.service.impl.AlipayTradeServiceImpl; import com.alipay.demo.trade.utils.ZxingUtils; import com.imooc.project.alipayService.IOrderService; import com.imooc.project.common.Const; import com.imooc.project.common.ResponseCode; import com.imooc.project.common.ServerResponse; import com.imooc.project.entity.mmall_order; import com.imooc.project.entity.mmall_order_item; import com.imooc.project.entity.mmall_pay_info; import com.imooc.project.mapper.mmall_orderMapper; import com.imooc.project.mapper.mmall_order_itemMapper; import com.imooc.project.mapper.mmall_pay_infoMapper; import com.imooc.project.util.BigDecimalUtil; import com.imooc.project.util.DateUtil; import com.imooc.project.util.PropertiesUtil; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Created by Administrator on 2017/12/18. */ @Service public class OrderServiceImpl implements IOrderService { public static Logger log= LoggerFactory.getLogger(OrderServiceImpl.class); @Autowired private mmall_orderMapper orderMapper; @Autowired private mmall_order_itemMapper itemMapper; @Autowired private mmall_pay_infoMapper payInfoMapper; @Override public ServerResponse paydo(Integer userId, Long orderNo,String path) { Map<String ,String> model=new HashMap<>(); //调用支付宝当面付的接口,返回需要支付的二维码信息 if (userId==null && orderNo==null){ return ServerResponse.createByErrorCodeMessage(ResponseCode.ILLEGAL_ARGUMENT.getCode(),ResponseCode.ILLEGAL_ARGUMENT.getDesc()); } //查看该用户的订单是否存在,如果存在则执行当面付 log.info("查询该用户的订单是否存在"); mmall_order order=orderMapper.selectOrder(userId,orderNo); if (order==null){ return ServerResponse.createByErrorMessage("用户订单不存在,系统错误"); } model.put("orderNo",order.getOrderNo().toString()); log.info("查询该用户的订单存在,接入支付宝当面付功能"); // (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线, // 需保证商户系统端不能重复,建议通过数据库sequence生成, String outTradeNo = order.getOrderNo().toString(); // (必填) 订单标题,粗略描述用户的支付目的。如“xxx品牌xxx门店当面付扫码消费” String subject = new StringBuilder().append("慕课商城扫码支付").toString(); // (必填) 订单总金额,单位为元,不能超过1亿元 // 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】 String totalAmount = order.getPayment().toString(); // (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段 // 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】 String undiscountableAmount = "0"; // 卖家支付宝账号ID,用于支持一个签约账号下支持打款到不同的收款账号,(打款到sellerId对应的支付宝账号) // 如果该字段为空,则默认为与支付宝签约的商户的PID,也就是appid对应的PID String sellerId = "2088102170124280"; // 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元" String body = new StringBuilder().append("订单号为").append(outTradeNo).append("商品金额共").append(totalAmount).append("元").toString(); // 商户操作员编号,添加此参数可以为商户操作员做销售统计 String operatorId = "001"; // (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持 String storeId = "storeId"; // 业务扩展参数,目前可添加由支付宝分配的系统商编号(通过setSysServiceProviderId方法),详情请咨询支付宝技术支持 ExtendParams extendParams = new ExtendParams(); extendParams.setSysServiceProviderId("2088100200300400500"); // 支付超时,定义为120分钟 String timeoutExpress = "120m"; // 创建一个商品信息,参数含义分别为商品id(使用国标)、名称、单价(单位为分)、数量,如果需要添加商品类别,详见GoodsDetail log.info("获取该订单的订单明细"); List<mmall_order_item> orderItemList=itemMapper.selectItem(userId,orderNo); // 商品明细列表,需填写购买商品详细信息, List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>(); for (mmall_order_item item: orderItemList) { GoodsDetail detail= GoodsDetail.newInstance(item.getProductId().toString(),item.getProductName(), BigDecimalUtil.mul(item.getCurrentUnitPrice().doubleValue(),new Double(100)).longValue(),item.getQuantity()); goodsDetailList.add(detail); } // 创建扫码支付请求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 tradeService = new AlipayTradeServiceImpl.ClientBuilder().build(); AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder); switch (result.getTradeStatus()) { case SUCCESS: log.info("支付宝预下单成功: )"); AlipayTradePrecreateResponse response = result.getResponse(); dumpResponse(response); log.info("打印出二维码,显示到前台页面"); //这一步是如果文件目录不存在,则创建一个文件 File foder=new File(path); if (!foder.exists()){ foder.setWritable(true); foder.mkdir(); } //二维码生成路径,即二维码生成后保存在哪里 String qrPath = String.format(path+"/qr-%s.png", response.getOutTradeNo()); String qrFileName=String.format("qr-%s.png",response.getOutTradeNo()); //这里主要获取到二维码的图片,拿到二维码,这里使用的是google的zxing工具生成二维码 ZxingUtils.getQRCodeImge(response.getQrCode(), 256, qrPath); File targetFile=new File(path,qrFileName); //将二维码上传到服务器,这里就不用了,然后拼接url String url=qrPath; model.put("quPath",url); log.info("filePath:" + qrPath); //ZxingUtils.getQRCodeImge(response.getQrCode(), 256, filePath); return ServerResponse.createBySuccess(model); case FAILED: log.error("支付宝预下单失败!!!"); return ServerResponse.createByErrorMessage("支付宝预下单失败!!!"); case UNKNOWN: log.error("系统异常,预下单状态未知!!!"); return ServerResponse.createByErrorMessage("系统异常,预下单状态未知!!!"); default: log.error("不支持的交易状态,交易返回异常!!!"); return ServerResponse.createByErrorMessage("不支持的交易状态,交易返回异常!!!"); } } // 简单打印应答 private void dumpResponse(AlipayResponse response) { if (response != null) { log.info(String.format("code:%s, msg:%s", response.getCode(), response.getMsg())); if (StringUtils.isNotEmpty(response.getSubCode())) { log.info(String.format("subCode:%s, subMsg:%s", response.getSubCode(), response.getSubMsg())); } log.info("body:" + response.getBody()); } } //测试订单的支付状态 @Override public ServerResponse payStatus(Integer userId, Long orderNo) { if (userId==null && orderNo==null){ return ServerResponse.createByErrorCodeMessage(ResponseCode.ILLEGAL_ARGUMENT.getCode(),ResponseCode.ILLEGAL_ARGUMENT.getDesc()); } //查看该用户的订单是否存在,如果存在则执行当面付 log.info("查询该用户的订单是否存在"); mmall_order order=orderMapper.selectOrder(userId,orderNo); if (order==null){ return ServerResponse.createByErrorMessage("用户订单不存在,系统错误"); } //如果订单状态的值比付款的大,则都认为付款成功,直接返回成功 if(Const.OrderStatus.PAID.getCode()<=order.getStatus()){ return ServerResponse.createBySuccess(); }else{ return ServerResponse.createByError(); } } //alipay的回调,查询下订单的状态,是否已经支付,更新下支付状态等等,也就是说获取到支付宝的一些返回信息,商户需要修改这些信息 @Override public ServerResponse aliCallback(Map<String, String> params) { //得到支付宝的返回的信息,修改订单的状态,封装支付信息插入到数据库表中 String tradeNo=params.get("trade_no"); Long orderNo=Long.parseLong(params.get("out_trade_no")); String tradeStatus=params.get("trade_status"); mmall_order order=orderMapper.selectOrderNo(orderNo); if (order==null){ return ServerResponse.createByErrorMessage("非此商城订单"); } if (order.getStatus()>=Const.OrderStatus.PAID.getCode()){ //如果此时订单的状态大于已付款,则表示支付宝重复调用 return ServerResponse.createBySuccess("支付宝重复调用"); } //如果状态是付款成功,则修改状态为付款成功 if (Const.AlipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)){ order.setCreateTime(DateUtil.strToDate(params.get("gmt_payment"))); order.setStatus(Const.OrderStatus.PAID.getCode()); orderMapper.updateByPrimaryKeySelective(order); } //封装支付信息 mmall_pay_info payInfo=new mmall_pay_info(); payInfo.setOrderNo(orderNo); payInfo.setUserId(order.getUserId()); payInfo.setPlatformStatus(tradeStatus); payInfo.setPlatformNumber(tradeNo); payInfo.setPayPlatform(Const.PayPlatformEnum.ALIPAY.getCode()); payInfoMapper.insert(payInfo); return ServerResponse.createBySuccess(); } }
5、总结
通过这次与三方的对接,学到了以下内容:
(1)第三方文档的阅读流程
(2)如何阅读SDK的源码
(3)如何使用demo和SDK
(4)对支付宝回调有了一个比较好的理解。