优化基础框架

This commit is contained in:
BBIT-Kai
2026-05-07 11:06:05 +08:00
parent f7a27d99e1
commit 9fa7f89666
23 changed files with 1139 additions and 900 deletions
@@ -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<DictTypeItem> {
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<DictItem> {
var where: Op<Boolean> = 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<Boolean> {
var where: Op<Boolean> = 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],
)
}
@@ -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<OperationLogItem> {
var where: Op<Boolean> = 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<ApiAccessLogItem> {
var where: Op<Boolean> = 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()
}
}
}
@@ -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<MenuTreeNode> = 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<Uuid>): List<MenuFlat> {
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<MenuFlat> { it.sort }.thenBy { it.id.toString() })
}
fun enabledMenusForSuperAdmin(): List<MenuFlat> =
SysMenuTable.selectAll()
.where { SysMenuTable.deletedAt.isNull() and (SysMenuTable.status eq "ENABLED") }
.toList()
.map { it.toMenuFlat() }
.sortedWith(compareBy<MenuFlat> { it.sort }.thenBy { it.id.toString() })
fun buildAuthTree(flatMenus: List<MenuFlat>): List<MenuNode> {
val parentMap = flatMenus.groupBy { it.parentId }
fun build(parentId: Uuid?): List<MenuNode> =
(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<MenuFlat> =
SysMenuTable.selectAll()
.where { SysMenuTable.deletedAt.isNull() }
.toList()
.map { it.toMenuFlat() }
private fun buildTree(items: List<MenuFlat>): List<MenuTreeNode> {
val grouped = items.groupBy { it.parentId }
fun children(parentId: Uuid?): List<MenuTreeNode> =
(grouped[parentId] ?: emptyList()).sortedBy { it.sort }.map { menu ->
MenuTreeNode(
id = menu.id.toString(),
parentId = menu.parentId?.toString(),
type = menu.type,
typeLabel = menuTypeLabel(menu.type),
title = menu.title,
name = menu.name,
path = menu.path,
component = menu.component,
icon = menu.icon,
permission = menu.permission,
sort = menu.sort,
visible = menu.visible,
keepAlive = menu.keepAlive,
builtIn = menu.builtIn,
status = menu.status,
statusLabel = statusLabel(menu.status),
children = children(menu.id),
)
}
return children(null)
}
private 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],
)
}
@@ -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<OrgTreeNode> {
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<OrgNodeFlat>): List<OrgTreeNode> {
val byParent = nodes.groupBy { it.parentId }
fun children(parentId: Uuid?): List<OrgTreeNode> =
(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,
)
}
@@ -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<RoleItem> {
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<Uuid>): Long =
SysMenuTable.selectAll().where {
(SysMenuTable.id inList menuIds) and
SysMenuTable.deletedAt.isNull() and
(SysMenuTable.status eq "ENABLED")
}.count()
fun replaceMenus(id: Uuid, menuIds: List<Uuid>) {
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<Boolean> {
var where: Op<Boolean> = 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<String>) = 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,
)
}
@@ -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() }
@@ -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<UserListItem> {
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<Uuid>) {
SysUserRoleTable.deleteWhere { userId eq id }
roleIds.forEach { roleId ->
SysUserRoleTable.insertIgnore {
it[userId] = id
it[SysUserRoleTable.roleId] = roleId
}
}
}
fun countEnabledRoles(roleIds: List<Uuid>): 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<String> =
(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<Uuid> =
(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<Boolean> {
var where: Op<Boolean> = SysUserTable.deletedAt.isNull()
if (!username.isNullOrBlank()) {
where = where and (SysUserTable.username like "%$username%")
}
if (!nickname.isNullOrBlank()) {
where = where and (SysUserTable.nickname like "%$nickname%")
}
if (!status.isNullOrBlank()) {
where = where and (SysUserTable.status eq status)
}
if (orgId != null) {
where = where and (SysUserTable.orgId eq orgId)
}
return where
}
private fun findRoleCodesByUserIds(userIds: List<Uuid>): Map<Uuid, List<String>> {
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<String>) = 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<String>) = 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,
)
}
@@ -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<MenuNode> = emptyList(),
)
@@ -1,5 +1,4 @@
package com.bbit.ticket.entity.system
import kotlinx.serialization.Serializable
@Serializable
@@ -1,5 +1,4 @@
package com.bbit.ticket.entity.system
import kotlinx.serialization.Serializable
@Serializable
@@ -1,5 +1,4 @@
package com.bbit.ticket.entity.system
import kotlinx.serialization.Serializable
@Serializable
@@ -1,5 +1,4 @@
package com.bbit.ticket.entity.system
import kotlinx.serialization.Serializable
@Serializable
@@ -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<MenuFlat> {
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<MenuFlat> { it.sort }.thenBy { it.id.toString() })
}
private fun buildMenuTree(flatMenus: List<MenuFlat>): List<MenuNode> {
val parentMap = flatMenus.groupBy { it.parentId }
fun build(parentId: Uuid?): List<MenuNode> =
(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,
)
}
@@ -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<DictTypeItem> = dbQuery {
var where = SysDictTypeTable.deletedAt.isNull()
if (!keyword.isNullOrBlank()) {
where = where and ((SysDictTypeTable.code like "%$keyword%") or (SysDictTypeTable.name like "%$keyword%"))
}
val total = SysDictTypeTable.selectAll().where { where }.count()
val rows = SysDictTypeTable.selectAll().where { where }
.orderBy(SysDictTypeTable.createdAt)
.limit(pageSize)
.offset(((page - 1) * pageSize).toLong())
.toList()
PageResult(
items = rows.map {
DictTypeItem(
id = it[SysDictTypeTable.id].toString(),
code = it[SysDictTypeTable.code],
name = it[SysDictTypeTable.name],
status = it[SysDictTypeTable.status],
statusLabel = statusLabel(it[SysDictTypeTable.status]),
remark = it[SysDictTypeTable.remark],
)
},
page = page,
pageSize = pageSize,
total = total,
)
}
suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult<DictTypeItem> =
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<DictItem> = dbQuery {
var where = SysDictItemTable.deletedAt.isNull()
if (typeId != null) where = where and (SysDictItemTable.typeId eq typeId)
val total = SysDictItemTable.selectAll().where { where }.count()
val rows = SysDictItemTable.selectAll().where { where }
.orderBy(SysDictItemTable.sort)
.limit(pageSize)
.offset(((page - 1) * pageSize).toLong())
.toList()
PageResult(
items = rows.map {
DictItem(
id = it[SysDictItemTable.id].toString(),
typeId = it[SysDictItemTable.typeId].toString(),
label = it[label],
value = it[SysDictItemTable.value],
color = it[color],
sort = it[SysDictItemTable.sort],
status = it[SysDictItemTable.status],
statusLabel = statusLabel(it[SysDictItemTable.status]),
remark = it[SysDictItemTable.remark],
)
},
page = page,
pageSize = pageSize,
total = total,
)
}
suspend fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult<DictItem> =
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
)
}
@@ -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<OperationLogItem> = dbQuery {
var where: Op<Boolean> = 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<OperationLogItem> =
dbQuery { LogDao.operationLogs(page, pageSize, keyword, status) }
suspend fun apiAccessLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<ApiAccessLogItem> = dbQuery {
var where: Op<Boolean> = 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,
)
}
}
suspend fun apiAccessLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<ApiAccessLogItem> =
dbQuery { LogDao.apiAccessLogs(page, pageSize, keyword, status) }
}
@@ -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<MenuTreeNode> = dbQuery {
val rows = SysMenuTable.selectAll().where { SysMenuTable.deletedAt.isNull() }.toList()
val flat = rows.map {
MenuFlat(
id = it[SysMenuTable.id],
parentId = it[SysMenuTable.parentId],
type = it[SysMenuTable.type],
title = it[SysMenuTable.title],
name = it[SysMenuTable.name],
path = it[path],
component = it[component],
icon = it[icon],
permission = it[permission],
sort = it[SysMenuTable.sort],
visible = it[visible],
keepAlive = it[keepAlive],
builtIn = it[builtIn],
status = it[SysMenuTable.status],
)
}
buildTree(flat)
}
suspend fun tree(): List<MenuTreeNode> = 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<MenuFlat>): List<MenuTreeNode> {
val grouped = items.groupBy { it.parentId }
fun children(parentId: Uuid?): List<MenuTreeNode> =
(grouped[parentId] ?: emptyList()).sortedBy { it.sort }.map { menu ->
MenuTreeNode(
id = menu.id.toString(),
parentId = menu.parentId?.toString(),
type = menu.type,
typeLabel = menuTypeLabel(menu.type),
title = menu.title,
name = menu.name,
path = menu.path,
component = menu.component,
icon = menu.icon,
permission = menu.permission,
sort = menu.sort,
visible = menu.visible,
keepAlive = menu.keepAlive,
builtIn = menu.builtIn,
status = menu.status,
statusLabel = statusLabel(menu.status),
children = children(menu.id),
)
}
return children(null)
}
}
@@ -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)
}
}
}
@@ -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<OrgTreeNode> = dbQuery {
val rows = SysOrgTable.selectAll()
.where { SysOrgTable.deletedAt.isNull() }
.orderBy(SysOrgTable.sort)
.toList()
val nodes = rows.map(::toNode)
buildTree(nodes)
}
suspend fun tree(): List<OrgTreeNode> = 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<OrgNodeFlat>): List<OrgTreeNode> {
val byParent = nodes.groupBy { it.parentId }
fun children(parentId: Uuid?): List<OrgTreeNode> =
(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,
)
}
@@ -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<RoleItem> = dbQuery {
var where = SysRoleTable.deletedAt.isNull()
if (!keyword.isNullOrBlank()) {
where = where and ((SysRoleTable.name like "%$keyword%") or (SysRoleTable.code like "%$keyword%"))
}
if (!status.isNullOrBlank()) {
where = where and (SysRoleTable.status eq status)
}
val total = SysRoleTable.selectAll().where { where }.count()
val rows = SysRoleTable.selectAll().where { where }
.orderBy(SysRoleTable.createdAt)
.limit(pageSize)
.offset(((page - 1) * pageSize).toLong())
.toList()
PageResult(
items = rows.map {
RoleItem(
id = it[SysRoleTable.id].toString(),
name = it[SysRoleTable.name],
code = it[SysRoleTable.code],
description = it[SysRoleTable.description],
status = it[SysRoleTable.status],
statusLabel = statusLabel(it[SysRoleTable.status]),
dataScope = it[dataScope],
dataScopeLabel = dataScopeLabel(it[dataScope]),
)
},
page = page,
pageSize = pageSize,
total = total,
)
}
suspend fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<RoleItem> =
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
)
}
@@ -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<UserListItem> = dbQuery {
val where = buildWhere(username, nickname, status, orgId)
val total = SysUserTable.selectAll().where { where }.count()
val rows = SysUserTable.selectAll()
.where { where }
.orderBy(SysUserTable.createdAt)
.limit(pageSize)
.offset(((page - 1) * pageSize).toLong())
.toList()
val userIds = rows.map { it[SysUserTable.id] }
val roleMap = if (userIds.isEmpty()) {
emptyMap()
} else {
(SysUserRoleTable innerJoin SysRoleTable).selectAll()
.where {
(SysUserRoleTable.userId inList userIds) and
SysRoleTable.deletedAt.isNull()
}
.groupBy { it[SysUserRoleTable.userId] }
.mapValues { entry -> entry.value.map { row -> row[SysRoleTable.code] }.distinct() }
}
PageResult(
items = rows.map { row ->
UserListItem(
id = row[SysUserTable.id].toString(),
username = row[SysUserTable.username],
nickname = row[SysUserTable.nickname],
realName = row[realName],
orgId = row[SysUserTable.orgId]?.toString(),
status = row[SysUserTable.status],
statusLabel = statusLabel(row[SysUserTable.status]),
roleCodes = roleMap[row[SysUserTable.id]] ?: emptyList(),
)
},
page = page,
pageSize = pageSize,
total = total,
)
}
): PageResult<UserListItem> = 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<Boolean> {
var where: Op<Boolean> = SysUserTable.deletedAt.isNull()
if (!username.isNullOrBlank()) {
where = where and (SysUserTable.username like "%$username%")
}
if (!nickname.isNullOrBlank()) {
where = where and (SysUserTable.nickname like "%$nickname%")
}
if (!status.isNullOrBlank()) {
where = where and (SysUserTable.status eq status)
}
if (orgId != null) {
where = where and (SysUserTable.orgId eq orgId)
}
return where
}
private fun requireUser(id: Uuid) =
SysUserTable.selectAll().where { (SysUserTable.id eq id) and SysUserTable.deletedAt.isNull() }.singleOrNull()
?: throw BizException(
ErrorCode.USER_NOT_FOUND.code,
ErrorCode.USER_NOT_FOUND.message,
HttpStatusCode.NotFound
)
private fun ensureOrgExists(orgId: Uuid) {
val exists = SysOrgTable.selectAll().where { (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() }.any()
if (!exists) {
throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message, HttpStatusCode.BadRequest)
if (!UserDao.orgExists(orgId)) {
throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message)
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import kotlin.test.Test
import kotlin.test.assertEquals
class Test {
@Test
fun helloWorld() {
print("Hello World!")
}
}