26 changed files with 1094 additions and 51 deletions
@ -0,0 +1,29 @@ |
|||
package com.base.springcloud.aspect.annotation; |
|||
|
|||
import com.base.springcloud.constant.ErrorConstant; |
|||
import lombok.Getter; |
|||
|
|||
@Getter |
|||
public class BusinessException extends RuntimeException{ |
|||
|
|||
private String code; |
|||
|
|||
/** |
|||
* 使用已有的错误类型 |
|||
* @param type 枚举类中的错误类型 |
|||
*/ |
|||
public BusinessException(ErrorConstant type){ |
|||
super(type.getMessage()); |
|||
this.code = type.getCode(); |
|||
} |
|||
|
|||
/** |
|||
* 自定义错误类型 |
|||
* @param code 自定义的错误码 |
|||
* @param msg 自定义的错误提示 |
|||
*/ |
|||
public BusinessException(String code, String msg){ |
|||
super(msg); |
|||
this.code = code; |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
package com.base.springcloud.aspect.annotation; |
|||
|
|||
import java.lang.annotation.*; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Target({ElementType.METHOD}) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Documented |
|||
@Inherited |
|||
public @interface Rlock { |
|||
|
|||
/** |
|||
* 分布式锁的key前缀 |
|||
*/ |
|||
String prefix() default "redisLockPrefix::"; |
|||
|
|||
/** |
|||
* 分布式锁的key动态参数 |
|||
*/ |
|||
String suffix() default "lockDynamicKey"; |
|||
|
|||
/** |
|||
* 等待时间 默认五秒 |
|||
* |
|||
* @return |
|||
*/ |
|||
long waitTime() default 5; |
|||
|
|||
/** |
|||
* 锁释放时间 默认十秒 |
|||
* |
|||
* @return |
|||
*/ |
|||
long leaseTime() default 10; |
|||
|
|||
/** |
|||
* 时间格式 默认:秒 |
|||
* |
|||
* @return |
|||
*/ |
|||
TimeUnit timeUnit() default TimeUnit.SECONDS; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
package com.base.springcloud.aspect.aspect; |
|||
|
|||
import com.base.springcloud.aspect.annotation.BusinessException; |
|||
import com.base.springcloud.base.CommonResult; |
|||
import org.springframework.web.bind.annotation.ControllerAdvice; |
|||
import org.springframework.web.bind.annotation.ExceptionHandler; |
|||
import org.springframework.web.bind.annotation.ResponseBody; |
|||
|
|||
@ControllerAdvice |
|||
public class BusinessExceptionHandler { |
|||
|
|||
/** |
|||
* @ExceptionHandler相当于controller的@RequestMapping |
|||
* 如果抛出的的是BusinessException,则调用该方法 |
|||
* @param se 业务异常 |
|||
* @return |
|||
*/ |
|||
@ExceptionHandler(BusinessException.class) |
|||
@ResponseBody |
|||
public CommonResult handle(BusinessException se){ |
|||
return CommonResult.error(se.getCode(),se.getMessage()); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,118 @@ |
|||
package com.base.springcloud.aspect.aspect; |
|||
|
|||
import com.base.springcloud.aspect.annotation.Rlock; |
|||
import com.base.springcloud.handle.RBSQueueHandler; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.aspectj.lang.ProceedingJoinPoint; |
|||
import org.aspectj.lang.annotation.Around; |
|||
import org.aspectj.lang.annotation.Aspect; |
|||
import org.aspectj.lang.annotation.Pointcut; |
|||
import org.aspectj.lang.reflect.MethodSignature; |
|||
import org.redisson.api.RLock; |
|||
import org.redisson.api.RedissonClient; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.core.DefaultParameterNameDiscoverer; |
|||
import org.springframework.expression.EvaluationContext; |
|||
import org.springframework.expression.Expression; |
|||
import org.springframework.expression.spel.standard.SpelExpressionParser; |
|||
import org.springframework.expression.spel.support.StandardEvaluationContext; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.lang.reflect.Method; |
|||
|
|||
@Aspect |
|||
@Component |
|||
@Slf4j |
|||
public class RlockAspect { |
|||
|
|||
Logger logger = LoggerFactory.getLogger(RBSQueueHandler.class); |
|||
|
|||
@Autowired |
|||
private RedissonClient redissonClient; |
|||
|
|||
/** |
|||
* 用于SpEL表达式解析. |
|||
*/ |
|||
private SpelExpressionParser parser = new SpelExpressionParser(); |
|||
/** |
|||
* 用于获取方法参数定义名字. |
|||
*/ |
|||
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); |
|||
|
|||
|
|||
|
|||
@Pointcut("@annotation(com.base.springcloud.aspect.annotation.Rlock)") |
|||
public void RlockAspect() { |
|||
} |
|||
|
|||
@Around("RlockAspect()") |
|||
public Object arround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { |
|||
Object object = null; |
|||
RLock lock = null; |
|||
try { |
|||
// 获取注解信息
|
|||
Rlock rlockInfo = getRlockInfo(proceedingJoinPoint); |
|||
String key = getLocalKey(proceedingJoinPoint, rlockInfo); |
|||
// 根据名字获取锁实例
|
|||
lock = redissonClient.getLock(key); |
|||
if (lock != null) { |
|||
final boolean status = lock.tryLock(rlockInfo.waitTime(), rlockInfo.leaseTime(), rlockInfo.timeUnit()); |
|||
if (status) { |
|||
logger.info("锁获取成功,加锁成功 : {}........",key); |
|||
object = proceedingJoinPoint.proceed(); |
|||
} else { |
|||
logger.info("锁被占用,加锁失败 : {} .........",key); |
|||
throw new RuntimeException("加锁失败"); |
|||
} |
|||
} |
|||
} finally { |
|||
// 方法执行完成释放锁
|
|||
if (lock != null && lock.isHeldByCurrentThread()) { |
|||
lock.unlock(); |
|||
} |
|||
} |
|||
return object; |
|||
} |
|||
|
|||
public Rlock getRlockInfo(ProceedingJoinPoint proceedingJoinPoint) { |
|||
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature(); |
|||
return methodSignature.getMethod().getAnnotation(Rlock.class); |
|||
} |
|||
|
|||
/** |
|||
* @param proceedingJoinPoint |
|||
* @return |
|||
*/ |
|||
public String getLocalKey(ProceedingJoinPoint proceedingJoinPoint, Rlock rlockInfo) { |
|||
StringBuilder localKey = new StringBuilder(); |
|||
//获取注解参数值
|
|||
String prefix = rlockInfo.prefix(); |
|||
String spELString = rlockInfo.suffix(); |
|||
String key = generateKeyBySpEL(spELString,proceedingJoinPoint); |
|||
localKey.append(prefix).append(key); |
|||
logger.info("加锁 key :{}",localKey.toString()); |
|||
return localKey.toString(); |
|||
} |
|||
|
|||
public String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) { |
|||
// 通过joinPoint获取被注解方法
|
|||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); |
|||
Method method = methodSignature.getMethod(); |
|||
// 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
|
|||
String[] paramNames = nameDiscoverer.getParameterNames(method); |
|||
// 解析过后的Spring表达式对象
|
|||
Expression expression = parser.parseExpression(spELString); |
|||
// spring的表达式上下文对象
|
|||
EvaluationContext context = new StandardEvaluationContext(); |
|||
// 通过joinPoint获取被注解方法的形参
|
|||
Object[] args = joinPoint.getArgs(); |
|||
// 给上下文赋值
|
|||
for(int i = 0 ; i < args.length ; i++) { |
|||
context.setVariable(paramNames[i], args[i]); |
|||
} |
|||
// 表达式从上下文中计算出实际参数值
|
|||
return expression.getValue(context).toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
package com.base.springcloud.constant; |
|||
|
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.parser.ParserConfig; |
|||
import com.alibaba.fastjson.serializer.SerializerFeature; |
|||
import com.fasterxml.jackson.databind.JavaType; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import com.fasterxml.jackson.databind.type.TypeFactory; |
|||
import org.springframework.data.redis.serializer.RedisSerializer; |
|||
import org.springframework.data.redis.serializer.SerializationException; |
|||
import org.springframework.util.Assert; |
|||
|
|||
import java.nio.charset.Charset; |
|||
|
|||
/** |
|||
* FastJson2JsonRedisSerializer |
|||
* Redis使用FastJson序列化 |
|||
*/ |
|||
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> { |
|||
private ObjectMapper objectMapper = new ObjectMapper(); |
|||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); |
|||
|
|||
private Class<T> clazz; |
|||
|
|||
static { |
|||
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |
|||
} |
|||
|
|||
public FastJson2JsonRedisSerializer(Class<T> clazz) { |
|||
super(); |
|||
this.clazz = clazz; |
|||
} |
|||
|
|||
public byte[] serialize(T t) throws SerializationException { |
|||
if (t == null) { |
|||
return new byte[0]; |
|||
} |
|||
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); |
|||
} |
|||
|
|||
public T deserialize(byte[] bytes) throws SerializationException { |
|||
if (bytes == null || bytes.length <= 0) { |
|||
return null; |
|||
} |
|||
String str = new String(bytes, DEFAULT_CHARSET); |
|||
|
|||
return JSON.parseObject(str, clazz); |
|||
} |
|||
|
|||
public void setObjectMapper(ObjectMapper objectMapper) { |
|||
Assert.notNull(objectMapper, "'objectMapper' must not be null"); |
|||
this.objectMapper = objectMapper; |
|||
} |
|||
|
|||
protected JavaType getJavaType(Class<?> clazz) { |
|||
return TypeFactory.defaultInstance().constructType(clazz); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
package com.base.springcloud.constant; |
|||
|
|||
|
|||
import com.fasterxml.jackson.annotation.JsonAutoDetect; |
|||
import com.fasterxml.jackson.annotation.PropertyAccessor; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import org.redisson.Redisson; |
|||
import org.redisson.config.Config; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.cache.annotation.CachingConfigurerSupport; |
|||
import org.springframework.cache.annotation.EnableCaching; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.data.redis.serializer.StringRedisSerializer; |
|||
|
|||
@Configuration |
|||
@EnableCaching |
|||
public class RedisConfig extends CachingConfigurerSupport { |
|||
|
|||
@Value("${spring.redis.host}") |
|||
private String host; |
|||
|
|||
@Value("${spring.redis.port}") |
|||
private String port; |
|||
|
|||
@Value("${spring.redis.password}") |
|||
private String password; |
|||
|
|||
@Bean |
|||
@SuppressWarnings(value = {"unchecked", "rawtypes"}) |
|||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { |
|||
RedisTemplate<Object, Object> template = new RedisTemplate<>(); |
|||
template.setConnectionFactory(redisConnectionFactory); |
|||
|
|||
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); |
|||
|
|||
ObjectMapper mapper = new ObjectMapper(); |
|||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); |
|||
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); |
|||
serializer.setObjectMapper(mapper); |
|||
|
|||
template.setValueSerializer(serializer); |
|||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
|||
template.setKeySerializer(new StringRedisSerializer()); |
|||
template.afterPropertiesSet(); |
|||
return template; |
|||
} |
|||
|
|||
@Bean |
|||
public Redisson redisson() { |
|||
Config config = new Config(); |
|||
config.useSingleServer().setAddress("redis://" + host + ":" + port); |
|||
config.useSingleServer().setPassword(password); |
|||
return (Redisson) Redisson.create(config); |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
package com.base.springcloud.entity; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 支付、退款数据回调通用 |
|||
* |
|||
* @author shuaifengjie.com |
|||
*/ |
|||
@Data |
|||
public class CallbackBodyVo implements Serializable { |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
private static final long serialVersionUID = -3279905156049535047L; |
|||
|
|||
|
|||
/** |
|||
* 通知Id |
|||
*/ |
|||
private String id; |
|||
/** |
|||
* 通知创建时间 |
|||
*/ |
|||
private String create_time; |
|||
/** |
|||
* 通知类型 |
|||
* 支付成功:TRANSACTION.SUCCESS |
|||
* 退款成功:REFUND.SUCCESS |
|||
*/ |
|||
private String resource_type; |
|||
/** |
|||
* 通知数据类型 |
|||
*/ |
|||
private String event_type; |
|||
/** |
|||
* 回调摘要 |
|||
*/ |
|||
private String summary; |
|||
/** |
|||
* 通知数据 |
|||
*/ |
|||
private Resource resource; |
|||
|
|||
/** |
|||
* 通知数据 |
|||
*/ |
|||
@Data |
|||
public static class Resource { |
|||
/** |
|||
* 对开启结果数据进行加密的加密算法,目前只支持AEAD_AES_256_GCM。 |
|||
*/ |
|||
private String algorithm; |
|||
/** |
|||
* Base64编码后的开启/停用结果数据密文。 |
|||
*/ |
|||
private String ciphertext; |
|||
/** |
|||
* 附加数据。 |
|||
*/ |
|||
private String associated_data; |
|||
/** |
|||
* 加密使用的随机串。 |
|||
*/ |
|||
private String nonce; |
|||
/** |
|||
* 原始回调类型。 |
|||
*/ |
|||
private String original_type; |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
package com.base.springcloud.entity; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 微信退款预下单-主体信息 |
|||
* @author shuaifengjie.com |
|||
* v3 |
|||
*/ |
|||
@Data |
|||
public class WxpayRefundVo implements Serializable { |
|||
|
|||
private static final long serialVersionUID = 8189139771687842957L; |
|||
// 原支付交易对应的微信订单号
|
|||
// 示例值:1217752501201407033233368018
|
|||
private String transaction_id; |
|||
// 若商户传入,会在下发给用户的退款消息中体现退款原因
|
|||
// 示例值:商品已售完
|
|||
private String reason; |
|||
// 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
|
|||
// 示例值:1217752501201407033233368018
|
|||
private String out_refund_no; |
|||
// 异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。
|
|||
// 示例值:https://weixin.qq.com
|
|||
private String notify_url; |
|||
// 订单金额信息
|
|||
private WxRefundAmountVo amount; |
|||
|
|||
public WxpayRefundVo() { |
|||
super(); |
|||
} |
|||
|
|||
@Data |
|||
public static class WxRefundAmountVo { |
|||
// 退款金额,币种的最小单位,只能为整数,不能超过原订单支付金额。
|
|||
// 示例值:888
|
|||
private long refund; |
|||
// 原支付交易的订单总金额,币种的最小单位,只能为整数。
|
|||
// 示例值:888
|
|||
private long total; |
|||
// CNY:人民币,境内商户号仅支持人民币。
|
|||
// 示例值:CNY
|
|||
private String currency = "CNY"; |
|||
|
|||
public WxRefundAmountVo() { |
|||
super(); |
|||
} |
|||
|
|||
public WxRefundAmountVo(long refund, long total) { |
|||
super(); |
|||
this.refund = refund; |
|||
this.total = total; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,119 @@ |
|||
package com.base.springcloud.entity; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 微信支付预下单-主体信息 |
|||
* @author shuaifengjie.com |
|||
* v3 |
|||
*/ |
|||
@Data |
|||
public class WxpayTradeVo implements Serializable { |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
private static final long serialVersionUID = 8189139771687842957L; |
|||
|
|||
// 由微信生成的应用ID,全局唯一。请求基础下单接口时请注意APPID的应用属性,应为公众号的APPID
|
|||
// 示例值:wxd678efh567hg6787
|
|||
private String appid; |
|||
|
|||
// 直连商户的商户号,由微信支付生成并下发。
|
|||
// 示例值:1230000109
|
|||
private String mchid; |
|||
|
|||
// 商品描述
|
|||
// 示例值:Image形象店-深圳腾大-QQ公仔
|
|||
private String description; |
|||
|
|||
// 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
|
|||
// 示例值:1217752501201407033233368018
|
|||
private String out_trade_no; |
|||
|
|||
// 通知URL必须为直接可访问的URL,不允许携带查询串。
|
|||
// 示例值:https://www.weixin.qq.com/wxpay/pay.php
|
|||
private String notify_url; |
|||
|
|||
// 订单金额信息
|
|||
private WxTradeAmountVo amount; |
|||
|
|||
// 支付者信息 - JSAPI,必传
|
|||
private WxPayerVo payer; |
|||
|
|||
// 支付场景描述 - H5,必传
|
|||
private WxSceneInfoVo scene_info; |
|||
|
|||
public WxpayTradeVo() { |
|||
super(); |
|||
} |
|||
|
|||
public WxpayTradeVo(String appid, String mchid, String description, String out_trade_no, String notify_url) { |
|||
super(); |
|||
this.appid = appid; |
|||
this.mchid = mchid; |
|||
this.description = description; |
|||
this.out_trade_no = out_trade_no; |
|||
this.notify_url = notify_url; |
|||
} |
|||
|
|||
@Data |
|||
public static class WxTradeAmountVo { |
|||
// 订单总金额,单位为分。
|
|||
// 示例值:100
|
|||
private long total; |
|||
// CNY:人民币,境内商户号仅支持人民币。
|
|||
// 示例值:CNY
|
|||
private String currency = "CNY"; |
|||
|
|||
public WxTradeAmountVo() { |
|||
super(); |
|||
} |
|||
public WxTradeAmountVo(long total) { |
|||
super(); |
|||
this.total = total; |
|||
} |
|||
} |
|||
|
|||
@Data |
|||
public static class WxPayerVo { |
|||
// 用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid,Openid获取详见
|
|||
// 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
|
|||
private String openid; |
|||
|
|||
public WxPayerVo() { |
|||
super(); |
|||
} |
|||
public WxPayerVo(String openid) { |
|||
super(); |
|||
this.openid = openid; |
|||
} |
|||
} |
|||
|
|||
@Data |
|||
public static class WxSceneInfoVo { |
|||
// 用户的客户端IP,支持IPv4和IPv6两种格式的IP地址。
|
|||
// 示例值:14.23.150.211
|
|||
private String payer_client_ip; |
|||
// H5场景信息
|
|||
private WxH5InfoVo h5_info; |
|||
|
|||
public WxSceneInfoVo() { |
|||
super(); |
|||
} |
|||
public WxSceneInfoVo(String payer_client_ip) { |
|||
super(); |
|||
this.payer_client_ip = payer_client_ip; |
|||
} |
|||
} |
|||
|
|||
@Data |
|||
public static class WxH5InfoVo { |
|||
// 场景类型
|
|||
// 示例值:iOS, Android, Wap
|
|||
private String type = "Wap"; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
package com.base.springcloud.util; |
|||
|
|||
import com.google.gson.Gson; |
|||
import com.google.gson.JsonSyntaxException; |
|||
import com.google.gson.reflect.TypeToken; |
|||
|
|||
import java.util.Map; |
|||
|
|||
public class StringHelper { |
|||
|
|||
public static Map<String, String> json2map(String str_json) { |
|||
Map<String, String> res = null; |
|||
try { |
|||
Gson gson = new Gson(); |
|||
res = gson.fromJson(str_json, new TypeToken<Map<String, String>>() { |
|||
}.getType()); |
|||
} catch (JsonSyntaxException e) { |
|||
} |
|||
return res; |
|||
} |
|||
} |
|||
@ -0,0 +1,344 @@ |
|||
package com.base.springcloud.util; |
|||
|
|||
import com.alibaba.fastjson.JSON; |
|||
import com.base.springcloud.constant.PaymentConstant; |
|||
import com.base.springcloud.entity.CallbackBodyVo; |
|||
import com.base.springcloud.entity.WxpayRefundVo; |
|||
import com.base.springcloud.entity.WxpayTradeVo; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; |
|||
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.http.HttpEntity; |
|||
import org.apache.http.client.methods.CloseableHttpResponse; |
|||
import org.apache.http.client.methods.HttpPost; |
|||
import org.apache.http.entity.ContentType; |
|||
import org.apache.http.entity.StringEntity; |
|||
import org.apache.http.impl.client.CloseableHttpClient; |
|||
import org.apache.http.util.EntityUtils; |
|||
import org.springframework.util.Base64Utils; |
|||
|
|||
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.ByteArrayInputStream; |
|||
import java.io.IOException; |
|||
import java.io.UnsupportedEncodingException; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.security.*; |
|||
import java.security.cert.X509Certificate; |
|||
import java.time.LocalDateTime; |
|||
import java.time.ZoneOffset; |
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 微信支付帮助类 |
|||
* |
|||
* @author shuaifengjie.com |
|||
*/ |
|||
@Slf4j |
|||
public class WxpayUtils { |
|||
|
|||
/** |
|||
* 发起支付 |
|||
* |
|||
* @param tradeVo |
|||
* @param scene |
|||
* @param privateKey |
|||
* @param certificate |
|||
* @param serialNo |
|||
* @return |
|||
*/ |
|||
public static ObjectNode getWxpay(WxpayTradeVo tradeVo, String scene, String privateKey, String certificate, String serialNo) { |
|||
// ...
|
|||
String responseBody = response(JSON.toJSONString(tradeVo), getUrl(scene), tradeVo.getMchid(), privateKey, certificate, serialNo); |
|||
log.info("======>>> wxpay response body: ", responseBody); |
|||
// parse
|
|||
@SuppressWarnings("unchecked") |
|||
Map<String, String> resultMap = StringHelper.json2map(responseBody); |
|||
// ...
|
|||
ObjectMapper objectMapper = new ObjectMapper(); |
|||
ObjectNode objectNode = objectMapper.createObjectNode(); |
|||
// ...
|
|||
if (PaymentConstant.NATIVE.equals(scene)) {// pc - 扫码支付(Native)
|
|||
objectNode.put("code_url", resultMap.get("code_url")); |
|||
} else if (PaymentConstant.H5.equals(scene)) {// 手机网页(H5)
|
|||
objectNode.put("h5_url", resultMap.get("h5_url")); |
|||
} else if (PaymentConstant.JSAPI.equals(scene)) {// 微信公众号(H5)
|
|||
String prepayId = resultMap.get("prepay_id"); |
|||
objectNode.put("prepay_id", prepayId); |
|||
// ...
|
|||
objectNode.put("appId", tradeVo.getAppid()); |
|||
String timestamp = getTimestamp(); |
|||
objectNode.put("timeStamp", timestamp); |
|||
String nonceStr = CreateNoncestr(); |
|||
objectNode.put("nonceStr", nonceStr); |
|||
String packageStr = "prepay_id=" + prepayId; |
|||
objectNode.put("package", packageStr); |
|||
// 签名
|
|||
String paySign = doRequestSign(privateKey, tradeVo.getAppid(), timestamp, nonceStr, packageStr); |
|||
objectNode.put("paySign", paySign); |
|||
objectNode.put("signType", "RSA"); |
|||
} |
|||
return objectNode; |
|||
} |
|||
|
|||
/** |
|||
* 发起退款 |
|||
* |
|||
* @param tradeVo |
|||
* @param mchid |
|||
* @param privateKey |
|||
* @param certificate |
|||
* @param serialNo |
|||
* @return |
|||
*/ |
|||
public static void refund(WxpayRefundVo tradeVo, String mchid, String privateKey, String certificate, String serialNo) { |
|||
// ...
|
|||
String responseBody = response(JSON.toJSONString(tradeVo), REFUND_URL, mchid, privateKey, certificate, serialNo); |
|||
log.info("======>>> wxrefund response body: ", responseBody); |
|||
} |
|||
|
|||
/** |
|||
* 公共请求 |
|||
* |
|||
* @param data |
|||
* @param url |
|||
* @param mchid |
|||
* @param privateKey |
|||
* @param certificate |
|||
* @param serialNo |
|||
* @return |
|||
*/ |
|||
public static String response(String data, String url, String mchid, String privateKey, String certificate, String serialNo) { |
|||
log.info("====>>> wxpay response, mchid: {}, serialNo: {}, url: {}, privateKey: {}, certificate: {}, data: {}", |
|||
mchid, serialNo, url, privateKey, certificate, data); |
|||
// ...
|
|||
HttpPost httpPost = new HttpPost(url); |
|||
StringEntity reqEntity = new StringEntity(data, ContentType.create("application/json", "utf-8")); |
|||
// ...
|
|||
httpPost.setEntity(reqEntity); |
|||
httpPost.addHeader("Accept", "application/json"); |
|||
// ...
|
|||
CloseableHttpResponse response = null; |
|||
// 返回
|
|||
String body = null; |
|||
try { |
|||
// 商户私钥
|
|||
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8"))); |
|||
// ...
|
|||
response = getClient(mchid, serialNo, merchantPrivateKey, certificate).execute(httpPost); |
|||
log.info("====>>> wxpay response, result: {}", JSON.toJSONString(response)); |
|||
// response = httpClient.execute(httpPost);
|
|||
// assertTrue(response.getStatusLine().getStatusCode() != 401);
|
|||
int statusCode = response.getStatusLine().getStatusCode(); |
|||
HttpEntity entity = response.getEntity(); |
|||
body = EntityUtils.toString(entity); |
|||
log.info("====>>> wxpay response, success: {}, body: {}", statusCode, body); |
|||
if (statusCode != 200) { |
|||
@SuppressWarnings("unchecked") |
|||
Map<String, String> errorMsg = StringHelper.json2map(body); |
|||
} |
|||
// 执行释放
|
|||
EntityUtils.consume(entity); |
|||
} catch (IOException e) { |
|||
log.error("======>>> wxpay response, client exception: {}", e); |
|||
} catch (Exception e) { |
|||
log.error("======>>> wxpay response, exception: {}", e); |
|||
} finally { |
|||
try { |
|||
response.close(); |
|||
} catch (IOException e) { |
|||
log.error("======>>> wxpay response, ioe exception: {}", e); |
|||
} |
|||
} |
|||
return body; |
|||
} |
|||
|
|||
/** |
|||
* 创建Client |
|||
* |
|||
* @param mchid |
|||
* @param serialNo |
|||
* @param privateKey |
|||
* @param certificate |
|||
* @return |
|||
* @throws UnsupportedEncodingException |
|||
*/ |
|||
public static CloseableHttpClient getClient(String mchid, String serialNo, PrivateKey privateKey, String certificate) throws UnsupportedEncodingException { |
|||
|
|||
// 使用自动更新的签名验证器,不需要传入证书
|
|||
// AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
|
|||
// new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes("utf-8"));
|
|||
|
|||
// builder
|
|||
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchid, serialNo, privateKey); |
|||
// .withValidator(new WechatPay2Validator(verifier));
|
|||
|
|||
List<X509Certificate> certs = new ArrayList<>(); |
|||
certs.add(PemUtil.loadCertificate(new ByteArrayInputStream(certificate.getBytes("utf-8")))); |
|||
builder.withWechatpay(certs); |
|||
return builder.build(); |
|||
} |
|||
|
|||
private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"; |
|||
|
|||
private static final String H5_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/h5"; |
|||
|
|||
private static final String NATIVE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/native"; |
|||
|
|||
private static final String REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"; |
|||
|
|||
// 获取支付订单类型
|
|||
private static String getUrl(String scene) { |
|||
String url = ""; |
|||
switch (scene) { |
|||
case PaymentConstant.NATIVE: |
|||
url = NATIVE_URL; |
|||
break; |
|||
case PaymentConstant.H5: |
|||
url = H5_URL; |
|||
break; |
|||
case PaymentConstant.JSAPI: |
|||
url = JSAPI_URL; |
|||
break; |
|||
default: |
|||
} |
|||
return url; |
|||
} |
|||
|
|||
/** |
|||
* 字符串 |
|||
* |
|||
* @return |
|||
*/ |
|||
public static String CreateNoncestr() { |
|||
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; |
|||
String res = ""; |
|||
for (int i = 0; i < 16; i++) { |
|||
Random rd = new Random(); |
|||
res += chars.charAt(rd.nextInt(chars.length() - 1)); |
|||
} |
|||
return res; |
|||
} |
|||
|
|||
/** |
|||
* 得到时间戳 |
|||
* |
|||
* @return |
|||
*/ |
|||
public static String getTimestamp() { |
|||
long epochSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8")); |
|||
return String.valueOf(epochSecond); |
|||
} |
|||
|
|||
/** |
|||
* 加密算法提供方 - BouncyCastle |
|||
*/ |
|||
private static final String BC_PROVIDER = "BC"; |
|||
|
|||
/** |
|||
* @param privateKey |
|||
* @param orderedComponents |
|||
* @return |
|||
*/ |
|||
public static String doRequestSign(String privateKey, String... orderedComponents) { |
|||
try { |
|||
// 商户私钥
|
|||
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8"))); |
|||
Signature signer = Signature.getInstance("SHA256withRSA", BC_PROVIDER); |
|||
signer.initSign(merchantPrivateKey); |
|||
final String signatureStr = createSign(true, orderedComponents); |
|||
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); |
|||
return Base64Utils.encodeToString(signer.sign()); |
|||
} catch (InvalidKeyException e) { |
|||
log.error("InvalidKeyException: {}", e); |
|||
} catch (SignatureException e) { |
|||
log.error("SignatureException: {}", e); |
|||
} catch (NoSuchProviderException e) { |
|||
log.error("NoSuchProviderException: {}", e); |
|||
} catch (NoSuchAlgorithmException e) { |
|||
log.error("NoSuchAlgorithmException: {}", e); |
|||
} catch (UnsupportedEncodingException e) { |
|||
log.error("UnsupportedEncodingException: {}", e); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 请求时设置签名 组件 |
|||
* |
|||
* @param components the components |
|||
* @return string string |
|||
*/ |
|||
private static String createSign(boolean newLine, String... components) { |
|||
String suffix = newLine ? "\n" : ""; |
|||
return Arrays.stream(components).collect(Collectors.joining("\n", "", suffix)); |
|||
} |
|||
|
|||
/** |
|||
* 使用微信平台证书,对微信回调数据验签,做应答签名比较 |
|||
* |
|||
* @param certificate |
|||
* @param wechatpaySignature |
|||
* @param wechatpayTimestamp |
|||
* @param wechatpayNonce |
|||
* @param body |
|||
* @return |
|||
*/ |
|||
public static boolean responseSignVerify(String certificate, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) { |
|||
try { |
|||
final String signatureStr = createSign(true, wechatpayTimestamp, wechatpayNonce, body); |
|||
Signature signer = Signature.getInstance("SHA256withRSA"); |
|||
signer.initVerify(PemUtil.loadCertificate(new ByteArrayInputStream(certificate.getBytes("utf-8")))); |
|||
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); |
|||
// ...
|
|||
return signer.verify(Base64Utils.decodeFromString(wechatpaySignature)); |
|||
} catch (UnsupportedEncodingException e) { |
|||
log.error("UnsupportedEncodingException: {}", e); |
|||
} catch (SignatureException e) { |
|||
log.error("SignatureException: {}", e); |
|||
} catch (NoSuchAlgorithmException e) { |
|||
log.error("NoSuchAlgorithmException: {}", e); |
|||
} catch (InvalidKeyException e) { |
|||
log.error("InvalidKeyException: {}", e); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* 微信V3密钥解密响应体 |
|||
* |
|||
* @param apiv3Key |
|||
* @param bodyVo |
|||
* @return |
|||
*/ |
|||
public static String decryptResponseBody(String apiv3Key, CallbackBodyVo bodyVo) { |
|||
try { |
|||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); |
|||
|
|||
SecretKeySpec key = new SecretKeySpec(apiv3Key.getBytes(StandardCharsets.UTF_8), "AES"); |
|||
GCMParameterSpec spec = new GCMParameterSpec(128, bodyVo.getResource().getNonce().getBytes(StandardCharsets.UTF_8)); |
|||
|
|||
cipher.init(Cipher.DECRYPT_MODE, key, spec); |
|||
cipher.updateAAD(bodyVo.getResource().getAssociated_data().getBytes(StandardCharsets.UTF_8)); |
|||
|
|||
byte[] bytes = cipher.doFinal(Base64Utils.decodeFromString(bodyVo.getResource().getCiphertext())); |
|||
return new String(bytes, StandardCharsets.UTF_8); |
|||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) { |
|||
log.error("NoSuchAlgorithmException: {}", e); |
|||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) { |
|||
log.error("InvalidKeyException: {}", e); |
|||
} catch (IllegalBlockSizeException e) { |
|||
log.error("IllegalBlockSizeException: {}", e); |
|||
} catch (BadPaddingException e) { |
|||
log.error("BadPaddingException: {}", e); |
|||
} |
|||
return null; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue