SpringBoot支付宝接入实战

文章目录

  • 支付宝支付后端实战——基于SpringBoot
    • 一、支付宝支付介绍及接入指引
      • 1. 支付宝开放能力介绍
        • (1)能力地图
        • (2)电脑网站支付产品
      • 2. 接入准备
        • (1)开放平台帐号注册
        • (2)常规接入流程
        • (3)使用沙箱
    • 二、项目的环境准备
      • 1.框架环境准备
      • 2. 数据库环境
      • 3. 支付宝沙箱支付配置文件
      • 4. 项目总配置文件
      • 5. 配置相关配置类
        • (1)Swagger接口配置类
        • (2)数据源相关配置
        • (3)配置mybatisplus
        • (4)配置支付宝支付客户端类
      • 6. 实体类相关配置
        • (1)实体类对象`BaseEntity`
        • (2)订单信息对象`OrderInfo`
        • (3)支付信息对象`paymentInfo`
        • (3)产品对象`Product`
        • (4)退款信息对象`RefundInfo`
        • (5)前后端交互信息对象`Results`
      • 7. 持久层相关配置
      • 8.定义相关的枚举配置
      • 9. 工具类准备
      • 10. 前端项目准备
    • 三、支付功能开发
      • 1. 统一收单下单并支付页面接口
        • (1)API预览
        • (2)创建支付业务层方法
        • (3)测试使用
        • (4)支付成功异步通知
          • a、安装并配置ngrok
          • b、异步通知参数API解析
          • c、异步返回结果验签
        • (5)异步通知验签成功商户系统处理
          • a、创建更新订单的方法
          • b、为支付创建支付日志
          • c、支付宝支付创建业务层处理订单的方法
          • d、控制层处理异步通知结果
          • e、接收异步通知后更新订单测试
          • f、过滤重复通知
          • g、添加数据锁
      • 2. 支付宝统一收单交易关闭接口
        • (1)API预览
        • (2)关闭订单
        • (3)测试
      • 3. 统一收单线下交易查询
        • (1)API预览
        • (2)主动查询订单
        • (3)定时查单之订单未创建
      • 4.统一收单交易退款接口
        • (1)API预览
        • (2)实现退款功能
        • (3)测试退款功能
      • 5.统一收单交易退款查询
        • (1)API预览
        • (2)退款查询功能实现
        • (3)测试结果
      • 6. 查询对账单下载地址
        • (1)API预览
        • (2)下载账单实现
        • (3) 测试下载账单功能

支付宝支付后端实战——基于SpringBoot

一、支付宝支付介绍及接入指引

1. 支付宝开放能力介绍

(1)能力地图

支付能力、支付扩展、资金能力、口碑能力、营销能力、会员能力、行业能力等等

登录支付宝开放平台即可:https://opendocs.alipay.com/home

(2)电脑网站支付产品

本次项目以电脑网站支付产品为例

SpringBoot支付宝接入实战_第1张图片

主要了解包括应用场景、准入条件、计费模式等内容。

2. 接入准备

(1)开放平台帐号注册

SpringBoot支付宝接入实战_第2张图片

使用支付宝帐号登录之后,进行开放平台帐号注册,选择入驻,期间会使用手机号接收验证码。

注册完成之后,就成为了一名开发者,可以自行创建应用了。

SpringBoot支付宝接入实战_第3张图片

(2)常规接入流程

  • 创建应用:选择应用类型,填写应用基本信息,添加应用功能,配置应用环境(获取支付宝公钥、应用公钥、应用私钥、支付宝网关地址。配置接口内容加密方式),查看APPID
  • 绑定应用:将开发者帐号中的APPID和商家帐号PID进行绑定(商家帐号的申请需要正规的营业执照)。
  • 配置密钥:创建应用中心的配置应用环境步骤
  • 上线应用:将应用提交审核
  • 签约功能:在商家中心上传营业执照,已备案网站信息等,提交审核进行签约。

(3)使用沙箱

本次应用实战主要基于沙箱环境下进行的,不需要涉及诸如营业执照、网站备案等信息的提交与审核,对于新手学习支付十分友好。

SpringBoot支付宝接入实战_第4张图片

  • 沙箱环境的配置

    • 获取对应的APPID以及PID(对应在沙箱环境中已经自动生成)
    • 设置接口加密的方式,选择系统默认的密钥,加密方式选择公钥加密,并查看相应的应用公钥、应用私钥以及支付宝公钥(以上均用于非对称加密过程中)。

SpringBoot支付宝接入实战_第5张图片

  • 获取支付宝网关地址

  • 设置接口内容加密方式(该内容主要用于应用接口的加密,自动生成)

  • 下载沙箱版支付宝并选择登录

SpringBoot支付宝接入实战_第6张图片

二、项目的环境准备

1.框架环境准备

  • SpringBoot项目,版本为2.3.12.RELEASE
  • JDK版本选择1.8
  • 导入web项目依赖spring-boot-starter-web
  • 导入热部署工具spring-boot-devtools
  • 导入Lombok:lombok
  • 导入SpringBoot的测试环境spring-boot-starter-test
  • 引入Spring中的项目事务管理模块spring-tx
  • 引入接口测试工具springfox-swagger2
  • 引入接口测试可视化工具springfox-swagger-ui
  • 引入数据库连接依赖mysql-connector-java
  • 引入持久层项目依赖mybatis-plus
  • 引入持久层工具依赖mybatis-plus-boot-starter
  • 引入数据库连接池依赖(德鲁伊)druid
  • 引入自定义元数据信息spring-boot-configuration-processor
  • 引入json数据处理依赖(谷歌)gson
  • 引入支付宝的SDK开发者工具alipay-sdk-java

对应的pom文件依赖为:

 <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
            <version>2.3.12.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-txartifactId>
        dependency>

        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>2.9.2version>
        dependency>
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
            <version>2.9.2version>
        dependency>


        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>


        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plusartifactId>
            <version>3.5.1version>
        dependency>

        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.5.1version>
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.8version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
        dependency>
        
        <dependency>
            <groupId>com.google.code.gsongroupId>
            <artifactId>gsonartifactId>
        dependency>


        <dependency>
            <groupId>com.alipay.sdkgroupId>
            <artifactId>alipay-sdk-javaartifactId>
            <version>4.22.57.ALLversion>
        dependency>

2. 数据库环境

  • 创建mysql数据,命名为payment_demo
  • 创建四张表,分别为:订单信息表、支付信息表、产品表、退款信息表

四张表内容如下所示:

CREATE TABLE `t_order_info` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `title` varchar(256) DEFAULT NULL COMMENT '订单标题',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(20) DEFAULT NULL COMMENT '支付产品id',
  `total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)',
  `code_url` varchar(50) DEFAULT NULL COMMENT '订单二维码连接',
  `order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;


/*Table structure for table `t_payment_info` */

CREATE TABLE `t_payment_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',
  `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',
  `trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',
  `trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',
  `payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',
  `content` text COMMENT '通知参数',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;


/*Table structure for table `t_product` */

CREATE TABLE `t_product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `title` varchar(20) DEFAULT NULL COMMENT '商品名称',
  `price` int(11) DEFAULT NULL COMMENT '价格(分)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

/*Data for the table `t_product` */

insert  into `t_product`(`title`,`price`) values ('Java课程',1);
insert  into `t_product`(`title`,`price`) values ('大数据课程',1);
insert  into `t_product`(`title`,`price`) values ('前端课程',1);
insert  into `t_product`(`title`,`price`) values ('UI课程',1);

/*Table structure for table `t_refund_info` */

CREATE TABLE `t_refund_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id',
  `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
  `refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
  `refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
  `total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
  `refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
  `reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
  `refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
  `content_return` text COMMENT '申请退款返回参数',
  `content_notify` text COMMENT '退款结果通知参数',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

注意:需要在产品信息表中添加四条信息

3. 支付宝沙箱支付配置文件

# 支付宝支付相关参数

# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=2021000120603279

# 商户PID,卖家支付宝账号ID
alipay.seller-id=2088621959241092

# 支付宝网关
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do

# 商户私钥,您的PKCS8格式RSA2私钥(应用私钥)
alipay.merchant-private-key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBvqvExLVwdcXKE9IaYI1oI5a57SMAZrwlCXw40g3+04PmNiIxfkKJVDzhqEm2OmlO5Wl45q2jwvm5UdqgKtwHIyFWt2hPJ/QRSGFFO/4NiUWkMVs5Q74jvAePapy434lxjhhtuZdHUjalNqkb21SJh22XQJl8hFf5mACHDl4hEw/YUC9DM94jZ+FsBctYLN1usQlIAUW2OVWWBeAJIWWjtk2fNjOQaZHWH+Y5dOd4x7OiHXvxuNpVFgxPcM6IsgarkNWMzZb7p7j9ymcw48d0JjOIhnW8qkrU/bskp6VCXjw0x2azvd/HYcfpSBjeFHUKNX5CMUpks1/k9CJWpeJTAgMBAAECggEAeJC99Xnv6ubvSZxh/9YbyTV0Y4lFYceMx4OKkRVubiiUCRug1anbn/gS1t5R2Juq0tUCeKEcZy87Fe7xHQDu4WYkJgGGYNPdFzAyj9IQe73z34RzX0Rfu38UOVQ/6O/6aPbjDs0SbeikZtWIEPTBO8BSG3Cw0wLMeF713RW8z9kkQZOPaiixZVPLoFTIL4KuCZhYdJK2RRchYuZnEYHRRFAqFoKN1jII5pR8EyxmvocFx7UJ7idRGrSWc1UB5xEyn2emYyiTu3uaVaa49ecBNZqvRRdAoHcVQOGIYiUSNlrqDYVOLOicdSOlO6bS6jmRk41pdgdze089uYT/ilh0iQKBgQD+RIS3dGUvVk5AysqzoA0v7zYEixMeqALrAxYKAP02bIHGISw39+O4Q2GbzKUqDt6dmGPBlQ9jW/o9h0zqLf+7aFiEEIystbD6gx3TeWAVDyoF9zNfFOCapKaTbDZ2BGj3P/CpFLm7DNUyk9f03/BykqskXJ+4ZeeYUtqg+iofhwKBgQDDEJdrCrwBQEcjDIEcWZQaN09d98VgmWLv3L0EQ230YrcsLL1rJFJOzQr3zSf1ibY9S/zrrIFlb0Vq9qUBNbLN7GY8nXjrmoordgdYwHwTYoGa+R0Rv5yB24tKkra4ZQBCD1au1+0Klgc3mhJgKJ8HrRwH1UexDLukEABthfzh1QKBgCtSRUJ0hGDiVYbYhlzAYj7OhOeVQnawrX6ZEgI2VO4W4q19LWmDxLq6UEEZRvK5gdhcBHMREIQfQa2GBebIW4/0oVAu+ajbdAHaoRRM08ACy2gkzA3hIrt2XiM0Brto2PF3ZWuJanOiJhjt85d3KCJ9NseFOHlUc3cSdsmClfa1AoGAT1F60Mr/od6aTpUyFu4R/AsLmeE7gEk+4tw2e/pTRrGxXCQhLeUKFwLnd9YTbpN96DTy9n4h67YwWwtKE1DbkUKUXAeIeP1RO9T1rdAvY86FdxffCy2IHYHBhSRdamOflD0aeWRR/iD9dE2RNUqvR/bLVCAU09iioFblZaO7LbUCgYEA43FByiYEuVCiGEJD7eRSwRsN2lCJS3ZxoE60iKIeTxs5uWQIBE0bTkqvb35JUBdeuAlB0H0GPZaEO9yL3tPL0i+PbMmpBPaZ6tnOk6Pc9sREQw2PIlrpscWHU2gL2BodKCDVOe2rW3Der39MrFMhW6yoTjCFBRW6qGaM5llVQEc=

# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApX9WFCjD08ErIlv1WvSF5hgM6kt9D+UJYYIDoMHpMw886DlFUiqPLYb+ZTy5eEE7N7TBS4Wl3NaUvsY2Z3dlwSk3HBpogsskgScm+qmdIEm/hEXL7xVB7WG7GD/M/ko8uihwQmH3WjOe9NU8HWUT4N4B6vwU6KrR6IHAmoPQ86zqWuQbUPrKZMZczhnF4uUcp+7DzpSWkz91U/TKdW18lFB7md8cwHEvKiQe23OEJMNS4utwDhaWIYhATxrxaEW5Yfj2VPt9NnaBbYYC2FUtHL4NLnJCF6uTgUuXzPauedeushS3WF0+mDrV8oRTKnBDtg6lF3JTrFoiocDJ076YlwIDAQAB

# 接口内容加密秘钥,对称秘钥
alipay.content-key=D8entyfafkkFwtMbUqj3Mw==

# 页面跳转同步通知页面路径
alipay.return-url=http://localhost:8080/#/success

# 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://2b4a-202-192-72-1.jp.ngrok.io/api/ali-pay/trade/notify

以上沙箱支付配置文件信息均来自于支付宝开放平台官网为个人生成的相关信息。(主要涉及APPID、PID,应用私钥、支付宝公钥)

同时将该文件加入到项目中的resources文件夹下,同时选择project structure,选择相应的配置(如下图所示)。需要选择该properties文件,设置到项目中即可。

SpringBoot支付宝接入实战_第7张图片

4. 项目总配置文件

server:
  port: 8090


spring:
  application:
    name: payment-demo

  thymeleaf:
    cache: false
  jackson:
    date-format: yyyy:MM:dd HH:mm:ss  # 定义json的时间格式
    time-zone: GMT+8

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/payment_demo?CharacterEncoding=utf87serverTimeZone=GMT%2B8&useUnicode=true
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource


mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志
  mapper-locations: classpath:com/example/mapper/xml/*.xml #配置xml文件的地址

logging:
  level:
    root: info
  • 设置对应的端口为8090
  • 设置应用名称为:payment-demo
  • 关闭thymeleaf模板引擎并且设置json的日期格式
  • 配置数据源,设置用户名密码以及对应的url地址
  • 设置mybatis-plus的配置信息,设置标准的日志输出,设置持久层的xml文件地址
  • 最后设置日志的输出级别为info,一般为info

5. 配置相关配置类

(1)Swagger接口配置类

该类主要用于接口测试的相关配置

@Configuration
@EnableSwagger2
public class Swagger2Config {
    ApiInfoBuilder apiInfoBuilder=new ApiInfoBuilder();

    @Bean
    public Docket getDocket(){
        return new Docket( DocumentationType.SWAGGER_2)
                .apiInfo(apiInfoBuilder.title("支付宝支付案例").description("payment -demo").build());
    }

}
  • 使用@EnableSwagger2注解,开启swagger2的服务
  • 使用@Bean表示将该方法的返回值对象注入到IOC容器中

(2)数据源相关配置

配置德鲁伊的数据源(SpringBoot的项目默认配置的不是德鲁伊数据源)

@Configuration
public class DataSourceConfig {

    /**
     * @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
     *        前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
     * @return
     *
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource getDataSource(){
        return new DruidDataSource();
    }
}

使用@ConfigurationProperties(prefix = "spring.datasource")实现配置类与配置文件的映射关系(完成属性值的自动注入)

(3)配置mybatisplus

@Configuration //定义为配置类
@MapperScan("com.example.mapper") //扫描mapper接口
@EnableTransactionManagement //启用事务管理(spring-tx)
public class MyBatisPlusConfig {
}

(4)配置支付宝支付客户端类

@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AliPayClientConfig {

    /**
     * 利用Environment对象获取配置文件alipay-sandbox.properties文件中的所有内容
     */
    @Resource
    private Environment config;


    /**
     * 创建一个获取AlipayClient对象的方法,用于封装签名的自动实现
     * @return AlipayClient
     */
    @Bean
    public AlipayClient getAlipayClient() throws AlipayApiException {
        //创建alipay配置对象,并设置相应的参数
        AlipayConfig alipayConfig = new AlipayConfig();
    //设置网关地址
        alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
    //设置应用Id
        alipayConfig.setAppId(config.getProperty("alipay.app-id"));
    //设置应用私钥
        alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
    //设置请求格式,固定值json
        alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
    //设置字符集
        alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
    //设置支付宝公钥
        alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
    //设置签名类型
        alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
    //构造client
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);

        return alipayClient;
    }

}
  • 使用@PropertySource("classpath:alipay-sandbox.properties")定位到配置文件的位置

  • 利用@Resource注解注入Evironment对象,用于读取指定位置的配置文件信息的相关内容,通过getProperties(xxx)获取指定项的配置信息。

  • 利用公有方法生成自定义的bean对象,注入到IOC容器中

  • 以上返回的AlipayClient对象会自动为请求生成签名,并对响应完成验签操作

6. 实体类相关配置

(1)实体类对象BaseEntity

@Data
public class BaseEntity {

    /**
     * 主键
     */
    @TableId(value = "id",type = IdType.AUTO)
    private String id;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 更新时间
     */
    private Date updateTime;
}
  • 该类中定义了后续所有实体类中所共有的部分属性。例如id以及创建时间、更新时间
  • 使用@TableId(value = "id",type = IdType.AUTO)注解为该属性设置dao层映射,value值与数据库中的id对应,并且设置主键的自增策略。

(2)订单信息对象OrderInfo

@Data
@TableName("t_order_info")  //表示指定表名
public class OrderInfo extends BaseEntity{

    /**
     * 订单标题
     */
    private  String title;
    /**
     * 订单编号
     */
    private  String orderNo;
    /**
     * 用户ID
     */
    private  Long userId;
    /**
     * 产品ID
      */
    private  Long productId;
    /**
     * 订单金额
     */
    private  Integer totalFee;
    /**
     * 订单二维码链接
     */
    private  String codeUrl;
    /**
     * 订单状态
     */
    private String orderStatus;
}
  • 使用@TableName("t_order_info")表示映射到数据库的具体名称的表中
  • 继承之前的公共的BaseEntity对象

(3)支付信息对象paymentInfo

@Data
@TableName("t_payment_info")
public class PaymentInfo extends BaseEntity{
    /**
     * 订单编号
     */
    private String orderNo;
    /**
     * 交易系统支付编号
     */
    private  String transactionId;
    /**
     * 支付类型
     */
    private String paymentType;
    /**
     * 交易类型
     */
    private  String tradeType;
    /**
     * 交易状态
     */
    private String tradeState;
    /**
     * 支付金额
     */
    private Integer payerTotal;
    /**
     * 通知参数
     */
    private  String content;


}
  • 设置@TableName("t_payment_info")表示将该对象映射到数据库的具体的表中
  • 设置继承自BaseEntity

(3)产品对象Product

@Data
@TableName("t_product")
public class Product extends BaseEntity{
    /**
     * 产品名称
     */
    private  String title;
    /**
     * 产品价格
     */
    private  Integer price;

}
  • 设置@TableName("t_product")表示映射到数据库的具体的表中
  • 继承BaseEntity

(4)退款信息对象RefundInfo

@Data
@TableName("t_refund_info")
public class RefundInfo extends BaseEntity {
    /**
     * 商品订单编号
     */
    private String orderNo;
    /**
     * 商品退款编号
     */
    private String refundNo;
    /**
     * 支付系统退款单号
     */
    private String refundId;
    /**
     * 原订单金额
     */
    private Integer totalFee;
    /**
     * 退款金额
     */
    private  Integer refund;
    /**
     * 退款原因
     */
    private  String reason;
    /**
     * 退款单状态
     */
    private String refundStatus;
    /**
     * 申请退款返回参数
     */
    private String contentReturn;
    /**
     * 退款结果通知参数
     */
    private  String contentNotify;

}

  • 设置@TableName("t_refund_info")表示映射到具体的某张表中
  • 继承BaseEntity

(5)前后端交互信息对象Results

/**
 * @author lambda
 * 该类用于前后端交互,为前端设置一个标准的响应结果
 * 即该类设置了需要交给前端的数据
 *
 */
@Data
@Accessors(chain = true)
public class Results {

    /**
     * 响应码
     */
    private Integer code;
    /**
     * 响应消息
     */
    private String message;
    /**
     * 封装其他信息
     */
    private Map<String, Object> data =new HashMap<>();


    /**
     * 用于返回正确的结果显示
     * @return Results 表示返回数据对象
     */
    public static Results returnOk(){
        Results results = new Results();
        results.setCode(0);
        results.setMessage("Succeed!");
        return results;

    }

    /**
     * 返回错误的显示信息
     * @return Results
     */
    public static  Results returnError(){
        Results results = new Results();
        results.setCode(-1);
        results.setMessage("Failed");
        return results;
    }


    /**
     * 用于返回k-v的信息
     * @param  key 给前端传递的键
     * @param  value 给前端传递的值
     * @return Results
     */
    public Results returnData(String key,Object value){
        this.data.put(key, value);
        return this;
    }
}

7. 持久层相关配置

主要是对订单信息、支付信息、产品信息、以及退款信息作持久层的设置

  • OrderInfoMapper用于处理数据库订单信息
@Mapper
public interface OrderInfoMapper  extends BaseMapper<OrderInfo> {
}

不需要编写任何的增删改查方法,因为继承了BaseMapper,并且指定了泛型的类型为OrderInfo。@Mapper注解表示该接口的实现对象交由MyBatisPlus底层去实现。

  • PaymentInfoMapper用于处理支付信息
@Mapper
public interface PaymentInfoMapper extends BaseMapper<PaymentInfo> {
}

不需要编写任何的增删改查方法,因为继承了BaseMapper,并且指定了泛型的类型为PaymentInfo。@Mapper注解表示该接口的实现对象交由MyBatisPlus底层去实现。

  • ProductMapper用于处理产品信息
@Mapper
public interface ProductMapper extends BaseMapper<Product> {

}

不需要编写任何的增删改查方法,因为继承了BaseMapper,并且指定了泛型的类型为Product。@Mapper注解表示该接口的实现对象交由MyBatisPlus底层去实现。

  • RefundInfoMapper用于处理退款信息
@Mapper
public interface RefundInfoMapper extends BaseMapper<RefundInfo> {
}

不需要编写任何的增删改查方法,因为继承了BaseMapper,并且指定了泛型的类型为RefundInfo。@Mapper注解表示该接口的实现对象交由MyBatisPlus底层去实现。

8.定义相关的枚举配置

枚举配置主要针对订单状态、支付类型设置

  • 订单状态
@AllArgsConstructor
@Getter
public enum OrderStatus {
    /**
     * 未支付
     */
    NOTPAY("未支付"),


    /**
     * 支付成功
     */
    SUCCESS("支付成功"),

    /**
     * 已关闭
     */
    CLOSED("超时已关闭"),

    /**
     * 已取消
     */
    CANCEL("用户已取消"),

    /**
     * 退款中
     */
    REFUND_PROCESSING("退款中"),

    /**
     * 已退款
     */
    REFUND_SUCCESS("已退款"),


    /**
     * 退款异常
     */
    REFUND_ABNORMAL("退款异常");

    /**
     * 类型
     */
    private final String type;


}
  • 支付类型
@AllArgsConstructor
@Getter
public enum PayType {
    /**
     * 微信
     */
    WXPAY("微信"),


    /**
     * 支付宝
     */
    ALIPAY("支付宝");

    /**
     * 类型
     */
    private final String type;
}
  • 支付宝端交易状态
public enum AliPayTradeState {
    /**
     * 支付成功
     */
    SUCCESS("TRADE_SUCCESS"),
    /**
     * 未支付
     */
    NOTPAY("WAIT_BUYER_PAY"),
    /**
     * 订单关闭
     */
    CLOSED("TRADE_SUCCESS"),
    /**
     * 退款成功
     */
    REFUND_SUCCESS("REFUND_SUCCESS"),

    /**
     * 退款失败
     */
    REFUND_ERROR("REFUND_ERROR");


    private final String type;

   private AliPayTradeState(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

9. 工具类准备

  • 获取订单信息的工具类
public class OrderNoUtils {

    /**
     * 获取订单编号
     * @return
     */
    public static String getOrderNo() {
        return "ORDER_" + getNo();
    }

    /**
     * 获取退款单编号
     * @return
     */
    public static String getRefundNo() {
        return "REFUND_" + getNo();
    }

    /**
     * 获取编号
     * @return
     */
    public static String getNo() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String newDate = sdf.format(new Date());
        String result = "";
        Random random = new Random();
        for (int i = 0; i < 3; i++) {
            result += random.nextInt(10);
        }
        return newDate + result;
    }

}

  • 关于http的工具类(只有一个静态方法用户读取请求的数据信息)
public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

10. 前端项目准备

前端项目的内容大致如下:

SpringBoot支付宝接入实战_第8张图片

三、支付功能开发

电脑网站支付的支付接口 alipay.trade.page.pay(统一收单下单并支付页面接口)调用时序图如下:

SpringBoot支付宝接入实战_第9张图片

1. 统一收单下单并支付页面接口

(1)API预览

主要使用alipay.trade.page.pay,即时序图中的1.1,发起支付请求

  • 以下是来自支付宝官方的设置的请求参数,其中部分参数在上述的AlipayClient对象中已经封装完成,部分需要后续设置。

SpringBoot支付宝接入实战_第10张图片

  • 以下是公共请求参数中的biz_content内容,该内容是具体的请求参数,需要我们手动设置。(均为必填项目)

SpringBoot支付宝接入实战_第11张图片

  • 以下是支付宝开放平台提供的公共响应参数,需要后续设置

SpringBoot支付宝接入实战_第12张图片

  • 以下是支付宝开放平台提供的响应参数(与具体业务相关的一些响应内容),也需要后续设置

SpringBoot支付宝接入实战_第13张图片

(2)创建支付业务层方法

a、首先是编写针对订单信息的服务层类以及方法(主要是根据产品号创建订单、通过产品号在数据库中查询订单)

public interface OrderInfoService  extends IService<OrderInfo> {
    
    
      /**
     * Create order by product id order info.
     * 根据产品的id生成对应的订单信息
     *
     * @param productId the product id
     * @return the order info
     */
    OrderInfo createOrderByProductId(Long productId);

}

//对应的实现类为:OrderInfoServiceImpl
@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
    
    @Resource
    private ProductMapper productMapper;

    @Resource
    private OrderInfoMapper orderInfoMapper;

    
     /**
     * 根据产品id创建订单信息
     * @param productId the product id
     * @return 订单信息
     */
    @Override
    public OrderInfo createOrderByProductId(Long productId) {

        //查找已存在,但是并未支付的订单信息
        OrderInfo orderInfoNoPay = getNoPayOrderByProductId(productId);
        if (orderInfoNoPay!=null){
            return orderInfoNoPay;
        }
        //获取商品的对象
        Product product = productMapper.selectById(productId);

        //生成订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setTitle(product.getTitle());
        //订单号
        orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
        orderInfo.setTotalFee(product.getPrice());
        orderInfo.setProductId(productId);
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());

        //将订单信息存入数据库
        orderInfoMapper.insert(orderInfo);

        return orderInfo;
    }

    
     /**
     * 该方法用于获取用户未支付的订单(由于只在该类中使用,所以定义为私有方法)
     * @param productId
     * @return
     */
    private  OrderInfo getNoPayOrderByProductId(Long productId){
        //使用MyBatis-plus的查询器
        QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
        //设置判断条件,id和类型信息
         orderInfoQueryWrapper.eq("product_id", productId);
         orderInfoQueryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());

         //使用自带的selectOne方法判断是否同时满足条件
        OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoQueryWrapper);
        return orderInfo;

    }
    
}
  • OrderInfoService也不需要书写任何方法,所有的增删该查分页等操作都是由IService完成 ,并且继承的IService接口的泛型是对应的实体类。我们只需要设置相应的具体业务方法即可。
  • getNoPayOrderByProductId用于根据产品信息获取用户未支付的订单。使用了 QueryWrapper查询器
  • 同时需要注入持久层操作对象productMapper以及orderInfoMapper

b、查看统一收单下单并支付页面接口开发文档

SpringBoot支付宝接入实战_第14张图片

需要我们编写对应格式的代码向支付宝平台发起支付请求。

c、编写对应的请求业务方法

//对应的业务层接口
public interface AliPayService {
    /**
     * Trade create string.
     * 创建支付宝支付订单
     *
     * @param productId the product id
     * @return the string
     */
    String tradeCreate(Long productId);
}
//对应业务层实现方法
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private  AlipayClient alipayClient;
    
     @Resource
    private  Environment config;//配置环境参数

    /**
     * 根据订单号创建订单并发起支付请求获取平台响应返回到前端
     * @param productId the product id
     * @return 返回支付请求调用的响应主体信息,返回到controller层
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String tradeCreate(Long productId)  {
        try {


            log.info("生成订单....");
            //调用orderInfoService对象在数据库中创建订单
            OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);

            //调用支付宝接口
            //创建支付宝请求对象
            AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
             //设置请求处理完成后的跳转的地址
            request.setReturnUrl(config.getProperty("alipay.return-url"));
            //创建具体请求参数对象,用于组装请求信息
            JSONObject bizContent = new JSONObject();
            //设置商户订单号
            bizContent.put("out_trade_no", orderInfo.getOrderNo());
            //设置订单总金额,由于订单金额单位为分,而参数中需要的是元,因此需要bigDecimal进行转换
            BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
            bizContent.put("total_amount", total);
            //设置订单标题
            bizContent.put("subject", orderInfo.getTitle());
            //设置支付产品码,比较固定(电脑支付场景下只支持一种类型)
            bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");

            //设置完成后,将bizContent具体请求对象转换成json并放置在请求中
            request.setBizContent(bizContent.toString());

            //利用alipay客户端执行请求
            AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
            //判断请求是否成功
            if (response.isSuccess()){
                //打印响应信息主体
                log.info("调用成功====》{}",response.getBody());
            }else {
                log.info("调用失败====》{},返回码"+response.getCode()+",返回描述为:"+response.getMsg());
                throw new RuntimeException("创建支付交易失败.....");
            }
        return response.getBody();
        }catch (AlipayApiException e){
            throw new RuntimeException("创建支付交易失败.....");
        }
    }
}

  • 首先注入OrderInfoServicealipayClient对象,用于相应的业务处理。
  • 其次调用OrderInfoService的通过订单号创建订单方法,新生成一个订单(具体的创建流程参考OrderInforServiceImpl类)
  • 接着创建一个向支付宝发起支付请求的请求对象AlipayTradePagePayRequest,同时使用JsonObject类创建一个请求参数对象,用于设置请求的参数,具体的参数值需要从新创建的订单信息中获取。同时利用config设置支付完成的返回地址
  • 再次,将组装好的请求参数对象设置到AlipayTradePagePayRequest请求对象中。
  • 最后利用alipayClient对象执行请求,执行方法为pageExecute。得到一个请求的响应对象AlipayTradePagePayResponse,后续对请求的响应对象进行处理,如果返回的响应成功,则将请求响应的主体信息返回到controller层,否则抛出异常提示。

d、编写控制层跳转方法

@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {

    @Resource
    private AliPayService aliPayService;

    @ApiOperation("统一收单下单并支付页面接口")
    @PostMapping("/trade/page/pay/{productId}")
    public Results tradePagePay(@PathVariable Long productId){
        log.info("统一收单下单并支付页面接口");
        //发起交易请求后,会返回一个form表单格式的字符串(要有前端渲染)
        String formStr=aliPayService.tradeCreate(productId);
        //最后需要将支付宝的响应信息传递给前端,到前端之后会自动提交表单到action指定的支付宝开放平台中
        //从而为用户展示支付页面。
        return Results.returnOk().returnData("formStr",formStr);
    }
}
  • @CrossOrigin设置请求跨域访问
  • 注入AliPayService对象依赖
  • 调用AliPayService对象的创建交易方法tradeCreate(具体实现在AliPayServiceImpl类中)
  • 最后返回一个form表单形式的json字符串,将最终的信息返回给前端渲染成form表单形式并执行自动提交脚本。发起支付成功

(3)测试使用

a、首先在swagger上执行测试

访问;http://localhost:8090/swagger-ui.html页面

执行controller层请求,得到如下信息(返回的form表单)

SpringBoot支付宝接入实战_第15张图片

b、启动前端项目

访问http://localhost:8080,即可看到前台的选择产品页面

SpringBoot支付宝接入实战_第16张图片

可以选择支付宝支付

c、选择一项课程点击确认支付,即可跳转到相应的支付页面

SpringBoot支付宝接入实战_第17张图片

可以使用沙箱帐号登陆支付

SpringBoot支付宝接入实战_第18张图片

输入支付密码成功支付

SpringBoot支付宝接入实战_第19张图片

也可以选择使用扫码完成支付

SpringBoot支付宝接入实战_第20张图片

使用沙箱版支付宝在手机端完成支付。

支付成功后跳转页面如下

但是依旧存在问题,买家已经付款,但是卖家的订单信息仍旧未更新(即使是支付成功,但后台数据库是显示未支付),如下图所示:

SpringBoot支付宝接入实战_第21张图片

原因在于:用户付款成功后,支付宝尚未对商户发起异步的通知结果,商户未收到支付宝的付款通知,自然也就不会对订单信息进行更新。

(4)支付成功异步通知

支付成功异步通知主要是支付宝端向商户端发送结果通知,由于商户的网络环境是处于局域网,因此支付宝平台要想通知到商户必须要进行内网穿透。使用ngrok工具来进行内网穿透。

a、安装并配置ngrok
  • 首先下载安装配置ngrok

前往ngrok的官网登陆后下载,官网地址:https://ngrok.com/download

此次测试基于linux环境下测试,因此,下载linux系统下的压缩包。

打开终端解压至/usr/local/bin目录下

sudo tar xvzf ~/Downloads/ngrok-v3-stable-linux-amd64.tgz -C  /usr/local/bin

之后测试ngrok是否安装成功

ngrok -v
ngrok version 3.0.3
# 此时表明ngrok安装成功
  • 其次连接指定的账户(新建立的账户)
 ngrok config add-authtoken 29ds9En84SWW7uOuqwIEMvjWnAy_71i4aLWQpTjoAXnsuVuEX

该操作会在.config/ngrok目录下生成ngrok.yml配置文件

SpringBoot支付宝接入实战_第22张图片

  • 将本机的某个需要穿透的端口开放即可(由于本次项目中端口基于8090,因此开放8090端口)
ngrok http 8090
ngrok                                                                                                            (Ctrl+C to quit)
                                                                                                                                 
Session Status                online                                                                                             
Account                       binbin (Plan: Free)                                                                                
Version                       3.0.3                                                                                              
Region                        Japan (jp)                                                                                         
Latency                       calculating...                                                                                     
Web Interface                 http://127.0.0.1:4040                                                                              
Forwarding                    https://2b4a-202-192-72-1.jp.ngrok.io -> http://localhost:8090                                     
                                                                                                                                 
Connections                   ttl     opn     rt1     rt5     p50     p90                                                        
                              0       0       0.00    0.00    0.00    0.00  

需要注意的是每次启动ngrok,对应的内网穿透地址都会发生改变。需要在配置文件中进行相应的更改

b、异步通知参数API解析

对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商户传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统。

其中也包括两部分内容,一部分是公共参数,一部分是业务参数。

公共参数部分

SpringBoot支付宝接入实战_第23张图片

其中的sign表示签名,后续商户需要对签名进行验证,如果确认是支付宝的通知,则进行操作,如果不是则不予理会。

业务参数部分

SpringBoot支付宝接入实战_第24张图片

此外需要特别注意的是:

商户的程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知,直到超过 24 小时 22 分钟。一般情况下,25 小时以内完成 8 次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h)。

如果商户处理异步通知请求失败,则向支付宝端返回“failure”。

c、异步返回结果验签
  • 首先是对异步通知的初次校验(下方为主要的业务逻辑处理流程)

此处是由商户端对支付宝从远端发来的异步通知结果进行签名验证(支付宝公钥进行验证,因为是非对称加密),如果确认是支付宝平台发送的,则证明可以执行相关操作。

Map<String, String> paramsMap = ... //将异步通知中收到的所有参数都存放到map中
boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, ALIPAY_PUBLIC_KEY, CHARSET, SIGN_TYPE) //调用SDK验证签名
if(signVerified){
    // TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
}else{
    // TODO 验签失败则记录异常日志,并在response中返回failure.
}
  • 其次对异步通知进行二次校验,主要校验订单号、订单金额、对应操作方、以及商户id(下方为主要的业务处理逻辑)
 String result = "failure";

            //异步通知验签(使用我们引入的支付宝SDK验证签名)
            //一个是异步通知的结果参数,一个是支付宝的公钥,一个是字符集,一个是加密方式,得到一个布尔值结果
            boolean signVerified = AlipaySignature.rsaCheckV1(params, config.getProperty("alipay.alipay-public-key"),
                    AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);

            //对签名结果进行判断
            if (!signVerified) {
                //TODO:验签失败则记录异常日志,并在response中返回failure
                log.error("支付成功,异步通知验签失败......");
                return result;
            }

            //TODO:验证成功,按照支付结果异步通知中的描述,
            // 对支付结果中的业务内容进行二次校验,
            // 校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure

            log.info("支付成功,异步通知验签成功.......");
            //1.商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;
            //获取对应的订单号
            String outTradeNo = params.get("out_trade_no");
            //利用获取的订单号查询对应的订单信息(返回一个订单对象)
            OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
            //对订单对象进行判断
            if (order==null){
                log.error("订单不存在......");
                return result;
            }

            //2.判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
            //从参数中获取金额(单位为元),但是数据库中的单位为分,因此需要进行转换
            String totalAmount = params.get("total_amount");
            int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
            //获取订单中的金额
            int totalFeeInt = order.getTotalFee();
            if (totalFeeInt!=totalAmountInt){
                //如果不等,则说明金额不对
                log.error("金额校验失败");
                return result;
            }

            //3.校验通知中的 seller_id(对应商户的PID)(或者 seller_email) 是否为 out_trade_no
            // 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email)
            String sellerId = params.get("seller_id");
            //获取实际的商户PID
            String pid = config.getProperty("alipay.seller-id");
            if (!sellerId.equals(pid)){
                //用商户的PID与参数中的sellerID进行比较
                log.error("商家PID校验失败....");
                return result;
            }

            //4.验证 app_id 是否为该商户本身
            String appId = params.get("app_id");
            String appIdProperty = config.getProperty("alipay.app-id");
            if (!appId.equals(appIdProperty)){
                log.error("appId校验失败");
                return result;
            }

            //在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS
            // 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
            //获取交易状态
            String tradeStatus = params.get("trade_status");
            if (!"TRADE_SUCCESS".equals(tradeStatus)){
                //如果不满足交易状态成功参数,则直接返回failure
                log.error("支付未成功....");
                return result;
            }
            //以上4步校验成功后设置为success,之后返回结果,商户可以自身进行后续的处理
            //商户处理自身业务
            
            result = "success";
		return result;

(5)异步通知验签成功商户系统处理

异步通知结果确认无误之后,商户系统需要对现有记录的订单进行处理,商户系统的处理主要包括:处理业务、修改订单状态、记录支付日志等等。

a、创建更新订单的方法

由于在商户更新其系统信息之时,需要更新对应的订单状态,因此需要在对应的订单信息处理的类中设置相应的方法处理订单状态的更新。

// 对应OrderInfoService接口添加新方法
public interface OrderInfoService  extends IService<OrderInfo> {

    /**
     * Create order by product id order info.
     * 根据产品的id生成对应的订单信息
     *
     * @param productId the product id
     * @return the order info
     */
    OrderInfo createOrderByProductId(Long productId);
    /**
     * Update status by order no.
     * 根据订单号更新数据库中的订单状态
     *
     * @param orderNo     the order no
     * @param orderStatus the order status
     */
    void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);
}

//对应OrderInfoServiceImpl的实现方法

@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
 xxxxxxxx
 /**
     * 此方法用于根据订单编号来更新数据库中的订单状态
     * @param orderNo 订单编号
     * @param orderStatus 成功响应码
     */
    @Override
    public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
        log.info("更新数据库中的订单状态=======>"+orderStatus.getType());
        //创建一个查询条件,主要针对OrderInfo订单信息
        QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
        //编写查询条件
        orderInfoQueryWrapper.eq("order_no",orderNo);
        //创建一个订单信息对象
        OrderInfo orderInfo = new OrderInfo();
        //设置要更新的订单状态
        orderInfo.setOrderStatus(orderStatus.getType());
        //执行更新操作
        orderInfoMapper.update(orderInfo,orderInfoQueryWrapper);


    }
}
  • 此处主要涉及订单号以及订单状态两个信息,作为参数传入。
  • 依旧适用QueryWrapper查询条件来对对应的订单信息作等值查询,并同时更新订单状态。
b、为支付创建支付日志
//创建支付日志接口并设置相应的方法
public interface PaymentInfoService {

    /**
     * Create payment info for ali pay.
     *为支付创建日志记录
     * @param params the params
     */
    void createPaymentInfoForAliPay(Map<String, String> params);

}
//为支付日志接口创建实现类,并实现创建支付日志方法
@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
     @Resource
    private PaymentInfoMapper paymentInfoMapper;
    
      /**
     * 记录支付宝的支付日志
     * @param params the params 支付通知参数
     */
    @Override
    public void createPaymentInfoForAliPay(Map<String, String> params) {
    
       log.info("记录支付宝支付日志.....");
        //创建支付信息对象
        PaymentInfo paymentInfo = new PaymentInfo();
        paymentInfo.setOrderNo(params.get("out_trade_no"));
        paymentInfo.setPaymentType(PayType.ALIPAY.getType());
        //设置业务编号(支付宝对应的是trade_no)
        paymentInfo.setTransactionId(params.get("trade_no"));
        //设置支付的场景
        paymentInfo.setTradeType("电脑网站支付");
        //设置交易状态
        paymentInfo.setTradeState(params.get("trade_status"));
        //设置交易金额,此处依旧需要转换(支付宝端对应的是元,数据库中对应分)
        int totalAmount=new BigDecimal(params.get("total_amount")).multiply(new BigDecimal("100")).intValue();
        paymentInfo.setPayerTotal(totalAmount);

        //之后设置备注信息,需要将平台传入的map集合信息转成字符串类型存入数据库
        Gson gson = new Gson();
        String content = gson.toJson(params, HashMap.class);
        paymentInfo.setContent(content);
        //将信息插入数据库中
        paymentInfoMapper.insert(paymentInfo);


    }
}
  • createPaymentInfoForAliPay方法需要传入平台传递给我们的正确的参数,是一个集合类型的。
  • 利用 @Resource注解在类中注入 PaymentInfoMapper paymentInfoMapper的对象
  • 在实现方法中创建一个新的支付信息对象,从params集合中获取相应的信息设置到该对象中,最后利用paymentInfoMapperinsert方法插入到数据库中(insert方法已有MyBatisPlus实现)
c、支付宝支付创建业务层处理订单的方法

此方法主要用于订单处理(接收到支付宝异步通知后验签成功的订单处理)

// 异步通知处理接口方法processOrder
public interface AliPayService {
 xxx

    /**
     * Process order.
     *订单处理方法
     * @param params the params
     */
    void processOrder(Map<String, String> params);
}

//对应接口的实现方法
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private  AlipayClient alipayClient;

    @Resource
    private  Environment config;

    @Resource
    private PaymentInfoService paymentInfoService;
    xxxxx
/**
     * 商户系统订单处理
     * @param params 支付宝平台异步通知传递的参数
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processOrder(Map<String, String> params) {
        log.info("处理订单.......");
        //获取传递信息中的订单号
        String orderNo = params.get("out_trade_no");
        //更新订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
        //记录支付日志
        paymentInfoService.createPaymentInfoForAliPay(params);
    }
  • 创建对应的处理订单的方法,方法中需要传入一个集合类型的参数。
  • 在对应的处理方法中注入 PaymentInfoService paymentInfoService用于支付信息处理。
  • 紧接着调用更新订单状态方法,之后调用支付信息接口创建支付信息日志(传入集合参数)。
  • @Transactional(rollbackFor = Exception.class)表示当遇到对应的异常时候进行回滚操作。
d、控制层处理异步通知结果
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {

    @Resource
    private AliPayService aliPayService;

    @Resource
    private Environment config;

    @Resource
    private OrderInfoService orderInfoService;
    
    /**
     * 支付宝异步通知处理结果
     * @param params 支付宝异步通知发过来的参数
     * @return 最终返回商户程序给予支付宝平台的信息
     */
    @ApiOperation("支付通知")
    @PostMapping("/trade/notify")
    public String tradeNotify(@RequestParam Map<String,String> params)  {
        try {
            //@RequestParam表示将参数从请求中取出放入map集合中
            log.info("支付通知正在执行");
            log.info("通知参数----》{}", params);
            //result表示商家需要给支付宝反馈的异步通知结果(success表示成功,需要后续的业务来规定是否为
            // success)
            String result = "failure";

            //异步通知验签(使用我们引入的支付宝SDK验证签名)
            //一个是异步通知的结果参数,一个是支付宝的公钥,一个是字符集,一个是加密方式,得到一个布尔值结果
            boolean signVerified = AlipaySignature.rsaCheckV1(params, config.getProperty("alipay.alipay-public-key"),
                    AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);

            //对签名结果进行判断
            if (!signVerified) {
                //TODO:验签失败则记录异常日志,并在response中返回failure
                log.error("支付成功,异步通知验签失败......");
                return result;
            }

            //TODO:验证成功,按照支付结果异步通知中的描述,
            // 对支付结果中的业务内容进行二次校验,
            // 校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure


            //1.商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;
            //获取对应的订单号
            String outTradeNo = params.get("out_trade_no");
            //利用获取的订单号查询对应的订单信息(返回一个订单对象)
            OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
            //对订单对象进行判断
            if (order==null){
                log.error("订单不存在......");
                return result;
            }

            //2.判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
            //从参数中获取金额(单位为元),但是数据库中的单位为分,因此需要进行转换
            String totalAmount = params.get("total_amount");
            int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
            //获取订单中的金额
            int totalFeeInt = order.getTotalFee();
            if (totalFeeInt!=totalAmountInt){
                //如果不等,则说明金额不对
                log.error("金额校验失败");
                return result;
            }

            //3.校验通知中的 seller_id(对应商户的PID)(或者 seller_email) 是否为 out_trade_no
            // 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email)
            String sellerId = params.get("seller_id");
            //获取实际的商户PID
            String pid = config.getProperty("alipay.seller-id");
            if (!sellerId.equals(pid)){
                //用商户的PID与参数中的sellerID进行比较
                log.error("商家PID校验失败....");
                return result;
            }

            //4.验证 app_id 是否为该商户本身
            String appId = params.get("app_id");
            String appIdProperty = config.getProperty("alipay.app-id");
            if (!appId.equals(appIdProperty)){
                log.error("appId校验失败");
                return result;
            }

            //在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS
            // 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
            //获取交易状态
            String tradeStatus = params.get("trade_status");
            if (!"TRADE_SUCCESS".equals(tradeStatus)){
                //如果不满足交易状态成功参数,则直接返回failure
                log.error("支付未成功....");
                return result;
            }
            //以上4步校验成功后设置为success,之后返回结果,商户可以自身进行后续的处理
            //商户处理自身业务
            aliPayService.processOrder(params);

            result = "success";

            log.info("支付成功,异步通知验签成功.......");
            return result;
        }catch (AlipayApiException e) {
            e.printStackTrace();
            throw new RuntimeException("异步通知验证签名出现异常....");
        }



    }
}
  • 对应的验证签名步骤参照异步结果返回验签流程。
  • 如果验证签名成功,商户系统需要反馈对应的Success字符给支付宝平台(如果失败则返回failure),之后商户系统自身需要依据获得的参数对订单信息进行处理。(调用processOrder方法)
e、接收异步通知后更新订单测试
  • 首先开启前端项目:访问:http://localhost:8080
  • 其次利用ngrok工具进行内网穿透(不进行此步骤,则无法获取到支付宝的异步通知),并且每次重启对应的地址映射都不相同,需要根据ngrok反馈的信息进行文件配置。
ngrok http 8090
# 由于需要通知到后端工程,需要开放对应的后端工程的端口

# 返回的信息
ngrok                                                                                                            (Ctrl+C to quit)
                                                                                                                                 
Session Status                online                                                                                             
Account                       binbin (Plan: Free)                                                                                
Version                       3.0.3                                                                                              
Region                        Japan (jp)                                                                                         
Latency                       69.9073ms                                                                                          
Web Interface                 http://127.0.0.1:4040                                                                              
Forwarding                    https://8141-183-238-79-57.jp.ngrok.io -> http://localhost:8090                                    
                                                                                                                                 
Connections                   ttl     opn     rt1     rt5     p50     p90                                                        
                              1       0       0.00    0.00    60.53   60.53                                                      
                                                                                
  • 启动后端项目,在页面中进行支付并购买相应的课程。
  • 最后查看订单表中订单的状态与支付信息中的支付日志信息。

首先对应的订单状态更新成功

SpringBoot支付宝接入实战_第25张图片

最后订单的支付日志信息更新成功

SpringBoot支付宝接入实战_第26张图片

至此,异步通知的处理完成。

f、过滤重复通知

存在的问题:

过滤重复通知发生在商户端接收到了支付宝平台的异步通知,并进行了相应的处理,给支付宝平台反馈信息,但由于某些原因(诸如网络等原因),支付宝并没有收到异步通知结果的反馈,支付宝会继续发送异步通知给商户端,商户接收到支付宝的二次异步通知仍旧会进行处理,记录第二次支付日志。

本质上来说是处理接口调用的幂等性问题。

  • 首先编写订单状态查询方法(在OrderInfoServiceImpl类中)
 
/**
     * Gets order status.
     * 获取订单状态
     *
     * @param orderNo the order no
     * @return the order status
     */
    String getOrderStatus(String orderNo);

/**
     * 根据订单号获取订单状态
     * @param orderNo the order no
     * @return
     */
    @Override
    public String getOrderStatus(String orderNo) {
        //进行查询订单的操作
        QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
        //构造查询条件
        orderInfoQueryWrapper.eq("order_no",orderNo);
        //根据订单号查询的订单信息必须是唯一的,因此使用selectOne
        OrderInfo orderInfo = orderInfoMapper.selectOne(orderInfoQueryWrapper);
        //判断订单信息是否为空,如果为空,直接将订单状态设置为null
        if (orderInfo==null){
            return null;
        }
        return orderInfo.getOrderStatus();
   }

首先需要传入一个订单号,创建对应的条件查询器,利用eq方法做相等比较(订单号唯一),利用orderInfoMapper持久层对象做查询操作selectOne,如果获取到的订单对象为空,则状态直接返回为空,否则返回对应的订单状态。

  • 接着在处理订单方法中设置相应的业务逻辑

AliPayServiceImpl类中的processOrder方法中进行逻辑处理。

//接口调用幂等性问题:在更新订单状态,记录支付日志之前过滤重复通知(无论接口被调用多少次,以下只执行一次)
        //首先获取订单状态
        String orderStatus = orderInfoService.getOrderStatus(orderNo);
        if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){
            //如果订单状态不是未支付,则直接返回,不需要任何处理
            return;
        }

如果获取到的订单状态不是未支付,则直接返回,因为更新订单状态以及记录支付日志只针对订单状态为未支付的订单。

g、添加数据锁

此处还有一个问题,基于上述处理重复通知的业务,可能存在同时多台服务器发起异步通知,同时到达判断订单状态的地方,同时判断为未支付,继而同时执行更新同一订单操作(影响较小),同时执行记录支付日志操作。此时就需要在对应的业务逻辑中添加可重入锁(数据锁),避免因为函数重入造成的数据混乱

		 /**
     * 添加可重入锁对象,进行数据的并发控制
     */
    private final ReentrantLock lock=new ReentrantLock();

        /**
         * 在对业务数据进行状态检查之前需要利用数据锁进行处理,进行并发控制
         * 避免数据重入造成混乱,
         * 此处使用尝试获取锁的判断,如果没有获取锁,此时则返回false,直接进行下面的操作
         * 不会等待锁释放,造成阻塞。
         */
        if (lock.tryLock()) {
            try {
                //接口调用幂等性问题:在更新订单状态,记录支付日志之前过滤重复通知(无论接口被调用多少次,以下只执行一次)
                //首先获取订单状态
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){
                    //如果订单状态不是未支付,则直接返回,不需要任何处理
                    return;
                }
                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
                //记录支付日志
                paymentInfoService.createPaymentInfoForAliPay(params);
            }finally {
                //必须要主动释放锁
                lock.unlock();
            }
        }

    }

对于进行订单处理的业务,则需要添加可重入锁进行并发控制。

2. 支付宝统一收单交易关闭接口

用于交易创建后,用户在一定时间内未进行支付,可调用该接口直接将未付款的交易进行关闭。

(1)API预览

SpringBoot支付宝接入实战_第27张图片

关单接口中的请求参数(即发起关单请求需要携带哪些参数)。

(2)关闭订单

  • 首先在业务层创建关闭订单的方法
 /**
     * Cancel order.
     * 根据订单号取消订单
     *
     * @param orderNo the order no
     */
    void cancelOrder(String orderNo);

/**
     * 用户取消订单方法编写
     * @param orderNo 订单号
     */
    @Override
    public void cancelOrder(String orderNo) {

        //调用支付统一收单交易关闭接口
        this.closeOrder(orderNo);

        //更新用户的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);

    }

    /**
     * 关单接口调用
     * @param orderNo 订单号
     */
    private void closeOrder(String orderNo) {
        try {


            log.info("关单接口调用,订单号---》{}", orderNo);
            //创建关单请求
            AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
            //创建请求参数对象
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no", orderNo);
            //将对应的参数设置到请求对象中
            request.setBizContent(bizContent.toString());
            //使用支付客户端对象执行请求
            AlipayTradeCloseResponse response = alipayClient.execute(request);
            //判断请求是否成功
            if (response.isSuccess()){
                //打印响应信息主体
                log.info("调用成功====》{}",response.getBody());
            }else {
                log.info("调用失败====》{},返回码"+response.getCode()+",返回描述为:"+response.getMsg());
               // throw new RuntimeException("关单接口调用失败....."); 让其正常结束
            }

        } catch (AlipayApiException e) {
            throw new RuntimeException("关单接口调用出现异常");
        }


    }

关闭订单需要先创建关闭订单请求对象,之后创建一个请求参数封装对象,将订单号传入,执行请求,并对响应进行相应的处理。

调用支付宝端关单接口成功之后,需要设置新的交易订单的状态。

如果在支付过程中,并未扫码登录,则支付宝端并不创建此次交易的记录,也就是在判断请求状态是否成功的时候,会返回调用失败,正常结束方法,在商户系统中直接修改订单状态。

  • 在控制层编写方法
  /**
     * 用户取消订单接口
     * @param orderNo 订单号
     * @return 返回取消结果
     */
    @ApiOperation("用户取消订单")
    @PostMapping("/trade/close/{orderNo}")
    public Results cancel(@PathVariable String orderNo){
        log.info("用户取消订单......");
        //处理取消订单业务
        aliPayService.cancelOrder(orderNo);
        //返回订单取消信息
        return Results.returnOk().setMessage("订单已取消");
    }

根据订单号关闭相应的订单操作

(3)测试

SpringBoot支付宝接入实战_第28张图片

3. 统一收单线下交易查询

该接口提供所有支付宝支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
需要调用查询接口的情况:
当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;
调用支付接口后,返回系统错误或未知交易状态情况;
调用alipay.trade.pay,返回INPROCESS的状态;
调用alipay.trade.cancel之前,需确认支付状态;

(1)API预览

  • 公共请求参数

首先是公共请求参数,提供了多个必填参数

SpringBoot支付宝接入实战_第29张图片

  • 请求参数

其次是查询订单所需要的请求参数

SpringBoot支付宝接入实战_第30张图片

  • 公共响应参数

再次是请求响应的公共响应参数

SpringBoot支付宝接入实战_第31张图片

  • 响应参数

最后是请求的响应参数

SpringBoot支付宝接入实战_第32张图片

  • 响应API

SpringBoot支付宝接入实战_第33张图片

(2)主动查询订单

  • 首先在AlipayService中创建新方法用于查询订单操作
/**
 * The interface Ali pay service.
 *
 * @author lambda
 */
public interface AliPayService {
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
/**
     * Query order string.
     * 商户向支付宝端查询订单结果
     *
     * @param orderNo the order no
     * @return the string
     */
    String queryOrder(String orderNo);
}
  • 接着实现查询订单的方法
  /**
     * 商户查询订单信息
     * @param orderNo 订单号
     * @return 返回订单查询结果,如果返回为null,说明支付宝端没有创建订单
     */
    @Override
    public String queryOrder(String orderNo) {

        try {

            log.info("查单接口调用----》{}", orderNo);
            //首先创建交易查询对象
            AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
            //组装请求参数对象(向支付宝端查单需要提供哪些参数)
            JSONObject bizContent = new JSONObject();
            //组装订单号
            bizContent.put("out_trade_no", orderNo);
            request.setBizContent(bizContent.toString());
            //执行查询请求
            AlipayTradeQueryResponse response= alipayClient.execute(request);
            if (response.isSuccess()){
                log.info("调用成功,返回结果---》{}",response.getBody());
                return response.getBody();
            }else{
                log.info("调用失败,返回响应码"+response.getCode()+",响应结果为"+response.getBody());
               // throw new RuntimeException("响应失败....");
                //调用失败直接返回为null
                return null;
            }
        } catch (AlipayApiException e) {
            throw new RuntimeException("查询订单接口调用失败.....");
        }
    }

  • 在控制层实现跳转
 /**
     *商户查询订单接口
     * 商户根据订单号查询相应的订单信息
     * @param orderNo 订单号
     * @return
     */
    @ApiOperation("商户查询订单")
    @GetMapping("/trade/query/{orderNo}")
    public Results queryOrder(@PathVariable String orderNo){
            log.info("商户查询订单====》{}",orderNo);
            //调用支付宝支付服务的查询订单方法
        String result=aliPayService.queryOrder(orderNo);
        return Results.returnOk().setMessage("查询订单信息").returnData("result",result);
    }

获取到查询的信息之后返回给前端。

  • 之后实现一个定时任务来实现查单操作
//orderInfoService
public interface OrderInfoService  extends IService<OrderInfo> {
 /**
     * Gets no pay order by duration.
     * 查询超过指定时间未支付的订单
     *
     * @param minutes     the
     * @param paymentType the payment type
     * @return the no pay order by duration
     */
    List<OrderInfo> getNoPayOrderByDuration(int minutes,String paymentType);
}

//orderInfoServiceImpl

@Service
@Slf4j
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {

/**
     *查询超过指定时间未支付的订单集合
     * @param minutes the
     * @return
     */
    @Override
    public List<OrderInfo> getNoPayOrderByDuration(int minutes,String paymentType) {
        //创建一个时间实例,减去超时时间的时间实例,与订单的创建时间相比
        Instant minus = Instant.now().minus(Duration.ofMinutes(minutes));

        //创建一个查询订单对象
        QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
        //组装订单的查询信息,首先是未支付
        orderInfoQueryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
        //如果当前时间减去超时时间的时间值比创建时间晚,则说明已经超时了
        orderInfoQueryWrapper.le("create_time",minus);
        orderInfoQueryWrapper.eq("payment_type",paymentType);
        //最后将查询的结果返回
        return orderInfoMapper.selectList(orderInfoQueryWrapper);
    }
} 

根据订单号以及支付的类型来查询尚未支付的订单。

  • 创建一个定时任务
@Slf4j
@Component
public class AliPayTask {
    @Resource
    private OrderInfoService orderInfoService;

    /**
     * 每30秒查询一次订单信息,查询创建1分钟并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm(){
        log.info("定时查询订单任务启动");
        //调用查询未支付订单的方法获取所有的订单信息
        List<OrderInfo> noPayOrderList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());

        //遍历超时订单
        for (OrderInfo orderInfo : noPayOrderList) {
            String orderNo = orderInfo.getOrderNo();
            log.info("超时1分钟未支付的订单---》{}",orderNo);
        }

    }
}

@Scheduled(cron = "0/30 * * * * ?")表示指定定时任务的周期。

(3)定时查单之订单未创建

此处主要说明在商户端发起向支付宝端发起的查询订单信息,以便商户端更新订单相关信息。因为本地显示未支付付,不能保证在支付宝端也是未支付的,如果支付宝端已经支付则需要更新本地的订单信息为已支付。

  • 添加向支付宝端查询订单信息的方法
/**
     * 根据订单号查询支付宝端的订单状态
     * 如果订单已经支付,则更新商户端订单状态,并记录支付日志
     * 如果订单没有支付,则调用关单接口,并更新商户端订单状态
     * 如果订单未创建,则直接更新商户端的订单状态即可
     * @param orderNo 订单号
     */
    @Override
    public void checkOrderStatus(String orderNo) {
        log.warn("根据订单号核实订单状态---》{}",orderNo);
        //商户端向支付宝端查询订单信息
        String result = this.queryOrder(orderNo);
        //1.订单未创建状态
        if (result==null){
            log.warn("核实订单未创建---》{}",orderNo);
            //更新本地订单状态(设置关闭)
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
        }

        //2.如果订单未支付,则调用关单接口并更新商户端订单状态
        Gson gson = new Gson();
        //由于result的值中也是属于键值对,String-{xxx:xxx,xxxx:xxxx,xxx:xxxx}
        Map<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);
        //参见统一收单线下交易查询中的响应示例
        LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
        //从map中获取订单状态(trade_status)
        String tradeStatus = (String)alipayTradeQueryResponse.get("trade_status");
        if (AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
            //判断如果订单未支付
            log.warn("核实订单未支付---》{}",orderNo);
            //订单未支付,则调用关单接口
            this.closeOrder(orderNo);
            //更新商户端状态
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
        }

        //3.如果订单已经支付,则更新商户端的订单状态,并记录支付日志
        if (AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){
            //判断订单已经支付
            log.warn("核实订单已支付---》{}",orderNo);
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
            paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);
        }


    }

如果订单已经支付,则更新商户端订单状态,并记录支付日志

如果订单没有支付,则调用关单接口,并更新商户端订单状态

如果订单未创建,则直接更新商户端的订单状态即可

  • 定时任务中调用查单接口
@Slf4j
@Component
public class AliPayTask {
    @Resource
    private OrderInfoService orderInfoService;
    @Resource
    private AliPayService aliPayService;

    /**
     * 每30秒查询一次订单信息,查询创建1分钟并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm(){
        log.info("定时查询订单任务启动");
        //调用查询未支付订单的方法获取所有的订单信息
        List<OrderInfo> noPayOrderList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());

        //遍历超时订单
        for (OrderInfo orderInfo : noPayOrderList) {
            String orderNo = orderInfo.getOrderNo();
            log.info("超时1分钟未支付的订单---》{}",orderNo);
            //核实订单状态,调用支付宝端查单接口
            aliPayService.checkOrderStatus(orderNo);
        }
    }
}
  • 测试

前提是:当用户选择支付宝支付的时候,商户系统会自动生成一个未支付的订单,无论用户是否扫码。如果用户未扫码,则显示未支付,若用户扫码并支付,则商户系统会等待支付宝端发起回调通知,通知商户用户支付成功,请商户及时更新状态。如果因为网络原因,商户系统无法正确接收支付宝端的回调通知,则用户需要在一段时间之后主动调用查单接口,根据反馈的信息进行相应的订单信息的更新。

如果订单没有在支付宝端创建(即没有进行扫码),超时一分钟后,商户系统会自动更新本次订单信息为超时已关闭。

如果订单已经扫码,但是没有进行实际的支付,超时一分钟后,商户系统也会自动更新本次订单信息为超时已关闭。

如果订单已经成功支付,但是由于网络原因,商户系统没有收到支付宝端的回调通知(此时尽管用户已经支付成功,但是商户系统并未收到通知,因此没有及时更新支付信息),此时就需要主要调用查单接口,如果查询到的信息是已经支付的,则需要商户主动更新本地订单信息,并且记录支付日志,更新订单状态。

SpringBoot支付宝接入实战_第34张图片

4.统一收单交易退款接口

SpringBoot支付宝接入实战_第35张图片

当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。

(1)API预览

  • 请求参数

SpringBoot支付宝接入实战_第36张图片

  • 响应参数

SpringBoot支付宝接入实战_第37张图片

(2)实现退款功能

  • 实现退款信息接口
public interface RefundInfoService extends IService<RefundInfo> {
     /**
     * Create refund by order no refund info.
     * 根据订单号创建退款订单
     *
     * @param orderNo the order no
     * @param reason  the reason
     * @return the refund info
     */
    RefundInfo createRefundByOrderNo(String orderNo, String reason);
	
     /**
     * Update refund.
     * 更新退款信息
     *
     * @param bodyAsString the body as string
     */
    void updateRefund(String bodyAsString);


    /**
     * Update refund for ali pay.
     * 支付宝支付退款
     * @param refundNo     the refund no
     * @param content      the content
     * @param refundStatus the refund status
     */
    void updateRefundForAliPay(String refundNo, String content, String refundStatus);
}

  • 实现更新退款信息的方法
@Service
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
    
    @Resource
    private OrderInfoService orderInfoService;
    @Resource
    private RefundInfoMapper refundInfoMapper;
    
     /**
     *
     * @param orderNo 订单编号
     * @param reason 退款原因
     * @return RefundInfo 退款单信息
     */
    @Override
    public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
        //根据订单号处理订单信息
        OrderInfo orderInfo=orderInfoService.getOrderByOrderNo(orderNo);

        //根据订单号生成退款单记录
        RefundInfo refundInfo = new RefundInfo();
        //订单编号
        refundInfo.setOrderNo(orderNo);
        //退款单编号
        refundInfo.setRefundNo(OrderNoUtils.getRefundNo());
        //原来订单金额
        refundInfo.setTotalFee(orderInfo.getTotalFee());
        //退款金额
        refundInfo.setRefund(orderInfo.getTotalFee());
        //退款原因
        refundInfo.setReason(reason);
        //将退款信息插入数据库
        refundInfoMapper.insert(refundInfo);
        return refundInfo;
    }

      @Override
    public void updateRefund(String content) {
        //将退款请求响应的返回对象转成Map类型信息
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
        //根据退款单编号,修改退款单
        QueryWrapper<RefundInfo> refundInfoQueryWrapper = new QueryWrapper<>();
        refundInfoQueryWrapper.eq("refund_no",resultMap.get("out_refund_no"));

        //设置要修改的字段
        RefundInfo refundInfo = new RefundInfo();
        //微信支付退款单号
        refundInfo.setRefundId(resultMap.get("refund_id"));

        //查询申请退款和退款中的返回参数(退款中)
        if (resultMap.get("status")!=null){
            //设置退款状态
            refundInfo.setRefundStatus(resultMap.get("status"));
            //将全部响应结果存入数据库的content字段中
            refundInfo.setContentReturn(content);
        }

        //退款回调中的回调参数(这是退款之后的状态)
        if (resultMap.get("refund_status")!=null){
                refundInfo.setRefundStatus(resultMap.get("refund-status"));
            //将全部响应结果存入数据库的content字段中
            refundInfo.setContentNotify(content);
        }

        //更新退款单
        refundInfoMapper.update(refundInfo,refundInfoQueryWrapper);

    }
/**
     *
     * @param refundNo 退款单号
     * @param content 退款信息主体
     * @param refundStatus 退款结果类型
     */
    @Override
    public void updateRefundForAliPay(String refundNo, String content, String refundStatus) {
        //根据退款单号修改退款单
        QueryWrapper<RefundInfo> refundInfoQueryWrapper = new QueryWrapper<>();
        refundInfoQueryWrapper.eq("refund_no",refundNo);

        //设置要修改的字段(新建一个退款单)
        RefundInfo refundInfo = new RefundInfo();
        //refundInfo.setRefundNo(refundNo);
        refundInfo.setRefundStatus(refundStatus);
        refundInfo.setContentReturn(content);

        //执行更新操作
        refundInfoMapper.update(refundInfo,refundInfoQueryWrapper);

    }

首先,注入RefundInfoMapper用于操作持久层的退款单信息(执行增删改查业务),接着创建一个修改退款信息的对象,匹配条件是退款单号一致即可。之后创建一个退款信息对象,设置对应的退款状态以及原因,执行更新操作。

  • 在支付宝支付的服务层方法中添加退款方法
public interface AliPayService { 
/**
     * Refund.
     * 退款操作
     * @param orderNo the order no
     * @param reason  the reason
     */
    void refund(String orderNo, String reason);
}

@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
    @Resource
    private RefundInfoService refundInfoService
    ......
 /**
     * 商户发起退款请求
     * @param orderNo 退款单号
     * @param reason 原因
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refund(String orderNo, String reason) {
        try {
            log.info("调用退款API");
            //调用退款信息方法创建退款信息
            RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
            //创建统一交易退款请求
            AlipayTradeRefundRequest request=new AlipayTradeRefundRequest();
            //组装当前业务交易的请求参数
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no",orderNo);
            //设置退款单金额(需要除以100),分转化成元
            BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
            bizContent.put("refund_amount",refund);
            bizContent.put("refund_reason",reason);
            //将参数设置到请求中
            request.setBizContent(bizContent.toString());
            AlipayTradeRefundResponse response = alipayClient.execute(request);
            if (response.isSuccess()){
                log.info("退款交易成功,对应信息为:"+response.getBody());
                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_SUCCESS);
                //更新退款单
                refundInfoService.updateRefundForAliPay( //表示退款成功
                        refundInfo.getRefundNo(),response.getBody(),AliPayTradeState.REFUND_SUCCESS.getType());
            }else{
                log.warn("退款交易失败,对应状态码为:"+response.getCode()+",返回体为:"+response.getBody());
                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_ABNORMAL);
                //更新退款单
                refundInfoService.updateRefundForAliPay(
                        refundInfo.getRefundNo(),response.getBody(),AliPayTradeState.REFUND_ERROR.getType()
                );
            }


        } catch (AlipayApiException e) {
            throw new RuntimeException("退款交易失败.....");
        }
    }

首先注入RefundInfoService对象,用于创建退款信息对象,之后创建支付宝交易退款请求,组装请求参数主要针对订单号、订单金额以及订单原因来进行组装,组合完毕之后进行相应的执行,得到支付宝端的响应,根据响应是否成功分别更新订单号以及退款单信息。

  • 在控制层完善相关信息
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
    /**
     * 商户退款接口
     * @param orderNo 退款单号
     * @param reason 退款原因
     * @return
     */
    @ApiOperation("商户退款接口")
    @PostMapping("/trade/refund/{orderNo}/{reason}")
    public Results refunds(@PathVariable String orderNo,@PathVariable String reason){
        log.info("申请退款....");
        //调用服务层退款方法
        aliPayService.refund(orderNo,reason);
        return Results.returnOk();

    }
}

获取前端参数之后调用后端的退款请求,执行退款操作

(3)测试退款功能

退款结果展示:

SpringBoot支付宝接入实战_第38张图片

SpringBoot支付宝接入实战_第39张图片

5.统一收单交易退款查询

商户可使用该接口查询自已通过alipay.trade.refund提交的退款请求是否执行成功。

若退款接口由于网络等原因返回异常,商户可调用退款查询接口 alipay.trade.fastpay.refund.query(统一收单交易退款查询接口)查询指定交易的退款信息。

(1)API预览

  • 请求参数

SpringBoot支付宝接入实战_第40张图片

(2)退款查询功能实现

  • 退款查询功能
public interface AliPayService {
    ...........
 /**
     * Query refund string.
     * 查询退款结果
     *
     * @param orderNo the order no
     * @return the string
     */
    String queryRefund(String orderNo);
}

@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService { 
/**
     * 根据订单号查询退款
     * @param orderNo the order no 订单号
     * @return 返回退款查询的结果
     */
    @Override
    public String queryRefund(String orderNo) {

        try {
            log.info("查询退款接口调用---》{}",orderNo);
            //定义一个查询退款的请求对象
            AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
            //组装请求参数
            JSONObject bizContent = new JSONObject();
            bizContent.put("out_trade_no",orderNo);
            //out_request_no表示退款请求号,如果退款的时候没有传入,则以订单号作为退款请求号。
            bizContent.put("out_request_no",orderNo);
            //组装到请求中
            request.setBizContent(bizContent.toString());
            //执行请求
            AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
            if (response.isSuccess()){
                log.info("调用成功,返回结果---》{}",response.getBody());
                return response.getBody();
            }else {
                log.info("调用失败,对应的响应码为:"+response.getCode()+",对应的响应内容为:"+response.getBody());
                //如果调用失败,返回空
                return null;
            }

        } catch (AlipayApiException e) {
            throw new RuntimeException("退款查询请求执行失败");
        }

    }
  • 控制层实现
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {

    @Resource
    private AliPayService aliPayService;

    @Resource
    private Environment config;

    @Resource
    private OrderInfoService orderInfoService;
/**
     * 退款结果查询(商户向支付宝端查询)
     * @param orderNo 订单号
     * @return 返回查询的结果
     */
    @ApiOperation("查询退款")
    @GetMapping("/trade/fastpay/refund/{orderNo}")
    public Results queryRefunds(@PathVariable("orderNo") String orderNo){
        log.info("查询退款.......");
        //执行退款查询并接收返回的字符串结果
        String result=aliPayService.queryRefund(orderNo);
        return Results.returnOk().setMessage("查询成功").returnData("result",result);
    }

(3)测试结果

在swagger中测试查询结果

SpringBoot支付宝接入实战_第41张图片

6. 查询对账单下载地址

为方便商户快速查账,支持商户通过本接口获取商户离线账单下载地址

(1)API预览

  • 请求参数

SpringBoot支付宝接入实战_第42张图片

  • 响应参数

SpringBoot支付宝接入实战_第43张图片

(2)下载账单实现

  • 获取账单地址实现
public interface AliPayService {
    ..............
/**
     * Query bill string.
     *  查询订单下载地址
     * @param billDate the bill date
     * @param type     the type
     * @return the string
     */
    String queryBill(String billDate, String type);
}
//根据账单的日期和类型查询
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {
    .........
        /**
     * 获取账单地址实现
     * @param billDate the bill date 账单日期
     * @param type     the type 账单类型
     * @return
     */
    @Override
    public String queryBill(String billDate, String type) {
       try {
           //设置查询账单请求对象
      AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
            //组装请求参数
           JSONObject bizContent = new JSONObject();
           bizContent.put("bill_type",type);
           bizContent.put("bill_date",billDate);
           //将请求参数设置到请求中
           request.setBizContent(bizContent.toString());
           //执行请求
           AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
           if (response.isSuccess()){
               log.info("查询账单url地址请求成功---》{}",response.getBody());
               //获取账单的下载地址
               Gson gson = new Gson();
               Map<String,LinkedTreeMap> resultMap=gson.fromJson(response.getBody(),HashMap.class);
               //获取交易账单地址
               LinkedTreeMap billDownLoadUrl= resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
               String billDownloadUrl = (String)billDownLoadUrl.get("bill_download_url");
               //返回url地址
               return billDownloadUrl;

           }else {
               log.info("查询账单地址失败。对应的响应码为:"+response.getCode()+",对应的响应体为:"+response.getBody());
                throw new RuntimeException("查询账单地址失败....");
           }

       } catch (AlipayApiException e) {
           throw new RuntimeException("查询账单请求执行失败.......");
       }

    }
}
  • 控制层
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "网站支付宝支付")
@Slf4j
public class AliPayController {
    .......
        /**
     * 根据账单类型和日期获取账单的url地址
     * @param billDate 账单的日期
     * @param type 账单的类型
     * @return 返回账单的url地址
     */
    @ApiOperation("获取账单url")
    @GetMapping("/bill/downloadurl/query/{billDate}/{type}")
    public Results queryTradeBill(@PathVariable String billDate,
                                  @PathVariable String type){

        log.info("获取账单的url地址");
        //获取账单的url地址
        String downloadUrl=aliPayService.queryBill(billDate,type);

        return Results.returnOk().setMessage("获取账单地址成功")
                .returnData("downloadUrl",downloadUrl);

    }
}

(3) 测试下载账单功能

对应的账单信息如下:

SpringBoot支付宝接入实战_第44张图片

SpringBoot支付宝接入实战_第45张图片

你可能感兴趣的:(spring,boot,java,后端)