refactor(vertx-demo):重构项目目录结构

This commit is contained in:
AiKrai 2025-05-12 15:41:14 +08:00
parent 047717ad71
commit 95f76404c2
34 changed files with 1029 additions and 125 deletions

View File

@ -1,3 +1,5 @@
闲暇时编写,问题较多,仅供参考。
# Vert.x 快速开发模板
这是一个基于 Vert.x 库的后端快速开发模板,采用 Kotlin 语言和协程实现高性能异步编程。本项目提供了完整的开发框架,能够帮助开发者快速搭建响应式、模块化、高性能的后端服务。
@ -7,7 +9,7 @@
- **核心框架**: Vert.x 4.5.x (响应式、事件驱动框架)
- **编程语言**: Kotlin 1.9.x (协程支持)
- **依赖注入**: Google Guice 7.0.0
- **数据库**: PostgreSQL (主), MySQL (可选)
- **数据库**: PostgreSQL, (不支持MySQL)
- **缓存**: Redis
- **认证**: JWT
- **构建工具**: Gradle with Kotlin DSL

View File

@ -2,6 +2,7 @@ package app.config
import app.config.provider.JWTAuthProvider
import app.config.provider.DbPoolProvider
import app.config.provider.RedisProvider
import cn.hutool.core.lang.Snowflake
import cn.hutool.core.util.IdUtil
import com.google.inject.AbstractModule
@ -10,6 +11,7 @@ import com.google.inject.Injector
import com.google.inject.Singleton
import io.vertx.core.Vertx
import io.vertx.ext.auth.jwt.JWTAuth
import io.vertx.redis.client.Redis
import io.vertx.sqlclient.Pool
import io.vertx.sqlclient.SqlClient
import kotlinx.coroutines.CoroutineScope
@ -41,6 +43,7 @@ class InjectorModule(
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
bind(Redis::class.java).toProvider(RedisProvider::class.java).`in`(Singleton::class.java)
bind(Pool::class.java).toProvider(DbPoolProvider::class.java).`in`(Singleton::class.java)
bind(SqlClient::class.java).to(Pool::class.java)

View File

@ -0,0 +1,24 @@
package app.config.provider
import com.google.inject.Inject
import com.google.inject.Provider
import io.vertx.core.Vertx
import io.vertx.redis.client.Redis
import io.vertx.redis.client.RedisClientType
import io.vertx.redis.client.RedisOptions
import org.aikrai.vertx.config.RedisConfig
class RedisProvider @Inject constructor(
private val vertx: Vertx,
private val redisConfig: RedisConfig
) : Provider<Redis> {
override fun get(): Redis {
val options = RedisOptions()
.setType(RedisClientType.STANDALONE)
.addConnectionString("redis://${redisConfig.host}:${redisConfig.port}/${redisConfig.db}")
.setMaxPoolSize(redisConfig.poolSize)
.setMaxPoolWaiting(redisConfig.maxPoolWaiting)
redisConfig.password?.let { options.setPassword(it) }
return Redis.createClient(vertx, options)
}
}

View File

@ -1,6 +1,6 @@
package app.controller
import app.data.domain.account.LoginDTO
import app.data.dto.account.LoginDTO
import app.service.account.AccountService
import com.google.inject.Inject
import io.vertx.ext.web.RoutingContext

View File

@ -1,7 +1,7 @@
package app.controller
import app.data.domain.account.Account
import app.data.domain.account.AccountRepository
import app.repository.AccountRepository
import app.data.emun.Status
import app.service.account.AccountService
import com.google.inject.Inject

View File

@ -1,5 +1,6 @@
package app.data.domain.account
import app.data.emun.SexType
import app.data.emun.Status
import org.aikrai.vertx.db.annotation.*
import org.aikrai.vertx.jackson.JsonUtil
@ -13,21 +14,47 @@ class Account : BaseEntity() {
@TableFieldComment("用户ID")
var userId: Long = 0L
@TableField("user_name")
var userName: String? = ""
@TableFieldComment("部门ID")
var deptId: Long? = 0L
@TableField(length = 30)
@TableFieldComment("用户账号")
var userName: String = ""
@TableField(length = 30)
@TableFieldComment("用户昵称")
var nickName: String = ""
@TableField(length = 2)
@TableFieldComment("用户类型")
var userType: String? = ""
@TableField(length = 50)
@TableFieldComment("用户邮箱")
var email: String? = ""
var phone: String? = ""
@TableField(length = 11)
@TableFieldComment("手机号码")
var phonenumber: String? = ""
@TableField(length = 1)
@TableFieldComment("用户性别0男 1女 2未知")
var sex: SexType? = SexType.UNKNOWN
@TableField(length = 100)
@TableFieldComment("头像地址")
var avatar: String? = null
@TableField(length = 100)
@TableFieldComment("密码")
var password: String? = null
@TableField(length = 1)
@TableFieldComment("帐号状态0正常 1停用")
var status: Status? = Status.ACTIVE
@TableField(length = 1)
@TableFieldComment("删除标志0代表存在 2代表删除")
var delFlag: Char? = null
var loginIp: String? = null

View File

@ -1,7 +0,0 @@
package app.data.domain.role
import com.google.inject.ImplementedBy
import org.aikrai.vertx.db.wrapper.Repository
@ImplementedBy(RoleRepositoryImpl::class)
interface RoleRepository : Repository<Long, Role>

View File

@ -1,4 +1,4 @@
package app.data.domain.account.modle
package app.data.dto.account
import app.data.domain.account.Account
import app.data.domain.menu.Menu

View File

@ -1,4 +1,4 @@
package app.data.domain.account.modle
package app.data.dto.account
import app.data.domain.role.Role

View File

@ -1,6 +1,6 @@
package app.data.domain.account
package app.data.dto.account
data class LoginDTO(
var username: String,
var password: String
)
)

View File

@ -1,4 +1,4 @@
package app.base.domain.auth.modle
package app.data.dto.account
class LoginUser {
var accountId: Long = 0L
@ -7,4 +7,4 @@ class LoginUser {
var expireTime: Long = 0L
var ipaddr: String = ""
var client: String = ""
}
}

View File

@ -1,4 +1,4 @@
package app.data.domain.menu.modle
package app.data.dto.menu
import app.data.domain.menu.Menu

View File

@ -1,4 +1,4 @@
package app.base.domain.auth.menu
package app.data.emun
enum class MenuType(val desc: String) {
M("目录"),
@ -8,7 +8,7 @@ enum class MenuType(val desc: String) {
companion object {
fun parse(value: String?): MenuType? {
if (value.isNullOrBlank()) return null
return MenuType.values().find { it.name == value || it.desc == value }
return MenuType.entries.find { it.name == value || it.desc == value }
}
}
}
}

View File

@ -0,0 +1,28 @@
package app.data.emun
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
import org.aikrai.vertx.db.annotation.EnumValue
enum class SexType(private val code: Int, private val description: String) {
MALE(0, ""),
FEMALE(1, ""),
UNKNOWN(2, "未知");
@JsonValue
@EnumValue
fun getCode(): Int {
return code
}
override fun toString(): String {
return description
}
companion object {
@JsonCreator
fun parse(code: Int): SexType? {
return SexType.entries.find { it.code == code }
}
}
}

View File

@ -1,46 +0,0 @@
package app.port.reids
import com.google.inject.Inject
import com.google.inject.Singleton
import io.vertx.core.Vertx
import io.vertx.kotlin.coroutines.coAwait
import io.vertx.redis.client.*
import io.github.oshai.kotlinlogging.KotlinLogging
import org.aikrai.vertx.config.RedisConfig
@Singleton
class RedisClient @Inject constructor(
vertx: Vertx,
redisConfig: RedisConfig
) {
private val logger = KotlinLogging.logger { }
private var redisClient = Redis.createClient(
vertx,
RedisOptions()
.setType(RedisClientType.STANDALONE)
.addConnectionString("redis://${redisConfig.host}:${redisConfig.port}/${redisConfig.db}")
.setPassword(redisConfig.pass ?: "")
.setMaxPoolSize(redisConfig.poolSize)
.setMaxPoolWaiting(redisConfig.maxPoolWaiting)
)
// EX秒PX毫秒
suspend fun set(key: String, value: String, expireSeconds: Int) {
redisClient.send(Request.cmd(Command.SET, key, value, "EX", expireSeconds))
}
suspend fun get(key: String): String? {
val res = redisClient.send(Request.cmd(Command.GET, key)).coAwait()
return res?.toString()
}
suspend fun incr(key: String): Int {
val res = redisClient.send(Request.cmd(Command.INCR, key)).coAwait()
return res?.toInteger() ?: 0
}
fun expire(key: String, expire: String) {
redisClient.send(Request.cmd(Command.EXPIRE, key, expire))
}
}

View File

@ -1,7 +1,9 @@
package app.data.domain.account
package app.repository
import app.data.domain.account.modle.AccountRoleAccessDTO
import app.data.domain.account.modle.AccountRoleDTO
import app.data.domain.account.Account
import app.repository.impl.AccountRepositoryImpl
import app.data.dto.account.AccountRoleAccessDTO
import app.data.dto.account.AccountRoleDTO
import com.google.inject.ImplementedBy
import org.aikrai.vertx.db.wrapper.Repository
@ -14,4 +16,4 @@ interface AccountRepository : Repository<Long, Account> {
suspend fun getAccountRole(id: Long): AccountRoleDTO?
suspend fun bindRoles(id: Long, roles: List<Long>)
suspend fun removeAllRole(id: Long): Int
}
}

View File

@ -1,9 +1,11 @@
package app.data.domain.menu
package app.repository
import app.data.domain.menu.Menu
import app.repository.impl.MenuRepositoryImpl
import com.google.inject.ImplementedBy
import org.aikrai.vertx.db.wrapper.Repository
@ImplementedBy(MenuRepositoryImpl::class)
interface MenuRepository : Repository<Long, Menu> {
suspend fun list(name: String? = null, accountId: Long? = null, roleId: Long? = null): List<Menu>
}
}

View File

@ -0,0 +1,9 @@
package app.repository
import app.data.domain.role.Role
import app.repository.impl.RoleRepositoryImpl
import com.google.inject.ImplementedBy
import org.aikrai.vertx.db.wrapper.Repository
@ImplementedBy(RoleRepositoryImpl::class)
interface RoleRepository : Repository<Long, Role>

View File

@ -1,7 +1,9 @@
package app.data.domain.account
package app.repository.impl
import app.data.domain.account.modle.AccountRoleAccessDTO
import app.data.domain.account.modle.AccountRoleDTO
import app.data.domain.account.Account
import app.data.dto.account.AccountRoleAccessDTO
import app.data.dto.account.AccountRoleDTO
import app.repository.AccountRepository
import com.google.inject.Inject
import io.vertx.sqlclient.SqlClient
import org.aikrai.vertx.db.wrapper.RepositoryImpl
@ -16,7 +18,7 @@ class AccountRepositoryImpl @Inject constructor(
): List<Account> {
return queryBuilder()
.eq(!userName.isNullOrBlank(), Account::userName, userName)
.eq(!phone.isNullOrBlank(), Account::phone, phone)
.eq(!phone.isNullOrBlank(), Account::phonenumber, phone)
.getList()
}
@ -101,4 +103,4 @@ class AccountRepositoryImpl @Inject constructor(
val sql = "DELETE FROM account_role WHERE account_id = #{$id}"
return execute(sql)
}
}
}

View File

@ -1,5 +1,7 @@
package app.data.domain.menu
package app.repository.impl
import app.data.domain.menu.Menu
import app.repository.MenuRepository
import com.google.inject.Inject
import io.vertx.sqlclient.SqlClient
import org.aikrai.vertx.db.wrapper.RepositoryImpl
@ -11,4 +13,4 @@ class MenuRepositoryImpl @Inject constructor(
override suspend fun list(name: String?, accountId: Long?, roleId: Long?): List<Menu> {
return emptyList()
}
}
}

View File

@ -1,9 +1,11 @@
package app.data.domain.role
package app.repository.impl
import app.data.domain.role.Role
import app.repository.RoleRepository
import com.google.inject.Inject
import io.vertx.sqlclient.SqlClient
import org.aikrai.vertx.db.wrapper.RepositoryImpl
class RoleRepositoryImpl @Inject constructor(
sqlClient: SqlClient
) : RepositoryImpl<Long, Role>(sqlClient), RoleRepository
) : RepositoryImpl<Long, Role>(sqlClient), RoleRepository

View File

@ -1,6 +1,8 @@
package app.data.domain.menu
package app.service
import app.data.domain.account.Account
import app.data.domain.menu.Menu
import app.repository.MenuRepository
import com.google.inject.Inject
import com.google.inject.Singleton
import io.vertx.ext.auth.User
@ -47,7 +49,7 @@ class MenuManager @Inject constructor(
perms: String
) {
if (menuRepository.list(menuName).isNotEmpty()) {
throw Meta.error("MenuNameConflict", "菜单名称已存在")
throw Meta.Companion.error("MenuNameConflict", "菜单名称已存在")
}
val menu = Menu().apply {
this.menuName = menuName
@ -73,10 +75,10 @@ class MenuManager @Inject constructor(
visible: String?,
perms: String?
) {
val menu = menuRepository.get(menuId) ?: throw Meta.notFound("MenuNotFound", "菜单不存在")
val menu = menuRepository.get(menuId) ?: throw Meta.Companion.notFound("MenuNotFound", "菜单不存在")
if (menuName != null && menuName != menu.menuName && menuRepository.list(menuName).isNotEmpty()) {
throw Meta.error("MenuNameConflict", "菜单名称已存在")
throw Meta.Companion.error("MenuNameConflict", "菜单名称已存在")
}
menu.apply {
@ -147,4 +149,4 @@ class MenuManager @Inject constructor(
return getChildList(list, t).isNotEmpty()
}
}
}
}

View File

@ -1,8 +1,8 @@
package app.service.account
import app.data.domain.account.Account
import app.data.domain.account.AccountRepository
import app.data.domain.account.LoginDTO
import app.repository.AccountRepository
import app.data.dto.account.LoginDTO
import app.service.auth.TokenService
import cn.hutool.core.lang.Snowflake
import cn.hutool.crypto.SecureUtil

View File

@ -1,7 +1,7 @@
package app.service.auth
import app.data.domain.account.AccountRepository
import app.port.reids.RedisClient
import app.repository.AccountRepository
import app.utils.RedisUtil
import cn.hutool.core.util.IdUtil
import com.google.inject.Inject
import com.google.inject.Singleton
@ -14,20 +14,24 @@ import io.vertx.ext.auth.jwt.JWTAuth
import io.vertx.ext.web.RoutingContext
import io.vertx.kotlin.coroutines.coAwait
import io.github.oshai.kotlinlogging.KotlinLogging
import io.vertx.redis.client.Redis
import org.aikrai.vertx.auth.AuthUser
import org.aikrai.vertx.constant.CacheConstants
import org.aikrai.vertx.constant.Constants
import org.aikrai.vertx.jackson.JsonUtil
import org.aikrai.vertx.utlis.Meta
import java.util.concurrent.TimeUnit
@Singleton
class TokenService @Inject constructor(
redis: Redis,
private val jwtAuth: JWTAuth,
private val redisClient: RedisClient,
private val accountRepository: AccountRepository,
) {
private val logger = KotlinLogging.logger { }
private val expireSeconds = 60 * 60 * 24 * 7
private val expireSeconds = 60L * 60 * 24 * 7
private val redisUtil = RedisUtil(redis)
suspend fun getLoginUser(ctx: RoutingContext): AuthUser {
val request = ctx.request()
@ -38,7 +42,7 @@ class TokenService @Inject constructor(
val token = authorization.substring(6)
val user = parseToken(token) ?: throw Meta.unauthorized("token")
val userToken = user.principal().getString(Constants.LOGIN_USER_KEY) ?: throw Meta.unauthorized("token")
val authInfoStr = redisClient.get(CacheConstants.LOGIN_TOKEN_KEY + userToken) ?: throw Meta.unauthorized("token")
val authInfoStr = redisUtil.getObject<String>(CacheConstants.LOGIN_TOKEN_KEY + userToken) ?: throw Meta.unauthorized("token")
return JsonUtil.parseObject(authInfoStr, AuthUser::class.java)
}
@ -48,7 +52,7 @@ class TokenService @Inject constructor(
val user = userInfo?.account ?: throw Meta.notFound("AccountNotFound", "账号不存在")
val authInfo = AuthUser(userInfo.account.userId, token, JsonUtil.toJsonObject(user), userInfo.rolesArr.toSet(), userInfo.accessArr.toSet(), ip, client)
val authInfoStr = JsonUtil.toJsonStr(authInfo)
redisClient.set(CacheConstants.LOGIN_TOKEN_KEY + token, authInfoStr, expireSeconds)
redisUtil.setObject(CacheConstants.LOGIN_TOKEN_KEY + token, authInfoStr, expireSeconds, TimeUnit.SECONDS)
return genToken(mapOf(Constants.LOGIN_USER_KEY to token))
}

View File

@ -1,4 +1,4 @@
package app.util
package app.utils
import com.github.benmanes.caffeine.cache.Caffeine
import com.google.inject.Singleton

View File

@ -0,0 +1,835 @@
package app.utils
import io.vertx.kotlin.coroutines.coAwait
import io.vertx.redis.client.Command
import io.vertx.redis.client.Redis
import io.vertx.redis.client.Request
import java.util.concurrent.TimeUnit
class RedisUtil constructor(private val redis: Redis) {
/**
* 缓存基本的对象IntegerString实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
suspend fun <T> setObject(key: String, value: T): Boolean {
val request = Request.cmd(Command.SET)
.arg(key)
.arg(value.toString())
.arg("KEEPTTL")
val response = redis.send(request).coAwait()
return response?.toString() == "OK"
}
/**
* 缓存基本的对象IntegerString实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
suspend fun <T> setObject(key: String, value: T, timeout: Long, timeUnit: TimeUnit): Boolean {
val expireSeconds = timeUnit.toSeconds(timeout)
val request = Request.cmd(Command.SET, key, value.toString(), "EX", expireSeconds.toString())
val response = redis.send(request).coAwait()
return response?.toString() == "OK"
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功false=设置失败
*/
suspend fun expire(key: String, timeout: Long): Boolean {
return expire(key, timeout, TimeUnit.SECONDS)
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功false=设置失败
*/
suspend fun expire(key: String, timeout: Long, unit: TimeUnit): Boolean {
val expireSeconds = unit.toSeconds(timeout)
val response = redis.send(Request.cmd(Command.EXPIRE, key, expireSeconds.toString())).coAwait()
return response?.toLong() == 1L
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
suspend fun getExpire(key: String): Long? {
val response = redis.send(Request.cmd(Command.TTL, key)).coAwait()
val ttl = response?.toLong()
return if (ttl == -1L || ttl == -2L) null else ttl
}
/**
* 判断 key是否存在
*
* @param key
* @return true 存在 false不存在
*/
suspend fun hasKey(key: String): Boolean {
val response = redis.send(Request.cmd(Command.EXISTS, key)).coAwait()
return (response?.toLong() ?: 0) > 0
}
/**
* 获得缓存的基本对象
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
suspend fun <T> getObject(key: String): T? {
val response = redis.send(Request.cmd(Command.GET, key)).coAwait()
return response?.toString() as? T
}
/**
* 删除单个对象
*
* @param key
*/
suspend fun deleteObject(key: String): Boolean {
val response = redis.send(Request.cmd(Command.DEL, key)).coAwait()
return response?.toLong() == 1L
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
suspend fun deleteObject(collection: Collection<String>): Boolean {
if (collection.isEmpty()) return false
val response = redis.send(Request.cmd(Command.DEL, *collection.toTypedArray())).coAwait()
return (response?.toLong() ?: 0) > 0
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
suspend fun <T> setList(key: String, dataList: List<T>): Long {
val args = mutableListOf<String>().apply {
add(key)
dataList.forEach { add(it.toString()) }
}
val response = redis.send(Request.cmd(Command.RPUSH, *args.toTypedArray())).coAwait()
return response?.toLong() ?: 0
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
suspend fun <T> getList(key: String): List<T>? {
val response = redis.send(Request.cmd(Command.LRANGE, key, "0", "-1")).coAwait()
return response?.map { it.toString() as T }?.toList()
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
suspend fun <T> setSet(key: String, dataSet: Set<T>): Long {
val args = mutableListOf<String>().apply {
add(key)
dataSet.forEach { add(it.toString()) }
}
val response = redis.send(Request.cmd(Command.SADD, *args.toTypedArray())).coAwait()
return response?.toLong() ?: 0
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
suspend fun <T> getSet(key: String): Set<T>? {
val response = redis.send(Request.cmd(Command.SMEMBERS, key)).coAwait()
return response?.map { it.toString() as T }?.toSet()
}
/**
* 向Set中添加一个或多个元素
*
* @param key 缓存键值
* @param values 要添加的元素
* @return 成功添加的元素数量
*/
suspend fun <T> sAdd(key: String, vararg values: T): Long {
val request = Request.cmd(Command.SADD)
.arg(key)
// 逐个添加参数
values.forEach {
request.arg(it.toString())
}
val response = redis.send(request).coAwait()
return response?.toLong() ?: 0
}
/**
* 向Set中添加一个或多个元素并设置过期时间
*
* @param key 缓存键值
* @param value 要添加的元素
* @param timeout 过期时间
* @param timeUnit 时间单位
* @return 是否成功添加元素
*/
suspend fun <T> sAdd(key: String, value: T, timeout: Long, timeUnit: TimeUnit): Boolean {
val added = sAdd(key, value) > 0
if (added) {
expire(key, timeout, timeUnit)
}
return added
}
/**
* 从Set中移除一个或多个元素
*
* @param key 缓存键值
* @param values 要移除的元素
* @return 成功移除的元素数量
*/
suspend fun <T> sRemove(key: String, vararg values: T): Long {
val args = mutableListOf<String>().apply {
add(key)
values.forEach { add(it.toString()) }
}
val response = redis.send(Request.cmd(Command.SREM, *args.toTypedArray())).coAwait()
return response?.toLong() ?: 0
}
/**
* 获取Set中的所有成员
*
* @param key 缓存键值
* @return Set中的所有成员
*/
suspend fun sMembers(key: String): List<String> {
val response = redis.send(Request.cmd(Command.SMEMBERS, key)).coAwait()
return response?.map { it.toString() } ?: emptyList()
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
suspend fun <T> setMap(key: String, dataMap: Map<String, T>): Boolean {
if (dataMap.isEmpty()) return false
val args = mutableListOf<String>().apply {
add(key)
dataMap.forEach { (k, v) ->
add(k)
add(v.toString())
}
}
val response = redis.send(Request.cmd(Command.HMSET, *args.toTypedArray())).coAwait()
return response?.toString() == "OK"
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
suspend fun <T> getMap(key: String): Map<String, T>? {
val response = redis.send(Request.cmd(Command.HGETALL, key)).coAwait()
if (response == null || response.size() == 0) return null
val map = mutableMapOf<String, T>()
// for (i in 0 until response.size() step 2) {
// val k = response.get(i).toString()
// val v = response.get(i + 1).toString() as T
// map[k] = v
// }
for (item in response) {
val list = item.toMutableList()
val k = list[0].toString()
val v = list[1].toString() as T
map[k] = v
}
return map
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value
*/
suspend fun <T> setMapValue(key: String, hKey: String, value: T): Boolean {
val response = redis.send(Request.cmd(Command.HSET, key, hKey, value.toString())).coAwait()
return response?.toLong() == 1L
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
suspend fun <T> getMapValue(key: String, hKey: String): T? {
val response = redis.send(Request.cmd(Command.HGET, key, hKey)).coAwait()
return response?.toString() as? T
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
suspend fun <T> getMultiMapValue(key: String, hKeys: Collection<String>): List<T?>? {
val args = mutableListOf<String>().apply {
add(key)
addAll(hKeys)
}
val response = redis.send(Request.cmd(Command.HMGET, *args.toTypedArray())).coAwait()
return response?.map { if (it == null) null else it.toString() as T }
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
suspend fun deleteMapValue(key: String, hKey: String): Boolean {
val response = redis.send(Request.cmd(Command.HDEL, key, hKey)).coAwait()
return response?.toLong() == 1L
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
suspend fun keys(pattern: String): List<String>? {
val response = redis.send(Request.cmd(Command.KEYS, pattern)).coAwait()
return response?.map { it.toString() }
}
/**
* 自增操作
*
* @param key Redis键
* @return 自增后的值
*/
suspend fun incr(key: String): Long {
val response = redis.send(Request.cmd(Command.INCR, key)).coAwait()
return response?.toLong() ?: 0L
}
/**
* 自增指定步长
*
* @param key Redis键
* @param increment 步长
* @return 自增后的值
*/
suspend fun incrBy(key: String, increment: Long): Long {
val response = redis.send(Request.cmd(Command.INCRBY, key, increment.toString())).coAwait()
return response?.toLong() ?: 0L
}
/**
* 自减操作
*
* @param key Redis键
* @return 自减后的值
*/
suspend fun decr(key: String): Long {
val response = redis.send(Request.cmd(Command.DECR, key)).coAwait()
return response?.toLong() ?: 0L
}
/**
* 自减指定步长
*
* @param key Redis键
* @param decrement 步长
* @return 自减后的值
*/
suspend fun decrBy(key: String, decrement: Long): Long {
val response = redis.send(Request.cmd(Command.DECRBY, key, decrement.toString())).coAwait()
return response?.toLong() ?: 0L
}
/**
* 浮点数自增
*
* @param key Redis键
* @param increment 增量
* @return 自增后的值
*/
suspend fun incrByFloat(key: String, increment: Double): Double {
val response = redis.send(Request.cmd(Command.INCRBYFLOAT, key, increment.toString())).coAwait()
return response?.toDouble() ?: 0.0
}
/**
* 设置键值并返回旧值
*
* @param key Redis键
* @param value 新值
* @return 旧值
*/
suspend fun <T> getSet(key: String, value: T): T? {
val response = redis.send(Request.cmd(Command.GETSET, key, value.toString())).coAwait()
return response?.toString() as? T
}
/**
* 设置键值仅当键不存在时
*
* @param key Redis键
* @param value
* @return 是否设置成功
*/
suspend fun <T> setIfAbsent(key: String, value: T): Boolean {
val response = redis.send(Request.cmd(Command.SETNX, key, value.toString())).coAwait()
return response?.toLong() == 1L
}
/**
* 获取字符串长度
*
* @param key Redis键
* @return 字符串长度
*/
suspend fun strlen(key: String): Long {
val response = redis.send(Request.cmd(Command.STRLEN, key)).coAwait()
return response?.toLong() ?: 0L
}
/**
* 追加字符串
*
* @param key Redis键
* @param value 追加的值
* @return 追加后的字符串长度
*/
suspend fun append(key: String, value: String): Long {
val response = redis.send(Request.cmd(Command.APPEND, key, value)).coAwait()
return response?.toLong() ?: 0L
}
/**
* 获取子字符串
*
* @param key Redis键
* @param start 开始位置
* @param end 结束位置
* @return 子字符串
*/
suspend fun getRange(key: String, start: Long, end: Long): String? {
val response = redis.send(Request.cmd(Command.GETRANGE, key, start.toString(), end.toString())).coAwait()
return response?.toString()
}
/**
* 设置子字符串
*
* @param key Redis键
* @param offset 偏移量
* @param value
* @return 修改后的字符串长度
*/
suspend fun setRange(key: String, offset: Long, value: String): Long {
val response = redis.send(Request.cmd(Command.SETRANGE, key, offset.toString(), value)).coAwait()
return response?.toLong() ?: 0L
}
/**
* 设置多个键值对
*
* @param keyValues 键值对key1, value1, key2, value2...
* @return 是否成功
*/
suspend fun mset(vararg keyValues: String): Boolean {
if (keyValues.size % 2 != 0) throw IllegalArgumentException("参数数量必须为偶数")
val response = redis.send(Request.cmd(Command.MSET, *keyValues)).coAwait()
return response?.toString() == "OK"
}
/**
* 获取多个键的值
*
* @param keys 键集合
* @return 值列表
*/
suspend fun mget(vararg keys: String): List<String?> {
val response = redis.send(Request.cmd(Command.MGET, *keys)).coAwait()
return response?.map { it?.toString() } ?: emptyList()
}
/**
* 向有序集合添加一个或多个成员或者更新已存在成员的分数
*
* @param key Redis键
* @param score 分数
* @param member 成员
* @return 成功添加的新成员的数量不包括那些被更新的已经存在的成员
*/
suspend fun zadd(key: String, score: Double, member: String): Long {
val request = Request.cmd(Command.ZADD)
.arg(key)
.arg(score.toString())
.arg(member)
val response = redis.send(request).coAwait()
return response?.toLong() ?: 0L
}
/**
* 为有序集合的成员增加分数
*
* @param key Redis键
* @param increment 增量分数
* @param member 成员
* @return 增加后的分数
*/
suspend fun zincrby(key: String, increment: Double, member: String): Double {
val request = Request.cmd(Command.ZINCRBY)
.arg(key)
.arg(increment.toString())
.arg(member)
val response = redis.send(request).coAwait()
return response?.toDouble() ?: 0.0
}
/**
* 获取有序集合中指定成员的分数
*
* @param key Redis键
* @param member 成员
* @return 分数如果成员不存在或键不存在则返回null
*/
suspend fun zscore(key: String, member: String): Double? {
val request = Request.cmd(Command.ZSCORE)
.arg(key)
.arg(member)
val response = redis.send(request).coAwait()
return response?.toDouble()
}
/**
* 获取有序集合中指定区间的成员按分数从高到低排序
*
* @param key Redis键
* @param start 开始位置
* @param stop 结束位置
* @param withScores 是否返回分数
* @return 指定区间的成员列表如果withScores为true则返回成员和分数的交替列表
*/
suspend fun zrevrange(key: String, start: Long, stop: Long, withScores: Boolean = false): List<String> {
// 使用可变参数列表而不是数组,避免类型转换问题
val request = if (withScores) {
Request.cmd(Command.ZREVRANGE)
.arg(key)
.arg(start.toString())
.arg(stop.toString())
.arg("WITHSCORES")
} else {
Request.cmd(Command.ZREVRANGE)
.arg(key)
.arg(start.toString())
.arg(stop.toString())
}
val response = redis.send(request).coAwait()
return response?.map { it.toString() } ?: emptyList()
}
/**
* 获取有序集合中所有成员的分数总和
*
* @param key Redis键
* @return 所有成员的分数总和
*/
suspend fun zsumScores(key: String): Double {
// 首先检查键是否存在
if (!hasKey(key)) return 0.0
val request = Request.cmd(Command.ZRANGE)
.arg(key)
.arg("0")
.arg("-1")
.arg("WITHSCORES")
val response = redis.send(request).coAwait()
if (response == null || response.size() == 0) return 0.0
var sum = 0.0
for (item in response) {
sum += item.toMutableList()[1]?.toString()?.toDoubleOrNull() ?: 0.0
}
return sum
}
/**
* 使用 SCAN 命令获取匹配指定模式的所有键
*
* @param pattern 匹配模式例如content:clicks:*
* @return 匹配模式的键列表
*/
suspend fun scan(pattern: String): List<String> {
val keys = mutableListOf<String>()
var cursor = "0"
do {
val request = Request.cmd(Command.SCAN)
.arg(cursor)
.arg("MATCH")
.arg(pattern)
.arg("COUNT")
.arg("100") // 每次迭代返回的键数量
val response = redis.send(request).coAwait()
if (response != null && response.size() >= 2) {
cursor = response.get(0).toString()
// 从索引1处获取键列表
val scanKeys = response.get(1)
for (i in 0 until scanKeys.size()) {
keys.add(scanKeys.get(i).toString())
}
} else {
break
}
} while (cursor != "0")
return keys
}
/**
* 计算多个有序集合的并集并将结果存储在新的键中
*
* @param destKey 目标键存储计算结果
* @param keys 要计算并集的有序集合键列表
* @param weights 各有序集的权重可选
* @param aggregate 结果集的聚合方式可选默认为 SUM
* @return 目标键中的元素数量
*/
suspend fun zunionstore(
destKey: String,
keys: Array<String>,
weights: DoubleArray? = null,
aggregate: String = "SUM"
): Long {
if (keys.isEmpty()) return 0
val request = Request.cmd(Command.ZUNIONSTORE)
.arg(destKey)
.arg(keys.size.toString())
// 添加源集合键
keys.forEach { request.arg(it) }
// 添加权重(如果指定)
if (weights != null && weights.size == keys.size) {
request.arg("WEIGHTS")
weights.forEach { request.arg(it.toString()) }
}
// 添加聚合方式
if (aggregate in listOf("SUM", "MIN", "MAX")) {
request.arg("AGGREGATE")
request.arg(aggregate)
}
val response = redis.send(request).coAwait()
return response?.toLong() ?: 0L
}
/**
* 计算多个有序集合的并集使用相同的权重
*
* @param destKey 目标键存储计算结果
* @param keys 要计算并集的有序集合键列表
* @return 目标键中的元素数量
*/
suspend fun zunionstoreWithEqualWeights(destKey: String, keys: Array<String>): Long {
val weights = DoubleArray(keys.size) { 1.0 }
return zunionstore(destKey, keys, weights)
}
/**
* 计算多个有序集合的并集对结果取最大值
*
* @param destKey 目标键存储计算结果
* @param keys 要计算并集的有序集合键列表
* @return 目标键中的元素数量
*/
suspend fun zunionstoreMax(destKey: String, keys: Array<String>): Long {
return zunionstore(destKey, keys, null, "MAX")
}
/**
* 计算两个或多个有序集合的差集并将结果存储在新的键中
* 此方法仅适用于Redis 6.2+版本
*
* @param destKey 目标键存储计算结果
* @param keys 要计算差集的有序集合键数组第一个集合是基准
* @return 目标键中的元素数量
*/
suspend fun zdiffstore(destKey: String, keys: Array<String>): Long {
if (keys.isEmpty() || keys.size < 2) return 0L
val request = Request.cmd(Command.ZDIFFSTORE)
.arg(destKey)
.arg(keys.size.toString())
// 添加源集合键
keys.forEach { request.arg(it) }
val response = redis.send(request).coAwait()
return response?.toLong() ?: 0L
}
/**
* 获取有序集合的大小成员数量
*
* @param key Redis键
* @return 有序集合的大小
*/
suspend fun zcard(key: String): Long {
val request = Request.cmd(Command.ZCARD)
.arg(key)
val response = redis.send(request).coAwait()
return response?.toLong() ?: 0L
}
/**
* 获取有序集合中所有成员
*
* @param key Redis键
* @return 有序集合的所有成员
*/
suspend fun zall(key: String): List<String> {
return zrevrange(key, 0, -1)
}
/**
* 计算两个有序集合的差集并返回结果
* 此方法仅适用于Redis 6.2+版本
*
* @param keys 要计算差集的有序集合键数组第一个集合是基准
* @return 差集结果
*/
suspend fun zdiff(vararg keys: String): List<String> {
if (keys.isEmpty() || keys.size < 2) return emptyList()
val request = Request.cmd(Command.ZDIFF)
// 添加集合数量
request.arg(keys.size.toString())
// 添加源集合键
keys.forEach { request.arg(it) }
val response = redis.send(request).coAwait()
return response?.map { it.toString() } ?: emptyList()
}
/**
* 执行 Lua 脚本
*
* @param script Lua 脚本
* @param keys 脚本中使用的 KEYS 参数
* @param args 脚本中使用的 ARGV 参数
* @return 脚本执行结果
*/
suspend fun eval(script: String, keys: List<String> = emptyList(), vararg args: String): List<String>? {
val request = Request.cmd(Command.EVAL)
.arg(script)
.arg(keys.size.toString())
// 添加 KEYS 参数
keys.forEach { request.arg(it) }
// 添加 ARGV 参数
args.forEach { request.arg(it) }
val response = redis.send(request).coAwait()
return response?.map { it.toString() }
}
/**
* 执行 Lua 脚本并返回整数结果
*
* @param script Lua 脚本
* @param keys 脚本中使用的 KEYS 参数
* @param args 脚本中使用的 ARGV 参数
* @return 脚本执行结果整数
*/
suspend fun evalToInt(script: String, keys: List<String> = emptyList(), vararg args: String): Int? {
val result = eval(script, keys, *args)
return result?.firstOrNull()?.toIntOrNull()
}
/**
* 执行 Lua 脚本并返回长整数结果
*
* @param script Lua 脚本
* @param keys 脚本中使用的 KEYS 参数
* @param args 脚本中使用的 ARGV 参数
* @return 脚本执行结果长整数
*/
suspend fun evalToLong(script: String, keys: List<String> = emptyList(), vararg args: String): Long? {
val result = eval(script, keys, *args)
return result?.firstOrNull()?.toLongOrNull()
}
/**
* 执行 Lua 脚本并返回布尔结果
*
* @param script Lua 脚本
* @param keys 脚本中使用的 KEYS 参数
* @param args 脚本中使用的 ARGV 参数
* @return 脚本执行结果布尔值
*/
suspend fun evalToBoolean(script: String, keys: List<String> = emptyList(), vararg args: String): Boolean {
val result = eval(script, keys, *args)
return result?.firstOrNull()?.toIntOrNull() == 1
}
}

View File

@ -1,16 +1,15 @@
package app.port.aipfox
package app.utils.openapi
import app.util.openapi.OpenApiSpecGenerator
import com.google.inject.Inject
import io.github.oshai.kotlinlogging.KotlinLogging
import io.vertx.core.Vertx
import io.vertx.core.http.HttpMethod
import io.vertx.core.json.JsonObject
import io.vertx.ext.web.client.WebClient
import io.vertx.ext.web.client.WebClientOptions
import io.github.oshai.kotlinlogging.KotlinLogging
import org.aikrai.vertx.config.Config
class ApifoxClient @Inject constructor(
class ApifoxUtil @Inject constructor(
private val vertx: Vertx,
) {
private val logger = KotlinLogging.logger { }
@ -47,4 +46,4 @@ class ApifoxClient @Inject constructor(
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package app.util.openapi
package app.utils.openapi
import cn.hutool.core.util.StrUtil
import io.swagger.v3.core.util.Json

View File

@ -2,7 +2,7 @@ package app.verticle
import app.config.handler.JwtAuthHandler
import app.config.handler.ResponseHandler
import app.port.aipfox.ApifoxClient
import app.utils.openapi.ApifoxUtil
import com.google.inject.Inject
import com.google.inject.Injector
import io.vertx.core.http.HttpMethod
@ -27,7 +27,7 @@ class WebVerticle @Inject constructor(
private val requestLogHandler: RequestLogHandler,
private val responseHandler: ResponseHandler,
private val globalErrorHandler: GlobalErrorHandler,
private val apiFoxClient: ApifoxClient,
private val apiFoxUtil: ApifoxUtil,
) : CoroutineVerticle() {
private val logger = KotlinLogging.logger { }
@ -42,7 +42,7 @@ class WebVerticle @Inject constructor(
.listen(serverConfig.port)
.coAwait()
// 生成ApiFox接口
apiFoxClient.importOpenapi()
apiFoxUtil.importOpenapi()
logger.info { "HTTP服务启动 - http://127.0.0.1:${server.actualPort()}${serverConfig.context}" }
}

View File

@ -16,12 +16,15 @@ CREATE TABLE sys_menu (
CREATE TABLE sys_user (
user_id BIGINT DEFAULT 0 NOT NULL,
user_name VARCHAR(255) DEFAULT '',
user_type VARCHAR(255) DEFAULT '',
email VARCHAR(255) DEFAULT '',
phone VARCHAR(255) DEFAULT '',
avatar VARCHAR(255) DEFAULT '',
password VARCHAR(255) DEFAULT '',
dept_id BIGINT DEFAULT 0,
user_name VARCHAR(30) DEFAULT '',
nick_name VARCHAR(30) DEFAULT '',
user_type VARCHAR(2) DEFAULT '',
email VARCHAR(50) DEFAULT '',
phonenumber VARCHAR(11) DEFAULT '',
sex INTEGER DEFAULT 2,
avatar VARCHAR(100) DEFAULT '',
password VARCHAR(100) DEFAULT '',
status INTEGER DEFAULT 0,
del_flag CHAR(1) DEFAULT 0,
login_ip VARCHAR(255) DEFAULT '',
@ -31,9 +34,17 @@ CREATE TABLE sys_user (
-- 添加字段注释
COMMENT ON COLUMN sys_user.user_id IS '用户ID';
CREATE UNIQUE INDEX idx_phone ON sys_user (phone);
COMMENT ON COLUMN sys_user.dept_id IS '部门ID';
COMMENT ON COLUMN sys_user.user_name IS '用户账号';
COMMENT ON COLUMN sys_user.nick_name IS '用户昵称';
COMMENT ON COLUMN sys_user.user_type IS '用户类型';
COMMENT ON COLUMN sys_user.email IS '用户邮箱';
COMMENT ON COLUMN sys_user.phonenumber IS '手机号码';
COMMENT ON COLUMN sys_user.sex IS '用户性别0男 1女 2未知';
COMMENT ON COLUMN sys_user.avatar IS '头像地址';
COMMENT ON COLUMN sys_user.password IS '密码';
COMMENT ON COLUMN sys_user.status IS '帐号状态0正常 1停用';
COMMENT ON COLUMN sys_user.del_flag IS '删除标志0代表存在 2代表删除';
CREATE TABLE sys_role (

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<migration generated="2025-03-21T15:27:45.6470429">
<migration generated="2025-05-12T11:18:05.3077594">
<changeSet type="apply">
<createTable name="sys_menu" pkName="pk_sys_menu">
<column name="menu_id" notnull="true" primaryKey="true" type="BIGINT"/>
@ -16,12 +16,15 @@
</createTable>
<createTable name="sys_user" pkName="pk_sys_user">
<column name="user_id" notnull="true" primaryKey="true" type="BIGINT"/>
<column name="user_name" type="VARCHAR(255)"/>
<column name="user_type" type="VARCHAR(255)"/>
<column name="email" type="VARCHAR(255)"/>
<column name="phone" type="VARCHAR(255)"/>
<column name="avatar" type="VARCHAR(255)"/>
<column name="password" type="VARCHAR(255)"/>
<column name="dept_id" type="BIGINT"/>
<column name="user_name" type="VARCHAR(30)"/>
<column name="nick_name" type="VARCHAR(30)"/>
<column name="user_type" type="VARCHAR(2)"/>
<column name="email" type="VARCHAR(50)"/>
<column name="phonenumber" type="VARCHAR(11)"/>
<column defaultValue="2" name="sex" type="INTEGER"/>
<column name="avatar" type="VARCHAR(100)"/>
<column name="password" type="VARCHAR(100)"/>
<column defaultValue="0" name="status" type="INTEGER"/>
<column name="del_flag" type="CHAR(1)"/>
<column name="login_ip" type="VARCHAR(255)"/>

View File

@ -19,7 +19,7 @@ data class RedisConfig(
val host: String,
val port: Int,
val db: Int,
val pass: String?,
val password: String?,
val poolSize: Int = 8,
val maxPoolWaiting: Int = 32
)

View File

@ -31,7 +31,7 @@ class FrameworkConfigModule : AbstractModule() {
host = Config.getString("redis.host", "localhost"),
port = Config.getInt("redis.port", 6379),
db = Config.getInt("redis.database", 0),
pass = Config.getStringOrNull("redis.password"),
password = Config.getStringOrNull("redis.password"),
poolSize = Config.getInt("redis.maxPoolSize", 8),
maxPoolWaiting = Config.getInt("redis.maxPoolWaiting", 32)
)