From f7a27d99e1f2d6633105ed8f1f386ff0d464820f Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Thu, 7 May 2026 10:25:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=8E=E7=AB=AF=E6=A1=86?= =?UTF-8?q?=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/代理商平台.txt | 1 - .../Codex三期工程执行文档.md | 0 .../docker-compose.yml | 0 .../C#仅供参考组装报文和加密/EncryptDes.cs | 0 .../old}/C#仅供参考组装报文和加密/PostJson.cs | 0 .../old}/C#仅供参考组装报文和加密/Program.cs | 0 .../C#仅供参考组装报文和加密/PublicData.cs | 0 .../old}/C#仅供参考组装报文和加密/RSA.cs | 0 .../old}/C#仅供参考组装报文和加密/StarDEMO.cs | 0 .../old}/C#仅供参考组装报文和加密/ToJson.cs | 0 .../old}/票通数电发票接口文档3.3.5.txt | 0 .../old}/票通数电平台对接指引v1.0(2).txt | 0 .../三期工程实施文档.md | 0 .../待开发功能梳理.md | 0 {doc => history}/票通公司介绍.pdf | Bin .../票通对接业务流程说明.md | 0 系统设计.md => history/系统设计.md | 0 .../通用票通开票模块设计书.md | 0 server/build.gradle.kts | 7 + .../kotlin/com/bbit/ticket/Application.kt | 18 +- .../ticket/{config => bootstrap}/AppConfig.kt | 4 +- .../ticket/bootstrap/DatabaseInitializer.kt | 3 - .../com/bbit/ticket/bootstrap/SeedData.kt | 2 +- .../ticket/{ => entity}/common/ApiResult.kt | 2 +- .../{ => entity}/common/BizException.kt | 2 +- .../{ => entity}/common/DisplayLabels.kt | 2 +- .../ticket/{ => entity}/common/ErrorCode.kt | 2 +- .../ticket/{ => entity}/common/PageQuery.kt | 2 +- .../ticket/{ => entity}/common/PageResult.kt | 2 +- .../AuthDtos.kt => entity/system/Auth.kt} | 2 +- .../com/bbit/ticket/entity/system/Dict.kt | 54 +++ .../com/bbit/ticket/entity/system/Log.kt | 33 ++ .../com/bbit/ticket/entity/system/Menu.kt | 77 ++++ .../com/bbit/ticket/entity/system/Org.kt | 32 ++ .../com/bbit/ticket/entity/system/Role.kt | 48 ++ .../com/bbit/ticket/entity/system/User.kt | 62 +++ .../ticket/modules/system/dict/DictModule.kt | 338 -------------- .../ticket/modules/system/menu/MenuModule.kt | 280 ------------ .../ticket/modules/system/role/RoleModule.kt | 285 ------------ .../ticket/modules/system/user/UserModule.kt | 421 ------------------ .../bbit/ticket/plugins/ApiAccessLogPlugin.kt | 2 +- .../com/bbit/ticket/plugins/CorsPlugin.kt | 11 +- .../com/bbit/ticket/plugins/DatabasePlugin.kt | 2 +- .../com/bbit/ticket/plugins/LoggingPlugin.kt | 3 +- .../com/bbit/ticket/plugins/RedisPlugin.kt | 2 +- .../com/bbit/ticket/plugins/SecurityPlugin.kt | 8 +- .../bbit/ticket/plugins/StatusPagesPlugin.kt | 8 +- .../com/bbit/ticket/plugins/TracePlugin.kt | 2 +- .../system/registerAuthRoutes.kt} | 12 +- .../ticket/route/system/registerDictRoutes.kt | 128 ++++++ .../route/system/registerLogsQueryRoutes.kt | 31 ++ .../ticket/route/system/registerMenuRoutes.kt | 73 +++ .../ticket/route/system/registerOrgRoutes.kt | 73 +++ .../ticket/route/system/registerRoleRoutes.kt | 97 ++++ .../ticket/route/system/registerUserRoutes.kt | 142 ++++++ .../auth => service/system}/AuthService.kt | 48 +- .../bbit/ticket/service/system/DictService.kt | 186 ++++++++ .../system}/JwtService.kt | 7 +- .../system/LogsQueryService.kt} | 72 +-- .../bbit/ticket/service/system/MenuService.kt | 161 +++++++ .../system}/OperationLogService.kt | 11 +- .../system/OrgService.kt} | 133 +----- .../system}/PasswordService.kt | 5 +- .../bbit/ticket/service/system/RoleService.kt | 165 +++++++ .../bbit/ticket/service/system/UserService.kt | 246 ++++++++++ .../{common => utils}/DateTimeFormats.kt | 2 +- .../ticket/{common => utils}/RequestUtils.kt | 4 +- .../{security => utils}/RequirePermission.kt | 6 +- .../{security => utils}/SecurityPrincipal.kt | 9 +- .../ticket/{common => utils}/TraceContext.kt | 2 +- web/src/api/http.ts | 4 +- web/src/components/AppMenu.vue | 2 +- web/vite.config.ts | 2 +- 73 files changed, 1742 insertions(+), 1596 deletions(-) delete mode 100644 doc/代理商平台.txt rename Codex三期工程执行文档.md => history/Codex三期工程执行文档.md (100%) rename docker-compose.yml => history/docker-compose.yml (100%) rename {doc => history/old}/C#仅供参考组装报文和加密/EncryptDes.cs (100%) rename {doc => history/old}/C#仅供参考组装报文和加密/PostJson.cs (100%) rename {doc => history/old}/C#仅供参考组装报文和加密/Program.cs (100%) rename {doc => history/old}/C#仅供参考组装报文和加密/PublicData.cs (100%) rename {doc => history/old}/C#仅供参考组装报文和加密/RSA.cs (100%) rename {doc => history/old}/C#仅供参考组装报文和加密/StarDEMO.cs (100%) rename {doc => history/old}/C#仅供参考组装报文和加密/ToJson.cs (100%) rename {doc => history/old}/票通数电发票接口文档3.3.5.txt (100%) rename {doc => history/old}/票通数电平台对接指引v1.0(2).txt (100%) rename 三期工程实施文档.md => history/三期工程实施文档.md (100%) rename 待开发功能梳理.md => history/待开发功能梳理.md (100%) rename {doc => history}/票通公司介绍.pdf (100%) rename 票通对接业务流程说明.md => history/票通对接业务流程说明.md (100%) rename 系统设计.md => history/系统设计.md (100%) rename 通用票通开票模块设计书.md => history/通用票通开票模块设计书.md (100%) rename server/src/main/kotlin/com/bbit/ticket/{config => bootstrap}/AppConfig.kt (98%) rename server/src/main/kotlin/com/bbit/ticket/{ => entity}/common/ApiResult.kt (92%) rename server/src/main/kotlin/com/bbit/ticket/{ => entity}/common/BizException.kt (84%) rename server/src/main/kotlin/com/bbit/ticket/{ => entity}/common/DisplayLabels.kt (93%) rename server/src/main/kotlin/com/bbit/ticket/{ => entity}/common/ErrorCode.kt (96%) rename server/src/main/kotlin/com/bbit/ticket/{ => entity}/common/PageQuery.kt (90%) rename server/src/main/kotlin/com/bbit/ticket/{ => entity}/common/PageResult.kt (82%) rename server/src/main/kotlin/com/bbit/ticket/{modules/auth/AuthDtos.kt => entity/system/Auth.kt} (96%) create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/system/Dict.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/system/Log.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/system/Menu.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/system/Org.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/system/Role.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/system/User.kt delete mode 100644 server/src/main/kotlin/com/bbit/ticket/modules/system/dict/DictModule.kt delete mode 100644 server/src/main/kotlin/com/bbit/ticket/modules/system/menu/MenuModule.kt delete mode 100644 server/src/main/kotlin/com/bbit/ticket/modules/system/role/RoleModule.kt delete mode 100644 server/src/main/kotlin/com/bbit/ticket/modules/system/user/UserModule.kt rename server/src/main/kotlin/com/bbit/ticket/{modules/auth/AuthRoutes.kt => route/system/registerAuthRoutes.kt} (83%) create mode 100644 server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt rename server/src/main/kotlin/com/bbit/ticket/{modules/auth => service/system}/AuthService.kt (88%) create mode 100644 server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt rename server/src/main/kotlin/com/bbit/ticket/{security => service/system}/JwtService.kt (92%) rename server/src/main/kotlin/com/bbit/ticket/{modules/logs/LogsQueryModule.kt => service/system/LogsQueryService.kt} (65%) create mode 100644 server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt rename server/src/main/kotlin/com/bbit/ticket/{modules/logs => service/system}/OperationLogService.kt (91%) rename server/src/main/kotlin/com/bbit/ticket/{modules/system/org/OrgModule.kt => service/system/OrgService.kt} (52%) rename server/src/main/kotlin/com/bbit/ticket/{security => service/system}/PasswordService.kt (86%) create mode 100644 server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt rename server/src/main/kotlin/com/bbit/ticket/{common => utils}/DateTimeFormats.kt (93%) rename server/src/main/kotlin/com/bbit/ticket/{common => utils}/RequestUtils.kt (90%) rename server/src/main/kotlin/com/bbit/ticket/{security => utils}/RequirePermission.kt (77%) rename server/src/main/kotlin/com/bbit/ticket/{security => utils}/SecurityPrincipal.kt (96%) rename server/src/main/kotlin/com/bbit/ticket/{common => utils}/TraceContext.kt (87%) diff --git a/doc/代理商平台.txt b/doc/代理商平台.txt deleted file mode 100644 index eae1a45..0000000 --- a/doc/代理商平台.txt +++ /dev/null @@ -1 +0,0 @@ -代理商后台入口https://dl.vpiaotong.com/#/login 邀请码:K1N9TP 密码:www.bbitcn.com \ No newline at end of file diff --git a/Codex三期工程执行文档.md b/history/Codex三期工程执行文档.md similarity index 100% rename from Codex三期工程执行文档.md rename to history/Codex三期工程执行文档.md diff --git a/docker-compose.yml b/history/docker-compose.yml similarity index 100% rename from docker-compose.yml rename to history/docker-compose.yml diff --git a/doc/C#仅供参考组装报文和加密/EncryptDes.cs b/history/old/C#仅供参考组装报文和加密/EncryptDes.cs similarity index 100% rename from doc/C#仅供参考组装报文和加密/EncryptDes.cs rename to history/old/C#仅供参考组装报文和加密/EncryptDes.cs diff --git a/doc/C#仅供参考组装报文和加密/PostJson.cs b/history/old/C#仅供参考组装报文和加密/PostJson.cs similarity index 100% rename from doc/C#仅供参考组装报文和加密/PostJson.cs rename to history/old/C#仅供参考组装报文和加密/PostJson.cs diff --git a/doc/C#仅供参考组装报文和加密/Program.cs b/history/old/C#仅供参考组装报文和加密/Program.cs similarity index 100% rename from doc/C#仅供参考组装报文和加密/Program.cs rename to history/old/C#仅供参考组装报文和加密/Program.cs diff --git a/doc/C#仅供参考组装报文和加密/PublicData.cs b/history/old/C#仅供参考组装报文和加密/PublicData.cs similarity index 100% rename from doc/C#仅供参考组装报文和加密/PublicData.cs rename to history/old/C#仅供参考组装报文和加密/PublicData.cs diff --git a/doc/C#仅供参考组装报文和加密/RSA.cs b/history/old/C#仅供参考组装报文和加密/RSA.cs similarity index 100% rename from doc/C#仅供参考组装报文和加密/RSA.cs rename to history/old/C#仅供参考组装报文和加密/RSA.cs diff --git a/doc/C#仅供参考组装报文和加密/StarDEMO.cs b/history/old/C#仅供参考组装报文和加密/StarDEMO.cs similarity index 100% rename from doc/C#仅供参考组装报文和加密/StarDEMO.cs rename to history/old/C#仅供参考组装报文和加密/StarDEMO.cs diff --git a/doc/C#仅供参考组装报文和加密/ToJson.cs b/history/old/C#仅供参考组装报文和加密/ToJson.cs similarity index 100% rename from doc/C#仅供参考组装报文和加密/ToJson.cs rename to history/old/C#仅供参考组装报文和加密/ToJson.cs diff --git a/doc/票通数电发票接口文档3.3.5.txt b/history/old/票通数电发票接口文档3.3.5.txt similarity index 100% rename from doc/票通数电发票接口文档3.3.5.txt rename to history/old/票通数电发票接口文档3.3.5.txt diff --git a/doc/票通数电平台对接指引v1.0(2).txt b/history/old/票通数电平台对接指引v1.0(2).txt similarity index 100% rename from doc/票通数电平台对接指引v1.0(2).txt rename to history/old/票通数电平台对接指引v1.0(2).txt diff --git a/三期工程实施文档.md b/history/三期工程实施文档.md similarity index 100% rename from 三期工程实施文档.md rename to history/三期工程实施文档.md diff --git a/待开发功能梳理.md b/history/待开发功能梳理.md similarity index 100% rename from 待开发功能梳理.md rename to history/待开发功能梳理.md diff --git a/doc/票通公司介绍.pdf b/history/票通公司介绍.pdf similarity index 100% rename from doc/票通公司介绍.pdf rename to history/票通公司介绍.pdf diff --git a/票通对接业务流程说明.md b/history/票通对接业务流程说明.md similarity index 100% rename from 票通对接业务流程说明.md rename to history/票通对接业务流程说明.md diff --git a/系统设计.md b/history/系统设计.md similarity index 100% rename from 系统设计.md rename to history/系统设计.md diff --git a/通用票通开票模块设计书.md b/history/通用票通开票模块设计书.md similarity index 100% rename from 通用票通开票模块设计书.md rename to history/通用票通开票模块设计书.md diff --git a/server/build.gradle.kts b/server/build.gradle.kts index c5af1da..aab2c7f 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -45,4 +45,11 @@ dependencies { // Redis implementation("org.redisson:redisson:3.38.1") + + // 票通集成 + implementation("org.bouncycastle:bcprov-jdk18on:1.84") + implementation("org.bouncycastle:bcpkix-jdk18on:1.84") + implementation("io.ktor:ktor-client-core") + implementation("io.ktor:ktor-client-cio") + implementation("io.ktor:ktor-client-content-negotiation") } diff --git a/server/src/main/kotlin/com/bbit/ticket/Application.kt b/server/src/main/kotlin/com/bbit/ticket/Application.kt index 4595536..c7f3709 100644 --- a/server/src/main/kotlin/com/bbit/ticket/Application.kt +++ b/server/src/main/kotlin/com/bbit/ticket/Application.kt @@ -2,15 +2,9 @@ package com.bbit.ticket import com.bbit.ticket.bootstrap.DatabaseInitializer import com.bbit.ticket.bootstrap.SeedData -import com.bbit.ticket.common.ok -import com.bbit.ticket.config.AppConfig -import com.bbit.ticket.modules.auth.registerAuthRoutes -import com.bbit.ticket.modules.logs.registerLogsQueryRoutes -import com.bbit.ticket.modules.system.dict.registerDictRoutes -import com.bbit.ticket.modules.system.menu.registerMenuRoutes -import com.bbit.ticket.modules.system.org.registerOrgRoutes -import com.bbit.ticket.modules.system.role.registerRoleRoutes -import com.bbit.ticket.modules.system.user.registerUserRoutes +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.bootstrap.AppConfig +import com.bbit.ticket.route.system.registerAuthRoutes import com.bbit.ticket.plugins.configureCors import com.bbit.ticket.plugins.configureDatabase import com.bbit.ticket.plugins.configureLogging @@ -20,6 +14,12 @@ import com.bbit.ticket.plugins.configureSecurity import com.bbit.ticket.plugins.configureSerialization import com.bbit.ticket.plugins.configureStatusPages import com.bbit.ticket.plugins.configureTrace +import com.bbit.ticket.route.system.registerDictRoutes +import com.bbit.ticket.route.system.registerLogsQueryRoutes +import com.bbit.ticket.route.system.registerMenuRoutes +import com.bbit.ticket.route.system.registerOrgRoutes +import com.bbit.ticket.route.system.registerRoleRoutes +import com.bbit.ticket.route.system.registerUserRoutes import kotlinx.coroutines.runBlocking import io.ktor.server.application.Application import io.ktor.server.netty.EngineMain diff --git a/server/src/main/kotlin/com/bbit/ticket/config/AppConfig.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/AppConfig.kt similarity index 98% rename from server/src/main/kotlin/com/bbit/ticket/config/AppConfig.kt rename to server/src/main/kotlin/com/bbit/ticket/bootstrap/AppConfig.kt index 6f5b579..1fe8dfc 100644 --- a/server/src/main/kotlin/com/bbit/ticket/config/AppConfig.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/AppConfig.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.config +package com.bbit.ticket.bootstrap import io.ktor.server.application.ApplicationEnvironment @@ -91,4 +91,4 @@ object AppConfig { private fun long(environment: ApplicationEnvironment, path: String, default: Long): Long = string(environment, path, default.toString()).toLong() -} +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt index 3ef460b..d86f25c 100644 --- a/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt @@ -32,9 +32,6 @@ object DatabaseInitializer { SysApiAccessLogTable, ) // 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。 - dbQuery { - MigrationUtils.statementsRequiredForDatabaseMigration(*tables, withLogs = true) - } transaction { val statements = MigrationUtils.statementsRequiredForDatabaseMigration( *tables, diff --git a/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt index bb814e4..bd28612 100644 --- a/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt @@ -11,7 +11,7 @@ import com.bbit.ticket.database.system.SysRoleTable import com.bbit.ticket.database.system.SysUserRoleTable import com.bbit.ticket.database.system.SysUserTable import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.PasswordService +import com.bbit.ticket.service.system.PasswordService import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.inList diff --git a/server/src/main/kotlin/com/bbit/ticket/common/ApiResult.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/ApiResult.kt similarity index 92% rename from server/src/main/kotlin/com/bbit/ticket/common/ApiResult.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/ApiResult.kt index a70ab45..cdb0700 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/ApiResult.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/ApiResult.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.entity.common import kotlinx.serialization.Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/common/BizException.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/BizException.kt similarity index 84% rename from server/src/main/kotlin/com/bbit/ticket/common/BizException.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/BizException.kt index 980a24e..00abafa 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/BizException.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/BizException.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.entity.common import io.ktor.http.HttpStatusCode diff --git a/server/src/main/kotlin/com/bbit/ticket/common/DisplayLabels.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/DisplayLabels.kt similarity index 93% rename from server/src/main/kotlin/com/bbit/ticket/common/DisplayLabels.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/DisplayLabels.kt index 5419dee..311cbee 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/DisplayLabels.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/DisplayLabels.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.entity.common fun statusLabel(status: String): String = when (status) { "ENABLED" -> "启用" diff --git a/server/src/main/kotlin/com/bbit/ticket/common/ErrorCode.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/ErrorCode.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/ticket/common/ErrorCode.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/ErrorCode.kt index 966050d..f6f7c75 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/ErrorCode.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/ErrorCode.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.entity.common enum class ErrorCode(val code: String, val message: String) { BAD_REQUEST("COMMON.BAD_REQUEST", "请求参数错误"), diff --git a/server/src/main/kotlin/com/bbit/ticket/common/PageQuery.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageQuery.kt similarity index 90% rename from server/src/main/kotlin/com/bbit/ticket/common/PageQuery.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/PageQuery.kt index 08f071a..86722b5 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/PageQuery.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageQuery.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.entity.common import kotlinx.serialization.Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/common/PageResult.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageResult.kt similarity index 82% rename from server/src/main/kotlin/com/bbit/ticket/common/PageResult.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/common/PageResult.kt index 286c386..5506efa 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/PageResult.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/PageResult.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.entity.common import kotlinx.serialization.Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthDtos.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/Auth.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthDtos.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/system/Auth.kt index 7810815..b250de2 100644 --- a/server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthDtos.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/Auth.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.modules.auth +package com.bbit.ticket.entity.system import kotlinx.serialization.Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Dict.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/Dict.kt new file mode 100644 index 0000000..474b405 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/Dict.kt @@ -0,0 +1,54 @@ +package com.bbit.ticket.entity.system + +import kotlinx.serialization.Serializable + +@Serializable +data class DictTypeItem( + val id: String, + val code: String, + val name: String, + val status: String, + val statusLabel: String, + val remark: String? = null, +) + +@Serializable +data class DictItem( + val id: String, + val typeId: String, + val label: String, + val value: String, + val color: String? = null, + val sort: Int, + val status: String, + val statusLabel: String, + val remark: String? = null, +) + +@Serializable +data class CreateDictTypeRequest(val code: String, val name: String, val status: String = "ENABLED", val remark: String? = null) + +@Serializable +data class UpdateDictTypeRequest(val name: String, val status: String = "ENABLED", val remark: String? = null) + +@Serializable +data class CreateDictItemRequest( + val typeId: String, + val label: String, + val value: String, + val color: String? = null, + val sort: Int = 0, + val status: String = "ENABLED", + val remark: String? = null, +) + +@Serializable +data class UpdateDictItemRequest( + val typeId: String, + val label: String, + val value: String, + val color: String? = null, + val sort: Int = 0, + val status: String = "ENABLED", + val remark: String? = null, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Log.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/Log.kt new file mode 100644 index 0000000..4081db8 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/Log.kt @@ -0,0 +1,33 @@ +package com.bbit.ticket.entity.system + +import kotlinx.serialization.Serializable + +@Serializable +data class OperationLogItem( + val id: String, + val traceId: String? = null, + val username: String? = null, + val operationType: String, + val operationName: String, + val httpMethod: String, + val requestPath: String, + val status: String, + val errorMessage: String? = null, + val costMs: Long, + val createdAt: String, +) + +@Serializable +data class ApiAccessLogItem( + val id: String, + val traceId: String? = null, + val appKey: String? = null, + val appName: String? = null, + val httpMethod: String, + val requestPath: String, + val responseCode: String? = null, + val status: String, + val errorMessage: String? = null, + val costMs: Long, + val createdAt: String, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Menu.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/Menu.kt new file mode 100644 index 0000000..811ed06 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/Menu.kt @@ -0,0 +1,77 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.entity.system + +import kotlinx.serialization.Serializable +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@Serializable +data class MenuTreeNode( + val id: String, + val parentId: String? = null, + val type: String, + val typeLabel: String, + val title: String, + val name: String? = null, + val path: String? = null, + val component: String? = null, + val icon: String? = null, + val permission: String? = null, + val sort: Int, + val visible: Boolean, + val keepAlive: Boolean, + val builtIn: Boolean = false, + val status: String, + val statusLabel: String, + val children: List = emptyList(), +) + +@Serializable +data class CreateMenuRequest( + val parentId: String? = null, + val type: String, + val title: String, + val name: String? = null, + val path: String? = null, + val component: String? = null, + val icon: String? = null, + val permission: String? = null, + val sort: Int = 0, + val visible: Boolean = true, + val keepAlive: Boolean = false, + val status: String = "ENABLED", +) + +@Serializable +data class UpdateMenuRequest( + val parentId: String? = null, + val type: String, + val title: String, + val name: String? = null, + val path: String? = null, + val component: String? = null, + val icon: String? = null, + val permission: String? = null, + val sort: Int = 0, + val visible: Boolean = true, + val keepAlive: Boolean = false, + val status: String = "ENABLED", +) + +data class MenuFlat( + val id: Uuid, + val parentId: Uuid?, + val type: String, + val title: String, + val name: String?, + val path: String?, + val component: String?, + val icon: String?, + val permission: String?, + val sort: Int, + val visible: Boolean, + val keepAlive: Boolean, + val builtIn: Boolean, + val status: String, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Org.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/Org.kt new file mode 100644 index 0000000..f171658 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/Org.kt @@ -0,0 +1,32 @@ +package com.bbit.ticket.entity.system + +import kotlinx.serialization.Serializable + +@Serializable +data class OrgTreeNode( + val id: String, + val parentId: String? = null, + val name: String, + val code: String, + val sort: Int, + val status: String, + val statusLabel: String, + val children: List = emptyList(), +) + +@Serializable +data class CreateOrgRequest( + val parentId: String? = null, + val name: String, + val code: String, + val sort: Int = 0, + val status: String = "ENABLED", +) + +@Serializable +data class UpdateOrgRequest( + val parentId: String? = null, + val name: String, + val sort: Int = 0, + val status: String = "ENABLED", +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/Role.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/Role.kt new file mode 100644 index 0000000..07b6d72 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/Role.kt @@ -0,0 +1,48 @@ +package com.bbit.ticket.entity.system + +import kotlinx.serialization.Serializable + +@Serializable +data class RoleItem( + val id: String, + val name: String, + val code: String, + val description: String? = null, + val status: String, + val statusLabel: String, + val dataScope: String, + val dataScopeLabel: String, +) + +@Serializable +data class RoleDetail( + val id: String, + val name: String, + val code: String, + val description: String? = null, + val status: String, + val statusLabel: String, + val dataScope: String, + val dataScopeLabel: String, + val menuIds: List, +) + +@Serializable +data class CreateRoleRequest( + val name: String, + val code: String, + val description: String? = null, + val status: String = "ENABLED", + val dataScope: String = "SELF", +) + +@Serializable +data class UpdateRoleRequest( + val name: String, + val description: String? = null, + val status: String = "ENABLED", + val dataScope: String = "SELF", +) + +@Serializable +data class UpdateRoleMenusRequest(val menuIds: List) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/User.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/User.kt new file mode 100644 index 0000000..3336083 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/User.kt @@ -0,0 +1,62 @@ +package com.bbit.ticket.entity.system + +import kotlinx.serialization.Serializable + +@Serializable +data class UserListItem( + val id: String, + val username: String, + val nickname: String? = null, + val realName: String? = null, + val orgId: String? = null, + val status: String, + val statusLabel: String, + val roleCodes: List, +) + +@Serializable +data class UserDetailResponse( + val id: String, + val username: String, + val nickname: String? = null, + val realName: String? = null, + val phone: String? = null, + val email: String? = null, + val avatar: String? = null, + val orgId: String? = null, + val status: String, + val statusLabel: String, + val roleIds: List, +) + +@Serializable +data class CreateUserRequest( + val username: String, + val password: String, + val nickname: String? = null, + val realName: String? = null, + val phone: String? = null, + val email: String? = null, + val avatar: String? = null, + val orgId: String? = null, + val status: String = "ENABLED", +) + +@Serializable +data class UpdateUserRequest( + val nickname: String? = null, + val realName: String? = null, + val phone: String? = null, + val email: String? = null, + val avatar: String? = null, + val orgId: String? = null, +) + +@Serializable +data class UpdateUserStatusRequest(val status: String) + +@Serializable +data class UpdateUserPasswordRequest(val password: String) + +@Serializable +data class UpdateUserRolesRequest(val roleIds: List) diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/system/dict/DictModule.kt b/server/src/main/kotlin/com/bbit/ticket/modules/system/dict/DictModule.kt deleted file mode 100644 index 636f3d0..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/modules/system/dict/DictModule.kt +++ /dev/null @@ -1,338 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.ticket.modules.system.dict - -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode -import com.bbit.ticket.common.PageResult -import com.bbit.ticket.common.ok -import com.bbit.ticket.common.parseUuid -import com.bbit.ticket.common.queryInt -import com.bbit.ticket.common.queryString -import com.bbit.ticket.common.statusLabel -import com.bbit.ticket.database.system.SysDictItemTable -import com.bbit.ticket.database.system.SysDictTypeTable -import com.bbit.ticket.modules.logs.OperationLogService -import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.requirePermission -import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.authenticate -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.routing.Route -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put -import io.ktor.server.routing.route -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.core.like -import org.jetbrains.exposed.v1.core.or -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime -import kotlin.time.TimeSource -import kotlin.uuid.Uuid - -@Serializable -data class DictTypeItem( - val id: String, - val code: String, - val name: String, - val status: String, - val statusLabel: String, - val remark: String? = null, -) - -@Serializable -data class DictItem( - val id: String, - val typeId: String, - val label: String, - val value: String, - val color: String? = null, - val sort: Int, - val status: String, - val statusLabel: String, - val remark: String? = null, -) - -@Serializable -data class CreateDictTypeRequest(val code: String, val name: String, val status: String = "ENABLED", val remark: String? = null) - -@Serializable -data class UpdateDictTypeRequest(val name: String, val status: String = "ENABLED", val remark: String? = null) - -@Serializable -data class CreateDictItemRequest( - val typeId: String, - val label: String, - val value: String, - val color: String? = null, - val sort: Int = 0, - val status: String = "ENABLED", - val remark: String? = null, -) - -@Serializable -data class UpdateDictItemRequest( - val typeId: String, - val label: String, - val value: String, - val color: String? = null, - val sort: Int = 0, - val status: String = "ENABLED", - val remark: String? = null, -) - -object DictService { - suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult = dbQuery { - var where = SysDictTypeTable.deletedAt.isNull() - if (!keyword.isNullOrBlank()) { - where = where and ((SysDictTypeTable.code like "%$keyword%") or (SysDictTypeTable.name like "%$keyword%")) - } - val total = SysDictTypeTable.selectAll().where { where }.count() - val rows = SysDictTypeTable.selectAll().where { where } - .orderBy(SysDictTypeTable.createdAt) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - PageResult( - items = rows.map { - DictTypeItem( - id = it[SysDictTypeTable.id].toString(), - code = it[SysDictTypeTable.code], - name = it[SysDictTypeTable.name], - status = it[SysDictTypeTable.status], - statusLabel = statusLabel(it[SysDictTypeTable.status]), - remark = it[SysDictTypeTable.remark], - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } - - suspend fun createType(request: CreateDictTypeRequest): String = dbQuery { - if (request.code.trim().isBlank() || request.name.trim().isBlank()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型编码和名称不能为空") - } - val exists = SysDictTypeTable.selectAll().where { - (SysDictTypeTable.code eq request.code.trim()) and SysDictTypeTable.deletedAt.isNull() - }.any() - if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "字典类型编码已存在") - val inserted = SysDictTypeTable.insert { - it[code] = request.code.trim() - it[name] = request.name.trim() - it[status] = request.status - it[remark] = request.remark?.trim() - it[createdAt] = OffsetDateTime.now() - } - inserted[SysDictTypeTable.id].toString() - } - - suspend fun updateType(id: Uuid, request: UpdateDictTypeRequest) = dbQuery { - requireType(id) - SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { - it[name] = request.name.trim() - it[status] = request.status - it[remark] = request.remark?.trim() - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun deleteType(id: Uuid) = dbQuery { - requireType(id) - val hasItems = SysDictItemTable.selectAll().where { - (SysDictItemTable.typeId eq id) and SysDictItemTable.deletedAt.isNull() - }.any() - if (hasItems) throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型下存在字典项,不能删除") - SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - } - - suspend fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult = dbQuery { - var where = SysDictItemTable.deletedAt.isNull() - if (typeId != null) where = where and (SysDictItemTable.typeId eq typeId) - val total = SysDictItemTable.selectAll().where { where }.count() - val rows = SysDictItemTable.selectAll().where { where } - .orderBy(SysDictItemTable.sort) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - PageResult( - items = rows.map { - DictItem( - id = it[SysDictItemTable.id].toString(), - typeId = it[SysDictItemTable.typeId].toString(), - label = it[SysDictItemTable.label], - value = it[SysDictItemTable.value], - color = it[SysDictItemTable.color], - sort = it[SysDictItemTable.sort], - status = it[SysDictItemTable.status], - statusLabel = statusLabel(it[SysDictItemTable.status]), - remark = it[SysDictItemTable.remark], - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } - - suspend fun createItem(request: CreateDictItemRequest): String = dbQuery { - val typeId = parseUuid(request.typeId, "typeId") - requireType(typeId) - val inserted = SysDictItemTable.insert { - it[SysDictItemTable.typeId] = typeId - it[label] = request.label.trim() - it[value] = request.value.trim() - it[color] = request.color?.trim() - it[sort] = request.sort - it[status] = request.status - it[remark] = request.remark?.trim() - it[createdAt] = OffsetDateTime.now() - } - inserted[SysDictItemTable.id].toString() - } - - suspend fun updateItem(id: Uuid, request: UpdateDictItemRequest) = dbQuery { - requireItem(id) - val typeId = parseUuid(request.typeId, "typeId") - requireType(typeId) - SysDictItemTable.update({ SysDictItemTable.id eq id }) { - it[SysDictItemTable.typeId] = typeId - it[label] = request.label.trim() - it[value] = request.value.trim() - it[color] = request.color?.trim() - it[sort] = request.sort - it[status] = request.status - it[remark] = request.remark?.trim() - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun deleteItem(id: Uuid) = dbQuery { - requireItem(id) - SysDictItemTable.update({ SysDictItemTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - } - - private fun requireType(id: Uuid): ResultRow = - SysDictTypeTable.selectAll().where { (SysDictTypeTable.id eq id) and SysDictTypeTable.deletedAt.isNull() }.singleOrNull() - ?: throw BizException(ErrorCode.DICT_TYPE_NOT_FOUND.code, ErrorCode.DICT_TYPE_NOT_FOUND.message, HttpStatusCode.NotFound) - - private fun requireItem(id: Uuid): ResultRow = - SysDictItemTable.selectAll().where { (SysDictItemTable.id eq id) and SysDictItemTable.deletedAt.isNull() }.singleOrNull() - ?: throw BizException(ErrorCode.DICT_ITEM_NOT_FOUND.code, ErrorCode.DICT_ITEM_NOT_FOUND.message, HttpStatusCode.NotFound) -} - -fun Route.registerDictRoutes() { - authenticate("auth-jwt") { - route("/api/system/dict-types") { - get { - call.requirePermission("system:dict:view") - val page = call.queryInt("page", 1) - val pageSize = call.queryInt("pageSize", 20) - call.respond(ok(DictService.listTypes(page, pageSize, call.queryString("keyword")))) - } - post { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:dict:create") - val request = call.receive() - runCatching { - val id = DictService.createType(request) - call.respond(ok(mapOf("id" to id))) - OperationLogService.success(call, currentUser, "CREATE", "新增字典类型", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "CREATE", "新增字典类型", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - put("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:dict:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - DictService.updateType(id, request) - call.respond(ok(message = "更新成功")) - OperationLogService.success(call, currentUser, "UPDATE", "更新字典类型", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "UPDATE", "更新字典类型", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - delete("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:dict:delete") - val id = parseUuid(call.parameters["id"] ?: "", "id") - runCatching { - DictService.deleteType(id) - call.respond(ok(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除字典类型", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除字典类型", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - route("/api/system/dict-items") { - get { - call.requirePermission("system:dict:view") - val page = call.queryInt("page", 1) - val pageSize = call.queryInt("pageSize", 20) - val typeId = call.queryString("typeId")?.let { parseUuid(it, "typeId") } - call.respond(ok(DictService.listItems(page, pageSize, typeId))) - } - post { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:dict:create") - val request = call.receive() - runCatching { - val id = DictService.createItem(request) - call.respond(ok(mapOf("id" to id))) - OperationLogService.success(call, currentUser, "CREATE", "新增字典项", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "CREATE", "新增字典项", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - put("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:dict:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - DictService.updateItem(id, request) - call.respond(ok(message = "更新成功")) - OperationLogService.success(call, currentUser, "UPDATE", "更新字典项", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "UPDATE", "更新字典项", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - delete("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:dict:delete") - val id = parseUuid(call.parameters["id"] ?: "", "id") - runCatching { - DictService.deleteItem(id) - call.respond(ok(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除字典项", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除字典项", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/system/menu/MenuModule.kt b/server/src/main/kotlin/com/bbit/ticket/modules/system/menu/MenuModule.kt deleted file mode 100644 index b95897c..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/modules/system/menu/MenuModule.kt +++ /dev/null @@ -1,280 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.ticket.modules.system.menu - -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode -import com.bbit.ticket.common.ok -import com.bbit.ticket.common.parseUuid -import com.bbit.ticket.common.menuTypeLabel -import com.bbit.ticket.common.statusLabel -import com.bbit.ticket.database.system.SysMenuTable -import com.bbit.ticket.database.system.SysRoleMenuTable -import com.bbit.ticket.modules.logs.OperationLogService -import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.requirePermission -import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.authenticate -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.routing.Route -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put -import io.ktor.server.routing.route -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime -import kotlin.time.TimeSource -import kotlin.uuid.Uuid - -@Serializable -data class MenuTreeNode( - val id: String, - val parentId: String? = null, - val type: String, - val typeLabel: String, - val title: String, - val name: String? = null, - val path: String? = null, - val component: String? = null, - val icon: String? = null, - val permission: String? = null, - val sort: Int, - val visible: Boolean, - val keepAlive: Boolean, - val builtIn: Boolean = false, - val status: String, - val statusLabel: String, - val children: List = emptyList(), -) - -@Serializable -data class CreateMenuRequest( - val parentId: String? = null, - val type: String, - val title: String, - val name: String? = null, - val path: String? = null, - val component: String? = null, - val icon: String? = null, - val permission: String? = null, - val sort: Int = 0, - val visible: Boolean = true, - val keepAlive: Boolean = false, - val status: String = "ENABLED", -) - -@Serializable -data class UpdateMenuRequest( - val parentId: String? = null, - val type: String, - val title: String, - val name: String? = null, - val path: String? = null, - val component: String? = null, - val icon: String? = null, - val permission: String? = null, - val sort: Int = 0, - val visible: Boolean = true, - val keepAlive: Boolean = false, - val status: String = "ENABLED", -) - -object MenuService { - suspend fun tree(): List = dbQuery { - val rows = SysMenuTable.selectAll().where { SysMenuTable.deletedAt.isNull() }.toList() - val flat = rows.map { - MenuFlat( - id = it[SysMenuTable.id], - parentId = it[SysMenuTable.parentId], - type = it[SysMenuTable.type], - title = it[SysMenuTable.title], - name = it[SysMenuTable.name], - path = it[SysMenuTable.path], - component = it[SysMenuTable.component], - icon = it[SysMenuTable.icon], - permission = it[SysMenuTable.permission], - sort = it[SysMenuTable.sort], - visible = it[SysMenuTable.visible], - keepAlive = it[SysMenuTable.keepAlive], - builtIn = it[SysMenuTable.builtIn], - status = it[SysMenuTable.status], - ) - } - buildTree(flat) - } - - suspend fun create(request: CreateMenuRequest): String = dbQuery { - validateMenuType(request.type) - val parentId = request.parentId?.let { parseUuid(it, "parentId") } - if (parentId != null) requireMenu(parentId) - val inserted = SysMenuTable.insert { - it[SysMenuTable.parentId] = parentId - it[SysMenuTable.type] = request.type - it[title] = request.title.trim() - it[name] = request.name?.trim() - it[path] = request.path?.trim() - it[component] = request.component?.trim() - it[icon] = request.icon?.trim() - it[permission] = request.permission?.trim() - it[sort] = request.sort - it[visible] = request.visible - it[keepAlive] = request.keepAlive - it[builtIn] = false - it[status] = request.status - it[createdAt] = OffsetDateTime.now() - } - inserted[SysMenuTable.id].toString() - } - - suspend fun update(id: Uuid, request: UpdateMenuRequest) = dbQuery { - requireMenu(id) - validateMenuType(request.type) - val parentId = request.parentId?.let { parseUuid(it, "parentId") } - if (parentId == id) throw BizException(ErrorCode.BAD_REQUEST.code, "上级菜单不能选择自身") - if (parentId != null) requireMenu(parentId) - SysMenuTable.update({ SysMenuTable.id eq id }) { - it[SysMenuTable.parentId] = parentId - it[SysMenuTable.type] = request.type - it[title] = request.title.trim() - it[name] = request.name?.trim() - it[path] = request.path?.trim() - it[component] = request.component?.trim() - it[icon] = request.icon?.trim() - it[permission] = request.permission?.trim() - it[sort] = request.sort - it[visible] = request.visible - it[keepAlive] = request.keepAlive - it[status] = request.status - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun delete(id: Uuid) = dbQuery { - requireMenu(id) - val hasChildren = SysMenuTable.selectAll().where { - (SysMenuTable.parentId eq id) and SysMenuTable.deletedAt.isNull() - }.any() - if (hasChildren) throw BizException(ErrorCode.BAD_REQUEST.code, "存在子菜单,不能删除") - val referenced = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.menuId eq id }.any() - if (referenced) throw BizException(ErrorCode.BAD_REQUEST.code, "菜单已被角色引用,不能删除") - val row = requireMenu(id) - if (row[SysMenuTable.builtIn]) throw BizException(ErrorCode.BAD_REQUEST.code, "基础框架内置菜单不可删除") - SysMenuTable.update({ SysMenuTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - } - - private fun requireMenu(id: Uuid): ResultRow = - SysMenuTable.selectAll().where { (SysMenuTable.id eq id) and SysMenuTable.deletedAt.isNull() }.singleOrNull() - ?: throw BizException(ErrorCode.MENU_NOT_FOUND.code, ErrorCode.MENU_NOT_FOUND.message, HttpStatusCode.NotFound) - - private fun validateMenuType(type: String) { - if (type !in setOf("CATALOG", "MENU", "BUTTON")) { - throw BizException(ErrorCode.BAD_REQUEST.code, "菜单类型必须是目录、菜单或按钮") - } - } - - private fun buildTree(items: List): List { - val grouped = items.groupBy { it.parentId } - fun children(parentId: Uuid?): List = - (grouped[parentId] ?: emptyList()).sortedBy { it.sort }.map { menu -> - MenuTreeNode( - id = menu.id.toString(), - parentId = menu.parentId?.toString(), - type = menu.type, - typeLabel = menuTypeLabel(menu.type), - title = menu.title, - name = menu.name, - path = menu.path, - component = menu.component, - icon = menu.icon, - permission = menu.permission, - sort = menu.sort, - visible = menu.visible, - keepAlive = menu.keepAlive, - builtIn = menu.builtIn, - status = menu.status, - statusLabel = statusLabel(menu.status), - children = children(menu.id), - ) - } - return children(null) - } -} - -private data class MenuFlat( - val id: Uuid, - val parentId: Uuid?, - val type: String, - val title: String, - val name: String?, - val path: String?, - val component: String?, - val icon: String?, - val permission: String?, - val sort: Int, - val visible: Boolean, - val keepAlive: Boolean, - val builtIn: Boolean, - val status: String, -) - -fun Route.registerMenuRoutes() { - authenticate("auth-jwt") { - route("/api/system/menus") { - get { - call.requirePermission("system:menu:view") - call.respond(ok(MenuService.tree())) - } - post { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:menu:create") - val request = call.receive() - runCatching { - val id = MenuService.create(request) - call.respond(ok(mapOf("id" to id))) - OperationLogService.success(call, currentUser, "CREATE", "新增菜单", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "CREATE", "新增菜单", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - put("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:menu:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - MenuService.update(id, request) - call.respond(ok(message = "更新成功")) - OperationLogService.success(call, currentUser, "UPDATE", "更新菜单", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "UPDATE", "更新菜单", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - delete("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:menu:delete") - val id = parseUuid(call.parameters["id"] ?: "", "id") - runCatching { - MenuService.delete(id) - call.respond(ok(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除菜单", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除菜单", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/system/role/RoleModule.kt b/server/src/main/kotlin/com/bbit/ticket/modules/system/role/RoleModule.kt deleted file mode 100644 index 9836c8c..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/modules/system/role/RoleModule.kt +++ /dev/null @@ -1,285 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.ticket.modules.system.role - -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode -import com.bbit.ticket.common.PageResult -import com.bbit.ticket.common.ok -import com.bbit.ticket.common.parseUuid -import com.bbit.ticket.common.queryInt -import com.bbit.ticket.common.queryString -import com.bbit.ticket.common.dataScopeLabel -import com.bbit.ticket.common.statusLabel -import com.bbit.ticket.database.system.SysMenuTable -import com.bbit.ticket.database.system.SysRoleMenuTable -import com.bbit.ticket.database.system.SysRoleTable -import com.bbit.ticket.database.system.SysUserRoleTable -import com.bbit.ticket.modules.logs.OperationLogService -import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.requirePermission -import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.authenticate -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.routing.Route -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put -import io.ktor.server.routing.route -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.inList -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.core.like -import org.jetbrains.exposed.v1.core.or -import org.jetbrains.exposed.v1.jdbc.deleteWhere -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.insertIgnore -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime -import kotlin.time.TimeSource -import kotlin.uuid.Uuid - -@Serializable -data class RoleItem( - val id: String, - val name: String, - val code: String, - val description: String? = null, - val status: String, - val statusLabel: String, - val dataScope: String, - val dataScopeLabel: String, -) - -@Serializable -data class RoleDetail( - val id: String, - val name: String, - val code: String, - val description: String? = null, - val status: String, - val statusLabel: String, - val dataScope: String, - val dataScopeLabel: String, - val menuIds: List, -) - -@Serializable -data class CreateRoleRequest( - val name: String, - val code: String, - val description: String? = null, - val status: String = "ENABLED", - val dataScope: String = "SELF", -) - -@Serializable -data class UpdateRoleRequest( - val name: String, - val description: String? = null, - val status: String = "ENABLED", - val dataScope: String = "SELF", -) - -@Serializable -data class UpdateRoleMenusRequest(val menuIds: List) - -object RoleService { - suspend fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = dbQuery { - var where = SysRoleTable.deletedAt.isNull() - if (!keyword.isNullOrBlank()) { - where = where and ((SysRoleTable.name like "%$keyword%") or (SysRoleTable.code like "%$keyword%")) - } - if (!status.isNullOrBlank()) { - where = where and (SysRoleTable.status eq status) - } - val total = SysRoleTable.selectAll().where { where }.count() - val rows = SysRoleTable.selectAll().where { where } - .orderBy(SysRoleTable.createdAt) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - PageResult( - items = rows.map { - RoleItem( - id = it[SysRoleTable.id].toString(), - name = it[SysRoleTable.name], - code = it[SysRoleTable.code], - description = it[SysRoleTable.description], - status = it[SysRoleTable.status], - statusLabel = statusLabel(it[SysRoleTable.status]), - dataScope = it[SysRoleTable.dataScope], - dataScopeLabel = dataScopeLabel(it[SysRoleTable.dataScope]), - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } - - suspend fun create(request: CreateRoleRequest): String = dbQuery { - if (request.name.trim().isBlank() || request.code.trim().isBlank()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "角色名称和编码不能为空") - } - val exists = SysRoleTable.selectAll().where { - (SysRoleTable.code eq request.code.trim()) and SysRoleTable.deletedAt.isNull() - }.any() - if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "角色编码已存在") - val inserted = SysRoleTable.insert { - it[name] = request.name.trim() - it[code] = request.code.trim() - it[description] = request.description?.trim() - it[status] = request.status - it[dataScope] = request.dataScope - it[createdAt] = OffsetDateTime.now() - } - inserted[SysRoleTable.id].toString() - } - - suspend fun detail(id: Uuid): RoleDetail = dbQuery { - val role = requireRole(id) - val menuIds = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.roleId eq id }.map { it[SysRoleMenuTable.menuId].toString() } - RoleDetail( - id = role[SysRoleTable.id].toString(), - name = role[SysRoleTable.name], - code = role[SysRoleTable.code], - description = role[SysRoleTable.description], - status = role[SysRoleTable.status], - statusLabel = statusLabel(role[SysRoleTable.status]), - dataScope = role[SysRoleTable.dataScope], - dataScopeLabel = dataScopeLabel(role[SysRoleTable.dataScope]), - menuIds = menuIds, - ) - } - - suspend fun update(id: Uuid, request: UpdateRoleRequest) = dbQuery { - requireRole(id) - SysRoleTable.update({ SysRoleTable.id eq id }) { - it[name] = request.name.trim() - it[description] = request.description?.trim() - it[status] = request.status - it[dataScope] = request.dataScope - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun delete(id: Uuid) = dbQuery { - val role = requireRole(id) - if (role[SysRoleTable.code] == "SUPER_ADMIN") { - throw BizException(ErrorCode.BAD_REQUEST.code, "超级管理员角色不可删除") - } - val inUse = SysUserRoleTable.selectAll().where { SysUserRoleTable.roleId eq id }.any() - if (inUse) { - throw BizException(ErrorCode.BAD_REQUEST.code, "角色已被用户使用,不能删除") - } - SysRoleTable.update({ SysRoleTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id } - } - - suspend fun updateMenus(id: Uuid, request: UpdateRoleMenusRequest) = dbQuery { - requireRole(id) - val menuIds = request.menuIds.distinct().map { parseUuid(it, "menuId") } - if (menuIds.isNotEmpty()) { - val validCount = SysMenuTable.selectAll().where { - (SysMenuTable.id inList menuIds) and - SysMenuTable.deletedAt.isNull() and - (SysMenuTable.status eq "ENABLED") - }.count() - if (validCount != menuIds.size.toLong()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或禁用菜单") - } - } - SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id } - menuIds.forEach { menuId -> - SysRoleMenuTable.insertIgnore { - it[roleId] = id - it[SysRoleMenuTable.menuId] = menuId - } - } - } - - private fun requireRole(id: Uuid): ResultRow = - SysRoleTable.selectAll().where { (SysRoleTable.id eq id) and SysRoleTable.deletedAt.isNull() }.singleOrNull() - ?: throw BizException(ErrorCode.ROLE_NOT_FOUND.code, ErrorCode.ROLE_NOT_FOUND.message, HttpStatusCode.NotFound) -} - -fun Route.registerRoleRoutes() { - authenticate("auth-jwt") { - route("/api/system/roles") { - get { - call.requirePermission("system:role:view") - val page = call.queryInt("page", 1) - val pageSize = call.queryInt("pageSize", 20) - call.respond(ok(RoleService.list(page, pageSize, call.queryString("keyword"), call.queryString("status")))) - } - post { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:role:create") - val request = call.receive() - runCatching { - val id = RoleService.create(request) - call.respond(ok(mapOf("id" to id))) - OperationLogService.success(call, currentUser, "CREATE", "新增角色", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "CREATE", "新增角色", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - get("/{id}") { - call.requirePermission("system:role:view") - val id = parseUuid(call.parameters["id"] ?: "", "id") - call.respond(ok(RoleService.detail(id))) - } - put("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:role:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - RoleService.update(id, request) - call.respond(ok(message = "更新成功")) - OperationLogService.success(call, currentUser, "UPDATE", "更新角色", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "UPDATE", "更新角色", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - delete("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:role:delete") - val id = parseUuid(call.parameters["id"] ?: "", "id") - runCatching { - RoleService.delete(id) - call.respond(ok(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除角色", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除角色", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - put("/{id}/menus") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:role:assign") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - RoleService.updateMenus(id, request) - call.respond(ok(message = "菜单分配成功")) - OperationLogService.success(call, currentUser, "ASSIGN_MENU", "分配角色菜单", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "ASSIGN_MENU", "分配角色菜单", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/system/user/UserModule.kt b/server/src/main/kotlin/com/bbit/ticket/modules/system/user/UserModule.kt deleted file mode 100644 index c96de46..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/modules/system/user/UserModule.kt +++ /dev/null @@ -1,421 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package com.bbit.ticket.modules.system.user - -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode -import com.bbit.ticket.common.PageResult -import com.bbit.ticket.common.ok -import com.bbit.ticket.common.parseUuid -import com.bbit.ticket.common.queryInt -import com.bbit.ticket.common.queryString -import com.bbit.ticket.common.statusLabel -import com.bbit.ticket.database.system.SysOrgTable -import com.bbit.ticket.database.system.SysRoleTable -import com.bbit.ticket.database.system.SysUserRoleTable -import com.bbit.ticket.database.system.SysUserTable -import com.bbit.ticket.modules.logs.OperationLogService -import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.PasswordService -import com.bbit.ticket.security.requirePermission -import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.authenticate -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.routing.Route -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put -import io.ktor.server.routing.route -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.v1.core.Op -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.inList -import org.jetbrains.exposed.v1.core.isNull -import org.jetbrains.exposed.v1.core.like -import org.jetbrains.exposed.v1.jdbc.deleteWhere -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.insertIgnore -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.time.OffsetDateTime -import kotlin.time.TimeSource -import kotlin.uuid.Uuid - -@Serializable -data class UserListItem( - val id: String, - val username: String, - val nickname: String? = null, - val realName: String? = null, - val orgId: String? = null, - val status: String, - val statusLabel: String, - val roleCodes: List, -) - -@Serializable -data class UserDetailResponse( - val id: String, - val username: String, - val nickname: String? = null, - val realName: String? = null, - val phone: String? = null, - val email: String? = null, - val avatar: String? = null, - val orgId: String? = null, - val status: String, - val statusLabel: String, - val roleIds: List, -) - -@Serializable -data class CreateUserRequest( - val username: String, - val password: String, - val nickname: String? = null, - val realName: String? = null, - val phone: String? = null, - val email: String? = null, - val avatar: String? = null, - val orgId: String? = null, - val status: String = "ENABLED", -) - -@Serializable -data class UpdateUserRequest( - val nickname: String? = null, - val realName: String? = null, - val phone: String? = null, - val email: String? = null, - val avatar: String? = null, - val orgId: String? = null, -) - -@Serializable -data class UpdateUserStatusRequest(val status: String) - -@Serializable -data class UpdateUserPasswordRequest(val password: String) - -@Serializable -data class UpdateUserRolesRequest(val roleIds: List) - -object UserService { - suspend fun list( - page: Int, - pageSize: Int, - username: String?, - nickname: String?, - status: String?, - orgId: Uuid?, - ): PageResult = dbQuery { - val where = buildWhere(username, nickname, status, orgId) - val total = SysUserTable.selectAll().where { where }.count() - val rows = SysUserTable.selectAll() - .where { where } - .orderBy(SysUserTable.createdAt) - .limit(pageSize) - .offset(((page - 1) * pageSize).toLong()) - .toList() - - val userIds = rows.map { it[SysUserTable.id] } - val roleMap = if (userIds.isEmpty()) { - emptyMap() - } else { - (SysUserRoleTable innerJoin SysRoleTable).selectAll() - .where { - (SysUserRoleTable.userId inList userIds) and - SysRoleTable.deletedAt.isNull() - } - .groupBy { it[SysUserRoleTable.userId] } - .mapValues { entry -> entry.value.map { row -> row[SysRoleTable.code] }.distinct() } - } - - PageResult( - items = rows.map { row -> - UserListItem( - id = row[SysUserTable.id].toString(), - username = row[SysUserTable.username], - nickname = row[SysUserTable.nickname], - realName = row[SysUserTable.realName], - orgId = row[SysUserTable.orgId]?.toString(), - status = row[SysUserTable.status], - statusLabel = statusLabel(row[SysUserTable.status]), - roleCodes = roleMap[row[SysUserTable.id]] ?: emptyList(), - ) - }, - page = page, - pageSize = pageSize, - total = total, - ) - } - - suspend fun create(request: CreateUserRequest): String = dbQuery { - val username = request.username.trim() - if (username.isBlank() || request.password.isBlank()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "用户名和密码不能为空") - } - val existed = SysUserTable.selectAll().where { - (SysUserTable.username eq username) and SysUserTable.deletedAt.isNull() - }.any() - if (existed) { - throw BizException(ErrorCode.DATA_CONFLICT.code, "用户名已存在") - } - - val orgUuid = request.orgId?.let { parseUuid(it, "orgId") } - if (orgUuid != null) { - ensureOrgExists(orgUuid) - } - val now = OffsetDateTime.now() - val row = SysUserTable.insert { - it[SysUserTable.username] = username - it[passwordHash] = PasswordService.hash(request.password) - it[nickname] = request.nickname?.trim() - it[realName] = request.realName?.trim() - it[phone] = request.phone?.trim() - it[email] = request.email?.trim() - it[avatar] = request.avatar?.trim() - it[orgId] = orgUuid - it[status] = request.status - it[tokenVersion] = 1 - it[createdAt] = now - } - row[SysUserTable.id].toString() - } - - suspend fun detail(id: Uuid): UserDetailResponse = dbQuery { - val user = requireUser(id) - val roleIds = SysUserRoleTable.selectAll().where { SysUserRoleTable.userId eq id } - .map { it[SysUserRoleTable.roleId].toString() } - UserDetailResponse( - id = user[SysUserTable.id].toString(), - username = user[SysUserTable.username], - nickname = user[SysUserTable.nickname], - realName = user[SysUserTable.realName], - phone = user[SysUserTable.phone], - email = user[SysUserTable.email], - avatar = user[SysUserTable.avatar], - orgId = user[SysUserTable.orgId]?.toString(), - status = user[SysUserTable.status], - statusLabel = statusLabel(user[SysUserTable.status]), - roleIds = roleIds, - ) - } - - suspend fun update(id: Uuid, request: UpdateUserRequest) = dbQuery { - requireUser(id) - val orgUuid = request.orgId?.let { parseUuid(it, "orgId") } - if (orgUuid != null) { - ensureOrgExists(orgUuid) - } - SysUserTable.update({ SysUserTable.id eq id }) { - it[nickname] = request.nickname?.trim() - it[realName] = request.realName?.trim() - it[phone] = request.phone?.trim() - it[email] = request.email?.trim() - it[avatar] = request.avatar?.trim() - it[orgId] = orgUuid - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun softDelete(id: Uuid) = dbQuery { - if (id.toString() == "00000000-0000-0000-0000-000000000000") { - throw BizException(ErrorCode.BAD_REQUEST.code, "系统保留用户不可删除") - } - requireUser(id) - SysUserTable.update({ SysUserTable.id eq id }) { - it[deletedAt] = OffsetDateTime.now() - } - SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id } - } - - suspend fun updateStatus(id: Uuid, request: UpdateUserStatusRequest) = dbQuery { - requireUser(id) - SysUserTable.update({ SysUserTable.id eq id }) { - it[status] = request.status - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun updatePassword(id: Uuid, request: UpdateUserPasswordRequest) = dbQuery { - val user = requireUser(id) - if (request.password.isBlank()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "密码不能为空") - } - val nextTokenVersion = user[SysUserTable.tokenVersion] + 1 - SysUserTable.update({ SysUserTable.id eq id }) { - it[passwordHash] = PasswordService.hash(request.password) - it[tokenVersion] = nextTokenVersion - it[updatedAt] = OffsetDateTime.now() - } - } - - suspend fun updateRoles(id: Uuid, request: UpdateUserRolesRequest) = dbQuery { - requireUser(id) - val roleIds = request.roleIds.distinct().map { parseUuid(it, "roleId") } - if (roleIds.isNotEmpty()) { - val validCount = SysRoleTable.selectAll().where { - (SysRoleTable.id inList roleIds) and - (SysRoleTable.status eq "ENABLED") and - SysRoleTable.deletedAt.isNull() - }.count() - if (validCount != roleIds.size.toLong()) { - throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或已禁用角色") - } - } - SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id } - roleIds.forEach { roleId -> - SysUserRoleTable.insertIgnore { - it[userId] = id - it[SysUserRoleTable.roleId] = roleId - } - } - } - - private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op { - var where: Op = SysUserTable.deletedAt.isNull() - if (!username.isNullOrBlank()) { - where = where and (SysUserTable.username like "%$username%") - } - if (!nickname.isNullOrBlank()) { - where = where and (SysUserTable.nickname like "%$nickname%") - } - if (!status.isNullOrBlank()) { - where = where and (SysUserTable.status eq status) - } - if (orgId != null) { - where = where and (SysUserTable.orgId eq orgId) - } - return where - } - - private fun requireUser(id: Uuid) = - SysUserTable.selectAll().where { (SysUserTable.id eq id) and SysUserTable.deletedAt.isNull() }.singleOrNull() - ?: throw BizException(ErrorCode.USER_NOT_FOUND.code, ErrorCode.USER_NOT_FOUND.message, HttpStatusCode.NotFound) - - private fun ensureOrgExists(orgId: Uuid) { - val exists = SysOrgTable.selectAll().where { (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() }.any() - if (!exists) { - throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message, HttpStatusCode.BadRequest) - } - } -} - -fun Route.registerUserRoutes() { - authenticate("auth-jwt") { - route("/api/system/users") { - get { - call.requirePermission("system:user:view") - val page = call.queryInt("page", 1) - val pageSize = call.queryInt("pageSize", 20) - val result = UserService.list( - page = page, - pageSize = pageSize, - username = call.queryString("username"), - nickname = call.queryString("nickname"), - status = call.queryString("status"), - orgId = call.queryString("orgId")?.let { parseUuid(it, "orgId") }, - ) - call.respond(ok(result)) - } - - post { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:user:create") - val request = call.receive() - runCatching { - val id = UserService.create(request) - call.respond(ok(mapOf("id" to id))) - OperationLogService.success(call, currentUser, "CREATE", "新增用户", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "CREATE", "新增用户", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - - get("/{id}") { - call.requirePermission("system:user:view") - val id = parseUuid(call.parameters["id"] ?: "", "id") - call.respond(ok(UserService.detail(id))) - } - - put("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:user:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - UserService.update(id, request) - call.respond(ok(message = "更新成功")) - OperationLogService.success(call, currentUser, "UPDATE", "更新用户", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "UPDATE", "更新用户", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - - delete("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:user:delete") - val id = parseUuid(call.parameters["id"] ?: "", "id") - runCatching { - UserService.softDelete(id) - call.respond(ok(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除用户", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除用户", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - - put("/{id}/status") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:user:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - UserService.updateStatus(id, request) - call.respond(ok(message = "状态更新成功")) - OperationLogService.success(call, currentUser, "UPDATE_STATUS", "更新用户状态", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "UPDATE_STATUS", "更新用户状态", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - - put("/{id}/password") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:user:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - UserService.updatePassword(id, request) - call.respond(ok(message = "密码更新成功")) - OperationLogService.success(call, currentUser, "RESET_PASSWORD", "重置用户密码", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "RESET_PASSWORD", "重置用户密码", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - - put("/{id}/roles") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:role:assign") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - UserService.updateRoles(id, request) - call.respond(ok(message = "角色分配成功")) - OperationLogService.success(call, currentUser, "ASSIGN_ROLE", "分配用户角色", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "ASSIGN_ROLE", "分配用户角色", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } -} diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/ApiAccessLogPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/ApiAccessLogPlugin.kt index be65cf5..2ca3fcc 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/ApiAccessLogPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/ApiAccessLogPlugin.kt @@ -1,6 +1,6 @@ package com.bbit.ticket.plugins -import com.bbit.ticket.common.TraceIdKey +import com.bbit.ticket.utils.TraceIdKey import com.bbit.ticket.database.system.SysApiAccessLogTable import io.ktor.server.application.Application import io.ktor.server.application.createApplicationPlugin diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/CorsPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/CorsPlugin.kt index ca2f2f5..5c8f7e9 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/CorsPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/CorsPlugin.kt @@ -1,6 +1,6 @@ package com.bbit.ticket.plugins -import com.bbit.ticket.config.AppConfig +import com.bbit.ticket.bootstrap.AppConfig import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.server.application.Application @@ -9,14 +9,7 @@ import io.ktor.server.plugins.cors.routing.CORS fun Application.configureCors() { install(CORS) { - if (AppConfig.cors.allowedHosts.contains("*")) { - anyHost() - } else { - AppConfig.cors.allowedHosts.forEach { allowedHost -> - allowHost(allowedHost, schemes = listOf("http", "https")) - } - } - + anyHost() allowMethod(HttpMethod.Get) allowMethod(HttpMethod.Post) allowMethod(HttpMethod.Put) diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/DatabasePlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/DatabasePlugin.kt index 9d44827..47dad6d 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/DatabasePlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/DatabasePlugin.kt @@ -1,6 +1,6 @@ package com.bbit.ticket.plugins -import com.bbit.ticket.config.AppConfig +import com.bbit.ticket.bootstrap.AppConfig import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import io.ktor.server.application.Application diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/LoggingPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/LoggingPlugin.kt index 371e7c8..54f64e7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/LoggingPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/LoggingPlugin.kt @@ -1,5 +1,6 @@ package com.bbit.ticket.plugins +import com.bbit.ticket.utils.TraceIdKey import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.plugins.calllogging.CallLogging @@ -10,7 +11,7 @@ import org.slf4j.event.Level fun Application.configureLogging() { install(CallLogging) { level = Level.INFO - mdc("traceId") { call -> call.attributes.getOrNull(com.bbit.ticket.common.TraceIdKey) } + mdc("traceId") { call -> call.attributes.getOrNull(TraceIdKey) } filter { call -> !call.request.path().startsWith("/health") } format { call -> val status = call.response.status() diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/RedisPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/RedisPlugin.kt index fcd6c72..3c542c6 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/RedisPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/RedisPlugin.kt @@ -1,6 +1,6 @@ package com.bbit.ticket.plugins -import com.bbit.ticket.config.AppConfig +import com.bbit.ticket.bootstrap.AppConfig import io.ktor.server.application.Application import org.redisson.Redisson import org.redisson.api.RedissonClient diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/SecurityPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/SecurityPlugin.kt index bb499cb..adb5345 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/SecurityPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/SecurityPlugin.kt @@ -2,10 +2,10 @@ package com.bbit.ticket.plugins import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import com.bbit.ticket.common.ErrorCode -import com.bbit.ticket.common.fail -import com.bbit.ticket.common.traceIdOrNull -import com.bbit.ticket.config.AppConfig +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.entity.common.fail +import com.bbit.ticket.utils.traceIdOrNull +import com.bbit.ticket.bootstrap.AppConfig import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application import io.ktor.server.application.install diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/StatusPagesPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/StatusPagesPlugin.kt index 7684e22..9eef36d 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/StatusPagesPlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/StatusPagesPlugin.kt @@ -1,9 +1,9 @@ package com.bbit.ticket.plugins -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode -import com.bbit.ticket.common.fail -import com.bbit.ticket.common.traceIdOrNull +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.entity.common.fail +import com.bbit.ticket.utils.traceIdOrNull import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application import io.ktor.server.application.install diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/TracePlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/TracePlugin.kt index 4828551..518d4e7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/plugins/TracePlugin.kt +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/TracePlugin.kt @@ -1,6 +1,6 @@ package com.bbit.ticket.plugins -import com.bbit.ticket.common.TraceIdKey +import com.bbit.ticket.utils.TraceIdKey import io.ktor.server.application.Application import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.install diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt similarity index 83% rename from server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthRoutes.kt rename to server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt index cb29a6c..cd85a1f 100644 --- a/server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt @@ -1,8 +1,11 @@ -package com.bbit.ticket.modules.auth +package com.bbit.ticket.route.system -import com.bbit.ticket.common.ok -import com.bbit.ticket.modules.logs.OperationLogService -import com.bbit.ticket.security.requireCurrentUser + +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.service.system.AuthService +import com.bbit.ticket.entity.system.LoginRequest +import com.bbit.ticket.service.system.OperationLogService +import com.bbit.ticket.utils.requireCurrentUser import io.ktor.server.auth.authenticate import io.ktor.server.request.receive import io.ktor.server.response.respond @@ -12,6 +15,7 @@ import io.ktor.server.routing.post import io.ktor.server.routing.route import kotlin.time.TimeSource + fun Route.registerAuthRoutes() { route("/api/auth") { post("/login") { diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt new file mode 100644 index 0000000..7a1a78a --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt @@ -0,0 +1,128 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.route.system + +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.utils.parseUuid +import com.bbit.ticket.utils.queryInt +import com.bbit.ticket.utils.queryString +import com.bbit.ticket.entity.system.CreateDictItemRequest +import com.bbit.ticket.entity.system.CreateDictTypeRequest +import com.bbit.ticket.entity.system.UpdateDictItemRequest +import com.bbit.ticket.entity.system.UpdateDictTypeRequest +import com.bbit.ticket.service.system.OperationLogService +import com.bbit.ticket.utils.requirePermission +import com.bbit.ticket.service.system.DictService +import io.ktor.server.auth.authenticate +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.server.routing.route +import kotlin.time.TimeSource +import kotlin.uuid.ExperimentalUuidApi + +fun Route.registerDictRoutes() { + authenticate("auth-jwt") { + route("/api/system/dict-types") { + get { + call.requirePermission("system:dict:view") + val page = call.queryInt("page", 1) + val pageSize = call.queryInt("pageSize", 20) + call.respond(ok(DictService.listTypes(page, pageSize, call.queryString("keyword")))) + } + post { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:dict:create") + val request = call.receive() + runCatching { + val id = DictService.createType(request) + call.respond(ok(mapOf("id" to id))) + OperationLogService.success(call, currentUser, "CREATE", "新增字典类型", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "CREATE", "新增字典类型", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + put("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:dict:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + DictService.updateType(id, request) + call.respond(ok(message = "更新成功")) + OperationLogService.success(call, currentUser, "UPDATE", "更新字典类型", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "UPDATE", "更新字典类型", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + delete("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:dict:delete") + val id = parseUuid(call.parameters["id"] ?: "", "id") + runCatching { + DictService.deleteType(id) + call.respond(ok(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除字典类型", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除字典类型", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + route("/api/system/dict-items") { + get { + call.requirePermission("system:dict:view") + val page = call.queryInt("page", 1) + val pageSize = call.queryInt("pageSize", 20) + val typeId = call.queryString("typeId")?.let { parseUuid(it, "typeId") } + call.respond(ok(DictService.listItems(page, pageSize, typeId))) + } + post { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:dict:create") + val request = call.receive() + runCatching { + val id = DictService.createItem(request) + call.respond(ok(mapOf("id" to id))) + OperationLogService.success(call, currentUser, "CREATE", "新增字典项", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "CREATE", "新增字典项", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + put("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:dict:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + DictService.updateItem(id, request) + call.respond(ok(message = "更新成功")) + OperationLogService.success(call, currentUser, "UPDATE", "更新字典项", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "UPDATE", "更新字典项", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + delete("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:dict:delete") + val id = parseUuid(call.parameters["id"] ?: "", "id") + runCatching { + DictService.deleteItem(id) + call.respond(ok(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除字典项", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除字典项", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt new file mode 100644 index 0000000..cc8bdc0 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt @@ -0,0 +1,31 @@ +package com.bbit.ticket.route.system + +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.utils.queryInt +import com.bbit.ticket.utils.queryString +import com.bbit.ticket.utils.requirePermission +import com.bbit.ticket.service.system.LogsQueryService +import io.ktor.server.auth.authenticate +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.route + +fun Route.registerLogsQueryRoutes() { + authenticate("auth-jwt") { + route("/api/logs") { + get("/operation") { + call.requirePermission("log:operation:view") + val page = call.queryInt("page", 1) + val pageSize = call.queryInt("pageSize", 20) + call.respond(ok(LogsQueryService.operationLogs(page, pageSize, call.queryString("keyword"), call.queryString("status")))) + } + get("/api-access") { + call.requirePermission("log:api-access:view") + val page = call.queryInt("page", 1) + val pageSize = call.queryInt("pageSize", 20) + call.respond(ok(LogsQueryService.apiAccessLogs(page, pageSize, call.queryString("keyword"), call.queryString("status")))) + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt new file mode 100644 index 0000000..1bbd607 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt @@ -0,0 +1,73 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.route.system + +import com.bbit.ticket.entity.system.CreateMenuRequest +import com.bbit.ticket.entity.system.UpdateMenuRequest +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.utils.parseUuid +import com.bbit.ticket.utils.requirePermission +import com.bbit.ticket.service.system.MenuService +import com.bbit.ticket.service.system.OperationLogService +import io.ktor.server.auth.authenticate +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.server.routing.route +import kotlin.time.TimeSource +import kotlin.uuid.ExperimentalUuidApi + +fun Route.registerMenuRoutes() { + authenticate("auth-jwt") { + route("/api/system/menus") { + get { + call.requirePermission("system:menu:view") + call.respond(ok(MenuService.tree())) + } + post { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:menu:create") + val request = call.receive() + runCatching { + val id = MenuService.create(request) + call.respond(ok(mapOf("id" to id))) + OperationLogService.success(call, currentUser, "CREATE", "新增菜单", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "CREATE", "新增菜单", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + put("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:menu:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + MenuService.update(id, request) + call.respond(ok(message = "更新成功")) + OperationLogService.success(call, currentUser, "UPDATE", "更新菜单", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "UPDATE", "更新菜单", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + delete("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:menu:delete") + val id = parseUuid(call.parameters["id"] ?: "", "id") + runCatching { + MenuService.delete(id) + call.respond(ok(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除菜单", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除菜单", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt new file mode 100644 index 0000000..568b757 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt @@ -0,0 +1,73 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.route.system + +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.entity.system.CreateOrgRequest +import com.bbit.ticket.entity.system.UpdateOrgRequest +import com.bbit.ticket.utils.parseUuid +import com.bbit.ticket.utils.requirePermission +import com.bbit.ticket.service.system.OperationLogService +import com.bbit.ticket.service.system.OrgService +import io.ktor.server.auth.authenticate +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.server.routing.route +import kotlin.time.TimeSource +import kotlin.uuid.ExperimentalUuidApi + +fun Route.registerOrgRoutes() { + authenticate("auth-jwt") { + route("/api/system/orgs") { + get { + call.requirePermission("system:org:view") + call.respond(ok(OrgService.tree())) + } + post { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:org:create") + val request = call.receive() + runCatching { + val id = OrgService.create(request) + call.respond(ok(mapOf("id" to id))) + OperationLogService.success(call, currentUser, "CREATE", "新增组织", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "CREATE", "新增组织", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + put("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:org:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + OrgService.update(id, request) + call.respond(ok(message = "更新成功")) + OperationLogService.success(call, currentUser, "UPDATE", "更新组织", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "UPDATE", "更新组织", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + delete("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:org:delete") + val id = parseUuid(call.parameters["id"] ?: "", "id") + runCatching { + OrgService.delete(id) + call.respond(ok(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除组织", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除组织", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt new file mode 100644 index 0000000..62c51db --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt @@ -0,0 +1,97 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.route.system + +import com.bbit.ticket.entity.system.CreateRoleRequest +import com.bbit.ticket.entity.system.UpdateRoleMenusRequest +import com.bbit.ticket.entity.system.UpdateRoleRequest +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.utils.parseUuid +import com.bbit.ticket.utils.queryInt +import com.bbit.ticket.utils.queryString +import com.bbit.ticket.utils.requirePermission +import com.bbit.ticket.service.system.OperationLogService +import com.bbit.ticket.service.system.RoleService +import io.ktor.server.auth.authenticate +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.server.routing.route +import kotlin.time.TimeSource +import kotlin.uuid.ExperimentalUuidApi + +fun Route.registerRoleRoutes() { + authenticate("auth-jwt") { + route("/api/system/roles") { + get { + call.requirePermission("system:role:view") + val page = call.queryInt("page", 1) + val pageSize = call.queryInt("pageSize", 20) + call.respond(ok(RoleService.list(page, pageSize, call.queryString("keyword"), call.queryString("status")))) + } + post { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:role:create") + val request = call.receive() + runCatching { + val id = RoleService.create(request) + call.respond(ok(mapOf("id" to id))) + OperationLogService.success(call, currentUser, "CREATE", "新增角色", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "CREATE", "新增角色", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + get("/{id}") { + call.requirePermission("system:role:view") + val id = parseUuid(call.parameters["id"] ?: "", "id") + call.respond(ok(RoleService.detail(id))) + } + put("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:role:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + RoleService.update(id, request) + call.respond(ok(message = "更新成功")) + OperationLogService.success(call, currentUser, "UPDATE", "更新角色", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "UPDATE", "更新角色", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + delete("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:role:delete") + val id = parseUuid(call.parameters["id"] ?: "", "id") + runCatching { + RoleService.delete(id) + call.respond(ok(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除角色", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除角色", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + put("/{id}/menus") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:role:assign") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + RoleService.updateMenus(id, request) + call.respond(ok(message = "菜单分配成功")) + OperationLogService.success(call, currentUser, "ASSIGN_MENU", "分配角色菜单", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "ASSIGN_MENU", "分配角色菜单", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt new file mode 100644 index 0000000..b66a08c --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt @@ -0,0 +1,142 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.route.system + +import com.bbit.ticket.entity.system.CreateUserRequest +import com.bbit.ticket.entity.system.UpdateUserPasswordRequest +import com.bbit.ticket.entity.system.UpdateUserRequest +import com.bbit.ticket.entity.system.UpdateUserRolesRequest +import com.bbit.ticket.entity.system.UpdateUserStatusRequest +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.utils.parseUuid +import com.bbit.ticket.utils.queryInt +import com.bbit.ticket.utils.queryString +import com.bbit.ticket.utils.requirePermission +import com.bbit.ticket.service.system.OperationLogService +import com.bbit.ticket.service.system.UserService +import io.ktor.server.auth.authenticate +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put +import io.ktor.server.routing.route +import kotlin.time.TimeSource +import kotlin.uuid.ExperimentalUuidApi + +fun Route.registerUserRoutes() { + authenticate("auth-jwt") { + route("/api/system/users") { + get { + call.requirePermission("system:user:view") + val page = call.queryInt("page", 1) + val pageSize = call.queryInt("pageSize", 20) + val result = UserService.list( + page = page, + pageSize = pageSize, + username = call.queryString("username"), + nickname = call.queryString("nickname"), + status = call.queryString("status"), + orgId = call.queryString("orgId")?.let { parseUuid(it, "orgId") }, + ) + call.respond(ok(result)) + } + + post { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:user:create") + val request = call.receive() + runCatching { + val id = UserService.create(request) + call.respond(ok(mapOf("id" to id))) + OperationLogService.success(call, currentUser, "CREATE", "新增用户", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "CREATE", "新增用户", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + + get("/{id}") { + call.requirePermission("system:user:view") + val id = parseUuid(call.parameters["id"] ?: "", "id") + call.respond(ok(UserService.detail(id))) + } + + put("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:user:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + UserService.update(id, request) + call.respond(ok(message = "更新成功")) + OperationLogService.success(call, currentUser, "UPDATE", "更新用户", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "UPDATE", "更新用户", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + + delete("/{id}") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:user:delete") + val id = parseUuid(call.parameters["id"] ?: "", "id") + runCatching { + UserService.softDelete(id) + call.respond(ok(message = "删除成功")) + OperationLogService.success(call, currentUser, "DELETE", "删除用户", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "DELETE", "删除用户", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + + put("/{id}/status") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:user:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + UserService.updateStatus(id, request) + call.respond(ok(message = "状态更新成功")) + OperationLogService.success(call, currentUser, "UPDATE_STATUS", "更新用户状态", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "UPDATE_STATUS", "更新用户状态", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + + put("/{id}/password") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:user:update") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + UserService.updatePassword(id, request) + call.respond(ok(message = "密码更新成功")) + OperationLogService.success(call, currentUser, "RESET_PASSWORD", "重置用户密码", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "RESET_PASSWORD", "重置用户密码", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + + put("/{id}/roles") { + val start = TimeSource.Monotonic.markNow() + val currentUser = call.requirePermission("system:role:assign") + val id = parseUuid(call.parameters["id"] ?: "", "id") + val request = call.receive() + runCatching { + UserService.updateRoles(id, request) + call.respond(ok(message = "角色分配成功")) + OperationLogService.success(call, currentUser, "ASSIGN_ROLE", "分配用户角色", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, currentUser, "ASSIGN_ROLE", "分配用户角色", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt similarity index 88% rename from server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthService.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt index 586e662..1ed2cfc 100644 --- a/server/src/main/kotlin/com/bbit/ticket/modules/auth/AuthService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt @@ -1,18 +1,21 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(ExperimentalUuidApi::class) -package com.bbit.ticket.modules.auth +package com.bbit.ticket.service.system -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode import com.bbit.ticket.database.system.SysMenuTable import com.bbit.ticket.database.system.SysRoleMenuTable import com.bbit.ticket.database.system.SysRoleTable import com.bbit.ticket.database.system.SysUserRoleTable import com.bbit.ticket.database.system.SysUserTable +import com.bbit.ticket.entity.system.CurrentUserProfile +import com.bbit.ticket.entity.system.LoginRequest +import com.bbit.ticket.entity.system.LoginResponse +import com.bbit.ticket.entity.system.MeResponse +import com.bbit.ticket.entity.system.MenuNode import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.CurrentUser -import com.bbit.ticket.security.JwtService -import com.bbit.ticket.security.PasswordService +import com.bbit.ticket.utils.CurrentUser import io.ktor.http.HttpStatusCode import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq @@ -21,6 +24,7 @@ import org.jetbrains.exposed.v1.core.isNull import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.update import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid object AuthService { @@ -186,19 +190,19 @@ object AuthService { return build(null) } -} + private data class MenuFlat( + val id: Uuid, + val parentId: Uuid?, + val type: String, + val title: String, + val name: String?, + val path: String?, + val component: String?, + val icon: String?, + val permission: String?, + val sort: Int, + val visible: Boolean, + val keepAlive: Boolean, + ) -private data class MenuFlat( - val id: Uuid, - val parentId: Uuid?, - val type: String, - val title: String, - val name: String?, - val path: String?, - val component: String?, - val icon: String?, - val permission: String?, - val sort: Int, - val visible: Boolean, - val keepAlive: Boolean, -) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt new file mode 100644 index 0000000..9bcba5b --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/DictService.kt @@ -0,0 +1,186 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.database.system.SysDictItemTable +import com.bbit.ticket.database.system.SysDictItemTable.color +import com.bbit.ticket.database.system.SysDictItemTable.label +import com.bbit.ticket.database.system.SysDictTypeTable +import com.bbit.ticket.entity.system.CreateDictItemRequest +import com.bbit.ticket.entity.system.CreateDictTypeRequest +import com.bbit.ticket.entity.system.DictItem +import com.bbit.ticket.entity.system.DictTypeItem +import com.bbit.ticket.entity.system.UpdateDictItemRequest +import com.bbit.ticket.entity.system.UpdateDictTypeRequest +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.entity.common.PageResult +import com.bbit.ticket.entity.common.statusLabel +import com.bbit.ticket.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +import io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.isNull +import org.jetbrains.exposed.v1.core.like +import org.jetbrains.exposed.v1.core.or +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.update +import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object DictService { + suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult = dbQuery { + var where = SysDictTypeTable.deletedAt.isNull() + if (!keyword.isNullOrBlank()) { + where = where and ((SysDictTypeTable.code like "%$keyword%") or (SysDictTypeTable.name like "%$keyword%")) + } + val total = SysDictTypeTable.selectAll().where { where }.count() + val rows = SysDictTypeTable.selectAll().where { where } + .orderBy(SysDictTypeTable.createdAt) + .limit(pageSize) + .offset(((page - 1) * pageSize).toLong()) + .toList() + PageResult( + items = rows.map { + DictTypeItem( + id = it[SysDictTypeTable.id].toString(), + code = it[SysDictTypeTable.code], + name = it[SysDictTypeTable.name], + status = it[SysDictTypeTable.status], + statusLabel = statusLabel(it[SysDictTypeTable.status]), + remark = it[SysDictTypeTable.remark], + ) + }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + suspend fun createType(request: CreateDictTypeRequest): String = dbQuery { + if (request.code.trim().isBlank() || request.name.trim().isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型编码和名称不能为空") + } + val exists = SysDictTypeTable.selectAll().where { + (SysDictTypeTable.code eq request.code.trim()) and SysDictTypeTable.deletedAt.isNull() + }.any() + if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "字典类型编码已存在") + val inserted = SysDictTypeTable.insert { + it[code] = request.code.trim() + it[name] = request.name.trim() + it[status] = request.status + it[remark] = request.remark?.trim() + it[createdAt] = OffsetDateTime.now() + } + inserted[SysDictTypeTable.id].toString() + } + + suspend fun updateType(id: Uuid, request: UpdateDictTypeRequest) = dbQuery { + requireType(id) + SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { + it[name] = request.name.trim() + it[status] = request.status + it[remark] = request.remark?.trim() + it[updatedAt] = OffsetDateTime.now() + } + } + + suspend fun deleteType(id: Uuid) = dbQuery { + requireType(id) + val hasItems = SysDictItemTable.selectAll().where { + (SysDictItemTable.typeId eq id) and SysDictItemTable.deletedAt.isNull() + }.any() + if (hasItems) throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型下存在字典项,不能删除") + SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { + it[deletedAt] = OffsetDateTime.now() + } + } + + suspend fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult = dbQuery { + var where = SysDictItemTable.deletedAt.isNull() + if (typeId != null) where = where and (SysDictItemTable.typeId eq typeId) + val total = SysDictItemTable.selectAll().where { where }.count() + val rows = SysDictItemTable.selectAll().where { where } + .orderBy(SysDictItemTable.sort) + .limit(pageSize) + .offset(((page - 1) * pageSize).toLong()) + .toList() + PageResult( + items = rows.map { + DictItem( + id = it[SysDictItemTable.id].toString(), + typeId = it[SysDictItemTable.typeId].toString(), + label = it[label], + value = it[SysDictItemTable.value], + color = it[color], + sort = it[SysDictItemTable.sort], + status = it[SysDictItemTable.status], + statusLabel = statusLabel(it[SysDictItemTable.status]), + remark = it[SysDictItemTable.remark], + ) + }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + suspend fun createItem(request: CreateDictItemRequest): String = dbQuery { + val typeId = parseUuid(request.typeId, "typeId") + requireType(typeId) + val inserted = SysDictItemTable.insert { + it[SysDictItemTable.typeId] = typeId + it[label] = request.label.trim() + it[value] = request.value.trim() + it[color] = request.color?.trim() + it[sort] = request.sort + it[status] = request.status + it[remark] = request.remark?.trim() + it[createdAt] = OffsetDateTime.now() + } + inserted[SysDictItemTable.id].toString() + } + + suspend fun updateItem(id: Uuid, request: UpdateDictItemRequest) = dbQuery { + requireItem(id) + val typeId = parseUuid(request.typeId, "typeId") + requireType(typeId) + SysDictItemTable.update({ SysDictItemTable.id eq id }) { + it[SysDictItemTable.typeId] = typeId + it[label] = request.label.trim() + it[value] = request.value.trim() + it[color] = request.color?.trim() + it[sort] = request.sort + it[status] = request.status + it[remark] = request.remark?.trim() + it[updatedAt] = OffsetDateTime.now() + } + } + + suspend fun deleteItem(id: Uuid) = dbQuery { + requireItem(id) + SysDictItemTable.update({ SysDictItemTable.id eq id }) { + it[deletedAt] = OffsetDateTime.now() + } + } + + private fun requireType(id: Uuid): ResultRow = + SysDictTypeTable.selectAll().where { (SysDictTypeTable.id eq id) and SysDictTypeTable.deletedAt.isNull() }.singleOrNull() + ?: throw BizException( + ErrorCode.DICT_TYPE_NOT_FOUND.code, + ErrorCode.DICT_TYPE_NOT_FOUND.message, + HttpStatusCode.NotFound + ) + + private fun requireItem(id: Uuid): ResultRow = + SysDictItemTable.selectAll().where { (SysDictItemTable.id eq id) and SysDictItemTable.deletedAt.isNull() }.singleOrNull() + ?: throw BizException( + ErrorCode.DICT_ITEM_NOT_FOUND.code, + ErrorCode.DICT_ITEM_NOT_FOUND.message, + HttpStatusCode.NotFound + ) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/security/JwtService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/JwtService.kt similarity index 92% rename from server/src/main/kotlin/com/bbit/ticket/security/JwtService.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/JwtService.kt index 11c3c78..1877d3c 100644 --- a/server/src/main/kotlin/com/bbit/ticket/security/JwtService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/JwtService.kt @@ -1,8 +1,8 @@ -package com.bbit.ticket.security +package com.bbit.ticket.service.system import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import com.bbit.ticket.config.AppConfig +import com.bbit.ticket.bootstrap.AppConfig import java.time.Instant import java.time.temporal.ChronoUnit @@ -32,5 +32,4 @@ object JwtService { return token to AppConfig.jwt.accessTokenTtlMinutes * 60 } -} - +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/logs/LogsQueryModule.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt similarity index 65% rename from server/src/main/kotlin/com/bbit/ticket/modules/logs/LogsQueryModule.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt index 4f55de0..da50a5a 100644 --- a/server/src/main/kotlin/com/bbit/ticket/modules/logs/LogsQueryModule.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/LogsQueryService.kt @@ -1,22 +1,14 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(ExperimentalUuidApi::class) -package com.bbit.ticket.modules.logs +package com.bbit.ticket.service.system -import com.bbit.ticket.common.PageResult -import com.bbit.ticket.common.formatDateTime -import com.bbit.ticket.common.ok -import com.bbit.ticket.common.queryInt -import com.bbit.ticket.common.queryString import com.bbit.ticket.database.system.SysApiAccessLogTable import com.bbit.ticket.database.system.SysOperationLogTable +import com.bbit.ticket.entity.system.ApiAccessLogItem +import com.bbit.ticket.entity.system.OperationLogItem +import com.bbit.ticket.entity.common.PageResult +import com.bbit.ticket.utils.formatDateTime import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.requirePermission -import io.ktor.server.auth.authenticate -import io.ktor.server.response.respond -import io.ktor.server.routing.Route -import io.ktor.server.routing.get -import io.ktor.server.routing.route -import kotlinx.serialization.Serializable import org.jetbrains.exposed.v1.core.Op import org.jetbrains.exposed.v1.core.SortOrder import org.jetbrains.exposed.v1.core.and @@ -24,36 +16,7 @@ import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.like import org.jetbrains.exposed.v1.core.or import org.jetbrains.exposed.v1.jdbc.selectAll - -@Serializable -data class OperationLogItem( - val id: String, - val traceId: String? = null, - val username: String? = null, - val operationType: String, - val operationName: String, - val httpMethod: String, - val requestPath: String, - val status: String, - val errorMessage: String? = null, - val costMs: Long, - val createdAt: String, -) - -@Serializable -data class ApiAccessLogItem( - val id: String, - val traceId: String? = null, - val appKey: String? = null, - val appName: String? = null, - val httpMethod: String, - val requestPath: String, - val responseCode: String? = null, - val status: String, - val errorMessage: String? = null, - val costMs: Long, - val createdAt: String, -) +import kotlin.uuid.ExperimentalUuidApi object LogsQueryService { suspend fun operationLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = dbQuery { @@ -123,23 +86,4 @@ object LogsQueryService { total = total, ) } -} - -fun Route.registerLogsQueryRoutes() { - authenticate("auth-jwt") { - route("/api/logs") { - get("/operation") { - call.requirePermission("log:operation:view") - val page = call.queryInt("page", 1) - val pageSize = call.queryInt("pageSize", 20) - call.respond(ok(LogsQueryService.operationLogs(page, pageSize, call.queryString("keyword"), call.queryString("status")))) - } - get("/api-access") { - call.requirePermission("log:api-access:view") - val page = call.queryInt("page", 1) - val pageSize = call.queryInt("pageSize", 20) - call.respond(ok(LogsQueryService.apiAccessLogs(page, pageSize, call.queryString("keyword"), call.queryString("status")))) - } - } - } -} +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt new file mode 100644 index 0000000..fbdce50 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/MenuService.kt @@ -0,0 +1,161 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.database.system.SysMenuTable +import com.bbit.ticket.database.system.SysMenuTable.builtIn +import com.bbit.ticket.database.system.SysMenuTable.component +import com.bbit.ticket.database.system.SysMenuTable.icon +import com.bbit.ticket.database.system.SysMenuTable.keepAlive +import com.bbit.ticket.database.system.SysMenuTable.path +import com.bbit.ticket.database.system.SysMenuTable.permission +import com.bbit.ticket.database.system.SysMenuTable.visible +import com.bbit.ticket.database.system.SysRoleMenuTable +import com.bbit.ticket.entity.system.CreateMenuRequest +import com.bbit.ticket.entity.system.MenuFlat +import com.bbit.ticket.entity.system.MenuTreeNode +import com.bbit.ticket.entity.system.UpdateMenuRequest +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.entity.common.menuTypeLabel +import com.bbit.ticket.entity.common.statusLabel +import com.bbit.ticket.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +import io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.isNull +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.update +import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object MenuService { + suspend fun tree(): List = dbQuery { + val rows = SysMenuTable.selectAll().where { SysMenuTable.deletedAt.isNull() }.toList() + val flat = rows.map { + MenuFlat( + id = it[SysMenuTable.id], + parentId = it[SysMenuTable.parentId], + type = it[SysMenuTable.type], + title = it[SysMenuTable.title], + name = it[SysMenuTable.name], + path = it[path], + component = it[component], + icon = it[icon], + permission = it[permission], + sort = it[SysMenuTable.sort], + visible = it[visible], + keepAlive = it[keepAlive], + builtIn = it[builtIn], + status = it[SysMenuTable.status], + ) + } + buildTree(flat) + } + + suspend fun create(request: CreateMenuRequest): String = dbQuery { + validateMenuType(request.type) + val parentId = request.parentId?.let { parseUuid(it, "parentId") } + if (parentId != null) requireMenu(parentId) + val inserted = SysMenuTable.insert { + it[SysMenuTable.parentId] = parentId + it[SysMenuTable.type] = request.type + it[title] = request.title.trim() + it[name] = request.name?.trim() + it[path] = request.path?.trim() + it[component] = request.component?.trim() + it[icon] = request.icon?.trim() + it[permission] = request.permission?.trim() + it[sort] = request.sort + it[visible] = request.visible + it[keepAlive] = request.keepAlive + it[builtIn] = false + it[status] = request.status + it[createdAt] = OffsetDateTime.now() + } + inserted[SysMenuTable.id].toString() + } + + suspend fun update(id: Uuid, request: UpdateMenuRequest) = dbQuery { + requireMenu(id) + validateMenuType(request.type) + val parentId = request.parentId?.let { parseUuid(it, "parentId") } + if (parentId == id) throw BizException(ErrorCode.BAD_REQUEST.code, "上级菜单不能选择自身") + if (parentId != null) requireMenu(parentId) + SysMenuTable.update({ SysMenuTable.id eq id }) { + it[SysMenuTable.parentId] = parentId + it[SysMenuTable.type] = request.type + it[title] = request.title.trim() + it[name] = request.name?.trim() + it[path] = request.path?.trim() + it[component] = request.component?.trim() + it[icon] = request.icon?.trim() + it[permission] = request.permission?.trim() + it[sort] = request.sort + it[visible] = request.visible + it[keepAlive] = request.keepAlive + it[status] = request.status + it[updatedAt] = OffsetDateTime.now() + } + } + + suspend fun delete(id: Uuid) = dbQuery { + requireMenu(id) + val hasChildren = SysMenuTable.selectAll().where { + (SysMenuTable.parentId eq id) and SysMenuTable.deletedAt.isNull() + }.any() + if (hasChildren) throw BizException(ErrorCode.BAD_REQUEST.code, "存在子菜单,不能删除") + val referenced = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.menuId eq id }.any() + if (referenced) throw BizException(ErrorCode.BAD_REQUEST.code, "菜单已被角色引用,不能删除") + val row = requireMenu(id) + if (row[builtIn]) throw BizException(ErrorCode.BAD_REQUEST.code, "基础框架内置菜单不可删除") + SysMenuTable.update({ SysMenuTable.id eq id }) { + it[deletedAt] = OffsetDateTime.now() + } + } + + private fun requireMenu(id: Uuid): ResultRow = + SysMenuTable.selectAll().where { (SysMenuTable.id eq id) and SysMenuTable.deletedAt.isNull() }.singleOrNull() + ?: throw BizException( + ErrorCode.MENU_NOT_FOUND.code, + ErrorCode.MENU_NOT_FOUND.message, + HttpStatusCode.NotFound + ) + + private fun validateMenuType(type: String) { + if (type !in setOf("CATALOG", "MENU", "BUTTON")) { + throw BizException(ErrorCode.BAD_REQUEST.code, "菜单类型必须是目录、菜单或按钮") + } + } + + private fun buildTree(items: List): 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/modules/logs/OperationLogService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt similarity index 91% rename from server/src/main/kotlin/com/bbit/ticket/modules/logs/OperationLogService.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt index f9c6176..563296f 100644 --- a/server/src/main/kotlin/com/bbit/ticket/modules/logs/OperationLogService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/OperationLogService.kt @@ -1,17 +1,18 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(ExperimentalUuidApi::class) -package com.bbit.ticket.modules.logs +package com.bbit.ticket.service.system -import com.bbit.ticket.common.traceIdOrNull import com.bbit.ticket.database.system.SysOperationLogTable +import com.bbit.ticket.utils.traceIdOrNull import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.CurrentUser +import com.bbit.ticket.utils.CurrentUser import io.ktor.http.formUrlEncode import io.ktor.server.application.ApplicationCall import io.ktor.server.request.httpMethod import io.ktor.server.request.path import org.jetbrains.exposed.v1.jdbc.insert import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi object OperationLogService { suspend fun success(call: ApplicationCall, currentUser: CurrentUser?, operationType: String, operationName: String, costMs: Long) { @@ -56,4 +57,4 @@ object OperationLogService { it[createdAt] = OffsetDateTime.now() } } -} +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/modules/system/org/OrgModule.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt similarity index 52% rename from server/src/main/kotlin/com/bbit/ticket/modules/system/org/OrgModule.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt index c3cfeb3..2a95f62 100644 --- a/server/src/main/kotlin/com/bbit/ticket/modules/system/org/OrgModule.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/OrgService.kt @@ -1,28 +1,18 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(ExperimentalUuidApi::class) -package com.bbit.ticket.modules.system.org +package com.bbit.ticket.service.system -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode -import com.bbit.ticket.common.ok -import com.bbit.ticket.common.parseUuid -import com.bbit.ticket.common.statusLabel import com.bbit.ticket.database.system.SysOrgTable import com.bbit.ticket.database.system.SysUserTable -import com.bbit.ticket.modules.logs.OperationLogService +import com.bbit.ticket.entity.system.CreateOrgRequest +import com.bbit.ticket.entity.system.OrgTreeNode +import com.bbit.ticket.entity.system.UpdateOrgRequest +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.entity.common.statusLabel import com.bbit.ticket.plugins.dbQuery -import com.bbit.ticket.security.requirePermission +import com.bbit.ticket.utils.parseUuid import io.ktor.http.HttpStatusCode -import io.ktor.server.auth.authenticate -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.routing.Route -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put -import io.ktor.server.routing.route -import kotlinx.serialization.Serializable import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq @@ -31,38 +21,9 @@ import org.jetbrains.exposed.v1.jdbc.insert import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.update import java.time.OffsetDateTime -import kotlin.time.TimeSource +import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid -@Serializable -data class OrgTreeNode( - val id: String, - val parentId: String? = null, - val name: String, - val code: String, - val sort: Int, - val status: String, - val statusLabel: String, - val children: List = emptyList(), -) - -@Serializable -data class CreateOrgRequest( - val parentId: String? = null, - val name: String, - val code: String, - val sort: Int = 0, - val status: String = "ENABLED", -) - -@Serializable -data class UpdateOrgRequest( - val parentId: String? = null, - val name: String, - val sort: Int = 0, - val status: String = "ENABLED", -) - object OrgService { suspend fun tree(): List = dbQuery { val rows = SysOrgTable.selectAll() @@ -137,7 +98,11 @@ object OrgService { private fun requireOrg(id: Uuid): ResultRow = SysOrgTable.selectAll().where { (SysOrgTable.id eq id) and SysOrgTable.deletedAt.isNull() }.singleOrNull() - ?: throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message, HttpStatusCode.NotFound) + ?: throw BizException( + ErrorCode.ORG_NOT_FOUND.code, + ErrorCode.ORG_NOT_FOUND.message, + HttpStatusCode.NotFound + ) private fun toNode(row: ResultRow): OrgNodeFlat = OrgNodeFlat( id = row[SysOrgTable.id], @@ -165,64 +130,12 @@ object OrgService { } return children(null) } -} - -private data class OrgNodeFlat( - val id: Uuid, - val parentId: Uuid?, - val name: String, - val code: String, - val sort: Int, - val status: String, -) - -fun Route.registerOrgRoutes() { - authenticate("auth-jwt") { - route("/api/system/orgs") { - get { - call.requirePermission("system:org:view") - call.respond(ok(OrgService.tree())) - } - post { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:org:create") - val request = call.receive() - runCatching { - val id = OrgService.create(request) - call.respond(ok(mapOf("id" to id))) - OperationLogService.success(call, currentUser, "CREATE", "新增组织", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "CREATE", "新增组织", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - put("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:org:update") - val id = parseUuid(call.parameters["id"] ?: "", "id") - val request = call.receive() - runCatching { - OrgService.update(id, request) - call.respond(ok(message = "更新成功")) - OperationLogService.success(call, currentUser, "UPDATE", "更新组织", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "UPDATE", "更新组织", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - delete("/{id}") { - val start = TimeSource.Monotonic.markNow() - val currentUser = call.requirePermission("system:org:delete") - val id = parseUuid(call.parameters["id"] ?: "", "id") - runCatching { - OrgService.delete(id) - call.respond(ok(message = "删除成功")) - OperationLogService.success(call, currentUser, "DELETE", "删除组织", start.elapsedNow().inWholeMilliseconds) - }.onFailure { - OperationLogService.fail(call, currentUser, "DELETE", "删除组织", it.message, start.elapsedNow().inWholeMilliseconds) - throw it - } - } - } - } + 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/security/PasswordService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/PasswordService.kt similarity index 86% rename from server/src/main/kotlin/com/bbit/ticket/security/PasswordService.kt rename to server/src/main/kotlin/com/bbit/ticket/service/system/PasswordService.kt index 8af082f..c043968 100644 --- a/server/src/main/kotlin/com/bbit/ticket/security/PasswordService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/PasswordService.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.security +package com.bbit.ticket.service.system import org.mindrot.jbcrypt.BCrypt @@ -6,5 +6,4 @@ object PasswordService { fun hash(rawPassword: String): String = BCrypt.hashpw(rawPassword, BCrypt.gensalt()) fun matches(rawPassword: String, passwordHash: String): Boolean = BCrypt.checkpw(rawPassword, passwordHash) -} - +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt new file mode 100644 index 0000000..8ef89a0 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/RoleService.kt @@ -0,0 +1,165 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.service.system + +import com.bbit.ticket.database.system.SysMenuTable +import com.bbit.ticket.database.system.SysRoleMenuTable +import com.bbit.ticket.database.system.SysRoleTable +import com.bbit.ticket.database.system.SysRoleTable.dataScope +import com.bbit.ticket.database.system.SysUserRoleTable +import com.bbit.ticket.entity.system.CreateRoleRequest +import com.bbit.ticket.entity.system.RoleDetail +import com.bbit.ticket.entity.system.RoleItem +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.entity.common.PageResult +import com.bbit.ticket.entity.common.dataScopeLabel +import com.bbit.ticket.entity.common.statusLabel +import com.bbit.ticket.entity.system.UpdateRoleMenusRequest +import com.bbit.ticket.entity.system.UpdateRoleRequest +import com.bbit.ticket.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +import io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.inList +import org.jetbrains.exposed.v1.core.isNull +import org.jetbrains.exposed.v1.core.like +import org.jetbrains.exposed.v1.core.or +import org.jetbrains.exposed.v1.jdbc.deleteWhere +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.insertIgnore +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.update +import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object RoleService { + @OptIn(ExperimentalUuidApi::class) + suspend fun list(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult = dbQuery { + var where = SysRoleTable.deletedAt.isNull() + if (!keyword.isNullOrBlank()) { + where = where and ((SysRoleTable.name like "%$keyword%") or (SysRoleTable.code like "%$keyword%")) + } + if (!status.isNullOrBlank()) { + where = where and (SysRoleTable.status eq status) + } + val total = SysRoleTable.selectAll().where { where }.count() + val rows = SysRoleTable.selectAll().where { where } + .orderBy(SysRoleTable.createdAt) + .limit(pageSize) + .offset(((page - 1) * pageSize).toLong()) + .toList() + PageResult( + items = rows.map { + RoleItem( + id = it[SysRoleTable.id].toString(), + name = it[SysRoleTable.name], + code = it[SysRoleTable.code], + description = it[SysRoleTable.description], + status = it[SysRoleTable.status], + statusLabel = statusLabel(it[SysRoleTable.status]), + dataScope = it[dataScope], + dataScopeLabel = dataScopeLabel(it[dataScope]), + ) + }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + suspend fun create(request: CreateRoleRequest): String = dbQuery { + if (request.name.trim().isBlank() || request.code.trim().isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "角色名称和编码不能为空") + } + val exists = SysRoleTable.selectAll().where { + (SysRoleTable.code eq request.code.trim()) and SysRoleTable.deletedAt.isNull() + }.any() + if (exists) throw BizException(ErrorCode.DATA_CONFLICT.code, "角色编码已存在") + val inserted = SysRoleTable.insert { + it[name] = request.name.trim() + it[code] = request.code.trim() + it[description] = request.description?.trim() + it[status] = request.status + it[dataScope] = request.dataScope + it[createdAt] = OffsetDateTime.now() + } + inserted[SysRoleTable.id].toString() + } + + suspend fun detail(id: Uuid): RoleDetail = dbQuery { + val role = requireRole(id) + val menuIds = SysRoleMenuTable.selectAll().where { SysRoleMenuTable.roleId eq id } + .map { it[SysRoleMenuTable.menuId].toString() } + RoleDetail( + id = role[SysRoleTable.id].toString(), + name = role[SysRoleTable.name], + code = role[SysRoleTable.code], + description = role[SysRoleTable.description], + status = role[SysRoleTable.status], + statusLabel = statusLabel(role[SysRoleTable.status]), + dataScope = role[dataScope], + dataScopeLabel = dataScopeLabel(role[dataScope]), + menuIds = menuIds, + ) + } + + suspend fun update(id: Uuid, request: UpdateRoleRequest) = dbQuery { + requireRole(id) + SysRoleTable.update({ SysRoleTable.id eq id }) { + it[name] = request.name.trim() + it[description] = request.description?.trim() + it[status] = request.status + it[dataScope] = request.dataScope + it[updatedAt] = OffsetDateTime.now() + } + } + + suspend fun delete(id: Uuid) = dbQuery { + val role = requireRole(id) + if (role[SysRoleTable.code] == "SUPER_ADMIN") { + throw BizException(ErrorCode.BAD_REQUEST.code, "超级管理员角色不可删除") + } + val inUse = SysUserRoleTable.selectAll().where { SysUserRoleTable.roleId eq id }.any() + if (inUse) { + throw BizException(ErrorCode.BAD_REQUEST.code, "角色已被用户使用,不能删除") + } + SysRoleTable.update({ SysRoleTable.id eq id }) { + it[deletedAt] = OffsetDateTime.now() + } + SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id } + } + + suspend fun updateMenus(id: Uuid, request: UpdateRoleMenusRequest) = dbQuery { + requireRole(id) + val menuIds = request.menuIds.distinct().map { parseUuid(it, "menuId") } + if (menuIds.isNotEmpty()) { + val validCount = SysMenuTable.selectAll().where { + (SysMenuTable.id inList menuIds) and + SysMenuTable.deletedAt.isNull() and + (SysMenuTable.status eq "ENABLED") + }.count() + if (validCount != menuIds.size.toLong()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或禁用菜单") + } + } + SysRoleMenuTable.deleteWhere { SysRoleMenuTable.roleId eq id } + menuIds.forEach { menuId -> + SysRoleMenuTable.insertIgnore { + it[roleId] = id + it[SysRoleMenuTable.menuId] = menuId + } + } + } + + private fun requireRole(id: Uuid): ResultRow = + SysRoleTable.selectAll().where { (SysRoleTable.id eq id) and SysRoleTable.deletedAt.isNull() }.singleOrNull() + ?: throw BizException( + ErrorCode.ROLE_NOT_FOUND.code, + ErrorCode.ROLE_NOT_FOUND.message, + HttpStatusCode.NotFound + ) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt new file mode 100644 index 0000000..c7d0c9d --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/UserService.kt @@ -0,0 +1,246 @@ +package com.bbit.ticket.service.system + +import com.bbit.ticket.database.system.SysOrgTable +import com.bbit.ticket.database.system.SysRoleTable +import com.bbit.ticket.database.system.SysUserRoleTable +import com.bbit.ticket.database.system.SysUserTable +import com.bbit.ticket.database.system.SysUserTable.avatar +import com.bbit.ticket.database.system.SysUserTable.email +import com.bbit.ticket.database.system.SysUserTable.nickname +import com.bbit.ticket.database.system.SysUserTable.phone +import com.bbit.ticket.database.system.SysUserTable.realName +import com.bbit.ticket.entity.system.CreateUserRequest +import com.bbit.ticket.entity.system.UpdateUserPasswordRequest +import com.bbit.ticket.entity.system.UpdateUserRequest +import com.bbit.ticket.entity.system.UpdateUserRolesRequest +import com.bbit.ticket.entity.system.UpdateUserStatusRequest +import com.bbit.ticket.entity.system.UserDetailResponse +import com.bbit.ticket.entity.system.UserListItem +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.entity.common.PageResult +import com.bbit.ticket.entity.common.statusLabel +import com.bbit.ticket.plugins.dbQuery +import com.bbit.ticket.utils.parseUuid +import io.ktor.http.HttpStatusCode +import org.jetbrains.exposed.v1.core.Op +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.inList +import org.jetbrains.exposed.v1.core.isNull +import org.jetbrains.exposed.v1.core.like +import org.jetbrains.exposed.v1.jdbc.deleteWhere +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.insertIgnore +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.update +import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +object UserService { + suspend fun list( + page: Int, + pageSize: Int, + username: String?, + nickname: String?, + status: String?, + orgId: Uuid?, + ): PageResult = dbQuery { + val where = buildWhere(username, nickname, status, orgId) + val total = SysUserTable.selectAll().where { where }.count() + val rows = SysUserTable.selectAll() + .where { where } + .orderBy(SysUserTable.createdAt) + .limit(pageSize) + .offset(((page - 1) * pageSize).toLong()) + .toList() + + val userIds = rows.map { it[SysUserTable.id] } + val roleMap = if (userIds.isEmpty()) { + emptyMap() + } else { + (SysUserRoleTable innerJoin SysRoleTable).selectAll() + .where { + (SysUserRoleTable.userId inList userIds) and + SysRoleTable.deletedAt.isNull() + } + .groupBy { it[SysUserRoleTable.userId] } + .mapValues { entry -> entry.value.map { row -> row[SysRoleTable.code] }.distinct() } + } + + PageResult( + items = rows.map { row -> + UserListItem( + id = row[SysUserTable.id].toString(), + username = row[SysUserTable.username], + nickname = row[SysUserTable.nickname], + realName = row[realName], + orgId = row[SysUserTable.orgId]?.toString(), + status = row[SysUserTable.status], + statusLabel = statusLabel(row[SysUserTable.status]), + roleCodes = roleMap[row[SysUserTable.id]] ?: emptyList(), + ) + }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + suspend fun create(request: CreateUserRequest): String = dbQuery { + val username = request.username.trim() + if (username.isBlank() || request.password.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "用户名和密码不能为空") + } + val existed = SysUserTable.selectAll().where { + (SysUserTable.username eq username) and SysUserTable.deletedAt.isNull() + }.any() + if (existed) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "用户名已存在") + } + + val orgUuid = request.orgId?.let { parseUuid(it, "orgId") } + if (orgUuid != null) { + ensureOrgExists(orgUuid) + } + val now = OffsetDateTime.now() + val row = SysUserTable.insert { + it[SysUserTable.username] = username + it[passwordHash] = PasswordService.hash(request.password) + it[nickname] = request.nickname?.trim() + it[realName] = request.realName?.trim() + it[phone] = request.phone?.trim() + it[email] = request.email?.trim() + it[avatar] = request.avatar?.trim() + it[orgId] = orgUuid + it[status] = request.status + it[tokenVersion] = 1 + it[createdAt] = now + } + row[SysUserTable.id].toString() + } + + suspend fun detail(id: Uuid): UserDetailResponse = dbQuery { + val user = requireUser(id) + val roleIds = SysUserRoleTable.selectAll().where { SysUserRoleTable.userId eq id } + .map { it[SysUserRoleTable.roleId].toString() } + UserDetailResponse( + id = user[SysUserTable.id].toString(), + username = user[SysUserTable.username], + nickname = user[nickname], + realName = user[realName], + phone = user[phone], + email = user[email], + avatar = user[avatar], + orgId = user[SysUserTable.orgId]?.toString(), + status = user[SysUserTable.status], + statusLabel = statusLabel(user[SysUserTable.status]), + roleIds = roleIds, + ) + } + + suspend fun update(id: Uuid, request: UpdateUserRequest) = dbQuery { + requireUser(id) + val orgUuid = request.orgId?.let { parseUuid(it, "orgId") } + if (orgUuid != null) { + ensureOrgExists(orgUuid) + } + SysUserTable.update({ SysUserTable.id eq id }) { + it[nickname] = request.nickname?.trim() + it[realName] = request.realName?.trim() + it[phone] = request.phone?.trim() + it[email] = request.email?.trim() + it[avatar] = request.avatar?.trim() + it[orgId] = orgUuid + it[updatedAt] = OffsetDateTime.now() + } + } + + suspend fun softDelete(id: Uuid) = dbQuery { + if (id.toString() == "00000000-0000-0000-0000-000000000000") { + throw BizException(ErrorCode.BAD_REQUEST.code, "系统保留用户不可删除") + } + requireUser(id) + SysUserTable.update({ SysUserTable.id eq id }) { + it[deletedAt] = OffsetDateTime.now() + } + SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id } + } + + suspend fun updateStatus(id: Uuid, request: UpdateUserStatusRequest) = dbQuery { + requireUser(id) + SysUserTable.update({ SysUserTable.id eq id }) { + it[status] = request.status + it[updatedAt] = OffsetDateTime.now() + } + } + + suspend fun updatePassword(id: Uuid, request: UpdateUserPasswordRequest) = dbQuery { + val user = requireUser(id) + if (request.password.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "密码不能为空") + } + val nextTokenVersion = user[SysUserTable.tokenVersion] + 1 + SysUserTable.update({ SysUserTable.id eq id }) { + it[passwordHash] = PasswordService.hash(request.password) + it[tokenVersion] = nextTokenVersion + it[updatedAt] = OffsetDateTime.now() + } + } + + suspend fun updateRoles(id: Uuid, request: UpdateUserRolesRequest) = dbQuery { + requireUser(id) + val roleIds = request.roleIds.distinct().map { parseUuid(it, "roleId") } + if (roleIds.isNotEmpty()) { + val validCount = SysRoleTable.selectAll().where { + (SysRoleTable.id inList roleIds) and + (SysRoleTable.status eq "ENABLED") and + SysRoleTable.deletedAt.isNull() + }.count() + if (validCount != roleIds.size.toLong()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "包含不存在或已禁用角色") + } + } + SysUserRoleTable.deleteWhere { SysUserRoleTable.userId eq id } + roleIds.forEach { roleId -> + SysUserRoleTable.insertIgnore { + it[userId] = id + it[SysUserRoleTable.roleId] = roleId + } + } + } + + private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op { + 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) + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/common/DateTimeFormats.kt b/server/src/main/kotlin/com/bbit/ticket/utils/DateTimeFormats.kt similarity index 93% rename from server/src/main/kotlin/com/bbit/ticket/common/DateTimeFormats.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/DateTimeFormats.kt index de2b4e3..8c80c41 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/DateTimeFormats.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/DateTimeFormats.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.utils import java.time.OffsetDateTime import java.time.ZoneId diff --git a/server/src/main/kotlin/com/bbit/ticket/common/RequestUtils.kt b/server/src/main/kotlin/com/bbit/ticket/utils/RequestUtils.kt similarity index 90% rename from server/src/main/kotlin/com/bbit/ticket/common/RequestUtils.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/RequestUtils.kt index 2e0d756..378b915 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/RequestUtils.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/RequestUtils.kt @@ -1,5 +1,7 @@ -package com.bbit.ticket.common +package com.bbit.ticket.utils +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall import kotlin.uuid.ExperimentalUuidApi diff --git a/server/src/main/kotlin/com/bbit/ticket/security/RequirePermission.kt b/server/src/main/kotlin/com/bbit/ticket/utils/RequirePermission.kt similarity index 77% rename from server/src/main/kotlin/com/bbit/ticket/security/RequirePermission.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/RequirePermission.kt index 41470a8..a7282e2 100644 --- a/server/src/main/kotlin/com/bbit/ticket/security/RequirePermission.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/RequirePermission.kt @@ -1,7 +1,7 @@ -package com.bbit.ticket.security +package com.bbit.ticket.utils -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall diff --git a/server/src/main/kotlin/com/bbit/ticket/security/SecurityPrincipal.kt b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt similarity index 96% rename from server/src/main/kotlin/com/bbit/ticket/security/SecurityPrincipal.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt index 5ecc31c..9c943e4 100644 --- a/server/src/main/kotlin/com/bbit/ticket/security/SecurityPrincipal.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt @@ -1,9 +1,9 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(ExperimentalUuidApi::class) -package com.bbit.ticket.security +package com.bbit.ticket.utils -import com.bbit.ticket.common.BizException -import com.bbit.ticket.common.ErrorCode +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode import com.bbit.ticket.database.system.SysMenuTable import com.bbit.ticket.database.system.SysRoleMenuTable import com.bbit.ticket.database.system.SysRoleTable @@ -21,6 +21,7 @@ import org.jetbrains.exposed.v1.core.inList import org.jetbrains.exposed.v1.core.isNotNull import org.jetbrains.exposed.v1.core.isNull import org.jetbrains.exposed.v1.jdbc.selectAll +import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid data class CurrentUser( diff --git a/server/src/main/kotlin/com/bbit/ticket/common/TraceContext.kt b/server/src/main/kotlin/com/bbit/ticket/utils/TraceContext.kt similarity index 87% rename from server/src/main/kotlin/com/bbit/ticket/common/TraceContext.kt rename to server/src/main/kotlin/com/bbit/ticket/utils/TraceContext.kt index dd3965f..8ffde11 100644 --- a/server/src/main/kotlin/com/bbit/ticket/common/TraceContext.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/TraceContext.kt @@ -1,4 +1,4 @@ -package com.bbit.ticket.common +package com.bbit.ticket.utils import io.ktor.server.application.ApplicationCall import io.ktor.util.AttributeKey diff --git a/web/src/api/http.ts b/web/src/api/http.ts index 2718523..b3bda0f 100644 --- a/web/src/api/http.ts +++ b/web/src/api/http.ts @@ -55,9 +55,7 @@ http.interceptors.response.use( } if (status === 403) { - if (router.currentRoute.value.path !== '/403') { - await router.replace('/403') - } + console.warn('[HTTP] 收到 403 响应, URL:', error.config?.url, 'Message:', backendMessage) message.error(backendMessage ?? '无权限访问该资源') return Promise.reject(new BizError('403', backendMessage ?? '无权限', traceId)) } diff --git a/web/src/components/AppMenu.vue b/web/src/components/AppMenu.vue index c1ab35c..7d04611 100644 --- a/web/src/components/AppMenu.vue +++ b/web/src/components/AppMenu.vue @@ -37,7 +37,7 @@ function toOption(menu: MenuNode): MenuOption | null { .map((item) => toOption(item)) .filter((item): item is MenuOption => Boolean(item)) - const key = menu.path || `catalog-${menu.id}` + const key = (menu.path && menu.type !== 'CATALOG') ? menu.path : `catalog-${menu.id}` return { key, label: menu.title, diff --git a/web/vite.config.ts b/web/vite.config.ts index dd17b71..5696bfc 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig(({ mode }) => { }, server: { host: '0.0.0.0', - port: 5173, + port: 5180, open: false, proxy: proxyTarget ? {