微信支付(https://pay.weixin.qq.com)是腾讯集团旗下中国领先的第三方支付平台,一直致力于为用户和企业提供安全、便捷、专业的在线支付服务。
付款码支付是指用户展示微信钱包内的“付款码”给商户系统扫描后直接完成支付,适用于线下场所面对面收银的场景,例如商超、便利店、餐饮、医院、学校、电影院和旅游景区等具有明确经营地址的实体场所。
JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。
小程序支付是指商户通过调用微信支付小程序支付接口,在微信小程序平台内实现支付功能;用户打开商家助手小程序下单,输入支付密码并完成支付后,返回商家小程序。
Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站、实体店单品或订单、媒体广告支付等场景。
APP支付是指商户通过在移动端应用APP中集成开放SDK调起微信支付模块来完成支付。适用于在移动端APP中集成微信支付功能的场景。
刷脸支付是指用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式,安全便捷。适用于线下实体场所的收银场景,如商超、餐饮、便利店、医院、学校等。
微信商户平台:https://pay.weixin.qq.com/
场景:Native支付
微信公众平台:https://mp.weixin.qq.com/
APPID:微信公众号 =》 开发管理 =》 开发设置 =》 获取AppID
注意:
以上所有API秘钥和证书需妥善保管防止泄露。
注意:
APIv2版本的接口需要此秘钥
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 =>设置API密钥
生成随机密码 https://suijimimashengcheng.bmcx.com/
密码学是网络安全、信息安全、区块链等产品的基础,常见的非对称加密、对称加密、散列函数等,都属于密码学范畴。
公元683年,唐中宗即位。随后,武则天废唐中宗,立第四子李旦为皇帝,但朝政大事均由她自己专断。裴炎、徐敬业和骆宾王等人对此非常不满。徐敬业聚兵十万,在江苏扬州起兵。裴炎做内应,欲以拆字手段为其传递秘密信息。后因有人告密,裴炎被捕,未发出的密信落到武则天手中。这封密信上只有“青鹅”二字,群臣对此大惑不解。
破解:
武则天破解了“青鹅”的秘密:“青”字拆开来就是“十二月”,而“鹅”字拆开来就是“我自与”。密信的意思是让徐敬业、骆宾王等率兵于十二月进发,裴炎在内部接应。“青鹅”破译后,裴炎被杀。接着,武则天派兵击败了徐敬业和骆宾王。
只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)。
摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
保证信息的完整性
百度搜索 MySQL ,进入官网下载 ,会经常发现有 sha1 , sha512 , 这些都是数字摘要。
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));
}
注意:
加密后编码表找不到对应字符, 出现乱码
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));
}
对称加密指的就是加密和解密使用同一个秘钥,所以叫做对称加密。对称加密只有一个秘钥,作为私钥。
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);
}
}
非对称加密指的是:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。
<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 是一款 API 开发协作工具,它可以帮助你测试和开发API,Postman 提供了测试 API 的友好界面和功能,使用简单便捷,安全可靠。Postman 是每一位前后端开发者必掌握的开发工具。
如何安装 Postman
官网安装 https://www.postman.com/
注意:
JDK版本选择8。
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
不同功能的类放在不同功能的包里面。
Domain和Dto区别:
* 从用法上来说:Domain用于java数据和数据库表记录映射,删除增加修改数据库的数据,用在service层和Mapper层。
* Dto用在前后端数据传输:用在controller层和Service层Service层介于Controller和Mapper之间,也是Domain和Dto的转换层。
@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;
<dependencies>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-bootstarterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
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 {
}
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusgeneratorartifactId>
<version>3.5.2version>
dependency>
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocity-enginecoreartifactId>
<version>2.0version>
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
*/
@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);
}
}
修改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证书时会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。私钥也可以通过工具从商户的p12证书中导出。请妥善保管好你的商户私钥文件。将下载的私钥文件复制到项目根目录。
注意:
不要把私钥文件暴露在公共场合,如上传到github,写在客户端代码等。
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
我们可以使用官方提供的 SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
<dependency>
<groupId>com.github.wechatpayapiv3groupId>
<artifactId>wechatpay-apachehttpclientartifactId>
<version>0.4.4version>
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);
}
}
微信支付平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。
注意:
不同的商户,对应的微信支付平台证书是不一样的,平台证书会周期性更换。商户应定时通过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 对象:是建立远程连接的基础,我们通过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;
}
微信支付官方文档
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
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 下单接口,微信后台系统返回链接参数 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)(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。
@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);
}
请求URL:/api/wx-pay/native/{orderNo}
请求方式:POST
商户端发起支付请求,微信端创建支付订单并生成支付二维码链
接,微信端将支付二维码返回给商户端,商户端显示支付二维码,
用户使用微信客户端扫码后发起支付。Native支付开发指引
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 是一个用于生成二维码的 JavaScript 库。主要是通过获取 DOM 的标签,再通过 HTML5 Canvas 绘制而成,不依赖任何库。
<div id="qrcode">div>
<script type="text/javascript">
new QRCode(document.getElementById("qrcode"),
"http://www.baidu.com"); // 设置要生成二维码的链接
script>
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 二维码生成库:QRCodetitle>
<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>
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;
/**
*
* 服务类
*
*
* @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
* @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穿透”。一句话来说就是,让外网能访问你的内网;把自己的内网(主机)当成服务器,让外网能访问。
./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;
}
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;
/**
*
* * 服务实现类
*
*
* @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基于AQS,在并发编程中可以实现公平锁和非公平锁来对资源进行同步,同时,和synchronized一样,ReentrantLock支持可重入,ReentrantLock在调度上更灵活,支持更多丰富的功能。
温馨提示:
ReentrantLock是java.util.concurrent包下提供的一套互斥锁,
相比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();
}
}
@GetMapping("/queryOrderStatus/{orderNo}")
public BaseResult queryOrder(@PathVariable String orderNo) throws Exception {
log.info("查询订单");
String result = iWxPaymentService.queryOrderStatus(orderNo);
return BaseResult.ok(result);
}
Spring 3.0后提供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点,4点
0 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;
}
从第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);
}
注意:
关单没有时间限制,建议在订单生成后间隔几分钟(最短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();
}
交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
注意:
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);
}
注意:
对后台通知交互时,如果微信收到应答不是成功或超时,微信
认为通知失败,微信会通过一定的策略定期重新发起通知,尽
可能提高通知的成功率,但微信不保证通知最终能成功。
特别提醒:商户系统对于开启结果通知的内容一定要做签名验
证,并校验通知的信息是否与商户侧的信息一致,防止数据泄
露导致出现“假通知”,造成资金损失
/**
* 退款结果通知
* @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();
}
}
}