查看: 109|回复: 1

支付宝沙箱支付详细教程

[复制链接]

1

主题

4

帖子

3

积分

新手上路

Rank: 1

积分
3
发表于 2022-9-20 13:01:26 | 显示全部楼层 |阅读模式
预期阅读的官方文档以及工具


  • 支付宝开放平台 https://open.alipay.com/platform/home.htm
  • 支付宝沙箱模式 https://open.alipay.com/platform/appDaily.htm?tab=info
  • 支付宝支付Demo下载 https://gw.alipayobjects.com/os/bmw-prod/a526522f-37a6-4a8e-9abc-d22cf240cbbd.zip
  • natapp内网穿透工具官网:https://natapp.cn/
支付宝支付的业务流程图



  • 支付过程详细描述:
  • 触发场景:

    • 当用户确认了订单=>选择支付方式=>点击立即支付,在点击立即支付后发送一个拉起支付请求到我们的服务器http://localhost:8080/alipay/pay,参数一般就是订单的基本信息。

  • 服务端响应:

    • 服务器端处理用户的下单请求http://localhost:8080/alipay/pay。
    • 根据客户端传来的订单信息,计算出每一个商品的单价、订单的总金额等(金额是不应该由客户端直接传的,应该由服务器端计算,更安全),再生成一个订单号。
    • 调用支付宝支付接口,订单信息进行上传,成功后支付宝会返回一段html文档,就是大家经常看到的带二维码的PC端支付页面,但是==本文中只获取对应的支付宝二维码信息,然后生成对应支付二维码返回给买家==




    • 第六步当买家扫码支付完成以后,支付宝会调用同步回调API接口跳转到一个自定义的成功页面(==这个页面只是告诉用户支付成功了,只做展示用途,并非真正的支付回调==)
    • 第七步支付完成以后,支付宝会调用异步回调API接口,将一些参数传给服务端,如交易流水号、订单号,服务端可以根据这些信息查询交易是否真的成功了(第8步),从而执行后续的业务,比如将订单状态变为已支付、给用户增加积分、扣减优惠券……等。

支付宝沙箱环境配置
​       支付宝有一个供开发者测试使用的沙箱环境,会提供一个沙箱版的支付宝app、一个商家账户、一个买家账户。有了这个,可以让我们跳过商家入驻、企业资质审核等过程,开箱即用。
1、配置沙箱环境
​        进入到支付宝支付官网,点击“我是开发者”,在新的页面右上角,用你自己的支付宝扫码登录,再点击开发服务中的研发服务:



进入到沙箱环境,这里为我们配置了一个唯一的APPID,以及提供了沙箱环境中的网关。



2、获取应用公钥和应用私钥
​           对于数据的安全考虑,我们需要生成一份公钥和私钥,公钥提供给支付宝,支付宝对数据进行加密;私钥用于解析支付宝传来的加密数据,由我们自行保管。支付宝开放平台为我们提供了密钥生成工具(开发助手)来对应用的客户端服务端之间的交互进行加密保护。工具主要功能有生成密钥、签名、验签、格式转换、密钥匹配、智能反馈、开放社区。



​           打开下载好的工具,在以下地方点击“生成密钥”,生成以后会出现一个应用私钥和应用公钥,这两者都需要自己保存,应用公钥需要传到支付宝中去换取支付宝公钥,==支付宝公钥和应用私钥需要在后面发起支付请求的时候使用。==



3、获取支付宝公钥
​       回到沙箱应用控制台,点击设置公钥(==注意不是公钥证书哦,我们用的不是证书的方式==)



将应用公钥输入到如图所示,获取到支付宝公钥



4、下载沙箱支付宝APP
​       在沙箱应用控制台,扫码下载沙箱版的支付宝(目前只有安卓版),提供了一个商家号、一个买家号,里面可以自由充值费用,并且该商家,买家账号用于登陆沙箱支付宝APP。



接下来我们进行代码实现!!!
创建数据库表
​       本案例中创建的表为课程表courses(商品),订单表order_detail
1、 课程表



2、订单表



搭建Springboot项目
1、配置POM文件,其中SDK采用的是alipay-sdk-java 4.16.2.ALL
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lonely</groupId>
    <artifactId>alipay_demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>alipay_demo</name>
    <description>alipay_demo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
        </dependency>
        <!--热部署插件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!--数据库连接驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!--Mybatis依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!-- alipay-sdk-java -->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.16.2.ALL</version>
        </dependency>
        <!--JSON转换类-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--工具类建议多看-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.10</version>
        </dependency>
        <!--二维码生成-->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!--加密依赖-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.11</version>
        </dependency>
        <!--http请求-->
        <dependency>
            <groupId>commons-httpclient</groupId>
            <artifactId>commons-httpclient</artifactId>
            <version>3.1</version>
        </dependency>
        <!--配置文件ENC加密处理-->
        <dependency>
            <groupId>com.github.ulisesbocchio</groupId>
            <artifactId>jasypt-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!--打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>2、项目整体结构介绍以及启动类配置
​           通过EasyCode工具类,生成课程表和订单表对应的实体类(entity),控制层(controller),服务层(service),以及数据操作层(dao,mapper),其余目录结构如下图所示。后面业务中会说明详细创建过程。


3、配置application.yml文件,配置数据源,日志等信息。
spring:
  # 数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/alipay?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
  # 启动配置文件
  profiles:
    active: dev
  # 配置时间格式和时差问题
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    locale: zh_CN
  # 解决json返回过程中long的精度丢失问题
  generator:
    write-numbers-as-strings: true
    write-bigdecimal-as-plain: true
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
  # 解决bean重复定义的
  main:
    allow-bean-definition-overriding: true
  # dispatcherServlet 是懒加载机制,配置为1 表示提前加载
  mvc:
    servlet:
      load-on-startup: 1

# 日志配置管理
logging:
  level:
    root: info

# mybatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.lonely.alipay_demo.entity
配置AlipayConfig类
1、 创建AlipauConfig工具类,主要用户配置支付请求创建所需要的公共参数
package com.lonely.alipay_demo.config;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* @Author: xiyang
* @FileName: AlipayConfig
* @Date: Created in 2021/8/5 10:32
* @Vserion:
* @Description: TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Component
public class AlipayConfig {

    /**
     * 商户appid
     */
    @Value("${alipay.APPID}")
    public String APPID;

    /**
     * 私钥 pkcs8格式的
     */
    @Value("${alipay.RSA_PRIVATE_KEY}")
    public String RSA_PRIVATE_KEY;
    /**
     * 服务器异步通知页面路径
     * 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
     */
    @Value("${alipay.notify_url}")
    public String notify_url;

    /**
     * 页面跳转同步通知页面路径
     * 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,
     * 必须外网可以正常访问 商户可以自定义同步跳转地址
     */
    @Value("${alipay.return_url}")
    public String return_url;

    /**
     * 请求网关地址
     */
    @Value("${alipay.URL}")
    public String URL;

    /**
     * 编码
     */
    @Value("${alipay.CHARSET}")
    public String CHARSET;

    /**
     * 返回格式
     */
    @Value("${alipay.FORMAT}")
    public String FORMAT;

    /**
     * 支付宝公钥
     */
    @Value("${alipay.ALIPAY_PUBLIC_KEY}")
    public String ALIPAY_PUBLIC_KEY;

    /**
     * 日志记录目录定义在 logFile 中
     */
    @Value("${alipay.log_path}")
    public String log_path;

    /**
     * RSA2
     */
    @Value("${alipay.SIGNTYPE}")
    public String SIGNTYPE;

}2、配置application-dev.yml文件,主要配置AlipauConfig工具类的参数值
#服务器端口号配置
server:
  port: 8080

# 数据源信息配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/alipay?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource

# 配置alipay数据
alipay:
  # 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
  APPID: 沙箱中的APPID
  # 商户私钥,您的PKCS8格式RSA2私钥
  RSA_PRIVATE_KEY: 应用私钥
  # 支付宝支付公钥
  ALIPAY_PUBLIC_KEY: 支付宝公钥(通过应用公钥换取的值)
  # 异步回调地址 必须外网能够访问(这里需要配置内网穿透),当支付成功后会调用该API
  notify_url: http://hpzekg.natappfree.cc/alipay/pay/callback
  # 同步回调地址 必须外网能够访问
  return_url:
  # 网关(注意沙箱网关和正式网关的区别,这里填写沙箱环境下的网关)
  URL: https://openapi.alipaydev.com/gateway.do
  # 编码
  CHARSET: UTF-8
  # 返回数据格式
  FORMAT: json
  # 日志地址
  log_path: /log
  # RSA2
  SIGNTYPE: RSA2
用户拉起支付请求,获取付款二维码
​       当买家确认了订单=>选择支付方式=>点击立即支付后发送一个拉起支付请求到我们的服务器http://localhost:8080/alipay/pay,参数一般就是商品的ID和支付方式payType(其余参数大家可以自行添加)。
1、 创建PayVo实体类,用于接收订单参数
package com.lonely.alipay_demo.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @Author: xiyang
* @FileName: PayVo
* @Date: Created in 2021/8/6 14:45
* @Vserion:
* @Description: TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayVo {

    private String courseId;

    private Integer payMethod;

}2、在AlipayController 中编写http://localhost:8080/alipay/pay的API
package com.lonely.alipay_demo.controller;

import com.lonely.alipay_demo.service.AlipayService;
import com.lonely.alipay_demo.vo.PayVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
* @Author: xiyang
* @FileName: AlipayController
* @Date: Created in 2021/8/6 12:30
* @Vserion:
* @Description: TODO
*/
@RestController
public class AlipayController {

    @Autowired
    private AlipayService alipayService;

    /**
     * 拉起支付请求
     * @param payVo
     * @return
     * @throws Exception
     */
    @GetMapping("/alipay/pay")
    public byte[] alipay(PayVo payVo) throws Exception{
        byte[] alipay = alipayService.alipay(payVo);
        return alipay;
    }
}3、编写拉起支付的服务层接口-->alipay接口
package com.lonely.alipay_demo.service;

import com.lonely.alipay_demo.vo.PayVo;

import javax.servlet.http.HttpServletRequest;

/**
* @Author: xiyang
* @FileName: AlipayService
* @Date: Created in 2021/8/6 12:27
* @Vserion:
* @Description: TODO
*/
public interface AlipayService {

    /**
     * 获取支付宝支付二维码
     * @param payVo
     * @return img
     */
    byte[] alipay(PayVo payVo) throws Exception;
}4、实现支付二维码业务逻辑-->alipay方法
/**
* @Author: xiyang
* @FileName: AlipayServicImpl
* @Date: Created in 2021/8/6 12:28
* @Vserion:
* @Description: TODO
*/
@Service
public class AlipayServiceImpl implements AlipayService {

    Logger logger = LoggerFactory.getLogger(AlipayServiceImpl.class);

    @Autowired
    private AlipayConfig alipayConfig;

    @Resource
    private KssCoursesDao kssCoursesDao;

    @Override
    public byte[] alipay(PayVo payVo) throws Exception {
        /**
         * 1. 获取阿里客户端
         * 2. 获取阿里请求对象
         * 3. 设置请求参数
         * 4. 设置同步通知回调路径
         * 5. 设置异步通知回调路径
         */
        KssCourses kssCourses = kssCoursesDao.queryById(payVo.getCourseId());
        if (kssCourses == null) {
            throw new BusinessException(ResultCodeEnum.SYSTEM_EXCEPTION);
        }
        String orderNumber = GenerateNum.generateOrder();

        //设置支付回调时可以在request中获取的参数
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("courseId", kssCourses.getCourseid());
        jsonObject.put("courseTitle", kssCourses.getTitle());
        jsonObject.put("courseImg", kssCourses.getImg());
        jsonObject.put("orderNumber", orderNumber);
        jsonObject.put("payType", payVo.getPayMethod());
        jsonObject.put("price", kssCourses.getPrice());
        String params = jsonObject.toString();

        //设置支付参数
        AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
        model.setBody(params);
        model.setTotalAmount(kssCourses.getPrice().toString());
        model.setOutTradeNo(orderNumber);
        model.setSubject(kssCourses.getTitle());
        //获取响应二维码信息
        QrCodeResponse qrCodeResponse = qrcodePay(model);
        //制作二维码并且返回给前端
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        String logopath = "";
        logopath = ResourceUtils.getFile("classpath:favicon.png").getAbsolutePath();
        logger.info("二维码的图片路径为===>" + logopath);
        BufferedImage encode = QRCodeUtil.encode(qrCodeResponse.getQr_code(), logopath, false);
        ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(byteArrayOutputStream);
        ImageIO.write(encode, "JPEG", imageOutputStream);
        imageOutputStream.close();
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        return FileCopyUtils.copyToByteArray(byteArrayInputStream);
    }

    /**
     * 支付宝客户端发送支付请求获取支付二维码信息
     */
    public QrCodeResponse qrcodePay(AlipayTradePrecreateModel model) throws AlipayApiException {
        //1.获取请求客户端
        AlipayClient alipayClient = getAlipayClient();

        //2. 获取请求对象
        AlipayTradePrecreateRequest alipayRequest = new AlipayTradePrecreateRequest();

        //3.设置请求参数
        alipayRequest.setBizModel(model);
        alipayRequest.setNotifyUrl(alipayConfig.getNotify_url());
        alipayRequest.setReturnUrl(alipayConfig.getReturn_url());
        AlipayTradePrecreateResponse execute = null;
        execute = alipayClient.execute(alipayRequest);
        String body = execute.getBody();
        logger.info("请求的响应二维码信息====>" + body);
        QrResponse qrResponse = JSON.parseObject(body, QrResponse.class);
        return qrResponse.getAlipay_trade_precreate_response();
    }

    /**
     * 获取阿里客户端
     *
     * @return
     */
    public AlipayClient getAlipayClient() {
        DefaultAlipayClient defaultAlipayClient = new DefaultAlipayClient(
                alipayConfig.getURL(),
                alipayConfig.getAPPID(),
                alipayConfig.getRSA_PRIVATE_KEY(),
                alipayConfig.getFORMAT(),
                alipayConfig.getCHARSET(),
                alipayConfig.getALIPAY_PUBLIC_KEY(),
                alipayConfig.getSIGNTYPE()
        );
        return defaultAlipayClient;
    }
}当请求响应成功以后会给前端返回一个字节数组,为当前订单的支付二维码。效果如下图所示。



5、前端代码如下:
<img
      id="im"
      src="http://localhost:8080/alipay/pay?courseId=1317503462556848129&payMethod=1"
      alt=""
    />6、在业务实现中需要使用到GenerateNum工具类来实现订单号生成--采用雪花算法
package com.lonely.alipay_demo.util;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* 根据时间生成随机订单号
*/
public class GenerateNum {
    /**
     * 全局自增数
     */

    private static int count = 0;

    /**
     *     每毫秒秒最多生成多少订单(最好是像9999这种准备进位的值)
     */
    private static final int total = 99;

    /**
     * 格式化的时间字符串
     */
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");

    /**
     * 获取当前时间年月日时分秒毫秒字符串
     * @return
     */
    private static String getNowDateStr() {
        return sdf.format(new Date());
    }

    /**
     * 记录上一次的时间,用来判断是否需要递增全局数
     */
    private static String now = null;

    /**
    *生成一个订单号
    */
    public static String generateOrder() {
        String dataStr = getNowDateStr();
        if (dataStr.equals(now)) {
            count++;// 自增
        } else {
            count = 1;
            now = dataStr;
        }
        // 算补位
        int countInteger = String.valueOf(total).length() - String.valueOf(count).length();
        //// 补字符串
        String bu = "";
        for (int i = 0; i < countInteger; i++) {
            bu += "0";
        }

        bu += String.valueOf(count);
        if (count >= total) {
            count = 0;
        }
        return dataStr + bu;
    }

}7、在向支付宝发起请求后,处理返回结果会使用到QrResponse和QrCodeResponse
QrResponse
package com.lonely.alipay_demo.qrcode;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @Author: xiyang
* @FileName: QrResponse
* @Date: Created in 2021/8/6 19:31
* @Vserion:
* @Description: TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class QrResponse {

    private QrCodeResponse alipay_trade_precreate_response;

    private String sign;
}QrCodeResponse
package com.lonely.alipay_demo.qrcode;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class QrCodeResponse {
    /**
     * 返回的状态码
     */
    private String code;

    /**
     * 返回的信息
     */
    private String msg;

    /**
     * 交易的流水号
     */
    private String out_trade_no;

    /**
     * 生成二维码的内容
     */
    private String qr_code;

}8、在生成字符二维码时,会用到BufferedImageLuminanceSource和 QrCodeUtil工具类
QrCodeUtil
package com.lonely.alipay_demo.qrcode;

import com.google.zxing.*;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

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

public class QRCodeUtil {
    private static final String CHARSET = "utf-8";
    private static final String FORMAT_NAME = "JPG";
    // 二维码尺寸
    private static final int QRCODE_SIZE = 300;
    // LOGO宽度
    private static final int WIDTH = 90;
    // LOGO高度
    private static final int HEIGHT = 90;

    /**
     * @Author xuke
     * @Description 二维码生成的方法
     * @Date 0:45 2021/4/2
     * @Param [content, imgPath, needCompress]
     * @return java.awt.image.BufferedImage
    **/
    private static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception {
        Hashtable hints = new Hashtable();
        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 (imgPath == null || "".equals(imgPath)) {
            return image;
        }
        // 插入图片
        QRCodeUtil.insertImage(image, imgPath, needCompress);
        return image;
    }

    private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception {
        File file = new File(imgPath);
        if (!file.exists()) {
            System.err.println("" + imgPath + "   该文件不存在!");
            return;
        }
        Image src = ImageIO.read(new File(imgPath));
        int width = src.getWidth(null);
        int height = src.getHeight(null);
        if (needCompress) { // 压缩LOGO
            if (width > WIDTH) {
                width = WIDTH;
            }
            if (height > HEIGHT) {
                height = 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();
    }

    public static void encode(String content, String imgPath, String destPath, boolean needCompress) throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
        mkdirs(destPath);
        ImageIO.write(image, FORMAT_NAME, new File(destPath));
    }

    public static BufferedImage encode(String content, String imgPath, boolean needCompress) throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
        return image;
    }

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

    public static void encode(String content, String imgPath, String destPath) throws Exception {
        QRCodeUtil.encode(content, imgPath, destPath, false);
    }


    public static void encode(String content, String destPath) throws Exception {
        QRCodeUtil.encode(content, null, destPath, false);
    }

    public static void encode(String content, String imgPath, OutputStream output, boolean needCompress)
            throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
        ImageIO.write(image, FORMAT_NAME, output);
    }

    public static void encode(String content, OutputStream output) throws Exception {
        QRCodeUtil.encode(content, null, output, false);
    }

    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 hints = new Hashtable();
        hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
        result = new MultiFormatReader().decode(bitmap, hints);
        String resultStr = result.getText();
        return resultStr;
    }

    public static String decode(String path) throws Exception {
        return QRCodeUtil.decode(new File(path));
    }

}BufferedImageLuminanceSource
package com.lonely.alipay_demo.qrcode;

import com.google.zxing.LuminanceSource;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;

/**
* @author xiyang
*/
public class BufferedImageLuminanceSource extends LuminanceSource {

   private final BufferedImage image;
   private final int left;
   private final int top;

   public BufferedImageLuminanceSource(BufferedImage image) {
      this(image, 0, 0, image.getWidth(), image.getHeight());
   }

   public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) {
      super(width, height);

      int sourceWidth = image.getWidth();
      int sourceHeight = image.getHeight();
      if (left + width > sourceWidth || top + height > sourceHeight) {
         throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
      }

      for (int y = top; y < top + height; y++) {
         for (int x = left; x < left + width; x++) {
            if ((image.getRGB(x, y) & 0xFF000000) == 0) {
               image.setRGB(x, y, 0xFFFFFFFF);
            }
         }
      }

      this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);
      this.image.getGraphics().drawImage(image, 0, 0, null);
      this.left = left;
      this.top = top;
   }


   @Override
   public byte[] getRow(int y, byte[] row) {
      if (y < 0 || y >= getHeight()) {
         throw new IllegalArgumentException("Requested row is outside the image: " + y);
      }
      int width = getWidth();
      if (row == null || row.length < width) {
         row = new byte[width];
      }
      image.getRaster().getDataElements(left, top + y, width, 1, row);
      return row;
   }
   @Override
   public byte[] getMatrix() {
      int width = getWidth();
      int height = getHeight();
      int area = width * height;
      byte[] matrix = new byte[area];
      image.getRaster().getDataElements(left, top, width, height, matrix);
      return matrix;
   }
   @Override
   public boolean isCropSupported() {
      return true;
   }
   @Override
   public LuminanceSource crop(int left, int top, int width, int height) {
      return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height);
   }
   @Override
   public boolean isRotateSupported() {
      return true;
   }
   @Override
   public LuminanceSource rotateCounterClockwise() {
      int sourceWidth = image.getWidth();
      int sourceHeight = image.getHeight();
      AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);
      BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);
      Graphics2D g = rotatedImage.createGraphics();
      g.drawImage(image, transform, null);
      g.dispose();
      int width = getWidth();
      return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);
   }

}
内网穿透natapp
​       回调地址肯定要外网可以访问,要不然支付宝怎么会调用得到呢?但是我们作为开发者可能没有自己到服务器和域名,所以我们使用内网穿透工具natapp获取临时域名。
1、登陆natapp官网,完成注册并且购买免费的web隧道



2、购买后的主页



3、购买后配置隧道



4、下载客户端



5、启动
​       安装成功后解压安装包 在终端中启动natapp:
./natapp




注意:将natapp放在和config.ini(手动创建)同一个目录,在项目运行时 这个终端窗口不能关闭 一旦关闭 通过以上域名去访问boot项目的资源时会404 找不到该页面,映射完成之后,启动项目,用临时域名测试一下上面的页面
其中congif.ini文件如下,其中authtoken为隧道的token
#将本文件放置于natapp同级目录 程序将读取 [default] 段
#在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
#命令行参数 -config= 可以指定任意config.ini文件
[default]
authtoken=c3dc61f54e48fbf1                    #对应一条隧道的authtoken
clienttoken=                    #对应客户端的clienttoken,将会忽略authtoken,若无请留空,
log=none                        #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none
loglevel=ERROR                  #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
http_proxy=                     #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空
支付成功后的异步回调,实现订单的添加
​       当买家使用沙箱支付宝扫码以后,会拉起支付宝付款,当付款成功以后,支付宝会直接调用之前配置的notify_url异步回调API去判定交易信息是否成功,将订单数据添加到数据库中。
​       异步回调:异步通知是指在请求参数中传入notify_url参数,在用户支付成功后,支付宝服务器会按照这个异步地址使用post方式给notify_url来发送交易信息。可以实现订单状态修改等操作。
​       同步回调: 是指在请求参数中传入return_url参数,支付成功后跳转到return_url地址后携带的返回参数。只是为了给用户显示支付消息
1、在AlipayController中编写http://localhost:8080/alipay/pay/callback的API,这个API必须返回一个success或者false,否则支付宝会一直重复调用
/**
* 支付成功以后的异步回调API
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/alipay/pay/callback")
public String  notify_url(HttpServletRequest request) throws Exception{
     Boolean result =  alipayService.alipayCallback(request);
    if(result){
        return "success";
    }else{
        return "false";
    }
}2、在AlipayService编写alipayCallback接口
/**
* 支付成功后的回调函数
* @param request
* @return
*/
Boolean alipayCallback(HttpServletRequest request) throws Exception;3、在AlipayServiceImpl编写业务实现alipayCallback
@Override
public Boolean alipayCallback(HttpServletRequest request) {
    try {
        Map<String, String> params = ParamsUtil.ParamstoMap(request);
        logger.info("回调参数=========>" + params);
        String trade_no = params.get("trade_no");
        String body1 = params.get("body");
        logger.info("交易的流水号和交易信息===========>", trade_no, body1);
        JSONObject body = JSONObject.parseObject(body1);
        //String userId = body.getString("userId");
        String ptype = body.getString("payType");
        String orderNumber = body.getString("orderNumber");
        if (ptype != null && ptype.equals("1")) {
            payCommonService.payproductcourse(body, "1", orderNumber, trade_no, "1");
        }
    } catch (Exception e) {
        e.printStackTrace();
        logger.info("异常====>", e.toString());
        return false;
    }
    return true;
}4、完成订单添加,在payCommonService编写payproductcourse业务
package com.lonely.alipay_demo.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.lonely.alipay_demo.dao.KssOrderDetailDao;
import com.lonely.alipay_demo.entity.KssOrderDetail;
import com.lonely.alipay_demo.service.PayCommonService;
import com.lonely.alipay_demo.util.GenerateNum;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
* @Author: xiyang
* @FileName: payCommonServiceImpl
* @Date: Created in 2021/8/7 10:29
* @Vserion:
* @Description: TODO
*/
@Service
public class PayCommonServiceImpl implements PayCommonService {
    @Resource
    private KssOrderDetailDao kssOrderDetailDao;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void payproductcourse(JSONObject body, String userId, String orderNumber, String trade_no, String paymethod) {
        // 支付的课程
        String courseId = body.getString("courseId");
        // 支付的金额
        String money = body.getString("price");
        // 保存订单明细表
        KssOrderDetail orderDetail = new KssOrderDetail();
        orderDetail.setId(Long.valueOf(GenerateNum.generateOrder()));
//        orderDetail.setUserid(userId);
        orderDetail.setCourseid(courseId);
        orderDetail.setUsername("靓仔");
        orderDetail.setPaymethod(paymethod);
        orderDetail.setCoursetitle(body.getString("courseTitle"));
        orderDetail.setOrdernumber(orderNumber);
        orderDetail.setTradeno(trade_no);
        orderDetail.setPrice(money == null ? "0.01" : money);
        kssOrderDetailDao.insert(orderDetail);
    }
}5、在这个过程中需要paramsUtil工具类去解析支付宝带用异步回调API的参数
package com.lonely.alipay_demo.util;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
* @author xiyang
*/
public class ParamsUtil {
    /**
     * 将异步通知的参数转化为Map
     * @return
     */
    public static Map<String, String> ParamstoMap(HttpServletRequest request) throws UnsupportedEncodingException {
        Map<String, String> params = new HashMap<String, String>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for (Iterator<String> 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 : valueStr + values + ",";
            }
            // 乱码解决,这段代码在出现乱码时使用。
//            valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }
        return params;
    }
}到此为止,整个支付过程已经完成,可以去支付宝的开放平台检查沙箱环境中的卖家账户余额变化
注意事项
1 、可以在前端页面中采用轮询的方式通过商品去查询支付是否成功
2、本文中更多是偏向测试,若有逻辑不完善问题,请大家自行添加和完善
3、源码git地址为:https://github.com/LonelyXy/alipay_demo.git, 源码中删除了application-dev.yml,请自行创建.
回复

使用道具 举报

1

主题

6

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2022-9-20 13:01:44 | 显示全部楼层
感谢[爱]
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表