java-微信支付

微信支付

微信支付介绍

微信支付(https://pay.weixin.qq.com)是腾讯集团旗下中国领先的第三方支付平台,一直致力于为用户和企业提供安全、便捷、专业的在线支付服务。

付款码支付

付款码支付是指用户展示微信钱包内的“付款码”给商户系统扫描后直接完成支付,适用于线下场所面对面收银的场景,例如商超、便利店、餐饮、医院、学校、电影院和旅游景区等具有明确经营地址的实体场所。

使用示例

在这里插入图片描述

JSAPI支付

JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。

应用场景有:

  • 线下场所:调用接口生成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
  • 公众号场景:用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
  • PC网站场景:在网站中展示二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

使用示例

在这里插入图片描述

小程序支付

小程序支付是指商户通过调用微信支付小程序支付接口,在微信小程序平台内实现支付功能;用户打开商家助手小程序下单,输入支付密码并完成支付后,返回商家小程序。

使用示例

在这里插入图片描述

Native支付

Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站、实体店单品或订单、媒体广告支付等场景。
在这里插入图片描述

APP支付

APP支付是指商户通过在移动端应用APP中集成开放SDK调起微信支付模块来完成支付。适用于在移动端APP中集成微信支付功能的场景。

使用实例

在这里插入图片描述

刷脸支付

刷脸支付是指用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式,安全便捷。适用于线下实体场所的收银场景,如商超、餐饮、便利店、医院、学校等。

使用示例

在这里插入图片描述

微信支付_前期准备工作

获取商户号

微信商户平台:https://pay.weixin.qq.com/
场景:Native支付

在这里插入图片描述

获取APPID

微信公众平台:https://mp.weixin.qq.com/
在这里插入图片描述

注册流程

在这里插入图片描述

关联流程

APPID:微信公众号 =》 开发管理 =》 开发设置 =》 获取AppID
在这里插入图片描述

申请商户API证书

获取秘钥和证书

注意:
以上所有API秘钥和证书需妥善保管防止泄露。

在这里插入图片描述

申请证书

在这里插入图片描述

下载安装证书工具

在这里插入图片描述

工具生成证书

在这里插入图片描述

复制请求串

在这里插入图片描述

验证密码

在这里插入图片描述

生成证书

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

本地文件

在这里插入图片描述

注意:

  • aplclient_cert.p12:包含了私钥信息得证书文件。
  • apiclient_cert.pem:从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管。
  • apiclient_key.pem:部分开发语言和环境不能使用p12文件,需要使用pem文件为了方便,
    直接提供。

获取API秘钥

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

生成随机密码 https://suijimimashengcheng.bmcx.com/
在这里插入图片描述

支付安全_信息安全的基础

密码学基本概念

密码学是网络安全、信息安全、区块链等产品的基础,常见的非对称加密、对称加密、散列函数等,都属于密码学范畴。

中国古代加密

公元683年,唐中宗即位。随后,武则天废唐中宗,立第四子李旦为皇帝,但朝政大事均由她自己专断。裴炎、徐敬业和骆宾王等人对此非常不满。徐敬业聚兵十万,在江苏扬州起兵。裴炎做内应,欲以拆字手段为其传递秘密信息。后因有人告密,裴炎被捕,未发出的密信落到武则天手中。这封密信上只有“青鹅”二字,群臣对此大惑不解。

破解:
武则天破解了“青鹅”的秘密:“青”字拆开来就是“十二月”,而“鹅”字拆开来就是“我自与”。密信的意思是让徐敬业、骆宾王等率兵于十二月进发,裴炎在内部接应。“青鹅”破译后,裴炎被杀。接着,武则天派兵击败了徐敬业和骆宾王。

明文

加密前的消息叫“明文”(plain text)。
在这里插入图片描述

密文

加密后的文本叫“密文”(cipher text)。
在这里插入图片描述

密钥

只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)。
在这里插入图片描述

加密算法

  • MD5信息摘要算法
  • DES是对称性加密算法
  • RSA是一种非对称加密算法

支付安全_消息摘要

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

作用:

保证信息的完整性

特点

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

常见算法

  • MD5
  • SHA1
  • SHA256
  • SHA512

回顾之前数字摘要

百度搜索 MySQL ,进入官网下载 ,会经常发现有 sha1 , sha512 , 这些都是数字摘要。

在这里插入图片描述

点击signature

在这里插入图片描述

消息摘要实现

public static void main(String[] args) throws Exception{
        // 原文
        String input = "baizhan";
        // 算法
        String algorithm = "MD5";
        // 获取数字摘要对象
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        // 获取消息数字摘要的字节数组
        byte[] digest = messageDigest.digest(input.getBytes());
        System.out.println(new String(digest));
   }

在这里插入图片描述

注意:
加密后编码表找不到对应字符, 出现乱码

base64 编码

public static void main(String[] args) throws Exception{
        // 原文
        String input = "aa";
        // 算法
        String algorithm = "MD5";
        // 获取数字摘要对象
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        // 消息数字摘要
        byte[] digest = messageDigest.digest(input.getBytes());
		// System.out.println(new String(digest));
        // base64编码
        System.out.println(new BASE64Encoder().encode(digest));
   }

支付安全_对称加密

在这里插入图片描述

对称加密

对称加密指的就是加密和解密使用同一个秘钥,所以叫做对称加密。对称加密只有一个秘钥,作为私钥。

对称加密算法

  • DES
  • AES
  • 3DES

特点

  • 加密速度快, 可以加密大文件
  • 密文可逆, 一旦密钥文件泄漏, 就会导致数据暴露
  • 加密后编码表找不到对应字符, 出现乱码
  • 一般结合Base64使用

对称加密实现

package com.itbaizhan.encryption;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
 * DesAesDemo
 */
public class DesAesDemo {
   public static void main(String[] args) throws Exception {
       // 原文
       String input = "baizhan";
       // des加密必须是8位
       String key = "123456";
       // 算法
       String algorithm = "DES";
       String transformation = "DES";
       // Cipher:密码,获取加密对象
       // transformation:参数表示使用什么类型加密
       Cipher cipher = Cipher.getInstance(transformation);
       // 指定秘钥规则
       // 第一个参数表示:密钥,key的字节数组
       // 第二个参数表示:算法
       SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
       // 对加密进行初始化
       // 第一个参数:表示模式,有加密模式和解密模式
       // 第二个参数:表示秘钥规则
       cipher.init(Cipher.ENCRYPT_MODE,sks);
       // 进行加密
       byte[] bytes = cipher.doFinal(input.getBytes());
       // 打印字节,因为ascii码有负数,解析不出来,所以乱码
	   //  for (byte b : bytes) {
       //       System.out.println(b);
       //   }
       // 打印密文
       System.out.println(new String(bytes));
   }
}

报错

在这里插入图片描述

注意:

密钥是6个字节,DES加密算法规定,密钥key必须是8个字节,所以需要修改上面key改成key=“12345678”

在这里插入图片描述

注意:
出现乱码是因为对应的字节出现负数,但负数,没有出现在
ascii 码表里面,所以出现乱码,需要配合base64进行转码。

解密

package com.itbaizhan.encryption;
import cn.hutool.core.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* DesAesDemo
*/
public class DesAesDemo {
    public static void main(String[] args) throws Exception {
        // 原文
        String input = "baizhan";
        // des加密必须是8位
        String key = "12345678";
        // 算法
        String algorithm = "DES";
        String transformation = "DES";
        String s = encryptDES(input, key,algorithm, transformation);
        String test = "znLsz/8WnU4=";
        String s1 = decryptDES(test, key,algorithm, transformation);
        System.out.println(s1);
   }
    /**
     * 使用DES加密数据
     *
     * @param input         : 原文
     * @param key           : 密钥(DES,密钥的长度必须是8个字节)
     * @param transformation : 获取Cipher对象的算法
	 * @param algorithm     : 获取密钥的算法
     * @return : 密文
     * @throws Exception
     */
    private static String encryptDES(String input, String key, String transformation, String algorithm) throws Exception {
        // 获取加密对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 创建加密规则
        // 第一个参数key的字节
        // 第二个参数表示加密算法
        SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
        // ENCRYPT_MODE:加密模式
        // DECRYPT_MODE: 解密模式
        // 初始化加密模式和算法
        cipher.init(Cipher.ENCRYPT_MODE,sks);
        // 加密
        byte[] bytes = cipher.doFinal(input.getBytes());
        // 输出加密后的数据
        String encode = Base64.encode(bytes);
        return encode;
   }
    /**
    * 使用DES解密
     *
     * @param input         : 密文
     * @param key           : 密钥
     * @param transformation : 获取Cipher对象的算法
     * @param algorithm     : 获取密钥的算法
     * @throws Exception
     * @return: 原文
     */
    private static String decryptDES(String input, String key, String transformation, String algorithm) throws Exception {
        // 1,获取Cipher对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 指定密钥规则
        SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
        cipher.init(Cipher.DECRYPT_MODE, sks);
        // 3. 解密,上面使用的base64编码,下面直接用密文
        byte[] bytes = cipher.doFinal(Base64.decode(input));
        // 因为是明文,所以直接返回
        return new String(bytes);
   }
}

支付安全_非对称加密

非对称加密

非对称加密指的是:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。

非对称加密算法

  • RSA
  • ECC

特点

  • 加密和解密使用不同的密钥
  • 如果使用私钥加密, 只能使用公钥解密
  • 如果使用公钥加密, 只能使用私钥解密
  • 处理数据的速度较慢, 因为安全级别高

非对称加密实现

引入依赖

<dependency>
     <groupId>commons-io</groupId>
     <artifactId>commons-io</artifactId>
     <version>2.6</version>
</dependency>

生成公钥和私钥

import com.sun.org.apache.xml.internal.security.utils.Base64;
import org.apache.commons.io.FileUtils;
import javax.crypto.Cipher;
import java.io.File;
import java.nio.charset.Charset;
import java.security.*;

import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* 生成公钥和私钥
*/
public class RSAdemo {  
public static void main(String[] args) throws Exception {
        String input = "baizhan";
        // 加密算法
        String algorithm = "RSA";
        //生成密钥对并保存在本地文件中
        generateKeyToFile(algorithm, "a.pub","a.pri");
   }
   /**
     * 生成密钥对并保存在本地文件中
     *
     * @param algorithm : 算法
     * @param pubPath   : 公钥保存路径
     * @param priPath   : 私钥保存路径
     * @throws Exception
     */
    private static void generateKeyToFile(String algorithm, String pubPath, String priPath) throws Exception {
        // 获取密钥对生成器
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
        // 获取密钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        // 获取公钥
        PublicKey publicKey = keyPair.getPublic();
        // 获取私钥
        PrivateKey privateKey = keyPair.getPrivate();
        // 获取byte数组
        byte[] publicKeyEncoded = publicKey.getEncoded();
        byte[] privateKeyEncoded = privateKey.getEncoded();
        // 进行Base64编码
        String publicKeyString = Base64.encode(publicKeyEncoded);
        String privateKeyString = Base64.encode(privateKeyEncoded);
        // 保存文件
        FileUtils.writeStringToFile(new File(pubPath), publicKeyString,Charset.forName("UTF-8"));
        FileUtils.writeStringToFile(new File(priPath), privateKeyString,Charset.forName("UTF-8"));
   }

运行程序:先打印的是私钥 , 后面打印的是公钥
在这里插入图片描述

私钥加密

	/**
     * 读取私钥
     * @param priPath
     * @param algorithm
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String priPath,String algorithm) throws Exception{
        // 将文件内容转为字符串
        String privateKeyString = FileUtils.readFileToString(new File(priPath),Charset.defaultCharset());
        // 获取密钥工厂
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        // 构建密钥规范 进行Base64解码
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(privateKeyString));
        // 生成私钥
        return keyFactory.generatePrivate(spec);
   }
public static void main(String[] args) throws Exception {
        String input = "baizhan";
        // 加密算法
        String algorithm = "RSA";
        
        // 加载私钥
        PrivateKey privateKey = getPrivateKey("a.pri", algorithm);
        // 私钥加密
        String s = encryptRSA(algorithm,privateKey, input);
        System.out.println(s);
    
 }

公钥加密

	/**
     * 读取公钥
     * @param pulickPath
     * @param algorithm
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(String pulickPath,String algorithm) throws Exception{
        // 将文件内容转为字符串
        String publicKeyString = FileUtils.readFileToString(new File(pulickPath), Charset.defaultCharset());
        // 获取密钥工厂
         KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        // 构建密钥规范 进行Base64解码
        X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decode(publicKeyString));
        // 生成公钥
        return keyFactory.generatePublic(spec);
   }
    public static void main(String[] args) throws Exception {
        String input = "baizhan";
        // 加密算法
        String algorithm = "RSA";
        // 加载公钥
        PublicKey publicKey = getPublicKey("a.pub", algorithm);
        // 公钥解密
        String s = encryptRSA(algorithm,publicKey, "密文");
        System.out.println(s);
        
   }

支付安全_数字签名

在这里插入图片描述

数字签名是什么

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。数字签名是非对称密钥加密技术与数字摘要技术的应用。

生活中的数据签名

相信我们都写过信,在写信的时候落款处总是要留下自己的名字,用来表示写信的人是谁。我们签的这个字就是生活中的签名。

注意:
在网络中传输数据时候,给数据添加一个数字签名,表示是谁
发的数据,而且还能证明数据没有被篡改。OK,数字签名的主
要作用就是保证了数据的有效性(验证是谁发的)和完整性
(证明信息没有被篡改)。

基本原理

在这里插入图片描述
张三有两把钥匙,一把是公钥,另一把是私钥。张三把公钥送给他的朋友们----铁蛋、幺妹、李四----每人一把。
在这里插入图片描述
幺妹要给张三写一封保密的信。她写完后用张三的公钥加密,就可以达到保密的效果。
在这里插入图片描述
张三收信后,用私钥解密,就看到了信件内容。这里要强调的是,只要张三的私钥不泄露,这封信就是安全的,即使落在别人手里,也无法解密。
在这里插入图片描述
张三给幺妹回信,决定采用"数字签名"。他写完后先用Hash函数,生成信件的摘要(digest)。
在这里插入图片描述
张三使用私钥,对这个摘要加密,生成"数字签名"(signature)。幺妹收信后,取下数字签名,用张三的公钥解密,得到信件的摘要。由此证明,这封信确实是张三发出的。幺妹再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。

在这里插入图片描述

支付安全_数字证书

在这里插入图片描述

为什么需要数字证书

复杂的情况出现了。李四想欺骗幺妹,他偷偷使用了幺妹的电脑,用自己的公钥换走了张三的公钥。此时,幺妹实际拥有的是李四的公钥,但是还以为这是张三的公钥。因此,李四就可以冒充张三,用自己的私钥做成"数字签名",写信给幺妹,让幺妹用假的张三公钥进行解密。
在这里插入图片描述
后来,幺妹感觉不对劲,发现自己无法确定公钥是否真的属于张三。她想到了一个办法,要求张三去找"证书中心"(certificate authority,简称CA),为公钥做认证。证书中心用自己的私钥,对张三的公钥和一些相关信息一起加密,生成"数字证书"(Digital Certificate)。

在这里插入图片描述

理解数据证书

比如说我们的毕业证书,任何公司都会承认。为什么会承认?因为那是国家发得,大家都信任国家。也就是说只要是国家的认证机构,我们都信任它是合法的。

原理

为了解决公钥的信任问题,张三和幺妹找一家认证公司(CA
Catificate Authority),把公钥进行认证,证书中心用自己的私钥,对A的公钥和一些相关信息一起加密,生成“数字证书”(Digital Certificate)

在这里插入图片描述
幺妹如果获取到证书,证书可以用CA的公钥(认证中心信用背书)进行解密,会得到发公钥人的信息,以及他的公钥,此时这个A的公钥是可信的。
在这里插入图片描述
所以张三给幺妹发送信息的时候,就会带上签名,和证书一并发送给到互联网上,幺妹接收到消息的时候,先用CA发布的公钥解密数字证书,得到张三的公钥,用张三的公钥解密签名,得到摘要,幺妹在用hash算法得到消息的摘要,对两个摘要对比,如果相等,说明消息在网络上没有被不法分子修改。
在这里插入图片描述

支付项目_功能演示

首页
在这里插入图片描述
我的订单
在这里插入图片描述
订单详情页
在这里插入图片描述

Postman工具使用

Postman 是什么
Postman 是一款 API 开发协作工具,它可以帮助你测试和开发API,Postman 提供了测试 API 的友好界面和功能,使用简单便捷,安全可靠。Postman 是每一位前后端开发者必掌握的开发工具。

如何安装 Postman

官网安装 https://www.postman.com/

在这里插入图片描述
发送get请求

在Postman工作空间选定get请求。
在这里插入图片描述
发送POST请求
在这里插入图片描述

支付工程准备_创建支付工程

新建工程

在这里插入图片描述

注意:
JDK版本选择8。

组件选择

在这里插入图片描述

字符编码

在这里插入图片描述

Java编译版本选择

在这里插入图片描述

配置application.yml文件

server:
  # 端口号
 port: 9090
spring:
 application:
    #应用名
   name: payment
    
logging:
 pattern:
   console: logging.pattern.console=%d{MM/ddHH:mm:ss.SSS} %clr(%-5level) ---  [%-15thread]%cyan(%-50logger{50}):%msg%n 

分层结构

不同功能的类放在不同功能的包里面。

  • config:所有的配置 用于存放Spring Boot相关的配置类,包括启动类。
  • controller:所有请求入口,前端访问后端的入口。
  • sevice:逻辑层,负责所有的业务逻辑。
  • mapper:或者叫Dao,持久层,负责java和数据库交互。包括interface和XML两类文件。
  • entity(Po):表映射实体,用一个java类来映射数据库表,类名就相当于表名,类的属性就相当于表字段。
  • dto:数据传输对象(Data Transfer Object),用于前后端数据交互。

Domain和Dto区别:
* 从用法上来说:Domain用于java数据和数据库表记录映射,删除增加修改数据库的数据,用在service层和Mapper层。
* Dto用在前后端数据传输:用在controller层和Service层Service层介于Controller和Mapper之间,也是Domain和Dto的转换层。

编写Controller

@RestController
public class TestController {            
   @GetMapping("/test")
   public String test(){ return "hello";
 }
}

测试
请求 http://localhost:9090/test

支付工程准备_创建数据库表

在这里插入图片描述

创建订单表

CREATE TABLE `order_info`  (
  `id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单标题',
  `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户订单号',
  `user_id` bigint(20) NULL DEFAULT NULL COMMENT '用户id',
  `total_fee` int(11) NULL DEFAULT NULL COMMENT '订单金额(分)',
  `code_url` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单二维码连接',
  `order_status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单状态',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1516240544441835523 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

创建支付表

CREATE TABLE `payment_info`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
  `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户订单编号',
  `transaction_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付系统交易编号',
  `payment_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付类型',
  `trade_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '交易类型',
  `trade_state` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '交易状态',
  `payer_total` int(11) NULL DEFAULT NULL COMMENT '支付金额(分)',
  `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '通知参数',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

创建退款表

CREATE TABLE `refund_info`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '退款单id',
  `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户订单编号',
  `refund_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商户退款单编号',
  `refund_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付系统退款单号',
  `total_fee` int(11) NULL DEFAULT NULL COMMENT '原订单金额(分)',
  `refund` int(11) NULL DEFAULT NULL COMMENT '退款金额(分)',
  `reason` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '退款原因',
  `refund_status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '退款状态',
  `content_return` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '申请退款返回参数',
  `content_notify` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '退款结果通知参数',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE 
  ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

支付工程准备_集成MyBatis-Plus

引入依赖

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-bootstarter</artifactId>
        <version>3.5.1</version>
    </dependency>
   <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
	    <version>5.1.49</version>
   </dependency>
</dependencies>

配置

在 application.yml 配置文件中添加MySQL数据库的相关配置:

# DataSource Config
spring:
 datasource:
   driver-class-name: com.mysql.jdbc.Driver
   url: jdbc:mysql://localhost:3306/wechat_payment?useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai&useSSL=false
   username: root
   password: 123456

在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 Mapper 文件夹:

@MapperScan("com.itbaizhan.mapper")
public class MyBitsPlusConfig {
}

支付工程准备_MyBatis-Plus代码生成器

引入依赖

 <dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plusgenerator</artifactId>
     <version>3.5.2</version>
 </dependency>
<!-- 模板引擎 -->
<dependency>
     <groupId>org.apache.velocity</groupId>
     <artifactId>velocity-enginecore</artifactId>
     <version>2.0</version>
</dependency>

快速生成

package com.itbaizhan.utils;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

import java.util.Arrays;
import java.util.List;

/**
 * 代码生成工作
 */
public class CodeGenerator {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/payment", "root", "123456")
                .globalConfig(builder -> {
                    builder.author("itbaizhan") // 设置作者
                            .commentDate("MM-dd") // 注释日期格式 C:\Users\wangc\IdeaProjects\payment-demo2\src\main\java
                            .outputDir(System.getProperty("user.dir")+ "/src/main/java/") // 指定输出目录
                            .fileOverride(); //覆盖文件
                })
                // 包配置
                .packageConfig(builder -> {
                    builder.parent("com.itbaizhan") // 包名前缀
                            .entity("entity") //实体类包名
                            .mapper("mapper") //mapper接口包名
                            .service("service"); //service包名
                })

                .strategyConfig(builder -> {
                    // 设置需要生成的表名
                    List<String> strings = Arrays.asList("order_info", "payment_info", "refund_info");
                    builder.addInclude(strings)
                            // 开始实体类配置
                            .entityBuilder()
                            // 开启lombok模型
                            .enableLombok()
                            //表名下划线转驼峰
                            .naming(NamingStrategy.underline_to_camel)
                            //列名下划线转驼峰
                            .columnNaming(NamingStrategy.underline_to_camel);
                })
                .execute();
    }
}

支付工程准备_统一结果返回封装类

统一接口返回类的意义

基于java的前后端分离项目中,前端获取后端controller层接口返回的JSON格式的数据,并展示出来。通常为了提高代码质量,会将后端返回的数据进行统一的格式处理。

创建枚举类

package com.itbaizhan.vo;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum CodeEnum {

    // 成功
    SUCCESS(200,"SUCCESS"),

    // 系统异常
    SYSTEM_ERROR(500,"系统异常"),
    PARAMETER_ERROR(500,"参数缺失"),

    // 支付异常
    ORDER_ERROR(600,"订单不存在"),
    PAYMENT_ERROR(601,"支付异常");


    // 状态码
    private final Integer code;

    //响应信息
    private final String message;
}

创建统一返回类

package com.itbaizhan.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

/**
 * 统一结果封装类
 * @param <T>
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseResult<T>  {

    // 状态码
    private  Integer code;

    //响应信息
    private  String message;

    // 数据
    private T data;

    // 构建请求成功结果
    public static <T> BaseResult<T> ok(){
        return new BaseResult<>(CodeEnum.SUCCESS.getCode(),CodeEnum.SUCCESS.getMessage(),null);
    }

    // 构建请求成功结果
    public static <T> BaseResult<T> ok(T data){
        return new BaseResult<>(CodeEnum.SUCCESS.getCode(),CodeEnum.SUCCESS.getMessage(),data);
    }

    // 构建请求成功结果
    public static <T> BaseResult<T> error(CodeEnum codeEnum){
        return new BaseResult<>(codeEnum.getCode(),codeEnum.getMessage(),null);
    }
}

修改TestController

修改test方法,返回统一结果

package com.itbaizhan.controller;

import com.itbaizhan.config.WxPayConfig;
import com.itbaizhan.config.ZfbPayConfig;
import com.itbaizhan.vo.BaseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试
 */
@RestController
public class TestController {
    @GetMapping("/test")
    public BaseResult test(){
        return BaseResult.ok("hello  payment");
    }
}

微信支付_引入微信支付配置参数

定义微信配置文件
创建wxpay.properties 文件到resources目录中。这个文件定义了之前我们准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等。

# 微信支付相关参数
# 商户号
wxpay.mch-id=1532379511
# 商户API证书序列号
wxpay.mch-serial_no=412710B5824A1B89427A5ACFA500F412E336BA78
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=U4graSir01LOzjesPkbjTavLyxB7r17K
# APPID
wxpay.appid=wx0ec7c1c17dac84f2
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io

证书序号
在这里插入图片描述
APIv3密钥生成
随机密码生成工具 https://suijimimashengcheng.bmcx.com/
在这里插入图片描述
读取微信支付参数

@Configuration
@PropertySource("classpath:wxpay.properties")
//读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
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;
}

配置Annotation Processor
可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方便开发。

<dependency>     
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-bootautoconfigure-processor</artifactId>
    <optional>true</optional>
</dependency>

在IDEA中设置SpringBoot配置文件
让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示。
在这里插入图片描述
测试支付参数数据

@RestController
public class TestController {
    
    @Autowired
    private WxPayConfig wxPayConfig;
    /**
     * 读写微信配置文件数据进行测试
     */
    @GetMapping("/getwxpayconfig")
    public BaseResult getWxPayConfig() {
        String mchId = wxPayConfig.getMchId();
        return BaseResult.ok(mchId);
   }
    
}

微信支付_配置商户证书

在这里插入图片描述

商户API私钥

商户申请商户API证书时会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。私钥也可以通过工具从商户的p12证书中导出。请妥善保管好你的商户私钥文件。将下载的私钥文件复制到项目根目录。

注意:
不要把私钥文件暴露在公共场合,如上传到github,写在客户端代码等。

引入微信SDK

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

<dependency>
    <groupId>com.github.wechatpayapiv3</groupId>
    <artifactId>wechatpay-apachehttpclient</artifactId>
    <version>0.4.4</version>
</dependency>

获取商户私钥

 	/**
     * 获取商户的私钥文件
     * @param filename
     * @return
     */
    public PrivateKey getPrivateKey(String filename){
        try {
            FileInputStream fileInputStream = new FileInputStream(filename);
            return PemUtil.loadPrivateKey(fileInputStream);
       } catch (FileNotFoundException e) {
            throw new RuntimeException("私钥文件不存在", e);
       }
   }

测试商户私钥的获取

在 PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。

@SpringBootTest
class PaymentDemoApplicationTests {
    @Autowired
    private WxPayConfig wxPayConfig;
    
    /**
     * 测试商户私钥
     */
   @Test
    public void testGetPrivateKey() {
       // 获取私钥路径
       String privateKeyPath = wxPayConfig.getPrivateKeyPath();
       //获取商户私钥
       PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
       System.out.println(privateKey);
   }
  
}

微信支付_加载平台证书和获取HttpClient对象

平台证书

微信支付平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。

注意:
不同的商户,对应的微信支付平台证书是不一样的,平台证书会周期性更换。商户应定时通过API下载新的证书。

获取签名验证器

https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient

  • 平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
  • 签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
    在这里插入图片描述
	/**
     * 获取签名验证器
     * @return
     */
    @Bean
    public ScheduledUpdateCertificatesVerifier getVerifier(){
        log.info("获取签名验证器");
        //获取商户私钥
        PrivateKey privateKey = this.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;
   }

获取HttpClient对象

HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。

	/**
     * 获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
        log.info("获取httpClient");
        //获取商户私钥
        PrivateKey privateKey = this.getPrivateKey(privateKeyPath);
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder
										        .create()
										        .withMerchant(mchId,mchSerialNo, privateKey)
										        .withValidator(new WechatPay2Validator(verifier));
        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient =builder.build();
        return httpClient;
        }

微信支付_Native支付API列表

在这里插入图片描述
微信支付官方文档
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml

定义枚举

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

支付接口api枚举

package com.itbaizhan.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
 * Native下单
 */
 NATIVE_PAY("/v3/pay/transactions/native"),
 /**
 * 查询订单
 */
 
ORDER_QUERY_BY_NO("/v3/pay/transactions/id/%s"),
 /**
 * 关闭订单
 */
 
CLOSE_ORDER_BY_NO("/v3/pay/transactions/outtrade-no/%s/close"),
 /**
 * 申请退款
 */
 
DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
 /**
 * 查询单笔退款
 */
 
DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s");
/**
 * 类型
 */
 private final String type;
}

支付状态枚举

package com.itbaizhan.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;
}

微信支付_Native支付流程

在这里插入图片描述

介绍

商户后台系统先调用微信支付的 Native 下单接口,微信后台系统返回链接参数 code_url ,商户后台系统将code_url 值生成二维码图片,用户使用微信客户端扫码后发起支付 。
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml

注意:
code_url有效期为2小时,过期后扫码不能再发起支付。

业务流程时序图

在这里插入图片描述

业务流程说明

  • 商户后台系统根据用户选购的商品生成订单
  • 用户确认支付后调用微信支付【Native 下单API】生成预支付交易
  • 微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接 code_url
  • 商户后台系统根据返回的 code_url 生成二维码
  • 用户打开微信 “扫一扫” 扫描二维码,微信客户端将扫码内容发送到微信支付系统
  • 微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权
  • 用户在微信客户端输入密码,确认支付后,微信客户端提交授权
  • 微信支付系统根据用户授权完成支付交易
  • 微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
  • 微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知
  • 未收到支付通知的情况,商户后台系统调用【查询订单API
  • 商户确认订单已支付后给用户发货

生成二维码规则

对应链接格式:
weixin://weixin://pay.weixin.qq.com/bizpayurl/up?
pr=NwY5Mz9&groupid=00
请商户调用第三方库将code_url生成二维码图片。该模式链接较短,生成的二维码打印到结账小票上的识别率较高。

例如:
将weixin://weixin://pay.weixin.qq.com/bizpayurl/up?
pr=NwY5Mz9&groupid=00 生成二维码见下图
在这里插入图片描述

二维码相关知识

参考文献:
商品二维码标准: 国家商品二维码标准
名片二维码: 名片二维码通用技术规范

微信支付_创建订单

在这里插入图片描述

接口说明

请求URL:/api/order/createOrder
请求方式:POST

请求参数

在这里插入图片描述

什么是DTO

数据传输对象(DTO)(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。

创建订单DTO

@Data
public class OrderInfoDTO {
    /**
     * 订单标题
     */
    private String title;
    /**
     * 订单金额(分)
     */
    private Integer totalFee;
}

创建订单接口

	/**
     * 添加订单
     * @param orderInfoDTO
     * @return
     */
    OrderInfo save(OrderInfoDTO orderInfoDTO);

创建订单状态枚举类

@AllArgsConstructor
@Getter
public enum OrderStatus {
    /**
     * 未支付
     */
    NOTPAY("未支付"),
     /**
     * 支付成功
     */
    SUCCESS("支付成功"),
    /**
     * 已关闭
     */
    CLOSED("超时已关闭"),
    /**
     * 已取消
     */
    CANCEL("用户已取消"),
    /**
     * 退款中
     */
    REFUND_PROCESSING("退款中"),
    /**
     * 已退款
     */
    REFUND_SUCCESS("已退款"),
    /**
     * 退款异常
     */
    REFUND_ABNORMAL("退款异常");
    /**
     * 类型
     * */
    private final String type;
}

编写订单接口实现类

@Slf4j
@Service
public class OrderInfoServiceImpl extends
ServiceImpl<OrderInfoMapper, OrderInfo>
implements IOrderInfoService {
    /**
     * 添加订单
     * @param orderInfoDTO
     * @return
     */
    @Override
    public OrderInfo save(OrderInfoDTO orderInfoDTO) {
        log.info("********* 生成订单 ********");
        OrderInfo orderInfo = new OrderInfo();
        // 订单id
        orderInfo.setTitle("苹果");
        // 商户订单编号
        orderInfo.setOrderNo(orderInfoDTO.getOrderNo());
        // 用户id
        orderInfo.setUserId(12313456L);
        // 订单金额
        orderInfo.setTotalFee(orderInfoDTO.getTotalFee());
        // 订单状态
        orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
        baseMapper.insert(orderInfo);
        return orderInfo;
   }
    
}

编写创建订单控制层

 	/**
     * 添加订单
     *
     * @return
     */
    @PostMapping("/save")
    public BaseResult save(OrderInfoDTO orderInfoDTO) {
        OrderInfo orderInfo = iOrderInfoService.save(orderInfoDTO);
        return BaseResult.ok(orderInfo);
   }

测试

在这里插入图片描述

微信支付_Native下单API

在这里插入图片描述

接口说明

请求URL:/api/wx-pay/native/{orderNo}
请求方式:POST

请求参数

在这里插入图片描述

Native支付开发指引

商户端发起支付请求,微信端创建支付订单并生成支付二维码链
接,微信端将支付二维码返回给商户端,商户端显示支付二维码,
用户使用微信客户端扫码后发起支付。Native支付开发指引

在这里插入图片描述

创建WxPayController

package com.itbaizhan.controller;
import com.itbaizhan.service.IWxPaymentService;
import com.itbaizhan.vo.BaseResult;
import org.springframework.beans.factory.annotation.Autowired;
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 java.io.IOException;
/**
* 微信支付接口
*/
@RestController
@RequestMapping("/api/wx-pay")
public class WxPayController {
    @Autowired
    private IWxPaymentService iWxPaymentService;
    /**
    * Native下单
     * weixin://wxpay/bizpayurl?pr=e5ta1spzz
     * @param orderNo
     * @return
     */
    @PostMapping("/native/{orderNo}")
    public BaseResult nativePay(@PathVariable String orderNo) throws Exception {
        BaseResult baseResult = iWxPaymentService.nativePay(orderNo);
        return baseResult;
   }
}

创建微信支付接口

public interface WxPayService {
	/**
     * 微信Native支付
     * @param paymentDTO
     * @return
     * @throws Exception
     */
    Map<String, Object> nativePay(PaymentDTO
paymentDTO)throws Exception;
}

创建微信支付实现类

package com.itbaizhan.service.impl;
import com.alibaba.fastjson.JSON;
import com.itbaizhan.config.WxPayConfig;
import com.itbaizhan.entity.OrderInfo;
import com.itbaizhan.enums.wx.WxApiType;
import com.itbaizhan.service.IOrderInfoService;
import com.itbaizhan.service.IWxPaymentService;
import com.itbaizhan.vo.BaseResult;
import com.itbaizhan.vo.CodeEnum;
import com.itbaizhan.vo.PayInfoVO;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* 微信支付
*/
@Slf4j
@Service
public class WxPaymentServiceImpl implements
IWxPaymentService {
    @Autowired
    private IOrderInfoService iOrderInfoService;// 订单接口
    @Autowired
    WxPayConfig wxPayConfig;// 微信支付配置参数
    @Autowired
    CloseableHttpClient wxPayClient;
    /**
     * Native下单
     *
     * @param orderNo
     * @return
     */
    @Override
    public BaseResult nativePay(String orderNo) throws Exception {
        log.info("*********** 开始 Native下单*********");
        // 1. 根据订单编号查询订单信息
        OrderInfo orderInfo = iOrderInfoService.findByOrderNo(orderNo);
        if (orderInfo == null) {
            return BaseResult.error(CodeEnum.ORDER_ERROR);
       }
        // 2. 调用统一下载API https://api.mch.weixin.qq.com/v3/pay/transactions/native
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
        // 3. 组装请求参数
        HashMap<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("appid", wxPayConfig.getAppid());// 应用id
        paramsMap.put("mchid", wxPayConfig.getMchId());// 商户id
        paramsMap.put("description", "test");//商品描述
        paramsMap.put("out_trade_no", orderInfo.getOrderNo());// 订单编号
        paramsMap.put("notify_url", wxPayConfig.getDomain().concat(wxPayConfig.getNotifyDomain()));// 通知地址
        HashMap<String, Object> amountMap = new HashMap<>();
        amountMap.put("total", orderInfo.getTotalFee());
		paramsMap.put("amount", amountMap);//订单金额
        // 4. 将参数转换为json字符串
        String jsonString = JSON.toJSONString(paramsMap);
        log.info("Native下单参数=======>{}" + jsonString);
        // 5. 准备消息 boby
        StringEntity entity = new StringEntity(jsonString, "UTF-8");
      
		entity.setContentType("application/json");
        httpPost.setEntity(entity);
        // 6. 准备请求头
        httpPost.setHeader("Accept", "application/json");
        // 7.完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            // 8. 拿出body 响应体
            String bodyString = EntityUtils.toString(response.getEntity());
            // 9. 获取响应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            // 10 判断响应码
            if (statusCode == 200) {
             HashMap<String, String> responseMap = JSON.parseObject(bodyString, HashMap.class);
                // 11 取出code_url
                String codeUrl = responseMap.get("code_url");
                PayInfoVO payInfoVO = new PayInfoVO();
                payInfoVO.setCodeUrl(codeUrl);
              
				payInfoVO.setOrderNo(orderInfo.getOrderNo());
                return BaseResult.ok(payInfoVO);
           } else {
                log.error("Native 下单失败响应码" + statusCode + "返回结果" + bodyString);
                return BaseResult.error(CodeEnum.PAYMENT_ERROR);
           }
       } finally {
            response.close();
       }
   }
}

微信支付_二维码生成

二维码概念

二维码又称QR Code ,QR全程 Quick Response,是一个近几年来移动设备上超流行的一种编码方式。是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的 “0”、“1”比特流的概念。

什么是 QRCode.js

QRCode.js 是一个用于生成二维码的 JavaScript 库。主要是通过获取 DOM 的标签,再通过 HTML5 Canvas 绘制而成,不依赖任何库。

基本用法

<div id="qrcode"></div>
<script type="text/javascript">
new QRCode(document.getElementById("qrcode"),
"http://www.baidu.com");  // 设置要生成二维码的链接
</script>

引入QRCode

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko" lang="ko">
<head>
    <title>Javascript 二维码生成库:QRCode</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width,initialscale=1,user-scalable=no" />
    <script type="text/javascript" src="//cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//static.runoob.com/assets/qrcode/qrcode.min.js"></script>
</head>
<body>
<div id="qrcode" style="width:100px;height:100px; margin-top:15px;"></div>
<script type="text/javascript">
    var qrcode = new QRCode(document.getElementById("qrcode"), {
        text:"weixin://wxpay/bizpayurl?pr=ymEqQFMzz",
        width : 100,
        height : 100
   });
</script>
</body>
</html>

微信支付_重复支付解决

编写更新CodeUrl接口

package com.itbaizhan.service;
import com.itbaizhan.controller.dto.OrderInfoDTO;
import com.itbaizhan.entity.OrderInfo;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.core.annotation.Order;
/**
* <p>
* 服务类
* </p>
*
* @author itbaizhan
* @since 04-21
*/
public interface IOrderInfoService extends
IService<OrderInfo> {
    /**
     * 创建订单
     * @param orderInfoDTO
     * @return
     */
    OrderInfo createOrder(OrderInfoDTO orderInfoDTO);
    /**
     * 根据订单编号查询订单信息
     * @param orderNo
     * @return
     */
    OrderInfo findByOrderNo(String orderNo);
    /**
     *
     * @param id 订单id
     * @param codeUrl 二维码
     */
    void saveCodeUrl(Long id,String codeUrl);
  }

编写更新CodeUrl接口实现类

 	/**
     * 更新codeurl
     * @param id id
     * @param codeUrl 二维码
     */
    @Override
    public void saveCodeUrl(Long id, String codeUrl) {
        UpdateWrapper<OrderInfo> updateWrapper = new UpdateWrapper<>();
        // 设置要更新的字段 key = db属性
        updateWrapper.set("code_url",codeUrl);
        //条件
        updateWrapper.eq("id",id);
        baseMapper.update(null,updateWrapper);
   }

支付业务层编写逻辑

package com.itbaizhan.service.impl;
import com.alibaba.fastjson.JSON;
import com.itbaizhan.config.WxPayConfig;
import com.itbaizhan.entity.OrderInfo;
import com.itbaizhan.enums.wx.WxApiType;
import com.itbaizhan.service.IOrderInfoService;
import com.itbaizhan.service.IWxPaymentService;
import com.itbaizhan.vo.BaseResult;
import com.itbaizhan.vo.CodeEnum;
import com.itbaizhan.vo.PayInfoVO;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 微信支付
 */
@Slf4j
@Service
public class WxPaymentServiceImpl implements
IWxPaymentService {
@Autowired
   private IOrderInfoService iOrderInfoService;// 订单接口
   @Autowired
   WxPayConfig wxPayConfig;// 微信支付配置参数
   @Autowired
   CloseableHttpClient wxPayClient;
   /**
     * Native下单
     *
     * @param orderNo
     * @return
     */
   @Transactional(rollbackFor = Exception.class)
   @Override
   public BaseResult nativePay(String orderNo) throws Exception {
       log.info("*********** 开始 Native下单*********");
       // 1. 根据订单编号查询订单信息
       OrderInfo orderInfo = iOrderInfoService.findByOrderNo(orderNo);
       if (orderInfo == null) {
       return BaseResult.error(CodeEnum.ORDER_ERROR);
        }
       if (orderInfo != null &&!StringUtils.isEmpty(orderInfo.getCodeUrl())){
           // 直接返回二维码
           PayInfoVO payInfoVO = new PayInfoVO();
           
		   payInfoVO.setCodeUrl(orderInfo.getCodeUrl());
           
		   payInfoVO.setOrderNo(orderInfo.getOrderNo());
           return BaseResult.ok(payInfoVO);
        }
       // 2. 调用统一下载API https://api.mch.weixin.qq.com/v3/pay/transactions/native
       HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
       // 3. 组装请求参数
       HashMap<String, Object> paramsMap = new HashMap<>();
       paramsMap.put("appid", wxPayConfig.getAppid());// 应用id
       paramsMap.put("mchid", wxPayConfig.getMchId());// 商户id
       paramsMap.put("description", "test");//商品描述
	   paramsMap.put("out_trade_no", orderInfo.getOrderNo());// 订单编号
       paramsMap.put("notify_url", wxPayConfig.getDomain().concat(wxPayConfig.getNotifyDomain()));// 通知地址
       HashMap<String, Object> amountMap = new HashMap<>();
       amountMap.put("total", orderInfo.getTotalFee());
       paramsMap.put("amount", amountMap);//订单金额
       // 4. 将参数转换为json字符串
       String jsonString = JSON.toJSONString(paramsMap);
       log.info("Native下单参数=======>{}" + jsonString);
       // 5. 准备消息 boby
       StringEntity entity = new StringEntity(jsonString, "UTF-8");
       
	   entity.setContentType("application/json");
       httpPost.setEntity(entity);
       // 6. 准备请求头
       httpPost.setHeader("Accept", "application/json");
       // 7.完成签名并执行请求
       CloseableHttpResponse response = wxPayClient.execute(httpPost);
		try {
           // 8. 拿出body 响应体
           String bodyString = EntityUtils.toString(response.getEntity());
           // 9. 获取响应状态码
           int statusCode = response.getStatusLine().getStatusCode();
           // 10 判断响应码
           if (statusCode == 200) {
               HashMap<String, String> responseMap = JSON.parseObject(bodyString,HashMap.class);
               // 11 取出code_url
               String codeUrl = responseMap.get("code_url");
               // 更新code_url
               iOrderInfoService.saveCodeUrl(orderInfo.getId(),codeUrl);
               PayInfoVO payInfoVO = new PayInfoVO();
               payInfoVO.setCodeUrl(codeUrl);
               
			   payInfoVO.setOrderNo(orderInfo.getOrderNo());
               return BaseResult.ok(payInfoVO);
            } else {
               log.error("Native 下单失败响应码" + statusCode + "返回结果" + bodyString);
               return BaseResult.error(CodeEnum.PAYMENT_ERROR);
            }
            } finally {
           response.close();
        }
    }
}

支付通知_内网穿透

为什么需要内网穿透

在这里插入图片描述

什么是内网穿透

内网穿透也叫做内网映射,也叫“NAT穿透”。一句话来说就是,让外网能访问你的内网;把自己的内网(主机)当成服务器,让外网能访问。
在这里插入图片描述

内网穿透有哪些作用

  • 访问内部网络
  • 映射成功后,你的电脑其实就变成了一台服务器,别人可以正常访问你的网站
  • 搭建一个临时的的服务器和网站

内网穿透的工具平台

  • Sunny-Ngrok
  • 花生壳
  • NATAPP

支付通知_下载安装内网穿透

下载注册ngrok

官网https://natapp.cn/login
在这里插入图片描述

购买隧道

在这里插入图片描述

我的隧道

在这里插入图片描述

下载安装客户端

在这里插入图片描述

启动服务

./natapp -authtoken=9ab6b9040a624f40

在这里插入图片描述
测试外网访问
请求 http://2b6bafd196724185.natapp.cc

支付通知_接收通知

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

设置通知地址
修改wxpay.properties配置文件,重新设置回调地址

wxpay.notify-domain=http://kalista.natapp1.cc

注意:
每次重新启动ngrok,都需要根据实际情况修改这个配置。

通知接口规则

用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。

注意:
通知频率
15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m。

通知报文

支付结果通知是以POST 方法访问商户设置的通知url,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情。

注意:
由于涉及到回调加密和解密,商户必须先设置好apiv3秘钥后才
能解密回调通知,apiv3秘钥设置文档指引详见APIv3秘钥设置
指引
)。

创建支付通知枚举

package com.itbaizhan.enums.wxpay;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付通知api枚举
*/
@AllArgsConstructor
@Getter
public enum WxNotifyType {
 /**
 * 支付通知
 */
 NATIVE_NOTIFY("/api/wx-pay/native/notify"),
 /**
 * 类型
 */
 private final String type;
}

编写HttpUtils工具

package com.itbaizhan.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();
               }
           }
       }
   }
}

创建通知接口

	/**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     */
    @PostMapping("/native/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {
        Map<String, String> resultMap = new HashMap<>();
        String body = HttpUtils.readData(request);
        Map<String, Object> bodyMap = JSON.parseObject(body, Map.class);
        String requestId = (String)bodyMap.get("id");
        log.info("支付通知的id ===> {}",requestId);
        log.info("支付通知的完整数据 ===> {}",body);
        //TODO : 签名的验证
        // TODO : 处理订单
        //成功应答:成功应答必须为200或204,否则就是失败应答
        response.setStatus(200);
        resultMap.put("code", "SUCCESS");
        resultMap.put("message", "成功");
        return JSON.toJSONString(resultMap);
   }

测试失败应答

@PostMapping("/native/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {
        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秒内 返回应答报文,否则微信支付认为通知失败,后续会重复发送通知。

支付通知_验签

在这里插入图片描述

验签工具类

package com.itbaizhan.utils;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 微信通知验签
*/
public class WxVerifierUtils {
 /**
     * 验签
     * @param request
     * @param verifier
     * @param body
     * Content-Type: application/json;charset=utf-8
     * Content-Length: 2204
     * Connection: keep-alive
     * Keep-Alive: timeout=8
     * Content-Language: zh-CN
     * Request-ID: e2762b10-b6b9-5108-a42c16fe2422fc8a
     * Wechatpay-Nonce:c5ac7061fccab6bf3e254dcf98995b8c
     * Wechatpay-Signature:
		CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8
		Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkI
		fhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0
		fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILff
		gAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2
		ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk
		51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoN
		KjCu8wnzyCFdA==
     * Wechatpay-Timestamp: 1554209980
     * Wechatpay-Serial:5157F09EFDC096DE15EBE81A47057A7232F1B8E1
     * Cache-Control: no-cache, must-revalidate
     * @return
     */
      public static boolean verifier(HttpServletRequest request, Verifier verifier, String body) throws UnsupportedEncodingException {
        // 1. 随机串
        String nonce = request.getHeader("Wechatpay-Nonce");
        // 2. 获取微信传递过来签名
        String signature = request.getHeader("Wechatpay-Signature");
        // 3. 证书序列号
        String serial = request.getHeader("Wechatpay-Serial");
        // 4. 时间戳
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        // 5. 构造签名串
        /**
         * 应答时间戳\n
         * 应答随机串\n
         * 应答报文主体\n
         */
        String signstr = Stream.of(timestamp,nonce,body).collect(Collectors.joining("\n","","\n"));
			return verifier.verify(serial,signstr.getBytes("utf8"),signature);
}
    public static void main(String[] args) {
        String collect = Stream.of("a", "b","c").collect(Collectors.joining("?", "=","%"));
        System.out.println(collect);
   }
}

编写验签逻辑

package com.itbaizhan.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.itbaizhan.config.WxPayConfig;
import com.itbaizhan.service.IWxPaymentService;
import com.itbaizhan.utils.HttpUtils;
import com.itbaizhan.utils.WxVerifierUtils;
import com.itbaizhan.vo.BaseResult;
import com.sun.xml.internal.messaging.saaj.util.LogDomainConstants;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* 微信支付接口
* */
@Slf4j
@RestController
@RequestMapping("/api/wx-pay")
public class WxPayController {
    @Autowired
    private IWxPaymentService iWxPaymentService;
    @Autowired
    private WxPayConfig wxPayConfig;
    @Autowired
    private Verifier verifier;
    /**
     * Native下单
     * weixin://wxpay/bizpayurl?pr=e5ta1spzz
     * @param orderNo
     * @return
     */
    @PostMapping("/native/{orderNo}")
    public BaseResult nativePay(@PathVariable String orderNo) throws Exception {
        BaseResult baseResult = iWxPaymentService.nativePay(orderNo);
        return baseResult;
   }
    //api/wx-pay/native/notify
    /**
     * 微信支付通知
     * @param request
     * @param response
     * @return
     */
    @PostMapping("/native/notify")
    public String notify(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException,GeneralSecurityException {
        HashMap<String, String> responseMap = new HashMap<>();
        // 1. 获取报文body
        String body = HttpUtils.readData(request);
        // 2. 把json转换为map
        HashMap<String,Object> bodyMap = JSON.parseObject(body, HashMap.class);
        log.info("支付通知id======>{}",bodyMap.get("id"));
        log.info("支付通知完整数据=======>{}",body);
        //TODO: 签名验证 确保是微信给我们发的消息
         boolean verifier = WxVerifierUtils.verifier(request, this.verifier, body);
        if (!verifier){
            response.setStatus(200);
            responseMap.put("code","FAIL");
            responseMap.put("message","失败");
            return JSON.toJSONString(responseMap);
       }
        //TODO: 处理订单
        /**
         *
         {
         "code": "FAIL",
         "message": "失败"
         }
         */
        // 成功应答: 成功就是200 否则就是失败的应答
        response.setStatus(200);
        responseMap.put("code","SUCCESS");
        responseMap.put("message","成功");
        return JSON.toJSONString(responseMap);
   }
    /**
     * 验证签名
     *
     * @param serialNo 微信平台-证书序列号
     * @param signStr   自己组装的签名串
     * @param signature 微信返回的签名
     * @return
     * @throws UnsupportedEncodingException
     */
    private boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException {
        return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature);
   }
    private String decryptBody(String body) throws UnsupportedEncodingException,GeneralSecurityException {
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes("utf-8"));
        JSONObject object = JSON.parseObject(body);
        JSONObject resource = object.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String associatedData = resource.getString("associated_data");
        String nonce = resource.getString("nonce");
 	return aesUtil.decryptToString(associatedData.getBytes("utf-8"),nonce.getBytes("utf-8"),ciphertext);
   }
}

支付通知_修改订单状态

编写更新订单状态接口

/**
     * 更新订单状态
     * @param id 订单id
     * @param orderStatus 订单状态
     */
    void updateByOrderStatus(Long id,OrderStatus orderStatus);

编写更新订单状态接口实现类

/**
     * 根据id更新订单状态
     * @param id
     * @param orderStatus 订单状态
     */
    @Override
    public void updateByOrderStatus(Long id,OrderStatus orderStatus) {
        LambdaUpdateWrapper<OrderInfo> olw = new LambdaUpdateWrapper<>();
      
		olw.eq(OrderInfo::getOrderStatus,orderStatus.getType());
        olw.set(OrderInfo::getId,id);
        baseMapper.update(null,olw);
   }

编写更新支付状态接口

/**
     * 修改支付状态
     * @param bodyMap
     */
    void updateOrderStatus(Map<String, Object> bodyMap);

实现更新支付状态接口

@Override
public void updateOrderStatus(Map<String, Object> bodyMap) {
        log.info("处理订单");
        // 1. 获取明文
          String plainText = WxVerifierUtils.decryptFromResource(bodyMap, wxPayConfig.getApiV3Key());
        // 2. 明文json转map
        HashMap<String,Object> plainTextMap = JSON.parseObject(plainText, HashMap.class);
        // 3. 获取订单id
        String orderNo = (String) plainTextMap.get("out_trade_no");
        // 4. 根据订单id查询订单信息
        OrderInfo orderInfo = iOrderInfoService.findByOrderNo(orderNo);
        // 5. 判断是否修改
        if (!orderInfo.getOrderStatus().equals(OrderStatus.NOTPAY.getType())){
            return ;
       }
        // 6. 更新订单状态
      
		iOrderInfoService.updateByOrderStatus(orderInfo.getId(),OrderStatus.SUCCESS);
        
   }

编写解密方法

	/**
     * 对称解密
     *
     * @param bodyMap
     * @return
     */
      public static String decryptFromResource(Map<String, Object> bodyMap,String apiv3) {
        // 通知数据
        Map<String, String> resourceMap = (Map<String, String>) bodyMap.get("resource");
        // 数据密文
        String ciphertext = resourceMap.get("ciphertext");
        // 随机串
        String nonce = resourceMap.get("nonce");
        // 附加数据
        String associatedData = resourceMap.get("associated_data");
        AesUtil aesUtil = new AesUtil(apiv3.getBytes(StandardCharsets.UTF_8));
        String plainText = null;
        try {
            // 解密
            plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                  nonce.getBytes(StandardCharsets.UTF_8),
                  ciphertext);
       } catch (GeneralSecurityException e) {
            e.printStackTrace();
       }
        return plainText;

支付通知_添加交易记录

创建交易类型枚举

package com.itbaizhan.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum  PayType {
    /**
     * 微信
     */
    WXPAY("微信"),
    /**
     * 支付宝
     */
    ALIPAY("支付宝");
    /**
     * 类型
     */
    private final String type;
}

创建交易记录接口

public interface IPaymentInfoService extends
IService<PaymentInfo> {
    /**
     * 保存交易记录
     * @param plainTextMap
     */
    void createPaymentInfo(Map<String,Object> plainTextMap);
}

实现交易记录接口

package com.itbaizhan.service.impl;
import com.alibaba.fastjson.JSON;
import com.itbaizhan.entity.PaymentInfo;
import com.itbaizhan.enums.PayType;
import com.itbaizhan.mapper.PaymentInfoMapper;
import com.itbaizhan.service.IPaymentInfoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* <p>
* * 服务实现类
* </p>
*
* @author itbaizhan
* @since 04-21
*/
@Service
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements IPaymentInfoService {
   /**
    * 添加交易记录
    * @param plainTextMap
    */
   @Override
   public void createPaymentInfo(Map<String,Object> plainTextMap) {
       // 订单编号
       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);
       // tradeState
       paymentInfo.setTradeState(tradeState);
       // 用户支付金额
       paymentInfo.setPayerTotal(payerTotal);
       // 通知参数
	   paymentInfo.setContent(JSON.toJSONString(plainTextMap));
       baseMapper.insert(paymentInfo);
   }
}

添加交易记录

	/**
    * 修改订单状态
    * @param bodyMap -> 订单编号 5   15 30
    */
   @Override
   public void updateOrderStatus(Map<String,Object> bodyMap) throws GeneralSecurityException {
       log.info("******************* 修改订单状态*********");
       // 1. 获取明文
       String plainText = WxVerifierUtils.decrypt(bodyMap,wxPayConfig.getApiV3Key());
       // 2. 字符串转为map
       HashMap plainTextMap = JSON.parseObject(plainText, HashMap.class);
       // 3. 获取订单编号
       String orderNo = (String)plainTextMap.get("out_trade_no");
       // 5. 根据订单编号获取订单信息
       OrderInfo orderInfo = iOrderInfoService.findByOrderNo(orderNo);
       // 6. 判定订单状态
       if (!OrderStatus.NOTPAY.getType().equals(orderInfo.getOrderStatus())){
           return ;
       }
       // 7. 更新订单状态
       iOrderInfoService.updateOrderStatus(orderInfo.getId(),OrderStatus.SUCCESS);
       // 8. 添加交易记录
      iPaymentInfoService.createPaymentInfo(plainTextMap);
   }

支付通知_ReentrantLock数据锁

在这里插入图片描述

什么是ReentrantLock

ReentrantLock基于AQS,在并发编程中可以实现公平锁和非公平锁来对资源进行同步,同时,和synchronized一样,ReentrantLock支持可重入,ReentrantLock在调度上更灵活,支持更多丰富的功能。

温馨提示:
ReentrantLock是java.util.concurrent包下提供的一套互斥锁,
相比Synchronized,ReentrantLock类提供了一些高级功能。

公平锁和非公平锁

在这里插入图片描述
第一只兔子还没喝完水又来一只兔子
在这里插入图片描述
第一只兔子喝水喝的有点慢,之后来了两只兔子,都被小狗安排进了队列。
在这里插入图片描述

非公平和公平锁的区别

在这里插入图片描述

ReentrantLock和synchronized区别

在这里插入图片描述

  • synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
  • synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

业务加锁

 @Override
 public void updateOrderStatus(Map<String,Object> bodyMap) throws GeneralSecurityException {
        log.info("******************* 修改订单状态 *********");
        // 1. 获取明文
        String plainText = WxVerifierUtils.decrypt(bodyMap,wxPayConfig.getApiV3Key());
        // 2. 字符串转为map
        HashMap plainTextMap = JSON.parseObject(plainText, HashMap.class);
        // 3. 获取订单编号
        String orderNo = (String)plainTextMap.get("out_trade_no");
          /*在对业务数据进行状态检查和处理之前,
	        要采用数据锁进行并发控制,
	        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()){
            try {
                // 5. 根据订单编号获取订单信息
                 OrderInfo orderInfo = iOrderInfoService.findByOrderNo(orderNo);
                // 6. 判定订单状态
                if (!OrderStatus.NOTPAY.getType().equals(orderInfo.getOrderStatus())){
                    return ;
               }
                // 7. 更新订单状态
              iOrderInfoService.updateOrderStatus(orderInfo.getId(),OrderStatus.SUCCESS);
                // 8. 添加交易记录
              iPaymentInfoService.createPaymentInfo(plainTextMap);
           }finally {
                //要主动释放锁
                lock.unlock();
           }
       }
   }

微信支付查单_查询订单

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

接口说明

请求URL:/api/wx-pay/queryOrderStatus/{orderNo}
请求方式:GET

请求参数

在这里插入图片描述

编写查询订单状态接口

/**
     * 查询订单状态
     * @param orderNo 订单ID
     * @return
     * @throws Exception
     */
    String queryOrderStatus(String orderNo) throws Exception;

编写查询订单状态接口实现类

@Override
public String queryOrderStatus(StringorderNo) throws Exception {
        log.info("查单接口调用 ===> {}",orderNo);
        // 1. 格式化参数
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        // 2. 组装请求微信接口URL
          url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept","application/json");
        // 3. 完成签名并执行请求
        CloseableHttpResponse response =wxPayClient.execute(httpGet);
        try {
            // 4. 获取响应体
            String bodyAsString = EntityUtils.toString(response.getEntity());
            // 5. 获取响应状态码
            int statusCode = response.getStatusLine().getStatusCode();
            // 6. 判断状态
            if (statusCode == 200) {
                log.info("成功, 返回结果 = " +bodyAsString);
           } else if (statusCode == 204) {
                log.info("成功");
           } else {
                log.info("查单接口调用,响应码 = "+ statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("requestfailed");
           }
            return bodyAsString;
       } finally {
        response.close();
       }
   }

WxPayController新增查询订单状态接口

@GetMapping("/queryOrderStatus/{orderNo}")
public BaseResult queryOrder(@PathVariable String orderNo) throws Exception {
        log.info("查询订单");
        String result = iWxPaymentService.queryOrderStatus(orderNo);
        return BaseResult.ok(result);
   }

微信支付查单_集成Spring Task

在这里插入图片描述

什么是Spring Task

Spring 3.0后提供Spring Task实现任务调度。

Cron表达式

在这里插入图片描述

参数:

* : 表示所有值; 
?: 表示未说明的值,即不关心它为何值; 
-:表示一个指定的范围; 
, :表示附加一个可能值; 
/ :符号前表示开始时间,符号后表示每次递增的值;

实现Spring Task

启动类添加注解

@Slf4j
@MapperScan("com.itbaizhan.mapper")
@SpringBootApplication
@EnableScheduling
public class PaymentDemoApplication {
    public static void main(String[] args) {
      SpringApplication.run(PaymentDemoApplication.class, args);
        log.info("*************** 支付系统启动成功************");
   }
}

定时任务案例

@Slf4j
@Component
public class WxPaymentTask {
    /** 测试 *
     * (cron="秒 分 时 日 月 周")
     * *:每隔一秒执行
     * 0/3:从第0秒开始,每隔3秒执行一次
     * 1-3: 从第1秒开始执行,到第3秒结束执行
     * 1,2,3:第1、2、3秒执行
     * ?:不指定,若指定日期,则不指定周,反之同理
     */
    @Scheduled(cron="0/3 * * * * ?")
    public void task() {
        log.info("task1 执行");
   }
}

常用表达式例子

0 0 2 1 * ? *   表示在每月的1日的凌晨2点调整任务
0 0 10,14,16 * * ?   每天上午10点,下午2点,40 0/30 9-17 * * ?   朝九晚五工作时间内每半小时
0 0 12 * * ?   每天中午12点触发
0 15 10 ? * * 每天上午10:15触发

微信支付查单_定时查找超时订单

创建超时订单接口

/**
     * 超时订单
     * @param minutes
     * @return
     */
    List<OrderInfo> getNoPayOrderByDuration(int minutes);

实现超时订单接口

 @Override
    public List<OrderInfo> getNoPayOrderByDuration(int minutes) {
        Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
        // 1、查询条件构造器
        QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
        // 2、订单类型
        queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
        // 3、订单创建时间
        queryWrapper.le("create_time", instant);
        List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
        return orderInfoList;
   }

创建定时任务WxPayTask

从第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);
       }
   }

编写核实订单状态接口

	/**
     * 检查订单状态
     * @param orderNo
     * @throws Exception
     */
    void checkOrderStatus(String orderNo) throws Exception;

实现核实订单状态接口

@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态 ===> {}",orderNo);
        // 1、调用微信支付查单接口
        String result = this.queryOrderStatus(orderNo);
        // 2、JSON转Map
        Map<String, Object> resultMap = JSON.parseObject(result, HashMap.class);
        // 3、获取微信支付端的订单状态
        String tradeState = (String)resultMap.get("trade_state");
        // 4、判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState)) {
            log.warn("核实订单已支付 ===> {}",orderNo);
            // 5、如果确认订单已支付则更新本地订单状态
          iOrderInfoService.updateOrderStatus(orderNo,OrderStatus.SUCCESS);
            // 6、记录支付日志
          iPaymentInfoService.createPaymentInfo(resultMap);
       }
        if (WxTradeState.NOTPAY.getType().equals(tradeState)) {
			log.warn("核实订单未支付 ===> {}",orderNo);
            //更新本地订单状态
            iOrderInfoService.updateOrderStatus(orderNo,OrderStatus.CLOSED);
       }
   }

微信支付查单_核实订单状态

编写核实订单状态接口

 /**
     * 检查订单状态
     * @param orderNo
     * @throws Exception
     */
    void checkOrderStatus(String orderNo) throws Exception;

实现核实订单状态接口

/**
     * 检查订单状态
     * 根据订单号查询微信支付查单接口,核实订单状态
     * 如果订单已支付,则更新商户端订单状态,并记录支付日志
     * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
	 *
     * @param orderNo
     * @throws Exception
     */
    @Override
    public void checkOrderStatus(String orderNo) throws Exception {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);
        // 1、调用微信支付查单接口
        String result = this.queryOrder(orderNo);
        // 2、json转map
        Map<String, String> resultMap = JSON.parseObject(result, HashMap.class);
        // 3、获取微信支付端的订单状态
        String tradeState = resultMap.get("trade_state");
        // 4、判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState)) {
            log.warn("核实订单已支付 ===> {}",orderNo);
            //如果确认订单已支付则更新本地订单状态
            iOrderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
            //记录支付日志
            iPaymentInfoService.createPaymentInfo(result);
       }
       if(WxTradeState.NOTPAY.getType().equals(tradeState)) {
            log.warn("核实订单未支付 ===> {}",orderNo);
            // TODO: 如果订单未支付,则调用关单接口
          
            //更新本地订单状态
            iOrderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
       }
   }

用户取消订单_关闭订单

接口说明

请求URL:/api/wx-pay/cancel/{orderNo}
请求方式:POST

请求参数

在这里插入图片描述

定义取消订单接口

WxPayController中添加接口方法

 /**
     * 用户取消订单
     * @param orderNo
     * @return
     * @throws Exception
     */
    @PostMapping("/cancel/{orderNo}")
    public BaseResult cancel(@PathVariable String orderNo) throws Exception {
        iWxPaymentService.cancelOrder(orderNo);
        return BaseResult.ok("订单已取消");
   }

编写取消订单接口

/**
     * 取消订单
     * @param orderNo 订单编号
     */
    void cancelOrder(String orderNo);

实现取消订单接口

 /**
     * 用户取消订单
     * @param orderNo 订单接口
     */
    @Override
    public void cancelOrder(String orderNo) throws Exception {
       // 更新订单状态
      iOrderInfoService.updateOrderStatus(orderNo,OrderStatus.CANCEL);
   }

用户取消订单_调用微信支付的关单接口

以下情况需要调用关单接口

  • 1、商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
  • 2、系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。

注意:
关单没有时间限制,建议在订单生成后间隔几分钟(最短5分
钟)再调用关单接口,避免出现订单状态同步不及时导致关单
失败。

关单接口的调用

	/**
     * 微信支付关单接口的调用
     * @param orderNo 订单编号
     * @throws Exception
     */
    private void closeOrder(String orderNo) throws Exception {
        log.info("关单接口的调用,订单号 ===> {}",orderNo);
        // 1、拼接请求URL地址
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);
        HttpPost httpPost = new HttpPost(url);
        // 2、组装JSON请求体
        Map<String, String> paramsMap = new HashMap<>();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        // 3、Map转JSON
        String jsonParams = JSON.toJSONString(paramsMap);
        log.info("请求参数 ===> {}",jsonParams);
        // 4、将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept","application/json");
        // 5、完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            // 6、响应状态码
            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("requestfailed");
           }
       } finally {
            response.close();
       }
   }

实现取消订单接口

 	/**
     * 用户取消订单
     * @param orderNo 订单接口
     */
    @Override
    public void cancelOrder(String orderNo)throws Exception {
        // 1、调用微信支付关单接口
        this.closeOrder(orderNo);
        // 2、更新订单状态
      iOrderInfoService.updateOrderStatus(orderNo,OrderStatus.CANCEL);
   }

申请退款_创建退款单

接口说明

请求URL:/api/wx-pay/refunds/{orderNo}/{reason}
请求方式:POST

请求参数

在这里插入图片描述

添加退款记录接口

	/**
     * 创建退款单
     * @param orderNo 订单id
     * @param reason 退款原因
     * @return
     */
    void createRefundsByOrderNo(String orderNo,String reason);

退款记录接口实现

@Resource
private IOrderInfoService iOrderInfoService;
    @Override
    public RefundInfo createRefundsByOrderNo(String orderNo, String reason) {
        //根据订单号获取订单信息
        OrderInfo orderInfo = iOrderInfoService.getOrderByOrderNo(orderNo);
        //根据订单号生成退款订单
        RefundInfo refundInfo = new RefundInfo();
  		//订单编号
        refundInfo.setOrderNo(orderNo);
        //退款单编号
        refundInfo.setRefundNo(String.valueOf(System.currentTimeMillis());
        //原订单金额(分)
        refundInfo.setTotalFee(orderInfo.getTotalFee());
        //退款金额(分)
        refundInfo.setRefund(orderInfo.getTotalFee());
        //退款原因
        refundInfo.setReason(reason);
        //保存退款订单
        baseMapper.insert(refundInfo);
        return refundInfo;
   }

编写申请退款接口

	/**
     *
     * 申请退款
     * @param orderNo 订单id
     * @param reason 退款原因
     * @return
     * @throws Exception
     */
    @PostMapping("/refunds/{orderNo}/{reason}")
      public BaseResult refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {
        log.info("申请退款");
        iwxPayService.refund(orderNo, reason);
        return BaseResult.ok();
   }

申请退款_调用微信支付退款API

在这里插入图片描述
交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。

注意:
1、交易时间超过一年的订单无法提交退款
2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单
号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
4、每个支付订单的部分退款次数不能超过50次
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
7、一个月之前的订单申请退款频率限制为:5000/min
8、同一笔订单多次退款的请求需相隔1分钟

创建微信退款接口

	/**
     * 退款
     *
     * @param orderNo 订单编号
     * @param reason 退款理由
     * @throws Exception
     */
    void refund(String orderNo, String reason) throws Exception;

新增退款通知枚举

在通知枚举新增退款通知接口地址。

package com.itbaizhan.enums.wx;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum WxNotifyType {
    /**
     * 支付通知
     * http://kalista.natapp1.cc/api/wxpay/native/notify
     */
    NATIVE_NOTIFY("/api/wx-pay/native/notify");
     /**
     * 退款结果通知
     */
    REFUND_NOTIFY("/api/wxpay/refunds/notify");
    /**
     * 类型
     */
    private final String type;
}

实现微信退款接口

@Override
    public void refund(String orderNo, String reason) throws Exception {
        log.info("******** 创建退款单记录*******");
        //根据订单编号创建退款单
        RefundInfo refundsInfo = iRefundInfoService.createRefundsByOrderNo(orderNo, reason);
 		log.info("********* 调用退款API ********");
        //调用统一下单API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);
        // 请求body参数
        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("currency", "CNY");//退款币种
        paramsMap.put("amount", amountMap);
        amountMap.put("total",refundsInfo.getTotalFee());//原订单金额
        //将参数转换成json字符串
        String jsonParams = JSON.toJSONString(paramsMap);
	 	log.info("请求参数 ===> {}" + jsonParams);
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");//设置请求报文格式
        httpPost.setHeader("Accept","application/json");//设置响应报文格式
        httpPost.setEntity(entity);//将请求报文放入请求对象
        //完成签名并执行请求,并完成验签
        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);
           }
            
       } finally {
            response.close();
       }
   }

创建订单状态接口

	/**
     * 根据订单编号修改订单状态
     * @param orderNo 订单编号
     * @param orderStatus 订单状态
     */
    void updateOrderStatus(String orderNo,OrderStatus orderStatus);

修改订单状态接口实现

	 /**
     * 根据订单编号修改订单状态
     * @param orderNo 订单编号
     * @param orderStatus 订单状态
     */
    public void updateOrderStatus(String orderNo, OrderStatus orderStatus) {
        LambdaUpdateWrapper<OrderInfo> lo = new LambdaUpdateWrapper<>();
        lo.eq(OrderInfo::getOrderNo,orderNo);
      	lo.set(OrderInfo::getOrderStatus,orderStatus.getType());
        baseMapper.update(null,lo);
   }

更新订单状态

//更新订单状态
iOrderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);

编写更新退款单状态接口

	/**
     * 根据内容更新退款状态
     * @param bodyAsString
     */
    void updateRefund(String bodyAsString);

实现更新退款单状态接口

@Override
public void updateRefund(String bodyAsString) {
        // 1、将json字符串转换成Map
        Map<String, String> resultMap = JSON.parseObject(bodyAsString, HashMap.class);
        // 2、根据退款单编号修改退款单
        QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("refund_no",resultMap.get("out_refund_no"));
        // 3、设置要修改的字段
        RefundInfo refundInfo = new RefundInfo();
        refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号
        //查询退款和申请退款中的返回参数
        if (resultMap.get("status") != null) {
          refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
          refundInfo.setContentReturn(bodyAsString);//将全部响应结果存入数据库的content字段
       }
       //退款回调中的回调参数
        if (resultMap.get("refund_status") != null) {
          refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
          refundInfo.setContentNotify(bodyAsString);//将全部响应结果存入数据库的content字段
       }
        //更新退款单
        baseMapper.update(refundInfo,queryWrapper);
   }

申请退款_退款结果通知

在这里插入图片描述
退款状态改变后,微信会把相关退款结果发送给商户。

注意:
对后台通知交互时,如果微信收到应答不是成功或超时,微信
认为通知失败,微信会通过一定的策略定期重新发起通知,尽
可能提高通知的成功率,但微信不保证通知最终能成功。
特别提醒:商户系统对于开启结果通知的内容一定要做签名验
证,并校验通知的信息是否与商户侧的信息一致,防止数据泄
露导致出现“假通知”,造成资金损失

调用微信退款API

	/**
     * 退款结果通知
     * @param request
     * @param response
     * @return
     */
    @PostMapping("/refunds/notify")
    public String refundsNotify(HttpServletRequest request,HttpServletResponse response) throws UnsupportedEncodingException {
        HashMap<String, String> responseMap = new HashMap<>();
        log.info("退款通知执行");
        // 1. 获取报文body
        String body = HttpUtils.readData(request);
        // 2. 把json转换为map
        HashMap<String, Object> bodyMap = JSON.parseObject(body, HashMap.class);
        log.info("支付通知id======> {}", bodyMap.get("id"));
		log.info("支付通知完整数据=======> {}", body);
        // 3. 验签
        boolean verifier = WxVerifierUtils.verifier(request,this.verifier, body);
        if (!verifier) {
            response.setStatus(200);
            responseMap.put("code", "FAIL");
            responseMap.put("message", "失败");
            return JSON.toJSONString(responseMap);
       }
        // TODO : 修改退款状态
        response.setStatus(200);
        responseMap.put("code", "SUCCESS");
        responseMap.put("message", "成功");
        return JSON.toJSONString(responseMap);
   }

编写处理退款单接口

	/**
     * 处理退款单
     * @param bodyMap
     * @throws Exception
     */
    void processRefund(Map<String, Object> bodyMap) throws Exception;

实现处理退款单接口

@Override
public void processRefund(Map<String,Object> bodyMap) throws Exception {
        log.info("**************** 处理退款单*****************");
        // 1、解密报文
        String plainText = WxVerifierUtils.decrypt(bodyMap,wxPayConfig.getApiV3Key());
        // 2、将明文转换成map
        Map plainTextMap = JSON.parseObject(plainText,HashMap.class);
        // 3、获取退款单号
        String orderNo = (String)plainTextMap.get("out_trade_no");
        if (lock.tryLock()) {
            try {
                OrderInfo orderInfo = iOrderInfoService.findByOrderNo(orderNo);
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderInfo.getOrderStatus())) {
                    return;
               }
                //更新订单状态
              iOrderInfoService.updateOrderStatus(orderNo,OrderStatus.REFUND_SUCCESS);
                //更新退款单
                iRefundInfoService.updateRefund(plainText);
           } finally {
                //要主动释放锁
                lock.unlock();
           }
       }
   }
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值