描述
本文主要讲解SpringBoot集成支付宝沙箱手机网站支付,即网页点击按钮发起支付,跳转到沙箱app付款
由于其他博客的流程大多笼统,有时候并不能找到正确的集成方式,本文尽可能详细的阐述付款,异步通知,验签,退款的全部流程以及踩坑的分享,希望可以帮助你们少走弯路
必要的准备工作如公私钥,沙箱申请详见支付宝沙箱简单集成
编译环境:IDEA
支付宝手机支付Demo
地址:https://docs./203/105910/
其中仅有一个信息配置类,其他的逻辑都集成在了,jsp页面中,个人感觉不是太友好
所以本文把逻辑集成到了service和controller中
流程
1. 添加Maven依赖
//支付宝SDK<dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.5.0.ALL</version></dependency>//不用写get和set方法的辅助依赖,可以根据需要选择不添加,那就要手动添加get()和set()方法//若仅添加依赖还是报错,需要在File-settings-Plugins中搜索添加,也可百度详细安装方式<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
2. 添加配置文件
为了实现模块化,看上去更专业(嘿嘿),在根目录(与Application同级)建立config文件夹,该文件夹下建立AlipayConfig.java文件
import lombok.Data;import org.ponent;@Data@Componentpublic class AlipayConfig {// 沙箱appidpublic static String APPID = "101400683082";// 私钥 pkcs8格式的public static String RSA_PRIVATE_KEY = "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";// 请求网关 固定public static String URL = "/gateway.do";//异步通知地址public static String notify_url = "http://xxx/alipay/notify";//同步地址//public static String return_url = "http://xxx/alipay/return";// 编码public static String CHARSET = "UTF-8";// 返回格式public static String FORMAT = "json";// 支付宝公钥public static String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjrEVFMOSiNJXaRNKicQuQdsREraftDA9Tua3WNZwcpeXeh8Wrt+V9JilLqSa7N7sVqwpvv8zWChgXhX/A96hEg97Oxe6GKUmzaZRNh0cZZ88vpkn5tlgL4mH/dhSr3Ip00kvM4rHq9PwuT4k7z1DpZAf1eghK8Q5BgxL88d0X07m9X96Ijd0yMkXArzD7jg+noqfbztEKoH3kPMRJC2w4ByVdweWUT2PwrlATpZZtYLmtDvUKG/sOkNAIKEMg3Rut1oKWpjyYanzDgS7Cg3awr1KPTl9rHCazk15aNYowmYtVabKwbGVToCAGK+qQ1gT3ELhkGnf3+h53fukNqRH+wIDAQAB";// 沙箱支付宝公钥public static String ZHIFUBAO_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArS0SLdzGCafgBwLv3IXIrr7cXKk2pzC5fgQxVz1M9F05ReSXQOfWfjDstNrToiI3kwY3XRGI/ULzywbpKwTm/IzOULxSnexCCJRxQonmQcV1C3ixsQi9rqkL0XaV7YBQl0DfmZoHKbbpmfj/7Uv9hQ2viJ/3n844bhaIwYhR7+Smu8xk+hbT0DEpp75cJV9pt+ngCHj6x3vkGHPj7w70JGKY73wkT6wBD0A7vz/cHHkMH6EeIkus1R5umd2rGXE/8zaPFRpysNKiys4ujAW7tOwCkqiuaon3AxGdQrHom3Twp+cpm7gkzc5v/p4qHgVUyZVKjrj6VJTRIgEQOegDtQIDAQAB";// RSA2public static String SIGNTYPE = "RSA2";}
这里说明一下
1.同步地址是支付成功后跳转的地址,由于我的业务逻辑是前端传给后台要跳转的地址,所以在这并未设置,根据需要配置
2.为什么有支付宝公钥和沙箱支付宝公钥那,demo中自身写的是支付宝公钥,然而和我的沙箱中写的支付宝公钥并不一样。但是使用demo的公钥下单并没有问题,但是在验签和退款时都会出错,要使用沙箱中的支付宝公钥。并没有尝试下单时也使用沙箱支付宝公钥,诸位可以试一试。这两个字符串长得很像,我一开始以为是一个!!!
3. 创建service层
同样根目录创建文件夹,后创建 AlipayService接口,分别写创建订单,异步通知和退款三个接口方法
public interface AlipayService {String create(String orderId, String returnUrl);//异步通知会返回一个requestvoid notify(HttpServletRequest request);void refund(String orderId);}
后创建Impl实现类 AlipayServiceImpl.java
import com.alipay.api.AlipayApiException;import com.alipay.api.AlipayClient;import com.alipay.api.DefaultAlipayClient;import com.alipay.api.domain.AlipayTradeRefundModel;import com.alipay.api.domain.AlipayTradeWapPayModel;import com.alipay.api.internal.util.AlipaySignature;import com.alipay.api.request.AlipayTradeRefundRequest;import com.alipay.api.request.AlipayTradeWapPayRequest;import com.imooc.config.AlipayConfig;import com.imooc.service.AlipayService;import com.imooc.utils.JsonUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;import java.util.HashMap;import java.util.Iterator;import java.util.Map;/*** @Author Sakura* @Date 9/9/**/@Service@Slf4jpublic class AlipayServiceImpl implements AlipayService {@Autowiredprivate AlipayConfig alipayConfig;//订单名称private static final String ORDER_NAME = "自定义订单名称";//和支付宝签约的产品码 固定值private static final String PRODUCTCODE = "QUICK_WAP_WAY";//支付成功标识(可退款的签约是TRADE_SUCCESS,不可退款的签约是TRADE_FINISHED)private static final String TRADE_SUCCESS = "TRADE_SUCCESS";@Overridepublic String create(String orderId, String returnUrl) {AlipayClient client = new DefaultAlipayClient(alipayConfig.URL, alipayConfig.APPID, alipayConfig.RSA_PRIVATE_KEY, alipayConfig.FORMAT, alipayConfig.CHARSET, alipayConfig.ALIPAY_PUBLIC_KEY,alipayConfig.SIGNTYPE);AlipayTradeWapPayRequest alipay_request=new AlipayTradeWapPayRequest();// 封装请求支付信息AlipayTradeWapPayModel model= new AlipayTradeWapPayModel();//订单编号,不可重复model.setOutTradeNo(orderId);//订单名称model.setSubject(ORDER_NAME);//订单金额model.setTotalAmount("0.01");//产品吗model.setProductCode(PRODUCTCODE);alipay_request.setBizModel(model);//支付成功后跳转的地址alipay_request.setReturnUrl(returnUrl);//异步通知地址alipay_request.setNotifyUrl(alipayConfig.notify_url);// form表单生产String result = "";try {// 调用SDK生成表单result = client.pageExecute(alipay_request).getBody();} catch (AlipayApiException e) {log.info("【支付宝支付】支付失败 error={}", e);}return result;}@Overridepublic void notify(HttpServletRequest request) {Map<String, String> map = new HashMap<>();Map<String, String[]> requestParams = request.getParameterMap();for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {String name = iter.next();String[] values = requestParams.get(name);String valueStr = "";for (int i = 0; i < values.length; i++) {valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";}map.put(name, valueStr);}//验证签名boolean signVerified = false;try {signVerified = AlipaySignature.rsaCheckV1(map, alipayConfig.ZHIFUBAO_PUBLIC_KEY, alipayConfig.CHARSET, alipayConfig.SIGNTYPE);} catch (com.alipay.api.AlipayApiException e) {log.info("[支付验证] 异常={}", JsonUtil.toJson(e));return;}if (signVerified) {//处理自己的业务逻辑}// log.info("[支付验证] 验证结果={}", signVerified);return;}@Overridepublic void refund(String orderId) {AlipayClient client = new DefaultAlipayClient(alipayConfig.URL, alipayConfig.APPID, alipayConfig.RSA_PRIVATE_KEY, alipayConfig.FORMAT, alipayConfig.CHARSET, alipayConfig.ZHIFUBAO_PUBLIC_KEY,alipayConfig.SIGNTYPE);AlipayTradeRefundRequest alipay_request = new AlipayTradeRefundRequest();AlipayTradeRefundModel model=new AlipayTradeRefundModel();//退款的订单Id,也可以设置流水号model.setOutTradeNo(orderId);//退款金额model.setRefundAmount("0.01");alipay_request.setBizModel(model);String alipay_response = "";try {alipay_response = client.execute(alipay_request).getBody();} catch (AlipayApiException e) {log.info("【支付宝支付】退款失败 error={}", e);}// log.info("[支付退款] response={}", alipay_response);}}
日志我使用的是Slf4j,也可以直接System.out.println()
以上代码下单,退款的部分是demo中的代码,拷贝改了一下,具体的支付等的注意事项在编写controller再详细阐述
这里重点说一下异步通知 官方文档:https://docs./203/105286/
异步通知的主要作用就是验证付款之后,修改数据库的支付信息等
支付成功后跳转return_url的同时回将支付的信息异步发送到notify_url,可以是一个网页,但最好是controller中的一个路径,便于业务处理
跳转url以 http://xxx/?...的方式返回,?之后是携带的信息,完整的是
/receive_notify.htm?total_amount=2.00&buyer_id=2088102116773037&body=大乐透2.1&trade_no=071921001003030200089909&refund_fee=0.00¬ify_time=-07-19 14:10:49&subject=大乐透2.1&sign_type=RSA2&charset=utf-8¬ify_type=trade_status_sync&out_trade_no=0719141034-6418&gmt_close=-07-19 14:10:46&gmt_payment=-07-19 14:10:47&trade_status=TRADE_SUCCESS&version=1.0&sign=kPbQIjX+xQc8F0/A6/AocELIjhhZnGbcBN6G4MM/HmfWL4ZiHM6fWl5NQhzXJusaklZ1LFuMo+lHQUELAYeugH8LYFvxnNajOvZhuxNFbN2LhF0l/KL8ANtj8oyPM4NN7Qft2kWJTDJUpQOzCzNnV9hDxh5AaT9FPqRS6ZKxnzM=&gmt_create=-07-19 14:10:44&app_id=102700040153&seller_id=2088102119685838¬ify_id=4a91b7a78a503640467525113fb7d8bg8e
所以我们用一个Map把key-value存储起来,也可以使用
@RequestParam("total_amout") String total_amout
的方式仅接收自己所需要的参数,因为很多参数都是多余的,看个人,闲麻烦就用Map
比较重要的信息有
out_trade_no 订单号
trade_status支付状态
total_amount订单金额
一般验证付款的严格流程是
使用AlipaySignature.rsaCheckV1验证订单是否正确out_trade_no订单号是否是后台支付时的订单号判断total_amount付款金额是否等于后台应付金额验证付款方是否是下单方
这里第四条看自己业务能不能支持他人代付
附我的map中的信息
4.创建controller
根目录创建controller文件夹,内创建 AlipayController.java
import com.imooc.exception.SellException;import com.imooc.service.AlipayService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import java.util.Map;/*** @Author Sakura* @Date 9/9/**/@Controller@RequestMapping("/alipay")public class AlipayController {@Autowiredprivate AlipayService alipayService;@GetMapping("/create")@ResponseBodypublic String create(@RequestParam("orderId") String orderId,@RequestParam("returnUrl") String returnUrl) {//发起支付String payUrl = alipayService.create(orderId,returnUrl);return payUrl;}/*** 支付宝异步通知*/@PostMapping("/notify")public void notify(HttpServletRequest request) {alipayService.notify(request);}}
1. 创建订单时,一定要使用 @ResponseBody ,并return 通过service返回的结果,因为前端点击按钮发起支付时,会先跳转至付款页面,后打开沙箱app付款
2. 退款的业务逻辑, 一般是自己的service中先进行数据库的退款信息更改,然后
@Autowiredprivate AlipayService alipayService;
调用refund方法传入订单编号
踩坑分享
1. 前端点击按钮发起支付无法跳转页面支付
controller中创建订单一定要使用 @ResponseBody ,并return 通过service返回的结果,使返回结果是一个页面
2. 验签,退款异常
newDefaultAlipayClient() 时,其中传递的时沙箱中支付宝公钥而不是应用公钥(通过开发助手生成的),也不是demo中自带的支付宝公钥
3. 支付宝网关地址是固定的,一定不要改
有不妥之处,欢迎交流~~