优化后端框架
This commit is contained in:
@@ -1 +0,0 @@
|
||||
代理商后台入口https://dl.vpiaotong.com/#/login 邀请码:K1N9TP 密码:www.bbitcn.com
|
||||
@@ -45,4 +45,11 @@ dependencies {
|
||||
|
||||
// Redis
|
||||
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.SeedData
|
||||
import com.bbit.ticket.common.ok
|
||||
import com.bbit.ticket.config.AppConfig
|
||||
import com.bbit.ticket.modules.auth.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.entity.common.ok
|
||||
import com.bbit.ticket.bootstrap.AppConfig
|
||||
import com.bbit.ticket.route.system.registerAuthRoutes
|
||||
import com.bbit.ticket.plugins.configureCors
|
||||
import com.bbit.ticket.plugins.configureDatabase
|
||||
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.configureStatusPages
|
||||
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 io.ktor.server.application.Application
|
||||
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
|
||||
|
||||
@@ -32,9 +32,6 @@ object DatabaseInitializer {
|
||||
SysApiAccessLogTable,
|
||||
)
|
||||
// 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。
|
||||
dbQuery {
|
||||
MigrationUtils.statementsRequiredForDatabaseMigration(*tables, withLogs = true)
|
||||
}
|
||||
transaction {
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||
*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.SysUserTable
|
||||
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.eq
|
||||
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
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.common
|
||||
package com.bbit.ticket.entity.common
|
||||
|
||||
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) {
|
||||
"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) {
|
||||
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
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.common
|
||||
package com.bbit.ticket.entity.common
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
import com.bbit.ticket.common.TraceIdKey
|
||||
import com.bbit.ticket.utils.TraceIdKey
|
||||
import com.bbit.ticket.database.system.SysApiAccessLogTable
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.createApplicationPlugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.HttpMethod
|
||||
import io.ktor.server.application.Application
|
||||
@@ -9,14 +9,7 @@ import io.ktor.server.plugins.cors.routing.CORS
|
||||
|
||||
fun Application.configureCors() {
|
||||
install(CORS) {
|
||||
if (AppConfig.cors.allowedHosts.contains("*")) {
|
||||
anyHost()
|
||||
} else {
|
||||
AppConfig.cors.allowedHosts.forEach { allowedHost ->
|
||||
allowHost(allowedHost, schemes = listOf("http", "https"))
|
||||
}
|
||||
}
|
||||
|
||||
anyHost()
|
||||
allowMethod(HttpMethod.Get)
|
||||
allowMethod(HttpMethod.Post)
|
||||
allowMethod(HttpMethod.Put)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.HikariDataSource
|
||||
import io.ktor.server.application.Application
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bbit.ticket.plugins
|
||||
|
||||
import com.bbit.ticket.utils.TraceIdKey
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.plugins.calllogging.CallLogging
|
||||
@@ -10,7 +11,7 @@ import org.slf4j.event.Level
|
||||
fun Application.configureLogging() {
|
||||
install(CallLogging) {
|
||||
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") }
|
||||
format { call ->
|
||||
val status = call.response.status()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.bbit.ticket.plugins
|
||||
|
||||
import com.bbit.ticket.config.AppConfig
|
||||
import com.bbit.ticket.bootstrap.AppConfig
|
||||
import io.ktor.server.application.Application
|
||||
import org.redisson.Redisson
|
||||
import org.redisson.api.RedissonClient
|
||||
|
||||
@@ -2,10 +2,10 @@ package com.bbit.ticket.plugins
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.bbit.ticket.common.ErrorCode
|
||||
import com.bbit.ticket.common.fail
|
||||
import com.bbit.ticket.common.traceIdOrNull
|
||||
import com.bbit.ticket.config.AppConfig
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.fail
|
||||
import com.bbit.ticket.utils.traceIdOrNull
|
||||
import com.bbit.ticket.bootstrap.AppConfig
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.bbit.ticket.plugins
|
||||
|
||||
import com.bbit.ticket.common.BizException
|
||||
import com.bbit.ticket.common.ErrorCode
|
||||
import com.bbit.ticket.common.fail
|
||||
import com.bbit.ticket.common.traceIdOrNull
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.fail
|
||||
import com.bbit.ticket.utils.traceIdOrNull
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.createApplicationPlugin
|
||||
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.security.requireCurrentUser
|
||||
|
||||
import com.bbit.ticket.entity.common.ok
|
||||
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.request.receive
|
||||
import io.ktor.server.response.respond
|
||||
@@ -12,6 +15,7 @@ import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.route
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
|
||||
fun Route.registerAuthRoutes() {
|
||||
route("/api/auth") {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-22
@@ -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.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
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.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.security.CurrentUser
|
||||
import com.bbit.ticket.security.JwtService
|
||||
import com.bbit.ticket.security.PasswordService
|
||||
import com.bbit.ticket.utils.CurrentUser
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
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.update
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
object AuthService {
|
||||
@@ -186,19 +190,19 @@ object AuthService {
|
||||
|
||||
return build(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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -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.algorithms.Algorithm
|
||||
import com.bbit.ticket.config.AppConfig
|
||||
import com.bbit.ticket.bootstrap.AppConfig
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@@ -33,4 +33,3 @@ object JwtService {
|
||||
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.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.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.SortOrder
|
||||
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.or
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
|
||||
@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,
|
||||
)
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
object LogsQueryService {
|
||||
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.utils.traceIdOrNull
|
||||
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.server.application.ApplicationCall
|
||||
import io.ktor.server.request.httpMethod
|
||||
import io.ktor.server.request.path
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
object OperationLogService {
|
||||
suspend fun success(call: ApplicationCall, currentUser: CurrentUser?, operationType: String, operationName: String, costMs: Long) {
|
||||
+23
-110
@@ -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.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.security.requirePermission
|
||||
import com.bbit.ticket.utils.parseUuid
|
||||
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
|
||||
@@ -31,38 +21,9 @@ 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.ExperimentalUuidApi
|
||||
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 {
|
||||
suspend fun tree(): List<OrgTreeNode> = dbQuery {
|
||||
val rows = SysOrgTable.selectAll()
|
||||
@@ -137,7 +98,11 @@ object OrgService {
|
||||
|
||||
private fun requireOrg(id: Uuid): ResultRow =
|
||||
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(
|
||||
id = row[SysOrgTable.id],
|
||||
@@ -165,64 +130,12 @@ object OrgService {
|
||||
}
|
||||
return children(null)
|
||||
}
|
||||
}
|
||||
|
||||
private data class OrgNodeFlat(
|
||||
val id: Uuid,
|
||||
val parentId: Uuid?,
|
||||
val name: String,
|
||||
val code: String,
|
||||
val sort: Int,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private data class OrgNodeFlat(
|
||||
val id: Uuid,
|
||||
val parentId: Uuid?,
|
||||
val name: String,
|
||||
val code: String,
|
||||
val sort: Int,
|
||||
val status: String,
|
||||
)
|
||||
}
|
||||
+1
-2
@@ -1,4 +1,4 @@
|
||||
package com.bbit.ticket.security
|
||||
package com.bbit.ticket.service.system
|
||||
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
|
||||
@@ -7,4 +7,3 @@ object PasswordService {
|
||||
|
||||
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.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.server.application.ApplicationCall
|
||||
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.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import io.ktor.http.HttpStatusCode
|
||||
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.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.database.system.SysMenuTable
|
||||
import com.bbit.ticket.database.system.SysRoleMenuTable
|
||||
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.isNull
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
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.util.AttributeKey
|
||||
+1
-3
@@ -55,9 +55,7 @@ http.interceptors.response.use(
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
if (router.currentRoute.value.path !== '/403') {
|
||||
await router.replace('/403')
|
||||
}
|
||||
console.warn('[HTTP] 收到 403 响应, URL:', error.config?.url, 'Message:', backendMessage)
|
||||
message.error(backendMessage ?? '无权限访问该资源')
|
||||
return Promise.reject(new BizError('403', backendMessage ?? '无权限', traceId))
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ function toOption(menu: MenuNode): MenuOption | null {
|
||||
.map((item) => toOption(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 {
|
||||
key,
|
||||
label: menu.title,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
port: 5180,
|
||||
open: false,
|
||||
proxy: proxyTarget
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user