前言
很久没有对接小程序的支付了,看了下文档,多了一个apiv3支付,去看了其他博客的内容,总感觉少了点什么,幸好多方借鉴调试成功了,在此写下具体步骤,帮助各位顺利调通,减少不必要的麻烦
小程序发起请求
pay(e: { currentTarget: { dataset: { pay: any } } }) {
const pay = e.currentTarget.dataset.pay
console.log("pay>>>", pay)
const userInfo = app.globalData.userInfo
const data = {
openid: userInfo.openid,
personId: userInfo.personId,
integralId: pay.integralId,
total: pay.spend * 100
}
https.postReq("createOrder", JSON.stringify(data), (res) => {
console.log("请求回调", res)
const data = {
appId: res.appId,
timeStamp: res.timeStamp,
nonceStr: res.nonceStr,
package: res.package,
signType: res.signType,
paySign: res.paySign,
}
console.log("查看发送的data>>>", data)
wx.requestPayment({
appId: res.appId,
timeStamp: res.timeStamp,
nonceStr: res.nonceStr,
package: res.package,
signType: res.signType,
paySign: res.paySign,
success: (res) => {
console.log("res>>>", res)
},
fail: (res) => {
console.log("error", res)
}
})
})
},
新建一张订单表,记录订单信息
DROP TABLE IF EXISTS `pay_record`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `pay_record` (
`person_id` bigint(20) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT NULL,
`order_id` varchar(255) DEFAULT NULL COMMENT '支付单号',
`payer_total` int(11) DEFAULT NULL COMMENT '交易金额',
`trade_state` varchar(128) DEFAULT NULL COMMENT '交易状态',
`pay_id` bigint(20) NOT NULL AUTO_INCREMENT,
`remark` varchar(500) DEFAULT NULL,
`transaction_id` varchar(64) DEFAULT NULL COMMENT '微信后台生成的订单号',
`trade_type` varchar(16) DEFAULT NULL,
`total` int(11) DEFAULT NULL,
`success_time` timestamp NULL DEFAULT NULL,
`integral_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`pay_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
maven引入包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0.M2</version>
</dependency>
<!-- 导入微信支付v3工具包 -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
所需要的各个参数
public class WXPayConstants {
/*appid*/
public static final String APP_ID = "";
/*商户号*/
public static final String MCH_ID = "";
/* 商户证书序列号 */
public static final String MCH_SERIAL_NO = "";
/*APIv3密钥*/
public static final String API_V3KEY = "";
/*支付成功回调地址*/
public static final String NOTIFY_URL = "https://xxx.xxxx.xx/wxPayNotify";
/*序列号地址*/
public static final String PRIVATE_KEY_PATH = "/XXXX/apiclient_key.pem";
}
定义微信结果数据接收对象
@Data
public class WeChatPayResultIn {
private String id;
private String create_time;
private String resource_type;
private String event_type;
private String summary;
private WechatPayResourceIn resource;
}
@Data
public class WechatPayResourceIn {
private String original_type;
private String algorithm;
private String ciphertext;
private String associated_data;
private String nonce;
}
创建订单号请求对象(openid和total是必填,后面两个参数是自己逻辑所需,可按自己业务调整)
@Data
public class PayBody {
private String openid;
private Integer total;
private Long personId;
/**
* 所选积分规则id
*/
private Long integralId;
}
主要controller
@RestController
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@PostMapping(value = "/wx/createOrder")
public Object create(@RequestBody PayBody data) {
try {
// Integer total = data.getTotal();
Integer total = 1;
Map<String, Object> order = createOrder(data.getOpenid(), "支付", total);
// 新建预支付记录,这里可以新建一张记录支付信息的表。
return order;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@PostMapping(value = "/wxPayNotify")
public Map wechatPayCallBack(HttpServletRequest request,
@RequestBody WeChatPayResultIn weChatPayResultIn) {
Map result = new HashMap();
result.put("code", wxPayService.wechatPayCallBack(weChatPayResultIn,request));
return result;
}
}
工具类
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.RandomUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
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.util.ResourceUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class V3WXPayUtil {
public static Map<String, Object> createOrder(String openId, String description, Integer total) throws Exception {
try {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(ResourceUtils.getFile(WXPayConstants.PRIVATE_KEY_PATH)));
log.warn("获取私钥:=========>" + merchantPrivateKey.toString());
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
log.warn("获取证书管理器实例:=========>" + certificatesManager.toString());
// 向证书管理器增加需要自动更新平台证书的商户信息
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(WXPayConstants.MCH_ID,
new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey));
log.warn("获取管理器增加需要自动更新平台证书的商户信息前:=========>" + wechatPay2Credentials.toString() + "\n");
byte[] api_v3KEYBytes = WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8);
log.warn("获取api_v3KEYBytes:=========>" + api_v3KEYBytes);
certificatesManager.putMerchant(WXPayConstants.MCH_ID, wechatPay2Credentials, api_v3KEYBytes);
log.warn("证书管理器实例:=========>" + certificatesManager.toString());
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(WXPayConstants.MCH_ID);
log.warn("从证书管理器中获取verifier:=========>" + verifier);
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(WXPayConstants.MCH_ID, WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
.build();
log.warn("从证书管理器中获取verifier后:=========>");
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
String str_no = new DateTime().toString("yyyyMMddHHmmssSSS");
rootNode.put("mchid", WXPayConstants.MCH_ID)
.put("appid", WXPayConstants.APP_ID)
.put("notify_url", WXPayConstants.NOTIFY_URL)
.put("description", description)
.put("out_trade_no", str_no);
rootNode.putObject("amount")
.put("total", total) // 支付总金额,单位为分, 需前端传入
.put("currency", "CNY");
rootNode.putObject("payer")
.put("openid", openId);
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
log.warn("成功获取微信预支付订单号{}", bodyAsString);
System.out.println("成功获取微信预支付订单号==========>" + bodyAsString);
// 时间戳
String currentTimeMillis = System.currentTimeMillis() / 1000 + "";
// 随机字符串 hutool工具类
String randomString = RandomUtil.randomString(32);
StringBuilder builder = new StringBuilder();
// 应用ID
builder.append(WXPayConstants.APP_ID).append("\n");
// 时间戳
builder.append(currentTimeMillis).append("\n");
// 随机字符串
builder.append(randomString).append("\n");
JsonNode node = objectMapper.readTree(bodyAsString);
log.warn("从获取node后:=========>" + node);
// 订单详情扩展字符串
builder.append("prepay_id=").append(node.get("prepay_id").toString().replace("\"", "")).append("\n");
String paySign = sign(builder.toString().getBytes(), merchantPrivateKey);
Map<String, Object> map = new HashMap();
map.put("package", "prepay_id=" + node.get("prepay_id").toString().replace("\"", ""));
map.put("prepayId", node.get("prepay_id").toString().replace("\"", ""));
map.put("nonceStr", randomString);
map.put("signType", "RSA");
map.put("paySign", paySign);
map.put("timeStamp", currentTimeMillis);
map.put("appId", WXPayConstants.APP_ID);
map.put("orderId", str_no);
return map;
} catch (Exception e) {
log.warn("异常:" + getStackTraceMessage(e));
throw e;
}
}
public static String getStackTraceMessage(Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
e.printStackTrace(pw);
pw.flush();
sw.flush();
return sw.toString();
}
public static String sign(byte[] message, PrivateKey privateKey) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
public static boolean signVerify(String serial, String message, String signature) {
// 从证书管理器中获取verifier
Verifier verifier = null;
try {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(ResourceUtils.getFile(WXPayConstants.PRIVATE_KEY_PATH)));
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(WXPayConstants.MCH_ID, new WechatPay2Credentials(WXPayConstants.MCH_ID,
new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)), WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(WXPayConstants.MCH_ID);
return verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature);
} catch (IOException e) {
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (HttpCodeException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
}
return false;
}
}
支付回调的参数处理及解密
package cn.lasons.eps.wxpay;
import cn.lasons.eps.mapper.PayRecordMapper;
import cn.lasons.eps.model.Integral;
import cn.lasons.eps.model.IntegralAdmin;
import cn.lasons.eps.model.PayRecord;
import cn.lasons.eps.model.auto.PersonInformation;
import cn.lasons.eps.service.IIntegralAdminService;
import cn.lasons.eps.service.IIntegralService;
import cn.lasons.eps.service.IPersonInformationService;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WxPayService {
@Resource
private PayRecordMapper payRecordMapper;
@Resource
private IIntegralAdminService iIntegralAdminService;
@Resource
private IIntegralService iIntegralService;
@Resource
private IPersonInformationService iPersonInformationService;
@Transactional
public Map wechatPayCallBack(WeChatPayResultIn weChatPayResultIn, HttpServletRequest request) {
log.error("微信结果通知字符串:{}" + JSON.toJSONString(weChatPayResultIn));
String nonce = weChatPayResultIn.getResource().getNonce();
String ciphertext = weChatPayResultIn.getResource().getCiphertext();
String associated_data = weChatPayResultIn.getResource().getAssociated_data();
AesUtil aesUtil = new AesUtil(WXPayConstants.API_V3KEY.getBytes());
Map result = new HashMap();
result.put("code", "FAIL");
BufferedReader reader = null;
try {
//验签
StringBuilder signStr = new StringBuilder();
// 请求时间戳\n
signStr.append(request.getHeader("Wechatpay-Timestamp")).append("\n");
// 请求随机串\n
signStr.append(request.getHeader("Wechatpay-Nonce")).append("\n");
reader = request.getReader();
log.error("reader:=========>" + reader.toString());
String str = null;
StringBuilder builder = new StringBuilder();
while ((str = reader.readLine()) != null) {
builder.append(str);
}
log.error("请求报文主体: {}", builder);
signStr.append(builder.toString()).append("\n");
// 1.验签
if (!V3WXPayUtil.signVerify(request.getHeader("Wechatpay-Serial"), signStr.toString(), request.getHeader("Wechatpay-Signature"))) {
result.put("message", "sign error");
log.error("验签失败-sign error");
return result;
}
// 解密请求体
String s = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
log.error("解密结果");
log.error(s);
JSONObject jsonObject = JSONObject.parseObject(s);
log.error("json格式化的结果");
log.error(jsonObject.toJSONString());
//订单号
String orderId = jsonObject.getString("out_trade_no");
//微信支付订单号
String transactionId = jsonObject.getString("transaction_id");
String tradeState = jsonObject.getString("trade_state");
//幂等性
if (StringUtils.equals(tradeState, "USERPAYING")) {
//还在支付中,不做处理
return null;
}
if (StringUtils.equals(tradeState, "SUCCESS")) {
QueryWrapper<PayRecord> payRecordQueryWrapper = new QueryWrapper<>();
payRecordQueryWrapper.eq("order_id", orderId);
PayRecord yhjOrderInfo = payRecordMapper.selectOne(payRecordQueryWrapper);
// 支付成功,将订单状态改为1
if (yhjOrderInfo != null && yhjOrderInfo.getPayId() > 0) {
JSONObject amount = jsonObject.getJSONObject("amount");
//总金额
Integer total = amount.getInteger("total");
//用户实际支付金额
Integer payerTotal = amount.getInteger("payer_total");
yhjOrderInfo.setTotal(total);
yhjOrderInfo.setPayerTotal(payerTotal);
Timestamp success_time = jsonObject.getTimestamp("success_time");
yhjOrderInfo.setSuccessTime(success_time == null ? new Timestamp(System.currentTimeMillis()) : success_time);
yhjOrderInfo.setTransactionId(transactionId);
yhjOrderInfo.setTradeState(tradeState);
payRecordMapper.updateById(yhjOrderInfo);
PersonInformation byId = iPersonInformationService.getById(yhjOrderInfo.getPersonId());
IntegralAdmin integralByTotal = iIntegralAdminService.getById(yhjOrderInfo.getIntegralId());
Integral integral = new Integral();
integral.setPersonId(yhjOrderInfo.getPersonId());
integral.setWay("用户充值" + integralByTotal.getSpend() + ";获得积分+" + integralByTotal.getIntegral());
byId.setIntegral(byId.getIntegral() == null ? integralByTotal.getIntegral() : byId.getIntegral() + integralByTotal.getIntegral());
iPersonInformationService.updateById(byId);
iIntegralService.save(integral);
}
result.put("code", "OK");
return result;
}
} catch (GeneralSecurityException e) {
log.error("微信支付回调数据处理及自己项目逻辑异常", e);
result.put("message", e.getMessage());
} catch (IOException e) {
log.error("微信支付回调获取body流异常", e);
result.put("message", e.getMessage());
}
return result;
}
}
注意
在使用reader = request.getReader();的时候会报错getInputStream() has already been called for this request,报这个错,说明在进入Controller这个过程前数据已经被读取了一次。为了解决这个问题,需要在拦截器中进行处理。
解决办法
由于采用采用application/json传输参数时,HttpServletRequest只能读取一次 body 中的内容。因为是读的字节流,读完就没了,因此需要需要做特殊处理。为实现述多次读取 Request 中的 Body 内容,需继承 HttpServletRequestWrapper 类,读取 Body 的内容,然后缓存到 byte[] 中;这样就可以实现多次读取 Body 的内容了。
具体如下
package cn.lasons.eps.config;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class RequestBodyReaderFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
try {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) request);
}
if (requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
}
@Override
public void destroy() {
}
}
package cn.lasons.eps.config;
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class RequestWrapper extends HttpServletRequestWrapper {
//参数字节数组
private byte[] requestBody;
//Http请求对象
private HttpServletRequest request;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.request = request;
}
@Override
public ServletInputStream getInputStream() throws IOException {
/**
* 每次调用此方法时将数据流中的数据读取出来,然后再回填到InputStream之中
* 解决通过@RequestBody和@RequestParam(POST方式)读取一次后控制器拿不到参数问题
*/
if (null == this.requestBody) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(), baos);
this.requestBody = baos.toByteArray();
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return bais.read();
}
};
}
public byte[] getRequestBody() {
return requestBody;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
package cn.lasons.eps.config;
import java.util.ArrayList;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<RequestBodyReaderFilter> fequestBodyReaderFilter() {
FilterRegistrationBean<RequestBodyReaderFilter> registrationBean = new FilterRegistrationBean<>();
RequestBodyReaderFilter filter = new RequestBodyReaderFilter();
registrationBean.setFilter(filter);
ArrayList<String> urls = new ArrayList<>();
urls.add("/*");//配置过滤规则
registrationBean.setUrlPatterns(urls);
registrationBean.setOrder(3);
return registrationBean;
}
}