参考视频:【尚硅谷】微信支付开发 https://www.bilibili.com/video/BV1hh411D7sb?p=1
微信支付介绍和接入指引 微信支付产品介绍 https://pay.weixin.qq.com/static/product/product_intro.shtml?name=qrcode
付款码支付:
用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。
JSAPI支付:
线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支付。
公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
PC网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支付。
特点:用户在客户端输入支付金额
小程序支付:
Native支付:
Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。
特点:商家预先指定支付金额
APP支付:
商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。
刷脸支付:
用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。
接入指引
官网:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_1.shtml
获取商户号:
获取APPID:
获取API秘钥:
获取APIv3秘钥:
随机密码生成工具:https://suijimimashengcheng.bmcx.com/
申请商户API证书以及对应证书序列号:
获取微信平台证书:
可以预先下载,也可以通过编程的方式获取。建议通过编程的方式来获取避免证书过期。
注意:以上所有API秘钥和证书需妥善保管防止泄露
支付安全(证书/秘钥/签名) 信息安全的基础 - 机密性
明文: 加密前的消息叫“明文”(plain text)
密文: 加密后的文本叫“密文”(cipher text)
密钥: 只有掌握特殊“钥匙”的人,才能对加密的文本进行解密,这里的“钥匙”就叫做“密钥”(key)
“密钥”就是一个字符串,度量单位是“位”(bit),比如,密钥长度是 128,就是 16 字节的二进制串
加密: 实现机密性最常用的手段是“加密”(encrypt)
按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。
解密: 使用密钥还原明文的过程叫“解密”(decrypt)
加密算法: 加密解密的操作过程就是“加密算法”
所有的加密算法都是公开的,而算法使用的“密钥”则必须保密
对称加密和非对称加密
身份认证
公钥加密,私钥解密的作用是加密信息
私钥加密,公钥解密的作用是身份认证
公钥加密:
私钥加密:
摘要算法(Digest Algorithm) 摘要算法就是我们常说的散列函数、哈希函数(Hash Function),它能够把任意长度的数据“压缩”成固定长度而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
作用 :保证信息的完整性
特性 :
常见摘要算法 :MD5、SHA1、SHA2(SHA224、SHA256、SHA384)【SHA2用的比较多,具有强的抗碰撞性】
注意:这样还是无法鉴别出信息传输的完整性,黑客可以更改明文和摘要达到欺骗的目的,所以还需要数字签名
数字签名 数字签名是使用私钥对摘要加密生成签名,需要由公钥将签名解密后进行验证,实现身份认证和不可否认。
签名和验证签名的流程 :
数字证书
数字证书解决“公钥的信任”问题,可以防止黑客伪造公钥。
不能直接分发公钥,公钥的分发必须使用数字证书,数字证书由CA颁发:
通过数字证书获取公钥进行验签流程:
https协议中的数字证书:
微信APIv3证书 商户证书 :
商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
商户证书在商户后台申请:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/
平台证书(微信支付平台):
微信支付平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。
平台证书的获取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
注意:API密钥和APIv3密钥都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API密钥对应V2版本的API
APIv3密钥对应V3版本的API
创建案例项目 初始化SpringBoot项目 IDEA->NEW Project->Spring Initializer
注意:Java版本选择8,Server URL可以用阿里云的: http://start.aliyun.com
,SringBoot版本是2.3.7
添加依赖:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
配置yaml:
1 2 3 4 5 6 7 server: port: 8090 spring: application: name: Wechat-Payment
创建Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package top.nanzx.learn_wechatpayment.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/api/product") public class ProductController { @GetMapping("/test") public String test () { return "Hello World" ; } }
测试访问:http://localhost:8090/api/product/test
引入Swagger 作用:自动生成接口文档和测试页面。
添加依赖:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > 2.7.0</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > 2.7.0</version > </dependency >
创建Swagger2的配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package top.nanzx.learn_wechatpayment.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.builders.ApiInfoBuilder;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket docket () { Docket docket = new Docket (DocumentationType.SWAGGER_2); docket.apiInfo(new ApiInfoBuilder ().title("微信支付案例接口文档" ).build()); return docket; } }
使用Swagger常用注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package top.nanzx.learn_wechatpayment.controller;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@Api(tags = "商品管理") @RestController @RequestMapping("/api/product") public class ProductController { @ApiOperation(value = "测试接口") @GetMapping("/test") public String test () { return "Hello World" ; } }
测试访问:http://localhost:8090/swagger-ui.html
注意:spring-boot-starter-web版本号如果太高会跟swagger匹配不上,可以配置如下:
1 2 3 4 5 6 spring: application: name:payment mvc: pathmatch: matching-strategy: ant_path_matcher
定义统一结果 作用:定义统一响应结果,为前端返回标准格式的数据。
引入lombok依赖,简化实体类的开发:
1 2 3 4 5 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency >
创建统一结果类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package top.nanzx.learn_wechatpayment.vo;import lombok.Data;import java.util.HashMap;import java.util.Map;@Data @Accessors(chain = true) public class R { private Integer code; private String message; private Map<String, Object> data = new HashMap <>(); public static R ok () { R r = new R (); r.setCode(0 ); r.setMessage("成功" ); return r; } public static R error () { R r = new R (); r.setCode(-1 ); r.setMessage("失败" ); return r; } public R data (String key, Object value) { this .data.put(key, value); return this ; } }
修改test方法,返回统一结果:
1 2 3 4 5 @ApiOperation(value = "测试接口") @GetMapping("/test") public R test () { return new R ().data("message" , "Hello World" ).data("dataTime" , new Date ()); }
配置json时间格式:
1 2 3 4 5 6 7 8 9 10 server: port: 8090 spring: application: name: Wechat-Payment jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
Swagger测试接口:http://localhost:8090/swagger-ui.html
新建数据库和表 在Mysql的查询窗口中执行以下命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 CREATE DATABASE wechat_payment;USE `wechat_payment`; CREATE TABLE `t_order_info` ( `id` bigint (11 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id' , `title` varchar (256 ) DEFAULT NULL COMMENT '订单标题' , `order_no` varchar (50 ) DEFAULT NULL COMMENT '商户订单编号' , `user_id` bigint (20 ) DEFAULT NULL COMMENT '用户id' , `product_id` bigint (20 ) DEFAULT NULL COMMENT '支付产品id' , `total_fee` int (11 ) DEFAULT NULL COMMENT '订单金额(分)' , `code_url` varchar (50 ) DEFAULT NULL COMMENT '订单二维码连接' , `order_status` varchar (10 ) DEFAULT NULL COMMENT '订单状态' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4; CREATE TABLE `t_payment_info` ( `id` bigint (20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id' , `order_no` varchar (50 ) DEFAULT NULL COMMENT '商户订单编号' , `transaction_id` varchar (50 ) DEFAULT NULL COMMENT '支付系统交易编号' , `payment_type` varchar (20 ) DEFAULT NULL COMMENT '支付类型' , `trade_type` varchar (20 ) DEFAULT NULL COMMENT '交易类型' , `trade_state` varchar (50 ) DEFAULT NULL COMMENT '交易状态' , `payer_total` int (11 ) DEFAULT NULL COMMENT '支付金额(分)' , `content` text COMMENT '通知参数' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4; CREATE TABLE `t_product` ( `id` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '商品id' , `title` varchar (20 ) DEFAULT NULL COMMENT '商品名称' , `price` int (11 ) DEFAULT NULL COMMENT '价格(分)' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4; insert into `t_product`(`title`,`price`) values ('Java课程' ,1 );insert into `t_product`(`title`,`price`) values ('大数据课程' ,1 );insert into `t_product`(`title`,`price`) values ('前端课程' ,1 );insert into `t_product`(`title`,`price`) values ('UI课程' ,1 );CREATE TABLE `t_refund_info` ( `id` bigint (20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id' , `order_no` varchar (50 ) DEFAULT NULL COMMENT '商户订单编号' , `refund_no` varchar (50 ) DEFAULT NULL COMMENT '商户退款单编号' , `refund_id` varchar (50 ) DEFAULT NULL COMMENT '支付系统退款单号' , `total_fee` int (11 ) DEFAULT NULL COMMENT '原订单金额(分)' , `refund` int (11 ) DEFAULT NULL COMMENT '退款金额(分)' , `reason` varchar (50 ) DEFAULT NULL COMMENT '退款原因' , `refund_status` varchar (10 ) DEFAULT NULL COMMENT '退款状态' , `content_return` text COMMENT '申请退款返回参数' , `content_notify` text COMMENT '退款结果通知参数' , `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET= utf8mb4;
集成MyBatis-Plus 引入依赖:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.25</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.3.1</version > </dependency >
配置数据库连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 8090 spring: application: name: Wechat-Payment jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/wechat_payment?serverTimezone=GMT%2B8&characterEncoding=utf-8 username: root password: 123456
定义实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package top.nanzx.learn_wechatpayment.entity;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import lombok.Data;import java.util.Date;@Data public class BaseEntity { @TableId(value = "id", type = IdType.AUTO) private String id; private Date createTime; private Date updateTime; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package top.nanzx.learn_wechatpayment.entity;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;@Data @TableName("t_refund_info") public class RefundInfo extends BaseEntity { private String orderNo; private String refundNo; private String refundId; private Integer totalFee; private Integer refund; private String reason; private String refundStatus; private String contentReturn; private String contentNotify; }
定义Dao层 1 2 3 4 5 6 7 8 package top.nanzx.learn_wechatpayment.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import top.nanzx.learn_wechatpayment.entity.RefundInfo;public interface RefundInfoMapper extends BaseMapper <RefundInfo> {}
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="top.nanzx.learn_wechatpayment.mapper.RefundInfoMapper" > </mapper >
定义Service层 1 2 3 4 5 6 7 8 9 package top.nanzx.learn_wechatpayment.service;import com.baomidou.mybatisplus.extension.service.IService;import top.nanzx.learn_wechatpayment.entity.RefundInfo;public interface RefundInfoService extends IService <RefundInfo> {}
1 2 3 4 5 6 7 8 9 10 11 12 package top.nanzx.learn_wechatpayment.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import org.springframework.stereotype.Service;import top.nanzx.learn_wechatpayment.entity.RefundInfo;import top.nanzx.learn_wechatpayment.mapper.RefundInfoMapper;import top.nanzx.learn_wechatpayment.service.RefundInfoService;@Service public class RefundInfoServiceImpl extends ServiceImpl <RefundInfoMapper, RefundInfo> implements RefundInfoService {}
配置Mybatis-Plus的config 1 2 3 4 5 6 7 8 9 10 11 12 package top.nanzx.learn_wechatpayment.config;import org.mybatis.spring.annotation.MapperScan;import org.springframework.context.annotation.Configuration;import org.springframework.transaction.annotation.EnableTransactionManagement;@Configuration @EnableTransactionManagement @MapperScan("top.nanzx.learn_wechatpayment.mapper") public class MybatisPlusConfig {}
测试:http://localhost:8090/api/product/list
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package top.nanzx.learn_wechatpayment.controller;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import top.nanzx.learn_wechatpayment.entity.Product;import top.nanzx.learn_wechatpayment.service.ProductService;import top.nanzx.learn_wechatpayment.vo.R;import java.util.Date;import java.util.List;@Api(tags = "商品管理") @RestController @RequestMapping("/api/product") @CrossOrigin public class ProductController { @Autowired private ProductService productService; @ApiOperation(value = "测试接口") @GetMapping("/test") public R test () { return new R ().data("message" , "Hello World" ).data("dataTime" , new Date ()); } @GetMapping("/list") public R list () { List<Product> list = productService.list(); return new R ().data("productList" , list); } }
扩展 一般情况下,java目录下的xml文件不会被打包,所以target里会找不到xml文件,可以在pom.xml中的build标签中添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <resources > <resource > <directory > src/main/java</directory > <includes > <include > **/*.properties</include > <include > **/*.xml</include > </includes > <filtering > false</filtering > </resource > <resource > <directory > src/main/resources</directory > <includes > <include > **/*.properties</include > <include > **/*.xml</include > <include > **/*.yml</include > </includes > <filtering > true</filtering > </resource > </resources >
添加持久层日志和xml文件位置的配置:
1 2 3 4 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:top/nanzx/learn_wechatpayment/mapper/xml/*.xml
基础支付API v3
官网开发指引:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml
APIv3证书与密钥使用说明 :
引入支付参数 将wxpay.properties 复制到resources目录中【参考1.2 接入指引 获取】:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 wxpay.mch-id =1558950191 wxpay.mch-serial-no =34345964330B66427E0D3D28826C4993C77E631F wxpay.private-key-path =D:\\javaProjects\\MyLearn\\Learn_WechatPayment\\apiclient_key.pem wxpay.api-v3-key =UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B wxpay.appid =wx74862e0dfcf69954 wxpay.domain =https://api.mch.weixin.qq.com wxpay.notify-domain =https://7d92-115-171-63-135.ngrok.io
配置WxPayConfifig.java读取支付参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package top.nanzx.learn_wechatpayment.config;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;@Configuration @PropertySource("classpath:wxpay.properties") @ConfigurationProperties(prefix="wxpay") @Data public class WxPayConfig { private String mchId; private String mchSerialNo; private String privateKeyPath; private String apiV3Key; private String appid; private String domain; private String notifyDomain; }
测试支付参数的获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package top.nanzx.learn_wechatpayment.controller;import io.swagger.annotations.Api;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import top.nanzx.learn_wechatpayment.config.WxPayConfig;import top.nanzx.learn_wechatpayment.vo.R;import javax.annotation.Resource;@Api(tags = "测试控制器") @RestController @RequestMapping("/api/test") public class TestController { @Resource private WxPayConfig wxPayConfig; @GetMapping("/get-wx-pay-config") public R getWxPayConfig () { String mchId = wxPayConfig.getMchId(); return R.ok().data("mchId" , mchId); } }
扩展
在IDEA中设置 SpringBoot 配置文件,让IDEA可以识别配置文件,将配置文件的图标展示成SpringBoot的图标,同时配置文件的内容可以高亮显示:File -> Project Structure -> Modules -> 展开对应项目文件夹 -> Spring -> 选择小叶子 -> 点击+号选择wxpay.properties
配置 Annotation Processor 可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位(ctrl+鼠标左键),方便开发。
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency >
加载商户私钥 将下载的私钥文件apiclient_key.pem复制到项目根目录下。
我们可以使用官方提供的 SDK ,帮助我们完成开发。实现请求签名的生成和应答签名的验证。
SDK 就是 Software Development Kit 的缩写,中文意思就是“软件开发工具包”。这是一个覆盖面相当广泛的名词,可以这么说:辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做“SDK”
微信官方Java SDK地址:https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
引入依赖:
1 2 3 4 5 <dependency > <groupId > com.github.wechatpay-apiv3</groupId > <artifactId > wechatpay-apache-httpclient</artifactId > <version > 0.4.0</version > </dependency >
加载商户私钥 Link :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Configuration @PropertySource("classpath:wxpay.properties") @ConfigurationProperties(pre fix = "wxpay") @Data public class WxPayConfig { private String mchId; private String mchSerialNo; private String privateKeyPath; private String apiV3Key; private String appid; private String domain; private String notifyDomain; private PrivateKey getPrivateKey (String fileName) { try { return PemUtil.loadPrivateKey(new FileInputStream (fileName)); } catch (FileNotFoundException e) { throw new RuntimeException ("私钥文件不存在" , e); } } }
测试商户私钥的获取(将前面的方法改成public的再进行测试,测试私钥对象是否能够获取出来):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package top.nanzx.learn_wechatpayment;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;import top.nanzx.learn_wechatpayment.config.WxPayConfig;import javax.annotation.Resource;import java.security.PrivateKey;@SpringBootTest class LearnWechatPaymentApplicationTests { @Resource private WxPayConfig wxPayConfig; @Test void contextLoads () { } @Test void getPrivateKey () { String privateKeyPath = wxPayConfig.getPrivateKeyPath(); PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath); System.out.println(privateKey); } }
注意:
如果获取的私钥只有一行短短的字符串,可以更换更高版本的jdk,运行时看第一句是在哪个jdk目录下
如果后面启动 项目报Caused by: java.io.FileNotFoundException: apiclient_key.pem (系统找不到指定的文件。),建议将wxpay.properties的私钥路径换成绝对路径
获取签名验证器和HttpClient 定时更新平台证书功能 Link
签名验证器:SDK版本>=0.4.0
可使用 CertificatesManager.getVerifier(mchId) 得到的验签器替代默认的验签器。它可以帮助我们进行签名和验签工作,同时也会定时下载和更新商户对应的微信支付平台证书 (默认下载间隔为UPDATE_INTERVAL_MINUTE)。我们单独将它定义出来,方便后面的开发。
HttpClient 对象:是建立远程连接的基础,我们通过SDK中配置了验签器(Verifier)的WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签 ,并进行证书自动更新。
视频中老师使用的是0.3.0
的ScheduledUpdateCertificatesVerifier类替代默认的验签器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 package top.nanzx.learn_wechatpayment.config;import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;... @Configuration @PropertySource("classpath:wxpay.properties") @ConfigurationProperties(prefix = "wxpay") @Data public class WxPayConfig { private String mchId; private String mchSerialNo; private String privateKeyPath; private String apiV3Key; private String appid; private String domain; private String notifyDomain; private PrivateKey getPrivateKey (String fileName) { try { return PemUtil.loadPrivateKey(new FileInputStream (fileName)); } catch (FileNotFoundException e) { throw new RuntimeException ("私钥文件不存在" , e); } } @Bean(name = "verifer") public Verifier getVerifier () throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException { CertificatesManager certificatesManager = CertificatesManager.getInstance(); PrivateKey privateKey = getPrivateKey(privateKeyPath); PrivateKeySigner privateKeySigner = new PrivateKeySigner (mchSerialNo, privateKey); WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials (mchId, privateKeySigner); certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); return certificatesManager.getVerifier(mchId); } @Bean(name = "wxPayClient") public CloseableHttpClient getWxPayClient () throws GeneralSecurityException, NotFoundException, IOException, HttpCodeException { PrivateKey privateKey = getPrivateKey(privateKeyPath); Verifier verifier = getVerifier(); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator (verifier)); return builder.build(); } }
API字典和相关工具 Native支付模块
功能列表
描述
Native下单
通过本接口提交微信支付Native支付订单
查询订单
通过此接口查询订单状态
关闭订单
通过此接口关闭待支付订单
Native调起支付
商户后台系统先调用微信支付的Native支付接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
支付结果通知
微信支付通过支付通知接口将用户支付成功消息通知给商户
申请退款
商户可以通过该接口将支付金额退还给买家
查询单笔退款
提交退款申请后,通过调用该接口查询退款状态
退款结果通知
微信支付通过退款通知接口将用户退款成功消息通知给商户
申请交易账单
商户可以通过该接口获取交易账单文件的下载地址
申请资金账单
商户可以通过该接口获取资金账单文件的下载地址
下载账单
通过申请交易/资金账单获取到download_url在该接口获取到对应的账单。
微信支付 APIv3 使用 JSON 作为消息体的数据交换格式,引入gson依赖处理json数据:
1 2 3 4 <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > </dependency >
定义枚举 内容包括接口地址,支付状态等信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package top.nanzx.learn_wechatpayment.enums.wxpay;import lombok.AllArgsConstructor;import lombok.Getter;@AllArgsConstructor @Getter public enum WxApiType { NATIVE_PAY("/v3/pay/transactions/native" ), ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s" ), CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close" ), DOMESTIC_REFUNDS("/v3/refund/domestic/refunds" ), DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s" ), TRADE_BILLS("/v3/bill/tradebill" ), FUND_FLOW_BILLS("/v3/bill/fundflowbill" ); private final String type; }
添加工具类 简化项目的开发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package top.nanzx.learn_wechatpayment.util;import javax.servlet.http.HttpServletRequest;import java.io.BufferedReader;import java.io.IOException;public class HttpUtils { 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(); } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package top.nanzx.learn_wechatpayment.util;import java.text.SimpleDateFormat;import java.util.Date;import java.util.Random;public class OrderNoUtils { public static String getOrderNo () { return "ORDER_" + getNo(); } public static String getRefundNo () { return "REFUND_" + getNo(); } public static String getNo () { SimpleDateFormat sdf = new SimpleDateFormat ("yyyyMMddHHmmss" ); String newDate = sdf.format(new Date ()); String result = "" ; Random random = new Random (); for (int i = 0 ; i < 3 ; i++) { result += random.nextInt(10 ); } return newDate + result; } }
业务流程时序图 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
生成订单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package top.nanzx.learn_wechatpayment.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;... import javax.annotation.Resource;@Service @Slf4j public class OrderInfoServiceImpl extends ServiceImpl <OrderInfoMapper, OrderInfo> implements OrderInfoService { @Resource private ProductMapper productMapper; @Override public OrderInfo createOrderByProductId (Long productId) { OrderInfo orderInfo = this .getNoPayOrderByProductId(productId); if (orderInfo != null ) { return orderInfo; } Product product = productMapper.selectById(productId); orderInfo = new OrderInfo (); orderInfo.setTitle(product.getTitle()); orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); orderInfo.setProductId(productId); orderInfo.setTotalFee(product.getPrice()); orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); baseMapper.insert(orderInfo); return orderInfo; } private OrderInfo getNoPayOrderByProductId (Long productId) { QueryWrapper<OrderInfo> wrapper = new QueryWrapper <>(); wrapper.eq("product_id" , productId); wrapper.eq("order_status" , OrderStatus.NOTPAY.getType()); return baseMapper.selectOne(wrapper); } @Override public void saveCodeUrl (String orderNo, String codeUrl) { QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("order_no" , orderNo); OrderInfo orderInfo = new OrderInfo (); orderInfo.setCodeUrl(codeUrl); baseMapper.update(orderInfo, queryWrapper); } }
Native下单API
官方Api指引:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
定义Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package top.nanzx.learn_wechatpayment.controller;import io.swagger.annotations.Api;... @CrossOrigin @RestController @RequestMapping("api/wx-pay") @Api(tags = "网站的微信支付API") @Slf4j public class WxPayController { @Autowired private WxPayService wxPayService; @RequestMapping("/native/{productId}") @ApiOperation("调用统一下单API,生成支付二维码") public R nativePay (@PathVariable Long productId) throws Exception { log.info("发起支付请求" ); Map<String, Object> map = wxPayService.nativePay(productId); return R.ok().setData(map); } }
定义Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 package top.nanzx.learn_wechatpayment.service.impl;import com.google.gson.Gson;... @Service @Slf4j public class WxPayServiceImpl implements WxPayService { @Autowired private WxPayConfig wxPayConfig; @Autowired private CloseableHttpClient wxPayClient; @Autowired private OrderInfoService orderInfoService; @Override public Map<String, Object> nativePay (Long productId) throws Exception { log.info("生成订单" ); OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId); String codeUrl = orderInfo.getCodeUrl(); if (!StringUtils.isEmpty(codeUrl)) { log.info("订单已存在,二维码已保存" ); Map<String, Object> map = new HashMap <>(); map.put("codeUrl" , codeUrl); map.put("orderNo" , orderInfo.getOrderNo()); return map; } log.info("调用统一下单API" ); Gson gson = new Gson (); Map<String, Object> paramsMap = new HashMap <>(); paramsMap.put("appid" , wxPayConfig.getAppid()); paramsMap.put("mchid" , wxPayConfig.getMchId()); paramsMap.put("description" , orderInfo.getTitle()); paramsMap.put("out_trade_no" , orderInfo.getOrderNo()); paramsMap.put("notify_url" , wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); Map<String, Object> amountMap = new HashMap <>(); amountMap.put("total" , orderInfo.getTotalFee()); amountMap.put("currency" , "CNY" ); paramsMap.put("amount" , amountMap); String jsonParams = gson.toJson(paramsMap); log.info("请求参数:" + jsonParams); HttpPost httpPost = new HttpPost (wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); StringEntity entity = new StringEntity (jsonParams, "utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); httpPost.setHeader("Accept" , "application/json" ); try (CloseableHttpResponse response = wxPayClient.execute(httpPost)) { int statusCode = response.getStatusLine().getStatusCode(); String bodyAsString = EntityUtils.toString(response.getEntity()); if (statusCode == 200 ) { log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { log.info("失败, 响应状态码 = " + statusCode + ",返回结果 = " + bodyAsString); throw new IOException ("请求失败" ); } Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class); codeUrl = resultMap.get("code_url" ); String orderNo = orderInfo.getOrderNo(); orderInfoService.saveCodeUrl(orderNo, codeUrl); Map<String, Object> map = new HashMap <>(); map.put("codeUrl" , codeUrl); map.put("orderNo" , orderInfo.getOrderNo()); return map; } } }
显示订单列表 定义Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package top.nanzx.learn_wechatpayment.controller;import io.swagger.annotations.Api;... @Api(tags = "商品订单管理") @RestController @RequestMapping("/api/order-info") @CrossOrigin public class OrderInfoController { @Autowired private OrderInfoService orderInfoService; @ApiOperation("订单列表") @GetMapping("/list") public R getOrderList () { List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc(); return R.ok().data("list" , list); } }
定义Service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package top.nanzx.learn_wechatpayment.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;... @Service @Slf4j public class OrderInfoServiceImpl extends ServiceImpl <OrderInfoMapper, OrderInfo> implements OrderInfoService { @Resource private ProductMapper productMapper; ... @Override public List<OrderInfo> listOrderByCreateTimeDesc () { QueryWrapper<OrderInfo> wrapper = new QueryWrapper <OrderInfo>().orderByDesc("create_time" ); return baseMapper.selectList(wrapper); } }
支付通知API
官方Api指引:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
内网穿透
访问ngrok的官网:https://ngrok.com/
注册账号并登录,也可以用Github账号登录
下载ngrok工具
设置 authToken,为本地计算机做授权配置,每个人令牌不同:ngrok authtoken 6aYc6Kp7kpxVr8pY88LkG_6x9o18yMY8BASrXiDFMeS
启动服务:ngrok http 8090
测试外网访问:你获得的外网地址/api/product/test
接收通知和返回应答
启动ngrok 服务:ngrok http 8090
在wxpay.properties设置通知地址(注意:每次重新启动ngrok,都需要根据实际情况修改这个配置):wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io
创建通知接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package top.nanzx.learn_wechatpayment.controller;import com.google.gson.Gson;... @CrossOrigin @RestController @RequestMapping("api/wx-pay") @Api(tags = "网站的微信支付API") @Slf4j public class WxPayController { @Autowired private WxPayService wxPayService; @ApiOperation("支付通知") @PostMapping("/native/notify") public String nativeNotify (HttpServletRequest request, HttpServletResponse response) { Gson gson = new Gson (); Map<String, String> map = new HashMap <>(); try { String body = HttpUtils.readData(request); Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id" ); log.info("支付通知的id ===> {}" , requestId); log.info("支付通知的完整数据 ===> {}" , body); response.setStatus(200 ); map.put("code" , "SUCCESS" ); map.put("message" , "成功" ); } catch (Exception e) { e.printStackTrace(); response.setStatus(500 ); map.put("code" , "ERROR" ); map.put("message" , "系统错误" ); return gson.toJson(map); } return gson.toJson(map); } }
验签
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。微信建议商户验证应答签名。(注解:这里的应答对应图中的第二步,由于使用微信SDK中httpClient发送的请求,所以会自动对响应进行验签)
1 2 3 4 CloseableHttpClient httpClient= WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, privateKey) .withValidator(new WechatPay2Validator (verifier)) .build();
同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须验证回调的签名,以确保回调是由微信支付发送。(注解:这里的应答对应图中的第三步,并没有通过httpclient发送请求,所以不会自动验签)我们可以参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 package top.nanzx.learn_wechatpayment.util;import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;... public class WechatPay2ValidatorForRequest { protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class); protected static final long RESPONSE_EXPIRED_MINUTES = 5 ; protected final Verifier verifier; protected final String body; protected final String requestId; public WechatPay2ValidatorForRequest (Verifier verifier, String body, String requestId) { this .verifier = verifier; this .body = body; this .requestId = requestId; } protected static IllegalArgumentException parameterError (String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException ("parameter error: " + message); } protected static IllegalArgumentException verifyFail (String message, Object... args) { message = String.format(message, args); return new IllegalArgumentException ("signature verify fail: " + message); } public final boolean validate (HttpServletRequest request) throws IOException { try { validateParameters(request); String message = buildMessage(request); String serial = request.getHeader(WECHAT_PAY_SERIAL); String signature = request.getHeader(WECHAT_PAY_SIGNATURE); if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]" , serial, message, signature, request.getHeader(REQUEST_ID)); } } catch (IllegalArgumentException e) { log.warn(e.getMessage()); return false ; } return true ; } protected final void validateParameters (HttpServletRequest request) { String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; String header = null ; for (String headerName : headers) { header = request.getHeader(headerName); if (header == null ) { throw parameterError("empty [%s], request-id=[%s]" , headerName, requestId); } } String timestampStr = header; try { Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { throw parameterError("timestamp=[%s] expires, request-id=[%s]" , timestampStr, requestId); } } catch (DateTimeException | NumberFormatException e) { throw parameterError("invalid timestamp=[%s], request-id=[%s]" , timestampStr, requestId); } } protected final String buildMessage (HttpServletRequest request) throws IOException { String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); String nonce = request.getHeader(WECHAT_PAY_NONCE); return timestamp + "\n" + nonce + "\n" + body + "\n" ; } }
通知接口添加验签:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package top.nanzx.learn_wechatpayment.controller;import com.google.gson.Gson;... @CrossOrigin @RestController @RequestMapping("api/wx-pay") @Api(tags = "网站的微信支付API") @Slf4j public class WxPayController { @Autowired private WxPayService wxPayService; @Resource private Verifier verifer; @RequestMapping("/native/{productId}") @ApiOperation("调用统一下单API,生成支付二维码") public R nativePay (@PathVariable Long productId) throws Exception { log.info("发起支付请求" ); Map<String, Object> map = wxPayService.nativePay(productId); return R.ok().setData(map); } @ApiOperation("支付通知") @PostMapping("/native/notify") public String nativeNotify (HttpServletRequest request, HttpServletResponse response) { Gson gson = new Gson (); Map<String, String> map = new HashMap <>(); try { String body = HttpUtils.readData(request); Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class); String requestId = (String) bodyMap.get("id" ); log.info("支付通知的id ===> {}" , requestId); log.info("支付通知的完整数据 ===> {}" , body); WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest (verifer, body, requestId); if (!wechatPay2ValidatorForRequest.validate(request)) { log.error("通知验签失败" ); response.setStatus(500 ); map.put("code" , "ERROR" ); map.put("message" , "系统错误" ); return gson.toJson(map); } response.setStatus(200 ); map.put("code" , "SUCCESS" ); map.put("message" , "成功" ); } catch (Exception e) { e.printStackTrace(); response.setStatus(500 ); map.put("code" , "ERROR" ); map.put("message" , "系统错误" ); return gson.toJson(map); } return gson.toJson(map); } }
参数解密
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
下面详细描述对通知数据进行解密的流程:
用商户平台上设置的APIv3密钥【微信商户平台 —>账户设置—>API安全—>设置APIv3密钥】,记为key;
针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;
我们可以通过参考AesUtil.Java 对证书和回调解密。
在nativeNotify 方法中添加处理订单的代码:
1 2 wxPayService.processOrder(bodyMap);
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package top.nanzx.learn_wechatpayment.service.impl;import com.google.gson.Gson;... @Service @Slf4j public class WxPayServiceImpl implements WxPayService { @Autowired private WxPayConfig wxPayConfig; @Autowired private CloseableHttpClient wxPayClient; @Autowired private OrderInfoService orderInfoService; ... @Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap); } private String decryptFromResource (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("密文解密" ); Map<String, String> resourceMap = (Map) bodyMap.get("resource" ); String ciphertext = resourceMap.get("ciphertext" ); String nonce = resourceMap.get("nonce" ); String associatedData = resourceMap.get("associated_data" ); log.info("密文 ===> {}" , ciphertext); AesUtil aesUtil = new AesUtil (wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8)); String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); log.info("明文 ===> {}" , plainText); return plainText; } }
更新订单状态和记录支付日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package top.nanzx.learn_wechatpayment.service.impl;import com.google.gson.Gson;... @Service @Slf4j public class WxPayServiceImpl implements WxPayService { @Autowired private WxPayConfig wxPayConfig; @Autowired private CloseableHttpClient wxPayClient; @Autowired private OrderInfoService orderInfoService; ... @Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap); Gson gson = new Gson (); Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no" ); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(plainText); }
更新订单状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package top.nanzx.learn_wechatpayment.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;... @Service @Slf4j public class OrderInfoServiceImpl extends ServiceImpl <OrderInfoMapper, OrderInfo> implements OrderInfoService { @Resource private ProductMapper productMapper; ... @Override public void updateStatusByOrderNo (String orderNo, OrderStatus orderStatus) { log.info("更新订单状态 ===> {}" , orderStatus.getType()); QueryWrapper<OrderInfo> wrapper = new QueryWrapper <>(); wrapper.eq("order_no" , orderNo); OrderInfo orderInfo = new OrderInfo (); orderInfo.setOrderStatus(orderStatus.getType()); baseMapper.update(orderInfo, wrapper); } }
记录支付日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package top.nanzx.learn_wechatpayment.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;... @Service @Slf4j public class PaymentInfoServiceImpl extends ServiceImpl <PaymentInfoMapper, PaymentInfo> implements PaymentInfoService { @Override public void createPaymentInfo (String plainText) { log.info("记录支付日志" ); Gson gson = new Gson (); Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no" ); String transactionId = (String) plainTextMap.get("transaction_id" ); String tradeType = (String) plainTextMap.get("trade_type" ); String tradeState = (String) plainTextMap.get("trade_state" ); Map<String, Object> amount = (Map) plainTextMap.get("amount" ); Integer payerTotal = ((Double) amount.get("payer_total" )).intValue(); PaymentInfo paymentInfo = new PaymentInfo (); paymentInfo.setOrderNo(orderNo); paymentInfo.setPaymentType(PayType.WXPAY.getType()); paymentInfo.setTransactionId(transactionId); paymentInfo.setTradeType(tradeType); paymentInfo.setTradeState(tradeState); paymentInfo.setPayerTotal(payerTotal); paymentInfo.setContent(plainText); baseMapper.insert(paymentInfo); } }
接口幂等性 微信支付通过支付通知接口将用户支付成功消息通知给商户
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap); Gson gson = new Gson (); Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no" ); String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { return ; } orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(plainText); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public String getOrderStatus (String orderNo) { QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("order_no" , orderNo); OrderInfo orderInfo = baseMapper.selectOne(queryWrapper); if (orderInfo == null ) { return null ; } return orderInfo.getOrderStatus(); }
数据锁 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
定义 ReentrantLock 进行并发控制。注意,必须手动释放锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package top.nanzx.learn_wechatpayment.service.impl;import com.google.gson.Gson;... @Service @Slf4j public class WxPayServiceImpl implements WxPayService { ... private final ReentrantLock lock = new ReentrantLock (); ... @Override public void processOrder (Map<String, Object> bodyMap) throws GeneralSecurityException { log.info("处理订单" ); String plainText = decryptFromResource(bodyMap); Gson gson = new Gson (); Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class); String orderNo = (String) plainTextMap.get("out_trade_no" ); if (lock.tryLock()) { try { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) { return ; } orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(plainText); } finally { lock.unlock(); } } } }
查单接口 后端定义商户查单接口 (支付成功后,商户侧查询本地数据库,订单是否支付成功):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package top.nanzx.learn_wechatpayment.controller;import io.swagger.annotations.Api;... @Api(tags = "商品订单管理") @RestController @RequestMapping("/api/order-info") @CrossOrigin public class OrderInfoController { @Autowired private OrderInfoService orderInfoService; ... @ApiOperation("查询本地订单状态") @GetMapping("/query-order-status/{orderNo}") public R queryOrderStatus (@PathVariable String orderNo) { String orderStatus = orderInfoService.getOrderStatus(orderNo); if (OrderStatus.SUCCESS.getType().equals(orderStatus)) { return R.ok().setMessage("支付成功" ); } return R.ok().setCode(101 ).setMessage("支付中..." ); } }
前端定时轮询查单 (在二维码展示页面,前端定时轮询查询订单是否已支付,如果支付成功则跳转到订单页面):
定义定时器
//启动定时器
this.timer = setInterval(() => {
//查询订单是否支付成功
this.queryOrderStatus()
}, 3000)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - 查询订单 - ```javascript // 查询订单状态 queryOrderStatus() { orderInfoApi.queryOrderStatus(this.orderNo).then(response => { console.log('查询订单状态:' + response.code) // 支付成功后的页面跳转 if (response.code === 0) { console.log('清除定时器') clearInterval(this.timer) // 三秒后跳转到订单列表 setTimeout(() => { this.$router.push({ path: '/success' }) }, 3000) } }) }
关闭订单API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml
实现用户主动取消订单的功能:
WxPayController:
1 2 3 4 5 6 7 @ApiOperation("用户取消订单") @PostMapping("/cancel/{orderNo}") public R cancel (@PathVariable String orderNo) throws Exception { log.info("取消订单" ); wxPayService.cancelOrder(orderNo); return R.ok().setMessage("订单已取消" ); }
WxPayServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Override public void cancelOrder (String orderNo) throws Exception { this .closeOrder(orderNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL); } private void closeOrder (String orderNo) throws Exception { log.info("关单接口的调用,订单号 ===> {}" , orderNo); String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url); HttpPost httpPost = new HttpPost (url); Gson gson = new Gson (); Map<String, String> paramsMap = new HashMap <>(); paramsMap.put("mchid" , wxPayConfig.getMchId()); String jsonParams = gson.toJson(paramsMap); log.info("请求参数 ===> {}" , jsonParams); StringEntity entity = new StringEntity (jsonParams, "utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); httpPost.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpPost); try { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功200" ); } else if (statusCode == 204 ) { log.info("成功204" ); } else { log.info("Native下单失败,响应码 = " + statusCode); throw new IOException ("request failed" ); } } finally { response.close(); } }
查询订单API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml
商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态。
WxPayController:
1 2 3 4 5 6 7 @ApiOperation("查询订单:测试订单状态用") @GetMapping("query/{orderNo}") public R queryOrder (@PathVariable String orderNo) throws Exception { log.info("查询订单" ); String bodyAsString = wxPayService.queryOrder(orderNo); return R.ok().setMessage("查询成功" ).data("bodyAsString" , bodyAsString); }
WxPayServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override public String queryOrder (String orderNo) throws Exception { log.info("查单接口调用 ===> {}" , orderNo); String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo); url = wxPayConfig.getDomain().concat(url).concat("? mchid=" ).concat(wxPayConfig.getMchId()); HttpGet httpGet = new HttpGet (url); httpGet.setHeader("Accept" , "application/json" ); CloseableHttpResponse response = wxPayClient.execute(httpGet); try { String bodyAsString = EntityUtils.toString(response.getEntity()); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200 ) { log.info("成功, 返回结果 = " + bodyAsString); } else if (statusCode == 204 ) { log.info("成功" ); } else { log.info("Native查单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString); throw new IOException ("request failed" ); } return bodyAsString; } finally { response.close(); } }
集成Spring Task Spring 3.0后提供Spring Task实现任务调度,启动类添加注解:@EnableScheduling
在线Cron表达式生成器:https://cron.qqe2.com/,其时间有**分、时、日、月、周**五种,操作符有
***** 取值范围内的所有数字
/ 每过多少个数字
- 从X到Z
, 散列数字
定时查找超时订单 WxPayTask:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package top.nanzx.learn_wechatpayment.task;import lombok.extern.slf4j.Slf4j;... @Slf4j public class WxPayTask { @Autowired private OrderInfoService orderInfoService; @Autowired private WxPayService wxPayService; @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); } } }
OrderInfoServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public List<OrderInfo> getNoPayOrderByDuration (int minutes) { Instant instant = Instant.now().minus(Duration.ofMinutes(minutes)); QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("order_status" , OrderStatus.NOTPAY.getType()); queryWrapper.le("create_time" , instant); return baseMapper.selectList(queryWrapper); }
处理超时订单 WxPayServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Override public void checkOrderStatus (String orderNo) throws Exception { log.warn("根据订单号核实订单状态 ===> {}" , orderNo); String result = this .queryOrder(orderNo); Gson gson = new Gson (); Map resultMap = gson.fromJson(result, HashMap.class); Object tradeState = resultMap.get("trade_state" ); if (WxTradeState.SUCCESS.getType().equals(tradeState)) { log.warn("核实订单已支付 ===> {}" , orderNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS); paymentInfoService.createPaymentInfo(result); } if (WxTradeState.NOTPAY.getType().equals(tradeState)) { log.warn("核实订单未支付 ===> {}" , orderNo); this .closeOrder(orderNo); orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED); } }
其他API类似,参考官方文档即可。