微信支付实战

一、微信支付介绍和接入指引

1、微信支付产品介绍

1.1、付款码支付

用户展示微信钱包内的 付款码 给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。

1.2JSAPI支付

  • 线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支付。
  • 公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
  • PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。
特点:用户在客户端输入支付金额

1.3、小程序支付

在微信小程序平台内实现支付的功能。

1.4Native支付

Native 支付是指商户展示支付二维码,用户再用微信 扫一扫 完成支付的模式。这种方式适用于 PC 网站。
特点:商家预先指定支付金额

1.5APP支付

商户通过在移动端独立的 APP 应用程序中集成微信支付模块,完成支付。

1.6、刷脸支付

用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。

2、接入指引

2.1、获取商户号

微信商户平台: https://pay.weixin.qq.com/
场景: Native 支付
步骤:提交资料 => 签署协议 => 获取商户号

2.2、获取APPID

微信公众平台: https://mp.weixin.qq.com/
步骤:注册服务号 => 服务号认证 => 获取 APPID => 绑定商户号

2.3、获取API秘钥

APIv2 版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API 安全 => 设置 API 密钥

2.4、获取APIv3秘钥

APIv3 版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API 安全 => 设置 APIv3 密钥
随机密码生成工具: https://suijimimashengcheng.bmcx.com/

2.5、申请商户API证书

APIv3 版本的所有接口都需要; APIv2 版本的高级接口需要(如:退款、企业红包、企业付款等)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API 安全 => 申请 API 证书

2.6、获取微信平台证书

可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
注意:以上所有 API 秘钥和证书需妥善保管防止泄露

二、支付安全(证书/秘钥/签名)

1、信息安全的基础 - 机密性

  • 明文:加密前的消息叫明文plain text
  • 密文:加密后的文本叫密文cipher text
  • 密钥:只有掌握特殊钥匙的人,才能对加密的文本进行解密,这里的钥匙就叫做密钥key
密钥 就是一个字符串,度量单位是 bit ),比如,密钥长度是 128 ,就是 16 字节的二
进制串
  • 加密:实现机密性最常用的手段是加密encrypt
按照密钥的使用方式,加密可以分为两大类: 对称加密和非对称加密
  • 解密:使用密钥还原明文的过程叫解密decrypt
  • 加密算法:加密解密的操作过程就是加密算法
所有的加密算法都是公开的,而算法使用的 密钥 则必须保密 2 、对称加密和非对称加密
  • 对称加密
    • 特点:只使用一个密钥,密钥必须保密,常用的有 AES算法
    • 优点:运算速度快
    • 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交
  • 非对称加密
    • 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有 RSA
    • 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
    • 缺点:运算速度非常慢
  • 混合加密
实际场景中把对称加密和非对称加密结合起来使用。

3、身份认证

公钥加密,私钥解密的作用是加密信息
私钥加密,公钥解密的作用是身份认证

4、摘要算法(Digest Algorithm

摘要算法就是我们常说的散列函数、哈希函数( Hash Function ),它能够把任意长度的数据 压缩 成固定长度、而且独一无二的“ 摘要 字符串,就好像是给这段数据生成了一个数字 指纹
作用
保证信息的完整性
特性
  • 不可逆:只有算法,没有秘钥,只能加密,不能解密
  • 难题友好性:想要破解,只能暴力枚举
  • 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
  • 抗碰撞性:原文不同,计算后的摘要也要不同
常见摘要算法
MD5 SHA1 SHA2 SHA224 SHA256 SHA384

5、数字签名

数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认
签名和验证签名的流程

6、数字证书

数字证书解决 公钥的信任 问题,可以防止黑客伪造公钥。
不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由 CA 颁发
https 协议中的数字证书:

7、微信APIv3证书

商户证书
商户 API 证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
商户证书在商户后台申请: https://pay.weixin.qq.com/index.php/core/cert/api_cert#/

平台证书(微信支付平台):
微信支付平台证书是指由 微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。
平台证书的获取: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml

8API密钥和APIv3密钥

都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API 密钥对应 V2 版本的 API
APIv3 密钥对应 V3 版本的 API

三、案例项目的创建

1、创建SpringBoot项目

1.1、新建项目

注意: Java 版本选择 8

1.2、添加依赖

添加 SpringBoot web 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

1.3、配置application.yml文件

server:
  port: 8090 #服务端口
spring:
  application:
  name: payment-demo # 应用名称

1.4、创建controller

创建 controller 包,创建 ProductController
package com.atguigu.paymentdemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/product")
@CrossOrigin //跨域
public class ProductController {
    @GetMapping("/test")
    public String test(){
        return "hello";
    }
}

1.5、测试

访问: http://localhost:8090/api/product/test

2、引入Swagger

作用:自动生成接口文档和测试页面。

2.1、引入依赖

<!--swagger-->

<dependency>
        <groupId> io.springfox </groupId>
        <artifactId> springfox-swagger2 </artifactId>
        <version> 2.7.0 </version>
</dependency>
<!--swagger ui-->
<dependency>
        <groupId> io.springfox </groupId>
        <artifactId> springfox-swagger-ui </artifactId>
        <version> 2.7.0 </version>
</dependency>

2.2Swagger配置文件

创建 confifig 包,创建 Swagger2Confifig
package com.atguigu.paymentdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder().title("微信支付案例接口文档").build());
    }
}

2.3Swagger注解

controller 中可以添加常用注解
@Api ( tags = " 商品管理 " ) // 用在类上
@ApiOperation ( " 测试接口 " ) // 用在方法上

2.4、测试

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

3、定义统一结果

作用:定义统一响应结果,为前端返回标准格式的数据。

3.1、引入lombok依赖

<!-- 实体对象工具类:低版本 idea 需要安装 lombok 插件 -->
<dependency>
        <groupId> org.projectlombok </groupId>
        <artifactId> lombok </artifactId>
</dependency>

3.2、创建R

创建统一结果类
package com.atguigu.paymentdemo.vo;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Data //生成set、get等方法
public class R {
    private Integer code;
    private String message;
    private Map<String, Object> data = new HashMap<>();
    public static R ok(){
        R r = new R();
        r.setCode(0);
        r.setMessage("成功");
        return r;
    }
    public static R error(){
        R r = new R();
        r.setCode(-1);
        r.setMessage("失败");
        return r;
    }
    public R data(String key, Object value){
        this.data.put(key, value);
        return this;
    }
}

3.3、修改controller

修改 test 方法,返回统一结果
@ApiOperation("测试接口")
@GetMapping("/test")
public R test(){
    return R
        .ok()
        .data("message", "hello")
        .data("now", new Date());
}

3.4、配置json时间格式

spring:

  jackson : #json 时间格式
  date-format : yyyy-MM-dd HH : mm : ss
  time-zone : GMT+8

3.5Swagger测试

4、创建数据库

4.1、创建数据库

mysql - uroot - p
mysql > create database payment_demo;

4.2IDEA配置数据库连接

1)打开数据库面板

2 )添加数据库

3)配置数据库连接参数

4.3、执行SQL脚本

payment_demo.sql
USE `payment_demo`;

/*Table structure for table `t_order_info` */

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;

5、集成MyBatis-Plus

5.1、引入依赖

<!--mysql驱动-->

<dependency>
        <groupId> mysql </groupId>
        <artifactId> mysql-connector-java </artifactId>
</dependency>
<!-- 持久层 -->
<dependency>
        <groupId> com.baomidou </groupId>
        <artifactId> mybatis-plus-boot-starter </artifactId>
        <version> 3.3.1 </version>
</dependency>

5.2、配置数据库连接

spring:

datasource : #mysql 数据库连接
  driver-class-name : com.mysql.cj.jdbc.Driver
  url : jdbc : mysql : //localhost : 3306/payment_demo? serverTimezone=GMT%2B8&characterEncoding=utf-8
  username : root
  password : 123456

5.3、定义实体类

BaseEntity 是父类,其他类继承 BaseEntity

BaseEntity  

package com.atguigu.paymentdemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.util.Date;

@Data
public class BaseEntity {

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

    //创建时间
    private Date createTime;

    //更新时间
    private Date updateTime;
}

Product

package com.atguigu.paymentdemo.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("t_product")
public class Product extends BaseEntity{

    //标题
    private String title;

    //价格(分)
    private Integer price;
}

 Order

package com.atguigu.paymentdemo.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@TableName("t_order_info")
@Data
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;
}

PaymentInfo

package com.atguigu.paymentdemo.entity;

import com.baomidou.mybatisplus.annotation.TableName;

@TableName("t_payment_info")
public class PaymentInfo extends BaseEntity{

    //商品订单编号
    private String orderNo;

    //支付系统交易编号
    private String transactionId;

    //支付类型
    private String paymentType;

    //交易类型
    private String tradeType;

    //交易状态
    private String tradeStatus;

    //支付金额
    private Integer payerTotal;

    //通知参数
    private String content;
}

RefundInfo

package com.atguigu.paymentdemo.entity;

import com.baomidou.mybatisplus.annotation.TableName;

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

5.4、定义持久层

定义 Mapper 接口继承 BaseMapper<>
定义 xml 配置文件

 

5.5、定义MyBatis-Plus的配置文件

confifig 包中创建配置文件 MybatisPlusConfifig
package com.atguigu.paymentdemo.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan("com.atguigu.paymentdemo.mapper")//扫描持久层
@EnableTransactionManagement//开启事务管理
public class MybatisPlusConfig {
}

5.6、定义yml配置文件

添加持久层日志和 xml 文件位置的配置
mybatis-plus :
  configuration : #sql 日志
    log-impl : org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations : classpath : com/atguigu/paymentdemo/mapper/xml/*.xml

5.7、定义业务层

定义业务层接口继承 IService<>
ProductService
package com.atguigu.paymentdemo.service;

import com.atguigu.paymentdemo.entity.Product;
import com.baomidou.mybatisplus.extension.service.IService;

public interface ProductService extends IService<Product> {

}

OrderInfoService

package com.atguigu.paymentdemo.service;

import com.atguigu.paymentdemo.entity.OrderInfo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface OrderInfoService extends IService<OrderInfo> {

}

PaymentInfoService

package com.atguigu.paymentdemo.service;

import com.atguigu.paymentdemo.entity.PaymentInfo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface PaymentInfoService extends IService<PaymentInfo> {

}

RefundInfoService

package com.atguigu.paymentdemo.service;

import com.atguigu.paymentdemo.entity.RefundInfo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface RefundInfoService extends IService<RefundInfo> {

}

定义业务层接口的实现类,并继承 ServiceImpl<,>

5.8、定义接口方法查询所有商品

public class ProductController 中添加一个方法
    @Resource
    private ProductService productService;

    @ApiOperation("商品列表")
    @GetMapping("/list")
    public R test() {

        List<Product> list = productService.list();
        return R.ok().data("productList",list);
    }

5.9Swagger中测试

5.10pom中配置build节点

因为 maven 工程在默认情况下 src/main/java 目录下的所有资源文件是不发布到 target 目录下的,我们在 pom 文件的 节点下配置一个资源发布过滤器
<!-- 项目打包时会将 java 目录中的 *.xml 文件也进行打包 -->
<resources>
        <resource>
                <directory> src/main/java </directory>
                <includes>
                        <include> **/*.xml </include>
                </includes>
                <filtering> false </filtering>
        </resource>
</resources>

6、搭建前端环境

6.1、安装Node.js

Node.js 是一个基于 JavaScript 引擎的服务器端环境,前端项目在开发环境下要基于 Node.js 来运行
安装: node-v14.18.0-x64.msi

6.2、运行前端项目

将项目放在磁盘的一个目录中,例如 D:\demo\payment-demo-front
进入项目目录,运行下面的命令启动项目:
npm run serve

6.3、安装VSCode

如果你希望方便的查看和修改前端代码,可以安装一个 VSCode
安装: VSCodeUserSetup-x64-1.56.2
安装插件:

7Vue.js入门

官网: https://cn.vuejs.org/
Vue.js 是一个前端框架,帮助我们快速构建前端项目。
使用 vue 有两种方式,一个是传统的在 html 文件中引入 js 脚本文件的方式,另一个是脚手架的方式。 我们的项目,使用的是脚手架的方式。

7.1、安装脚手架

配置淘宝镜像
# 经过下面的配置,所有的 npm install 都会经过淘宝的镜像地址下载
npm config set registry https :// registry.npm.taobao.org
全局安装脚手架
npm install - g @vue / cli

7.2、创建一个项目

先进入项目目录( Ctrl + ~ ),然后创建一个项目
vue create vue-demo

7.3、运行项目

npm run serve
指定运行端口
npm run serve -- -- port 8888

7.4、数据绑定

修改 src/App.vue
<!-- 定义页面结构 -->
<template>
<div>
        <h1> Vue 案例 </h1>
        <!-- 插值 -->
        <p> {{course}} </p>
</div>
</template>
<!-- 定义页面脚本 -->
<script>
export default {
        // 定义数据
        data () {
                return {
                        course : ' 微信支付 '
                }
        }
}
</script>

7.5、安装Vue调试工具

Chrome 的扩展程序中安装: Vue.jsDevtools.zip
1 )扩展程序的安装

2 )扩展程序的使用

7.6、双向数据绑定

数据会绑定到组件,组件的改变也会影响数据定义
<p>
<!-- 指令 -->
<input type = "text" v-model = "course" >
</p>

7.7、事件处理

1)定义事件

// 定义方法
methods : {
        toPay (){
                console . log ( ' 去支付 ' )
        }
}
2 )调用事件
<p>
<!-- 事件 -->
<button @click = "toPay()" > 去支付 </button>
</p>

四、基础支付API V3

1、引入支付参数

1.1、定义微信支付相关参数

将资料文件夹中的 wxpay.properties 复制到 resources 目录中
这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、 APPID API 秘钥等等

1.2、读取支付参数

将资料文件夹中的 confifig 目录中的 WxPayConfifig.java 复制到源码目录中。

1.3、测试支付参数的获取

controller 包中创建 TestController

package com.atguigu.paymentdemo.controller;

import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {

    @Resource
    private WxPayConfig wxPayConfig;

    @GetMapping("/get-wx-pay-config")
    public R getWxPayConfig(){
        String mchId = wxPayConfig.getMchId();
        return R.ok().data("mchId",mchId);
    }
}

1.4、配置 Annotation Processor

可以帮助我们生成自定义配置的元数据信息,让配置文件和 Java 代码之间的对应参数可以自动定位,方便开发。
<dependency>
        <groupId> org.springframework.boot </groupId>
        <artifactId> spring-boot-configuration-processor </artifactId>
        <optional> true </optional>
</dependency>

1.5、在IDEA中设置 SpringBoot 配置文件

IDEA 可以识别配置文件,将配置文件的图标展示成 SpringBoot 的图标,同时配置文件的内容可以高亮显示
File -> Project Structure -> Modules -> 选择小叶子

点击(+) 图标

选中配置文件:

2、加载商户私钥

2.1、复制商户私钥

将下载的私钥文件复制到项目根目录下:

2.2、引入SDK

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
我们可以使用官方提供的 SDK ,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
<dependency>
        <groupId> com.github.wechatpay-apiv3 </groupId>
        <artifactId> wechatpay-apache-httpclient </artifactId>
        <version> 0.3.0 </version>
</dependency>

2.3、获取商户私钥

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (如何加载商户私钥)
    /**
     * 获取商户的私钥文件
     * @param fileName
     * @return
     */
    private PrivateKey getPrivateKey(String fileName){
        System.out.println("fileName = " + fileName);
        // 加载商户私钥(privateKey:私钥字符串)
        try {
            return PemUtil.loadPrivateKey(new FileInputStream(fileName));
        } catch (Exception e) {
            throw new RuntimeException("私钥文件不存在",e);
        }
    }

2.4、测试商户私钥的获取

PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。
(将前面的方法改成 public 的再进行测试)
package com.atguigu.paymentdemo;
import com.atguigu.paymentdemo.config.WxPayConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.security.PrivateKey;
@SpringBootTest
class PaymentDemoApplicationTests {
    @Resource
    private WxPayConfig wxPayConfig;
    /**
    * 获取商户私钥
    */
    @Test
    public void testGetPrivateKey(){
        //获取私钥路径
        String privateKeyPath = wxPayConfig.getPrivateKeyPath();
        //获取商户私钥
        PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
        System.out.println(privateKey);
    }
}

3、获取签名验证器和HttpClient

3.1、证书密钥使用说明

https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml

3.2、获取签名验证器

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
    /**
     * 获取签名验证器
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier() {

        System.out.println("privateKeyPath = " + privateKeyPath);
        log.info("获取签名验证器");
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);

        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        //使用定时更新的签名验证器,不需要传入证书
        ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
                wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));

        return verifier;
    }

3.4、获取 HttpClient 对象

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
HttpClient 对象:是建立远程连接的基础,我们通过 SDK 创建这个对象。
    /**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier)            {
        log.info("获取httpClient");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        //接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

        //通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();

        return httpClient;
    }

4API字典和相关工具

4.1API列表

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
我们的项目中要实现以下所有 API 的功能。

4.2、接口规则

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。
<!--json 处理 -->
<dependency>
        <groupId> com.google.code.gson </groupId>
        <artifactId> gson </artifactId>
</dependency>

4.3、定义枚举

将资料文件夹中的 enums 目录复制到源码目录中。
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。

 OrderStatus

package com.atguigu.paymentdemo.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum OrderStatus {

    /**
     * 未支付
     */
    NOTPAY("未支付"),

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

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

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

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

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

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

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

PayType

package com.atguigu.paymentdemo.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum PayType {

    /**
     * 微信
     */
    WXPAY("微信"),

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

    private final String type;
}

WxApiType

package com.atguigu.paymentdemo.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxApiType {

	/**
	 * Native下单
	 */
	NATIVE_PAY("/v3/pay/transactions/native"),

	/**
	 * Native下单
	 */
	NATIVE_PAY_V2("/pay/unifiedorder"),

	/**
	 * 查询订单
	 */
	ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),

	/**
	 * 关闭订单
	 */
	CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),

	/**
	 * 申请退款
	 */
	DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),

	/**
	 * 查询单笔退款
	 */
	DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),

	/**
	 * 申请交易账单
	 */
	TRADE_BILLS("/v3/bill/tradebill"),

	/**
	 * 申请资金账单
	 */
	FUND_FLOW_BILLS("/v3/bill/fundflowbill");


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

WxNotifyType

package com.atguigu.paymentdemo.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxNotifyType {

	/**
	 * 支付通知
	 */
	NATIVE_NOTIFY("/api/wx-pay/native/notify"),

	/**
	 * 支付通知
	 */
	NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),


	/**
	 * 退款结果通知
	 */
	REFUND_NOTIFY("/api/wx-pay/refunds/notify");

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

WxRefundStatus

package com.atguigu.paymentdemo.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxRefundStatus {

    /**
     * 退款成功
     */
    SUCCESS("SUCCESS"),

    /**
     * 退款关闭
     */
    CLOSED("CLOSED"),

    /**
     * 退款处理中
     */
    PROCESSING("PROCESSING"),

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

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

WxTradeState

package com.atguigu.paymentdemo.enums.wxpay;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxTradeState {

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

    /**
     * 未支付
     */
    NOTPAY("NOTPAY"),

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

    /**
     * 转入退款
     */
    REFUND("REFUND");

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

4.4、添加工具类

将资料文件夹中的 util 目录复制到源码目录中,我们将会使用这些辅助工具简化项目的开发

HttpUtils

package com.atguigu.paymentdemo.util;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;


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();
                }
            }
        }
    }
}

OrderNoUtils

package com.atguigu.paymentdemo.util;

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

/**
 * 订单号工具类
 *
 * @author qy
 * @since 1.0
 */
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;
    }

}

5Native下单API

5.1Native支付流程

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml

5.2Native下单API

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付。

1)创建 WxPayController

package com.atguigu.paymentdemo.controller;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付")
@Slf4j
public class WxPayController {
}

2)创建 WxPayService

接口
package com.atguigu.paymentdemo.service;
public interface WxPayService {
}

实现
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
}

3 )定义 WxPayController 方法
package com.atguigu.paymentdemo.controller;

import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags ="网站微信支付")
public class WxPayController {

    @Resource
    private WxPayService wxPayService;

    @ApiOperation("调用统一下单API,生成支付二维码")
    @PostMapping("/native/{productId}")
    public R nativePay(@PathVariable Long productId) throws IOException {
        
        log.info("发起支付请求");

        //返回支付二维码链接和订单号
        Map<String, Object> map = wxPayService.nativePay(productId);

        return R.ok().setData(map);
    }
}

R对象中添加 @Accessors(chain = true),使其可以链式操作

@Data
@Accessors(chain = true) //链式操作
public class R {

4 )定义 WxPayService 方法
参考:
API 字典 -> 基础支付 -> Native 支付 -> Native 下单:
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
指引文档 -> 基础支付 -> Native 支付 -> 开发指引 -> 【服务端】 Native 下单:
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml
接口
Map < String , Object > nativePay ( Long productId ) throws Exception ;
实现
package com.atguigu.paymentdemo.service.impl;

import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Override
    public Map<String, Object> nativePay(Long productId) throws IOException {

        log.info("生成订单");
        //生成订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setTitle("test");
        orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
        orderInfo.setProductId(productId);
        orderInfo.setTotalFee(1);
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
        //TODO 存入数据库

        log.info("调用统一下单API");
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));

        //请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("appid",wxPayConfig.getAppId());
        paramsMap.put("mchid",wxPayConfig.getMchId());
        paramsMap.put("description",orderInfo.getTitle());
        paramsMap.put("out_trade_no", orderInfo.getOrderNo());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

        Map amountMap = new HashMap<>();
        amountMap.put("total",orderInfo.getTotalFee());
        amountMap.put("currency","CNY");

        paramsMap.put("amount", amountMap);

        //将参数转换为json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数:{}", jsonParams);

        StringEntity entity = new StringEntity(jsonParams, "UTF-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            //相应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200){
                log.info("成功,返回结果:{}",bodyAsString);
            }else if (statusCode == 204){
                log.info("成功");
            }else {
                log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " +
                        bodyAsString);
                throw new IOException("request failed");
            }

            //相应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            String codeUrl = resultMap.get("code_url");
            Map<String, Object> map = new HashMap<>();

            map.put("codeUrl",codeUrl);
            map.put("orderNo",orderInfo.getOrderNo());

            return map;
        } finally {
            response.close();
        }
    }
}

5.3、签名和验签源码解析

1)签名原理

开启 debug 日志
logging :
  level :
    root : info
签名生成流程:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
签名生成源码:

2)验签原理

签名验证流程:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
签名验证源码:

5.4、创建课程订单

1)保存订单

OrderInfoService
接口:
OrderInfo createOrderByProductId(Long productId);
实现:
    @Resource
    private ProductMapper productMapper;

    @Override
    public OrderInfo createOrderByProductId(Long productId) {

        //查找已存在但未支付的订单
        OrderInfo orderInfo = this.getNoPayOrderByProductId(productId);
        if (orderInfo != null) {
            return orderInfo;
        }

        //获取商品信息
        Product product = productMapper.selectById(productId);

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

        return orderInfo;
    }

查找未支付订单: OrderInfoService 中添加辅助方法
    /**
    * 根据商品id查询未支付订单
    * 防止重复创建订单对象
    * @param productId
    * @return
    */
    private OrderInfo getNoPayOrderByProductId(Long productId){

        LambdaQueryWrapper<OrderInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(OrderInfo::getProductId, productId)
                .eq(OrderInfo::getOrderStatus, OrderStatus.NOTPAY.getType());
        OrderInfo orderInfo = baseMapper.selectOne(lambdaQueryWrapper);
        return orderInfo;
    }

2)缓存二维码

OrderInfoService
接口:
void saveCodeUrl ( String orderNo , String codeUrl );
实现:
    /**
     * 存储订单二维码
     * @param orderNo
     * @param codeUrl
     */
    @Override
    public void saveCodeUrl(String orderNo, String codeUrl) {

        LambdaQueryWrapper<OrderInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(OrderInfo::getOrderNo, orderNo);

        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCodeUrl(codeUrl);

        baseMapper.update(orderInfo,lambdaQueryWrapper);
    }

3 )修改 WxPayServiceImpl nativePay 方法
package com.atguigu.paymentdemo.service.impl;

import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.service.WxPayService;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    private OrderInfoService orderInfoService;

    @Override
    public Map<String, Object> nativePay(Long productId) throws IOException {

        log.info("生成订单");
        //生成订单
        OrderInfo orderInfo =  orderInfoService.createOrderByProductId(productId);
        String codeUrl1 = orderInfo.getCodeUrl();
        if (orderInfo != null && !StringUtils.isEmpty(codeUrl1)){
            log.info("订单已存在,二维码已保存");
            //返回二维码
            Map<String, Object> map = new HashMap<>();
            map.put("orderNo", orderInfo.getOrderNo());
            map.put("codeUrl", codeUrl1);
            return map;
        }

        log.info("调用统一下单API");
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));

        //请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("appid",wxPayConfig.getAppId());
        paramsMap.put("mchid",wxPayConfig.getMchId());
        paramsMap.put("description",orderInfo.getTitle());
        paramsMap.put("out_trade_no", orderInfo.getOrderNo());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

        Map amountMap = new HashMap<>();
        amountMap.put("total",orderInfo.getTotalFee());
        amountMap.put("currency","CNY");

        paramsMap.put("amount", amountMap);

        //将参数转换为json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数:{}", jsonParams);

        StringEntity entity = new StringEntity(jsonParams, "UTF-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            //相应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200){
                log.info("成功,返回结果:{}",bodyAsString);
            }else if (statusCode == 204){
                log.info("成功");
            }else {
                log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " +
                        bodyAsString);
                throw new IOException("request failed");
            }

            //相应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            //二维码
            String codeUrl = resultMap.get("code_url");
            //保存二维码
            orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);

            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl",codeUrl);
            map.put("orderNo",orderInfo.getOrderNo());
            //返回二维码
            return map;
        } finally {
            response.close();
        }
    }
}

5.5、显示订单列表

在我的订单页面按时间倒序显示订单列表

1)创建OrderInfoController

package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@CrossOrigin //开放前端的跨域访问
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@ApiOperation("订单列表")
@GetMapping("/list")
public R list(){
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list", list);
}
}

2 )定义 OrderInfoService 方法
接口
List < OrderInfo > listOrderByCreateTimeDesc ();
实现
/**
* 查询订单列表,并倒序查询
* @return
*/
@Override
public List<OrderInfo> listOrderByCreateTimeDesc() {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<OrderInfo>
().orderByDesc("create_time");
return baseMapper.selectList(queryWrapper);
}

6、支付通知API

6.1、内网穿透

1)访问ngrok官网

https://ngrok.com/

2)注册账号、登录

3)下载内网穿透工具

ngrok-stable-windows-amd64.zip

4)设置你的 authToken

为本地计算机做授权配置
ngrok authtoken 6 aYc6Kp7kpxVr8pY88LkG_6x9o18yMY8BASrXiDFMeS

5)启动服务

ngrok http 8090

6)测试外网访问

你获得的外网地址/api/test

6.2、接收通知和返回应答

支付通知 API https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

1)启动ngrok

ngrok http 8090

2)设置通知地址

wxpay.properties
注意:每次重新启动 ngrok ,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io

3)创建通知接口

通知规则:用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理
该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认
为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保
证通知最终能成功。(通知频率为
15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@ApiOperation("支付通知")
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse
response){
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();//应答对象
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
log.info("支付通知的id ===> {}", bodyMap.get("id"));
log.info("支付通知的完整数据 ===> {}", body);
//TODO : 签名的验证
//TODO : 处理订单
//成功应答:成功应答必须为200或204,否则就是失败应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
}

4)测试失败应答

用失败应答替换成功应答
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse
response) throws Exception {
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();
try {
} catch (Exception e) {
e.printStackTrace();
// 测试错误应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "系统错误");
return gson.toJson(map);
}
}

5)测试超时应答

回调通知注意事项: https://pay.weixin.qq.com/wiki/doc/apiv3/Practices/chapter1_1_5.shtml
商户系统收到支付结果通知,需要在 5 秒内 返回应答报文,否则微信支付认为通知失败,后续会
重复发送通知。
// 测试超时应答:添加睡眠时间使应答超时
TimeUnit . SECONDS . sleep ( 5 );

6.3、验签

1)工具类

参考 SDK 源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest

2)验签

@Resource
private Verifier verifier ;
// 签名的验证
WechatPay2ValidatorForRequest validator
= new WechatPay2ValidatorForRequest ( verifier , body , requestId );
if ( ! validator . validate ( request )) {
log . error ( " 通知验签失败 " );
// 失败应答
response . setStatus ( 500 );
map . put ( "code" , "ERROR" );
map . put ( "message" , " 通知验签失败 " );
return gson . toJson ( map );
}
log . info ( " 通知验签成功 " );
//TODO : 处理订单

6.4、解密

 

1WxPayController

nativeNotify 方法中添加处理订单的代码
// 处理订单
wxPayService . processOrder ( bodyMap );

1WxPayService

接口:
void processOrder ( Map < String , Object > bodyMap ) throws GeneralSecurityException ;
实现:
@Override
public void processOrder ( Map < String , Object > bodyMap ) throws
GeneralSecurityException {
log . info ( " 处理订单 " );
String plainText = decryptFromResource ( bodyMap );
// 转换明文
// 更新订单状态
// 记录支付日志
}
辅助方法:
/**
* 对称解密
* @param bodyMap
* @return
*/
private String decryptFromResource ( Map < String , Object > bodyMap ) throws
GeneralSecurityException {
log . info ( " 密文解密 " );
// 通知数据
Map < String , String > resourceMap = ( Map ) bodyMap . get ( "resource" );
// 数据密文
String ciphertext = resourceMap . get ( "ciphertext" );
// 随机串
String nonce = resourceMap . get ( "nonce" );
// 附加数据
String associatedData = resourceMap . get ( "associated_data" );
log . info ( " 密文 ===> {}" , ciphertext );
AesUtil aesUtil = new
AesUtil ( wxPayConfig . getApiV3Key (). getBytes ( StandardCharsets . UTF_8 ));
String plainText =
aesUtil . decryptToString ( associatedData . getBytes ( StandardCharsets . UTF_8 ),
nonce . getBytes ( StandardCharsets . UTF_8 ),
ciphertext );
log . info ( " 明文 ===> {}" , plainText );
return plainText ;
}

6.5、处理订单

1)完善processOrder方法

@Resource
private PaymentInfoService paymentInfoService;
@Override
public void processOrder(Map<String, Object> bodyMap) throws
GeneralSecurityException {
log.info("处理订单");
String plainText = decryptFromResource(bodyMap);
//转换明文
Gson gson = new Gson();
Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plainTextMap.get("out_trade_no");
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
}

2)更新订单状态

OrderInfoService
接口:
void updateStatusByOrderNo ( String orderNo , OrderStatus orderStatus );
实现:
/**
* 根据订单编号更新订单状态
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo ( String orderNo , OrderStatus orderStatus ) {
log . info ( " 更新订单状态 ===> {}" , orderStatus . getType ());
QueryWrapper < OrderInfo > queryWrapper = new QueryWrapper <> ();
queryWrapper . eq ( "order_no" , orderNo );
OrderInfo orderInfo = new OrderInfo ();
orderInfo . setOrderStatus ( orderStatus . getType ());
baseMapper . update ( orderInfo , queryWrapper );
}

3)处理支付日志

PaymentInfoService
接口:
void createPaymentInfo ( String plainText );
实现:
/**
* 记录支付日志
* @param plainText
*/
@Override
public void createPaymentInfo ( String plainText ) {
log . info ( " 记录支付日志 " );
Gson gson = new Gson ();
Map < String , Object > plainTextMap = gson . fromJson ( plainText , HashMap . class );
String orderNo = ( String ) plainTextMap . get ( "out_trade_no" );
String transactionId = ( String ) plainTextMap . get ( "transaction_id" );
String tradeType = ( String ) plainTextMap . get ( "trade_type" );
String tradeState = ( String ) plainTextMap . get ( "trade_state" );
Map < String , Object > amount = ( Map ) plainTextMap . get ( "amount" );
Integer payerTotal = (( Double ) amount . get ( "payer_total" )). intValue ();
PaymentInfo paymentInfo = new PaymentInfo ();
paymentInfo . setOrderNo ( orderNo );
paymentInfo . setPaymentType ( PayType . WXPAY . getType ());
paymentInfo . setTransactionId ( transactionId );
paymentInfo . setTradeType ( tradeType );
paymentInfo . setTradeState ( tradeState );
paymentInfo . setPayerTotal ( payerTotal );
paymentInfo . setContent ( plainText );
baseMapper . insert ( paymentInfo );
}

6.6、处理重复通知

1)测试重复的通知

// 应答超时
// 设置响应超时,可以接收到微信支付的重复的支付结果通知。
// 通知重复,数据库会记录多余的支付日志
TimeUnit . SECONDS . sleep ( 5 );

2)处理重复通知

processOrder 方法中,更新订单状态之前,添加如下代码
// 处理重复通知
// 保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
String orderStatus = orderInfoService . getOrderStatus ( orderNo );
if ( ! OrderStatus . NOTPAY . getType (). equals ( orderStatus )) {
return ;
}
OrderInfoService
接口:
String getOrderStatus ( String orderNo );
实现:
/**
* 根据订单号获取订单状态
* @param orderNo
* @return
*/
@Override
public String getOrderStatus ( String orderNo ) {
QueryWrapper < OrderInfo > queryWrapper = new QueryWrapper <> ();
queryWrapper . eq ( "order_no" , orderNo );
OrderInfo orderInfo = baseMapper . selectOne ( queryWrapper );
// 防止被删除的订单的回调通知的调用
if ( orderInfo == null ){
return null ;
}
return orderInfo . getOrderStatus ();
}

6.7、数据锁  

1)测试通知并发

// 处理重复的通知
// 模拟通知并发
try {
TimeUnit . SECONDS . sleep ( 5 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
// 更新订单状态
// 记录支付日志

2)定义ReentrantLock

定义 ReentrantLock 进行并发控制。注意,必须手动释放锁。
private final ReentrantLock lock = new ReentrantLock ();
@Override
public void processOrder ( Map < String , Object > bodyMap ) throws
GeneralSecurityException {
log . info ( " 处理订单 " );
// 解密报文
String plainText = decryptFromResource ( bodyMap );
// 将明文转换成 map
Gson gson = new Gson ();
HashMap plainTextMap = gson . fromJson ( plainText , HashMap . class );
String orderNo = ( String ) plainTextMap . get ( "out_trade_no" );
/* 在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱 */
// 尝试获取锁:
// 成功获取则立即返回 true ,获取失败则立即返回 false 。不必一直等待锁的释放
if ( lock . tryLock ()){
try {
// 处理重复的通知
// 接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderInfoService . getOrderStatus ( orderNo );
if ( ! OrderStatus . NOTPAY . getType (). equals ( orderStatus )){
return ;
}
// 模拟通知并发
try {
TimeUnit . SECONDS . sleep ( 5 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
// 更新订单状态
orderInfoService . updateStatusByOrderNo ( orderNo ,
OrderStatus . SUCCESS );
// 记录支付日志
paymentInfoService . createPaymentInfo ( plainText );
} finally {
// 要主动释放锁
lock . unlock ();
}
}
}

7、商户定时查询本地订单

7.1、后端定义商户查单接口

支付成功后,商户侧查询本地数据库,订单是否支付成功
/**
* 查询本地订单状态
*/
@ApiOperation ( " 查询本地订单状态 " )
@GetMapping ( "/query-order-status/{orderNo}" )
public R queryOrderStatus ( @PathVariable String orderNo ) {
String orderStatus = orderInfoService . getOrderStatus ( orderNo );
if ( OrderStatus . SUCCESS . getType (). equals ( orderStatus )) { // 支付成功
return R . ok ();
}
return R . ok (). setCode ( 101 ). setMessage ( " 支付中 ..." );
}

7.2、前端定时轮询查单

在二维码展示页面,前端定时轮询查询订单是否已支付,如果支付成功则跳转到订单页面

1)定义定时器

// 启动定时器
this . timer = setInterval (() => {
// 查询订单是否支付成功
this . queryOrderStatus ()
}, 3000 )

2)查询订单

// 查询订单状态
queryOrderStatus () {
orderInfoApi . queryOrderStatus ( this . orderNo ). then ( response => {
console . log ( ' 查询订单状态: ' + response . code )
// 支付成功后的页面跳转
if ( response . code === 0 ) {
console . log ( ' 清除定时器 ' )
clearInterval ( this . timer )
// 三秒后跳转到订单列表
setTimeout (() => {
this . $router . push ({ path : '/success' })
}, 3000 )
}
})
}

8、用户取消订单API

实现用户主动取消订单的功能

8.1、定义取消订单接口

WxPayController 中添加接口方法
/**
* 用户取消订单
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation ( " 用户取消订单 " )
@PostMapping ( "/cancel/{orderNo}" )
public R cancel ( @PathVariable String orderNo ) throws Exception {
log . info ( " 取消订单 " );
wxPayService . cancelOrder ( orderNo );
return R . ok (). setMessage ( " 订单已取消 " );
}
8.2 WxPayService
接口
void cancelOrder ( String orderNo ) throws Exception ;
实现
/**
* 用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder ( String orderNo ) throws Exception {
// 调用微信支付的关单接口
this . closeOrder ( orderNo );
// 更新商户端的订单状态
orderInfoService . updateStatusByOrderNo ( orderNo , OrderStatus . CANCEL );
}
关单方法
/**
* 关单接口的调用
* @param orderNo
*/
private void closeOrder ( String orderNo ) throws Exception {
log . info ( " 关单接口的调用,订单号 ===> {}" , orderNo );
// 创建远程请求对象
String url = String . format ( WxApiType . CLOSE_ORDER_BY_NO . getType (), orderNo );
url = wxPayConfig . getDomain (). concat ( url );
HttpPost httpPost = new HttpPost ( url );
// 组装 json 请求体
Gson gson = new Gson ();
Map < String , String > paramsMap = new HashMap <> ();
paramsMap . put ( "mchid" , wxPayConfig . getMchId ());
String jsonParams = gson . toJson ( paramsMap );
log . info ( " 请求参数 ===> {}" , jsonParams );
// 将请求参数设置到请求对象中
StringEntity entity = new StringEntity ( jsonParams , "utf-8" );
entity . setContentType ( "application/json" );
httpPost . setEntity ( entity );
httpPost . setHeader ( "Accept" , "application/json" );
// 完成签名并执行请求
CloseableHttpResponse response = wxPayClient . execute ( httpPost );
try {
int statusCode = response . getStatusLine (). getStatusCode (); // 响应状态码
if ( statusCode == 200 ) { // 处理成功
log . info ( " 成功 200" );
} else if ( statusCode == 204 ) { // 处理成功,无返回 Body
log . info ( " 成功 204" );
} else {
log . info ( "Native 下单失败 , 响应码 = " + statusCode );
throw new IOException ( "request failed" );
}
} finally {
response . close ();
}
}

9、微信支付查单API

9.1、查单接口的调用

商户后台未收到异步支付结果通知时,商户应该主动调用《 微信支付查单接口 》,同步订单状态。

1WxPayController

/**
* 查询订单
* @param orderNo
* @return
* @throws URISyntaxException
* @throws IOException
*/
@ApiOperation ( " 查询订单:测试订单状态用 " )
@GetMapping ( "query/{orderNo}" )
public R queryOrder ( @PathVariable String orderNo ) throws Exception {
log . info ( " 查询订单 " );
String bodyAsString = wxPayService . queryOrder ( orderNo );
return R . ok (). setMessage ( " 查询成功 " ). data ( "bodyAsString" , bodyAsString );
}

2WxPayService

接口
String queryOrder ( String orderNo ) throws Exception ;
实现
/**
* 查单接口调用
*/
@Override
public String queryOrder ( String orderNo ) throws Exception {
log . info ( " 查单接口调用 ===> {}" , orderNo );
String url = String . format ( WxApiType . ORDER_QUERY_BY_NO . getType (), orderNo );
url = wxPayConfig . getDomain (). concat ( url ). concat ( "?
mchid=" ). concat ( wxPayConfig . getMchId ());
HttpGet httpGet = new HttpGet ( url );
httpGet . setHeader ( "Accept" , "application/json" );
// 完成签名并执行请求
CloseableHttpResponse response = wxPayClient . execute ( httpGet );
try {
String bodyAsString = EntityUtils . toString ( response . getEntity ()); // 响应体
int statusCode = response . getStatusLine (). getStatusCode (); // 响应状态码
if ( statusCode == 200 ) { // 处理成功
log . info ( " 成功 , 返回结果 = " + bodyAsString );
} else if ( statusCode == 204 ) { // 处理成功,无返回 Body
log . info ( " 成功 " );
} else {
log . info ( "Native 下单失败 , 响应码 = " + statusCode + ", 返回结果 = " +
bodyAsString );
throw new IOException ( "request failed" );
}
return bodyAsString ;
} finally {
response . close ();
}
}

9.2、集成Spring Task

Spring 3.0 后提供 Spring Task 实现任务调度
1 )启动类添加注解
statistics 启动类添加注解
@EnableScheduling

2)测试定时任务

创建 task 包,创建 WxPayTask.java
package com . atguigu . paymentdemo . task ;
import lombok . extern . slf4j . Slf4j ;
import org . springframework . scheduling . annotation . Scheduled ;
import org . springframework . stereotype . Component ;
@Slf4j
@Component
public class WxPayTask {
/**
* 测试
* (cron=" 秒 分 时 日 月 周 ")
* * :每隔一秒执行
* 0/3 :从第 0 秒开始,每隔 3 秒执行一次
* 1-3: 从第 1 秒开始执行,到第 3 秒结束执行
* 1,2,3 :第 1 2 3 秒执行
* ? :不指定,若指定日期,则不指定周,反之同理
*/
@Scheduled ( cron = "0/3 * * * * ?" )
public void task1 () {
log . info ( "task1 执行 " );
}
}

9.3、定时查找超时订单

1WxPayTask

@Resource
private OrderInfoService orderInfoService ;
@Resource
private WxPayService wxPayService ;
/**
* 从第 0 秒开始每隔 30 秒执行 1 次,查询创建超过 5 分钟,并且未支付的订单
*/
@Scheduled ( cron = "0/30 * * * * ?" )
public void orderConfirm () throws Exception {
log . info ( "orderConfirm 被执行 ......" );
List < OrderInfo > orderInfoList = orderInfoService . getNoPayOrderByDuration ( 5 );
for ( OrderInfo orderInfo : orderInfoList ) {
String orderNo = orderInfo . getOrderNo ();
log . warn ( " 超时订单 ===> {}" , orderNo );
// 核实订单状态:调用微信支付查单接口
wxPayService . checkOrderStatus ( orderNo );
}
}

2OrderInfoService

接口
List < OrderInfo > getNoPayOrderByDuration ( int minutes );
实现
/**
* 找出创建超过 minutes 分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List < OrderInfo > getNoPayOrderByDuration ( int minutes ) {
//minutes 分钟之前的时间
Instant instant = Instant . now (). minus ( Duration . ofMinutes ( minutes ));
QueryWrapper < OrderInfo > queryWrapper = new QueryWrapper <> ();
queryWrapper . eq ( "order_status" , OrderStatus . NOTPAY . getType ());
queryWrapper . le ( "create_time" , instant );
List < OrderInfo > orderInfoList = baseMapper . selectList ( queryWrapper );
return orderInfoList ;
}

9.4、处理超时订单

WxPayService
核实订单状态
接口:
void checkOrderStatus ( String orderNo ) throws Exception ;
实现:
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态,并记录支付日志
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* @param orderNo
*/
@Override
public void checkOrderStatus ( String orderNo ) throws Exception {
log . warn ( " 根据订单号核实订单状态 ===> {}" , orderNo );
// 调用微信支付查单接口
String result = this . queryOrder ( orderNo );
Gson gson = new Gson ();
Map resultMap = gson . fromJson ( result , HashMap . class );
// 获取微信支付端的订单状态
Object tradeState = resultMap . get ( "trade_state" );
// 判断订单状态
if ( WxTradeState . SUCCESS . getType (). equals ( tradeState )){
log . warn ( " 核实订单已支付 ===> {}" , orderNo );
// 如果确认订单已支付则更新本地订单状态
orderInfoService . updateStatusByOrderNo ( orderNo , OrderStatus . SUCCESS );
// 记录支付日志
paymentInfoService . createPaymentInfo ( result );
}
if ( WxTradeState . NOTPAY . getType (). equals ( tradeState )){
log . warn ( " 核实订单未支付 ===> {}" , orderNo );
// 如果订单未支付,则调用关单接口
this . closeOrder ( orderNo );
// 更新本地订单状态
orderInfoService . updateStatusByOrderNo ( orderNo , OrderStatus . CLOSED );
}
}
6.8
接口:
OrderInfo getOrderByOrderNo ( String orderNo );
实现:
/**
* 根据订单号获取订单
* @param orderNo
* @return
*/
@Override
public OrderInfo getOrderByOrderNo ( String orderNo ) {
QueryWrapper < OrderInfo > queryWrapper = new QueryWrapper <> ();
queryWrapper . eq ( "order_no" , orderNo );
OrderInfo orderInfo = baseMapper . selectOne ( queryWrapper );
return orderInfo ;
}

11、申请退款API

文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml

11.1、创建退款单

1)根据订单号查询订单

OrderInfoService
接口:
OrderInfo getOrderByOrderNo ( String orderNo );
实现:
/**
* 根据订单号获取订单
* @param orderNo
* @return
*/
@Override
public OrderInfo getOrderByOrderNo ( String orderNo ) {
QueryWrapper < OrderInfo > queryWrapper = new QueryWrapper <> ();
queryWrapper . eq ( "order_no" , orderNo );
OrderInfo orderInfo = baseMapper . selectOne ( queryWrapper );
return orderInfo ;
}

2)创建退款单记录

RefundsInfoService
接口:
RefundInfo createRefundByOrderNo ( String orderNo , String reason );
实现:
@Resource
private OrderInfoService orderInfoService ;
/**
* 根据订单号创建退款订单
* @param orderNo
* @return
*/
@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 ); // 退款原因
// 保存退款订单
baseMapper . insert ( refundInfo );
return refundInfo ;
}

11.2、更新退款单

RefundInfoService
接口:
void updateRefund ( String content );
实现:
/**
* 记录退款记录
* @param content
*/
@Override
public void updateRefund ( String content ) {
// json 字符串转换成 Map
Gson gson = new Gson ();
Map < String , String > resultMap = gson . fromJson ( content , HashMap . class );
// 根据退款单编号修改退款单
QueryWrapper < RefundInfo > queryWrapper = new QueryWrapper <> ();
queryWrapper . 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" )); // 退款状态
refundInfo . setContentReturn ( content ); // 将全部响应结果存入数据库的 content 字段
}
// 退款回调中的回调参数
if ( resultMap . get ( "refund_status" ) != null ){
refundInfo . setRefundStatus ( resultMap . get ( "refund_status" )); // 退款状态
refundInfo . setContentNotify ( content ); // 将全部响应结果存入数据库的 content 字段
}
// 更新退款单
baseMapper . update ( refundInfo , queryWrapper );
}

11.3、申请退款

1 WxPayController
@ApiOperation ( " 申请退款 " )
@PostMapping ( "/refunds/{orderNo}/{reason}" )
public R refunds ( @PathVariable String orderNo , @PathVariable String reason )
throws Exception {
log . info ( " 申请退款 " );
wxPayService . refund ( orderNo , reason );
return R . ok ();
}
2 WxPayService
接口:
void refund ( String orderNo , String reason ) throws Exception ;
实现:
@Resource
private RefundInfoService refundsInfoService ;
/**
* 退款
* @param orderNo
* @param reason
* @throws IOException
*/
@Transactional ( rollbackFor = Exception . class )
@Override
public void refund ( String orderNo , String reason ) throws Exception {
log . info ( " 创建退款单记录 " );
// 根据订单编号创建退款单
RefundInfo refundsInfo = refundsInfoService . createRefundByOrderNo ( orderNo ,
reason );
log . info ( " 调用退款 API" );
// 调用统一下单 API
String url =
wxPayConfig . getDomain (). concat ( WxApiType . DOMESTIC_REFUNDS . getType ());
HttpPost httpPost = new HttpPost ( url );
// 请求 body 参数
Gson gson = new Gson ();
Map paramsMap = new HashMap ();
paramsMap . put ( "out_trade_no" , orderNo ); // 订单编号
paramsMap . put ( "out_refund_no" , refundsInfo . getRefundNo ()); // 退款单编号
paramsMap . put ( "reason" , reason ); // 退款原因
paramsMap . put ( "notify_url" ,
wxPayConfig . getNotifyDomain (). concat ( WxNotifyType . REFUND_NOTIFY . getType ())); // 退
款通知地址
Map amountMap = new HashMap ();
amountMap . put ( "refund" , refundsInfo . getRefund ()); // 退款金额
amountMap . put ( "total" , refundsInfo . getTotalFee ()); // 原订单金额
amountMap . put ( "currency" , "CNY" ); // 退款币种
paramsMap . put ( "amount" , amountMap );
// 将参数转换成 json 字符串
String jsonParams = gson . toJson ( paramsMap );
log . info ( " 请求参数 ===> {}" + jsonParams );
StringEntity entity = new StringEntity ( jsonParams , "utf-8" );
entity . setContentType ( "application/json" ); // 设置请求报文格式
httpPost . setEntity ( entity ); // 将请求报文放入请求对象
httpPost . setHeader ( "Accept" , "application/json" ); // 设置响应报文格式
// 完成签名并执行请求,并完成验签
CloseableHttpResponse response = wxPayClient . execute ( httpPost );
try {
// 解析响应结果
String bodyAsString = EntityUtils . toString ( response . getEntity ());
int statusCode = response . getStatusLine (). getStatusCode ();
if ( statusCode == 200 ) {
log . info ( " 成功 , 退款返回结果 = " + bodyAsString );
} else if ( statusCode == 204 ) {
log . info ( " 成功 " );
} else {
throw new RuntimeException ( " 退款异常 , 响应码 = " + statusCode + ", 退款
返回结果 = " + bodyAsString );
}
// 更新订单状态
orderInfoService . updateStatusByOrderNo ( orderNo ,
OrderStatus . REFUND_PROCESSING );
// 更新退款单
refundsInfoService . updateRefund ( bodyAsString );
} finally {
response . close ();
}
}

12、查询退款API

文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml

12.1、查单接口的调用

1WxPayController

/**
* 查询退款
* @param refundNo
* @return
* @throws Exception
*/
@ApiOperation ( " 查询退款:测试用 " )
@GetMapping ( "/query-refund/{refundNo}" )
public R queryRefund ( @PathVariable String refundNo ) throws Exception {
log . info ( " 查询退款 " );
String result = wxPayService . queryRefund ( refundNo );
return R . ok (). setMessage ( " 查询成功 " ). data ( "result" , result );
}

2WxPayService

接口:
String queryRefund ( String orderNo ) throws Exception ;
实现:
/**
* 查询退款接口调用
* @param refundNo
* @return
*/
@Override
public String queryRefund ( String refundNo ) throws Exception {
log . info ( " 查询退款接口调用 ===> {}" , refundNo );
String url = String . format ( WxApiType . DOMESTIC_REFUNDS_QUERY . getType (),
refundNo );
url = wxPayConfig . getDomain (). concat ( url );
// 创建远程 Get 请求对象
HttpGet httpGet = new HttpGet ( url );
httpGet . setHeader ( "Accept" , "application/json" );
// 完成签名并执行请求
CloseableHttpResponse response = wxPayClient . execute ( httpGet );
try {
String bodyAsString = EntityUtils . toString ( response . getEntity ());
int statusCode = response . getStatusLine (). getStatusCode ();
if ( statusCode == 200 ) {
log . info ( " 成功 , 查询退款返回结果 = " + bodyAsString );
} else if ( statusCode == 204 ) {
log . info ( " 成功 " );
} else {
throw new RuntimeException ( " 查询退款异常 , 响应码 = " + statusCode + ",
查询退款返回结果 = " + bodyAsString );
}
return bodyAsString ;
} finally {
response . close ();
}
}

12.2、定时查找退款中的订单

1WxPayTask

/**
* 从第 0 秒开始每隔 30 秒执行 1 次,查询创建超过 5 分钟,并且未成功的退款单
*/
@Scheduled ( cron = "0/30 * * * * ?" )
public void refundConfirm () throws Exception {
log . info ( "refundConfirm 被执行 ......" );
// 找出申请退款超过 5 分钟并且未成功的退款单
List < RefundInfo > refundInfoList =
refundInfoService . getNoRefundOrderByDuration ( 5 );
for ( RefundInfo refundInfo : refundInfoList ) {
String refundNo = refundInfo . getRefundNo ();
log . warn ( " 超时未退款的退款单号 ===> {}" , refundNo );
// 核实订单状态:调用微信支付查询退款接口
wxPayService . checkRefundStatus ( refundNo );
}
}

2RefundInfoService

接口
List < RefundInfo > getNoRefundOrderByDuration ( int minutes );
实现
/**
* 找出申请退款超过 minutes 分钟并且未成功的退款单
* @param minutes
* @return
*/
@Override
public List < RefundInfo > getNoRefundOrderByDuration ( int minutes ) {
//minutes 分钟之前的时间
Instant instant = Instant . now (). minus ( Duration . ofMinutes ( minutes ));
QueryWrapper < RefundInfo > queryWrapper = new QueryWrapper <> ();
queryWrapper . eq ( "refund_status" , WxRefundStatus . PROCESSING . getType ());
queryWrapper . le ( "create_time" , instant );
List < RefundInfo > refundInfoList = baseMapper . selectList ( queryWrapper );
return refundInfoList ;
}

12.3、处理超时未退款订单

WxPayService
核实订单状态
接口:
void checkRefundStatus ( String refundNo );
实现:
/**
* 根据退款单号核实退款单状态
* @param refundNo
* @return
*/
@Transactional ( rollbackFor = Exception . class )
@Override
public void checkRefundStatus ( String refundNo ) throws Exception {
log . warn ( " 根据退款单号核实退款单状态 ===> {}" , refundNo );
// 调用查询退款单接口
String result = this . queryRefund ( refundNo );
// 组装 json 请求体字符串
Gson gson = new Gson ();
Map < String , String > resultMap = gson . fromJson ( result , HashMap . class );
// 获取微信支付端退款状态
String status = resultMap . get ( "status" );
String orderNo = resultMap . get ( "out_trade_no" );
if ( WxRefundStatus . SUCCESS . getType (). equals ( status )) {
log . warn ( " 核实订单已退款成功 ===> {}" , refundNo );
// 如果确认退款成功,则更新订单状态
orderInfoService . updateStatusByOrderNo ( orderNo ,
OrderStatus . REFUND_SUCCESS );
// 更新退款单
refundsInfoService . updateRefund ( result );
}
if ( WxRefundStatus . ABNORMAL . getType (). equals ( status )) {
log . warn ( " 核实订单退款异常 ===> {}" , refundNo );
// 如果确认退款成功,则更新订单状态
orderInfoService . updateStatusByOrderNo ( orderNo ,
OrderStatus . REFUND_ABNORMAL );
// 更新退款单
refundsInfoService . updateRefund ( result );
}
}

13、退款结果通知API

文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml

13.1、接收退款通知

WxPayController
/**
* 退款结果通知
* 退款状态改变后,微信会把相关退款结果发送给商户。
*/
@PostMapping ( "/refunds/notify" )
public String refundsNotify ( HttpServletRequest request , HttpServletResponse
response ){
log . info ( " 退款通知执行 " );
Gson gson = new Gson ();
Map < String , String > map = new HashMap <> (); // 应答对象
try {
// 处理通知参数
String body = HttpUtils . readData ( request );
Map < String , Object > bodyMap = gson . fromJson ( body , HashMap . class );
String requestId = ( String ) bodyMap . get ( "id" );
log . info ( " 支付通知的 id ===> {}" , requestId );
// 签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest ( verifier , requestId , body );
if ( ! wechatPay2ValidatorForRequest . validate ( request )){
log . error ( " 通知验签失败 " );
// 失败应答
response . setStatus ( 500 );
map . put ( "code" , "ERROR" );
map . put ( "message" , " 通知验签失败 " );
return gson . toJson ( map );
}
log . info ( " 通知验签成功 " );
// 处理退款单
wxPayService . processRefund ( bodyMap );
// 成功应答
response . setStatus ( 200 );
map . put ( "code" , "SUCCESS" );
map . put ( "message" , " 成功 " );
return gson . toJson ( map );
} catch ( Exception e ) {
e . printStackTrace ();
// 失败应答
response . setStatus ( 500 );
map . put ( "code" , "ERROR" );
map . put ( "message" , " 失败 " );
return gson . toJson ( map );
}
}

13.2、处理订单和退款单

WxPayService
接口:
void processRefund ( Map < String , Object > bodyMap ) throws Exception ;
实现:
/**
* 处理退款单
*/
@Transactional ( rollbackFor = Exception . class )
@Override
public void processRefund ( Map < String , Object > bodyMap ) throws Exception {
log . info ( " 退款单 " );
// 解密报文
String plainText = decryptFromResource ( bodyMap );
// 将明文转换成 map
Gson gson = new Gson ();
HashMap plainTextMap = gson . fromJson ( plainText , HashMap . class );
String orderNo = ( String ) plainTextMap . get ( "out_trade_no" );
if ( lock . tryLock ()){
try {
String orderStatus = orderInfoService . getOrderStatus ( orderNo );
if ( ! OrderStatus . REFUND_PROCESSING . getType (). equals ( orderStatus )) {
return ;
}
// 更新订单状态
orderInfoService . updateStatusByOrderNo ( orderNo ,
OrderStatus . REFUND_SUCCESS );
// 更新退款单
refundsInfoService . updateRefund ( plainText );
} finally {
// 要主动释放锁
lock . unlock ();
}
}
}

14、账单

14.1、申请交易账单和资金账单

1WxPayController

@ApiOperation ( " 获取账单 url :测试用 " )
@GetMapping ( "/querybill/{billDate}/{type}" )
public R queryTradeBill (
@PathVariable String billDate ,
@PathVariable String type ) throws Exception {
log . info ( " 获取账单 url" );
String downloadUrl = wxPayService . queryBill ( billDate , type );
return R . ok (). setMessage ( " 获取账单 url 成功 " ). data ( "downloadUrl" , downloadUrl );
}

2WxPayService

接口:
String queryBill ( String billDate , String type ) throws Exception ;
实现
/**
* 申请账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String queryBill ( String billDate , String type ) throws Exception {
log . warn ( " 申请账单接口调用 {}" , billDate );
String url = "" ;
if ( "tradebill" . equals ( type )){
url = WxApiType . TRADE_BILLS . getType ();
} else if ( "fundflowbill" . equals ( type )){
url = WxApiType . FUND_FLOW_BILLS . getType ();
} else {
throw new RuntimeException ( " 不支持的账单类型 " );
}
url = wxPayConfig . getDomain (). concat ( url ). concat ( "?
bill_date=" ). concat ( billDate );
// 创建远程 Get 请求对象
HttpGet httpGet = new HttpGet ( url );
httpGet . addHeader ( "Accept" , "application/json" );
// 使用 wxPayClient 发送请求得到响应
CloseableHttpResponse response = wxPayClient . execute ( httpGet );
try {
String bodyAsString = EntityUtils . toString ( response . getEntity ());
int statusCode = response . getStatusLine (). getStatusCode ();
if ( statusCode == 200 ) {
log . info ( " 成功 , 申请账单返回结果 = " + bodyAsString );
} else if ( statusCode == 204 ) {
log . info ( " 成功 " );
} else {
throw new RuntimeException ( " 申请账单异常 , 响应码 = " + statusCode + ",
申请账单返回结果 = " + bodyAsString );
}
// 获取账单下载地址
Gson gson = new Gson ();
Map < String , String > resultMap = gson . fromJson ( bodyAsString ,
HashMap . class );
return resultMap . get ( "download_url" );
} finally {
response . close ();
}
}

14.2、下载账单

1WxPayController

@ApiOperation ( " 下载账单 " )
@GetMapping ( "/downloadbill/{billDate}/{type}" )
public R downloadBill (
@PathVariable String billDate ,
@PathVariable String type ) throws Exception {
log . info ( " 下载账单 " );
String result = wxPayService . downloadBill ( billDate , type );
return R . ok (). data ( "result" , result );
}

2WxPayService

接口:
String downloadBill ( String billDate , String type ) throws Exception ;
实现:
/**
* 下载账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String downloadBill ( String billDate , String type ) throws Exception {
log . warn ( " 下载账单接口调用 {}, {}" , billDate , type );
// 获取账单 url 地址
String downloadUrl = this . queryBill ( billDate , type );
// 创建远程 Get 请求对象
HttpGet httpGet = new HttpGet ( downloadUrl );
httpGet . addHeader ( "Accept" , "application/json" );
// 使用 wxPayClient 发送请求得到响应
CloseableHttpResponse response = wxPayNoSignClient . execute ( httpGet );
try {
String bodyAsString = EntityUtils . toString ( response . getEntity ());
int statusCode = response . getStatusLine (). getStatusCode ();
if ( statusCode == 200 ) {
log . info ( " 成功 , 下载账单返回结果 = " + bodyAsString );
} else if ( statusCode == 204 ) {
log . info ( " 成功 " );
} else {
throw new RuntimeException ( " 下载账单异常 , 响应码 = " + statusCode + ",
下载账单返回结果 = " + bodyAsString );
}
return bodyAsString ;
} finally {
response . close ();
}
}

五、基础支付API V2

1V2V3的比较

2、引入依赖和工具

2.1、引入依赖

<!-- 微信支付 -->
<dependency>
<groupId> com.github.wxpay </groupId>
<artifactId> wxpay-sdk </artifactId>
<version> 0.0.3 </version>
</dependency>

2.2、复制工具类

2.3、添加商户APIv2 key

yml 文件
# APIv2 密钥
wxpay.partnerKey : T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
WxPayConfifig.java
private String partnerKey ;

2.4、添加枚举

enum WxApiType
/**
* Native 下单 V2
*/
NATIVE_PAY_V2 ( "/pay/unifiedorder" ),
enum WxNotifyType
/**
* 支付通知 V2
*/
NATIVE_NOTIFY_V2 ( "/api/wx-pay-v2/native/notify" ),

3、统一下单

3.1、创建WxPayV2Controller

package com . atguigu . paymentdemo . controller ;
import com . atguigu . paymentdemo . service . WxPayService ;
import com . atguigu . paymentdemo . vo . R ;
import io . swagger . annotations . Api ;
import io . swagger . annotations . ApiOperation ;
import lombok . extern . slf4j . Slf4j ;
import org . springframework . web . bind . annotation . * ;
import javax . annotation . Resource ;
import javax . servlet . http . HttpServletRequest ;
import java . util . Map ;
@CrossOrigin // 跨域
@RestController
@RequestMapping ( "/api/wx-pay-v2" )
@Api ( tags = " 网站微信支付 APIv2" )
@Slf4j
public class WxPayV2Controller {
@Resource
private WxPayService wxPayService ;
/**
* Native 下单
* @param productId
* @return
* @throws Exception
*/
@ApiOperation ( " 调用统一下单 API ,生成支付二维码 " )
@PostMapping ( "/native/{productId}" )
public R createNative ( @PathVariable Long productId , HttpServletRequest
request ) throws Exception {
log . info ( " 发起支付请求 v2" );
String remoteAddr = request . getRemoteAddr ();
Map < String , Object > map = wxPayService . nativePayV2 ( productId ,
remoteAddr );
return R . ok (). setData ( map );
}
}

3.2WxPayService

接口:
Map < String , Object > nativePayV2 ( Long productId , String remoteAddr ) throws
Exception ;
实现:
@Override
public Map < String , Object > nativePayV2 ( Long productId , String remoteAddr ) throws
Exception {
log . info ( " 生成订单 " );
// 生成订单
OrderInfo orderInfo = orderInfoService . createOrderByProductId ( productId );
String codeUrl = orderInfo . getCodeUrl ();
if ( orderInfo != null && ! StringUtils . isEmpty ( codeUrl )){
log . info ( " 订单已存在,二维码已保存 " );
// 返回二维码
Map < String , Object > map = new HashMap <> ();
map . put ( "codeUrl" , codeUrl );
map . put ( "orderNo" , orderInfo . getOrderNo ());
return map ;
}
log . info ( " 调用统一下单 API" );
HttpClientUtils client = new
HttpClientUtils ( "https://api.mch.weixin.qq.com/pay/unifiedorder" );
// 组装接口参数
Map < String , String > params = new HashMap <> ();
params . put ( "appid" , wxPayConfig . getAppid ()); // 关联的公众号的 appid
params . put ( "mch_id" , wxPayConfig . getMchId ()); // 商户号
params . put ( "nonce_str" , WXPayUtil . generateNonceStr ()); // 生成随机字符串
params . put ( "body" , orderInfo . getTitle ());
params . put ( "out_trade_no" , orderInfo . getOrderNo ());
// 注意,这里必须使用字符串类型的参数(总金额:分)
String totalFee = orderInfo . getTotalFee () + "" ;
params . put ( "total_fee" , totalFee );
params . put ( "spbill_create_ip" , remoteAddr );
params . put ( "notify_url" ,
wxPayConfig . getNotifyDomain (). concat ( WxNotifyType . NATIVE_NOTIFY . getType ()));
params . put ( "trade_type" , "NATIVE" );
// 将参数转换成 xml 字符串格式:生成带有签名的 xml 格式字符串
String xmlParams = WXPayUtil . generateSignedXml ( params ,
wxPayConfig . getPartnerKey ());
log . info ( "\n xmlParams \n" + xmlParams );
client . setXmlParam ( xmlParams ); // 将参数放入请求对象的方法体
client . setHttps ( true ); // 使用 https 形式发送
client . post (); // 发送请求
String resultXml = client . getContent (); // 得到响应结果
log . info ( "\n resultXml \n" + resultXml );
// xml 响应结果转成 map 对象
Map < String , String > resultMap = WXPayUtil . xmlToMap ( resultXml );
// 错误处理
if ( "FAIL" . equals ( resultMap . get ( "return_code" )) ||
"FAIL" . equals ( resultMap . get ( "result_code" ))){
log . error ( " 微信支付统一下单错误 ===> {} " , resultXml );
throw new RuntimeException ( " 微信支付统一下单错误 " );
}
// 二维码
codeUrl = resultMap . get ( "code_url" );
// 保存二维码
String orderNo = orderInfo . getOrderNo ();
orderInfoService . saveCodeUrl ( orderNo , codeUrl );
// 返回二维码
Map < String , Object > map = new HashMap <> ();
map . put ( "codeUrl" , codeUrl );
map . put ( "orderNo" , orderInfo . getOrderNo ());
return map ;
}

4、支付回调

@Resource
private WxPayService wxPayService ;
@Resource
private WxPayConfig wxPayConfig ;
@Resource
private OrderInfoService orderInfoService ;
@Resource
private PaymentInfoService paymentInfoService ;
private final ReentrantLock lock = new ReentrantLock ();
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@PostMapping ( "/native/notify" )
public String wxNotify ( HttpServletRequest request ) throws Exception {
System . out . println ( " 微信发送的回调 " );
Map < String , String > returnMap = new HashMap <> (); // 应答对象
// 处理通知参数
String body = HttpUtils . readData ( request );
// 验签
if ( ! WXPayUtil . isSignatureValid ( body , wxPayConfig . getPartnerKey ())) {
log . error ( " 通知验签失败 " );
// 失败应答
returnMap . put ( "return_code" , "FAIL" );
returnMap . put ( "return_msg" , " 验签失败 " );
String returnXml = WXPayUtil . mapToXml ( returnMap );
return returnXml ;
}
// 解析 xml 数据
Map < String , String > notifyMap = WXPayUtil . xmlToMap ( body );
// 判断通信和业务是否成功
if ( ! "SUCCESS" . equals ( notifyMap . get ( "return_code" )) ||
! "SUCCESS" . equals ( notifyMap . get ( "result_code" ))) {
log . error ( " 失败 " );
// 失败应答
returnMap . put ( "return_code" , "FAIL" );
returnMap . put ( "return_msg" , " 失败 " );
String returnXml = WXPayUtil . mapToXml ( returnMap );
return returnXml ;
}
// 获取商户订单号
String orderNo = notifyMap . get ( "out_trade_no" );
OrderInfo orderInfo = orderInfoService . getOrderByOrderNo ( orderNo );
// 并校验返回的订单金额是否与商户侧的订单金额一致
if ( orderInfo != null && orderInfo . getTotalFee () !=
Long . parseLong ( notifyMap . get ( "total_fee" ))) {
log . error ( " 金额校验失败 " );
// 失败应答
returnMap . put ( "return_code" , "FAIL" );
returnMap . put ( "return_msg" , " 金额校验失败 " );
String returnXml = WXPayUtil . mapToXml ( returnMap );
return returnXml ;
}
// 处理订单
if ( lock . tryLock ()){
try {
// 处理重复的通知
// 接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderInfoService . getOrderStatus ( orderNo );
if ( OrderStatus . NOTPAY . getType (). equals ( orderStatus )){
// 更新订单状态
orderInfoService . updateStatusByOrderNo ( orderNo ,
OrderStatus . SUCCESS );
// 记录支付日志
paymentInfoService . createPaymentInfo ( body );
}
} finally {
// 要主动释放锁
lock . unlock ();
}
}
returnMap . put ( "return_code" , "SUCCESS" );
returnMap . put ( "return_msg" , "OK" );
String returnXml = WXPayUtil . mapToXml ( returnMap );
log . info ( " 支付成功,已应答 " );
return returnXml ;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值