commit 1c8238bbb711cb8a7e2fcc3e6c04a3e1a1609748
Author: bunny <1319900154@qq.com>
Date: Thu Sep 26 09:38:54 2024 +0800
init
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8f18fff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,33 @@
+services/HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### 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..1878cd5
--- /dev/null
+++ b/common/common-generator/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ cn.bunny
+ common
+ 1.0.0
+
+
+ common-generator
+
+
+
+ javax.xml.bind
+ jaxb-api
+ 2.1
+
+
+
+ com.baomidou
+ mybatis-plus-generator
+ 3.5.6
+
+
+ org.apache.velocity
+ velocity-engine-core
+ 2.3
+
+
+
+
+ src/main/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+ maven-failsafe-plugin
+ 2.22.2
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+ MainKt
+
+
+
+
+
\ No newline at end of file
diff --git a/common/common-generator/src/main/kotlin/cn/bunny/common/generator/AdminGenerator.kt b/common/common-generator/src/main/kotlin/cn/bunny/common/generator/AdminGenerator.kt
new file mode 100644
index 0000000..1db25d8
--- /dev/null
+++ b/common/common-generator/src/main/kotlin/cn/bunny/common/generator/AdminGenerator.kt
@@ -0,0 +1,82 @@
+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.*
+
+/**
+ * 代码生成器
+ */
+class AdminGenerator {
+ companion object {
+ // 数据连接
+ private const val SQL_HOST: String =
+ "jdbc:mysql://192.168.3.98:3304/auth_admin?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true"
+
+ // 作者名称
+ private const val AUTHOR: String = "Bunny"
+
+ // 公共路径
+ private const val OUTPUT_DIR: String = "D:\\MyFolder\\auth-admin\\auth-server\\services"
+
+ // 实体类名称
+ private const val ENTITY: String = "Bunny"
+
+ @JvmStatic
+ fun main(args: Array) {
+ generation("sys_user")
+ }
+
+ /**
+ * 根据表名生成相应结构代码
+ *
+ * @param tableName 表名
+ */
+ fun generation(vararg tableName: String) {
+ FastAutoGenerator.create(SQL_HOST, "root", "02120212")
+ .globalConfig { builder ->
+ // 添加作者名称
+ builder.author(AUTHOR) // 启用swagger
+ .enableSwagger() // 指定输出目录
+ .outputDir("$OUTPUT_DIR/src/main/kotlin")
+ }
+ .packageConfig { builder ->
+ builder.entity(ENTITY) // 实体类包名
+ .parent("cn.bunny.services")
+ .controller("controller") // 控制层包名
+ .mapper("mapper") // mapper层包名
+ .service("service") // service层包名
+ .serviceImpl("service.impl") // service实现类包名
+ // 自定义mapper.xml文件输出目录
+ .pathInfo(Collections.singletonMap(OutputFile.xml, "$OUTPUT_DIR/src/main/resources/mapper"))
+ }
+ .strategyConfig { builder ->
+ // 设置要生成的表名
+ builder.addInclude(*tableName)
+ .addTablePrefix("sys_")// 设置表前缀过滤
+ .entityBuilder()
+ .enableLombok()
+ .enableChainModel()
+ .naming(NamingStrategy.underline_to_camel) // 数据表映射实体命名策略:默认下划线转驼峰underline_to_camel
+ .columnNaming(NamingStrategy.underline_to_camel) // 表字段映射实体属性命名规则:默认null,不指定按照naming执行
+ .idType(IdType.AUTO) // 添加全局主键类型
+ .formatFileName("%s") // 格式化实体名称,%s取消首字母I,
+ .mapperBuilder()
+ .mapperAnnotation(Mapper::class.java) // 开启mapper注解
+ .enableBaseResultMap() // 启用xml文件中的BaseResultMap 生成
+ .enableBaseColumnList() // 启用xml文件中的BaseColumnList
+ .formatMapperFileName("%sMapper") // 格式化Dao类名称
+ .formatXmlFileName("%sMapper") // 格式化xml文件名称
+ .serviceBuilder()
+ .formatServiceFileName("%sService") // 格式化 service 接口文件名称
+ .formatServiceImplFileName("%sServiceImpl") // 格式化 service 接口文件名称
+ .controllerBuilder()
+ .enableRestStyle()
+ }
+ .execute()
+ }
+ }
+}
diff --git a/common/common-generator/src/main/kotlin/cn/bunny/common/generator/I18nGenerator.kt b/common/common-generator/src/main/kotlin/cn/bunny/common/generator/I18nGenerator.kt
new file mode 100644
index 0000000..60d45b8
--- /dev/null
+++ b/common/common-generator/src/main/kotlin/cn/bunny/common/generator/I18nGenerator.kt
@@ -0,0 +1,26 @@
+package cn.bunny.common.generator
+
+/**
+ * 代码生成器
+ */
+class I18nGenerator {
+ companion object {
+ // 数据连接
+ private const val SQL_HOST: String =
+ "jdbc:mysql://106.15.251.123:3305/auth_i18n?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true"
+
+ // 作者名称
+ private const val AUTHOR: String = "Bunny"
+
+ // 公共路径
+ private const val OUTPUT_DIR: String = "D:\\MyFolder\\auth-admin\\auth-server\\services"
+
+ // 实体类名称
+ private const val ENTITY: String = "Bunny"
+
+ @JvmStatic
+ fun main(args: Array) {
+ AdminGenerator.generation("i18n", "i18n_type")
+ }
+ }
+}
diff --git a/common/common-service/pom.xml b/common/common-service/pom.xml
new file mode 100644
index 0000000..c95fd1b
--- /dev/null
+++ b/common/common-service/pom.xml
@@ -0,0 +1,97 @@
+
+
+ 4.0.0
+
+ cn.bunny
+ common
+ 1.0.0
+
+
+ common-service
+
+
+
+
+ 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
+
+
+
+ io.minio
+ minio
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+
+ org.lionsoul
+ ip2region
+ 2.6.5
+
+
+
+
+ src/main/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ process-sources
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+ maven-failsafe-plugin
+ 2.22.2
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+ MainKt
+
+
+
+
+
\ No newline at end of file
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/config/Knife4jConfig.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/Knife4jConfig.kt
new file mode 100644
index 0000000..15860dc
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/Knife4jConfig.kt
@@ -0,0 +1,40 @@
+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
+open class Knife4jConfig {
+ @Bean
+ open fun openAPI(): OpenAPI {
+ // 作者等信息
+ val contact = Contact().name("Bunny").email("1319900154@qq.com").url("https://kotlinlang.org/docs")
+ // 使用协议
+ val license = License().name("MIT").url("https://MUT.com")
+ // 相关信息
+ val info = Info().title("Bunny-Java-Template").description("Bunny的Java模板").version("v1.0.0").contact(contact)
+ .license(license).termsOfService("维护不易~求个start")
+
+ return OpenAPI().info(info).externalDocs(ExternalDocumentation())
+ }
+
+ // 前台相关分类接口
+ @Bean
+ open fun groupedOpenApi(): GroupedOpenApi {
+ return GroupedOpenApi.builder().group("web前台接口管理").pathsToMatch("/api/**").build()
+ }
+
+ // 管理员相关分类接口
+ @Bean
+ open fun groupedOpenAdminApi(): GroupedOpenApi {
+ return GroupedOpenApi.builder().group("admin管理员接口请求").pathsToMatch("/admin/**").build()
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/config/MyBatisPlusFieldConfig.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/MyBatisPlusFieldConfig.kt
new file mode 100644
index 0000000..5e27b2a
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/MyBatisPlusFieldConfig.kt
@@ -0,0 +1,34 @@
+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.time.LocalDateTime
+
+/**
+ * 配置MP在修改和新增时的操作
+ */
+@Component
+class MyBatisPlusFieldConfig : MetaObjectHandler {
+ /**
+ * 使用mp做添加操作时候,这个方法执行
+ */
+ override fun insertFill(metaObject: MetaObject) {
+ this.strictInsertFill(metaObject, "isDeleted", Int::class.java, 0)
+
+ this.strictInsertFill(metaObject, "createTime", LocalDateTime::class.java, LocalDateTime.now())
+ this.strictInsertFill(metaObject, "updateTime", LocalDateTime::class.java, LocalDateTime.now())
+
+ this.strictInsertFill(metaObject, "createUser", Long::class.java, BaseContext.getUserId())
+ this.strictInsertFill(metaObject, "updateUser", Long::class.java, BaseContext.getUserId())
+ }
+
+ /**
+ * 使用mp做修改操作时候,这个方法执行
+ */
+ override fun updateFill(metaObject: MetaObject) {
+ this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::class.java, LocalDateTime.now())
+ this.strictUpdateFill(metaObject, "updateUser", Long::class.java, BaseContext.getUserId())
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/config/MybatisPlusConfig.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/MybatisPlusConfig.kt
new file mode 100644
index 0000000..c429e76
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/MybatisPlusConfig.kt
@@ -0,0 +1,34 @@
+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
+open class MybatisPlusConfig {
+ @Bean
+ open fun mybatisPlusInterceptor(): MybatisPlusInterceptor {
+ val interceptor = MybatisPlusInterceptor()
+ // 分页插件
+ val paginationInnerInterceptor = PaginationInnerInterceptor(DbType.MYSQL)
+ paginationInnerInterceptor.maxLimit = 100L // ? 设置最大分页为100
+ interceptor.addInnerInterceptor(paginationInnerInterceptor)
+ // 乐观锁
+ interceptor.addInnerInterceptor(OptimisticLockerInnerInterceptor())
+ // 防止全表删除
+ interceptor.addInnerInterceptor(BlockAttackInnerInterceptor())
+
+ return interceptor
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/config/RedisConfiguration.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/RedisConfiguration.kt
new file mode 100644
index 0000000..93e3996
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/RedisConfiguration.kt
@@ -0,0 +1,140 @@
+package cn.bunny.common.service.config
+
+import cn.bunny.dao.pojo.constant.LocalDateTimeConstant
+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.context.annotation.Primary
+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
+class RedisConfiguration {
+ /**
+ * 使用StringRedisSerializer序列化为字符串
+ */
+ @Bean
+ fun redisTemplate(connectionFactory: LettuceConnectionFactory?): RedisTemplate {
+ val redisTemplate = RedisTemplate()
+ redisTemplate.connectionFactory = connectionFactory
+ // 设置key序列化为string
+ redisTemplate.keySerializer = StringRedisSerializer()
+ // 设置value序列化为JSON,使用GenericJackson2JsonRedisSerializer替换默认序列化
+ redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer()
+ redisTemplate.hashKeySerializer = StringRedisSerializer()
+ redisTemplate.hashValueSerializer = GenericJackson2JsonRedisSerializer()
+
+ return redisTemplate
+ }
+
+ /**
+ * * 配置Redis过期时间30天
+ * 解决cache(@Cacheable)把数据缓存到redis中的value是乱码问题
+ */
+ @Bean
+ @Primary
+ fun cacheManagerWithMouth(factory: RedisConnectionFactory?): CacheManager {
+ // 配置序列化
+ val config = RedisCacheConfiguration.defaultCacheConfig()
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer()))
+ .entryTtl(Duration.ofDays(30))
+
+ return RedisCacheManager.builder(factory!!).cacheDefaults(config).build()
+ }
+
+ /**
+ * * 配置redis过期时间3分钟
+ *
+ * @param factory
+ * @return
+ */
+ @Bean
+ fun cacheManagerWithMinutes(factory: RedisConnectionFactory?): CacheManager {
+ // 配置序列化
+ val config = RedisCacheConfiguration.defaultCacheConfig()
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer()))
+ .entryTtl(Duration.ofMinutes(3))
+
+ return RedisCacheManager.builder(factory!!).cacheDefaults(config).build()
+ }
+
+ /**
+ * * 配置Redis过期时间1小时
+ *
+ * @param factory
+ * @return
+ */
+ @Bean
+ fun cacheManagerWithHours(factory: RedisConnectionFactory?): CacheManager {
+ // 配置序列化
+ val config = RedisCacheConfiguration.defaultCacheConfig()
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer()))
+ .entryTtl(Duration.ofHours(1))
+
+ return RedisCacheManager.builder(factory!!).cacheDefaults(config).build()
+ }
+
+ /**
+ * 指定的日期模式
+ */
+ private fun jsonRedisSerializer(): Jackson2JsonRedisSerializer {
+ val mapper = ObjectMapper()
+ // 设置ObjectMapper访问权限
+ mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
+ // 记录序列化之后的数据类型,方便反序列化
+ mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
+ // LocalDatetime序列化,默认不兼容jdk8日期序列化
+ val timeModule = JavaTimeModule()
+ timeModule.addDeserializer(
+ LocalDate::class.java,
+ LocalDateDeserializer(DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD))
+ )
+ timeModule.addSerializer(
+ LocalDate::class.java,
+ LocalDateSerializer(DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD))
+ )
+
+ timeModule.addDeserializer(
+ LocalDateTime::class.java,
+ LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD_HH_MM_SS))
+ )
+ timeModule.addSerializer(
+ LocalDateTime::class.java,
+ LocalDateTimeSerializer(DateTimeFormatter.ofPattern(LocalDateTimeConstant.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 Jackson2JsonRedisSerializer(mapper, Any::class.java)
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/config/WebMvcConfiguration.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/WebMvcConfiguration.kt
new file mode 100644
index 0000000..33b3808
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/config/WebMvcConfiguration.kt
@@ -0,0 +1,40 @@
+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
+open class WebMvcConfiguration : WebMvcConfigurer {
+ @Autowired
+ private val userTokenInterceptor: UserTokenInterceptor? = null
+
+ /**
+ * 跨域配置
+ *
+ * @param registry 跨域注册表
+ */
+ override fun addCorsMappings(registry: CorsRegistry) {
+ registry.addMapping("/**") // 是否发送Cookies
+ .allowCredentials(true) // 放行哪些原始域
+ .allowedOriginPatterns("*").allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("*")
+ .exposedHeaders("*")
+ }
+
+ override fun addInterceptors(registry: InterceptorRegistry) {
+ // TODO Spring Security 和这个拦截器任选一个
+ val excludeList = arrayOf(
+ "/", "/test/**", "/*.html", "/*/*/noAuth/**", "/*/noAuth/**", "/favicon.ico",
+ "/swagger-resources/**", "/swagger-ui.html/**", "/admin/login", "/v3/**", "/api/**"
+ )
+ registry.addInterceptor(userTokenInterceptor!!).excludePathPatterns(*excludeList)
+
+ // TODO 如果想使用普通JWT可以使用这个,不使用 SpringSecurity6
+ // registry.addInterceptor(userTokenInterceptor).addPathPatterns("/api/**").excludePathPatterns(excludeList);
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/context/BaseContext.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/context/BaseContext.kt
new file mode 100644
index 0000000..e4a985a
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/context/BaseContext.kt
@@ -0,0 +1,50 @@
+package cn.bunny.common.service.context
+
+import cn.bunny.dao.vo.user.LoginVo
+
+open class BaseContext {
+ companion object {
+ private val userId = ThreadLocal()
+ private val username = ThreadLocal()
+ private val loginVo = ThreadLocal()
+
+ // 用户id相关
+ @JvmStatic
+ fun getUserId(): Long {
+ return userId.get()
+ }
+
+ @JvmStatic
+ fun setUserId(id: Long) {
+ userId.set(id)
+ }
+
+ @JvmStatic
+ fun getUsername(): String {
+ return username.get()
+ }
+
+ @JvmStatic
+ fun setUsername(username: String) {
+ this.username.set(username)
+ }
+
+ @JvmStatic
+ fun getLoginVo(): LoginVo {
+ return loginVo.get()
+ }
+
+ @JvmStatic
+ fun setLoginVo(loginVo: LoginVo) {
+ this.loginVo.set(loginVo)
+ }
+
+ @JvmStatic
+ fun removeUser() {
+ username.remove()
+ userId.remove()
+ loginVo.remove()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/BunnyException.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/BunnyException.kt
new file mode 100644
index 0000000..ce9c616
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/BunnyException.kt
@@ -0,0 +1,31 @@
+package cn.bunny.common.service.exception
+
+import cn.bunny.dao.pojo.result.ResultCodeEnum
+import lombok.Getter
+import lombok.ToString
+import lombok.extern.slf4j.Slf4j
+
+@Getter
+@ToString
+@Slf4j
+class BunnyException : RuntimeException {
+ // 状态码
+ var code: Int? = null
+
+ // 描述信息
+ override var message: String
+
+ constructor(code: Int?, message: String) : super(message) {
+ this.code = code
+ this.message = message
+ }
+
+ constructor(message: String) : super(message) {
+ this.message = message
+ }
+
+ constructor(codeEnum: ResultCodeEnum) : super(codeEnum.message) {
+ this.code = codeEnum.code
+ this.message = codeEnum.message
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/GlobalExceptionHandler.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/GlobalExceptionHandler.kt
new file mode 100644
index 0000000..2bd5277
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/exception/GlobalExceptionHandler.kt
@@ -0,0 +1,98 @@
+package cn.bunny.common.service.exception
+
+import cn.bunny.dao.pojo.constant.ExceptionConstant
+import cn.bunny.dao.pojo.result.Result
+import cn.bunny.dao.pojo.result.ResultCodeEnum
+import lombok.extern.slf4j.Slf4j
+import org.apache.logging.log4j.LogManager
+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
+import java.util.regex.Pattern
+
+
+@RestControllerAdvice
+@Slf4j
+class GlobalExceptionHandler {
+ private val logger = LogManager.getLogger(GlobalExceptionHandler::class.java)
+
+ // 自定义异常信息
+ @ExceptionHandler(BunnyException::class)
+ @ResponseBody
+ fun exceptionHandler(exception: BunnyException): Result {
+ logger.error("GlobalExceptionHandler===>自定义异常信息:{}", exception.message)
+
+ val code = if (exception.code != null) exception.code else 500
+ return Result.error(null, code, exception.message)
+ }
+
+ // 运行时异常信息
+ @ExceptionHandler(RuntimeException::class)
+ @ResponseBody
+ @Throws(FileNotFoundException::class)
+ fun exceptionHandler(exception: RuntimeException): Result {
+ logger.error("GlobalExceptionHandler===>运行时异常信息:{}", exception.message)
+ exception.printStackTrace()
+ return Result.error(null, 500, "出错了啦")
+ }
+
+ // 捕获系统异常
+ @ExceptionHandler(Exception::class)
+ @ResponseBody
+ fun error(exception: Exception): Result {
+ logger.error("GlobalExceptionHandler===>系统异常信息:{}", exception.message)
+
+ // 错误消息
+ val message = exception.message ?: ""
+ // 匹配到内容
+ val patternString = "Request method '(\\w+)' is not supported"
+ val matcher = Pattern.compile(patternString).matcher(message)
+
+ return when {
+ matcher.find() -> Result.error(null, 500, "请求方法错误,不是 " + matcher.group(1))
+ else -> Result.error(null, 500, "系统异常")
+ }
+ }
+
+ // 特定异常处理
+ @ExceptionHandler(ArithmeticException::class)
+ @ResponseBody
+ fun error(exception: ArithmeticException): Result {
+ logger.error("GlobalExceptionHandler===>特定异常信息:{}", exception.message)
+
+ return Result.error(null, 500, exception.message)
+ }
+
+ // spring security异常
+ @ExceptionHandler(AccessDeniedException::class)
+ @ResponseBody
+ @Throws(
+ AccessDeniedException::class
+ )
+ fun error(exception: AccessDeniedException): Result {
+ logger.error("GlobalExceptionHandler===>spring security异常:{}", exception.message)
+
+ return Result.error(ResultCodeEnum.SERVICE_ERROR)
+ }
+
+ // 处理SQL异常
+ @ExceptionHandler(SQLIntegrityConstraintViolationException::class)
+ @ResponseBody
+ fun exceptionHandler(exception: SQLIntegrityConstraintViolationException): Result {
+ logger.error("GlobalExceptionHandler===>处理SQL异常:{}", exception.message)
+
+ val message = exception.message
+ if (message!!.contains("Duplicate entry")) {
+ // 截取用户名
+ val username = message.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[2]
+ // 错误信息
+ val errorMessage = username + ExceptionConstant.ALREADY_USER_EXCEPTION
+ return Result.error(errorMessage)
+ } else {
+ return Result.error(ExceptionConstant.UNKNOWN_EXCEPTION)
+ }
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/EmptyUtil.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/EmptyUtil.kt
new file mode 100644
index 0000000..f196a64
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/EmptyUtil.kt
@@ -0,0 +1,25 @@
+package cn.bunny.common.service.utils
+
+import cn.bunny.common.service.exception.BunnyException
+import lombok.extern.slf4j.Slf4j
+import org.apache.logging.log4j.LogManager
+import org.springframework.util.StringUtils
+
+@Slf4j
+object EmptyUtil {
+ private val logger = LogManager.getLogger(EmptyUtil::class.java)
+
+ /**
+ * 是否为空
+ *
+ * @param value 判断值
+ * @param message 错误消息
+ */
+ @JvmStatic
+ fun isEmpty(value: Any?, message: String?) {
+ if (value == null || !StringUtils.hasText(value.toString())) {
+ logger.error("为空对象错误:{},{}", value, message)
+ throw BunnyException(message!!)
+ }
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/FileUtil.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/FileUtil.kt
new file mode 100644
index 0000000..8c3fff7
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/FileUtil.kt
@@ -0,0 +1,27 @@
+package cn.bunny.common.service.utils
+
+import org.springframework.stereotype.Component
+
+/**
+ * * 计算文件大小
+ */
+@Component
+class FileUtil {
+ companion object {
+ /**
+ * * 获取文件大小字符串
+ */
+ fun getSize(fileSize: Long): String {
+ val fileSizeInKB = fileSize / 1024.00
+ val fileSizeInMB = fileSizeInKB / 1024
+ val fileSizeInGB = fileSizeInMB / 1024
+
+ return when {
+ fileSizeInGB >= 1 -> "${String.format("%.2f", fileSizeInGB).toDouble()}GB"
+ fileSizeInMB >= 1 -> "${String.format("%.2f", fileSizeInMB).toDouble()}MB"
+ fileSizeInKB >= 1 -> "${String.format("%.2f", fileSizeInKB).toDouble()}KB"
+ else -> "${fileSize}B"
+ }
+ }
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/IpUtil.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/IpUtil.kt
new file mode 100644
index 0000000..21c797c
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/IpUtil.kt
@@ -0,0 +1,66 @@
+package cn.bunny.common.service.utils
+
+import cn.bunny.common.service.exception.BunnyException
+import jakarta.annotation.PostConstruct
+import lombok.extern.slf4j.Slf4j
+import org.apache.logging.log4j.LogManager
+import org.lionsoul.ip2region.xdb.Searcher
+import org.springframework.core.io.ClassPathResource
+import org.springframework.util.FileCopyUtils
+import java.util.regex.Pattern
+
+@Slf4j
+class IpUtil {
+
+ companion object {
+ private val logger = LogManager.getLogger(IpUtil::class.java)
+ private var searcher: Searcher? = null
+
+ /**
+ * 判断是否为合法 IP
+ */
+ private fun checkIp(ipAddress: String): Boolean {
+ val ip = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}"
+ val pattern = Pattern.compile(ip)
+ val matcher = pattern.matcher(ipAddress)
+
+ return matcher.matches()
+ }
+
+ /**
+ * 在服务启动时,将 ip2region 加载到内存中
+ */
+ @PostConstruct
+ private fun initIp2Region() {
+ try {
+ val inputStream = ClassPathResource("/ipdb/ip2region.xdb").inputStream
+ val bytes = FileCopyUtils.copyToByteArray(inputStream)
+ searcher = Searcher.newWithBuffer(bytes)
+ } catch (exception: Exception) {
+ logger.error(exception.message)
+ }
+ }
+
+ /**
+ * 获取 ip 所属地址
+ *
+ * @param checkIp ip
+ */
+ fun getIpRegion(checkIp: String): String {
+ val ip = if (checkIp == "0:0:0:0:0:0:0:1") "127.0.0.1" else checkIp
+ if (!checkIp(ip)) throw BunnyException("非法的IP地址")
+
+ val searchIpInfo = searcher?.search(ip)
+ return searchIpInfo?.let { it ->
+ val splitIpInfo = it.split("\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ if (splitIpInfo.isNotEmpty()) {
+ when {
+ splitIpInfo[0] == "中国" -> splitIpInfo[2] // 国内属地返回省份
+ splitIpInfo[0] == "0" && splitIpInfo[4] == "内网IP" -> splitIpInfo[4] // 内网 IP
+ else -> splitIpInfo[0] // 国外属地返回国家
+ }
+ } else ""
+ } ?: ""
+ }
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/JwtHelper.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/JwtHelper.kt
new file mode 100644
index 0000000..76166d1
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/JwtHelper.kt
@@ -0,0 +1,368 @@
+package cn.bunny.common.service.utils
+
+import io.jsonwebtoken.CompressionCodecs
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.SignatureAlgorithm
+import io.micrometer.common.lang.Nullable
+import org.springframework.util.StringUtils
+import java.util.*
+
+object JwtHelper {
+ // 时间 按天 计算
+ private const val TOKEN_EXPIRATION = (24 * 60 * 60 * 1000).toLong()
+
+ // JWT 的 秘钥
+ private const val TOKEN_SIGN_KEY = "Bunny-Java-Template"
+
+ // 默认主题
+ private const val SUBJECT = "Bunny"
+
+ // 默认时间
+ private val time = Date(System.currentTimeMillis() + TOKEN_EXPIRATION * 7)
+
+ /**
+ * 使用默认主题,默认时间,默认秘钥,创建自定义集合token
+ *
+ * @param map 集合
+ * @return token
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?): String {
+ return Jwts.builder()
+ .setSubject(SUBJECT)
+ .setExpiration(time)
+ .signWith(SignatureAlgorithm.HS256, TOKEN_SIGN_KEY)
+ .setClaims(map)
+ .setId(UUID.randomUUID().toString())
+ .compressWith(CompressionCodecs.GZIP).compact()
+ }
+
+ /**
+ * 使用默认主题,默认秘钥,自定义时间,创建集合形式token
+ *
+ * @param map 集合
+ * @param time 过期时间
+ * @return token
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, time: Date?): String {
+ return Jwts.builder()
+ .setSubject(SUBJECT)
+ .signWith(SignatureAlgorithm.HS256, TOKEN_SIGN_KEY)
+ .setExpiration(time)
+ .setClaims(map)
+ .setId(UUID.randomUUID().toString())
+ .compressWith(CompressionCodecs.GZIP).compact()
+ }
+
+ /**
+ * 使用默认主题,默认秘钥,自定义时间,创建集合形式token
+ *
+ * @param map 集合
+ * @param day 过期时间
+ * @return token
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, day: Int): String {
+ return Jwts.builder()
+ .setSubject(SUBJECT)
+ .signWith(SignatureAlgorithm.HS256, TOKEN_SIGN_KEY)
+ .setExpiration(Date(System.currentTimeMillis() + TOKEN_EXPIRATION * day))
+ .setClaims(map)
+ .setId(UUID.randomUUID().toString())
+ .compressWith(CompressionCodecs.GZIP).compact()
+ }
+
+ /**
+ * 使用默认主题,默认秘钥,自定义key,创建集合形式token
+ *
+ * @param map 集合
+ * @param tokenSignKey 自定义key
+ * @return token
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, tokenSignKey: String?): String {
+ 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
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, subject: String?, time: Date?): String {
+ return Jwts.builder()
+ .setSubject(subject)
+ .setExpiration(time)
+ .setClaims(map)
+ .setId(UUID.randomUUID().toString())
+ .signWith(SignatureAlgorithm.HS256, TOKEN_SIGN_KEY)
+ .compressWith(CompressionCodecs.GZIP)
+ .compact()
+ }
+
+ /**
+ * 创建集合形式token
+ *
+ * @param map 集合
+ * @param subject 主题
+ * @param tokenSignKey 过期时间
+ * @return token
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, subject: String?, tokenSignKey: String?): String {
+ 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
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, tokenSignKey: String?, time: Int): String {
+ return Jwts.builder()
+ .setSubject(SUBJECT)
+ .setExpiration(Date(System.currentTimeMillis() + TOKEN_EXPIRATION * time))
+ .setClaims(map)
+ .setId(UUID.randomUUID().toString())
+ .signWith(SignatureAlgorithm.HS256, tokenSignKey)
+ .compressWith(CompressionCodecs.GZIP)
+ .compact()
+ }
+
+ /**
+ * 创建集合形式token
+ *
+ * @param map 集合
+ * @param subject 主题
+ * @param day 过期时间
+ * @return token
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, subject: String?, tokenSignKey: String?, day: Int): String {
+ return Jwts.builder()
+ .setSubject(subject)
+ .setExpiration(Date(System.currentTimeMillis() + TOKEN_EXPIRATION * day))
+ .setClaims(map)
+ .setId(UUID.randomUUID().toString())
+ .signWith(SignatureAlgorithm.HS256, tokenSignKey)
+ .compressWith(CompressionCodecs.GZIP)
+ .compact()
+ }
+
+ /**
+ * 创建集合形式token
+ *
+ * @param map 集合
+ * @param subject 主题
+ * @param time 过期时间
+ * @return token
+ */
+ @JvmStatic
+ fun createTokenWithMap(map: Map?, subject: String?, tokenSignKey: String?, time: Date?): String {
+ 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值
+ */
+ @JvmStatic
+ fun createToken(userId: Long?, userName: String?, day: Int): String {
+ return Jwts.builder()
+ .setSubject(SUBJECT)
+ .setExpiration(Date(System.currentTimeMillis() + TOKEN_EXPIRATION * day))
+ .claim("userId", userId)
+ .claim("userName", userName)
+ .setId(UUID.randomUUID().toString())
+ .signWith(SignatureAlgorithm.HS256, TOKEN_SIGN_KEY)
+ .compressWith(CompressionCodecs.GZIP)
+ .compact()
+ }
+
+ /**
+ * 使用token获取map集合,使用默认秘钥
+ *
+ * @param token token
+ * @return map集合
+ */
+ @JvmStatic
+ fun getMapByToken(token: String?): Map? {
+ try {
+ if (!StringUtils.hasText(token)) return null
+ val claims = Jwts.parser().setSigningKey(TOKEN_SIGN_KEY).parseClaimsJws(token).body
+
+ // 将 body 值转为map
+ return HashMap(claims)
+ } catch (exception: Exception) {
+ return null
+ }
+ }
+
+ /**
+ * 使用token获取map集合
+ *
+ * @param token token
+ * @param signKey 秘钥
+ * @return map集合
+ */
+ @JvmStatic
+ fun getMapByToken(token: String?, signKey: String?): Map? {
+ try {
+ if (!StringUtils.hasText(token)) return null
+ val claimsJws = Jwts.parser().setSigningKey(signKey).parseClaimsJws(token)
+ val body = claimsJws.body
+ // 将 body 值转为map
+ return HashMap(body)
+ } catch (exception: Exception) {
+ return null
+ }
+ }
+
+ /**
+ * 根据token获取主题
+ *
+ * @param token token
+ * @return 主题
+ */
+ @JvmStatic
+ fun getSubjectByToken(token: String): String? {
+ return getSubjectByTokenHandler(token, TOKEN_SIGN_KEY)
+ }
+
+ @Nullable
+ @JvmStatic
+ private fun getSubjectByTokenHandler(token: String, tokenSignKey: String): String? {
+ try {
+ if (!StringUtils.hasText(token)) return null
+ val claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token)
+ val body = claimsJws.body
+
+ return body.subject
+ } catch (exception: Exception) {
+ return null
+ }
+ }
+
+ /**
+ * 根据token获取主题
+ *
+ * @param token token
+ * @return 主题
+ */
+ @JvmStatic
+ fun getSubjectByToken(token: String, tokenSignKey: String): String? {
+ return getSubjectByTokenHandler(token, tokenSignKey)
+ }
+
+ /**
+ * 根据token获取用户ID
+ *
+ * @param token token
+ * @return 用户ID
+ */
+ @JvmStatic
+ fun getUserId(token: String?): Long? {
+ try {
+ if (!StringUtils.hasText(token)) return null
+
+ val claimsJws = Jwts.parser().setSigningKey(TOKEN_SIGN_KEY).parseClaimsJws(token)
+ val claims = claimsJws.body
+
+ return claims["userId"].toString().toLong()
+ } catch (exception: Exception) {
+ return null
+ }
+ }
+
+ /**
+ * 根据token获取用户名
+ *
+ * @param token token
+ * @return 用户名
+ */
+ @JvmStatic
+ fun getUsername(token: String?): String? {
+ try {
+ if (!StringUtils.hasText(token)) return ""
+
+ val claimsJws = Jwts.parser().setSigningKey(TOKEN_SIGN_KEY).parseClaimsJws(token)
+ val claims = claimsJws.body
+ return claims["userName"] as String?
+ } catch (exception: Exception) {
+ return null
+ }
+ }
+
+ /**
+ * 判断token是否过期
+ *
+ * @param token token
+ * @return 是否过期
+ */
+ @JvmStatic
+ fun isExpired(token: String): Boolean {
+ return isExpiredUtil(token, TOKEN_SIGN_KEY)
+ }
+
+ /**
+ * 判断token是否过期
+ *
+ * @param token token
+ * @return 是否过期
+ */
+ @JvmStatic
+ fun isExpired(token: String, tokenSignKey: String): Boolean {
+ return isExpiredUtil(token, tokenSignKey)
+ }
+
+ /**
+ * 判断是否过期
+ *
+ * @param token token
+ * @param tokenSignKey key值
+ * @return 是否过期
+ */
+ @JvmStatic
+ private fun isExpiredUtil(token: String, tokenSignKey: String): Boolean {
+ try {
+ val claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token)
+ val expiration = claimsJws.body.expiration
+
+ return expiration != null && expiration.before(Date())
+ } catch (exception: Exception) {
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/ResponseHandlerUtil.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/ResponseHandlerUtil.kt
new file mode 100644
index 0000000..5103749
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/ResponseHandlerUtil.kt
@@ -0,0 +1,14 @@
+package cn.bunny.common.service.utils
+
+import cn.bunny.dao.pojo.result.Result
+import cn.bunny.dao.pojo.result.ResultCodeEnum
+import jakarta.servlet.http.HttpServletResponse
+
+class ResponseHandlerUtil {
+ companion object {
+ fun loginAuthHandler(response: HttpServletResponse, loginAuth: ResultCodeEnum): Boolean {
+ ResponseUtil.out(response, Result.error(loginAuth))
+ return false
+ }
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/ResponseUtil.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/ResponseUtil.kt
new file mode 100644
index 0000000..3d13b59
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/ResponseUtil.kt
@@ -0,0 +1,29 @@
+package cn.bunny.common.service.utils
+
+import cn.bunny.dao.pojo.email.EmailSend
+import cn.bunny.dao.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
+
+open class ResponseUtil {
+ companion object {
+ @JvmStatic
+ fun out(response: HttpServletResponse, result: Result?) {
+ try {
+ val mapper = ObjectMapper()
+ // 注册JavaTimeModule模块
+ mapper.registerModule(JavaTimeModule())
+ response.contentType = "application/json;charset=UTF-8"
+ response.status = HttpStatus.OK.value()
+ mapper.writeValue(response.writer, result)
+ val emailSend = EmailSend()
+
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/MailSendCheckUtil.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/MailSendCheckUtil.kt
new file mode 100644
index 0000000..30a06aa
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/MailSendCheckUtil.kt
@@ -0,0 +1,26 @@
+package cn.bunny.common.service.utils.email
+
+import cn.bunny.common.service.utils.EmptyUtil.isEmpty
+import cn.bunny.dao.pojo.constant.MailMessageConstant
+import cn.bunny.dao.pojo.email.EmailSend
+
+class MailSendCheckUtil {
+ companion object {
+ /**
+ * 检测发送对象是否为空的对象
+ *
+ * @param emailSend 邮件发送对象
+ */
+ @JvmStatic
+ fun check(emailSend: EmailSend) {
+ // 空发送对象
+ isEmpty(emailSend, MailMessageConstant.EMPTY_SEND_OBJECT)
+ // 收件人不能为空
+ isEmpty(emailSend.sendTo, MailMessageConstant.ADDRESS_NOT_NULL)
+ // 标题不能为空
+ isEmpty(emailSend.subject, MailMessageConstant.TITLE_NOT_NULL)
+ // 发送消息不能为空
+ isEmpty(emailSend.message, MailMessageConstant.SEND_MESSAGE_NOT_NULL)
+ }
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/MailSenderUtil.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/MailSenderUtil.kt
new file mode 100644
index 0000000..06e1077
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/MailSenderUtil.kt
@@ -0,0 +1,64 @@
+package cn.bunny.common.service.utils.email
+
+import cn.bunny.common.service.utils.email.MailSendCheckUtil.Companion.check
+import cn.bunny.dao.pojo.email.EmailSend
+import cn.bunny.dao.pojo.email.EmailSendInit
+import jakarta.mail.MessagingException
+import org.springframework.mail.javamail.JavaMailSenderImpl
+import org.springframework.mail.javamail.MimeMessageHelper
+import java.util.*
+
+class MailSenderUtil(emailSendInit: EmailSendInit) {
+ private val username: String?
+ private val javaMailSender: JavaMailSenderImpl
+
+ /**
+ * 初始化构造函数进行当前类赋值
+ */
+ init {
+ val javaMailSender = JavaMailSenderImpl()
+ javaMailSender.host = emailSendInit.host
+ javaMailSender.port = emailSendInit.port!!
+ javaMailSender.username = emailSendInit.username
+ javaMailSender.password = emailSendInit.password
+ javaMailSender.protocol = "smtps"
+ javaMailSender.defaultEncoding = "UTF-8"
+
+ this.username = emailSendInit.username
+ this.javaMailSender = javaMailSender
+ }
+
+ /**
+ * 综合邮箱发送
+ *
+ * @param emailSend 邮件消息
+ */
+ @Throws(MessagingException::class)
+ fun sendEmail(emailSend: EmailSend) {
+ check(emailSend)
+
+ // 创建 MimeMessage 对象用于发送邮件富文本或者附件
+ val message = javaMailSender.createMimeMessage()
+ // 创建 MimeMessageHelper
+ val helper = MimeMessageHelper(message, true)
+
+ // 设置发送人
+ helper.setFrom(username!!)
+ // 设置邮件接受者
+ helper.setTo(emailSend.sendTo!!)
+ // 设置邮件主题
+ helper.setSubject(emailSend.subject!!)
+ // 设置发送消息 为富文本
+ helper.setText(emailSend.message!!, emailSend.isRichText!!)
+ // 设置抄送人
+ helper.setCc(emailSend.ccParam!!.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
+ // 邮件添加附件
+ val files = emailSend.file
+ for (file in files!!) {
+ helper.addAttachment(Objects.requireNonNull(file.originalFilename), file)
+ }
+
+ // 发送邮件
+ javaMailSender.send(message)
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/template-propties b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/template-propties
new file mode 100644
index 0000000..2ce3978
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/email/template-propties
@@ -0,0 +1,21 @@
+mail:
+ host: smtp.qq.com # 邮箱地址
+ port: 465 # 邮箱端口号
+ username: xxx@qq.com # 设置发送邮箱
+ password: xx # 如果是纯数字要加引号
+ default-encoding: UTF-8 # 设置编码格式
+ protocol: smtps
+ properties:
+ mail:
+ debug: true # 是否开启debug模式发送邮件
+ smtp:
+ auth: true
+ connectionTimeout: 5000 # 设置连接延迟
+ timeout: 5000 # 延迟时间
+ writeTimeout: 5000 # 写入邮箱延迟
+ allow8BitMime: true
+ sendPartial: true
+ ssl:
+ enabled: true # 是否开启SSL连接
+ socketFactory:
+ class: javax.net.ssl.SSLSocketFactory # 必要设置!!!
\ No newline at end of file
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/minio/MinioProperties.kt b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/minio/MinioProperties.kt
new file mode 100644
index 0000000..603ee19
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/minio/MinioProperties.kt
@@ -0,0 +1,26 @@
+package cn.bunny.common.service.utils.minio
+
+import io.minio.MinioClient
+import lombok.Data
+import lombok.extern.slf4j.Slf4j
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+@ConfigurationProperties(prefix = "bunny.minio")
+@ConditionalOnProperty(name = ["bunny.minio.bucket-name"]) // 当属性有值时这个配置才生效
+@Data
+@Slf4j
+open class MinioProperties {
+ val endpointUrl: String? = null
+ val accessKey: String? = null
+ val secretKey: String? = null
+ val bucketName: String? = null
+
+ @Bean
+ open fun minioClient(): MinioClient {
+ return MinioClient.builder().endpoint(endpointUrl).credentials(accessKey, secretKey).build()
+ }
+}
diff --git a/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/minio/MinioUtil.java b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/minio/MinioUtil.java
new file mode 100644
index 0000000..2ead1df
--- /dev/null
+++ b/common/common-service/src/main/kotlin/cn/bunny/common/service/utils/minio/MinioUtil.java
@@ -0,0 +1,142 @@
+package cn.bunny.common.service.utils.minio;
+
+import cn.bunny.common.service.exception.BunnyException;
+import cn.bunny.dao.pojo.constant.FileMessageConstant;
+import cn.bunny.dao.pojo.constant.MinioConstant;
+import cn.bunny.dao.pojo.file.MinioFIlePath;
+import io.minio.GetObjectArgs;
+import io.minio.GetObjectResponse;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+/**
+ * Minio操作工具类 简化操作步骤
+ * By:Bunny0212
+ */
+@Component
+@Slf4j
+public class MinioUtil {
+ @Autowired
+ private MinioProperties properties;
+ @Autowired
+ private MinioClient minioClient;
+
+ /**
+ * 获取Minio文件路径
+ */
+ public static MinioFIlePath getMinioFilePath(String buckName, String minioPreType, MultipartFile file) {
+ String uuid = UUID.randomUUID().toString();
+ // 定义日期时间格式
+ LocalDateTime currentDateTime = LocalDateTime.now();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM-dd");
+ String extension = "";
+
+ // 原始文件名
+ String filename = file.getOriginalFilename();
+ if (StringUtils.hasText(filename) && filename.contains(".")) {
+ extension = "." + filename.substring(filename.lastIndexOf(".") + 1);
+ }
+
+ // UUID防止重名
+ String uuidFilename = uuid + extension;
+
+ // 拼接时间+UUID文件名
+ String timeUuidFilename = currentDateTime.format(formatter) + "/" + uuidFilename;// 加上时间路径
+
+ // 上传根文件夹+拼接时间+UUID文件名
+ String filepath = MinioConstant.getType(minioPreType) + timeUuidFilename;
+
+ // 桶名称+上传根文件夹+拼接时间+UUID文件名
+ String buckNameFilepath = "/" + buckName + MinioConstant.getType(minioPreType) + timeUuidFilename;
+
+ // 设置及Minio基础信息
+ MinioFIlePath minioFIlePath = new MinioFIlePath();
+ minioFIlePath.setFilename(filename);
+ minioFIlePath.setUuidFilename(uuidFilename);
+ minioFIlePath.setTimeUuidFilename(timeUuidFilename);
+ minioFIlePath.setFilepath(filepath);
+ minioFIlePath.setBucketNameFilepath(buckNameFilepath);
+
+ return minioFIlePath;
+ }
+
+ /**
+ * * 上传文件并返回处理信息
+ */
+ public MinioFIlePath getUploadMinioObjectFilePath(MultipartFile file, String minioPreType) throws IOException {
+ // 如果buckName为空,设置为默认的桶
+ String bucketName = properties.getBucketName();
+ if (file != null) {
+ MinioFIlePath minioFile = getMinioFilePath(bucketName, minioPreType, file);
+ String filepath = minioFile.getFilepath();
+
+ // 上传对象
+ putObject(bucketName, filepath, file.getInputStream(), file.getSize());
+
+ // 设置图片地址
+ return minioFile;
+ }
+ return null;
+ }
+
+ /**
+ * 获取默认bucket文件,并返回字节数组
+ *
+ * @param objectName 对象名称
+ * @return 文件流对象
+ */
+ public byte[] getBucketObjectByte(String objectName) {
+ // 如果buckName为空,设置为默认的桶
+ String bucketName = properties.getBucketName();
+
+ try {
+ objectName = objectName.replace("/" + bucketName, "");
+ GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
+
+ return getObjectResponse.readAllBytes();
+ } catch (Exception exception) {
+ exception.getStackTrace();
+ }
+ throw new BunnyException(FileMessageConstant.GET_BUCKET_EXCEPTION);
+ }
+
+ /**
+ * 获取Minio全路径名,Object带有桶名称
+ *
+ * @param objectName 对象名称
+ * @return 全路径
+ */
+ public String getObjectNameFullPath(String objectName) {
+ String url = properties.getEndpointUrl();
+
+ return url + objectName;
+ }
+
+ /**
+ * 上传文件
+ *
+ * @param bucketName 桶名称
+ * @param filename 文件名
+ * @param inputStream 输入流
+ * @param size 大小
+ */
+ public void putObject(String bucketName, String filename, InputStream inputStream, Long size) {
+ try {
+ minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(filename).stream(inputStream, size, -1).build());
+ } catch (Exception exception) {
+ exception.getStackTrace();
+ throw new BunnyException(FileMessageConstant.FILE_UPLOAD_EXCEPTION);
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/pom.xml b/common/pom.xml
new file mode 100644
index 0000000..37baf9f
--- /dev/null
+++ b/common/pom.xml
@@ -0,0 +1,64 @@
+
+ 4.0.0
+
+ cn.bunny
+ kotlin-single
+ 0.0.1-SNAPSHOT
+
+
+ common
+ 1.0.0
+ pom
+
+ common
+ https://maven.apache.org
+
+ common-generator
+ common-service
+
+
+
+ UTF-8
+ official
+ 17
+ 17
+ 2.0.20
+
+
+
+
+
+ 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
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+ com.zaxxer
+ HikariCP
+
+
+
diff --git a/dao/pom.xml b/dao/pom.xml
new file mode 100644
index 0000000..59a58b9
--- /dev/null
+++ b/dao/pom.xml
@@ -0,0 +1,129 @@
+
+
+ 4.0.0
+
+ cn.bunny
+ kotlin-single
+ 0.0.1-SNAPSHOT
+
+
+ dao
+
+
+ UTF-8
+ official
+ 17
+ 17
+ 2.0.20
+
+
+
+
+ mavenCentral
+ https://repo1.maven.org/maven2/
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+
+
+
+ com.github.xiaoymin
+ knife4j-openapi3-jakarta-spring-boot-starter
+
+
+
+ com.alibaba.fastjson2
+ fastjson2
+
+
+
+ io.swagger
+ swagger-annotations
+ 1.6.14
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ 2.0.0
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit
+ ${kotlin.version}
+
+
+
+
+ src/main/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+ compile
+ process-sources
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+ maven-failsafe-plugin
+ 2.22.2
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+ MainKt
+
+
+
+
+
\ No newline at end of file
diff --git a/dao/src/main/kotlin/cn/bunny/dao/dto/email/EmailTemplateDto.kt b/dao/src/main/kotlin/cn/bunny/dao/dto/email/EmailTemplateDto.kt
new file mode 100644
index 0000000..fde361e
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/dto/email/EmailTemplateDto.kt
@@ -0,0 +1,25 @@
+package cn.bunny.dao.dto.email
+
+import io.swagger.v3.oas.annotations.media.Schema
+import jakarta.validation.constraints.NotBlank
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.NoArgsConstructor
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Schema(name = "EmailTemplateDto", title = "邮箱模板请求内容", description = "邮箱模板请求内容")
+class EmailTemplateDto {
+ @Schema(name = "templateName", title = "模板名称")
+ var templateName: @NotBlank(message = "模板名称不能为空") String? = null
+
+ @Schema(name = "subject", title = "主题")
+ var subject: @NotBlank(message = "主题不能为空") String? = null
+
+ @Schema(name = "body", title = "邮件内容")
+ var body: @NotBlank(message = "邮件内容不能为空") String? = null
+
+ @Schema(name = "type", title = "邮件类型")
+ var type: String? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/dto/email/EmailUsersDto.kt b/dao/src/main/kotlin/cn/bunny/dao/dto/email/EmailUsersDto.kt
new file mode 100644
index 0000000..5cc1dd4
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/dto/email/EmailUsersDto.kt
@@ -0,0 +1,40 @@
+package cn.bunny.dao.dto.email
+
+import io.swagger.v3.oas.annotations.media.Schema
+import jakarta.validation.constraints.NotBlank
+import jakarta.validation.constraints.NotNull
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+
+/**
+ * 添加邮箱用户
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Schema(name = "EmailUsersDto", title = "邮箱用户发送基础内容", description = "邮箱用户发送基础内容")
+class EmailUsersDto {
+ @Schema(name = "id", title = "主键")
+ var id: @NotBlank(message = "id不能为空") Long? = null
+
+ @Schema(name = "email", title = "邮箱")
+ var email: @NotBlank(message = "邮箱不能为空") String? = null
+
+ @Schema(name = "password", title = "密码")
+ var password: @NotBlank(message = "密码不能为空") String? = null
+
+ @Schema(name = "host", title = "SMTP服务器")
+ var host: String? = null
+
+ @Schema(name = "port", title = "端口号")
+ var port: @NotNull(message = "端口号不能为空") Int? = null
+
+ @Schema(name = "smtpAgreement", title = "邮箱协议")
+ var smtpAgreement: Int? = null
+
+ @Schema(name = "isDefault", title = "是否为默认邮件")
+ var isDefault: Boolean? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/dto/user/LoginDto.kt b/dao/src/main/kotlin/cn/bunny/dao/dto/user/LoginDto.kt
new file mode 100644
index 0000000..313582c
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/dto/user/LoginDto.kt
@@ -0,0 +1,24 @@
+package cn.bunny.dao.dto.user
+
+import io.swagger.v3.oas.annotations.media.Schema
+import jakarta.validation.constraints.NotBlank
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Schema(name = "LoginDto", title = "登录表单内容", description = "登录表单内容")
+class LoginDto {
+ @Schema(name = "username", title = "用户名")
+ var username: @NotBlank(message = "用户名不能为空") String? = null
+
+ @Schema(name = "password", title = "密码")
+ var password: @NotBlank(message = "密码不能为空") String? = null
+
+ @Schema(name = "emailCode", title = "邮箱验证码")
+ var emailCode: @NotBlank(message = "邮箱验证码不能为空") String? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/entity/BaseEntity.kt b/dao/src/main/kotlin/cn/bunny/dao/entity/BaseEntity.kt
new file mode 100644
index 0000000..d65f110
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/entity/BaseEntity.kt
@@ -0,0 +1,46 @@
+package cn.bunny.dao.entity
+
+import com.alibaba.fastjson2.annotation.JSONField
+import com.baomidou.mybatisplus.annotation.*
+import com.fasterxml.jackson.annotation.JsonFormat
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
+import io.swagger.annotations.ApiModelProperty
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.Data
+import java.io.Serializable
+import java.time.LocalDateTime
+
+@Data
+@Schema(name = "BaseEntity", title = "基础实体类型", description = "基础实体类型")
+open class BaseEntity : Serializable {
+ @Schema(name = "id", title = "唯一标识")
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ @JsonFormat(shape = JsonFormat.Shape.STRING)
+ @JSONField(serializeUsing = ToStringSerializer::class)
+ var id: Long? = null
+
+ @ApiModelProperty("创建时间")
+ @Schema(name = "createTime", title = "创建时间")
+ @TableField(fill = FieldFill.INSERT)
+ var createTime: LocalDateTime? = null
+
+ @Schema(name = "updateTime", title = "更新时间")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ var updateTime: LocalDateTime? = null
+
+ @Schema(name = "createUser", title = "创建用户")
+ @TableField(fill = FieldFill.INSERT)
+ @JsonFormat(shape = JsonFormat.Shape.STRING)
+ @JSONField(serializeUsing = ToStringSerializer::class)
+ var createUser: Long? = null
+
+ @Schema(name = "updateUser", title = "操作用户")
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ @JsonFormat(shape = JsonFormat.Shape.STRING)
+ @JSONField(serializeUsing = ToStringSerializer::class)
+ var updateUser: Long? = null
+
+ @Schema(name = "isDeleted", title = "是否被删除")
+ @TableLogic
+ var isDeleted: Boolean? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/entity/email/EmailTemplate.kt b/dao/src/main/kotlin/cn/bunny/dao/entity/email/EmailTemplate.kt
new file mode 100644
index 0000000..ed75e66
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/entity/email/EmailTemplate.kt
@@ -0,0 +1,36 @@
+package cn.bunny.dao.entity.email
+
+import cn.bunny.dao.entity.BaseEntity
+import com.baomidou.mybatisplus.annotation.TableName
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.Getter
+import lombok.Setter
+import lombok.experimental.Accessors
+
+/**
+ *
+ *
+ *
+ *
+ *
+ * @author Bunny
+ * @since 2024-05-19
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+@TableName("email_template")
+@Schema(name = "EmailTemplate", title = "邮件模板", description = "邮件模板")
+class EmailTemplate : BaseEntity() {
+ @Schema(name = "templateName", title = "模板名称")
+ var templateName: String? = null
+
+ @Schema(name = "subject", title = "主题")
+ var subject: String? = null
+
+ @Schema(name = "body", title = "邮件内容")
+ var body: String? = null
+
+ @Schema(name = "type", title = "邮件类型")
+ var type: String? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/entity/email/EmailUsers.kt b/dao/src/main/kotlin/cn/bunny/dao/entity/email/EmailUsers.kt
new file mode 100644
index 0000000..73450c1
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/entity/email/EmailUsers.kt
@@ -0,0 +1,42 @@
+package cn.bunny.dao.entity.email
+
+import cn.bunny.dao.entity.BaseEntity
+import com.baomidou.mybatisplus.annotation.TableName
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.Getter
+import lombok.Setter
+import lombok.experimental.Accessors
+
+/**
+ *
+ *
+ * 邮箱发送表
+ *
+ *
+ * @author Bunny
+ * @since 2024-05-17
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+@TableName("email_users")
+@Schema(name = "EmailUsers对象", title = "邮箱发送表", description = "邮箱发送表")
+class EmailUsers : BaseEntity() {
+ @Schema(name = "email", title = "邮箱")
+ var email: String? = null
+
+ @Schema(name = "password", title = "密码")
+ var password: String? = null
+
+ @Schema(name = "host", title = "Host地址")
+ var host: String? = null
+
+ @Schema(name = "port", title = "端口号")
+ var port: Int? = null
+
+ @Schema(name = "smtpAgreement", title = "邮箱协议")
+ var smtpAgreement: String? = null
+
+ @Schema(name = "isDefault", title = "是否为默认邮件")
+ var isDefault: Int? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/entity/i18n/I18n.kt b/dao/src/main/kotlin/cn/bunny/dao/entity/i18n/I18n.kt
new file mode 100644
index 0000000..aa11434
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/entity/i18n/I18n.kt
@@ -0,0 +1,31 @@
+package cn.bunny.dao.entity.i18n
+
+import cn.bunny.dao.entity.BaseEntity
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.Getter
+import lombok.Setter
+import lombok.experimental.Accessors
+
+/**
+ *
+ *
+ * 多语言表
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+@Schema(name = "I18n对象", title = "多语言表", description = "多语言表")
+class I18n : BaseEntity() {
+ @Schema(name = "typeId", title = "语言类型id")
+ var typeId: Long? = null
+
+ @Schema(name = "keyName", title = "多语言key")
+ var keyName: String? = null
+
+ @Schema(name = "summary", title = "翻译")
+ var summary: String? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/entity/i18n/I18nType.kt b/dao/src/main/kotlin/cn/bunny/dao/entity/i18n/I18nType.kt
new file mode 100644
index 0000000..35765b5
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/entity/i18n/I18nType.kt
@@ -0,0 +1,33 @@
+package cn.bunny.dao.entity.i18n
+
+import cn.bunny.dao.entity.BaseEntity
+import com.baomidou.mybatisplus.annotation.TableName
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.Getter
+import lombok.Setter
+import lombok.experimental.Accessors
+
+/**
+ *
+ *
+ * 多语言类型表
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+@TableName("i18n_type")
+@Schema(name = "I18nType", title = "多语言类型表", description = "多语言类型表")
+class I18nType : BaseEntity() {
+ @Schema(name = "languageName", title = "语言名称")
+ var languageName: String? = null
+
+ @Schema(name = "summary", title = "语言名")
+ var summary: String? = null
+
+ @Schema(name = "isDefault", title = "是否作为默认语言")
+ var isDefault: Byte? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/entity/user/AdminUser.kt b/dao/src/main/kotlin/cn/bunny/dao/entity/user/AdminUser.kt
new file mode 100644
index 0000000..cdf5ec6
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/entity/user/AdminUser.kt
@@ -0,0 +1,57 @@
+package cn.bunny.dao.entity.user
+
+import cn.bunny.dao.entity.BaseEntity
+import com.baomidou.mybatisplus.annotation.TableName
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.Getter
+import lombok.Setter
+import lombok.experimental.Accessors
+
+/**
+ *
+ *
+ * 用户信息
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+@TableName("sys_user")
+@Schema(name = "AdminUser对象", title = "用户信息", description = "用户信息")
+class AdminUser : BaseEntity() {
+ @Schema(name = "username", title = "用户名")
+ var username: String? = null
+
+ @Schema(name = "nickName", title = "昵称")
+ var nickName: String? = null
+
+ @Schema(name = "email", title = "邮箱")
+ var email: String? = null
+
+ @Schema(name = "phone", title = "手机号")
+ var phone: String? = null
+
+ @Schema(name = "password", title = "密码")
+ var password: String? = null
+
+ @Schema(name = "avatar", title = "头像")
+ var avatar: String? = null
+
+ @Schema(name = "sex", title = "性别", description = "0:女 1:男")
+ var sex: Byte? = null
+
+ @Schema(name = "summary", title = "个人描述")
+ var summary: String? = null
+
+ @Schema(name = "lastLoginIp", title = "最后登录IP")
+ var lastLoginIp: String? = null
+
+ @Schema(name = "lastLoginIpAddress", title = "最后登录ip归属地")
+ var lastLoginIpAddress: String? = null
+
+ @Schema(name = "status", title = "状态", description = "1:禁用 0:正常")
+ var status: Byte? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/ExceptionConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/ExceptionConstant.kt
new file mode 100644
index 0000000..1e5ae96
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/ExceptionConstant.kt
@@ -0,0 +1,23 @@
+package cn.bunny.dao.pojo.constant
+
+import lombok.Data
+
+@Data
+class ExceptionConstant {
+ companion object {
+ const val UNKNOWN_EXCEPTION: String = "未知错误"
+
+ // 用户相关
+ const val USER_NOT_LOGIN_EXCEPTION: String = "用户未登录"
+ const val USERNAME_IS_EMPTY_EXCEPTION: String = "用户名不能为空"
+ const val ALREADY_USER_EXCEPTION: String = "用户已存在"
+ const val USER_NOT_FOUND_EXCEPTION: String = "用户不存在"
+
+ // 密码相关
+ const val PASSWORD_EXCEPTION: String = "密码错误"
+ const val PASSWORD_NOT_EMPTY_EXCEPTION: String = "密码不能为空"
+ const val OLD_PASSWORD_EXCEPTION: String = "旧密码不匹配"
+ const val PASSWORD_EDIT_EXCEPTION: String = "密码修改失败"
+ const val OLD_PASSWORD_SAME_NEW_PASSWORD_EXCEPTION: String = "旧密码与新密码相同"
+ }
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/FileMessageConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/FileMessageConstant.kt
new file mode 100644
index 0000000..8132594
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/FileMessageConstant.kt
@@ -0,0 +1,12 @@
+package cn.bunny.dao.pojo.constant
+
+import lombok.Data
+
+@Data
+class FileMessageConstant {
+ companion object {
+ const val STORAGE_OBJECT_EXCEPTION: String = "对象错误"
+ const val GET_BUCKET_EXCEPTION: String = "获取文件信息失败"
+ const val FILE_UPLOAD_EXCEPTION: String = "文件上传失败"
+ }
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/LocalDateTimeConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/LocalDateTimeConstant.kt
new file mode 100644
index 0000000..c351641
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/LocalDateTimeConstant.kt
@@ -0,0 +1,11 @@
+package cn.bunny.dao.pojo.constant
+
+import lombok.Data
+
+@Data
+class LocalDateTimeConstant {
+ companion object {
+ const val YYYY_MM_DD: String = "yyyy-MM-dd"
+ const val YYYY_MM_DD_HH_MM_SS: String = "yyyy-MM-dd HH:mm:ss"
+ }
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/MailMessageConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/MailMessageConstant.kt
new file mode 100644
index 0000000..fecead2
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/MailMessageConstant.kt
@@ -0,0 +1,17 @@
+package cn.bunny.dao.pojo.constant
+
+import lombok.Data
+
+/**
+ * 邮箱消息
+ */
+@Data
+class MailMessageConstant {
+ companion object {
+ const val EMPTY_SEND_OBJECT: String = "空发送对象"
+ const val ADDRESS_NOT_NULL: String = "收件人不能为空"
+ const val TITLE_NOT_NULL: String = "标题不能为空"
+ const val SEND_MESSAGE_NOT_NULL: String = "发送消息不能为空"
+ const val EMAIL_CONFIG_NOT_FOUND: String = "邮箱配置为空"
+ }
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/MinioConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/MinioConstant.kt
new file mode 100644
index 0000000..f4d3a7c
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/MinioConstant.kt
@@ -0,0 +1,39 @@
+package cn.bunny.dao.pojo.constant
+
+import lombok.Data
+
+@Data
+class MinioConstant {
+ companion object {
+ private const val FAVICON: String = "favicon"
+ private const val AVATAR: String = "avatar"
+ private const val ARTICLE: String = "article"
+ private const val CAROUSEL: String = "carousel"
+ private const val FEEDBACK: String = "feedback"
+ private const val ARTICLE_COVERS: String = "articleCovers"
+ private const val ARTICLE_ATTACHMENT: String = "articleAttachment"
+ private val typeMap: MutableMap = HashMap()
+
+ init {
+ typeMap[FAVICON] = "/favicon/"
+ typeMap[AVATAR] = "/avatar/"
+ typeMap[ARTICLE] = "/article/"
+ typeMap[CAROUSEL] = "/carousel/"
+ typeMap[FEEDBACK] = "/feedback/"
+ typeMap["articleImages"] = "/articleImages/"
+ typeMap["articleVideo"] = "/articleVideo/"
+ typeMap[ARTICLE_COVERS] = "/articleCovers/"
+ typeMap[ARTICLE_ATTACHMENT] = "/articleAttachment/"
+ typeMap["images"] = "/images/"
+ typeMap["video"] = "/video/"
+ typeMap["default"] = "/default/"
+ }
+
+ @JvmStatic
+ fun getType(type: String): String {
+ val value = typeMap[type]
+ if (value != null) return value
+ throw RuntimeException(FileMessageConstant.STORAGE_OBJECT_EXCEPTION)
+ }
+ }
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/RedisUserConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/RedisUserConstant.kt
new file mode 100644
index 0000000..ad08558
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/RedisUserConstant.kt
@@ -0,0 +1,75 @@
+package cn.bunny.dao.pojo.constant
+
+import lombok.Data
+
+/**
+ * Redis用户前缀设置
+ */
+@Data
+class RedisUserConstant {
+ companion object {
+ // 管理员用户
+ private const val ADMIN_LOGIN_INFO_PREFIX: String = "ADMIN::LOGIN_INFO::"
+ private const val ADMIN_EMAIL_CODE_PREFIX: String = "ADMIN::EMAIL_CODE::"
+
+ // 普通用户
+ private const val USER_LOGIN_INFO_PREFIX: String = "USER::LOGIN_INFO::"
+ private const val USER_EMAIL_CODE_PREFIX: String = "USER::EMAIL_CODE::"
+ private const val USER_DO_LIKE_PREFIX: String = "USER::doLike::"
+
+ /**
+ * * 管理员用户登录信息
+ *
+ * @param adminUser 管理员用户
+ * @return 登录信息key
+ */
+ @JvmStatic
+ fun getAdminLoginInfoPrefix(adminUser: String): String {
+ return ADMIN_LOGIN_INFO_PREFIX + adminUser
+ }
+
+ /**
+ * * 管理员用户邮箱验证码
+ *
+ * @param adminUser 管理员用户
+ * @return 管理员用户邮箱验证码key
+ */
+ @JvmStatic
+ fun getAdminUserEmailCodePrefix(adminUser: String): String {
+ return ADMIN_EMAIL_CODE_PREFIX + adminUser
+ }
+
+ /**
+ * * 用户登录信息
+ *
+ * @param user 用户名
+ * @return 登录信息key
+ */
+ @JvmStatic
+ fun getUserLoginInfoPrefix(user: String): String {
+ return USER_LOGIN_INFO_PREFIX + user
+ }
+
+ /**
+ * * 用户邮箱验证码
+ *
+ * @param user 用户名
+ * @return 用户邮箱验证码key
+ */
+ @JvmStatic
+ fun getUserEmailCodePrefix(user: String): String {
+ return USER_EMAIL_CODE_PREFIX + user
+ }
+
+ /**
+ * * 用户点赞操作
+ *
+ * @param user 用户名
+ * @return 用户点赞key
+ */
+ @JvmStatic
+ fun getUserDoLikePrefix(user: String): String {
+ return USER_DO_LIKE_PREFIX + user
+ }
+ }
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/UserConstant.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/UserConstant.kt
new file mode 100644
index 0000000..9a86195
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/constant/UserConstant.kt
@@ -0,0 +1,11 @@
+package cn.bunny.dao.pojo.constant
+
+import lombok.Data
+
+@Data
+class UserConstant {
+ companion object {
+ const val USER_AVATAR: String =
+ "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132"
+ }
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/email/EmailSend.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/email/EmailSend.kt
new file mode 100644
index 0000000..54472a6
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/email/EmailSend.kt
@@ -0,0 +1,37 @@
+package cn.bunny.dao.pojo.email
+
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+import org.springframework.web.multipart.MultipartFile
+
+/**
+ * 邮件发送对象
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(name = "EmailSend", title = "邮件发送表单", description = "邮件发送表单")
+class EmailSend {
+
+ @Schema(name = "sendTo", title = "给谁发送")
+ var sendTo: String? = null
+
+ @Schema(name = "subject", title = "发送主题")
+ var subject: String? = null
+
+ @Schema(name = "isRichText", title = "是否为富文本")
+ var isRichText: Boolean? = null
+
+ @Schema(name = "message", title = "发送内容")
+ var message: String? = null
+
+ @Schema(name = "ccParam", title = "抄送人")
+ var ccParam: String? = null
+
+ @Schema(name = "file", title = "发送的文件")
+ var file: Array? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/email/EmailSendInit.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/email/EmailSendInit.kt
new file mode 100644
index 0000000..ed6415b
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/email/EmailSendInit.kt
@@ -0,0 +1,31 @@
+package cn.bunny.dao.pojo.email
+
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+
+/**
+ * 邮箱发送初始化参数
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(name = "EmailSendInit", title = "邮件发送初始化", description = "邮件发送初始化")
+class EmailSendInit {
+
+ @Schema(name = "port", title = "端口")
+ var port: Int? = null
+
+ @Schema(name = "host", title = "主机")
+ var host: String? = null
+
+ @Schema(name = "username", title = "用户名")
+ var username: String? = null
+
+ @Schema(name = "password", title = "密码")
+ var password: String? = null
+
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/enums/OperationType.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/enums/OperationType.kt
new file mode 100644
index 0000000..c14aff3
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/enums/OperationType.kt
@@ -0,0 +1,8 @@
+package cn.bunny.dao.pojo.enums
+
+/**
+ * 数据库操作类型
+ */
+enum class OperationType {
+ UPDATE, INSERT
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/file/MinioFIlePath.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/file/MinioFIlePath.kt
new file mode 100644
index 0000000..5d7cf6f
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/file/MinioFIlePath.kt
@@ -0,0 +1,18 @@
+package cn.bunny.dao.pojo.file
+
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+class MinioFIlePath {
+ var filename: String? = null
+ var uuidFilename: String? = null
+ var timeUuidFilename: String? = null
+ var filepath: String? = null
+ var bucketNameFilepath: String? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/result/PageResult.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/PageResult.kt
new file mode 100644
index 0000000..a45df3a
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/PageResult.kt
@@ -0,0 +1,28 @@
+package cn.bunny.dao.pojo.result
+
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+import java.io.Serializable
+
+/**
+ * 封装分页查询结果
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+class PageResult : Serializable {
+ // 当前页
+ var pageNo: Int? = null
+
+ // 每页记录数
+ var pageSize: Int? = null
+
+ // 总记录数
+ var total: Long = 0
+
+ // 当前页数据集合
+ var list: List? = null
+}
\ No newline at end of file
diff --git a/dao/src/main/kotlin/cn/bunny/dao/pojo/result/Result.java b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/Result.java
new file mode 100644
index 0000000..d941e7c
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/Result.java
@@ -0,0 +1,173 @@
+package cn.bunny.dao.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/kotlin/cn/bunny/dao/pojo/result/ResultCodeEnum.kt b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/ResultCodeEnum.kt
new file mode 100644
index 0000000..f257b08
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/pojo/result/ResultCodeEnum.kt
@@ -0,0 +1,51 @@
+package cn.bunny.dao.pojo.result
+
+import lombok.Getter
+
+/**
+ * 统一返回结果状态信息类
+ */
+@Getter
+enum class ResultCodeEnum(val code: Int, val message: String) {
+ // 成功操作 200
+ SUCCESS(200, "操作成功"),
+ SUCCESS_LOGOUT(200, "退出成功"),
+ EMAIL_CODE_REFRESH(200, "邮箱验证码已刷新"),
+
+ // 验证错误 201
+ USERNAME_OR_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, "失败"),
+}
\ No newline at end of file
diff --git a/dao/src/main/kotlin/cn/bunny/dao/vo/BaseVo.kt b/dao/src/main/kotlin/cn/bunny/dao/vo/BaseVo.kt
new file mode 100644
index 0000000..e6715c3
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/vo/BaseVo.kt
@@ -0,0 +1,46 @@
+package cn.bunny.dao.vo
+
+import com.alibaba.fastjson2.annotation.JSONField
+import com.fasterxml.jackson.annotation.JsonFormat
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.Data
+import java.io.Serializable
+import java.time.LocalDateTime
+
+@Data
+@Schema(name = "BaseVo", title = "基础返回对象内容", description = "基础返回对象内容")
+open class BaseVo : Serializable {
+ @Schema(name = "id", title = "主键")
+ @JsonProperty("id")
+ @JsonFormat(shape = JsonFormat.Shape.STRING)
+ @JSONField(serializeUsing = ToStringSerializer::class)
+ var id: Long? = null
+
+ @Schema(name = "updateTime", title = "更新时间")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonSerialize(using = LocalDateTimeSerializer::class)
+ @JsonDeserialize(using = LocalDateTimeDeserializer::class)
+ var updateTime: LocalDateTime? = null
+
+ @Schema(name = "createTime", title = "发布时间")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonSerialize(using = LocalDateTimeSerializer::class)
+ @JsonDeserialize(using = LocalDateTimeDeserializer::class)
+ var createTime: LocalDateTime? = null
+
+ @Schema(name = "createUser", title = "创建用户")
+ @JsonFormat(shape = JsonFormat.Shape.STRING)
+ @JSONField(serializeUsing = ToStringSerializer::class)
+ var createUser: Long? = null
+
+ @Schema(name = "updateUser", title = "操作用户")
+ @JsonFormat(shape = JsonFormat.Shape.STRING)
+ @JSONField(serializeUsing = ToStringSerializer::class)
+ var updateUser: Long? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/vo/email/EmailTemplateVo.kt b/dao/src/main/kotlin/cn/bunny/dao/vo/email/EmailTemplateVo.kt
new file mode 100644
index 0000000..9454c60
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/vo/email/EmailTemplateVo.kt
@@ -0,0 +1,23 @@
+package cn.bunny.dao.vo.email
+
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Schema(name = "EmailTemplateVo对象", title = "邮箱模板返回内容", description = "邮箱模板返回内容")
+class EmailTemplateVo {
+ @Schema(name = "templateName", title = "模板名称")
+ var templateName: String? = null
+
+ @Schema(name = "subject", title = "主题")
+ var subject: String? = null
+
+ @Schema(name = "body", title = "邮件内容")
+ var body: String? = null
+}
\ No newline at end of file
diff --git a/dao/src/main/kotlin/cn/bunny/dao/vo/user/LoginVo.kt b/dao/src/main/kotlin/cn/bunny/dao/vo/user/LoginVo.kt
new file mode 100644
index 0000000..9b412d2
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/vo/user/LoginVo.kt
@@ -0,0 +1,70 @@
+package cn.bunny.dao.vo.user
+
+import cn.bunny.dao.vo.BaseVo
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.*
+
+/**
+ * 用户登录返回内容
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Schema(name = "LoginVo对象", title = "登录成功返回内容", description = "登录成功返回内容")
+class LoginVo : BaseVo() {
+ @Schema(name = "nickName", title = "昵称")
+ var nickName: String? = null
+
+ @Schema(name = "username", title = "用户名")
+ var username: String? = null
+
+ @Schema(name = "email", title = "邮箱")
+ var email: String? = null
+
+ @Schema(name = "phone", title = "手机号")
+ var phone: String? = null
+
+ @Schema(name = "password", title = "密码")
+ var password: String? = null
+
+ @Schema(name = "avatar", title = "头像")
+ var avatar: String? = null
+
+ @Schema(name = "sex", title = "0:女 1:男")
+ var sex: Byte? = null
+
+ @Schema(name = "personDescription", title = "个人描述")
+ var personDescription: String? = null
+
+ @Schema(name = "articleMode", title = "文章显示模式")
+ var articleMode: String? = null
+
+ @Schema(name = "layout", title = "页面布局方式")
+ var layout: String? = null
+
+ @Schema(name = "lastLoginIp", title = "最后登录IP")
+ var lastLoginIp: String? = null
+
+ @Schema(name = "lastLoginIpAddress", title = "最后登录ip地址")
+ var lastLoginIpAddress: String? = null
+
+ @Schema(name = "totalIntegral", title = "积分")
+ var totalIntegral: Int? = null
+
+ @Schema(name = "currentIntegral", title = "当前积分")
+ var currentIntegral: Int? = null
+
+ @Schema(name = "status", title = "0:禁用 1:正常")
+ var status: Boolean? = null
+
+ @Schema(name = "token", title = "令牌")
+ var token: String? = null
+
+ @Schema(name = "roleList", title = "角色列表")
+ var roleList: List? = null
+
+ @Schema(name = "powerList", title = "权限列表")
+ var powerList: List? = null
+}
diff --git a/dao/src/main/kotlin/cn/bunny/dao/vo/user/ValidateCodeVo.kt b/dao/src/main/kotlin/cn/bunny/dao/vo/user/ValidateCodeVo.kt
new file mode 100644
index 0000000..76c91b1
--- /dev/null
+++ b/dao/src/main/kotlin/cn/bunny/dao/vo/user/ValidateCodeVo.kt
@@ -0,0 +1,20 @@
+package cn.bunny.dao.vo.user
+
+import io.swagger.v3.oas.annotations.media.Schema
+import lombok.AllArgsConstructor
+import lombok.Builder
+import lombok.Data
+import lombok.NoArgsConstructor
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(name = "ValidateCodeVo", title = "验证码响应结果实体类", description = "验证码响应结果实体类")
+class ValidateCodeVo {
+ @Schema(name = "codeKey", title = "验证码key")
+ var codeKey: String? = null
+
+ @Schema(name = "codeValue", title = "验证码value")
+ var codeValue: String? = null
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..df34d84
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,199 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.3
+
+
+ cn.bunny
+ kotlin-single
+ 0.0.1-SNAPSHOT
+ pom
+ bunny-template
+ bunny-template
+
+
+ services
+ common
+ dao
+
+
+
+ 17
+ 17
+ 17
+ 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
+ 2.0.20
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-test
+ ${kotlin.version}
+ test
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+ ${maven.compiler.target}
+
+
+
+
+
diff --git a/services/Dockerfile b/services/Dockerfile
new file mode 100644
index 0000000..4b3e1e6
--- /dev/null
+++ b/services/Dockerfile
@@ -0,0 +1,21 @@
+FROM openjdk:17
+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/services/pom.xml b/services/pom.xml
new file mode 100644
index 0000000..4323c25
--- /dev/null
+++ b/services/pom.xml
@@ -0,0 +1,182 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.3
+
+
+ cn.bunny
+ services
+ 0.0.1-SNAPSHOT
+ services
+ services
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UTF-8
+ official
+ 17
+ 17
+ 2.0.20
+
+
+
+ cn.bunny
+ common-service
+ 1.0.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.security
+ spring-security-test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+ org.springframework.amqp
+ spring-rabbit-test
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+ 2.16.0-rc1
+
+
+
+ org.aspectj
+ aspectjrt
+
+
+ org.aspectj
+ aspectjweaver
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+ com.baomidou
+ dynamic-datasource-spring-boot3-starter
+ 4.3.1
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit
+ ${kotlin.version}
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ -Xjsr305=strict
+
+
+ spring
+
+ ${java.version}
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ default-compile
+ none
+
+
+ default-testCompile
+ none
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ testCompile
+ test-compile
+
+ testCompile
+
+
+
+
+
+
+
+
diff --git a/services/src/main/kotlin/cn/bunny/services/ServicesApplication.kt b/services/src/main/kotlin/cn/bunny/services/ServicesApplication.kt
new file mode 100644
index 0000000..995a34d
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/ServicesApplication.kt
@@ -0,0 +1,28 @@
+package cn.bunny.services
+
+import lombok.extern.slf4j.Slf4j
+import org.mybatis.spring.annotation.MapperScan
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.cache.annotation.EnableCaching
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.scheduling.annotation.EnableScheduling
+import org.springframework.transaction.annotation.EnableTransactionManagement
+
+@MapperScan("cn.bunny.services.mapper")
+@ComponentScan("cn.bunny")
+@EnableScheduling
+@EnableCaching
+@EnableTransactionManagement
+@Slf4j
+@SpringBootApplication
+class ServicesApplication {
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ runApplication(*args)
+ }
+ }
+}
+
+
diff --git a/services/src/main/kotlin/cn/bunny/services/controller/I18nController.java b/services/src/main/kotlin/cn/bunny/services/controller/I18nController.java
new file mode 100644
index 0000000..2c624c2
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/controller/I18nController.java
@@ -0,0 +1,20 @@
+package cn.bunny.services.controller;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * 多语言表 前端控制器
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@RestController
+@RequestMapping("/i18n")
+@Tag(name = "多语言", description = "多语言相关接口")
+public class I18nController {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/controller/I18nTypeController.java b/services/src/main/kotlin/cn/bunny/services/controller/I18nTypeController.java
new file mode 100644
index 0000000..b380356
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/controller/I18nTypeController.java
@@ -0,0 +1,20 @@
+package cn.bunny.services.controller;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * 多语言类型表 前端控制器
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@RestController
+@RequestMapping("/i18nType")
+@Tag(name = "多语言类型", description = "多语言类型相关接口")
+public class I18nTypeController {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/controller/IndexController.kt b/services/src/main/kotlin/cn/bunny/services/controller/IndexController.kt
new file mode 100644
index 0000000..7411636
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/controller/IndexController.kt
@@ -0,0 +1,34 @@
+package cn.bunny.services.controller
+
+import cn.hutool.captcha.CaptchaUtil
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.tags.Tag
+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 = "访问首页内容", description = "访问首页内容相关接口")
+@RestController
+@RequestMapping("/")
+class IndexController {
+ @Operation(summary = "访问首页", description = "访问首页")
+ @GetMapping("")
+ fun index(): String {
+ return "欢迎访问 Bunny Java Template,欢迎去Gitee:https://gitee.com/BunnyBoss/java_single.git"
+ }
+
+ @Operation(summary = "生成验证码", description = "生成验证码")
+ @GetMapping("noAuth/checkCode")
+ fun checkCode(): ResponseEntity {
+ val headers = HttpHeaders()
+ headers.contentType = MediaType.IMAGE_JPEG
+ // 生成验证码
+ val captcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2)
+ val image = captcha.imageBytes
+ return ResponseEntity(image, headers, HttpStatus.OK)
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/controller/UserController.kt b/services/src/main/kotlin/cn/bunny/services/controller/UserController.kt
new file mode 100644
index 0000000..8d9ee8d
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/controller/UserController.kt
@@ -0,0 +1,19 @@
+package cn.bunny.services.controller
+
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ *
+ *
+ * 用户信息 前端控制器
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@RestController
+@RequestMapping("/user")
+@Tag(name = "系统用户", description = "系统用户相关接口")
+class UserController
diff --git a/services/src/main/kotlin/cn/bunny/services/mapper/I18nMapper.java b/services/src/main/kotlin/cn/bunny/services/mapper/I18nMapper.java
new file mode 100644
index 0000000..dae6df7
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/mapper/I18nMapper.java
@@ -0,0 +1,18 @@
+package cn.bunny.services.mapper;
+
+import cn.bunny.dao.entity.i18n.I18n;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ *
+ * 多语言表 Mapper 接口
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Mapper
+public interface I18nMapper extends BaseMapper {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/mapper/I18nTypeMapper.java b/services/src/main/kotlin/cn/bunny/services/mapper/I18nTypeMapper.java
new file mode 100644
index 0000000..53e40fc
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/mapper/I18nTypeMapper.java
@@ -0,0 +1,18 @@
+package cn.bunny.services.mapper;
+
+import cn.bunny.dao.entity.i18n.I18nType;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ *
+ * 多语言类型表 Mapper 接口
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Mapper
+public interface I18nTypeMapper extends BaseMapper {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/mapper/UserMapper.java b/services/src/main/kotlin/cn/bunny/services/mapper/UserMapper.java
new file mode 100644
index 0000000..5957bda
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/mapper/UserMapper.java
@@ -0,0 +1,18 @@
+package cn.bunny.services.mapper;
+
+import cn.bunny.dao.entity.user.AdminUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ *
+ * 用户信息 Mapper 接口
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Mapper
+public interface UserMapper extends BaseMapper {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/config/WebSecurityConfig.kt b/services/src/main/kotlin/cn/bunny/services/security/config/WebSecurityConfig.kt
new file mode 100644
index 0000000..84c75c1
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/config/WebSecurityConfig.kt
@@ -0,0 +1,102 @@
+package cn.bunny.services.security.config
+
+import cn.bunny.services.security.custom.CustomPasswordEncoder
+import cn.bunny.services.security.filter.TokenAuthenticationFilter
+import cn.bunny.services.security.filter.TokenLoginFilterService
+import cn.bunny.services.security.handelr.SecurityAccessDeniedHandler
+import cn.bunny.services.security.handelr.SecurityAuthenticationEntryPoint
+import cn.bunny.services.security.service.CustomAuthorizationManagerService
+import cn.bunny.services.security.service.CustomUserDetailsService
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.builders.WebSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
+import org.springframework.security.config.annotation.web.configurers.*
+import org.springframework.security.core.session.SessionRegistry
+import org.springframework.security.core.session.SessionRegistryImpl
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+import org.springframework.security.web.util.matcher.RegexRequestMatcher
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+open class WebSecurityConfig {
+ @Autowired
+ private val redisTemplate: RedisTemplate? = null
+
+ // 自定义用户接口
+ @Autowired
+ private val customUserDetailsService: CustomUserDetailsService? = null
+
+ // 自定义密码加密器
+ @Autowired
+ private val customPasswordEncoder: CustomPasswordEncoder? = null
+
+ // 自定义验证码
+ @Autowired
+ private val customAuthorizationManager: CustomAuthorizationManagerService? = null
+
+ @Autowired
+ private val authenticationConfiguration: AuthenticationConfiguration? = null
+
+ @Bean
+ @Throws(Exception::class)
+ open fun filterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
+ httpSecurity // 前端段分离不需要---禁用明文验证
+ .httpBasic { obj: HttpBasicConfigurer -> obj.disable() } // 前端段分离不需要---禁用默认登录页
+ .formLogin { obj: FormLoginConfigurer -> obj.disable() } // 前端段分离不需要---禁用退出页
+ .logout { obj: LogoutConfigurer -> obj.disable() } // 前端段分离不需要---csrf攻击
+ .csrf { obj: CsrfConfigurer -> obj.disable() } // 跨域访问权限,如果需要可以关闭后自己配置跨域访问
+ .cors { obj: CorsConfigurer -> obj.disable() } // 前后端分离不需要---因为是无状态的
+ .sessionManagement { obj: SessionManagementConfigurer -> obj.disable() } // 前后端分离不需要---记住我,e -> e.rememberMeParameter("rememberBunny").rememberMeCookieName("rememberBunny").key("BunnyKey")
+ .rememberMe { obj: RememberMeConfigurer -> obj.disable() }
+ .authorizeHttpRequests { authorize ->
+ // 有样式文件,不需要访问权限
+ // authorize.requestMatchers(RegexRequestMatcher.regexMatcher("^\\S*[css|js]$")).permitAll()
+ authorize.requestMatchers(RegexRequestMatcher.regexMatcher("^.*\\.(css|js)$")).permitAll()
+ // 上面都不是需要鉴权访问
+ authorize.anyRequest().access(customAuthorizationManager)
+ }
+ .exceptionHandling { exception: ExceptionHandlingConfigurer ->
+ // 请求未授权接口
+ exception.authenticationEntryPoint(SecurityAuthenticationEntryPoint())
+ // 没有权限访问
+ exception.accessDeniedHandler(SecurityAccessDeniedHandler())
+ } // 登录验证过滤器
+ .addFilterBefore(
+ TokenLoginFilterService(authenticationConfiguration!!, redisTemplate!!, customUserDetailsService!!),
+ UsernamePasswordAuthenticationFilter::class.java
+ ) // 其它权限鉴权过滤器
+ .addFilterAt(
+ TokenAuthenticationFilter(redisTemplate),
+ UsernamePasswordAuthenticationFilter::class.java
+ ) // 自定义密码加密器和用户登录
+ .passwordManagement(customPasswordEncoder).userDetailsService(customUserDetailsService)
+
+ return httpSecurity.build()
+ }
+
+ @Bean
+ fun sessionRegistry(): SessionRegistry {
+ return SessionRegistryImpl()
+ }
+
+ // 排出鉴定路径
+ @Bean
+ open fun webSecurityCustomizer(): WebSecurityCustomizer {
+ val annotations = arrayOf(
+ "/", "/ws/**",
+ "/*/*/noAuth/**", "/*/noAuth/**", "/noAuth/**",
+ "/favicon.ico", "*.html",
+ "/swagger-resources/**", "/v3/**", "/swagger-ui/**"
+ )
+ return WebSecurityCustomizer { web: WebSecurity -> web.ignoring().requestMatchers(*annotations) }
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/custom/CustomPasswordEncoder.kt b/services/src/main/kotlin/cn/bunny/services/security/custom/CustomPasswordEncoder.kt
new file mode 100644
index 0000000..e459f58
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/custom/CustomPasswordEncoder.kt
@@ -0,0 +1,25 @@
+package cn.bunny.services.security.custom
+
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.Customizer
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.PasswordManagementConfigurer
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.util.DigestUtils
+
+/**
+ * 自定义密码加密比对
+ */
+@Configuration
+class CustomPasswordEncoder : PasswordEncoder, Customizer?> {
+ override fun encode(rawPassword: CharSequence): String {
+ return DigestUtils.md5DigestAsHex(rawPassword.toString().toByteArray())
+ }
+
+ override fun matches(rawPassword: CharSequence, encodedPassword: String): Boolean {
+ return encodedPassword.matches(DigestUtils.md5DigestAsHex(rawPassword.toString().toByteArray()).toRegex())
+ }
+
+ override fun customize(httpSecurityPasswordManagementConfigurer: PasswordManagementConfigurer?) {
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/custom/CustomUser.kt b/services/src/main/kotlin/cn/bunny/services/security/custom/CustomUser.kt
new file mode 100644
index 0000000..ccbbc07
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/custom/CustomUser.kt
@@ -0,0 +1,15 @@
+package cn.bunny.services.security.custom
+
+import cn.bunny.dao.entity.user.AdminUser
+import lombok.Getter
+import lombok.Setter
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.userdetails.User
+
+/**
+ * 重写自带的User
+ */
+@Getter
+@Setter
+class CustomUser(user: AdminUser, authorities: Collection?) :
+ User(user.email, user.password, authorities)
\ No newline at end of file
diff --git a/services/src/main/kotlin/cn/bunny/services/security/filter/TokenAuthenticationFilter.kt b/services/src/main/kotlin/cn/bunny/services/security/filter/TokenAuthenticationFilter.kt
new file mode 100644
index 0000000..4baf974
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/filter/TokenAuthenticationFilter.kt
@@ -0,0 +1,59 @@
+package cn.bunny.services.security.filter
+
+import cn.bunny.common.service.exception.BunnyException
+import jakarta.servlet.FilterChain
+import jakarta.servlet.ServletException
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.web.filter.OncePerRequestFilter
+import java.io.IOException
+import java.util.function.Consumer
+
+class TokenAuthenticationFilter(private val redisTemplate: RedisTemplate) : OncePerRequestFilter() {
+ @Throws(ServletException::class, IOException::class, BunnyException::class)
+ override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
+ val token = request.getHeader("token")
+
+ // 判断是否有token,如果后面还有过滤器就这样写
+ // if (token == null) {
+ // doFilter(request, response, chain)
+ // return
+ // }
+
+
+ // 自定义实现内容
+ val authentication = getAuthentication(request)
+ if (authentication != null) {
+ // 设置用户详细信息
+ SecurityContextHolder.getContext().authentication = authentication
+ }
+
+ chain.doFilter(request, response)
+ }
+
+ /**
+ * * 用户请求判断
+ *
+ * @param request 请求
+ * @return 验证码方法
+ */
+ private fun getAuthentication(request: HttpServletRequest): UsernamePasswordAuthenticationToken {
+ // 请求头是否有token
+ val token = request.getHeader("token")
+ val username = "admin"
+ val authList: MutableList = ArrayList()
+
+ // 设置角色内容
+ if (token != null) {
+ val roleList = ArrayList()
+ roleList.forEach(Consumer { role: String? -> authList.add(SimpleGrantedAuthority(role)) })
+ return UsernamePasswordAuthenticationToken(username, null, authList)
+ } else {
+ return UsernamePasswordAuthenticationToken(username, null, ArrayList())
+ }
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/filter/TokenLoginFilterService.kt b/services/src/main/kotlin/cn/bunny/services/security/filter/TokenLoginFilterService.kt
new file mode 100644
index 0000000..2b2e15c
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/filter/TokenLoginFilterService.kt
@@ -0,0 +1,124 @@
+package cn.bunny.services.security.filter
+
+import cn.bunny.common.service.utils.ResponseUtil.Companion.out
+import cn.bunny.dao.dto.user.LoginDto
+import cn.bunny.dao.pojo.constant.RedisUserConstant.Companion.getAdminLoginInfoPrefix
+import cn.bunny.dao.pojo.constant.RedisUserConstant.Companion.getAdminUserEmailCodePrefix
+import cn.bunny.dao.pojo.result.Result
+import cn.bunny.dao.pojo.result.ResultCodeEnum
+import cn.bunny.services.security.handelr.SecurityAuthenticationFailureHandler
+import cn.bunny.services.security.handelr.SecurityAuthenticationSuccessHandler
+import cn.bunny.services.security.service.CustomUserDetailsService
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.data.redis.core.RedisTemplate
+import org.springframework.http.HttpMethod
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher
+import java.io.IOException
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+
+/**
+ * * UsernamePasswordAuthenticationFilter
+ * 也可以在这里添加验证码、短信等的验证
+ * 由于SpringSecurity的登录只能是表单形式 并且用户名密码需要时username、password,可以通过继承 UsernamePasswordAuthenticationFilter 获取登录请求的参数
+ * 再去设置到 UsernamePasswordAuthenticationToken 中 来改变请求传参方式、参数名等 或者也可以在登录的时候加入其他参数等等
+ */
+class TokenLoginFilterService(
+ authenticationConfiguration: AuthenticationConfiguration,
+ redisTemplate: RedisTemplate,
+ customUserDetailsService: CustomUserDetailsService
+) : UsernamePasswordAuthenticationFilter() {
+ private val redisTemplate: RedisTemplate
+ private val customUserDetailsService: CustomUserDetailsService
+ private lateinit var loginDto: LoginDto
+
+ // 依赖注入
+ init {
+ this.setAuthenticationSuccessHandler(SecurityAuthenticationSuccessHandler())
+ this.setAuthenticationFailureHandler(SecurityAuthenticationFailureHandler())
+ this.setPostOnly(false)
+ // ? 指定登录接口及提交方式,可以指定任意路径
+ this.setRequiresAuthenticationRequestMatcher(AntPathRequestMatcher("/*/login", HttpMethod.POST.name()))
+ this.authenticationManager = authenticationConfiguration.authenticationManager
+ // 依赖注入
+ this.redisTemplate = redisTemplate
+ this.customUserDetailsService = customUserDetailsService
+ }
+
+ /**
+ * * 登录认证,获取输入的用户名和密码,调用方法认证
+ * 接受前端login登录参数
+ * 在这里可以设置短信验证登录
+ */
+ @Throws(AuthenticationException::class)
+ override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication? {
+ try {
+ // 获取用户信息相关内容
+ val loginDto = LoginDto()
+ loginDto.username = "admin"
+ loginDto.password = "password"
+
+ // 封装对象,将用户名密码传入
+ val authenticationToken: Authentication = UsernamePasswordAuthenticationToken(loginDto.username, loginDto.password)
+ return authenticationManager.authenticate(authenticationToken)
+ } catch (e: IOException) {
+ throw RuntimeException(e.localizedMessage)
+ }
+ }
+
+ /**
+ * * 认证成功调用方法
+ * 返回登录成功后的信息
+ */
+ override fun successfulAuthentication(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ chain: FilterChain,
+ auth: Authentication
+ ) {
+ // 封装返回对象
+ val loginAdminVo = customUserDetailsService.login(loginDto)
+
+ // 判断用户是否被锁定
+ if (loginAdminVo.status!!) {
+ out(response, Result.error(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED))
+ return
+ }
+
+ // 将值存入Redis中
+ redisTemplate.opsForValue()[getAdminLoginInfoPrefix(loginAdminVo.email!!), loginAdminVo, 15] =
+ TimeUnit.DAYS
+ // 将Redis中验证码删除
+ redisTemplate.delete(getAdminUserEmailCodePrefix(loginAdminVo.email!!))
+
+ // 返回登录信息
+ out(response, Result.success(loginAdminVo))
+ }
+
+ /**
+ * * 认证失败调用方法,失败判断
+ * 1. 是否包含用户名
+ * 2. 是否包含密码
+ */
+ override fun unsuccessfulAuthentication(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ failed: AuthenticationException
+ ) {
+ val password = loginDto.password
+ val username = loginDto.username
+
+ when {
+ password.isNullOrBlank() || username.isNullOrBlank() -> out(response, Result.error(ResultCodeEnum.USERNAME_OR_PASSWORD_NOT_EMPTY))
+ else -> out(response, Result.error(null, ResultCodeEnum.LOGIN_ERROR))
+ }
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAccessDeniedHandler.kt b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAccessDeniedHandler.kt
new file mode 100644
index 0000000..5dd86ad
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAccessDeniedHandler.kt
@@ -0,0 +1,30 @@
+package cn.bunny.services.security.handelr
+
+import cn.bunny.dao.pojo.result.Result
+import cn.bunny.dao.pojo.result.ResultCodeEnum
+import com.alibaba.fastjson2.JSON
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import lombok.SneakyThrows
+import org.springframework.security.access.AccessDeniedException
+import org.springframework.security.web.access.AccessDeniedHandler
+
+/**
+ * 没有权限访问
+ */
+class SecurityAccessDeniedHandler : AccessDeniedHandler {
+ @SneakyThrows
+ override fun handle(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ accessDeniedException: AccessDeniedException
+ ) {
+ val result = Result.error(ResultCodeEnum.FAIL_NO_ACCESS_DENIED)
+
+ val json = JSON.toJSON(result)
+
+ // 返回响应
+ response.contentType = "application/json;charset=UTF-8"
+ response.writer.println(json)
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationEntryPoint.kt b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationEntryPoint.kt
new file mode 100644
index 0000000..7235757
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationEntryPoint.kt
@@ -0,0 +1,44 @@
+package cn.bunny.services.security.handelr
+
+import cn.bunny.common.service.utils.JwtHelper
+import cn.bunny.common.service.utils.ResponseUtil.Companion.out
+import cn.bunny.dao.pojo.result.Result
+import cn.bunny.dao.pojo.result.ResultCodeEnum
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.apache.logging.log4j.LogManager
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.web.AuthenticationEntryPoint
+
+/**
+ * 请求未认证接口
+ */
+class SecurityAuthenticationEntryPoint : AuthenticationEntryPoint {
+ private val logger = LogManager.getLogger(SecurityAuthenticationEntryPoint::class.java)
+
+ override fun commence(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ authException: AuthenticationException
+ ) {
+ val token = response.getHeader("token")
+ val message = authException.message
+ // 创建结果对象
+ val result: Result = when {
+ token.isNullOrEmpty() -> Result.error(ResultCodeEnum.LOGIN_AUTH)
+
+ JwtHelper.isExpired(token) -> {
+ logger.info("登录Token过期:{},用户id:{}", message, null)
+ Result.error(ResultCodeEnum.AUTHENTICATION_EXPIRED)
+ }
+
+ else -> {
+ logger.info("请求未授权接口:{},用户id:{}", message, token)
+ Result.error(ResultCodeEnum.LOGGED_IN_FROM_ANOTHER_DEVICE)
+ }
+ }
+
+ // 返回响应
+ out(response, result)
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationFailureHandler.kt b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationFailureHandler.kt
new file mode 100644
index 0000000..75543eb
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationFailureHandler.kt
@@ -0,0 +1,30 @@
+package cn.bunny.services.security.handelr
+
+import cn.bunny.dao.pojo.result.Result
+import com.alibaba.fastjson2.JSON
+import jakarta.servlet.ServletException
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.web.authentication.AuthenticationFailureHandler
+import java.io.IOException
+
+class SecurityAuthenticationFailureHandler : AuthenticationFailureHandler {
+ @Throws(IOException::class, ServletException::class)
+ override fun onAuthenticationFailure(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ exception: AuthenticationException
+ ) {
+ // 错误消息
+ val localizedMessage = exception.localizedMessage
+ val result = Result.error(localizedMessage)
+
+ // 转成JSON
+ val json = JSON.toJSON(result)
+
+ // 返回响应
+ response.contentType = "application/json;charset=UTF-8"
+ response.writer.println(json)
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationSuccessHandler.kt b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationSuccessHandler.kt
new file mode 100644
index 0000000..0ae3df6
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/handelr/SecurityAuthenticationSuccessHandler.kt
@@ -0,0 +1,29 @@
+package cn.bunny.services.security.handelr
+
+import cn.bunny.dao.pojo.result.Result
+import com.alibaba.fastjson2.JSON
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.springframework.security.core.Authentication
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler
+import java.io.IOException
+
+/**
+ * 登录成功
+ */
+class SecurityAuthenticationSuccessHandler : AuthenticationSuccessHandler {
+ @Throws(IOException::class)
+ override fun onAuthenticationSuccess(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ authentication: Authentication
+ ) {
+ // 获取用户身份信息
+ val principal = authentication.principal
+ val result = Result.success(principal)
+
+ // 返回
+ response.contentType = "application/json;charset=UTF-8"
+ response.writer.println(JSON.toJSON(result))
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/service/CustomAuthorizationManagerService.kt b/services/src/main/kotlin/cn/bunny/services/security/service/CustomAuthorizationManagerService.kt
new file mode 100644
index 0000000..f3ce3b2
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/service/CustomAuthorizationManagerService.kt
@@ -0,0 +1,6 @@
+package cn.bunny.services.security.service
+
+import org.springframework.security.authorization.AuthorizationManager
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext
+
+interface CustomAuthorizationManagerService : AuthorizationManager
diff --git a/services/src/main/kotlin/cn/bunny/services/security/service/CustomUserDetailsService.kt b/services/src/main/kotlin/cn/bunny/services/security/service/CustomUserDetailsService.kt
new file mode 100644
index 0000000..5867c43
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/service/CustomUserDetailsService.kt
@@ -0,0 +1,23 @@
+package cn.bunny.services.security.service
+
+import cn.bunny.dao.dto.user.LoginDto
+import cn.bunny.dao.vo.user.LoginVo
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+
+interface CustomUserDetailsService : UserDetailsService {
+ /**
+ * 根据用户名获取用户对象(获取不到直接抛异常)
+ */
+ @Throws(UsernameNotFoundException::class)
+ override fun loadUserByUsername(username: String): UserDetails
+
+ /**
+ * 前台用户登录接口
+ *
+ * @param loginDto 登录参数
+ * @return 登录后结果返回
+ */
+ fun login(loginDto: LoginDto): LoginVo
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/service/impl/CustomAuthorizationManagerServiceImpl.java b/services/src/main/kotlin/cn/bunny/services/security/service/impl/CustomAuthorizationManagerServiceImpl.java
new file mode 100644
index 0000000..7eb09f5
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/service/impl/CustomAuthorizationManagerServiceImpl.java
@@ -0,0 +1,57 @@
+package cn.bunny.services.security.service.impl;
+
+import cn.bunny.common.service.utils.JwtHelper;
+import cn.bunny.services.security.service.CustomAuthorizationManagerService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+
+/**
+ * 自定义权限判断
+ * 判断用户有哪些权限
+ */
+@Component
+@Slf4j
+public class CustomAuthorizationManagerServiceImpl implements CustomAuthorizationManagerService {
+
+ @Override
+ public void verify(Supplier authentication, RequestAuthorizationContext requestAuthorizationContext) {
+ CustomAuthorizationManagerService.super.verify(authentication, requestAuthorizationContext);
+ }
+
+ @Override
+ public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext context) {
+ // 用户的token和用户id、请求Url
+ HttpServletRequest request = context.getRequest();
+ String token = request.getHeader("token");
+ // 用户id
+ Long userId = JwtHelper.getUserId(token);
+ // 请求地址
+ String requestURI = request.getRequestURI();
+ // 请求方式
+ String method = request.getMethod();
+ // 角色代码列表
+ List roleCodeList = authentication.get().getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
+
+ return new AuthorizationDecision(hasRoleList(requestURI, method, userId));
+ }
+
+ /**
+ * 查询用户所属的角色信息
+ *
+ * @param requestURI 请求url地址
+ * @param method 请求方式
+ * @param userId 用户id
+ */
+ private Boolean hasRoleList(String requestURI, String method, Long userId) {
+ return true;
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/security/service/impl/CustomUserDetailsServiceImpl.kt b/services/src/main/kotlin/cn/bunny/services/security/service/impl/CustomUserDetailsServiceImpl.kt
new file mode 100644
index 0000000..52fc37a
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/security/service/impl/CustomUserDetailsServiceImpl.kt
@@ -0,0 +1,47 @@
+package cn.bunny.services.security.service.impl
+
+import cn.bunny.dao.dto.user.LoginDto
+import cn.bunny.dao.entity.user.AdminUser
+import cn.bunny.dao.vo.user.LoginVo
+import cn.bunny.services.mapper.UserMapper
+import cn.bunny.services.security.custom.CustomUser
+import cn.bunny.services.security.service.CustomUserDetailsService
+import com.baomidou.mybatisplus.core.toolkit.Wrappers
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.core.authority.AuthorityUtils
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.stereotype.Component
+
+@Component
+class CustomUserDetailsServiceImpl : CustomUserDetailsService {
+ @Autowired
+ private val userMapper: UserMapper? = null
+
+ @Throws(UsernameNotFoundException::class)
+ override fun loadUserByUsername(username: String): UserDetails {
+ // 根据邮箱查询用户名
+ val user = userMapper!!.selectOne(
+ Wrappers.lambdaQuery().eq(SFunction { obj: AdminUser -> obj.email }, username)
+ )
+ // 都为空抛出异常,用户不存在
+ if (user == null) {
+ throw UsernameNotFoundException("")
+ }
+
+ // 查询所有的角色
+ return CustomUser(user, AuthorityUtils.createAuthorityList(listOf("admin", "common")))
+ }
+
+ /**
+ * 前台用户登录接口
+ *
+ * @param loginDto 登录参数
+ * @return 登录后结果返回
+ */
+ override fun login(loginDto: LoginDto): LoginVo {
+ // 自定义登录逻辑
+ return LoginVo()
+ }
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/service/I18nService.java b/services/src/main/kotlin/cn/bunny/services/service/I18nService.java
new file mode 100644
index 0000000..37a209a
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/service/I18nService.java
@@ -0,0 +1,16 @@
+package cn.bunny.services.service;
+
+import cn.bunny.dao.entity.i18n.I18n;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ *
+ * 多语言表 服务类
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+public interface I18nService extends IService {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/service/I18nTypeService.java b/services/src/main/kotlin/cn/bunny/services/service/I18nTypeService.java
new file mode 100644
index 0000000..db0c25a
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/service/I18nTypeService.java
@@ -0,0 +1,16 @@
+package cn.bunny.services.service;
+
+import cn.bunny.dao.entity.i18n.I18nType;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ *
+ * 多语言类型表 服务类
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+public interface I18nTypeService extends IService {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/service/UserService.java b/services/src/main/kotlin/cn/bunny/services/service/UserService.java
new file mode 100644
index 0000000..9b19acc
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/service/UserService.java
@@ -0,0 +1,16 @@
+package cn.bunny.services.service;
+
+import cn.bunny.dao.entity.user.AdminUser;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ *
+ * 用户信息 服务类
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+public interface UserService extends IService {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/service/impl/I18nServiceImpl.java b/services/src/main/kotlin/cn/bunny/services/service/impl/I18nServiceImpl.java
new file mode 100644
index 0000000..2ebd537
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/service/impl/I18nServiceImpl.java
@@ -0,0 +1,20 @@
+package cn.bunny.services.service.impl;
+
+import cn.bunny.dao.entity.i18n.I18n;
+import cn.bunny.services.mapper.I18nMapper;
+import cn.bunny.services.service.I18nService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ *
+ * 多语言表 服务实现类
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Service
+public class I18nServiceImpl extends ServiceImpl implements I18nService {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/service/impl/I18nTypeServiceImpl.java b/services/src/main/kotlin/cn/bunny/services/service/impl/I18nTypeServiceImpl.java
new file mode 100644
index 0000000..5bc8c5e
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/service/impl/I18nTypeServiceImpl.java
@@ -0,0 +1,20 @@
+package cn.bunny.services.service.impl;
+
+import cn.bunny.dao.entity.i18n.I18nType;
+import cn.bunny.services.mapper.I18nTypeMapper;
+import cn.bunny.services.service.I18nTypeService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ *
+ * 多语言类型表 服务实现类
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Service
+public class I18nTypeServiceImpl extends ServiceImpl implements I18nTypeService {
+
+}
diff --git a/services/src/main/kotlin/cn/bunny/services/service/impl/UserServiceImpl.java b/services/src/main/kotlin/cn/bunny/services/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..2744591
--- /dev/null
+++ b/services/src/main/kotlin/cn/bunny/services/service/impl/UserServiceImpl.java
@@ -0,0 +1,20 @@
+package cn.bunny.services.service.impl;
+
+import cn.bunny.dao.entity.user.AdminUser;
+import cn.bunny.services.mapper.UserMapper;
+import cn.bunny.services.service.UserService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ *
+ * 用户信息 服务实现类
+ *
+ *
+ * @author Bunny
+ * @since 2024-09-26
+ */
+@Service
+public class UserServiceImpl extends ServiceImpl implements UserService {
+
+}
diff --git a/services/src/main/resources/application-dev.yml b/services/src/main/resources/application-dev.yml
new file mode 100644
index 0000000..74a6699
--- /dev/null
+++ b/services/src/main/resources/application-dev.yml
@@ -0,0 +1,25 @@
+bunny:
+ datasource1:
+ host: 192.168.3.98
+ port: 3304
+ sqlData: auth_admin
+ username: root
+ password: "02120212"
+ datasource2:
+ host: 106.15.251.123
+ port: 3304
+ sqlData: auth_admin_i18n
+ username: root
+ password: "02120212"
+
+ redis:
+ host: 192.168.3.98
+ port: 6379
+ database: 0
+ password: "123456"
+
+ minio:
+ endpointUrl: "http://192.168.3.98:9000"
+ accessKey: bunny
+ secretKey: "02120212"
+ bucket-name: bunny-bbs
diff --git a/services/src/main/resources/application-prod.yml b/services/src/main/resources/application-prod.yml
new file mode 100644
index 0000000..11b23c3
--- /dev/null
+++ b/services/src/main/resources/application-prod.yml
@@ -0,0 +1,25 @@
+bunny:
+ datasource:
+ host: 192.168.3.98
+ port: 3304
+ sqlData: bunny_docs
+ username: root
+ password: "02120212"
+ datasource2:
+ host: 192.168.3.98
+ port: 3304
+ sqlData: bunny_docs_i18n
+ username: root
+ password: "02120212"
+
+ redis:
+ host: 192.168.3.98
+ port: 6379
+ database: 0
+ password: "123456"
+
+ minio:
+ endpointUrl: "http://192.168.3.98:9000"
+ accessKey: bunny
+ secretKey: "02120212"
+ bucket-name: bunny-bbs
diff --git a/services/src/main/resources/application.yml b/services/src/main/resources/application.yml
new file mode 100644
index 0000000..32f0599
--- /dev/null
+++ b/services/src/main/resources/application.yml
@@ -0,0 +1,79 @@
+server:
+ port: 8800
+spring:
+ profiles:
+ active: dev
+ application:
+ name: service-admin
+ main:
+ lazy-initialization: true
+ jmx:
+ enabled: false
+ 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.datasource1.host}:${bunny.datasource1.port}/${bunny.datasource1.sqlData}?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true
+ username: ${bunny.datasource1.username}
+ password: ${bunny.datasource1.password}
+ i18n:
+ type: com.zaxxer.hikari.HikariDataSource
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://${bunny.datasource2.host}:${bunny.datasource2.port}/${bunny.datasource2.sqlData}?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true
+ username: ${bunny.datasource2.username}
+ password: ${bunny.datasource2.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
+ servlet:
+ multipart:
+ max-file-size: 5MB
+
+mybatis-plus:
+ mapper-locations: classpath:mapper/*.xml
+ global-config:
+ db-config:
+ logic-delete-field: isDeleted
+ configuration:
+ map-underscore-to-camel-case: true
+# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
+
+logging:
+ level:
+ cn.bunny.service.controller: info
+ cn.bunny.service.service: info
+ cn.bunny.service.mapper: info
+ root: info
+
+ pattern:
+ dateformat: HH:mm:ss:SSS
+ file:
+ path: "logs/${spring.application.name}"
+
+bunny:
+ minio:
+ endpointUrl: ${bunny.minio.endpointUrl}
+ accessKey: ${bunny.minio.accessKey}
+ secretKey: ${bunny.minio.secretKey}
+ bucket-name: ${bunny.minio.bucket-name}
\ No newline at end of file
diff --git a/services/src/main/resources/banner.txt b/services/src/main/resources/banner.txt
new file mode 100644
index 0000000..c34ce34
--- /dev/null
+++ b/services/src/main/resources/banner.txt
@@ -0,0 +1,6 @@
+__________ ____ __. __ .__ .__
+\______ \__ __ ____ ____ ___.__. | |/ _|_____/ |_| | |__| ____
+ | | _/ | \/ \ / < | | | < / _ \ __\ | | |/ \
+ | | \ | / | \ | \___ | | | ( <_> ) | | |_| | | \
+ |______ /____/|___| /___| / ____| |____|__ \____/|__| |____/__|___| /
+ \/ \/ \/\/ \/ \/
\ No newline at end of file
diff --git a/services/src/main/resources/ipdb/ip2region.xdb b/services/src/main/resources/ipdb/ip2region.xdb
new file mode 100644
index 0000000..7052c05
Binary files /dev/null and b/services/src/main/resources/ipdb/ip2region.xdb differ
diff --git a/services/src/main/resources/logback.xml b/services/src/main/resources/logback.xml
new file mode 100644
index 0000000..04ecbe3
--- /dev/null
+++ b/services/src/main/resources/logback.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ %cyan([%thread]) %yellow(%-5level) %green(%logger{100}).%boldRed(%method)-%boldMagenta(%line) - %blue(%msg%n)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/src/main/resources/mapper/I18nMapper.xml b/services/src/main/resources/mapper/I18nMapper.xml
new file mode 100644
index 0000000..c8184aa
--- /dev/null
+++ b/services/src/main/resources/mapper/I18nMapper.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, type_id, key_name, summary, create_user, create_time, update_time, update_user, is_deleted
+
+
+
diff --git a/services/src/main/resources/mapper/I18nTypeMapper.xml b/services/src/main/resources/mapper/I18nTypeMapper.xml
new file mode 100644
index 0000000..1d5347d
--- /dev/null
+++ b/services/src/main/resources/mapper/I18nTypeMapper.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, language_name, summary, is_default, create_user, create_time, update_time, update_user, is_deleted
+
+
+
diff --git a/services/src/main/resources/mapper/UserMapper.xml b/services/src/main/resources/mapper/UserMapper.xml
new file mode 100644
index 0000000..a0ef60a
--- /dev/null
+++ b/services/src/main/resources/mapper/UserMapper.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, username, nick_name, email, phone, password, avatar, sex, summary, last_login_ip, last_login_ip_address, status, create_user, create_time, update_time, update_user, is_deleted
+
+
+
diff --git a/services/src/main/resources/static/favicon.ico b/services/src/main/resources/static/favicon.ico
new file mode 100644
index 0000000..385f8a6
Binary files /dev/null and b/services/src/main/resources/static/favicon.ico differ