小程序微信支付V3使用SDK
1.先引入依赖,引入微信支付的SDK依赖,Redisson分布式锁
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
<!-- redission -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.6</version>
</dependency>
2.Redisson的配置信息
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setPassword(null); //Redis设置的无密码所以必须为null,有密码的话 引入密码
return Redisson.create(config);
}
}
3.所需要的各种配置信息都放在 配置文件中 application.yml
wechatpay:
appId: 微信公众号或者小程序等的appId
merchantId: 微信支付商户号
merchantSerialNumber: 商戶API ID
apiV3key: 商户APIv3密钥
certPemPath: 商户证书路径(此处绝对路径,为下载商户证书中的apiclient_key.pem) D:\Code\enterprise-management-master\ruoyi-enterprise\src\main\resources\apiclient_key.pem
# certPemPath: /home/ruoyi/serve/pem/apiclient_key.pem
payNotifyUrl: https://ip:8080/weChatPay/callback #支付成功回调地址,必须为https请求
appSecret: APP 秘钥
4.参数初始化到内存中
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* Description: 微信支付配置类
*
* @author shangzewei
* @date 2023/11/29 10:27
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "wechatpay")
public class WechatPayProperties {
/**
* 微信公众号或者小程序等的appId
*/
private String appId;
/**
* 微信支付商户号
*/
private String merchantId;
/**
* 商户证书路径
*/
private String certPemPath;
/**
* 商户API私钥路径
*/
private String privateKeyPath;
/**
* 商户APIv3密钥
*/
private String apiV3key;
/**
* 支付回调通知地址
*/
private String payNotifyUrl;
/**
* 商戶API ID
*/
private String merchantSerialNumber;
/**
* APP 秘钥
*/
private String appSecret;
}
6初始化商户对象,服务对象
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* Description: 微信支付相关自动配置
*
* @author shangzewei
* @date 2023/12/1 10:50
*/
@Slf4j
@Configuration
public class WechatPayAutoConfiguration {
@Autowired
private WechatPayProperties properties;
/**
* 自动更新证书,初始化商户
* RSAAutoCertificateConfig 会利用 AutoCertificateService 自动下载微信支付平台证书。
* AutoCertificateService 将启动一个后台线程,定期(目前为每60分钟)更新证书,以实现证书过期时的平滑切换。
*
* 在每次构建 RSAAutoCertificateConfig 时,SDK 首先会使用传入的商户参数下载一次微信支付平台证书。
* 如果下载成功,SDK 会将商户参数注册或更新至 AutoCertificateService。若下载失败,将会抛出异常。
*
* 为了提高性能,建议将配置类作为全局变量。 复用 RSAAutoCertificateConfig 可以减少不必要的证书下载,避免资源浪费。
* 只有在配置发生变更时,才需要重新构造 RSAAutoCertificateConfig
*
* @return RSAAutoCertificateConfig
*/
@Bean
public Config config() throws IOException {
log.info("==========加载商戶對象");
// String privateKeyPath = WechatPayAutoConfiguration.class.getClassLoader().getResource(properties.getCertPemPath()).getFile();
// 初始化商户配置
RSAAutoCertificateConfig config =
new RSAAutoCertificateConfig.Builder()
.merchantId(properties.getMerchantId())
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
// .privateKeyFromPath(privateKeyPath)
.privateKeyFromPath(properties.getCertPemPath())
.merchantSerialNumber(properties.getMerchantSerialNumber())
.apiV3Key(properties.getApiV3key())
.build();
log.info("==========商戶對象加載完成");
return config;
}
/**
* 微信支付对象
* @param config Config
* @return JsapiServiceExtension
*/
@Bean
public JsapiServiceExtension jsapiServiceExtension(Config config){
log.info("==========加载微信支付对象");
JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
log.info("==========微信支付对象加載完成");
return service;
}
/**
* 微信回调对象
*
* @param config Config
* @return NotificationParser
*/
@Bean
public NotificationParser notificationParser(Config config) {
log.info("==========加载微信回调解析对象");
NotificationParser parser = new NotificationParser((NotificationConfig) config);
return parser;
}
}
7.Controller
import com.ruoyi.enterprise.config.WechatPayProperties;
import com.ruoyi.enterprise.entity.CreateOrderPayRequest;
import com.ruoyi.enterprise.service.WechatPayExternalService;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/weChatPay")
public class WeChatPayController {
@Autowired
private WechatPayProperties properties;
@Autowired
WechatPayExternalService wechatPayExternalService;
@PostMapping("/createorder")
public PrepayWithRequestPaymentResponse createOrder(@RequestBody CreateOrderPayRequest createOrderPayRequest) throws Exception {
//生成订单号 openid+时间戳
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String timestamp = dateFormat.format(new Date());
createOrderPayRequest.setOutTradeNo(createOrderPayRequest.getOpenId().substring(0,10) + timestamp);
createOrderPayRequest.setPayContent("小程序下單");
return wechatPayExternalService.prepayWithRequestPayment(createOrderPayRequest);
}
/**
* 回调接口
*
* @param request
* @return
* @throws
*/
@RequestMapping(value = "/callback", method = {RequestMethod.POST, RequestMethod.GET})
public Map<String, String> payCallback(HttpServletRequest request, HttpServletResponse response) {
log.info("------收到支付通知------");
Map<String, String> result = new HashMap<>();
try {
wechatPayExternalService.payCallBack(request);
result.put("code", "SUCCESS");
result.put("message", "成功");
return result;
} catch (Exception e) {
log.error("支付处理失败,req:{}", request, e);
// alarm();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
result.put("code", "FAIL");
result.put("message", e.getMessage());
return result;
}
}
}
7.Service
import com.ruoyi.enterprise.entity.CreateOrderPayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.ruoyi.enterprise.entity.WechatPayCallBackRequest;
import javax.servlet.http.HttpServletRequest;
/**
* Description: 微信支付对接 V3(基于JSAPI 支付的扩展类实现)
*
* @author shangzewei
* @date 2023/11/30 11:38
*/
public interface WechatPayExternalService {
/**
* 提交预支付请求付款
* @param createOrderPay 订单请求体
* @return PrepayWithRequestPaymentResponse 预付费与请求付款响应
*/
PrepayWithRequestPaymentResponse prepayWithRequestPayment(CreateOrderPayRequest createOrderPay);
/**
* 查询状态
*
* @param outTradeNo 商户支付no
* @return 状态信息
*/
Transaction queryStatus(String outTradeNo);
/**
* 取消订单
*
* @param outTradeNo
*/
void closeOrder(String outTradeNo);
/**
* 解析签名
* 官网地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
*
* @param wechatPayCallBackRequest
* @param clazz
* @param <T>
* @return
*/
<T> T analysisSign(WechatPayCallBackRequest wechatPayCallBackRequest, Class<T> clazz);
/**
* 处理回调
* 官网地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
*
*/
public void payCallBack(HttpServletRequest request) throws Exception;
}
8.ServiceImpl
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.enterprise.Enum.OrderPayStatusEnum;
import com.ruoyi.enterprise.config.WechatPayProperties;
import com.ruoyi.enterprise.domain.EmEnterprise;
import com.ruoyi.enterprise.entity.CreateOrderPayRequest;
import com.ruoyi.enterprise.entity.WechatPayCallBackRequest;
import com.ruoyi.enterprise.service.IEmEnterpriseService;
import com.ruoyi.enterprise.service.WechatPayExternalService;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.cipher.PrivacyEncryptor;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.redisson.api.RedissonClient;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
/**
* Description: 微信支付对接(基于JSAPI 支付的扩展类实现)
*
* @author shangzewei
* @date 2023/12/1 11:49
*/
@Slf4j
@Service
public class WechatPayExternalServiceImpl implements WechatPayExternalService {
@Autowired
IEmEnterpriseService emEnterpriseService;
@Resource
private Config config;
@Resource
private WechatPayProperties properties;
@Resource
private JsapiServiceExtension jsapiServiceExtension;
@Resource
private NotificationParser notificationParser;
@Resource
private RedissonClient redissonClient;
/**
* 分布式锁
*/
private final String LOCK_KEY = "WECHAT_PAY_LOCK:";
/*
预支付订单
*/
@Override
public PrepayWithRequestPaymentResponse prepayWithRequestPayment(CreateOrderPayRequest createOrderPay) {
EmEnterprise emEnterprise = emEnterpriseService.selectEmEnterpriseByCode(createOrderPay.getSocietalCode());
log.info("prepayWithRequestPayment start");
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
BigDecimal payMoney = createOrderPay.getPayMoney();
BigDecimal amountTotal = payMoney.multiply(new BigDecimal("100").setScale(0, RoundingMode.DOWN));
amount.setTotal(amountTotal.intValue());
request.setAmount(amount);
Payer payer = new Payer();
payer.setOpenid(createOrderPay.getOpenId());
request.setPayer(payer);
request.setTimeExpire(getExpiredTimeStr());
request.setAppid(properties.getAppId());
request.setMchid(properties.getMerchantId());
request.setDescription(createOrderPay.getPayContent());
//回调地址
request.setNotifyUrl(properties.getPayNotifyUrl());
//添加附加数据,代表社会代码
request.setAttach(createOrderPay.getSocietalCode());
//这里生成流水号,后续用这个流水号与微信交互,查询订单状态
request.setOutTradeNo(createOrderPay.getOutTradeNo());
PrepayWithRequestPaymentResponse result;
try {
result = jsapiServiceExtension.prepayWithRequestPayment(request);
} catch (HttpException e) {
log.error("微信下单发送HTTP请求失败,错误信息:{}", e.getHttpRequest());
throw new RuntimeException("微信下单发送HTTP请求失败", e);
} catch (ServiceException e) {
// 服务返回状态小于200或大于等于300,例如500
log.error("微信下单服务状态错误,错误信息:{}", e.getErrorMessage());
throw new RuntimeException("微信下单服务状态错误", e);
} catch (MalformedMessageException e) {
// 服务返回成功,返回体类型不合法,或者解析返回体失败
log.error("服务返回成功,返回体类型不合法,或者解析返回体失败,错误信息:{}", e.getMessage());
throw new RuntimeException("服务返回成功,返回体类型不合法,或者解析返回体失败", e);
}
log.info("prepayWithRequestPayment end");
return result;
}
@Override
public Transaction queryStatus(String outTradeNo) {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
request.setMchid(properties.getMerchantId());
request.setOutTradeNo(outTradeNo);
try {
return jsapiServiceExtension.queryOrderByOutTradeNo(request);
} catch (ServiceException e) {
log.error("订单查询失败,返回码:{},返回信息:{}", e.getErrorCode(), e.getErrorMessage());
throw new RuntimeException("订单查询失败", e);
}
}
/*
关闭订单
*/
@Override
public void closeOrder(String outTradeNo) {
log.info("closeOrder");
CloseOrderRequest closeRequest = new CloseOrderRequest();
closeRequest.setMchid(properties.getMerchantId());
closeRequest.setOutTradeNo(outTradeNo);
try {
//方法没有返回值,意味着成功时API返回204 No Content
jsapiServiceExtension.closeOrder(closeRequest);
} catch (ServiceException e) {
log.error("订单关闭失败,返回码:{},返回信息:{}", e.getErrorCode(), e.getErrorMessage());
throw new RuntimeException("订单关闭失败", e);
}
}
@Override
public <T> T analysisSign(WechatPayCallBackRequest wechatPayCallBackRequest, Class<T> clazz) {
log.info("payCallBack");
PrivacyEncryptor privacyEncryptor = config.createEncryptor();
String weChatPayCertificateSerialNumber = privacyEncryptor.getWechatpaySerial();
if (!wechatPayCallBackRequest.getSerial().equals(weChatPayCertificateSerialNumber)) {
log.error("证书不一致");
throw new RuntimeException("证书不一致");
}
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(wechatPayCallBackRequest.getSerial())
.nonce(wechatPayCallBackRequest.getNonce())
.signType(wechatPayCallBackRequest.getSignatureType())
.signature(wechatPayCallBackRequest.getSignature())
.timestamp(wechatPayCallBackRequest.getTimestamp())
.body(wechatPayCallBackRequest.getBody())
.build();
// 以支付通知回调为例,验签、解密并转换成 Transaction
return notificationParser.parse(requestParam, clazz);
}
/**
* 获取失效时间 5分钟
*/
private String getExpiredTimeStr() {
//失效时间,10分钟
LocalDateTime now = LocalDateTime.now();
LocalDateTime expiredTime = now.plusMinutes(5);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
return formatter.format(expiredTime);
}
@Override
public void payCallBack(HttpServletRequest request) throws Exception {
BufferedReader reader = request.getReader();
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
WechatPayCallBackRequest callBackRequest = new WechatPayCallBackRequest();
callBackRequest.setBody(sb.toString());
callBackRequest.setNonce(request.getHeader("Wechatpay-Nonce"));
callBackRequest.setSerial(request.getHeader("Wechatpay-Serial"));
callBackRequest.setSignature(request.getHeader("Wechatpay-Signature"));
callBackRequest.setSignatureType(request.getHeader("Wechatpay-Signature-Type"));
callBackRequest.setTimestamp(request.getHeader("Wechatpay-Timestamp"));
log.info("验签参数{}", JSONObject.toJSONString(callBackRequest));
Transaction transaction = analysisSign(callBackRequest, Transaction.class);
log.info("验签成功!-支付回调结果:{}", transaction.toString());
String lockKey = LOCK_KEY + transaction.getOutTradeNo();
RLock lock = redissonClient.getLock(lockKey);
try {
boolean isLock = lock.tryLock();
if (!isLock) {
throw new RuntimeException("请勿重复操作");
}
log.info("开始用户支付后业务处理");
// 以支付通知回调为例,验签、解密并转换成 Transaction
processTransaction(transaction);
log.info("用户支付后业务处理成功");
} catch (Exception e) {
log.error("用户支付后业务处理错误, e{}", e);
throw e;
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 处理回调业务(需要保证事务操作哦)
* @param transaction Transaction
*/
private void processTransaction(Transaction transaction) {
// 修改订单前,主动请求微信查询订单是否支付成功,防止恶意post
transaction = queryStatus(transaction.getOutTradeNo());
if (Transaction.TradeStateEnum.SUCCESS != transaction.getTradeState()) {
log.info("内部订单号【{}】,微信支付订单号【{}】支付未成功", transaction.getOutTradeNo(), transaction.getTransactionId());
throw new RuntimeException("订单支付未成功");
}
// 修改支付信息 代码修改数据库的支付状态
// PayLogPO payLog = payLogDao.getByOutTradeNo(transaction.getOutTradeNo());
EmEnterprise emEnterprise = emEnterpriseService.selectEmEnterpriseByCode(transaction.getAttach());
if (Objects.isNull(emEnterprise)){
log.error("用户支付后业务处理错误, 统一社会信用代码不存在" + transaction.getAttach());
}
if (OrderPayStatusEnum.PAY_SUCCESS.getCode().equals(emEnterprise.getCode())) {
// 若订单状态已为支付成功则不处理
return;
}
//微信支付系统生成的订单号
// payLog.setTransactionId(transaction.getTransactionId());
//支付完成时间
// if (Objects.nonNull(transaction.getSuccessTime())) {
// payLog.setPayTime(LocalDateTime.parse(transaction.getSuccessTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME));
// }
emEnterprise.setStatus(Integer.valueOf(OrderPayStatusEnum.PAY_SUCCESS.getCode()));
// payLogDao.store(payLog);
// 修改订单信息
// OrderPO order = orderDao.getById(payLog.getOrderId());
// order.setStatus(OrderStatusEnum.DELIVER_GOODS.getCode());
// orderDao.store(order);
emEnterpriseService.updateEmEnterprise(emEnterprise);
// 其他业务操作
}
}
9.CreateOrderPayRequest
/**
* Description: 提交预支付请求付款请求体
*
* @author shangzewei
* @date 2023/12/1 17:06
*/
@Data
public class CreateOrderPayRequest {
/**
* 主键id
*/
private Long id;
/**
* 商户支付no 和微信交互 查询订单使用(outTradeNo)
*/
private String outTradeNo;
/**
* 用户openid
*/
private String openId;
/**
* 支付金额
*/
private BigDecimal payMoney;
/**
* 支付内容
*/
private String payContent;
/**
* 社会代码
*/
private String societalCode;
}
import lombok.Data;
@Data
public class WechatPayCallBackRequest {
private String body;
/*
Wechatpay-Nonce
*/
private String Nonce;
/*
Wechatpay-Serial
*/
private String Serial;
/*
Wechatpay-Signature
*/
private String Signature;
/*
Wechatpay-Signature-Type
*/
private String SignatureType;
/*
Wechatpay-Timestamp
*/
private String Timestamp;
}