支付代码暂存

This commit is contained in:
haown 2025-07-24 14:28:49 +08:00
parent 0a158ab1a3
commit 6f71c6abb6
11 changed files with 511 additions and 0 deletions

1
.gitignore vendored
View File

@ -39,5 +39,6 @@ nbdist/
.nb-gradle/ .nb-gradle/
target/ target/
logs/
!.mvn/wrapper/maven-wrapper.jar !.mvn/wrapper/maven-wrapper.jar

View File

@ -27,6 +27,7 @@
<poi.version>3.9</poi.version> <poi.version>3.9</poi.version>
<log4j2.version>2.17.2</log4j2.version> <log4j2.version>2.17.2</log4j2.version>
<project.name>exam-admin</project.name> <project.name>exam-admin</project.name>
<wechatpay-apiv3.version>0.4.4</wechatpay-apiv3.version>
</properties> </properties>
<parent> <parent>
@ -170,6 +171,20 @@
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<version>2.11.0</version> <version>2.11.0</version>
</dependency> </dependency>
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>${wechatpay-apiv3.version}</version>
</dependency>
<!-- httpclient 依赖-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -0,0 +1,62 @@
package com.yf.exam.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Description 微信支付接口地址
* @Author 纪寒
* @Date 2022-10-17 17:56:25
* @Version 1.0
*/
@Component
@Data
@ConfigurationProperties(prefix = "we-chat-payment-url-config")
public class WeChatPaymentUrlConfig {
/**
* 小程序JSAPI下单接口地址
*/
private String jsapiPalceOrderUrl;
/**
* 微信支付订单号查询接口地址
*/
private String queryOrderNoUrl;
/**
* 商户订单号接口查询
*/
private String queryMchIdUrl;
/**
* 关闭订单接口地址
*/
private String closeOrderUrl;
/**
* 申请退款接口地址
*/
private String refundApplyUrl;
/**
* 查询单笔退款接口地址
*/
private String refundQueryOrderUrl;
/**
* 申请交易账单接口地址
*/
private String tradeApplyBillUrl;
/**
* 申请资金账单接口地址
*/
private String capitalApplyBillUrl;
/**
* App下单接口地址
*/
private String appPlaceOrderUrl;
}

View File

@ -0,0 +1,42 @@
package com.yf.exam.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Description 新医路商户号微信支付参数配置类
* @Author 纪寒
* @Date 2022-10-17 16:55:04
* @Version 1.0
*/
@Data
@Component
@ConfigurationProperties(prefix = "xyl-we-chat-config")
public class XylWeChatPaymentConfig {
/**
* 新医路商户号
*/
private String xylMchId;
/**
* 新医路API证书序号getXylVerifier
*/
private String xylMchSerialNo;
/**
* 新医路私钥文件
*/
private String xylPrivateKeyPath;
/**
* 新医路API V3版本密钥
*/
private String xylPaymentKey;
/**
* 新医路支付回调地址
*/
private String xylWeChatNotifyUrl;
}

View File

@ -0,0 +1,109 @@
package com.yf.exam.config.payment;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import com.yf.exam.config.XylWeChatPaymentConfig;
import com.yf.exam.core.exception.ServiceException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
/**
* @Description 新医路商户号微信支付核心配置类
* @Author 纪寒
* @Date 2022-10-17 18:56:22
* @Version 1.0
*/
@Configuration
@Slf4j
public class XylWeChatPaymentUtilConfig {
@Resource
private XylWeChatPaymentConfig xylWeChatPaymentConfig;
/**
* 获取新医路签名信息
*
* @return 签名信息
* @throws Exception 异常信息
*/
@Bean(name = "xylVerifier")
public Verifier getXylVerifier() throws Exception {
//获取商户私钥
PrivateKey privateKey = getPrivateKey(xylWeChatPaymentConfig.getXylPrivateKeyPath());
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(xylWeChatPaymentConfig.getXylMchSerialNo(), privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(xylWeChatPaymentConfig.getXylMchId(), privateKeySigner);
// 获取证书管理器实例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(xylWeChatPaymentConfig.getXylMchId(), wechatPay2Credentials, xylWeChatPaymentConfig.getXylPaymentKey().getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
return certificatesManager.getVerifier(xylWeChatPaymentConfig.getXylMchId());
}
/**
* 获取新医路商户号带有签名的http请求对象
*
* @return CloseableHttpClient对象
*/
@Bean(name = "xinYiLuWeChatPayClient")
public CloseableHttpClient getXinYiLuWeChatPayClient(Verifier xylVerifier) {
log.info("开始获取新医路商户带有签名信息的http请求对象........");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(xylWeChatPaymentConfig.getXylPrivateKeyPath());
CloseableHttpClient closeableHttpClient = null;
try {
log.info("新医路签名验证证书xinYiLuCertificatesVerifier{}", xylVerifier);
//通过WeChatPayHttpClientBuilder构造的HttpClient会自动的处理签名和验签并进行证书自动更新
closeableHttpClient = WechatPayHttpClientBuilder.create()
.withMerchant(xylWeChatPaymentConfig.getXylMchId(), xylWeChatPaymentConfig.getXylMchSerialNo(), privateKey)
.withValidator(new WechatPay2Validator(xylVerifier)).build();
} catch (Exception e) {
log.error("新医路验证签名信息失败,失败信息:{}", e.getMessage());
}
return closeableHttpClient;
}
/**
* 获取新医路商户号无签名的http请求对象
*
* @return CloseableHttpClient对象
*/
@Bean(name = "xinYiLuWeChatPayNoSignClient")
public CloseableHttpClient getXinYiLuWeChatPayNoSignClient() {
log.info("获取新医路商户的无签名信息的http请求对象.........");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(xylWeChatPaymentConfig.getXylPrivateKeyPath());
//通过WeChatPayHttpClientBuilder构造的HttpClient会自动的处理签名和验签并进行证书自动更新
return WechatPayHttpClientBuilder.create()
.withMerchant(xylWeChatPaymentConfig.getXylMchId(), xylWeChatPaymentConfig.getXylMchSerialNo(), privateKey)
.withValidator((response) -> true).build();
}
/**
* 获取商户的私钥文件
*
* @param filename 获取商户的私钥文件
* @return 商户私钥
*/
private PrivateKey getPrivateKey(String filename) {
try {
return PemUtil.loadPrivateKey(new ClassPathResource(filename).getInputStream());
} catch (Exception e) {
log.error("新医路商户私钥文件不存在,错误原因:{}", e.getMessage());
throw new ServiceException("新医路商户私钥文件不存在,请联系管理员!");
}
}
}

View File

@ -0,0 +1,6 @@
package com.yf.exam.modules.payment.service;
public interface PaymentService {
}

View File

@ -0,0 +1,152 @@
package com.yf.exam.modules.payment.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.yf.exam.config.WeChatPaymentUrlConfig;
import com.yf.exam.config.XylWeChatPaymentConfig;
import com.yf.exam.core.exception.ServiceException;
import com.yf.exam.modules.payment.service.PaymentService;
import com.yf.exam.modules.payment.vo.WeChatAppletSignVO;
import com.yf.exam.modules.utils.WeChatUtil;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Resource;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.util.Base64Utils;
/**
* @description:
* @author: haown
* @create: 2025-07-23 09:44
**/
public class PaymentServiceImpl implements PaymentService {
@Resource(name = "xinYiLuWeChatPayClient")
private CloseableHttpClient xinYiLuWeChatPayClient;
@Resource
private WeChatPaymentUrlConfig weChatPaymentUrlConfig;
@Resource
private WeChatUtil weChatUtil;
@Resource
private XylWeChatPaymentConfig xylWeChatPaymentConfig;
/**
* H5下单成功状态码
*/
private static final int HAVE_BODY_SUCCESS_CODE = 200;
/**
* H5下单成功状态码
*/
private static final int NO_HAVE_BODY_SUCCESS_CODE = 204;
/**
* 签名算法
*/
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* 签名方式
*/
private static final String SIGN_TYPE = "RSA";
/**
* 请求h5下单接口方法
*
* @param jsApiParams 下单参数
* @param paymentDTO 输入参数
* @param prepayId JsAp下单接口返回值
* @return 结果
* @throws Exception 异常信息
*/
private String requestH5Interface(String jsApiParams, PaymentDTO paymentDTO, String prepayId) throws Exception {
//log.info("JsApi请求下单参数 ====> {}", jsApiParams);
StringEntity stringEntity = new StringEntity(jsApiParams, StandardCharsets.UTF_8);
stringEntity.setContentType("application/json");
HttpPost httpPost = new HttpPost(weChatPaymentUrlConfig.getJsapiPalceOrderUrl());
httpPost.setEntity(stringEntity);
httpPost.setHeader("Accept", "application/json");
CloseableHttpResponse response = null;
String payAccount = "";
try {
response = xinYiLuWeChatPayClient.execute(httpPost);
payAccount = "山东柏杏新医健康服务有限公司";
//处理响应结果
if (Objects.isNull(response)) {
//log.error("JsApi下单接口执行错误 执行账户为 ====> {}", payAccount);
throw new ServiceException("JsApi下单接口执行异常请联系管理员");
}
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
//响应体
String result = EntityUtils.toString(response.getEntity());
if (statusCode == HAVE_BODY_SUCCESS_CODE) {
//请求成功取出prepay_id的值并放入Redis缓存中prepay_id的有效期为两小时此处缓存一个半小时
Map<String, Object> resultMap = JSONObject.parseObject(result);
prepayId = resultMap.getOrDefault("prepay_id", "").toString();
redisTemplate.opsForValue().set(Constants.PREPAY_ID_KEY + paymentDTO.getOrderNo(), prepayId, 5400, TimeUnit.SECONDS);
} else if (statusCode == NO_HAVE_BODY_SUCCESS_CODE) {
//请求成功无返回体
log.info("JsApi下单成功无返回值信息");
} else {
//请求失败
log.error("JsApi下单失败失败原因为 ====> {}", result);
throw new ServiceException("JsApi下单失败失败原因为" + result);
}
} catch (Exception e) {
log.error("JsApi下单失败失败原因 =====> {}", e.getMessage());
throw new ServiceException("JsApi下单失败请联系管理员");
} finally {
if (response != null) {
response.close();
}
}
return prepayId;
}
/**
* 构建微信小程序调起支付参数设置
*
* @param prepayId jsapi下单返回参数信息
* @param buySource 购买来源
* @return 参数信息
* @throws Exception 异常信息
*/
private WeChatAppletSignVO getSignInfo(String prepayId, String buySource) throws Exception {
//根据购买来源判断使用泉医到家小程序id还是泉医助手小程序id
String appId = BuySourceEnum.TRAINING.getInfo().equals(buySource) ? nurseAppletChatConfig.getAppletId() : appletChatConfig.getAppletId();
//随机字符串
String nonceStr = UUID.randomUUID().toString().replace("-", "");
//时间戳
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
//获取商户私钥
PrivateKey privateKey = null;
privateKey = weChatUtil.getPrivateKey(xylWeChatPaymentConfig.getXylPrivateKeyPath());
if (privateKey == null) {
throw new ServiceException("获取商户私钥失败,请联系管理员!");
}
//计算签名信息
prepayId = "prepay_id=" + prepayId;
String signatureStr = Stream.of(appId, timestamp, nonceStr, prepayId)
.collect(Collectors.joining("\n", "", "\n"));
log.info("计算签名认证信息 =====> {}", signatureStr);
Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM);
sign.initSign(privateKey);
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
String paySign = Base64Utils.encodeToString(sign.sign());
return WeChatAppletSignVO.builder().appId(appId).timeStamp(timestamp).nonceStr(nonceStr).prepayId(prepayId)
.signType(SIGN_TYPE).paySign(paySign).build();
}
}

View File

@ -0,0 +1,46 @@
package com.yf.exam.modules.payment.vo;
import java.io.Serializable;
import lombok.Builder;
import lombok.Data;
/**
* @Description 微信小程序调起支付参数信息
* @Author 纪寒
* @Date 2022-10-20 09:59:19
* @Version 1.0
*/
@Builder
@Data
public class WeChatAppletSignVO implements Serializable {
private static final long serialVersionUID = -2527792374746553807L;
/**
* 小程序id
*/
private String appId;
/**
* 时间戳
*/
private String timeStamp;
/**
* 随机字符串
*/
private String nonceStr;
/**
* 订单详情扩展字符串小程序为prepay_id的值
*/
private String prepayId;
/**
* 签名方式
*/
private String signType;
/**
* 签名
*/
private String paySign;
}

View File

@ -0,0 +1,34 @@
package com.yf.exam.modules.utils;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import com.yf.exam.core.exception.ServiceException;
import java.security.PrivateKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
/**
* @Description 微信工具类
* @Author 纪寒
* @Date 2022-10-20 10:11:38
* @Version 1.0
*/
@Component
@Slf4j
public class WeChatUtil {
/**
* 获取商户的私钥文件
*
* @param filename 获取商户的私钥文件
* @return 商户私钥
*/
public PrivateKey getPrivateKey(String filename) {
try {
return PemUtil.loadPrivateKey(new ClassPathResource(filename).getInputStream());
} catch (Exception e) {
log.error("商户私钥文件不存在,错误原因:{}", e.getMessage());
throw new ServiceException("商户私钥文件不存在,请联系管理员!");
}
}
}

View File

@ -29,3 +29,19 @@ server:
min-response-size: 10 min-response-size: 10
mime-types: application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css mime-types: application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css
# 新医路微信商户号配置参数
xyl-we-chat-config:
# 新医路商户号 1633348407 山东新医路信息科技有限公司 山东柏杏新医健康服务有限公司
xyl-mch-id: 1690248007
# 新医路商户号API证书序号 7C6A18FC8E1F0445901B1BE1C4DD1ACE284C3D79
xyl-mch-serial-no: 760D3316C2F3DF8D1DB05B56A37BFCD3C34EEDA8
# 新医路商户私钥文件
xyl-private-key-path: baixing_apiclient_key.pem
# 新医路API V3版本密钥 Xyl699003981qazVFR4xsw23edcASDFG
xyl-payment-key: baixingXINYIHULIZHAN202400000000
# 新医路微信支付回调地址 https://quanyidaojia.xinelu.cn
xyl-wechat-notify-url: http://8.131.93.145:54097
# h5支付接口地址
we-chat-payment-url-config:
# h5下单接口地址
h5-palce-order-url: https://api.mch.weixin.qq.com/v3/pay/transactions/h5

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUI5gWlq+q0K1k
+NZZe2qJhUH1qce00Dc6j7Rc6Ubnb7u7/UEqJMc/N2m/oIBdb5Lmb3OJZDhV8kHE
S1/U9aM/UZEW2fJWkcHxCyIreVJvETSCdFtgvk/lzwkkLZZiA8Hb0Irq4nJB+QFl
K4o7LLDMYhTI14WeH46jgOTdk5e1jKl2F9ShAt9TX3ZHVgnY4wp+iG3xAZytjyLJ
szbPLVrYAt3JTfzstsdfIIafKssBFExglzjnCeS6DCBDZkr9vEqG921nAKVVlC6j
CPDtxITddF25+uI7c0CAozCA0TAoXL7tyzgfnzfy3KkLz3wmMyb5IN/6vDg4A559
wNNmE1jdAgMBAAECggEAKKs6OPpiawjedQEPdtAmOH8HiiUmWA+ixuNN3JIMOco9
32hJ2d57JC9nYolOkpsVOoAbUjeYZUGwQgWBNb5xsW3dttfJkbcXyzIrNtJnb0uJ
GYldC0rw3km2ouzqa5f0zkNInocTCHrIPbD/KyECM+yJt4c54nqCSe4n8iKdbvxS
vNKcpQatfs2Egm1PcHsZ00bNSWlii/cz30CVUGCPYx0TZ0WhcjI5nFHINpRIM9rJ
A0K2R0rqE6/9MXNAJe4+h2TRvmWghBhx+QHCQRnc5ExIGoVsSiGvQkkMQmW8wmmw
SMxjAWjWNphFh4a+6Zv3UAfRyIjaS40RYyN7y3MeAQKBgQD+kjDOlNrsGoP35dbo
NuZieFWqm1gowA2cRwoAJ/qImllU7TTgVAyrMo9vu36ABw+rhDW6rSFB9r7hGGXj
NS5RfiezazwGj7ymQj3WtUoJlCmCuOGVIxCbQPUgSRc7+VBdvNf+6oUhIua7ncEG
WNAmcbU0gdAjG5S7j/LzOoStoQKBgQDVVG4gDeGktG+EQAsdcItm78Wzb+7P/5Bc
uyISCfENqbmzzfogRHqZEN5C+ZZP3S/On6q7jpDUWFP+WBBED6hezESo4kVA7Zh9
LsGU58nTGijxMoo05eUaXmZzvmjgNmpMHOMCKBCPnGZ67DeRwB/oFAfKwx4RRa++
VHrYnLSJvQKBgQCnjJRvAu3rw6/j8vQ1Nhz/5m+LsF6fw3exyde9LpLoYZ42FUFd
/DOyYb6iLWce4IbhvkyWpuhiwAH1qNc2aYQowr1ii0ugje3+B08oB33JPCjuDrz6
KW/+nww2yaRvJrJMX8RR1Qz2OEBgb33hrYaiJui6vE2/LaIAqMegan10wQKBgCVU
XODnDPERUeJGgqtoIjylAGdh0tw60Dwp0tBtRO0rIf/Ar2AuG3Xlab82hYL8JX12
mx2u2NEPk8MSDeabBs9v1yPmVFAEQ4bEQ8OWu85g6YErYtvWzrxKgIsIarmxS/B9
rYuXDy7SI2ynISI0CGFIAAUPF5fWJeacSdLVuRHRAoGBAO2cBz0bZJdKhoR+NMqN
CHpiTY+72hLhy4LB0XV7KsEumI0KI2i5EA4/cD+VCb+Qe6Hagoy78W/0s5MwBWzk
OwJ5JxBPZZROGwegxfb1P3NC1wazprKVTYbIUSomFbON4qQDjou13Y7gFeWsv2gE
Xq/BCiAbrbmltVKdxdqtRaQP
-----END PRIVATE KEY-----