支付系列-对接支付宝支付

前言:在商业开发中,支付业务是必不可少的一个环节。如今支付平台常用微信支付、支付宝支付。相较而言,支付宝支付对接起来比较简单,所以本文以支付宝支付为例。

支付宝提供的沙箱环境,入门门槛非常低,不需要商家认证那一套,这一点对开发者是非常友好的,可以直接在本地运行测试,跟最终的产品上线效果是一样的,好了,废话不说,进入正题。

一、开发流程

image-20211128000156670

二、前景准备

登录支付宝开放平台:https://open.alipay.com/

企业支付宝注册(非必备):https://memberprod.alipay.com/account/reg/index.htm

支付宝支付API:https://opendocs.alipay.com/apis

密钥生成地址:https://miniu.alipay.com/keytool/create

请求参数API:https://opendocs.alipay.com/open/02ekfg?scene=19

01、进入沙箱环境

image-20211126190046301

02、获取参数

  • APPID
  • 商家私钥
  • 商家公钥
  • 支付宝公钥
  • 支付宝回调地址
  • 网关地址
  • 加密算法签名RSA2
image-20211126191700583

密钥生成地址:https://miniu.alipay.com/keytool/create

注意: 线上开发获取支付宝公钥的方式为:使用应用公钥去换取支付宝公钥!!!

03、体验测试账户

因为环境为沙箱测试环境,它提供商户号跟买家号,但是该账户只能在支付宝提供的测试APK上使用。

image-20211126192852310

三、PC网页支付实现

01、创建springboot工程

自行创建

02、导入依赖

<!--freemarker模板引擎-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--web依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>
<!--test测试类-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<!--阿里云支付SDK-->
<dependency>
  <groupId>com.alipay.sdk</groupId>
  <artifactId>alipay-sdk-java</artifactId>
  <version>4.10.192.ALL</version>
</dependency>
<!--二维码生成-->
<dependency>
  <groupId>com.google.zxing</groupId>
  <artifactId>core</artifactId>
  <version>3.4.1</version>
</dependency>
<!--解析二维码-->
<dependency>
  <groupId>com.google.zxing</groupId>
  <artifactId>javase</artifactId>
  <version>3.4.1</version>
</dependency>
<!--加密依赖-->
<dependency>
  <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.15</version>
</dependency>
<!--http请求-->
<dependency>
  <groupId>commons-httpclient</groupId>
  <artifactId>commons-httpclient</artifactId>
  <version>3.1</version>
  <exclusions>
    <exclusion>
      <artifactId>commons-logging</artifactId>
      <groupId>commons-logging</groupId>
    </exclusion>
  </exclusions>
</dependency>
<!--配置文件加密处理-->
<dependency>
  <groupId>com.github.ulisesbocchio</groupId>
  <artifactId>jasypt-spring-boot-starter</artifactId>
  <version>2.1.0</version>
</dependency>

03、配置yml

server:
  port: 8081

spring:
  freemarker:
    suffix: .html
    cache: false

## 支付宝支付参数配置
alipay:
  app_id:   # APPID
  merchant_private_key:  #支付宝商户私钥
  alipay_public_key:  #支付宝公钥
  notify_url: #服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
  return_url:  #页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问(如果是二维码扫码可以不配置)
  sing_type: RSA2
  charset: utf-8
  gatewayurl: https://openapi.alipaydev.com/gateway.do  # https://openapi.alipay.com/gateway.do  线上地址
  # 保存支付日志的地址
  log_path: D:/tmp/

04、定义支付配置类 AlipayConfig

package com.qd.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.FileWriter;
import java.io.IOException;
/**
 * 支付宝支付配置类
 *
 * @Blog: https://www.cnblogs.com/qd666
 * @Author: qiandu
 * @Date: 2021/11/27 11:22
 */
@Component
public class AlipayConfig {
    // 应用ID
    public static String app_id;
    // 商户私钥
    public static String merchant_private_key;
    // 支付宝公钥
    public static String alipay_public_key;
    // 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    public static String notify_url;
    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    public static String return_url;
    // 签名方式
    public static String sign_type;
    // 字符编码格式
    public static String charset;
    // 支付宝网关
    public static String gatewayUrl;
    // 日志地址
    public static String log_path;
    /**
     * 写日志,方便测试(看网站需求,也可以改成把记录存入数据库)
     */
    public static void logResult(String sWord) {
        FileWriter writer = null;
        try {
            writer = new FileWriter(log_path + "alipay_log_" + System.currentTimeMillis() + ".txt");
            writer.write(sWord);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /*** 配置文件静态属性注入*/
    @Value("${alipay.app_id}")
    public void setApp_id(String app_id) {this.app_id = app_id;}

    @Value("${alipay.merchant_private_key}")
    public void setMerchant_private_key(String merchant_private_key) {this.merchant_private_key = merchant_private_key;}

    @Value("${alipay.alipay_public_key}")
    public void setAlipay_public_key(String alipay_public_key) {this.alipay_public_key = alipay_public_key;}

    @Value("${alipay.notify_url}")
    public void setNotify_url(String notify_url) {this.notify_url = notify_url;}

    @Value("${alipay.return_url}")
    public void setReturn_url(String return_url) {this.return_url = return_url;}

    @Value("${alipay.sing_type}")
    public void setSign_type(String sign_type) {this.sign_type = sign_type;}

    @Value("${alipay.charset}")
    public void setCharset(String charset) { this.charset = charset;}

    @Value("${alipay.gatewayurl}")
    public void setGatewayUrl(String gatewayUrl) {this.gatewayUrl = gatewayUrl;}

    @Value("${alipay.log_path}")
    public void setLog_path(String log_path) {this.log_path = log_path;}
}

notify_url:异步通知,当你支付成功时,支付宝回调你本地启动项目的接口,必须是外网可以访问的,否则支付宝请求不到,但是又得是你本地的方法「不讨论线上测试」可以使用工具实现内网穿透

05、支付宝支付工具类

package com.qd.controller;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.internal.util.AlipaySignature;
import com.qd.config.AlipayConfig;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * 支付宝支付工具类
 *
 * @Blog: https://www.cnblogs.com/qd666
 * @Author: qiandu
 * @Date: 2021/11/27 11:25
 */
@Slf4j
public class AliPayUtil {

    /*** 初始化AlipayClient*/
    public static AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type);

    /*** 转码*/
    public static String getByte(String param) {
        try {
            return new String(param.getBytes("ISO-8859-1"), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    /*** 校验签名*/
    public static boolean rsaCheckV1(HttpServletRequest request) {
        // https://docs.open.alipay.com/54/106370
        // 获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<>();
        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 {
            boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type);
            return signVerified;
        } catch (AlipayApiException e) {
            log.debug("verify sigin error, exception is:{}", e);
            return false;
        }
    }
}

06、参数封装成实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements java.io.Serializable{
    // 商品订单号 必填
    private String out_trade_no;;
    // 订单名称 必填
    private String subject;
    // 付款金额 必填
    private String total_amount;
    // 商品描述
    private String description;
    // 超时时间
    private String timeout_express = "10m";
    // 产品编号 必填
    private String product_code = "FAST_INSTANT_TRADE_PAY"; 
}

07、AliController(自行拆分service)

@Slf4j
@Controller
public class AliController {

    @ResponseBody
    @RequestMapping("/paySuccess")
    public String paySuccess() {
        return "支付成功!";
    }

    @ResponseBody
    @RequestMapping("/payFail")
    public String payFail() {
        return "支付失败!";
    }

    /*** 调用支付*/
    @RequestMapping(value = "/goAlipay", produces = "text/html; charset=UTF-8")
    @ResponseBody
    public String goAlipay(String subject, String totalAmount, String description) throws IOException, AlipayApiException {
        // 获得初始化的AlipayClient
        AlipayClient alipayClient = AliPayUtil.alipayClient;

        // 订单模型测试
        //AlipayTradePagePayModel model = new AlipayTradePagePayModel();
        //model.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
        //model.setSubject("subject:120元");
        //model.setTotalAmount("120");
        //model.setBody("这是一个description");
        //model.setProductCode("FAST_INSTANT_TRADE_PAY");
        //AlipayTradePagePayRequest pagePayRequest = new AlipayTradePagePayRequest();
        //pagePayRequest.setReturnUrl(AlipayConfig.return_url);
        //pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
        //pagePayRequest.setBizModel(model);

        // 1:设置参数
        Order order = new Order();
        order.setOut_trade_no(String.valueOf(System.currentTimeMillis()));
        order.setSubject("subject");
        order.setTotal_amount("120.00");
        order.setDescription("这是一个description");
        // 2:设置请求参数
        AlipayTradePagePayRequest pagePayRequest = new AlipayTradePagePayRequest();
        // 3:设置页面跳转同步通知页面路径
        pagePayRequest.setReturnUrl(AlipayConfig.return_url);
        // 4:设置服务器异步通知路径
        pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
        // 5:以json格式封装数据
        pagePayRequest.setBizContent(new ObjectMapper().writeValueAsString(order));
        // 请求
        String result = alipayClient.pageExecute(pagePayRequest).getBody();
        return result;
    }

    /*** 同步回调*/
    @RequestMapping("/return_url")
    public ModelAndView return_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
        log.info(">>>>>>>>支付成功, 进入同步通知接口...");
        boolean verifyResult = AliPayUtil.rsaCheckV1(request);
        ModelAndView mv = null;
        if (verifyResult) {
            //商户订单号
            String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
            //支付宝交易号
            String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
            log.info("商户订单号:{},支付宝交易号,{}", out_trade_no, trade_no);
            mv = new ModelAndView("paySuccess");
        } else {
            mv = new ModelAndView("payFail");
        }
        return mv;
    }

    /*** 异步回调*/
    @ResponseBody
    @RequestMapping(value = "/notify_url", method = RequestMethod.POST)
    public String notify_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
        log.info(">>>>>>>>支付成功, 进入异步通知接口...");
        // 一定要验签,防止黑客篡改参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        StringBuilder notifyBuild = new StringBuilder(">>>>>>>>>> alipay notify >>>>>>>>>>>>>>\n");
        parameterMap.forEach((key, value) -> notifyBuild.append(key + "=" + value[0] + "\n"));
        notifyBuild.append(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        log.info(notifyBuild.toString());
        boolean flag = AliPayUtil.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 = AliPayUtil.getByte(request.getParameter("trade_status"));
            // 商户订单号
            String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
            //支付宝交易号
            String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
            //付款金额
            String total_amount = AliPayUtil.getByte(request.getParameter("total_amount"));
            log.info("交易状态:{},商户订单号:{},支付宝交易号:{},付款金额:{}", tradeStatus, out_trade_no, trade_no, total_amount);
            // TRADE_FINISHED(表示交易已经成功结束,并不能再对该交易做后续操作);
            // TRADE_SUCCESS(表示交易已经成功结束,可以对该交易做后续操作,如:分润、退款等);
            if (tradeStatus.equals("TRADE_FINISHED")) {
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
                // 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
                //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
                //如果有做过处理,不执行商户的业务程序

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

                //注意:
                //如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。

            }
            return "success";
        }
        return "fail";
    }

    /***查看支付流水*/
    @RequestMapping(value = "/queryPay")
    @ResponseBody
    public String queryPay(String orderId) throws IOException, AlipayApiException {
        AlipayClient alipayClient = AliPayUtil.alipayClient;
        AlipayTradePagePayModel model = new AlipayTradePagePayModel();
        model.setOutTradeNo(orderId);
        //设置请求参数
        AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
        alipayRequest.setBizModel(model);
        //请求
        String result = alipayClient.execute(alipayRequest).getBody();
        return result;
    }

    /**
     * 退款
     *
     * @param orderNo 商户订单号
     * @return
     */
    @PostMapping("/refund")
    @ResponseBody
    public String refund(String orderNo) throws AlipayApiException {
        AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();
        AlipayTradeRefundModel model = new AlipayTradeRefundModel();
        // 商户订单号
        model.setOutTradeNo(orderNo);
        // 退款金额
        model.setRefundAmount("0.1");
        // 退款原因
        model.setRefundReason("无理由退货");
        // 退款订单号(同一个订单可以分多次部分退款,当分多次时必传)
        String outOrderId = UUID.randomUUID().toString();
        model.setOutRequestNo(outOrderId);
        log.info("退款请求号:{}", outOrderId);
        alipayRequest.setBizModel(model);
        AlipayTradeRefundResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
        return alipayResponse.getBody();
    }

    /**
     * 退款查询
     *
     * @param orderNo       商户订单号
     * @param refundOrderNo 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的外部订单号
     * @return
     * @throws AlipayApiException
     */
    @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 = AliPayUtil.alipayClient.execute(alipayRequest);
        System.out.println(alipayResponse.getBody());
        return alipayResponse.getBody();
    }

    /**
     * 关闭交易
     *
     * @param orderNo
     * @return
     * @throws AlipayApiException
     */
    @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 = AliPayUtil.alipayClient.execute(alipayRequest);
        System.out.println(alipayResponse.getBody());
        return alipayResponse.getBody();
    }
}

08、运行结果

image-20211127131712739

四、二维码当面付实现

01、创建springboot项目

02、导入依赖

03、定义支付配置类 AlipayConfig

04、支付宝支付工具类

这些步骤与前面一样,请看上

05、配置yml文件

server:
  port: 8081

spring:
  freemarker:
    suffix: .html
    cache: false

## 支付宝支付参数配置
alipay:
  app_id:   # APPID
  merchant_private_key:  #支付宝商户私钥
  alipay_public_key:  #支付宝公钥
  notify_url: #服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
  return_url:  #页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问(如果是二维码扫码可以不配置)
  sing_type: RSA2
  charset: utf-8
  gatewayurl: https://openapi.alipaydev.com/gateway.do  # https://openapi.alipay.com/gateway.do  线上地址
  # 保存支付日志的地址
  log_path: D:/tmp/
  
## 支付二维码配置
qrcode:
  # 二维码logo地址
  logo_location: D:\tmp\yjtp.png
  # 生成的二维码地址
  file_path: D:\tmp\pay\

06、生成二维码工具类

package com.qd.utils;

import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.bouncycastle.util.encoders.Base64Encoder;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Hashtable;
import java.util.Random;

/**
 * 二维码生成工具类
 *
 * @Blog: https://www.cnblogs.com/qd666
 * @Author: qiandu
 * @Date: 2021/11/27 19:05
 */
public class QRCodeUtil {
    private static final String CHARSET = "utf-8";
    private static final String FORMAT = "JPG";
    // 二维码尺寸
    private static final int QRCODE_SIZE = 300;
    // LOGO宽度
    private static final int LOGO_WIDTH = 60;
    // LOGO高度
    private static final int LOGO_HEIGHT = 60;

    private static BufferedImage createImage(String content, String logoPath, boolean needCompress) throws Exception {
        Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
        hints.put(EncodeHintType.MARGIN, 1);
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,
                hints);
        int width = bitMatrix.getWidth();
        int height = bitMatrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
            }
        }
        if (logoPath == null || "".equals(logoPath)) {
            return image;
        }
        // 插入图片
        QRCodeUtil.insertImage(image, logoPath, needCompress);
        return image;
    }

    /**
     * 插入LOGO
     *
     * @param source       二维码图片
     * @param logoPath     LOGO图片地址
     * @param needCompress 是否压缩
     * @throws Exception
     */
    private static void insertImage(BufferedImage source, String logoPath, boolean needCompress) throws Exception {
        File file = new File(logoPath);
        if (!file.exists()) {
            throw new Exception("logo file not found.");
        }
        Image src = ImageIO.read(new File(logoPath));
        int width = src.getWidth(null);
        int height = src.getHeight(null);
        if (needCompress) { // 压缩LOGO
            if (width > LOGO_WIDTH) {
                width = LOGO_WIDTH;
            }
            if (height > LOGO_HEIGHT) {
                height = LOGO_HEIGHT;
            }
            Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
            BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics g = tag.getGraphics();
            g.drawImage(image, 0, 0, null); // 绘制缩小后的图
            g.dispose();
            src = image;
        }
        // 插入LOGO
        Graphics2D graph = source.createGraphics();
        int x = (QRCODE_SIZE - width) / 2;
        int y = (QRCODE_SIZE - height) / 2;
        graph.drawImage(src, x, y, width, height, null);
        Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
        graph.setStroke(new BasicStroke(3f));
        graph.draw(shape);
        graph.dispose();
    }

    /**
     * 生成二维码(内嵌LOGO)
     * 二维码文件名随机,文件名可能会有重复
     *
     * @param content      内容
     * @param logoPath     LOGO地址
     * @param destPath     存放目录
     * @param needCompress 是否压缩LOGO
     * @throws Exception
     */
    public static String encode(String content, String logoPath, String destPath, boolean needCompress) throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress);
        mkdirs(destPath);
        String fileName = System.currentTimeMillis() + new Random().nextInt(99999999) + "." + FORMAT.toLowerCase();
        ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName));
        return fileName;
    }

    /**
     * 生成二维码(内嵌LOGO)
     * 调用者指定二维码文件名
     *
     * @param content      内容
     * @param logoPath     LOGO地址
     * @param destPath     存放目录
     * @param fileName     二维码文件名
     * @param needCompress 是否压缩LOGO
     * @throws Exception
     */
    public static String encode(String content, String logoPath, String destPath, String fileName, boolean needCompress) throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress);
        mkdirs(destPath);
        fileName = fileName.substring(0, fileName.indexOf(".") > 0 ? fileName.indexOf(".") : fileName.length())
                + "." + FORMAT.toLowerCase();
        ImageIO.write(image, FORMAT, new File(destPath + "/" + fileName));
        return fileName;
    }

    /**
     * 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.
     * (mkdir如果父目录不存在则会抛出异常)
     *
     * @param destPath 存放目录
     */
    public static void mkdirs(String destPath) {
        File file = new File(destPath);
        if (!file.exists() && !file.isDirectory()) {
            file.mkdirs();
        }
    }

    /**
     * 生成二维码(内嵌LOGO)
     *
     * @param content  内容
     * @param logoPath LOGO地址
     * @param destPath 存储地址
     * @throws Exception
     */
    public static String encode(String content, String logoPath, String destPath) throws Exception {
        return QRCodeUtil.encode(content, logoPath, destPath, false);
    }

    /**
     * 生成二维码
     *
     * @param content      内容
     * @param destPath     存储地址
     * @param needCompress 是否压缩LOGO
     * @throws Exception
     */
    public static String encode(String content, String destPath, boolean needCompress) throws Exception {
        return QRCodeUtil.encode(content, null, destPath, needCompress);
    }

    /**
     * 生成二维码
     *
     * @param content  内容
     * @param destPath 存储地址
     * @throws Exception
     */
    public static String encode(String content, String destPath) throws Exception {
        return QRCodeUtil.encode(content, null, destPath, false);
    }

    /**
     * 生成二维码(内嵌LOGO)
     *
     * @param content      内容
     * @param logoPath     LOGO地址
     * @param output       输出流
     * @param needCompress 是否压缩LOGO
     * @throws Exception
     */
    public static void encode(String content, String logoPath, OutputStream output, boolean needCompress)
            throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, logoPath, needCompress);
        ImageIO.write(image, FORMAT, output);
    }

    /**
     * 生成二维码
     *
     * @param content 内容
     * @param output  输出流
     * @throws Exception
     */
    public static void encode(String content, OutputStream output) throws Exception {
        QRCodeUtil.encode(content, null, output, false);
    }

    /**
     * 解析二维码
     *
     * @param file 二维码图片
     * @return
     * @throws Exception
     */
    public static String decode(File file) throws Exception {
        BufferedImage image;
        image = ImageIO.read(file);
        if (image == null) {
            return null;
        }
        BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        Result result;
        Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>();
        hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
        result = new MultiFormatReader().decode(bitmap, hints);
        String resultStr = result.getText();
        return resultStr;
    }

    /**
     * 解析二维码
     *
     * @param path 二维码图片地址
     * @return
     * @throws Exception
     */
    public static String decode(String path) throws Exception {
        return QRCodeUtil.decode(new File(path));
    }


    /**
     * 二维码图片转Base64
     *
     * @param imagePath 图片路径
     * @return
     */
    public static String imageToBase64(String imagePath) throws IOException {
        // 获取文件流
        FileInputStream inputStream = new FileInputStream(imagePath);
        // 转成字节
        byte[] bytes = new byte[inputStream.available()];
        inputStream.read(bytes);
        inputStream.close();
        // 对字符数组进行base64编码
        BASE64Encoder base64Encoder = new BASE64Encoder();
        return base64Encoder.encode(bytes);
    }

    public static void main(String[] args) throws Exception {
        String text = "http://www.baidu.com";
        //不含Logo
        //QRCodeUtil.encode(text, null, "e:\\", true);
        //含Logo,不指定二维码图片名
        //QRCodeUtil.encode(text, "e:\\csdn.jpg", "e:\\", true);
        //含Logo,指定二维码图片名
        String qrcode = QRCodeUtil.encode(text, "D:\\qdSystem\\Resources\\Pictures\\Saved Pictures\\yjtp.png", "D:\\tmp", true);
        System.out.println(qrcode);
        String decode = QRCodeUtil.decode("D:\\tmp\\qrcode.jpg");
        System.out.println(decode);
    }
}

07、参数封装成实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements java.io.Serializable {

  // 商品订单号 必填
  private String out_trade_no;
  // 订单名称 必填
  private String subject;
  // 付款金额 必填
  private String total_amount;
  // 商品描述
  private String description;
  // 超时时间
  private String timeout_express = "10m";
  // 产品编号 必填
  private String product_code = "FACE_TO_FACE_PAYMENT";  // 二维码当面支付填这个 FACE_TO_FACE_PAYMENT  PC:FAST_INSTANT_TRADE_PAY
}

08、AliQrController(自行拆分service)

/**
 * 支付宝二维码支付demo
 */
@Slf4j
@Controller
@RequestMapping("/qr")
public class AliQrController {


    @Value("${qrcode.logo_location}")
    private String logoLocation;
    @Value("${qrcode.file_path}")
    private String filePath;


    @ResponseBody
    @RequestMapping("/paySuccess")
    public String paySuccess() {
        return "支付成功!";
    }

    @ResponseBody
    @RequestMapping("/payFail")
    public String payFail() {
        return "支付失败!";
    }

    /*** 调用支付*/
    @RequestMapping(value = "/goAlipay", produces = "text/html; charset=UTF-8")
    @ResponseBody
    public String goAlipay(String subject, String totalAmount, String description) throws IOException, AlipayApiException {
        // 获得初始化的AlipayClient
        AlipayClient alipayClient = AliPayUtil.alipayClient;

        // 订单模型测试
        //AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
        //model.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
        //model.setSubject("subject:120元");
        //model.setTotalAmount("120");
        //model.setBody("这是一个description");
        //model.setProductCode("FACE_TO_FACE_PAYMENT");
        //model.setTimeoutExpress("10m");
        //AlipayTradePrecreateRequest pagePayRequest = new AlipayTradePrecreateRequest();
        //pagePayRequest.setReturnUrl(AlipayConfig.return_url);
        //pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
        //pagePayRequest.setBizModel(model);

         1:设置参数
        Order order = new Order();
        order.setOut_trade_no(String.valueOf(System.currentTimeMillis()));
        order.setSubject("subject");
        order.setTotal_amount("120.00");
        order.setDescription("这是一个description");
        //2:设置请求参数
        AlipayTradePrecreateRequest pagePayRequest = new AlipayTradePrecreateRequest();
        // 3:设置页面跳转同步通知页面路径
        pagePayRequest.setReturnUrl(AlipayConfig.return_url);
        // 4:设置服务器异步通知路径
        pagePayRequest.setNotifyUrl(AlipayConfig.notify_url);
        // 5:以json格式封装数据
        pagePayRequest.setBizContent(JSON.toJSONString(order));
        //请求
        AlipayTradePrecreateResponse response = alipayClient.execute(pagePayRequest);
        // 二维码base64编码内容
        String base64Image = "";
        // 判断是否调用成功
        if (response.isSuccess()) {
            JSONObject jsonObject = JSON.parseObject(response.getBody());
            JSONObject rsj = (JSONObject) jsonObject.get("alipay_trade_precreate_response");
            // 生成的二维码地址内容
            String qr_code = (String) rsj.get("qr_code");
            // 生成二维码
            try {
                // 支付宝返回内容  logo地址  二维码生成地址  是否压缩
                String encode = QRCodeUtil.encode(qr_code, logoLocation, filePath, true);
                log.info("工具生成的二维码地址内容------->{}", encode);
                base64Image = QRCodeUtil.imageToBase64(filePath + encode);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        log.info("支付宝返回内容------->{}", response.getBody());
        // 返回base64结果 前端自行处理
        return base64Image;
    }

    /*** 同步回调*/
    @RequestMapping("/return_url")
    public ModelAndView return_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
        log.info(">>>>>>>>支付成功, 进入同步通知接口...");
        boolean verifyResult = AliPayUtil.rsaCheckV1(request);
        ModelAndView mv = null;
        if (verifyResult) {
            //商户订单号
            String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
            //支付宝交易号
            String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
            log.info("商户订单号:{},支付宝交易号,{}", out_trade_no, trade_no);
            mv = new ModelAndView("/qr/paySuccess");
        } else {
            mv = new ModelAndView("/qr/payFail");
        }
        return mv;
    }

    /*** 异步回调*/
    @ResponseBody
    @RequestMapping(value = "/notify_url", method = RequestMethod.POST)
    public String notify_url(HttpServletResponse response, HttpServletRequest request) throws IOException, AlipayApiException {
        log.info(">>>>>>>>支付成功, 进入异步通知接口...");
        // 一定要验签,防止黑客篡改参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        StringBuilder notifyBuild = new StringBuilder(">>>>>>>>>> alipay notify >>>>>>>>>>>>>>\n");
        parameterMap.forEach((key, value) -> notifyBuild.append(key + "=" + value[0] + "\n"));
        notifyBuild.append(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        log.info(notifyBuild.toString());
        boolean flag = AliPayUtil.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 = AliPayUtil.getByte(request.getParameter("trade_status"));
            // 商户订单号
            String out_trade_no = AliPayUtil.getByte(request.getParameter("out_trade_no"));
            //支付宝交易号
            String trade_no = AliPayUtil.getByte(request.getParameter("trade_no"));
            //付款金额
            String total_amount = AliPayUtil.getByte(request.getParameter("total_amount"));
            log.info("交易状态:{},商户订单号:{},支付宝交易号:{},付款金额:{}", tradeStatus, out_trade_no, trade_no, total_amount);
            // TRADE_FINISHED(表示交易已经成功结束,并不能再对该交易做后续操作);
            // TRADE_SUCCESS(表示交易已经成功结束,可以对该交易做后续操作,如:分润、退款等);
            if (tradeStatus.equals("TRADE_FINISHED")) {
                //判断该笔订单是否在商户网站中已经做过处理
                //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,
                // 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序
                //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的
                //如果有做过处理,不执行商户的业务程序

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

                //注意:
                //如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。

            }
            return "success";
        }
        return "fail";
    }

    /***查看支付流水*/
    @RequestMapping(value = "/queryPay")
    @ResponseBody
    public String queryPay(String orderId) throws IOException, AlipayApiException {
        AlipayClient alipayClient = AliPayUtil.alipayClient;
        AlipayTradePagePayModel model = new AlipayTradePagePayModel();
        model.setOutTradeNo(orderId);
        //设置请求参数
        AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
        alipayRequest.setBizModel(model);
        //请求
        String result = alipayClient.execute(alipayRequest).getBody();
        return result;
    }

    /**
     * 退款
     *
     * @param orderNo 商户订单号
     * @return
     */
    @PostMapping("/refund")
    @ResponseBody
    public String refund(String orderNo) throws AlipayApiException {
        AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest();
        AlipayTradeRefundModel model = new AlipayTradeRefundModel();
        // 商户订单号
        model.setOutTradeNo(orderNo);
        // 退款金额
        model.setRefundAmount("0.1");
        // 退款原因
        model.setRefundReason("无理由退货");
        // 退款订单号(同一个订单可以分多次部分退款,当分多次时必传)
        String outOrderId = UUID.randomUUID().toString();
        model.setOutRequestNo(outOrderId);
        log.info("退款请求号:{}", outOrderId);
        alipayRequest.setBizModel(model);
        AlipayTradeRefundResponse alipayResponse = AliPayUtil.alipayClient.execute(alipayRequest);
        return alipayResponse.getBody();
    }

    /**
     * 退款查询
     *
     * @param orderNo       商户订单号
     * @param refundOrderNo 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的外部订单号
     * @return
     * @throws AlipayApiException
     */
    @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 = AliPayUtil.alipayClient.execute(alipayRequest);
        System.out.println(alipayResponse.getBody());

        return alipayResponse.getBody();
    }

    /**
     * 关闭交易
     *
     * @param orderNo
     * @return
     * @throws AlipayApiException
     */
    @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 = AliPayUtil.alipayClient.execute(alipayRequest);
        System.out.println(alipayResponse.getBody());
        return alipayResponse.getBody();
    }
}

09、访问测试

image-20211127230344987

tip:为了数据更加的安全,可以加密部分数据,加密部分本文暂未涉及

五、yml文件加密

前言:在实际的项目开发中,在我们的配置文件中会有隐私数不想以明文方式展现,所以要加密yml文件

01、导入依赖

<!--配置文件加密处理-->
<dependency>
  <groupId>com.github.ulisesbocchio</groupId>
  <artifactId>jasypt-spring-boot-starter</artifactId>
  <version>2.1.0</version>
</dependency>

02、编写测试类

@SpringBootTest
public class JasypTest {
    // 加密测试
    @Test
    public void encryptTest() {
        StandardPBEStringEncryptor standardPBEBigDecimalEncryptor = new StandardPBEStringEncryptor();
        EnvironmentPBEConfig config = new EnvironmentPBEConfig();
        config.setPassword("fsdfs8979!~#!@#rewr"); //密钥 不要告诉别人!!!
        standardPBEBigDecimalEncryptor.setConfig(config);
        String plainText = "hell,qiandu"; // 要加密的内容
        String encryptText = standardPBEBigDecimalEncryptor.encrypt(plainText);
        System.out.println("加密结果---->" + encryptText);
    }

    // 解密测试
    @Test
    public void decTest() {
        StandardPBEStringEncryptor standardPBEBigDecimalEncryptor = new StandardPBEStringEncryptor();
        EnvironmentPBEConfig config = new EnvironmentPBEConfig();
        config.setPassword("fsdfs8979!~#!@#rewr"); //密钥 不要告诉别人!!!
        standardPBEBigDecimalEncryptor.setConfig(config);
        String encryptText = "6fJIuViOKsNpxPk8ce6CAUiIftXuKb9x"; // 要解密的内容
        String plainText = standardPBEBigDecimalEncryptor.decrypt(encryptText);
        System.out.println("解密结果---->" + plainText);
    }
}

03、启动类上加入注解

@SpringBootApplication
@EnableEncryptableProperties //开启加密注解
public class AlipayApplication {

  public static void main(String[] args) {
    SpringApplication.run(AlipayApplication.class, args);
  }

}

04、在yml中配置(记得作隔离!)

## jasypt配置
jasypt:
  encryptor:
    # 密钥
    password:  #你的密钥

结束~

示例代码:https://chenyu6666.lanzoui.com/iK7qewz1o3a


参考文章:https://blog.csdn.net/qq_40673786/article/details/90260736

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值