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/ticket/entity/system/Auth.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt similarity index 93% rename from server/src/main/kotlin/com/bbit/ticket/entity/system/Auth.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt index b250de2..55d9ac7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/system/Auth.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt @@ -1,6 +1,9 @@ +@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/Dict.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/DictDto.kt similarity index 99% rename from server/src/main/kotlin/com/bbit/ticket/entity/system/Dict.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/DictDto.kt index 474b405..dbde321 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/system/Dict.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/DictDto.kt @@ -1,5 +1,4 @@ package com.bbit.ticket.entity.system - import kotlinx.serialization.Serializable @Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Log.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/LogDto.kt similarity index 99% rename from server/src/main/kotlin/com/bbit/ticket/entity/system/Log.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/LogDto.kt index 4081db8..e3565ce 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/system/Log.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/LogDto.kt @@ -1,5 +1,4 @@ package com.bbit.ticket.entity.system - import kotlinx.serialization.Serializable @Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Menu.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/MenuDto.kt similarity index 100% rename from server/src/main/kotlin/com/bbit/ticket/entity/system/Menu.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/MenuDto.kt diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Org.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/OrgDto.kt similarity index 99% rename from server/src/main/kotlin/com/bbit/ticket/entity/system/Org.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/OrgDto.kt index f171658..266267e 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/system/Org.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/OrgDto.kt @@ -1,5 +1,4 @@ package com.bbit.ticket.entity.system - import kotlinx.serialization.Serializable @Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Role.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/RoleDto.kt similarity index 100% rename from server/src/main/kotlin/com/bbit/ticket/entity/system/Role.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/RoleDto.kt diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/User.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt similarity index 99% rename from server/src/main/kotlin/com/bbit/ticket/entity/system/User.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt index 3336083..6f2a564 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/system/User.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt @@ -1,5 +1,4 @@ package com.bbit.ticket.entity.system - import kotlinx.serialization.Serializable @Serializable 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 index 1ed2cfc..89a61b0 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt @@ -2,30 +2,19 @@ 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.database.system.SysMenuTable -import com.bbit.ticket.database.system.SysRoleMenuTable -import com.bbit.ticket.database.system.SysRoleTable -import com.bbit.ticket.database.system.SysUserRoleTable -import com.bbit.ticket.database.system.SysUserTable import com.bbit.ticket.entity.system.CurrentUserProfile import com.bbit.ticket.entity.system.LoginRequest import com.bbit.ticket.entity.system.LoginResponse import com.bbit.ticket.entity.system.MeResponse -import com.bbit.ticket.entity.system.MenuNode import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.CurrentUser 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.ExperimentalUuidApi -import kotlin.uuid.Uuid object AuthService { suspend fun login(request: LoginRequest, loginIp: String?): LoginResponse { @@ -34,17 +23,13 @@ object AuthService { 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( + 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[SysUserTable.passwordHash])) { + 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, @@ -52,50 +37,31 @@ object AuthService { ) } - if (user[SysUserTable.status] != "ENABLED") { + 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[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 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[SysUserTable.username], - orgId = user[SysUserTable.orgId]?.toString(), + username = user[com.bbit.ticket.database.system.SysUserTable.username], + orgId = user[com.bbit.ticket.database.system.SysUserTable.orgId]?.toString(), roles = roleCodes, - tokenVersion = user[SysUserTable.tokenVersion], + tokenVersion = user[com.bbit.ticket.database.system.SysUserTable.tokenVersion], ) - dbQuery { - SysUserTable.update({ SysUserTable.id eq userId }) { - it[lastLoginAt] = OffsetDateTime.now() - it[lastLoginIp] = loginIp - it[updatedAt] = OffsetDateTime.now() - } - } + dbQuery { UserDao.updateLoginInfo(userId, loginIp) } 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 userRow = dbQuery { UserDao.requireActive(currentUser.id) } val allMenus = loadMenusForUser(currentUser) - val menuTree = buildMenuTree(allMenus) + val menuTree = MenuDao.buildAuthTree(allMenus) val permissions = allMenus.mapNotNull { it.permission }.toSet() return MeResponse( @@ -112,97 +78,11 @@ object AuthService { ) } - 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() - } + private suspend fun loadMenusForUser(currentUser: CurrentUser) = dbQuery { + if (currentUser.isSuperAdmin) { + MenuDao.enabledMenusForSuperAdmin() } 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() - } - } + MenuDao.enabledMenusForRoleIds(UserDao.findEnabledRoleIds(currentUser.id)) } - - 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/ticket/service/system/DictService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt index 9bcba5b..3836f91 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt @@ -2,10 +2,7 @@ package com.bbit.ticket.service.system -import com.bbit.ticket.database.system.SysDictItemTable -import com.bbit.ticket.database.system.SysDictItemTable.color -import com.bbit.ticket.database.system.SysDictItemTable.label -import com.bbit.ticket.database.system.SysDictTypeTable +import com.bbit.ticket.dao.system.DictDao import com.bbit.ticket.entity.system.CreateDictItemRequest import com.bbit.ticket.entity.system.CreateDictTypeRequest import com.bbit.ticket.entity.system.DictItem @@ -15,172 +12,54 @@ import com.bbit.ticket.entity.system.UpdateDictTypeRequest import com.bbit.ticket.entity.common.BizException import com.bbit.ticket.entity.common.ErrorCode import com.bbit.ticket.entity.common.PageResult -import com.bbit.ticket.entity.common.statusLabel import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.parseUuid -import io.ktor.http.HttpStatusCode -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.core.like -import org.jetbrains.exposed.v1.core.or -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid object DictService { - suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult = 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 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, "字典类型编码和名称不能为空") } - 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() + if (DictDao.typeCodeExists(request.code.trim())) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "字典类型编码已存在") } - inserted[SysDictTypeTable.id].toString() + DictDao.createType(request) } 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() - } + DictDao.requireType(id) + DictDao.updateType(id, request) } 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() - } + 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 { - var where = SysDictItemTable.deletedAt.isNull() - if (typeId != null) where = where and (SysDictItemTable.typeId eq typeId) - val total = SysDictItemTable.selectAll().where { where }.count() - val rows = SysDictItemTable.selectAll().where { where } - .orderBy(SysDictItemTable.sort) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - PageResult( - items = rows.map { - DictItem( - id = it[SysDictItemTable.id].toString(), - typeId = it[SysDictItemTable.typeId].toString(), - label = it[label], - value = it[SysDictItemTable.value], - color = it[color], - sort = it[SysDictItemTable.sort], - status = it[SysDictItemTable.status], - statusLabel = statusLabel(it[SysDictItemTable.status]), - remark = it[SysDictItemTable.remark], - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } + suspend fun 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") - 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() + DictDao.requireType(typeId) + DictDao.createItem(request, typeId) } suspend fun updateItem(id: Uuid, request: UpdateDictItemRequest) = dbQuery { - requireItem(id) + DictDao.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() - } + DictDao.requireType(typeId) + DictDao.updateItem(id, request, typeId) } suspend fun deleteItem(id: Uuid) = dbQuery { - requireItem(id) - SysDictItemTable.update({ SysDictItemTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } + DictDao.requireItem(id) + DictDao.softDeleteItem(id) } - - 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 - ) } 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 index da50a5a..dc7846d 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt @@ -2,88 +2,17 @@ package com.bbit.ticket.service.system -import com.bbit.ticket.database.system.SysApiAccessLogTable -import com.bbit.ticket.database.system.SysOperationLogTable +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.utils.formatDateTime import com.bbit.ticket.plugins.dbQuery -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 import kotlin.uuid.ExperimentalUuidApi 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 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 { - 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, - ) - } -} \ No newline at end of file + 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 index fbdce50..8467d2d 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt @@ -2,160 +2,49 @@ package com.bbit.ticket.service.system -import com.bbit.ticket.database.system.SysMenuTable -import com.bbit.ticket.database.system.SysMenuTable.builtIn -import com.bbit.ticket.database.system.SysMenuTable.component -import com.bbit.ticket.database.system.SysMenuTable.icon -import com.bbit.ticket.database.system.SysMenuTable.keepAlive -import com.bbit.ticket.database.system.SysMenuTable.path -import com.bbit.ticket.database.system.SysMenuTable.permission -import com.bbit.ticket.database.system.SysMenuTable.visible -import com.bbit.ticket.database.system.SysRoleMenuTable +import com.bbit.ticket.dao.system.MenuDao import com.bbit.ticket.entity.system.CreateMenuRequest -import com.bbit.ticket.entity.system.MenuFlat import com.bbit.ticket.entity.system.MenuTreeNode import com.bbit.ticket.entity.system.UpdateMenuRequest import com.bbit.ticket.entity.common.BizException import com.bbit.ticket.entity.common.ErrorCode -import com.bbit.ticket.entity.common.menuTypeLabel -import com.bbit.ticket.entity.common.statusLabel import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.parseUuid -import io.ktor.http.HttpStatusCode -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid object MenuService { - suspend fun tree(): List = dbQuery { - val rows = SysMenuTable.selectAll().where { SysMenuTable.deletedAt.isNull() }.toList() - val flat = rows.map { - MenuFlat( - id = it[SysMenuTable.id], - parentId = it[SysMenuTable.parentId], - type = it[SysMenuTable.type], - title = it[SysMenuTable.title], - name = it[SysMenuTable.name], - path = it[path], - component = it[component], - icon = it[icon], - permission = it[permission], - sort = it[SysMenuTable.sort], - visible = it[visible], - keepAlive = it[keepAlive], - builtIn = it[builtIn], - status = it[SysMenuTable.status], - ) - } - buildTree(flat) - } + suspend fun 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) 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() + if (parentId != null) MenuDao.requireActive(parentId) + MenuDao.create(request, parentId) } suspend fun update(id: Uuid, request: UpdateMenuRequest) = dbQuery { - requireMenu(id) + 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) 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() - } + if (parentId != null) MenuDao.requireActive(parentId) + MenuDao.update(id, request, parentId) } suspend fun delete(id: Uuid) = dbQuery { - requireMenu(id) - val hasChildren = SysMenuTable.selectAll().where { - (SysMenuTable.parentId eq id) and SysMenuTable.deletedAt.isNull() - }.any() - if (hasChildren) throw BizException(ErrorCode.BAD_REQUEST.code, "存在子菜单,不能删除") - val referenced = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.menuId eq id }.any() - if (referenced) throw BizException(ErrorCode.BAD_REQUEST.code, "菜单已被角色引用,不能删除") - val row = requireMenu(id) - if (row[builtIn]) throw BizException(ErrorCode.BAD_REQUEST.code, "基础框架内置菜单不可删除") - SysMenuTable.update({ SysMenuTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() + 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 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) - } } 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 index 563296f..8fd7104 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt @@ -2,16 +2,10 @@ package com.bbit.ticket.service.system -import com.bbit.ticket.database.system.SysOperationLogTable -import com.bbit.ticket.utils.traceIdOrNull +import com.bbit.ticket.dao.system.LogDao import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.CurrentUser -import io.ktor.http.formUrlEncode import io.ktor.server.application.ApplicationCall -import io.ktor.server.request.httpMethod -import io.ktor.server.request.path -import org.jetbrains.exposed.v1.jdbc.insert -import java.time.OffsetDateTime import kotlin.uuid.ExperimentalUuidApi object OperationLogService { @@ -39,22 +33,6 @@ object OperationLogService { 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() - } + LogDao.saveOperationLog(call, currentUser, operationType, operationName, status, errorMessage, costMs) } -} \ No newline at end of file +} 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 index 2a95f62..9dfb62d 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt @@ -2,140 +2,54 @@ package com.bbit.ticket.service.system -import com.bbit.ticket.database.system.SysOrgTable -import com.bbit.ticket.database.system.SysUserTable +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.entity.common.statusLabel import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.parseUuid -import io.ktor.http.HttpStatusCode -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid object 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 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, "组织名称和编码不能为空") } - val exists = SysOrgTable.selectAll().where { - (SysOrgTable.code eq code) and SysOrgTable.deletedAt.isNull() - }.any() - if (exists) { + if (OrgDao.codeExists(code)) { 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() + if (parentId != null) OrgDao.requireActive(parentId) + OrgDao.create(request, parentId) } suspend fun update(id: Uuid, request: UpdateOrgRequest) = dbQuery { - requireOrg(id) + OrgDao.requireActive(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() - } + if (parentId != null) OrgDao.requireActive(parentId) + OrgDao.update(id, request, parentId) } suspend fun delete(id: Uuid) = dbQuery { - val org = requireOrg(id) - if (org[SysOrgTable.code] == "DEFAULT_ORG") { + val org = OrgDao.requireActive(id) + if (org[com.bbit.ticket.database.system.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) { + if (OrgDao.hasChildren(id)) { throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在子组织,不能删除") } - val hasUsers = SysUserTable.selectAll() - .where { (SysUserTable.orgId eq id) and SysUserTable.deletedAt.isNull() } - .any() - if (hasUsers) { + if (OrgDao.hasUsers(id)) { throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在用户,不能删除") } - SysOrgTable.update({ SysOrgTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } + OrgDao.softDelete(id) } - - 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, - ) } 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 index 8ef89a0..1d43dfb 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt @@ -2,164 +2,61 @@ package com.bbit.ticket.service.system -import com.bbit.ticket.database.system.SysMenuTable -import com.bbit.ticket.database.system.SysRoleMenuTable -import com.bbit.ticket.database.system.SysRoleTable -import com.bbit.ticket.database.system.SysRoleTable.dataScope -import com.bbit.ticket.database.system.SysUserRoleTable +import com.bbit.ticket.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.entity.common.dataScopeLabel -import com.bbit.ticket.entity.common.statusLabel -import com.bbit.ticket.entity.system.UpdateRoleMenusRequest -import com.bbit.ticket.entity.system.UpdateRoleRequest import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.parseUuid -import io.ktor.http.HttpStatusCode -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.inList -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.core.like -import org.jetbrains.exposed.v1.core.or -import org.jetbrains.exposed.v1.jdbc.deleteWhere -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.insertIgnore -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid object RoleService { @OptIn(ExperimentalUuidApi::class) - suspend fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = dbQuery { - var where = SysRoleTable.deletedAt.isNull() - if (!keyword.isNullOrBlank()) { - where = where and ((SysRoleTable.name like "%$keyword%") or (SysRoleTable.code like "%$keyword%")) - } - if (!status.isNullOrBlank()) { - where = where and (SysRoleTable.status eq status) - } - val total = SysRoleTable.selectAll().where { where }.count() - val rows = SysRoleTable.selectAll().where { where } - .orderBy(SysRoleTable.createdAt) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - PageResult( - items = rows.map { - RoleItem( - id = it[SysRoleTable.id].toString(), - name = it[SysRoleTable.name], - code = it[SysRoleTable.code], - description = it[SysRoleTable.description], - status = it[SysRoleTable.status], - statusLabel = statusLabel(it[SysRoleTable.status]), - dataScope = it[dataScope], - dataScopeLabel = dataScopeLabel(it[dataScope]), - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } + suspend fun 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, "角色名称和编码不能为空") } - 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() + if (RoleDao.codeExists(request.code.trim())) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "角色编码已存在") } - inserted[SysRoleTable.id].toString() + RoleDao.create(request) } - suspend fun detail(id: Uuid): RoleDetail = dbQuery { - val role = requireRole(id) - val menuIds = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.roleId eq id } - .map { it[SysRoleMenuTable.menuId].toString() } - RoleDetail( - id = role[SysRoleTable.id].toString(), - name = role[SysRoleTable.name], - code = role[SysRoleTable.code], - description = role[SysRoleTable.description], - status = role[SysRoleTable.status], - statusLabel = statusLabel(role[SysRoleTable.status]), - dataScope = role[dataScope], - dataScopeLabel = dataScopeLabel(role[dataScope]), - menuIds = menuIds, - ) - } + suspend fun detail(id: Uuid): RoleDetail = dbQuery { RoleDao.detail(id) } 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() - } + RoleDao.requireActive(id) + RoleDao.update(id, request) } suspend fun delete(id: Uuid) = dbQuery { - val role = requireRole(id) - if (role[SysRoleTable.code] == "SUPER_ADMIN") { + val role = RoleDao.requireActive(id) + if (role[com.bbit.ticket.database.system.SysRoleTable.code] == "SUPER_ADMIN") { throw BizException(ErrorCode.BAD_REQUEST.code, "超级管理员角色不可删除") } - val inUse = SysUserRoleTable.selectAll().where { SysUserRoleTable.roleId eq id }.any() - if (inUse) { + if (RoleDao.inUse(id)) { throw BizException(ErrorCode.BAD_REQUEST.code, "角色已被用户使用,不能删除") } - SysRoleTable.update({ SysRoleTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id } + RoleDao.softDelete(id) } suspend fun updateMenus(id: Uuid, request: UpdateRoleMenusRequest) = dbQuery { - requireRole(id) + RoleDao.requireActive(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()) { + if (RoleDao.countEnabledMenus(menuIds) != 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 - } - } + RoleDao.replaceMenus(id, menuIds) } - - 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 - ) } 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 index c7d0c9d..a11e6a9 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt @@ -1,14 +1,6 @@ package com.bbit.ticket.service.system -import com.bbit.ticket.database.system.SysOrgTable -import com.bbit.ticket.database.system.SysRoleTable -import com.bbit.ticket.database.system.SysUserRoleTable -import com.bbit.ticket.database.system.SysUserTable -import com.bbit.ticket.database.system.SysUserTable.avatar -import com.bbit.ticket.database.system.SysUserTable.email -import com.bbit.ticket.database.system.SysUserTable.nickname -import com.bbit.ticket.database.system.SysUserTable.phone -import com.bbit.ticket.database.system.SysUserTable.realName +import com.bbit.ticket.dao.system.UserDao import com.bbit.ticket.entity.system.CreateUserRequest import com.bbit.ticket.entity.system.UpdateUserPasswordRequest import com.bbit.ticket.entity.system.UpdateUserRequest @@ -19,22 +11,8 @@ import com.bbit.ticket.entity.system.UserListItem import com.bbit.ticket.entity.common.BizException import com.bbit.ticket.entity.common.ErrorCode import com.bbit.ticket.entity.common.PageResult -import com.bbit.ticket.entity.common.statusLabel import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.parseUuid -import io.ktor.http.HttpStatusCode -import org.jetbrains.exposed.v1.core.Op -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.inList -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.core.like -import org.jetbrains.exposed.v1.jdbc.deleteWhere -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.insertIgnore -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -47,57 +25,14 @@ object UserService { 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[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, - ) - } + ): 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, "用户名和密码不能为空") } - val existed = SysUserTable.selectAll().where { - (SysUserTable.username eq username) and SysUserTable.deletedAt.isNull() - }.any() - if (existed) { + if (UserDao.findByUsername(username) != null) { throw BizException(ErrorCode.DATA_CONFLICT.code, "用户名已存在") } @@ -105,142 +40,59 @@ object UserService { 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() + UserDao.create(request, PasswordService.hash(request.password), orgUuid) } - suspend fun detail(id: Uuid): UserDetailResponse = dbQuery { - val user = requireUser(id) - val roleIds = SysUserRoleTable.selectAll().where { SysUserRoleTable.userId eq id } - .map { it[SysUserRoleTable.roleId].toString() } - UserDetailResponse( - id = user[SysUserTable.id].toString(), - username = user[SysUserTable.username], - nickname = user[nickname], - realName = user[realName], - phone = user[phone], - email = user[email], - avatar = user[avatar], - orgId = user[SysUserTable.orgId]?.toString(), - status = user[SysUserTable.status], - statusLabel = statusLabel(user[SysUserTable.status]), - roleIds = roleIds, - ) - } + suspend fun detail(id: Uuid): UserDetailResponse = dbQuery { UserDao.detail(id) } suspend fun update(id: Uuid, request: UpdateUserRequest) = dbQuery { - requireUser(id) + UserDao.requireActive(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() - } + 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, "系统保留用户不可删除") } - requireUser(id) - SysUserTable.update({ SysUserTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id } + UserDao.requireActive(id) + UserDao.softDelete(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() - } + UserDao.requireActive(id) + UserDao.updateStatus(id, request.status) } suspend fun updatePassword(id: Uuid, request: UpdateUserPasswordRequest) = dbQuery { - val user = requireUser(id) + val user = UserDao.requireActive(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() - } + 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 { - requireUser(id) + UserDao.requireActive(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()) { + if (UserDao.countEnabledRoles(roleIds) != 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 - } - } + UserDao.replaceRoles(id, roleIds) } - 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) + if (!UserDao.orgExists(orgId)) { + throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message) } } } 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!") + } +}