基本功能
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.springframework.boot") version "3.2.2"
|
id("org.springframework.boot") version "3.2.4"
|
||||||
id("io.spring.dependency-management") version "1.1.4"
|
id("io.spring.dependency-management") version "1.1.4"
|
||||||
kotlin("jvm") version "1.9.22"
|
kotlin("jvm") version "1.9.22"
|
||||||
kotlin("plugin.spring") version "1.9.22"
|
kotlin("plugin.spring") version "1.9.22"
|
||||||
@@ -25,15 +25,41 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//版本号定义
|
||||||
|
object Versions {
|
||||||
|
const val FAST_JSON_2 = "2.0.45"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Kotlin
|
// Kotlin
|
||||||
|
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
|
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
|
||||||
runtimeOnly("org.jetbrains.kotlin:kotlin-reflect")
|
runtimeOnly("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
|
// Apache Commons CSV https://mvnrepository.com/artifact/org.apache.commons/commons-csv
|
||||||
|
implementation("org.apache.commons:commons-csv:1.10.0")
|
||||||
// Spring Boot
|
// Spring Boot
|
||||||
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
|
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
// Spring Boot Test
|
||||||
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test
|
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
// Spring Security
|
||||||
|
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
// Spring Validation
|
||||||
|
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-validation:3.2.4")
|
||||||
|
// Redis
|
||||||
|
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||||
|
// Fast Json 2
|
||||||
|
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
|
||||||
|
implementation("com.alibaba.fastjson2:fastjson2:${Versions.FAST_JSON_2}")
|
||||||
|
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-kotlin
|
||||||
|
implementation("com.alibaba.fastjson2:fastjson2-kotlin:${Versions.FAST_JSON_2}")
|
||||||
|
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring6
|
||||||
|
implementation("com.alibaba.fastjson2:fastjson2-extension-spring6:${Versions.FAST_JSON_2}")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
rootProject.name = "fortern-api"
|
rootProject.name = "message-api"
|
||||||
|
|||||||
39
src/main/kotlin/xyz/fortern/forternapi/config/RedisConfig.kt
Normal file
39
src/main/kotlin/xyz/fortern/forternapi/config/RedisConfig.kt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package xyz.fortern.forternapi.config
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.support.spring6.data.redis.GenericFastJsonRedisSerializer
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis配置类
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
class RedisConfig {
|
||||||
|
@Bean
|
||||||
|
fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> {
|
||||||
|
// Redis模板对象
|
||||||
|
val template = RedisTemplate<String, Any>()
|
||||||
|
|
||||||
|
// 设置连接工厂
|
||||||
|
template.connectionFactory = redisConnectionFactory
|
||||||
|
|
||||||
|
// 设置自定义序列化方式
|
||||||
|
// key:字符串类型,使用String的序列化方式
|
||||||
|
val stringRedisSerializer = StringRedisSerializer()
|
||||||
|
// 使用fastjson2的序列化方式,直接序列化对象
|
||||||
|
val fastJsonRedisSerializer = GenericFastJsonRedisSerializer()
|
||||||
|
|
||||||
|
// 指定序列化和反序列化方式
|
||||||
|
template.keySerializer = stringRedisSerializer
|
||||||
|
template.valueSerializer = fastJsonRedisSerializer
|
||||||
|
template.hashKeySerializer = stringRedisSerializer
|
||||||
|
template.hashValueSerializer = fastJsonRedisSerializer
|
||||||
|
|
||||||
|
// 初始化模板
|
||||||
|
template.afterPropertiesSet()
|
||||||
|
return template
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package xyz.fortern.forternapi.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
class SecurityConfig {
|
||||||
|
@Bean
|
||||||
|
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http
|
||||||
|
.csrf { csrf -> csrf.disable() }
|
||||||
|
.requestCache { requestCache -> requestCache.disable() }
|
||||||
|
.authorizeHttpRequests { authorizeHttpRequests ->
|
||||||
|
authorizeHttpRequests
|
||||||
|
// 登录接口
|
||||||
|
.requestMatchers("/auth").permitAll()
|
||||||
|
// 读取消息的接口
|
||||||
|
.requestMatchers(HttpMethod.GET, "/main/**").permitAll()
|
||||||
|
// 内部转发到错误信息接口
|
||||||
|
.requestMatchers("/error").permitAll()
|
||||||
|
// 测试类接口
|
||||||
|
.requestMatchers("/test/**").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
}
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package xyz.fortern.forternapi.controller
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
|
||||||
|
import org.springframework.security.web.context.SecurityContextRepository
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import xyz.fortern.forternapi.exception.BusinessException
|
||||||
|
import xyz.fortern.forternapi.service.AuthService
|
||||||
|
import xyz.fortern.forternapi.web.ERROR_PASSWORD
|
||||||
|
|
||||||
|
//TODO 统一鉴权服务
|
||||||
|
/**
|
||||||
|
* 账号与会话管理Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
class AuthController(
|
||||||
|
val authService: AuthService
|
||||||
|
) {
|
||||||
|
private val contextRepository: SecurityContextRepository = HttpSessionSecurityContextRepository()
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
fun login(password: String, request: HttpServletRequest, response: HttpServletResponse): ResponseEntity<Any> {
|
||||||
|
if (!authService.login(password)) {
|
||||||
|
throw BusinessException(HttpStatus.UNAUTHORIZED, ERROR_PASSWORD, "login.error_password")
|
||||||
|
}
|
||||||
|
val authentication = UsernamePasswordAuthenticationToken("master", null, emptyList())
|
||||||
|
val context = SecurityContextHolder.getContext().also { it.authentication = authentication }
|
||||||
|
contextRepository.saveContext(context, request, response)
|
||||||
|
return ResponseEntity.ok(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package xyz.fortern.forternapi.controller
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max
|
||||||
|
import jakarta.validation.constraints.NotNull
|
||||||
|
import jakarta.validation.constraints.Size
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import xyz.fortern.forternapi.model.Message
|
||||||
|
import xyz.fortern.forternapi.service.MessageService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/main")
|
||||||
|
class MessageController(
|
||||||
|
private val messageService: MessageService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取一条消息
|
||||||
|
*
|
||||||
|
* @param name 读者名字
|
||||||
|
* @param id 消息id
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
fun test(
|
||||||
|
@NotNull @Size(min = 1, max = 30) name: String,
|
||||||
|
@NotNull @Size(min = 1, max = 50) id: String
|
||||||
|
): ResponseEntity<Message> {
|
||||||
|
val message = messageService.readMessage(name, id)
|
||||||
|
return if (message != null) {
|
||||||
|
ResponseEntity.ok(message)
|
||||||
|
} else {
|
||||||
|
ResponseEntity.notFound().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*
|
||||||
|
* @param name 接受者名字
|
||||||
|
* @param content 消息内容
|
||||||
|
* @param expire 过期时间
|
||||||
|
*
|
||||||
|
* @return 消息的ID
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
fun send(
|
||||||
|
@NotNull @Size(min = 1, max = 30) name: String,
|
||||||
|
@NotNull @Size(max = 1024) content: String,
|
||||||
|
@NotNull @Max(14400) expire: Long,
|
||||||
|
): ResponseEntity<String> {
|
||||||
|
val id = messageService.sendMessage(name, content, expire)
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一条消息
|
||||||
|
*
|
||||||
|
* @param name 消息接收者
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
@DeleteMapping
|
||||||
|
fun delete(
|
||||||
|
@NotNull @Size(min = 1, max = 30) name: String,
|
||||||
|
@NotNull @Size(min = 1, max = 50) id: String
|
||||||
|
): ResponseEntity<Any> {
|
||||||
|
messageService.delMessage(name, id)
|
||||||
|
return ResponseEntity.ok(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取消息列表
|
||||||
|
*
|
||||||
|
* @param name 消息接受者
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
fun list(@Size(min = 1, max = 30) name: String?): Map<String, List<Message>> {
|
||||||
|
return messageService.messageList(name ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package xyz.fortern.forternapi.controller
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/test")
|
||||||
|
class TestController {
|
||||||
|
|
||||||
|
@PostMapping("/hello")
|
||||||
|
fun hello(): String {
|
||||||
|
return "Hello world"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package xyz.fortern.forternapi.exception
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 Web 开发中,有时会使用异常进行流程控制。比如在一些 Web 框架中,可以使用一些
|
||||||
|
* 断言工具,对用户参数进行检查,如果有问题则快速抛出异常,再通过统一异常处理,转为
|
||||||
|
* 用户可理解的信息。
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 一般情况下产生一个异常会有不小的性能开销,因为异常对象需要捕获当前的栈信息。但是,
|
||||||
|
* 如果禁止异常对象存入栈信息,则不会有这些性能开销。此时创建异常对象就和创建一个普通
|
||||||
|
* 对象几乎没有区别。
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 当前类表示一个通用的业务异常,可用于在 Controller 层抛出,然后走到对应的异常处理,
|
||||||
|
* 将错误转为正常的响应。
|
||||||
|
*
|
||||||
|
* @author Fortern
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
class BusinessException(
|
||||||
|
/**
|
||||||
|
* Http响应状态码
|
||||||
|
*/
|
||||||
|
val httpCode: HttpStatus,
|
||||||
|
/**
|
||||||
|
* 业务状态码
|
||||||
|
*/
|
||||||
|
val statusCode: String,
|
||||||
|
/**
|
||||||
|
* 消息的i18n的翻译key
|
||||||
|
*/
|
||||||
|
message: String
|
||||||
|
) : RuntimeException(message, null, true, false)
|
||||||
16
src/main/kotlin/xyz/fortern/forternapi/model/Message.kt
Normal file
16
src/main/kotlin/xyz/fortern/forternapi/model/Message.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package xyz.fortern.forternapi.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息实体
|
||||||
|
*
|
||||||
|
* @author Fortern
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
class Message (
|
||||||
|
val from: String,
|
||||||
|
val to: String,
|
||||||
|
val expire: Long,
|
||||||
|
val content: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package xyz.fortern.forternapi.service
|
||||||
|
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import xyz.fortern.forternapi.util.RedisKeys
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AuthService (
|
||||||
|
redisTemplate: RedisTemplate<String, Any>
|
||||||
|
) {
|
||||||
|
val opsForValue = redisTemplate.opsForValue()
|
||||||
|
|
||||||
|
fun login(password: String): Boolean {
|
||||||
|
return opsForValue[RedisKeys.PASSWORD_KEY] == password
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package xyz.fortern.forternapi.service
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import xyz.fortern.forternapi.model.Message
|
||||||
|
import xyz.fortern.forternapi.util.Generator
|
||||||
|
import xyz.fortern.forternapi.util.RedisKeys
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MessageService(
|
||||||
|
private val redisTemplate: RedisTemplate<String, Any>,
|
||||||
|
) {
|
||||||
|
private val forValue = redisTemplate.opsForValue()
|
||||||
|
|
||||||
|
@Value("\${fortern.msg.defaultSender}")
|
||||||
|
private lateinit var defaultSender: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给别人发送一条消息
|
||||||
|
*
|
||||||
|
* @param name 接受者名字
|
||||||
|
* @param content 消息内容
|
||||||
|
* @param expire 过期时间
|
||||||
|
*/
|
||||||
|
fun sendMessage(
|
||||||
|
name: String,
|
||||||
|
content: String,
|
||||||
|
expire: Long
|
||||||
|
): String {
|
||||||
|
val id = Generator.randomString(16)
|
||||||
|
forValue.set(
|
||||||
|
"${RedisKeys.MESSAGE_PREFIX}$name:$id",
|
||||||
|
Message(defaultSender, name, System.currentTimeMillis() + expire * 1000L, content),
|
||||||
|
expire,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息列表
|
||||||
|
*/
|
||||||
|
fun messageList(name: String): Map<String, List<Message>> {
|
||||||
|
// key命令虽然时间复杂度为 O(N),但扫描速度极快,在当前项目中可以使用
|
||||||
|
val keys = redisTemplate.keys("${RedisKeys.MESSAGE_PREFIX}${if (name.isNotEmpty()) "$name:" else ""}*")
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
val map = HashMap<String, MutableList<Message>>()
|
||||||
|
keys.forEach { key ->
|
||||||
|
val message = forValue[key] as Message
|
||||||
|
val list = map.getOrPut(message.to) { ArrayList() }
|
||||||
|
list.add(message)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
} else {
|
||||||
|
val list = keys.map { key -> forValue[key] as Message }
|
||||||
|
return mapOf(Pair(name, list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取一条消息
|
||||||
|
*
|
||||||
|
* @param name 消息接受者
|
||||||
|
* @param id 消息的ID
|
||||||
|
*/
|
||||||
|
fun readMessage(name: String, id: String): Message? {
|
||||||
|
return forValue["${RedisKeys.MESSAGE_PREFIX}$name:$id"] as Message?
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一条消息
|
||||||
|
*
|
||||||
|
* @param name 消息接收者
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
fun delMessage(name: String, id: String): Boolean {
|
||||||
|
return redisTemplate.delete("${RedisKeys.MESSAGE_PREFIX}$name:$id")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/kotlin/xyz/fortern/forternapi/util/Generator.kt
Normal file
17
src/main/kotlin/xyz/fortern/forternapi/util/Generator.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package xyz.fortern.forternapi.util
|
||||||
|
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
object Generator {
|
||||||
|
private const val CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
private val random = Random(System.currentTimeMillis())
|
||||||
|
|
||||||
|
fun randomString(length: Int): String {
|
||||||
|
val randomString = StringBuilder(length)
|
||||||
|
|
||||||
|
for (i in 0 until length)
|
||||||
|
randomString.append(CHARACTERS[random.nextInt(CHARACTERS.length)])
|
||||||
|
|
||||||
|
return randomString.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main/kotlin/xyz/fortern/forternapi/util/RedisKeys.kt
Normal file
14
src/main/kotlin/xyz/fortern/forternapi/util/RedisKeys.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package xyz.fortern.forternapi.util
|
||||||
|
|
||||||
|
object RedisKeys {
|
||||||
|
/**
|
||||||
|
* Fortern的密码的key
|
||||||
|
*/
|
||||||
|
const val PASSWORD_KEY = "fortern:auth:password"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息key的前缀
|
||||||
|
*/
|
||||||
|
const val MESSAGE_PREFIX = "fortern:msg:"
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package xyz.fortern.forternapi.web
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.context.MessageSource
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.validation.FieldError
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
import org.springframework.web.method.annotation.HandlerMethodValidationException
|
||||||
|
import xyz.fortern.forternapi.exception.BusinessException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于处理参数校验结果的Advice
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
class ControllerAdvice(
|
||||||
|
val messageSource: MessageSource
|
||||||
|
) {
|
||||||
|
val logger: Logger = LoggerFactory.getLogger(ControllerAdvice::class.java)
|
||||||
|
/**
|
||||||
|
* 对于Controller参数中有[Valid]注解的参数,
|
||||||
|
* 如果校验失败会抛出[MethodArgumentNotValidException],
|
||||||
|
* 使用此函数进行处理
|
||||||
|
*/
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException::class)
|
||||||
|
fun handleMethodArgumentNotValidExceptions(ex: MethodArgumentNotValidException): ExResponse {
|
||||||
|
val errors: MutableMap<String, StringBuilder> = HashMap()
|
||||||
|
ex.bindingResult.allErrors.forEach { error ->
|
||||||
|
val fieldName = (error as FieldError).field
|
||||||
|
val errorMessage = error.getDefaultMessage() ?: return@forEach
|
||||||
|
val stringBuilder = errors.getOrPut(fieldName) { StringBuilder() }
|
||||||
|
stringBuilder.append(errorMessage).append(';').append(' ')
|
||||||
|
}
|
||||||
|
val list = errors.map { (k, v) ->
|
||||||
|
if (v.isNotEmpty())
|
||||||
|
v.deleteCharAt(v.length - 1).deleteCharAt(v.length - 1)
|
||||||
|
StringBuilder("${k}: ").append(v).toString()
|
||||||
|
}
|
||||||
|
return ExResponse(BAD_REQUEST, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对于Controller参数中有[jakarta.validation.constraints]包下的注解的参数,
|
||||||
|
* 如果校验失败会抛出[HandlerMethodValidationException],
|
||||||
|
* 使用此函数进行处理
|
||||||
|
*/
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
@ExceptionHandler(HandlerMethodValidationException::class)
|
||||||
|
fun handleHandlerMethodValidationException(ex: HandlerMethodValidationException): ExResponse {
|
||||||
|
val errors: MutableMap<String, StringBuilder> = HashMap()
|
||||||
|
ex.allValidationResults.forEach o@ { result ->
|
||||||
|
val fieldName = result.methodParameter.parameterName!!
|
||||||
|
val stringBuilder = StringBuilder()
|
||||||
|
result.resolvableErrors.forEach { error ->
|
||||||
|
val message = error.defaultMessage ?: return@forEach
|
||||||
|
stringBuilder.append(message).append(';').append(' ')
|
||||||
|
}
|
||||||
|
errors[fieldName] = stringBuilder
|
||||||
|
}
|
||||||
|
val list = errors.map { (k, v) ->
|
||||||
|
if (v.isNotEmpty())
|
||||||
|
v.deleteCharAt(v.length - 1).deleteCharAt(v.length - 1)
|
||||||
|
StringBuilder("${k}: ").append(v).toString()
|
||||||
|
}
|
||||||
|
return ExResponse(BAD_REQUEST, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 常规的业务异常处理
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(BusinessException::class)
|
||||||
|
fun handlerBusinessException(ex: BusinessException, request: HttpServletRequest): ResponseEntity<ExResponse> {
|
||||||
|
val locale = request.locale
|
||||||
|
val message = try {
|
||||||
|
messageSource.getMessage(ex.message!!, null, locale)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Message translation failed.", e)
|
||||||
|
"{{untranslated message}}"
|
||||||
|
}
|
||||||
|
val response = ExResponse(ex.statusCode, listOf(message))
|
||||||
|
return ResponseEntity.status(ex.httpCode).body(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/kotlin/xyz/fortern/forternapi/web/ExResponse.kt
Normal file
9
src/main/kotlin/xyz/fortern/forternapi/web/ExResponse.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package xyz.fortern.forternapi.web
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用的错误响应,一般在异常处理中使用
|
||||||
|
*/
|
||||||
|
class ExResponse(
|
||||||
|
val status: String,
|
||||||
|
val errors: List<String>
|
||||||
|
)
|
||||||
16
src/main/kotlin/xyz/fortern/forternapi/web/ExStatus.kt
Normal file
16
src/main/kotlin/xyz/fortern/forternapi/web/ExStatus.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package xyz.fortern.forternapi.web
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数错误
|
||||||
|
*/
|
||||||
|
const val BAD_REQUEST = "BAD_REQUEST"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码错误
|
||||||
|
*/
|
||||||
|
const val ERROR_PASSWORD = "ERROR_PASSWORD"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户不存在
|
||||||
|
*/
|
||||||
|
const val NO_USER = "NO_USER"
|
||||||
35
src/main/resources/application.yml
Normal file
35
src/main/resources/application.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
spring:
|
||||||
|
profiles:
|
||||||
|
active: dev
|
||||||
|
web:
|
||||||
|
locale: zh_CN
|
||||||
|
messages:
|
||||||
|
basename: i18n/messages
|
||||||
|
fortern:
|
||||||
|
msg:
|
||||||
|
defaultSender: Fortern
|
||||||
|
server:
|
||||||
|
servlet:
|
||||||
|
context-path: /message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
activate:
|
||||||
|
on-profile: dev
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
password: Redis20868354
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
activate:
|
||||||
|
on-profile: prod
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: redis-server
|
||||||
|
password: Redis20868354
|
||||||
1
src/main/resources/i18n/messages.properties
Normal file
1
src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 必须有默认的资源文件,否则 MessageSourceAutoConfiguration 不会执行
|
||||||
7
src/main/resources/i18n/messages_en_US.properties
Normal file
7
src/main/resources/i18n/messages_en_US.properties
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
login.no_user=No user found with the given username.
|
||||||
|
login.error_password=The password is incorrect.
|
||||||
|
ex_msg.read_msg.no_msg=No message found with the given id.
|
||||||
|
ex_msg.msg.username_error=Name cannot be empty and the length is less than 17.
|
||||||
|
ex_msg.msg.id_error=The message ID must be non-null.
|
||||||
|
ex_msg.send_msg.expire_error=The message expiration time must be less than 14400 seconds.
|
||||||
|
ex_msg.send_msg.length_error=The message length must be less than 10000 characters.
|
||||||
7
src/main/resources/i18n/messages_zh_CN.properties
Normal file
7
src/main/resources/i18n/messages_zh_CN.properties
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
login.no_user=用户不存在
|
||||||
|
login.error_password=密码错误
|
||||||
|
ex_msg.read_msg.no_msg=没有对应此 ID 的消息
|
||||||
|
ex_msg.msg.username_error=name 不能为空且长度要小于 17
|
||||||
|
ex_msg.msg.id_error=消息ID不能为空
|
||||||
|
ex_msg.send_msg.expire_error=消息到期时间必须小于 14400 秒
|
||||||
|
ex_msg.send_msg.length_error=消息长度必须小于 10000 字符
|
||||||
Reference in New Issue
Block a user