后端源代码
从git上拉取
https://e.coding.net/dongjiming5/weixinzhifu/zbr_weixinzhifu.git
前端代码
https://e.coding.net/dongjiming5/weixinzhifu/zbr_weizinzhifu_front.git
教程所用资料
尚硅谷在线支付实战教程
B站直达:https://www.bilibili.com/video/BV1US4y1D77m
百度网盘:https://pan.baidu.com/s/1HlViJcMZBVID8pRNd7aRHQ 提取码:yyds
阿里云盘:https://www.aliyundrive.com/s/Vz4ZtCtPLSz(因阿里云盘暂不支持压缩包分享,视频之外的资料请从百度网盘下载)
围观尚硅谷Java课程:http://www.atguigu.com/kecheng.shtml
更多尚硅谷视频教程请访问:http://www.atguigu.com/download.shtml
项目内容
创建spring-boot项目并初始化
脚手架用阿里云
首先打开pom文件把starter改成starter-web
不要忘记刷新maven
接下来我把配置文件改成yml
配置一下基本信息代码很简单就直接截图了
创建商品测试接口
启动应用在postman访问一下
测试成功
引入swagger
<!-- Swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- SwaggerUI依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
为其设置配置文件
package com.atguigu.paymentdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean//返回文档对象Docket
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2);
}
}
package com.atguigu.paymentdemo.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//返回文档对象Docket
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder().title("微信支付案例接口文档").build());
}
}
如图所示设置成功
为控制层添加文档属性
定义统一结果
创建软件包vo和类R
引入工具lombok简化实体类的开发自动生成get,set之类的方法
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
接下来完善统一类
package com.atguigu.paymentdemo.vo;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Data
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;
}
}
在测试接口修改测试一下
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.vo.R;
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;
import java.util.Date;
@Api(tags = "商品管理")
@RestController
@RequestMapping("/api/product")
public class ProductController {
@ApiOperation("测试接口")
@GetMapping("/test")
public R test(){
return R.ok().data("message","hello").data("now",new Date());
}
}
我们发现时间格式有问题所以我们在配置文件中对json时间格式进行定义
可以这样写但是我换了个写法
创建数据库
连接数据库
接下来执行sql脚本
集成mybatis-plus
引入依赖
<!-- mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
配置数据库连接
server:
port: 8090 #服务端口
spring:
application:
name: payment-demo #应用名字
jackson:
date-format: java.text.SimpleDateFormat
time-zone: GMT+8
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/payment_demo?
serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: root
把实体类和服务层的包粘过去
在配置文件中配置mapper扫描
package com.atguigu.paymentdemo.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@MapperScan("com.atguigu.paymentdemo.mapper")
@EnableTransactionManagement//启用事务管理
public class MyBatisPlusConfig {
}
接下来定义一个接口方法获取商品列表list
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.entity.Product;
import com.atguigu.paymentdemo.service.ProductService;
import com.atguigu.paymentdemo.vo.R;
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 java.util.Date;
import java.util.List;
@Api(tags = "商品管理")
@RestController
@RequestMapping("/api/product")
public class ProductController {
@Autowired
private ProductService productService;
@ApiOperation("测试接口")
@GetMapping("/test")
public R test(){
return R.ok().data("message","hello").data("now",new Date());
}
@GetMapping("/list")
public R list(){
List<Product> list = productService.list();
return R.ok().data("productList",list);
}
}
启动服务器
访问接口访问成功
关于target的目录中为什么没有xml文件
这是因为maven对于java目录下的非java文件不会进行编译操作目前我们的增删改查用不到复杂的xml文件,但是如果以后写复杂sql会爆出配置文件无法找到的问题,为了解决这个问题可以在pom文件中的build配置资源发布选项
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
这样上传资源文件打包的时候文件就不会被过滤掉
接下来执行clean删除target目录然后重新启动编译程序发现xml文件存在
接下来还需要让应用程序在运行时可以找到xml文件添加配置项顺便配置一下sql日志
mybatis-plus:
configuration: #sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:com/atguigu/paymentdemo/mapper/xml/*.xml
运行sql日志也重新被打印
前端
准备
node.js
我之前已经装过了
查看版本号验证安装
node -v
接下来运行前端项目
在前端文件中打开cmd输入
npm run serve
启动成功访问一下
前端和后端的接口不同属于跨域在后端接口位置开放跨域如图所示添加一个注解
刷新之后便有了课程列表
基础支付
引入支付参数
在资源目录下新建wxpay.proterties
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://500c-219-143-130-12.ngrok.io
# APIv2密钥
wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
接下来创建配置文件读取这些信息
package com.atguigu.paymentdemo.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") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
}
接下来新建测试接口
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.vo.R;
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 javax.annotation.Resource;
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {
@Resource
private WxPayConfig wxPayConfig;
@GetMapping
public R getWxPayConfig(){
String mchId =wxPayConfig.getMchId();
return R.ok().data("mchId",mchId);
}
}
接下来对配置文件进行优化
点击文件项目结构
点击模块spinrg
添加配置文件
然后添加一个注解自动处理器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
加载商户私钥
首先拷贝私钥文件
把这个文件粘贴在项目的根目录下
然后读取私钥文件
进入微信支付开发者文档
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.8</version>
</dependency>
商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem中。商户开发者可以使用方法PemUtil.loadPrivateKey()加载证书。
# 示例:私钥存储在文件
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream("/path/to/apiclient_key.pem"));
# 示例:私钥为String字符串
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
在WxPayConfig中创建一个获取私钥的方法
package com.atguigu.paymentdemo.config;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.security.PrivateKey;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
//@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@ConfigurationProperties(prefix = "wxpay")
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
public PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在",e);
}
}
}
注意这里方法设置成公有是为了测试需要测试完要改成私有
在测试类中进行测试
package com.atguigu.paymentdemo;
import com.atguigu.paymentdemo.config.WxPayConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.security.PrivateKey;
@SpringBootTest
class PaymentDemoApplicationTests {
@Resource
private WxPayConfig wxPayConfig;
@Test
void contextLoads() {
}
@Test
void testGetPrivateKey(){
//获取私钥路径
String privateKeyPath = wxPayConfig.getPrivateKeyPath();
//获取私钥
PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
System.out.println(privateKey);
}
}
测试通过
接下来就要把测试的私钥的方法改成私有,这个一定不要忘了!
获取验签器和HttpClient
为了防止证书过期需要实现定时更新平台证书的功能
// 获取证书管理器实例
certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId,
new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(merchantId);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
// 后面跟使用Apache HttpClient一样
CloseableHttpResponse response = httpClient.execute(...);
在WxPayConfig中实现这个方法与尚硅谷的教程中不同官方文档已经改版这里是我自己封装的代码还不知道之后能不能用
package com.atguigu.paymentdemo.config;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.SneakyThrows;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
//@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@ConfigurationProperties(prefix = "wxpay")
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在",e);
}
}
/**
* 获取签名验证器
* @return
*/
@SneakyThrows
@Bean //不希望程序执行多次就添加Bean注解让程序启动执行一次就好了
public Verifier getVerifier(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
//对称加密密钥
byte[] bytes = apiV3Key.getBytes(StandardCharsets.UTF_8);
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(mchId,wechatPay2Credentials, bytes);
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(mchId);
return verifier;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean
public CloseableHttpClient getWxPayClient(Verifier verifier){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
// 从证书管理器中获取verifier
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
}
apiv3 api字典和相关工具
添加json处理工具
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
我们注意到官方文档订单的接口都是固定的这里可以将他们写成枚举类
这里我直接把素材的枚举包粘贴到项目中
然后引入工具类直接粘
native下单
native支付流程
定义接口
首先为微信支付创建三层服务
控制层这里控制层我还有一个实现方法没有定义
这里我引入了SLF4J日志来打印信息
还要为r加一个注解
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
@CrossOrigin//跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付api")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@ApiOperation("调用统一下单API ,生成支付二维码")
public R nativePay(@PathVariable Long productId){
log.info("发起支付请求");
//返回支付二维码连接和订单号
Map<String,Object> map=wxPayService.nativePay(productId);
return R.ok().setData(map);
}
}
service层
package com.atguigu.paymentdemo.service;
public interface WxPayService {
}
实现类
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.service.WxPayService;
import org.springframework.stereotype.Service;
@Service
public class WxPayServiceImpl implements WxPayService {
}
接下来将会用实例完善这三个代码
创建临时订单
这节就写了这么多详细代码我最后再放建议去看一下工具类
组装接口参数并发送请求
看一下下单代码
public void CreateOrder() throws Exception{
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
// 请求body参数
String reqdata = "{"
+ "\"time_expire\":\"2018-06-08T10:34:56+08:00\","
+ "\"amount\": {"
+ "\"total\":100,"
+ "\"currency\":\"CNY\""
+ "},"
+ "\"mchid\":\"1230000109\","
+ "\"description\":\"Image形象店-深圳腾大-QQ公仔\","
+ "\"notify_url\":\"https://www.weixin.qq.com/wxpay/pay.php\","
+ "\"out_trade_no\":\"1217752501201407033233368018\","
+ "\"goods_tag\":\"WXG\","
+ "\"appid\":\"wxd678efh567hg6787\","
+ "\"attach\":\"自定义数据说明\","
+ "\"detail\": {"
+ "\"invoice_id\":\"wx123\","
+ "\"goods_detail\": ["
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "},"
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "}"
+ "],"
+ "\"cost_price\":608800"
+ "},"
+ "\"scene_info\": {"
+ "\"store_info\": {"
+ "\"address\":\"广东省深圳市南山区科技中一道10000号\","
+ "\"area_code\":\"440305\","
+ "\"name\":\"腾讯大厦分店\","
+ "\"id\":\"0001\""
+ "},"
+ "\"device_id\":\"013467007045764\","
+ "\"payer_client_ip\":\"14.23.150.211\""
+ "}"
+ "}";
StringEntity entity = new StringEntity(reqdata,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
把这段代码粘贴到刚才写的实现
当然在粘贴的时候不能把外围的public粘贴进来接下来需要该一些数据然后需要抛出一些异常
这里会抛出io异常我们把这个接口的异常改为Expection从下到上全部更改这部分url用之前写的枚举类进行替代
这里的gettype其实就是在枚举类中定义的string字符串
接下来我们就需要些json字符串的组装了
这种直接封装的方式是一定要换掉的
在这里我们引用之前引入的json依赖对应着下单表进行添加当然有些是非必要添加的我们可以暂时不用添加
简单看一下封装json过程完整代码我会最后粘贴出来
接下来为了解决爆红直接注入之前封装好的bean对象
接下来看一下这部分的代码因为还没有全部完成还是有报错的
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
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.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Override
public Map<String,Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle("test");
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//订单号
orderInfo.setProductId(productId);
orderInfo.setTotalFee(1);//1分
log.info("调用统一下单api");
//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);
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) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
}
处理结果并展示支付二维码
首先把输出语句打印为日志
之后根据返回结果封装参数这里看一下完整代码没有报错的
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
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.stereotype.Service;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
/**
*创建订单调用Native支付接口
* @param productId
* @return code_url和订单号
* @throws Exception
*/
@Override
public Map<String,Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle("test");
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//订单号
orderInfo.setProductId(productId);
orderInfo.setTotalFee(1);//1分
log.info("调用统一下单api");
//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);
log.info("请求参数:"+jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response =wxPayClient.execute(httpPost);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
//响应结果
HashMap<String,String> resultMap=gson.fromJson(bodyAsString,HashMap.class);
//二维码
String codeUrl=resultMap.get("code_url");
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
}
接下来还要回到控制层抛出异常
在这类再次看一下控制层代码
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@CrossOrigin//跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付api")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求");
//返回支付二维码连接和订单号
Map<String,Object> map=wxPayService.nativePay(productId);
return R.ok().setData(map);
}
}
接下来启动程序测试一下
因为我不知道postman怎么控制路径传参所以productId我这里直接设置成了1
启动前段
点击v3扫码后可以支付
二维码在vue中展示
需要引入这个依赖
之后可以直接用html标签的方式来显示二维码
弹出二维码后一直报错是因为还有一个判断用户是否扫码的接口没有写
可以暂时屏蔽这个定时器
签名
签名原理实现流程分析
日志info级别的输出
内容是非常少的所以这里我们更改日志级别
首先是构造签名串
我们启动项目点击下单会弹出二维码在这类我们观察后台输出面板的变化
这两张图片是之前封装好的微信sdk中的身份验证的类所以首先是进行身份验证
这里其实也就是微信文档种所提到的签名串也就是第一步构造签名串
计算签名值
应该是这一串东西
设置http头
签名原理源码分析
生成订单
存入数据库
先把日志级别改成info
然后编写生成订单的实体类因为之前下单的时候已经写过生成订单的实体类所以我们这里可以封装一下
这里我只放一下我写的订单实体类接口只需要把方法拉过去就好了
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.entity.Product;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.mapper.OrderInfoMapper;
import com.atguigu.paymentdemo.mapper.ProductMapper;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
@Override
public OrderInfo createOrderByProductId(Long productId) {
//获取商品信息
Product product=productMapper.selectById(productId);
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle(product.getTitle());
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//订单号
orderInfo.setProductId(productId);
orderInfo.setTotalFee(product.getPrice());//1分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//存入数据库
baseMapper.insert(orderInfo);
return orderInfo;
}
}
接下来我们可以修改wxpayserviceimpl
把我们新写的添加订单注入进去
把这一块给替换掉
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
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.stereotype.Service;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
OrderInfoService orderInfoService;
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
/**
*创建订单调用Native支付接口
* @param productId
* @return code_url和订单号
* @throws Exception
*/
@Override
public Map<String,Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
log.info("调用统一下单api");
//调用一下订单api
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("appid",wxPayConfig.getAppid());
paramsMap.put("mchid",wxPayConfig.getMchId());
paramsMap.put("description",orderInfo.getTitle());
paramsMap.put("out_trade_no",orderInfo.getOrderNo());
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//结果通知地址之类采用拼接
//订单金额
Map amountMap = new HashMap();
amountMap.put("total",orderInfo.getTotalFee());
amountMap.put("currency","CNY");
paramsMap.put("amount",amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数:"+jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response =wxPayClient.execute(httpPost);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
//响应结果
HashMap<String,String> resultMap=gson.fromJson(bodyAsString,HashMap.class);
//二维码
String codeUrl=resultMap.get("code_url");
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
}
接下来测试一下启动应用程序重新点击确认支付可以发现数据库中的订单被添加了
但是这里还是存在一个问题如果在没有支付的情况下用户再次支付相同的产品数据库中的订单会一直增加我们不希望看到这个情况我们希望用户完成这个订单之后才能继续支付相同产品的订单为此来减少系统压力接下来我们会对这个业务进行优化
获取已存在订单
首先我们需要在下单前查找已经存在但是没有支付的订单
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.entity.Product;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.mapper.OrderInfoMapper;
import com.atguigu.paymentdemo.mapper.ProductMapper;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
@Service
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());//1分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//存入数据库
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 根据商品od查询未支付订单
* 防止重复创建订单对象
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId){
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id",productId);
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
}
接下来重启测试一下多次点击支付数据库中的订单号并没有增加
存储二维码地址
虽然已经可以获取已存在订单但是每次下单都会调用下单api
微信端对于code_url的有效期为两个小时所以我们可以以设置存储二维码地址
首先写存储二维码的实现类传入二维码和订单号,根据订单号存储二维码这里应该是先下单生成订单号获取二维码然后再存储
在这里我们看一下这段代码的引用位置
在此之前我们需要对二维码进行一个非空判断已存在并且没有支付的订单会被直接返回而不是重新创建相同的订单对于返回的订单的订单号和订单二维码进行一个判断第一次的下单后二维码会被保存所以二次下单二维码不会为空所以可以直接返回二维码而不是再次调用下单接口这样做当然有一个坏处就是二维码过期了会怎么办,这点教程并没有解决这个问题
接下来看一下完整相关代码
OrderInfoServiceImpl
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.entity.Product;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.mapper.OrderInfoMapper;
import com.atguigu.paymentdemo.mapper.ProductMapper;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
@Service
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());//1分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//存入数据库
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 存储订单二维码
* @param orderNo
* @param codeUrl
*/
@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);
}
/**
* 根据商品od查询未支付订单
* 防止重复创建订单对象
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId){
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id",productId);
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
}
WxPayServiceImple
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
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.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
OrderInfoService orderInfoService;
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
/**
*创建订单调用Native支付接口
* @param productId
* @return code_url和订单号
* @throws Exception
*/
@Override
public Map<String,Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
String codeUrl = orderInfo.getCodeUrl();
if (orderInfo!=null && !StringUtils.isEmpty(codeUrl)){
log.info("二维码已保存");
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单api");
//调用一下订单api
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("appid",wxPayConfig.getAppid());
paramsMap.put("mchid",wxPayConfig.getMchId());
paramsMap.put("description",orderInfo.getTitle());
paramsMap.put("out_trade_no",orderInfo.getOrderNo());
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//结果通知地址之类采用拼接
//订单金额
Map amountMap = new HashMap();
amountMap.put("total",orderInfo.getTotalFee());
amountMap.put("currency","CNY");
paramsMap.put("amount",amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数:"+jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response =wxPayClient.execute(httpPost);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
//响应结果
HashMap<String,String> resultMap=gson.fromJson(bodyAsString,HashMap.class);
//二维码
codeUrl=resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo,codeUrl);
//返回二维码
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
}
显示订单列表
这里是根据创造的时间顺序进行查询的
控制层
=package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@CrossOrigin//开放前端的跨域访问
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@GetMapping("/list")
@ApiOperation("商品订单列表")
public R list(){
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list",list);
}
}
实现类
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.entity.Product;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.mapper.OrderInfoMapper;
import com.atguigu.paymentdemo.mapper.ProductMapper;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import java.util.List;
@Service
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());//1分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//存入数据库
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 存储订单二维码
* @param orderNo
* @param codeUrl
*/
@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);
}
@Override
public List<OrderInfo> listOrderByCreateTimeDesc() {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
//组装倒叙排序条件
queryWrapper.orderByDesc("create_time");
return baseMapper.selectList(queryWrapper);
}
/**
* 根据商品od查询未支付订单
* 防止重复创建订单对象
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId){
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id",productId);
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
}
内网穿透
https://dashboard.ngrok.com/get-started/your-authtoken
微信的服务器端向商户系统发送请求,所以需要进行内网穿透让服务器从外网访问到内网这里下载一个工具
访问网址进行账号注册在这里我用自己的谷歌账户登录
首先为计算机做授权
ngrok config add-authtoken 2GZfyNsZx8Q3aRAgxfjlhihebsO_58ydeA2iRxBwpS2dsFdTm
响应端口号8090
ngrok http 8090
执行成功后为我们提供了两个访问地址
接下来将内网地址换成外网地址访问一下
测试成功
但是这个地址是不稳定的每次启动都会发生变化
把这个位置改掉就能从外网访问了
支付通知
接受通知返回应答
可以看一下支付通知的文档说明‘
’
这里要求用post请求我们再来看一下参数
剩下的我就不截图了自己慢慢看
接下来看一下实现过程这里并没有调用实现类而是在控制层进行
上图所示的postmaping中的url是之前下单的时候设置的如果用户支付成功微信服务器会像我们指定的这个通知url发送请求如下图所示在发送的时候我采用的是字符串拼接的形式
接下来就是拼装返回体用map封装为json格式返回给微信服务端并且要设置状态码
这里我们看一下完整的源代码
@PostMapping("
/native/notify
")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
Gson gson = new Gson();
//创建应答对象
HashMap<String, String> map = new HashMap<>();
//处理通知参数
String body = HttpUtils.readData(request);
HashMap<String,Object> bodyMap = gson.fromJson(body,HashMap.class);
log.info("支付通知的id ===> {}" + bodyMap.get("id"));
log.info("支付通知的完整数据 ===> {}" + body);
Object id = bodyMap.get("id");
//TODO:签名的验证
//TODO:订单处理
//应答对象
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
//成功应答
return gson.toJson(map);
}
来看一下里面用到的将request数据转化为string字符串的工具类
package com.atguigu.paymentdemo.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();
}
}
}
}
}
扫描二维码并支付查看日志
可以看到支付通知方法已经被执行
应答异常和应答超时
把200改成201之类的错误的
因为错误会一直发通知
应答超时也会被一直发通知
验签
对微信发送的通知也进行签名验证
首先我们需要先找到之前请求下单的时候的签名验证的工具类
我们把这个类粘贴在util工具类中并且重命名
添加请求体构造参数是因为请求id被包含在请求体中是通知唯一标识
接下来进行签名验证如果不通过进行失败应答这里我没有传入body是因为案例这个地方也还没有传入但是个uitils粘的是最终版本所以这里会报红但是不影响
接下来把构造响应体也换成request
完善一下构造函数获取body好像是buffer流会关闭所以要这样写这里我也听不懂
这样body就可以直接利用成员来构建了
重写后的validate
看一下完整代码
package com.atguigu.paymentdemo.util;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
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) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
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";
}
}
控制层
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.HttpUtils;
import com.atguigu.paymentdemo.util.WechatPay2ValidatorForRequest;
import com.atguigu.paymentdemo.vo.R;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@CrossOrigin//跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付api")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@Resource
private Verifier verifier;
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求");
//返回支付二维码连接和订单号
Map<String,Object> map=wxPayService.nativePay(productId);
return R.ok().setData(map);
}
@SneakyThrows
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
Gson gson = new Gson();
//创建应答对象
HashMap<String, String> map = new HashMap<>();
//处理通知参数
String body = HttpUtils.readData(request);
HashMap<String,Object> bodyMap = gson.fromJson(body,HashMap.class);
log.info("支付通知的id ===> {}" + bodyMap.get("id"));
log.info("支付通知的完整数据 ===> {}" + body);
String requestId = (String) bodyMap.get("id");
//TODO:签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier,requestId,body);
//如果验签不通过进行失败应答
if (!wechatPay2ValidatorForRequest.validate(request)){
log.info("通知验签失败");
response.setStatus(500);
map.put("code","ERROR");
map.put("message","失败");
return gson.toJson(map);
}
//TODO:订单处理
log.info("通知验签成功");
//应答对象
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
//成功应答
return gson.toJson(map);
}
}
报文解密
用这些参数进行响应的对称解密
解密过程
这是得到的resource数据
主要是解密数据密文
这是官方给出的解密后的案例
自定义参数原样传出原样返回
即使附加数据没有值解密运算也需要用到依然需要传
接下来我们需要用到解密工具如图所示点击
那么解密的时候我们需要调用这个工具类首先是在控制层订单处理接受通知参数的那个位置定义一个实现类的方法订单处理在其中创建一个私有方法用来解密这是因为我们订单通知返回的数据中包含密文
因为订单解密会被多次使用所以这里我们把他封装起来
解密需要传参接下来我们根据官方文档传递参数
这里的key指的是apiv3的那个key这个key是需要自己设置的不是随机生成的
然后我们查看函数构造类
这里就说明了key是由构造类传入的然后就是获取其余的三个数据分别是两个解密需要的参数和密文
统统获取成字符串形式接下来调用解密函数
查看源代码我们可以清楚地看到需要传入的类型
接下来就是实现类调用这个函数了
接下来看一下相关源代码和效果
控制层就多了一个对实现类的引用
package com.atguigu.paymentdemo.controller;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.HttpUtils;
import com.atguigu.paymentdemo.util.WechatPay2ValidatorForRequest;
import com.atguigu.paymentdemo.vo.R;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@CrossOrigin//跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付api")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@Resource
private Verifier verifier;
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求");
//返回支付二维码连接和订单号
Map<String,Object> map=wxPayService.nativePay(productId);
return R.ok().setData(map);
}
@SneakyThrows
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
Gson gson = new Gson();
//创建应答对象
HashMap<String, String> map = new HashMap<>();
//处理通知参数
String body = HttpUtils.readData(request);
HashMap<String,Object> bodyMap = gson.fromJson(body,HashMap.class);
log.info("支付通知的id ===> {}" + bodyMap.get("id"));
log.info("支付通知的完整数据 ===> {}" + body);
String requestId = (String) bodyMap.get("id");
//TODO:签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier,requestId,body);
//如果验签不通过进行失败应答
if (!wechatPay2ValidatorForRequest.validate(request)){
log.info("通知验签失败");
response.setStatus(500);
map.put("code","ERROR");
map.put("message","失败");
return gson.toJson(map);
}
//TODO:订单处理
log.info("通知验签成功");
wxPayService.processOrder(bodyMap);
//应答对象
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
//成功应答
return gson.toJson(map);
}
}
实现类以及封装的解密函数
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.SneakyThrows;
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.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
OrderInfoService orderInfoService;
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
/**
*创建订单调用Native支付接口
* @param productId
* @return code_url和订单号
* @throws Exception
*/
@Override
public Map<String,Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
String codeUrl = orderInfo.getCodeUrl();
if (orderInfo!=null && !StringUtils.isEmpty(codeUrl)){
log.info("二维码已保存");
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单api");
//调用一下订单api
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("appid",wxPayConfig.getAppid());
paramsMap.put("mchid",wxPayConfig.getMchId());
paramsMap.put("description",orderInfo.getTitle());
paramsMap.put("out_trade_no",orderInfo.getOrderNo());
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//结果通知地址之类采用拼接
//订单金额
Map amountMap = new HashMap();
amountMap.put("total",orderInfo.getTotalFee());
amountMap.put("currency","CNY");
paramsMap.put("amount",amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数:"+jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response =wxPayClient.execute(httpPost);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
//响应结果
HashMap<String,String> resultMap=gson.fromJson(bodyAsString,HashMap.class);
//二维码
codeUrl=resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo,codeUrl);
//返回二维码
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
@Override
public void processOrder(HashMap<String, Object> bodyMap) {
log.info("处理订单");
//因为解密过程后面也会弄到这里我们把他抽取成一个方法
String planText =decryptFromResource(bodyMap);
}
@SneakyThrows
private String decryptFromResource(HashMap<String, Object> bodyMap) {
log.info("密文解密");
//这里需要传入一个apiv3key的类型为bite的参数
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
//运用指定解密算法解密
//通知数据
Map<String,String> resourceMap = (Map<String, String>) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
log.info("密文===》{}",ciphertext);
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
String associatedData = resourceMap.get("associated_data");
String plainText=aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info("明文===>{}",plainText);
log.info("密文===》{}",ciphertext);
return plainText;
}
}
看一下后台效果
这里我打印了两遍密文
更新订单状态记录支付日志
报文解密之后需要将明文转化为map
处理订单状态和记录支付日志我们在之前WxpayServerImpl中的处理订单进行之前在处理订单中已经对返回的通知密文进行解密码。接下来我们把这串明文转化为map
并且构造两个实现类的方法更新订单装态就是根据返回的订单号更新
记录订单日志就是把返回的信息再次封装为日志类
这里需要注意的是amount返回的不是字符串而是一个map集合总价格需要先转换为double类型再转换为Integer
看一下相关源代码
WxPayServiceImpl
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.service.PaymentInfoService;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.SneakyThrows;
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.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
OrderInfoService orderInfoService;
@Resource
private WxPayConfig wxPayConfig;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private CloseableHttpClient wxPayClient;
/**
*创建订单调用Native支付接口
* @param productId
* @return code_url和订单号
* @throws Exception
*/
@Override
public Map<String,Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
String codeUrl = orderInfo.getCodeUrl();
if (orderInfo!=null && !StringUtils.isEmpty(codeUrl)){
log.info("二维码已保存");
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单api");
//调用一下订单api
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("appid",wxPayConfig.getAppid());
paramsMap.put("mchid",wxPayConfig.getMchId());
paramsMap.put("description",orderInfo.getTitle());
paramsMap.put("out_trade_no",orderInfo.getOrderNo());
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//结果通知地址之类采用拼接
//订单金额
Map amountMap = new HashMap();
amountMap.put("total",orderInfo.getTotalFee());
amountMap.put("currency","CNY");
paramsMap.put("amount",amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数:"+jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response =wxPayClient.execute(httpPost);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
//响应结果
HashMap<String,String> resultMap=gson.fromJson(bodyAsString,HashMap.class);
//二维码
codeUrl=resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo,codeUrl);
//返回二维码
HashMap<String, Object> map = new HashMap<>();
map.put("codeUrl",codeUrl);
map.put("orderNo",orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
@Override
public void processOrder(HashMap<String, Object> bodyMap) {
log.info("处理订单");
//因为解密过程后面也会弄到这里我们把他抽取成一个方法
String planText =decryptFromResource(bodyMap);
//将明文转换为map
Gson gson = new Gson();
HashMap plantTextMap = gson.fromJson(planText,HashMap.class);
//拿到订单号
String orderNo = (String) plantTextMap.get(out_trade_no");
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,
OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(planText);
}
@SneakyThrows
private String decryptFromResource(HashMap<String, Object> bodyMap) {
log.info("密文解密");
//这里需要传入一个apiv3key的类型为bite的参数
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
//运用指定解密算法解密
//通知数据
Map<String,String> resourceMap = (Map<String, String>) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
log.info("密文===》{}",ciphertext);
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
String associatedData = resourceMap.get("associated_data");
String plainText=aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info("明文===>{}",plainText);
log.info("密文===》{}",ciphertext);
return plainText;
}
}
PaymentInfoServiceImpl.java
package com.atguigu.paymentdemo.service.impl;
import com.atguigu.paymentdemo.entity.PaymentInfo;
import com.atguigu.paymentdemo.enums.PayType;
import com.atguigu.paymentdemo.mapper.PaymentInfoMapper;
import com.atguigu.paymentdemo.service.PaymentInfoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
@Override
public void createPaymentInfo(String plainText) {
log.info("记录支付日志");
Gson gson = new Gson();
HashMap 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);
}
}
OrderInfoServiceImpl.java
/**
* 根据订单号更新订单状态
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("更新订单状态===>",orderStatus.getType());
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
baseMapper.update(orderInfo,queryWrapper);
}
记录成功
状态更新成功
处理重复通知和接口调用的幂等性
设置应答超时
在微信支付控制层设置一个超时
如果设置应答超时微信服务器会重复发送通知本地服务器也会重复的记录订单这个显然是不合理的
首先在订单处理中获取订单状态如果不是未支付就会对其进行处理
获取订单状态的服务层OrderInfoServiceImpl.java
@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();
}
如果不是未支付说明之前已经处理过订单了这里不需要二次处理所以直接结束方法即可
接下来就算超时微信服务器多次向本地服务器发送请求也不会多次存储支付订单了
数据锁
虽然我们之前对重复返回通知的支付状态进行了判断避免数据库一直存储信息但是如果有两个相同通知同时到达的话还是会被同时存储这里我来模拟一下
按照教程来的我也不知道为什么睡眠可以模拟并发
然后我也没有出现这种情况接下来就需要使用数据所解决问题
加锁
多线程我也不是太会这里就看一下源代码把
@SneakyThrows
@Override
public void processOrder(HashMap<String, Object> bodyMap) {
log.info("处理订单");
//因为解密过程后面也会弄到这里我们把他抽取成一个方法
String planText =decryptFromResource(bodyMap);
//将明文转换为map
Gson gson = new Gson();
HashMap plantTextMap = gson.fromJson(planText,HashMap.class);
//拿到订单号
String orderNo = (String) plantTextMap.get("out_trade_no");
/**
* 在对业务数据进行状态检查和处理之前,
* 要采用数据锁进行并发控制,
* 一面函数重入造成的数据混乱
*/
//尝试获取锁:成功获取则立即返回true,获取失败则立即返回false,不必一直等待锁的释放
if (lock.tryLock()){
try {
//处理重复的通知
String orderStatus = orderInfoService.getOrderStatus(orderNo);
//如果支付成功说明之前已经处理过了直接return
if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){
return;
}
//模拟通知并发
TimeUnit.SECONDS.sleep(5);
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,
OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(planText);
} finally {
//主动释放锁
lock.unlock();
}
}
}
商户定时查单
我们希望用户扫码成功之后为用户展示支付成功页面
这个很简单就是判断一下支付状态与前端对应如果支付成功就取消
@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("支付中......");
}
用户取消订单
首先编写控制层
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo){
log.info("取消订单");
wxPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}
实现类首先要调用微信支付的关闭顶端再修改本地数据库的订单状态
在这里关闭订单的方法可以封装传入参数订单号之后可以多次使用
注释写的很详细我就不截图一一说代码了
这是封装的方法
/**
* 关单接口的调用
* @param orderNo
*/
@SneakyThrows
private void closOrder(String orderNo) {
log.info("关单接口的调用,订单号===> {}",orderNo);
//创建远程调用对象
//此处是将字符串占位符替换为订单号
String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(),orderNo);
url = wxPayConfig.getDomain().concat(url);
HttpPost httpPost = new HttpPost(url);
//组装json请求体
Gson gson = new Gson();
Map<String,String> paramsMap = new HashMap<>();
paramsMap.put("mchid",wxPayConfig.getMchId());
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数===>{}",jsonParams);
//将请求参数设置到请求对象中
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept","application/json");
log.info("httpPost的值===>{}",httpPost);
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功200");
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功204");
} else {
log.info("Native下单失败,响应码 = " + statusCode);
throw new IOException("request failed");
}
} finally {
response.close();
}
}
接下来在前台下单后不支付选择取消订单,可以看到订单取消成功
查询订单
微信支付订单查询
商户后台如果没有收到异步通知商户支付结果应该主动向微信后台顶用查询订单api接口查看返回的支付状态然后根据支付状态修改本地存储的支付状态
因此查单可以放到实现类中的定时任务中去,在这里我们创建查单接口只是为了做测试
控制层
@GetMapping("/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo){
log.info("查询订单");
String result = wxPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result",result);
}
服务层
@SneakyThrows
@Override
public String queryOrder(String orderNo) {
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) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
测试结果
引入定时任务
在启动类引入启动定时任务的注解
创建软件包task并在里面创建定时任务的测试类
具体的使用方法如下
/**
* 秒 分 时 日 月 周
* *:每秒都执行
* 1-3:从第1秒开始执行到第3秒结束执行
* 0/3:从0秒开始每隔三秒执行1次
* 1,2,3:在指定的第1,2,3秒执行
* ?:不指定
* 日和周不能同时指定,指定其中一个则另一个设置为?
*/
在这里我设置的每秒执行一次
定时任务查找超时订单
不要忘记注入依赖
在服务层写一个订单状态查询方法
/**
* 查询超过minutes分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List<OrderInfo> getNoPayOederByDuration(int minutes) {
//创建时间实例
Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
//比较我的订单创建时间要早于5分钟前
queryWrapper.le("create_time",instant);
List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
return orderInfoList;
}
在微信支付定时任务类中创建一个新的订单状态查询方法参数为整型分钟
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm(){
log.info("定时任务查找超时订单");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOederByDuration(5);
for (OrderInfo orderInfo:
orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 === {}",orderNo);
//核实订单状态,调用微信支付查单接口
}
}
处理超时订单
首先在定时任务中编写核实订单状态的接口
看一下服务层方法方法描述在图片上
看一下完整代码
定时任务
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm(){
log.info("定时任务查找超时订单");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOederByDuration(5);
for (OrderInfo orderInfo:
orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 === {}",orderNo);
//核实订单状态,调用微信支付查单接口
wxPayService.checkOrderStatus(orderNo);
}
}
检查订单状态
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态
* 如果订单未支付,则调用关单接口关闭订单,并更新商户订单状态
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) {
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.closOrder(orderNo);
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
}
}
查询微信订单信息的代码
@SneakyThrows
@Override
public String queryOrder(String orderNo) {
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) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
再次刷新页面超时任务已经关闭
退款
申请退款api
实现大致流程首先创建退款单
调用退款api拼接参数向微信服务器发送请求然后解析参数并且根据回调参数更新订单状态和退款单状态
控制层
@ApiOperation("申请退款")
@PostMapping("/refunds/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo,String reason){
log.info("申请退款");
wxPayService.refund(orderNo,reason);
return R.ok();
}
实现层
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason){
log.info("创建退款单记录");
//根据订单编号创建退款单
RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo,reason);
//根据订单号生成退款订单
log.info("调用退款API");
//调用统一下单API
String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost = new HttpPost(url);
//请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("out_trade_no",orderNo);//订单编号
paramsMap.put("out_refund_no",refundInfo.getRefundNo());//退款单编号
paramsMap.put("reason",reason);//退款原因
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
Map amountMap = new HashMap();
amountMap.put("refund",refundInfo.getRefund());//退款金额
amountMap.put("total",refundInfo.getTotalFee());//原订单金额
amountMap.put("currency","CNY");//退款币种
paramsMap.put("amount",amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");//设置请求报文格式
httpPost.setEntity(entity);
httpPost.setHeader("Accept","application/json");//设置报文响应格式
//完成签名并执行请求,并完成验签
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功,退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
throw new RuntimeException("退款一场,响应码 = " + statusCode + ",退款返回结果 = "+ bodyAsString);
}
//响应结果
HashMap<String,String> resultMap=gson.fromJson(bodyAsString,HashMap.class);
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_PROCESSING);
//更新退款单
refundInfoService.updateRefund(bodyAsString);
} finally {
response.close();
}
}
创建退款单
@Override
public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
//根据订单号获取订单信息
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
RefundInfo refundInfo = new RefundInfo();
refundInfo.setOrderNo(orderNo);//订单编号
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
refundInfo.setReason(reason);//退款原因
//保存退款订单
baseMapper.insert(refundInfo);
return refundInfo;
}
更新退款单
@Override
public void updateRefund(String content) {
//将json字符串转成Map
Gson gson = new Gson();
Map<String,String> resultMap = gson.fromJson(content, HashMap.class);
//根据退款单编号修改退款单
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no",resultMap.get("out_refund_no"));
//设置要修改的字段
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号
//查询退款和申请退款中的返回参数
if (resultMap.get("status")!=null){
refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
}
//退款回调中的回调参数
if (resultMap.get("refund_status")!=null){
refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
refundInfo.setContentNotify(content);//将全部响应结果存入数据库content字段
}
//更新退款单
baseMapper.update(refundInfo,queryWrapper);
}
微信支付对于不同退款方式返回的状态的编码不同这个函数封装后可以多用可以用于申请退款查询退款退款结果通知
查询退款api
控制层
@ApiOperation("查询退款,测试用")
@GetMapping("/query-refund/{refundNo}")
public R queryRefund(@PathVariable String refundNo){
log.info("查询退款");
String result = wxPayService.queryRefund(refundNo);
return R.ok().setMessage("查询成功").data("result",result);
}
实现层
@SneakyThrows
@Override
public String queryRefund(String refundNo) {
log.info("查询退款接口调用 ===> {}",refundNo);
String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(),refundNo);
url=wxPayConfig.getDomain().concat(url);
log.info("查询退款测试url===>{}",url);
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept","application/json");
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
//响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功,查询退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native查询退款异常,响应码 = " + statusCode+ ",返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
退款结果通知
与支付通知类似其主要是发起退款后微信服务端向本地服务器发送请求访问接口然后在本地服务器修改数据库更新退款单状态和订单状态
控制层
@SneakyThrows
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
log.info("执行退款结果通知");
Gson gson = new Gson();
//创建应答对象
HashMap<String, String> map = new HashMap<>();
//处理通知参数
String body = HttpUtils.readData(request);
HashMap<String,Object> bodyMap = gson.fromJson(body,HashMap.class);
log.info("支付通知的id ===> {}" + bodyMap.get("id"));
log.info("支付通知的完整数据 ===> {}" + body);
String requestId = (String) bodyMap.get("id");
//TODO:签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier,requestId,body);
//如果验签不通过进行失败应答
if (!wechatPay2ValidatorForRequest.validate(request)){
log.info("通知验签失败");
response.setStatus(500);
map.put("code","ERROR");
map.put("message","失败");
return gson.toJson(map);
}
//TODO:订单处理
log.info("通知验签成功");
//处理退款单
wxPayService.processRefund(bodyMap);
//应答超时模拟和接受微信端的重复通知
// TimeUnit.SECONDS.sleep(5);
//应答对象
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
//成功应答
return gson.toJson(map);
}
服务层
@Override
public void processRefund(HashMap<String, Object> bodyMap) {
log.info("退款通知接口调用");
//解密返回的数据
String planText = decryptFromResource(bodyMap);
//将明文转化为map
Gson gson = new Gson();
HashMap plantextMap = gson.fromJson(planText,HashMap.class);
//拿到订单号
String orderNo = (String) plantextMap.get("out_trade_no");
if (lock.tryLock()){
try {
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)){
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_SUCCESS);
//更新退款单
refundInfoService.updateRefund(planText);
} finally {
//要主动释放锁
lock.unlock();
}
}
}
账单
申请交易账单API
通过填写账单和数据类型访问测试接口之后在实现层本地服务器会向微信服务器发送请求获取账单的url
控制层
@ApiOperation("获取账单url,测试用")
@GetMapping("/querybill/{billDate}/{type}")
public R queryTradeBill(@PathVariable String billDate,
@PathVariable String type){
log.info("获取账单url");
String downloadUrl = wxPayService.queryBill(billDate,type);
return R.ok().setMessage("获取账单url成功").data("downloadUrl",downloadUrl);
}
实现层
@SneakyThrows
@Override
public String queryBill(String billDate, String type) {
log.warn("申请账单接口调用{}",billDate);
String url = "";
if ("tradebill".equals(type)){
url = WxApiType.TRADE_BILLS.getType();
}else if ("fundflowbill".equals(type)){
url =WxApiType.FUND_FLOW_BILLS.getType();
}else {
throw new RuntimeException("不支持的账单类型");
}
url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Accept","application/json");
//使用WxPayClient发送请求得到响应
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 {
throw new RuntimeException("申请账单异常,响应码 = "+statusCode+",返回结果 = " + EntityUtils.toString(response.getEntity()));
}
//获取账单下载地址
Gson gson = new Gson();
Map<String,String> resultMap = gson.fromJson(bodyAsString,HashMap.class);
return resultMap.get("download_url");
} finally {
response.close();
}
}
但是这个下载地址是无法直接访问的必须通过接口访问
下载账单API
控制层`
@ApiOperation("下载账单")
@GetMapping("/downloadbill/{billDate}/{type}")
public R downloadBill(@PathVariable String billDate, @PathVariable String type){
log.info("下载账单");
String result = wxPayService.downloadBill(billDate,type);
return R.ok().data("result",result);
}
实现层
@SneakyThrows
@Override
public String downloadBill(String billDate, String type) {
log.warn("下载账单接口调用{},{}",billDate,type);
//获取账单url地址
String downloadUrl = this.queryBill(billDate,type);
//创建远程Get,请求对象
HttpGet httpGet = new HttpGet(downloadUrl);
httpGet.addHeader("Accept","application/json");
CloseableHttpResponse response = wxPayNoSignClient.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 {
throw new RuntimeException("下载账单异常,响应码 = "+statusCode+",返回结果 = " + EntityUtils.toString(response.getEntity()));
}
return bodyAsString;
} finally {
response.close();
}
}
注意这里要注入一个新的依赖
在WxConfig中定义
package com.atguigu.paymentdemo.config;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
//@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@ConfigurationProperties(prefix = "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;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
try {
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在",e);
}
}
/**
* 获取签名验证器
* @return
*/
@SneakyThrows
@Bean //不希望程序执行多次就添加Bean注解让程序启动执行一次就好了
public Verifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
//对称加密密钥
byte[] bytes = apiV3Key.getBytes(StandardCharsets.UTF_8);
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(mchId,wechatPay2Credentials, bytes);
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(mchId);
return verifier;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(Verifier verifier){
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
// 从证书管理器中获取verifier
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
}
点击下载可以成功下载