| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- package com.yingpai.gupiao.service.impl;
- import com.wechat.pay.java.core.Config;
- import com.wechat.pay.java.core.RSAAutoCertificateConfig;
- import com.wechat.pay.java.core.RSAPublicKeyConfig;
- import com.wechat.pay.java.core.notification.NotificationConfig;
- 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 com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.yingpai.gupiao.config.WxConfig;
- import com.yingpai.gupiao.domain.po.User;
- import com.yingpai.gupiao.domain.vo.WxPayVO;
- import com.yingpai.gupiao.mapper.UserMapper;
- import com.yingpai.gupiao.service.WxPayConfigService;
- import com.yingpai.gupiao.service.WxPayService;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.stereotype.Service;
- import java.io.BufferedReader;
- import java.io.ByteArrayInputStream;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.nio.charset.StandardCharsets;
- import java.security.cert.CertificateFactory;
- import java.security.cert.X509Certificate;
- import java.util.stream.Collectors;
- /**
- * 微信支付服务实现(微信支付SDK)
- */
- @Slf4j
- @Service
- @RequiredArgsConstructor
- public class WxPayServiceImpl implements WxPayService {
- private final WxConfig wxConfig;
- private final WxPayConfigService wxPayConfigService;
- private final UserMapper userMapper;
-
- private Config config;
- private JsapiServiceExtension jsapiService;
- private NotificationParser notificationParser;
-
- // 缓存当前配置的hash,用于检测配置变化
- private String currentConfigHash = "";
-
- /**
- * 获取配置hash,用于检测配置是否变化
- */
- private String getConfigHash() {
- String mchId = wxPayConfigService.getMchId();
- String apiV3Key = wxPayConfigService.getApiV3Key();
- String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
- String certPath = wxPayConfigService.getCertPath();
- String publicKeyPath = wxPayConfigService.getPublicKeyPath();
- String publicKeyId = wxPayConfigService.getPublicKeyId();
- return mchId + "|" + apiV3Key + "|" + privateKeyPath + "|" + certPath + "|" + publicKeyPath + "|" + publicKeyId;
- }
-
- /**
- * 确保SDK已初始化,配置变化时自动重新初始化
- */
- private synchronized void ensureInitialized() {
- String newHash = getConfigHash();
- if (!newHash.equals(currentConfigHash)) {
- log.info("检测到支付配置变化,重新初始化SDK");
- init();
- currentConfigHash = newHash;
- }
- }
-
- private void init() {
- try {
- String mchId = wxPayConfigService.getMchId();
- String apiV3Key = wxPayConfigService.getApiV3Key();
- String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
- String certPath = wxPayConfigService.getCertPath();
- String publicKeyPath = wxPayConfigService.getPublicKeyPath();
- String publicKeyId = wxPayConfigService.getPublicKeyId();
-
- if (mchId.isEmpty() || apiV3Key.isEmpty() || privateKeyPath.isEmpty()) {
- log.warn("微信支付配置不完整,跳过初始化");
- config = null;
- jsapiService = null;
- notificationParser = null;
- return;
- }
-
- // 读取私钥
- String privateKey = loadPrivateKey(privateKeyPath);
-
- // 从证书文件自动读取序列号
- String mchSerialNo = loadCertSerialNo(certPath);
- log.info("从证书读取序列号: {}", mchSerialNo);
-
- // 判断使用公钥模式还是自动证书模式
- if (!publicKeyPath.isEmpty() && !publicKeyId.isEmpty()) {
- // 使用微信支付公钥模式(新商户)
- log.info("使用微信支付公钥模式初始化,公钥路径: {}", publicKeyPath);
- String publicKey = loadPublicKey(publicKeyPath);
-
- config = new RSAPublicKeyConfig.Builder()
- .merchantId(mchId)
- .privateKey(privateKey)
- .merchantSerialNumber(mchSerialNo)
- .apiV3Key(apiV3Key)
- .publicKey(publicKey)
- .publicKeyId(publicKeyId)
- .build();
- } else {
- // 使用自动下载平台证书模式(老商户)
- log.info("使用自动证书模式初始化");
- config = new RSAAutoCertificateConfig.Builder()
- .merchantId(mchId)
- .privateKey(privateKey)
- .merchantSerialNumber(mchSerialNo)
- .apiV3Key(apiV3Key)
- .build();
- }
-
- // 初始化JSAPI服务
- jsapiService = new JsapiServiceExtension.Builder().config(config).build();
-
- // 初始化回调解析器
- notificationParser = new NotificationParser((NotificationConfig) config);
-
- log.info("微信支付SDK初始化成功,商户号: {}", mchId);
- } catch (Exception e) {
- log.error("微信支付SDK初始化失败", e);
- config = null;
- jsapiService = null;
- notificationParser = null;
- }
- }
- @Override
- public WxPayVO createPrepayOrder(String orderNo, String openid, Integer amount, String description) {
- log.info("创建预支付订单,orderNo: {}, openid: {}, amount: {}分", orderNo, openid, amount);
- // 确保SDK已初始化,配置变化时自动重新初始化
- ensureInitialized();
- if (jsapiService == null) {
- throw new RuntimeException("微信支付未初始化,请检查配置");
- }
- // 判断openid类型,选择对应的appid
- String appid = determineAppid(openid);
- log.info("使用appid: {}", appid);
- String notifyUrl = wxPayConfigService.getNotifyUrl();
- // 构建下单请求
- PrepayRequest request = new PrepayRequest();
- request.setAppid(appid);
- request.setMchid(wxPayConfigService.getMchId());
- request.setDescription(description);
- request.setOutTradeNo(orderNo);
- request.setNotifyUrl(notifyUrl);
- Amount amountObj = new Amount();
- amountObj.setTotal(amount);
- amountObj.setCurrency("CNY");
- request.setAmount(amountObj);
- Payer payer = new Payer();
- payer.setOpenid(openid);
- request.setPayer(payer);
- // 调用JSAPI下单并获取调起支付参数
- PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(request);
- log.info("预支付成功,orderNo: {}", orderNo);
- return WxPayVO.builder()
- .orderNo(orderNo)
- .appId(appid)
- .timeStamp(response.getTimeStamp())
- .nonceStr(response.getNonceStr())
- .packageValue(response.getPackageVal())
- .signType(response.getSignType())
- .paySign(response.getPaySign())
- .totalFee(amount)
- .build();
- }
- /**
- * 根据openid判断应该使用哪个appid
- * @param openid 用户的openid(可能是H5的openid或小程序的mini_openid)
- * @return 对应的appid
- */
- private String determineAppid(String openid) {
- // 查询User表,判断openid是存储在openid字段还是mini_openid字段
- LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(User::getMiniOpenid, openid);
- User userByMiniOpenid = userMapper.selectOne(wrapper);
- if (userByMiniOpenid != null) {
- // 是小程序openid,使用小程序appid
- log.info("检测到小程序openid,使用小程序appid");
- return wxConfig.getMiniappAppid();
- }
- // 否则是H5 openid,使用H5 appid
- log.info("检测到H5 openid,使用H5公众号appid");
- return wxConfig.getH5Appid();
- }
-
- @Override
- public String verifyAndDecryptNotify(String serialNumber, String nonce, String timestamp,
- String signature, String body) {
- log.info("验证支付回调,serialNumber: {}", serialNumber);
-
- // 确保SDK已初始化
- ensureInitialized();
-
- if (notificationParser == null) {
- throw new RuntimeException("微信支付未初始化");
- }
-
- // 构建回调参数
- RequestParam requestParam = new RequestParam.Builder()
- .serialNumber(serialNumber)
- .nonce(nonce)
- .timestamp(timestamp)
- .signature(signature)
- .body(body)
- .build();
-
- // 解析并验证回调(SDK自动验签和解密)
- Transaction transaction = notificationParser.parse(requestParam, Transaction.class);
-
- log.info("回调验证成功,orderNo: {}, transactionId: {}, state: {}",
- transaction.getOutTradeNo(), transaction.getTransactionId(), transaction.getTradeState());
-
- return transaction.getOutTradeNo() + "|" + transaction.getTransactionId() + "|" + transaction.getTradeState();
- }
-
- @Override
- public String queryPaymentStatus(String orderNo) {
- log.info("查询支付状态,orderNo: {}", orderNo);
-
- // 确保SDK已初始化
- ensureInitialized();
-
- if (jsapiService == null) {
- throw new RuntimeException("微信支付未初始化");
- }
-
- QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
- request.setMchid(wxPayConfigService.getMchId());
- request.setOutTradeNo(orderNo);
-
- Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
-
- return transaction.getTradeState().name();
- }
-
- @Override
- public String queryOrderByOutTradeNo(String orderNo) {
- log.info("查询微信订单,orderNo: {}", orderNo);
-
- // 确保SDK已初始化
- ensureInitialized();
-
- if (jsapiService == null) {
- throw new RuntimeException("微信支付未初始化");
- }
-
- QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
- request.setMchid(wxPayConfigService.getMchId());
- request.setOutTradeNo(orderNo);
-
- try {
- Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
- if (transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
- return transaction.getTransactionId();
- }
- } catch (Exception e) {
- log.error("查询微信订单失败,orderNo: {}", orderNo, e);
- }
- return null;
- }
-
- @Override
- public boolean refund(String orderNo, String refundNo, Integer totalAmount,
- Integer refundAmount, String reason) {
- throw new UnsupportedOperationException("不支持退款");
- }
-
- /**
- * 加载私钥文件
- */
- private String loadPrivateKey(String path) {
- try {
- if (path.startsWith("classpath:")) {
- String resourcePath = path.replace("classpath:", "");
- ClassPathResource resource = new ClassPathResource(resourcePath);
- try (BufferedReader reader = new BufferedReader(
- new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
- return reader.lines().collect(Collectors.joining("\n"));
- }
- } else {
- return java.nio.file.Files.readString(java.nio.file.Paths.get(path));
- }
- } catch (Exception e) {
- log.error("加载私钥失败: {}", path, e);
- throw new RuntimeException("加载私钥失败", e);
- }
- }
-
- /**
- * 加载微信支付公钥文件
- */
- private String loadPublicKey(String path) {
- try {
- if (path.startsWith("classpath:")) {
- String resourcePath = path.replace("classpath:", "");
- ClassPathResource resource = new ClassPathResource(resourcePath);
- try (BufferedReader reader = new BufferedReader(
- new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
- return reader.lines().collect(Collectors.joining("\n"));
- }
- } else {
- return java.nio.file.Files.readString(java.nio.file.Paths.get(path));
- }
- } catch (Exception e) {
- log.error("加载公钥失败: {}", path, e);
- throw new RuntimeException("加载公钥失败", e);
- }
- }
-
- /**
- * 从证书文件读取序列号
- */
- private String loadCertSerialNo(String certPath) {
- try {
- String certContent;
- if (certPath.startsWith("classpath:")) {
- String resourcePath = certPath.replace("classpath:", "");
- ClassPathResource resource = new ClassPathResource(resourcePath);
- try (BufferedReader reader = new BufferedReader(
- new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
- certContent = reader.lines().collect(Collectors.joining("\n"));
- }
- } else {
- certContent = java.nio.file.Files.readString(java.nio.file.Paths.get(certPath));
- }
-
- // 解析PEM格式证书
- String certPem = certContent
- .replace("-----BEGIN CERTIFICATE-----", "")
- .replace("-----END CERTIFICATE-----", "")
- .replaceAll("\\s", "");
-
- byte[] certBytes = java.util.Base64.getDecoder().decode(certPem);
- CertificateFactory cf = CertificateFactory.getInstance("X.509");
- try (InputStream is = new ByteArrayInputStream(certBytes)) {
- X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
- // 获取序列号并转为16进制大写
- String serialNo = cert.getSerialNumber().toString(16).toUpperCase();
- log.info("证书序列号: {}", serialNo);
- return serialNo;
- }
- } catch (Exception e) {
- log.error("读取证书序列号失败: {}", certPath, e);
- throw new RuntimeException("读取证书序列号失败", e);
- }
- }
- }
|