From 7b61753d43481b58eab27fb998e83b886da14656 Mon Sep 17 00:00:00 2001 From: Fortern Date: Sun, 14 Apr 2024 22:25:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 28 +++++- settings.gradle.kts | 2 +- .../fortern/forternapi/config/RedisConfig.kt | 39 ++++++++ .../forternapi/config/SecurityConfig.kt | 32 +++++++ .../forternapi/controller/AuthController.kt | 39 ++++++++ .../controller/MessageController.kt | 84 +++++++++++++++++ .../forternapi/controller/TestController.kt | 16 ++++ .../forternapi/exception/BusinessException.kt | 35 ++++++++ .../xyz/fortern/forternapi/model/Message.kt | 16 ++++ .../fortern/forternapi/service/AuthService.kt | 16 ++++ .../forternapi/service/MessageService.kt | 83 +++++++++++++++++ .../xyz/fortern/forternapi/util/Generator.kt | 17 ++++ .../xyz/fortern/forternapi/util/RedisKeys.kt | 14 +++ .../forternapi/web/ControllerAdvice.kt | 90 +++++++++++++++++++ .../xyz/fortern/forternapi/web/ExResponse.kt | 9 ++ .../xyz/fortern/forternapi/web/ExStatus.kt | 16 ++++ src/main/resources/application.properties | 0 src/main/resources/application.yml | 35 ++++++++ src/main/resources/i18n/messages.properties | 1 + .../resources/i18n/messages_en_US.properties | 7 ++ .../resources/i18n/messages_zh_CN.properties | 7 ++ 21 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/xyz/fortern/forternapi/config/RedisConfig.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/config/SecurityConfig.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/controller/AuthController.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/controller/MessageController.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/controller/TestController.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/exception/BusinessException.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/model/Message.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/service/AuthService.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/service/MessageService.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/util/Generator.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/util/RedisKeys.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/web/ControllerAdvice.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/web/ExResponse.kt create mode 100644 src/main/kotlin/xyz/fortern/forternapi/web/ExStatus.kt delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/i18n/messages.properties create mode 100644 src/main/resources/i18n/messages_en_US.properties create mode 100644 src/main/resources/i18n/messages_zh_CN.properties diff --git a/build.gradle.kts b/build.gradle.kts index 90ac213..badd087 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget 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" kotlin("jvm") version "1.9.22" kotlin("plugin.spring") version "1.9.22" @@ -25,15 +25,41 @@ repositories { mavenCentral() } +//版本号定义 +object Versions { + const val FAST_JSON_2 = "2.0.45" +} + dependencies { // 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 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 // https://mvnrepository.com/artifact/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 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 { diff --git a/settings.gradle.kts b/settings.gradle.kts index 45443df..6560946 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "fortern-api" +rootProject.name = "message-api" diff --git a/src/main/kotlin/xyz/fortern/forternapi/config/RedisConfig.kt b/src/main/kotlin/xyz/fortern/forternapi/config/RedisConfig.kt new file mode 100644 index 0000000..03d854e --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/config/RedisConfig.kt @@ -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 { + // Redis模板对象 + val template = RedisTemplate() + + // 设置连接工厂 + 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 + } +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/config/SecurityConfig.kt b/src/main/kotlin/xyz/fortern/forternapi/config/SecurityConfig.kt new file mode 100644 index 0000000..6ff0c4f --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/config/SecurityConfig.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/fortern/forternapi/controller/AuthController.kt b/src/main/kotlin/xyz/fortern/forternapi/controller/AuthController.kt new file mode 100644 index 0000000..98e1e9c --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/controller/AuthController.kt @@ -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 { + 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) + } +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/controller/MessageController.kt b/src/main/kotlin/xyz/fortern/forternapi/controller/MessageController.kt new file mode 100644 index 0000000..db280de --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/controller/MessageController.kt @@ -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 { + 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 { + 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 { + messageService.delMessage(name, id) + return ResponseEntity.ok(null) + } + + /** + * 读取消息列表 + * + * @param name 消息接受者 + */ + @GetMapping("/list") + fun list(@Size(min = 1, max = 30) name: String?): Map> { + return messageService.messageList(name ?: "") + } +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/controller/TestController.kt b/src/main/kotlin/xyz/fortern/forternapi/controller/TestController.kt new file mode 100644 index 0000000..d4356e1 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/controller/TestController.kt @@ -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" + } + +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/exception/BusinessException.kt b/src/main/kotlin/xyz/fortern/forternapi/exception/BusinessException.kt new file mode 100644 index 0000000..a9fe113 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/exception/BusinessException.kt @@ -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) diff --git a/src/main/kotlin/xyz/fortern/forternapi/model/Message.kt b/src/main/kotlin/xyz/fortern/forternapi/model/Message.kt new file mode 100644 index 0000000..48f6376 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/model/Message.kt @@ -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, +) { + +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/service/AuthService.kt b/src/main/kotlin/xyz/fortern/forternapi/service/AuthService.kt new file mode 100644 index 0000000..2488759 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/service/AuthService.kt @@ -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 +) { + val opsForValue = redisTemplate.opsForValue() + + fun login(password: String): Boolean { + return opsForValue[RedisKeys.PASSWORD_KEY] == password + } +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/service/MessageService.kt b/src/main/kotlin/xyz/fortern/forternapi/service/MessageService.kt new file mode 100644 index 0000000..a3e1a76 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/service/MessageService.kt @@ -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, +) { + 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> { + // key命令虽然时间复杂度为 O(N),但扫描速度极快,在当前项目中可以使用 + val keys = redisTemplate.keys("${RedisKeys.MESSAGE_PREFIX}${if (name.isNotEmpty()) "$name:" else ""}*") + if (name.isEmpty()) { + val map = HashMap>() + 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") + + } +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/util/Generator.kt b/src/main/kotlin/xyz/fortern/forternapi/util/Generator.kt new file mode 100644 index 0000000..896f01c --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/util/Generator.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/fortern/forternapi/util/RedisKeys.kt b/src/main/kotlin/xyz/fortern/forternapi/util/RedisKeys.kt new file mode 100644 index 0000000..02a0d7e --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/util/RedisKeys.kt @@ -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:" + +} \ No newline at end of file diff --git a/src/main/kotlin/xyz/fortern/forternapi/web/ControllerAdvice.kt b/src/main/kotlin/xyz/fortern/forternapi/web/ControllerAdvice.kt new file mode 100644 index 0000000..60fd8a1 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/web/ControllerAdvice.kt @@ -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 = 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 = 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 { + 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) + } +} diff --git a/src/main/kotlin/xyz/fortern/forternapi/web/ExResponse.kt b/src/main/kotlin/xyz/fortern/forternapi/web/ExResponse.kt new file mode 100644 index 0000000..d395d83 --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/web/ExResponse.kt @@ -0,0 +1,9 @@ +package xyz.fortern.forternapi.web + +/** + * 通用的错误响应,一般在异常处理中使用 + */ +class ExResponse( + val status: String, + val errors: List +) diff --git a/src/main/kotlin/xyz/fortern/forternapi/web/ExStatus.kt b/src/main/kotlin/xyz/fortern/forternapi/web/ExStatus.kt new file mode 100644 index 0000000..461ea6e --- /dev/null +++ b/src/main/kotlin/xyz/fortern/forternapi/web/ExStatus.kt @@ -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" diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..a47c163 --- /dev/null +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..0e79384 --- /dev/null +++ b/src/main/resources/i18n/messages.properties @@ -0,0 +1 @@ +# 必须有默认的资源文件,否则 MessageSourceAutoConfiguration 不会执行 \ No newline at end of file diff --git a/src/main/resources/i18n/messages_en_US.properties b/src/main/resources/i18n/messages_en_US.properties new file mode 100644 index 0000000..3371894 --- /dev/null +++ b/src/main/resources/i18n/messages_en_US.properties @@ -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. diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 0000000..fdbd7b0 --- /dev/null +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -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 字符