作者 徐宝林

支付功能复制

... ... @@ -234,6 +234,20 @@
<type>pom</type>
</dependency>
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.17</version>
</dependency>
<!-- 支付宝支付 -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.40.354.ALL</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
... ...
package com.aigeo.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class ApiConfig implements Serializable {
/**
* 主键id
*/
private Integer id;
/**
* 接口类型(1.文档转换 2.文档扫描 3.图片识别 4.智能翻译 5.短信 6.微信 7.邮件 8.EDM)
*/
private Integer apiType;
/*
* 接口类型子模块
* */
private Integer apiClassify;
/**
* 接口提供方
*/
private String apiName;
/**
* 企业id
*/
private Integer companyId;
/**
* API的ID号
*/
private String apiId;
/**
* API的key
*/
private String apiKey;
/**
* API的code值
*/
private String apiCode;
/**
* API的host
*/
private String apiUrl;
/**
* API的方法名
*/
private String apiPath;
/*
* Api回调地址
* */
private String apiRedirectUri;
/**
* API其它
*/
private String apiTest;
/**
* API的更新日期
*/
private String updateTime;
/**
* 备注说明
*/
private String remarks;
/*
* 系统标识
* */
private String code;
/*
* 是否启用(0:否 1:是)
* */
private Integer isEnable;
//--------------------20250804新增以下微信支付相关配置字段----------------------
/**
* 微信支付私钥路径
*/
private String privateKeyPath;
/**
* 微信支付公钥路径
*/
private String publicKeyPath;
/**
* 微信退款回调地址
*/
private String refundRedirectUrl;
/**
* 商户号
*/
private String merchantId;
//--------------------20250804新增以上微信支付相关配置字段----------------------
private static final long serialVersionUID = 1L;
}
... ...
package com.aigeo.entity;
import lombok.Data;
@Data
public class PayDto {
private String desc;//描述
private String amount;//金额
private String orderId;
private ApiConfig apiConfig;
private Integer companyId;
}
\ No newline at end of file
... ...
package com.aigeo.entity;
import lombok.Data;
@Data
public class PayPalDTO {
private String intent;
private PaymentSource payment_source;
private PurchaseUnit[] purchase_units;
@Data
public static class PaymentSource {
private Paypal paypal;
@Data
public static class Paypal {
private ExperienceContext experience_context;
@Data
public static class ExperienceContext {
private String return_url;
private String cancel_url;
}
}
}
@Data
public static class PurchaseUnit {
private String custom_id;
private Amount amount;
@Data
public static class Amount {
private String currency_code;
private String value;
}
}
}
... ...
package com.aigeo.entity;
import lombok.Data;
@Data
public class PayPalOrderRecord {
private Long id;
private Integer companyId;
private String paypalId;
private String payerId;
private String orderId;
private String createTime;
private String updateTime;
private Integer state;
}
\ No newline at end of file
... ...
package com.aigeo.entity;
import lombok.Data;
@Data
public class PayPalResponse {
private String id;
private String status;
private ResMsg[] links;
@Data
public static class ResMsg{
private String href;
private String rel;
private String method;
}
}
... ...
package com.aigeo.util.pay;
import com.aigeo.entity.ApiConfig;
import com.aigeo.entity.PayDto;
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.diagnosis.DiagnosisUtils;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class AliPay implements PaymentStrategy{
// 环境切换开关,"sandbox" 或 "prod",根据实际环境设置
private static final String environment = "sandbox";
// 定义环境配置
public static Map<String, Map<String, String>> CONFIG_MAG = new HashMap<String, Map<String, String>>();
static {
// 沙箱环境配置
Map<String, String> sandbox = new HashMap<String, String>();
// AppId,对应配置项为“应用信息”。
sandbox.put("appId", "9021000123615361");
// 支付宝沙箱网关地址(sandboxAlipayGateway),对应配置项为“支付宝网关”。
sandbox.put("gatewayUrl", "https://openapi-sandbox.dl.alipaydev.com/gateway.do");
// 支付宝沙箱公钥(sandboxAlipayPublicKey),对应配置项为“密钥配置”。
sandbox.put("alipayPublicKey", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkksZzpOGWoGMz9lz7y79DtaXuvsOs4d4ChdYir3/FSRHbYn/4G25LYw+7h00s8JCXIPhLoozWi9ZFN3hEMUHreT0/UPk0LgX7AS43VQHmR0cLhOOf2o5zxpzN9SjU/zK3ZQT0rEuNyoikPaY/UK4fqHvySBc7OfeKefDqdS/v/BchwzlzefEqXg4imz0uUMhn8micUuAeT7iF72NxlqCBmRXzOBx/CaUCOA2BMn2b3KDUix2qiy8FzkPQRj5dXQbDu8JFsHDAU/T9as8JY1ZRDI3wPgHverb5SuyFydeQsIEHSmC89k+TySa4bqbQRZ5+LOFzbkyrCXXQ9y1V/4jgQIDAQAB");
// 应用沙箱私钥(sandboxAppPrivateKey),对应配置项为“密钥配置”。
sandbox.put("appPrivateKey", "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCl7rLc1vU1YMij5nGna2Tiqw08TgCEoaviLgHaenBJi8JfUDndm01XAZ6Vfia53F4yvj8woPkiOzn/HYEfqs3+SAPbdQF/KyZ6GocAxwZBAhXTLD+c/D3pRX9lrOo+krw6H8JoJeVrDD2v4N1fWJBfh8U4x+pMcysYnDZbS9xquGNaPIpEnGZ7FHCvQo5Uq2xDv4iApwmiWdwNcgN9xatCy5deM9ot08cd+rL9NcnFnuWdxeQVBLvYS6w28kJUKKOIIUXKhWkpVTspZksUJRqKevlgw+wL2ORFB0CiAV9l7wugdt0V2KqLeroUNuOYkh9fBTwXTEv/NHZ+mUjmECETAgMBAAECggEAFdmm5/nOdGb4RMz1rmwv7sFDjutemhiH/9K7H7/7xVOIFvEDjbEDsC9gaKMiCluAdAIlZAvugpl1+6gw9rCRS6jECUQRHGPcPzv7BxkuM6VIpnQvNEbrCCrEqejpN7Au5SCFgx1qZnxIKz7bJ9ELweipCd8ZdVO9GxKG/eKxdcP3ZntujxkcSsgaYLKp7KozWmFYTFphGTsRjrJYofg6KpIONxDIigQfNWpyBHCre13lAsP+nxccOCw1Lx2/m1Xda0U7XWDeRePiutrsOV2KPZa59qv15tezjsIs0yhQnTPmDLMRQ2a5oJLRuPRreLLJwDGsG0FZXNUEQK6mWnD4gQKBgQDqXsNwmgTEPW/jcq2Myh4iLlY4Ag3kR7iVOsFn4PbZrqf+B52wSepp4uT9+10z6nuKADMWyUMS+jwPAeoWlhygUkhdB8L50KDcXztsz9NGQ+4LX9OGJncp616Lw3pdIFMkptPuCvjUISi/xy/24VbkgLzosfr2a1rIf7QwHpwI8wKBgQC1PwXaw1eEstzwK/+ov0ptIRQKQlw7frJHx3rRPFCu94G6pbNOLN2FhzZ5D0sIAHlQFhkUR34qOTw/bgS1WduUHWKH+pSGhW7g/hb2G6/gKsGgTUvdW8/jVQoTdS++iKJkI6q08lAdZG769c+PH1gb/RdArhHk979OTwUpmIuPYQKBgQC3/jE8ow1+6iR+0TbCQqKpfusdCS/SuSOFtBzF6mygtGcaHOyEy2KGQiQ36DBhY4Ic8s8o4lQP//dKaNxXYv2SN0asEj3VVR4UI4dJg1Z/4TBuKYqr1GLUu/z7iDj2Bzx+l6HlTSPPlUCiXkOfc4R92ztzCtbUwa8BnI/e/wjMVQKBgE1J+conIOwShgrZCGXCZQ+SWoRhdglc5tObKjGNoe5q12RrgsHkefaWCVGohtt7sD/JnJo8Pn3s+FcoKFPZZLyNp1XLMokG35iMYehDOEXEldsw+xgDLeyIeAqCH4EFrRI872/IIQfXq0fTiNZEjIMq8z5+vFzClU7kfZQWW/UhAoGBANjKR0NJKOMBTGJhSuSL4mL96ayoUoiYDXkQMVvP0oGAt6jdq34lyl+e4n02yhd5IHIDOXrIdx2xnd+HAOBI1YPAYMJ4gHC8WQM9dbjxtqyyNGSQnpVLCAdxvVInCrJdYR82+26oPIUbMtOHVkjtCr47wXlSHb82Ax9UgL5Bhl7N");
// 线上环境配置
Map<String, String> prod = new HashMap<String, String>();
// AppId,对应配置项为“应用信息”。
prod.put("appId", "${appId}");
// 支付宝线上网关地址(prodAlipayGateway),对应配置项为“支付宝网关”。
prod.put("gatewayUrl", "${prodAlipayGateway}");
// 支付宝线上公钥(prodAlipayPublicKey),对应配置项为“密钥配置”。
prod.put("alipayPublicKey", "${prodAlipayPublicKey}");
// 应用线上私钥(prodAppPrivateKey),需要替换为线上环境使用的应用私钥,对应配置项为“密钥配置”,注意 Java 语言需要 PKCS8 格式的密钥。
prod.put("appPrivateKey", "${prodAppPrivateKey}");
CONFIG_MAG.put("sandbox", sandbox);
CONFIG_MAG.put("prod", prod);
}
@Override
public String pay(PayDto payDto) {
try {
AlipayConfig alipayConfig = new AlipayConfig();
ApiConfig config = payDto.getApiConfig();
alipayConfig.setServerUrl(config.getApiUrl());
alipayConfig.setAppId(config.getApiId());
String path = this.getClass().getResource("/").getPath();
path = path.replace("/", "//");
path = path.replace("//target//classes//", "");
path = path.replace("//WEB-INF//classes//", "");
//获取秘钥内容
String publicKey = getKey(path+config.getPublicKeyPath());
String privateKey = getKey(path+config.getPrivateKeyPath());
alipayConfig.setPrivateKey(privateKey);
alipayConfig.setAlipayPublicKey(publicKey);
alipayConfig.setFormat("json");
alipayConfig.setCharset("UTF-8");
alipayConfig.setSignType("RSA2");
// 初始化SDK
AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
// 构造请求参数以调用接口
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
// 设置商户订单号
model.setOutTradeNo(payDto.getOrderId());
model.setProductCode("QR_CODE_OFFLINE");
// 设置订单总金额
model.setTotalAmount(payDto.getAmount());
// 设置订单标题
model.setSubject(payDto.getOrderId());
// 设置商户的原始订单号
model.setMerchantOrderNo(payDto.getOrderId());
request.setBizModel(model);
request.setNotifyUrl(config.getApiRedirectUri()+"/"+payDto.getCompanyId());
AlipayTradePrecreateResponse response = alipayClient.execute(request);
System.out.println(response.getBody());
if (response.isSuccess()) {
log.info("订单号:{}请求支付宝支付",response.getOutTradeNo());
return response.getQrCode();
} else {
System.out.println(response.getSubMsg());
System.out.println("调用失败");
String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
System.out.println(diagnosisUrl);
return response.getSubMsg();
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public void closeTrade(String orderId, String mchId, ApiConfig apiConfig) {
}
@Override
public void refund() {
}
@Override
public String getChannel() {
return "alipay";
}
private String getKey(String filePath) throws IOException{
String fixed = filePath
.replaceAll("^/+([A-Za-z]):/+", "$1:/") // 去掉前导 // 并保留盘符
.replace('/', File.separatorChar); // 把 / 换成平台分隔符
// 2. 读取
byte[] bytes = Files.readAllBytes(Paths.get(fixed));
return new String(bytes, StandardCharsets.UTF_8);
}
}
... ...
package com.aigeo.util.pay;
import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* 项目名称: 微信支付回调解密
* 版权所有: 深圳市科飞时速网络技术有限公司(0755-88843776)
* 程序版本: V3.0
* 技术支持: info@21gmail.com
* 单元名称: 支付接口
* 开始时间: 2025/08/04 15:13
* 最后修改: 2025/08/04 15:13
* 开发人员: 温志锋
* 备 注: 如需修改请联系开发人员
*/
public class CheckUtil {
/**
* 验签
* @param wechatPublicKeyPath 公钥文件路径
* @param timestamp 时间戳
* @param nonce 验签的随机字符串
* @param body 请求体
* @param signature 签名值
* @return 验签成功
*/
public static boolean verifySignature(String wechatPublicKeyPath, String timestamp, String nonce, String body, String signature){
try{
// 1. 构建验签串
String signMessage = buildSignMessage(timestamp, nonce, body);
// 2. 解码Base64签名
byte[] signatureBytes = Base64.getDecoder().decode(signature);
// 3. 加载微信支付公钥
PublicKey publicKey = loadPublicKey(wechatPublicKeyPath);
// 4. 执行验签
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(publicKey);
signer.update(signMessage.getBytes(StandardCharsets.UTF_8));
return signer.verify(signatureBytes);
}catch(Exception e){
e.printStackTrace();
System.out.println("---------验签异常---------"+e);
return false;
}
}
/**
* 解密方法
* @param ciphertext 数据密文
* @param associatedData 附加数据
* @param nonce 随机串
* @param vkey v3key
* @return 解密数据
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
public static String decryptData(String ciphertext, String associatedData, String nonce,String vkey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] keyBytes = vkey.getBytes(StandardCharsets.UTF_8);
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
if (associatedData != null) {
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
}
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 构建验签串
* 格式: timestamp + "\n" + nonce + "\n" + body + "\n"
*/
private static String buildSignMessage(String timestamp, String nonce, String body) {
// 处理空body情况(如204响应)
if (body == null) {
body = "";
}
return timestamp + "\n" + nonce + "\n" + body + "\n";
}
/**
* 从PEM格式字符串加载公钥
*/
private static PublicKey loadPublicKey(String filePath) throws GeneralSecurityException {
try {
String pemContent = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
//1. 移除PEM头尾标记和空白字符
String publicKeyPEM = pemContent
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
System.out.println("----公钥:"+publicKeyPEM);
// 2. Base64解码
byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
// 3. 创建KeyFactory
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 4. 生成公钥
return keyFactory.generatePublic(new X509EncodedKeySpec(encoded));
//byte[] certBytes = Files.readAllBytes(Paths.get(filePath));
//CertificateFactory cf = CertificateFactory.getInstance("X.509");
//X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes));
//return cert.getPublicKey();
} catch (Exception e) {
e.printStackTrace();
throw new GeneralSecurityException("加载微信支付公钥失败", e);
}
}
}
... ...
package com.aigeo.util.pay;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
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.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.HashMap;
import java.util.Map;
public class HttpClientUtils
{
public static final String SunX509 = "SunX509";
public static final String JKS = "JKS";
public static final String PKCS12 = "PKCS12";
public static final String TLS = "TLS";
public static HttpURLConnection getHttpURLConnection(String strUrl)
throws IOException
{
URL url = new URL(strUrl);
HttpURLConnection httpURLConnection = (HttpURLConnection)url
.openConnection();
return httpURLConnection;
}
/**
* 发送HTTP_GET请求
*
* @see
* @param reqURL
* 请求地址(含参数)
* @param decodeCharset
* 解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码
* @return 远程主机响应正文
*/
public static String sendGetRequest(String reqURL, String decodeCharset) {
long responseLength = 0; // 响应长度
String responseContent = null; // 响应内容
HttpClient httpClient = new DefaultHttpClient(); // 创建默认的httpClient实例
HttpGet httpGet = new HttpGet(reqURL); // 创建org.apache.http.client.methods.HttpGet
try {
HttpResponse response = httpClient.execute(httpGet); // 执行GET请求
HttpEntity entity = response.getEntity(); // 获取响应实体
if (null != entity) {
responseLength = entity.getContentLength();
responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
EntityUtils.consume(entity); // Consume response content
}
System.out.println("请求地址: " + httpGet.getURI());
System.out.println("响应状态: " + response.getStatusLine());
System.out.println("响应长度: " + responseLength);
System.out.println("响应内容: " + responseContent);
} catch (ClientProtocolException e) {
System.out.println("该异常通常是协议错误导致,比如构造HttpGet对象时传入的协议不对(将'http'写成'htp')或者服务器端返回的内容不符合HTTP协议要求等,堆栈信息如下");
} catch (ParseException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println("该异常通常是网络原因引起的,如HTTP服务器未启动等,堆栈信息如下");
} finally {
httpClient.getConnectionManager().shutdown(); // 关闭连接,释放资源
}
return responseContent;
}
public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder) {
return sendPostRequest(reqURL, sendData, isEncoder, null, null,"application/json");
}
public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder, String encodeCharset,
String decodeCharset,String contentType) {
String responseContent = null;
HttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(reqURL);
// httpPost.setHeader(HTTP.CONTENT_TYPE,
// "application/x-www-form-urlencoded; charset=UTF-8");
httpPost.setHeader(HTTP.CONTENT_TYPE, contentType);
try {
if (isEncoder) {
httpPost.setEntity(new StringEntity(sendData, encodeCharset == null ? "UTF-8" : encodeCharset));
} else {
httpPost.setEntity(new StringEntity(sendData));
}
// 设置请求超时时间
httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 5000);
HttpResponse response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();
if (null != entity) {
responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
EntityUtils.consume(entity);
}
} catch (Exception e) {
System.out.println("与[" + reqURL + "]通信过程中发生异常,堆栈信息如下");
e.printStackTrace();
} finally {
httpClient.getConnectionManager().shutdown();
}
return responseContent;
}
public static HttpsURLConnection getHttpsURLConnection(String strUrl)
throws IOException
{
URL url = new URL(strUrl);
HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url
.openConnection();
return httpsURLConnection;
}
public static String getURL(String strUrl)
{
if (strUrl != null) {
int indexOf = strUrl.indexOf("?");
if (-1 != indexOf) {
return strUrl.substring(0, indexOf);
}
return strUrl;
}
return strUrl;
}
public static String getQueryString(String strUrl)
{
if (strUrl != null) {
int indexOf = strUrl.indexOf("?");
if (-1 != indexOf) {
return strUrl.substring(indexOf + 1, strUrl.length());
}
return "";
}
return strUrl;
}
public static Map queryString2Map(String queryString)
{
if ((queryString == null) || ("".equals(queryString))) {
return null;
}
Map m = new HashMap();
String[] strArray = queryString.split("&");
for (int index = 0; index < strArray.length; index++) {
String pair = strArray[index];
putMapByPair(pair, m);
}
return m;
}
public static void putMapByPair(String pair, Map m)
{
if ((pair == null) || ("".equals(pair))) {
return;
}
int indexOf = pair.indexOf("=");
if (-1 != indexOf) {
String k = pair.substring(0, indexOf);
String v = pair.substring(indexOf + 1, pair.length());
if ((k != null) && (!"".equals(k))) {
m.put(k, v);
}
} else {
m.put(pair, "");
}
}
public static String bufferedReader2String(BufferedReader reader)
throws IOException
{
StringBuffer buf = new StringBuffer();
String line = null;
while ((line = reader.readLine()) != null) {
buf.append(line);
buf.append("\r\n");
}
return buf.toString();
}
public static void doOutput(OutputStream out, byte[] data, int len)
throws IOException
{
int dataLen = data.length;
int off = 0;
while (off < dataLen) {
if (len >= dataLen) {
out.write(data, off, dataLen);
} else {
out.write(data, off, len);
}
out.flush();
off += len;
dataLen -= len;
}
}
public static SSLContext getSSLContext(FileInputStream trustFileInputStream, String trustPasswd, FileInputStream keyFileInputStream, String keyPasswd)
throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, KeyManagementException
{
TrustManagerFactory tmf =
TrustManagerFactory.getInstance("SunX509");
KeyStore trustKeyStore = KeyStore.getInstance("JKS");
trustKeyStore.load(trustFileInputStream,
str2CharArray(trustPasswd));
tmf.init(trustKeyStore);
char[] kp = str2CharArray(keyPasswd);
KeyManagerFactory kmf =
KeyManagerFactory.getInstance("SunX509");
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(keyFileInputStream, kp);
kmf.init(ks, kp);
SecureRandom rand = new SecureRandom();
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), rand);
return ctx;
}
public static Certificate getCertificate(File cafile)
throws CertificateException, IOException
{
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream in = new FileInputStream(cafile);
Certificate cert = cf.generateCertificate(in);
in.close();
return cert;
}
public static char[] str2CharArray(String str)
{
if (str == null) {
return null;
}
return str.toCharArray();
}
public static void storeCACert(Certificate cert, String alias, String password, OutputStream out)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException
{
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(null, null);
ks.setCertificateEntry(alias, cert);
ks.store(out, str2CharArray(password));
}
public static InputStream String2Inputstream(String str)
{
return new ByteArrayInputStream(str.getBytes());
}
}
... ...
package com.aigeo.util.pay;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.*;
import java.util.*;
public class PayKit {
/**
* 获取证书
*
* @param inputStream 证书文件
* @return {@link X509Certificate} 获取证书
*/
public static X509Certificate getCertificate(InputStream inputStream) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
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);
}
}
/**
* 简化的UUID,去掉了横线,使用性能更好的 ThreadLocalRandom 生成UUID
*
* @return 简化的 UUID,去掉了横线
*/
public static String generateStr() {
return IdUtil.fastSimpleUUID();
}
/**
* 构造签名串
*
* @param method {@link} GET,POST,PUT等
* @param url 请求接口 /v3/certificates
* @param timestamp 获取发起请求时的系统当前时间戳
* @param nonceStr 随机字符串
* @param body 请求报文主体
* @return 待签名字符串
*/
public static String buildSignMessage(String method, String url, String timestamp, String nonceStr, String body) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add(method);
arrayList.add(url);
arrayList.add(String.valueOf(timestamp));
arrayList.add(nonceStr);
arrayList.add(body);
return buildSignMessage(arrayList);
}
/**
* 构造签名串
*
* @param signMessage 待签名的参数
* @return 构造后带待签名串
*/
public static String buildSignMessage(ArrayList<String> signMessage) {
if (signMessage == null || signMessage.size() <= 0) {
return null;
}
StringBuilder sbf = new StringBuilder();
for (String str : signMessage) {
sbf.append(str).append("\n");
}
System.out.println("待签名字符串内容:" + sbf.toString());
System.out.println("待签名字符串长度:" + sbf.toString().length());
return sbf.toString();
}
/**
* v3 接口创建签名
*
* @param signMessage 待签名的参数
* @param keyPath key.pem 证书路径
* @return 生成 v3 签名
* @throws Exception 异常信息
*/
public static String createSign(String signMessage, String keyPath) throws Exception {
if (StrUtil.isEmpty(signMessage)) {
return null;
}
// 获取商户私钥
PrivateKey privateKey = PayKit.getPrivateKey(keyPath);
// 生成签名
return RsaKit.encryptByPrivateKey(signMessage, privateKey);
}
/**
* 获取商户私钥
*
* @param keyPath 商户私钥证书路径
* @return {@link PrivateKey} 商户私钥
* @throws Exception 异常信息
*/
public static PrivateKey getPrivateKey(String keyPath) throws Exception {
String originalKey = FileUtil.readUtf8String(keyPath);
String privateKey = originalKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
return RsaKit.loadPrivateKey(privateKey);
}
/**
* 获取授权认证信息
*
* @param mchId 商户号
* @param serialNo 商户API证书序列号
* @param nonceStr 请求随机串
* @param timestamp 时间戳
* @param signature 签名值
* @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048
* @return 请求头 Authorization
*/
public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) {
Map<String, String> params = new HashMap<>(5);
params.put("mchid", mchId);
params.put("serial_no", serialNo);
params.put("nonce_str", nonceStr);
params.put("timestamp", timestamp);
params.put("signature", signature);
return authType.concat(" ").concat(createLinkString(params, ",", false, true));
}
/**
* @param params 需要排序并参与字符拼接的参数组
* @param encode 是否进行URLEncoder
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params, boolean encode) {
return createLinkString(params, "&", encode);
}
/**
* @param params 需要排序并参与字符拼接的参数组
* @param connStr 连接符号
* @param encode 是否进行URLEncoder
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params, String connStr, boolean encode) {
return createLinkString(params, connStr, encode, false);
}
public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder content = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
// 拼接时,不包括最后一个&字符
if (i == keys.size() - 1) {
if (quotes) {
content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"');
} else {
content.append(key).append("=").append(encode ? urlEncode(value) : value);
}
} else {
if (quotes) {
content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr);
} else {
content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr);
}
}
}
return content.toString();
}
/**
* URL 编码
*
* @param src 需要编码的字符串
* @return 编码后的字符串
*/
public static String urlEncode(String src) {
try {
return URLEncoder.encode(src, CharsetUtil.UTF_8).replace("+", "%20");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
/**
* 构造签名串
*
* @param timestamp 应答时间戳
* @param nonceStr 应答随机串
* @param body 应答报文主体
* @return 应答待签名字符串
*/
public static String buildSignMessage(String timestamp, String nonceStr, String body) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add(timestamp);
arrayList.add(nonceStr);
arrayList.add(body);
return buildSignMessage(arrayList);
}
/**
* 公钥加密
*
* @param data 待加密数据
* @param certificate 平台公钥证书
* @return 加密后的数据
* @throws Exception 异常信息
*/
public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
byte[] cipherData = cipher.doFinal(dataByte);
return Base64.encode(cipherData);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的证书", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
}
}
/**
* 私钥解密
*
* @param cipherText 加密字符
* @param privateKey 私钥
* @return 解密后的数据
* @throws Exception 异常信息
*/
public static String rsaDecryptOAEP(String cipherText, PrivateKey privateKey) throws Exception {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] data = Base64.decode(cipherText);
return new String(cipher.doFinal(data), StandardCharsets.UTF_8);
} catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的私钥", e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new BadPaddingException("解密失败");
}
}
}
... ...
package com.aigeo.util.pay;
import com.aigeo.entity.*;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.Objects;
import java.util.UUID;
@Slf4j
@Component
public class PayPal implements PaymentStrategy{
private static final String TOKEN_URL = "/v1/oauth2/token";
private static final String ORDER_URL = "/v2/checkout/orders";
// @Autowired
// private PaypalOrderRecordDao paypalOrderRecordDao;
@Override
public String pay(PayDto payDto) throws IOException {
ApiConfig config = payDto.getApiConfig();
String tokenUrl=config.getApiUrl()+TOKEN_URL;
String token = getAccessToken(tokenUrl,config.getApiId(),config.getApiKey());
OkHttpClient client = new OkHttpClient();
String url=config.getApiUrl()+ORDER_URL;
PayPalDTO palDTO = new PayPalDTO();
palDTO.setIntent("CAPTURE");
PayPalDTO.PaymentSource paymentSource = new PayPalDTO.PaymentSource();
PayPalDTO.PaymentSource.Paypal paypal = new PayPalDTO.PaymentSource.Paypal();
PayPalDTO.PaymentSource.Paypal.ExperienceContext experienceContext = new PayPalDTO.PaymentSource.Paypal.ExperienceContext();
experienceContext.setReturn_url(config.getApiRedirectUri()+"/"+payDto.getCompanyId());
experienceContext.setCancel_url(config.getApiRedirectUri());
paypal.setExperience_context(experienceContext);
paymentSource.setPaypal(paypal);
palDTO.setPayment_source(paymentSource);
PayPalDTO.PurchaseUnit.Amount amount = new PayPalDTO.PurchaseUnit.Amount();
amount.setCurrency_code("USD");
amount.setValue(payDto.getAmount());
PayPalDTO.PurchaseUnit purchaseUnit = new PayPalDTO.PurchaseUnit();
purchaseUnit.setCustom_id(payDto.getOrderId());
purchaseUnit.setAmount(amount);
PayPalDTO.PurchaseUnit[] list = new PayPalDTO.PurchaseUnit[]{purchaseUnit};
palDTO.setPurchase_units(list);
String toJson = JSONObject.toJSONString(palDTO);
RequestBody body = RequestBody.create(toJson, MediaType.parse("application/json; charset=utf-8"));
String string = UUID.randomUUID().toString();
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("PayPal-Request-Id", string)
.addHeader("Authorization", "Bearer "+token)
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String res = Objects.requireNonNull(response.body()).string();
log.info("响应内容:{}",res);
PayPalResponse payPalResponse = JSONObject.parseObject(res, PayPalResponse.class);
PayPalResponse.ResMsg[] link = payPalResponse.getLinks();
log.info("订单:{}支付地址:{}",payDto.getOrderId(),link[1].getHref());
Date now = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String formattedTime = formatter.format(now);
PayPalOrderRecord orderRecord = new PayPalOrderRecord();
orderRecord.setCompanyId(payDto.getCompanyId());
orderRecord.setOrderId(payDto.getOrderId());
orderRecord.setPaypalId(payPalResponse.getId());
orderRecord.setState(0);
orderRecord.setCreateTime(formattedTime);
orderRecord.setUpdateTime(formattedTime);
// paypalOrderRecordDao.addPojo(orderRecord);
return link[1].getHref();
} else {
System.out.println("Request failed: " + response.code());
System.out.println("Response: " + response.body().string());
return null;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public void closeTrade(String orderId, String mchId, ApiConfig apiConfig) {
}
@Override
public void refund() {
}
public static String getAccessToken(String tokenUrl,String clientId,String clientSecret) throws IOException {
OkHttpClient client = new OkHttpClient();
// 创建请求体
RequestBody body = new FormBody.Builder()
.add("grant_type", "client_credentials")
.build();
// 创建请求
Request request = new Request.Builder()
.url(tokenUrl)
.post(body)
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Authorization", "Basic " + getBasicAuth(clientId,clientSecret))
.build();
// 发送请求
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
// 解析响应
String responseBody = response.body().string();
System.out.println("*-*--**--*-*-*-*-**--**-*-"+parseAccessToken(responseBody));
return parseAccessToken(responseBody);
} else {
throw new IOException("Failed to get access token: " + response.code());
}
}
}
private static String getBasicAuth(String clientId,String clientSecret) {
return Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
}
private static String parseAccessToken(String responseBody) {
// 简单解析 JSON 响应
String[] parts = responseBody.split(",");
for (String part : parts) {
if (part.contains("access_token")) {
return part.split(":")[1].trim().replace("\"", "");
}
}
throw new IllegalArgumentException("Access token not found in response: " + responseBody);
}
public static String extractPayerActionUrl(String jsonResponse) {
// 将 JSON 字符串解析为 JSONObject
JSONObject jsonObject = JSON.parseObject(jsonResponse);
// 获取 links 数组
JSONArray links = jsonObject.getJSONArray("links");
// 遍历 links 数组
for (int i = 0; i < links.size(); i++) {
JSONObject link = links.getJSONObject(i);
// 检查 rel 是否为 payer-action
if ("payer-action".equals(link.getString("rel"))) {
// 返回 href 值
return link.getString("href");
}
}
// 如果没有找到 payer-action 链接,返回 null
return null;
}
@Override
public String getChannel() {
return "paypal";
}
}
... ...
package com.aigeo.util.pay;
import com.aigeo.entity.ApiConfig;
import com.aigeo.entity.PayDto;
import com.alipay.api.AlipayApiException;
import java.io.IOException;
public interface PaymentStrategy {
//支付
String pay(PayDto payDto) throws AlipayApiException, IOException;
//根据系统订单号查询订单
//根据微信支付订单号查询订单
//关闭订单
void closeTrade(String orderId, String mchId, ApiConfig apiConfig);
//退款申请
void refund();
//查询退款申请
// 支持的支付渠道标识
String getChannel();
}
... ...
package com.aigeo.util.pay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class PaymentStrategyFactory {
@Autowired
private List<PaymentStrategy> strategies;
private final Map<String, PaymentStrategy> strategyMap = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
strategies.forEach(strategy -> strategyMap.put(strategy.getChannel(), strategy));
}
public PaymentStrategy getStrategy(String channel) {
PaymentStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("Unsupported payment channel: " + channel);
}
return strategy;
}
}
... ...
package com.aigeo.util.pay;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
public class RsaKit {
/**
* 加密算法RSA
*/
private static final String KEY_ALGORITHM = "RSA";
/**
* 私钥签名
*
* @param data 需要加密的数据
* @param privateKey 私钥
* @return 加密后的数据
* @throws Exception 异常信息
*/
public static String encryptByPrivateKey(String data, PrivateKey privateKey) throws Exception {
java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signed = signature.sign();
return StrUtil.str(Base64.encode(signed));
}
/**
* 从字符串中加载私钥<br>
* 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。
*
* @param privateKeyStr 私钥
* @return {@link PrivateKey}
* @throws Exception 异常信息
*/
public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception {
try {
byte[] buffer = Base64.decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法");
} catch (InvalidKeySpecException e) {
throw new Exception("私钥非法");
} catch (NullPointerException e) {
throw new Exception("私钥数据为空");
}
}
/**
* 公钥验证签名
*
* @param data 需要加密的数据
* @param sign 签名
* @param publicKey 公钥
* @return 验证结果
* @throws Exception 异常信息
*/
public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception {
java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8)));
}
}
\ No newline at end of file
... ...
package com.aigeo.util.pay;
import com.aigeo.entity.ApiConfig;
import com.aigeo.entity.PayDto;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class WechatPay implements PaymentStrategy{
/** 全局单例配置 */
//private static volatile Config globalConfig;
//@Autowired
//private ApiConfigDao apiConfigDao;
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/close";
/**
* 初始化完毕(所有 @Value 已注入)后,构造一次并缓存
*/
//@PostConstruct
//public void initConfig() throws Exception {
// if (globalConfig == null) {
// synchronized (WechatPay.class) {
// if (globalConfig == null) {
// ApiConfig wechatapy = apiConfigDao.findByApiCode(7,"wechatpay");
// if(wechatapy==null){
// throw new RuntimeException("微信支付初始化失败!");
// }
// globalConfig = new RSAAutoCertificateConfig.Builder()
// .merchantId(wechatapy.getMerchantId())
// .privateKeyFromPath(wechatapy.getPrivateKeyPath())
// .merchantSerialNumber(wechatapy.getApiCode())
// .apiV3Key(wechatapy.getApiKey())
// .build();
// }
// }
// }
//}
@Override
public String pay(PayDto payDto) {
ApiConfig apiConfig = payDto.getApiConfig();
String path = this.getClass().getResource("/").getPath();
path = path.replace("/", "//");
path = path.replace("//target//classes//", "");
path = path.replace("//WEB-INF//classes//", "");
Config globalConfig=new RSAAutoCertificateConfig.Builder()
.merchantId(apiConfig.getMerchantId())
.privateKeyFromPath(path+apiConfig.getPrivateKeyPath())
.merchantSerialNumber(apiConfig.getApiCode())
.apiV3Key(apiConfig.getApiKey())
.build();
NativePayService service = new NativePayService.Builder()
.config(globalConfig) // 复用全局配置
.build();
// request.setXxx(val)设置所需参数,具体参数可见Request定义
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
BigDecimal money = new BigDecimal(payDto.getAmount()).multiply(new BigDecimal("100"));
amount.setTotal(money.setScale(0, RoundingMode.FLOOR).intValueExact());
request.setAmount(amount);
request.setAppid(payDto.getApiConfig().getApiId());
request.setMchid(payDto.getApiConfig().getMerchantId());
request.setDescription(payDto.getDesc());
request.setNotifyUrl(payDto.getApiConfig().getApiRedirectUri()+"/"+payDto.getCompanyId());
request.setOutTradeNo(payDto.getOrderId());
request.setTimeExpire(getTime());
// 调用下单方法,得到应答
PrepayResponse response = service.prepay(request);
// 使用微信扫描 code_url 对应的二维码,即可体验Native支付
System.out.println("**-**-*--*-*-**--*-*-*-*-*-*-*"+response);
System.out.println("*-*-*-*-*-*-*-*-*-*-*-"+response.getCodeUrl()+"-*-**-*-*--**--*-*");
return response.getCodeUrl();
}
/**
* 关闭订单
* @param orderId 订单号
* @param mchId 商户号
*/
@Override
public void closeTrade(String orderId, String mchId,ApiConfig apiConfig) {
}
/**
* 退款申请
*/
@Override
public void refund() {
}
private String getTime(){
// 获取当前时间
ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
// 加上十分钟
ZonedDateTime thirtyMinutesLater = now.plusMinutes(10);
// 定义目标格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX");
// 格式化为字符串
String formattedTime = thirtyMinutesLater.format(formatter);
System.out.println("订单结束时间:"+formattedTime);
// 输出结果
return formattedTime;
}
@Override
public String getChannel() {
return "wechatpay";
}
}
... ...