JAVA微信小程序支付(V3)
1.yml配置
spring:
application:
name: jeecg-system
profiles:
active: prod
small_wechat:
sessionHost: https://api.weixin.qq.com/sns/jscode2session
appId: 小程序APPId
secret: 秘钥
grantType: authorization_code
pay:
info:
# 统一下单接口
payUrl: https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
# 微信商户id
mchId: xxxx
# 支付回调
notifyUrl: xxx - 必须是https
# 证书序列号
certificateSerialNo: xxx
# 私钥存放路径
# privateKeyPath: D:/xxx -- windows
privateKeyPath: xxxx # linux - 存放证书文件的路径
# v3秘钥key
wechatV3Key: xxx
# 微信获取平台证书列表地址
wechatCertificatesUrl: https://api.mch.weixin.qq.com/v3/certificates
2 . 小程序支付配置类
package org.jeecg.modules.wxpay.config;
import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.utils.PayResponseUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.Map;
/**
* 微信支付配置类
*
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 17:57
*/
@Component
@PropertySource("classpath:application.yml")
public class WeixinConfig implements InitializingBean {
@Value("${small_wechat.pay.info.payUrl}")
private String payUrl;
@Value("${small_wechat.pay.info.mchId}")
private String mchId;
@Value("${small_wechat.pay.info.notifyUrl}")
private String notifyUrl;
@Value("${small_wechat.appId}")
private String appId;
@Value("${small_wechat.pay.info.certificateSerialNo}")
private String certificateSerialNo;
@Value("${small_wechat.pay.info.privateKeyPath}")
private String privateKeyPath;
@Value("${small_wechat.pay.info.wechatV3Key}")
private String wechatV3Key;
@Value("${small_wechat.pay.info.wechatCertificatesUrl}")
private String wechatCertificatesUrl;
/**
* 统一支付接口地址
*/
public static String PAY_URL;
/**
* 商户id
*/
public static String MCH_ID;
/**
* 回调地址
*/
public static String NOTIFY_URL;
/**
* 微信APPid
*/
public static String WX_APP_ID;
/**
* 私钥地址
*/
public static String PRIVATE_KEY_PATH;
/**
* 证书序列号
*/
public static String CERTI_SERIAL_NO;
/**
* 微信获取平台证书列表地址
*/
public static String CERTI_FICATES_URL;
/**
* V3秘钥
*/
public static String WECHATV3KEY;
/**
* 证书map
*/
public static Map<String, X509Certificate> CERTI_MAP;
@Override
public void afterPropertiesSet() throws ParseException, RequestWechatException, CertificateException {
WX_APP_ID = appId;
PAY_URL = payUrl;
MCH_ID = mchId;
NOTIFY_URL = notifyUrl;
CERTI_SERIAL_NO = certificateSerialNo;
PRIVATE_KEY_PATH = privateKeyPath;
WECHATV3KEY = wechatV3Key;
CERTI_FICATES_URL = wechatCertificatesUrl;
CERTI_MAP = PayResponseUtils.refreshCertificate();
}
}
3 .定义常量类
package org.jeecg.modules.wxpay.constant;
/**
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 17:07
*/
public class PayConstant {
/**
* 通知类型 支付成功通知的类型为TRANSACTION.SUCCESS
*/
public static final String TRANSACTION_SUCCESS = "TRANSACTION.SUCCESS";
}
4 . 回调和签名的工具类
package org.jeecg.modules.wxpay.utils;
import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.springframework.util.Base64Utils;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 回调工具类
*
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 18:06
*/
public class CallBackUtil {
/**
* 验证微信签名
*
* @param request request
* @param body body
* @return boolean
*/
public static boolean verifiedSign(HttpServletRequest request, String body) throws ParseException, RequestWechatException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
//微信返回的证书序列号
String serialNo = request.getHeader("Wechatpay-Serial");
//微信返回的随机字符串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信返回的时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
//微信返回的签名
String wechatSign = request.getHeader("Wechatpay-Signature");
//组装签名字符串
String signStr = Stream.of(timestamp, nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
//当证书容器为空 或者 响应提供的证书序列号不在容器中时 就应该刷新了
if (WeixinConfig.CERTI_MAP.isEmpty() || !WeixinConfig.CERTI_MAP.containsKey(serialNo)) {
WeixinConfig.CERTI_MAP = PayResponseUtils.refreshCertificate();
}
//根据序列号获取平台证书
X509Certificate certificate = WeixinConfig.CERTI_MAP.get(serialNo);
//获取失败 验证失败
if (certificate == null) {
return false;
}
//SHA256withRSA签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(certificate);
signature.update(signStr.getBytes());
//返回验签结果
return signature.verify(Base64Utils.decodeFromString(wechatSign));
}
/**
* 获取请求文体
*
* @param request request
* @return 返回请求问
* @throws IOException io
*/
public static String getRequestBody(HttpServletRequest request) throws IOException {
ServletInputStream stream;
BufferedReader reader = null;
StringBuffer sb = new StringBuffer();
try {
stream = request.getInputStream();
// 获取响应
reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
throw new IOException("读取返回支付接口数据流出现异常!");
} finally {
if (reader != null) {
reader.close();
}
}
return sb.toString();
}
}
// =============================================请求验签工具类=======================================================
package org.jeecg.modules.wxpay.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
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.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.springframework.util.Base64Utils;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 17:46
*/
public class PayRequestUtils {
/**
* V3 SHA256withRSA 移动端请求签名.
*
* @param timestamp 当前时间戳
* @param nonceStr 随机字符串
* @param prepayId 统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
* @return 签名
*/
public static String appSign(long timestamp, String nonceStr, String prepayId) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
String signatureStr = Stream.of(WeixinConfig.WX_APP_ID, String.valueOf(timestamp), nonceStr, prepayId)
.collect(Collectors.joining("\n", "", "\n"));
return getSign(signatureStr);
}
/**
* V3 SHA256withRSA http请求签名.
*
* @param method 请求方法 GET POST PUT DELETE 等
* @param canonicalUrl 请求地址
* @param timestamp 当前时间戳 因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致
* @param nonceStr 随机字符串 要和TOKEN中的保持一致
* @param body 请求体 GET 为 "" POST 为JSON
* @return the string
*/
private static String httpSign(String method, String canonicalUrl, String body, long timestamp, String nonceStr) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
URL url = new URL(canonicalUrl);
String signUrl;
if ("GET".equals(method) && url.getQuery() != null) {
signUrl = url.getPath() + "?" + url.getQuery();
} else {
signUrl = url.getPath();
}
String signatureStr = Stream.of(method, signUrl, String.valueOf(timestamp), nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
return getSign(signatureStr);
}
/**
* 获取签名
*
* @param signatureStr 签名串
* @return 签名
* @throws InvalidKeyException InvalidKeyException
* @throws NoSuchAlgorithmException NoSuchAlgorithmException
* @throws SignatureException SignatureException
* @throws IOException IOException
*/
public static String getSign(String signatureStr) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(SignUtil.getPrivateKey(WeixinConfig.PRIVATE_KEY_PATH));
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
/**
* 生成Token http请求
*
* @param method 请求方法 GET POST PUT DELETE 等
* @param canonicalUrl 请求地址
* @param body 请求体 GET 为 "" POST 为JSON
* @return the string
*/
private static String httpToken(String method, String canonicalUrl, String body) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String signature = httpSign(method, canonicalUrl, body, timestamp, nonceStr);
final String TOKEN_PATTERN = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
// 生成token
return String.format(TOKEN_PATTERN,
WeixinConfig.MCH_ID,
nonceStr, timestamp, WeixinConfig.CERTI_SERIAL_NO, signature);
}
public static <T> T wechatHttpPost(String url, String jsonStr, Class<T> t) throws RequestWechatException {
//创建httpClient实例
CloseableHttpClient httpClient = HttpClients.createDefault();
T instance = null;
try {
instance = t.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
//生成Token http请求
String token = null;
try {
token = httpToken("POST", url, jsonStr);
} catch (Exception e) {
e.printStackTrace();
}
HttpPost httppost = new HttpPost(url);
httppost.addHeader("Content-Type", "application/json;charset=UTF-8");
httppost.addHeader("Accept", "application/json");
httppost.addHeader("Authorization", token);
//设置连接超时时间和数据获取超时时间--单位:ms
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000000).setConnectionRequestTimeout(5000000)
.setSocketTimeout(5000000).build();
httppost.setConfig(requestConfig);
//设置http request body请求体
if (null != jsonStr) {
//解决中文乱码问题
StringEntity myEntity = new StringEntity(jsonStr, "UTF-8");
httppost.setEntity(myEntity);
}
HttpResponse response;
try {
response = httpClient.execute(httppost);
} catch (IOException e) {
e.printStackTrace();
throw new RequestWechatException();
}
String result = null;
try {
result = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
//得到返回的字符串
JSONObject jsonObject = JSON.parseObject(result);
if (instance instanceof JSONObject) {
return (T) jsonObject;
}
T resultBean = (T) JSONObject.parseObject(jsonObject.toString(), t);
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
return resultBean;
}
/**
* httpget调用http接口
*
* @param url 地址
* @param jsonStr json穿
* @param t t
* @param <T> <T>
* @return 结果
*/
public static <T> T wechatHttpGet(String url, String jsonStr, Class<T> t) throws RequestWechatException {
String result = "";
T instance = null;
try {
instance = t.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
//生成Token http请求
String token = null;
try {
token = httpToken("GET", url, jsonStr);
} catch (Exception e) {
e.printStackTrace();
}
//创建httpClient实例
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建实例方法
HttpGet httpget = new HttpGet(url);
httpget.addHeader("Content-Type", "application/json;charset=UTF-8");
httpget.addHeader("Accept", "application/json");
httpget.addHeader("Authorization", token);
HttpResponse response;
try {
response = httpClient.execute(httpget);
} catch (IOException e) {
throw new RequestWechatException();
}
//如果状态码为200,就是正常返回
if (response.getStatusLine().getStatusCode() == 200) {
try {
result = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
}
JSONObject jsonObject = JSON.parseObject(result);
if (instance instanceof JSONObject) {
return (T) jsonObject;
}
T resultBean = (T) JSONObject.parseObject(jsonObject.toString(), t);
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
return resultBean;
}
/**
* 获取证书。
*
* @param fis 证书文件流
* @return X509证书
*/
public static X509Certificate getCertificate(InputStream fis) throws IOException {
BufferedInputStream bis = new BufferedInputStream(fis);
try {
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(bis);
cert.checkValidity();
return cert;
} catch (CertificateExpiredException e) {
throw new RuntimeException("证书已过期", e);
} catch (CertificateNotYetValidException e) {
throw new RuntimeException("证书尚未生效", e);
} catch (CertificateException e) {
throw new RuntimeException("无效的证书文件", e);
} finally {
bis.close();
}
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
}
// ============================================微信付款返回数据工具类==========================================================
package org.jeecg.modules.wxpay.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.jeecg.common.exception.RequestWechatException;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.jeecg.modules.wxpay.model.CertificateVO;
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 微信付款返回数据工具类
*
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/24 9:21
*/
public class PayResponseUtils {
/**
* 用微信V3密钥解密响应体.
*
* @param associatedData response.body.data[i].encrypt_certificate.associated_data
* @param nonce response.body.data[i].encrypt_certificate.nonce
* @param ciphertext response.body.data[i].encrypt_certificate.ciphertext
* @return the string
*/
public static String decryptResponseBody(String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(WeixinConfig.WECHATV3KEY.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
/**
* 获取平台证书Map
*
* @return 证书Map
*/
public static Map<String, X509Certificate> refreshCertificate() throws ParseException, CertificateException, RequestWechatException {
//获取平台证书json
JSONObject jsonObject = PayRequestUtils.wechatHttpGet(WeixinConfig.CERTI_FICATES_URL, "", JSONObject.class);
List<CertificateVO> certificateList = JSON.parseArray(jsonObject.getString("data"), CertificateVO.class);
//最新证书响应实体类
CertificateVO newestCertificate = null;
//最新时间
Date newestTime = null;
for (CertificateVO certificate : certificateList) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
//如果最新时间是null
if (newestTime == null) {
newestCertificate = certificate;
//设置最新启用时间
newestTime = formatter.parse(certificate.getEffective_time());
} else {
Date effectiveTime = formatter.parse(certificate.getEffective_time());
//如果启用时间大于最新时间
if (effectiveTime.getTime() > newestTime.getTime()) {
//更换最新证书响应实体类
newestCertificate = certificate;
}
}
}
CertificateVO.EncryptCertificate encryptCertificate = newestCertificate.getEncrypt_certificate();
//获取证书字公钥
String publicKey = decryptResponseBody(encryptCertificate.getAssociated_data(), encryptCertificate.getNonce(), encryptCertificate.getCiphertext());
CertificateFactory cf = CertificateFactory.getInstance("X509");
//获取证书
ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
X509Certificate certificate = null;
try {
certificate = (X509Certificate) cf.generateCertificate(inputStream);
} catch (CertificateException e) {
e.printStackTrace();
}
//保存微信平台证书公钥
Map<String, X509Certificate> certificateMap = new ConcurrentHashMap<>();
// 清理HashMap
certificateMap.clear();
// 放入证书
certificateMap.put(newestCertificate.getSerial_no(), certificate);
return certificateMap;
}
}
// ===============================这有一个官方文档上的例子,我把他整理了一下,可用于签名和验签======================================================================
package org.jeecg.modules.wxpay.utils;
import okhttp3.HttpUrl;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class SignUtil {
private static String SCHEMA = "WECHATPAY2-SHA256-RSA2048 ";
public static String getToken(String urlPath, String method, String param, long timestamp, String nonceStr) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
HttpUrl url = HttpUrl.parse(urlPath);
if (url == null) {
throw new RuntimeException("url = null");
}
String message = buildMessage(method, url, timestamp, nonceStr, param);
String signature = sign(message.getBytes(StandardCharsets.UTF_8));
return SignUtil.SCHEMA + "mchid=\"" + WeixinConfig.MCH_ID + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + WeixinConfig.CERTI_SERIAL_NO + "\","
+ "signature=\"" + signature + "\"";
}
public static String getTokenTow(String packageStr, String nonceStr, long timestamp) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// 拼接签名字符串
String signStr = WeixinConfig.WX_APP_ID + "\n" + timestamp + "\n" + nonceStr + "\n" + packageStr + "\n";
return sign(signStr.getBytes(StandardCharsets.UTF_8));
}
public static String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(SignUtil.getPrivateKey(WeixinConfig.PRIVATE_KEY_PATH));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
private static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.encodedPath();
if (url.encodedQuery() != null) {
canonicalUrl += "?" + url.encodedQuery();
}
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8);
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
}
//============================================================================================================
package org.jeecg.modules.wxpay.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 元转分工具类
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/24 9:32
*/
public class AmountUtil {
public static String converAmount(BigDecimal fee) {
// 四舍五入,精确2位小数
fee = fee.setScale(2, BigDecimal.ROUND_HALF_UP);
// 转换单位为:分
fee = fee.multiply(new BigDecimal(100));
// 取整,过滤小数部分
fee = fee.setScale(0, RoundingMode.DOWN);
return String.valueOf(fee);
}
}
5 . 各种VO
package org.jeecg.modules.wxpay.model;
/**
* 微信平台证书VO
*
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 17:58
*/
public class CertificateVO {
/**
* 平台证书序列号
*/
private String serial_no;
/**
* 平台证书有效时间
*/
private String effective_time;
/**
* 平台证书过期时间
*/
private String expire_time;
/**
* 加密证书
*/
private EncryptCertificate encrypt_certificate;
/**
* 加密证书
*/
public class EncryptCertificate {
/**
* 算法
*/
private String algorithm;
/**
* 随机字符串
*/
private String nonce;
/**
* 相关数据
*/
private String associated_data;
/**
* 密文
*/
private String ciphertext;
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public String getNonce() {
return nonce;
}
public void setNonce(String nonce) {
this.nonce = nonce;
}
public String getAssociated_data() {
return associated_data;
}
public void setAssociated_data(String associated_data) {
this.associated_data = associated_data;
}
public String getCiphertext() {
return ciphertext;
}
public void setCiphertext(String ciphertext) {
this.ciphertext = ciphertext;
}
}
public String getSerial_no() {
return serial_no;
}
public void setSerial_no(String serial_no) {
this.serial_no = serial_no;
}
public String getEffective_time() {
return effective_time;
}
public void setEffective_time(String effective_time) {
this.effective_time = effective_time;
}
public String getExpire_time() {
return expire_time;
}
public void setExpire_time(String expire_time) {
this.expire_time = expire_time;
}
public EncryptCertificate getEncrypt_certificate() {
return encrypt_certificate;
}
public void setEncrypt_certificate(EncryptCertificate encrypt_certificate) {
this.encrypt_certificate = encrypt_certificate;
}
}
// ==========================================================================================================
package org.jeecg.modules.wxpay.model;
/**
* 微信通知数据解密后对象
*
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 17:24
*/
public class NotifyResourceVO {
/**
* 公众号ID
*/
private String appid;
/**
* 直连商户号
*/
private String mchid;
/**
* 商户订单号
*/
private String out_trade_no;
/**
* 微信支付订单号
*/
private String transaction_id;
/**
* 交易类型
*/
private String trade_type;
/**
* 交易状态
*/
private String trade_state;
/**
* 交易状态描述
*/
private String trade_state_desc;
/**
* 付款银行
*/
private String bank_type;
/**
* 支付完成时间
*/
private String success_time;
/**
* 支付者
*/
private Payer payer;
/**
* 订单金额
*/
private Amount amount;
/**
* 支付者
*/
public class Payer {
/**
* 用户标识
*/
private String openid;
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
}
/**
* 订单金额
*/
public class Amount {
/**
* 总金额
*/
private Integer total;
/**
* 用户支付金额
*/
private Integer payer_total;
/**
* 货币类型
*/
private String currency;
/**
* 用户支付币种
*/
private String payer_currency;
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
public Integer getPayer_total() {
return payer_total;
}
public void setPayer_total(Integer payer_total) {
this.payer_total = payer_total;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
public String getPayer_currency() {
return payer_currency;
}
public void setPayer_currency(String payer_currency) {
this.payer_currency = payer_currency;
}
}
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getMchid() {
return mchid;
}
public void setMchid(String mchid) {
this.mchid = mchid;
}
public String getOut_trade_no() {
return out_trade_no;
}
public void setOut_trade_no(String out_trade_no) {
this.out_trade_no = out_trade_no;
}
public String getTransaction_id() {
return transaction_id;
}
public void setTransaction_id(String transaction_id) {
this.transaction_id = transaction_id;
}
public String getTrade_type() {
return trade_type;
}
public void setTrade_type(String trade_type) {
this.trade_type = trade_type;
}
public String getTrade_state() {
return trade_state;
}
public void setTrade_state(String trade_state) {
this.trade_state = trade_state;
}
public String getTrade_state_desc() {
return trade_state_desc;
}
public void setTrade_state_desc(String trade_state_desc) {
this.trade_state_desc = trade_state_desc;
}
public String getBank_type() {
return bank_type;
}
public void setBank_type(String bank_type) {
this.bank_type = bank_type;
}
public String getSuccess_time() {
return success_time;
}
public void setSuccess_time(String success_time) {
this.success_time = success_time;
}
public Payer getPayer() {
return payer;
}
public void setPayer(Payer payer) {
this.payer = payer;
}
public Amount getAmount() {
return amount;
}
public void setAmount(Amount amount) {
this.amount = amount;
}
}
// ==========================================================================================================
package org.jeecg.modules.wxpay.model;
/**
* 接收微信支付通知VO
*
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 16:57
*/
public class PayNotifyVO {
/**
* 通知的唯一ID
*/
private String id;
/**
* 通知创建时间
*/
private String create_time;
/**
* 通知类型 支付成功通知的类型为TRANSACTION.SUCCESS
*/
private String event_type;
/**
* 通知数据类型 支付成功通知为encrypt-resource
*/
private String resource_type;
/**
* 通知资源数据
*/
private Resource resource;
/**
* 回调摘要
*/
private String summary;
/**
* 通知资源数据
*/
public class Resource {
/**
* 加密算法类型
*/
private String algorithm;
/**
* 数据密文
*/
private String ciphertext;
/**
* 附加数据
*/
private String associated_data;
/**
* 随机串
*/
private String nonce;
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public String getCiphertext() {
return ciphertext;
}
public void setCiphertext(String ciphertext) {
this.ciphertext = ciphertext;
}
public String getAssociated_data() {
return associated_data;
}
public void setAssociated_data(String associated_data) {
this.associated_data = associated_data;
}
public String getNonce() {
return nonce;
}
public void setNonce(String nonce) {
this.nonce = nonce;
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCreate_time() {
return create_time;
}
public void setCreate_time(String create_time) {
this.create_time = create_time;
}
public String getEvent_type() {
return event_type;
}
public void setEvent_type(String event_type) {
this.event_type = event_type;
}
public String getResource_type() {
return resource_type;
}
public void setResource_type(String resource_type) {
this.resource_type = resource_type;
}
public Resource getResource() {
return resource;
}
public void setResource(Resource resource) {
this.resource = resource;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
}
// ==========================================================================================================
package org.jeecg.modules.wxpay.model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
/**
* @Description: mec_user
* @Author: chao
* @Date: 2021-07-22
* @Version: V1.0
*/
@Data
public class WxParticipate implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 应用ID
*/
@ApiModelProperty(value = "应用ID")
private String appid;
/**
* 直连商户号
*/
@ApiModelProperty(value = "直连商户号")
private String mchid;
/**
* 商品描述
*/
@ApiModelProperty(value = "商品描述")
private String description;
/**
* 商户订单号
*/
@ApiModelProperty(value = "商户订单号")
private String out_trade_no;
/**
* 附加数据
*/
@ApiModelProperty(value = "附加数据")
private String attach;
/**
* 通知地址
*/
@ApiModelProperty(value = "通知地址")
private String notify_url;
/**
* 订单金额
*/
@ApiModelProperty(value = "订单金额")
private Map<String, Object> amount;
/**
* 支付者
*/
@ApiModelProperty(value = "支付者")
private Map<String, Object> payer;
/**
* 场景信息
*/
@ApiModelProperty(value = "场景信息")
private Map<String, Object> scene_info;
}
6 . controller
package org.jeecg.modules.wxpay.controller;
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.mecorder.service.IMecOrderService;
import org.jeecg.modules.wxpay.constant.PayConstant;
import org.jeecg.modules.wxpay.model.NotifyResourceVO;
import org.jeecg.modules.wxpay.model.PayNotifyVO;
import org.jeecg.modules.wxpay.service.PaymentService;
import org.jeecg.modules.wxpay.utils.CallBackUtil;
import org.jeecg.modules.wxpay.utils.PayResponseUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/23 17:26
*/
@Api(tags = "微信预支付")
@RestController
@Slf4j
@RequestMapping(value = "/wxPay")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Autowired
private IMecOrderService iMecOrderService;
@ResponseBody
@PostMapping(value = "/prepaid")
@ApiOperation(value = "微信预支付接口", notes = "微信预支付接口")
public Result<?> toPay(@RequestParam(name = "orderId") String orderId, HttpServletRequest request) {
try {
return Result.OK(paymentService.toPay(orderId, request));
} catch (Exception e) {
e.printStackTrace();
return Result.error(e.getMessage());
}
}
/**
* 微信支付结果回调地址
*
* @param request request
*/
@PostMapping(value = "/payNotify")
@Transactional(rollbackFor = Exception.class)
public Map<String, String> payNotify(HttpServletRequest request) {
Map<String, String> map = new HashMap<>(2);
try {
log.info("接受到微信回调 == > 准备验签");
//微信返回的请求体
String body = CallBackUtil.getRequestBody(request);
//如果验证签名序列号通过
if (CallBackUtil.verifiedSign(request, body)) {
log.info("验证签名成功,准备校验通知类型");
//微信支付通知实体类
PayNotifyVO payNotifyVO = JSONObject.parseObject(body, PayNotifyVO.class);
//如果支付成功
if (PayConstant.TRANSACTION_SUCCESS.equals(payNotifyVO.getEvent_type())) {
log.info("通知类型为==>支付成功,开始执行业务代码");
// 通知资源数据
PayNotifyVO.Resource resource = payNotifyVO.getResource();
// 解密后资源数据
String notifyResourceStr = PayResponseUtils.decryptResponseBody(resource.getAssociated_data(), resource.getNonce(), resource.getCiphertext());
// 通知资源数据对象
NotifyResourceVO notifyResourceVO = JSONObject.parseObject(notifyResourceStr, NotifyResourceVO.class);
// 处理业务代码 --- > 这个地方根据需求不同自行修改
paymentService.callbackHandler(notifyResourceVO);
} else {
log.info("微信返回支付错误摘要:" + payNotifyVO.getSummary());
}
//通知微信正常接收到消息,否则微信会轮询该接口
map.put("code", "SUCCESS");
map.put("message", "");
return map;
}
log.info("验证签名失败");
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
7. service
package org.jeecg.modules.wxpay.service;
import org.jeecg.modules.wxpay.model.NotifyResourceVO;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @Author 王超
* @Version V1.0.0
* @Date 2021/8/21 9:30
*/
public interface PaymentService {
/**
* 预支付接口
*
* @param orderId 订单id
* @param request request
* @return prepay_id
*/
Map toPay(String orderId, HttpServletRequest request);
/**
* 回调的业务代码
*
* @param notifyResourceVO vo
*/
void callbackHandler(NotifyResourceVO notifyResourceVO);
}
8.serviceImpl
package org.jeecg.modules.wxpay.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.constant.MecCategoryConstants;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.CheckUtils;
import org.jeecg.common.util.IPUtils;
import org.jeecg.modules.mecorder.constant.OrderStatusConstant;
import org.jeecg.modules.mecorder.entity.MecOrder;
import org.jeecg.modules.mecorder.service.IMecOrderService;
import org.jeecg.modules.wxpay.config.WeixinConfig;
import org.jeecg.modules.wxpay.model.NotifyResourceVO;
import org.jeecg.modules.wxpay.model.WxParticipate;
import org.jeecg.modules.wxpay.service.PaymentService;
import org.jeecg.modules.wxpay.utils.AmountUtil;
import org.jeecg.modules.wxpay.utils.SignUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Slf4j
@Service
public class WeChatServiceImpl implements PaymentService {
@Autowired
private IMecOrderService iMecOrderService;
@SneakyThrows
@Override
public Map toPay(String orderId, HttpServletRequest request) {
// 根据orderid查询订单
MecOrder mecOrder = Optional.ofNullable(iMecOrderService.getById(orderId))
.orElseThrow(() -> new JeecgBootException("预支付失败,订单id : " + orderId + "不存在"));
log.info("准备预支付,openid is {} , orderid is {}", mecOrder.getOpenId(), orderId);
String ipAddr = IPUtils.getIpAddr(request);
// 获取金额
BigDecimal price = CheckUtils.checkBigDecimal(mecOrder.getPrice(), mecOrder.getCardCount());
String converPrice = AmountUtil.converAmount(price);
log.info("调用结束,准备返回参数");
return packageParameters(mecOrder, ipAddr, converPrice);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void callbackHandler(NotifyResourceVO notifyResourceVO) {
MecOrder order = iMecOrderService.getById(notifyResourceVO.getOut_trade_no());
if (order != null) {
// 如果订单状态是未支付状态,改成支付成功
if ("自己订单的状态".equals(order.getStatus())) {
//如果付款成功
MecOrder update = new MecOrder();
update.setId(order.getId());
log.info("当前订单:" + order.getId() + " 符合内部校验,准备验证微信返回的交易状态");
if ("SUCCESS".equals(notifyResourceVO.getTrade_state())) {
// 处理成功后的业务
} else {
// 处理失败业务
}
}
}
}
@SneakyThrows
private Map packageParameters(MecOrder mecOrder, String ipAddr, String converPrice) {
// 定义附加数据
Map<String, Object> attach = new HashMap<>();
attach.put("userId", mecOrder.getUserId());
// 定义订单金额
Map<String, Object> amount = new HashMap<>();
amount.put("total", Integer.valueOf(converPrice));
// 定义支付者
Map<String, Object> payer = new HashMap<>();
payer.put("openid", mecOrder.getOpenId());
// 定义场景信息 -- ip地址
Map<String, Object> sceneInfo = new HashMap<>();
sceneInfo.put("payer_client_ip", ipAddr);
WxParticipate wxParticipate = new WxParticipate();
wxParticipate.setAppid(WeixinConfig.WX_APP_ID);
wxParticipate.setMchid(WeixinConfig.MCH_ID);
wxParticipate.setDescription(mecOrder.getDescription());
wxParticipate.setOut_trade_no(mecOrder.getId());
wxParticipate.setAttach(JSONObject.toJSONString(attach));
wxParticipate.setNotify_url(WeixinConfig.NOTIFY_URL);
wxParticipate.setAmount(amount);
wxParticipate.setPayer(payer);
wxParticipate.setScene_info(sceneInfo);
// 转换请求参数
String param = JSON.toJSONString(wxParticipate);
String nonceStr = RandomUtil.randomStringUpper(32);
long timestamp = System.currentTimeMillis() / 1000;
String post = HttpRequest.post(WeixinConfig.PAY_URL)
.header(Header.AUTHORIZATION, SignUtil.getToken(WeixinConfig.PAY_URL, "POST", param, timestamp, nonceStr))
.header(Header.CONTENT_TYPE, "application/json")
.body(param)
.execute()
.body();
if (StringUtils.isBlank(post)) {
return null;
}
Map<String, Object> map = JSON.parseObject(post, new TypeReference<Map<String, Object>>() {
});
if (MapUtil.isEmpty(map) || StringUtils.isBlank(MapUtil.get(map, "prepay_id", String.class))) {
throw new JeecgBootException("预支付订单生成失败,原因:" + post);
}
setResultMap(map, timestamp, nonceStr);
return map;
}
@SneakyThrows
private void setResultMap(Map<String, Object> map, long timestamp, String nonceStr) {
// 获取微信返回的必要参数
String prepayId = MapUtil.get(map, "prepay_id", String.class);
// 拼接package
String packageStr = "prepay_id=" + prepayId;
// 获取二次签名
String tokenTow = SignUtil.getTokenTow(packageStr, nonceStr, timestamp);
map.put("timeStamp", timestamp + "");
map.put("nonceStr", nonceStr);
map.put("package", packageStr);
map.put("paySign", tokenTow);
}
}
我自己写的时候参考了下面这个大佬的文章
参考链接-> https://blog.csdn.net/qq_39706128/article/details/111558994