文章目录
- 前言
- 1. 微信支付产品介绍
- 2 接入指引
- 3 支付安全
- 4 基础支付apiv3
-
- 4.1 基础支付APly3-引入支付参数
- 4.2 基础支付APly3-加载用户私钥
- 4.3 基础支付APIv3-获取验签器和HttpClient
- 4.4 基础支付APIv3-API字典和相关工具
- 4.5 基础支付APIv3-Native支付流程
- 4.6 基础支付APIv3-Native返回二维码链接
- 4.7 基础支付APIv3-签名原理实现流程分析
- 4.8 基础支付Apiv3生成订单-存入数据库
- 4.9 基础支付APy3i--生成订单--获取已存在订单(结合项目实际使用 非必要)
- 4.10 基础支付ARV3生成讶单-存储二维码地址(结合项目实际使用 非必要)
- 4.11 基础支付APlv3-生成订单-显示订单
- 4.12 基础支付APlv3-实现内网穿透
- 4.13 基础支付APIv3-支付通知-接收通知和返回应答
- 4.14 基础支付APIv3-支付通知-应答异常和应答超时
- 4.15 基础支付API3—支付通知--验签
- 4.16 基础支付APlv3-支付通知-报文解密
- 4.17 基础支付Apaly3m支付通知--更新订单状态--记录支付日志
- 4.18 基础支付APlv3-支付通知-处理重复通知和接口调用的幂等性
- 4.19 基础支付APIv3-支付通知-数据锁
- 4.20 基础支付AP3-关闭订单(调用关单接口)-用户取消订单
- 4.21 基础支付APlv3-查询订单API-微信支付查询订单
- 4.22 基础支付APIv3-查询订单API-引入定时任务
- 4.23 基础支付APl3-查询订单APi--定时查找超时订单
- 4.24 其础支付APIv3-查询订单APi-处理超时订单
- 4.25 基础支付APIv3-申请退款APl
- 4.26 基础支付APIv3-查询退款API
- 4.27 基础支付APIv3-处理退款通知
- 4.28 基础支付APIv3-账单-1获取账单的下载地址
- 4.29 基础支付APIv3-账单-2 下载账单
- 总结
前言
本篇文章基于PC网站的情景来实现微信支付
学习自尚硅谷
1. 微信支付产品介绍
官网
https://pay.weixin.qq.com/static/product/product_index.shtml#payment_product
2 接入指引
2.1 获取商户号
接入引链接
https://pay.weixin.qq.com/static/applyment_guide/applyment_index.shtml
选择支付产品后接入微信支付
2.2 获取appid
来到微信公众号平台选择注册小程序或公众号等
获取appid
https://mp.weixin.qq.com
然后到微信支付商户平台中
选择关联appid
这样你的商户号和appid就联系起来了
接着在公众号平台中选择同意绑定
2.3 获取密钥和证书
进入微信商户平台
选择 账户中心 安全中心 api安全
对这三项进行设置
3 支付安全
3.1 对称加密和非对称加密
3.2 身份认证
私钥加密 公钥解密的目的是为了身份认证
3.3 数字证书
3.4 https中的数字证书
3.5 微信支付中的证书密钥和签名
因为在微信支付的过程中是我们商户和微信平台的数据信息互通
所以需要商户的证书,公钥私钥 和 微信平台的证书公钥私钥
1 之前我们生成的商户的api证书中包括了
商户的公钥(在证书cert中)和私钥(在key中)
2 我们可以通过api接口获取的方式获取微信平台证书
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_2.shtml
4 基础支付apiv3
4.1 基础支付APly3-引入支付参数
1 在resource中定义如下配置文件
# 微信支付相关参数
# 商户号
wxpay.mch-id=1xxxxxx
# 商户API证书序列号
wxpay.mch-serial-no=3xxxxxxxxxxxxx
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFxxxxxxxxxxx
# APPID
wxpay.appid=wx748xxxxxxx
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://500c-219-1xxxxxxxxx
## APIv2密钥
#wxpay.partnerKey: T6m9xxxxxxxxx
2 定义config加载在resource中定义的资源
package com.atguigu.paymentdemo.config;
//import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
//import com.wechat.pay.contrib.apache.httpclient.auth.*;
//import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
//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节点
@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;
// /**
// * 获取商户的私钥文件
// * @param filename
// * @return
// */
// private PrivateKey getPrivateKey(String filename){
//
// try {
// return PemUtil.loadPrivateKey(new FileInputStream(filename));
// } catch (FileNotFoundException e) {
// throw new RuntimeException("私钥文件不存在", e);
// }
// }
//
// /**
// * 获取签名验证器
// * @return
// */
// @Bean
// public ScheduledUpdateCertificatesVerifier getVerifier(){
//
// log.info("获取签名验证器");
//
// //获取商户私钥
// 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;
// }
//
//
// /**
// * 获取http请求对象
// * @param verifier
// * @return
// */
// @Bean(name = "wxPayClient")
// public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
//
// log.info("获取httpClient");
//
// //获取商户私钥
// 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;
// }
//
// /**
// * 获取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;
// }
}
4.2 基础支付APly3-加载用户私钥
1 导入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.8</version>
</dependency>
在文档中搜索私钥
# 示例:私钥存储在文件
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream("/path/to/apiclient_key.pem"));
# 示例:私钥为String字符串
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
public PrivateKey getPrivateKey(String filename){
try {
//通过导入的SDK 和 商户的数字证书路径filename 获得 PrivateKey 并且返回
return PemUtil.loadPrivateKey(new FileInputStream(filename));
} catch (FileNotFoundException e ) {
throw new RuntimeException("私钥文件不存在", e);
}
}
4.3 基础支付APIv3-获取验签器和HttpClient
我们所要做的是使用商户私钥加密和
使用微信支付平台的公钥进行解密
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
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;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
log.info("获取httpClient");
//获取商户私钥
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;
}
4.4 基础支付APIv3-API字典和相关工具
由于
所以我们引入处理json的工具
<!--json处理器-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
接着配置一些enum枚举
方便开发
4.5 基础支付APIv3-Native支付流程
4.6 基础支付APIv3-Native返回二维码链接
完成了支付流程的1-5步骤
1 Controller
/**
* Native下单
* @param productId
* @return
* @throws Exception
*/
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求 v3");
//返回支付二维码连接和订单号
//接受前端发来的productid
//返回code_url
Map<String, Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
2 service
Map<String, Object> nativePay(Long productId) throws Exception;
3 serviceimpl
/**
* 创建订单,调用Native支付接口
* @param productId
* @return code_url 和 订单号
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
@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);
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//TODO: 把生成的订单对象存入数据库
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");
//把处理好的实体类放到post请求对象中
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");
//保存二维码
// String orderNo = orderInfo.getOrderNSo();
// orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
效果
4.7 基础支付APIv3-签名原理实现流程分析
4.8 基础支付Apiv3生成订单-存入数据库
//调用方法生成订单对象(这个方法中会将生成的订单对象加入数据库)
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
// OrderInfo orderInfo = new OrderInfo();
// orderInfo.setTitle("test");
// orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
// orderInfo.setProductId(productId);
// orderInfo.setTotalFee(1);
// orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
Service层
package com.atguigu.paymentdemo.service;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.baomidou.mybatisplus.extension.service.IService;
public interface OrderInfoService extends IService<OrderInfo> {
OrderInfo createOrderByProductId(Long productId);
}
ServiceImpl层
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Autowired
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()); //分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//把订单对象添加到数据库中(使用了mybaits-plus)
baseMapper.insert(orderInfo);
return orderInfo;
}
}
4.9 基础支付APy3i–生成订单–获取已存在订单(结合项目实际使用 非必要)
如果按照之前那样写
用户无论是否支付在数据库中都会创建新的订单对象
我们对其进行优化
@Override
public OrderInfo createOrderByProductId(Long productId) {
//查找已存在但未支付的订单(没有验证userid 正式记得验证)
//有这样的订单直接返回这个orderinfo
//不再创建新的订单对象 节省数据库空间
OrderInfo orderInfo = this.getNoPayOrderByProductId(productId);
if( orderInfo != null){
return orderInfo;
}
//获取商品信息
Product product = productMapper.selectById(productId);
//生成订单
orderInfo = new OrderInfo();
orderInfo.setTitle(product.getTitle());
orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //订单号
orderInfo.setProductId(productId);
orderInfo.setTotalFee(product.getPrice()); //分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//把订单对象添加到数据库中(使用了mybaits-plus)
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 根据商品id查询未支付订单
* 防止重复创建订单对象
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId) {
//生成查询OrderInfo的queryWrapper
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
//组装queryWrapper
queryWrapper.eq("product_id", productId);
queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
//正式项目记得验证user_id
// queryWrapper.eq("user_id", userId);
//使用selectone查询是否有这样的订单对象
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
4.10 基础支付ARV3生成讶单-存储二维码地址(结合项目实际使用 非必要)
1 新建一个service
public interface OrderInfoService extends IService<OrderInfo> {
//根据产品id创建订单
OrderInfo createOrderByProductId(Long productId);
//保存订单的二维码
void saveCodeUrl(String orderNo, String codeUrl);
}
2 对应的serviceimpl
/**
* 存储订单二维码
* @param orderNo
* @param codeUrl
*/
@Override
public void saveCodeUrl(String orderNo, String codeUrl) {
//建立一个queryWrapper 方便后续查询
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
//创建一个orderInfo对象 并设置其二维码url
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCodeUrl(codeUrl);
//mybatis-plus 根据订单号更新状态
baseMapper.update(orderInfo, queryWrapper);
}
3 payseviceimpl中对应使用 实现保存二维码
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
4 在 开始时候 校验是否有code_url 有的话直接返回这个结果
不再调用api
public Map<String, Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//调用方法生成订单对象(这个方法中会将生成的订单对象加入数据库)
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
//如果返回的订单对象中有code_url 直接返回结果 非必须代码
String codeUrl = orderInfo.getCodeUrl();
if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
4.11 基础支付APlv3-生成订单-显示订单
1 controller
@CrossOrigin //开放前端的跨域访问
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@ApiOperation("订单列表")
@GetMapping("/list")
public R list(){
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list", list);
}
}
2 service
public interface OrderInfoService extends IService<OrderInfo> {
//根据产品id创建订单
OrderInfo createOrderByProductId(Long productId);
//保存订单的二维码
void saveCodeUrl(String orderNo, String codeUrl);
//根据倒叙创建事件查询订单对象
List<OrderInfo> listOrderByCreateTimeDesc();
}
3 serviceimpl
/**
* 查询订单列表,并倒序查询
* @return
*/
@Override
public List<OrderInfo> listOrderByCreateTimeDesc() {
//创建一个倒叙时间查询所有订单对象
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<OrderInfo>().orderByDesc("create_time");
//调用Mapper层查询
return baseMapper.selectList(queryWrapper);
}
4.12 基础支付APlv3-实现内网穿透
可以在网上搜索cpolar 免费实现内网穿透
4.13 基础支付APIv3-支付通知-接收通知和返回应答
官方文档
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
1 在配置文件中配置好我们的外网服务器ip地址 方便后续支付成功后微信服务器的访问
2 我们之前在基础下单接口中的请求参数 notify_url中设置了回调地址
微信服务器在完成支付交易之后会访问这个地址给我们发送请求
3 编写controller接口
(签名的验证 处理订单 超时等在后面做处理)
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@ApiOperation("支付通知")
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
Gson gson = new Gson();
//创建响应应答对象
Map<String, String> map = new HashMap<>();
try {
//处理通知参数 把接收到的request处理成String
String body = HttpUtils.readData(request);
//调用工具类把String 转化为 hashmap
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String)bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
log.info("支付通知的完整数据 ===> {}", body);
//int a = 9 / 0;
//签名的验证
// WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
// = new WechatPay2ValidatorForRequest(verifier, requestId, body);
// if(!wechatPay2ValidatorForRequest.validate(request)){
//
// log.error("通知验签失败");
// //失败应答
// response.setStatus(500);
// map.put("code", "ERROR");
// map.put("message", "通知验签失败");
// return gson.toJson(map);
// }
// log.info("通知验签成功");
//处理订单
// wxPayService.processOrder(bodyMap);
//应答超时
//模拟接收微信端的重复通知
// TimeUnit.SECONDS.sleep(5);
//成功应答
//返回状态码
response.setStatus(200);
//返回应答报文
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
//返回状态码
map.put("code", "ERROR");
//返回应答报文
map.put("message", "失败");
return gson.toJson(map);
}
}
4 当支付成功后微信访问了我们提供的接口
这里使用了日志打印
4.14 基础支付APIv3-支付通知-应答异常和应答超时
对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)
4.15 基础支付API3—支付通知–验签
之前的基础通知我们针对response进行验签
现在我们针对request进行验签
1 自定义请求验签器
package com.atguigu.paymentdemo.util;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
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.*;
/**
* @author xy-peng
*/
//处理微信服务器给我们发请求的验签
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 requestId;
protected final String body;
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
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, requestId);
}
} 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";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
2 调用请求验签器验证
在支付通知接口中进行验证
//签名的验证
//调用我们自定义的请求延签器 实现签名的验证 验证Request消息的真实性
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
//验签失败通知
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
4.16 基础支付APlv3-支付通知-报文解密
在验签成功后我们可以对密文的信息进行解密
1 调用service层方法
}
log.info("通知验签成功");
//处理订单(调用service层方法 传递接收到的body)
wxPayService.processOrder(bodyMap);
2 service层
public interface WxPayService {
Map<String, Object> nativePay(Long productId) throws Exception;
void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException;
3 impl层
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("处理订单");
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plainTextMap.get("out_trade_no");
}
/**
* 对称解密
* @param bodyMap
* @return
*/
private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("密文解密");
//通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
String associatedData = resourceMap.get("associated_data");
log.info("密文 ===> {}", ciphertext);
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
String plainText = aesUtil.decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext
);
log.info("明文 ===> {}", plainText);
return plainText;
}
4.17 基础支付Apaly3m支付通知–更新订单状态–记录支付日志
当我们解密回调报文后
我们对订单状态更新
并且记录支付日志
1 更改订单状态
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
Service
public interface OrderInfoService extends IService<OrderInfo> {
//根据产品id创建订单
OrderInfo createOrderByProductId(Long productId);
//保存订单的二维码
void saveCodeUrl(String orderNo, String codeUrl);
//根据倒叙创建事件查询订单对象
List<OrderInfo> listOrderByCreateTimeDesc();
void updateStatusByOrderNo(String orderNo, OrderStatus success);
}
serviceimpl
/**
* 根据订单号更新订单状态
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("更新订单状态 ===> {}", orderStatus.getType());
//创建OrderInfo的queryWrapper
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
//构造更新对象 准备更改订单状态
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
//执行更新操作
baseMapper.update(orderInfo, queryWrapper);
}
2 记录支付日志
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
service
public interface PaymentInfoService {
void createPaymentInfo(String plainText);
}
serviceimpl
/**
* 记录支付日志
* @param plainText
*/
@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);
}
4.18 基础支付APlv3-支付通知-处理重复通知和接口调用的幂等性
如果因为网络波动等问题
我们没有及时给微信的回调请求发送结果
微信会重复给我们发送回调通知
这时我们必须正确的处理重复的通知
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderInfoService.getOrderStatus(orderNo);
//如果订单的状态不是未支付(即订单的状态已经被处理 代表之前已经处理过 直接return不再处理)
if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
return;
}
/**
* 根据订单号获取订单状态
* @param orderNo
* @return
*/
@Override
public String getOrderStatus(String orderNo) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
//如果没查到数据 返回null 防止空指针异常
if(orderInfo == null){
return null;
}
return orderInfo.getOrderStatus();
}
4.19 基础支付APIv3-支付通知-数据锁
要是用数据锁进行并发控制
如果两个请求并发来到处理重复通知的这段代码
则会有并发性的问题
更新订单状态和记录支付日志
都会执行两遍
我们使用锁进行优化
//锁
private final ReentrantLock lock = new ReentrantLock();
/*在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱*/
//尝试获取锁:
// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if(lock.tryLock()){
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
return;
}
//模拟通知并发
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
} finally {
//要主动释放锁
lock.unlock();
}
}
这样确保无论重复通知发送多少次
能够正确的处理重复通知
更新订单状态和记录支付日志只执行一遍
4.20 基础支付AP3-关闭订单(调用关单接口)-用户取消订单
1 controller
/**
* 用户取消订单
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("用户取消订单")
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws Exception {
log.info("取消订单");
//调用关单service
wxPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}
2 service
void cancelOrder(String orderNo) throws Exception;
3 serviceimpl
/**
* 用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) throws Exception {
//调用微信支付的关单接口
this.closeOrder(orderNo);
//更新商户端的订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}
/**
* 关单接口的调用
* @param orderNo
*/
private void closeOrder(String orderNo) throws Exception {
log.info("关单接口的调用,订单号 ===> {}", orderNo);
//创建远程请求对象
String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url);
HttpPost httpPost = new HttpPost(url);
//组装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");
//完成签名并执行请求
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();
}
}
4.21 基础支付APlv3-查询订单API-微信支付查询订单
在这里插入图片描述
如果我们没有接收到通知的商户支付结果
我们应该主动调用查询订单api
查询订单状态
1 controller
/**
* 查询订单
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("查询订单:测试订单状态用")
@GetMapping("/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo) throws Exception {
log.info("查询订单");
String result = wxPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
2 service
String queryOrder(String orderNo) throws Exception;
3 serviceimpl
@Override
public String queryOrder(String orderNo) throws Exception {
log.info("查单接口调用 ===> {}", orderNo);
String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) {
//处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
//处理成功,无返回Body
log.info("成功");
} else {
log.info("查单接口调用,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
4.22 基础支付APIv3-查询订单API-引入定时任务
1 首先在启动类中
@SpringBootApplication
//引入Spring Task 执行定时任务
@EnableScheduling
public class PaymentDemoApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentDemoApplication.class, args);
}
}
2 进行测试
@Slf4j
@Component
public class WxPayTask {
/**
* 秒 分 时 日 月 周
* 以秒为例
* *:每秒都执行
* 1-3:从第1秒开始执行,到第3秒结束执行
* 0/3:从第0秒开始,每隔3秒执行1次
* 1,2,3:在指定的第1、2、3秒执行
* ?:不指定
* 日和周不能同时制定,指定其中之一,则另一个设置为?
*/
@Scheduled(cron = "0/3 * * * * ?")
public void task1(){
log.info("task1 被执行......");
}
}
4.23 基础支付APl3-查询订单APi–定时查找超时订单
1 controller
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
log.info("orderConfirm 被执行......");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5);
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 ===> {}", orderNo);
//核实订单状态:调用微信支付查单接口
// wxPayService.checkOrderStatus(orderNo);
}
}
2 service
//获取在一定时间间隔内没有支付的订单列表
List<OrderInfo> getNoPayOrderByDuration(int minutes);
3 serviceimpl
/**
* 查询创建超过minutes分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List<OrderInfo> getNoPayOrderByDuration(int minutes) {
Instant now = Instant.now();
log.info("instant.now产生的时间是====》{}",now);
Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
log.info("instant.now减去传来的时间后结果是===》{}",instant);
//创建一个queryWrapper
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
//设置查询条件为未支付
queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
//设置查询条件 小于等于instant
queryWrapper.le("create_time", instant);
//查询list
List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
return orderInfoList;
}
4.24 其础支付APIv3-查询订单APi-处理超时订单
我们首先模拟没有接收到回调的情况
将自己的配置文件中接收结果通知地址进行更改
这样我们就无法接收回调了
1 controller
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
log.info("orderConfirm 被执行......");
//查询创建超过1分钟,并且未支付的订单
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1);
//对查询出来的这些未支付订单进行核查
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 ===> {}", orderNo);
//核实订单状态:调用微信支付查单接口
wxPayService.checkOrderStatus(orderNo);
}
}
2 service
//检查订单状态 (确定到底是否支付钱)
void checkOrderStatus(String orderNo) throws Exception;
3 serviceimpl
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态,并记录支付日志
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* @param orderNo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态 ===> {}", orderNo);
//调用微信支付查单接口 获取调用查单接口后返回的结果
String result = this.queryOrder(orderNo);
//把String类型的返回结果通过Gson处理为HashMap
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
//从hashmap中获取微信支付端的订单状态
String tradeState = resultMap.get("trade_state");
//判断订单状态
//1 如果支付状态成功的话 更新本地订单状态并且记录支付日志
if(WxTradeState.SUCCESS.getType().equals(tradeState)){
log.warn("核实订单已支付 ===> {}", orderNo);
//如果确认订单已支付则更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志(调用查单接口返回的数据和 回调数据中的密文解密后的数据一致)
paymentInfoService.createPaymentInfo(result);
}
//2 如果订单未支付的话 调用关单接口 并更新本地订单状态
if(WxTradeState.NOTPAY.getType().equals(tradeState)){
log.warn("核实订单未支付 ===> {}", orderNo);
//如果订单未支付,则调用关单接口
this.closeOrder(orderNo);
//更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
}
4.25 基础支付APIv3-申请退款APl
1 整体逻辑
1 controller
@ApiOperation("申请退款")
@PostMapping("/refunds/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {
log.info("申请退款");
wxPayService.refund(orderNo, reason);
return R.ok();
}
2 service
void refund(String orderNo, String reason) throws Exception;
3 serviceimpl
/**
* 退款
* @param orderNo
* @param reason
* @throws IOException
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) throws Exception {
log.info("创建退款单记录");
//根据订单编号创建退款单
RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);
log.info("调用退款API");
//调用统一下单API
//1 构造url 创建HttpPost对象
String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost = new HttpPost(url);
// 2
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("out_trade_no", orderNo);//订单编号
paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
paramsMap.put("reason",reason);//退款原因
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
Map amountMap = new HashMap();
amountMap.put("refund", refundsInfo.getRefund());//退款金额
amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
amountMap.put("currency", "CNY");//退款币种
paramsMap.put("amount", amountMap);
//将参数转换成json字符串
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
//3 将请求参数设置到httpPost中并发送
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 {
//1 解析响应结果
String bodyAsString = EntityUtils.toString(response.getEntity());
//获取响应code
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功, 退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
}
//2 更新状态
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
//更新退款单 (传递的是处理过后的String 接口响应结果)
refundsInfoService.updateRefund(bodyAsString);
} finally {
response.close();
}
}
2 createRefundByOrderNo逻辑
2 service
//管理退款的service层
public interface RefundInfoService extends IService<RefundInfo> {
//创建退款对象
RefundInfo createRefundByOrderNo(String orderNo, String reason);
//更新退款 (传递过来的是调用退款接口后返回的数据)
void updateRefund(String content);
}
3 serviceimpl
/**
* 根据订单号创建退款订单
* @param orderNo
* @return
*/
@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;
}
3 refundsInfoService.updateRefund(bodyAsString); 逻辑
2 service
//管理退款的service层
public interface RefundInfoService extends IService<RefundInfo> {
//创建退款对象
RefundInfo createRefundByOrderNo(String orderNo, String reason);
//更新退款 (传递过来的是调用退款接口后返回的数据)
void updateRefund(String content);
}
3 serviceimpl
/**
* 记录退款记录
* @param content
*/
//传递来的是调用退款接口后返回的结果
@Override
public void updateRefund(String content) {
//1 将json字符串转换成Map
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(content, HashMap.class);
//2 根据退款单编号修改退款单
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));
//3 设置要修改的字段
//这些字段从 接口返回的结果中获取
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号
//查询退款和申请退款中的返回参数
if(resultMap.get("status") != null){
refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
}
//退款回调中的回调参数
if(resultMap.get("refund_status") != null){
refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段
}
//更新退款单
baseMapper.update(refundInfo, queryWrapper);
}
4.26 基础支付APIv3-查询退款API
1 controller
/**
* 查询退款
* @param refundNo
* @return
* @throws Exception
*/
@ApiOperation("查询退款:测试用")
@GetMapping("/query-refund/{refundNo}")
public R queryRefund(@PathVariable String refundNo) throws Exception {
log.info("查询退款");
String result = wxPayService.queryRefund(refundNo);
return R.ok().setMessage("查询成功").data("result", result);
}
2 service
//查询退款
String queryRefund(String orderNo) throws Exception;
3 serviceimpl
/**
* 查询退款接口调用
* @param refundNo
* @return
*/
@Override
public String queryRefund(String refundNo) throws Exception {
log.info("查询退款接口调用 ===> {}", refundNo);
//1 根据refundNo退款单号 构建url
String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
url = wxPayConfig.getDomain().concat(url);
//创建远程Get 请求对象
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("成功, 查询退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
}
return bodyAsString;
} finally {
response.close();
}
}
在Swagger中测试
4.27 基础支付APIv3-处理退款通知
1 controller
/**
* 退款结果通知
* 退款状态改变后,微信会把相关退款结果发送给商户。
*/
@ApiOperation("退款结果通知")
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
log.info("退款通知执行");
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();//应答对象
try {
// 1 处理收到的request
//处理通知参数
String body = HttpUtils.readData(request);
//把收到的数据包从Json变为hashmap
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String)bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
//2
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
//3 验签通过后处理退款单
//处理退款单
wxPayService.processRefund(bodyMap);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return gson.toJson(map);
}
}
2 wxPayService.processRefund(bodyMap)
void processRefund(Map<String, Object> bodyMap) throws Exception;
3 serviceimpl
/**
* 处理退款单
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void processRefund(Map<String, Object> bodyMap) throws Exception {
log.info("退款单");
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plainTextMap.get("out_trade_no");
if(lock.tryLock()){
try {
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
//更新退款单
refundsInfoService.updateRefund(plainText);
} finally {
//要主动释放锁
lock.unlock();
}
}
}
4.28 基础支付APIv3-账单-1获取账单的下载地址
1 controller
@ApiOperation("获取账单url:测试用")
@GetMapping("/querybill/{billDate}/{type}")
public R queryTradeBill(
@PathVariable String billDate,
@PathVariable String type) throws Exception {
log.info("获取账单url");
String downloadUrl = wxPayService.queryBill(billDate, type);
return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
}
2 service
String queryBill(String billDate, String type) throws Exception;
3 serviceimpl
/**
* 申请账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String queryBill(String billDate, String type) throws Exception {
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);
//创建远程Get 请求对象
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+ ", 申请账单返回结果 = " + bodyAsString);
}
//获取账单下载地址
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
return resultMap.get("download_url");
} finally {
response.close();
}
}
4.29 基础支付APIv3-账单-2 下载账单
1 controller
@ApiOperation("下载账单")
@GetMapping("/downloadbill/{billDate}/{type}")
public R downloadBill(
@PathVariable String billDate,
@PathVariable String type) throws Exception {
log.info("下载账单");
String result = wxPayService.downloadBill(billDate, type);
return R.ok().data("result", result);
}
2 service
//下载账单
String downloadBill(String billDate, String type) throws Exception;
3 serviceimpl
/**
* 下载账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String downloadBill(String billDate, String type) throws Exception {
log.warn("下载账单接口调用 {}, {}", billDate, type);
//获取账单url地址
String downloadUrl = this.queryBill(billDate, type);
//创建远程Get 请求对象
HttpGet httpGet = new HttpGet(downloadUrl);
httpGet.addHeader("Accept", "application/json");
//使用wxPayClient发送请求得到响应
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+ ", 下载账单返回结果 = " + bodyAsString);
}
return bodyAsString;
} finally {
response.close();
}
}
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。