SpringBoot集成支付宝沙箱手机网站支付详细流程和踩坑分享

描述

本文主要讲解SpringBoot集成支付宝沙箱手机网站支付,即网页点击按钮发起支付,跳转到沙箱app付款

由于其他博客的流程大多笼统,有时候并不能找到正确的集成方式,本文尽可能详细的阐述付款,异步通知,验签,退款的全部流程以及踩坑的分享,希望可以帮助你们少走弯路

必要的准备工作如公私钥,沙箱申请详见  支付宝沙箱简单集成

编译环境:IDEA

支付宝手机支付Demo

地址:https://docs.open.alipay.com/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.springframework.stereotype.Component;

@Data
@Component
public class AlipayConfig {
	// 沙箱appid
	public static String APPID = "2016101400683082";
	// 私钥 pkcs8格式的
	public static String RSA_PRIVATE_KEY = "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";
	// 请求网关  固定
	public static String URL = "https://openapi.alipaydev.com/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";
	// RSA2
	public static String SIGNTYPE = "RSA2";
}

这里说明一下

1.同步地址是支付成功后跳转的地址,由于我的业务逻辑是前端传给后台要跳转的地址,所以在这并未设置,根据需要配置

2.为什么有支付宝公钥和沙箱支付宝公钥那,demo中自身写的是支付宝公钥,然而和我的沙箱中写的支付宝公钥并不一样。但是使用demo的公钥下单并没有问题,但是在验签和退款时都会出错,要使用沙箱中的支付宝公钥。并没有尝试下单时也使用沙箱支付宝公钥,诸位可以试一试。这两个字符串长得很像,我一开始以为是一个!!!

3. 创建service层

同样根目录创建文件夹,后创建 AlipayService接口,分别写创建订单,异步通知和退款三个接口方法

public interface AlipayService {
    String create(String orderId, String returnUrl);

    //异步通知会返回一个request
    void 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/2019
 **/
@Service
@Slf4j
public class AlipayServiceImpl implements AlipayService {

    @Autowired
    private 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";

    @Override
    public 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;
    }

    @Override
    public 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;

    }

    @Override
    public 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.open.alipay.com/203/105286/

异步通知的主要作用就是验证付款之后,修改数据库的支付信息等

支付成功后跳转return_url的同时回将支付的信息异步发送到notify_url,可以是一个网页,但最好是controller中的一个路径,便于业务处理

跳转url以 http://xxx/?...的方式返回,?之后是携带的信息,完整的是

https://api.xx.com/receive_notify.htm?total_amount=2.00&buyer_id=2088102116773037&body=大乐透2.1&trade_no=2016071921001003030200089909&refund_fee=0.00&notify_time=2016-07-19 14:10:49&subject=大乐透2.1&sign_type=RSA2&charset=utf-8&notify_type=trade_status_sync&out_trade_no=0719141034-6418&gmt_close=2016-07-19 14:10:46&gmt_payment=2016-07-19 14:10:47&trade_status=TRADE_SUCCESS&version=1.0&sign=kPbQIjX+xQc8F0/A6/AocELIjhhZnGbcBN6G4MM/HmfWL4ZiHM6fWl5NQhzXJusaklZ1LFuMo+lHQUELAYeugH8LYFvxnNajOvZhuxNFbN2LhF0l/KL8ANtj8oyPM4NN7Qft2kWJTDJUpQOzCzNnV9hDxh5AaT9FPqRS6ZKxnzM=&gmt_create=2016-07-19 14:10:44&app_id=2015102700040153&seller_id=2088102119685838&notify_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/2019
 **/
@Controller
@RequestMapping("/alipay")
public class AlipayController {

    @Autowired
    private AlipayService alipayService;

    @GetMapping("/create")
    @ResponseBody
    public 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中先进行数据库的退款信息更改,然后

@Autowired
private AlipayService alipayService;

调用refund方法传入订单编号

踩坑分享

1. 前端点击按钮发起支付无法跳转页面支付

controller中创建订单一定要使用  @ResponseBody ,并return 通过service返回的结果,使返回结果是一个页面

2. 验签,退款异常

new DefaultAlipayClient() 时,其中传递的时沙箱中支付宝公钥而不是应用公钥(通过开发助手生成的),也不是demo中自带的支付宝公钥

3. 支付宝网关地址是固定的,一定不要改

有不妥之处,欢迎交流~~

发布了154 篇原创文章 · 获赞 55 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42089175/article/details/100747529