微信支付功能实现
一、创建SpringBoot项目
我们首先创建一个基本的SpringBoot项目。添加相关的依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
然后引入Swagger。目的是自动生成接口文档和测试页面
<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>
然后添加Swagger的配置文件。创建对应的config包和对应的Swagger2Config配置类
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder().title("微信支付接口文档").build());
}
}
创建测试接口
@RestController
public class WebController {
@ApiOperation("测试接口")
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
swagger中的两个核心注解要注意:
- @Api(tags=‘xxx’) 作用在类上
- @ApiOperation(‘xxxx’) 作用在方法上
启动服务后。访问:http://localhost:8080/swagger-ui.html 测试即可
引入lombok依赖。简化实体类的开发
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
二、集成MyBatisPlus
本案例中还是有些数据需要持久化到数据库中。这块我们通过MyBatisPlus来实现处理。先添加相关的Maven依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
然后添加对应的数据库配置信息
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/payment_demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true&nullCatalogMeansCurrent=true
spring.datasource.username=root
spring.datasource.password=123456
mybatis-plus.mapper-locations=classpath:mapper/*.xml
然后通过MyBatis的自动代码生成器来生成相关的模板代码
DROP TABLE IF EXISTS `t_order_info`;
CREATE TABLE `t_order_info` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(32) DEFAULT NULL COMMENT '订单标题',
`orderNo` varchar(64) DEFAULT NULL COMMENT '商户订单编号',
`userId` int DEFAULT NULL COMMENT '用户id',
`productId` int DEFAULT NULL COMMENT '支付产品id',
`totalFee` int DEFAULT NULL COMMENT '订单金额(分)',
`codeUrl` varchar(128) DEFAULT NULL COMMENT '订单二维码链接',
`orderStatus` varchar(32) DEFAULT NULL COMMENT '订单状态',
`createTime` datetime DEFAULT NULL COMMENT '创建时间',
`updateTime` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Data for the table `t_order_info` */
/*Table structure for table `t_payment_info` */
DROP TABLE IF EXISTS `t_payment_info`;
CREATE TABLE `t_payment_info` (
`id` int NOT NULL AUTO_INCREMENT,
`orderNo` varchar(64) DEFAULT NULL COMMENT '商品订单编号',
`transactionId` varchar(64) DEFAULT NULL COMMENT '支付系统交易编号',
`paymentType` varchar(32) DEFAULT NULL COMMENT '支付类型',
`tradeType` varchar(32) DEFAULT NULL COMMENT '交易类型',
`tradeState` varchar(32) DEFAULT NULL COMMENT '交易状态',
`payerTotal` int DEFAULT NULL COMMENT '支付金额(分)',
`content` varchar(64) DEFAULT NULL COMMENT '通知参数',
`createTime` datetime DEFAULT NULL COMMENT '创建时间',
`updateTime` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Data for the table `t_payment_info` */
/*Table structure for table `t_product` */
DROP TABLE IF EXISTS `t_product`;
CREATE TABLE `t_product` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(32) DEFAULT NULL,
`price` int DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_refund_info` */
DROP TABLE IF EXISTS `t_refund_info`;
CREATE TABLE `t_refund_info` (
`id` int NOT NULL AUTO_INCREMENT,
`orderNo` varchar(64) DEFAULT NULL COMMENT '商品订单编号',
`refundNo` varchar(64) DEFAULT NULL COMMENT '退款单编号',
`refundId` varchar(64) DEFAULT NULL COMMENT '支付系统退款单号',
`totalFee` int DEFAULT NULL COMMENT '原订单金额(分)',
`refund` int DEFAULT NULL COMMENT '退款金额(分)',
`reason` varchar(64) DEFAULT NULL COMMENT '退款原因',
`refundStatus` varchar(32) DEFAULT NULL COMMENT '退款单状态',
`contentReturn` varchar(64) DEFAULT NULL COMMENT '申请退款返回参数',
`contentNotify` varchar(128) DEFAULT NULL COMMENT '退款结果通知参数',
`createTime` datetime DEFAULT NULL COMMENT '创建时间',
`updateTime` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
<!-- MyBatisPlus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 在MyBatisPlus的代码生成器中我们需要导入 freemarker的依赖 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
代码生成器的相关代码
public class MyFastGeneratorConfiguration {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/payment_demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true"
, "root", "123456")
.globalConfig(builder -> {
builder.author("boge") // 设置作者
//.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D://pay"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.boge") // 设置父包名
.moduleName("pay") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "D://pay")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("t_order_info","t_payment_info","t_product","t_refund_info") // 设置需要生成的表名
.addTablePrefix("t_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
最后添加扫描的路径
三、添加支付页面
导入我们提前准备好的支付页面,具体的代码在附近中
四、定义相关枚举类型
在支付案例中我们会涉及到各种类型的使用。所以我们会定义各种类型来应用。
支付类型:
@AllArgsConstructor
@Getter
public enum PayType {
/**
* 微信
*/
WXPAY("微信"),
/**
* 支付宝
*/
ALIPAY("支付宝");
/**
* 类型
*/
private final String type;
}
订单状态:
@AllArgsConstructor
@Getter
public enum OrderStatus {
/**
* 未支付
*/
NOTPAY("未支付"),
/**
* 支付成功
*/
SUCCESS("支付成功"),
/**
* 已关闭
*/
CLOSED("超时已关闭"),
/**
* 已取消
*/
CANCEL("用户已取消"),
/**
* 退款中
*/
REFUND_PROCESSING("退款中"),
/**
* 已退款
*/
REFUND_SUCCESS("已退款"),
/**
* 退款异常
*/
REFUND_ABNORMAL("退款异常");
/**
* 类型
*/
private final String type;
}
微信Native下单接口
@AllArgsConstructor
@Getter
public enum WxApiType {
/**
* Native下单
*/
NATIVE_PAY("/v3/pay/transactions/native"),
/**
* Native下单
*/
NATIVE_PAY_V2("/pay/unifiedorder"),
/**
* 查询订单
*/
ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),
/**
* 关闭订单
*/
CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),
/**
* 申请退款
*/
DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
/**
* 查询单笔退款
*/
DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),
/**
* 申请交易账单
*/
TRADE_BILLS("/v3/bill/tradebill"),
/**
* 申请资金账单
*/
FUND_FLOW_BILLS("/v3/bill/fundflowbill");
/**
* 类型
*/
private final String type;
}
微信支付通知相关接口
@AllArgsConstructor
@Getter
public enum WxNotifyType {
/**
* 支付通知
*/
NATIVE_NOTIFY("/api/wx-pay/native/notify"),
/**
* 支付通知
*/
NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),
/**
* 退款结果通知
*/
REFUND_NOTIFY("/api/wx-pay/refunds/notify");
/**
* 类型
*/
private final String type;
}
退款相关状态
@AllArgsConstructor
@Getter
public enum WxRefundStatus {
/**
* 退款成功
*/
SUCCESS("SUCCESS"),
/**
* 退款关闭
*/
CLOSED("CLOSED"),
/**
* 退款处理中
*/
PROCESSING("PROCESSING"),
/**
* 退款异常
*/
ABNORMAL("ABNORMAL");
/**
* 类型
*/
private final String type;
}
支付相关的状态
@AllArgsConstructor
@Getter
public enum WxTradeState {
/**
* 支付成功
*/
SUCCESS("SUCCESS"),
/**
* 未支付
*/
NOTPAY("NOTPAY"),
/**
* 已关闭
*/
CLOSED("CLOSED"),
/**
* 转入退款
*/
REFUND("REFUND");
/**
* 类型
*/
private final String type;
}
五、基础支付API V3
1.引入相关参数
在项目的resources目录中创建wxpay.properties文件。并在其中定义相关的支付属性.
# 微信支付相关参数
# 商户号
wxpay.mch-id=1640588*****
# 商户API证书序列号
wxpay.mch-serial-no=36E5C0892B813B5B78BC44CFB90******
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=Y7CgFTppIyzzOXLm3RU1IFS*****
# APPID
wxpay.appid=wx1d4eab9******
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://b513049e52.zicp.fun
# APIv2密钥
wxpay.partnerKey: xxxxxxxxxxxxxxx
2.读取支付参数
我们在配置文件中配置的数据。系统启动的时候还是需要加载到内存中的。为了便于管理。我们创建一个WxPayConfig这个配置文件。来存储对应的配置信息
@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;
// APIv2密钥
private String partnerKey;
}
对应的我们需要添加对应的依赖信息
<!-- 生成自定义配置的元数据信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
然后我们简单测试下是否能够读取到对应的信息:
@Controller
public class WebController {
@Autowired
private WxPayConfig wxPayConfig;
@GetMapping({"/index","/"})
public String index(){
System.out.println(wxPayConfig.getAppid());
System.out.println(wxPayConfig.getMchId());
return "index";
}
}
重启服务后访问。可以看到获取到了相关的配置信息
3.加载商户私钥
3.1 复制商户私钥
把我们前面下载的私钥文件复制到项目的根目录下:
3.2 引入SDK
我们可以使用官方提供的 SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.9</version>
</dependency>
3.3 获取商户私钥
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
public PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(getClass().getResourceAsStream("/"+filename));
} catch (RuntimeException e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
3.4 测试获取私钥
我们在测试的时候可以把上面的方法设置为public。
@Autowired
private WxPayConfig wxPayConfig;
@GetMapping({"/index","/"})
public String index(){
System.out.println(wxPayConfig.getAppid());
System.out.println(wxPayConfig.getMchId());
System.out.println("------获取私钥信息------");
PrivateKey privateKey = wxPayConfig.getPrivateKey(wxPayConfig.getPrivateKeyPath());
System.out.println(privateKey);
return "index";
}
效果如下:
4.获取签名验证器和HttpClient
4.1 证书秘钥使用说明
微信支付-开发者文档 (qq.com)
4.2 获取签名验证器
签名验证器:(定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
4.3 获取HttpClient对象
HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象。
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
5. API字典和相关工具
5.1 API列表
我们的项目中要实现以下所有API的功能。
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
5.2 接口规则
基本规则-接口规则 | 微信支付商户平台文档中心 (qq.com)
微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。
5.3 添加工具类
将资料文件夹中的 util 目录复制到源码目录中,我们将会使用这些辅助工具简化项目的开发
6. Native下单API
6.1 Native支付流程
要完成Native下单我们需要先搞清楚Native的完整流程。这个在官网有详细的介绍:微信支付-开发者文档 (qq.com)
业务流程说明:
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【Native下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
6.2 Native下单API
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
商户端发起支付请求,微信端创建支付订单并生成支付二维码链接,微信端将支付二维码返回给商户
端,商户端显示支付二维码,用户使用微信客户端扫码后发起支付。
处理的核心代码:
@Override
public Map<String, Object> nativePay(Long productId) throws Exception{
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle("test");
orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号
orderInfo.setProductId(productId.intValue());
orderInfo.setTotalFee(1); //分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//TODO:存入数据库
//调用统一下单API
HttpPost httpPost = new
HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("description", orderInfo.getTitle());
paramsMap.put("out_trade_no", orderInfo.getOrderNo());
paramsMap.put("notify_url",
wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
Map amountMap = new HashMap();
amountMap.put("total", orderInfo.getTotalFee());
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " +
bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString,
HashMap.class);
//二维码
String codeUrl = resultMap.get("code_url");
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
运行后的提示报错:
登录商户平台发下Native支付为开通。我们需要开通改服务
申请开通:
然后我们再通过PostMan来测试访问:
在控制台也可以看到成功的信息
6.3 二维码展示
上面响应返回了对应的二维码地址。我们还需要把这个内容以图片的方式展示给客户。但是我们没有办法直接通过这个url地址来生成二维码图片,我们需要使用第三方库将 code_url 转化为二维码图片,例如 qrcode 库。
QRCode库是一个用于生成和解析二维码的开源库,它支持多种编程语言,如Java、Python、C++等。该库提供了丰富的API,可以用于生成不同大小、颜色和格式的二维码。同时,它还支持错误校验和纠正,可以确保生成的二维码在有损情况下仍然可读。
QRCode库的主要功能包括:
- 生成二维码:可以生成不同大小、颜色和格式的二维码,支持自定义错误校验和纠正。
- 解析二维码:可以解析已有的二维码并获取其中的信息。
- 自定义样式:可以自定义二维码的样式,如颜色、背景图片等。
- 支持多种编程语言:支持多种编程语言,如Java、Python、C++等。
QRCode库的使用非常简单,只需要导入库并调用相应的API即可。由于其开源的特性,用户也可以根据自己的需要对其进行二次开发。
导入相关的依赖:
<!-- 生成二维码-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
然后创建相关的测试代码
static final String BASEPATH = "D://weixin";
public static void generateQRCodeImage(String text, int width, int height, String fileName)
throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height);
File file = new File(BASEPATH);
if(!file.exists()){
file.mkdir();
}
Path path = FileSystems.getDefault().getPath(BASEPATH+"/"+fileName);
MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
}
public static void main(String[] args) {
try {
generateQRCodeImage("weixin://wxpay/bizpayurl?pr=YC9gpgMzz", 350, 350, "QRTest.png");
} catch (WriterException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
在对应的controller中调用生成即可
@ResponseBody
@PostMapping("/native/{productId}")
public Map<String,Object> nativePay(@PathVariable Long productId) throws Exception{
Map<String, Object> map = wxPayService.nativePay(productId);
// 把文件存储在 D://weixin/orderId.png
WxPayConfig.generateQRCodeImage(map.get("codeUrl").toString(),350, 350,map.get("orderNo")+".png");
return map;
}
我们先提供一个下载二维码图片的功能
@GetMapping("/download")
public void downloadImg(String fileName, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 加载需要下载的文件
InputStream in = new FileInputStream(WxPayConfig.BASE_PATH+"/"+fileName);
int size = in.available();
byte[] data = new byte[size];
in.read(data);
in.close();
// 把读取的数据响应给客户端
response.setContentType("image/jpg");
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(data);
outputStream.flush();
}
然后在页面中点击支付弹出对应的支付二维码信息
function goPay(){
$.post("/api/wx-pay/native/1001",function(data) {
console.log(data.orderNo);
$("#payImg").attr("src","/api/wx-pay/download?fileName="+data.orderNo+".png")
$(".mask").show();
$("body").css({overflow: "hidden"});
// 检查是否已经存在倒计时定时器,避免重复启动
if (!countdownInterval) {
startCountdown();
}
})
}
具体的效果如下:
支付成功的截图
还有重复支付的截图
7.签名生成和验签
7.1 签名生成
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
上面的生成订单展示对应的支付二维码是如下的完整的流程
那么在这个流程中设计到了签名的生成和服务端的签名验证:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
签名的流程:
- 构造签名串:签名是密文,那么这个签名串就是这个密文对应的明文
- 计算签名值:对签名串加密,通过特定的规则加密
- 将签名发送给微信服务器:通过在http的请求头中传递
源码层面的逻辑:SignatureExec中的executeWithSignature方法中
然后进入到getToken中
@Override
public final String getToken(HttpRequestWrapper request) throws IOException {
String nonceStr = generateNonceStr(); // 获取随机值
long timestamp = generateTimestamp(); // 获取时间戳
String message = buildMessage(nonceStr, timestamp, request); // 构建签名串
log.debug("authorization message=[{}]", message);
// 获取签名信息
Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));
// 组装签名信息
String token = "mchid=\"" + getMerchantId() + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + signature.certificateSerialNumber + "\","
+ "signature=\"" + signature.sign + "\"";
log.debug("authorization token=[{}]", token);
return token;
}
7.2 签名验证
商户可以按照下述步骤验证应答或者回调的签名。
如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。
同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。
获取平台证书:
微信支付API v3使用微信支付 的平台私钥(不是商户私钥 )进行应答签名。相应的,商户的技术人员应使用微信支付平台证书中的公钥验签。目前平台证书只提供API进行下载
Get方法:https://api.mch.weixin.qq.com/v3/certificates
在具体的代码中。我们在系统启动的时候需要加载微信的证书列表
设置更新的频率是60分钟
处理的核心代码
签名验证
做超时时间处理
验证签名的逻辑
对 Wechatpay-Signature
的字段值使用Base64进行解码,得到应答签名
8.内网穿透
在用户支付完成后。微信服务的会调用我们本地服务来做支付的通知。这时就需要让我们本地的服务可以被微信的服务端访问到。这时需要利用内网穿透的方式来解决。 http://ngrok.com .下载对应操作系统的工具
然后下载对应的客户端段。解压缩后在对应的目录下打开cmd窗口。
然后我们利用对应的地址访问即可
9.支付通知
用户支付成功后。微信服务器会回调我们在发起支付的时候传递的回调通知的地址
Map<String,Object> map = new HashMap<>();
map.put("appid",wxPayConfig.getAppid());
map.put("mchid",wxPayConfig.getMchId());
map.put("description",orderInfo.getTitle());
map.put("out_trade_no",orderInfo.getOrderNo());
map.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
那么我们需要修改在配置文件中的通知地址为我们内网穿透设置的地址
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://c65f-183-215-31-250.ngrok-free.app
然后就可以定义对应的回调接口了
/**
* 微信服务的通知支付结果的方法
* @param request
* @param response
* @return
*/
@ResponseBody
@PostMapping("/native/notify")
public String nativePayNotify(HttpServletRequest request,HttpServletResponse response){
// 1.获取微信服务器发送的通知内容 读取的内容是一个JSON格式的字符串
String body = HttpUtils.readData(request);
Gson gson = new Gson();
Map<String,Object> bodyJson = gson.fromJson(body,HashMap.class);
System.out.println("响应数据的ID===》"+bodyJson.get("id"));
System.out.println("响应的JSON数据===》"+body);
// 1.1 签名验证
// 1.2 更新订单信息
// 2.响应成功应答给微信服务器
response.setStatus(200);
Map<String,String> map = new HashMap<>();
map.put("code","SUCCESS");
map.put("message","成功");
return gson.toJson(map);
}
然后当我们支付成功后。就可以看到下面的打印信息了。说明接口实现是成功。
响应数据的ID===》379de717-0f49-5f76-8fda-fd91688642e5
响应的JSON数据===》{"id":"379de717-0f49-5f76-8fda-fd91688642e5","create_time":"2023-06-27T20:43:44+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"t96vhF2Z/VptyJ8m+3PUtxp5RQcDcqezxgV/K/Ik4qJtFjO80TVDDbsrGaCCmJKbN9eV997vgkxOhjeDPt4SN09JPAiK7cQitlcvY9BsSMvji+fFoigbP0sae/4dSccW12g/DCXA3MMgPItRuy2Tq84rvgPcGF/fSZ6ZFDEQ4lLQpAO3Dk7RPYFeDJEhYRtC9FFjuM4J2voSg8O3Rg+VarHne0Hou14JZoPep0Q1Zdi3j34kULHPNIHLLrxf7bWUgtM0aCv2fjscqPaIH9CVB8bih9l30xjxrlIbolziuwY8bo45oT/N6z9mM5Dxb6glga3AQOrJkFKmKOYSigHo/jqRumpnCD024wlfBYtDRrJh8/n3gVlf1JguLzWcA5Ed9EAsShCxBt9Zb4/DqQt1LaFU9bivNV1T4Jpi2+LG55zNR/0fygG5Xi/MLAcjwdt7TGfqjeHStKZnTDkw7pcrri/svKGooZ2rJCr4APmd27OO/TX6P3ks1GmzRoaJI4WL85VaiUDrix889ngRjNetoU4FolaCrulg+TxHKX+AlBDvO2ajTFhIafd7MOZ4XaFDMEXczFn5cg==","associated_data":"transaction","nonce":"mrOQHEfn99Qw"}}
响应数据的ID===》af5afe61-af37-508d-9e04-5d27094ac30d
响应的JSON数据===》{"id":"af5afe61-af37-508d-9e04-5d27094ac30d","create_time":"2023-06-27T20:44:57+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"FeE/+/rD0SqNKXTCMLMoWnms0deI6VnQzm15N2jUzvJoJ4RN5DvOiXpoqEX5XQ2+o+jd5ho/h4EZvsWP4Cb+x9/6MbkRu5IVVRMbNXI+teMpv3yMi++vqoYwutwYc7K31bv6AeofH921U0kzBdFpOO9YtSogSaVHA6UlXxleyKdmXulSH1e2QzC9vAzzh/pxWgFWfu3+bJ/MmucWrVMmpCb0iWKBgUMU8V1HFTcyeofm9GDoMk1SjffiA+USL3jSxxql6pP+4j9DfcpShmj27qROO8c/Tzcgwi+Oy3jdPjWnvkHHHk2oozcLzqrc7cWHxmcXfvfvQ56RqEXQL5wJZHZe3kteL7TH/bZonnS7ZPrYLHGeQCy5N1zRX/6wH/jAl7y2IwAocIVSZgsrgTOIElM9UncqpxYZn1pL0fywEkmHHNgqHVphyuMtPI3PBkKSiHFl8jlT2WbTLe8eTHIL+6Lsp6h/KvzGjkdKJaR0MZRMUxSwLun+99u+zDAnKsgKpJV41XwXZc9k1MD0L8c2nMTI+9Z4OYwZ7T+m8V6Gf+rIe76mLtJNw1QFhTmyVFKDlZYfIHVdtw==","associated_data":"transaction","nonce":"rvBWUUlQaVLh"}}