Java-微信支付

微信支付介绍和接入指引

微信支付产品介绍

付款码支付

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

JSAPI支付

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

特点:用户在客户端输入支付金额

小程序支付

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

Native支付

Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。

特点:商家预先指定支付金额

APP支付

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

刷脸支付

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

接入指引

获取商户号

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

获取APPID

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

获取API秘钥

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

获取APIV3秘钥

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

申请商户API证书

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

获取微信平台证书

可以预先下载,也可以通过编程的方式获取。

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

信息安全的基础 - 机密性

  • 明文:加密前的消息叫“明文”(plain text)
  • 密文:加密后的文本叫“密文”(cipher text)
  • 密钥:只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)

“密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二
进制串

  • 加密:实现机密性最常用的手段是“加密”(encrypt)

按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。

  • 解密:使用密钥还原明文的过程叫“解密”(decrypt)
  • 加密算法:加密解密的操作过程就是“加密算法”

所有的加密算法都是公开的,而算法使用的“密钥”则必须保密

对称加密和非对称加密

  • 对称加密
  • 特点:只使用一个密钥,密钥必须保密,常用的有 AES算法
  • 优点:运算速度快
  • 缺点:秘钥需要信息交换的双方共享,一旦被窃取,消息会被破解,无法做到安全的密钥交换
  • 非对称加密
  • 特点:使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,常用的有 RSA
  • 优点:黑客获取公钥无法破解密文,解决了密钥交换的问题
  • 缺点:运算速度非常慢
  • 混合加密

实际场景中把对称加密和非对称加密结合起来使用。

身份认证

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

摘要算法(Digest Algorithm)

摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹
作用:
保证信息的完整性
特点:

  • 不可逆:只有算法,没有秘钥,只能加密,不能解密
  • 难题友好性:想要破解,只能暴力枚举
  • 发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
  • 抗碰撞性:原文不同,计算后的摘要也要不同

常见摘要算法:
MD5、SHA1、SHA2(SHA224、SHA256、SHA384)

数字签名

数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认
签名和验证签名的流程:
在这里插入图片描述

数字证书

数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。
不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA颁发
https协议中的数字证书:
在这里插入图片描述

微信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

API密钥和APIv3密钥

都是对称加密需要使用的加密和解密密钥
API密钥对应V2版本的API
APIv3密钥对应V3版本的API

案例项目创建

创建SpringBoot项目

  1. 创建module(payment-demo)
  2. pom
	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zdz</groupId>
    <artifactId>payment-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>payment-demo</name>
    <description>payment-demo</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
  1. yml
server:
  port: 8090 #服务端口
spring:
  application:
    name: payment-demo # 应用名称
  1. controller
@RestController
@RequestMapping("/api/product")
@CrossOrigin // 跨域
public class ProductController {
    @GetMapping("/test")
    public String test(){
        return "hello";
    }
}
  1. 测试
    在这里插入图片描述

引入Swagger

  1. pom
		<!--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>
  1. yml
spring:
  application:
    name: payment-demo # 应用名称
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher # 不配置swagger会报错
  1. Swagger注解
    @Api(tags="商品管理") //用在类上
    @ApiOperation("测试接口") //用在方法上
  2. 配置类
@EnableSwagger2
@Configuration
public class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(true)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.zdz.paymentdemo")) //你自己的package
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("微信支付案例接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot整合微信支付")
                .version("1.0")
                .termsOfServiceUrl("https://www.baidu.com/")
                .build();
    }
}
  1. 测试

访问:http://localhost:8090/swagger-ui.html
在这里插入图片描述

定义统一结果

  1. pom
		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
  1. 统一返回结果类
@Data
public class R {

    @ApiModelProperty(value = "是否成功")
    private Boolean success;

    @ApiModelProperty(value = "返回码")
    private Integer code;

    @ApiModelProperty(value = "返回消息")
    private String message;

    @ApiModelProperty(value = "返回数据")
    private Map<String, Object> data = new HashMap<String, Object>();

    private R(){}

    public static R ok(){
        R r = new R();
        r.setSuccess(true);
        r.setCode(0);
        r.setMessage("成功");
        return r;
    }

    public static R error(){
        R r = new R();
        r.setSuccess(false);
        r.setCode(-1);
        r.setMessage("失败");
        return r;
    }

    public R success(Boolean success){
        this.setSuccess(success);
        return this;
    }

    public R message(String message){
        this.setMessage(message);
        return this;
    }

    public R code(Integer code){
        this.setCode(code);
        return this;
    }

    public R data(String key, Object value){
        this.data.put(key, value);
        return this;
    }

    public R data(Map<String, Object> map){
        this.setData(map);
        return this;
    }
}
  1. 修改controller
@RestController
@RequestMapping("/api/product")
@CrossOrigin // 跨域
@Api(tags = "商品管理")
public class ProductController {
    @GetMapping("/test")
    @ApiOperation("测试接口")
    public R test(){
        return R.ok().data("message", "hello").data("now", new Date());
    }
}
  1. 配置json时间格式
#================ #json时间格式==============
spring:
  jackson:
    date-format: yyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  1. 测试
    在这里插入图片描述

创建数据库

集成Mybatis-Plus

  1. pom
		<!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>
        <!--mp-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
  1. yml
#===============#mysql配置==============
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8
    username: root
    password: 123456

#=================MP配置=================
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml
  1. 使用mp自动生成service、mapper
  2. 定义MyBatis-Plus的配置文件
@Configuration
@MapperScan("com.zdz.paymentdemo.mapper") //持久层扫描
@EnableTransactionManagement //启用事务管理
public class MPConfig {
}
  1. 定义接口方法查询所有商品
	@ApiOperation("商品列表")
    @GetMapping("/list")
    public R list() {
        List<Product> list = productService.list();
        return R.ok().data("productList", list);
    }
  1. 测试
    在这里插入图片描述

搭建前端环境

基本支付API V3

引入支付参数

  1. wxpay.properties
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io
  1. 读取支付参数配置文件
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig {

    // 商户号
    private String mchId;

    // 商户API证书序列号
    private String mchSerialNo;

    // 商户私钥文件
    private String privateKeyPath;

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // 微信服务器地址
    private String domain;

    // 接收结果通知地址
    private String notifyDomain;

}
  1. 配置Annotation Processor,可以让配置文件和java代码之间的对应参数可以定位
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
  1. 测试支付参数的获取
@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);
    }
}

在这里插入图片描述
5. 将wxpay.perperties配置成spring boot配置文件
在这里插入图片描述

加载商户私钥

  1. 复制商户私钥到项目根目录下:
    在这里插入图片描述
  2. 引入SDK,我们可以使用官方提供的 SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
		<dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.3.0</version>
        </dependency>
  1. 获取商户私钥
    https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient(如何加载商户私钥)
  2. 在wxpayconfig类中添加获取商户私钥方法
	public PrivateKey getPrivateKey(String filename) {
        try {
            return PemUtil.loadPrivateKey(new FileInputStream(filename));
        }catch (Exception e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }
  1. 测试
@SpringBootTest
class PaymentDemoApplicationTests {
    @Resource
    private WxPayConfig wxPayConfig;
    @Test
    public void testGetPrivateKey() {
        // 获取私钥路径
        String privateKeyPath = wxPayConfig.getPrivateKeyPath();
        System.out.println(privateKeyPath);
        // 获取商户私钥
        PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
        System.out.println(privateKey);
    }
}

在这里插入图片描述

获取签名验证器和HttpClient

  1. 证书密钥使用说明

https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
在这里插入图片描述
2. 获取签名验证器

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

	 /**
     * 获取签名验证器
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier() {
        // 获取商户私钥
        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;
    }
  1. 获取 HttpClient 对象

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient(定时更新平台证书功能)
HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。

	/**
     * 获取HttpClient
     * @param verifier
     * @return
     */
    @Bean
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {
        // 获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        // 用户构建HttpClient
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));

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

API字典和相关工具

  1. API列表

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
在这里插入图片描述
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>
  1. 定义枚举

为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
在这里插入图片描述

  1. 添加工具类

使用这些辅助工具简化项目的开发
在这里插入图片描述

Native下单API

Native支付流程

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
在这里插入图片描述

Native下单API

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

  1. service

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

@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {
    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception {
        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");

        //调用统一下单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) { //处理成功,无返回Body
                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();
        }
    }
}
  1. 定义WxPayController方法
@RestController
@CrossOrigin
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付")
@Slf4j
public class WxPayController {

    @Resource
    private WxPayService wxPayService;

    @ApiOperation("调用统一下单API,生成支付二维码")
    @PostMapping("/native/{productId}")
    public R nativePay(@PathVariable Long productId) {
        log.info("发起支付请求");
        // 返回支付二维码链接和订单号
        Map<String, Object> map = null;
        try {
            map = wxPayService.nativePay(productId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return R.ok().data(map);
    }
}
  1. 前后端联调测试
    在这里插入图片描述

创建课程订单

  1. 保存订单

OrderInfoService
接口:

OrderInfo createOrderByProductId(Long productId);

实现:

	@Resource
    private ProductMapper productMapper;

    @Resource
    private OrderInfoMapper orderInfoMapper;

    @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());
        orderInfoMapper.insert(orderInfo);

        return orderInfo;
    }

	/**
     * 根据商品id查询未支付订单
     * 防止重复创建订单对象
     * @param productId
     * @return
     */
    private OrderInfo getNoPayOrderByProductId(Long productId) {
        QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("product_id", productId);
        wrapper.eq("order_status", OrderStatus.NOTPAY.getType());
//        wrapper.eq("user_id", userId);
        OrderInfo orderInfo = orderInfoMapper.selectOne(wrapper);
        return orderInfo;
    }
  1. 缓存二维码

OrderInfoService
接口:

void saveCodeUrl(String orderNo, String codeUrl);

实现:

	/**
     * 存储订单二维码
     * @param orderNo
     * @param codeUrl
     */
    @Override
    public void saveCodeUrl(String orderNo, String codeUrl) {
        QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCodeUrl(codeUrl);
        orderInfoMapper.update(orderInfo, wrapper);
    }
  1. 修改WxPayServiceImpl的nativePay方法
	@Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    private OrderInfoService orderInfoService;

    @Override
    public Map<String, Object> nativePay(Long productId) 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");
        //其他代码

		//完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
     
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            // 其他代码

            // 响应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            // 二维码
            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();
        }
    }

显示订单列表

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

  • controller
@RestController
@CrossOrigin
@Slf4j
@Api(tags = "商品订单管理")
@RequestMapping("/api/order-info")
public class OrderInfoController {
    @Resource
    private OrderInfoService orderInfoService;

    @GetMapping("/list")
    @ApiOperation("商品订单列表")
    public R list() {
        log.info("商品订单列表");
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        // 倒序查询
        queryWrapper.orderByDesc("create_time");
        List<OrderInfo> list = orderInfoService.list(queryWrapper);
        return R.ok().data("list", list);
    }
}
  • 测试

在这里插入图片描述

支付通知API

内网穿透

  1. 访问ngrok官网,注册登录,下载内网穿透工具
    https://ngrok.com/
  2. 设置authToken
ngrok config add-authtoken 2b7v0jmfPcUC82awNwQ4Bihq0Lb_49tKz1V8Vy8qk4utYwPK2
  1. 启动服务
ngrok http 8090
  1. 测试外网访问
    在这里插入图片描述
    在这里插入图片描述

接收通知和返回应答

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

  1. 启动ngrok
ngrok http 8090
  1. 设置wxpay.properties(每次重新启动ngrok,都需要根据实际情况修改这个配置)
https://ed07-120-216-162-152.ngrok-free.app
  1. 创建通知接口

通知规则:用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为
15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

	/**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     * @param request
     * @param response
     * @return
     */
    @ApiOperation("支付通知")
    @PostMapping("/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {
        Gson gson = new Gson();
        // 应答对象
        Map<String, String> map = new HashMap<>();

        // 处理通知参数
        String body = HttpUtils.readData(request);
        HashMap 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. 超时应答
回调通知注意事项:https://pay.weixin.qq.com/wiki/doc/apiv3/Practices/chapter1_1_5.shtml

商户系统收到支付结果通知,需要在 5秒内 返回应答报文,否则微信支付认为通知失败,后续会
重复发送通知。

验收

  1. 工具类

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

  1. 验签
	@Resource
    private Verifier verifier;
	/**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     * @param request
     * @param response
     * @return
     */
    @ApiOperation("支付通知")
    @PostMapping("/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {
        Gson gson = new Gson();
        // 应答对象
        Map<String, String> map = new HashMap<>();

        try {
            // 处理通知参数
            String body = HttpUtils.readData(request);
            HashMap bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);
            //log.info("支付通知的完整数据 ===> {}", body);
            
            //签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest =
                    new WechatPay2ValidatorForRequest(verifier, body, requestId);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                log.error("通知验签失败");
                // 失败应答
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
            //TODO 处理订单

            //应答成功,成功应答必须为200或204,否则就是失败应答
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            return gson.toJson(map);
        } catch (IOException e) {
            e.printStackTrace();
            // 错误应答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "系统错误");
            return gson.toJson(map);
        }
    }

解密

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. WxPayController

notify 方法中添加处理订单的代码

		 //TODO 处理订单
         wxPayService.processOrder(bodyMap);
  1. WxPayService
	@Override
    public void processOrder(HashMap<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("处理订单");

        String plainText = decryptFromResource(bodyMap);

        // 转换明文

        // 更新订单状态

        // 记录支付日志
    }

    /**
     * 对称解密
     * @param bodyMap
     * @return
     */
    private String decryptFromResource(HashMap<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;
    }

处理订单

  1. 完善processOrder方法
	@Override
    public void processOrder(HashMap<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);
    }
  1. 更新订单状态

OrderInfoService

/**
     * 根据订单编号更新订单状态
     * @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);
    }
  1. 处理支付日志

PaymentInfoService

	/**
     * 记录支付日志
     * @param plainText
     */
    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);
    }

处理重复通知

在这里插入图片描述
处理重复通知
在 processOrder 方法中,更新订单状态之前,添加如下代码

       //处理重复通知
       //保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
       String orderStatus = orderInfoService.getOrderStatus(orderNo);
       if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
           return;
       }

       // 更新订单状态
       orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

OrderInfoService

	@Override
    public String getOrderStatus(String orderNo) {
        QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("order_no", orderNo);
        OrderInfo orderInfo = orderInfoMapper.selectOne(wrapper);
        if (orderInfo == null) {
            return null;
        }
        return orderInfo.getOrderStatus();
    }

数据锁

在这里插入图片描述
定义 ReentrantLock 进行并发控制。注意,必须手动释放锁。

	private final ReentrantLock lock = new ReentrantLock();
	@Override
    public void processOrder(HashMap<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");

        /*在对业务数据进行状态检查和处理之前,
        要采用数据锁进行并发控制,
        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复通知
                //保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
                    return;
                }
                // 更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
                // 记录支付日志
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                lock.unlock();
            }
        }
    }

在这里插入图片描述

商户定时查询本地订单

  • 后端定义商户查单接口OrderInfoController

支付成功后,商户侧查询本地数据库,订单是否支付成功

	@GetMapping("/query-order-status/{orderNo}")
    @ApiOperation("查询本地订单状态")
    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("支付中...");
    }
  • 前端定时轮询查单

在二维码展示页面,前端定时轮询查询订单是否已支付,如果支付成功则跳转到订单页面
(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: '/orders' })
          }, 3000)
        }
      })
    }

用户取消订单API

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

  1. 在WxPayController中添加取消订单接口
	@PostMapping("/cancel/{orderNo}")
    @ApiOperation("用户取消订单")
    public R cancel(@PathVariable String orderNo) {
        log.info("取消订单");

        try {
            wxPayService.cancelOrder(orderNo);
            return R.ok().message("订单已取消");
        } catch (IOException e) {
            log.error("用户取消订单失败 ====> {}", e);
            return R.error().message("订单取消失败");
        }
    }
  1. WxPayService
	/**
     * 用户取消订单
     * @param orderNo
     */
    @Override
    public void cancelOrder(String orderNo) throws IOException {
        // 微信支付的关单接口
        this.closeOrder(orderNo);
        // 更新商户端的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
    }

    /**
     * 关闭接口的调用
     * @param orderNo
     */
    private void closeOrder(String orderNo) throws IOException {
        log.info("关单接口的调用,订单号 ===> {}", orderNo);

        //创建远程请求对象
        // CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close")
        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("关闭订单失败,响应码 = " + statusCode);
                throw new IOException("request failed");
            }
        }finally {
            response.close();
        }
    }
  1. 测试
    在这里插入图片描述

微信支付查单API

官网指南: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml

查单接口的调用

  • WxPayController
    /**
     * 查询订单
     * @return
     */
    @GetMapping("/query/{orderNo}")
    @ApiOperation("查询订单:测试订单状态用")
    public R queryOrder(@PathVariable String orderNo) {
        log.info("查询订单,订单号 ====> {}", orderNo);
        try {
            String bodyAsString = wxPayService.queryOrder(orderNo);
            return R.ok().setMessage("查询成功").data("bodyAsString", bodyAsString);
        } catch (Exception e) {
            log.error("查询订单错误 ===> {}", e);
            return R.error().setMessage("查询失败");
        }
    }
  • WxPayService

    /**
     * 查询订单
     * @param orderNo
     * @return
     * @throws IOException
     */
    @Override
    public String queryOrder(String orderNo) throws IOException {
        log.info("查询订单接口调用,订单号 ===> {}", orderNo);
        // ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s")
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo).concat("?mchid="+wxPayConfig.getMchId());
        url = wxPayConfig.getDomain().concat(url);
        log.info("查询订单路径 ====> {}", url);

        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("查询订单,响应码 = " + statusCode+ ",返回结果 = " +
                        bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        }finally {
            response.close();
        }
    }
  • 测试
    在这里插入图片描述
    在这里插入图片描述

集成Spring Task,定时查找处理超时订单

  • WxPayTask
@Component
@Slf4j
public class WxPayTask {
    @Resource
    private OrderInfoService orderInfoService;
    @Resource
    private WxPayService wxPayService;
    /**
     * 测试
     * (cron="秒 分 时 日 月 周")
     * *:每隔一秒执行
     * 0/3:从第0秒开始,每隔3秒执行一次
     * 1-3: 从第1秒开始执行,到第3秒结束执行
     * 1,2,3:第1、2、3秒执行
     * ?:不指定,若指定日期,则不指定周,反之同理
     *
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void orderConfirm() {
        log.info("定时查询超时订单任务执行");
        List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1);
        for (OrderInfo orderInfo : orderInfoList) {
            String orderNo = orderInfo.getOrderNo();
            log.warn("超时订单 ===> {}", orderNo);
            //核实订单状态:调用微信支付查单接口
            try {
                wxPayService.checkOrderStatus(orderNo);
            } catch (IOException e) {
                log.error("核实订单失败 ===> {}", orderNo);
            }
        }
    }
}
  • OrderInfoService
    /**
     * 找出创建超过minutes分钟并且未支付的订单
     * @param minutes
     * @return
     */
    @Override
    public List<OrderInfo> getNoPayOrderByDuration(int minutes) {
        //minutes分钟之前的时间
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));

        QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("order_status", OrderStatus.NOTPAY.getType());
        wrapper.le("create_time", instant);
        List<OrderInfo> list = orderInfoMapper.selectList(wrapper);
        return list;
    }
  • WxPayService
    /**
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已支付,则更新商户端订单状态,并记录支付日志
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
     * @param orderNo
     */
    @Override
    public void checkOrderStatus(String orderNo) throws IOException {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);

        //调用微信支付查单接口
        String result = this.queryOrder(orderNo);

        Gson gson = new Gson();
        HashMap resultMap = gson.fromJson(result, HashMap.class);

        //获取微信支付端的订单状态
        String tradeState = (String) 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);
        }
    }
  • 测试
    在这里插入图片描述

申请退款API

官网指南: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml

创建退款单

  • 根据订单号查询订单 OrderInfoService
    /**
     * 根据订单号获取订单
     * @param orderNo
     * @return
     */
    @Override
    public OrderInfo getOrderByOrderNo(String orderNo) {
        QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("order_no", orderNo);
        return orderInfoMapper.selectOne(wrapper);
    }
  • 创建退款单记录 RefundsInfoService
	@Resource
    private RefundInfoMapper refundInfoMapper;

    @Resource
    private OrderInfoService orderInfoService;
    /**
     * 根据订单号创建退款订单
     * @param orderNo
     * @param reason
     * @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);

        // 保存退款订单
        refundInfoMapper.insert(refundInfo);

        return refundInfo;
    }

更新退款单

RefundInfoService

	/**
     * 记录退款记录
     * @param content
     */
    @Override
    public void updateRefund(String content) {
        log.info("修改退款记录");
        //将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字段
        }

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

申请退款

  • WxPayController
    /**
     * 用户申请退款
     * @return
     */
    @PostMapping("/refunds/{orderNo}/{reason}")
    @ApiOperation("申请退款")
    public R refunds(@PathVariable String orderNo, @PathVariable String reason) {
        log.info("申请退款 ===> 订单号:{}, 原因: {}", orderNo, reason);
        try {
            wxPayService.refund(orderNo, reason);
            return R.ok();
        } catch (IOException e) {
            log.error("申请退款失败 ===> {} ", e);
            return R.error();
        }
    }
  • WxPayService

    /**
     * 退款
     * @param orderNo
     * @param reason
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void refund(String orderNo, String reason) throws IOException {
        log.info("申请退款业务接口被调用...");

        log.info("创建退款记录");
        RefundInfo refundsInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);


        // DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);
        log.info("调用退款API ===> {}", 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);
            //更新退款单
            refundInfoService.updateRefund(bodyAsString);
        }finally {
            response.close();
        }
    }

查询退款API

官网指南: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml

  • WxPayController

查单接口的调用


    /**
     * 查询退款
     * @param refundNo
     * @return
     */
    @ApiOperation("查询退款:测试用")
    @GetMapping("/query-refund/{refundNo}")
    public R queryRefund(@PathVariable String refundNo){
        log.info("查询退款 ===> {}", refundNo);
        String result = null;
        try {
            result = wxPayService.queryRefund(refundNo);
            return R.ok().setMessage("查询成功").data("result", result);
        } catch (IOException e) {
            log.error("查询退款失败");
            return R.error().setMessage("查询失败");
        }
    }
  • WxPayService

    /**
     * 根据退款单号核实退款单状态
     * @param refundNo
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void checkRefundStatus(String refundNo) throws IOException {
        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);
            //更新退款单
            refundInfoService.updateRefund(result);
        }

        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            log.warn("核实订单退款异常 ===> {}", refundNo);
            //如果确认退款成功,则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,
                    OrderStatus.REFUND_ABNORMAL);
            //更新退款单
            refundInfoService.updateRefund(result);
        }
    }

定时处理超时未退款订单

  • WxPayTask
    /**
     * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
     */
    @Scheduled(cron = "1 * * * * ?")
    public void refundConfirm() throws Exception {
        log.info("定时查询创建超过5分钟,并且未成功的退款单任务执行");
        //找出申请退款超过5分钟并且未成功的退款单
        List<RefundInfo> refundInfoList =
                refundInfoService.getNoRefundOrderByDuration(5);
        for (RefundInfo refundInfo : refundInfoList) {
            String refundNo = refundInfo.getRefundNo();
            log.warn("超时未退款的退款单号 ===> {}", refundNo);
            //核实订单状态:调用微信支付查询退款接口
            wxPayService.checkRefundStatus(refundNo);
        }
    }
  • RefundInfoService
    /**
     * 找出申请退款超过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);
        return refundInfoMapper.selectList(queryWrapper);
    }
  • WxPayService
    /**
     * 根据退款单号核实退款单状态
     * @param refundNo
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void checkRefundStatus(String refundNo) throws IOException {
        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);
            //更新退款单
            refundInfoService.updateRefund(result);
        }

        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            log.warn("核实订单退款异常 ===> {}", refundNo);
            //如果确认退款成功,则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,
                    OrderStatus.REFUND_ABNORMAL);
            //更新退款单
            refundInfoService.updateRefund(result);
        }
    }

退款结果通知API

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

接收退款通知

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

处理订单和退款单

WxPayService

	@Override
    @Transactional(rollbackFor = Exception.class)
    public void processRefund(HashMap<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);
                //更新退款单
                refundInfoService.updateRefund(plainText);
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }
  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值