提交修改
This commit is contained in:
parent
0b6b4a9b5c
commit
0b68fe0c9e
@ -22,6 +22,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class CommonAmountInfo {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CommonSceneInfo {
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("store_info")
|
||||
public StoreInfo storeInfo;
|
||||
}
|
||||
19
api-third/src/main/java/com/heyu/api/jsapi/CouponInfo.java
Normal file
19
api-third/src/main/java/com/heyu/api/jsapi/CouponInfo.java
Normal file
@ -0,0 +1,19 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class CouponInfo {
|
||||
@SerializedName("cost_price")
|
||||
public Long costPrice;
|
||||
|
||||
@SerializedName("invoice_id")
|
||||
public String invoiceId;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DirectAPIv3JsapiPrepayRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("support_fapiao")
|
||||
public Boolean supportFapiao;
|
||||
|
||||
@SerializedName("amount")
|
||||
public CommonAmountInfo amount;
|
||||
|
||||
@SerializedName("payer")
|
||||
public JsapiReqPayerInfo payer;
|
||||
|
||||
@SerializedName("detail")
|
||||
public CouponInfo detail;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public CommonSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public SettleInfo settleInfo;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DirectAPIv3JsapiPrepayResponse {
|
||||
@SerializedName("prepay_id")
|
||||
public String prepayId;
|
||||
}
|
||||
22
api-third/src/main/java/com/heyu/api/jsapi/GoodsDetail.java
Normal file
22
api-third/src/main/java/com/heyu/api/jsapi/GoodsDetail.java
Normal file
@ -0,0 +1,22 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
}
|
||||
145
api-third/src/main/java/com/heyu/api/jsapi/JsapiPrepay.java
Normal file
145
api-third/src/main/java/com/heyu/api/jsapi/JsapiPrepay.java
Normal file
@ -0,0 +1,145 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
// 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
||||
/**
|
||||
* JSAPI下单
|
||||
*/
|
||||
@Slf4j
|
||||
public class JsapiPrepay {
|
||||
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
|
||||
private static String METHOD = "POST";
|
||||
|
||||
private static String PATH = "/v3/pay/transactions/jsapi";
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public JsapiPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
JsapiPrepay client = new JsapiPrepay(
|
||||
"1731491745", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"793D48CCEB62C6B227E0A4F46AD90279B149A7BE", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/Users/quyixiao/Desktop/weixincert/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_0117314917452025110400382304001401", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/Users/quyixiao/Desktop/weixincert/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
DirectAPIv3JsapiPrepayRequest request = new DirectAPIv3JsapiPrepayRequest();
|
||||
|
||||
request.appid = "wx75fa59c097bd3dfd";
|
||||
request.mchid = "1731491745";
|
||||
request.description = "Image形象店-深圳腾大-QQ公仔";
|
||||
request.outTradeNo = "121775250";
|
||||
|
||||
request.timeExpire = WXPayUtility.generateExpireTime(); // 2025-11-05T21:02:16+08:00
|
||||
request.attach = "自定义数据说明";
|
||||
request.notifyUrl = "https://api.1024api.com/api-interface/app/weixin/payNotify";
|
||||
request.goodsTag = "WXG";
|
||||
request.supportFapiao = false;
|
||||
request.amount = new CommonAmountInfo();
|
||||
request.amount.total = 5L;
|
||||
request.amount.currency = "CNY";
|
||||
request.payer = new JsapiReqPayerInfo();
|
||||
|
||||
request.payer.openid = "o6t1512tT-JuBeT6rIu6RhFGf3BQ";
|
||||
|
||||
request.detail = new CouponInfo();
|
||||
request.detail.costPrice = 608800L;
|
||||
request.detail.invoiceId = "微信123";
|
||||
request.detail.goodsDetail = new ArrayList<>();
|
||||
{
|
||||
GoodsDetail goodsDetailItem = new GoodsDetail();
|
||||
goodsDetailItem.merchantGoodsId = "1246464644";
|
||||
goodsDetailItem.wechatpayGoodsId = "1001";
|
||||
goodsDetailItem.goodsName = "iPhoneX 256G";
|
||||
goodsDetailItem.quantity = 1L;
|
||||
goodsDetailItem.unitPrice = 528800L;
|
||||
request.detail.goodsDetail.add(goodsDetailItem);
|
||||
}
|
||||
;
|
||||
request.sceneInfo = new CommonSceneInfo();
|
||||
request.sceneInfo.payerClientIp = "14.23.150.211";
|
||||
request.sceneInfo.deviceId = "013467007045764";
|
||||
request.sceneInfo.storeInfo = new StoreInfo();
|
||||
request.sceneInfo.storeInfo.id = "0001";
|
||||
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
|
||||
request.sceneInfo.storeInfo.areaCode = "440305";
|
||||
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
|
||||
request.settleInfo = new SettleInfo();
|
||||
request.settleInfo.profitSharing = false;
|
||||
try {
|
||||
DirectAPIv3JsapiPrepayResponse response = client.rePay(request);
|
||||
System.out.println(JSON.toJSONString(response));
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (Exception e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectAPIv3JsapiPrepayResponse rePay(DirectAPIv3JsapiPrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
log.info("JsapiPrepay respBody:{}", respBody);
|
||||
if (respBody != null) {
|
||||
}
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class JsapiReqPayerInfo {
|
||||
@SerializedName("openid")
|
||||
public String openid;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class SettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
17
api-third/src/main/java/com/heyu/api/jsapi/StoreInfo.java
Normal file
17
api-third/src/main/java/com/heyu/api/jsapi/StoreInfo.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class StoreInfo {
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("area_code")
|
||||
public String areaCode;
|
||||
|
||||
@SerializedName("address")
|
||||
public String address;
|
||||
}
|
||||
894
api-third/src/main/java/com/heyu/api/jsapi/WXPayUtility.java
Normal file
894
api-third/src/main/java/com/heyu/api/jsapi/WXPayUtility.java
Normal file
@ -0,0 +1,894 @@
|
||||
package com.heyu.api.jsapi;
|
||||
|
||||
import com.google.gson.ExclusionStrategy;
|
||||
import com.google.gson.FieldAttributes;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSource;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.security.MessageDigest;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.security.Security;
|
||||
|
||||
public class WXPayUtility {
|
||||
private static final Gson gson = new GsonBuilder()
|
||||
.disableHtmlEscaping()
|
||||
.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.addDeserializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.deserialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.create();
|
||||
private static final char[] SYMBOLS =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
/**
|
||||
* 将 Object 转换为 JSON 字符串
|
||||
*/
|
||||
public static String toJson(Object object) {
|
||||
return gson.toJson(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串解析为特定类型的实例
|
||||
*/
|
||||
public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
|
||||
return gson.fromJson(json, classOfT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从公私钥文件路径中读取文件内容
|
||||
*
|
||||
* @param keyPath 文件路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
private static String readKeyStringFromPath(String keyPath) {
|
||||
try {
|
||||
return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
|
||||
*
|
||||
* @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
|
||||
* @return PrivateKey 对象
|
||||
*/
|
||||
public static PrivateKey loadPrivateKeyFromString(String keyString) {
|
||||
try {
|
||||
keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return KeyFactory.getInstance("RSA").generatePrivate(
|
||||
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PKCS#8 格式的私钥文件中加载私钥
|
||||
*
|
||||
* @param keyPath 私钥文件路径
|
||||
* @return PrivateKey 对象
|
||||
*/
|
||||
public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
|
||||
return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
|
||||
*
|
||||
* @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
|
||||
* @return PublicKey 对象
|
||||
*/
|
||||
public static PublicKey loadPublicKeyFromString(String keyString) {
|
||||
try {
|
||||
keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return KeyFactory.getInstance("RSA").generatePublic(
|
||||
new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PKCS#8 格式的公钥文件中加载公钥
|
||||
*
|
||||
* @param keyPath 公钥文件路径
|
||||
* @return PublicKey 对象
|
||||
*/
|
||||
public static PublicKey loadPublicKeyFromPath(String keyPath) {
|
||||
return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
|
||||
*/
|
||||
public static String createNonce(int length) {
|
||||
char[] buf = new char[length];
|
||||
for (int i = 0; i < length; ++i) {
|
||||
buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
|
||||
}
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
|
||||
*
|
||||
* @param publicKey 加密用公钥对象
|
||||
* @param plaintext 待加密明文
|
||||
* @return 加密后密文
|
||||
*/
|
||||
public static String encrypt(PublicKey publicKey, String plaintext) {
|
||||
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException("Plaintext is too long", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce,
|
||||
byte[] ciphertext) {
|
||||
final String transformation = "AES/GCM/NoPadding";
|
||||
final String algorithm = "AES";
|
||||
final int tagLengthBit = 128;
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
new SecretKeySpec(key, algorithm),
|
||||
new GCMParameterSpec(tagLengthBit, nonce));
|
||||
if (associatedData != null) {
|
||||
cipher.updateAAD(associatedData);
|
||||
}
|
||||
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
|
||||
} catch (InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException
|
||||
| NoSuchAlgorithmException
|
||||
| NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed",
|
||||
transformation), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用私钥按照指定算法进行签名
|
||||
*
|
||||
* @param message 待签名串
|
||||
* @param algorithm 签名算法,如 SHA256withRSA
|
||||
* @param privateKey 签名用私钥对象
|
||||
* @return 签名结果
|
||||
*/
|
||||
public static String sign(String message, String algorithm, PrivateKey privateKey) {
|
||||
byte[] sign;
|
||||
try {
|
||||
Signature signature = Signature.getInstance(algorithm);
|
||||
signature.initSign(privateKey);
|
||||
signature.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
sign = signature.sign();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException("An error occurred during the sign process.", e);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(sign);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥按照特定算法验证签名
|
||||
*
|
||||
* @param message 待签名串
|
||||
* @param signature 待验证的签名内容
|
||||
* @param algorithm 签名算法,如:SHA256withRSA
|
||||
* @param publicKey 验签用公钥对象
|
||||
* @return 签名验证是否通过
|
||||
*/
|
||||
public static boolean verify(String message, String signature, String algorithm,
|
||||
PublicKey publicKey) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance(algorithm);
|
||||
sign.initVerify(publicKey);
|
||||
sign.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
return sign.verify(Base64.getDecoder().decode(signature));
|
||||
} catch (SignatureException e) {
|
||||
return false;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("verify uses an illegal publickey.", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信支付APIv3请求签名规则构造 Authorization 签名
|
||||
*
|
||||
* @param mchid 商户号
|
||||
* @param certificateSerialNo 商户API证书序列号
|
||||
* @param privateKey 商户API证书私钥
|
||||
* @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
|
||||
* @param uri 请求接口的URL
|
||||
* @param body 请求接口的Body
|
||||
* @return 构造好的微信支付APIv3 Authorization 头
|
||||
*/
|
||||
public static String buildAuthorization(String mchid, String certificateSerialNo,
|
||||
PrivateKey privateKey,
|
||||
String method, String uri, String body) {
|
||||
String nonce = createNonce(32);
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
|
||||
String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
|
||||
body == null ? "" : body);
|
||||
|
||||
String signature = sign(message, "SHA256withRSA", privateKey);
|
||||
|
||||
return String.format(
|
||||
"WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
|
||||
"timestamp=\"%d\",serial_no=\"%s\"",
|
||||
mchid, nonce, signature, timestamp, certificateSerialNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算输入流的哈希值
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @param algorithm 哈希算法名称,如 "SHA-256", "SHA-1"
|
||||
* @return 哈希值的十六进制字符串
|
||||
*/
|
||||
private static String calculateHash(InputStream inputStream, String algorithm) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance(algorithm);
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] hashBytes = digest.digest();
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(algorithm + " algorithm not available", e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error reading from input stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算输入流的 SHA256 哈希值
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @return SHA256 哈希值的十六进制字符串
|
||||
*/
|
||||
public static String sha256(InputStream inputStream) {
|
||||
return calculateHash(inputStream, "SHA-256");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算输入流的 SHA1 哈希值
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @return SHA1 哈希值的十六进制字符串
|
||||
*/
|
||||
public static String sha1(InputStream inputStream) {
|
||||
return calculateHash(inputStream, "SHA-1");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算输入流的 SM3 哈希值
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @return SM3 哈希值的十六进制字符串
|
||||
*/
|
||||
public static String sm3(InputStream inputStream) {
|
||||
// 确保Bouncy Castle Provider已注册
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
try {
|
||||
SM3Digest digest = new SM3Digest();
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] hashBytes = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(hashBytes, 0);
|
||||
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error reading from input stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对参数进行 URL 编码
|
||||
*
|
||||
* @param content 参数内容
|
||||
* @return 编码后的内容
|
||||
*/
|
||||
public static String urlEncode(String content) {
|
||||
try {
|
||||
return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对参数Map进行 URL 编码,生成 QueryString
|
||||
*
|
||||
* @param params Query参数Map
|
||||
* @return QueryString
|
||||
*/
|
||||
public static String urlEncode(Map<String, Object> params) {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Entry<String, Object> entry : params.entrySet()) {
|
||||
if (entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof List) {
|
||||
List<?> list = (List<?>) entry.getValue();
|
||||
for (Object temp : list) {
|
||||
appendParam(result, key, temp);
|
||||
}
|
||||
} else {
|
||||
appendParam(result, key, value);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将键值对 放入返回结果
|
||||
*
|
||||
* @param result 返回的query string
|
||||
* @param key 属性
|
||||
* @param value 属性值
|
||||
*/
|
||||
private static void appendParam(StringBuilder result, String key, Object value) {
|
||||
if (result.length() > 0) {
|
||||
result.append("&");
|
||||
}
|
||||
|
||||
String valueString;
|
||||
// 如果是基本类型、字符串或枚举,直接转换;如果是对象,序列化为JSON
|
||||
if (value instanceof String || value instanceof Number ||
|
||||
value instanceof Boolean || value instanceof Enum) {
|
||||
valueString = value.toString();
|
||||
} else {
|
||||
valueString = toJson(value);
|
||||
}
|
||||
|
||||
result.append(key)
|
||||
.append("=")
|
||||
.append(urlEncode(valueString));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 生成订单过期时间
|
||||
*/
|
||||
public static String generateExpireTime() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.MINUTE, 30); // 30分钟后过期
|
||||
return sdf.format(calendar.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从应答中提取 Body
|
||||
*
|
||||
* @param response HTTP 请求应答对象
|
||||
* @return 应答中的Body内容,Body为空时返回空字符串
|
||||
*/
|
||||
public static String extractBody(Response response) {
|
||||
if (response.body() == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedSource source = response.body().source();
|
||||
return source.readUtf8();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(String.format("An error occurred during reading response body. " +
|
||||
"Status: %d", response.code()), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
|
||||
*
|
||||
* @param wechatpayPublicKeyId 微信支付公钥ID
|
||||
* @param wechatpayPublicKey 微信支付公钥对象
|
||||
* @param headers 微信支付应答 Header 列表
|
||||
* @param body 微信支付应答 Body
|
||||
*/
|
||||
public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
|
||||
Headers headers,
|
||||
String body) {
|
||||
String timestamp = headers.get("Wechatpay-Timestamp");
|
||||
String requestId = headers.get("Request-ID");
|
||||
try {
|
||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
|
||||
// 拒绝过期请求
|
||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]",
|
||||
timestamp, requestId));
|
||||
}
|
||||
} catch (DateTimeException | NumberFormatException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]",
|
||||
timestamp, requestId));
|
||||
}
|
||||
String serialNumber = headers.get("Wechatpay-Serial");
|
||||
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " +
|
||||
"%s", wechatpayPublicKeyId, serialNumber));
|
||||
}
|
||||
|
||||
String signature = headers.get("Wechatpay-Signature");
|
||||
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
|
||||
body == null ? "" : body);
|
||||
|
||||
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
|
||||
if (!success) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed,the WechatPay signature is incorrect.%n"
|
||||
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
|
||||
headers.get("Request-ID"), headers, body));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信支付APIv3通知验签规则对通知签名进行验证,验证不通过时抛出异常
|
||||
*
|
||||
* @param wechatpayPublicKeyId 微信支付公钥ID
|
||||
* @param wechatpayPublicKey 微信支付公钥对象
|
||||
* @param headers 微信支付通知 Header 列表
|
||||
* @param body 微信支付通知 Body
|
||||
*/
|
||||
public static void validateNotification(String wechatpayPublicKeyId,
|
||||
PublicKey wechatpayPublicKey, Headers headers,
|
||||
String body) {
|
||||
String timestamp = headers.get("Wechatpay-Timestamp");
|
||||
try {
|
||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
|
||||
// 拒绝过期请求
|
||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, timestamp[%s] is expired", timestamp));
|
||||
}
|
||||
} catch (DateTimeException | NumberFormatException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, timestamp[%s] is invalid", timestamp));
|
||||
}
|
||||
String serialNumber = headers.get("Wechatpay-Serial");
|
||||
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " +
|
||||
"Remote: %s",
|
||||
wechatpayPublicKeyId,
|
||||
serialNumber));
|
||||
}
|
||||
|
||||
String signature = headers.get("Wechatpay-Signature");
|
||||
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
|
||||
body == null ? "" : body);
|
||||
|
||||
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
|
||||
if (!success) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, WechatPay signature is incorrect.\n"
|
||||
+ "responseHeader[%s]\tresponseBody[%.1024s]",
|
||||
headers, body));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常
|
||||
*
|
||||
* @param apiv3Key 商户的 APIv3 Key
|
||||
* @param wechatpayPublicKeyId 微信支付公钥ID
|
||||
* @param wechatpayPublicKey 微信支付公钥对象
|
||||
* @param headers 微信支付请求 Header 列表
|
||||
* @param body 微信支付请求 Body
|
||||
* @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问
|
||||
*/
|
||||
public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId,
|
||||
PublicKey wechatpayPublicKey, Headers headers,
|
||||
String body) {
|
||||
validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body);
|
||||
Notification notification = gson.fromJson(body, Notification.class);
|
||||
notification.decrypt(apiv3Key);
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
|
||||
*/
|
||||
public static class ApiException extends RuntimeException {
|
||||
private static final long serialVersionUID = 2261086748874802175L;
|
||||
|
||||
private final int statusCode;
|
||||
private final String body;
|
||||
private final Headers headers;
|
||||
private final String errorCode;
|
||||
private final String errorMessage;
|
||||
|
||||
public ApiException(int statusCode, String body, Headers headers) {
|
||||
super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode,
|
||||
body, headers));
|
||||
this.statusCode = statusCode;
|
||||
this.body = body;
|
||||
this.headers = headers;
|
||||
|
||||
if (body != null && !body.isEmpty()) {
|
||||
JsonElement code;
|
||||
JsonElement message;
|
||||
|
||||
try {
|
||||
JsonObject jsonObject = gson.fromJson(body, JsonObject.class);
|
||||
code = jsonObject.get("code");
|
||||
message = jsonObject.get("message");
|
||||
} catch (JsonSyntaxException ignored) {
|
||||
code = null;
|
||||
message = null;
|
||||
}
|
||||
this.errorCode = code == null ? null : code.getAsString();
|
||||
this.errorMessage = message == null ? null : message.getAsString();
|
||||
} else {
|
||||
this.errorCode = null;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 应答状态码
|
||||
*/
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 应答包体内容
|
||||
*/
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 应答 Header
|
||||
*/
|
||||
public Headers getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 错误码 (错误应答中的 code 字段)
|
||||
*/
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 错误消息 (错误应答中的 message 字段)
|
||||
*/
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Notification {
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
@SerializedName("create_time")
|
||||
private String createTime;
|
||||
@SerializedName("event_type")
|
||||
private String eventType;
|
||||
@SerializedName("resource_type")
|
||||
private String resourceType;
|
||||
@SerializedName("summary")
|
||||
private String summary;
|
||||
@SerializedName("resource")
|
||||
private Resource resource;
|
||||
private String plaintext;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public String getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return resourceType;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public Resource getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解密后的业务数据(JSON字符串,需要自行解析)
|
||||
*/
|
||||
public String getPlaintext() {
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException("Missing required field `resource` in notification");
|
||||
}
|
||||
resource.validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。
|
||||
* 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public
|
||||
*
|
||||
* @param apiv3Key 商户APIv3 Key
|
||||
*/
|
||||
private void decrypt(String apiv3Key) {
|
||||
validate();
|
||||
|
||||
plaintext = aesAeadDecrypt(
|
||||
apiv3Key.getBytes(StandardCharsets.UTF_8),
|
||||
resource.associatedData.getBytes(StandardCharsets.UTF_8),
|
||||
resource.nonce.getBytes(StandardCharsets.UTF_8),
|
||||
Base64.getDecoder().decode(resource.ciphertext)
|
||||
);
|
||||
}
|
||||
|
||||
public static class Resource {
|
||||
@SerializedName("algorithm")
|
||||
private String algorithm;
|
||||
|
||||
@SerializedName("ciphertext")
|
||||
private String ciphertext;
|
||||
|
||||
@SerializedName("associated_data")
|
||||
private String associatedData;
|
||||
|
||||
@SerializedName("nonce")
|
||||
private String nonce;
|
||||
|
||||
@SerializedName("original_type")
|
||||
private String originalType;
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public String getCiphertext() {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
public String getAssociatedData() {
|
||||
return associatedData;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public String getOriginalType() {
|
||||
return originalType;
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (algorithm == null || algorithm.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `algorithm` in Notification" +
|
||||
".Resource");
|
||||
}
|
||||
if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) {
|
||||
throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " +
|
||||
"Notification.Resource", algorithm));
|
||||
}
|
||||
|
||||
if (ciphertext == null || ciphertext.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" +
|
||||
".Resource");
|
||||
}
|
||||
|
||||
if (associatedData == null || associatedData.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `associatedData` in " +
|
||||
"Notification.Resource");
|
||||
}
|
||||
|
||||
if (nonce == null || nonce.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `nonce` in Notification" +
|
||||
".Resource");
|
||||
}
|
||||
|
||||
if (originalType == null || originalType.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `originalType` in " +
|
||||
"Notification.Resource");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名获取对应的Content-Type
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return Content-Type字符串
|
||||
*/
|
||||
public static String getContentTypeByFileName(String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
String extension = "";
|
||||
int lastDotIndex = fileName.lastIndexOf('.');
|
||||
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
|
||||
extension = fileName.substring(lastDotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
// 常见文件类型映射
|
||||
Map<String, String> contentTypeMap = new HashMap<>();
|
||||
// 图片类型
|
||||
contentTypeMap.put("png", "image/png");
|
||||
contentTypeMap.put("jpg", "image/jpeg");
|
||||
contentTypeMap.put("jpeg", "image/jpeg");
|
||||
contentTypeMap.put("gif", "image/gif");
|
||||
contentTypeMap.put("bmp", "image/bmp");
|
||||
contentTypeMap.put("webp", "image/webp");
|
||||
contentTypeMap.put("svg", "image/svg+xml");
|
||||
contentTypeMap.put("ico", "image/x-icon");
|
||||
|
||||
// 文档类型
|
||||
contentTypeMap.put("pdf", "application/pdf");
|
||||
contentTypeMap.put("doc", "application/msword");
|
||||
contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
contentTypeMap.put("xls", "application/vnd.ms-excel");
|
||||
contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
contentTypeMap.put("ppt", "application/vnd.ms-powerpoint");
|
||||
contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
|
||||
|
||||
// 文本类型
|
||||
contentTypeMap.put("txt", "text/plain");
|
||||
contentTypeMap.put("html", "text/html");
|
||||
contentTypeMap.put("css", "text/css");
|
||||
contentTypeMap.put("js", "application/javascript");
|
||||
contentTypeMap.put("json", "application/json");
|
||||
contentTypeMap.put("xml", "application/xml");
|
||||
contentTypeMap.put("csv", "text/csv");
|
||||
|
||||
// 音视频类型
|
||||
contentTypeMap.put("mp3", "audio/mpeg");
|
||||
contentTypeMap.put("wav", "audio/wav");
|
||||
contentTypeMap.put("mp4", "video/mp4");
|
||||
contentTypeMap.put("avi", "video/x-msvideo");
|
||||
contentTypeMap.put("mov", "video/quicktime");
|
||||
|
||||
// 压缩文件类型
|
||||
contentTypeMap.put("zip", "application/zip");
|
||||
contentTypeMap.put("rar", "application/x-rar-compressed");
|
||||
contentTypeMap.put("7z", "application/x-7z-compressed");
|
||||
|
||||
|
||||
return contentTypeMap.getOrDefault(extension, "application/octet-stream");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx/QqdSnkS8wd7xFH0LHV
|
||||
pZTFVI+eWL383eAP7RaxHfRwXNDfZYrdi/Bt8Enko+1Wkr2FGY2kS4Hol5NCvuZ7
|
||||
UY28/L4r95dBEgXtflkrxeVbiNbWHtiIdNXZSgkrpUkZEFoh7ks5Ou3pAlpG+94/
|
||||
Nnc//D/yckM7AuD85G1foZMSO5niyVeFAed6z7CeBMAhRVdnOIDUqsI/NaHruT4f
|
||||
vWNzPnn5SQKoKum00vRHckxftUqMkZ1D/YxHiAycEc9H0hCgLW7ZM6UV88Loa6SP
|
||||
vhM0fmXIuk6p277ldlxFl6Bcxd5jOcCTjdpWaB3CeSXnSIBl6KsZNcV3vPC3xey1
|
||||
WwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
Loading…
x
Reference in New Issue
Block a user