基本功能
This commit is contained in:
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