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