diff --git a/server/build.gradle.kts b/server/build.gradle.kts index c5af1da..aab2c7f 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -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") } diff --git a/server/src/main/kotlin/com/bbit/platform/Application.kt b/server/src/main/kotlin/com/bbit/platform/Application.kt deleted file mode 100644 index f6f33ea..0000000 --- a/server/src/main/kotlin/com/bbit/platform/Application.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.bbit.platform - -import com.bbit.platform.bootstrap.DatabaseInitializer -import com.bbit.platform.bootstrap.SeedData -import com.bbit.platform.common.ok -import com.bbit.platform.config.AppConfig -import com.bbit.platform.modules.auth.registerAuthRoutes -import com.bbit.platform.modules.logs.registerLogsQueryRoutes -import com.bbit.platform.modules.system.dict.registerDictRoutes -import com.bbit.platform.modules.system.menu.registerMenuRoutes -import com.bbit.platform.modules.system.org.registerOrgRoutes -import com.bbit.platform.modules.system.role.registerRoleRoutes -import com.bbit.platform.modules.system.user.registerUserRoutes -import com.bbit.platform.plugins.configureCors -import com.bbit.platform.plugins.configureDatabase -import com.bbit.platform.plugins.configureLogging -import com.bbit.platform.plugins.configureApiAccessLog -import com.bbit.platform.plugins.configureRedis -import com.bbit.platform.plugins.configureSecurity -import com.bbit.platform.plugins.configureSerialization -import com.bbit.platform.plugins.configureStatusPages -import com.bbit.platform.plugins.configureTrace -import kotlinx.coroutines.runBlocking -import io.ktor.server.application.Application -import io.ktor.server.netty.EngineMain -import io.ktor.server.response.respond -import io.ktor.server.routing.get -import io.ktor.server.routing.routing - -fun main(args: Array) { - EngineMain.main(args) -} - -fun Application.module() { - AppConfig.init(environment) - - configureTrace() - configureSerialization() - configureStatusPages() - configureLogging() - configureApiAccessLog() - configureCors() - configureSecurity() - configureDatabase() - configureRedis() - runBlocking { - DatabaseInitializer.initialize() - SeedData.seed() - } - - routing { - get("/health") { - call.respond(ok(mapOf("status" to "UP", "service" to AppConfig.app.name))) - } - registerAuthRoutes() - registerUserRoutes() - registerOrgRoutes() - registerRoleRoutes() - registerMenuRoutes() - registerDictRoutes() - registerLogsQueryRoutes() - } -} diff --git a/server/src/main/kotlin/com/bbit/platform/modules/auth/AuthService.kt b/server/src/main/kotlin/com/bbit/platform/modules/auth/AuthService.kt deleted file mode 100644 index b5097f0..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/auth/AuthService.kt +++ /dev/null @@ -1,204 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.auth - -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.database.system.SysMenuTable -import com.bbit.platform.database.system.SysRoleMenuTable -import com.bbit.platform.database.system.SysRoleTable -import com.bbit.platform.database.system.SysUserRoleTable -import com.bbit.platform.database.system.SysUserTable -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.security.CurrentUser -import com.bbit.platform.security.JwtService -import com.bbit.platform.security.PasswordService -import io.ktor.http.HttpStatusCode -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.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime -import kotlin.uuid.Uuid - -object AuthService { - suspend fun login(request: LoginRequest, loginIp: String?): LoginResponse { - val username = request.username.trim() - if (username.isBlank() || request.password.isBlank()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "用户名和密码不能为空", HttpStatusCode.BadRequest) - } - - val user = dbQuery { - SysUserTable.selectAll() - .where { (SysUserTable.username eq username) and SysUserTable.deletedAt.isNull() } - .singleOrNull() - } ?: throw BizException( - ErrorCode.USERNAME_OR_PASSWORD_INVALID.code, - ErrorCode.USERNAME_OR_PASSWORD_INVALID.message, - HttpStatusCode.BadRequest, - ) - - if (!PasswordService.matches(request.password, user[SysUserTable.passwordHash])) { - throw BizException( - ErrorCode.USERNAME_OR_PASSWORD_INVALID.code, - ErrorCode.USERNAME_OR_PASSWORD_INVALID.message, - HttpStatusCode.BadRequest, - ) - } - - if (user[SysUserTable.status] != "ENABLED") { - throw BizException(ErrorCode.USER_DISABLED.code, ErrorCode.USER_DISABLED.message, HttpStatusCode.BadRequest) - } - - val userId = user[SysUserTable.id] - val roleCodes = dbQuery { - (SysUserRoleTable innerJoin SysRoleTable) - .selectAll() - .where { - (SysUserRoleTable.userId eq userId) and - SysRoleTable.deletedAt.isNull() and - (SysRoleTable.status eq "ENABLED") - } - .map { it[SysRoleTable.code] } - } - - val (accessToken, expiresIn) = JwtService.issueAccessToken( - userId = userId.toString(), - username = user[SysUserTable.username], - orgId = user[SysUserTable.orgId]?.toString(), - roles = roleCodes, - tokenVersion = user[SysUserTable.tokenVersion], - ) - - dbQuery { - SysUserTable.update({ SysUserTable.id eq userId }) { - it[lastLoginAt] = OffsetDateTime.now() - it[lastLoginIp] = loginIp - it[updatedAt] = OffsetDateTime.now() - } - } - - return LoginResponse(accessToken = accessToken, expiresIn = expiresIn) - } - - suspend fun me(currentUser: CurrentUser): MeResponse { - val userRow = dbQuery { - SysUserTable.selectAll() - .where { (SysUserTable.id eq currentUser.id) and SysUserTable.deletedAt.isNull() } - .single() - } - - val allMenus = loadMenusForUser(currentUser) - val menuTree = buildMenuTree(allMenus) - val permissions = allMenus.mapNotNull { it.permission }.toSet() - - return MeResponse( - user = CurrentUserProfile( - id = currentUser.id.toString(), - username = userRow[SysUserTable.username], - nickname = userRow[SysUserTable.nickname], - realName = userRow[SysUserTable.realName], - orgId = userRow[SysUserTable.orgId]?.toString(), - status = userRow[SysUserTable.status], - ), - menus = menuTree, - permissions = permissions, - ) - } - - private suspend fun loadMenusForUser(currentUser: CurrentUser): List { - val rows = if (currentUser.isSuperAdmin) { - dbQuery { - SysMenuTable.selectAll() - .where { SysMenuTable.deletedAt.isNull() and (SysMenuTable.status eq "ENABLED") } - .toList() - } - } else { - val roleIds = dbQuery { - (SysUserRoleTable innerJoin SysRoleTable) - .selectAll() - .where { - (SysUserRoleTable.userId eq currentUser.id) and - SysRoleTable.deletedAt.isNull() and - (SysRoleTable.status eq "ENABLED") - } - .map { it[SysRoleTable.id] } - } - - if (roleIds.isEmpty()) { - emptyList() - } else { - dbQuery { - (SysRoleMenuTable innerJoin SysMenuTable) - .selectAll() - .where { - (SysRoleMenuTable.roleId inList roleIds) and - SysMenuTable.deletedAt.isNull() and - (SysMenuTable.status eq "ENABLED") - } - .distinct() - .toList() - } - } - } - - return rows.map { row -> - MenuFlat( - id = row[SysMenuTable.id], - parentId = row[SysMenuTable.parentId], - type = row[SysMenuTable.type], - title = row[SysMenuTable.title], - name = row[SysMenuTable.name], - path = row[SysMenuTable.path], - component = row[SysMenuTable.component], - icon = row[SysMenuTable.icon], - permission = row[SysMenuTable.permission], - sort = row[SysMenuTable.sort], - visible = row[SysMenuTable.visible], - keepAlive = row[SysMenuTable.keepAlive], - ) - }.sortedWith(compareBy { it.sort }.thenBy { it.id.toString() }) - } - - private fun buildMenuTree(flatMenus: List): List { - val parentMap = flatMenus.groupBy { it.parentId } - - fun build(parentId: Uuid?): List = - (parentMap[parentId] ?: emptyList()).map { menu -> - MenuNode( - id = menu.id.toString(), - parentId = menu.parentId?.toString(), - type = 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, - children = build(menu.id), - ) - } - - 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, -) diff --git a/server/src/main/kotlin/com/bbit/platform/modules/logs/LogsQueryModule.kt b/server/src/main/kotlin/com/bbit/platform/modules/logs/LogsQueryModule.kt deleted file mode 100644 index 5b37a1e..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/logs/LogsQueryModule.kt +++ /dev/null @@ -1,145 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.logs - -import com.bbit.platform.common.PageResult -import com.bbit.platform.common.formatDateTime -import com.bbit.platform.common.ok -import com.bbit.platform.common.queryInt -import com.bbit.platform.common.queryString -import com.bbit.platform.database.system.SysApiAccessLogTable -import com.bbit.platform.database.system.SysOperationLogTable -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.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 -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, -) - -object LogsQueryService { - suspend fun operationLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = dbQuery { - var where: Op = Op.TRUE - if (!keyword.isNullOrBlank()) { - where = where and ((SysOperationLogTable.username like "%$keyword%") or (SysOperationLogTable.requestPath like "%$keyword%")) - } - if (!status.isNullOrBlank()) where = where and (SysOperationLogTable.status eq status) - val total = SysOperationLogTable.selectAll().where { where }.count() - val rows = SysOperationLogTable.selectAll().where { where } - .orderBy(SysOperationLogTable.createdAt, SortOrder.DESC) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - PageResult( - items = rows.map { - OperationLogItem( - id = it[SysOperationLogTable.id].toString(), - traceId = it[SysOperationLogTable.traceId], - username = it[SysOperationLogTable.username], - operationType = it[SysOperationLogTable.operationType], - operationName = it[SysOperationLogTable.operationName], - httpMethod = it[SysOperationLogTable.httpMethod], - requestPath = it[SysOperationLogTable.requestPath], - status = it[SysOperationLogTable.status], - errorMessage = it[SysOperationLogTable.errorMessage], - costMs = it[SysOperationLogTable.costMs], - createdAt = formatDateTime(it[SysOperationLogTable.createdAt]) ?: "", - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } - - suspend fun apiAccessLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = dbQuery { - var where: Op = Op.TRUE - if (!keyword.isNullOrBlank()) { - where = where and ((SysApiAccessLogTable.appName like "%$keyword%") or (SysApiAccessLogTable.requestPath like "%$keyword%")) - } - if (!status.isNullOrBlank()) where = where and (SysApiAccessLogTable.status eq status) - val total = SysApiAccessLogTable.selectAll().where { where }.count() - val rows = SysApiAccessLogTable.selectAll().where { where } - .orderBy(SysApiAccessLogTable.createdAt, SortOrder.DESC) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - PageResult( - items = rows.map { - ApiAccessLogItem( - id = it[SysApiAccessLogTable.id].toString(), - traceId = it[SysApiAccessLogTable.traceId], - appKey = it[SysApiAccessLogTable.appKey], - appName = it[SysApiAccessLogTable.appName], - httpMethod = it[SysApiAccessLogTable.httpMethod], - requestPath = it[SysApiAccessLogTable.requestPath], - responseCode = it[SysApiAccessLogTable.responseCode], - status = it[SysApiAccessLogTable.status], - errorMessage = it[SysApiAccessLogTable.errorMessage], - costMs = it[SysApiAccessLogTable.costMs], - createdAt = formatDateTime(it[SysApiAccessLogTable.createdAt]) ?: "", - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } -} - -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")))) - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/platform/modules/logs/OperationLogService.kt b/server/src/main/kotlin/com/bbit/platform/modules/logs/OperationLogService.kt deleted file mode 100644 index 47267ad..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/logs/OperationLogService.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.logs - -import com.bbit.platform.common.traceIdOrNull -import com.bbit.platform.database.system.SysOperationLogTable -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.security.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 - -object OperationLogService { - suspend fun success(call: ApplicationCall, currentUser: CurrentUser?, operationType: String, operationName: String, costMs: Long) { - save(call, currentUser, operationType, operationName, "SUCCESS", null, costMs) - } - - suspend fun fail( - call: ApplicationCall, - currentUser: CurrentUser?, - operationType: String, - operationName: String, - errorMessage: String?, - costMs: Long, - ) { - save(call, currentUser, operationType, operationName, "FAIL", errorMessage?.take(500), costMs) - } - - private suspend fun save( - call: ApplicationCall, - currentUser: CurrentUser?, - operationType: String, - operationName: String, - status: String, - errorMessage: String?, - costMs: Long, - ) = dbQuery { - SysOperationLogTable.insert { - it[traceId] = call.traceIdOrNull() - it[userId] = currentUser?.id - it[username] = currentUser?.username - it[orgId] = currentUser?.orgId - it[SysOperationLogTable.operationType] = operationType - it[SysOperationLogTable.operationName] = operationName - it[httpMethod] = call.request.httpMethod.value - it[requestPath] = call.request.path().take(255) - it[requestParams] = call.request.queryParameters.formUrlEncode().take(1000) - it[ip] = call.request.local.remoteHost.take(64) - it[userAgent] = call.request.headers["User-Agent"]?.take(255) - it[SysOperationLogTable.status] = status - it[SysOperationLogTable.errorMessage] = errorMessage - it[SysOperationLogTable.costMs] = costMs - it[createdAt] = OffsetDateTime.now() - } - } -} diff --git a/server/src/main/kotlin/com/bbit/platform/modules/system/dict/DictModule.kt b/server/src/main/kotlin/com/bbit/platform/modules/system/dict/DictModule.kt deleted file mode 100644 index a7713fc..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/system/dict/DictModule.kt +++ /dev/null @@ -1,338 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.system.dict - -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.common.PageResult -import com.bbit.platform.common.ok -import com.bbit.platform.common.parseUuid -import com.bbit.platform.common.queryInt -import com.bbit.platform.common.queryString -import com.bbit.platform.common.statusLabel -import com.bbit.platform.database.system.SysDictItemTable -import com.bbit.platform.database.system.SysDictTypeTable -import com.bbit.platform.modules.logs.OperationLogService -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.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 = 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 = 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() - 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() - runCatching { - DictService.updateType(id, request) - call.respond(ok(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(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() - 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() - runCatching { - DictService.updateItem(id, request) - call.respond(ok(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(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除字典项", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除字典项", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/platform/modules/system/menu/MenuModule.kt b/server/src/main/kotlin/com/bbit/platform/modules/system/menu/MenuModule.kt deleted file mode 100644 index 75cba6c..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/system/menu/MenuModule.kt +++ /dev/null @@ -1,281 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.system.menu - -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.common.ok -import com.bbit.platform.common.parseUuid -import com.bbit.platform.common.menuTypeLabel -import com.bbit.platform.common.statusLabel -import com.bbit.platform.database.system.SysMenuTable -import com.bbit.platform.database.system.SysRoleMenuTable -import com.bbit.platform.modules.logs.OperationLogService -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.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.deleteWhere -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 = 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 = 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): List { - val grouped = items.groupBy { it.parentId } - fun children(parentId: Uuid?): List = - (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() - 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() - runCatching { - MenuService.update(id, request) - call.respond(ok(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(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除菜单", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除菜单", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/platform/modules/system/org/OrgModule.kt b/server/src/main/kotlin/com/bbit/platform/modules/system/org/OrgModule.kt deleted file mode 100644 index 4d2c14a..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/system/org/OrgModule.kt +++ /dev/null @@ -1,228 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.system.org - -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.common.ok -import com.bbit.platform.common.parseUuid -import com.bbit.platform.common.statusLabel -import com.bbit.platform.database.system.SysOrgTable -import com.bbit.platform.database.system.SysUserTable -import com.bbit.platform.modules.logs.OperationLogService -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.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 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 = 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 = dbQuery { - val rows = SysOrgTable.selectAll() - .where { SysOrgTable.deletedAt.isNull() } - .orderBy(SysOrgTable.sort) - .toList() - val nodes = rows.map(::toNode) - buildTree(nodes) - } - - suspend fun create(request: CreateOrgRequest): String = dbQuery { - val code = request.code.trim() - if (code.isBlank() || request.name.trim().isBlank()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "组织名称和编码不能为空") - } - val exists = SysOrgTable.selectAll().where { - (SysOrgTable.code eq code) and SysOrgTable.deletedAt.isNull() - }.any() - if (exists) { - throw BizException(ErrorCode.DATA_CONFLICT.code, "组织编码已存在") - } - val parentId = request.parentId?.let { parseUuid(it, "parentId") } - if (parentId != null) requireOrg(parentId) - val inserted = SysOrgTable.insert { - it[SysOrgTable.parentId] = parentId - it[name] = request.name.trim() - it[SysOrgTable.code] = code - it[sort] = request.sort - it[status] = request.status - it[createdAt] = OffsetDateTime.now() - } - inserted[SysOrgTable.id].toString() - } - - suspend fun update(id: Uuid, request: UpdateOrgRequest) = dbQuery { - requireOrg(id) - val parentId = request.parentId?.let { parseUuid(it, "parentId") } - if (parentId == id) { - throw BizException(ErrorCode.BAD_REQUEST.code, "上级组织不能选择自身") - } - if (parentId != null) requireOrg(parentId) - SysOrgTable.update({ SysOrgTable.id eq id }) { - it[SysOrgTable.parentId] = parentId - it[name] = request.name.trim() - it[sort] = request.sort - it[status] = request.status - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun delete(id: Uuid) = dbQuery { - val org = requireOrg(id) - if (org[SysOrgTable.code] == "DEFAULT_ORG") { - throw BizException(ErrorCode.BAD_REQUEST.code, "默认组织不可删除") - } - val hasChildren = SysOrgTable.selectAll() - .where { (SysOrgTable.parentId eq id) and SysOrgTable.deletedAt.isNull() } - .any() - if (hasChildren) { - throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在子组织,不能删除") - } - val hasUsers = SysUserTable.selectAll() - .where { (SysUserTable.orgId eq id) and SysUserTable.deletedAt.isNull() } - .any() - if (hasUsers) { - throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在用户,不能删除") - } - SysOrgTable.update({ SysOrgTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - } - - 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) - - private fun toNode(row: ResultRow): OrgNodeFlat = OrgNodeFlat( - id = row[SysOrgTable.id], - parentId = row[SysOrgTable.parentId], - name = row[SysOrgTable.name], - code = row[SysOrgTable.code], - sort = row[SysOrgTable.sort], - status = row[SysOrgTable.status], - ) - - private fun buildTree(nodes: List): List { - val byParent = nodes.groupBy { it.parentId } - fun children(parentId: Uuid?): List = - (byParent[parentId] ?: emptyList()).sortedBy { it.sort }.map { item -> - OrgTreeNode( - id = item.id.toString(), - parentId = item.parentId?.toString(), - name = item.name, - code = item.code, - sort = item.sort, - status = item.status, - statusLabel = statusLabel(item.status), - children = children(item.id), - ) - } - 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() - 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() - runCatching { - OrgService.update(id, request) - call.respond(ok(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(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除组织", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除组织", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/platform/modules/system/role/RoleModule.kt b/server/src/main/kotlin/com/bbit/platform/modules/system/role/RoleModule.kt deleted file mode 100644 index 5fe4f5e..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/system/role/RoleModule.kt +++ /dev/null @@ -1,285 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.system.role - -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.common.PageResult -import com.bbit.platform.common.ok -import com.bbit.platform.common.parseUuid -import com.bbit.platform.common.queryInt -import com.bbit.platform.common.queryString -import com.bbit.platform.common.dataScopeLabel -import com.bbit.platform.common.statusLabel -import com.bbit.platform.database.system.SysMenuTable -import com.bbit.platform.database.system.SysRoleMenuTable -import com.bbit.platform.database.system.SysRoleTable -import com.bbit.platform.database.system.SysUserRoleTable -import com.bbit.platform.modules.logs.OperationLogService -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.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, -) - -@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) - -object RoleService { - suspend fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = 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() - 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() - runCatching { - RoleService.update(id, request) - call.respond(ok(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(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() - runCatching { - RoleService.updateMenus(id, request) - call.respond(ok(message = "菜单分配成功")) - OperationLogService.success(call, currentUser, "ASSIGN_MENU", "分配角色菜单", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "ASSIGN_MENU", "分配角色菜单", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/platform/modules/system/user/UserModule.kt b/server/src/main/kotlin/com/bbit/platform/modules/system/user/UserModule.kt deleted file mode 100644 index c9a04dc..0000000 --- a/server/src/main/kotlin/com/bbit/platform/modules/system/user/UserModule.kt +++ /dev/null @@ -1,423 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.platform.modules.system.user - -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.common.PageResult -import com.bbit.platform.common.ok -import com.bbit.platform.common.parseUuid -import com.bbit.platform.common.queryInt -import com.bbit.platform.common.queryString -import com.bbit.platform.common.statusLabel -import com.bbit.platform.database.system.SysOrgTable -import com.bbit.platform.database.system.SysRoleTable -import com.bbit.platform.database.system.SysUserRoleTable -import com.bbit.platform.database.system.SysUserTable -import com.bbit.platform.modules.logs.OperationLogService -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.security.PasswordService -import com.bbit.platform.security.requireCurrentUser -import com.bbit.platform.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.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.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, -) - -@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, -) - -@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) - -object UserService { - suspend fun list( - page: Int, - pageSize: Int, - username: String?, - nickname: String?, - status: String?, - orgId: Uuid?, - ): PageResult = 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 { - var where: Op = 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() - 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() - runCatching { - UserService.update(id, request) - call.respond(ok(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(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() - runCatching { - UserService.updateStatus(id, request) - call.respond(ok(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() - runCatching { - UserService.updatePassword(id, request) - call.respond(ok(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() - runCatching { - UserService.updateRoles(id, request) - call.respond(ok(message = "角色分配成功")) - OperationLogService.success(call, currentUser, "ASSIGN_ROLE", "分配用户角色", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "ASSIGN_ROLE", "分配用户角色", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/ticket/Application.kt b/server/src/main/kotlin/com/bbit/ticket/Application.kt new file mode 100644 index 0000000..c7f3709 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/Application.kt @@ -0,0 +1,63 @@ +package com.bbit.ticket + +import com.bbit.ticket.bootstrap.DatabaseInitializer +import com.bbit.ticket.bootstrap.SeedData +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 +import com.bbit.ticket.plugins.configureApiAccessLog +import com.bbit.ticket.plugins.configureRedis +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 +import io.ktor.server.response.respond +import io.ktor.server.routing.get +import io.ktor.server.routing.routing + +fun main(args: Array) { + EngineMain.main(args) +} + +fun Application.module() { + AppConfig.init(environment) + + configureTrace() + configureSerialization() + configureStatusPages() + configureLogging() + configureApiAccessLog() + configureCors() + configureSecurity() + configureDatabase() + configureRedis() + runBlocking { + DatabaseInitializer.initialize() + SeedData.seed() + } + + routing { + get("/health") { + call.respond(ok(mapOf("status" to "UP", "service" to AppConfig.app.name))) + } + registerAuthRoutes() + registerUserRoutes() + registerOrgRoutes() + registerRoleRoutes() + registerMenuRoutes() + registerDictRoutes() + registerLogsQueryRoutes() + } +} diff --git a/server/src/main/kotlin/com/bbit/platform/config/AppConfig.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/AppConfig.kt similarity index 98% rename from server/src/main/kotlin/com/bbit/platform/config/AppConfig.kt rename to server/src/main/kotlin/com/bbit/ticket/bootstrap/AppConfig.kt index f97b7ab..1fe8dfc 100644 --- a/server/src/main/kotlin/com/bbit/platform/config/AppConfig.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/AppConfig.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.config +package com.bbit.ticket.bootstrap import io.ktor.server.application.ApplicationEnvironment @@ -91,4 +91,4 @@ object AppConfig { private fun long(environment: ApplicationEnvironment, path: String, default: Long): Long = string(environment, path, default.toString()).toLong() -} +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/platform/bootstrap/DatabaseInitializer.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt similarity index 61% rename from server/src/main/kotlin/com/bbit/platform/bootstrap/DatabaseInitializer.kt rename to server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt index b48a59b..d86f25c 100644 --- a/server/src/main/kotlin/com/bbit/platform/bootstrap/DatabaseInitializer.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt @@ -1,17 +1,16 @@ -package com.bbit.platform.bootstrap +package com.bbit.ticket.bootstrap -import com.bbit.platform.database.system.SysApiAccessLogTable -import com.bbit.platform.database.system.SysDictItemTable -import com.bbit.platform.database.system.SysDictTypeTable -import com.bbit.platform.database.system.SysMenuTable -import com.bbit.platform.database.system.SysOperationLogTable -import com.bbit.platform.database.system.SysOrgTable -import com.bbit.platform.database.system.SysRoleMenuTable -import com.bbit.platform.database.system.SysRoleTable -import com.bbit.platform.database.system.SysUserRoleTable -import com.bbit.platform.database.system.SysUserTable -import com.bbit.platform.plugins.dbQuery -import org.jetbrains.exposed.v1.jdbc.SchemaUtils +import com.bbit.ticket.database.system.SysApiAccessLogTable +import com.bbit.ticket.database.system.SysDictItemTable +import com.bbit.ticket.database.system.SysDictTypeTable +import com.bbit.ticket.database.system.SysMenuTable +import com.bbit.ticket.database.system.SysOperationLogTable +import com.bbit.ticket.database.system.SysOrgTable +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.plugins.dbQuery import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils import org.slf4j.LoggerFactory @@ -33,9 +32,6 @@ object DatabaseInitializer { SysApiAccessLogTable, ) // 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。 - dbQuery { - MigrationUtils.statementsRequiredForDatabaseMigration(*tables, withLogs = true) - } transaction { val statements = MigrationUtils.statementsRequiredForDatabaseMigration( *tables, diff --git a/server/src/main/kotlin/com/bbit/platform/bootstrap/SeedData.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/platform/bootstrap/SeedData.kt rename to server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt index 19684ca..bd28612 100644 --- a/server/src/main/kotlin/com/bbit/platform/bootstrap/SeedData.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt @@ -1,17 +1,17 @@ @file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package com.bbit.platform.bootstrap +package com.bbit.ticket.bootstrap -import com.bbit.platform.database.system.SysDictItemTable -import com.bbit.platform.database.system.SysDictTypeTable -import com.bbit.platform.database.system.SysMenuTable -import com.bbit.platform.database.system.SysOrgTable -import com.bbit.platform.database.system.SysRoleMenuTable -import com.bbit.platform.database.system.SysRoleTable -import com.bbit.platform.database.system.SysUserRoleTable -import com.bbit.platform.database.system.SysUserTable -import com.bbit.platform.plugins.dbQuery -import com.bbit.platform.security.PasswordService +import com.bbit.ticket.database.system.SysDictItemTable +import com.bbit.ticket.database.system.SysDictTypeTable +import com.bbit.ticket.database.system.SysMenuTable +import com.bbit.ticket.database.system.SysOrgTable +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.plugins.dbQuery +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 diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/DictDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/DictDao.kt new file mode 100644 index 0000000..1f24746 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/DictDao.kt @@ -0,0 +1,181 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.dao.system + +import com.bbit.ticket.database.system.SysDictItemTable +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 io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.v1.core.Op +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 DictDao { + fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult { + val where = buildTypeWhere(keyword) + val total = SysDictTypeTable.selectAll().where { where }.count() + val rows = SysDictTypeTable.selectAll().where { where } + .orderBy(SysDictTypeTable.createdAt) + .limit(pageSize) + .offset(pageOffset(page, pageSize)) + .toList() + return PageResult( + items = rows.map { it.toDictTypeItem() }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + fun typeCodeExists(code: String): Boolean = + SysDictTypeTable.selectAll().where { + (SysDictTypeTable.code eq code) and SysDictTypeTable.deletedAt.isNull() + }.any() + + fun createType(request: CreateDictTypeRequest): String { + val inserted = SysDictTypeTable.insert { + it[SysDictTypeTable.code] = request.code.trim() + it[SysDictTypeTable.name] = request.name.trim() + it[SysDictTypeTable.status] = request.status + it[SysDictTypeTable.remark] = request.remark.trimToNull() + it[SysDictTypeTable.createdAt] = OffsetDateTime.now() + } + return inserted[SysDictTypeTable.id].toString() + } + + fun updateType(id: Uuid, request: UpdateDictTypeRequest) { + SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { + it[SysDictTypeTable.name] = request.name.trim() + it[SysDictTypeTable.status] = request.status + it[SysDictTypeTable.remark] = request.remark.trimToNull() + it[SysDictTypeTable.updatedAt] = OffsetDateTime.now() + } + } + + 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, + ) + + fun typeHasItems(id: Uuid): Boolean = + SysDictItemTable.selectAll().where { + (SysDictItemTable.typeId eq id) and SysDictItemTable.deletedAt.isNull() + }.any() + + fun softDeleteType(id: Uuid) { + SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { + it[SysDictTypeTable.deletedAt] = OffsetDateTime.now() + } + } + + fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult { + var where: Op = 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(pageOffset(page, pageSize)) + .toList() + return PageResult( + items = rows.map { it.toDictItem() }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + fun createItem(request: CreateDictItemRequest, typeId: Uuid): String { + val inserted = SysDictItemTable.insert { + it[SysDictItemTable.typeId] = typeId + it[SysDictItemTable.label] = request.label.trim() + it[SysDictItemTable.value] = request.value.trim() + it[SysDictItemTable.color] = request.color.trimToNull() + it[SysDictItemTable.sort] = request.sort + it[SysDictItemTable.status] = request.status + it[SysDictItemTable.remark] = request.remark.trimToNull() + it[SysDictItemTable.createdAt] = OffsetDateTime.now() + } + return inserted[SysDictItemTable.id].toString() + } + + 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 updateItem(id: Uuid, request: UpdateDictItemRequest, typeId: Uuid) { + SysDictItemTable.update({ SysDictItemTable.id eq id }) { + it[SysDictItemTable.typeId] = typeId + it[SysDictItemTable.label] = request.label.trim() + it[SysDictItemTable.value] = request.value.trim() + it[SysDictItemTable.color] = request.color.trimToNull() + it[SysDictItemTable.sort] = request.sort + it[SysDictItemTable.status] = request.status + it[SysDictItemTable.remark] = request.remark.trimToNull() + it[SysDictItemTable.updatedAt] = OffsetDateTime.now() + } + } + + fun softDeleteItem(id: Uuid) { + SysDictItemTable.update({ SysDictItemTable.id eq id }) { + it[SysDictItemTable.deletedAt] = OffsetDateTime.now() + } + } + + private fun buildTypeWhere(keyword: String?): Op { + var where: Op = SysDictTypeTable.deletedAt.isNull() + if (!keyword.isNullOrBlank()) { + where = where and ((SysDictTypeTable.code like "%$keyword%") or (SysDictTypeTable.name like "%$keyword%")) + } + return where + } + + private fun ResultRow.toDictTypeItem() = DictTypeItem( + id = this[SysDictTypeTable.id].toString(), + code = this[SysDictTypeTable.code], + name = this[SysDictTypeTable.name], + status = this[SysDictTypeTable.status], + statusLabel = statusLabel(this[SysDictTypeTable.status]), + remark = this[SysDictTypeTable.remark], + ) + + private fun ResultRow.toDictItem() = DictItem( + id = this[SysDictItemTable.id].toString(), + typeId = this[SysDictItemTable.typeId].toString(), + label = this[SysDictItemTable.label], + value = this[SysDictItemTable.value], + color = this[SysDictItemTable.color], + sort = this[SysDictItemTable.sort], + status = this[SysDictItemTable.status], + statusLabel = statusLabel(this[SysDictItemTable.status]), + remark = this[SysDictItemTable.remark], + ) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/LogDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/LogDao.kt new file mode 100644 index 0000000..8786725 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/LogDao.kt @@ -0,0 +1,128 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.dao.system + +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.CurrentUser +import com.bbit.ticket.utils.formatDateTime +import com.bbit.ticket.utils.traceIdOrNull +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.core.Op +import org.jetbrains.exposed.v1.core.SortOrder +import org.jetbrains.exposed.v1.core.and +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.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi + +object LogDao { + fun operationLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult { + var where: Op = Op.TRUE + if (!keyword.isNullOrBlank()) { + where = where and ((SysOperationLogTable.username like "%$keyword%") or (SysOperationLogTable.requestPath like "%$keyword%")) + } + if (!status.isNullOrBlank()) { + where = where and (SysOperationLogTable.status eq status) + } + val total = SysOperationLogTable.selectAll().where { where }.count() + val rows = SysOperationLogTable.selectAll().where { where } + .orderBy(SysOperationLogTable.createdAt, SortOrder.DESC) + .limit(pageSize) + .offset(pageOffset(page, pageSize)) + .toList() + return PageResult( + items = rows.map { row -> + OperationLogItem( + id = row[SysOperationLogTable.id].toString(), + traceId = row[SysOperationLogTable.traceId], + username = row[SysOperationLogTable.username], + operationType = row[SysOperationLogTable.operationType], + operationName = row[SysOperationLogTable.operationName], + httpMethod = row[SysOperationLogTable.httpMethod], + requestPath = row[SysOperationLogTable.requestPath], + status = row[SysOperationLogTable.status], + errorMessage = row[SysOperationLogTable.errorMessage], + costMs = row[SysOperationLogTable.costMs], + createdAt = formatDateTime(row[SysOperationLogTable.createdAt]) ?: "", + ) + }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + fun apiAccessLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult { + var where: Op = Op.TRUE + if (!keyword.isNullOrBlank()) { + where = where and ((SysApiAccessLogTable.appName like "%$keyword%") or (SysApiAccessLogTable.requestPath like "%$keyword%")) + } + if (!status.isNullOrBlank()) { + where = where and (SysApiAccessLogTable.status eq status) + } + val total = SysApiAccessLogTable.selectAll().where { where }.count() + val rows = SysApiAccessLogTable.selectAll().where { where } + .orderBy(SysApiAccessLogTable.createdAt, SortOrder.DESC) + .limit(pageSize) + .offset(pageOffset(page, pageSize)) + .toList() + return PageResult( + items = rows.map { row -> + ApiAccessLogItem( + id = row[SysApiAccessLogTable.id].toString(), + traceId = row[SysApiAccessLogTable.traceId], + appKey = row[SysApiAccessLogTable.appKey], + appName = row[SysApiAccessLogTable.appName], + httpMethod = row[SysApiAccessLogTable.httpMethod], + requestPath = row[SysApiAccessLogTable.requestPath], + responseCode = row[SysApiAccessLogTable.responseCode], + status = row[SysApiAccessLogTable.status], + errorMessage = row[SysApiAccessLogTable.errorMessage], + costMs = row[SysApiAccessLogTable.costMs], + createdAt = formatDateTime(row[SysApiAccessLogTable.createdAt]) ?: "", + ) + }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + fun saveOperationLog( + call: ApplicationCall, + currentUser: CurrentUser?, + operationType: String, + operationName: String, + status: String, + errorMessage: String?, + costMs: Long, + ) { + SysOperationLogTable.insert { + it[traceId] = call.traceIdOrNull() + it[userId] = currentUser?.id + it[SysOperationLogTable.username] = currentUser?.username + it[orgId] = currentUser?.orgId + it[SysOperationLogTable.operationType] = operationType + it[SysOperationLogTable.operationName] = operationName + it[httpMethod] = call.request.httpMethod.value + it[requestPath] = call.request.path().take(255) + it[requestParams] = call.request.queryParameters.formUrlEncode().take(1000) + it[ip] = call.request.local.remoteHost.take(64) + it[userAgent] = call.request.headers["User-Agent"]?.take(255) + it[SysOperationLogTable.status] = status + it[SysOperationLogTable.errorMessage] = errorMessage + it[SysOperationLogTable.costMs] = costMs + it[createdAt] = OffsetDateTime.now() + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/MenuDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/MenuDao.kt new file mode 100644 index 0000000..8688aca --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/MenuDao.kt @@ -0,0 +1,186 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.dao.system + +import com.bbit.ticket.database.system.SysMenuTable +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.MenuNode +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 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.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 MenuDao { + fun tree(): List = buildTree(loadAllFlatMenus()) + + fun requireActive(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, + ) + + fun create(request: CreateMenuRequest, parentId: Uuid?): String { + val inserted = SysMenuTable.insert { + it[SysMenuTable.parentId] = parentId + it[SysMenuTable.type] = request.type + it[SysMenuTable.title] = request.title.trim() + it[SysMenuTable.name] = request.name.trimToNull() + it[SysMenuTable.path] = request.path.trimToNull() + it[SysMenuTable.component] = request.component.trimToNull() + it[SysMenuTable.icon] = request.icon.trimToNull() + it[SysMenuTable.permission] = request.permission.trimToNull() + it[SysMenuTable.sort] = request.sort + it[SysMenuTable.visible] = request.visible + it[SysMenuTable.keepAlive] = request.keepAlive + it[SysMenuTable.builtIn] = false + it[SysMenuTable.status] = request.status + it[SysMenuTable.createdAt] = OffsetDateTime.now() + } + return inserted[SysMenuTable.id].toString() + } + + fun update(id: Uuid, request: UpdateMenuRequest, parentId: Uuid?) { + SysMenuTable.update({ SysMenuTable.id eq id }) { + it[SysMenuTable.parentId] = parentId + it[SysMenuTable.type] = request.type + it[SysMenuTable.title] = request.title.trim() + it[SysMenuTable.name] = request.name.trimToNull() + it[SysMenuTable.path] = request.path.trimToNull() + it[SysMenuTable.component] = request.component.trimToNull() + it[SysMenuTable.icon] = request.icon.trimToNull() + it[SysMenuTable.permission] = request.permission.trimToNull() + it[SysMenuTable.sort] = request.sort + it[SysMenuTable.visible] = request.visible + it[SysMenuTable.keepAlive] = request.keepAlive + it[SysMenuTable.status] = request.status + it[SysMenuTable.updatedAt] = OffsetDateTime.now() + } + } + + fun hasChildren(id: Uuid): Boolean = + SysMenuTable.selectAll().where { + (SysMenuTable.parentId eq id) and SysMenuTable.deletedAt.isNull() + }.any() + + fun isReferenced(id: Uuid): Boolean = + SysRoleMenuTable.selectAll().where { SysRoleMenuTable.menuId eq id }.any() + + fun softDelete(id: Uuid) { + SysMenuTable.update({ SysMenuTable.id eq id }) { + it[SysMenuTable.deletedAt] = OffsetDateTime.now() + } + } + + fun enabledMenusForRoleIds(roleIds: List): List { + if (roleIds.isEmpty()) return emptyList() + val rows = (SysRoleMenuTable innerJoin SysMenuTable) + .selectAll() + .where { + (SysRoleMenuTable.roleId inList roleIds) and + SysMenuTable.deletedAt.isNull() and + (SysMenuTable.status eq "ENABLED") + } + .distinct() + .toList() + return rows.map { it.toMenuFlat() }.sortedWith(compareBy { it.sort }.thenBy { it.id.toString() }) + } + + fun enabledMenusForSuperAdmin(): List = + SysMenuTable.selectAll() + .where { SysMenuTable.deletedAt.isNull() and (SysMenuTable.status eq "ENABLED") } + .toList() + .map { it.toMenuFlat() } + .sortedWith(compareBy { it.sort }.thenBy { it.id.toString() }) + + fun buildAuthTree(flatMenus: List): List { + val parentMap = flatMenus.groupBy { it.parentId } + fun build(parentId: Uuid?): List = + (parentMap[parentId] ?: emptyList()).map { menu -> + MenuNode( + id = menu.id.toString(), + parentId = menu.parentId?.toString(), + type = 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, + children = build(menu.id), + ) + } + return build(null) + } + + private fun loadAllFlatMenus(): List = + SysMenuTable.selectAll() + .where { SysMenuTable.deletedAt.isNull() } + .toList() + .map { it.toMenuFlat() } + + private fun buildTree(items: List): List { + val grouped = items.groupBy { it.parentId } + fun children(parentId: Uuid?): List = + (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 fun ResultRow.toMenuFlat() = MenuFlat( + id = this[SysMenuTable.id], + parentId = this[SysMenuTable.parentId], + type = this[SysMenuTable.type], + title = this[SysMenuTable.title], + name = this[SysMenuTable.name], + path = this[SysMenuTable.path], + component = this[SysMenuTable.component], + icon = this[SysMenuTable.icon], + permission = this[SysMenuTable.permission], + sort = this[SysMenuTable.sort], + visible = this[SysMenuTable.visible], + keepAlive = this[SysMenuTable.keepAlive], + builtIn = this[SysMenuTable.builtIn], + status = this[SysMenuTable.status], + ) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/OrgDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/OrgDao.kt new file mode 100644 index 0000000..93568ac --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/OrgDao.kt @@ -0,0 +1,122 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.dao.system + +import com.bbit.ticket.database.system.SysOrgTable +import com.bbit.ticket.database.system.SysUserTable +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 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 OrgDao { + fun tree(): List { + val rows = SysOrgTable.selectAll() + .where { SysOrgTable.deletedAt.isNull() } + .orderBy(SysOrgTable.sort) + .toList() + val nodes = rows.map(::toFlatNode) + return buildTree(nodes) + } + + fun codeExists(code: String): Boolean = + SysOrgTable.selectAll().where { + (SysOrgTable.code eq code) and SysOrgTable.deletedAt.isNull() + }.any() + + fun requireActive(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, + ) + + fun create(request: CreateOrgRequest, parentId: Uuid?): String { + val inserted = SysOrgTable.insert { + it[SysOrgTable.parentId] = parentId + it[SysOrgTable.name] = request.name.trim() + it[SysOrgTable.code] = request.code.trim() + it[SysOrgTable.sort] = request.sort + it[SysOrgTable.status] = request.status + it[SysOrgTable.createdAt] = OffsetDateTime.now() + } + return inserted[SysOrgTable.id].toString() + } + + fun update(id: Uuid, request: UpdateOrgRequest, parentId: Uuid?) { + SysOrgTable.update({ SysOrgTable.id eq id }) { + it[SysOrgTable.parentId] = parentId + it[SysOrgTable.name] = request.name.trim() + it[SysOrgTable.sort] = request.sort + it[SysOrgTable.status] = request.status + it[SysOrgTable.updatedAt] = OffsetDateTime.now() + } + } + + fun hasChildren(id: Uuid): Boolean = + SysOrgTable.selectAll().where { + (SysOrgTable.parentId eq id) and SysOrgTable.deletedAt.isNull() + }.any() + + fun hasUsers(id: Uuid): Boolean = + SysUserTable.selectAll().where { + (SysUserTable.orgId eq id) and SysUserTable.deletedAt.isNull() + }.any() + + fun softDelete(id: Uuid) { + SysOrgTable.update({ SysOrgTable.id eq id }) { + it[SysOrgTable.deletedAt] = OffsetDateTime.now() + } + } + + private fun toFlatNode(row: ResultRow) = OrgNodeFlat( + id = row[SysOrgTable.id], + parentId = row[SysOrgTable.parentId], + name = row[SysOrgTable.name], + code = row[SysOrgTable.code], + sort = row[SysOrgTable.sort], + status = row[SysOrgTable.status], + ) + + private fun buildTree(nodes: List): List { + val byParent = nodes.groupBy { it.parentId } + fun children(parentId: Uuid?): List = + (byParent[parentId] ?: emptyList()).sortedBy { it.sort }.map { item -> + OrgTreeNode( + id = item.id.toString(), + parentId = item.parentId?.toString(), + name = item.name, + code = item.code, + sort = item.sort, + status = item.status, + statusLabel = statusLabel(item.status), + children = children(item.id), + ) + } + 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, + ) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/RoleDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/RoleDao.kt new file mode 100644 index 0000000..5a777d7 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/RoleDao.kt @@ -0,0 +1,155 @@ +package com.bbit.ticket.dao.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.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.system.UpdateRoleRequest +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 io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.v1.core.Op +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 + +@OptIn(ExperimentalUuidApi::class) +object RoleDao { + fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult { + val where = buildWhere(keyword, status) + val total = SysRoleTable.selectAll().where { where }.count() + val rows = SysRoleTable.selectAll().where { where } + .orderBy(SysRoleTable.createdAt) + .limit(pageSize) + .offset(pageOffset(page, pageSize)) + .toList() + return PageResult( + items = rows.map { it.toRoleItem() }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + fun codeExists(code: String): Boolean = + SysRoleTable.selectAll().where { + (SysRoleTable.code eq code) and SysRoleTable.deletedAt.isNull() + }.any() + + fun create(request: CreateRoleRequest): String { + val inserted = SysRoleTable.insert { + it[SysRoleTable.name] = request.name.trim() + it[SysRoleTable.code] = request.code.trim() + it[SysRoleTable.description] = request.description.trimToNull() + it[SysRoleTable.status] = request.status + it[SysRoleTable.dataScope] = request.dataScope + it[SysRoleTable.createdAt] = OffsetDateTime.now() + } + return inserted[SysRoleTable.id].toString() + } + + fun requireActive(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 detail(id: Uuid): RoleDetail { + val role = requireActive(id) + val menuIds = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.roleId eq id } + .map { it[SysRoleMenuTable.menuId].toString() } + return role.toRoleDetail(menuIds) + } + + fun update(id: Uuid, request: UpdateRoleRequest) { + SysRoleTable.update({ SysRoleTable.id eq id }) { + it[SysRoleTable.name] = request.name.trim() + it[SysRoleTable.description] = request.description.trimToNull() + it[SysRoleTable.status] = request.status + it[SysRoleTable.dataScope] = request.dataScope + it[SysRoleTable.updatedAt] = OffsetDateTime.now() + } + } + + fun inUse(id: Uuid): Boolean = + SysUserRoleTable.selectAll().where { SysUserRoleTable.roleId eq id }.any() + + fun softDelete(id: Uuid) { + SysRoleTable.update({ SysRoleTable.id eq id }) { + it[SysRoleTable.deletedAt] = OffsetDateTime.now() + } + SysRoleMenuTable.deleteWhere { roleId eq id } + } + + fun countEnabledMenus(menuIds: List): Long = + SysMenuTable.selectAll().where { + (SysMenuTable.id inList menuIds) and + SysMenuTable.deletedAt.isNull() and + (SysMenuTable.status eq "ENABLED") + }.count() + + fun replaceMenus(id: Uuid, menuIds: List) { + SysRoleMenuTable.deleteWhere { roleId eq id } + menuIds.forEach { menuId -> + SysRoleMenuTable.insertIgnore { + it[roleId] = id + it[SysRoleMenuTable.menuId] = menuId + } + } + } + + private fun buildWhere(keyword: String?, status: String?): Op { + var where: Op = 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) + } + return where + } + + private fun ResultRow.toRoleItem() = RoleItem( + id = this[SysRoleTable.id].toString(), + name = this[SysRoleTable.name], + code = this[SysRoleTable.code], + description = this[SysRoleTable.description], + status = this[SysRoleTable.status], + statusLabel = statusLabel(this[SysRoleTable.status]), + dataScope = this[SysRoleTable.dataScope], + dataScopeLabel = dataScopeLabel(this[SysRoleTable.dataScope]), + ) + + private fun ResultRow.toRoleDetail(menuIds: List) = RoleDetail( + id = this[SysRoleTable.id].toString(), + name = this[SysRoleTable.name], + code = this[SysRoleTable.code], + description = this[SysRoleTable.description], + status = this[SysRoleTable.status], + statusLabel = statusLabel(this[SysRoleTable.status]), + dataScope = this[SysRoleTable.dataScope], + dataScopeLabel = dataScopeLabel(this[SysRoleTable.dataScope]), + menuIds = menuIds, + ) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/SystemDaoSupport.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/SystemDaoSupport.kt new file mode 100644 index 0000000..ccc5d06 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/SystemDaoSupport.kt @@ -0,0 +1,5 @@ +package com.bbit.ticket.dao.system + +internal fun pageOffset(page: Int, pageSize: Int): Long = ((page - 1) * pageSize).toLong() + +internal fun String?.trimToNull(): String? = this?.trim()?.takeIf { it.isNotEmpty() } diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt new file mode 100644 index 0000000..e0c8154 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt @@ -0,0 +1,237 @@ +package com.bbit.ticket.dao.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.entity.system.CreateUserRequest +import com.bbit.ticket.entity.system.UserDetailResponse +import com.bbit.ticket.entity.system.UserListItem +import com.bbit.ticket.entity.system.UpdateUserRequest +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 io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.v1.core.Op +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.jdbc.Query +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 UserDao { + fun list( + page: Int, + pageSize: Int, + username: String?, + nickname: String?, + status: String?, + orgId: Uuid?, + ): PageResult { + 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(pageOffset(page, pageSize)) + .toList() + + val roleMap = findRoleCodesByUserIds(rows.map { it[SysUserTable.id] }) + return PageResult( + items = rows.map { row -> row.toUserListItem(roleMap[row[SysUserTable.id]] ?: emptyList()) }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + fun findByUsername(username: String): ResultRow? = + activeUsers().where { SysUserTable.username eq username }.singleOrNull() + + fun requireActive(id: Uuid): ResultRow = + activeUsers().where { SysUserTable.id eq id }.singleOrNull() + ?: throw BizException( + ErrorCode.USER_NOT_FOUND.code, + ErrorCode.USER_NOT_FOUND.message, + HttpStatusCode.NotFound, + ) + + fun create(request: CreateUserRequest, passwordHash: String, orgId: Uuid?): String { + val now = OffsetDateTime.now() + val row = SysUserTable.insert { + it[SysUserTable.username] = request.username.trim() + it[SysUserTable.passwordHash] = passwordHash + it[SysUserTable.nickname] = request.nickname.trimToNull() + it[SysUserTable.realName] = request.realName.trimToNull() + it[SysUserTable.phone] = request.phone.trimToNull() + it[SysUserTable.email] = request.email.trimToNull() + it[SysUserTable.avatar] = request.avatar.trimToNull() + it[SysUserTable.orgId] = orgId + it[SysUserTable.status] = request.status + it[SysUserTable.tokenVersion] = 1 + it[SysUserTable.createdAt] = now + } + return row[SysUserTable.id].toString() + } + + fun detail(id: Uuid): UserDetailResponse { + val user = requireActive(id) + val roleIds = SysUserRoleTable.selectAll() + .where { SysUserRoleTable.userId eq id } + .map { it[SysUserRoleTable.roleId].toString() } + return user.toUserDetail(roleIds) + } + + fun updateProfile(id: Uuid, request: UpdateUserRequest, orgId: Uuid?) { + SysUserTable.update({ SysUserTable.id eq id }) { + it[SysUserTable.nickname] = request.nickname.trimToNull() + it[SysUserTable.realName] = request.realName.trimToNull() + it[SysUserTable.phone] = request.phone.trimToNull() + it[SysUserTable.email] = request.email.trimToNull() + it[SysUserTable.avatar] = request.avatar.trimToNull() + it[SysUserTable.orgId] = orgId + it[SysUserTable.updatedAt] = OffsetDateTime.now() + } + } + + fun softDelete(id: Uuid) { + SysUserTable.update({ SysUserTable.id eq id }) { + it[SysUserTable.deletedAt] = OffsetDateTime.now() + } + SysUserRoleTable.deleteWhere { userId eq id } + } + + fun updateStatus(id: Uuid, status: String) { + SysUserTable.update({ SysUserTable.id eq id }) { + it[SysUserTable.status] = status + it[SysUserTable.updatedAt] = OffsetDateTime.now() + } + } + + fun updatePassword(id: Uuid, passwordHash: String, nextTokenVersion: Int) { + SysUserTable.update({ SysUserTable.id eq id }) { + it[SysUserTable.passwordHash] = passwordHash + it[SysUserTable.tokenVersion] = nextTokenVersion + it[SysUserTable.updatedAt] = OffsetDateTime.now() + } + } + + fun replaceRoles(id: Uuid, roleIds: List) { + SysUserRoleTable.deleteWhere { userId eq id } + roleIds.forEach { roleId -> + SysUserRoleTable.insertIgnore { + it[userId] = id + it[SysUserRoleTable.roleId] = roleId + } + } + } + + fun countEnabledRoles(roleIds: List): Long = + SysRoleTable.selectAll().where { + (SysRoleTable.id inList roleIds) and + (SysRoleTable.status eq "ENABLED") and + SysRoleTable.deletedAt.isNull() + }.count() + + fun orgExists(orgId: Uuid): Boolean = + SysOrgTable.selectAll().where { + (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() + }.any() + + fun findEnabledRoleCodes(userId: Uuid): List = + (SysUserRoleTable innerJoin SysRoleTable) + .selectAll() + .where { + (SysUserRoleTable.userId eq userId) and + SysRoleTable.deletedAt.isNull() and + (SysRoleTable.status eq "ENABLED") + } + .map { it[SysRoleTable.code] } + + fun findEnabledRoleIds(userId: Uuid): List = + (SysUserRoleTable innerJoin SysRoleTable) + .selectAll() + .where { + (SysUserRoleTable.userId eq userId) and + SysRoleTable.deletedAt.isNull() and + (SysRoleTable.status eq "ENABLED") + } + .map { it[SysRoleTable.id] } + + fun updateLoginInfo(userId: Uuid, loginIp: String?) { + SysUserTable.update({ SysUserTable.id eq userId }) { + it[SysUserTable.lastLoginAt] = OffsetDateTime.now() + it[SysUserTable.lastLoginIp] = loginIp + it[SysUserTable.updatedAt] = OffsetDateTime.now() + } + } + + private fun activeUsers(): Query = + SysUserTable.selectAll().where { SysUserTable.deletedAt.isNull() } + + private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op { + var where: Op = 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 findRoleCodesByUserIds(userIds: List): Map> { + if (userIds.isEmpty()) return emptyMap() + return (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() } + } + + private fun ResultRow.toUserListItem(roleCodes: List) = UserListItem( + id = this[SysUserTable.id].toString(), + username = this[SysUserTable.username], + nickname = this[SysUserTable.nickname], + realName = this[SysUserTable.realName], + orgId = this[SysUserTable.orgId]?.toString(), + status = this[SysUserTable.status], + statusLabel = statusLabel(this[SysUserTable.status]), + roleCodes = roleCodes, + ) + + private fun ResultRow.toUserDetail(roleIds: List) = UserDetailResponse( + id = this[SysUserTable.id].toString(), + username = this[SysUserTable.username], + nickname = this[SysUserTable.nickname], + realName = this[SysUserTable.realName], + phone = this[SysUserTable.phone], + email = this[SysUserTable.email], + avatar = this[SysUserTable.avatar], + orgId = this[SysUserTable.orgId]?.toString(), + status = this[SysUserTable.status], + statusLabel = statusLabel(this[SysUserTable.status]), + roleIds = roleIds, + ) +} diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysApiAccessLogTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysApiAccessLogTable.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysApiAccessLogTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysApiAccessLogTable.kt index b4f3c5c..a8ae394 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysApiAccessLogTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysApiAccessLogTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysDictItemTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysDictItemTable.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysDictItemTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysDictItemTable.kt index 987f31d..a4047a3 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysDictItemTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysDictItemTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysDictTypeTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysDictTypeTable.kt similarity index 95% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysDictTypeTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysDictTypeTable.kt index d82346e..a71ca5b 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysDictTypeTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysDictTypeTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysMenuTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysMenuTable.kt similarity index 97% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysMenuTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysMenuTable.kt index c921c4d..6809831 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysMenuTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysMenuTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysOperationLogTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysOperationLogTable.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysOperationLogTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysOperationLogTable.kt index 72af8f6..ea33043 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysOperationLogTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysOperationLogTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysOrgTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysOrgTable.kt similarity index 95% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysOrgTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysOrgTable.kt index b7f7803..9c79398 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysOrgTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysOrgTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysRoleMenuTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysRoleMenuTable.kt similarity index 89% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysRoleMenuTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysRoleMenuTable.kt index 7157a33..2507a8e 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysRoleMenuTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysRoleMenuTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table import kotlin.uuid.ExperimentalUuidApi diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysRoleTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysRoleTable.kt similarity index 95% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysRoleTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysRoleTable.kt index 00cc98f..b994943 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysRoleTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysRoleTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysUserRoleTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserRoleTable.kt similarity index 89% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysUserRoleTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysUserRoleTable.kt index c50dc60..1aa9609 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysUserRoleTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserRoleTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table import kotlin.uuid.ExperimentalUuidApi diff --git a/server/src/main/kotlin/com/bbit/platform/database/system/SysUserTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt similarity index 97% rename from server/src/main/kotlin/com/bbit/platform/database/system/SysUserTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt index 539b772..0fe49bf 100644 --- a/server/src/main/kotlin/com/bbit/platform/database/system/SysUserTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.database.system +package com.bbit.ticket.database.system import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone diff --git a/server/src/main/kotlin/com/bbit/platform/common/ApiResult.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/ApiResult.kt similarity index 92% rename from server/src/main/kotlin/com/bbit/platform/common/ApiResult.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/ApiResult.kt index 6f1f684..cdb0700 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/ApiResult.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/ApiResult.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.entity.common import kotlinx.serialization.Serializable diff --git a/server/src/main/kotlin/com/bbit/platform/common/BizException.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/BizException.kt similarity index 84% rename from server/src/main/kotlin/com/bbit/platform/common/BizException.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/BizException.kt index 29dbdfd..00abafa 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/BizException.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/BizException.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.entity.common import io.ktor.http.HttpStatusCode diff --git a/server/src/main/kotlin/com/bbit/platform/common/DisplayLabels.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/DisplayLabels.kt similarity index 93% rename from server/src/main/kotlin/com/bbit/platform/common/DisplayLabels.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/DisplayLabels.kt index 063f364..311cbee 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/DisplayLabels.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/DisplayLabels.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.entity.common fun statusLabel(status: String): String = when (status) { "ENABLED" -> "启用" diff --git a/server/src/main/kotlin/com/bbit/platform/common/ErrorCode.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/ErrorCode.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/platform/common/ErrorCode.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/ErrorCode.kt index c6bcaae..f6f7c75 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/ErrorCode.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/ErrorCode.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.entity.common enum class ErrorCode(val code: String, val message: String) { BAD_REQUEST("COMMON.BAD_REQUEST", "请求参数错误"), diff --git a/server/src/main/kotlin/com/bbit/platform/common/PageQuery.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageQuery.kt similarity index 90% rename from server/src/main/kotlin/com/bbit/platform/common/PageQuery.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/PageQuery.kt index a6773e1..86722b5 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/PageQuery.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageQuery.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.entity.common import kotlinx.serialization.Serializable diff --git a/server/src/main/kotlin/com/bbit/platform/common/PageResult.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageResult.kt similarity index 82% rename from server/src/main/kotlin/com/bbit/platform/common/PageResult.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/PageResult.kt index 860e4b9..5506efa 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/PageResult.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageResult.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.entity.common import kotlinx.serialization.Serializable diff --git a/server/src/main/kotlin/com/bbit/platform/modules/auth/AuthDtos.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt similarity index 89% rename from server/src/main/kotlin/com/bbit/platform/modules/auth/AuthDtos.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt index 76e3926..55d9ac7 100644 --- a/server/src/main/kotlin/com/bbit/platform/modules/auth/AuthDtos.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt @@ -1,6 +1,9 @@ -package com.bbit.platform.modules.auth +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.entity.system import kotlinx.serialization.Serializable +import kotlin.uuid.ExperimentalUuidApi @Serializable data class LoginRequest( @@ -48,4 +51,3 @@ data class MenuNode( val keepAlive: Boolean, val children: List = emptyList(), ) - diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/DictDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/DictDto.kt new file mode 100644 index 0000000..dbde321 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/DictDto.kt @@ -0,0 +1,53 @@ +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, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/LogDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/LogDto.kt new file mode 100644 index 0000000..e3565ce --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/LogDto.kt @@ -0,0 +1,32 @@ +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, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/MenuDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/MenuDto.kt new file mode 100644 index 0000000..811ed06 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/MenuDto.kt @@ -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 = 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, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/OrgDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/OrgDto.kt new file mode 100644 index 0000000..266267e --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/OrgDto.kt @@ -0,0 +1,31 @@ +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 = 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", +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/RoleDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/RoleDto.kt new file mode 100644 index 0000000..07b6d72 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/RoleDto.kt @@ -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, +) + +@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) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt new file mode 100644 index 0000000..6f2a564 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt @@ -0,0 +1,61 @@ +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, +) + +@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, +) + +@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) diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/ApiAccessLogPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/ApiAccessLogPlugin.kt similarity index 95% rename from server/src/main/kotlin/com/bbit/platform/plugins/ApiAccessLogPlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/ApiAccessLogPlugin.kt index bc001e3..2ca3fcc 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/ApiAccessLogPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/ApiAccessLogPlugin.kt @@ -1,9 +1,8 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins -import com.bbit.platform.common.TraceIdKey -import com.bbit.platform.database.system.SysApiAccessLogTable +import com.bbit.ticket.utils.TraceIdKey +import com.bbit.ticket.database.system.SysApiAccessLogTable import io.ktor.server.application.Application -import io.ktor.server.application.call import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.install import io.ktor.server.auth.jwt.JWTPrincipal diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/CorsPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/CorsPlugin.kt similarity index 63% rename from server/src/main/kotlin/com/bbit/platform/plugins/CorsPlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/CorsPlugin.kt index 3947da8..5c8f7e9 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/CorsPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/CorsPlugin.kt @@ -1,6 +1,6 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins -import com.bbit.platform.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) diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/DatabasePlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/DatabasePlugin.kt similarity index 95% rename from server/src/main/kotlin/com/bbit/platform/plugins/DatabasePlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/DatabasePlugin.kt index 38b2bea..47dad6d 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/DatabasePlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/DatabasePlugin.kt @@ -1,6 +1,6 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins -import com.bbit.platform.config.AppConfig +import com.bbit.ticket.bootstrap.AppConfig import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import io.ktor.server.application.Application diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/LoggingPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/LoggingPlugin.kt similarity index 81% rename from server/src/main/kotlin/com/bbit/platform/plugins/LoggingPlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/LoggingPlugin.kt index 57d29d7..54f64e7 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/LoggingPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/LoggingPlugin.kt @@ -1,5 +1,6 @@ -package com.bbit.platform.plugins +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.platform.common.TraceIdKey) } + mdc("traceId") { call -> call.attributes.getOrNull(TraceIdKey) } filter { call -> !call.request.path().startsWith("/health") } format { call -> val status = call.response.status() diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/RedisPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/RedisPlugin.kt similarity index 88% rename from server/src/main/kotlin/com/bbit/platform/plugins/RedisPlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/RedisPlugin.kt index f0c841d..3c542c6 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/RedisPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/RedisPlugin.kt @@ -1,6 +1,6 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins -import com.bbit.platform.config.AppConfig +import com.bbit.ticket.bootstrap.AppConfig import io.ktor.server.application.Application import org.redisson.Redisson import org.redisson.api.RedissonClient diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/SecurityPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/SecurityPlugin.kt similarity index 87% rename from server/src/main/kotlin/com/bbit/platform/plugins/SecurityPlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/SecurityPlugin.kt index 0d6a27e..adb5345 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/SecurityPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/SecurityPlugin.kt @@ -1,11 +1,11 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.common.fail -import com.bbit.platform.common.traceIdOrNull -import com.bbit.platform.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 diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/SerializationPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/SerializationPlugin.kt similarity index 93% rename from server/src/main/kotlin/com/bbit/platform/plugins/SerializationPlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/SerializationPlugin.kt index 316bb5e..95f2c9e 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/SerializationPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/SerializationPlugin.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.Application diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/StatusPagesPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/StatusPagesPlugin.kt similarity index 84% rename from server/src/main/kotlin/com/bbit/platform/plugins/StatusPagesPlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/StatusPagesPlugin.kt index cc587bb..9eef36d 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/StatusPagesPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/StatusPagesPlugin.kt @@ -1,9 +1,9 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.common.fail -import com.bbit.platform.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 diff --git a/server/src/main/kotlin/com/bbit/platform/plugins/TracePlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/TracePlugin.kt similarity index 86% rename from server/src/main/kotlin/com/bbit/platform/plugins/TracePlugin.kt rename to server/src/main/kotlin/com/bbit/ticket/plugins/TracePlugin.kt index 3bf739b..518d4e7 100644 --- a/server/src/main/kotlin/com/bbit/platform/plugins/TracePlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/TracePlugin.kt @@ -1,7 +1,6 @@ -package com.bbit.platform.plugins +package com.bbit.ticket.plugins -import com.bbit.platform.common.TraceIdKey -import io.ktor.http.HttpHeaders +import com.bbit.ticket.utils.TraceIdKey import io.ktor.server.application.Application import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.install diff --git a/server/src/main/kotlin/com/bbit/platform/modules/auth/AuthRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt similarity index 83% rename from server/src/main/kotlin/com/bbit/platform/modules/auth/AuthRoutes.kt rename to server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt index b371601..cd85a1f 100644 --- a/server/src/main/kotlin/com/bbit/platform/modules/auth/AuthRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt @@ -1,8 +1,11 @@ -package com.bbit.platform.modules.auth +package com.bbit.ticket.route.system -import com.bbit.platform.common.ok -import com.bbit.platform.modules.logs.OperationLogService -import com.bbit.platform.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") { diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt new file mode 100644 index 0000000..7a1a78a --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt @@ -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() + 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() + runCatching { + DictService.updateType(id, request) + call.respond(ok(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(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() + 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() + runCatching { + DictService.updateItem(id, request) + call.respond(ok(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(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除字典项", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除字典项", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt new file mode 100644 index 0000000..cc8bdc0 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt @@ -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")))) + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt new file mode 100644 index 0000000..1bbd607 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt @@ -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() + 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() + runCatching { + MenuService.update(id, request) + call.respond(ok(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(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除菜单", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除菜单", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt new file mode 100644 index 0000000..568b757 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt @@ -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() + 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() + runCatching { + OrgService.update(id, request) + call.respond(ok(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(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除组织", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除组织", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt new file mode 100644 index 0000000..62c51db --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt @@ -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() + 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() + runCatching { + RoleService.update(id, request) + call.respond(ok(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(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() + runCatching { + RoleService.updateMenus(id, request) + call.respond(ok(message = "菜单分配成功")) + OperationLogService.success(call, currentUser, "ASSIGN_MENU", "分配角色菜单", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "ASSIGN_MENU", "分配角色菜单", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt new file mode 100644 index 0000000..b66a08c --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt @@ -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() + 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() + runCatching { + UserService.update(id, request) + call.respond(ok(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(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() + runCatching { + UserService.updateStatus(id, request) + call.respond(ok(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() + runCatching { + UserService.updatePassword(id, request) + call.respond(ok(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() + runCatching { + UserService.updateRoles(id, request) + call.respond(ok(message = "角色分配成功")) + OperationLogService.success(call, currentUser, "ASSIGN_ROLE", "分配用户角色", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "ASSIGN_ROLE", "分配用户角色", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt new file mode 100644 index 0000000..89a61b0 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt @@ -0,0 +1,88 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.MenuDao +import com.bbit.ticket.dao.system.UserDao +import com.bbit.ticket.database.system.SysUserTable +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +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.plugins.dbQuery +import com.bbit.ticket.utils.CurrentUser +import io.ktor.http.HttpStatusCode +import kotlin.uuid.ExperimentalUuidApi + +object AuthService { + suspend fun login(request: LoginRequest, loginIp: String?): LoginResponse { + val username = request.username.trim() + if (username.isBlank() || request.password.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "用户名和密码不能为空", HttpStatusCode.BadRequest) + } + + val user = dbQuery { UserDao.findByUsername(username) } ?: throw BizException( + ErrorCode.USERNAME_OR_PASSWORD_INVALID.code, + ErrorCode.USERNAME_OR_PASSWORD_INVALID.message, + HttpStatusCode.BadRequest, + ) + + if (!PasswordService.matches(request.password, user[com.bbit.ticket.database.system.SysUserTable.passwordHash])) { + throw BizException( + ErrorCode.USERNAME_OR_PASSWORD_INVALID.code, + ErrorCode.USERNAME_OR_PASSWORD_INVALID.message, + HttpStatusCode.BadRequest, + ) + } + + if (user[com.bbit.ticket.database.system.SysUserTable.status] != "ENABLED") { + throw BizException(ErrorCode.USER_DISABLED.code, ErrorCode.USER_DISABLED.message, HttpStatusCode.BadRequest) + } + + val userId = user[com.bbit.ticket.database.system.SysUserTable.id] + val roleCodes = dbQuery { UserDao.findEnabledRoleCodes(userId) } + + val (accessToken, expiresIn) = JwtService.issueAccessToken( + userId = userId.toString(), + username = user[com.bbit.ticket.database.system.SysUserTable.username], + orgId = user[com.bbit.ticket.database.system.SysUserTable.orgId]?.toString(), + roles = roleCodes, + tokenVersion = user[com.bbit.ticket.database.system.SysUserTable.tokenVersion], + ) + + dbQuery { UserDao.updateLoginInfo(userId, loginIp) } + + return LoginResponse(accessToken = accessToken, expiresIn = expiresIn) + } + + suspend fun me(currentUser: CurrentUser): MeResponse { + val userRow = dbQuery { UserDao.requireActive(currentUser.id) } + + val allMenus = loadMenusForUser(currentUser) + val menuTree = MenuDao.buildAuthTree(allMenus) + val permissions = allMenus.mapNotNull { it.permission }.toSet() + + return MeResponse( + user = CurrentUserProfile( + id = currentUser.id.toString(), + username = userRow[SysUserTable.username], + nickname = userRow[SysUserTable.nickname], + realName = userRow[SysUserTable.realName], + orgId = userRow[SysUserTable.orgId]?.toString(), + status = userRow[SysUserTable.status], + ), + menus = menuTree, + permissions = permissions, + ) + } + + private suspend fun loadMenusForUser(currentUser: CurrentUser) = dbQuery { + if (currentUser.isSuperAdmin) { + MenuDao.enabledMenusForSuperAdmin() + } else { + MenuDao.enabledMenusForRoleIds(UserDao.findEnabledRoleIds(currentUser.id)) + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt new file mode 100644 index 0000000..3836f91 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt @@ -0,0 +1,65 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.DictDao +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.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object DictService { + suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult = + dbQuery { DictDao.listTypes(page, pageSize, keyword) } + + suspend fun createType(request: CreateDictTypeRequest): String = dbQuery { + if (request.code.trim().isBlank() || request.name.trim().isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型编码和名称不能为空") + } + if (DictDao.typeCodeExists(request.code.trim())) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "字典类型编码已存在") + } + DictDao.createType(request) + } + + suspend fun updateType(id: Uuid, request: UpdateDictTypeRequest) = dbQuery { + DictDao.requireType(id) + DictDao.updateType(id, request) + } + + suspend fun deleteType(id: Uuid) = dbQuery { + DictDao.requireType(id) + if (DictDao.typeHasItems(id)) throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型下存在字典项,不能删除") + DictDao.softDeleteType(id) + } + + suspend fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult = + dbQuery { DictDao.listItems(page, pageSize, typeId) } + + suspend fun createItem(request: CreateDictItemRequest): String = dbQuery { + val typeId = parseUuid(request.typeId, "typeId") + DictDao.requireType(typeId) + DictDao.createItem(request, typeId) + } + + suspend fun updateItem(id: Uuid, request: UpdateDictItemRequest) = dbQuery { + DictDao.requireItem(id) + val typeId = parseUuid(request.typeId, "typeId") + DictDao.requireType(typeId) + DictDao.updateItem(id, request, typeId) + } + + suspend fun deleteItem(id: Uuid) = dbQuery { + DictDao.requireItem(id) + DictDao.softDeleteItem(id) + } +} diff --git a/server/src/main/kotlin/com/bbit/platform/security/JwtService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/JwtService.kt similarity index 92% rename from server/src/main/kotlin/com/bbit/platform/security/JwtService.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/JwtService.kt index a95e810..1877d3c 100644 --- a/server/src/main/kotlin/com/bbit/platform/security/JwtService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/JwtService.kt @@ -1,8 +1,8 @@ -package com.bbit.platform.security +package com.bbit.ticket.service.system import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import com.bbit.platform.config.AppConfig +import com.bbit.ticket.bootstrap.AppConfig import java.time.Instant import java.time.temporal.ChronoUnit @@ -32,5 +32,4 @@ object JwtService { return token to AppConfig.jwt.accessTokenTtlMinutes * 60 } -} - +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt new file mode 100644 index 0000000..dc7846d --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt @@ -0,0 +1,18 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.LogDao +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.plugins.dbQuery +import kotlin.uuid.ExperimentalUuidApi + +object LogsQueryService { + suspend fun operationLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = + dbQuery { LogDao.operationLogs(page, pageSize, keyword, status) } + + suspend fun apiAccessLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = + dbQuery { LogDao.apiAccessLogs(page, pageSize, keyword, status) } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt new file mode 100644 index 0000000..8467d2d --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt @@ -0,0 +1,50 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.MenuDao +import com.bbit.ticket.entity.system.CreateMenuRequest +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.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object MenuService { + suspend fun tree(): List = dbQuery { MenuDao.tree() } + + suspend fun create(request: CreateMenuRequest): String = dbQuery { + validateMenuType(request.type) + val parentId = request.parentId?.let { parseUuid(it, "parentId") } + if (parentId != null) MenuDao.requireActive(parentId) + MenuDao.create(request, parentId) + } + + suspend fun update(id: Uuid, request: UpdateMenuRequest) = dbQuery { + MenuDao.requireActive(id) + validateMenuType(request.type) + val parentId = request.parentId?.let { parseUuid(it, "parentId") } + if (parentId == id) throw BizException(ErrorCode.BAD_REQUEST.code, "上级菜单不能选择自身") + if (parentId != null) MenuDao.requireActive(parentId) + MenuDao.update(id, request, parentId) + } + + suspend fun delete(id: Uuid) = dbQuery { + val row = MenuDao.requireActive(id) + if (MenuDao.hasChildren(id)) throw BizException(ErrorCode.BAD_REQUEST.code, "存在子菜单,不能删除") + if (MenuDao.isReferenced(id)) throw BizException(ErrorCode.BAD_REQUEST.code, "菜单已被角色引用,不能删除") + if (row[com.bbit.ticket.database.system.SysMenuTable.builtIn]) { + throw BizException(ErrorCode.BAD_REQUEST.code, "基础框架内置菜单不可删除") + } + MenuDao.softDelete(id) + } + + private fun validateMenuType(type: String) { + if (type !in setOf("CATALOG", "MENU", "BUTTON")) { + throw BizException(ErrorCode.BAD_REQUEST.code, "菜单类型必须是目录、菜单或按钮") + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt new file mode 100644 index 0000000..8fd7104 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt @@ -0,0 +1,38 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.LogDao +import com.bbit.ticket.plugins.dbQuery +import com.bbit.ticket.utils.CurrentUser +import io.ktor.server.application.ApplicationCall +import kotlin.uuid.ExperimentalUuidApi + +object OperationLogService { + suspend fun success(call: ApplicationCall, currentUser: CurrentUser?, operationType: String, operationName: String, costMs: Long) { + save(call, currentUser, operationType, operationName, "SUCCESS", null, costMs) + } + + suspend fun fail( + call: ApplicationCall, + currentUser: CurrentUser?, + operationType: String, + operationName: String, + errorMessage: String?, + costMs: Long, + ) { + save(call, currentUser, operationType, operationName, "FAIL", errorMessage?.take(500), costMs) + } + + private suspend fun save( + call: ApplicationCall, + currentUser: CurrentUser?, + operationType: String, + operationName: String, + status: String, + errorMessage: String?, + costMs: Long, + ) = dbQuery { + LogDao.saveOperationLog(call, currentUser, operationType, operationName, status, errorMessage, costMs) + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt new file mode 100644 index 0000000..9dfb62d --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt @@ -0,0 +1,55 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.OrgDao +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.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object OrgService { + suspend fun tree(): List = dbQuery { OrgDao.tree() } + + suspend fun create(request: CreateOrgRequest): String = dbQuery { + val code = request.code.trim() + if (code.isBlank() || request.name.trim().isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "组织名称和编码不能为空") + } + if (OrgDao.codeExists(code)) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "组织编码已存在") + } + val parentId = request.parentId?.let { parseUuid(it, "parentId") } + if (parentId != null) OrgDao.requireActive(parentId) + OrgDao.create(request, parentId) + } + + suspend fun update(id: Uuid, request: UpdateOrgRequest) = dbQuery { + OrgDao.requireActive(id) + val parentId = request.parentId?.let { parseUuid(it, "parentId") } + if (parentId == id) { + throw BizException(ErrorCode.BAD_REQUEST.code, "上级组织不能选择自身") + } + if (parentId != null) OrgDao.requireActive(parentId) + OrgDao.update(id, request, parentId) + } + + suspend fun delete(id: Uuid) = dbQuery { + val org = OrgDao.requireActive(id) + if (org[com.bbit.ticket.database.system.SysOrgTable.code] == "DEFAULT_ORG") { + throw BizException(ErrorCode.BAD_REQUEST.code, "默认组织不可删除") + } + if (OrgDao.hasChildren(id)) { + throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在子组织,不能删除") + } + if (OrgDao.hasUsers(id)) { + throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在用户,不能删除") + } + OrgDao.softDelete(id) + } +} diff --git a/server/src/main/kotlin/com/bbit/platform/security/PasswordService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/PasswordService.kt similarity index 86% rename from server/src/main/kotlin/com/bbit/platform/security/PasswordService.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/PasswordService.kt index 92894da..c043968 100644 --- a/server/src/main/kotlin/com/bbit/platform/security/PasswordService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/PasswordService.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.security +package com.bbit.ticket.service.system import org.mindrot.jbcrypt.BCrypt @@ -6,5 +6,4 @@ object PasswordService { fun hash(rawPassword: String): String = BCrypt.hashpw(rawPassword, BCrypt.gensalt()) fun matches(rawPassword: String, passwordHash: String): Boolean = BCrypt.checkpw(rawPassword, passwordHash) -} - +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt new file mode 100644 index 0000000..1d43dfb --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt @@ -0,0 +1,62 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.RoleDao +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.system.UpdateRoleMenusRequest +import com.bbit.ticket.entity.system.UpdateRoleRequest +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.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +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 = + dbQuery { RoleDao.list(page, pageSize, keyword, status) } + + suspend fun create(request: CreateRoleRequest): String = dbQuery { + if (request.name.trim().isBlank() || request.code.trim().isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "角色名称和编码不能为空") + } + if (RoleDao.codeExists(request.code.trim())) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "角色编码已存在") + } + RoleDao.create(request) + } + + suspend fun detail(id: Uuid): RoleDetail = dbQuery { RoleDao.detail(id) } + + suspend fun update(id: Uuid, request: UpdateRoleRequest) = dbQuery { + RoleDao.requireActive(id) + RoleDao.update(id, request) + } + + suspend fun delete(id: Uuid) = dbQuery { + val role = RoleDao.requireActive(id) + if (role[com.bbit.ticket.database.system.SysRoleTable.code] == "SUPER_ADMIN") { + throw BizException(ErrorCode.BAD_REQUEST.code, "超级管理员角色不可删除") + } + if (RoleDao.inUse(id)) { + throw BizException(ErrorCode.BAD_REQUEST.code, "角色已被用户使用,不能删除") + } + RoleDao.softDelete(id) + } + + suspend fun updateMenus(id: Uuid, request: UpdateRoleMenusRequest) = dbQuery { + RoleDao.requireActive(id) + val menuIds = request.menuIds.distinct().map { parseUuid(it, "menuId") } + if (menuIds.isNotEmpty()) { + if (RoleDao.countEnabledMenus(menuIds) != menuIds.size.toLong()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或禁用菜单") + } + } + RoleDao.replaceMenus(id, menuIds) + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt new file mode 100644 index 0000000..a11e6a9 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt @@ -0,0 +1,98 @@ +package com.bbit.ticket.service.system + +import com.bbit.ticket.dao.system.UserDao +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.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +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 = dbQuery { UserDao.list(page, pageSize, username, nickname, status, orgId) } + + suspend fun create(request: CreateUserRequest): String = dbQuery { + val username = request.username.trim() + if (username.isBlank() || request.password.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "用户名和密码不能为空") + } + if (UserDao.findByUsername(username) != null) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "用户名已存在") + } + + val orgUuid = request.orgId?.let { parseUuid(it, "orgId") } + if (orgUuid != null) { + ensureOrgExists(orgUuid) + } + UserDao.create(request, PasswordService.hash(request.password), orgUuid) + } + + suspend fun detail(id: Uuid): UserDetailResponse = dbQuery { UserDao.detail(id) } + + suspend fun update(id: Uuid, request: UpdateUserRequest) = dbQuery { + UserDao.requireActive(id) + val orgUuid = request.orgId?.let { parseUuid(it, "orgId") } + if (orgUuid != null) { + ensureOrgExists(orgUuid) + } + UserDao.updateProfile(id, request, orgUuid) + } + + suspend fun softDelete(id: Uuid) = dbQuery { + if (id.toString() == "00000000-0000-0000-0000-000000000000") { + throw BizException(ErrorCode.BAD_REQUEST.code, "系统保留用户不可删除") + } + UserDao.requireActive(id) + UserDao.softDelete(id) + } + + suspend fun updateStatus(id: Uuid, request: UpdateUserStatusRequest) = dbQuery { + UserDao.requireActive(id) + UserDao.updateStatus(id, request.status) + } + + suspend fun updatePassword(id: Uuid, request: UpdateUserPasswordRequest) = dbQuery { + val user = UserDao.requireActive(id) + if (request.password.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "密码不能为空") + } + UserDao.updatePassword( + id = id, + passwordHash = PasswordService.hash(request.password), + nextTokenVersion = user[com.bbit.ticket.database.system.SysUserTable.tokenVersion] + 1, + ) + } + + suspend fun updateRoles(id: Uuid, request: UpdateUserRolesRequest) = dbQuery { + UserDao.requireActive(id) + val roleIds = request.roleIds.distinct().map { parseUuid(it, "roleId") } + if (roleIds.isNotEmpty()) { + if (UserDao.countEnabledRoles(roleIds) != roleIds.size.toLong()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或已禁用角色") + } + } + UserDao.replaceRoles(id, roleIds) + } + + private fun ensureOrgExists(orgId: Uuid) { + if (!UserDao.orgExists(orgId)) { + throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message) + } + } +} diff --git a/server/src/main/kotlin/com/bbit/platform/common/DateTimeFormats.kt b/server/src/main/kotlin/com/bbit/ticket/utils/DateTimeFormats.kt similarity index 92% rename from server/src/main/kotlin/com/bbit/platform/common/DateTimeFormats.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/DateTimeFormats.kt index 68d344a..8c80c41 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/DateTimeFormats.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/DateTimeFormats.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.utils import java.time.OffsetDateTime import java.time.ZoneId diff --git a/server/src/main/kotlin/com/bbit/platform/common/RequestUtils.kt b/server/src/main/kotlin/com/bbit/ticket/utils/RequestUtils.kt similarity index 90% rename from server/src/main/kotlin/com/bbit/platform/common/RequestUtils.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/RequestUtils.kt index da2e452..378b915 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/RequestUtils.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/RequestUtils.kt @@ -1,5 +1,7 @@ -package com.bbit.platform.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 diff --git a/server/src/main/kotlin/com/bbit/platform/security/RequirePermission.kt b/server/src/main/kotlin/com/bbit/ticket/utils/RequirePermission.kt similarity index 77% rename from server/src/main/kotlin/com/bbit/platform/security/RequirePermission.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/RequirePermission.kt index 4df30a3..a7282e2 100644 --- a/server/src/main/kotlin/com/bbit/platform/security/RequirePermission.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/RequirePermission.kt @@ -1,7 +1,7 @@ -package com.bbit.platform.security +package com.bbit.ticket.utils -import com.bbit.platform.common.BizException -import com.bbit.platform.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 diff --git a/server/src/main/kotlin/com/bbit/platform/security/SecurityPrincipal.kt b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt similarity index 90% rename from server/src/main/kotlin/com/bbit/platform/security/SecurityPrincipal.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt index 4e1d089..9c943e4 100644 --- a/server/src/main/kotlin/com/bbit/platform/security/SecurityPrincipal.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt @@ -1,15 +1,15 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(ExperimentalUuidApi::class) -package com.bbit.platform.security +package com.bbit.ticket.utils -import com.bbit.platform.common.BizException -import com.bbit.platform.common.ErrorCode -import com.bbit.platform.database.system.SysMenuTable -import com.bbit.platform.database.system.SysRoleMenuTable -import com.bbit.platform.database.system.SysRoleTable -import com.bbit.platform.database.system.SysUserRoleTable -import com.bbit.platform.database.system.SysUserTable -import com.bbit.platform.plugins.dbQuery +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.plugins.dbQuery import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall import io.ktor.server.auth.principal @@ -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( diff --git a/server/src/main/kotlin/com/bbit/platform/common/TraceContext.kt b/server/src/main/kotlin/com/bbit/ticket/utils/TraceContext.kt similarity index 86% rename from server/src/main/kotlin/com/bbit/platform/common/TraceContext.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/TraceContext.kt index a643f4e..8ffde11 100644 --- a/server/src/main/kotlin/com/bbit/platform/common/TraceContext.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/TraceContext.kt @@ -1,4 +1,4 @@ -package com.bbit.platform.common +package com.bbit.ticket.utils import io.ktor.server.application.ApplicationCall import io.ktor.util.AttributeKey diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index 7caf9ab..75099cf 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -1,24 +1,24 @@ ktor: - application: - modules: - - com.bbit.platform.ApplicationKt.module - deployment: - port: 8070 + application: + modules: + - com.bbit.ticket.ApplicationKt.module + deployment: + port: 8070 app: - name: "Platform" + name: "Ticket" env: "local" database: - url: "jdbc:postgresql://localhost:5432/platform" - user: "platform" - password: "platform_password" + url: "jdbc:postgresql://localhost:5432/ticket" + user: "ticket" + password: "ticket_password" maximumPoolSize: 16 minimumIdle: 4 redis: url: "redis://127.0.0.1:6379" - password: "platform_password" + password: "ticket_password" security: jwt: diff --git a/server/src/test/kotlin/Test.kt b/server/src/test/kotlin/Test.kt new file mode 100644 index 0000000..1a312b7 --- /dev/null +++ b/server/src/test/kotlin/Test.kt @@ -0,0 +1,9 @@ +import kotlin.test.Test +import kotlin.test.assertEquals + +class Test { + @Test + fun helloWorld() { + print("Hello World!") + } +}