Spring boot集成支付宝沙箱支付

825 阅读13分钟

集成步骤

1.准备工作

  1. 进入支付宝沙箱环境 按提示生成RSA2(SHA256)密钥

  2. 获取alipay-trade-sdkjar包

    支付宝开放平台下载当面付Demo

    \TradePayDemo\WebRoot\WEB-INF\lib路径中获取jar包alipay-trade-sdk-20161215.jar

  3. 打包jar包进Maven仓库

    在jar包所在的目录使用以下命令打包

    mvn install:install-file -DgroupId=com.alipay -DartifactId=alipay-trade-sdk -Dversion=20161215 -Dpackaging=jar -Dfile=alipay-trade-sdk-20161215.jar
    

2.引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.9.106.ALL</version>
</dependency>
<!--本地依赖-->
<dependency>
    <groupId>com.alipay</groupId>
    <artifactId>alipay-trade-sdk</artifactId>
    <version>20161215</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--实现二维码扫描-->
<!-- https://mvnrepository.com/artifact/com.google.zxing/core -->
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/commons-lang/commons-lang -->
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

3. 配置application.yml

server:
  port: 8081

# 沙箱配置
alipay:
  gatewayUrl: https://openapi.alipaydev.com/gateway.do #支付宝网关
  appid: 2016101100659869 #沙箱的APPID
  privateKey: *** #填写支付宝开放平台开发助手生成的应用私钥
  alipayPublicKey: *** #RSA2(SHA256)密钥里的支付宝公钥
  returnUrl: http://dqidj6.natappfree.cc/alipay/return #同步回调地址 可以使用natapp内网穿透
  notifyUrl: http://dqidj6.natappfree.cc/alipay/notify #异步回调地址

spring:
  thymeleaf:
    prefix: classpath:/templates/ #设置thymeleaf页面的存储路径
    suffix: .html #设置thymeleaf页面的后缀
    encoding: UTF-8 #设置thymeleaf页面的编码
    cache: false #关闭 thymeleaf 的缓存开发过程中无需重启
    servlet:
      content-type: text/html

4.读取配置文件

/**
 * 支付宝支付的参数配置
 *
 * @author manaphy
 */
@Data
@Slf4j
@ConfigurationProperties(prefix = "alipay")
public class AlipayProperties {

    /**
     * 支付宝gatewayUrl
     */
    private String gatewayUrl;
    /**
     * 商户应用id
     */
    private String appid;
    /**
     * RSA私钥,用于对商户请求报文加签
     */
    private String privateKey;
    /**
     * 支付宝RSA公钥,用于验签支付宝应答
     */
    private String alipayPublicKey;
    /**
     * 签名类型
     */
    private String signType = "RSA2";

    /**
     * 格式
     */
    private String format = "json";
    /**
     * 编码
     */
    private String charset = "UTF-8";

    /**
     * 同步地址
     */
    private String returnUrl;

    /**
     * 异步地址
     */
    private String notifyUrl;

    /**
     * 最大查询次数
     */
    private static int maxQueryRetry = 5;
    /**
     * 查询间隔(毫秒)
     */
    private static long queryDuration = 5000;
    /**
     * 最大撤销次数
     */
    private static int maxCancelRetry = 3;
    /**
     * 撤销间隔(毫秒)
     */
    private static long cancelDuration = 3000;

    private AlipayProperties() {
    }

    /**
     * PostConstruct是spring框架的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
     */
    @PostConstruct
    public void init() {
        log.info(description());
    }

    public String description() {

        return "\nConfigs{" + "支付宝网关: " + gatewayUrl + "\n" +
                ", appid: " + appid + "\n" +
                ", 商户RSA私钥: " + getKeyDescription(privateKey) + "\n" +
                ", 支付宝RSA公钥: " + getKeyDescription(alipayPublicKey) + "\n" +
                ", 签名类型: " + signType + "\n" +
                ", 查询重试次数: " + maxQueryRetry + "\n" +
                ", 查询间隔(毫秒): " + queryDuration + "\n" +
                ", 撤销尝试次数: " + maxCancelRetry + "\n" +
                ", 撤销重试间隔(毫秒): " + cancelDuration + "\n" +
                "}";
    }

    private String getKeyDescription(String key) {
        int showLength = 6;
        if (!StringUtils.isEmpty(key) && key.length() > showLength) {
            return key.substring(0, showLength) + "******" +
                    key.substring(key.length() - showLength);
        }
        return null;
    }
}

5.配置支付宝

/**
 * 支付宝配置
 * <p>
 * 两个支付宝客户端,用户可以使用任意一个
 * alipay-trade-sdk 是对alipay-sdk-java的封装,建议使用alipay-trade-sdk.
 *
 * @author Manaphy
 */
@Configuration
@EnableConfigurationProperties(AlipayProperties.class)
public class AlipayConfig {

    @Resource
    private AlipayProperties properties;

    /**
     * 支付宝客户端
     * alipay-sdk-java
     *
     * @return {@link AlipayClient}
     */
    @Bean
    public AlipayClient alipayClient() {
        return new DefaultAlipayClient(properties.getGatewayUrl(),
                properties.getAppid(),
                properties.getPrivateKey(),
                properties.getFormat(),
                properties.getCharset(),
                properties.getAlipayPublicKey(),
                properties.getSignType());
    }

    /**
     * 支付宝交易服务
     * alipay-trade-sdk
     *
     * @return {@link AlipayTradeService}
     */
    @Bean
    public AlipayTradeService alipayTradeService() {
        return new AlipayTradeServiceImpl.ClientBuilder()
                .setGatewayUrl(properties.getGatewayUrl())
                .setAppid(properties.getAppid())
                .setPrivateKey(properties.getPrivateKey())
                .setAlipayPublicKey(properties.getAlipayPublicKey())
                .setSignType(properties.getSignType())
                .build();
    }
}

6.支付工具

生成二维码图片用于当面付

/**
 * 支付工具
 *
 * @author Manaphy
 */
@Slf4j
public class PayUtil {

    /**
     * 根据url生成二位图片对象
     *
     * @param codeUrl url的代码
     * @return {@link BufferedImage}
     * @throws WriterException 写入异常
     */
    public static BufferedImage getQrCodeImage(String codeUrl) throws WriterException {
        Map<EncodeHintType, Object> hints = new Hashtable(2);
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
        hints.put(EncodeHintType.CHARACTER_SET, "UTF8");
        int width = 256;
        int height = 256;
        BitMatrix bitMatrix = (new MultiFormatWriter()).encode(codeUrl, BarcodeFormat.QR_CODE, width, height, hints);
        BufferedImage image = new BufferedImage(width, height, 1);
        for (int x = 0; x < width; ++x) {
            for (int y = 0; y < width; ++y) {
                image.setRGB(x, y, bitMatrix.get(x, y) ? -16777216 : -1);
            }
        }
        return image;
    }
}

7.支付宝通用接口

包含了通用的 同步通知 异步通知 订单查询 退款 退款查询 关闭交易 查询账单

/**
 * 支付宝通用接口
 *
 * @author Manaphy
 */
@Slf4j
@Controller
@RequestMapping("/alipay")
@SuppressWarnings("unused")
public class AlipayController {

    @Resource
    private AlipayProperties aliPayProperties;

    @Resource
    private AlipayTradeService alipayTradeService;

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private AlipayController alipayController;


    /**
     * 支付宝同步通知
     *
     * @param request  请求
     * @param response 响应
     */
    @RequestMapping("/return")
    public String callback(HttpServletRequest request, HttpServletResponse response) {
        log.info("------进入alipay同步通知------");
        boolean verifyResult = alipayController.rsaCheckV1(request);
        if (verifyResult) {
            Enumeration<String> enu = request.getParameterNames();
            while (enu.hasMoreElements()) {
                String paraName = enu.nextElement();
                System.out.println(paraName + ": " + request.getParameter(paraName));
            }
            return "success";
        }
        return "fail";
    }
    

    /**
     * 支付异步通知
     * <p>
     * 接收到异步通知并验签通过后,一定要检查通知内容,包括通知中的app_id、out_trade_no、total_amount是否与请求中的一致,
     * 并根据trade_status进行后续业务处理。
     * <p>
     * https://docs.open.alipay.com/194/103296
     *
     * @param request 请求
     * @return {@link String}
     */
    @RequestMapping("/notify")
    public String notify(HttpServletRequest request) {
        log.info("------进入alipay异步通知------");
        Map<String, String[]> parameterMap = request.getParameterMap();
        StringBuilder notifyBuild = new StringBuilder();
        parameterMap.forEach((key, value) -> notifyBuild.append(key).append("=").append(value[0]).append("\n"));
        log.info(notifyBuild.toString());
        // 一定要验签,防止黑客篡改参数
        boolean flag = this.rsaCheckV1(request);
        if (flag) {
            /*
             * TODO 需要严格按照如下描述校验通知数据的正确性
             *
             * 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
             * 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
             * 同时需要校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email),
             *
             * 上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。
             * 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。
             * 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时,支付宝才会认定为买家付款成功。
             */

            //交易状态
            String tradeStatus = new String(request.getParameter("trade_status").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // 商户订单号
            String outTradeNo = new String(request.getParameter("out_trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            //支付宝交易号
            String tradeNo = new String(request.getParameter("trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            //付款金额
            String totalAmount = new String(request.getParameter("total_amount").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
            // TRADE_FINISHED(表示交易已经成功结束,并不能再对该交易做后续操作);
            // TRADE_SUCCESS(表示交易已经成功结束,可以对该交易做后续操作,如:分润、退款等);
            if ("TRADE_FINISHED".equals(tradeStatus)) {
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(outTradeNo)在商户网站的订单系统中查到该笔订单的详细,
                // 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
                //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
                //如果有做过处理,不执行商户的业务程序

                //注意:
                //如果签约的是可退款协议,退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
                //如果没有签约可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
            } else if ("TRADE_SUCCESS".equals(tradeStatus)) {
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(outTradeNo)在商户网站的订单系统中查到该笔订单的详细,
                // 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
                //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
                //如果有做过处理,不执行商户的业务程序
                //注意:
                //如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。
            }
            return "success";
        }
        return "fail";
    }

    /**
     * 订单查询(最主要用于查询订单的支付状态)
     *
     * @param orderNo 商户订单号
     * @return {@link String}
     */
    @GetMapping("/query")
    @ResponseBody
    public String query(String orderNo) {
        AlipayTradeQueryRequestBuilder builder = new AlipayTradeQueryRequestBuilder().setOutTradeNo(orderNo);
        AlipayF2FQueryResult result = alipayTradeService.queryTradeResult(builder);
        switch (result.getTradeStatus()) {
            case SUCCESS:
                log.info("查询返回该订单支付成功!!!");
                AlipayTradeQueryResponse resp = result.getResponse();
                log.info(resp.getTradeStatus());
                break;
            case FAILED:
                log.error("查询返回该订单支付失败!!!");
                break;
            case UNKNOWN:
                log.error("系统异常,订单支付状态未知!!!");
                break;
            default:
                log.error("不支持的交易状态,交易返回异常!!!");
                break;
        }
        return result.getResponse().getBody();
    }

    /**
     * 退款
     *
     * @param orderNo 商户订单号
     * @return {@link String}
     */
    @PostMapping("/refund")
    @ResponseBody
    public String refund(String orderNo, String amount) throws AlipayApiException {
        AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();
        AlipayTradeRefundModel model = new AlipayTradeRefundModel();
        // 商户订单号
        model.setOutTradeNo(orderNo);
        // 退款金额
        model.setRefundAmount(amount);
        // 退款原因
        model.setRefundReason("无理由退货");
        // 退款订单号(同一个订单可以分多次部分退款,当分多次时必传)
        //model.setOutRequestNo(UUID.randomUUID().toString());

        alipayRequest.setBizModel(model);
        AlipayTradeRefundResponse alipayResponse = alipayClient.execute(alipayRequest);
        return alipayResponse.getBody();
    }

    /**
     * 退款查询
     *
     * @param orderNo       商户订单号
     * @param refundOrderNo 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的外部订单号
     * @return {@link String}
     * @throws AlipayApiException 支付宝api异常
     */
    @GetMapping("/refundQuery")
    @ResponseBody
    public String refundQuery(String orderNo, String refundOrderNo) throws AlipayApiException {
        AlipayTradeFastpayRefundQueryRequest alipayRequest = new AlipayTradeFastpayRefundQueryRequest();
        AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
        model.setOutTradeNo(orderNo);
        model.setOutRequestNo(refundOrderNo);
        alipayRequest.setBizModel(model);

        AlipayTradeFastpayRefundQueryResponse alipayResponse = alipayClient.execute(alipayRequest);
        return alipayResponse.getBody();
    }

    /**
     * 关闭交易
     *
     * @param orderNo 商户订单号
     * @return {@link String}
     * @throws AlipayApiException 支付宝api异常
     */
    @PostMapping("/close")
    @ResponseBody
    public String close(String orderNo) throws AlipayApiException {
        AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest();
        AlipayTradeCloseModel model = new AlipayTradeCloseModel();
        model.setOutTradeNo(orderNo);
        alipayRequest.setBizModel(model);
        AlipayTradeCloseResponse alipayResponse = alipayClient.execute(alipayRequest);
        return alipayResponse.getBody();
    }


    /**
     * 查询账单
     * billDate : 账单时间:日账单格式为yyyy-MM-dd,月账单格式为yyyy-MM。
     * 查询对账单下载地址: https://docs.open.alipay.com/api_15/alipay.data.dataservice.bill.downloadurl.query/
     *
     * @param billDate 账单日期
     */
    @GetMapping("/bill")
    @ResponseBody
    public void queryBill(String billDate) {
        // 1. 查询对账单下载地址
        AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
        AlipayDataDataserviceBillDownloadurlQueryModel model = new AlipayDataDataserviceBillDownloadurlQueryModel();
        model.setBillType("trade");
        model.setBillDate(billDate);
        request.setBizModel(model);
        try {
            AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
            if (response.isSuccess()) {
                String billDownloadUrl = response.getBillDownloadUrl();
                System.out.println(billDownloadUrl);

                // 2. 下载对账单
                List<String> orderList = this.downloadBill(billDownloadUrl);
                System.out.println(orderList);
                // 3. 先比较支付宝的交易合计/退款合计笔数/实收金额是否和自己数据库中的数据一致,如果不一致证明有异常,再具体找出那些订单有异常
                // 查找支付宝支付成功而自己支付失败的记录和支付宝支付失败而自己认为支付成功的异常订单记录到数据库
            } else {
                // 失败
                String code = response.getCode();
                String msg = response.getMsg();
                String subCode = response.getSubCode();
                String subMsg = response.getSubMsg();
            }
        } catch (AlipayApiException | IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 下载下来的是一个【账号_日期.csv.zip】文件(zip压缩文件名,里面有多个.csv文件)
     * 账号_日期_业务明细 : 支付宝业务明细查询
     * 账号_日期_业务明细(汇总):支付宝业务汇总查询
     * <p>
     * 注意:如果数据量比较大,该方法可能需要更长的执行时间
     *
     * @param billDownLoadUrl 比尔转载网址
     * @return {@link List<String>}
     * @throws IOException IOException
     */
    private List<String> downloadBill(String billDownLoadUrl) throws IOException {
        String ordersStr = "";
        CloseableHttpClient httpClient = HttpClients.createDefault();
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(60000)
                .setConnectionRequestTimeout(60000)
                .setSocketTimeout(60000)
                .build();
        HttpGet httpRequest = new HttpGet(billDownLoadUrl);
        httpRequest.setConfig(config);
        CloseableHttpResponse response = null;
        byte[] data;
        try {
            response = httpClient.execute(httpRequest);
            HttpEntity entity = response.getEntity();
            data = EntityUtils.toByteArray(entity);
        } finally {
            assert response != null;
            response.close();
            httpClient.close();
        }
        try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(data), Charset.forName("GBK"))) {
            ZipEntry zipEntry;
            while ((zipEntry = zipInputStream.getNextEntry()) != null) {
                try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
                    String name = zipEntry.getName();
                    // 只要明细不要汇总
                    if (name.contains("汇总")) {
                        continue;
                    }
                    byte[] byteBuff = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = zipInputStream.read(byteBuff)) != -1) {
                        byteArrayOutputStream.write(byteBuff, 0, bytesRead);
                    }
                    ordersStr = byteArrayOutputStream.toString("GBK");
                } finally {
                    zipInputStream.closeEntry();
                }
            }
        }

        if ("".equals(ordersStr)) {
            return null;
        }
        String[] bills = ordersStr.split("\r\n");
        List<String> billList = Arrays.asList(bills);
        return billList.parallelStream().map(item -> item.replace("\t", "")).collect(Collectors.toList());
    }

    /**
     * 校验签名
     *
     * @param request 请求
     * @return boolean
     */
    public boolean rsaCheckV1(HttpServletRequest request) {
        // https://docs.open.alipay.com/54/106370
        // 获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<>(1);
        Map requestParams = request.getParameterMap();
        for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            params.put(name, valueStr);
        }
        try {
            return AlipaySignature.rsaCheckV1(params, aliPayProperties.getAlipayPublicKey(), aliPayProperties.getCharset(), aliPayProperties.getSignType());
        } catch (AlipayApiException e) {
            log.debug("verify sign error, exception is:", e);
            return false;
        }
    }
}

接入各种支付场景

1.支付宝当面付(线下扫码付)

包含 商户扫码枪扫码支付 自己扫码支付

/**
 * 支付宝面对面支付控制器
 *
 * @author Manaphy
 */
@Slf4j
@RestController
@RequestMapping("/alipay/f2fPay")
public class AlipayFace2FaceController {

    @Resource
    private AlipayProperties aliPayProperties;

    @Resource
    private AlipayTradeService alipayTradeService;

    /**
     * 当面付-条码付
     * <p>
     * 商家使用扫码工具(扫码枪等)扫描用户支付宝的付款码
     *
     * @param authCode 支付宝付款码
     * @return {@link String}
     */
    @PostMapping("/barCodePay")
    public String barCodePay(String authCode) {
        // 实际使用时需要根据商品id查询商品的基本信息并计算价格(可能还有什么优惠),这里只是写死值来测试

        // (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线,
        String outTradeNo = UUID.randomUUID().toString();

        // (必填) 订单标题,粗略描述用户的支付目的。如“喜士多(浦东店)消费”
        String subject = "测试订单";

        // 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元"
        String body = "购买商品2件共5.05元";

        // (必填) 订单总金额,单位为元,不能超过1亿元
        // 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】
        String totalAmount = "5.05";

        // (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段
        // 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】
//        String nonDiscountableAmount = "";

        // (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持
        String storeId = "test_store_id";

        // 商户操作员编号,添加此参数可以为商户操作员做销售统计
        String operatorId = "test_operator_id";


        // 商品明细列表,需填写购买商品详细信息,
        List<GoodsDetail> goodsDetailList = new ArrayList<>();
        GoodsDetail goods1 = GoodsDetail.newInstance("goods_id001", "全麦小面包", 1, 1);
        goodsDetailList.add(goods1);
        GoodsDetail goods2 = GoodsDetail.newInstance("goods_id002", "黑人牙刷", 1, 2);
        goodsDetailList.add(goods2);

        // 支付超时,线下扫码交易定义为5分钟
        String timeoutExpress = "5m";

        AlipayTradePayRequestBuilder builder = new AlipayTradePayRequestBuilder()
                .setOutTradeNo(outTradeNo)
                .setSubject(subject)
                .setBody(body)
                .setTotalAmount(totalAmount)
                .setAuthCode(authCode)
                .setTotalAmount(totalAmount)
                .setStoreId(storeId)
                .setOperatorId(operatorId)
                .setGoodsDetailList(goodsDetailList)
                .setTimeoutExpress(timeoutExpress);

        // 当面付,面对面付,face to face pay -> face 2 face pay -> f2f pay
        // 同步返回支付结果
        AlipayF2FPayResult f2fPayResult = alipayTradeService.tradePay(builder);
        // 注意:一定要处理支付的结果,因为不是每次支付都一定会成功,可能会失败
        switch (f2fPayResult.getTradeStatus()) {
            case SUCCESS:
                log.info("支付宝支付成功: )");
                break;
            case FAILED:
                log.error("支付宝支付失败!!!");
                break;
            case UNKNOWN:
                log.error("系统异常,订单状态未知!!!");
                break;
            default:
                log.error("不支持的交易状态,交易返回异常!!!");
                break;
        }
        return f2fPayResult.getResponse().getBody();
    }

    /**
     * 当面付-扫码付
     * <p>
     * 扫码支付,指用户打开支付宝钱包中的“扫一扫”功能,扫描商户针对每个订单实时生成的订单二维码,并在手机端确认支付。
     * <p>
     * 发起预下单请求,同步返回订单二维码
     * <p>
     * 适用场景:商家获取二维码展示在屏幕上,然后用户去扫描屏幕上的二维码
     *
     * @param response 响应
     * @throws Exception 异常
     */
    @GetMapping("/preCreate")
    public void preCreate(HttpServletResponse response) throws Exception {
        // 实际使用时需要根据商品id查询商品的基本信息并计算价格(可能还有什么优惠),这里只是写死值来测试

        // (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线,
        String outTradeNo = UUID.randomUUID().toString();
        log.info(outTradeNo);
        // (必填) 订单标题,粗略描述用户的支付目的。如“喜士多(浦东店)消费”
        String subject = "测试订单";

        // 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元"
        String body = "购买商品2件共2.05元";

        // (必填) 订单总金额,单位为元,不能超过1亿元
        // 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】
        String totalAmount = "2.05";

        // (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段
        // 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】
        String nonDiscountableAmount = "";

        // 卖家支付宝账号ID,用于支持一个签约账号下支持打款到不同的收款账号,(打款到sellerId对应的支付宝账号)
        // 如果该字段为空,则默认为与支付宝签约的商户的PID,也就是appid对应的PID
        String sellerId = "";

        // (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持
        String storeId = "test_store_id";

        // 商户操作员编号,添加此参数可以为商户操作员做销售统计
        String operatorId = "test_operator_id";

        // 商品明细列表,需填写购买商品详细信息,
        List<GoodsDetail> goodsDetailList = new ArrayList<>();
        GoodsDetail goods1 = GoodsDetail.newInstance("goods_id003", "高露洁牙膏", 1, 1);
        goodsDetailList.add(goods1);
        GoodsDetail goods2 = GoodsDetail.newInstance("goods_id004", "黑人牙刷", 1, 2);
        goodsDetailList.add(goods2);

        // 支付超时,线下扫码交易定义为5分钟
        String timeoutExpress = "5m";

        AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
                .setSubject(subject)
                .setTotalAmount(totalAmount)
                .setOutTradeNo(outTradeNo)
                .setUndiscountableAmount(nonDiscountableAmount)
                .setSellerId(sellerId)
                .setBody(body)
                .setOperatorId(operatorId)
                .setStoreId(storeId)
                .setTimeoutExpress(timeoutExpress)
                //支付宝服务器主动通知商户服务器里指定的页面http路径,根据需要设置
                .setNotifyUrl(aliPayProperties.getNotifyUrl())
                .setGoodsDetailList(goodsDetailList);

        AlipayF2FPrecreateResult result = alipayTradeService.tradePrecreate(builder);
//        String qrCodeUrl = null;
        switch (result.getTradeStatus()) {
            case SUCCESS:
                log.info("支付宝预下单成功: )");
                AlipayTradePrecreateResponse res = result.getResponse();
                BufferedImage image = PayUtil.getQrCodeImage(res.getQrCode());

                response.setContentType("image/jpeg");
                response.setHeader("Pragma", "no-cache");
                response.setHeader("Cache-Control", "no-cache");
                response.setIntHeader("Expires", -1);
                ImageIO.write(image, "JPEG", response.getOutputStream());
                break;
            case FAILED:
                log.error("支付宝预下单失败!!!");
                break;
            case UNKNOWN:
                log.error("系统异常,预下单状态未知!!!");
                break;
            default:
                log.error("不支持的交易状态,交易返回异常!!!");
                break;
        }
    }
}

2.支付宝网页支付

/**
 * 支付宝网页支付控制器
 *
 * @author Manaphy
 */
@Controller
@RequestMapping("/alipay/web")
public class AlipayWebPayController {

    @Resource
    private AlipayProperties aliPayProperties;

    @Resource
    private AlipayClient alipayClient;

    @PostMapping("/pay")
    public void testPay(HttpServletResponse response) {
        try {
            //订单模型
            String productCode = "FAST_INSTANT_TRADE_PAY";
            AlipayTradePagePayModel model = new AlipayTradePagePayModel();
            model.setOutTradeNo(UUID.randomUUID().toString());
            model.setSubject("测试网页支付");
            model.setTotalAmount("5");
            model.setBody("网页支付测试,共5元");
            model.setTimeoutExpress("2m");
            model.setProductCode(productCode);

            AlipayTradePagePayRequest pagePayRequest = new AlipayTradePagePayRequest();
            pagePayRequest.setReturnUrl(aliPayProperties.getReturnUrl());
            pagePayRequest.setNotifyUrl(aliPayProperties.getNotifyUrl());
            pagePayRequest.setBizModel(model);

            // 调用SDK生成表单, 并直接将完整的表单html输出到页面
            String retStr = alipayClient.pageExecute(pagePayRequest).getBody();
            response.setContentType("text/html; charset=utf-8");
            response.getWriter().write(retStr);
            response.getWriter().flush();
            response.getWriter().close();
        } catch (AlipayApiException | IOException e) {
            e.printStackTrace();
        }
    }
}

3.支付宝手机支付

/**
 * 支付宝手机支付控制器
 *
 * @author Manaphy
 */
@Controller
@Slf4j
@RequestMapping("/alipay/wap")
public class AlipayWapPayController {

    @Resource
    private AlipayProperties aliPayProperties;

    @Resource
    private AlipayClient alipayClient;

    @PostMapping("/pay")
    public void testPay(HttpServletResponse response) {
        try {
            //订单模型
            String productCode = "QUICK_WAP_WAY";
            AlipayTradePagePayModel model = new AlipayTradePagePayModel();
            model.setOutTradeNo(UUID.randomUUID().toString());
            model.setSubject("手机支付测试");
            model.setTotalAmount("10");
            model.setBody("手机支付测试,共10元");
            model.setTimeoutExpress("2m");
            model.setProductCode(productCode);

            AlipayTradeWapPayRequest wapPayRequest = new AlipayTradeWapPayRequest();
            wapPayRequest.setReturnUrl(aliPayProperties.getReturnUrl());
            wapPayRequest.setNotifyUrl(aliPayProperties.getNotifyUrl());
            wapPayRequest.setBizModel(model);

            // 调用SDK生成表单, 并直接将完整的表单html输出到页面
            String retStr = alipayClient.pageExecute(wapPayRequest).getBody();
            response.setContentType("text/html; charset=" + aliPayProperties.getCharset());
            response.getWriter().write(retStr);
            response.getWriter().flush();
            response.getWriter().close();
        } catch (IOException | AlipayApiException e) {
            e.printStackTrace();
        }
    }
}

测试

1.配置视图跳转

/**
 * 视图配置类
 * 处理页面跳转
 *
 * @author Manaphy
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/alipay/webTest").setViewName("WebPay");
        registry.addViewController("/alipay/wapTest").setViewName("WapPay");
    }
}

2.测试用网页

PC端(WebPay.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body style="font-size: 30px">

<form method="post" action="/alipay/web/pay">
  <h3>购买商品:测试商品</h3>
  <h3>价格:5</h3>
  <h3>数量:1个</h3>

  <button style="width: 100%; height: 60px; alignment: center; background: #b49e8f" type="submit">立即支付</button>
</form>

</body>
</html>

手机端(WebPay.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body style="font-size: 30px">

<form method="post" action="/alipay/wap/pay">
  <h3>购买商品:测试商品</h3>
  <h3>价格:10</h3>
  <h3>数量:1个</h3>

  <button style="width: 100%; height: 60px; alignment: center; background: blue" type="submit">支付</button>
</form>
</body>
</html>

3.支付结果跳转页

成功(success.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>支付成功</h1>
</body>
</html>

失败(fail.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>支付失败</h1>
</body>
</html>

4.测试

  1. 网页支付测试:

​ 浏览器中访问: http://localhost:8081/alipay/webTest

  1. 手机支付测试:

​ 手机浏览器访问:dqidj6.natappfree.cc/alipay/wapT…

​ 或访问局域网地址:http://电脑端ip/alipay/wapTest

  1. 当面付测试:

​ 利用POSTMAN等接口测试工具访问:http://localhost:8081/alipay/f2fPay/barCodePay?authCode=付款码

  1. 线下扫码付测试:

​ 在浏览器中访问:http://localhost:8081/alipay/f2fPay/preCreate