优化后端框架
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
代理商后台入口https://dl.vpiaotong.com/#/login 邀请码:K1N9TP 密码:www.bbitcn.com
|
|
||||||
@@ -45,4 +45,11 @@ dependencies {
|
|||||||
|
|
||||||
// Redis
|
// Redis
|
||||||
implementation("org.redisson:redisson:3.38.1")
|
implementation("org.redisson:redisson:3.38.1")
|
||||||
|
|
||||||
|
// 票通集成
|
||||||
|
implementation("org.bouncycastle:bcprov-jdk18on:1.84")
|
||||||
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.84")
|
||||||
|
implementation("io.ktor:ktor-client-core")
|
||||||
|
implementation("io.ktor:ktor-client-cio")
|
||||||
|
implementation("io.ktor:ktor-client-content-negotiation")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,9 @@ package com.bbit.ticket
|
|||||||
|
|
||||||
import com.bbit.ticket.bootstrap.DatabaseInitializer
|
import com.bbit.ticket.bootstrap.DatabaseInitializer
|
||||||
import com.bbit.ticket.bootstrap.SeedData
|
import com.bbit.ticket.bootstrap.SeedData
|
||||||
import com.bbit.ticket.common.ok
|
import com.bbit.ticket.entity.common.ok
|
||||||
import com.bbit.ticket.config.AppConfig
|
import com.bbit.ticket.bootstrap.AppConfig
|
||||||
import com.bbit.ticket.modules.auth.registerAuthRoutes
|
import com.bbit.ticket.route.system.registerAuthRoutes
|
||||||
import com.bbit.ticket.modules.logs.registerLogsQueryRoutes
|
|
||||||
import com.bbit.ticket.modules.system.dict.registerDictRoutes
|
|
||||||
import com.bbit.ticket.modules.system.menu.registerMenuRoutes
|
|
||||||
import com.bbit.ticket.modules.system.org.registerOrgRoutes
|
|
||||||
import com.bbit.ticket.modules.system.role.registerRoleRoutes
|
|
||||||
import com.bbit.ticket.modules.system.user.registerUserRoutes
|
|
||||||
import com.bbit.ticket.plugins.configureCors
|
import com.bbit.ticket.plugins.configureCors
|
||||||
import com.bbit.ticket.plugins.configureDatabase
|
import com.bbit.ticket.plugins.configureDatabase
|
||||||
import com.bbit.ticket.plugins.configureLogging
|
import com.bbit.ticket.plugins.configureLogging
|
||||||
@@ -20,6 +14,12 @@ import com.bbit.ticket.plugins.configureSecurity
|
|||||||
import com.bbit.ticket.plugins.configureSerialization
|
import com.bbit.ticket.plugins.configureSerialization
|
||||||
import com.bbit.ticket.plugins.configureStatusPages
|
import com.bbit.ticket.plugins.configureStatusPages
|
||||||
import com.bbit.ticket.plugins.configureTrace
|
import com.bbit.ticket.plugins.configureTrace
|
||||||
|
import com.bbit.ticket.route.system.registerDictRoutes
|
||||||
|
import com.bbit.ticket.route.system.registerLogsQueryRoutes
|
||||||
|
import com.bbit.ticket.route.system.registerMenuRoutes
|
||||||
|
import com.bbit.ticket.route.system.registerOrgRoutes
|
||||||
|
import com.bbit.ticket.route.system.registerRoleRoutes
|
||||||
|
import com.bbit.ticket.route.system.registerUserRoutes
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.netty.EngineMain
|
import io.ktor.server.netty.EngineMain
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.config
|
package com.bbit.ticket.bootstrap
|
||||||
|
|
||||||
import io.ktor.server.application.ApplicationEnvironment
|
import io.ktor.server.application.ApplicationEnvironment
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ object DatabaseInitializer {
|
|||||||
SysApiAccessLogTable,
|
SysApiAccessLogTable,
|
||||||
)
|
)
|
||||||
// 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。
|
// 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。
|
||||||
dbQuery {
|
|
||||||
MigrationUtils.statementsRequiredForDatabaseMigration(*tables, withLogs = true)
|
|
||||||
}
|
|
||||||
transaction {
|
transaction {
|
||||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
|
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||||
*tables,
|
*tables,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import com.bbit.ticket.database.system.SysRoleTable
|
|||||||
import com.bbit.ticket.database.system.SysUserRoleTable
|
import com.bbit.ticket.database.system.SysUserRoleTable
|
||||||
import com.bbit.ticket.database.system.SysUserTable
|
import com.bbit.ticket.database.system.SysUserTable
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
import com.bbit.ticket.security.PasswordService
|
import com.bbit.ticket.service.system.PasswordService
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.entity.common
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.entity.common
|
||||||
|
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.entity.common
|
||||||
|
|
||||||
fun statusLabel(status: String): String = when (status) {
|
fun statusLabel(status: String): String = when (status) {
|
||||||
"ENABLED" -> "启用"
|
"ENABLED" -> "启用"
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.entity.common
|
||||||
|
|
||||||
enum class ErrorCode(val code: String, val message: String) {
|
enum class ErrorCode(val code: String, val message: String) {
|
||||||
BAD_REQUEST("COMMON.BAD_REQUEST", "请求参数错误"),
|
BAD_REQUEST("COMMON.BAD_REQUEST", "请求参数错误"),
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.entity.common
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.entity.common
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.modules.auth
|
package com.bbit.ticket.entity.system
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.bbit.ticket.entity.system
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DictTypeItem(
|
||||||
|
val id: String,
|
||||||
|
val code: String,
|
||||||
|
val name: String,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val remark: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DictItem(
|
||||||
|
val id: String,
|
||||||
|
val typeId: String,
|
||||||
|
val label: String,
|
||||||
|
val value: String,
|
||||||
|
val color: String? = null,
|
||||||
|
val sort: Int,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val remark: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateDictTypeRequest(val code: String, val name: String, val status: String = "ENABLED", val remark: String? = null)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateDictTypeRequest(val name: String, val status: String = "ENABLED", val remark: String? = null)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateDictItemRequest(
|
||||||
|
val typeId: String,
|
||||||
|
val label: String,
|
||||||
|
val value: String,
|
||||||
|
val color: String? = null,
|
||||||
|
val sort: Int = 0,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
val remark: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateDictItemRequest(
|
||||||
|
val typeId: String,
|
||||||
|
val label: String,
|
||||||
|
val value: String,
|
||||||
|
val color: String? = null,
|
||||||
|
val sort: Int = 0,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
val remark: String? = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.bbit.ticket.entity.system
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OperationLogItem(
|
||||||
|
val id: String,
|
||||||
|
val traceId: String? = null,
|
||||||
|
val username: String? = null,
|
||||||
|
val operationType: String,
|
||||||
|
val operationName: String,
|
||||||
|
val httpMethod: String,
|
||||||
|
val requestPath: String,
|
||||||
|
val status: String,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val costMs: Long,
|
||||||
|
val createdAt: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApiAccessLogItem(
|
||||||
|
val id: String,
|
||||||
|
val traceId: String? = null,
|
||||||
|
val appKey: String? = null,
|
||||||
|
val appName: String? = null,
|
||||||
|
val httpMethod: String,
|
||||||
|
val requestPath: String,
|
||||||
|
val responseCode: String? = null,
|
||||||
|
val status: String,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val costMs: Long,
|
||||||
|
val createdAt: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.entity.system
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MenuTreeNode(
|
||||||
|
val id: String,
|
||||||
|
val parentId: String? = null,
|
||||||
|
val type: String,
|
||||||
|
val typeLabel: String,
|
||||||
|
val title: String,
|
||||||
|
val name: String? = null,
|
||||||
|
val path: String? = null,
|
||||||
|
val component: String? = null,
|
||||||
|
val icon: String? = null,
|
||||||
|
val permission: String? = null,
|
||||||
|
val sort: Int,
|
||||||
|
val visible: Boolean,
|
||||||
|
val keepAlive: Boolean,
|
||||||
|
val builtIn: Boolean = false,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val children: List<MenuTreeNode> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateMenuRequest(
|
||||||
|
val parentId: String? = null,
|
||||||
|
val type: String,
|
||||||
|
val title: String,
|
||||||
|
val name: String? = null,
|
||||||
|
val path: String? = null,
|
||||||
|
val component: String? = null,
|
||||||
|
val icon: String? = null,
|
||||||
|
val permission: String? = null,
|
||||||
|
val sort: Int = 0,
|
||||||
|
val visible: Boolean = true,
|
||||||
|
val keepAlive: Boolean = false,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateMenuRequest(
|
||||||
|
val parentId: String? = null,
|
||||||
|
val type: String,
|
||||||
|
val title: String,
|
||||||
|
val name: String? = null,
|
||||||
|
val path: String? = null,
|
||||||
|
val component: String? = null,
|
||||||
|
val icon: String? = null,
|
||||||
|
val permission: String? = null,
|
||||||
|
val sort: Int = 0,
|
||||||
|
val visible: Boolean = true,
|
||||||
|
val keepAlive: Boolean = false,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MenuFlat(
|
||||||
|
val id: Uuid,
|
||||||
|
val parentId: Uuid?,
|
||||||
|
val type: String,
|
||||||
|
val title: String,
|
||||||
|
val name: String?,
|
||||||
|
val path: String?,
|
||||||
|
val component: String?,
|
||||||
|
val icon: String?,
|
||||||
|
val permission: String?,
|
||||||
|
val sort: Int,
|
||||||
|
val visible: Boolean,
|
||||||
|
val keepAlive: Boolean,
|
||||||
|
val builtIn: Boolean,
|
||||||
|
val status: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.bbit.ticket.entity.system
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OrgTreeNode(
|
||||||
|
val id: String,
|
||||||
|
val parentId: String? = null,
|
||||||
|
val name: String,
|
||||||
|
val code: String,
|
||||||
|
val sort: Int,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val children: List<OrgTreeNode> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateOrgRequest(
|
||||||
|
val parentId: String? = null,
|
||||||
|
val name: String,
|
||||||
|
val code: String,
|
||||||
|
val sort: Int = 0,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateOrgRequest(
|
||||||
|
val parentId: String? = null,
|
||||||
|
val name: String,
|
||||||
|
val sort: Int = 0,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.bbit.ticket.entity.system
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RoleItem(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val code: String,
|
||||||
|
val description: String? = null,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val dataScope: String,
|
||||||
|
val dataScopeLabel: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RoleDetail(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val code: String,
|
||||||
|
val description: String? = null,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val dataScope: String,
|
||||||
|
val dataScopeLabel: String,
|
||||||
|
val menuIds: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateRoleRequest(
|
||||||
|
val name: String,
|
||||||
|
val code: String,
|
||||||
|
val description: String? = null,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
val dataScope: String = "SELF",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateRoleRequest(
|
||||||
|
val name: String,
|
||||||
|
val description: String? = null,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
val dataScope: String = "SELF",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateRoleMenusRequest(val menuIds: List<String>)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.bbit.ticket.entity.system
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserListItem(
|
||||||
|
val id: String,
|
||||||
|
val username: String,
|
||||||
|
val nickname: String? = null,
|
||||||
|
val realName: String? = null,
|
||||||
|
val orgId: String? = null,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val roleCodes: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserDetailResponse(
|
||||||
|
val id: String,
|
||||||
|
val username: String,
|
||||||
|
val nickname: String? = null,
|
||||||
|
val realName: String? = null,
|
||||||
|
val phone: String? = null,
|
||||||
|
val email: String? = null,
|
||||||
|
val avatar: String? = null,
|
||||||
|
val orgId: String? = null,
|
||||||
|
val status: String,
|
||||||
|
val statusLabel: String,
|
||||||
|
val roleIds: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateUserRequest(
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
val nickname: String? = null,
|
||||||
|
val realName: String? = null,
|
||||||
|
val phone: String? = null,
|
||||||
|
val email: String? = null,
|
||||||
|
val avatar: String? = null,
|
||||||
|
val orgId: String? = null,
|
||||||
|
val status: String = "ENABLED",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateUserRequest(
|
||||||
|
val nickname: String? = null,
|
||||||
|
val realName: String? = null,
|
||||||
|
val phone: String? = null,
|
||||||
|
val email: String? = null,
|
||||||
|
val avatar: String? = null,
|
||||||
|
val orgId: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateUserStatusRequest(val status: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateUserPasswordRequest(val password: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UpdateUserRolesRequest(val roleIds: List<String>)
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package com.bbit.ticket.modules.system.dict
|
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
|
||||||
import com.bbit.ticket.common.ErrorCode
|
|
||||||
import com.bbit.ticket.common.PageResult
|
|
||||||
import com.bbit.ticket.common.ok
|
|
||||||
import com.bbit.ticket.common.parseUuid
|
|
||||||
import com.bbit.ticket.common.queryInt
|
|
||||||
import com.bbit.ticket.common.queryString
|
|
||||||
import com.bbit.ticket.common.statusLabel
|
|
||||||
import com.bbit.ticket.database.system.SysDictItemTable
|
|
||||||
import com.bbit.ticket.database.system.SysDictTypeTable
|
|
||||||
import com.bbit.ticket.modules.logs.OperationLogService
|
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
|
||||||
import com.bbit.ticket.security.requirePermission
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.request.receive
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.delete
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import io.ktor.server.routing.put
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
|
||||||
import org.jetbrains.exposed.v1.core.and
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
|
||||||
import org.jetbrains.exposed.v1.core.isNull
|
|
||||||
import org.jetbrains.exposed.v1.core.like
|
|
||||||
import org.jetbrains.exposed.v1.core.or
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import kotlin.time.TimeSource
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class DictTypeItem(
|
|
||||||
val id: String,
|
|
||||||
val code: String,
|
|
||||||
val name: String,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val remark: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class DictItem(
|
|
||||||
val id: String,
|
|
||||||
val typeId: String,
|
|
||||||
val label: String,
|
|
||||||
val value: String,
|
|
||||||
val color: String? = null,
|
|
||||||
val sort: Int,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val remark: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateDictTypeRequest(val code: String, val name: String, val status: String = "ENABLED", val remark: String? = null)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateDictTypeRequest(val name: String, val status: String = "ENABLED", val remark: String? = null)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateDictItemRequest(
|
|
||||||
val typeId: String,
|
|
||||||
val label: String,
|
|
||||||
val value: String,
|
|
||||||
val color: String? = null,
|
|
||||||
val sort: Int = 0,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
val remark: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateDictItemRequest(
|
|
||||||
val typeId: String,
|
|
||||||
val label: String,
|
|
||||||
val value: String,
|
|
||||||
val color: String? = null,
|
|
||||||
val sort: Int = 0,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
val remark: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
object DictService {
|
|
||||||
suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult<DictTypeItem> = dbQuery {
|
|
||||||
var where = SysDictTypeTable.deletedAt.isNull()
|
|
||||||
if (!keyword.isNullOrBlank()) {
|
|
||||||
where = where and ((SysDictTypeTable.code like "%$keyword%") or (SysDictTypeTable.name like "%$keyword%"))
|
|
||||||
}
|
|
||||||
val total = SysDictTypeTable.selectAll().where { where }.count()
|
|
||||||
val rows = SysDictTypeTable.selectAll().where { where }
|
|
||||||
.orderBy(SysDictTypeTable.createdAt)
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset(((page - 1) * pageSize).toLong())
|
|
||||||
.toList()
|
|
||||||
PageResult(
|
|
||||||
items = rows.map {
|
|
||||||
DictTypeItem(
|
|
||||||
id = it[SysDictTypeTable.id].toString(),
|
|
||||||
code = it[SysDictTypeTable.code],
|
|
||||||
name = it[SysDictTypeTable.name],
|
|
||||||
status = it[SysDictTypeTable.status],
|
|
||||||
statusLabel = statusLabel(it[SysDictTypeTable.status]),
|
|
||||||
remark = it[SysDictTypeTable.remark],
|
|
||||||
)
|
|
||||||
},
|
|
||||||
page = page,
|
|
||||||
pageSize = pageSize,
|
|
||||||
total = total,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createType(request: CreateDictTypeRequest): String = dbQuery {
|
|
||||||
if (request.code.trim().isBlank() || request.name.trim().isBlank()) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型编码和名称不能为空")
|
|
||||||
}
|
|
||||||
val exists = SysDictTypeTable.selectAll().where {
|
|
||||||
(SysDictTypeTable.code eq request.code.trim()) and SysDictTypeTable.deletedAt.isNull()
|
|
||||||
}.any()
|
|
||||||
if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "字典类型编码已存在")
|
|
||||||
val inserted = SysDictTypeTable.insert {
|
|
||||||
it[code] = request.code.trim()
|
|
||||||
it[name] = request.name.trim()
|
|
||||||
it[status] = request.status
|
|
||||||
it[remark] = request.remark?.trim()
|
|
||||||
it[createdAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
inserted[SysDictTypeTable.id].toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateType(id: Uuid, request: UpdateDictTypeRequest) = dbQuery {
|
|
||||||
requireType(id)
|
|
||||||
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
|
|
||||||
it[name] = request.name.trim()
|
|
||||||
it[status] = request.status
|
|
||||||
it[remark] = request.remark?.trim()
|
|
||||||
it[updatedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteType(id: Uuid) = dbQuery {
|
|
||||||
requireType(id)
|
|
||||||
val hasItems = SysDictItemTable.selectAll().where {
|
|
||||||
(SysDictItemTable.typeId eq id) and SysDictItemTable.deletedAt.isNull()
|
|
||||||
}.any()
|
|
||||||
if (hasItems) throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型下存在字典项,不能删除")
|
|
||||||
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
|
|
||||||
it[deletedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult<DictItem> = dbQuery {
|
|
||||||
var where = SysDictItemTable.deletedAt.isNull()
|
|
||||||
if (typeId != null) where = where and (SysDictItemTable.typeId eq typeId)
|
|
||||||
val total = SysDictItemTable.selectAll().where { where }.count()
|
|
||||||
val rows = SysDictItemTable.selectAll().where { where }
|
|
||||||
.orderBy(SysDictItemTable.sort)
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset(((page - 1) * pageSize).toLong())
|
|
||||||
.toList()
|
|
||||||
PageResult(
|
|
||||||
items = rows.map {
|
|
||||||
DictItem(
|
|
||||||
id = it[SysDictItemTable.id].toString(),
|
|
||||||
typeId = it[SysDictItemTable.typeId].toString(),
|
|
||||||
label = it[SysDictItemTable.label],
|
|
||||||
value = it[SysDictItemTable.value],
|
|
||||||
color = it[SysDictItemTable.color],
|
|
||||||
sort = it[SysDictItemTable.sort],
|
|
||||||
status = it[SysDictItemTable.status],
|
|
||||||
statusLabel = statusLabel(it[SysDictItemTable.status]),
|
|
||||||
remark = it[SysDictItemTable.remark],
|
|
||||||
)
|
|
||||||
},
|
|
||||||
page = page,
|
|
||||||
pageSize = pageSize,
|
|
||||||
total = total,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createItem(request: CreateDictItemRequest): String = dbQuery {
|
|
||||||
val typeId = parseUuid(request.typeId, "typeId")
|
|
||||||
requireType(typeId)
|
|
||||||
val inserted = SysDictItemTable.insert {
|
|
||||||
it[SysDictItemTable.typeId] = typeId
|
|
||||||
it[label] = request.label.trim()
|
|
||||||
it[value] = request.value.trim()
|
|
||||||
it[color] = request.color?.trim()
|
|
||||||
it[sort] = request.sort
|
|
||||||
it[status] = request.status
|
|
||||||
it[remark] = request.remark?.trim()
|
|
||||||
it[createdAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
inserted[SysDictItemTable.id].toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateItem(id: Uuid, request: UpdateDictItemRequest) = dbQuery {
|
|
||||||
requireItem(id)
|
|
||||||
val typeId = parseUuid(request.typeId, "typeId")
|
|
||||||
requireType(typeId)
|
|
||||||
SysDictItemTable.update({ SysDictItemTable.id eq id }) {
|
|
||||||
it[SysDictItemTable.typeId] = typeId
|
|
||||||
it[label] = request.label.trim()
|
|
||||||
it[value] = request.value.trim()
|
|
||||||
it[color] = request.color?.trim()
|
|
||||||
it[sort] = request.sort
|
|
||||||
it[status] = request.status
|
|
||||||
it[remark] = request.remark?.trim()
|
|
||||||
it[updatedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteItem(id: Uuid) = dbQuery {
|
|
||||||
requireItem(id)
|
|
||||||
SysDictItemTable.update({ SysDictItemTable.id eq id }) {
|
|
||||||
it[deletedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requireType(id: Uuid): ResultRow =
|
|
||||||
SysDictTypeTable.selectAll().where { (SysDictTypeTable.id eq id) and SysDictTypeTable.deletedAt.isNull() }.singleOrNull()
|
|
||||||
?: throw BizException(ErrorCode.DICT_TYPE_NOT_FOUND.code, ErrorCode.DICT_TYPE_NOT_FOUND.message, HttpStatusCode.NotFound)
|
|
||||||
|
|
||||||
private fun requireItem(id: Uuid): ResultRow =
|
|
||||||
SysDictItemTable.selectAll().where { (SysDictItemTable.id eq id) and SysDictItemTable.deletedAt.isNull() }.singleOrNull()
|
|
||||||
?: throw BizException(ErrorCode.DICT_ITEM_NOT_FOUND.code, ErrorCode.DICT_ITEM_NOT_FOUND.message, HttpStatusCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Route.registerDictRoutes() {
|
|
||||||
authenticate("auth-jwt") {
|
|
||||||
route("/api/system/dict-types") {
|
|
||||||
get {
|
|
||||||
call.requirePermission("system:dict:view")
|
|
||||||
val page = call.queryInt("page", 1)
|
|
||||||
val pageSize = call.queryInt("pageSize", 20)
|
|
||||||
call.respond(ok(DictService.listTypes(page, pageSize, call.queryString("keyword"))))
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:dict:create")
|
|
||||||
val request = call.receive<CreateDictTypeRequest>()
|
|
||||||
runCatching {
|
|
||||||
val id = DictService.createType(request)
|
|
||||||
call.respond(ok(mapOf("id" to id)))
|
|
||||||
OperationLogService.success(call, currentUser, "CREATE", "新增字典类型", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "CREATE", "新增字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:dict:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateDictTypeRequest>()
|
|
||||||
runCatching {
|
|
||||||
DictService.updateType(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "UPDATE", "更新字典类型", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:dict:delete")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
runCatching {
|
|
||||||
DictService.deleteType(id)
|
|
||||||
call.respond(ok<Unit>(message = "删除成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "DELETE", "删除字典类型", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "DELETE", "删除字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
route("/api/system/dict-items") {
|
|
||||||
get {
|
|
||||||
call.requirePermission("system:dict:view")
|
|
||||||
val page = call.queryInt("page", 1)
|
|
||||||
val pageSize = call.queryInt("pageSize", 20)
|
|
||||||
val typeId = call.queryString("typeId")?.let { parseUuid(it, "typeId") }
|
|
||||||
call.respond(ok(DictService.listItems(page, pageSize, typeId)))
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:dict:create")
|
|
||||||
val request = call.receive<CreateDictItemRequest>()
|
|
||||||
runCatching {
|
|
||||||
val id = DictService.createItem(request)
|
|
||||||
call.respond(ok(mapOf("id" to id)))
|
|
||||||
OperationLogService.success(call, currentUser, "CREATE", "新增字典项", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "CREATE", "新增字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:dict:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateDictItemRequest>()
|
|
||||||
runCatching {
|
|
||||||
DictService.updateItem(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "UPDATE", "更新字典项", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:dict:delete")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
runCatching {
|
|
||||||
DictService.deleteItem(id)
|
|
||||||
call.respond(ok<Unit>(message = "删除成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "DELETE", "删除字典项", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "DELETE", "删除字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package com.bbit.ticket.modules.system.menu
|
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
|
||||||
import com.bbit.ticket.common.ErrorCode
|
|
||||||
import com.bbit.ticket.common.ok
|
|
||||||
import com.bbit.ticket.common.parseUuid
|
|
||||||
import com.bbit.ticket.common.menuTypeLabel
|
|
||||||
import com.bbit.ticket.common.statusLabel
|
|
||||||
import com.bbit.ticket.database.system.SysMenuTable
|
|
||||||
import com.bbit.ticket.database.system.SysRoleMenuTable
|
|
||||||
import com.bbit.ticket.modules.logs.OperationLogService
|
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
|
||||||
import com.bbit.ticket.security.requirePermission
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.request.receive
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.delete
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import io.ktor.server.routing.put
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
|
||||||
import org.jetbrains.exposed.v1.core.and
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
|
||||||
import org.jetbrains.exposed.v1.core.isNull
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import kotlin.time.TimeSource
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MenuTreeNode(
|
|
||||||
val id: String,
|
|
||||||
val parentId: String? = null,
|
|
||||||
val type: String,
|
|
||||||
val typeLabel: String,
|
|
||||||
val title: String,
|
|
||||||
val name: String? = null,
|
|
||||||
val path: String? = null,
|
|
||||||
val component: String? = null,
|
|
||||||
val icon: String? = null,
|
|
||||||
val permission: String? = null,
|
|
||||||
val sort: Int,
|
|
||||||
val visible: Boolean,
|
|
||||||
val keepAlive: Boolean,
|
|
||||||
val builtIn: Boolean = false,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val children: List<MenuTreeNode> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateMenuRequest(
|
|
||||||
val parentId: String? = null,
|
|
||||||
val type: String,
|
|
||||||
val title: String,
|
|
||||||
val name: String? = null,
|
|
||||||
val path: String? = null,
|
|
||||||
val component: String? = null,
|
|
||||||
val icon: String? = null,
|
|
||||||
val permission: String? = null,
|
|
||||||
val sort: Int = 0,
|
|
||||||
val visible: Boolean = true,
|
|
||||||
val keepAlive: Boolean = false,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateMenuRequest(
|
|
||||||
val parentId: String? = null,
|
|
||||||
val type: String,
|
|
||||||
val title: String,
|
|
||||||
val name: String? = null,
|
|
||||||
val path: String? = null,
|
|
||||||
val component: String? = null,
|
|
||||||
val icon: String? = null,
|
|
||||||
val permission: String? = null,
|
|
||||||
val sort: Int = 0,
|
|
||||||
val visible: Boolean = true,
|
|
||||||
val keepAlive: Boolean = false,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
)
|
|
||||||
|
|
||||||
object MenuService {
|
|
||||||
suspend fun tree(): List<MenuTreeNode> = dbQuery {
|
|
||||||
val rows = SysMenuTable.selectAll().where { SysMenuTable.deletedAt.isNull() }.toList()
|
|
||||||
val flat = rows.map {
|
|
||||||
MenuFlat(
|
|
||||||
id = it[SysMenuTable.id],
|
|
||||||
parentId = it[SysMenuTable.parentId],
|
|
||||||
type = it[SysMenuTable.type],
|
|
||||||
title = it[SysMenuTable.title],
|
|
||||||
name = it[SysMenuTable.name],
|
|
||||||
path = it[SysMenuTable.path],
|
|
||||||
component = it[SysMenuTable.component],
|
|
||||||
icon = it[SysMenuTable.icon],
|
|
||||||
permission = it[SysMenuTable.permission],
|
|
||||||
sort = it[SysMenuTable.sort],
|
|
||||||
visible = it[SysMenuTable.visible],
|
|
||||||
keepAlive = it[SysMenuTable.keepAlive],
|
|
||||||
builtIn = it[SysMenuTable.builtIn],
|
|
||||||
status = it[SysMenuTable.status],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
buildTree(flat)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun create(request: CreateMenuRequest): String = dbQuery {
|
|
||||||
validateMenuType(request.type)
|
|
||||||
val parentId = request.parentId?.let { parseUuid(it, "parentId") }
|
|
||||||
if (parentId != null) requireMenu(parentId)
|
|
||||||
val inserted = SysMenuTable.insert {
|
|
||||||
it[SysMenuTable.parentId] = parentId
|
|
||||||
it[SysMenuTable.type] = request.type
|
|
||||||
it[title] = request.title.trim()
|
|
||||||
it[name] = request.name?.trim()
|
|
||||||
it[path] = request.path?.trim()
|
|
||||||
it[component] = request.component?.trim()
|
|
||||||
it[icon] = request.icon?.trim()
|
|
||||||
it[permission] = request.permission?.trim()
|
|
||||||
it[sort] = request.sort
|
|
||||||
it[visible] = request.visible
|
|
||||||
it[keepAlive] = request.keepAlive
|
|
||||||
it[builtIn] = false
|
|
||||||
it[status] = request.status
|
|
||||||
it[createdAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
inserted[SysMenuTable.id].toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun update(id: Uuid, request: UpdateMenuRequest) = dbQuery {
|
|
||||||
requireMenu(id)
|
|
||||||
validateMenuType(request.type)
|
|
||||||
val parentId = request.parentId?.let { parseUuid(it, "parentId") }
|
|
||||||
if (parentId == id) throw BizException(ErrorCode.BAD_REQUEST.code, "上级菜单不能选择自身")
|
|
||||||
if (parentId != null) requireMenu(parentId)
|
|
||||||
SysMenuTable.update({ SysMenuTable.id eq id }) {
|
|
||||||
it[SysMenuTable.parentId] = parentId
|
|
||||||
it[SysMenuTable.type] = request.type
|
|
||||||
it[title] = request.title.trim()
|
|
||||||
it[name] = request.name?.trim()
|
|
||||||
it[path] = request.path?.trim()
|
|
||||||
it[component] = request.component?.trim()
|
|
||||||
it[icon] = request.icon?.trim()
|
|
||||||
it[permission] = request.permission?.trim()
|
|
||||||
it[sort] = request.sort
|
|
||||||
it[visible] = request.visible
|
|
||||||
it[keepAlive] = request.keepAlive
|
|
||||||
it[status] = request.status
|
|
||||||
it[updatedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(id: Uuid) = dbQuery {
|
|
||||||
requireMenu(id)
|
|
||||||
val hasChildren = SysMenuTable.selectAll().where {
|
|
||||||
(SysMenuTable.parentId eq id) and SysMenuTable.deletedAt.isNull()
|
|
||||||
}.any()
|
|
||||||
if (hasChildren) throw BizException(ErrorCode.BAD_REQUEST.code, "存在子菜单,不能删除")
|
|
||||||
val referenced = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.menuId eq id }.any()
|
|
||||||
if (referenced) throw BizException(ErrorCode.BAD_REQUEST.code, "菜单已被角色引用,不能删除")
|
|
||||||
val row = requireMenu(id)
|
|
||||||
if (row[SysMenuTable.builtIn]) throw BizException(ErrorCode.BAD_REQUEST.code, "基础框架内置菜单不可删除")
|
|
||||||
SysMenuTable.update({ SysMenuTable.id eq id }) {
|
|
||||||
it[deletedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requireMenu(id: Uuid): ResultRow =
|
|
||||||
SysMenuTable.selectAll().where { (SysMenuTable.id eq id) and SysMenuTable.deletedAt.isNull() }.singleOrNull()
|
|
||||||
?: throw BizException(ErrorCode.MENU_NOT_FOUND.code, ErrorCode.MENU_NOT_FOUND.message, HttpStatusCode.NotFound)
|
|
||||||
|
|
||||||
private fun validateMenuType(type: String) {
|
|
||||||
if (type !in setOf("CATALOG", "MENU", "BUTTON")) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "菜单类型必须是目录、菜单或按钮")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildTree(items: List<MenuFlat>): List<MenuTreeNode> {
|
|
||||||
val grouped = items.groupBy { it.parentId }
|
|
||||||
fun children(parentId: Uuid?): List<MenuTreeNode> =
|
|
||||||
(grouped[parentId] ?: emptyList()).sortedBy { it.sort }.map { menu ->
|
|
||||||
MenuTreeNode(
|
|
||||||
id = menu.id.toString(),
|
|
||||||
parentId = menu.parentId?.toString(),
|
|
||||||
type = menu.type,
|
|
||||||
typeLabel = menuTypeLabel(menu.type),
|
|
||||||
title = menu.title,
|
|
||||||
name = menu.name,
|
|
||||||
path = menu.path,
|
|
||||||
component = menu.component,
|
|
||||||
icon = menu.icon,
|
|
||||||
permission = menu.permission,
|
|
||||||
sort = menu.sort,
|
|
||||||
visible = menu.visible,
|
|
||||||
keepAlive = menu.keepAlive,
|
|
||||||
builtIn = menu.builtIn,
|
|
||||||
status = menu.status,
|
|
||||||
statusLabel = statusLabel(menu.status),
|
|
||||||
children = children(menu.id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return children(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class MenuFlat(
|
|
||||||
val id: Uuid,
|
|
||||||
val parentId: Uuid?,
|
|
||||||
val type: String,
|
|
||||||
val title: String,
|
|
||||||
val name: String?,
|
|
||||||
val path: String?,
|
|
||||||
val component: String?,
|
|
||||||
val icon: String?,
|
|
||||||
val permission: String?,
|
|
||||||
val sort: Int,
|
|
||||||
val visible: Boolean,
|
|
||||||
val keepAlive: Boolean,
|
|
||||||
val builtIn: Boolean,
|
|
||||||
val status: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Route.registerMenuRoutes() {
|
|
||||||
authenticate("auth-jwt") {
|
|
||||||
route("/api/system/menus") {
|
|
||||||
get {
|
|
||||||
call.requirePermission("system:menu:view")
|
|
||||||
call.respond(ok(MenuService.tree()))
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:menu:create")
|
|
||||||
val request = call.receive<CreateMenuRequest>()
|
|
||||||
runCatching {
|
|
||||||
val id = MenuService.create(request)
|
|
||||||
call.respond(ok(mapOf("id" to id)))
|
|
||||||
OperationLogService.success(call, currentUser, "CREATE", "新增菜单", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "CREATE", "新增菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:menu:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateMenuRequest>()
|
|
||||||
runCatching {
|
|
||||||
MenuService.update(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "UPDATE", "更新菜单", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:menu:delete")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
runCatching {
|
|
||||||
MenuService.delete(id)
|
|
||||||
call.respond(ok<Unit>(message = "删除成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "DELETE", "删除菜单", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "DELETE", "删除菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package com.bbit.ticket.modules.system.role
|
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
|
||||||
import com.bbit.ticket.common.ErrorCode
|
|
||||||
import com.bbit.ticket.common.PageResult
|
|
||||||
import com.bbit.ticket.common.ok
|
|
||||||
import com.bbit.ticket.common.parseUuid
|
|
||||||
import com.bbit.ticket.common.queryInt
|
|
||||||
import com.bbit.ticket.common.queryString
|
|
||||||
import com.bbit.ticket.common.dataScopeLabel
|
|
||||||
import com.bbit.ticket.common.statusLabel
|
|
||||||
import com.bbit.ticket.database.system.SysMenuTable
|
|
||||||
import com.bbit.ticket.database.system.SysRoleMenuTable
|
|
||||||
import com.bbit.ticket.database.system.SysRoleTable
|
|
||||||
import com.bbit.ticket.database.system.SysUserRoleTable
|
|
||||||
import com.bbit.ticket.modules.logs.OperationLogService
|
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
|
||||||
import com.bbit.ticket.security.requirePermission
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.request.receive
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.delete
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import io.ktor.server.routing.put
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
|
||||||
import org.jetbrains.exposed.v1.core.and
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
|
||||||
import org.jetbrains.exposed.v1.core.isNull
|
|
||||||
import org.jetbrains.exposed.v1.core.like
|
|
||||||
import org.jetbrains.exposed.v1.core.or
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.insertIgnore
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import kotlin.time.TimeSource
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class RoleItem(
|
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
val code: String,
|
|
||||||
val description: String? = null,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val dataScope: String,
|
|
||||||
val dataScopeLabel: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class RoleDetail(
|
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
val code: String,
|
|
||||||
val description: String? = null,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val dataScope: String,
|
|
||||||
val dataScopeLabel: String,
|
|
||||||
val menuIds: List<String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateRoleRequest(
|
|
||||||
val name: String,
|
|
||||||
val code: String,
|
|
||||||
val description: String? = null,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
val dataScope: String = "SELF",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateRoleRequest(
|
|
||||||
val name: String,
|
|
||||||
val description: String? = null,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
val dataScope: String = "SELF",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateRoleMenusRequest(val menuIds: List<String>)
|
|
||||||
|
|
||||||
object RoleService {
|
|
||||||
suspend fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<RoleItem> = dbQuery {
|
|
||||||
var where = SysRoleTable.deletedAt.isNull()
|
|
||||||
if (!keyword.isNullOrBlank()) {
|
|
||||||
where = where and ((SysRoleTable.name like "%$keyword%") or (SysRoleTable.code like "%$keyword%"))
|
|
||||||
}
|
|
||||||
if (!status.isNullOrBlank()) {
|
|
||||||
where = where and (SysRoleTable.status eq status)
|
|
||||||
}
|
|
||||||
val total = SysRoleTable.selectAll().where { where }.count()
|
|
||||||
val rows = SysRoleTable.selectAll().where { where }
|
|
||||||
.orderBy(SysRoleTable.createdAt)
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset(((page - 1) * pageSize).toLong())
|
|
||||||
.toList()
|
|
||||||
PageResult(
|
|
||||||
items = rows.map {
|
|
||||||
RoleItem(
|
|
||||||
id = it[SysRoleTable.id].toString(),
|
|
||||||
name = it[SysRoleTable.name],
|
|
||||||
code = it[SysRoleTable.code],
|
|
||||||
description = it[SysRoleTable.description],
|
|
||||||
status = it[SysRoleTable.status],
|
|
||||||
statusLabel = statusLabel(it[SysRoleTable.status]),
|
|
||||||
dataScope = it[SysRoleTable.dataScope],
|
|
||||||
dataScopeLabel = dataScopeLabel(it[SysRoleTable.dataScope]),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
page = page,
|
|
||||||
pageSize = pageSize,
|
|
||||||
total = total,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun create(request: CreateRoleRequest): String = dbQuery {
|
|
||||||
if (request.name.trim().isBlank() || request.code.trim().isBlank()) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "角色名称和编码不能为空")
|
|
||||||
}
|
|
||||||
val exists = SysRoleTable.selectAll().where {
|
|
||||||
(SysRoleTable.code eq request.code.trim()) and SysRoleTable.deletedAt.isNull()
|
|
||||||
}.any()
|
|
||||||
if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "角色编码已存在")
|
|
||||||
val inserted = SysRoleTable.insert {
|
|
||||||
it[name] = request.name.trim()
|
|
||||||
it[code] = request.code.trim()
|
|
||||||
it[description] = request.description?.trim()
|
|
||||||
it[status] = request.status
|
|
||||||
it[dataScope] = request.dataScope
|
|
||||||
it[createdAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
inserted[SysRoleTable.id].toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun detail(id: Uuid): RoleDetail = dbQuery {
|
|
||||||
val role = requireRole(id)
|
|
||||||
val menuIds = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.roleId eq id }.map { it[SysRoleMenuTable.menuId].toString() }
|
|
||||||
RoleDetail(
|
|
||||||
id = role[SysRoleTable.id].toString(),
|
|
||||||
name = role[SysRoleTable.name],
|
|
||||||
code = role[SysRoleTable.code],
|
|
||||||
description = role[SysRoleTable.description],
|
|
||||||
status = role[SysRoleTable.status],
|
|
||||||
statusLabel = statusLabel(role[SysRoleTable.status]),
|
|
||||||
dataScope = role[SysRoleTable.dataScope],
|
|
||||||
dataScopeLabel = dataScopeLabel(role[SysRoleTable.dataScope]),
|
|
||||||
menuIds = menuIds,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun update(id: Uuid, request: UpdateRoleRequest) = dbQuery {
|
|
||||||
requireRole(id)
|
|
||||||
SysRoleTable.update({ SysRoleTable.id eq id }) {
|
|
||||||
it[name] = request.name.trim()
|
|
||||||
it[description] = request.description?.trim()
|
|
||||||
it[status] = request.status
|
|
||||||
it[dataScope] = request.dataScope
|
|
||||||
it[updatedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(id: Uuid) = dbQuery {
|
|
||||||
val role = requireRole(id)
|
|
||||||
if (role[SysRoleTable.code] == "SUPER_ADMIN") {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "超级管理员角色不可删除")
|
|
||||||
}
|
|
||||||
val inUse = SysUserRoleTable.selectAll().where { SysUserRoleTable.roleId eq id }.any()
|
|
||||||
if (inUse) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "角色已被用户使用,不能删除")
|
|
||||||
}
|
|
||||||
SysRoleTable.update({ SysRoleTable.id eq id }) {
|
|
||||||
it[deletedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateMenus(id: Uuid, request: UpdateRoleMenusRequest) = dbQuery {
|
|
||||||
requireRole(id)
|
|
||||||
val menuIds = request.menuIds.distinct().map { parseUuid(it, "menuId") }
|
|
||||||
if (menuIds.isNotEmpty()) {
|
|
||||||
val validCount = SysMenuTable.selectAll().where {
|
|
||||||
(SysMenuTable.id inList menuIds) and
|
|
||||||
SysMenuTable.deletedAt.isNull() and
|
|
||||||
(SysMenuTable.status eq "ENABLED")
|
|
||||||
}.count()
|
|
||||||
if (validCount != menuIds.size.toLong()) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或禁用菜单")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id }
|
|
||||||
menuIds.forEach { menuId ->
|
|
||||||
SysRoleMenuTable.insertIgnore {
|
|
||||||
it[roleId] = id
|
|
||||||
it[SysRoleMenuTable.menuId] = menuId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requireRole(id: Uuid): ResultRow =
|
|
||||||
SysRoleTable.selectAll().where { (SysRoleTable.id eq id) and SysRoleTable.deletedAt.isNull() }.singleOrNull()
|
|
||||||
?: throw BizException(ErrorCode.ROLE_NOT_FOUND.code, ErrorCode.ROLE_NOT_FOUND.message, HttpStatusCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Route.registerRoleRoutes() {
|
|
||||||
authenticate("auth-jwt") {
|
|
||||||
route("/api/system/roles") {
|
|
||||||
get {
|
|
||||||
call.requirePermission("system:role:view")
|
|
||||||
val page = call.queryInt("page", 1)
|
|
||||||
val pageSize = call.queryInt("pageSize", 20)
|
|
||||||
call.respond(ok(RoleService.list(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:role:create")
|
|
||||||
val request = call.receive<CreateRoleRequest>()
|
|
||||||
runCatching {
|
|
||||||
val id = RoleService.create(request)
|
|
||||||
call.respond(ok(mapOf("id" to id)))
|
|
||||||
OperationLogService.success(call, currentUser, "CREATE", "新增角色", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "CREATE", "新增角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get("/{id}") {
|
|
||||||
call.requirePermission("system:role:view")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
call.respond(ok(RoleService.detail(id)))
|
|
||||||
}
|
|
||||||
put("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:role:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateRoleRequest>()
|
|
||||||
runCatching {
|
|
||||||
RoleService.update(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "UPDATE", "更新角色", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:role:delete")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
runCatching {
|
|
||||||
RoleService.delete(id)
|
|
||||||
call.respond(ok<Unit>(message = "删除成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "DELETE", "删除角色", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "DELETE", "删除角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put("/{id}/menus") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:role:assign")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateRoleMenusRequest>()
|
|
||||||
runCatching {
|
|
||||||
RoleService.updateMenus(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "菜单分配成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "ASSIGN_MENU", "分配角色菜单", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "ASSIGN_MENU", "分配角色菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package com.bbit.ticket.modules.system.user
|
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
|
||||||
import com.bbit.ticket.common.ErrorCode
|
|
||||||
import com.bbit.ticket.common.PageResult
|
|
||||||
import com.bbit.ticket.common.ok
|
|
||||||
import com.bbit.ticket.common.parseUuid
|
|
||||||
import com.bbit.ticket.common.queryInt
|
|
||||||
import com.bbit.ticket.common.queryString
|
|
||||||
import com.bbit.ticket.common.statusLabel
|
|
||||||
import com.bbit.ticket.database.system.SysOrgTable
|
|
||||||
import com.bbit.ticket.database.system.SysRoleTable
|
|
||||||
import com.bbit.ticket.database.system.SysUserRoleTable
|
|
||||||
import com.bbit.ticket.database.system.SysUserTable
|
|
||||||
import com.bbit.ticket.modules.logs.OperationLogService
|
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
|
||||||
import com.bbit.ticket.security.PasswordService
|
|
||||||
import com.bbit.ticket.security.requirePermission
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.request.receive
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.delete
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import io.ktor.server.routing.put
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
|
||||||
import org.jetbrains.exposed.v1.core.and
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
|
||||||
import org.jetbrains.exposed.v1.core.inList
|
|
||||||
import org.jetbrains.exposed.v1.core.isNull
|
|
||||||
import org.jetbrains.exposed.v1.core.like
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.insertIgnore
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import kotlin.time.TimeSource
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserListItem(
|
|
||||||
val id: String,
|
|
||||||
val username: String,
|
|
||||||
val nickname: String? = null,
|
|
||||||
val realName: String? = null,
|
|
||||||
val orgId: String? = null,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val roleCodes: List<String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UserDetailResponse(
|
|
||||||
val id: String,
|
|
||||||
val username: String,
|
|
||||||
val nickname: String? = null,
|
|
||||||
val realName: String? = null,
|
|
||||||
val phone: String? = null,
|
|
||||||
val email: String? = null,
|
|
||||||
val avatar: String? = null,
|
|
||||||
val orgId: String? = null,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val roleIds: List<String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateUserRequest(
|
|
||||||
val username: String,
|
|
||||||
val password: String,
|
|
||||||
val nickname: String? = null,
|
|
||||||
val realName: String? = null,
|
|
||||||
val phone: String? = null,
|
|
||||||
val email: String? = null,
|
|
||||||
val avatar: String? = null,
|
|
||||||
val orgId: String? = null,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateUserRequest(
|
|
||||||
val nickname: String? = null,
|
|
||||||
val realName: String? = null,
|
|
||||||
val phone: String? = null,
|
|
||||||
val email: String? = null,
|
|
||||||
val avatar: String? = null,
|
|
||||||
val orgId: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateUserStatusRequest(val status: String)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateUserPasswordRequest(val password: String)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateUserRolesRequest(val roleIds: List<String>)
|
|
||||||
|
|
||||||
object UserService {
|
|
||||||
suspend fun list(
|
|
||||||
page: Int,
|
|
||||||
pageSize: Int,
|
|
||||||
username: String?,
|
|
||||||
nickname: String?,
|
|
||||||
status: String?,
|
|
||||||
orgId: Uuid?,
|
|
||||||
): PageResult<UserListItem> = dbQuery {
|
|
||||||
val where = buildWhere(username, nickname, status, orgId)
|
|
||||||
val total = SysUserTable.selectAll().where { where }.count()
|
|
||||||
val rows = SysUserTable.selectAll()
|
|
||||||
.where { where }
|
|
||||||
.orderBy(SysUserTable.createdAt)
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset(((page - 1) * pageSize).toLong())
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
val userIds = rows.map { it[SysUserTable.id] }
|
|
||||||
val roleMap = if (userIds.isEmpty()) {
|
|
||||||
emptyMap()
|
|
||||||
} else {
|
|
||||||
(SysUserRoleTable innerJoin SysRoleTable).selectAll()
|
|
||||||
.where {
|
|
||||||
(SysUserRoleTable.userId inList userIds) and
|
|
||||||
SysRoleTable.deletedAt.isNull()
|
|
||||||
}
|
|
||||||
.groupBy { it[SysUserRoleTable.userId] }
|
|
||||||
.mapValues { entry -> entry.value.map { row -> row[SysRoleTable.code] }.distinct() }
|
|
||||||
}
|
|
||||||
|
|
||||||
PageResult(
|
|
||||||
items = rows.map { row ->
|
|
||||||
UserListItem(
|
|
||||||
id = row[SysUserTable.id].toString(),
|
|
||||||
username = row[SysUserTable.username],
|
|
||||||
nickname = row[SysUserTable.nickname],
|
|
||||||
realName = row[SysUserTable.realName],
|
|
||||||
orgId = row[SysUserTable.orgId]?.toString(),
|
|
||||||
status = row[SysUserTable.status],
|
|
||||||
statusLabel = statusLabel(row[SysUserTable.status]),
|
|
||||||
roleCodes = roleMap[row[SysUserTable.id]] ?: emptyList(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
page = page,
|
|
||||||
pageSize = pageSize,
|
|
||||||
total = total,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun create(request: CreateUserRequest): String = dbQuery {
|
|
||||||
val username = request.username.trim()
|
|
||||||
if (username.isBlank() || request.password.isBlank()) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "用户名和密码不能为空")
|
|
||||||
}
|
|
||||||
val existed = SysUserTable.selectAll().where {
|
|
||||||
(SysUserTable.username eq username) and SysUserTable.deletedAt.isNull()
|
|
||||||
}.any()
|
|
||||||
if (existed) {
|
|
||||||
throw BizException(ErrorCode.DATA_CONFLICT.code, "用户名已存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
val orgUuid = request.orgId?.let { parseUuid(it, "orgId") }
|
|
||||||
if (orgUuid != null) {
|
|
||||||
ensureOrgExists(orgUuid)
|
|
||||||
}
|
|
||||||
val now = OffsetDateTime.now()
|
|
||||||
val row = SysUserTable.insert {
|
|
||||||
it[SysUserTable.username] = username
|
|
||||||
it[passwordHash] = PasswordService.hash(request.password)
|
|
||||||
it[nickname] = request.nickname?.trim()
|
|
||||||
it[realName] = request.realName?.trim()
|
|
||||||
it[phone] = request.phone?.trim()
|
|
||||||
it[email] = request.email?.trim()
|
|
||||||
it[avatar] = request.avatar?.trim()
|
|
||||||
it[orgId] = orgUuid
|
|
||||||
it[status] = request.status
|
|
||||||
it[tokenVersion] = 1
|
|
||||||
it[createdAt] = now
|
|
||||||
}
|
|
||||||
row[SysUserTable.id].toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun detail(id: Uuid): UserDetailResponse = dbQuery {
|
|
||||||
val user = requireUser(id)
|
|
||||||
val roleIds = SysUserRoleTable.selectAll().where { SysUserRoleTable.userId eq id }
|
|
||||||
.map { it[SysUserRoleTable.roleId].toString() }
|
|
||||||
UserDetailResponse(
|
|
||||||
id = user[SysUserTable.id].toString(),
|
|
||||||
username = user[SysUserTable.username],
|
|
||||||
nickname = user[SysUserTable.nickname],
|
|
||||||
realName = user[SysUserTable.realName],
|
|
||||||
phone = user[SysUserTable.phone],
|
|
||||||
email = user[SysUserTable.email],
|
|
||||||
avatar = user[SysUserTable.avatar],
|
|
||||||
orgId = user[SysUserTable.orgId]?.toString(),
|
|
||||||
status = user[SysUserTable.status],
|
|
||||||
statusLabel = statusLabel(user[SysUserTable.status]),
|
|
||||||
roleIds = roleIds,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun update(id: Uuid, request: UpdateUserRequest) = dbQuery {
|
|
||||||
requireUser(id)
|
|
||||||
val orgUuid = request.orgId?.let { parseUuid(it, "orgId") }
|
|
||||||
if (orgUuid != null) {
|
|
||||||
ensureOrgExists(orgUuid)
|
|
||||||
}
|
|
||||||
SysUserTable.update({ SysUserTable.id eq id }) {
|
|
||||||
it[nickname] = request.nickname?.trim()
|
|
||||||
it[realName] = request.realName?.trim()
|
|
||||||
it[phone] = request.phone?.trim()
|
|
||||||
it[email] = request.email?.trim()
|
|
||||||
it[avatar] = request.avatar?.trim()
|
|
||||||
it[orgId] = orgUuid
|
|
||||||
it[updatedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun softDelete(id: Uuid) = dbQuery {
|
|
||||||
if (id.toString() == "00000000-0000-0000-0000-000000000000") {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "系统保留用户不可删除")
|
|
||||||
}
|
|
||||||
requireUser(id)
|
|
||||||
SysUserTable.update({ SysUserTable.id eq id }) {
|
|
||||||
it[deletedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateStatus(id: Uuid, request: UpdateUserStatusRequest) = dbQuery {
|
|
||||||
requireUser(id)
|
|
||||||
SysUserTable.update({ SysUserTable.id eq id }) {
|
|
||||||
it[status] = request.status
|
|
||||||
it[updatedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updatePassword(id: Uuid, request: UpdateUserPasswordRequest) = dbQuery {
|
|
||||||
val user = requireUser(id)
|
|
||||||
if (request.password.isBlank()) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "密码不能为空")
|
|
||||||
}
|
|
||||||
val nextTokenVersion = user[SysUserTable.tokenVersion] + 1
|
|
||||||
SysUserTable.update({ SysUserTable.id eq id }) {
|
|
||||||
it[passwordHash] = PasswordService.hash(request.password)
|
|
||||||
it[tokenVersion] = nextTokenVersion
|
|
||||||
it[updatedAt] = OffsetDateTime.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateRoles(id: Uuid, request: UpdateUserRolesRequest) = dbQuery {
|
|
||||||
requireUser(id)
|
|
||||||
val roleIds = request.roleIds.distinct().map { parseUuid(it, "roleId") }
|
|
||||||
if (roleIds.isNotEmpty()) {
|
|
||||||
val validCount = SysRoleTable.selectAll().where {
|
|
||||||
(SysRoleTable.id inList roleIds) and
|
|
||||||
(SysRoleTable.status eq "ENABLED") and
|
|
||||||
SysRoleTable.deletedAt.isNull()
|
|
||||||
}.count()
|
|
||||||
if (validCount != roleIds.size.toLong()) {
|
|
||||||
throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或已禁用角色")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id }
|
|
||||||
roleIds.forEach { roleId ->
|
|
||||||
SysUserRoleTable.insertIgnore {
|
|
||||||
it[userId] = id
|
|
||||||
it[SysUserRoleTable.roleId] = roleId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op<Boolean> {
|
|
||||||
var where: Op<Boolean> = SysUserTable.deletedAt.isNull()
|
|
||||||
if (!username.isNullOrBlank()) {
|
|
||||||
where = where and (SysUserTable.username like "%$username%")
|
|
||||||
}
|
|
||||||
if (!nickname.isNullOrBlank()) {
|
|
||||||
where = where and (SysUserTable.nickname like "%$nickname%")
|
|
||||||
}
|
|
||||||
if (!status.isNullOrBlank()) {
|
|
||||||
where = where and (SysUserTable.status eq status)
|
|
||||||
}
|
|
||||||
if (orgId != null) {
|
|
||||||
where = where and (SysUserTable.orgId eq orgId)
|
|
||||||
}
|
|
||||||
return where
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requireUser(id: Uuid) =
|
|
||||||
SysUserTable.selectAll().where { (SysUserTable.id eq id) and SysUserTable.deletedAt.isNull() }.singleOrNull()
|
|
||||||
?: throw BizException(ErrorCode.USER_NOT_FOUND.code, ErrorCode.USER_NOT_FOUND.message, HttpStatusCode.NotFound)
|
|
||||||
|
|
||||||
private fun ensureOrgExists(orgId: Uuid) {
|
|
||||||
val exists = SysOrgTable.selectAll().where { (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() }.any()
|
|
||||||
if (!exists) {
|
|
||||||
throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message, HttpStatusCode.BadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Route.registerUserRoutes() {
|
|
||||||
authenticate("auth-jwt") {
|
|
||||||
route("/api/system/users") {
|
|
||||||
get {
|
|
||||||
call.requirePermission("system:user:view")
|
|
||||||
val page = call.queryInt("page", 1)
|
|
||||||
val pageSize = call.queryInt("pageSize", 20)
|
|
||||||
val result = UserService.list(
|
|
||||||
page = page,
|
|
||||||
pageSize = pageSize,
|
|
||||||
username = call.queryString("username"),
|
|
||||||
nickname = call.queryString("nickname"),
|
|
||||||
status = call.queryString("status"),
|
|
||||||
orgId = call.queryString("orgId")?.let { parseUuid(it, "orgId") },
|
|
||||||
)
|
|
||||||
call.respond(ok(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:user:create")
|
|
||||||
val request = call.receive<CreateUserRequest>()
|
|
||||||
runCatching {
|
|
||||||
val id = UserService.create(request)
|
|
||||||
call.respond(ok(mapOf("id" to id)))
|
|
||||||
OperationLogService.success(call, currentUser, "CREATE", "新增用户", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "CREATE", "新增用户", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get("/{id}") {
|
|
||||||
call.requirePermission("system:user:view")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
call.respond(ok(UserService.detail(id)))
|
|
||||||
}
|
|
||||||
|
|
||||||
put("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:user:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateUserRequest>()
|
|
||||||
runCatching {
|
|
||||||
UserService.update(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "UPDATE", "更新用户", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新用户", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delete("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:user:delete")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
runCatching {
|
|
||||||
UserService.softDelete(id)
|
|
||||||
call.respond(ok<Unit>(message = "删除成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "DELETE", "删除用户", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "DELETE", "删除用户", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
put("/{id}/status") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:user:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateUserStatusRequest>()
|
|
||||||
runCatching {
|
|
||||||
UserService.updateStatus(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "状态更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "UPDATE_STATUS", "更新用户状态", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "UPDATE_STATUS", "更新用户状态", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
put("/{id}/password") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:user:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateUserPasswordRequest>()
|
|
||||||
runCatching {
|
|
||||||
UserService.updatePassword(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "密码更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "RESET_PASSWORD", "重置用户密码", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "RESET_PASSWORD", "重置用户密码", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
put("/{id}/roles") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:role:assign")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateUserRolesRequest>()
|
|
||||||
runCatching {
|
|
||||||
UserService.updateRoles(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "角色分配成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "ASSIGN_ROLE", "分配用户角色", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "ASSIGN_ROLE", "分配用户角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.bbit.ticket.plugins
|
package com.bbit.ticket.plugins
|
||||||
|
|
||||||
import com.bbit.ticket.common.TraceIdKey
|
import com.bbit.ticket.utils.TraceIdKey
|
||||||
import com.bbit.ticket.database.system.SysApiAccessLogTable
|
import com.bbit.ticket.database.system.SysApiAccessLogTable
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.createApplicationPlugin
|
import io.ktor.server.application.createApplicationPlugin
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.bbit.ticket.plugins
|
package com.bbit.ticket.plugins
|
||||||
|
|
||||||
import com.bbit.ticket.config.AppConfig
|
import com.bbit.ticket.bootstrap.AppConfig
|
||||||
import io.ktor.http.HttpHeaders
|
import io.ktor.http.HttpHeaders
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.http.HttpMethod
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
@@ -9,14 +9,7 @@ import io.ktor.server.plugins.cors.routing.CORS
|
|||||||
|
|
||||||
fun Application.configureCors() {
|
fun Application.configureCors() {
|
||||||
install(CORS) {
|
install(CORS) {
|
||||||
if (AppConfig.cors.allowedHosts.contains("*")) {
|
|
||||||
anyHost()
|
anyHost()
|
||||||
} else {
|
|
||||||
AppConfig.cors.allowedHosts.forEach { allowedHost ->
|
|
||||||
allowHost(allowedHost, schemes = listOf("http", "https"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allowMethod(HttpMethod.Get)
|
allowMethod(HttpMethod.Get)
|
||||||
allowMethod(HttpMethod.Post)
|
allowMethod(HttpMethod.Post)
|
||||||
allowMethod(HttpMethod.Put)
|
allowMethod(HttpMethod.Put)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.bbit.ticket.plugins
|
package com.bbit.ticket.plugins
|
||||||
|
|
||||||
import com.bbit.ticket.config.AppConfig
|
import com.bbit.ticket.bootstrap.AppConfig
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.bbit.ticket.plugins
|
package com.bbit.ticket.plugins
|
||||||
|
|
||||||
|
import com.bbit.ticket.utils.TraceIdKey
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.plugins.calllogging.CallLogging
|
import io.ktor.server.plugins.calllogging.CallLogging
|
||||||
@@ -10,7 +11,7 @@ import org.slf4j.event.Level
|
|||||||
fun Application.configureLogging() {
|
fun Application.configureLogging() {
|
||||||
install(CallLogging) {
|
install(CallLogging) {
|
||||||
level = Level.INFO
|
level = Level.INFO
|
||||||
mdc("traceId") { call -> call.attributes.getOrNull(com.bbit.ticket.common.TraceIdKey) }
|
mdc("traceId") { call -> call.attributes.getOrNull(TraceIdKey) }
|
||||||
filter { call -> !call.request.path().startsWith("/health") }
|
filter { call -> !call.request.path().startsWith("/health") }
|
||||||
format { call ->
|
format { call ->
|
||||||
val status = call.response.status()
|
val status = call.response.status()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.bbit.ticket.plugins
|
package com.bbit.ticket.plugins
|
||||||
|
|
||||||
import com.bbit.ticket.config.AppConfig
|
import com.bbit.ticket.bootstrap.AppConfig
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import org.redisson.Redisson
|
import org.redisson.Redisson
|
||||||
import org.redisson.api.RedissonClient
|
import org.redisson.api.RedissonClient
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package com.bbit.ticket.plugins
|
|||||||
|
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import com.bbit.ticket.common.ErrorCode
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
import com.bbit.ticket.common.fail
|
import com.bbit.ticket.entity.common.fail
|
||||||
import com.bbit.ticket.common.traceIdOrNull
|
import com.bbit.ticket.utils.traceIdOrNull
|
||||||
import com.bbit.ticket.config.AppConfig
|
import com.bbit.ticket.bootstrap.AppConfig
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.bbit.ticket.plugins
|
package com.bbit.ticket.plugins
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
import com.bbit.ticket.entity.common.BizException
|
||||||
import com.bbit.ticket.common.ErrorCode
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
import com.bbit.ticket.common.fail
|
import com.bbit.ticket.entity.common.fail
|
||||||
import com.bbit.ticket.common.traceIdOrNull
|
import com.bbit.ticket.utils.traceIdOrNull
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.bbit.ticket.plugins
|
package com.bbit.ticket.plugins
|
||||||
|
|
||||||
import com.bbit.ticket.common.TraceIdKey
|
import com.bbit.ticket.utils.TraceIdKey
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.createApplicationPlugin
|
import io.ktor.server.application.createApplicationPlugin
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
|
|||||||
+8
-4
@@ -1,8 +1,11 @@
|
|||||||
package com.bbit.ticket.modules.auth
|
package com.bbit.ticket.route.system
|
||||||
|
|
||||||
import com.bbit.ticket.common.ok
|
|
||||||
import com.bbit.ticket.modules.logs.OperationLogService
|
import com.bbit.ticket.entity.common.ok
|
||||||
import com.bbit.ticket.security.requireCurrentUser
|
import com.bbit.ticket.service.system.AuthService
|
||||||
|
import com.bbit.ticket.entity.system.LoginRequest
|
||||||
|
import com.bbit.ticket.service.system.OperationLogService
|
||||||
|
import com.bbit.ticket.utils.requireCurrentUser
|
||||||
import io.ktor.server.auth.authenticate
|
import io.ktor.server.auth.authenticate
|
||||||
import io.ktor.server.request.receive
|
import io.ktor.server.request.receive
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
@@ -12,6 +15,7 @@ import io.ktor.server.routing.post
|
|||||||
import io.ktor.server.routing.route
|
import io.ktor.server.routing.route
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
|
|
||||||
fun Route.registerAuthRoutes() {
|
fun Route.registerAuthRoutes() {
|
||||||
route("/api/auth") {
|
route("/api/auth") {
|
||||||
post("/login") {
|
post("/login") {
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.route.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.entity.common.ok
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import com.bbit.ticket.utils.queryInt
|
||||||
|
import com.bbit.ticket.utils.queryString
|
||||||
|
import com.bbit.ticket.entity.system.CreateDictItemRequest
|
||||||
|
import com.bbit.ticket.entity.system.CreateDictTypeRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateDictItemRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateDictTypeRequest
|
||||||
|
import com.bbit.ticket.service.system.OperationLogService
|
||||||
|
import com.bbit.ticket.utils.requirePermission
|
||||||
|
import com.bbit.ticket.service.system.DictService
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.request.receive
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.delete
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import io.ktor.server.routing.put
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
import kotlin.time.TimeSource
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
fun Route.registerDictRoutes() {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
route("/api/system/dict-types") {
|
||||||
|
get {
|
||||||
|
call.requirePermission("system:dict:view")
|
||||||
|
val page = call.queryInt("page", 1)
|
||||||
|
val pageSize = call.queryInt("pageSize", 20)
|
||||||
|
call.respond(ok(DictService.listTypes(page, pageSize, call.queryString("keyword"))))
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:dict:create")
|
||||||
|
val request = call.receive<CreateDictTypeRequest>()
|
||||||
|
runCatching {
|
||||||
|
val id = DictService.createType(request)
|
||||||
|
call.respond(ok(mapOf("id" to id)))
|
||||||
|
OperationLogService.success(call, currentUser, "CREATE", "新增字典类型", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "CREATE", "新增字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:dict:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateDictTypeRequest>()
|
||||||
|
runCatching {
|
||||||
|
DictService.updateType(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "UPDATE", "更新字典类型", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "UPDATE", "更新字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:dict:delete")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
runCatching {
|
||||||
|
DictService.deleteType(id)
|
||||||
|
call.respond(ok<Unit>(message = "删除成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "DELETE", "删除字典类型", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "DELETE", "删除字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
route("/api/system/dict-items") {
|
||||||
|
get {
|
||||||
|
call.requirePermission("system:dict:view")
|
||||||
|
val page = call.queryInt("page", 1)
|
||||||
|
val pageSize = call.queryInt("pageSize", 20)
|
||||||
|
val typeId = call.queryString("typeId")?.let { parseUuid(it, "typeId") }
|
||||||
|
call.respond(ok(DictService.listItems(page, pageSize, typeId)))
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:dict:create")
|
||||||
|
val request = call.receive<CreateDictItemRequest>()
|
||||||
|
runCatching {
|
||||||
|
val id = DictService.createItem(request)
|
||||||
|
call.respond(ok(mapOf("id" to id)))
|
||||||
|
OperationLogService.success(call, currentUser, "CREATE", "新增字典项", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "CREATE", "新增字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:dict:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateDictItemRequest>()
|
||||||
|
runCatching {
|
||||||
|
DictService.updateItem(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "UPDATE", "更新字典项", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "UPDATE", "更新字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:dict:delete")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
runCatching {
|
||||||
|
DictService.deleteItem(id)
|
||||||
|
call.respond(ok<Unit>(message = "删除成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "DELETE", "删除字典项", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "DELETE", "删除字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.bbit.ticket.route.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.entity.common.ok
|
||||||
|
import com.bbit.ticket.utils.queryInt
|
||||||
|
import com.bbit.ticket.utils.queryString
|
||||||
|
import com.bbit.ticket.utils.requirePermission
|
||||||
|
import com.bbit.ticket.service.system.LogsQueryService
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
|
||||||
|
fun Route.registerLogsQueryRoutes() {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
route("/api/logs") {
|
||||||
|
get("/operation") {
|
||||||
|
call.requirePermission("log:operation:view")
|
||||||
|
val page = call.queryInt("page", 1)
|
||||||
|
val pageSize = call.queryInt("pageSize", 20)
|
||||||
|
call.respond(ok(LogsQueryService.operationLogs(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
||||||
|
}
|
||||||
|
get("/api-access") {
|
||||||
|
call.requirePermission("log:api-access:view")
|
||||||
|
val page = call.queryInt("page", 1)
|
||||||
|
val pageSize = call.queryInt("pageSize", 20)
|
||||||
|
call.respond(ok(LogsQueryService.apiAccessLogs(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.route.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.entity.system.CreateMenuRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateMenuRequest
|
||||||
|
import com.bbit.ticket.entity.common.ok
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import com.bbit.ticket.utils.requirePermission
|
||||||
|
import com.bbit.ticket.service.system.MenuService
|
||||||
|
import com.bbit.ticket.service.system.OperationLogService
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.request.receive
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.delete
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import io.ktor.server.routing.put
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
import kotlin.time.TimeSource
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
fun Route.registerMenuRoutes() {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
route("/api/system/menus") {
|
||||||
|
get {
|
||||||
|
call.requirePermission("system:menu:view")
|
||||||
|
call.respond(ok(MenuService.tree()))
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:menu:create")
|
||||||
|
val request = call.receive<CreateMenuRequest>()
|
||||||
|
runCatching {
|
||||||
|
val id = MenuService.create(request)
|
||||||
|
call.respond(ok(mapOf("id" to id)))
|
||||||
|
OperationLogService.success(call, currentUser, "CREATE", "新增菜单", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "CREATE", "新增菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:menu:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateMenuRequest>()
|
||||||
|
runCatching {
|
||||||
|
MenuService.update(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "UPDATE", "更新菜单", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "UPDATE", "更新菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:menu:delete")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
runCatching {
|
||||||
|
MenuService.delete(id)
|
||||||
|
call.respond(ok<Unit>(message = "删除成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "DELETE", "删除菜单", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "DELETE", "删除菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.route.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.entity.common.ok
|
||||||
|
import com.bbit.ticket.entity.system.CreateOrgRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateOrgRequest
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import com.bbit.ticket.utils.requirePermission
|
||||||
|
import com.bbit.ticket.service.system.OperationLogService
|
||||||
|
import com.bbit.ticket.service.system.OrgService
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.request.receive
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.delete
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import io.ktor.server.routing.put
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
import kotlin.time.TimeSource
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
fun Route.registerOrgRoutes() {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
route("/api/system/orgs") {
|
||||||
|
get {
|
||||||
|
call.requirePermission("system:org:view")
|
||||||
|
call.respond(ok(OrgService.tree()))
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:org:create")
|
||||||
|
val request = call.receive<CreateOrgRequest>()
|
||||||
|
runCatching {
|
||||||
|
val id = OrgService.create(request)
|
||||||
|
call.respond(ok(mapOf("id" to id)))
|
||||||
|
OperationLogService.success(call, currentUser, "CREATE", "新增组织", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "CREATE", "新增组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:org:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateOrgRequest>()
|
||||||
|
runCatching {
|
||||||
|
OrgService.update(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "UPDATE", "更新组织", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "UPDATE", "更新组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:org:delete")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
runCatching {
|
||||||
|
OrgService.delete(id)
|
||||||
|
call.respond(ok<Unit>(message = "删除成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "DELETE", "删除组织", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "DELETE", "删除组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.route.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.entity.system.CreateRoleRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateRoleMenusRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateRoleRequest
|
||||||
|
import com.bbit.ticket.entity.common.ok
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import com.bbit.ticket.utils.queryInt
|
||||||
|
import com.bbit.ticket.utils.queryString
|
||||||
|
import com.bbit.ticket.utils.requirePermission
|
||||||
|
import com.bbit.ticket.service.system.OperationLogService
|
||||||
|
import com.bbit.ticket.service.system.RoleService
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.request.receive
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.delete
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import io.ktor.server.routing.put
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
import kotlin.time.TimeSource
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
fun Route.registerRoleRoutes() {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
route("/api/system/roles") {
|
||||||
|
get {
|
||||||
|
call.requirePermission("system:role:view")
|
||||||
|
val page = call.queryInt("page", 1)
|
||||||
|
val pageSize = call.queryInt("pageSize", 20)
|
||||||
|
call.respond(ok(RoleService.list(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:role:create")
|
||||||
|
val request = call.receive<CreateRoleRequest>()
|
||||||
|
runCatching {
|
||||||
|
val id = RoleService.create(request)
|
||||||
|
call.respond(ok(mapOf("id" to id)))
|
||||||
|
OperationLogService.success(call, currentUser, "CREATE", "新增角色", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "CREATE", "新增角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get("/{id}") {
|
||||||
|
call.requirePermission("system:role:view")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
call.respond(ok(RoleService.detail(id)))
|
||||||
|
}
|
||||||
|
put("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:role:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateRoleRequest>()
|
||||||
|
runCatching {
|
||||||
|
RoleService.update(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "UPDATE", "更新角色", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "UPDATE", "更新角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:role:delete")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
runCatching {
|
||||||
|
RoleService.delete(id)
|
||||||
|
call.respond(ok<Unit>(message = "删除成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "DELETE", "删除角色", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "DELETE", "删除角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("/{id}/menus") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:role:assign")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateRoleMenusRequest>()
|
||||||
|
runCatching {
|
||||||
|
RoleService.updateMenus(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "菜单分配成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "ASSIGN_MENU", "分配角色菜单", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "ASSIGN_MENU", "分配角色菜单", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.route.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.entity.system.CreateUserRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserPasswordRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserRolesRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserStatusRequest
|
||||||
|
import com.bbit.ticket.entity.common.ok
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import com.bbit.ticket.utils.queryInt
|
||||||
|
import com.bbit.ticket.utils.queryString
|
||||||
|
import com.bbit.ticket.utils.requirePermission
|
||||||
|
import com.bbit.ticket.service.system.OperationLogService
|
||||||
|
import com.bbit.ticket.service.system.UserService
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.request.receive
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.delete
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import io.ktor.server.routing.put
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
import kotlin.time.TimeSource
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
fun Route.registerUserRoutes() {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
route("/api/system/users") {
|
||||||
|
get {
|
||||||
|
call.requirePermission("system:user:view")
|
||||||
|
val page = call.queryInt("page", 1)
|
||||||
|
val pageSize = call.queryInt("pageSize", 20)
|
||||||
|
val result = UserService.list(
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
username = call.queryString("username"),
|
||||||
|
nickname = call.queryString("nickname"),
|
||||||
|
status = call.queryString("status"),
|
||||||
|
orgId = call.queryString("orgId")?.let { parseUuid(it, "orgId") },
|
||||||
|
)
|
||||||
|
call.respond(ok(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:user:create")
|
||||||
|
val request = call.receive<CreateUserRequest>()
|
||||||
|
runCatching {
|
||||||
|
val id = UserService.create(request)
|
||||||
|
call.respond(ok(mapOf("id" to id)))
|
||||||
|
OperationLogService.success(call, currentUser, "CREATE", "新增用户", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "CREATE", "新增用户", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/{id}") {
|
||||||
|
call.requirePermission("system:user:view")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
call.respond(ok(UserService.detail(id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
put("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:user:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateUserRequest>()
|
||||||
|
runCatching {
|
||||||
|
UserService.update(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "UPDATE", "更新用户", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "UPDATE", "更新用户", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete("/{id}") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:user:delete")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
runCatching {
|
||||||
|
UserService.softDelete(id)
|
||||||
|
call.respond(ok<Unit>(message = "删除成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "DELETE", "删除用户", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "DELETE", "删除用户", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put("/{id}/status") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:user:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateUserStatusRequest>()
|
||||||
|
runCatching {
|
||||||
|
UserService.updateStatus(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "状态更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "UPDATE_STATUS", "更新用户状态", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "UPDATE_STATUS", "更新用户状态", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put("/{id}/password") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:user:update")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateUserPasswordRequest>()
|
||||||
|
runCatching {
|
||||||
|
UserService.updatePassword(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "密码更新成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "RESET_PASSWORD", "重置用户密码", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "RESET_PASSWORD", "重置用户密码", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put("/{id}/roles") {
|
||||||
|
val start = TimeSource.Monotonic.markNow()
|
||||||
|
val currentUser = call.requirePermission("system:role:assign")
|
||||||
|
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||||
|
val request = call.receive<UpdateUserRolesRequest>()
|
||||||
|
runCatching {
|
||||||
|
UserService.updateRoles(id, request)
|
||||||
|
call.respond(ok<Unit>(message = "角色分配成功"))
|
||||||
|
OperationLogService.success(call, currentUser, "ASSIGN_ROLE", "分配用户角色", start.elapsedNow().inWholeMilliseconds)
|
||||||
|
}.onFailure {
|
||||||
|
OperationLogService.fail(call, currentUser, "ASSIGN_ROLE", "分配用户角色", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-9
@@ -1,18 +1,21 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package com.bbit.ticket.modules.auth
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
import com.bbit.ticket.entity.common.BizException
|
||||||
import com.bbit.ticket.common.ErrorCode
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
import com.bbit.ticket.database.system.SysMenuTable
|
import com.bbit.ticket.database.system.SysMenuTable
|
||||||
import com.bbit.ticket.database.system.SysRoleMenuTable
|
import com.bbit.ticket.database.system.SysRoleMenuTable
|
||||||
import com.bbit.ticket.database.system.SysRoleTable
|
import com.bbit.ticket.database.system.SysRoleTable
|
||||||
import com.bbit.ticket.database.system.SysUserRoleTable
|
import com.bbit.ticket.database.system.SysUserRoleTable
|
||||||
import com.bbit.ticket.database.system.SysUserTable
|
import com.bbit.ticket.database.system.SysUserTable
|
||||||
|
import com.bbit.ticket.entity.system.CurrentUserProfile
|
||||||
|
import com.bbit.ticket.entity.system.LoginRequest
|
||||||
|
import com.bbit.ticket.entity.system.LoginResponse
|
||||||
|
import com.bbit.ticket.entity.system.MeResponse
|
||||||
|
import com.bbit.ticket.entity.system.MenuNode
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
import com.bbit.ticket.security.CurrentUser
|
import com.bbit.ticket.utils.CurrentUser
|
||||||
import com.bbit.ticket.security.JwtService
|
|
||||||
import com.bbit.ticket.security.PasswordService
|
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
@@ -21,6 +24,7 @@ import org.jetbrains.exposed.v1.core.isNull
|
|||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
object AuthService {
|
object AuthService {
|
||||||
@@ -186,8 +190,6 @@ object AuthService {
|
|||||||
|
|
||||||
return build(null)
|
return build(null)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private data class MenuFlat(
|
private data class MenuFlat(
|
||||||
val id: Uuid,
|
val id: Uuid,
|
||||||
val parentId: Uuid?,
|
val parentId: Uuid?,
|
||||||
@@ -202,3 +204,5 @@ private data class MenuFlat(
|
|||||||
val visible: Boolean,
|
val visible: Boolean,
|
||||||
val keepAlive: Boolean,
|
val keepAlive: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.database.system.SysDictItemTable
|
||||||
|
import com.bbit.ticket.database.system.SysDictItemTable.color
|
||||||
|
import com.bbit.ticket.database.system.SysDictItemTable.label
|
||||||
|
import com.bbit.ticket.database.system.SysDictTypeTable
|
||||||
|
import com.bbit.ticket.entity.system.CreateDictItemRequest
|
||||||
|
import com.bbit.ticket.entity.system.CreateDictTypeRequest
|
||||||
|
import com.bbit.ticket.entity.system.DictItem
|
||||||
|
import com.bbit.ticket.entity.system.DictTypeItem
|
||||||
|
import com.bbit.ticket.entity.system.UpdateDictItemRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateDictTypeRequest
|
||||||
|
import com.bbit.ticket.entity.common.BizException
|
||||||
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
|
import com.bbit.ticket.entity.common.PageResult
|
||||||
|
import com.bbit.ticket.entity.common.statusLabel
|
||||||
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.isNull
|
||||||
|
import org.jetbrains.exposed.v1.core.like
|
||||||
|
import org.jetbrains.exposed.v1.core.or
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
object DictService {
|
||||||
|
suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult<DictTypeItem> = dbQuery {
|
||||||
|
var where = SysDictTypeTable.deletedAt.isNull()
|
||||||
|
if (!keyword.isNullOrBlank()) {
|
||||||
|
where = where and ((SysDictTypeTable.code like "%$keyword%") or (SysDictTypeTable.name like "%$keyword%"))
|
||||||
|
}
|
||||||
|
val total = SysDictTypeTable.selectAll().where { where }.count()
|
||||||
|
val rows = SysDictTypeTable.selectAll().where { where }
|
||||||
|
.orderBy(SysDictTypeTable.createdAt)
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(((page - 1) * pageSize).toLong())
|
||||||
|
.toList()
|
||||||
|
PageResult(
|
||||||
|
items = rows.map {
|
||||||
|
DictTypeItem(
|
||||||
|
id = it[SysDictTypeTable.id].toString(),
|
||||||
|
code = it[SysDictTypeTable.code],
|
||||||
|
name = it[SysDictTypeTable.name],
|
||||||
|
status = it[SysDictTypeTable.status],
|
||||||
|
statusLabel = statusLabel(it[SysDictTypeTable.status]),
|
||||||
|
remark = it[SysDictTypeTable.remark],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
total = total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createType(request: CreateDictTypeRequest): String = dbQuery {
|
||||||
|
if (request.code.trim().isBlank() || request.name.trim().isBlank()) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型编码和名称不能为空")
|
||||||
|
}
|
||||||
|
val exists = SysDictTypeTable.selectAll().where {
|
||||||
|
(SysDictTypeTable.code eq request.code.trim()) and SysDictTypeTable.deletedAt.isNull()
|
||||||
|
}.any()
|
||||||
|
if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "字典类型编码已存在")
|
||||||
|
val inserted = SysDictTypeTable.insert {
|
||||||
|
it[code] = request.code.trim()
|
||||||
|
it[name] = request.name.trim()
|
||||||
|
it[status] = request.status
|
||||||
|
it[remark] = request.remark?.trim()
|
||||||
|
it[createdAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
inserted[SysDictTypeTable.id].toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateType(id: Uuid, request: UpdateDictTypeRequest) = dbQuery {
|
||||||
|
requireType(id)
|
||||||
|
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
|
||||||
|
it[name] = request.name.trim()
|
||||||
|
it[status] = request.status
|
||||||
|
it[remark] = request.remark?.trim()
|
||||||
|
it[updatedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteType(id: Uuid) = dbQuery {
|
||||||
|
requireType(id)
|
||||||
|
val hasItems = SysDictItemTable.selectAll().where {
|
||||||
|
(SysDictItemTable.typeId eq id) and SysDictItemTable.deletedAt.isNull()
|
||||||
|
}.any()
|
||||||
|
if (hasItems) throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型下存在字典项,不能删除")
|
||||||
|
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
|
||||||
|
it[deletedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult<DictItem> = dbQuery {
|
||||||
|
var where = SysDictItemTable.deletedAt.isNull()
|
||||||
|
if (typeId != null) where = where and (SysDictItemTable.typeId eq typeId)
|
||||||
|
val total = SysDictItemTable.selectAll().where { where }.count()
|
||||||
|
val rows = SysDictItemTable.selectAll().where { where }
|
||||||
|
.orderBy(SysDictItemTable.sort)
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(((page - 1) * pageSize).toLong())
|
||||||
|
.toList()
|
||||||
|
PageResult(
|
||||||
|
items = rows.map {
|
||||||
|
DictItem(
|
||||||
|
id = it[SysDictItemTable.id].toString(),
|
||||||
|
typeId = it[SysDictItemTable.typeId].toString(),
|
||||||
|
label = it[label],
|
||||||
|
value = it[SysDictItemTable.value],
|
||||||
|
color = it[color],
|
||||||
|
sort = it[SysDictItemTable.sort],
|
||||||
|
status = it[SysDictItemTable.status],
|
||||||
|
statusLabel = statusLabel(it[SysDictItemTable.status]),
|
||||||
|
remark = it[SysDictItemTable.remark],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
total = total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createItem(request: CreateDictItemRequest): String = dbQuery {
|
||||||
|
val typeId = parseUuid(request.typeId, "typeId")
|
||||||
|
requireType(typeId)
|
||||||
|
val inserted = SysDictItemTable.insert {
|
||||||
|
it[SysDictItemTable.typeId] = typeId
|
||||||
|
it[label] = request.label.trim()
|
||||||
|
it[value] = request.value.trim()
|
||||||
|
it[color] = request.color?.trim()
|
||||||
|
it[sort] = request.sort
|
||||||
|
it[status] = request.status
|
||||||
|
it[remark] = request.remark?.trim()
|
||||||
|
it[createdAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
inserted[SysDictItemTable.id].toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateItem(id: Uuid, request: UpdateDictItemRequest) = dbQuery {
|
||||||
|
requireItem(id)
|
||||||
|
val typeId = parseUuid(request.typeId, "typeId")
|
||||||
|
requireType(typeId)
|
||||||
|
SysDictItemTable.update({ SysDictItemTable.id eq id }) {
|
||||||
|
it[SysDictItemTable.typeId] = typeId
|
||||||
|
it[label] = request.label.trim()
|
||||||
|
it[value] = request.value.trim()
|
||||||
|
it[color] = request.color?.trim()
|
||||||
|
it[sort] = request.sort
|
||||||
|
it[status] = request.status
|
||||||
|
it[remark] = request.remark?.trim()
|
||||||
|
it[updatedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteItem(id: Uuid) = dbQuery {
|
||||||
|
requireItem(id)
|
||||||
|
SysDictItemTable.update({ SysDictItemTable.id eq id }) {
|
||||||
|
it[deletedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireType(id: Uuid): ResultRow =
|
||||||
|
SysDictTypeTable.selectAll().where { (SysDictTypeTable.id eq id) and SysDictTypeTable.deletedAt.isNull() }.singleOrNull()
|
||||||
|
?: throw BizException(
|
||||||
|
ErrorCode.DICT_TYPE_NOT_FOUND.code,
|
||||||
|
ErrorCode.DICT_TYPE_NOT_FOUND.message,
|
||||||
|
HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun requireItem(id: Uuid): ResultRow =
|
||||||
|
SysDictItemTable.selectAll().where { (SysDictItemTable.id eq id) and SysDictItemTable.deletedAt.isNull() }.singleOrNull()
|
||||||
|
?: throw BizException(
|
||||||
|
ErrorCode.DICT_ITEM_NOT_FOUND.code,
|
||||||
|
ErrorCode.DICT_ITEM_NOT_FOUND.message,
|
||||||
|
HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
-3
@@ -1,8 +1,8 @@
|
|||||||
package com.bbit.ticket.security
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import com.bbit.ticket.config.AppConfig
|
import com.bbit.ticket.bootstrap.AppConfig
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
@@ -33,4 +33,3 @@ object JwtService {
|
|||||||
return token to AppConfig.jwt.accessTokenTtlMinutes * 60
|
return token to AppConfig.jwt.accessTokenTtlMinutes * 60
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+7
-63
@@ -1,22 +1,14 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package com.bbit.ticket.modules.logs
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
import com.bbit.ticket.common.PageResult
|
|
||||||
import com.bbit.ticket.common.formatDateTime
|
|
||||||
import com.bbit.ticket.common.ok
|
|
||||||
import com.bbit.ticket.common.queryInt
|
|
||||||
import com.bbit.ticket.common.queryString
|
|
||||||
import com.bbit.ticket.database.system.SysApiAccessLogTable
|
import com.bbit.ticket.database.system.SysApiAccessLogTable
|
||||||
import com.bbit.ticket.database.system.SysOperationLogTable
|
import com.bbit.ticket.database.system.SysOperationLogTable
|
||||||
|
import com.bbit.ticket.entity.system.ApiAccessLogItem
|
||||||
|
import com.bbit.ticket.entity.system.OperationLogItem
|
||||||
|
import com.bbit.ticket.entity.common.PageResult
|
||||||
|
import com.bbit.ticket.utils.formatDateTime
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
import com.bbit.ticket.security.requirePermission
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
import org.jetbrains.exposed.v1.core.Op
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
@@ -24,36 +16,7 @@ import org.jetbrains.exposed.v1.core.eq
|
|||||||
import org.jetbrains.exposed.v1.core.like
|
import org.jetbrains.exposed.v1.core.like
|
||||||
import org.jetbrains.exposed.v1.core.or
|
import org.jetbrains.exposed.v1.core.or
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
@Serializable
|
|
||||||
data class OperationLogItem(
|
|
||||||
val id: String,
|
|
||||||
val traceId: String? = null,
|
|
||||||
val username: String? = null,
|
|
||||||
val operationType: String,
|
|
||||||
val operationName: String,
|
|
||||||
val httpMethod: String,
|
|
||||||
val requestPath: String,
|
|
||||||
val status: String,
|
|
||||||
val errorMessage: String? = null,
|
|
||||||
val costMs: Long,
|
|
||||||
val createdAt: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ApiAccessLogItem(
|
|
||||||
val id: String,
|
|
||||||
val traceId: String? = null,
|
|
||||||
val appKey: String? = null,
|
|
||||||
val appName: String? = null,
|
|
||||||
val httpMethod: String,
|
|
||||||
val requestPath: String,
|
|
||||||
val responseCode: String? = null,
|
|
||||||
val status: String,
|
|
||||||
val errorMessage: String? = null,
|
|
||||||
val costMs: Long,
|
|
||||||
val createdAt: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
object LogsQueryService {
|
object LogsQueryService {
|
||||||
suspend fun operationLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<OperationLogItem> = dbQuery {
|
suspend fun operationLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<OperationLogItem> = dbQuery {
|
||||||
@@ -124,22 +87,3 @@ object LogsQueryService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.registerLogsQueryRoutes() {
|
|
||||||
authenticate("auth-jwt") {
|
|
||||||
route("/api/logs") {
|
|
||||||
get("/operation") {
|
|
||||||
call.requirePermission("log:operation:view")
|
|
||||||
val page = call.queryInt("page", 1)
|
|
||||||
val pageSize = call.queryInt("pageSize", 20)
|
|
||||||
call.respond(ok(LogsQueryService.operationLogs(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
|
||||||
}
|
|
||||||
get("/api-access") {
|
|
||||||
call.requirePermission("log:api-access:view")
|
|
||||||
val page = call.queryInt("page", 1)
|
|
||||||
val pageSize = call.queryInt("pageSize", 20)
|
|
||||||
call.respond(ok(LogsQueryService.apiAccessLogs(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable.builtIn
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable.component
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable.icon
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable.keepAlive
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable.path
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable.permission
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable.visible
|
||||||
|
import com.bbit.ticket.database.system.SysRoleMenuTable
|
||||||
|
import com.bbit.ticket.entity.system.CreateMenuRequest
|
||||||
|
import com.bbit.ticket.entity.system.MenuFlat
|
||||||
|
import com.bbit.ticket.entity.system.MenuTreeNode
|
||||||
|
import com.bbit.ticket.entity.system.UpdateMenuRequest
|
||||||
|
import com.bbit.ticket.entity.common.BizException
|
||||||
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
|
import com.bbit.ticket.entity.common.menuTypeLabel
|
||||||
|
import com.bbit.ticket.entity.common.statusLabel
|
||||||
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.isNull
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
object MenuService {
|
||||||
|
suspend fun tree(): List<MenuTreeNode> = dbQuery {
|
||||||
|
val rows = SysMenuTable.selectAll().where { SysMenuTable.deletedAt.isNull() }.toList()
|
||||||
|
val flat = rows.map {
|
||||||
|
MenuFlat(
|
||||||
|
id = it[SysMenuTable.id],
|
||||||
|
parentId = it[SysMenuTable.parentId],
|
||||||
|
type = it[SysMenuTable.type],
|
||||||
|
title = it[SysMenuTable.title],
|
||||||
|
name = it[SysMenuTable.name],
|
||||||
|
path = it[path],
|
||||||
|
component = it[component],
|
||||||
|
icon = it[icon],
|
||||||
|
permission = it[permission],
|
||||||
|
sort = it[SysMenuTable.sort],
|
||||||
|
visible = it[visible],
|
||||||
|
keepAlive = it[keepAlive],
|
||||||
|
builtIn = it[builtIn],
|
||||||
|
status = it[SysMenuTable.status],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
buildTree(flat)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun create(request: CreateMenuRequest): String = dbQuery {
|
||||||
|
validateMenuType(request.type)
|
||||||
|
val parentId = request.parentId?.let { parseUuid(it, "parentId") }
|
||||||
|
if (parentId != null) requireMenu(parentId)
|
||||||
|
val inserted = SysMenuTable.insert {
|
||||||
|
it[SysMenuTable.parentId] = parentId
|
||||||
|
it[SysMenuTable.type] = request.type
|
||||||
|
it[title] = request.title.trim()
|
||||||
|
it[name] = request.name?.trim()
|
||||||
|
it[path] = request.path?.trim()
|
||||||
|
it[component] = request.component?.trim()
|
||||||
|
it[icon] = request.icon?.trim()
|
||||||
|
it[permission] = request.permission?.trim()
|
||||||
|
it[sort] = request.sort
|
||||||
|
it[visible] = request.visible
|
||||||
|
it[keepAlive] = request.keepAlive
|
||||||
|
it[builtIn] = false
|
||||||
|
it[status] = request.status
|
||||||
|
it[createdAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
inserted[SysMenuTable.id].toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(id: Uuid, request: UpdateMenuRequest) = dbQuery {
|
||||||
|
requireMenu(id)
|
||||||
|
validateMenuType(request.type)
|
||||||
|
val parentId = request.parentId?.let { parseUuid(it, "parentId") }
|
||||||
|
if (parentId == id) throw BizException(ErrorCode.BAD_REQUEST.code, "上级菜单不能选择自身")
|
||||||
|
if (parentId != null) requireMenu(parentId)
|
||||||
|
SysMenuTable.update({ SysMenuTable.id eq id }) {
|
||||||
|
it[SysMenuTable.parentId] = parentId
|
||||||
|
it[SysMenuTable.type] = request.type
|
||||||
|
it[title] = request.title.trim()
|
||||||
|
it[name] = request.name?.trim()
|
||||||
|
it[path] = request.path?.trim()
|
||||||
|
it[component] = request.component?.trim()
|
||||||
|
it[icon] = request.icon?.trim()
|
||||||
|
it[permission] = request.permission?.trim()
|
||||||
|
it[sort] = request.sort
|
||||||
|
it[visible] = request.visible
|
||||||
|
it[keepAlive] = request.keepAlive
|
||||||
|
it[status] = request.status
|
||||||
|
it[updatedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(id: Uuid) = dbQuery {
|
||||||
|
requireMenu(id)
|
||||||
|
val hasChildren = SysMenuTable.selectAll().where {
|
||||||
|
(SysMenuTable.parentId eq id) and SysMenuTable.deletedAt.isNull()
|
||||||
|
}.any()
|
||||||
|
if (hasChildren) throw BizException(ErrorCode.BAD_REQUEST.code, "存在子菜单,不能删除")
|
||||||
|
val referenced = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.menuId eq id }.any()
|
||||||
|
if (referenced) throw BizException(ErrorCode.BAD_REQUEST.code, "菜单已被角色引用,不能删除")
|
||||||
|
val row = requireMenu(id)
|
||||||
|
if (row[builtIn]) throw BizException(ErrorCode.BAD_REQUEST.code, "基础框架内置菜单不可删除")
|
||||||
|
SysMenuTable.update({ SysMenuTable.id eq id }) {
|
||||||
|
it[deletedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireMenu(id: Uuid): ResultRow =
|
||||||
|
SysMenuTable.selectAll().where { (SysMenuTable.id eq id) and SysMenuTable.deletedAt.isNull() }.singleOrNull()
|
||||||
|
?: throw BizException(
|
||||||
|
ErrorCode.MENU_NOT_FOUND.code,
|
||||||
|
ErrorCode.MENU_NOT_FOUND.message,
|
||||||
|
HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun validateMenuType(type: String) {
|
||||||
|
if (type !in setOf("CATALOG", "MENU", "BUTTON")) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "菜单类型必须是目录、菜单或按钮")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTree(items: List<MenuFlat>): List<MenuTreeNode> {
|
||||||
|
val grouped = items.groupBy { it.parentId }
|
||||||
|
fun children(parentId: Uuid?): List<MenuTreeNode> =
|
||||||
|
(grouped[parentId] ?: emptyList()).sortedBy { it.sort }.map { menu ->
|
||||||
|
MenuTreeNode(
|
||||||
|
id = menu.id.toString(),
|
||||||
|
parentId = menu.parentId?.toString(),
|
||||||
|
type = menu.type,
|
||||||
|
typeLabel = menuTypeLabel(menu.type),
|
||||||
|
title = menu.title,
|
||||||
|
name = menu.name,
|
||||||
|
path = menu.path,
|
||||||
|
component = menu.component,
|
||||||
|
icon = menu.icon,
|
||||||
|
permission = menu.permission,
|
||||||
|
sort = menu.sort,
|
||||||
|
visible = menu.visible,
|
||||||
|
keepAlive = menu.keepAlive,
|
||||||
|
builtIn = menu.builtIn,
|
||||||
|
status = menu.status,
|
||||||
|
statusLabel = statusLabel(menu.status),
|
||||||
|
children = children(menu.id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return children(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -1,17 +1,18 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package com.bbit.ticket.modules.logs
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
import com.bbit.ticket.common.traceIdOrNull
|
|
||||||
import com.bbit.ticket.database.system.SysOperationLogTable
|
import com.bbit.ticket.database.system.SysOperationLogTable
|
||||||
|
import com.bbit.ticket.utils.traceIdOrNull
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
import com.bbit.ticket.security.CurrentUser
|
import com.bbit.ticket.utils.CurrentUser
|
||||||
import io.ktor.http.formUrlEncode
|
import io.ktor.http.formUrlEncode
|
||||||
import io.ktor.server.application.ApplicationCall
|
import io.ktor.server.application.ApplicationCall
|
||||||
import io.ktor.server.request.httpMethod
|
import io.ktor.server.request.httpMethod
|
||||||
import io.ktor.server.request.path
|
import io.ktor.server.request.path
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
object OperationLogService {
|
object OperationLogService {
|
||||||
suspend fun success(call: ApplicationCall, currentUser: CurrentUser?, operationType: String, operationName: String, costMs: Long) {
|
suspend fun success(call: ApplicationCall, currentUser: CurrentUser?, operationType: String, operationName: String, costMs: Long) {
|
||||||
+15
-102
@@ -1,28 +1,18 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package com.bbit.ticket.modules.system.org
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
|
||||||
import com.bbit.ticket.common.ErrorCode
|
|
||||||
import com.bbit.ticket.common.ok
|
|
||||||
import com.bbit.ticket.common.parseUuid
|
|
||||||
import com.bbit.ticket.common.statusLabel
|
|
||||||
import com.bbit.ticket.database.system.SysOrgTable
|
import com.bbit.ticket.database.system.SysOrgTable
|
||||||
import com.bbit.ticket.database.system.SysUserTable
|
import com.bbit.ticket.database.system.SysUserTable
|
||||||
import com.bbit.ticket.modules.logs.OperationLogService
|
import com.bbit.ticket.entity.system.CreateOrgRequest
|
||||||
|
import com.bbit.ticket.entity.system.OrgTreeNode
|
||||||
|
import com.bbit.ticket.entity.system.UpdateOrgRequest
|
||||||
|
import com.bbit.ticket.entity.common.BizException
|
||||||
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
|
import com.bbit.ticket.entity.common.statusLabel
|
||||||
import com.bbit.ticket.plugins.dbQuery
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
import com.bbit.ticket.security.requirePermission
|
import com.bbit.ticket.utils.parseUuid
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.request.receive
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.delete
|
|
||||||
import io.ktor.server.routing.get
|
|
||||||
import io.ktor.server.routing.post
|
|
||||||
import io.ktor.server.routing.put
|
|
||||||
import io.ktor.server.routing.route
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
@@ -31,38 +21,9 @@ import org.jetbrains.exposed.v1.jdbc.insert
|
|||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.time.TimeSource
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OrgTreeNode(
|
|
||||||
val id: String,
|
|
||||||
val parentId: String? = null,
|
|
||||||
val name: String,
|
|
||||||
val code: String,
|
|
||||||
val sort: Int,
|
|
||||||
val status: String,
|
|
||||||
val statusLabel: String,
|
|
||||||
val children: List<OrgTreeNode> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateOrgRequest(
|
|
||||||
val parentId: String? = null,
|
|
||||||
val name: String,
|
|
||||||
val code: String,
|
|
||||||
val sort: Int = 0,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UpdateOrgRequest(
|
|
||||||
val parentId: String? = null,
|
|
||||||
val name: String,
|
|
||||||
val sort: Int = 0,
|
|
||||||
val status: String = "ENABLED",
|
|
||||||
)
|
|
||||||
|
|
||||||
object OrgService {
|
object OrgService {
|
||||||
suspend fun tree(): List<OrgTreeNode> = dbQuery {
|
suspend fun tree(): List<OrgTreeNode> = dbQuery {
|
||||||
val rows = SysOrgTable.selectAll()
|
val rows = SysOrgTable.selectAll()
|
||||||
@@ -137,7 +98,11 @@ object OrgService {
|
|||||||
|
|
||||||
private fun requireOrg(id: Uuid): ResultRow =
|
private fun requireOrg(id: Uuid): ResultRow =
|
||||||
SysOrgTable.selectAll().where { (SysOrgTable.id eq id) and SysOrgTable.deletedAt.isNull() }.singleOrNull()
|
SysOrgTable.selectAll().where { (SysOrgTable.id eq id) and SysOrgTable.deletedAt.isNull() }.singleOrNull()
|
||||||
?: throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message, HttpStatusCode.NotFound)
|
?: throw BizException(
|
||||||
|
ErrorCode.ORG_NOT_FOUND.code,
|
||||||
|
ErrorCode.ORG_NOT_FOUND.message,
|
||||||
|
HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
|
||||||
private fun toNode(row: ResultRow): OrgNodeFlat = OrgNodeFlat(
|
private fun toNode(row: ResultRow): OrgNodeFlat = OrgNodeFlat(
|
||||||
id = row[SysOrgTable.id],
|
id = row[SysOrgTable.id],
|
||||||
@@ -165,8 +130,6 @@ object OrgService {
|
|||||||
}
|
}
|
||||||
return children(null)
|
return children(null)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private data class OrgNodeFlat(
|
private data class OrgNodeFlat(
|
||||||
val id: Uuid,
|
val id: Uuid,
|
||||||
val parentId: Uuid?,
|
val parentId: Uuid?,
|
||||||
@@ -175,54 +138,4 @@ private data class OrgNodeFlat(
|
|||||||
val sort: Int,
|
val sort: Int,
|
||||||
val status: String,
|
val status: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Route.registerOrgRoutes() {
|
|
||||||
authenticate("auth-jwt") {
|
|
||||||
route("/api/system/orgs") {
|
|
||||||
get {
|
|
||||||
call.requirePermission("system:org:view")
|
|
||||||
call.respond(ok(OrgService.tree()))
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:org:create")
|
|
||||||
val request = call.receive<CreateOrgRequest>()
|
|
||||||
runCatching {
|
|
||||||
val id = OrgService.create(request)
|
|
||||||
call.respond(ok(mapOf("id" to id)))
|
|
||||||
OperationLogService.success(call, currentUser, "CREATE", "新增组织", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "CREATE", "新增组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:org:update")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
val request = call.receive<UpdateOrgRequest>()
|
|
||||||
runCatching {
|
|
||||||
OrgService.update(id, request)
|
|
||||||
call.respond(ok<Unit>(message = "更新成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "UPDATE", "更新组织", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete("/{id}") {
|
|
||||||
val start = TimeSource.Monotonic.markNow()
|
|
||||||
val currentUser = call.requirePermission("system:org:delete")
|
|
||||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
|
||||||
runCatching {
|
|
||||||
OrgService.delete(id)
|
|
||||||
call.respond(ok<Unit>(message = "删除成功"))
|
|
||||||
OperationLogService.success(call, currentUser, "DELETE", "删除组织", start.elapsedNow().inWholeMilliseconds)
|
|
||||||
}.onFailure {
|
|
||||||
OperationLogService.fail(call, currentUser, "DELETE", "删除组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+1
-2
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.security
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
import org.mindrot.jbcrypt.BCrypt
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
|
||||||
@@ -7,4 +7,3 @@ object PasswordService {
|
|||||||
|
|
||||||
fun matches(rawPassword: String, passwordHash: String): Boolean = BCrypt.checkpw(rawPassword, passwordHash)
|
fun matches(rawPassword: String, passwordHash: String): Boolean = BCrypt.checkpw(rawPassword, passwordHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.database.system.SysMenuTable
|
||||||
|
import com.bbit.ticket.database.system.SysRoleMenuTable
|
||||||
|
import com.bbit.ticket.database.system.SysRoleTable
|
||||||
|
import com.bbit.ticket.database.system.SysRoleTable.dataScope
|
||||||
|
import com.bbit.ticket.database.system.SysUserRoleTable
|
||||||
|
import com.bbit.ticket.entity.system.CreateRoleRequest
|
||||||
|
import com.bbit.ticket.entity.system.RoleDetail
|
||||||
|
import com.bbit.ticket.entity.system.RoleItem
|
||||||
|
import com.bbit.ticket.entity.common.BizException
|
||||||
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
|
import com.bbit.ticket.entity.common.PageResult
|
||||||
|
import com.bbit.ticket.entity.common.dataScopeLabel
|
||||||
|
import com.bbit.ticket.entity.common.statusLabel
|
||||||
|
import com.bbit.ticket.entity.system.UpdateRoleMenusRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateRoleRequest
|
||||||
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
|
import org.jetbrains.exposed.v1.core.isNull
|
||||||
|
import org.jetbrains.exposed.v1.core.like
|
||||||
|
import org.jetbrains.exposed.v1.core.or
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insertIgnore
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
object RoleService {
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
suspend fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<RoleItem> = dbQuery {
|
||||||
|
var where = SysRoleTable.deletedAt.isNull()
|
||||||
|
if (!keyword.isNullOrBlank()) {
|
||||||
|
where = where and ((SysRoleTable.name like "%$keyword%") or (SysRoleTable.code like "%$keyword%"))
|
||||||
|
}
|
||||||
|
if (!status.isNullOrBlank()) {
|
||||||
|
where = where and (SysRoleTable.status eq status)
|
||||||
|
}
|
||||||
|
val total = SysRoleTable.selectAll().where { where }.count()
|
||||||
|
val rows = SysRoleTable.selectAll().where { where }
|
||||||
|
.orderBy(SysRoleTable.createdAt)
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(((page - 1) * pageSize).toLong())
|
||||||
|
.toList()
|
||||||
|
PageResult(
|
||||||
|
items = rows.map {
|
||||||
|
RoleItem(
|
||||||
|
id = it[SysRoleTable.id].toString(),
|
||||||
|
name = it[SysRoleTable.name],
|
||||||
|
code = it[SysRoleTable.code],
|
||||||
|
description = it[SysRoleTable.description],
|
||||||
|
status = it[SysRoleTable.status],
|
||||||
|
statusLabel = statusLabel(it[SysRoleTable.status]),
|
||||||
|
dataScope = it[dataScope],
|
||||||
|
dataScopeLabel = dataScopeLabel(it[dataScope]),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
total = total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun create(request: CreateRoleRequest): String = dbQuery {
|
||||||
|
if (request.name.trim().isBlank() || request.code.trim().isBlank()) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "角色名称和编码不能为空")
|
||||||
|
}
|
||||||
|
val exists = SysRoleTable.selectAll().where {
|
||||||
|
(SysRoleTable.code eq request.code.trim()) and SysRoleTable.deletedAt.isNull()
|
||||||
|
}.any()
|
||||||
|
if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "角色编码已存在")
|
||||||
|
val inserted = SysRoleTable.insert {
|
||||||
|
it[name] = request.name.trim()
|
||||||
|
it[code] = request.code.trim()
|
||||||
|
it[description] = request.description?.trim()
|
||||||
|
it[status] = request.status
|
||||||
|
it[dataScope] = request.dataScope
|
||||||
|
it[createdAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
inserted[SysRoleTable.id].toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun detail(id: Uuid): RoleDetail = dbQuery {
|
||||||
|
val role = requireRole(id)
|
||||||
|
val menuIds = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.roleId eq id }
|
||||||
|
.map { it[SysRoleMenuTable.menuId].toString() }
|
||||||
|
RoleDetail(
|
||||||
|
id = role[SysRoleTable.id].toString(),
|
||||||
|
name = role[SysRoleTable.name],
|
||||||
|
code = role[SysRoleTable.code],
|
||||||
|
description = role[SysRoleTable.description],
|
||||||
|
status = role[SysRoleTable.status],
|
||||||
|
statusLabel = statusLabel(role[SysRoleTable.status]),
|
||||||
|
dataScope = role[dataScope],
|
||||||
|
dataScopeLabel = dataScopeLabel(role[dataScope]),
|
||||||
|
menuIds = menuIds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(id: Uuid, request: UpdateRoleRequest) = dbQuery {
|
||||||
|
requireRole(id)
|
||||||
|
SysRoleTable.update({ SysRoleTable.id eq id }) {
|
||||||
|
it[name] = request.name.trim()
|
||||||
|
it[description] = request.description?.trim()
|
||||||
|
it[status] = request.status
|
||||||
|
it[dataScope] = request.dataScope
|
||||||
|
it[updatedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(id: Uuid) = dbQuery {
|
||||||
|
val role = requireRole(id)
|
||||||
|
if (role[SysRoleTable.code] == "SUPER_ADMIN") {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "超级管理员角色不可删除")
|
||||||
|
}
|
||||||
|
val inUse = SysUserRoleTable.selectAll().where { SysUserRoleTable.roleId eq id }.any()
|
||||||
|
if (inUse) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "角色已被用户使用,不能删除")
|
||||||
|
}
|
||||||
|
SysRoleTable.update({ SysRoleTable.id eq id }) {
|
||||||
|
it[deletedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateMenus(id: Uuid, request: UpdateRoleMenusRequest) = dbQuery {
|
||||||
|
requireRole(id)
|
||||||
|
val menuIds = request.menuIds.distinct().map { parseUuid(it, "menuId") }
|
||||||
|
if (menuIds.isNotEmpty()) {
|
||||||
|
val validCount = SysMenuTable.selectAll().where {
|
||||||
|
(SysMenuTable.id inList menuIds) and
|
||||||
|
SysMenuTable.deletedAt.isNull() and
|
||||||
|
(SysMenuTable.status eq "ENABLED")
|
||||||
|
}.count()
|
||||||
|
if (validCount != menuIds.size.toLong()) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或禁用菜单")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id }
|
||||||
|
menuIds.forEach { menuId ->
|
||||||
|
SysRoleMenuTable.insertIgnore {
|
||||||
|
it[roleId] = id
|
||||||
|
it[SysRoleMenuTable.menuId] = menuId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireRole(id: Uuid): ResultRow =
|
||||||
|
SysRoleTable.selectAll().where { (SysRoleTable.id eq id) and SysRoleTable.deletedAt.isNull() }.singleOrNull()
|
||||||
|
?: throw BizException(
|
||||||
|
ErrorCode.ROLE_NOT_FOUND.code,
|
||||||
|
ErrorCode.ROLE_NOT_FOUND.message,
|
||||||
|
HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package com.bbit.ticket.service.system
|
||||||
|
|
||||||
|
import com.bbit.ticket.database.system.SysOrgTable
|
||||||
|
import com.bbit.ticket.database.system.SysRoleTable
|
||||||
|
import com.bbit.ticket.database.system.SysUserRoleTable
|
||||||
|
import com.bbit.ticket.database.system.SysUserTable
|
||||||
|
import com.bbit.ticket.database.system.SysUserTable.avatar
|
||||||
|
import com.bbit.ticket.database.system.SysUserTable.email
|
||||||
|
import com.bbit.ticket.database.system.SysUserTable.nickname
|
||||||
|
import com.bbit.ticket.database.system.SysUserTable.phone
|
||||||
|
import com.bbit.ticket.database.system.SysUserTable.realName
|
||||||
|
import com.bbit.ticket.entity.system.CreateUserRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserPasswordRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserRolesRequest
|
||||||
|
import com.bbit.ticket.entity.system.UpdateUserStatusRequest
|
||||||
|
import com.bbit.ticket.entity.system.UserDetailResponse
|
||||||
|
import com.bbit.ticket.entity.system.UserListItem
|
||||||
|
import com.bbit.ticket.entity.common.BizException
|
||||||
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
|
import com.bbit.ticket.entity.common.PageResult
|
||||||
|
import com.bbit.ticket.entity.common.statusLabel
|
||||||
|
import com.bbit.ticket.plugins.dbQuery
|
||||||
|
import com.bbit.ticket.utils.parseUuid
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import org.jetbrains.exposed.v1.core.Op
|
||||||
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
|
import org.jetbrains.exposed.v1.core.isNull
|
||||||
|
import org.jetbrains.exposed.v1.core.like
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insertIgnore
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
object UserService {
|
||||||
|
suspend fun list(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
username: String?,
|
||||||
|
nickname: String?,
|
||||||
|
status: String?,
|
||||||
|
orgId: Uuid?,
|
||||||
|
): PageResult<UserListItem> = dbQuery {
|
||||||
|
val where = buildWhere(username, nickname, status, orgId)
|
||||||
|
val total = SysUserTable.selectAll().where { where }.count()
|
||||||
|
val rows = SysUserTable.selectAll()
|
||||||
|
.where { where }
|
||||||
|
.orderBy(SysUserTable.createdAt)
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(((page - 1) * pageSize).toLong())
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val userIds = rows.map { it[SysUserTable.id] }
|
||||||
|
val roleMap = if (userIds.isEmpty()) {
|
||||||
|
emptyMap()
|
||||||
|
} else {
|
||||||
|
(SysUserRoleTable innerJoin SysRoleTable).selectAll()
|
||||||
|
.where {
|
||||||
|
(SysUserRoleTable.userId inList userIds) and
|
||||||
|
SysRoleTable.deletedAt.isNull()
|
||||||
|
}
|
||||||
|
.groupBy { it[SysUserRoleTable.userId] }
|
||||||
|
.mapValues { entry -> entry.value.map { row -> row[SysRoleTable.code] }.distinct() }
|
||||||
|
}
|
||||||
|
|
||||||
|
PageResult(
|
||||||
|
items = rows.map { row ->
|
||||||
|
UserListItem(
|
||||||
|
id = row[SysUserTable.id].toString(),
|
||||||
|
username = row[SysUserTable.username],
|
||||||
|
nickname = row[SysUserTable.nickname],
|
||||||
|
realName = row[realName],
|
||||||
|
orgId = row[SysUserTable.orgId]?.toString(),
|
||||||
|
status = row[SysUserTable.status],
|
||||||
|
statusLabel = statusLabel(row[SysUserTable.status]),
|
||||||
|
roleCodes = roleMap[row[SysUserTable.id]] ?: emptyList(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
total = total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun create(request: CreateUserRequest): String = dbQuery {
|
||||||
|
val username = request.username.trim()
|
||||||
|
if (username.isBlank() || request.password.isBlank()) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "用户名和密码不能为空")
|
||||||
|
}
|
||||||
|
val existed = SysUserTable.selectAll().where {
|
||||||
|
(SysUserTable.username eq username) and SysUserTable.deletedAt.isNull()
|
||||||
|
}.any()
|
||||||
|
if (existed) {
|
||||||
|
throw BizException(ErrorCode.DATA_CONFLICT.code, "用户名已存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
val orgUuid = request.orgId?.let { parseUuid(it, "orgId") }
|
||||||
|
if (orgUuid != null) {
|
||||||
|
ensureOrgExists(orgUuid)
|
||||||
|
}
|
||||||
|
val now = OffsetDateTime.now()
|
||||||
|
val row = SysUserTable.insert {
|
||||||
|
it[SysUserTable.username] = username
|
||||||
|
it[passwordHash] = PasswordService.hash(request.password)
|
||||||
|
it[nickname] = request.nickname?.trim()
|
||||||
|
it[realName] = request.realName?.trim()
|
||||||
|
it[phone] = request.phone?.trim()
|
||||||
|
it[email] = request.email?.trim()
|
||||||
|
it[avatar] = request.avatar?.trim()
|
||||||
|
it[orgId] = orgUuid
|
||||||
|
it[status] = request.status
|
||||||
|
it[tokenVersion] = 1
|
||||||
|
it[createdAt] = now
|
||||||
|
}
|
||||||
|
row[SysUserTable.id].toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun detail(id: Uuid): UserDetailResponse = dbQuery {
|
||||||
|
val user = requireUser(id)
|
||||||
|
val roleIds = SysUserRoleTable.selectAll().where { SysUserRoleTable.userId eq id }
|
||||||
|
.map { it[SysUserRoleTable.roleId].toString() }
|
||||||
|
UserDetailResponse(
|
||||||
|
id = user[SysUserTable.id].toString(),
|
||||||
|
username = user[SysUserTable.username],
|
||||||
|
nickname = user[nickname],
|
||||||
|
realName = user[realName],
|
||||||
|
phone = user[phone],
|
||||||
|
email = user[email],
|
||||||
|
avatar = user[avatar],
|
||||||
|
orgId = user[SysUserTable.orgId]?.toString(),
|
||||||
|
status = user[SysUserTable.status],
|
||||||
|
statusLabel = statusLabel(user[SysUserTable.status]),
|
||||||
|
roleIds = roleIds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(id: Uuid, request: UpdateUserRequest) = dbQuery {
|
||||||
|
requireUser(id)
|
||||||
|
val orgUuid = request.orgId?.let { parseUuid(it, "orgId") }
|
||||||
|
if (orgUuid != null) {
|
||||||
|
ensureOrgExists(orgUuid)
|
||||||
|
}
|
||||||
|
SysUserTable.update({ SysUserTable.id eq id }) {
|
||||||
|
it[nickname] = request.nickname?.trim()
|
||||||
|
it[realName] = request.realName?.trim()
|
||||||
|
it[phone] = request.phone?.trim()
|
||||||
|
it[email] = request.email?.trim()
|
||||||
|
it[avatar] = request.avatar?.trim()
|
||||||
|
it[orgId] = orgUuid
|
||||||
|
it[updatedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun softDelete(id: Uuid) = dbQuery {
|
||||||
|
if (id.toString() == "00000000-0000-0000-0000-000000000000") {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "系统保留用户不可删除")
|
||||||
|
}
|
||||||
|
requireUser(id)
|
||||||
|
SysUserTable.update({ SysUserTable.id eq id }) {
|
||||||
|
it[deletedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateStatus(id: Uuid, request: UpdateUserStatusRequest) = dbQuery {
|
||||||
|
requireUser(id)
|
||||||
|
SysUserTable.update({ SysUserTable.id eq id }) {
|
||||||
|
it[status] = request.status
|
||||||
|
it[updatedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updatePassword(id: Uuid, request: UpdateUserPasswordRequest) = dbQuery {
|
||||||
|
val user = requireUser(id)
|
||||||
|
if (request.password.isBlank()) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "密码不能为空")
|
||||||
|
}
|
||||||
|
val nextTokenVersion = user[SysUserTable.tokenVersion] + 1
|
||||||
|
SysUserTable.update({ SysUserTable.id eq id }) {
|
||||||
|
it[passwordHash] = PasswordService.hash(request.password)
|
||||||
|
it[tokenVersion] = nextTokenVersion
|
||||||
|
it[updatedAt] = OffsetDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateRoles(id: Uuid, request: UpdateUserRolesRequest) = dbQuery {
|
||||||
|
requireUser(id)
|
||||||
|
val roleIds = request.roleIds.distinct().map { parseUuid(it, "roleId") }
|
||||||
|
if (roleIds.isNotEmpty()) {
|
||||||
|
val validCount = SysRoleTable.selectAll().where {
|
||||||
|
(SysRoleTable.id inList roleIds) and
|
||||||
|
(SysRoleTable.status eq "ENABLED") and
|
||||||
|
SysRoleTable.deletedAt.isNull()
|
||||||
|
}.count()
|
||||||
|
if (validCount != roleIds.size.toLong()) {
|
||||||
|
throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或已禁用角色")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id }
|
||||||
|
roleIds.forEach { roleId ->
|
||||||
|
SysUserRoleTable.insertIgnore {
|
||||||
|
it[userId] = id
|
||||||
|
it[SysUserRoleTable.roleId] = roleId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op<Boolean> {
|
||||||
|
var where: Op<Boolean> = SysUserTable.deletedAt.isNull()
|
||||||
|
if (!username.isNullOrBlank()) {
|
||||||
|
where = where and (SysUserTable.username like "%$username%")
|
||||||
|
}
|
||||||
|
if (!nickname.isNullOrBlank()) {
|
||||||
|
where = where and (SysUserTable.nickname like "%$nickname%")
|
||||||
|
}
|
||||||
|
if (!status.isNullOrBlank()) {
|
||||||
|
where = where and (SysUserTable.status eq status)
|
||||||
|
}
|
||||||
|
if (orgId != null) {
|
||||||
|
where = where and (SysUserTable.orgId eq orgId)
|
||||||
|
}
|
||||||
|
return where
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireUser(id: Uuid) =
|
||||||
|
SysUserTable.selectAll().where { (SysUserTable.id eq id) and SysUserTable.deletedAt.isNull() }.singleOrNull()
|
||||||
|
?: throw BizException(
|
||||||
|
ErrorCode.USER_NOT_FOUND.code,
|
||||||
|
ErrorCode.USER_NOT_FOUND.message,
|
||||||
|
HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun ensureOrgExists(orgId: Uuid) {
|
||||||
|
val exists = SysOrgTable.selectAll().where { (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() }.any()
|
||||||
|
if (!exists) {
|
||||||
|
throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message, HttpStatusCode.BadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.utils
|
||||||
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.utils
|
||||||
|
|
||||||
|
import com.bbit.ticket.entity.common.BizException
|
||||||
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.application.ApplicationCall
|
import io.ktor.server.application.ApplicationCall
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
package com.bbit.ticket.security
|
package com.bbit.ticket.utils
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
import com.bbit.ticket.entity.common.BizException
|
||||||
import com.bbit.ticket.common.ErrorCode
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.application.ApplicationCall
|
import io.ktor.server.application.ApplicationCall
|
||||||
|
|
||||||
+5
-4
@@ -1,9 +1,9 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package com.bbit.ticket.security
|
package com.bbit.ticket.utils
|
||||||
|
|
||||||
import com.bbit.ticket.common.BizException
|
import com.bbit.ticket.entity.common.BizException
|
||||||
import com.bbit.ticket.common.ErrorCode
|
import com.bbit.ticket.entity.common.ErrorCode
|
||||||
import com.bbit.ticket.database.system.SysMenuTable
|
import com.bbit.ticket.database.system.SysMenuTable
|
||||||
import com.bbit.ticket.database.system.SysRoleMenuTable
|
import com.bbit.ticket.database.system.SysRoleMenuTable
|
||||||
import com.bbit.ticket.database.system.SysRoleTable
|
import com.bbit.ticket.database.system.SysRoleTable
|
||||||
@@ -21,6 +21,7 @@ import org.jetbrains.exposed.v1.core.inList
|
|||||||
import org.jetbrains.exposed.v1.core.isNotNull
|
import org.jetbrains.exposed.v1.core.isNotNull
|
||||||
import org.jetbrains.exposed.v1.core.isNull
|
import org.jetbrains.exposed.v1.core.isNull
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
data class CurrentUser(
|
data class CurrentUser(
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.bbit.ticket.common
|
package com.bbit.ticket.utils
|
||||||
|
|
||||||
import io.ktor.server.application.ApplicationCall
|
import io.ktor.server.application.ApplicationCall
|
||||||
import io.ktor.util.AttributeKey
|
import io.ktor.util.AttributeKey
|
||||||
+1
-3
@@ -55,9 +55,7 @@ http.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
if (router.currentRoute.value.path !== '/403') {
|
console.warn('[HTTP] 收到 403 响应, URL:', error.config?.url, 'Message:', backendMessage)
|
||||||
await router.replace('/403')
|
|
||||||
}
|
|
||||||
message.error(backendMessage ?? '无权限访问该资源')
|
message.error(backendMessage ?? '无权限访问该资源')
|
||||||
return Promise.reject(new BizError('403', backendMessage ?? '无权限', traceId))
|
return Promise.reject(new BizError('403', backendMessage ?? '无权限', traceId))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function toOption(menu: MenuNode): MenuOption | null {
|
|||||||
.map((item) => toOption(item))
|
.map((item) => toOption(item))
|
||||||
.filter((item): item is MenuOption => Boolean(item))
|
.filter((item): item is MenuOption => Boolean(item))
|
||||||
|
|
||||||
const key = menu.path || `catalog-${menu.id}`
|
const key = (menu.path && menu.type !== 'CATALOG') ? menu.path : `catalog-${menu.id}`
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
label: menu.title,
|
label: menu.title,
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5180,
|
||||||
open: false,
|
open: false,
|
||||||
proxy: proxyTarget
|
proxy: proxyTarget
|
||||||
? {
|
? {
|
||||||
|
|||||||
Reference in New Issue
Block a user