Complete core functions #9
@@ -36,19 +36,7 @@ class AvatarController(
|
||||
@Operation(summary = "获取随机头像")
|
||||
@GetMapping(produces = [MediaType.IMAGE_PNG_VALUE])
|
||||
fun getRandom(@Valid avatarBaseParam: AvatarBaseParam?): ByteArray =
|
||||
when ((1..4).random()) {
|
||||
1 -> avatarService.triangle(avatarBaseParam)
|
||||
2 -> avatarService.square(avatarBaseParam)
|
||||
3 -> avatarService.identicon(avatarBaseParam)
|
||||
else -> avatarService.github(AvatarGitHubParam().apply {
|
||||
seed = avatarBaseParam?.seed
|
||||
size = avatarBaseParam?.size
|
||||
margin = avatarBaseParam?.margin
|
||||
padding = avatarBaseParam?.padding
|
||||
colors = avatarBaseParam?.colors
|
||||
background = avatarBaseParam?.background
|
||||
})
|
||||
}
|
||||
avatarService.random(avatarBaseParam)
|
||||
|
||||
/**
|
||||
* Get random avatar as base64
|
||||
@@ -67,19 +55,7 @@ class AvatarController(
|
||||
@Valid avatarBaseParam: AvatarBaseParam?
|
||||
): ResponseResult<AvatarBase64Vo> =
|
||||
ResponseResult.success(
|
||||
ResponseCode.API_AVATAR_SUCCESS, data = when ((1..4).random()) {
|
||||
1 -> avatarService.triangleBase64(avatarBaseParam)
|
||||
2 -> avatarService.squareBase64(avatarBaseParam)
|
||||
3 -> avatarService.identiconBase64(avatarBaseParam)
|
||||
else -> avatarService.githubBase64(AvatarGitHubParam().apply {
|
||||
seed = avatarBaseParam?.seed
|
||||
size = avatarBaseParam?.size
|
||||
margin = avatarBaseParam?.margin
|
||||
padding = avatarBaseParam?.padding
|
||||
colors = avatarBaseParam?.colors
|
||||
background = avatarBaseParam?.background
|
||||
})
|
||||
}
|
||||
ResponseCode.API_AVATAR_SUCCESS, data = avatarService.randomBase64(avatarBaseParam)
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,8 @@ import top.fatweb.api.annotation.BaseController
|
||||
import top.fatweb.api.entity.common.ResponseCode
|
||||
import top.fatweb.api.entity.common.ResponseResult
|
||||
import top.fatweb.api.param.permission.LoginParam
|
||||
import top.fatweb.api.param.permission.RegisterParam
|
||||
import top.fatweb.api.param.permission.VerifyParam
|
||||
import top.fatweb.api.service.permission.IAuthenticationService
|
||||
import top.fatweb.api.util.WebUtil
|
||||
import top.fatweb.api.vo.permission.LoginVo
|
||||
@@ -26,6 +28,34 @@ import top.fatweb.api.vo.permission.TokenVo
|
||||
class AuthenticationController(
|
||||
private val authenticationService: IAuthenticationService
|
||||
) {
|
||||
/**
|
||||
* Register
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Operation(summary = "注册")
|
||||
@PostMapping("/register")
|
||||
fun register(@Valid @RequestBody registerParam: RegisterParam): ResponseResult<Nothing> {
|
||||
authenticationService.register(registerParam)
|
||||
|
||||
return ResponseResult.success(ResponseCode.PERMISSION_REGISTER_SUCCESS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Operation(summary = "验证")
|
||||
@PostMapping("/verify")
|
||||
fun verify(@Valid @RequestBody verifyParam: VerifyParam): ResponseResult<Nothing> {
|
||||
authenticationService.verify(verifyParam)
|
||||
|
||||
return ResponseResult.success(ResponseCode.PERMISSION_VERIFY_SUCCESS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
|
||||
@@ -74,6 +74,7 @@ object UserConverter {
|
||||
fun userToUserWithRoleInfoVo(user: User) = UserWithRoleInfoVo(
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
verify = user.verify,
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
credentialsExpiration = user.credentialsExpiration,
|
||||
@@ -137,6 +138,7 @@ object UserConverter {
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
password = user.password,
|
||||
verify = user.verify,
|
||||
locking = user.locking?.let { it == 1 },
|
||||
expiration = user.expiration,
|
||||
credentialsExpiration = user.credentialsExpiration,
|
||||
|
||||
@@ -18,6 +18,8 @@ enum class ResponseCode(val code: Int) {
|
||||
PERMISSION_PASSWORD_CHANGE_SUCCESS(BusinessCode.PERMISSION, 1),
|
||||
PERMISSION_LOGOUT_SUCCESS(BusinessCode.PERMISSION, 2),
|
||||
PERMISSION_TOKEN_RENEW_SUCCESS(BusinessCode.PERMISSION, 3),
|
||||
PERMISSION_REGISTER_SUCCESS(BusinessCode.PERMISSION, 4),
|
||||
PERMISSION_VERIFY_SUCCESS(BusinessCode.PERMISSION, 5),
|
||||
|
||||
PERMISSION_UNAUTHORIZED(BusinessCode.PERMISSION, 50),
|
||||
PERMISSION_USERNAME_NOT_FOUND(BusinessCode.PERMISSION, 51),
|
||||
|
||||
@@ -46,6 +46,15 @@ class User() : Serializable {
|
||||
@TableField("password")
|
||||
var password: String? = null
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@TableField("verify")
|
||||
var verify: String? = null
|
||||
|
||||
/**
|
||||
* Locking
|
||||
*
|
||||
|
||||
@@ -19,7 +19,7 @@ data class LoginParam(
|
||||
*/
|
||||
@Schema(description = "账户", example = "test", required = true)
|
||||
@field:NotBlank(message = "Account can not be blank")
|
||||
val account: String? = null,
|
||||
val account: String?,
|
||||
|
||||
/**
|
||||
* Password
|
||||
@@ -29,5 +29,5 @@ data class LoginParam(
|
||||
*/
|
||||
@Schema(description = "密码", example = "test123456", required = true)
|
||||
@field:NotBlank(message = "Password can not be blank")
|
||||
val password: String? = null
|
||||
val password: String?
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
package top.fatweb.api.param.permission
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.Email
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
|
||||
/**
|
||||
* Register parameters
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "注册请求参数")
|
||||
data class RegisterParam(
|
||||
/**
|
||||
* Email
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "邮箱", example = "guest@fatweb.top", required = true)
|
||||
@field:Email(message = "Illegal email address")
|
||||
val email: String?,
|
||||
|
||||
/**
|
||||
* Password
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "密码", example = "test123456", required = true)
|
||||
@field:NotBlank(message = "Password can not be blank")
|
||||
val password: String?
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
package top.fatweb.api.param.permission
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
|
||||
/**
|
||||
* Verify parameters
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "验证请求参数")
|
||||
data class VerifyParam(
|
||||
/**
|
||||
* Code
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "验证码", example = "9c4b8199-1dbe-4f6f-96a5-fe1d75cc6a65", required = true)
|
||||
@field:NotBlank(message = "Code can not be blank")
|
||||
val code: String?,
|
||||
|
||||
/**
|
||||
* Username
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "用户名", example = "adb")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Nickname
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "昵称", example = "QwQ")
|
||||
val nickname: String?,
|
||||
|
||||
/**
|
||||
* Avatar
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "头像")
|
||||
val avatar: String?
|
||||
)
|
||||
@@ -37,5 +37,5 @@ data class GroupAddParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "角色 ID 列表")
|
||||
val roleIds: List<Long>? = null
|
||||
val roleIds: List<Long>?
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ data class GroupGetParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "查询用户组名称")
|
||||
val searchName: String? = null,
|
||||
val searchName: String?,
|
||||
|
||||
/**
|
||||
* Use regex
|
||||
|
||||
@@ -48,5 +48,5 @@ data class GroupUpdateParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "角色 ID 列表")
|
||||
val roleIds: List<Long>? = null
|
||||
val roleIds: List<Long>?
|
||||
)
|
||||
|
||||
@@ -37,5 +37,5 @@ data class RoleAddParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "权限 ID 列表")
|
||||
val powerIds: List<Long>? = null
|
||||
val powerIds: List<Long>?
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ data class RoleGetParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "查询角色名称")
|
||||
val searchName: String? = null,
|
||||
val searchName: String?,
|
||||
|
||||
/**
|
||||
* Use regex
|
||||
|
||||
@@ -48,5 +48,5 @@ data class RoleUpdateParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "权限 ID 列表")
|
||||
val powerIds: List<Long>? = null
|
||||
val powerIds: List<Long>?
|
||||
)
|
||||
|
||||
@@ -32,6 +32,15 @@ data class UserAddParam(
|
||||
@Schema(description = "密码(为空自动生成随机密码)")
|
||||
val password: String?,
|
||||
|
||||
/**
|
||||
* Verified
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "是否已验证")
|
||||
val verified: Boolean = false,
|
||||
|
||||
/**
|
||||
* Locking
|
||||
*
|
||||
|
||||
@@ -28,7 +28,7 @@ data class UserGetParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "查询内容")
|
||||
val searchValue: String? = null,
|
||||
val searchValue: String?,
|
||||
|
||||
/**
|
||||
* Use regex
|
||||
|
||||
@@ -32,6 +32,15 @@ data class UserUpdateParam(
|
||||
@Schema(description = "用户名")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Verified
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "是否已验证")
|
||||
val verified: Boolean = false,
|
||||
|
||||
/**
|
||||
* Locking
|
||||
*
|
||||
|
||||
@@ -21,7 +21,7 @@ data class SysLogGetParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "类型过滤(多个使用逗号分隔)", example = "INFO", allowableValues = ["INFO", "ERROR"])
|
||||
val logType: String? = null,
|
||||
val logType: String?,
|
||||
|
||||
/**
|
||||
* Request method to filter
|
||||
@@ -34,7 +34,7 @@ data class SysLogGetParam(
|
||||
example = "GET,POST",
|
||||
allowableValues = ["GET", "POST", "PUT", "PATCH", "DELETE", "DELETE", "OPTIONS"]
|
||||
)
|
||||
val requestMethod: String? = null,
|
||||
val requestMethod: String?,
|
||||
|
||||
/**
|
||||
* Request URL to search for
|
||||
@@ -43,7 +43,7 @@ data class SysLogGetParam(
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "查询请求 Url")
|
||||
val searchRequestUrl: String? = null,
|
||||
val searchRequestUrl: String?,
|
||||
|
||||
/**
|
||||
* Use regex
|
||||
@@ -65,7 +65,7 @@ data class SysLogGetParam(
|
||||
*/
|
||||
@Schema(description = "查询开始时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
val searchStartTime: LocalDateTime? = null,
|
||||
val searchStartTime: LocalDateTime?,
|
||||
|
||||
/**
|
||||
* End time to search for
|
||||
@@ -76,5 +76,5 @@ data class SysLogGetParam(
|
||||
*/
|
||||
@Schema(description = "查询结束时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
val searchEndTime: LocalDateTime? = null
|
||||
val searchEndTime: LocalDateTime?
|
||||
) : PageSortParam()
|
||||
@@ -11,6 +11,22 @@ import top.fatweb.api.vo.api.v1.avatar.AvatarBase64Vo
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface IAvatarService {
|
||||
/**
|
||||
* Generate random avatar
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun random(avatarBaseParam: AvatarBaseParam?): ByteArray
|
||||
|
||||
/**
|
||||
* Generate random avatar as base64
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun randomBase64(avatarBaseParam: AvatarBaseParam?): AvatarBase64Vo
|
||||
|
||||
/**
|
||||
* Generate triangle style avatar
|
||||
*
|
||||
|
||||
@@ -25,6 +25,34 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
@Service
|
||||
class AvatarServiceImpl : IAvatarService {
|
||||
override fun random(avatarBaseParam: AvatarBaseParam?): ByteArray =
|
||||
when ((1..4).random()) {
|
||||
1 -> triangle(avatarBaseParam)
|
||||
2 -> square(avatarBaseParam)
|
||||
3 -> identicon(avatarBaseParam)
|
||||
else -> github(AvatarGitHubParam().apply {
|
||||
seed = avatarBaseParam?.seed
|
||||
size = avatarBaseParam?.size
|
||||
margin = avatarBaseParam?.margin
|
||||
padding = avatarBaseParam?.padding
|
||||
colors = avatarBaseParam?.colors
|
||||
background = avatarBaseParam?.background
|
||||
})
|
||||
}
|
||||
|
||||
override fun randomBase64(avatarBaseParam: AvatarBaseParam?): AvatarBase64Vo = when ((1..4).random()) {
|
||||
1 -> triangleBase64(avatarBaseParam)
|
||||
2 -> squareBase64(avatarBaseParam)
|
||||
3 -> identiconBase64(avatarBaseParam)
|
||||
else -> githubBase64(AvatarGitHubParam().apply {
|
||||
seed = avatarBaseParam?.seed
|
||||
size = avatarBaseParam?.size
|
||||
margin = avatarBaseParam?.margin
|
||||
padding = avatarBaseParam?.padding
|
||||
colors = avatarBaseParam?.colors
|
||||
background = avatarBaseParam?.background
|
||||
})
|
||||
}
|
||||
|
||||
override fun triangle(avatarBaseParam: AvatarBaseParam?): ByteArray {
|
||||
val avatar = (
|
||||
|
||||
@@ -3,6 +3,8 @@ package top.fatweb.api.service.permission
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import top.fatweb.api.entity.permission.User
|
||||
import top.fatweb.api.param.permission.LoginParam
|
||||
import top.fatweb.api.param.permission.RegisterParam
|
||||
import top.fatweb.api.param.permission.VerifyParam
|
||||
import top.fatweb.api.vo.permission.LoginVo
|
||||
import top.fatweb.api.vo.permission.TokenVo
|
||||
|
||||
@@ -13,6 +15,22 @@ import top.fatweb.api.vo.permission.TokenVo
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface IAuthenticationService {
|
||||
/**
|
||||
* Register
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun register(registerParam: RegisterParam)
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun verify(verifyParam: VerifyParam)
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
|
||||
@@ -6,15 +6,22 @@ import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import top.fatweb.api.annotation.EventLogRecord
|
||||
import top.fatweb.api.entity.permission.LoginUser
|
||||
import top.fatweb.api.entity.permission.User
|
||||
import top.fatweb.api.entity.permission.UserInfo
|
||||
import top.fatweb.api.entity.system.EventLog
|
||||
import top.fatweb.api.exception.TokenHasExpiredException
|
||||
import top.fatweb.api.param.permission.LoginParam
|
||||
import top.fatweb.api.param.permission.RegisterParam
|
||||
import top.fatweb.api.param.permission.VerifyParam
|
||||
import top.fatweb.api.properties.SecurityProperties
|
||||
import top.fatweb.api.service.api.v1.IAvatarService
|
||||
import top.fatweb.api.service.permission.IAuthenticationService
|
||||
import top.fatweb.api.service.permission.IUserInfoService
|
||||
import top.fatweb.api.service.permission.IUserService
|
||||
import top.fatweb.api.util.JwtUtil
|
||||
import top.fatweb.api.util.RedisUtil
|
||||
@@ -23,6 +30,7 @@ import top.fatweb.api.vo.permission.LoginVo
|
||||
import top.fatweb.api.vo.permission.TokenVo
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Authentication service implement
|
||||
@@ -37,11 +45,33 @@ import java.time.ZoneOffset
|
||||
@Service
|
||||
class AuthenticationServiceImpl(
|
||||
private val authenticationManager: AuthenticationManager,
|
||||
private val passwordEncoder: PasswordEncoder,
|
||||
private val redisUtil: RedisUtil,
|
||||
private val userService: IUserService
|
||||
private val userService: IUserService,
|
||||
private val userInfoService: IUserInfoService,
|
||||
private val avatarService: IAvatarService
|
||||
) : IAuthenticationService {
|
||||
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional
|
||||
override fun register(registerParam: RegisterParam) {
|
||||
val user = User().apply {
|
||||
username = "\$UNNAMED_${UUID.randomUUID()}"
|
||||
password = passwordEncoder.encode(registerParam.password)
|
||||
locking = 0
|
||||
enable = 1
|
||||
}
|
||||
userService.save(user)
|
||||
userInfoService.save(UserInfo().apply {
|
||||
userId = user.id
|
||||
avatar = avatarService.randomBase64(null).base64
|
||||
email = registerParam.email
|
||||
})
|
||||
}
|
||||
|
||||
override fun verify(verifyParam: VerifyParam) {
|
||||
}
|
||||
|
||||
@EventLogRecord(EventLog.Event.LOGIN)
|
||||
override fun login(request: HttpServletRequest, loginParam: LoginParam): LoginVo {
|
||||
val usernamePasswordAuthenticationToken =
|
||||
|
||||
@@ -43,6 +43,15 @@ data class UserWithPasswordRoleInfoVo(
|
||||
@Schema(description = "密码")
|
||||
val password: String?,
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "验证信息")
|
||||
val verify: String?,
|
||||
|
||||
/**
|
||||
* Locking
|
||||
*
|
||||
|
||||
@@ -34,6 +34,15 @@ data class UserWithRoleInfoVo(
|
||||
@Schema(description = "用户名", example = "User")
|
||||
val username: String?,
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "验证信息")
|
||||
val verify: String?,
|
||||
|
||||
/**
|
||||
* Locking
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ create table if not exists t_user
|
||||
id bigint not null primary key,
|
||||
username varchar(20) not null comment '用户名',
|
||||
password char(70) not null comment '密码',
|
||||
verify varchar(36) null comment '验证信息',
|
||||
locking int not null comment '锁定',
|
||||
expiration datetime comment '过期时间',
|
||||
credentials_expiration datetime comment '认证过期时间',
|
||||
@@ -17,5 +18,6 @@ create table if not exists t_user
|
||||
update_time datetime not null default (utc_timestamp()) comment '修改时间',
|
||||
deleted bigint not null default 0,
|
||||
version int not null default 0,
|
||||
constraint t_user_unique unique (username, deleted)
|
||||
constraint t_user_unique_username unique (username, deleted),
|
||||
constraint t_user_unique_verify unique (verify, deleted)
|
||||
) comment '用户表';
|
||||
Reference in New Issue
Block a user