From 6dd6dc98cd07d3577c88f722fcfd82bfa75752f2 Mon Sep 17 00:00:00 2001 From: Bunny <1319900154@qq.com> Date: Sun, 28 Jul 2024 03:13:27 +0800 Subject: [PATCH] :sparkles: feat(init): init --- .gitignore | 33 ++ common/common-generator/pom.xml | 50 +++ .../cn/bunny/common/generator/NewCodeGet.java | 83 +++++ .../cn/bunny/common/generator/OldCodeGet.java | 57 +++ common/pom.xml | 58 +++ common/service-utils/pom.xml | 50 +++ .../common/service/config/Knife4jConfig.java | 39 ++ .../config/MyBatisPlusFieldConfig.java | 39 ++ .../service/config/MybatisPlusConfig.java | 36 ++ .../service/config/RedisConfiguration.java | 98 +++++ .../service/config/WebMvcConfiguration.java | 42 +++ .../common/service/context/BaseContext.java | 52 +++ .../service/exception/BunnyException.java | 33 ++ .../exception/GlobalExceptionHandler.java | 82 ++++ .../interceptor/UserTokenInterceptor.java | 42 +++ .../bunny/common/service/utils/EmptyUtil.java | 21 ++ .../bunny/common/service/utils/FileUtil.java | 31 ++ .../bunny/common/service/utils/HttpUtil.java | 206 +++++++++++ .../bunny/common/service/utils/JwtHelper.java | 350 ++++++++++++++++++ .../service/utils/ResponseHandlerUtil.java | 12 + .../common/service/utils/ResponseUtil.java | 26 ++ dao/pom.xml | 43 +++ .../main/java/cn/bunny/dto/user/LoginDto.java | 19 + .../java/cn/bunny/entity/base/BaseEntity.java | 41 ++ .../cn/bunny/entity/system/user/User.java | 61 +++ .../java/cn/bunny/pojo/result/Result.java | 173 +++++++++ .../cn/bunny/pojo/result/ResultCodeEnum.java | 54 +++ .../result/constant/ExceptionConstant.java | 52 +++ .../constant/LocalDateTimeConstant.java | 11 + .../pojo/result/constant/UserConstant.java | 8 + .../cn/bunny/pojo/tree/AbstractTreeNode.java | 11 + .../java/cn/bunny/pojo/tree/TreeBuilder.java | 29 ++ .../java/cn/bunny/vo/page/PageResult.java | 27 ++ .../cn/bunny/vo/system/user/UserInfoVo.java | 43 +++ pom.xml | 182 +++++++++ service/Dockerfile | 21 ++ service/pom.xml | 117 ++++++ .../cn/bunny/service/ServiceApplication.java | 23 ++ .../service/controller/IndexController.java | 18 + .../service/controller/WebController.java | 31 ++ .../bunny/service/service/LoginService.java | 10 + .../service/impl/LoginServiceImpl.java | 21 ++ .../src/main/resources/application-dev.yml | 13 + .../src/main/resources/application-prod.yml | 13 + service/src/main/resources/application.yml | 59 +++ service/src/main/resources/banner.txt | 16 + service/src/main/resources/static/favicon.ico | Bin 0 -> 17014 bytes 47 files changed, 2536 insertions(+) create mode 100644 .gitignore create mode 100644 common/common-generator/pom.xml create mode 100644 common/common-generator/src/main/java/cn/bunny/common/generator/NewCodeGet.java create mode 100644 common/common-generator/src/main/java/cn/bunny/common/generator/OldCodeGet.java create mode 100644 common/pom.xml create mode 100644 common/service-utils/pom.xml create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/config/Knife4jConfig.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/config/MyBatisPlusFieldConfig.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/config/MybatisPlusConfig.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/config/RedisConfiguration.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/config/WebMvcConfiguration.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/context/BaseContext.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/exception/BunnyException.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/exception/GlobalExceptionHandler.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/interceptor/UserTokenInterceptor.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/utils/EmptyUtil.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/utils/FileUtil.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/utils/HttpUtil.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/utils/JwtHelper.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseHandlerUtil.java create mode 100644 common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseUtil.java create mode 100644 dao/pom.xml create mode 100644 dao/src/main/java/cn/bunny/dto/user/LoginDto.java create mode 100644 dao/src/main/java/cn/bunny/entity/base/BaseEntity.java create mode 100644 dao/src/main/java/cn/bunny/entity/system/user/User.java create mode 100644 dao/src/main/java/cn/bunny/pojo/result/Result.java create mode 100644 dao/src/main/java/cn/bunny/pojo/result/ResultCodeEnum.java create mode 100644 dao/src/main/java/cn/bunny/pojo/result/constant/ExceptionConstant.java create mode 100644 dao/src/main/java/cn/bunny/pojo/result/constant/LocalDateTimeConstant.java create mode 100644 dao/src/main/java/cn/bunny/pojo/result/constant/UserConstant.java create mode 100644 dao/src/main/java/cn/bunny/pojo/tree/AbstractTreeNode.java create mode 100644 dao/src/main/java/cn/bunny/pojo/tree/TreeBuilder.java create mode 100644 dao/src/main/java/cn/bunny/vo/page/PageResult.java create mode 100644 dao/src/main/java/cn/bunny/vo/system/user/UserInfoVo.java create mode 100644 pom.xml create mode 100644 service/Dockerfile create mode 100644 service/pom.xml create mode 100644 service/src/main/java/cn/bunny/service/ServiceApplication.java create mode 100644 service/src/main/java/cn/bunny/service/controller/IndexController.java create mode 100644 service/src/main/java/cn/bunny/service/controller/WebController.java create mode 100644 service/src/main/java/cn/bunny/service/service/LoginService.java create mode 100644 service/src/main/java/cn/bunny/service/service/impl/LoginServiceImpl.java create mode 100644 service/src/main/resources/application-dev.yml create mode 100644 service/src/main/resources/application-prod.yml create mode 100644 service/src/main/resources/application.yml create mode 100644 service/src/main/resources/banner.txt create mode 100644 service/src/main/resources/static/favicon.ico diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..359dca5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +logs/ +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/common/common-generator/pom.xml b/common/common-generator/pom.xml new file mode 100644 index 0000000..8a81ce5 --- /dev/null +++ b/common/common-generator/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + cn.bunny + common + 0.0.1-SNAPSHOT + + + common-generator + jar + + common-utils + https://maven.apache.org + + + UTF-8 + + + + + javax.xml.bind + jaxb-api + 2.1 + + + + com.baomidou + mybatis-plus-generator + 3.5.6 + + + org.apache.velocity + velocity-engine-core + 2.3 + + + + + + + + + + + + + + + diff --git a/common/common-generator/src/main/java/cn/bunny/common/generator/NewCodeGet.java b/common/common-generator/src/main/java/cn/bunny/common/generator/NewCodeGet.java new file mode 100644 index 0000000..27d0fe9 --- /dev/null +++ b/common/common-generator/src/main/java/cn/bunny/common/generator/NewCodeGet.java @@ -0,0 +1,83 @@ +package cn.bunny.common.generator; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collections; + +public class NewCodeGet { + // 数据连接 + public static final String sqlHost = "jdbc:mysql://106.15.251.123:3305/bunny_docs?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true"; + // 作者名称 + public static final String author = "Bunny"; + // 公共路径 + public static final String outputDir = "F:\\web项目\\PC\\BunnyNote\\BunnyBBS-server\\service\\service-web"; + // 实体类名称 + public static final String entity = "Bunny"; + + public static void main(String[] args) { + Generation("article"); + } + + /** + * 根据表名生成相应结构代码 + * + * @param tableName 表名 + */ + public static void Generation(String... tableName) { + // TODO 修改数据库路径、账户、密码 + FastAutoGenerator.create(sqlHost, "root", "02120212") + .globalConfig(builder -> { + // 添加作者名称 + builder.author(author) + // 启用swagger + .enableSwagger() + // 指定输出目录 + .outputDir(outputDir + "/src/main/java"); + }) + .packageConfig(builder -> { + builder.entity(entity)// 实体类包名 + // TODO 父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名 + .parent("cn.bunny.service.web") + .controller("controller")// 控制层包名 + .mapper("mapper")// mapper层包名 + .service("service")// service层包名 + .serviceImpl("service.impl")// service实现类包名 + // 自定义mapper.xml文件输出目录 + .pathInfo(Collections.singletonMap(OutputFile.xml, outputDir + "/src/main/resources/mapper")); + }) + .strategyConfig(builder -> { + // 设置要生成的表名 + builder.addInclude(tableName) + //.addTablePrefix("sys_")// TODO 设置表前缀过滤 + .entityBuilder() + .enableLombok() + .enableChainModel() + .naming(NamingStrategy.underline_to_camel)// 数据表映射实体命名策略:默认下划线转驼峰underline_to_camel + .columnNaming(NamingStrategy.underline_to_camel)// 表字段映射实体属性命名规则:默认null,不指定按照naming执行 + .idType(IdType.AUTO)// TODO 添加全局主键类型 + .formatFileName("%s")// 格式化实体名称,%s取消首字母I, + .mapperBuilder() + .mapperAnnotation(Mapper.class)// 开启mapper注解 + .enableBaseResultMap()// 启用xml文件中的BaseResultMap 生成 + .enableBaseColumnList()// 启用xml文件中的BaseColumnList + .formatMapperFileName("%sMapper")// 格式化Dao类名称 + .formatXmlFileName("%sMapper")// 格式化xml文件名称 + .serviceBuilder() + .formatServiceFileName("%sService")// 格式化 service 接口文件名称 + .formatServiceImplFileName("%sServiceImpl")// 格式化 service 接口文件名称 + .controllerBuilder() + .enableRestStyle(); + }) + // .injectionConfig(consumer -> { + // Map customFile = new HashMap<>(); + // // 配置DTO(需要的话)但是需要有能配置Dto的模板引擎,比如freemarker,但是这里我们用的VelocityEngine,因此不多作介绍 + // customFile.put(outputDir, "/src/main/resources/templates/entityDTO.java.ftl"); + // consumer.customFile(customFile); + // }) + .execute(); + } +} diff --git a/common/common-generator/src/main/java/cn/bunny/common/generator/OldCodeGet.java b/common/common-generator/src/main/java/cn/bunny/common/generator/OldCodeGet.java new file mode 100644 index 0000000..d43819c --- /dev/null +++ b/common/common-generator/src/main/java/cn/bunny/common/generator/OldCodeGet.java @@ -0,0 +1,57 @@ +package cn.bunny.common.generator; + +public class OldCodeGet { + public static void main(String[] args) { + // // 1、创建代码生成器 + // AutoGenerator mpg = new AutoGenerator(); + // + // // 2、全局配置 + // // 全局配置 + // GlobalConfig gc = new GlobalConfig(); + // // TODO 需要修改路径名称 + // gc.setOutputDir("F:\\web项目\\Bunny-Cli\\Java\\java-template\\service" + "/src/main/java"); + // gc.setServiceName("%sService"); // 去掉Service接口的首字母I + // gc.setAuthor("bunny"); + // gc.setOpen(false); + // mpg.setGlobalConfig(gc); + // + // // 3、数据源配置 + // DataSourceConfig dsc = new DataSourceConfig(); + // // TODO 需要修改数据库 + // dsc.setUrl("jdbc:mysql://106.15.251.123:3305/guigu-oa?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true"); + // dsc.setDriverName("com.mysql.cj.jdbc.Driver"); + // dsc.setUsername("root"); + // dsc.setPassword("02120212"); + // dsc.setDbType(DbType.MYSQL); + // mpg.setDataSource(dsc); + // + // // 4、包配置 + // PackageConfig pc = new PackageConfig(); + // pc.setParent("cn.bunny"); + // // TODO 需要修改模块名 + // pc.setModuleName("service"); + // pc.setController("controller"); + // pc.setService("service"); + // pc.setMapper("mapper"); + // mpg.setPackageInfo(pc); + // + // // 5、策略配置 + // StrategyConfig strategy = getStrategyConfig(); + // mpg.setStrategy(strategy); + // + // // 6、执行 + // mpg.execute(); + // } + // + // private static StrategyConfig getStrategyConfig() { + // StrategyConfig strategy = new StrategyConfig(); + // // TODO 要生成的表 + // strategy.setInclude("sys_menu", "sys_role_menu"); + // strategy.setNaming(NamingStrategy.underline_to_camel);// 数据库表映射到实体的命名策略 + // strategy.setColumnNaming(NamingStrategy.underline_to_camel);// 数据库表字段映射到实体的命名策略 + // strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作 + // strategy.setRestControllerStyle(true); // restful api风格控制器 + // strategy.setControllerMappingHyphenStyle(true); // url中驼峰转连字符 + // return strategy; + } +} diff --git a/common/pom.xml b/common/pom.xml new file mode 100644 index 0000000..6b185ed --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,58 @@ + + 4.0.0 + + cn.bunny + bunny-template + 0.0.1-SNAPSHOT + + + common + pom + common Maven Webapp + https://maven.apache.org + + service-utils + common-generator + + + + + cn.bunny + dao + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + io.jsonwebtoken + jjwt + + + + cn.hutool + hutool-all + + + + com.alibaba.fastjson2 + fastjson2 + + + + mysql + mysql-connector-java + + + + com.zaxxer + HikariCP + + + diff --git a/common/service-utils/pom.xml b/common/service-utils/pom.xml new file mode 100644 index 0000000..23a0cf0 --- /dev/null +++ b/common/service-utils/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + cn.bunny + common + 0.0.1-SNAPSHOT + + + service-utils + jar + service-utils + https://maven.apache.org + + + UTF-8 + + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.redisson + redisson + 3.26.1 + + + + org.lionsoul + ip2region + 2.6.5 + + + diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/config/Knife4jConfig.java b/common/service-utils/src/main/java/cn/bunny/common/service/config/Knife4jConfig.java new file mode 100644 index 0000000..6eff47b --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/config/Knife4jConfig.java @@ -0,0 +1,39 @@ +package cn.bunny.common.service.config; + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +public class Knife4jConfig { + @Bean + public OpenAPI openAPI() { + // 作者等信息 + Contact contact = new Contact().name("Bunny").email("1319900154@qq.com").url("http://z-bunny.com"); + // 使用协议 + License license = new License().name("MIT").url("http://MUT.com"); + // 相关信息 + Info info = new Info().title("Bunny-Java-Template").description("Bunny的Java模板").version("v1.0.0").contact(contact).license(license).termsOfService("记得给我start"); + + return new OpenAPI().info(info).externalDocs(new ExternalDocumentation()); + } + + // 前台相关分类接口 + @Bean + public GroupedOpenApi groupedOpenApi() { + return GroupedOpenApi.builder().group("web前台接口管理").pathsToMatch("/api/**").build(); + } + + // 管理员相关分类接口 + @Bean + public GroupedOpenApi groupedOpenAdminApi() { + return GroupedOpenApi.builder().group("admin管理员接口请求").pathsToMatch("/admin/**").build(); + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/config/MyBatisPlusFieldConfig.java b/common/service-utils/src/main/java/cn/bunny/common/service/config/MyBatisPlusFieldConfig.java new file mode 100644 index 0000000..d32b125 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/config/MyBatisPlusFieldConfig.java @@ -0,0 +1,39 @@ +package cn.bunny.common.service.config; + +import cn.bunny.common.service.context.BaseContext; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/** + * 配置MP在修改和新增时的操作 + */ +@Component +public class MyBatisPlusFieldConfig implements MetaObjectHandler { + + /** + * 使用mp做添加操作时候,这个方法执行 + */ + @Override + public void insertFill(MetaObject metaObject) { + // 设置属性值 + this.setFieldValByName("createTime", new Date(), metaObject); + this.setFieldValByName("updateTime", new Date(), metaObject); + this.setFieldValByName("deleteStatus", 1, metaObject); + if (BaseContext.getUsername() != null) { + this.setFieldValByName("createBy", BaseContext.getUsername(), metaObject); + this.setFieldValByName("updateBy", BaseContext.getUsername(), metaObject); + } + } + + /** + * 使用mp做修改操作时候,这个方法执行 + */ + @Override + public void updateFill(MetaObject metaObject) { + this.setFieldValByName("updateTime", new Date(), metaObject); + this.setFieldValByName("updateBy", BaseContext.getUsername(), metaObject); + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/config/MybatisPlusConfig.java b/common/service-utils/src/main/java/cn/bunny/common/service/config/MybatisPlusConfig.java new file mode 100644 index 0000000..519a465 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/config/MybatisPlusConfig.java @@ -0,0 +1,36 @@ +package cn.bunny.common.service.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * Mybatis-Plus配置类 + */ +@EnableTransactionManagement +@Configuration +@Slf4j +public class MybatisPlusConfig { + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + log.info("MybatisPlusInterceptor===>注入Mybatis-Plus配置..."); + + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); + paginationInnerInterceptor.setMaxLimit(100L);// ? 设置最大分页为100 + interceptor.addInnerInterceptor(paginationInnerInterceptor); + // 乐观锁 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + // 防止全表删除 + interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + + return interceptor; + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/config/RedisConfiguration.java b/common/service-utils/src/main/java/cn/bunny/common/service/config/RedisConfiguration.java new file mode 100644 index 0000000..1b5c8d7 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/config/RedisConfiguration.java @@ -0,0 +1,98 @@ +package cn.bunny.common.service.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 设置Redis序列化 + */ +@Component +@Slf4j +public class RedisConfiguration { + /** + * 使用StringRedisSerializer序列化为字符串 + */ + @Bean + public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory) { + log.info("RedisConfiguration===>使用StringRedisSerializer序列化为字符串"); + + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + // 设置key序列化为string + redisTemplate.setKeySerializer(new StringRedisSerializer()); + // 设置value序列化为JSON,使用GenericJackson2JsonRedisSerializer替换默认序列化 + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return redisTemplate; + } + + /** + * 解决cache(@Cacheable)把数据缓存到redis中的value是乱码问题 + */ + @Bean + @SuppressWarnings("all") + public CacheManager cacheManager(RedisConnectionFactory factory) { + log.info("RedisConfiguration===>解决cache(@Cacheable)把数据缓存到redis中的value是乱码问题"); + + // 配置序列化 + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer())) + .entryTtl(Duration.ofDays(30)); + + return RedisCacheManager.builder(factory).cacheDefaults(config).build(); + } + + /** + * 指定的日期模式 + */ + public Jackson2JsonRedisSerializer jsonRedisSerializer() { + log.info("RedisConfiguration===>指定的日期模式"); + + ObjectMapper mapper = new ObjectMapper(); + // 设置ObjectMapper访问权限 + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // 记录序列化之后的数据类型,方便反序列化 + mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); + // LocalDatetime序列化,默认不兼容jdk8日期序列化 + JavaTimeModule timeModule = new JavaTimeModule(); + timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + + timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + // 关闭默认的日期格式化方式,默认UTC日期格式 yyyy-MM-dd’T’HH:mm:ss.SSS + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.registerModule(timeModule); + + return new Jackson2JsonRedisSerializer<>(mapper, Object.class); + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/config/WebMvcConfiguration.java b/common/service-utils/src/main/java/cn/bunny/common/service/config/WebMvcConfiguration.java new file mode 100644 index 0000000..e050e0f --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/config/WebMvcConfiguration.java @@ -0,0 +1,42 @@ +package cn.bunny.common.service.config; + +import cn.bunny.common.service.interceptor.UserTokenInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@Slf4j +public class WebMvcConfiguration implements WebMvcConfigurer { + @Autowired + private UserTokenInterceptor userTokenInterceptor; + + /** + * 跨域配置 + * + * @param registry 跨域注册表 + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") + // 是否发送Cookies + .allowCredentials(true) + // 放行哪些原始域 + .allowedOriginPatterns("*").allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("*").exposedHeaders("*"); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + log.info("WebMvcConfiguration===>开始注册自定义拦截器..."); + + String[] excludeList = {"/", "/test/**", "/*.html", "/*/*/noAuth/**", "/*/noAuth/**", "/favicon.ico", + "/swagger-resources/**", "/swagger-ui.html/**", "/v3/**", "/api/**"}; + registry.addInterceptor(userTokenInterceptor).excludePathPatterns(excludeList); + + // TODO 如果想使用普通JWT可以使用这个,不使用 SpringSecurity6 + // registry.addInterceptor(userTokenInterceptor).addPathPatterns("/api/**").excludePathPatterns(excludeList); + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/context/BaseContext.java b/common/service-utils/src/main/java/cn/bunny/common/service/context/BaseContext.java new file mode 100644 index 0000000..1a01c70 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/context/BaseContext.java @@ -0,0 +1,52 @@ +package cn.bunny.common.service.context; + +public class BaseContext { + private static final ThreadLocal userId = new ThreadLocal<>(); + private static final ThreadLocal username = new ThreadLocal(); + private static final ThreadLocal adminId = new ThreadLocal<>(); + private static final ThreadLocal adminName = new ThreadLocal<>(); + + // 用户id相关 + public static Long getUserId() { + return userId.get(); + } + + public static void setUserId(Long _userId) { + userId.set(_userId); + } + + public static String getUsername() { + return username.get(); + } + + public static void setUsername(String _username) { + username.set(_username); + } + + public static void removeUser() { + username.remove(); + userId.remove(); + } + + // adminId 相关 + public static Long getAdminId() { + return adminId.get(); + } + + public static void setAdminId(Long _adminId) { + adminId.set(_adminId); + } + + public static String getAdminName() { + return adminName.get(); + } + + public static void setAdminName(String _adminName) { + adminName.set(_adminName); + } + + public static void removeAdmin() { + adminName.remove(); + adminId.remove(); + } +} \ No newline at end of file diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/exception/BunnyException.java b/common/service-utils/src/main/java/cn/bunny/common/service/exception/BunnyException.java new file mode 100644 index 0000000..680c503 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/exception/BunnyException.java @@ -0,0 +1,33 @@ +package cn.bunny.common.service.exception; + +import cn.bunny.pojo.result.ResultCodeEnum; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@NoArgsConstructor +@Getter +@ToString +@Slf4j +public class BunnyException extends RuntimeException { + Integer code;// 状态码 + String message;// 描述信息 + + public BunnyException(Integer code, String message) { + super(message); + this.code = code; + this.message = message; + } + + public BunnyException(String message) { + super(message); + this.message = message; + } + + public BunnyException(ResultCodeEnum codeEnum) { + super(codeEnum.getMessage()); + this.code = codeEnum.getCode(); + this.message = codeEnum.getMessage(); + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/exception/GlobalExceptionHandler.java b/common/service-utils/src/main/java/cn/bunny/common/service/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..30ec51c --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/exception/GlobalExceptionHandler.java @@ -0,0 +1,82 @@ +package cn.bunny.common.service.exception; + + +import cn.bunny.pojo.result.Result; +import cn.bunny.pojo.result.ResultCodeEnum; +import cn.bunny.pojo.result.constant.ExceptionConstant; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.io.FileNotFoundException; +import java.nio.file.AccessDeniedException; +import java.sql.SQLIntegrityConstraintViolationException; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + // 自定义异常信息 + @ExceptionHandler(BunnyException.class) + @ResponseBody + public Result exceptionHandler(BunnyException exception) { + log.error("GlobalExceptionHandler===>自定义异常信息:{}", exception.getMessage()); + + Integer code = exception.getCode() != null ? exception.getCode() : 500; + return Result.error(null, code, exception.getMessage()); + } + + // 运行时异常信息 + @ExceptionHandler(RuntimeException.class) + @ResponseBody + public Result exceptionHandler(RuntimeException exception) throws FileNotFoundException { + log.error("GlobalExceptionHandler===>运行时异常信息:{}", exception.getMessage()); + exception.printStackTrace(); + return Result.error(null, 500, "出错了啦"); + } + + // 捕获系统异常 + @ExceptionHandler(Exception.class) + @ResponseBody + public Result error(Exception exception) { + log.error("GlobalExceptionHandler===>系统异常信息:{}", exception.getMessage()); + + return Result.error(null, 500, "系统异常"); + } + + // 特定异常处理 + @ExceptionHandler(ArithmeticException.class) + @ResponseBody + public Result error(ArithmeticException exception) { + log.error("GlobalExceptionHandler===>特定异常信息:{}", exception.getMessage()); + + return Result.error(null, 500, exception.getMessage()); + } + + // spring security异常 + @ExceptionHandler(AccessDeniedException.class) + @ResponseBody + public Result error(AccessDeniedException exception) throws AccessDeniedException { + log.error("GlobalExceptionHandler===>spring security异常:{}", exception.getMessage()); + + return Result.error(ResultCodeEnum.SERVICE_ERROR); + } + + // 处理SQL异常 + @ExceptionHandler(SQLIntegrityConstraintViolationException.class) + @ResponseBody + public Result exceptionHandler(SQLIntegrityConstraintViolationException exception) { + log.error("GlobalExceptionHandler===>处理SQL异常:{}", exception.getMessage()); + + String message = exception.getMessage(); + if (message.contains("Duplicate entry")) { + // 截取用户名 + String username = message.split(" ")[2]; + // 错误信息 + String errorMessage = username + ExceptionConstant.ALREADY_USER_Exception; + return Result.error(errorMessage); + } else { + return Result.error(ExceptionConstant.UNKNOWN_Exception); + } + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/interceptor/UserTokenInterceptor.java b/common/service-utils/src/main/java/cn/bunny/common/service/interceptor/UserTokenInterceptor.java new file mode 100644 index 0000000..c6b6e5f --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/interceptor/UserTokenInterceptor.java @@ -0,0 +1,42 @@ +package cn.bunny.common.service.interceptor; + + +import cn.bunny.common.service.context.BaseContext; +import cn.bunny.common.service.utils.JwtHelper; +import cn.bunny.common.service.utils.ResponseUtil; +import cn.bunny.pojo.result.Result; +import cn.bunny.pojo.result.ResultCodeEnum; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@Slf4j +public class UserTokenInterceptor implements HandlerInterceptor { + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + log.info("UserTokenInterceptor===>设置拦截器"); + String token = request.getHeader("token"); + + // 不是动态方法直接返回 + if (!(handler instanceof HandlerMethod)) return true; + + // token过期-提示身份验证过期 + if (JwtHelper.isExpired(token)) { + ResponseUtil.out(response, Result.error(ResultCodeEnum.AUTHENTICATION_EXPIRED)); + return false; + } + + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + BaseContext.removeUser(); + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/utils/EmptyUtil.java b/common/service-utils/src/main/java/cn/bunny/common/service/utils/EmptyUtil.java new file mode 100644 index 0000000..9cbfe0c --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/utils/EmptyUtil.java @@ -0,0 +1,21 @@ +package cn.bunny.common.service.utils; + +import cn.bunny.common.service.exception.BunnyException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +@Slf4j +public class EmptyUtil { + /** + * 是否为空 + * + * @param value 判断值 + * @param message 错误消息 + */ + public static void isEmpty(Object value, String message) { + if (value == null || !StringUtils.hasText(value.toString())) { + log.error("为空对象错误:{},{}", value, message); + throw new BunnyException(message); + } + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/utils/FileUtil.java b/common/service-utils/src/main/java/cn/bunny/common/service/utils/FileUtil.java new file mode 100644 index 0000000..3daf5cd --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/utils/FileUtil.java @@ -0,0 +1,31 @@ +package cn.bunny.common.service.utils; + +import org.springframework.stereotype.Component; + +@Component +public class FileUtil { + /** + * * 获取文件大小字符串 + */ + public static String getSize(Long fileSize) { + double fileSizeInKB = fileSize / 1024.00; + double fileSizeInMB = fileSizeInKB / 1024; + double fileSizeInGB = fileSizeInMB / 1024; + + String size; + if (fileSizeInGB >= 1) { + fileSizeInGB = Double.parseDouble(String.format("%.2f", fileSizeInGB)); + size = fileSizeInGB + "GB"; + } else if (fileSizeInMB >= 1) { + fileSizeInMB = Double.parseDouble(String.format("%.2f", fileSizeInMB)); + size = fileSizeInMB + "MB"; + } else if (fileSizeInKB >= 1) { + fileSizeInKB = Double.parseDouble(String.format("%.2f", fileSizeInKB)); + size = fileSizeInKB + "KB"; + } else { + size = fileSize + "B"; + } + + return size; + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/utils/HttpUtil.java b/common/service-utils/src/main/java/cn/bunny/common/service/utils/HttpUtil.java new file mode 100644 index 0000000..bfb9b51 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/utils/HttpUtil.java @@ -0,0 +1,206 @@ +package cn.bunny.common.service.utils; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class HttpUtil { + public static HttpResponse doGet(String host, String path, String method, Map headers, Map querys) throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpGet request = new HttpGet(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + return httpClient.execute(request); + } + + public static HttpResponse doPost(String host, String path, String method, Map headers, Map querys, Map bodys) throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPost request = new HttpPost(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (bodys != null) { + List nameValuePairList = new ArrayList(); + + for (String key : bodys.keySet()) { + nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key))); + } + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8"); + formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8"); + request.setEntity(formEntity); + } + + return httpClient.execute(request); + } + + + public static HttpResponse doPost(String host, String path, String method, Map headers, Map querys, String body) throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPost request = new HttpPost(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (StringUtils.isNotBlank(body)) { + request.setEntity(new StringEntity(body, "utf-8")); + } + + return httpClient.execute(request); + } + + + public static HttpResponse doPost(String host, String path, String method, Map headers, Map querys, byte[] body) throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPost request = new HttpPost(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (body != null) { + request.setEntity(new ByteArrayEntity(body)); + } + + return httpClient.execute(request); + } + + + public static HttpResponse doPut(String host, String path, String method, Map headers, Map querys, String body) throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPut request = new HttpPut(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (StringUtils.isNotBlank(body)) { + request.setEntity(new StringEntity(body, "utf-8")); + } + + return httpClient.execute(request); + } + + + public static HttpResponse doPut(String host, String path, String method, Map headers, Map querys, byte[] body) throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpPut request = new HttpPut(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (body != null) { + request.setEntity(new ByteArrayEntity(body)); + } + + return httpClient.execute(request); + } + + + public static HttpResponse doDelete(String host, String path, String method, Map headers, Map querys) throws Exception { + HttpClient httpClient = wrapClient(host); + + HttpDelete request = new HttpDelete(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + return httpClient.execute(request); + } + + private static String buildUrl(String host, String path, Map querys) throws UnsupportedEncodingException { + StringBuilder sbUrl = new StringBuilder(); + sbUrl.append(host); + if (!StringUtils.isBlank(path)) { + sbUrl.append(path); + } + if (null != querys) { + StringBuilder sbQuery = new StringBuilder(); + for (Map.Entry query : querys.entrySet()) { + if (!sbQuery.isEmpty()) { + sbQuery.append("&"); + } + if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) { + sbQuery.append(query.getValue()); + } + if (!StringUtils.isBlank(query.getKey())) { + sbQuery.append(query.getKey()); + if (!StringUtils.isBlank(query.getValue())) { + sbQuery.append("="); + sbQuery.append(URLEncoder.encode(query.getValue(), StandardCharsets.UTF_8)); + } + } + } + if (!sbQuery.isEmpty()) { + sbUrl.append("?").append(sbQuery); + } + } + + return sbUrl.toString(); + } + + private static HttpClient wrapClient(String host) { + HttpClient httpClient = new DefaultHttpClient(); + if (host.startsWith("https://")) { + sslClient(httpClient); + } + + return httpClient; + } + + private static void sslClient(HttpClient httpClient) { + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + X509TrustManager tm = new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] xcs, String str) { + } + + public void checkServerTrusted(X509Certificate[] xcs, String str) { + } + }; + ctx.init(null, new TrustManager[]{tm}, null); + SSLSocketFactory ssf = new SSLSocketFactory(ctx); + ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + ClientConnectionManager ccm = httpClient.getConnectionManager(); + SchemeRegistry registry = ccm.getSchemeRegistry(); + registry.register(new Scheme("https", 443, ssf)); + } catch (Exception ex) { + throw new RuntimeException(); + } + } +} \ No newline at end of file diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/utils/JwtHelper.java b/common/service-utils/src/main/java/cn/bunny/common/service/utils/JwtHelper.java new file mode 100644 index 0000000..07394bf --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/utils/JwtHelper.java @@ -0,0 +1,350 @@ +package cn.bunny.common.service.utils; + +import io.jsonwebtoken.*; +import io.micrometer.common.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class JwtHelper { + // 时间 按天 计算 + private static final long tokenExpiration = 24 * 60 * 60 * 1000; + // JWT 的 秘钥 + private static final String tokenSignKey = "Bunny-Java-Template"; + // 默认主题 + private static final String subject = "Bunny"; + // 默认时间 + private static final Date time = new Date(System.currentTimeMillis() + tokenExpiration * 7); + + /** + * 使用默认主题,默认时间,默认秘钥,创建自定义集合token + * + * @param map 集合 + * @return token + */ + public static String createTokenWithMap(Map map) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(time) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .compressWith(CompressionCodecs.GZIP).compact(); + } + + /** + * 使用默认主题,默认秘钥,自定义时间,创建集合形式token + * + * @param map 集合 + * @param time 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, Date time) { + return Jwts.builder() + .setSubject(subject) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .setExpiration(time) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .compressWith(CompressionCodecs.GZIP).compact(); + } + + /** + * 使用默认主题,默认秘钥,自定义时间,创建集合形式token + * + * @param map 集合 + * @param day 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, Integer day) { + return Jwts.builder() + .setSubject(subject) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration * day)) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .compressWith(CompressionCodecs.GZIP).compact(); + } + + /** + * 使用默认主题,默认秘钥,自定义key,创建集合形式token + * + * @param map 集合 + * @param tokenSignKey 自定义key + * @return token + */ + public static String createTokenWithMap(Map map, String tokenSignKey) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(time) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .compressWith(CompressionCodecs.GZIP).compact(); + } + + /** + * 使用自定义主题,自定义时间,创建集合形式token + * + * @param map 集合 + * @param subject 主题 + * @param time 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String subject, Date time) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(time) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .compressWith(CompressionCodecs.GZIP) + .compact(); + } + + /** + * 创建集合形式token + * + * @param map 集合 + * @param subject 主题 + * @param tokenSignKey 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String subject, String tokenSignKey) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(time) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .compressWith(CompressionCodecs.GZIP).compact(); + } + + /** + * 创建集合形式token + * + * @param map 集合 + * @param tokenSignKey 主题 + * @param time 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String tokenSignKey, Integer time) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration * time)) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .compressWith(CompressionCodecs.GZIP) + .compact(); + } + + /** + * 创建集合形式token + * + * @param map 集合 + * @param subject 主题 + * @param day 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String subject, String tokenSignKey, Integer day) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration * day)) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .compressWith(CompressionCodecs.GZIP) + .compact(); + } + + /** + * 创建集合形式token + * + * @param map 集合 + * @param subject 主题 + * @param time 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String subject, String tokenSignKey, Date time) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(time) + .setClaims(map) + .setId(UUID.randomUUID().toString()) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .compressWith(CompressionCodecs.GZIP) + .compact(); + } + + /** + * 根据用户名和ID创建token + * + * @param userId 用户ID + * @param userName 用户名 + * @param day 过期时间 + * @return token值 + */ + public static String createToken(Long userId, String userName, Integer day) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration * day)) + .claim("userId", userId) + .claim("userName", userName) + .setId(UUID.randomUUID().toString()) + .signWith(SignatureAlgorithm.HS256, tokenSignKey) + .compressWith(CompressionCodecs.GZIP) + .compact(); + } + + /** + * 使用token获取map集合,使用默认秘钥 + * + * @param token token + * @return map集合 + */ + public static Map getMapByToken(String token) { + try { + if (!StringUtils.hasText(token)) return null; + Claims claims = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody(); + + // 将 body 值转为map + return new HashMap<>(claims); + + } catch (Exception exception) { + return null; + } + } + + /** + * 使用token获取map集合 + * + * @param token token + * @param signKey 秘钥 + * @return map集合 + */ + public static Map getMapByToken(String token, String signKey) { + try { + if (!StringUtils.hasText(token)) return null; + Jws claimsJws = Jwts.parser().setSigningKey(signKey).parseClaimsJws(token); + Claims body = claimsJws.getBody(); + // 将 body 值转为map + return new HashMap<>(body); + + } catch (Exception exception) { + return null; + } + } + + /** + * 根据token获取主题 + * + * @param token token + * @return 主题 + */ + public static String getSubjectByToken(String token) { + return getSubjectByTokenHandler(token, tokenSignKey); + } + + @Nullable + private static String getSubjectByTokenHandler(String token, String tokenSignKey) { + try { + if (!StringUtils.hasText(token)) return null; + Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); + Claims body = claimsJws.getBody(); + + return body.getSubject(); + + } catch (Exception exception) { + return null; + } + } + + /** + * 根据token获取主题 + * + * @param token token + * @return 主题 + */ + public static String getSubjectByToken(String token, String tokenSignKey) { + return getSubjectByTokenHandler(token, tokenSignKey); + } + + /** + * 根据token获取用户ID + * + * @param token token + * @return 用户ID + */ + public static Long getUserId(String token) { + try { + if (!StringUtils.hasText(token)) return null; + + Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); + Claims claims = claimsJws.getBody(); + + return Long.valueOf(String.valueOf(claims.get("userId"))); + } catch (Exception exception) { + return null; + } + } + + /** + * 根据token获取用户名 + * + * @param token token + * @return 用户名 + */ + public static String getUsername(String token) { + try { + if (!StringUtils.hasText(token)) return ""; + + Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); + Claims claims = claimsJws.getBody(); + return (String) claims.get("userName"); + } catch (Exception exception) { + return null; + } + } + + /** + * 判断token是否过期 + * + * @param token token + * @return 是否过期 + */ + public static boolean isExpired(String token) { + return isExpiredUtil(token, tokenSignKey); + } + + /** + * 判断token是否过期 + * + * @param token token + * @return 是否过期 + */ + public static boolean isExpired(String token, String tokenSignKey) { + return isExpiredUtil(token, tokenSignKey); + } + + /** + * 判断是否过期 + * + * @param token token + * @param tokenSignKey key值 + * @return 是否过期 + */ + private static boolean isExpiredUtil(String token, String tokenSignKey) { + try { + Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); + Date expiration = claimsJws.getBody().getExpiration(); + + return expiration != null && expiration.before(new Date()); + } catch (Exception exception) { + return true; + } + } +} \ No newline at end of file diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseHandlerUtil.java b/common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseHandlerUtil.java new file mode 100644 index 0000000..2376798 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseHandlerUtil.java @@ -0,0 +1,12 @@ +package cn.bunny.common.service.utils; + +import cn.bunny.pojo.result.Result; +import cn.bunny.pojo.result.ResultCodeEnum; +import jakarta.servlet.http.HttpServletResponse; + +public class ResponseHandlerUtil { + public static boolean loginAuthHandler(HttpServletResponse response, ResultCodeEnum loginAuth) { + ResponseUtil.out(response, Result.error(loginAuth)); + return false; + } +} diff --git a/common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseUtil.java b/common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseUtil.java new file mode 100644 index 0000000..d6f2019 --- /dev/null +++ b/common/service-utils/src/main/java/cn/bunny/common/service/utils/ResponseUtil.java @@ -0,0 +1,26 @@ +package cn.bunny.common.service.utils; + +import cn.bunny.pojo.result.Result; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; + +import java.io.IOException; + +public class ResponseUtil { + + public static void out(HttpServletResponse response, Result result) { + ObjectMapper mapper = new ObjectMapper(); + + // 注册JavaTimeModule模块 + mapper.registerModule(new JavaTimeModule()); + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpStatus.OK.value()); + try { + mapper.writeValue(response.getWriter(), result); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/dao/pom.xml b/dao/pom.xml new file mode 100644 index 0000000..dfcd89a --- /dev/null +++ b/dao/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + + cn.bunny + bunny-template + 0.0.1-SNAPSHOT + + + dao + jar + + model + https://maven.apache.org + + + UTF-8 + + + + + + org.projectlombok + lombok + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + io.swagger + swagger-annotations + 1.6.14 + + + diff --git a/dao/src/main/java/cn/bunny/dto/user/LoginDto.java b/dao/src/main/java/cn/bunny/dto/user/LoginDto.java new file mode 100644 index 0000000..1f4213b --- /dev/null +++ b/dao/src/main/java/cn/bunny/dto/user/LoginDto.java @@ -0,0 +1,19 @@ +package cn.bunny.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LoginDto { + // 用户名 + private String username; + // 密码 + private String password; + // 邮箱验证码 + private String emailCode; +} diff --git a/dao/src/main/java/cn/bunny/entity/base/BaseEntity.java b/dao/src/main/java/cn/bunny/entity/base/BaseEntity.java new file mode 100644 index 0000000..451c19b --- /dev/null +++ b/dao/src/main/java/cn/bunny/entity/base/BaseEntity.java @@ -0,0 +1,41 @@ +package cn.bunny.entity.base; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Data +public class BaseEntity implements Serializable { + + @TableId(value = "id", type = IdType.ASSIGN_ID) + @ApiModelProperty("唯一标识") + private Long id; + + @TableField("create_time") + @ApiModelProperty("创建时间") + private Date createTime; + + @TableField("update_time") + @ApiModelProperty("更新时间") + private Date updateTime; + + @TableField("update_user") + @ApiModelProperty("操作用户ID") + private Long updateUser; + + @TableLogic + @TableField("is_deleted") + @ApiModelProperty("是否被删除") + private Boolean isDeleted; + + @TableField(exist = false) + private Map param = new HashMap<>(); +} diff --git a/dao/src/main/java/cn/bunny/entity/system/user/User.java b/dao/src/main/java/cn/bunny/entity/system/user/User.java new file mode 100644 index 0000000..33c086d --- /dev/null +++ b/dao/src/main/java/cn/bunny/entity/system/user/User.java @@ -0,0 +1,61 @@ +package cn.bunny.entity.system.user; + +import cn.bunny.entity.base.BaseEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 用户信息 + *

+ * + * @author Bunny + * @since 2024-05-17 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@ApiModel(value = "User对象", description = "用户信息") +public class User extends BaseEntity implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + @ApiModelProperty("昵称") + private String nickName; + @ApiModelProperty("邮箱") + private String email; + @ApiModelProperty("密码") + private String password; + @ApiModelProperty("头像") + private String avatar; + @ApiModelProperty("0:女 1:男") + private Byte sex; + @ApiModelProperty("个人描述") + private String personDescription; + @ApiModelProperty("加入时间") + private LocalDateTime joinTime; + + @ApiModelProperty("最后登录时间") + private LocalDateTime lastLoginTime; + + @ApiModelProperty("最后登录IP") + private String lastLoginIp; + + @ApiModelProperty("最后登录ip地址") + private String lastLoginIpAddress; + + @ApiModelProperty("积分") + private Integer totalIntegral; + + @ApiModelProperty("当前积分") + private Integer currentIntegral; + + @ApiModelProperty("0:禁用 1:正常") + private Byte status; +} diff --git a/dao/src/main/java/cn/bunny/pojo/result/Result.java b/dao/src/main/java/cn/bunny/pojo/result/Result.java new file mode 100644 index 0000000..d85b26d --- /dev/null +++ b/dao/src/main/java/cn/bunny/pojo/result/Result.java @@ -0,0 +1,173 @@ +package cn.bunny.pojo.result; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Result { + // 状态码 + private Integer code; + // 返回消息 + private String message; + // 返回数据 + private T data; + + /** + * * 自定义返回体 + * + * @param data 返回体 + * @return Result + */ + protected static Result build(T data) { + Result result = new Result<>(); + result.setData(data); + return result; + } + + /** + * * 自定义返回体,使用ResultCodeEnum构建 + * + * @param body 返回体 + * @param codeEnum 返回状态码 + * @return Result + */ + public static Result build(T body, ResultCodeEnum codeEnum) { + Result result = build(body); + result.setCode(codeEnum.getCode()); + result.setMessage(codeEnum.getMessage()); + return result; + } + + /** + * * 自定义返回体 + * + * @param body 返回体 + * @param code 返回状态码 + * @param message 返回消息 + * @return Result + */ + public static Result build(T body, Integer code, String message) { + Result result = build(body); + result.setCode(code); + result.setMessage(message); + result.setData(null); + return result; + } + + /** + * * 操作成功 + * + * @return Result + */ + public static Result success() { + return success(null, ResultCodeEnum.SUCCESS); + } + + /** + * * 操作成功 + * + * @param data baseCategory1List + */ + public static Result success(T data) { + return build(data, ResultCodeEnum.SUCCESS); + } + + /** + * * 操作成功-状态码 + * + * @param codeEnum 状态码 + */ + public static Result success(ResultCodeEnum codeEnum) { + return success(null, codeEnum); + } + + /** + * * 操作成功-自定义返回数据和状态码 + * + * @param data 返回体 + * @param codeEnum 状态码 + */ + public static Result success(T data, ResultCodeEnum codeEnum) { + return build(data, codeEnum); + } + + /** + * * 操作失败-自定义返回数据和状态码 + * + * @param data 返回体 + * @param message 错误信息 + */ + public static Result success(T data, String message) { + return build(data, 200, message); + } + + /** + * * 操作失败-自定义返回数据和状态码 + * + * @param data 返回体 + * @param code 状态码 + * @param message 错误信息 + */ + public static Result success(T data, Integer code, String message) { + return build(data, code, message); + } + + /** + * * 操作失败 + */ + public static Result error() { + return Result.build(null); + } + + /** + * * 操作失败-自定义返回数据 + * + * @param data 返回体 + */ + public static Result error(T data) { + return build(data, ResultCodeEnum.FAIL); + } + + /** + * * 操作失败-状态码 + * + * @param codeEnum 状态码 + */ + public static Result error(ResultCodeEnum codeEnum) { + return build(null, codeEnum); + } + + /** + * * 操作失败-自定义返回数据和状态码 + * + * @param data 返回体 + * @param codeEnum 状态码 + */ + public static Result error(T data, ResultCodeEnum codeEnum) { + return build(data, codeEnum); + } + + /** + * * 操作失败-自定义返回数据和状态码 + * + * @param data 返回体 + * @param code 状态码 + * @param message 错误信息 + */ + public static Result error(T data, Integer code, String message) { + return build(data, code, message); + } + + /** + * * 操作失败-自定义返回数据和状态码 + * + * @param data 返回体 + * @param message 错误信息 + */ + public static Result error(T data, String message) { + return build(null, 500, message); + } +} diff --git a/dao/src/main/java/cn/bunny/pojo/result/ResultCodeEnum.java b/dao/src/main/java/cn/bunny/pojo/result/ResultCodeEnum.java new file mode 100644 index 0000000..d2a98d7 --- /dev/null +++ b/dao/src/main/java/cn/bunny/pojo/result/ResultCodeEnum.java @@ -0,0 +1,54 @@ +package cn.bunny.pojo.result; + +import lombok.Getter; + +/** + * 统一返回结果状态信息类 + */ +@Getter +public enum ResultCodeEnum { + // 成功操作 200 + SUCCESS(200, "操作成功"), + SUCCESS_LOGOUT(200, "退出成功"), + EMAIL_CODE_REFRESH(200, "邮箱验证码已刷新"), + // 验证错误 201 + USERNAME_NOT_EMPTY(201, "用户名不能为空"), + PASSWORD_NOT_EMPTY(201, "密码不能为空"), + EMAIL_CODE_NOT_EMPTY(201, "邮箱验证码不能为空"), + SEND_EMAIL_CODE_NOT_EMPTY(201, "请先发送邮箱验证码"), + EMAIL_CODE_NOT_MATCHING(201, "邮箱验证码不匹配"), + LOGIN_ERROR(201, "账号或密码错误"), + LOGIN_ERROR_USERNAME_PASSWORD_NOT_EMPTY(201, "登录信息不能为空"), + // 数据相关 206 + ILLEGAL_REQUEST(206, "非法请求"), + REPEAT_SUBMIT(206, "重复提交"), + DATA_ERROR(206, "数据异常"), + // 身份过期 208 + LOGIN_AUTH(208, "请先登陆"), + AUTHENTICATION_EXPIRED(208, "身份验证过期"), + SESSION_EXPIRATION(208, "会话过期"), + // 封禁 209 + FAIL_NO_ACCESS_DENIED_USER_LOCKED(209, "该账户被封禁"), + THE_SAME_USER_HAS_LOGGED_IN(209, "相同用户已登录"), + // 提示错误 + URL_ENCODE_ERROR(216, "URL编码失败"), + ILLEGAL_CALLBACK_REQUEST_ERROR(217, "非法回调请求"), + FETCH_USERINFO_ERROR(219, "获取用户信息失败"), + // 无权访问 403 + FAIL_REQUEST_NOT_AUTH(403, "用户未认证"), + FAIL_NO_ACCESS_DENIED(403, "无权访问"), + FAIL_NO_ACCESS_DENIED_USER_OFFLINE(403, "用户强制下线"), + LOGGED_IN_FROM_ANOTHER_DEVICE(403, "没有权限访问"), + // 系统错误 500 + SERVICE_ERROR(500, "服务异常"), + FAIL(500, "失败"), + ; + + private final Integer code; + private final String message; + + ResultCodeEnum(Integer code, String message) { + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/dao/src/main/java/cn/bunny/pojo/result/constant/ExceptionConstant.java b/dao/src/main/java/cn/bunny/pojo/result/constant/ExceptionConstant.java new file mode 100644 index 0000000..cdb8945 --- /dev/null +++ b/dao/src/main/java/cn/bunny/pojo/result/constant/ExceptionConstant.java @@ -0,0 +1,52 @@ +package cn.bunny.pojo.result.constant; + +import lombok.Data; + + +@Data +public class ExceptionConstant { + public static final String UNKNOWN_Exception = "未知错误"; + public static final String TOKEN_IS_EMPTY = "token为空"; + public static final String DATA_IS_EMPTY = "数据为空"; + public static final String REQUEST_DATA_NOT_EMPTY_Exception = "请求参数为空"; + public static final String UPDATE_DTO_IS_NULL_Exception = "修改参数为空"; + public static final String ADD_DATA_IS_EMPTY_Exception = "添加数据为空"; + public static final String DELETE_ID_IS_NOT_EMPTY_Exception = "删除id不能为空"; + // 文章操作相关 + public static final String DO_LIKE_COMMENT_NOT_EXIST = "点赞内容不存在"; + public static final String REPLY_USER_EMPTY_EXCEPTION = "回复的用户不存在"; + public static final String REPLY_USER_ID_EMPTY_EXCEPTION = "回复的用户不能为空"; + public static final String MENU_IS_NOT_EXIST_Exception = "菜单不存在"; + public static final String POST_COMMENT_EMPTY_Exception = "评论内容不能为空"; + public static final String ARTICLE_ID_NOT_EMPTY_Exception = "文章id不能为空"; + public static final String UPDATE_ID_IS_NOT_EMPTY_Exception = "修改id不能为空"; + public static final String CANNOT_TOP_OTHER_USER = "不能操作此内容"; + public static final String ARTICLE_NOT_FOUND_EXCEPTION = "文章未找到"; + // 登录相关 + public static final String USER_TOKEN_OUT_OF_DATE_Exception = "用户登录过期"; + public static final String LOGIN_DTO_IS_EMPTY_Exception = "登录参数不能为空"; + public static final String LOGIN_FAILED_Exception = "登录失败"; + // 账号相关 + public static final String ACCOUNT_NOT_FOUND_Exception = "账号不存在"; + public static final String ACCOUNT_LOCKED_Exception = "账号被锁定"; + // 用户相关 + public static final String USER_NOT_LOGIN_Exception = "用户未登录"; + public static final String USERNAME_IS_EMPTY_Exception = "用户名不能为空"; + public static final String ALREADY_USER_Exception = "用户已存在"; + public static final String USER_NOT_FOUND_Exception = "用户不存在"; + // 密码相关 + public static final String PASSWORD_Exception = "密码错误"; + public static final String PASSWORD_NOT_EMPTY_Exception = "密码不能为空"; + public static final String OLD_PASSWORD_Exception = "旧密码不匹配"; + public static final String PASSWORD_EDIT_Exception = "密码修改失败"; + public static final String OLD_PASSWORD_SAME_NEW_PASSWORD_Exception = "旧密码与新密码相同"; + // 验证码错误 + public static final String PLEASE_SEND_EMAIL_CODE_Exception = "请先发送验证码"; + public static final String MESSAGE_CODE_NOT_PASS_Exception = "短信验证码未过期"; + public static final String MESSAGE_CODE_UNAUTHORIZED_Exception = "短信验证码未授权,请联系管理员"; + public static final String VERIFICATION_CODE_ERROR_Exception = "验证码错误"; + public static final String CAPTCHA_IS_EMPTY_Exception = "验证码不能为空"; + public static final String KEY_IS_EMPTY_Exception = "验证码key不能为空"; + public static final String VERIFICATION_CODE_DOES_NOT_MATCH_Exception = "验证码不匹配"; + public static final String VERIFICATION_CODE_IS_EMPTY_Exception = "验证码失效或不存在"; +} diff --git a/dao/src/main/java/cn/bunny/pojo/result/constant/LocalDateTimeConstant.java b/dao/src/main/java/cn/bunny/pojo/result/constant/LocalDateTimeConstant.java new file mode 100644 index 0000000..afda4da --- /dev/null +++ b/dao/src/main/java/cn/bunny/pojo/result/constant/LocalDateTimeConstant.java @@ -0,0 +1,11 @@ +package cn.bunny.pojo.result.constant; + +import lombok.Data; + +@Data +public class LocalDateTimeConstant { + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; + public static final String DEFAULT_DATE_TIME_SECOND_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; +} diff --git a/dao/src/main/java/cn/bunny/pojo/result/constant/UserConstant.java b/dao/src/main/java/cn/bunny/pojo/result/constant/UserConstant.java new file mode 100644 index 0000000..b36248a --- /dev/null +++ b/dao/src/main/java/cn/bunny/pojo/result/constant/UserConstant.java @@ -0,0 +1,8 @@ +package cn.bunny.pojo.result.constant; + +import lombok.Data; + +@Data +public class UserConstant { + public static final String USER_AVATAR = "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132"; +} diff --git a/dao/src/main/java/cn/bunny/pojo/tree/AbstractTreeNode.java b/dao/src/main/java/cn/bunny/pojo/tree/AbstractTreeNode.java new file mode 100644 index 0000000..643a62e --- /dev/null +++ b/dao/src/main/java/cn/bunny/pojo/tree/AbstractTreeNode.java @@ -0,0 +1,11 @@ +package cn.bunny.pojo.tree; + +import java.util.List; + +public interface AbstractTreeNode { + Long getId(); + + Long getParentId(); + + void setChildren(List children); +} diff --git a/dao/src/main/java/cn/bunny/pojo/tree/TreeBuilder.java b/dao/src/main/java/cn/bunny/pojo/tree/TreeBuilder.java new file mode 100644 index 0000000..0b6214f --- /dev/null +++ b/dao/src/main/java/cn/bunny/pojo/tree/TreeBuilder.java @@ -0,0 +1,29 @@ +package cn.bunny.pojo.tree; + +import java.util.ArrayList; +import java.util.List; + +public class TreeBuilder { + + public List buildTree(List nodeList) { + List tree = new ArrayList<>(); + for (T node : nodeList) { + if (node.getParentId() == 0) { + node.setChildren(getChildren(node.getId(), nodeList)); + tree.add(node); + } + } + return tree; + } + + private List getChildren(Long nodeId, List nodeList) { + List children = new ArrayList<>(); + for (T node : nodeList) { + if (node.getParentId().equals(nodeId)) { + node.setChildren(getChildren(node.getId(), nodeList)); + children.add(node); + } + } + return children; + } +} diff --git a/dao/src/main/java/cn/bunny/vo/page/PageResult.java b/dao/src/main/java/cn/bunny/vo/page/PageResult.java new file mode 100644 index 0000000..feb66b2 --- /dev/null +++ b/dao/src/main/java/cn/bunny/vo/page/PageResult.java @@ -0,0 +1,27 @@ +package cn.bunny.vo.page; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 封装分页查询结果 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PageResult implements Serializable { + // 当前页 + private Integer pageNo; + // 每页记录数 + private Integer pageSize; + // 总记录数 + private long total; + // 当前页数据集合 + private List list; +} \ No newline at end of file diff --git a/dao/src/main/java/cn/bunny/vo/system/user/UserInfoVo.java b/dao/src/main/java/cn/bunny/vo/system/user/UserInfoVo.java new file mode 100644 index 0000000..78e954d --- /dev/null +++ b/dao/src/main/java/cn/bunny/vo/system/user/UserInfoVo.java @@ -0,0 +1,43 @@ +package cn.bunny.vo.system.user; + +import cn.bunny.pojo.result.constant.LocalDateTimeConstant; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 获取用户信息返回参数 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UserInfoVo { + private Long userId; + private String nickName; + private String email; + private String avatar; + private Byte sex; + private String personDescription; + @JsonFormat(pattern = LocalDateTimeConstant.DEFAULT_DATE_TIME_SECOND_FORMAT) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private LocalDateTime joinTime; + @JsonFormat(pattern = LocalDateTimeConstant.DEFAULT_DATE_TIME_SECOND_FORMAT) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private LocalDateTime lastLoginTime; + private String lastLoginIp; + private String lastLoginIpAddress; + private Integer totalIntegral; + private Integer currentIntegral; + private Byte status; +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..84a278c --- /dev/null +++ b/pom.xml @@ -0,0 +1,182 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + + cn.bunny + bunny-template + 0.0.1-SNAPSHOT + pom + bunny-template + bunny-template + + + common + dao + service + + + + 22 + 22 + 21 + 3.8.1 + 3.5.6 + 8.0.30 + 4.5.0 + 2.0.47 + 8.5.9 + 1.18.32 + 0.9.1 + 3.3.3 + 2.10.1 + 1.9.21 + 6.1.0 + 2.2 + 3.1 + 5.1.0 + 4.3.1 + + + + + + junit + junit + ${junit.version} + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + org.apache.velocity.tools + velocity-tools-generic + ${velocity-tools.version} + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + mysql + mysql-connector-java + ${mysql.version} + + + + com.zaxxer + HikariCP + ${HikariCP.version} + + + + com.baomidou + dynamic-datasource-spring-boot3-starter + ${dynamic.datasource.version} + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + + io.minio + minio + ${minio.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + + cn.hutool + hutool-all + 5.8.27 + + + + io.jsonwebtoken + jjwt + ${jwt.version} + + + + com.alibaba + easyexcel + ${easyexcel.version} + + + + org.aspectj + aspectjrt + ${aspectj} + + + + org.aspectj + aspectjweaver + ${aspectj} + + + joda-time + joda-time + ${jodatime.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.12.3 + + + + + + + + dev + + dev + + + true + + + + + test + + test + + + + + prod + + prod + + + + diff --git a/service/Dockerfile b/service/Dockerfile new file mode 100644 index 0000000..447a2bc --- /dev/null +++ b/service/Dockerfile @@ -0,0 +1,21 @@ +FROM openjdk:21 +MAINTAINER bunny + +#系统编码 +ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 + +# 设置时区,构建镜像时执行的命令 +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +RUN echo "Asia/Shanghai" > /etc/timezone + +# 设定工作目录 +WORKDIR /home/bunny + +# 复制jar包 +COPY target/*.jar /home/bunny/app.jar + +#启动容器时的进程 +ENTRYPOINT ["java","-jar","/home/bunny/app.jar"] + +#暴露 8800 端口 +EXPOSE 8800 \ No newline at end of file diff --git a/service/pom.xml b/service/pom.xml new file mode 100644 index 0000000..a13047f --- /dev/null +++ b/service/pom.xml @@ -0,0 +1,117 @@ + + 4.0.0 + + cn.bunny + bunny-template + 0.0.1-SNAPSHOT + + + service + jar + service + https://maven.apache.org + + + UTF-8 + 192.168.3.98:1100 + 192.168.3.98:2375 + bunny-service + + + + + cn.bunny + service-utils + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + com.google.protobuf + protobuf-java + 4.27.2 + + + + junit + junit + 3.8.1 + + + + org.springframework.boot + spring-boot-starter-test + + + + org.aspectj + aspectjrt + + + org.aspectj + aspectjweaver + + + + com.baomidou + dynamic-datasource-spring-boot3-starter + + + + org.apache.shardingsphere + shardingsphere-jdbc-core-spring-boot-starter + 5.2.1 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + com.spotify + docker-maven-plugin + 1.2.2 + + + + build-image + + package + + build + push + + + + + harbor + http://${docker.repostory} + + http://${docker.host} + + + ${docker.repostory}/${docker.registry.name}/${project.artifactId}:${project.version} + + + ${project.basedir} + + false + + + + + diff --git a/service/src/main/java/cn/bunny/service/ServiceApplication.java b/service/src/main/java/cn/bunny/service/ServiceApplication.java new file mode 100644 index 0000000..c9022ce --- /dev/null +++ b/service/src/main/java/cn/bunny/service/ServiceApplication.java @@ -0,0 +1,23 @@ +package cn.bunny.service; + +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@ComponentScan(basePackages = {"cn.bunny"}) +@MapperScan("cn.bunny.service.mapper") +@EnableScheduling// 定时任务 +@EnableCaching// 开启缓存注解 +@EnableTransactionManagement// 开启事务注解 +@SpringBootApplication +@Slf4j +public class ServiceApplication { + public static void main(String[] args) { + SpringApplication.run(ServiceApplication.class, args); + } +} diff --git a/service/src/main/java/cn/bunny/service/controller/IndexController.java b/service/src/main/java/cn/bunny/service/controller/IndexController.java new file mode 100644 index 0000000..d645557 --- /dev/null +++ b/service/src/main/java/cn/bunny/service/controller/IndexController.java @@ -0,0 +1,18 @@ +package cn.bunny.service.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "访问首页内容") +@RestController +@RequestMapping("/") +public class IndexController { + @Operation(summary = "访问首页", description = "访问首页") + @GetMapping("") + public String index() { + return "欢迎访问 Bunny Java Template,欢迎去Gitee:https://gitee.com/BunnyBoss/java_single.git"; + } +} diff --git a/service/src/main/java/cn/bunny/service/controller/WebController.java b/service/src/main/java/cn/bunny/service/controller/WebController.java new file mode 100644 index 0000000..88e4fb6 --- /dev/null +++ b/service/src/main/java/cn/bunny/service/controller/WebController.java @@ -0,0 +1,31 @@ +package cn.bunny.service.controller; + +import cn.bunny.service.service.LoginService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "web相关接口") +@RestController +@RequestMapping("/api") +public class WebController { + @Autowired + private LoginService loginService; + + @Operation(summary = "生成验证码", description = "生成验证码") + @GetMapping("checkCode") + public ResponseEntity checkCode() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_JPEG); + + byte[] image = loginService.checkCode(); + return new ResponseEntity(image, headers, HttpStatus.OK); + } +} diff --git a/service/src/main/java/cn/bunny/service/service/LoginService.java b/service/src/main/java/cn/bunny/service/service/LoginService.java new file mode 100644 index 0000000..6a2dd06 --- /dev/null +++ b/service/src/main/java/cn/bunny/service/service/LoginService.java @@ -0,0 +1,10 @@ +package cn.bunny.service.service; + +public interface LoginService { + /** + * * 生成验证码 + * + * @return 验证码图片数组 + */ + byte[] checkCode(); +} diff --git a/service/src/main/java/cn/bunny/service/service/impl/LoginServiceImpl.java b/service/src/main/java/cn/bunny/service/service/impl/LoginServiceImpl.java new file mode 100644 index 0000000..36023b1 --- /dev/null +++ b/service/src/main/java/cn/bunny/service/service/impl/LoginServiceImpl.java @@ -0,0 +1,21 @@ +package cn.bunny.service.service.impl; + +import cn.bunny.service.service.LoginService; +import cn.hutool.captcha.CaptchaUtil; +import cn.hutool.captcha.CircleCaptcha; +import org.springframework.stereotype.Service; + +@Service +public class LoginServiceImpl implements LoginService { + /** + * * 生成验证码 + * + * @return 验证码图片数组 + */ + @Override + public byte[] checkCode() { + // 生成验证码 + CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2); + return captcha.getImageBytes(); + } +} diff --git a/service/src/main/resources/application-dev.yml b/service/src/main/resources/application-dev.yml new file mode 100644 index 0000000..8021204 --- /dev/null +++ b/service/src/main/resources/application-dev.yml @@ -0,0 +1,13 @@ +bunny: + datasource: + host: 192.168.3.98 + port: 3306 + sqlData: bunny_docs + username: root + password: "02120212" + + redis: + host: 1192.168.3.98 + port: 6379 + database: 3 + password: "123456" diff --git a/service/src/main/resources/application-prod.yml b/service/src/main/resources/application-prod.yml new file mode 100644 index 0000000..8021204 --- /dev/null +++ b/service/src/main/resources/application-prod.yml @@ -0,0 +1,13 @@ +bunny: + datasource: + host: 192.168.3.98 + port: 3306 + sqlData: bunny_docs + username: root + password: "02120212" + + redis: + host: 1192.168.3.98 + port: 6379 + database: 3 + password: "123456" diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml new file mode 100644 index 0000000..cd246c1 --- /dev/null +++ b/service/src/main/resources/application.yml @@ -0,0 +1,59 @@ +server: + port: 8800 + +spring: + profiles: + active: @profiles.active@ + application: + name: bunny-service + + datasource: + dynamic: + primary: master #设置默认的数据源或者数据源组,默认值即为master + strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源 + grace-destroy: false #是否优雅关闭数据源,默认为false,设置为true时,关闭数据源时如果数据源中还存在活跃连接,至多等待10s后强制关闭 + datasource: + master: + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${bunny.datasource.host}:${bunny.datasource.port}/${bunny.datasource.sqlData}?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true + username: ${bunny.datasource.username} + password: ${bunny.datasource.password} + aop: + enabled: true + + data: + redis: + host: ${bunny.redis.host} + port: ${bunny.redis.port} + database: ${bunny.redis.database} + password: ${bunny.redis.password} + lettuce: + pool: + max-active: 20 #最大连接数 + max-wait: -1 #最大阻塞等待时间(负数表示没限制) + max-idle: 5 #最大空闲 + min-idle: 0 #最小空闲 + + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + +mybatis-plus: + mapper-locations: classpath:mapper/*.xml + global-config: + db-config: + logic-delete-field: isDelete + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志 + +logging: + level: + cn.bunny.service.mapper: debug + cn.bunny.service.controller: info + cn.bunny.service.service: info + pattern: + dateformat: HH:mm:ss:SSS + file: + path: "logs/${spring.application.name}" + diff --git a/service/src/main/resources/banner.txt b/service/src/main/resources/banner.txt new file mode 100644 index 0000000..cc77fc2 --- /dev/null +++ b/service/src/main/resources/banner.txt @@ -0,0 +1,16 @@ +-----------------▄██-█▄--------- +-----------------███▄██▄-------- +-----------------███████-------- +-----------------▀███████------- +-------------------██████▄▄----- +-------------------█████████▄--- +-------------------██████▄████-- +-------▄███████████████████████- +-----▄███████████████████████▀-- +---▄██████████████████████------ +---███████████████████████------ +---███████████████████████------ +-▄▄██████████████████████▀------ +-█████████████████▀█████-------- +-▀██████████████▀▀-▀█████▄------ +-------▀▀▀▀▀▀▀▀▀------▀▀▀▀------ \ No newline at end of file diff --git a/service/src/main/resources/static/favicon.ico b/service/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..385f8a67127e1978b08387383f5935feaf561d04 GIT binary patch literal 17014 zcmeI2`HvJu6vt<;x!8+k7i3+=ML=*-6c11|xGE9x29+2OJP1)y@m4}qz&l1o#rwb$ zQBVvhh$y&gjK-gh(ck^WL=zMJ1N8Hq?V8l|HQlqlGI4q2JKbIN>RYc~SG{`GODa{z zzyAIdzrCqz&Pb)kr&6gIfTGkopz@;h4_%V@?O(8<38XDF4TrbZ`TWLK>o!NRPda{M zi&_??kEmSI`>*-vPjh(G`oJ0gsP%z0{!!@z&Hd5rg98zB;>5#|w$IoyTIYco|494U zSz|S~&)DN1jXrRXKY|Z>(76Dt1b2Yt;2bas_)4v>EkC2(Ha9jlUWVRo@D^AB&Ib#? z#o$)(D%cO+16P1Cpd@ZSJ(nu0xJjDNZ9-jL-E_vhw4tHl_WJtzPk8>LzM)|qddomR z=mLH+fx=tB7vLpuJSd86o_)dzS+3^XRBJwv?N5L&!82eLKDacUPS0;@YC5~IscA_= zW8+nnKLECZeP9c?9&`gEhjXK{LGU-wy!Z_4#h2T_S6~;|TUTGdhw`0Z1CVd~Ks|^g z3C@G_=x{GM+1+u4K6KocPc-j$fv>=3a0xgJc*M=u-P6;;{L;9^^DK~G-vC-;Sr962 zK0Svv@K*LyB%2$&RYN5a`{+b_HPHKBG}O}nHv*sLkm9S>(MB*C1WE+Hz_wJVkFjA2 zs*BM_JL-CWcdK2H#Nay-4y`l2qnQct*%W$bn!AxChRx!uICl@9IyFy>y^~n58|(p! z6N(2~2kx~vg|cnnagYHXiD{qO=fKw&6trR9vnrZ z=fEc5UeAgxiZk+uN2;(-{(JychVc2R*HL&8xCg8Nr-P|r7SI|V1j_r$BxWCFZJmcxxvnu4;+xpr-9C5T zybSmD_Xx!&Kau}F2jcNa6#Jy(Y3MCbZa3Fk9Qxk;)ZJxgP;i%B$ohXMlgZ3;#&3L} z93eO> zFi{Lw?xOuN?N$u4&l$h*v%9IDZwAWoLnW+_kcN%aY5v;s8V@VU3kMnZcCe?dy}euf zwvx}i*09bfKLW+Zcfcdy2C%TXxw&ACwKQJ8GW2cHepGgru`BKSDt(}ReUi;%?v)ot zu+ONQYk?c>XIihvfW<&->|OSh@9^iR+Hb~obc}cG4g3D5d{Ae;$Wnz5`XPTfsDmo_ zM5Q%50J=e$w6?WP`%p*gH+@4g1`xz!>+SIM`A4wNsBb9cn}S2KR?`6Y4pLH$`& zDyCK?ZazI%MdUP!$fvwmxuf2D9{dWFC*22>(?|{l10CbWdHiDc!9F~fb@FJjMU_7` zANQrWTP0oCS~ZmGx!DA?XQ#nq;7p*i^y^?3cpu0|zG7@vPR{su=-F2>Vxi%4#y?U% zP@b+a>0RsHZwAT_X9MN(u|zhrM($wG_-*`z2_3cWgIq2*odNy;UU3GhoKT`1BPf3^ zDg^v2;@!W__pjxA^SZ%#ceOANF2&wI!LQj|ZiY3dlq+h#SFWsl@h4#7yYU4@k!3Gq zZ<$mmG~qc(>1-H7j?3=Yh_nn%{zATrn>-KIkbFTJ@e-w>OeMo19YLQWf*!$RQ}dX*YoL zT70l+dS74u0Q(ydTHblCJTAzS9%8|KaZyYpK*($g|Iv8U^2Ik1V*WvH3WBunYU|?dD|<7W;0lFnpK^lC*a(f5Q8 zfaXw{s1IVz<@E78wr{}~H>K+8PDW`W(4N}{dcb^e8_-!)`?7Kooy*EN#mC=RPJ?SV zTx%6`(Wy!_=i%9@zu8!SYl`}g+<(r`*AAmG#Y$aR+dJb(6T&X;Fnc)+sgKGJ%g-`p(QlK2;RG{&#POuN?_y{nx z*KT7nEU41v+o5^3NXAbT`=n#^L#~?B*8zR2x*yC0MG5I6gF=g=R_UViy8QeD>*^wB z?8W!S`kiC#WkEcXsn`P60ngg9`P}1m>Z14nYJU-@c=SDi^}7ap_o2B1Yz5j!3^b1f z#XmvcRs_Y`D?!m;;v3i5*~L283SMq$Z9P=E81=>do~vX$(417;kHB%jSB!nK(Lrgv zer3KS6$*8h#8%~u%bd7V>Z3waPc?D1fDZ>16J=k1%$R9>SqMc?mG&#m z_ee1(h=g+OjoK%*kER%R7k>CAm(TZEb|>ls^^<@PGzVW3m!wL*l{D3vqg@<`zTJGu z_-9wiS0wTKvH48BUB-RfwV;@*_f~p0G5C6E_c73SHWRzk+;0bGfuh9VGfhIThR;?q zWn4XR($)*OUrf2q-KAWq0$&HZ>w(6p?>0uV+^DSV+oUQh@JS_>h|k?%n0+jVPjR5n z@Y;Mj=MdACKRpAqCoVL4&Q*Jw_Qyr8^3tQt$9>#+TH*=28D<~K5#Ive1LeSv6UR0H z{eP$ZoO7PcX0!JDMx`jH@%uG6dgw9ucbN7C6I(S2^r5*m4ZIBAZXY{Ve{VAD%6@O% z)a%@!wbKeBNdop&>?ff<6w~eo`^XJf>2KTMB=RJEOV~`gV&5qswj^Xe(M3lq z81qK(9OGX;rnz~F_(Kh-P??~8fjuSNN3HYuIil8WPTIbMKJfFv$mAfrJHLIw-+&6G z2j@ZZ@5s;PN&C6h`Tnmykl+2r7PYL_-&-a7sJC;`sKYMPtJFuCa)gIk=Zi3egDU@X G5Bv)+nYH-< literal 0 HcmV?d00001