初始话

This commit is contained in:
zk
2026-03-10 18:27:11 +08:00
commit c4efa7e917
75 changed files with 5083 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jiayunet</groupId>
<artifactId>back_end</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.13</version> <!-- 请根据需要选择最新版本 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加AspectJ依赖管理 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.15</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,56 @@
package org.jiayunet.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Aspect
@Component
@Slf4j
public class ControllerLogAspect {
// 定义切点:所有Controller包下的方法
@Pointcut("execution(* org.jiayunet..controller..*.*(..))")
public void controllerPointcut() {
}
// 方法执行前记录请求信息
@Before("controllerPointcut()")
public void logBefore(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
// 处理参数,过滤掉HttpServletRequest/Response和文件流
List<Object> filteredArgs = Arrays.stream(joinPoint.getArgs()).filter(arg -> !(arg instanceof HttpServletRequest || arg instanceof HttpServletResponse || arg instanceof MultipartFile || (arg != null && arg.getClass().isArray() && MultipartFile.class.isAssignableFrom(arg.getClass().getComponentType())))).collect(Collectors.toList());
log.info("接口参数 方法:{} {}" + "参数: {} ", className, methodName, filteredArgs);
}
// 方法执行后记录返回结果
@AfterReturning(pointcut = "controllerPointcut()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
log.info("接口返回:{}", result);
}
// 方法异常时记录错误信息
@AfterThrowing(pointcut = "controllerPointcut()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
log.error(" 类名: {} 方法: {} 异常: {} 堆栈: {}", className, methodName, ex.getMessage(), getStackTrace(ex));
}
private String getStackTrace(Throwable ex) {
return Arrays.stream(ex.getStackTrace()).map(StackTraceElement::toString).collect(Collectors.joining("\n"));
}
}
@@ -0,0 +1,48 @@
package org.jiayunet.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.io.IOException;
/**
* Jackson 全局配置
* <p>
* 将 Long/long 类型序列化为字符串,避免 JavaScript 精度丢失问题
*/
@JsonComponent
public class JacksonConfig {
private static final JsonSerializer<Long> LONG_SERIALIZER = new JsonSerializer<Long>() {
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.toString());
}
};
private static final JsonDeserializer<Long> LONG_DESERIALIZER = new JsonDeserializer<Long>() {
@Override
public Long deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
String text = p.getText();
return (text == null || text.trim().isEmpty()) ? null : Long.parseLong(text.trim());
}
};
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
return builder
.serializerByType(Long.class, LONG_SERIALIZER)
.serializerByType(long.class, LONG_SERIALIZER)
.deserializerByType(Long.class, LONG_DESERIALIZER)
.deserializerByType(long.class, LONG_DESERIALIZER)
.build();
}
}
@@ -0,0 +1,49 @@
package org.jiayunet.config;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* @author zk
*/
@Configuration
public class MybatisConfig {
/**
* SQL注入器
* 在默认的基础上田间InsertBatchSomeColumn
*/
@Bean
public DefaultSqlInjector easySqlInjector() {
return new DefaultSqlInjector(){
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass,tableInfo);
methodList.add(new InsertBatchSomeColumn());
methodList.add(new UpdateBatchMethod("updateBatchMethod"));
return methodList;
}
};
}
/**
* 拦截器实现得分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
@@ -0,0 +1,36 @@
package org.jiayunet.config;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
@Configuration
public class OssConfig {
/**
* 阿里云配置
*/
@Value("${app.oss.aliyun.access_key_id}")
private String accessKeyId;
@Value("${app.oss.aliyun.access_key_secret}")
private String accessKeySecret;
/**
* 密码凭证提供者
*/
@Bean
@ConditionalOnProperty(name = "app.oss.service_provider", havingValue = "aliyun")
public DefaultCredentialProvider aliOssCredentialProvider() {
Assert.hasText(accessKeyId, "app.oss.aliyun.access_key_id配置缺失");
Assert.hasText(accessKeySecret, "app.oss.aliyun.access_key_secret配置缺失");
return CredentialsProviderFactory.newDefaultCredentialProvider(accessKeyId,accessKeySecret);
}
}
@@ -0,0 +1,35 @@
package org.jiayunet.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.redisson.config.Config;
/**
* @author zk
*/
@Configuration
public class RedissonConf {
/**
* redis 配置
*/
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private String port;
@Bean
public Config redissonConfig() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://"+host+":"+port)
.setPassword(password)
.setDatabase(1);
return config;
}
}
@@ -0,0 +1,95 @@
package org.jiayunet.config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.jiayunet.interceptor.JwtAuthenticationTokenFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author zk
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${app.ignore.urls}")
private String ignoreUrls;
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 处理跨域问题
*/
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration config = new CorsConfiguration();
config.setAllowedHeaders(List.of("*"));
config.setAllowedMethods(List.of("*"));
config.setAllowedOrigins(List.of("*"));
//config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable().httpBasic().disable().formLogin().disable().logout().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers(ignoreUrls.split(",")).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.cors().configurationSource(corsConfigurationSource());
// 把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("status", HttpServletResponse.SC_UNAUTHORIZED);
errorResponse.put("message", "Authentication failed");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 将错误消息写入响应体
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getWriter(), errorResponse);
});
}
}
@@ -0,0 +1,37 @@
package org.jiayunet.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
/**
* @author zk
*/
@Configuration
public class SmsConfig {
/**
* 阿里云配置
*/
@Value("${app.sms.aliyun.access_key_id}")
private String accessKeyId;
@Value("${app.sms.aliyun.access_key_secret}")
private String accessKeySecret;
@Bean
@ConditionalOnProperty(name = "app.sms.service_provider", havingValue = "aliyun")
public com.aliyun.dysmsapi20170525.Client aliyunSmsClient() throws Exception {
Assert.hasText(accessKeyId, "app.sms.aliyun.access_key_id配置缺失");
Assert.hasText(accessKeySecret, "app.sms.aliyun.access_key_secret配置缺失");
com.aliyun.teaopenapi.models.Config config =
new com.aliyun.teaopenapi.models.Config().setAccessKeyId(accessKeyId).setAccessKeySecret(accessKeySecret);
config.endpoint = "dysmsapi.aliyuncs.com";
return new com.aliyun.dysmsapi20170525.Client(config);
}
}
@@ -0,0 +1,26 @@
package org.jiayunet.config;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource; /**
* @author zk
*/
public class UpdateBatchMethod extends AbstractMethod {
protected UpdateBatchMethod(String methodName) {
super(methodName);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = "<script>\n<foreach collection=\"list\" item=\"item\" separator=\";\">\nupdate %s %s where %s=#{%s} %s\n</foreach>\n</script>";
String additional = tableInfo.isWithVersion() ? tableInfo.getVersionFieldInfo().getVersionOli("item", "item.") : "" + tableInfo.getLogicDeleteSql(true, true);
String setSql = sqlSet(tableInfo.isWithLogicDelete(), false, tableInfo, false, "item", "item.");
String sqlResult = String.format(sql, tableInfo.getTableName(), setSql, tableInfo.getKeyColumn(), "item." + tableInfo.getKeyProperty(), additional);
//log.debug("sqlResult----->{}", sqlResult);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
// 第三个参数必须和RootMapper的自定义方法名一致
return this.addUpdateMappedStatement(mapperClass, modelClass, "updateBatchMethod", sqlSource);
}
}
@@ -0,0 +1,28 @@
package org.jiayunet.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.jiayunet.interceptor.BlackListInterceptor;
import org.jiayunet.interceptor.PreventReplayInterceptor;
/**
* @author zk
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private PreventReplayInterceptor preventReplayInterceptor;
@Autowired
private BlackListInterceptor blackListInterceptor;
//配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(blackListInterceptor).addPathPatterns("/**").order(1);
registry.addInterceptor(preventReplayInterceptor).addPathPatterns("/**").order(2);
}
}
@@ -0,0 +1,142 @@
package org.jiayunet.config;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.util.PemUtil;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.refund.RefundService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
import org.jiayunet.wxPay.server.TransferServer;
import java.security.PrivateKey;
/**
* @author zk
*/
@Configuration
public class WxPayConfig {
/**
* 商户号
*/
@Value("${wx_pay.merchant_id}")
private String merchantId = "";
/**
* 商户API私钥路径
*/
@Value("${wx_pay.privateKey_path}")
private String privateKeyPath = "";
/**
* 商户证书序列号
*/
@Value("${wx_pay.merchant_serial_number}")
private String merchantSerialNumber = "";
/**
* 商户APIV3密钥
*/
@Value("${wx_pay.api_v3_key}")
private String apiV3Key = "";
/**
* 平台公钥地址
*/
@Value("${wx_pay.public_key_path}")
private String publicKeyPath = "";
/**
* 平台公钥地址ID
*/
@Value("${wx_pay.public_key_id}")
private String publicKeyId = "";
/**
* 微信自动签名验签配置
*/
@Bean
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
public RSAPublicKeyConfig rSAAutoCertificateConfig() {
Assert.hasText(merchantId, "微信支付的商户号不能为空");
Assert.hasText(privateKeyPath, "商户API私钥路径不能为空");
Assert.hasText(merchantSerialNumber, "商户证书序列号不能为空");
Assert.hasText(apiV3Key, "商户APIV3密钥不能为空");
Assert.hasText(publicKeyPath, "微信支付平台公钥地址不能为空");
Assert.hasText(publicKeyId, "微信支付平台公钥Id不能为空");
return new RSAPublicKeyConfig.Builder()
.merchantId(merchantId)
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.publicKeyFromPath(publicKeyPath)
.publicKeyId(publicKeyId)
.build();
}
/**
* 微信JsApi支付接口能能力server
*/
@Bean
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
public JsapiService jsapiService(RSAPublicKeyConfig rSAPublicKeyConfig) {
return new JsapiService.Builder().config(rSAPublicKeyConfig).build();
}
/**
* 微信Native支付接口能能力server
*/
@Bean
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
public NativePayService nativePayService(RSAPublicKeyConfig rSAPublicKeyConfig) {
return new NativePayService.Builder().config(rSAPublicKeyConfig).build();
}
/**
* 微信单家付款接口能能力server
*/
@Bean
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
public TransferServer transferServer(RSAPublicKeyConfig rSAPublicKeyConfig) {
return new TransferServer.Builder().config(rSAPublicKeyConfig).build();
}
/**
* 微信支付退款能力server
*/
@Bean
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
public RefundService refundService(RSAPublicKeyConfig rSAPublicKeyConfig) {
return new RefundService.Builder().config(rSAPublicKeyConfig).build();
}
/**
* 通知解析器
*/
@Bean
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
public NotificationParser notificationParser(RSAPublicKeyConfig rSAPublicKeyConfig) {
return new NotificationParser(rSAPublicKeyConfig);
}
/**
* 商户私钥
*/
@Bean
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
public PrivateKey privateKey(){
return PemUtil.loadPrivateKeyFromPath(privateKeyPath);
}
}
@@ -0,0 +1,35 @@
package org.jiayunet.constant;
/**
* redisKey
*
* @author zk
*/
public interface PreRedisKeyName {
/**
* 防刷key名
*/
String PREVENT_REPLAY = "preventReplay:ip:";
/**
* 黑名单ip
*/
String BLACK_LIST = "blackList:ip:";
/**
* 单次执行任务Id
*/
String EXECUTE_SINGLE_TASK = "execute:single-task-id:";
/**
* token Key名 前缀
*/
String LOGIN_TOKEN = "login:token:";
/**
* 图片验证码Key名 前缀
*/
String LOGIN_IMAGES_CODE_UUID = "login:images:uuid:";
}
@@ -0,0 +1,111 @@
package org.jiayunet.email;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
/**
* @author zk
*/
/**
* @author zk
*/
@Component
@ConditionalOnProperty(name = "email.status", havingValue = "open")
@Slf4j
public class EmailAbility {
/**
* 发送邮箱账户
*/
@Value("${email.account}")
private String emailAccount;
/**
* 发送邮箱授权码
*/
@Value("${email.authorization}")
private String authorization;
/**
* 获取连接
* @return 连接Session
*/
private Session createSession() {
Assert.isTrue(StringUtils.hasText(authorization) && StringUtils.hasText(authorization),"邮箱账户授权为配置");
//连接配置 勿动 163
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.163.com");
props.put("mail.smtp.port", "25");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
Session session = Session.getInstance(props,new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(emailAccount,authorization);
}
});
log.info("获取邮件发送Session成功");
//session.setDebug(true);
return session;
}
/**
* 发送邮件
* @param emailAdder 收信人地址
* @param subject 邮件主题
* @param content 邮件类容
* @return 是否成功
*/
public boolean send(String emailAdder,String subject, String content) {
//获取session
Session session = createSession();
try {
//构建发送消息
MimeMessage message=new MimeMessage(session);
//设置邮件主题
message.setSubject(subject);
//设置邮件内容
message.setText(content);
//设置发件人
message.setFrom(new InternetAddress(emailAccount));
//设置收件人
message.setRecipient(Message.RecipientType.TO, new InternetAddress(emailAdder));
//发送消息
Transport.send(message);
} catch (MessagingException e) {
e.printStackTrace();
log.error(e.getMessage());
return false;
}
return true;
}
}
@@ -0,0 +1,77 @@
package org.jiayunet.exception;
/**
* @author zk
*/
public class BusinessException extends RuntimeException {
private static final String DELIMITER = " ";
private final BusinessExpCodeOperations expCode;
private Object data;
private String description;
private Boolean replace = true;
public BusinessException(BusinessExpCodeOperations expCode) {
super(String.join(" ", expCode.getCode(), expCode.getMsg()));
this.expCode = expCode;
}
public BusinessException(BusinessExpCodeOperations expCode, String description) {
super(String.join(" ", expCode.getCode(), expCode.getMsg(), description));
this.expCode = expCode;
this.description = description;
}
public BusinessException(BusinessExpCodeOperations expCode, String description, boolean replace) {
super(String.join(" ", expCode.getCode(), expCode.getMsg(), description));
this.expCode = expCode;
this.description = description;
this.replace = replace;
}
public BusinessException(BusinessExpCodeOperations expCode, Throwable throwable) {
super(String.join(" ", expCode.getCode(), expCode.getMsg()), throwable);
this.expCode = expCode;
}
public BusinessException(BusinessExpCodeOperations expCode, String description, Throwable throwable) {
super(String.join(" ", expCode.getCode(), expCode.getMsg(), description), throwable);
this.expCode = expCode;
this.description = description;
}
public BusinessException(BusinessExpCodeOperations expCode, String description, Object data) {
super(String.join(" ", expCode.getCode(), expCode.getMsg(), description));
this.expCode = expCode;
this.description = description;
this.data = data;
}
public BusinessException(BusinessExpCodeOperations expCode, String description, boolean replace, Object data) {
super(String.join(" ", expCode.getCode(), expCode.getMsg(), description));
this.expCode = expCode;
this.description = description;
this.replace = replace;
this.data = data;
}
public String getBusinessCode() {
return this.expCode.getCode();
}
public String getBusinessMsg() {
return this.expCode.getMsg();
}
public Object getData() {
return this.data;
}
public String getDescription() {
return this.description;
}
public Boolean getReplace() {
return this.replace;
}
}
@@ -0,0 +1,50 @@
package org.jiayunet.exception;
/**
* @author zk
*/
public enum BusinessExpCodeEnum implements BusinessExpCodeOperations {
UNKNOWN_ERROR("系统忙不过来了, 稍后再试试"),
PARAMS_INCORRECT("接口参数错误"),
TOKEN_EXPIRED("登录已过期, 请重新登录"),
TOKEN_INCORRECT("访问令牌异常, 请重新登录"),
NOT_LOGGED_IN("未登录"),
USER_EXISTED("用户已存在"),
USER_NOT_EXIST("用户不存在"),
ACCOUNT_EXISTED("账号已存在"),
ACCOUNT_NOT_EXIST("账号不存在"),
ACCOUNT_DISABLED("账号已停用"),
ACCOUNT_UNAUTHORIZED("账号未认证"),
PASSWORD_INCORRECT("密码不正确"),
ACCOUNT_OR_PASSWORD_INCORRECT("账号或密码不正确"),
PHONE_EXISTED("手机号已存在"),
PHONE_NOT_EXIST("手机号不存在"),
BAD_CAPTCHA("验证码不正确"),
BAD_EXPIRED("验证码已失效"),
FAILED_SEND_CAPTCHA("验证码发送失败"),
REPEAT_SEND_CAPTCHA("验证码已发送过, 注意查收"),
TOO_MANY_REQUESTS("请求过多, API限流"),
REPEAT_REQUEST("请勿重复请求"),
PERMISSION_DENIED("无权访问"),
READ_ONLY("不可修改或删除"),
DATA_EXISTED("数据已存在"),
DATA_NOT_EXIST("数据不存在"),
DATA_EXIST_RELATION("数据存在关联"),
DATA_DUPLICATED("数据重复");
private final String msg;
BusinessExpCodeEnum(String msg) {
this.msg = msg;
}
public String getCode() {
return this.name();
}
public String getMsg() {
return this.msg;
}
}
@@ -0,0 +1,10 @@
package org.jiayunet.exception;
/**
* @author zk
*/
public interface BusinessExpCodeOperations {
String getCode();
String getMsg();
}
@@ -0,0 +1,29 @@
package org.jiayunet.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.jiayunet.constant.PreRedisKeyName;
import org.jiayunet.tool.HttpIpTool;
import org.jiayunet.tool.server.RedisServerTool;
/**
* 禁止黑名单
*
* @author zk
*/
@Component
public class BlackListInterceptor implements HandlerInterceptor {
@Autowired
private RedisServerTool redisServerTool;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String key = PreRedisKeyName.BLACK_LIST + HttpIpTool.gteRealIP(request);
return Boolean.FALSE.equals(redisServerTool.hasKey(key));
}
}
@@ -0,0 +1,165 @@
package org.jiayunet.interceptor;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.jiayunet.constant.PreRedisKeyName;
import org.jiayunet.pojo.login.RedisLoginTokenInfo;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.tool.server.RedisServerTool;
/**
* jwt过滤器
*
* @author zk
*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
/**
* 加密密钥
*/
@Value("${app.secret.token:sh.0807.}")
private String secret;
/**
* 在线设备数量
*/
@Value("${app.login.device_online_quantity:5}")
private int deviceOnlineQuantity;
/**
* token过期时间
*/
@Value("${app.login.token.exceed_time:43200}")
private int tokenExceedTime;
/**
* 忽略请求路径
*/
@Value("${app.ignore.urls}")
private String ignoreUrls;
/**
* 全局配置路径
*/
@Value("${server.servlet.context-path}")
private String contextPath;
@Autowired
private RedisServerTool redisServerTool;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 忽略接口放行
if (ifCurrentUrl(ignoreUrls, request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
// 获取token
String token = request.getHeader("Token");
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(request, response);
return;
}
// 验证token
Algorithm algorithm = Algorithm.HMAC256(secret);
DecodedJWT decodedJwt = JWT.require(algorithm).build().verify(token);
Long userId = decodedJwt.getClaim("userId").asLong();
String uuId = decodedJwt.getClaim("uuId").asString();
// 获取redis信息
String redisKey = PreRedisKeyName.LOGIN_TOKEN + userId;
RedisLoginTokenInfo info = null;
info = redisServerTool.get(redisKey, RedisLoginTokenInfo.class);
Assert.notNull(info, "用户未登录");
// 判断登录有效
// 获取登录设备信息
List<RedisLoginTokenInfo.LoginDevice> devices = info.getLoginDevices();
// 过滤过期
devices = devices.stream().filter(v -> v.getLastLoginTime().isBefore(new Date(System.currentTimeMillis() + tokenExceedTime * 1000L).toInstant())).collect(Collectors.toList());
Map<String, RedisLoginTokenInfo.LoginDevice> map = devices.stream().collect(Collectors.toMap(RedisLoginTokenInfo.LoginDevice::getUuId, v -> v));
Assert.isTrue(map.containsKey(uuId), "登录过期");
// 续期时间
map.get(uuId).setLastLoginTime(Instant.now());
info.setLoginDevices(devices);
redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS);
// 登录
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(info.getUserId(), info,
info.getAuthority().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
authenticationToken.setDetails(info);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
MDC.put("userId", userId.toString());
filterChain.doFilter(request, response);
}
/**
* 判断是否为被忽略的路径
*
* @param ignoreUrls 忽略路径
* @param currentUrl 当前路径
* @return 是否忽略当前
*/
private Boolean ifCurrentUrl(String ignoreUrls, String currentUrl) {
String charToRemove = "[*/ ]";
if (!StringUtils.hasText(ignoreUrls)) {
return false;
}
// 处理本次请求路径 剔除全局路径
if (!StringUtils.hasText(currentUrl)) {
return true;
}
if (StringUtils.hasText(contextPath)) {
currentUrl = currentUrl.replaceFirst(contextPath, "").replaceAll(charToRemove, "");
}
// 处理需要被忽略的字段
List<String> urls = Arrays.stream(ignoreUrls.split(",")).map(item -> item.replaceAll(charToRemove, "")).filter(item -> !item.isEmpty()).distinct().collect(Collectors.toList());
for (String str : urls) {
if (currentUrl.startsWith(str)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,32 @@
package org.jiayunet.interceptor;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
/**
* 请求日志
*
* @author zk
*/
@Order(2)
@Slf4j
@Component
public class LoggingOriginalRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String methodName = request.getMethod();
log.info(">> Request Url: {} {}", methodName, request.getRequestURI());
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,78 @@
package org.jiayunet.interceptor;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.jiayunet.constant.PreRedisKeyName;
import org.jiayunet.pojo.interceptor.RedisPreventReplayInfo;
import org.jiayunet.tool.HttpIpTool;
import org.jiayunet.tool.server.RedisServerTool;
/**
* 防止高频重复请求
*
* @author zk
*/
@Component
public class PreventReplayInterceptor implements HandlerInterceptor {
/**
* 是否开启
*/
@Value("${app.prevent_replay.if_open:true}")
private Boolean ifOpen;
/**
* 间隔时间
*/
@Value("${app.prevent_replay.interval_time:10}")
private Integer intervalTime;
/**
* 单位时间次数
*/
@Value("${app.prevent_replay.limit_number:20}")
private Integer limitNumber;
@Autowired
private RedisServerTool redisServerTool;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 关闭时 不执行
if (!ifOpen) {
return true;
}
String redisKeyName = PreRedisKeyName.PREVENT_REPLAY + HttpIpTool.gteRealIP(request);
// 获取redis中的数据
RedisPreventReplayInfo replayInfo = redisServerTool.get(redisKeyName, RedisPreventReplayInfo.class);
replayInfo = Objects.nonNull(replayInfo) ? replayInfo : new RedisPreventReplayInfo();
// 计算时间差 并更新数据
Duration duration = Duration.between(replayInfo.getLastTiming(), Instant.now());
long seconds = duration.getSeconds();
replayInfo = seconds <= intervalTime ? replayInfo : new RedisPreventReplayInfo();
// 增加次数 写回redis
replayInfo.setFrequency(replayInfo.getFrequency() + 1);
redisServerTool.set(redisKeyName, replayInfo);
boolean type = replayInfo.getFrequency() <= limitNumber;
// 加黑
if (!type) {
redisServerTool.set(PreRedisKeyName.BLACK_LIST + request.getRemoteAddr(), "高频请求", 1, TimeUnit.DAYS);
}
return type;
}
}
@@ -0,0 +1,119 @@
package org.jiayunet.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.stereotype.Component;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
@Slf4j
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, org.apache.ibatis.session.ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
@Component
public class SqlLoggerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 获取 MappedStatement
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
BoundSql boundSql = statementHandler.getBoundSql();
// 记录执行开始时间
long startTime = System.currentTimeMillis();
try {
// 执行原始方法
return invocation.proceed();
} finally {
// 计算执行耗时
long costTime = System.currentTimeMillis() - startTime;
// 获取完整的 SQL(包含参数值)
String completeSql = getCompleteSql(mappedStatement.getConfiguration(), boundSql);
// 输出日志
log.info("SQL : {} | Time: {} ms", completeSql, costTime);
}
}
/**
* 获取完整的 SQL 语句(将 ? 替换为实际参数值)
*/
private String getCompleteSql(Configuration configuration, BoundSql boundSql) {
String sql = boundSql.getSql().replaceAll("\\s+", " ").trim();
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
return sql;
}
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 替换 SQL 中的 ? 为实际参数值
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
value = metaObject.getValue(propertyName);
}
String paramValueStr = getParameterValue(value);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValueStr));
}
}
return sql;
}
/**
* 将参数值转换为字符串形式
*/
private String getParameterValue(Object obj) {
if (obj == null) {
return "NULL";
}
if (obj instanceof String) {
return "'" + obj + "'";
}
if (obj instanceof Date) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return "'" + formatter.format(obj) + "'";
}
return obj.toString();
}
}
@@ -0,0 +1,51 @@
package org.jiayunet.interceptor;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
/**
* 请求链路标识链接器
*/
@Component
@Order(1)
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 判断请求头中是否存在链路id
String traceId = request.getHeader("X-Request-ID");
// 不存在 即时生成
if (!StringUtils.hasText(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
// 存在在日志上下文环境中
MDC.put("traceId", traceId);
// 设置到响应头,方便前端追踪
response.setHeader("X-Trace-ID", traceId);
filterChain.doFilter(request, response);
} finally {
// 请求结束时清除
MDC.clear();
}
}
}
@@ -0,0 +1,78 @@
package org.jiayunet.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author zk
*/
public interface CommonMapper<T> extends BaseMapper<T> {
int DEFAULT_BATCH_SIZE = 5000;
int insertBatchSomeColumn(Collection<T> entityList);
int updateBatchMethod(Collection<T> entityList);
default int batchInsert(List<T> entityList) {
return this.batchInsert(entityList, DEFAULT_BATCH_SIZE);
}
default int batchInsert(List<T> entityList, int batchSize) {
if (CollectionUtils.isEmpty(entityList)) {
return 0;
} else {
if (batchSize <= 0) {
batchSize = 5000;
}
List<List<T>> partition = partition(entityList, batchSize);
AtomicInteger total = new AtomicInteger();
partition.forEach((e) -> {
total.addAndGet(this.insertBatchSomeColumn(e));
});
return total.get();
}
}
default int batchUpdate(List<T> entityList) {
return this.batchUpdate(entityList, 5000);
}
default int batchUpdate(List<T> entityList, int batchSize) {
if (CollectionUtils.isEmpty(entityList)) {
return 0;
} else {
if (batchSize <= 0) {
batchSize = 5000;
}
List<List<T>> partition = this.partition(entityList, batchSize);
AtomicInteger total = new AtomicInteger();
partition.forEach((e) -> {
total.addAndGet(updateBatchMethod(e));
});
return total.get();
}
}
default List<List<T>> partition(List<T> entityList, Integer batchSize) {
List<List<T>> partition = new ArrayList<>();
for (int i = 0; i < entityList.size(); i += batchSize) {
int toIndex = Math.min(i + batchSize, entityList.size());
partition.add(entityList.subList(i, toIndex));
}
return partition;
}
}
@@ -0,0 +1,132 @@
package org.jiayunet.oss;
import com.aliyun.oss.HttpMethod;
import com.aliyun.oss.model.GeneratePresignedUrlRequest;
import com.aliyun.oss.model.OSSObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
/**
* 阿里云oss能力
*/
@Slf4j
@Component("ossAbility")
@ConditionalOnProperty(name = "app.oss.service_provider", havingValue = "aliyun")
public class AliOssAbility {
@Autowired
private DefaultCredentialProvider credentialsProvider;
/**
* 简单上传
*
* @param endpoint 接入口地址 https://oss-cn-guangzhou.aliyuncs.co
* @param bucketName Bucket名称
* @param objectPathName Object完整路径名称 /user/iii.log
*/
public void simpleUpload(String endpoint, String bucketName, String objectPathName, InputStream inputStream) throws Exception {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectPathName, inputStream);
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
PutObjectResult result = ossClient.putObject(putObjectRequest);
return;
} catch (Exception e) {
log.error("aliOss 请求封装出现异常 异常信息:{}", e.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
throw new Exception("AliOss上传出现问题");
}
/**
* 简单下载
*
* @param endpoint 接入端口地址
* @param bucketName Bucket名称
* @param objectPathName Object完整路径名称 /user/iii.log
* @return 输入流
*/
public byte[] simpleDownload(String endpoint, String bucketName, String objectPathName) throws Exception {
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
// ossObject包含文件所在的存储空间名称、文件名称、文件元数据以及一个输入流。
OSSObject ossObject = ossClient.getObject(bucketName, objectPathName);
InputStream inputStream = ossObject.getObjectContent();
byte[] fileBytes = IOUtils.toByteArray(inputStream);
inputStream.close();
ossObject.close();
return fileBytes;
} catch (Exception e) {
log.error("aliOss 请求封装出现异常 异常信息:{}", e.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
throw new Exception("AliOss下载出现问题");
}
/**
* 获取代签名的url
*
* @param endpoint 接入端口地址
* @param bucketName Bucket名称
* @param objectPathName Object完整路径名称 /user/iii.log
* @param httpMethod 方法 仅支持 get:获取 put:修改
*/
public String signatureUrl(String endpoint, String bucketName, String objectPathName, int second, HttpMethod httpMethod) throws Exception {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
// 封装生成签名url需要的信息
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectPathName);
// 过期时间 毫秒
generatePresignedUrlRequest.setExpiration(new Date(new Date().getTime() + second * 1000L));
// 设置请求方法
generatePresignedUrlRequest.setMethod(httpMethod);
generatePresignedUrlRequest.setContentType("application/octet-stream");
// 获取
URL url = ossClient.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString().replace("http://", "https://");
} catch (Exception e) {
log.error("aliOss 请求封装出现异常 异常信息:{}", e.getMessage());
} finally {
ossClient.shutdown();
}
throw new Exception("AliOss 获取签名Url出现异常");
}
}
@@ -0,0 +1,44 @@
package org.jiayunet.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 接口数据同一响应格式
*
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UnifiedResponse<T> {
private String code;
private String msg;
private T data;
public static <K> UnifiedResponse<K> normalResponse(K date) {
UnifiedResponse<K> unifiedResponse = new UnifiedResponse<K>();
unifiedResponse.setCode("0");
unifiedResponse.setMsg("正常响应");
unifiedResponse.setData(date);
return unifiedResponse;
}
public static UnifiedResponse<?> fail(String code, String msg) {
UnifiedResponse<?> unifiedResponse = new UnifiedResponse<Object>();
unifiedResponse.setCode(code);
unifiedResponse.setMsg(msg);
return unifiedResponse;
}
public static <T> UnifiedResponse<T> fail(String code, String msg, T data) {
UnifiedResponse<T> unifiedResponse = new UnifiedResponse<T>();
unifiedResponse.setCode(code);
unifiedResponse.setMsg(msg);
unifiedResponse.setData(data);
return unifiedResponse;
}
}
@@ -0,0 +1,22 @@
package org.jiayunet.pojo.interceptor;
import java.time.Instant;
import lombok.Data;
/**
* redis中保存的的防止重复请求的信息
*
* @author zk
*/
@Data
public class RedisPreventReplayInfo {
/**
* 当前时间间隔秒内请求次数
*/
private Integer frequency = 0;
/**
* 最后间隔更新时间
*/
private Instant lastTiming = Instant.now();
}
@@ -0,0 +1,45 @@
package org.jiayunet.pojo.login;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* redis中保存的用户登录信息
*
* @author zk
*/
@Data
public class RedisLoginTokenInfo {
private Long userId;
/**
* 登录设备授权信息
*/
private List<LoginDevice> loginDevices = new ArrayList<>();
/**
* 拥有权限
*/
private List<String> authority = new ArrayList<>();
/**
* 角色
*/
private List<String> role = new ArrayList<>();
@Data
public static class LoginDevice {
/**
* 最后请求时间
*/
private Instant lastLoginTime;
/**
* 单次登陆唯一标识
*/
private String uuId;
/**
* 登录ip
*/
private String loginIp;
}
}
@@ -0,0 +1,92 @@
package org.jiayunet.sms;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import org.springframework.util.Assert;
import org.jiayunet.tool.server.RedisServerTool;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @author zk
*/
@Component("smsAbility")
@ConditionalOnProperty(name = "app.sms.service_provider", havingValue = "aliyun")
@Slf4j
public class AliYunSmsAbility implements ISmsAbility {
@Autowired
private com.aliyun.dysmsapi20170525.Client aliyunSmsClient;
@Autowired
private RedisServerTool redisServerTool;
@Override
public void sendVerificationCode(String phone, VerifyCodeAttribute attribute) {
String redisKey = attribute.getRedisKeyPre()+phone;
// 验证是否发送
boolean existKey = redisServerTool.hasKey(redisKey);
if (!attribute.getCover()){
Assert.isTrue(!existKey,"验证码已发送");
}
// 生成验证码
int randomNumber = new Random().nextInt(1000000);
String number = String.format("%06d", randomNumber);
// 发送验证码
Assert.isTrue(sendVerificationCode(phone,number,attribute.getSignName(),attribute.getTemplateCode()),"短信发送失败,请稍后重试");
// 跟新redisK
redisServerTool.set(redisKey,number,attribute.getEffectiveTime(), TimeUnit.MINUTES);
}
@Override
public String getVerificationCode(String phone, VerifyCodeAttribute attribute) {
return redisServerTool.get(attribute.getRedisKeyPre()+phone,String.class);
}
@Override
public void delVerificationCode(String phone, VerifyCodeAttribute attribute) {
redisServerTool.delete(attribute.getRedisKeyPre()+phone);
}
@Override
public void sendNotification(String phone, String message) {
}
/**
* ali 发送验证码
* @param phone 手机号
* @param validCode 验证码
* @param signName 签名
* @param templateCode 模板
* @return 发送结果
*/
private boolean sendVerificationCode(String phone, String validCode, String signName, String templateCode) {
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest =
new com.aliyun.dysmsapi20170525.models.SendSmsRequest().setSignName(signName).setTemplateCode(templateCode)
.setPhoneNumbers(phone).setTemplateParam("{\"code\":\"" + validCode + "\"}");
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
// 复制代码运行请自行打印 API 的返回值
SendSmsResponse response = aliyunSmsClient.sendSmsWithOptions(sendSmsRequest, runtime);
if (!response.getStatusCode().equals(200)||!response.getBody().getCode().equals("OK")){
log.error("短信发送失败: 失败原因:{}",response.getBody());
return false;
}
return true;
}catch (Throwable e){
log.error("短信发送异常: 异常:{}",e.getMessage());
return false;
}
}
}
@@ -0,0 +1,43 @@
package org.jiayunet.sms;
/**
* 抽象短信发送接口
*
* @author zk
*/
public interface ISmsAbility {
/**
* 发送验证码
* @param phone 手机号
* @param attribute 配置
*/
void sendVerificationCode(String phone, VerifyCodeAttribute attribute);
/**
* 获取验证码
* @param phone 手机号
* @param attribute 配置
*/
String getVerificationCode(String phone, VerifyCodeAttribute attribute);
/**
* 删除验证码
* @param phone 手机号
* @param attribute 配置
*/
void delVerificationCode(String phone, VerifyCodeAttribute attribute);
/**
* 发送通知短信
*
* @param phone 手机号
* @param message 消息
*/
void sendNotification(String phone, String message);
}
@@ -0,0 +1,125 @@
package org.jiayunet.sms;
import lombok.Getter;
import org.springframework.util.Assert;
/**
* 短信发送配配置
*
* @author zk
*/
@Getter
public class VerificationConfig implements VerifyCodeAttribute {
/**
* 模板签名
*/
private final String signName;
/**
* 模板code
*/
private final String templateCode;
/**
* redis 名
*/
private final String redisKeyPre;
/**
* 有效时间 单位分钟
*/
private final Integer effectiveTime;
/**
* 覆盖已存在的redis值
*/
private final Boolean cover;
/**
* 构造器
*/
public static class Builder {
private String signName;
private String templateCode;
private String redisKeyPre;
private Integer effectiveTime;
private Boolean cover;
public VerificationConfig.Builder config(VerifyCodeAttribute attribute){
this.signName = attribute.getSignName();
this.templateCode = attribute.getTemplateCode();
this.redisKeyPre = attribute.getRedisKeyPre();
this.effectiveTime = attribute.getEffectiveTime();
this.cover = attribute.getCover();
return this;
}
public VerificationConfig.Builder config(String signName, String templateCode,String redisKeyPre) {
this.signName = signName;
this.templateCode = templateCode;
this.redisKeyPre = redisKeyPre;
this.effectiveTime = 5;
this.cover = true;
return this;
}
public VerificationConfig.Builder config(String signName, String templateCode,String redisKeyPre,Integer effectiveTime) {
this.signName = signName;
this.templateCode = templateCode;
this.redisKeyPre = redisKeyPre;
this.effectiveTime = effectiveTime;
this.cover = true;
return this;
}
public VerificationConfig.Builder config(String signName, String templateCode,String redisKeyPre,Integer effectiveTime,Boolean cover) {
this.signName = signName;
this.templateCode = templateCode;
this.redisKeyPre = redisKeyPre;
this.effectiveTime = effectiveTime;
this.cover = cover;
return this;
}
public VerificationConfig.Builder redisKeyPre(String redisKeyPre) {
this.redisKeyPre = redisKeyPre;
return this;
}
public VerificationConfig.Builder templateCode(String templateCode) {
this.templateCode = templateCode;
return this;
}
public VerificationConfig.Builder signName(String signName) {
this.signName = signName;
return this;
}
public VerificationConfig.Builder effectiveTime(Integer effectiveTime) {
this.effectiveTime = effectiveTime;
return this;
}
public VerificationConfig.Builder cover(Boolean cover) {
this.cover = cover;
return this;
}
public VerificationConfig build() {
Assert.hasText(signName,"signName不能为空");
Assert.hasText(templateCode,"templateCode不能为空");
Assert.hasText(redisKeyPre,"redisKey不能为空");
Assert.notNull(effectiveTime,"effectiveTime不能为空");
Assert.notNull(cover,"cover不能为空");
return new VerificationConfig(signName, templateCode, redisKeyPre,effectiveTime,cover);
}
}
/**
* 私有化构造
* @param signName 签名
* @param templateCode 短信模板
* @param redisKeyPre redis键名
* @param effectiveTime 有效时间
* @param cover 是否覆盖发送
*/
private VerificationConfig(String signName, String templateCode, String redisKeyPre, Integer effectiveTime, Boolean cover) {
this.signName = signName;
this.templateCode = templateCode;
this.redisKeyPre = redisKeyPre;
this.effectiveTime = effectiveTime;
this.cover = cover;
}
}
@@ -0,0 +1,33 @@
package org.jiayunet.sms;
/**
* @author zk
*/
public interface VerifyCodeAttribute {
/**
* 签名
*
*/
String getSignName();
/**
* 模板code
*/
String getTemplateCode();
/**
* redisKey 名字
*/
String getRedisKeyPre();
/**
* 有效时间
*/
Integer getEffectiveTime();
/**
* 是否覆盖发送
*/
Boolean getCover();
}
@@ -0,0 +1,121 @@
package org.jiayunet.tool;
import org.springframework.util.StringUtils;
import java.util.regex.Pattern;
/**
* @author zk
*/
public class AuthenticTool {
/**
* 验证身份证号码
*
* @param idCard 身份证号
*/
public static Boolean idCard(String idCard) {
// 验证身份证号码
// 定义18位和15位身份证号码的正则表达式模式
String REGEX_18 = "^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$";
String REGEX_15 = "^[1-9]\\d{5}\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}$";
String REGEX = "^(?:" + REGEX_15 + "|" + REGEX_18 + ")$";
Pattern pattern = Pattern.compile(REGEX);
if (!StringUtils.hasText(idCard)){
return false;
}
return pattern.matcher(idCard).matches();
}
/**
* 验证企业社会代码
*
* @param companyAccount 企业社会代码
*/
public static Boolean companyAccount(String companyAccount) {
// 校验码对应表
final String CHECK_CODE_ARRAY = "0123456789ABCDEFGHJKLMNPQRTUWXY"; // 不包含I、O、S、V、Z
// 权重数组
final int[] WEIGHT_ARRAY = {1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28};
if (companyAccount == null || companyAccount.length() != 18) {
System.out.println("请输入18位代码");
return false;
}
// 简单验证:只包含数字和大写字母(根据标准,I、O、S、V、Z 不应出现)
if (!companyAccount.matches("^[0-9A-HJ-NP-RT-UW-Y]{18}$")) {
System.out.println("工商社会代码格式验证错误");
return false;
}
// 计算校验码
int sum = 0;
for (int i = 0; i < 17; i++) {
char c = companyAccount.charAt(i);
int num = CHECK_CODE_ARRAY.indexOf(c);
if (num == -1) { // 如果字符不在对应表中
System.out.println("工商社会代码包含无效字符");
return false;
}
sum += num * WEIGHT_ARRAY[i];
}
// 计算校验码位置
int mod = sum % 31;
char expectedCheckCode;
if (mod == 0) {
expectedCheckCode = CHECK_CODE_ARRAY.charAt(30); // 31 - 31 = 0
} else {
expectedCheckCode = CHECK_CODE_ARRAY.charAt(31 - mod);
}
// 获取实际的校验码
char actualCheckCode = Character.toUpperCase(companyAccount.charAt(17));
// 比较校验码
if (expectedCheckCode != actualCheckCode) {
return false;
}
// 验证通过
return true;
}
/**
* 公司名
*
* @param companyName 公司名
*/
public static Boolean companyName(String companyName) {
String COMPANY_NAME_REGEX = "^[\u4e00-\u9fa5A-Za-z0-9\\-_]{1,150}$";
Pattern COMPANY_NAME_PATTERN = Pattern.compile(COMPANY_NAME_REGEX);
if (!StringUtils.hasText(companyName)){
return false;
}
return COMPANY_NAME_PATTERN.matcher(companyName).matches();
}
public static Boolean chineseName(String name) {
// 简单的2 - 4个汉字人名验证正则表达式
String SIMPLE_NAME_REGEX = "^[\u4e00-\u9fa5]{2,6}$";
// 包含间隔符“·”的中文人名验证正则表达式
String COMPLEX_NAME_REGEX = "^([\u4e00-\u9fa5]{2,6})(·[\u4e00-\u9fa5]{2,4})?$";
Pattern SIMPLE_NAME_PATTERN = Pattern.compile(SIMPLE_NAME_REGEX);
Pattern COMPLEX_NAME_PATTERN = Pattern.compile(COMPLEX_NAME_REGEX);
if (!StringUtils.hasText(name)){
return false;
}
return SIMPLE_NAME_PATTERN.matcher(name).matches()||COMPLEX_NAME_PATTERN.matcher(name).matches();
}
}
@@ -0,0 +1,63 @@
package org.jiayunet.tool;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @author zk
*/
public class HttpIpTool {
/**
* 获取请求的真实ip
*/
public static String gteRealIP() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
Assert.notNull(request, "当前非Http请求,无法获取request");
//存在转发
String realIp = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(realIp)) {
return realIp.split(",")[0].trim();
}
//Nginx 直接链接客户端
realIp = request.getHeader("X-Real-IP");
if (StringUtils.hasText(realIp)) {
return realIp;
}
// 客户端直链服务
return request.getRemoteAddr();
}
/**
* 获取请求真实ip
* @param request http请求头
*/
public static String gteRealIP(HttpServletRequest request) {
//存在转发
String realIp = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(realIp)) {
return realIp.split(",")[0].trim();
}
//Nginx 直接链接客户端
realIp = request.getHeader("X-Real-IP");
if (StringUtils.hasText(realIp)) {
return realIp;
}
// 客户端直链服务
return request.getRemoteAddr();
}
}
@@ -0,0 +1,224 @@
package org.jiayunet.tool;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.validation.constraints.NotNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author zk
*/
public class HttpTool {
public static final ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* 发送请求Body为json的请求
* @param body 参数对象
* @param url 路径
* @return 返回Str
*/
public static String sendJsonPost(@NotNull Object body, @NotNull String url){
return sendJsonPost(body,url,new HashMap<>());
}
/**
* 发送post 请求
* @param body body
* @param url url
* @param headerMap 请求头
* @return
*/
public static String sendJsonPost(@NotNull Object body, @NotNull String url,Map<String,String> headerMap){
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// 创建一个 HttpPost 对象
HttpPost httpPost = new HttpPost(url);
// 创建一个 StringEntity 对象,用于封装 JSON 数据
String jsonStr = objectMapper.writeValueAsString(body);
StringEntity entity = new StringEntity(jsonStr, StandardCharsets.UTF_8);
httpPost.setEntity(entity);
// 遍历 Map 并设置请求头
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
httpPost.setHeader(entry.getKey(), entry.getValue());
}
httpPost.setHeader("Content-Type","application/json");
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String bodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (response.getStatusLine().getStatusCode() == 200) {
return bodyStr;
}
throw new IOException("Http请求出现错误,响应码:" + response.getStatusLine().getStatusCode());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 发送post请求
* @param bodyMap body
* @param url url
* @return
*/
public static String sendFormDataPost(@NotNull Map<String,Object> bodyMap, @NotNull String url){
return sendFormDataPost(bodyMap,url,new HashMap<>());
}
/**
* 发送post请求
* @param bodyMap body
* @param url url
* @param headerMap 请求头
* @return
*/
public static String sendFormDataPost(@NotNull Map<String,Object> bodyMap, @NotNull String url,Map<String,String> headerMap){
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// 创建一个 HttpPost 对象
HttpPost httpPost = new HttpPost(url);
// 创建 MultipartEntityBuilder
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
// 遍历 Map 并设置请求头
for (Map.Entry<String, Object> entry : bodyMap.entrySet()) {
builder.addTextBody(entry.getKey(), entry.getValue().toString(), ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
}
httpPost.setEntity(builder.build());
// 遍历 Map 并设置请求头
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
httpPost.setHeader(entry.getKey(), entry.getValue());
}
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String bodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (response.getStatusLine().getStatusCode() == 200) {
return bodyStr;
}
throw new IOException("Http请求出现错误,响应码:" + response.getStatusLine().getStatusCode());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* post下载文件
* @param body body
* @param url url
*/
public static InputStream downloadPost(@NotNull Object body, @NotNull String url){
return downloadPost(body,url, new HashMap<>());
}
/**
* post下载文件
* @param body body
* @param url url
* @param headerMap 请求头
*/
public static InputStream downloadPost(@NotNull Object body, @NotNull String url,Map<String,String> headerMap){
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// 创建一个 HttpPost 对象
HttpPost httpPost = new HttpPost(url);
// 创建一个 StringEntity 对象,用于封装 JSON 数据
String jsonStr = objectMapper.writeValueAsString(body);
StringEntity entity = new StringEntity(jsonStr, StandardCharsets.UTF_8);
httpPost.setEntity(entity);
// 遍历 Map 并设置请求头
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
httpPost.setHeader(entry.getKey(), entry.getValue());
}
httpPost.setHeader("Content-Type","application/json");
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity responseEntity = response.getEntity();
byte[] byteArray = EntityUtils.toByteArray(response.getEntity());
return new ByteArrayInputStream(byteArray);
}
throw new IOException("Http请求出现错误,响应码:" + response.getStatusLine().getStatusCode());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 发送请求GET请求
* @param paramMap 参数Map
* @param url 路径
* @return 返回Str
*/
public static String sendGet(@NotNull Map<String,Object> paramMap, @NotNull String url){
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// 创建一个 HttpPost 对象
HttpGet httpGet = new HttpGet(url);
//处理请求参数
URIBuilder uriBuilder = new URIBuilder(httpGet.getURI());
for (String key : paramMap.keySet()) {
Object object = paramMap.get(key);
if (Objects.nonNull(object)){
uriBuilder.addParameter(key,object.toString());
}
}
httpGet.setURI(uriBuilder.build());
//发起请求
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
if (response.getStatusLine().getStatusCode() == 200) {
return EntityUtils.toString(response.getEntity(),StandardCharsets.UTF_8);
}
throw new IOException("Http请求出现异常,响应码:" + response.getStatusLine().getStatusCode());
}
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,42 @@
package org.jiayunet.tool;
import org.springframework.beans.BeanUtils;
import org.springframework.util.Assert;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
/**
* 对象操作工具类
*/
public class ObjectTool {
/**
* 复制非null值
* @param source 数据源
* @param target 目标对象
*/
public static void copyNonNullProperties(Object source, Object target) {
Assert.notNull(target, "target must not be null");
Assert.notNull(source, "source must not be null");
PropertyDescriptor[] sourceDescriptors = BeanUtils.getPropertyDescriptors(source.getClass());
for (PropertyDescriptor descriptor : sourceDescriptors) {
try {
Object value = descriptor.getReadMethod().invoke(source);
if (value != null) { // 只复制非空字段
PropertyDescriptor targetDescriptor = BeanUtils.getPropertyDescriptor(target.getClass(), descriptor.getName());
if (targetDescriptor != null && targetDescriptor.getWriteMethod() != null) {
targetDescriptor.getWriteMethod().invoke(target, value);
}
}
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
@@ -0,0 +1,23 @@
package org.jiayunet.tool;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert;
/**
* 获取当前用户信息
*
* @author zk
*/
public class UserSecurityTool {
/**
* 获取当前用户ID
*
* @return id
*/
public static Long getUserId() {
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Assert.notNull(userId, "用户未登录");
return userId;
}
}
@@ -0,0 +1,207 @@
package org.jiayunet.tool;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Random;
import javax.imageio.ImageIO;
/**
* 图片验证码工具类
*
* @author zk
*/
public class VerifyImageCodeUtils {
/**
* 使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
*/
public static final String VERIFY_CODES = "123456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static final Random random = new SecureRandom();
/**
* 使用系统默认字符源生成验证码
*
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize) {
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
*
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources) {
if (sources == null || sources.length() == 0) {
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for (int i = 0; i < verifySize; i++) {
verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
}
return verifyCode.toString();
}
/**
* 输出指定验证码图片流
*
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] {Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA,
Color.ORANGE, Color.PINK, Color.YELLOW};
float[] fractions = new float[colors.length];
for (int i = 0; i < colors.length; i++) {
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h - 4);
// 绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int)(yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < verifySize; i++) {
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1),
(w / verifySize) * i + fontSize / 2, h / 2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double)(period >> 1)
* Math.sin((double)i / (double)period + (6.2831853071795862D * (double)phase) / (double)frames);
g.copyArea(0, i, w1, 1, (int)d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int)d, i, 0, i);
g.drawLine((int)d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double)(period >> 1)
* Math.sin((double)i / (double)period + (6.2831853071795862D * (double)phase) / (double)frames);
g.copyArea(i, 0, 1, h1, 0, (int)d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int)d, i, 0);
g.drawLine(i, (int)d + h1, i, h1);
}
}
}
}
@@ -0,0 +1,121 @@
package org.jiayunet.tool.server;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* redis 操作工具类
*
* @author zk
*/
@Component
public class RedisServerTool {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ObjectMapper objectMapper;
@Value("${spring.application.name}")
private String appName;
public void set(Object key, Object value) {
redisTemplate.opsForValue().set(getKeyName(key), toJsonString(value));
}
public void set(Object key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(getKeyName(key), toJsonString(value), timeout, unit);
}
public <V> V get(Object key, Class<V> vClass) {
String str = redisTemplate.opsForValue().get(getKeyName(key));
return toObject(str, vClass);
}
public <V> V get(Object key, Class<V> vClass,Boolean ifPreKeyName) {
String keyName = ifPreKeyName ? getKeyName(key) : key.toString();
String str = redisTemplate.opsForValue().get(keyName);
return toObject(str, vClass);
}
public <V> Collection<V> getArr(Object key, Class<V> vClass) {
String str = redisTemplate.opsForValue().get(getKeyName(key));
return toArrObject(str, vClass);
}
public boolean hasKey(Object key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(getKeyName(key)));
}
/**
* 获取key的剩余过期时间(秒)
* @param key key
* @return 剩余秒数,-1表示永不过期,-2表示key不存在
*/
public Long getExpire(Object key) {
return redisTemplate.getExpire(getKeyName(key), TimeUnit.SECONDS);
}
public boolean delete(Object key) {
return Boolean.TRUE.equals(redisTemplate.delete(getKeyName(key)));
}
public boolean setNx(Object key, Object value) {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(getKeyName(key), toJsonString(value)));
}
public boolean setNx(Object key, Object value, long timeout, TimeUnit unit) {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(getKeyName(key), toJsonString(value),timeout,unit));
}
private String toJsonString(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private <V> V toObject(String str, Class<V> vClass) {
if (!StringUtils.hasText(str)) {
return null;
}
try {
return objectMapper.readValue(str, vClass);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private <V> Collection<V> toArrObject(String str, Class<V> vClass) {
if (!StringUtils.hasText(str)) {
return List.of();
}
try {
return objectMapper.readValue(str, new TypeReference<Collection<V>>() {
});
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 将key的键名包装上服务名字
* @param key keu
*/
private String getKeyName(Object key){
if (StringUtils.hasText(appName)){
return appName+":"+key.toString();
}
return key.toString();
}
}
@@ -0,0 +1,87 @@
package org.jiayunet.web;
import javax.validation.ValidationException;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.pojo.UnifiedResponse;
import lombok.extern.slf4j.Slf4j;
/**
* @author zk
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionAdvice {
@ExceptionHandler({MethodArgumentTypeMismatchException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifiedResponse<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
ex.printStackTrace();
return this.unifiedExpResponse(BusinessExpCodeEnum.PARAMS_INCORRECT.getCode(), ex.getMessage());
}
@ExceptionHandler({BusinessException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public UnifiedResponse<?> businessCheckExceptionHandler(BusinessException ex) {
ex.printStackTrace();
String description = ex.getDescription();
String msg = ex.getBusinessMsg();
if (StringUtils.hasText(description)) {
if (!ex.getReplace()) {
msg = msg + "[" + description + "]";
} else {
msg = description;
}
}
return UnifiedResponse.fail(ex.getBusinessCode(), msg, ex.getData());
}
@ExceptionHandler({IllegalArgumentException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public UnifiedResponse<?> mybatisPlusException(IllegalArgumentException ex) {
ex.printStackTrace();
return this.unifiedExpResponse("断言异常", ex.getMessage());
}
@ExceptionHandler({ValidationException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public UnifiedResponse<?> validationException(ValidationException ex) {
ex.printStackTrace();
return this.unifiedExpResponse("参数异常拦截", ex.getMessage());
}
@ExceptionHandler({BindException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public UnifiedResponse<?> bindException(BindException ex) {
ex.printStackTrace();
return this.unifiedExpResponse("参数绑定异常", ex.getMessage());
}
@ExceptionHandler({Exception.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public UnifiedResponse<?> lastExceptionHandler(Exception ex) {
BusinessExpCodeEnum unknownError = BusinessExpCodeEnum.UNKNOWN_ERROR;
ex.printStackTrace();
return this.unifiedExpResponse(unknownError.getCode(), unknownError.getMsg());
}
private UnifiedResponse<?> unifiedExpResponse(String code, String msg) {
return UnifiedResponse.fail(code, msg);
}
private UnifiedResponse<?> unifiedExpResponse(String errorFrom, String code, String msg) {
return UnifiedResponse.fail(code, msg);
}
}
@@ -0,0 +1,44 @@
package org.jiayunet.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.jiayunet.pojo.UnifiedResponse;
/**
* 统一返回数据格式
*
* @author zk
*/
@RestControllerAdvice
public class UnifiedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !ResponseEntity.class.isAssignableFrom(returnType.getParameterType());
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 异常
if (body instanceof UnifiedResponse) {
return body;
}
UnifiedResponse<Object> unifiedResponse = UnifiedResponse.normalResponse(body);
return String.class.equals(returnType.getParameterType()) ? objectMapper.writeValueAsString(unifiedResponse) : unifiedResponse;
}
}
@@ -0,0 +1,223 @@
package org.jiayunet.wxPay;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.model.CloseOrderRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByIdRequest;
import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.wxPay.server.model.JsRequestPay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.UUID;
/**
* 微信支付能力
*
* @author zk
*/
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
@Component
@Slf4j
public class WxJsPayAbility {
@Autowired
private JsapiService jsapiService;
@Autowired
private RefundService refundService;
@Autowired
private PrivateKey privateKey;
/**
* 回调域名地址
*/
@Value("${wx_pay.notify_domain}")
private String notifyDomain;
/**
* 商户号
*/
@Value("${wx_pay.merchant_id}")
private String merchantId;
/**
* 小程序id
*/
@Value("${app.wx.app_id}")
private String appId;
/**
* 下单 已默认回调地址
*
* @param paramRequest 请求参数
* @return 预支付订单号
*/
public String prepay(PrepayRequest paramRequest) {
try {
paramRequest.setNotifyUrl(notifyDomain + "/public/wxNotify/pay");
log.info("以默认设置支付回调地址为:{}", paramRequest.getNotifyUrl());
paramRequest.setAppid(appId);
paramRequest.setMchid(merchantId);
PrepayResponse prepay = jsapiService.prepay(paramRequest);
return prepay.getPrepayId();
} catch (Exception e) {
log.error("JSAPI支付下单出现异常,错误信息:{}", e.getMessage());
}
throw new RuntimeException("JSAPI支付下单出现异常");
}
/**
* 关闭订单
*
* @param outTradeNo 外部订单
* @param child 商户id
*/
public void closeOrder(String outTradeNo, String child) {
CloseOrderRequest closeRequest = new CloseOrderRequest();
closeRequest.setMchid(child);
closeRequest.setOutTradeNo(outTradeNo);
try {
jsapiService.closeOrder(closeRequest);
} catch (Exception e) {
log.error("JSAPI支付关闭订单出现异常,outTradeNo:{},mchId:{},错误信息:{}", outTradeNo, child, e.getMessage());
}
throw new RuntimeException("JSAPI支付关闭订单出现异常");
}
/**
* 微信支付订单号查询订单
*
* @param transactionId 交易订单id
* @param mchId 商户id
* @return 交易信息
*/
public Transaction queryOrderById(String transactionId, String mchId) {
QueryOrderByIdRequest request = new QueryOrderByIdRequest();
request.setMchid(mchId);
request.setTransactionId(transactionId);
try {
return jsapiService.queryOrderById(request);
} catch (Exception e) {
log.error("JSAPI微信支付订单号查询订单出现异常,transactionId:{},mchId:{},错误信息:{}", transactionId, mchId, e.getMessage());
}
throw new RuntimeException("JSAPI微信支付订单号查询订单出现异常");
}
/**
* 商户订单号查询订单
*
* @param outTradeNo 外部订单号
* @param mchId 商户号
*/
public Transaction queryOrderByOutTradeNo(String outTradeNo, String mchId) {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
request.setMchid(mchId);
request.setOutTradeNo(outTradeNo);
try {
return jsapiService.queryOrderByOutTradeNo(request);
} catch (Exception e) {
log.error("JSAPI支付关闭订单出现异常,outTradeNo:{},mchId:{},错误信息:{}", outTradeNo, mchId, e.getMessage());
}
throw new RuntimeException("JSAPI支付关闭订单出现异常");
}
/**
* 退款申请
*
* @param request 退款申请参数
*/
public Refund refund(CreateRequest request) {
try {
request.setNotifyUrl(notifyDomain + "/public/wxNotify/refund");
log.info("以默认设置支付回调地址为:{}", request.getNotifyUrl());
return refundService.create(request);
} catch (Exception e) {
log.error("JSAPI创建退款申请出现异常,请求参数:{},错误信息:{}", request, e.getMessage());
}
throw new RuntimeException("JSAPI创建退款申请出现异常");
}
/**
* 查询单笔退款(通过商户退款单号)
*
* @param outRefundNo 外部订单号
*/
public Refund queryByOutRefundNo(String outRefundNo) {
try {
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
request.setOutRefundNo(outRefundNo);
return refundService.queryByOutRefundNo(request);
} catch (Exception e) {
log.error("JSAPI查询单笔退款出现异常,outRefundNo:{},错误信息:{}", outRefundNo, e.getMessage());
}
throw new RuntimeException("JSAPI查询单笔退款出现异常");
}
/**
* 签名
*/
public String createSign(String appId, String timeStamp, String nonceStr, String packageStr) {
try {
// 构造签名串
String signStr = String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageStr);
// 加载私钥
byte[] keyBytes = privateKey.getEncoded();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyFactory.generatePrivate(keySpec);
// 使用 SHA256withRSA 签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(priKey);
signature.update(signStr.getBytes(StandardCharsets.UTF_8));
byte[] signed = signature.sign();
// 返回 Base64 编码的签名
return Base64.getEncoder().encodeToString(signed);
} catch (Exception e) {
throw new RuntimeException("生成签名失败", e);
}
}
/**
* 前端发起js支付需要的参数
* @param prepayId 预订单号
* @return JsRequestPay
*/
public JsRequestPay getJsRequestPay(String prepayId) {
JsRequestPay jsRequestPay = new JsRequestPay();
jsRequestPay.setAppId(appId);
jsRequestPay.setNonceStr(UUID.randomUUID().toString().replace("-", "").substring(0,24));
jsRequestPay.setSignType("RSA");
jsRequestPay.setPackageStr("prepay_id=" + prepayId);
jsRequestPay.setTimeStamp(String.valueOf(System.currentTimeMillis()/1000));
jsRequestPay.setPaySign(createSign(jsRequestPay.getAppId(),jsRequestPay.getTimeStamp(),jsRequestPay.getNonceStr(),jsRequestPay.getPackageStr()));
return jsRequestPay;
}
}
@@ -0,0 +1,65 @@
package org.jiayunet.wxPay;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import com.wechat.pay.java.service.payments.nativepay.model.QueryOrderByOutTradeNoRequest;
import com.wechat.pay.java.service.refund.RefundService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
/**
* @author zk
*/
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
@Component
@Slf4j
public class WxNativePayAbility {
@Autowired
private NativePayService nativePayService;
/**
* 回调域名地址
*/
@Value("${wx_pay.notify_domain}")
private String notifyDomain;
/**
* 预支付
* @param request 请求头
* @return
*/
public String prepay(PrepayRequest request) {
try {
request.setNotifyUrl(notifyDomain+"/public/wxNotify/pay");
log.info("以默认设置支付回调地址为:{}",request.getNotifyUrl());
PrepayResponse prepay = nativePayService.prepay(request);
return prepay.getCodeUrl();
} catch (Exception e) {
log.error("nativePay支付下单出现异常,错误信息:{}", e.getMessage());
}
throw new RuntimeException("nativePay支付下单出现异常");
}
public Transaction queryOrderByOutTradeNo(String outTradeNo, String mchid) {
try {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
request.setMchid(mchid);
request.setOutTradeNo(outTradeNo);
return nativePayService.queryOrderByOutTradeNo(request);
} catch (Exception e) {
log.error("nativePay支付下单出现异常,错误信息:{}", e.getMessage());
}
throw new RuntimeException("nativePay支付下单出现异常");
}
}
@@ -0,0 +1,121 @@
package org.jiayunet.wxPay;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.partnerpayments.nativepay.model.Transaction;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.jiayunet.wxPay.server.model.TransferNotification;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
@RestController
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
@RequestMapping("/public/wxNotify")
@Slf4j
public class WxPayNotifyController {
@Autowired
private NotificationParser parser;
@Autowired
private WxPayNotifyMessageAbstract wxPayNotifyMessageAbstract;
/**
* 支付结果回调
*/
@PostMapping("/pay")
public ResponseEntity<String> pay(HttpServletRequest request) {
return communalRequestHandle(request, Transaction.class);
}
/**
* 退款结果回调
*/
@PostMapping("/refund")
public ResponseEntity<String> refund(HttpServletRequest request) {
return communalRequestHandle(request, RefundNotification.class);
}
/**
* 商家转账回调
*/
@PostMapping("/transfer")
public ResponseEntity<String> transfer(HttpServletRequest request) {
return communalRequestHandle(request, TransferNotification.class);
}
public <T> ResponseEntity<String> communalRequestHandle(HttpServletRequest request, Class<T> tClass) {
String wechatPaySerial = request.getHeader("Wechatpay-Serial");
String wechatpayNonce = request.getHeader("Wechatpay-Nonce");
String wechatSignature = request.getHeader("Wechatpay-Signature");
String wechatTimestamp = request.getHeader("Wechatpay-Timestamp");
String requestBody;
try {
requestBody = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
log.error("微信支付回调获取输入流出错", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("请求体读取失败");
}
if (!StringUtils.hasText(requestBody)) {
log.error("微信支付回调请求体为空");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("请求体为空");
}
// 打印日志
log.info("收到微信支付回调数据, body数据:{}",requestBody);
log.info("收到微信支付回调数据, 请求头wechatPaySerial数据:{}",wechatPaySerial);
log.info("收到微信支付回调数据, 请求头wechatpayNonce数据:{}",wechatpayNonce);
log.info("收到微信支付回调数据, 请求头wechatSignature数据:{}",wechatSignature);
log.info("收到微信支付回调数据, 请求头wechatTimestamp数据:{}",wechatTimestamp);
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(wechatPaySerial)
.nonce(wechatpayNonce)
.signature(wechatSignature)
.timestamp(wechatTimestamp)
.body(requestBody)
.build();
try {
T parse = parser.parse(requestParam, tClass);
// 支付回调
if (Objects.equals(tClass, Transaction.class)) {
wxPayNotifyMessageAbstract.payMessageHandle((Transaction) parse);
}
// 退款回调
if (tClass == RefundNotification.class) {
wxPayNotifyMessageAbstract.refundMessageHandle((RefundNotification) parse);
}
// 商户转账回调
if (tClass == TransferNotification.class){
wxPayNotifyMessageAbstract.transferMessageHandle((TransferNotification) parse);
}
} catch (ValidationException e) {
log.error("微信退回调数据验签错误, 请求体: {}, 请求头: {}", requestBody, request.getHeaderNames(), e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("验签失败");
} catch (Exception e) {
log.error("微信回调业务错误", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("内部错误");
}
return ResponseEntity.ok("200");
}
}
@@ -0,0 +1,36 @@
package org.jiayunet.wxPay;
import com.wechat.pay.java.service.partnerpayments.nativepay.model.Transaction;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import org.jiayunet.wxPay.server.model.TransferNotification;
/**
* 微信支付回调消息处理接口
* 使用前需要实集成接口 实现方法
* @author zk
*/
public interface WxPayNotifyMessageAbstract {
/**
* 支付结果回调消息处理
* @param transaction 支付回调消息
*/
default void payMessageHandle(Transaction transaction){
System.out.println("微信支付的支付结果回调消息未处理");
}
/**
* 退款回调消息
* @param refundNotification 退款回调
*/
default void refundMessageHandle(RefundNotification refundNotification){
System.out.println("微信支付的退款回调消息未处理");
}
/**
* 商户支付回调消息
* @param refundNotification 退款回调
*/
default void transferMessageHandle(TransferNotification refundNotification){
System.out.println("微信支付的商家转账回调未处理");
}
}
@@ -0,0 +1,64 @@
package org.jiayunet.wxPay;
import com.wechat.pay.java.core.exception.WechatPayException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.jiayunet.wxPay.server.TransferServer;
import org.jiayunet.wxPay.server.model.BillNoQueryRequest;
import org.jiayunet.wxPay.server.model.TransferBillsRequest;
import org.jiayunet.wxPay.server.model.TransferBillsResponse;
import org.jiayunet.wxPay.server.model.TransferResultResponse;
/**
* @author zk
*/
@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open")
@Component
@Slf4j
public class WxTransferPayAbility {
@Autowired
private TransferServer transferServer;
/**
* 回调域名地址
*/
@Value("${wx_pay.notify_domain}")
private String notifyDomain;
/**
* 发起转账
* @param request 请求参数
*/
public TransferBillsResponse transferBills(TransferBillsRequest request) {
try {
request.setNotifyUrl(notifyDomain+"/public/wxNotify/transfer");
log.info("以默认设置回调地址为:{}",request.getNotifyUrl());
return transferServer.transferBills(request);
} catch (WechatPayException e) {
log.error("TransferPay发起转账出现异常,错误信息:{}", e.getMessage());
throw new IllegalArgumentException(e.getMessage());
}catch (Exception e){
log.error("TransferPay发起转账出现异常,错误信息:{}", e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
/**
* 转账订单查询
* @param request 请求参数
*/
public TransferResultResponse billNoQuery(BillNoQueryRequest request) {
try {
return transferServer.billNoQuery(request);
} catch (Exception e){
log.error("TransferPay查询账出现异常,错误信息:{}", e.getMessage());
throw new RuntimeException("TransferPay查询账出现异常");
}
}
}
@@ -0,0 +1,172 @@
package org.jiayunet.wxPay.server;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.cipher.PrivacyDecryptor;
import com.wechat.pay.java.core.cipher.PrivacyEncryptor;
import com.wechat.pay.java.core.http.Constant;
import com.wechat.pay.java.core.http.DefaultHttpClientBuilder;
import com.wechat.pay.java.core.http.HostName;
import com.wechat.pay.java.core.http.HttpClient;
import com.wechat.pay.java.core.http.HttpHeaders;
import com.wechat.pay.java.core.http.HttpMethod;
import com.wechat.pay.java.core.http.HttpRequest;
import com.wechat.pay.java.core.http.HttpResponse;
import com.wechat.pay.java.core.http.JsonRequestBody;
import com.wechat.pay.java.core.http.MediaType;
import com.wechat.pay.java.core.http.RequestBody;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.jiayunet.wxPay.server.model.BillNoQueryRequest;
import org.jiayunet.wxPay.server.model.CancelRequest;
import org.jiayunet.wxPay.server.model.CancelResponse;
import org.jiayunet.wxPay.server.model.TransferBillsRequest;
import org.jiayunet.wxPay.server.model.TransferBillsResponse;
import org.jiayunet.wxPay.server.model.TransferResultResponse;
import static com.wechat.pay.java.core.http.UrlEncoder.urlEncode;
import static com.wechat.pay.java.core.util.GsonUtil.toJson;
/**
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class TransferServer {
private HttpClient httpClient;
private HostName hostName;
private PrivacyEncryptor encryptor;
private PrivacyDecryptor decryptor;
/** TransferServer构造器 */
public static class Builder {
private HttpClient httpClient;
private HostName hostName;
private PrivacyEncryptor encryptor;
private PrivacyDecryptor decryptor;
public TransferServer.Builder config(Config config) {
this.httpClient = new DefaultHttpClientBuilder().config(config).build();
this.encryptor = config.createEncryptor();
this.decryptor = config.createDecryptor();
return this;
}
public TransferServer.Builder hostName(HostName hostName) {
this.hostName = hostName;
return this;
}
public TransferServer.Builder httpClient(HttpClient httpClient) {
this.httpClient = httpClient;
return this;
}
public TransferServer.Builder encryptor(PrivacyEncryptor encryptor) {
this.encryptor = encryptor;
return this;
}
public TransferServer.Builder decryptor(PrivacyDecryptor decryptor) {
this.decryptor = decryptor;
return this;
}
public TransferServer build() {
return new TransferServer(httpClient, hostName, encryptor, decryptor);
}
}
/**
* 发起转账
* @param request 请求参数
*/
public TransferBillsResponse transferBills(TransferBillsRequest request) {
String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills";
if (StringUtils.hasText(request.getUserName())){
request.setUserName(encryptor.encrypt(request.getUserName()));
if (request.getTransferAmount()<=30){
log.info("微信商户转账0.3元一下不支持实名验证,以默认设置为空");
request.setUserName(null);
}
}
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue());
headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue());
headers.addHeader(Constant.WECHAT_PAY_SERIAL, encryptor.getWechatpaySerial());
HttpRequest httpRequest =
new HttpRequest.Builder()
.httpMethod(HttpMethod.POST)
.url(requestPath)
.headers(headers)
.body(createRequestBody(request))
.build();
HttpResponse<TransferBillsResponse> httpResponse = httpClient.execute(httpRequest, TransferBillsResponse.class);
return httpResponse.getServiceResponse();
}
/**
* 撤销转账
* @param request 请求参数
*/
public CancelResponse cancel(CancelRequest request) {
String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel";
// 添加 path param
requestPath = requestPath.replace("{" + "out_bill_no" + "}", urlEncode(request.getOutBillNo()));
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue());
headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue());
HttpRequest httpRequest =
new HttpRequest.Builder()
.httpMethod(HttpMethod.POST)
.url(requestPath)
.headers(headers)
.body(createRequestBody(request))
.build();
HttpResponse<CancelResponse> httpResponse = httpClient.execute(httpRequest, CancelResponse.class);
return httpResponse.getServiceResponse();
}
/**
* 订单查询
* @param request 参数
*/
public TransferResultResponse billNoQuery(BillNoQueryRequest request) {
String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}";
// 添加 path param
requestPath = requestPath.replace("{" + "transfer_bill_no" + "}", urlEncode(request.getTransferBillNo()));
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue());
headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue());
HttpRequest httpRequest =
new HttpRequest.Builder()
.httpMethod(HttpMethod.GET)
.url(requestPath)
.headers(headers)
.build();
HttpResponse<TransferResultResponse> httpResponse = httpClient.execute(httpRequest, TransferResultResponse.class);
return httpResponse.getServiceResponse();
}
private RequestBody createRequestBody(Object request) {
return new JsonRequestBody.Builder().body(toJson(request)).build();
}
}
@@ -0,0 +1,16 @@
package org.jiayunet.wxPay.server.model;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
/**
* @author zk
*/
@Data
public class BillNoQueryRequest {
/**
* 外部订单号
*/
@SerializedName("transfer_bill_no")
private String transferBillNo;
}
@@ -0,0 +1,23 @@
package org.jiayunet.wxPay.server.model;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CancelRequest {
/**
* 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
*/
@SerializedName("outBillNo")
@Expose(serialize = false)
private String outBillNo;
}
@@ -0,0 +1,52 @@
package org.jiayunet.wxPay.server.model;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CancelResponse {
/**
* 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
**/
@SerializedName("out_bill_no")
private String outBillNo;
/**
* 【微信转账单号】 商家转账订单的主键,唯一定义此资源的标识
*/
@SerializedName("transfer_bill_no")
private String transferBillNo;
/**
* 【单据状态】 CANCELING: 撤销中;CANCELLED:已撤销
*/
@SerializedName("state")
private CancelState state;
/**
* 【最后一次单据状态变更时间】 按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
*/
@SerializedName("update_time")
private String update_time;
public enum CancelState{
/**
* 撤销中
*/
@SerializedName("CANCELING")
CANCELING,
/**
* 已撤销
*/
@SerializedName("CANCELLED")
CANCELLED
}
}
@@ -0,0 +1,33 @@
package org.jiayunet.wxPay.server.model;
import lombok.Data;
@Data
public class JsRequestPay {
/**
* 填写下单时传入的appid,且必需与当前实际调起支付的公众号appid一致,否则无法调起支付。
*/
private String appId;
/**
* Unix 时间戳,是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数。
*/
private String timeStamp;
/**
* 随机字符串,不长于32位。该值建议使用随机数算法生成。
*/
private String nonceStr;
/**
* 订单详情扩展字符串,JSAPI下单接口返回的prepay_id参数值,提交格式如:prepay_id=***。
*/
private String packageStr;
/**
* 签名类型,固定填RSA。
*/
private String signType;
/**
* 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值 注意:取值RSA格式。
*/
private String paySign;
}
@@ -0,0 +1,73 @@
package org.jiayunet.wxPay.server.model;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.List;
/**
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class TransferBillsRequest {
/** 公众号ID 说明:公众号ID */
@SerializedName("appid")
private String appid;
/** 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 */
@SerializedName("out_bill_no")
private String outBillNo;
/**【转账场景ID】 该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。如:1001-现金营销*/
@SerializedName("transfer_scene_id")
private String transferSceneId;
/**【收款用户OpenID】 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见参数说明。**/
@SerializedName("openid")
private String openid;
/**【收款用户姓名】 收款方真实姓名。需要加密传入,支持标准RSA算法和国密算法,公钥由微信侧提供。
转账金额 >= 2,000元时,该笔明细必须填写
若商户传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致,并提供电子回单**/
@SerializedName("user_name")
private String userName;
/**【转账金额】 转账金额单位为“分”。*/
@SerializedName("transfer_amount")
private Integer transferAmount;
/**【转账备注】 转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符**/
@SerializedName("transfer_remark")
private String transferRemark;
/**【通知地址】 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数**/
@SerializedName("notify_url")
private String notifyUrl;
/**【用户收款感知】 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求,可在本字段传入。各场景展示的默认内容和支持传入的内容,可查看产品文档了解**/
@SerializedName("user_recv_perception")
private String userRecvPerception;
@SerializedName("transfer_scene_report_infos")
private List<TransferSceneReportInfo> transferSceneReportInfos;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class TransferSceneReportInfo{
/**【信息类型】 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照转账场景报备信息字段说明传参。**/
@SerializedName("info_type")
private String infoType;
/**【信息内容】 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照转账场景报备信息字段说明传参。**/
@SerializedName("info_content")
private String infoContent;
}
}
@@ -0,0 +1,94 @@
package org.jiayunet.wxPay.server.model;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransferBillsResponse {
/**
* 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
**/
@SerializedName("out_bill_no")
private String outBillNo;
/**
* 【微信转账单号】 微信转账单号,微信商家转账系统返回的唯一标识
**/
@SerializedName("transfer_bill_no")
private String transferBillNo;
/**
* 【单据创建时间】 单据受理成功时返回,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
**/
@SerializedName("create_time")
private String createTime;
/**
* 【单据状态】 商家转账订单状态
**/
@SerializedName("state")
private StateEnum state;
/**
* 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因
**/
@SerializedName("fail_reason")
private String failReason;
/**
* 【跳转领取页面的package信息】 跳转微信支付收款页的package信息,APP调起用户确认收款或者JSAPI调起用户确认收款 时需要使用的参数。
* 单据创建后,用户24小时内不领取将过期关闭,建议拉起用户确认收款页面前,先查单据状态:如单据状态为待收款用户确认,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。
*/
@SerializedName("package_info")
private String packageInfo;
/**
* tradeState
*/
public static enum StateEnum {
/**
* 转账已受理
*/
@SerializedName("ACCEPTED")
ACCEPTED,
/**
* 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试
*/
@SerializedName("PROCESSING")
PROCESSING,
/**
* 待收款用户确认,可拉起微信收款确认页面进行收款确认
*/
@SerializedName("WAIT_USER_CONFIRM")
WAIT_USER_CONFIRM,
/**
* 转账中,可拉起微信收款确认页面再次重试确认收款
*/
@SerializedName("TRANSFERING")
TRANSFERING,
/**
* 转账成功
*/
@SerializedName("SUCCESS")
SUCCESS,
/**
* 转账已受理
*/
@SerializedName("FAIL")
FAIL,
/**
* 转账已受理
*/
@SerializedName("CANCELLED")
CANCELLED,
}
}
@@ -0,0 +1,93 @@
package org.jiayunet.wxPay.server.model;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 商户支付回调
*
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransferNotification {
/**
* 【商户单号】商户系统内部的商家单号,在商户系统内部唯一
*/
@SerializedName("out_bill_no")
private String outBillNo;
/**
* 【商家转账订单号】微信单号,微信商家转账系统返回的唯一标识
*/
@SerializedName("transfer_bill_no")
private String transferBillNo;
/**
* 【单据状态】微信单号,微信商家转账系统返回的唯一标识
*/
@SerializedName("state")
private TransferState state;
/**
* 【商户号】微信支付分配的商户号
*/
@SerializedName("mch_id")
private String mchId;
/**
* 【转账金额】转账总金额,单位为“分”
*/
@SerializedName("transfer_amount")
private String transferAmount ;
/**
* 【收款用户OpenID】用户在商户appid下的唯一标识
*/
@SerializedName("openid")
private String openid ;
/**
* 【失败原因】单已失败或者已退资金时,会返回订单失败原因
*/
@SerializedName("fail_reason")
private String failReason ;
/**
* 【单据创建时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONEyyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
*/
@SerializedName("create_time")
private String createTime ;
/**
* 【最后一次状态变更时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONEyyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
*/
@SerializedName("update_time")
private String updateTime;
public static enum TransferState {
@SerializedName("ACCEPTED")
ACCEPTED,
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("WAIT_USER_CONFIRM")
WAIT_USER_CONFIRM,
@SerializedName("TRANSFERING")
TRANSFERING,
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("FAIL")
FAIL,
@SerializedName("CANCELING")
CANCELING,
@SerializedName("CANCELLED")
CANCELLED
}
}
@@ -0,0 +1,116 @@
package org.jiayunet.wxPay.server.model;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
/**
* @author zk
*/
@Data
public class TransferResultResponse {
/**
* 微信支付分配的商户号
**/
@SerializedName("mch_id")
private String mchId;
/**
* 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
*/
@SerializedName("out_bill_no")
private String outBillNo;
/**
* 【商家转账订单号】 商家转账订单的主键,唯一定义此资源的标识
*/
@SerializedName("transfer_bill_no")
private String transferBillNo;
/**
* 【商户AppID】 是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处,可以填写这四种类型中的任意一种APPID,但请确保该appid与商户号有绑定关系。详见:普通商户模式开发必要参数说明。
*/
@SerializedName("appid")
private String appid;
/**
* 【单据状态】
*/
@SerializedName("state")
private StateEnum state;
/**
* 【转账金额】 转账金额单位为“分”
*/
@SerializedName("transfer_amount")
private Integer transferAmount;
/**
* 【转账备注】 单条转账备注(微信用户会收到该备注),UTF8编码,最多允许32个字符
*/
@SerializedName("transfer_remark")
private String transferRemark;
/**
* 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因
*/
@SerializedName("fail_reason")
private String failReason;
/**
* 【收款用户OpenID】 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见参数说明。
*/
@SerializedName("openid")
private String openid;
/**
* 【收款用户姓名】 收款方真实姓名。支持标准RSA算法和国密算法,公钥由微信侧提供转账金额 >= 2,000元时,该笔明细必须填写若商户传入收款用户姓名,微信支付会校验用户OpenID与姓名是否一致,并提供电子回单
*/
@SerializedName("user_name")
private String userName;
/**
* 【单据创建时间】 单据受理成功时返回,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
*/
@SerializedName("create_time")
private String createTime;
/**
* 【最后一次状态变更时间】 单据最后更新时间,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
*/
@SerializedName("update_time")
private String update_time;
/**
* tradeState
*/
public enum StateEnum {
/**
* 转账已受理
*/
@SerializedName("ACCEPTED")
ACCEPTED,
/**
* 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试
*/
@SerializedName("PROCESSING")
PROCESSING,
/**
* 待收款用户确认,可拉起微信收款确认页面进行收款确认
*/
@SerializedName("WAIT_USER_CONFIRM")
WAIT_USER_CONFIRM,
/**
* 转账中,可拉起微信收款确认页面再次重试确认收款
*/
@SerializedName("TRANSFERING")
TRANSFERING,
/**
* 转账成功
*/
@SerializedName("SUCCESS")
SUCCESS,
/**
* 转账已受理
*/
@SerializedName("FAIL")
FAIL,
/**
* 转账已受理
*/
@SerializedName("CANCELLED")
CANCELLED,
}
}
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 公共属性 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%X{userId}] [%thread] %-5level %logger{36} - %msg%n"/>
<property name="CONSOLE_LOG_PATTERN" value="%cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) %magenta([%X{traceId}]) %blue([%X{userId}]) %green([%thread]) %highlight(%-5level) %yellow(%logger{36}) - %msg%n"/>
<property name="LOG_PATH" value="logs"/>
<property name="LOG_FILE" value="${LOG_PATH}/application.log"/>
<!-- 控制台输出(带颜色) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 本地(local)环境,仅使用控制台日志 -->
<springProfile name="local">
<logger name="SQL_LOGGER" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 非本地环境,输出到文件和控制台 -->
<springProfile name="!local">
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/application-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>14</maxHistory>
</rollingPolicy>
</appender>
<!-- SQL日志单独文件 -->
<appender name="SQL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/sql.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/sql-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>14</maxHistory>
</rollingPolicy>
</appender>
<logger name="SQL_LOGGER" level="DEBUG" additivity="false">
<appender-ref ref="SQL_FILE"/>
<appender-ref ref="CONSOLE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>