微信native支付
微信native支付
微信APIv3证书
商户证书:商户API证书由商户申请,包含商品的商户号、公司名称、公钥信息的证书
平台证书(微信支付平台):微信支付平台证书由微信支付负责申请,包含微信支付平台标识,公钥信息的证书。该证书可通过API接口下载,商户可使用平台证书中的公钥进行验签
API密钥和APIv3密钥:
- 都是对称加密需要使用的加密和解密密钥,要保管好,不能泄露
- API密钥对应V2版本的API
- APIv3密钥对应V3版本的API
创建案例项目
流程:
- 创建SpringBoot项目(Java,SpringBoot,SpringMVC,RESTful,json)
- 引入Swagger(接口文档和测试页面生成工具)
- 定义统一结果(让前后端数据通信更规范)
- 创建和连接数据库(MySQL,IDEA内置的数据库管理工具)
- 集成Mybatis-Plus
- 搭建前端环境(了解HTML和CSS,熟悉JavaScript,了解Vue)
- 认识Vue.js
Swagger:为测试方便。可展示项目已有的controller及其详情。可通过localhost:8090/swagger-ui.html访问。可结合Apifox一起使用
Mybatis-plus:最基本的sql在xml中都不用写,mybatis-plus直接配置好,使用BaseMapper
搭建前端环境:
- 安装node.js
- 运行 npm run serve
- 前后端分离。前端在8080端口,后端在8090端口(ajax),跨域@CrossOrigin
基础支付APIv3
流程:
- 引入支付参数
- 加载商户私钥
- 获取平台证书和验签器
- 获取HttpClient对象
- API字典和接口规则
- 内网穿透
- APIv3
wxpay.properties
# 微信支付相关参数
# 商户号
wxpay.mch-id=***
# 商户API证书序列号
wxpay.mch-serial-no=***
# 商户私钥文件 (非对称加密)
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥 (对称加密)
wxpay.api-v3-key=***
# APPID (微信公众号)
wxpay.appid=***
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
# ngrok 内网穿透?
wxpay.notify-domain= ***
# APIv2密钥
wxpay.partnerKey: ***
application.yml
server:
port: 8090 # 服务端口
spring:
application:
name: payment-demo # 应用的名字
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/payment-demo?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
mybatis-plus:
configuration: # sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-loc ations: classpath:com/pay/patmentdemo/mapper/xml/*.xml
logging:
level:
root: info
可配置wxpay.properties为Spring的配置文件,这样配置文件中的属性可以使用Environment对象直接使用(两个重要注解@Resource和@AutoWired)
加载商户私钥,获取验签器和HttpClient
WxPayConfig.java
package com.pay.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;
}
}
微信支付使用JSON作为数据交互的格式,native支付包括下单,查单,关单等。native支付相关功能枚举类:WxApiType.java,WxNotifyType.java,WxRefundStatus.java,WxTradeState.java。相关工具类:生成订单号OrderNoUtils.java,处理微信端发送的回调请求信息HttpClientUtils.java等
native支付
native支付流程
对应Controller
WxPayController.java
package com.pay.paymentdemo.controller;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.pay.paymentdemo.enums.OrderStatus;
import com.pay.paymentdemo.service.OrderInfoService;
import com.pay.paymentdemo.service.WxPayService;
import com.pay.paymentdemo.util.HttpUtils;
import com.pay.paymentdemo.util.WechatPay2ValidatorForRequest;
import com.pay.paymentdemo.vo.R;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@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);
}
@PostMapping("/native/notify")
public String nativeNotity(HttpServletRequest request, HttpServletResponse response) {
Gson gson = new Gson();
// 应答对象
Map<String, String> map = new HashMap<>();
try {
// 处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String) bodyMap.get("id");
log.info("支付通知的id ====> {}", bodyMap.get("id"));
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);
}
}
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws Exception {
log.info("取消订单");
wxPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}
@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);
}
@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();
}
/**
* 查询退款
* @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);
}
/**
* 退款结果通知
* 退款状态改变后,微信会把相关退款结果发送给商户。
*/
@ApiOperation("退款结果通知")
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
log.info("退款通知执行");
Gson gson = new Gson();
Map<String, String> map = new HashMap<>();//应答对象
try {
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String)bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
//签名的验证
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.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);
}
}
@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);
}
@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);
}
}
日志打印:@Slf4j。请求Body使用hashMap存储,然后转化为JSON。请求类型application/json。响应是url,前端将url转为二维码(包vue-qriously)
WxPayServiceImpl.java
package com.pay.paymentdemo.service.impl;
import com.github.wxpay.sdk.WXPayUtil;
import com.google.gson.Gson;
import com.mysql.jdbc.StringUtils;
import com.pay.paymentdemo.config.WxPayConfig;
import com.pay.paymentdemo.entity.OrderInfo;
import com.pay.paymentdemo.entity.RefundInfo;
import com.pay.paymentdemo.enums.OrderStatus;
import com.pay.paymentdemo.enums.PayType;
import com.pay.paymentdemo.enums.wxpay.WxApiType;
import com.pay.paymentdemo.enums.wxpay.WxNotifyType;
import com.pay.paymentdemo.enums.wxpay.WxTradeState;
import com.pay.paymentdemo.mapper.OrderInfoMapper;
import com.pay.paymentdemo.service.OrderInfoService;
import com.pay.paymentdemo.service.PaymentInfoService;
import com.pay.paymentdemo.service.RefundInfoService;
import com.pay.paymentdemo.service.WxPayService;
import com.pay.paymentdemo.util.HttpClientUtils;
import com.pay.paymentdemo.util.OrderNoUtils;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
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.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private OrderInfoService orderInfoService;
@Resource
private PaymentInfoService paymentInfoService;
private ReentrantLock lock = new ReentrantLock();
@Resource
private RefundInfoService refundInfoService;
@Resource
private CloseableHttpClient wxPayNoSignClient; //无需应答签名
/*
* 创建订单,调用native支付接口
* @param productId
* @return code_url和订单号
* */
@Override
public Map<String, Object> nativePay(Long productId) throws Exception{
log.info("生成订单");
// 生成订单
// 用户信息这里没做:登录信息(token,redis)
// 存入数据库
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
// 存在未支付订单,那么返回该订单的二维码,避免重复创建订单以及调用统一下单API
String codeUrl = orderInfo.getCodeUrl();
if(orderInfo != null && !StringUtils.isNullOrEmpty(codeUrl)) {
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单API");
// 调用统一下单API
// 连接远程链接URL地址
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+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
@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");
// 在对业务数据进行状态检查和处理前,要才有数据锁进行并发控制
// 以避免函数重入造成的数据混乱
// 尝试获取锁:获取成功立即返回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();
}
}
}
/**
* 用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) throws Exception {
// 调用微信支付的关单接口
this.closeOrder(orderNo);
// 更新商户端的订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}
@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("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态,并更新支付日志
* 若订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态 ===> {}", orderNo);
// 调用微信支付查单接口
String result = this.queryOrder(orderNo);
Gson gson = new Gson();
Map resultMap = gson.fromJson(result, HashMap.class);
// 获取微信支付端的订单状态
Object tradeState = resultMap.get("trade_state");
// 判断订单状态
if(WxTradeState.SUCCESS.getType().equals(tradeState)) {
log.warn("核实订单已支付 ===> {}", orderNo);
// 若确认订单已支付,则更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
// 记录支付日志
paymentInfoService.createPaymentInfo(result);
}
if(WxTradeState.NOTPAY.getType().equals(tradeState)) {
log.warn("核实订单未支付 ===> {}", orderNo);
// 若确认订单未支付,则调用关单接口
this.closeOrder(orderNo);
// 记录本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
}
}
/**
* 退款
* @param orderNo
* @param reason
* @throws IOException
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) throws Exception {
log.info("创建退款单记录");
//根据订单编号创建退款单
RefundInfo refundsInfo = 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", 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);
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) {
log.info("成功");
} else {
throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
//更新退款单
refundInfoService.updateRefund(bodyAsString);
} finally {
response.close();
}
}
/**
* 查询退款接口调用
* @param refundNo
* @return
*/
@Override
public String queryRefund(String refundNo) throws Exception {
log.info("查询退款接口调用 ===> {}", refundNo);
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();
}
}
/**
* 处理退款单
*/
@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);
//更新退款单
refundInfoService.updateRefund(plainText);
} finally {
//要主动释放锁
lock.unlock();
}
}
}
/**
* 申请账单
* @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();
}
}
/**
* 下载账单
* @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();
}
}
@Override
public Map<String, Object> nativePayV2(Long productId, String remoteAddr) throws Exception {
log.info("生成订单");
//生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
String codeUrl = orderInfo.getCodeUrl();
if(orderInfo != null && !StringUtils.isNullOrEmpty(codeUrl)){
log.info("订单已存在,二维码已保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
log.info("调用统一下单API");
HttpClientUtils client = new HttpClientUtils(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY_V2.getType()));
//组装接口参数
Map<String, String> params = new HashMap<>();
params.put("appid", wxPayConfig.getAppid());//关联的公众号的appid
params.put("mch_id", wxPayConfig.getMchId());//商户号
params.put("nonce_str", WXPayUtil.generateNonceStr());//生成随机字符串
params.put("body", orderInfo.getTitle());
params.put("out_trade_no", orderInfo.getOrderNo());
//注意,这里必须使用字符串类型的参数(总金额:分)
String totalFee = orderInfo.getTotalFee() + "";
params.put("total_fee", totalFee);
params.put("spbill_create_ip", remoteAddr);
params.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY_V2.getType()));
params.put("trade_type", "NATIVE");
//将参数转换成xml字符串格式:生成带有签名的xml格式字符串
String xmlParams = WXPayUtil.generateSignedXml(params, wxPayConfig.getPartnerKey());
log.info("\n xmlParams:\n" + xmlParams);
client.setXmlParam(xmlParams);//将参数放入请求对象的方法体
client.setHttps(true);//使用https形式发送
client.post();//发送请求
String resultXml = client.getContent();//得到响应结果
log.info("\n resultXml:\n" + resultXml);
//将xml响应结果转成map对象
Map<String, String> resultMap = WXPayUtil.xmlToMap(resultXml);
//错误处理
if("FAIL".equals(resultMap.get("return_code")) || "FAIL".equals(resultMap.get("result_code"))){
log.error("微信支付统一下单错误 ===> {} ", resultXml);
throw new RuntimeException("微信支付统一下单错误");
}
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
/**
* 关单接口的调用
* @param orderNo
*/
public 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();
HashMap<String, String> paramMap = new HashMap<>();
paramMap.put("mchid", wxPayConfig.getMchId());
String jsonParams = gson.toJson(paramMap);
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();
}
}
/*
* 对称解密
* */
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 associated_data = resourceMap.get("associated_data");
log.info("密文 ====> {}", ciphertext);
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
String plainText = aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info("明文 ====> {}", plainText);
return plainText;
}
}
签名流程:
- 签名验证器@Bean:应用程序启动时就被初始化出来
- nativePay调用统一下单API时进行验签:wxPayClient.execute(http)。此步具体流程:
- 构造签名串(HTTP请求方法,url,请求时间戳,请求随机串,请求报文主体)
- 计算签名值:使用SHA256进行签名,再使用RSA对签名进行加密,最后使用Base64编码获得签名值
- 设置HTTP头:Authorization:认证类型(摘要算法WECHATPAY2-SHA256-RSA2048) 签名信息
验签原理:使用平台证书。响应(回调)必须验签,请求建议验签。应答和回调的签名验证使用的是微信支付平台证书。是平台证书列表,新旧平滑更替(定期进行证书下载和更新)。
验签流程:
- 构造验签名串(商户从应答中获取)
- 获取应答签名:Base64解码
- 验证签名:使用微信支付平台公钥解码得到签名,再使用摘要算法得到签名,对比验签
生成订单以及显示订单列表
将生成的订单存入数据库,当对同一个商品发起交易,且该商品还存在未支付订单且二维码未过期,则不会重复创建,而是直接将此未支付订单的支付二维码返回。按创建时间倒序显示订单列表。
OrderInfoController.java
package com.pay.paymentdemo.controller;
import com.pay.paymentdemo.entity.OrderInfo;
import com.pay.paymentdemo.enums.OrderStatus;
import com.pay.paymentdemo.service.OrderInfoService;
import com.pay.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@CrossOrigin // 开放前端的跨域访问
@Api(tags = "商品订单管理")
@RestController
@RequestMapping("/api/order-info")
@Slf4j
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@GetMapping("/list")
public R list() {
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list", list);
}
/*
* 查询本地订单状态
* */
@GetMapping("query-order-status/{orderNo}")
public R queryOrderStatus(@PathVariable String orderNo) {
log.info("查询订单状态");
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if(OrderStatus.SUCCESS.getType().equals(orderStatus)) {
return R.ok().setMessage("支付成功"); // 支付成功
}
return R.ok().setCode(101).setMessage("支付中...");
}
}
支付通知
支付结果:异步通知商户支付结果
微信如何向商户发送请求?这里使用的是局域网内网地址,需要使用内网穿透,使得微信可找到商户的外网地址。使用内网穿透工具ngrok。
ngrok:本地安全隧道,将本地服务器暴露在NAT或防火墙后面的互联网。本质是反向代理
接收微信端的回调通知,来更新订单状态。
应答异常和应答超时:若微信收到商户的应答不符合规范(非200)或超时,微信则认为通知失败,微信会通过一定策略定期重新发起通知
验签:对请求验签(请求头)(有专门的API对响应验签):这里仿照对响应验签实现对请求的验签
package com.pay.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) : "";
}
}
处理重复通知和接口调用的幂等性:
- 同样的通知可能会多次发送给商户系统,商户系统需要正确处理重要通知。
- 商户系统收到通知时,先检查对应业务数据,若已处理,则直接返回结果成功;负责再进行处理
- 接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的
数据锁:通知并发时,上面的判断处理也避免不了商户多次记录支付日志。检查业务数据前加锁
商户定时查单:
- 支付成功后,显示支付成功页面:前端设置定时器查单,若支付成功,则跳转支付成功页面,并取消定时器
关闭订单API:用户取消订单
- 调用微信支付的关单接口(SDK)
- 更新商户端的订单状态
因为网络原因,可能商户系统收不到回调通知,此时需要向微信查单
微信支付查询订单流程:
- 定时器引入定时任务:在主启动类加上注解@EnableScheduling。在定时任务类上加上@Component注解。方法上加@Scheduled(corn=定时策略)
- 查单接口:查找未支付且超时订单
- 修改订单状态
查询退款API:同样是定时任务
退款结果通知:回调通知,然后更新商户订单状态
账单:下载账单API。交易账单/资金账单
APIv2和APIv3区别
APIv3更安全。APIv2的某些功能,APIv3还未实现
WxPayV2Controller.java
package com.pay.paymentdemo.controller;
import com.pay.paymentdemo.config.WxPayConfig;
import com.pay.paymentdemo.entity.OrderInfo;
import com.pay.paymentdemo.enums.OrderStatus;
import com.pay.paymentdemo.service.OrderInfoService;
import com.pay.paymentdemo.service.PaymentInfoService;
import com.pay.paymentdemo.service.WxPayService;
import com.pay.paymentdemo.util.HttpUtils;
import com.pay.paymentdemo.vo.R;
import com.github.wxpay.sdk.WXPayUtil;
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 javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay-v2")
@Api(tags = "网站微信支付APIv2")
@Slf4j
public class WxPayV2Controller {
@Resource
private WxPayService wxPayService;
@Resource
private WxPayConfig wxPayConfig;
@Resource
private OrderInfoService orderInfoService;
@Resource
private PaymentInfoService paymentInfoService;
private final ReentrantLock lock = new ReentrantLock();
/**
* Native下单
* @param productId
* @return
* @throws Exception
*/
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R createNative(@PathVariable Long productId, HttpServletRequest request) throws Exception {
log.info("发起支付请求 v2");
String remoteAddr = request.getRemoteAddr();
Map<String, Object> map = wxPayService.nativePayV2(productId, remoteAddr);
return R.ok().setData(map);
}
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@PostMapping("/native/notify")
public String wxNotify(HttpServletRequest request) throws Exception {
System.out.println("微信发送的回调");
Map<String, String> returnMap = new HashMap<>();//应答对象
//处理通知参数
String body = HttpUtils.readData(request);
//验签
if(!WXPayUtil.isSignatureValid(body, wxPayConfig.getPartnerKey())) {
log.error("通知验签失败");
//失败应答
returnMap.put("return_code", "FAIL");
returnMap.put("return_msg", "验签失败");
String returnXml = WXPayUtil.mapToXml(returnMap);
return returnXml;
}
//解析xml数据
Map<String, String> notifyMap = WXPayUtil.xmlToMap(body);
//判断通信和业务是否成功
if(!"SUCCESS".equals(notifyMap.get("return_code")) || !"SUCCESS".equals(notifyMap.get("result_code"))) {
log.error("失败");
//失败应答
returnMap.put("return_code", "FAIL");
returnMap.put("return_msg", "失败");
String returnXml = WXPayUtil.mapToXml(returnMap);
return returnXml;
}
//获取商户订单号
String orderNo = notifyMap.get("out_trade_no");
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
//并校验返回的订单金额是否与商户侧的订单金额一致
if (orderInfo != null && orderInfo.getTotalFee() != Long.parseLong(notifyMap.get("total_fee"))) {
log.error("金额校验失败");
//失败应答
returnMap.put("return_code", "FAIL");
returnMap.put("return_msg", "金额校验失败");
String returnXml = WXPayUtil.mapToXml(returnMap);
return returnXml;
}
//处理订单
if(lock.tryLock()){
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if(OrderStatus.NOTPAY.getType().equals(orderStatus)){
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(body);
}
} finally {
//要主动释放锁
lock.unlock();
}
}
returnMap.put("return_code", "SUCCESS");
returnMap.put("return_msg", "OK");
String returnXml = WXPayUtil.mapToXml(returnMap);
log.info("支付成功,已应答");
return returnXml;
}
}
更多推荐
所有评论(0)