From 0c1b1ea48e232ef80aec4785a307f2a9c9fba01a Mon Sep 17 00:00:00 2001 From: DongZeLiang <2746733890@qq.com> Date: Mon, 21 Aug 2023 16:39:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=AE=E7=AD=BE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 8 +- .../wechat/clients/WeChatNativeClient.java | 33 ++ .../interceptor/WeChatAuthInterceptor.java | 74 +++ .../pay/wechat/config/WechatPayConfig.java | 56 ++ .../pay/wechat/config/WechatPayConfigure.java | 33 ++ .../wechat/constant/WechatPayConstant.java | 20 + .../pay/wechat/domain/WeChatPayBaseReq.java | 27 + .../domain/model/WeChatPayAmountModel.java | 40 ++ .../domain/req/WeChatPayReqWeChatPay.java | 45 ++ .../pay/wechat/service/WeChatSignService.java | 99 ++++ .../com/muyu/pay/wechat/utils/SignUtils.java | 55 -- src/main/java/com/muyu/utils/IdUtils.java | 72 +++ src/main/java/com/muyu/utils/UUID.java | 485 ++++++++++++++++++ .../com/muyu/wechat/pay/NativeClientTest.java | 41 ++ 14 files changed, 1029 insertions(+), 59 deletions(-) create mode 100644 src/main/java/com/muyu/pay/wechat/clients/WeChatNativeClient.java create mode 100644 src/main/java/com/muyu/pay/wechat/clients/interceptor/WeChatAuthInterceptor.java create mode 100644 src/main/java/com/muyu/pay/wechat/config/WechatPayConfig.java create mode 100644 src/main/java/com/muyu/pay/wechat/config/WechatPayConfigure.java create mode 100644 src/main/java/com/muyu/pay/wechat/constant/WechatPayConstant.java create mode 100644 src/main/java/com/muyu/pay/wechat/domain/WeChatPayBaseReq.java create mode 100644 src/main/java/com/muyu/pay/wechat/domain/model/WeChatPayAmountModel.java create mode 100644 src/main/java/com/muyu/pay/wechat/domain/req/WeChatPayReqWeChatPay.java create mode 100644 src/main/java/com/muyu/pay/wechat/service/WeChatSignService.java delete mode 100644 src/main/java/com/muyu/pay/wechat/utils/SignUtils.java create mode 100644 src/main/java/com/muyu/utils/IdUtils.java create mode 100644 src/main/java/com/muyu/utils/UUID.java create mode 100644 src/test/java/com/muyu/wechat/pay/NativeClientTest.java diff --git a/pom.xml b/pom.xml index 31f05d2..5212e8e 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 8 UTF-8 1.5.32 - 2.0.38 + 1.2.73 @@ -40,9 +40,9 @@ - com.alibaba.fastjson2 - fastjson2 - ${fastjson2.version} + com.alibaba + fastjson + ${fastjson.version} diff --git a/src/main/java/com/muyu/pay/wechat/clients/WeChatNativeClient.java b/src/main/java/com/muyu/pay/wechat/clients/WeChatNativeClient.java new file mode 100644 index 0000000..2721c53 --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/clients/WeChatNativeClient.java @@ -0,0 +1,33 @@ +package com.muyu.pay.wechat.clients; + +import com.dtflys.forest.annotation.BaseRequest; +import com.dtflys.forest.annotation.JSONBody; +import com.dtflys.forest.annotation.Post; +import com.muyu.pay.wechat.clients.interceptor.WeChatAuthInterceptor; +import com.muyu.pay.wechat.domain.req.WeChatPayReqWeChatPay; + +/** + * @author DongZl + * @description: 微信Native客户端 + * @Date 2023-8-21 下午 02:42 + */ +@BaseRequest( + baseURL = "https://api.mch.weixin.qq.com/v3/pay/transactions", + contentType = "application/json", + headers = { + "Accept: application/json" + }, + interceptor = WeChatAuthInterceptor.class +) +public interface WeChatNativeClient { + + /** + * Native 支付下单 + * @param jsonObject 请求参数 + * @return 响应结果 + */ + @Post( + url = "/native" + ) + public String order(@JSONBody WeChatPayReqWeChatPay weChatPayReq); +} diff --git a/src/main/java/com/muyu/pay/wechat/clients/interceptor/WeChatAuthInterceptor.java b/src/main/java/com/muyu/pay/wechat/clients/interceptor/WeChatAuthInterceptor.java new file mode 100644 index 0000000..cee02f7 --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/clients/interceptor/WeChatAuthInterceptor.java @@ -0,0 +1,74 @@ +package com.muyu.pay.wechat.clients.interceptor; + +import com.dtflys.forest.http.ForestBody; +import com.dtflys.forest.http.ForestRequest; +import com.dtflys.forest.interceptor.Interceptor; +import com.dtflys.forest.reflection.ForestMethod; +import com.muyu.pay.wechat.config.WechatPayConfig; +import com.muyu.pay.wechat.domain.WeChatPayBaseReq; +import com.muyu.pay.wechat.service.WeChatSignService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; + +/** + * @author DongZl + * @description: 微信支付请求拦截器 + * @Date 2023-8-21 下午 03:06 + */ +@Log4j2 +@Component +public class WeChatAuthInterceptor implements Interceptor { + + @Autowired + private WechatPayConfig wechatPayConfig; + + @Autowired + private WeChatSignService weChatSignService; + + public WeChatAuthInterceptor () { + log.info("微信支付请求拦截器 - 初始化成功"); + } + + /** + * 该方法在被调用时,并在beforeExecute前被调用 + * @Param request Forest请求对象 + * @Param args 方法被调用时传入的参数数组 + */ + @Override + public void onInvokeMethod(ForestRequest req, ForestMethod method, Object[] args) { + log.debug("进行用户微信公众号和商户号填写"); + for (Object arg : args) { + if (arg instanceof WeChatPayBaseReq){ + WeChatPayBaseReq weChatPayBaseReq = (WeChatPayBaseReq) arg; + weChatPayBaseReq.setAppid(wechatPayConfig.getAppid()); + weChatPayBaseReq.setMchid(wechatPayConfig.getMerchantId()); + } + } + } + + /** + * 该方法在请求发送之前被调用, 若返回false则不会继续发送请求 + * @Param request Forest请求对象 + */ + @Override + public boolean beforeExecute(ForestRequest req) { + log.debug("微信请求前拦截 - 添加Authorization授权请求头"); + ForestBody forestBody = req.getBody(); + String body = forestBody.encodeToString(); + ForestMethod forestMethod = req.getMethod(); + String methodName = req.getType().getName(); + String url = req.getUrl(); + String authorization = null; + if (HttpMethod.GET.matches(methodName)){ + authorization = weChatSignService.tokenByGet(url); + }else if (HttpMethod.POST.matches(methodName)){ + authorization = weChatSignService.tokenByPost(url, body); + } + // 添加Header + req.addHeader("Authorization", authorization); + // 继续执行请求返回true + return true; + } +} diff --git a/src/main/java/com/muyu/pay/wechat/config/WechatPayConfig.java b/src/main/java/com/muyu/pay/wechat/config/WechatPayConfig.java new file mode 100644 index 0000000..c7b312c --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/config/WechatPayConfig.java @@ -0,0 +1,56 @@ +package com.muyu.pay.wechat.config; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.security.PrivateKey; + +/** + * @author DongZl + * @description: 微信支付配置类 + * @Date 2023-8-21 下午 01:54 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatPayConfig { + + /** + * 微信公众号Id + */ + private String appid; + + /** + * 商户ID + */ + private String merchantId ; + + /** + * 支付Key + */ + private String payKey; + + /** + * 加密算法 默认 SHA256withRSA + */ + private String algorithmType; + + + /** + * 加密模式 + */ + private String schema; + + /** + * 证书编号 + */ + private String serialNo; + + /** + * 私有证书 + */ + private PrivateKey privateKey; +} diff --git a/src/main/java/com/muyu/pay/wechat/config/WechatPayConfigure.java b/src/main/java/com/muyu/pay/wechat/config/WechatPayConfigure.java new file mode 100644 index 0000000..a692d75 --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/config/WechatPayConfigure.java @@ -0,0 +1,33 @@ +package com.muyu.pay.wechat.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import static com.muyu.pay.wechat.constant.WechatPayConstant.*; + +/** + * @author DongZl + * @description: 微信支付配置 + * @Date 2023-8-21 下午 02:34 + */ +@Configuration +public class WechatPayConfigure { + + /** + * 初始化微信支付配置 + * @return 微信支付配置 + */ + @Bean + public WechatPayConfig initWechatPayConfig(){ + return WechatPayConfig.builder() + .appid("wx1a60b1d281c88896") + .merchantId("1645775803") + .payKey("wentaoyun1234567wentaoyun1234567") + .algorithmType(ALGORITHM_TYPE) + .schema(SCHEMA) + // 自己填我不知道 + .serialNo(null) + // 自己填我不知道 + .privateKey(null) + .build(); + } +} diff --git a/src/main/java/com/muyu/pay/wechat/constant/WechatPayConstant.java b/src/main/java/com/muyu/pay/wechat/constant/WechatPayConstant.java new file mode 100644 index 0000000..e06cc8e --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/constant/WechatPayConstant.java @@ -0,0 +1,20 @@ +package com.muyu.pay.wechat.constant; + +/** + * @author DongZl + * @description: 微信常量 + * @Date 2023-8-21 下午 02:36 + */ +public class WechatPayConstant { + + /** + * 加密算法 默认 SHA256withRSA + */ + public final static String ALGORITHM_TYPE = "SHA256withRSA"; + + + /** + * 加密模式 + */ + public final static String SCHEMA = "WECHATPAY2-SHA256-RSA2048"; +} diff --git a/src/main/java/com/muyu/pay/wechat/domain/WeChatPayBaseReq.java b/src/main/java/com/muyu/pay/wechat/domain/WeChatPayBaseReq.java new file mode 100644 index 0000000..7ba535e --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/domain/WeChatPayBaseReq.java @@ -0,0 +1,27 @@ +package com.muyu.pay.wechat.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author DongZl + * @description: 请求基础对象 + * @Date 2023-8-21 下午 03:29 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class WeChatPayBaseReq { + /** + * 公众号ID + */ + private String appid; + + /** + * 商户ID + */ + private String mchid; +} diff --git a/src/main/java/com/muyu/pay/wechat/domain/model/WeChatPayAmountModel.java b/src/main/java/com/muyu/pay/wechat/domain/model/WeChatPayAmountModel.java new file mode 100644 index 0000000..b432102 --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/domain/model/WeChatPayAmountModel.java @@ -0,0 +1,40 @@ +package com.muyu.pay.wechat.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author DongZl + * @description: 微信支付金额 + * @Date 2023-8-21 下午 04:04 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WeChatPayAmountModel { + + /** + * 总金额 单位 分 + */ + private int total; + + /** + * 货币类型 默认 CNY(人民币) + */ + private String currency; + + /** + * 构建 支付金额 + * @param total 支付金额 单位 分 + * @return 支付总额对象 + */ + public static WeChatPayAmountModel totalBuild(int total){ + return WeChatPayAmountModel.builder() + .total(total) + .currency("CNY") + .build(); + } +} diff --git a/src/main/java/com/muyu/pay/wechat/domain/req/WeChatPayReqWeChatPay.java b/src/main/java/com/muyu/pay/wechat/domain/req/WeChatPayReqWeChatPay.java new file mode 100644 index 0000000..5e86faa --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/domain/req/WeChatPayReqWeChatPay.java @@ -0,0 +1,45 @@ +package com.muyu.pay.wechat.domain.req; + +import com.alibaba.fastjson.annotation.JSONField; +import com.muyu.pay.wechat.domain.WeChatPayBaseReq; +import com.muyu.pay.wechat.domain.model.WeChatPayAmountModel; +import lombok.*; +import lombok.experimental.SuperBuilder; + + +/** + * @author DongZl + * @description: 微信支付模型 + * @Date 2023-8-21 下午 03:17 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WeChatPayReqWeChatPay extends WeChatPayBaseReq { + + /** + * 商品描述 + */ + private String description; + + /** + * 商户订单号 + */ + @JSONField(name = "out_trade_no") + private String outTradeNo; + + /** + * 通知地址 + */ + @JSONField(name = "notify_url") + private String notifyUrl; + + + /** + * 订单金额 + */ + @JSONField(name = "amount") + private WeChatPayAmountModel payAmountModel; +} diff --git a/src/main/java/com/muyu/pay/wechat/service/WeChatSignService.java b/src/main/java/com/muyu/pay/wechat/service/WeChatSignService.java new file mode 100644 index 0000000..cf7d8f1 --- /dev/null +++ b/src/main/java/com/muyu/pay/wechat/service/WeChatSignService.java @@ -0,0 +1,99 @@ +package com.muyu.pay.wechat.service; + +import com.muyu.pay.wechat.config.WechatPayConfig; +import com.muyu.utils.IdUtils; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import okhttp3.HttpUrl; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Base64; + +/** + * @author DongZl + * @description: 签名工具类 + * @Date 2023-8-21 上午 11:23 + */ +@Log4j2 +@Service +@AllArgsConstructor +public class WeChatSignService { + /** + * 微信配置 + */ + private final WechatPayConfig wechatPayConfig; + + + public String tokenByPost(String url, String bodyJson){ + return wechatPayConfig.getSchema() + " " + getToken("POST", HttpUrl.parse(url), bodyJson); + } + + public String tokenByGet(String url){ + return wechatPayConfig.getSchema() + " " + getToken("GET", HttpUrl.parse(url), ""); + } + + /** + * 获取token + * @param method 请求方式 + * @param url url + * @param body 请求参数 + * @return 签名 token + */ + public String getToken(String method, HttpUrl url, String body) { + String nonceStr = IdUtils.simpleUUID().toUpperCase(); + long timestamp = System.currentTimeMillis() / 1000; + String message = buildMessage(method, url, timestamp, nonceStr, body); + String signature = sign(message.getBytes(StandardCharsets.UTF_8)); + return "mchid=\"" + wechatPayConfig.getPayKey() + "\"," + + "nonce_str=\"" + nonceStr + "\"," + + "timestamp=\"" + timestamp + "\"," + + "serial_no=\"" + wechatPayConfig.getSerialNo() + "\"," + + "signature=\"" + signature + "\""; + } + + /** + * 进行算法签名 + * @param message 信息 + * @return 签名信息 + */ + private String sign(byte[] message) { + Signature sign = null; + try { + sign = Signature.getInstance(wechatPayConfig.getAlgorithmType()); + sign.initSign(wechatPayConfig.getPrivateKey()); + sign.update(message); + return Base64.getEncoder().encodeToString(sign.sign()); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + log.info("微信支付获取签名失败:{}", e.getMessage(), e); + throw new RuntimeException(e.getMessage()); + } + } + + /** + * 构建签名明细 + * @param method 请求方式 + * @param url 请求rul + * @param timestamp 请求时间戳 + * @param nonceStr 随机字符串 + * @param body 请求体 + * @return 请求明细 + */ + private String buildMessage(String method, HttpUrl url, long timestamp, + String nonceStr, String body) { + String canonicalUrl = url.encodedPath(); + if (url.encodedQuery() != null) { + canonicalUrl += "?" + url.encodedQuery(); + } + + return method + "\n" + + canonicalUrl + "\n" + + timestamp + "\n" + + nonceStr + "\n" + + body + "\n"; + } +} diff --git a/src/main/java/com/muyu/pay/wechat/utils/SignUtils.java b/src/main/java/com/muyu/pay/wechat/utils/SignUtils.java deleted file mode 100644 index 4de2479..0000000 --- a/src/main/java/com/muyu/pay/wechat/utils/SignUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.muyu.pay.wechat.utils; - -import okhttp3.HttpUrl; - -import java.security.Signature; -import java.util.Base64; - -/** - * @author DongZl - * @description: 签名工具类 - * @Date 2023-8-21 上午 11:23 - */ -public class SignUtils { - - // Authorization: -// GET - getToken("GET", httpurl, "") -// POST - getToken("POST", httpurl, json) - String schema = "WECHATPAY2-SHA256-RSA2048"; - HttpUrl httpurl = HttpUrl.parse(url); - - String getToken(String method, HttpUrl url, String body) { - String nonceStr = "your nonce string"; - long timestamp = System.currentTimeMillis() / 1000; - String message = buildMessage(method, url, timestamp, nonceStr, body); - String signature = sign(message.getBytes("utf-8")); - - return "mchid=\"" + yourMerchantId + "\"," - + "nonce_str=\"" + nonceStr + "\"," - + "timestamp=\"" + timestamp + "\"," - + "serial_no=\"" + yourCertificateSerialNo + "\"," - + "signature=\"" + signature + "\""; - } - - String sign(byte[] message) { - Signature sign = Signature.getInstance("SHA256withRSA"); - sign.initSign(yourPrivateKey); - sign.update(message); - - return Base64.getEncoder().encodeToString(sign.sign()); - } - - - String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { - String canonicalUrl = url.encodedPath(); - if (url.encodedQuery() != null) { - canonicalUrl += "?" + url.encodedQuery(); - } - - return method + "\n" - + canonicalUrl + "\n" - + timestamp + "\n" - + nonceStr + "\n" - + body + "\n"; - } -} diff --git a/src/main/java/com/muyu/utils/IdUtils.java b/src/main/java/com/muyu/utils/IdUtils.java new file mode 100644 index 0000000..b3ecaec --- /dev/null +++ b/src/main/java/com/muyu/utils/IdUtils.java @@ -0,0 +1,72 @@ +package com.muyu.utils; + +import java.util.Random; + +/** + * ID生成器工具类 + * + * @author leiyonghui + */ +public class IdUtils +{ + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } + + + /** + * + * @param tag + * @return + */ + public static String getNewNumber(String tag) + { + + StringBuffer sb = new StringBuffer(""); + Random random = new Random(); + for (int i=0;i<6;i++) + { + sb.append(random.nextInt(10)); + } + + String number = tag+System.currentTimeMillis()+sb.toString(); + + return number; + } +} diff --git a/src/main/java/com/muyu/utils/UUID.java b/src/main/java/com/muyu/utils/UUID.java new file mode 100644 index 0000000..fef2e11 --- /dev/null +++ b/src/main/java/com/muyu/utils/UUID.java @@ -0,0 +1,485 @@ +package com.muyu.utils; + +import com.sun.xml.internal.ws.util.UtilException; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author leiyonghui + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的本地线程伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (false == isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (false == isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (false == isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (false == isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/src/test/java/com/muyu/wechat/pay/NativeClientTest.java b/src/test/java/com/muyu/wechat/pay/NativeClientTest.java new file mode 100644 index 0000000..a1475e6 --- /dev/null +++ b/src/test/java/com/muyu/wechat/pay/NativeClientTest.java @@ -0,0 +1,41 @@ +package com.muyu.wechat.pay; + +import com.muyu.WechatPayApplication; +import com.muyu.pay.wechat.clients.WeChatNativeClient; +import com.muyu.pay.wechat.domain.model.WeChatPayAmountModel; +import com.muyu.pay.wechat.domain.req.WeChatPayReqWeChatPay; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * @author DongZl + * @description: 微信支付下单测试类 + * @Date 2023-8-21 下午 02:55 + */ +@Log4j2 +@SpringBootTest(classes = WechatPayApplication.class) +public class NativeClientTest { + + /** + * 微信 native支付 + */ + @Autowired + private WeChatNativeClient weChatNativeClient; + + @Test + public void order(){ + String orderResult = weChatNativeClient.order( + WeChatPayReqWeChatPay.builder() + .description("测试商品") + .outTradeNo("12341684615") + .notifyUrl("http://doc.qinmian.online") + .payAmountModel( + WeChatPayAmountModel.totalBuild(100) + ) + .build() + ); + log.info(orderResult); + } +}