在进行JSAPI微信支付之前,需要准备好一下配置
申请小程序的appid:wxaxxxxxxxxxxbxx8a (类似这样的)
申请商户号:1xxxxxxxxx6
小程序开通微信支付,绑定已经申请好的商户号。登录小程序后台(mp.weixin.qq.com)。点击左侧导航栏的微信支付,在页面中进行开通。(
注意:以上信息的申请都需要使用企业账户,个人账户不行
商户号官网地址:pay.weixin.qq.com
小程序官网地址: mp.weixin.qq.com
- 需要在商户端(pay.weixin.qq.com),api安全配置好apiv3的密钥
博主这篇博客,主要是小程序对接微信支付(JSAPI)
后端:spring boot
前端:微信小程序,uinapp
适用人群:已经申请好所有的资料,小程序平台,微信商户平台等等,本文不提供任何资料。并且需要有自己的业务场景,部分代码无法直接运行,需要加入自己的订单结构
<dependencies>
<dependency>
<groupId>org.jdomgroupId>
<artifactId>jdom2artifactId>
<version>2.0.6.1version>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
dependency>
<dependency>
<groupId>com.github.wechatpay-apiv3groupId>
<artifactId>wechatpay-apache-httpclientartifactId>
<version>0.4.7version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.25version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
1-1、如上图,第2点请求下单,访问我们自己的后端接口所需的参数
字段名 | 变量名 | 类型 | 必填 | 示例值 | 描述 |
---|---|---|---|---|---|
应用ID | appid | string[1,32] | 是 | wxd678efh567hg6787 | 由微信生成的应用ID,全局唯一。请求基础下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的服务号APPID |
直连商户号 | mchid | string[1,32] | 是 | 1230000109 | 直连商户的商户号,由微信支付生成并下发。 |
商品描述 | description | string[1,127] | 是 | Image形象店-深圳腾大-QQ公仔 | 商品描述 |
商户订单号 | out_trade_no | string[6,32] | 是 | 1217752501201407033233368018 | 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一 |
通知地址 | notify_url | string[1,256] | 是 | 可以先随便写一个不存的地址都行,不影响正常支付,但是获取不到支付结果信息,无法进行修改订单状态 | 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http |
订单金额 | amount | object | 是 | HashMap | 订单金额信息,他需要一个map,需要进行一层嵌套,可以去参考官网https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml |
支付者 | payer | object | 是 | //支付者 HashMap | 支付者信息,用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid |
以上参数需要注意,openid的获取
需要在小程序端调用wx.login获取临时登陆凭证在获取openid
wx.login({
success (res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://example.com/onLogin',//这个接口是需要自己去编写的。也就是下方的那个controller @GetMapping("/onLogin") public string onLogin(HttpServletRequest request)方法
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
通过上面这个方法获取到res.code然后我们自己在编写一个后端接口,去获取openid
下面这个是我自己写的controller
@Resource
private WxPayConfig wxPayConfig;//这个是一个wx的配置类
@Resource
private CloseableHttpClient wxPayClient;//配置类中的一个bean
@GetMapping("/onLogin")
public string onLogin(HttpServletRequest request){
String js_code = request.getParameter("code");//前端发起请求携带上面获取到的code,后端接收
//app Secret是小程序密钥(在mp.weixin.qq.com中的开发管理-》开发设置-》AppSecret(小程序密钥)中设置)
String baseUrl="https://api.weixin.qq.com/sns/jscode2session?appid="+wxPayConfig.getAppid()+"&secret="+wxPayConfig.getAppSecret()
+"&js_code="+js_code+"&grant_type=authorization_code";
String res=null;
try {
//这里发起请求获取到session-key,和openid
res = requestByGetMethod(baseUrl).split("/n")[0];
System.out.println(res);
} catch (Exception e) {
e.printStackTrace();
}
log.info("res:"+res);
return res;//返回给前端
}
//这个方法就是用于发起get请求的
/**
* 模拟发送url Get 请求
* @param url
* @return
*/
public String requestByGetMethod(String url) {
log.info("发起get请求");
CloseableHttpClient httpClient = HttpClients.createDefault();
StringBuilder entityStringBuilder = null;
try {
HttpGet get = new HttpGet(url);
CloseableHttpResponse httpResponse = null;
httpResponse = httpClient.execute(get);
try {
HttpEntity entity = httpResponse.getEntity();
entityStringBuilder = new StringBuilder();
if (null != entity) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent(), "UTF-8"), 8 * 1024);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
entityStringBuilder.append(line + "/n");
}
}
} finally {
httpResponse.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return entityStringBuilder.toString();
}
调用下单接口,返回prepay_id等信息提供给前端,供前端调起支付页面,这里也对应官方图的第二点下单请求
注意:在请求中你需要携带一下参数,具体需要的参数可以看1-1的表格
下面这个controller是在前端
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private Verifier verifier;
@ResponseBody
@RequestMapping("returnparam")
public HashMap<String, String> doOrder(HttpServletRequest request, HttpServletResponse response) throws Exception{
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
//得到openid(微信用户唯一的openid)
String openid = request.getParameter("openid");
//得到价钱(自定义)
int fee = 0;//单位是分
if (null != request.getParameter("price")) {
fee = Integer.parseInt(request.getParameter("price").toString());
}
//得到商品的ID(自定义)
String goodsid=request.getParameter("goodsid");
//订单标题(自定义)
String title = request.getParameter("title");
//时间戳,
String times = System.currentTimeMillis() + "";
//订单编号(自定义 这里以时间戳+随机数)
Random random = new Random();
String did = times+random.nextInt(1000);
log.info("生成订单");
//调用统一下单API
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
// 请求body参数
Gson gson = new Gson();
HashMap<Object, Object> paramsMap = new HashMap<>();
paramsMap.put("appid",wxPayConfig.getAppid());//appid
paramsMap.put("mchid",wxPayConfig.getMchId());//商户号
paramsMap.put("description",title);//商品描述
paramsMap.put("out_trade_no",did);//商户订单号
paramsMap.put("notify_url","http://d4a93w.natappfree.cc/wxBuy");//通知地址,可随便写,如果不需要通知的话,不影响支付,但是影响后续修改订单状态
//订单金额
HashMap<Object, Object> amountMap = new HashMap<>();
amountMap.put("total",fee);//金额
amountMap.put("currency","CNY");//货币类型
paramsMap.put("amount",amountMap);
//支付者
HashMap<Object, Object> playerMap = new HashMap<>();
playerMap.put("openid",openid);
paramsMap.put("payer",playerMap);
//将参数转化未json字符串
String jsonParamsMap = gson.toJson(paramsMap);
log.info("请求参数:"+jsonParamsMap);
StringEntity entity = new StringEntity(jsonParamsMap,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse resp = wxPayClient.execute(httpPost);
try {
int statusCode = resp.getStatusLine().getStatusCode();
String bodyAsString = EntityUtils.toString(resp.getEntity());
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
System.out.println("小程序下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//相应结果
HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//获取prepay—id
String prepayId = resultMap.get("prepay_id");
//获取到perpayid之后需要对数据进行二次封装,前端调起支付必须存在的参数
HashMap<String, String> payMap = new HashMap<>();
payMap.put("appid",wxPayConfig.getAppid());//appid
long currentTimestamp = System.currentTimeMillis();//时间戳,别管那么多,他就是需要
payMap.put("timeStamp",currentTimestamp+"");
String nonceStr = UUID.randomUUID().toString()
.replaceAll("-", "")
.substring(0, 32);;//随机字符串,别管那么多他就是需要,要咱就给
payMap.put("nonceStr",nonceStr);
//apiv3只支持这种加密方式
payMap.put("signType","RSA");
payMap.put("package","prepay_id="+prepayId);
//通过appid,timeStamp,nonceStr,signType,package以及商户密钥进行key=value形式进行拼接加密
//加密方法我会放在这个代码段段下面
String aPackage = buildMessageTwo("传入你的appid", currentTimestamp, nonceStr, payMap.get("package"));
//获取对应的签名
//加密方法我会放在这个代码段段下面
String paySign = sign(wxPayConfig.getPrivateKeyPath(),aPackage.getBytes("utf-8"));
payMap.put("paySign",paySign);
/**
* 在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了
* 在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了
* 在这里你可以加入自己的数据库操作,存储一条订单信息,状态为未支付就行了
*/
log.info("给前端的玩意:"+payMap);//前端会根据这些参数调起支付页面
//到这里,就已经完成了官网图中的第8步了
return payMap;
}finally {
resp.close();
}
}
注意:下面这个配置文件需要读取resources中的wxpay.properties配置文件,等下我也会把我的wxpay.properties贴到下方,还有一个证书文件,需要放置在与src同级的目录中《apiclient_key.pem》,这个文件的获取在https://pay.weixin.qq.com/index.php/core/cert/api_cert#/中申请API证书,也就是申请APIV3的那个页面
一定要记得把证书放入到项目目录中!!!
aoiclient_key.pem就是证书,放置在与src同级目录中
下方这个是一个properties文件,跟yml配置同级
package com.wanliu.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.cert.CertificatesManager;
//import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
@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;
// APIv2密钥
private String partnerKey;
//小程序密匙
private String appSecret;
/**
* 获取商户的私钥文件
* @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 Verifier getVerifier() throws Exception {
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
// 私钥签名对象
PrivateKeySigner keySigner = new PrivateKeySigner(mchSerialNo, privateKey);
// 身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, keySigner);
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(mchId, wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
Verifier verifier = certificatesManager.getVerifier(mchId);
return verifier;
}
/**
* 获取http请求对象
* @param verifier
* @return
*/
@Bean("wxPayClient")
public CloseableHttpClient getWxPayClient(Verifier verifier){
log.info("获取httpclient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
// 从证书管理器中获取verifier
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
log.info("== getWxPayNoSignClient END ==");
return httpClient;
}
public String decryptFromResource(Map<String,Object> bodyMap) throws GeneralSecurityException {
log.info("秘文解密");
//通知数据
Map<String,String > resourceMap =(Map<String, String>) 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(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;
}
/**
* 拼接五个参数
* @param appId
* @param timestamp
* @param nonceStr
* @param packag
* @return
*/
public String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
return appId + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ packag + "\n";
}
/**
* 进行二次封装
* @param wxCertPath
* @param message
* @return
* @throws NoSuchAlgorithmException
* @throws SignatureException
* @throws IOException
* @throws InvalidKeyException
* @throws java.security.InvalidKeyException
*/
public String sign(String wxCertPath,byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException, java.security.InvalidKeyException {
Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSA
sign.initSign(PemUtil.loadPrivateKey(new FileInputStream(wxCertPath))); // 微信证书私钥
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 获取32位随机字符串
* @return
*/
public String getNonceStr(){
return UUID.randomUUID().toString()
.replaceAll("-", "")
.substring(0, 32);
}
/**
* 获取当前时间戳,单位秒
* @return
*/
public long getCurrentTimestamp() {
return System.currentTimeMillis()/1000;
}
}
我的wxpay.properties,这里我没有用完自己小程序的配置,这里使用的是尚硅谷的,谷粒学院的公众号的配置,请各位修改成自己小程序的配置,这点很重要!!!
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B # 你申请的APIv3密钥
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://500c-219-143-130-12.ngrok.io
# APIv2密钥
wxpay.partnerKey=T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
#小程序密匙
wxpay.appSecret=你的小程序密钥,在mp.weixin.qq.com中的开发设置里面,appid的下面可以去配置,或重置
httpUtils解析通知信息
这个HttpUtils主要是用于解析notify_url回调地址,传回来的数据,解析请求头中的数据,微信官方会告诉你支付结果,并由你根据这个结果的状态做一些处理,比如修改数据库订单状态或者其他的
httpUtils主要是解析微信给我们的通知信息,有什么(强调,解析出来的东西也有加密的信息,需要再次解密,也就是验签的过程了)
注意:你接收到通知后,需要应答微信官方,否则微信官方会认为通知失败,然后在24h4m内,反复通知你
package com.wanliu.paymentdemo.util;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
public class HttpUtils {
/**
* 将通知参数转化为字符串
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
WechatPay2ValidatorForRequest验签
WechatPay2ValidatorForRequest就是从微信官方发给我的加密信息,进行验签解密。大家主要看WechatPay2ValidatorForRequest中的validate方法即可
package com.wanliu.paymentdemo.util;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import org.apache.http.Header;
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(WechatPay2Validator.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) : "";
}
}
在这里调起支付页面之后,用户进行输入密码,支付等等,如果支付那么微信商户平台会收到你的汇款,然后微信官方会发起请求通知你支付状态,也就是刚刚你在统一下单的时候设置的notify_url参数的接口地址
这里的必要参数也就是第三步统一下单那个步骤中的,@RequestMapping(“returnparam”)接口返回的参数
uni.request({//这个是uniapp版本,下方我会贴微信小程序版本,微信小程序的往下看
url: that.$api.baseURL+"/returnparam",//你的后端接口
method:"GET",
data:{//要传入的参数
//openid
openid: openId,
//订单总金额
price: order.amount,
//订单号
goodsid: order.number,
//title
title: ''
},//上面这里是去调用下单
success: (res2) => {//这里是访问returnparam接口成功后就会调起支付
console.log(res2);
uni.requestPayment({//这里是官方图中的第9步,这里是uniapp官方的调起支付方法
//后端返回的参数,这里下面就是returnparam接口返回的参数
timeStamp: res2.data.data.timeStamp,
nonceStr: res2.data.data.nonceStr,
package: res2.data.data.package,
signType: res2.data.data.signType,
paySign: res2.data.data.paySign,
appId: res2.data.data.appid,
success (res3) {
console.log(res3);
},
fail (res3) {
console.log(res3);
}
})
}
})
这里就是微信小程序的一个请求过程
let result = await request({
url: '/returnparam',
method: 'get',
data: {
//openid
openid:wx.getStorageSync("openid"),
//订单总金额
price:this.data.price,
//订单号
goodsid:wx.getStorageSync("orderCode"),
//title
title: '订单描述',
},
});
console.log(result);
if(result.appid!=null){
wx.requestPayment({
timeStamp:result.timeStamp,
nonceStr:result.nonceStr,
package: result.package,
signType: result.signType,
paySign: result.paySign,
appId: request.appid,
success:function(res){
wx.showToast({
title: '支付成功!',
}),
setTimeout(()=>
{
wx.redirectTo({
url: '/pages/index/index'
})
}, 3000)
},
fail :function(res) {
console.log("调用失败");
console.log(res);
},
complete:function(res){
console.log("成功与否");
}
})
这个一定要是外网可以访问的地址,建议使用内网穿透
给大家推荐一篇内网穿透的博客:https://blog.csdn.net/Lfl202116888/article/details/124932062?spm=1001.2014.3001.5502
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private Verifier verifier;
/**
* 微信通知回调地址
* @param request
* @param response
* @return
*/
@PostMapping("/wxBuy")
public String wxBuy(HttpServletRequest request,HttpServletResponse response){
Gson gson = new Gson();
//创建一个应答对象
HashMap<String, String> map = new HashMap<>();
try {
//处理通知参数
String body = HttpUtils.readData(request);
HashMap<String,Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String) bodyMap.get("id");
log.info("支付通知的id=====》》》{}",bodyMap.get("id"));
// log.info("支付通知的完整数据=====》》》{}",body);
//TODO : 签名的验证
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("通知验签成功");
//TODO : 处理订单
//这里可以调用你要处理业务逻辑的service,我这里就不写了,
//然后解密数据在业务service中调用就行,我在这里就直接调用了
//解密密文,获取明文
String plaintText = wxPayConfig.decryptFromResource(bodyMap);
//将明文转为map
HashMap plaintTextMap = gson.fromJson(plaintText, HashMap.class);
//获取支付下单的时候,传入的商户订单号,可以根据这个订单号去获取我们的一个订单记录,从而更新订单状态
String orderNo = (String) plaintTextMap.get("out_trade_no");
//业务编号
String transactionId = (String) plaintTextMap.get("transaction_id");
//trade_type,支付类型,如果有需要的话, 你可以存储在数据库中,这里我们的数据,基本上都是JSapi支付类型
String tradeType = (String) plaintTextMap.get("trade_type");
//交易状态
String tradeState = (String) plaintTextMap.get("trade_state");
//还有很多,为这里就不一一去写了
/**
* 在更新你订单状态之前,可以先根据orderNo,查询数据库中是否有这个订单
* 然后查询这个订单是否已经被处理过,也就是状态是否是已经支付的状态
* 如果这个订单已经被处理了,那么我们可以直接return,没有被处理过我们在处理
* 这样可以避免数据库被反复的操作
*
* 微信官方的解释是:
* 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
* 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,
* 并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,
* 则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,
* 以避免函数重入造成的数据混乱。
*/
//更新订单状态
/*
* 你的数据库操作
* 一定要存储解密出来的transaction_id字段在数据库中
* 如果需要跟微信官方对账的话,是需要提供这个字段进行一个查账的
* */
//成功应答
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);
}
}
在上面这个controller中,处理订单那个部分,其实还有很多参数可以去获取,根据业务需要求,我这里只是举例,获取了几个比较重要的,具体详细可以去官网中查看:微信支付-开发者文档 (qq.com)
还有具体的业务,我也没有去写了,大家根据你们自己的业务去完成即可
支付成功跳转支付成功的页面,然后清除定时器停止循环查单,如果用户一直没有支付的话,定时器反复查单,接口只会返回一个101的状态码,和一个支付中的状态信息。如果用户退出支付页面,也要记得关闭定时器.。这里的查单是查询我们自己数据库中的订单状态不是,微信官方的查单
因为我这里没有具体的业务操作,所以我吧大体思路给大家写了一下,大家可以参考一下
/**
* 提供给前端查询支付订单状态的接口
* @param orderNo
* @return
*/
@GetMapping("/query-order-status/{orderNo}")
public R queryOderStatus(@PathVariable("orderNo") String orderNo){
//调用你的service去根据orderNo*(订单id)去获取当前订单
//if判断,你根据订单id查询到到订单对象中到订单状态是否为成功状态(已经支付)
//成功返回,状态码200,已经消息,支付成功
//如果状态不是已经支付,则返回状态码101(这个状态码自己自己设定,只是提供给前端做判断的),消息,支付中
}
用户如果取消订单的话,那么要干两件事情
- 调用微信支付官方的,关单接口
- 更改为我们自己数据库中,这条订单的订单状态为,取消订单
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws Exception {
log.info("取消订单");
//调用service业务,我这里为了给大家方便掩饰,就直接写controller中了
//调用微信支付的关单接口
closeOrder(orderNo);
//修改我们自己数据库中的订单状态为订单已取消
//这里我没有具体业务,我就不写了,大家根据自己的情况去写
return R.ok().setMessage("订单已取消");
}
public void closeOrder(String orderNo) throws Exception {
log.info("关单接口的调用,订单号===》{}",orderNo);
//微信官方提供的关单url,我们进行一个拼串
String url="https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+orderNo+"/close";
HttpPost httpPost = new HttpPost(url);
//在请求体中还需要携带商户号
Gson gson = new Gson();
HashMap<String, String> paramsMap = new HashMap<>();
paramsMap.put("mchid",wxPayConfig.getMchId());
//参数都组装完成之后,需要转为json
String jsonParamsMap = gson.toJson(paramsMap);
//将请求参数设置到请求对象中
StringEntity entity = new StringEntity(jsonParamsMap,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse resp = wxPayClient.execute(httpPost);
try {
int statusCode = resp.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
log.info("成功200," );
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功204");
} else {
System.out.println("小程序下单失败,响应码 = " + statusCode );
throw new IOException("request failed");
}
}finally {
resp.close();
}
}
如果我们商户后台,迟迟没有收到异步通知的结果,那么我们应该主动的去调用微信官方的查单接口,查询这个订单到底有没有支付成功,如果成功了我们就要去修改我们的数据库中这条订单的,订单状态
为什么要这么做呢?
因为有的时候,用户支付成功之后,可能因为网络的一个延迟啊,或者有些其他的原因导致,消息没有及时的更新,微信官方,也没有及时的给我们发送一个通知,没有发送通知的话,我们商户端,是不可能去修改用户的这个订单为,已支付的一个状态。
所以我们如果一直没有收到这个订单的消息,我们需要主动的查询这个订单的一个情况
所以说,这个查单的这个流程,肯定是需要我们在后端写一个定时任务去调用的
这里只是一个查询订单的一个接口,如果有需要的话大家可以看看
@GetMapping("/query/{orderNo}")
public R queryOrder(@PathVariable String orderNo) throws Exception {
String result = findOrder(orderNo);
return R.ok().setMessage("查询成功").data("result",result);
}
public String findOrder(String orderNo) throws Exception {
log.info("查询订单");
// 同理,这里应该是调用业务方法。为了方便演示,我直接写在controller中
String url="https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+orderNo+"?mchid="+wxPayConfig.getMchId();
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse resp = wxPayClient.execute(httpGet);
try {
int statusCode = resp.getStatusLine().getStatusCode();
String bodyAsString = EntityUtils.toString(resp.getEntity());
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
System.out.println("小程序下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
log.info("bodyAsString:"+bodyAsString);
return bodyAsString;
}finally {
resp.close();
}
}
这个才是定时任务,开始轮询查单
注意:这里开启定时任务的这个注解,一定要先在spring boot的启动类上加一个注解:@EnableScheduling
/**
* 从第0秒开始每隔30秒执行一次,查询创建超过5分钟,并且没有支付第订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
//这里还是一样,应该是去调用业务方法,为了演示我直接写出来
//TODO :查询创建超过5分钟并且没有支付的订单
Instant instant=Instant.now().minus(Duration.ofMinutes(5));//得到五分钟之前的时间
//创建查询对象
QueryWrapper<OrderInfo> queryWrapper=new QueryWrapper<>();
//拼接查询调教
queryWrapper.eq("order_status","未支付");//这里的未支付,应该是写枚举,或者0,1,2这些数据的,这里为了一眼能让大家看出来我就直接写了,状态=未支付
queryWrapper.le("create_time",instant);
//这里调用查询所以订单的方法,吧queryWrapper传入进去,得到一个list,便是所有超过五分钟,并且未支付的订单了
//这里我就模拟一个结果出来,当然他没有数据
List<OrderInfo> orderInfoList=new ArrayList<>();
//这里查询出所有的超时订单之后,我们应该去核实一下,这些超时的订到单,是不是真的都是未支付的
//所以我们需要去调用,微信官方的,查单接口
//因为这里需要去判断,是用户真的没有支付,还是用户支付了,但是微信官方给我们发起的通知,我们没有接收到,导致订单状态没有被更改
//如果是用户没有支付,那我们需要对这个订单进行一个关单,并修改状态为超时
//如果用户支付了,但是是因为我们没有收到通知,那么我们则主动查单,修改订单状态为支付成功
for (OrderInfo orderInfo : orderInfoList) {
//直接调用写好对查单方法即可
String result = findOrder(orderInfo.getOrderNo());
Gson gson = new Gson();
HashMap hashMap = gson.fromJson(result, HashMap.class);
//获取微信官方的订单状态,获取到到数据应该是"trade_state": "SUCCESS",这样的
Object trade_state = hashMap.get("trade_state");
//判断订单状态
//这里所有的状态还是建议写枚举,然后根据枚举去做判断
//如果查询出来的这个订单是已经支付的话,那么我们吧我们自己的数据库中的订单状态也改成已经支付
//如果判断是未支付的话,那么就要调用微信的关单接口,也就是我的博客中的第9点用户取消订单中的closeOrder方法去进行关单
//并且修改我们自己数据库中的订单状态为超时关闭
}
}
申请退款的流程,跟支付是差不多的
先申请退款,然后微信收到退款申请后,退款
退款成功后,给我们商户端发起一个通知
我们接收通知,修改状态为退款成功,并应答微信官方,说我们已经收到你的通知了
如因为其他原因商户端没有收到异步通知,如博客中第10步,也是一样的,进行一个查单
//《orderNo》是我们自己生成的订单号
//《reason》是退款理由
//这里退款有两种模式,
//第一种是使用我们生成的订单号进行查询并退款
//第二种就是,在第三步的时候,微信会返回给我们一个微信官方的订单号进行退款
//这里我选择的是第一种方式
@PostMapping("/refunds/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo,@PathVariable String reason) throws IOException {
log.info("申请退款");
//先根据订单号去查询到,当前要退款到这个订单
//我这里就创建一个空对象来代替
OrderInfo orderInfo = new OrderInfo();
//这里还要创建一个退款单对象
RefundInfo refundInfo = new RefundInfo();
//设置退款单中有那些信息
refundInfo.setOrderNo(orderNo);//订单编号
refundInfo.setRefundNo(wxPayConfig.getNonceStr());//退款单号,这里的退款单号,也是我们自己生成的订单,供保存退款信息的,微信那边也需要我们提供一个这个单号,一般的业务都会需要保存一个退款的信息,所以这里需要这个退款单号
refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额
refundInfo.setRefund(orderInfo.getTotalFee());//要退款的金额
refundInfo.setReason(reason);//退款原因
//大家的退款单对象,可以根据自己的业务需求去设计。退款单也是需要一张表去存储的
//这里去调用数据库操作,去保存refundInfo这样的一条退款单信息
//保存好退款到记录之后
//调用退款Api进行退款操作
log.info("调用退款APi");
String url="https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
HttpPost httpPost = new HttpPost(url);
//组装请求参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("out_trade_no",orderNo);//订单编号
paramsMap.put("out_refund_no",refundInfo.getRefundNo());//退款单号
paramsMap.put("reason",reason);//退款原因
paramsMap.put("notify_url","http://d4a93w.natappfree.cc/refunds/notify");//退款成功异步回调地址
Map amountMap = new HashMap<>();
//正确对写法应该是这样,但是我上吗创建的是空对象,所里这里我需要吧退款金额之类的写死
// amountMap.put("refund",refundInfo.getRefund());//退款金额
// amountMap.put("total",refundInfo.getTotalFee());//原订单金额、
amountMap.put("refund",1);//退款金额
amountMap.put("total",1);//原订单金额
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 resp = wxPayClient.execute(httpPost);
try {
//解析响应结果
String bodyAsString = EntityUtils.toString(resp.getEntity());
int statusCode = resp.getStatusLine().getStatusCode();
if (statusCode == 200) {
log.info("退款成功,退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("退款异常,响应码 = " + statusCode + ",退款返回结果 = " + bodyAsString);
}
//更新订单状态为退款中
//这里大家自己去写数据库操作更改状态
//更新退款单,根据退款的bodyAsString的数据,获取你想要保留的参数,保存到退款单中,以便后续的退款
//比如退款单号(微信官方的),可以供我们后续如果对这笔退款有疑问,可以进行一个退款查询
return R.ok();
}finally {
resp.close();
}
}
这里给大家看看我的这个退款过程
这里的退款参数具体的大家可以去看看官网上,有哪些是你们需要存入退款单中的:微信支付-开发者文档 (qq.com)
这个是在申请退款中notify_url填写的这个参数的地址,会被微信回调的接口。微信回告知我们退款的成功的信息,需要我们进行接收,解密信息,然后应答微信官方我们收到了信息,在接收信息解密的过程中,我们可以判断是否退款成功,我们就可以进行修改退款单、订单的一些状态,把他们修改成退款成功
这里的回调逻辑,与博客中的第6点基本一致,只是进行的操作不同,一个是退款,一个是支付
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request,HttpServletResponse response) {
log.info("退款结果通知");
Gson gson = new Gson();
HashMap<String, String> map = new HashMap<>();//应答对象
try {
//处理通知参数
String body = HttpUtils.readData(request);
HashMap<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("通知验签成功");
//处理退款单
//解密密文,获取明文
String plaintText = wxPayConfig.decryptFromResource(bodyMap);
//将明文转为map
HashMap plaintTextMap = gson.fromJson(plaintText, HashMap.class);
//获取支付下单的时候,传入的商户订单号,可以根据这个订单号去获取我们的一个订单记录,从而更新订单状态
String orderNo = (String) plaintTextMap.get("out_trade_no");
/**
* 这里获取了单号可以去查询一个,这个订单的状态是不是退款中
* 如果不是退款中,那直接return返回就行了,不用在执行下面的更新状态
* 如果是退款中的话,我们需要更新状态为退款成功。这里写你自己的数据库操作更新即可
* 如果你有退款单的话,同理,更新退款单状态即可
*
*/
//成功应答
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);
}
}
以上基本上就是所有的微信小程序JSAPI支付的完整流程,
- 统一下单
- 支付通知
- 查询支付结果
- 申请退款
- 退款通知
- 查询退款结果
- 关闭订单
~~~