From 3bbd8941a90ded4cce7a523530bd224d9b02634c Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Thu, 7 May 2026 16:47:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=91=E9=80=9APT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/bbit/ticket/Application.kt | 21 +- .../com/bbit/ticket/bootstrap/Global.kt | 52 +++ .../com/bbit/ticket/bootstrap/SeedData.kt | 406 ++++++++++++++++-- .../com/bbit/ticket/dao/system/UserDao.kt | 10 +- .../ticket/database/system/SysUserTable.kt | 3 + .../bbit/ticket/entity/common/PTException.kt | 7 + .../ticket/entity/request/TaxBureauAuthReq.kt | 9 + .../bbit/ticket/entity/response/PTResponse.kt | 20 + .../response/TaxBureauAccountAuthContent.kt | 75 ++++ .../route/piaotong/registerPTTestRoutes.kt | 43 ++ .../ticket/route/system/registerAuthRoutes.kt | 2 +- .../ticket/route/system/registerDictRoutes.kt | 2 +- .../route/system/registerLogsQueryRoutes.kt | 2 +- .../ticket/route/system/registerMenuRoutes.kt | 2 +- .../ticket/route/system/registerOrgRoutes.kt | 2 +- .../ticket/route/system/registerRoleRoutes.kt | 2 +- .../ticket/route/system/registerUserRoutes.kt | 2 +- .../ticket/service/piaotong/PTAuthService.kt | 24 ++ .../bbit/ticket/utils/SecurityPrincipal.kt | 5 + .../com/bbit/ticket/utils/net/PTClient.kt | 184 ++++++++ .../com/bbit/ticket/utils/net/RSAUtil.kt | 120 ++++++ .../com/bbit/ticket/utils/net/SecurityUtil.kt | 65 +++ 22 files changed, 999 insertions(+), 59 deletions(-) create mode 100644 server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/common/PTException.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/TaxBureauAuthReq.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/PTResponse.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/utils/net/RSAUtil.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/utils/net/SecurityUtil.kt diff --git a/server/src/main/kotlin/com/bbit/ticket/Application.kt b/server/src/main/kotlin/com/bbit/ticket/Application.kt index c7f3709..f2b1abd 100644 --- a/server/src/main/kotlin/com/bbit/ticket/Application.kt +++ b/server/src/main/kotlin/com/bbit/ticket/Application.kt @@ -4,6 +4,7 @@ import com.bbit.ticket.bootstrap.DatabaseInitializer import com.bbit.ticket.bootstrap.SeedData import com.bbit.ticket.entity.common.ok import com.bbit.ticket.bootstrap.AppConfig +import com.bbit.ticket.bootstrap.Global import com.bbit.ticket.route.system.registerAuthRoutes import com.bbit.ticket.plugins.configureCors import com.bbit.ticket.plugins.configureDatabase @@ -14,6 +15,7 @@ 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.piaotong.registerPTTestRoutes import com.bbit.ticket.route.system.registerDictRoutes import com.bbit.ticket.route.system.registerLogsQueryRoutes import com.bbit.ticket.route.system.registerMenuRoutes @@ -25,6 +27,7 @@ import io.ktor.server.application.Application import io.ktor.server.netty.EngineMain import io.ktor.server.response.respond import io.ktor.server.routing.get +import io.ktor.server.routing.route import io.ktor.server.routing.routing fun main(args: Array) { @@ -52,12 +55,16 @@ fun Application.module() { get("/health") { call.respond(ok(mapOf("status" to "UP", "service" to AppConfig.app.name))) } - registerAuthRoutes() - registerUserRoutes() - registerOrgRoutes() - registerRoleRoutes() - registerMenuRoutes() - registerDictRoutes() - registerLogsQueryRoutes() + route("/api"){ + registerAuthRoutes() + registerUserRoutes() + registerOrgRoutes() + registerRoleRoutes() + registerMenuRoutes() + registerDictRoutes() + registerLogsQueryRoutes() + + registerPTTestRoutes() + } } } diff --git a/server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt new file mode 100644 index 0000000..68b9099 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt @@ -0,0 +1,52 @@ +package com.bbit.ticket.bootstrap + +object Global { + + // 测试账号 销售方税号 + const val testTaxpayerNum = "500102201007206608" + const val testAccount = "DEMOadmin" + + val isDev = true + + // 请求基础地址 + var baseUrl: String + + // 票通私钥 + var ptPrivateKey: String + + // 票通公钥 + var ptPublicKey: String + + // 票通密码 + var ptPassword: String + + // 票通平台简称 + var ptPlatformAlias: String + + // 票通编码 + var ptPlatformCode: String + + + init { + if (isDev) { + baseUrl = "http://fpkj.testnw.vpiaotong.cn/tp/openapi/" + ptPrivateKey = + "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIVLAoolDaE7m5oMB1ZrILHkMXMF6qmC8I/FCejz4hwBcj59H3rbtcycBEmExOJTGwexFkNgRakhqM+3uP3VybWu1GBYNmqVzggWKKzThul9VPE3+OTMlxeG4H63RsCO1//J0MoUavXMMkL3txkZBO5EtTqek182eePOV8fC3ZxpAgMBAAECgYBp4Gg3BTGrZaa2mWFmspd41lK1E/kPBrRA7vltMfPj3P47RrYvp7/js/Xv0+d0AyFQXcjaYelTbCokPMJT1nJumb2A/Cqy3yGKX3Z6QibvByBlCKK29lZkw8WVRGFIzCIXhGKdqukXf8RyqfhInqHpZ9AoY2W60bbSP6EXj/rhNQJBAL76SmpQOrnCI8Xu75di0eXBN/bE9tKsf7AgMkpFRhaU8VLbvd27U9vRWqtu67RY3sOeRMh38JZBwAIS8tp5hgcCQQCyrOS6vfXIUxKoWyvGyMyhqoLsiAdnxBKHh8tMINo0ioCbU+jc2dgPDipL0ym5nhvg5fCXZC2rvkKUltLEqq4PAkAqBf9b932EpKCkjFgyUq9nRCYhaeP6JbUPN3Z5e1bZ3zpfBjV4ViE0zJOMB6NcEvYpy2jNR/8rwRoUGsFPq8//AkAklw18RJyJuqFugsUzPznQvad0IuNJV7jnsmJqo6ur6NUvef6NA7ugUalNv9+imINjChO8HRLRQfRGk6B0D/P3AkBt54UBMtFefOLXgUdilwLdCUSw4KpbuBPw+cyWlMjcXCkj4rHoeksekyBH1GrBJkLqDMRqtVQUubuFwSzBAtlc" + ptPublicKey = + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJkx3HelhEm/U7jOCor29oHsIjCMSTyKbX5rpoAY8KDIs9mmr5Y9r+jvNJH8pK3u5gNnvleT6rQgJQW1mk0zHuPO00vy62tSA53fkSjtM+n0oC1Fkm4DRFd5qJgoP7uFQHR5OEffMjy2qIuxChY4Au0kq+6RruEgIttb7wUxy8TwIDAQAB" + + ptPassword = "lsBnINDxtct8HZB7KCMyhWSJ" + ptPlatformAlias = "DEMK" + ptPlatformCode = "11111111" + } else { + baseUrl = "https://fpkj.vpiaotong.com/tp/openapi/" + ptPrivateKey = "" + ptPublicKey = "" + ptPassword = "" + ptPlatformAlias = "" + ptPlatformCode = "" + } + } + + +} \ No newline at end of file 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 bd28612..d7325b1 100644 --- a/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt @@ -114,21 +114,24 @@ object SeedData { it[SysUserTable.orgId] = orgId it[status] = "ENABLED" it[updatedAt] = now + it[taxpayerNum] = "500102201007206608" + it[account] = "DEMOadmin" } return@dbQuery id + } else { + val inserted = SysUserTable.insert { + it[username] = ADMIN_USERNAME + it[passwordHash] = PasswordService.hash(ADMIN_INIT_PASSWORD) + it[nickname] = "管理员" + it[realName] = "系统管理员" + it[SysUserTable.orgId] = orgId + it[status] = "ENABLED" + it[tokenVersion] = 1 + it[taxpayerNum] = "500102201007206608" + it[account] = "DEMOadmin" + } + inserted[SysUserTable.id] } - - val inserted = SysUserTable.insert { - it[username] = ADMIN_USERNAME - it[passwordHash] = PasswordService.hash(ADMIN_INIT_PASSWORD) - it[nickname] = "管理员" - it[realName] = "系统管理员" - it[SysUserTable.orgId] = orgId - it[status] = "ENABLED" - it[tokenVersion] = 1 - it[createdAt] = now - } - inserted[SysUserTable.id] } private suspend fun upsertUserRole(userId: Uuid, roleId: Uuid) = dbQuery { @@ -145,32 +148,357 @@ object SeedData { private suspend fun upsertMenus(now: OffsetDateTime): List { val seedMenus = listOf( - SeedMenu("dashboard", null, "MENU", "工作台", "Dashboard", "/dashboard", "dashboard/index", "LayoutDashboard", null, 10, true, true), - SeedMenu("system", null, "CATALOG", "系统管理", "SystemRoot", "/system", null, "Settings", null, 20, true, false), - SeedMenu("system_user", "system", "MENU", "用户管理", "SystemUsers", "/system/users", "system/users/index", "Users", "system:user:view", 10, true, true), - SeedMenu("system_user_create", "system_user", "BUTTON", "新增用户", "SystemUserCreate", null, null, null, "system:user:create", 1, true, false), - SeedMenu("system_user_update", "system_user", "BUTTON", "修改用户", "SystemUserUpdate", null, null, null, "system:user:update", 2, true, false), - SeedMenu("system_user_delete", "system_user", "BUTTON", "删除用户", "SystemUserDelete", null, null, null, "system:user:delete", 3, true, false), - SeedMenu("system_org", "system", "MENU", "组织管理", "SystemOrgs", "/system/orgs", "system/orgs/index", "Building2", "system:org:view", 20, true, true), - SeedMenu("system_org_create", "system_org", "BUTTON", "新增组织", "SystemOrgCreate", null, null, null, "system:org:create", 1, true, false), - SeedMenu("system_org_update", "system_org", "BUTTON", "更新组织", "SystemOrgUpdate", null, null, null, "system:org:update", 2, true, false), - SeedMenu("system_org_delete", "system_org", "BUTTON", "删除组织", "SystemOrgDelete", null, null, null, "system:org:delete", 3, true, false), - SeedMenu("system_role", "system", "MENU", "角色管理", "SystemRoles", "/system/roles", "system/roles/index", "Shield", "system:role:view", 30, true, true), - SeedMenu("system_role_create", "system_role", "BUTTON", "新增角色", "SystemRoleCreate", null, null, null, "system:role:create", 1, true, false), - SeedMenu("system_role_update", "system_role", "BUTTON", "更新角色", "SystemRoleUpdate", null, null, null, "system:role:update", 2, true, false), - SeedMenu("system_role_delete", "system_role", "BUTTON", "删除角色", "SystemRoleDelete", null, null, null, "system:role:delete", 3, true, false), - SeedMenu("system_role_assign", "system_role", "BUTTON", "分配角色权限", "SystemRoleAssign", null, null, null, "system:role:assign", 4, true, false), - SeedMenu("system_menu", "system", "MENU", "菜单管理", "SystemMenus", "/system/menus", "system/menus/index", "PanelLeft", "system:menu:view", 40, true, true), - SeedMenu("system_menu_create", "system_menu", "BUTTON", "新增菜单", "SystemMenuCreate", null, null, null, "system:menu:create", 1, true, false), - SeedMenu("system_menu_update", "system_menu", "BUTTON", "更新菜单", "SystemMenuUpdate", null, null, null, "system:menu:update", 2, true, false), - SeedMenu("system_menu_delete", "system_menu", "BUTTON", "删除菜单", "SystemMenuDelete", null, null, null, "system:menu:delete", 3, true, false), - SeedMenu("system_dict", "system", "MENU", "字典管理", "SystemDict", "/system/dicts", "system/dicts/index", "BookType", "system:dict:view", 50, true, true), - SeedMenu("system_dict_create", "system_dict", "BUTTON", "新增字典", "SystemDictCreate", null, null, null, "system:dict:create", 1, true, false), - SeedMenu("system_dict_update", "system_dict", "BUTTON", "更新字典", "SystemDictUpdate", null, null, null, "system:dict:update", 2, true, false), - SeedMenu("system_dict_delete", "system_dict", "BUTTON", "删除字典", "SystemDictDelete", null, null, null, "system:dict:delete", 3, true, false), + SeedMenu( + "dashboard", + null, + "MENU", + "工作台", + "Dashboard", + "/dashboard", + "dashboard/index", + "LayoutDashboard", + null, + 10, + true, + true + ), + SeedMenu( + "system", + null, + "CATALOG", + "系统管理", + "SystemRoot", + "/system", + null, + "Settings", + null, + 20, + true, + false + ), + SeedMenu( + "system_user", + "system", + "MENU", + "用户管理", + "SystemUsers", + "/system/users", + "system/users/index", + "Users", + "system:user:view", + 10, + true, + true + ), + SeedMenu( + "system_user_create", + "system_user", + "BUTTON", + "新增用户", + "SystemUserCreate", + null, + null, + null, + "system:user:create", + 1, + true, + false + ), + SeedMenu( + "system_user_update", + "system_user", + "BUTTON", + "修改用户", + "SystemUserUpdate", + null, + null, + null, + "system:user:update", + 2, + true, + false + ), + SeedMenu( + "system_user_delete", + "system_user", + "BUTTON", + "删除用户", + "SystemUserDelete", + null, + null, + null, + "system:user:delete", + 3, + true, + false + ), + SeedMenu( + "system_org", + "system", + "MENU", + "组织管理", + "SystemOrgs", + "/system/orgs", + "system/orgs/index", + "Building2", + "system:org:view", + 20, + true, + true + ), + SeedMenu( + "system_org_create", + "system_org", + "BUTTON", + "新增组织", + "SystemOrgCreate", + null, + null, + null, + "system:org:create", + 1, + true, + false + ), + SeedMenu( + "system_org_update", + "system_org", + "BUTTON", + "更新组织", + "SystemOrgUpdate", + null, + null, + null, + "system:org:update", + 2, + true, + false + ), + SeedMenu( + "system_org_delete", + "system_org", + "BUTTON", + "删除组织", + "SystemOrgDelete", + null, + null, + null, + "system:org:delete", + 3, + true, + false + ), + SeedMenu( + "system_role", + "system", + "MENU", + "角色管理", + "SystemRoles", + "/system/roles", + "system/roles/index", + "Shield", + "system:role:view", + 30, + true, + true + ), + SeedMenu( + "system_role_create", + "system_role", + "BUTTON", + "新增角色", + "SystemRoleCreate", + null, + null, + null, + "system:role:create", + 1, + true, + false + ), + SeedMenu( + "system_role_update", + "system_role", + "BUTTON", + "更新角色", + "SystemRoleUpdate", + null, + null, + null, + "system:role:update", + 2, + true, + false + ), + SeedMenu( + "system_role_delete", + "system_role", + "BUTTON", + "删除角色", + "SystemRoleDelete", + null, + null, + null, + "system:role:delete", + 3, + true, + false + ), + SeedMenu( + "system_role_assign", + "system_role", + "BUTTON", + "分配角色权限", + "SystemRoleAssign", + null, + null, + null, + "system:role:assign", + 4, + true, + false + ), + SeedMenu( + "system_menu", + "system", + "MENU", + "菜单管理", + "SystemMenus", + "/system/menus", + "system/menus/index", + "PanelLeft", + "system:menu:view", + 40, + true, + true + ), + SeedMenu( + "system_menu_create", + "system_menu", + "BUTTON", + "新增菜单", + "SystemMenuCreate", + null, + null, + null, + "system:menu:create", + 1, + true, + false + ), + SeedMenu( + "system_menu_update", + "system_menu", + "BUTTON", + "更新菜单", + "SystemMenuUpdate", + null, + null, + null, + "system:menu:update", + 2, + true, + false + ), + SeedMenu( + "system_menu_delete", + "system_menu", + "BUTTON", + "删除菜单", + "SystemMenuDelete", + null, + null, + null, + "system:menu:delete", + 3, + true, + false + ), + SeedMenu( + "system_dict", + "system", + "MENU", + "字典管理", + "SystemDict", + "/system/dicts", + "system/dicts/index", + "BookType", + "system:dict:view", + 50, + true, + true + ), + SeedMenu( + "system_dict_create", + "system_dict", + "BUTTON", + "新增字典", + "SystemDictCreate", + null, + null, + null, + "system:dict:create", + 1, + true, + false + ), + SeedMenu( + "system_dict_update", + "system_dict", + "BUTTON", + "更新字典", + "SystemDictUpdate", + null, + null, + null, + "system:dict:update", + 2, + true, + false + ), + SeedMenu( + "system_dict_delete", + "system_dict", + "BUTTON", + "删除字典", + "SystemDictDelete", + null, + null, + null, + "system:dict:delete", + 3, + true, + false + ), SeedMenu("logs", null, "CATALOG", "日志管理", "LogsRoot", "/logs", null, "Logs", null, 30, true, false), - SeedMenu("logs_operation", "logs", "MENU", "操作日志", "LogsOperation", "/logs/operation", "logs/operation/index", "ScrollText", "log:operation:view", 10, true, true), - SeedMenu("logs_api_access", "logs", "MENU", "接口日志", "LogsApiAccess", "/logs/api-access", "logs/api-access/index", "Waypoints", "log:api-access:view", 20, true, true), + SeedMenu( + "logs_operation", + "logs", + "MENU", + "操作日志", + "LogsOperation", + "/logs/operation", + "logs/operation/index", + "ScrollText", + "log:operation:view", + 10, + true, + true + ), + SeedMenu( + "logs_api_access", + "logs", + "MENU", + "接口日志", + "LogsApiAccess", + "/logs/api-access", + "logs/api-access/index", + "Waypoints", + "log:api-access:view", + 20, + true, + true + ), ) val idMap = mutableMapOf() @@ -311,8 +639,8 @@ object SeedData { val existing = SysDictItemTable.selectAll() .where { (SysDictItemTable.typeId eq typeId) and - (SysDictItemTable.value eq value) and - SysDictItemTable.deletedAt.isNull() + (SysDictItemTable.value eq value) and + SysDictItemTable.deletedAt.isNull() } .singleOrNull() diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt index e0c8154..0ce821a 100644 --- a/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt @@ -20,7 +20,6 @@ import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.inList import org.jetbrains.exposed.v1.core.isNull import org.jetbrains.exposed.v1.core.like -import org.jetbrains.exposed.v1.jdbc.Query import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.insert import org.jetbrains.exposed.v1.jdbc.insertIgnore @@ -59,10 +58,10 @@ object UserDao { } fun findByUsername(username: String): ResultRow? = - activeUsers().where { SysUserTable.username eq username }.singleOrNull() + SysUserTable.selectAll().where { activeWhere() and (SysUserTable.username eq username) }.singleOrNull() fun requireActive(id: Uuid): ResultRow = - activeUsers().where { SysUserTable.id eq id }.singleOrNull() + SysUserTable.selectAll().where { activeWhere() and (SysUserTable.id eq id) }.singleOrNull() ?: throw BizException( ErrorCode.USER_NOT_FOUND.code, ErrorCode.USER_NOT_FOUND.message, @@ -179,11 +178,10 @@ object UserDao { } } - private fun activeUsers(): Query = - SysUserTable.selectAll().where { SysUserTable.deletedAt.isNull() } + private fun activeWhere(): Op = SysUserTable.deletedAt.isNull() private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op { - var where: Op = SysUserTable.deletedAt.isNull() + var where: Op = activeWhere() if (!username.isNullOrBlank()) { where = where and (SysUserTable.username like "%$username%") } diff --git a/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt index 0fe49bf..8b65ce7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt @@ -28,5 +28,8 @@ object SysUserTable : Table("sys_user") { val deletedBy = uuid("deleted_by").nullable() val version = integer("version").default(1) + val taxpayerNum = varchar("taxpayer_num", 50).nullable() + val account = varchar("account", 50).nullable() + override val primaryKey = PrimaryKey(id) } diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/common/PTException.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/PTException.kt new file mode 100644 index 0000000..20eb1d0 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/PTException.kt @@ -0,0 +1,7 @@ +package com.bbit.ticket.entity.common + +class PTException( + val code: String, + override val message: String, + val serialNo: String? = null +) : RuntimeException(message) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxBureauAuthReq.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxBureauAuthReq.kt new file mode 100644 index 0000000..d995547 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxBureauAuthReq.kt @@ -0,0 +1,9 @@ +package com.bbit.ticket.entity.request + +import kotlinx.serialization.Serializable + +@Serializable +data class TaxBureauAuthReq( + val taxpayerNum: String, + val account: String +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/PTResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/PTResponse.kt new file mode 100644 index 0000000..0bbcecb --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/PTResponse.kt @@ -0,0 +1,20 @@ +package com.bbit.ticket.entity.response + +import kotlinx.serialization.Serializable + +@Serializable +data class PTResponse( + val code: String, + val msg: String, + val sign: String, + val serialNo: String, + val content: T? = null +) +@Serializable +data class PTResponseString( + val code: String, + val msg: String, + val sign: String, + val serialNo: String, + val content: String? = null +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt new file mode 100644 index 0000000..84268a8 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt @@ -0,0 +1,75 @@ +package com.bbit.ticket.entity.response + +import kotlinx.serialization.Serializable + +@Serializable +data class TaxBureauAccountAuthContent( + + /** + * 微信用户绑定状态 + * 0-未绑定 + * 1-已绑定 + */ + val wechatUserBindStatus: String, + + /** + * 操作建议 + */ + val operationProposed: String, + + /** + * 实名认证状态 + */ + val authStatus: String, + + /** + * 登录认证状态 + */ + val loginAuthStatus: String, + + /** + * 最后登录认证时间 + */ + val lastLoginAuthTime: String, + + /** + * 最后风险认证时间 + */ + val lastRiskAuthTime: String, + + /** + * 最后认证成功时间 + */ + val lastAuthSuccTime: String, + + /** + * 身份类型 + * 01-法人 + */ + val identityType: String, + + /** + * 姓名 + */ + val name: String, + + /** + * 纳税人识别号 + */ + val taxpayerNum: String, + + /** + * 是否允许切换 + */ + val switchable: String, + + /** + * 风险认证状态 + */ + val riskAuthStatus: String, + + /** + * 电子税局账号 + */ + val account: String +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt new file mode 100644 index 0000000..ae6e6ec --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt @@ -0,0 +1,43 @@ +package com.bbit.ticket.route.piaotong + +import com.bbit.ticket.bootstrap.Global +import com.bbit.ticket.entity.common.PTException +import com.bbit.ticket.entity.common.fail +import com.bbit.ticket.entity.common.ok +import com.bbit.ticket.entity.request.TaxBureauAuthReq +import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent +import com.bbit.ticket.service.piaotong.PTAuthService +import com.bbit.ticket.utils.requireCurrentUser +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.json.Json +import kotlinx.serialization.json.decodeFromJsonElement + +fun Route.registerPTTestRoutes() { + route("/pt") { + authenticate("auth-jwt") { + get("/info") { + try { + val currentUser = call.requireCurrentUser() + val taxpayerNum = currentUser.taxpayerNum ?: Global.testTaxpayerNum + val account = currentUser.account ?: Global.testAccount + val response = PTAuthService.getTaxBureauAccountAuthStatus( + TaxBureauAuthReq(taxpayerNum, account) + ) + call.respond(ok(response)) + } catch (e: PTException) { + call.respond( + fail( + code = e.code, + message = e.message, + traceId = e.serialNo + ) + ) + } + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt index cd85a1f..57adfff 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt @@ -17,7 +17,7 @@ import kotlin.time.TimeSource fun Route.registerAuthRoutes() { - route("/api/auth") { + route("/auth") { post("/login") { val start = TimeSource.Monotonic.markNow() val request = call.receive() 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 index 7a1a78a..92509f2 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt @@ -27,7 +27,7 @@ import kotlin.uuid.ExperimentalUuidApi fun Route.registerDictRoutes() { authenticate("auth-jwt") { - route("/api/system/dict-types") { + route("/system/dict-types") { get { call.requirePermission("system:dict:view") val page = call.queryInt("page", 1) 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 index cc8bdc0..ddfea56 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerLogsQueryRoutes.kt @@ -13,7 +13,7 @@ import io.ktor.server.routing.route fun Route.registerLogsQueryRoutes() { authenticate("auth-jwt") { - route("/api/logs") { + route("/logs") { get("/operation") { call.requirePermission("log:operation:view") val page = call.queryInt("page", 1) 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 index 1bbd607..5172690 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerMenuRoutes.kt @@ -23,7 +23,7 @@ import kotlin.uuid.ExperimentalUuidApi fun Route.registerMenuRoutes() { authenticate("auth-jwt") { - route("/api/system/menus") { + route("/system/menus") { get { call.requirePermission("system:menu:view") call.respond(ok(MenuService.tree())) 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 index 568b757..bfedd0f 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerOrgRoutes.kt @@ -23,7 +23,7 @@ import kotlin.uuid.ExperimentalUuidApi fun Route.registerOrgRoutes() { authenticate("auth-jwt") { - route("/api/system/orgs") { + route("/system/orgs") { get { call.requirePermission("system:org:view") call.respond(ok(OrgService.tree())) 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 index 62c51db..5dce41f 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerRoleRoutes.kt @@ -26,7 +26,7 @@ import kotlin.uuid.ExperimentalUuidApi fun Route.registerRoleRoutes() { authenticate("auth-jwt") { - route("/api/system/roles") { + route("/system/roles") { get { call.requirePermission("system:role:view") val page = call.queryInt("page", 1) 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 index b66a08c..d573e43 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerUserRoutes.kt @@ -28,7 +28,7 @@ import kotlin.uuid.ExperimentalUuidApi fun Route.registerUserRoutes() { authenticate("auth-jwt") { - route("/api/system/users") { + route("/system/users") { get { call.requirePermission("system:user:view") val page = call.queryInt("page", 1) diff --git a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt new file mode 100644 index 0000000..62d7d4e --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt @@ -0,0 +1,24 @@ +package com.bbit.ticket.service.piaotong + +import com.bbit.ticket.entity.request.TaxBureauAuthReq +import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent +import com.bbit.ticket.utils.net.PTClient + +object PTAuthService { + + /** + * 查询数电账号认证状态 + * 此接口用来查询数电账号的认证状态,会返回当 + * + * @param taxpayerNum 纳税人识别号 + * @param account 账号 + */ + suspend fun getTaxBureauAccountAuthStatus(req : TaxBureauAuthReq): String { + val res = PTClient.ptPost( + url = "getTaxBureauAccountAuthStatus.pt", + body = req + ) + println("res = $res") + return res.taxpayerNum + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt index 9c943e4..8625e29 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt @@ -9,6 +9,7 @@ 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.database.system.SysUserTable.account import com.bbit.ticket.plugins.dbQuery import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall @@ -31,6 +32,8 @@ data class CurrentUser( val tokenVersion: Int, val roleCodes: Set, val permissions: Set, + val taxpayerNum: String?, + val account: String?, ) { val isSuperAdmin: Boolean get() = roleCodes.contains("SUPER_ADMIN") @@ -135,6 +138,8 @@ suspend fun ApplicationCall.requireCurrentUser(): CurrentUser { tokenVersion = userRow[SysUserTable.tokenVersion], roleCodes = roleCodes, permissions = permissions, + taxpayerNum = userRow[SysUserTable.taxpayerNum], + account = userRow[SysUserTable.account], ) attributes.put(CurrentUserKey, currentUser) diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt b/server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt new file mode 100644 index 0000000..659c900 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt @@ -0,0 +1,184 @@ +package com.bbit.ticket.utils.net + +import com.bbit.ticket.bootstrap.Global +import com.bbit.ticket.entity.common.PTException +import com.bbit.ticket.entity.response.PTResponse +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject +import java.text.SimpleDateFormat +import java.util.* + +object PTClient { + + val client = HttpClient(CIO) { + + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + prettyPrint = true + isLenient = true + } + ) + } + } + + /** + * GET 请求 + */ + suspend inline fun ptGet( + url: String, + queryParams: Req, + headers: Map = emptyMap() + ): Resp { + + val response = client.get(Global.baseUrl + url) { + + url { + // ⚠️ 把 Req 转成 JSON 再拆成 query(统一协议口径) + val json = Json.encodeToString(queryParams) + val element = Json.parseToJsonElement(json).jsonObject + + element.forEach { (k, v) -> + parameters.append(k, v.toString().trim('"')) + } + } + + headers.forEach { (k, v) -> + header(k, v) + } + }.bodyAsText() + + val decrypted = disposeResponse(response) + + val result = Json.decodeFromString>(decrypted) + + if (result.code != "0000") { + throw PTException( + code = result.code, + message = result.msg, + serialNo = result.serialNo + ) + } + + return Json.decodeFromJsonElement(result.content!!) + } + + suspend inline fun ptPost( + url: String, + body: Req, + headers: Map = emptyMap() + ): Resp { + + val response = client.post(Global.baseUrl + url) { + contentType(ContentType.Application.Json) + + headers.forEach { (k, v) -> + header(k, v) + } + + setBody(buildRequestData(Json.encodeToString(body))) + }.bodyAsText() + + val decrypted = disposeResponse(response) + + val result = Json.decodeFromString>(decrypted) + + if (result.code != "0000") { + throw PTException( + code = result.code, + message = result.msg, + serialNo = result.serialNo + ) + } + + return Json.decodeFromJsonElement(result.content!!) + } + + /** + * 关闭 + */ + fun close() { + client.close() + } + + + @Throws(Exception::class) + fun buildRequestData(content: String): String { + val reqContent: String = SecurityUtil.encrypt3DES(Global.ptPassword, content) ?: "" + val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + val map = HashMap() + map["platformCode"] = Global.ptPlatformCode + map["signType"] = "RSA" + map["format"] = "JSON" + map["version"] = "1.0" + map["content"] = reqContent + map["timestamp"] = sdf.format(Date()) + map["serialNo"] = ptDate(Global.ptPlatformAlias) + map["sign"] = RSAUtil.sign(RSAUtil.getSignatureContent(map), Global.ptPrivateKey) ?: "" + return Json.encodeToString(map) + } + + fun disposeResponse( + jsonStr: String, + ): String { + + val json = Json.parseToJsonElement(jsonStr).jsonObject + // 1. 转 Map(用于签名验证) + val mutableMap = json + .toMutableMap() + .mapValues { it.value.toString().trim('"') } + .toMutableMap() + + // 2. 取出 sign + val sign = mutableMap.remove("sign") + ?: throw IllegalStateException("sign 为空") + + // 3. 验签内容拼接 + val signContent = RSAUtil.getSignatureContent(mutableMap) + + val verifyOk = RSAUtil.verify(signContent, sign, Global.ptPublicKey) + + if (!verifyOk) { + throw IllegalStateException("验签失败") + } + + // 4. 解密 content(3DES) + val encryptedContent = mutableMap["content"] + ?: throw IllegalStateException("content 为空") + + val plainContent = SecurityUtil.decrypt3DES(Global.ptPassword, encryptedContent) ?: "" + + // 5. 替换 content + val resultJson = buildJsonObject { + json.forEach { (k, v) -> + if (k == "content") { + put("content", Json.parseToJsonElement(plainContent)) + } else { + put(k, v) + } + } + } + + return resultJson.toString() + } + + fun ptDate(prefix: String?): String { + val date = Date() + val sdf = SimpleDateFormat("YYYYMMddHHmmss") + val str = prefix + sdf.format(date) + (Math.random() * 90 + 10).toInt() + println(str) + return str + } + +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/net/RSAUtil.kt b/server/src/main/kotlin/com/bbit/ticket/utils/net/RSAUtil.kt new file mode 100644 index 0000000..e6e7334 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/utils/net/RSAUtil.kt @@ -0,0 +1,120 @@ +package com.bbit.ticket.utils.net + +import java.security.* +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import java.util.* +import javax.crypto.Cipher + +object RSAUtil { + + private const val SIGN_ALGORITHMS = "SHA1WithRSA" + private val DEFAULT_CHARSET = Charsets.UTF_8 + + // ========================= + // 签名 + // ========================= + fun sign(content: String, privateKey: String): String? { + return try { + val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)) + val keyFactory = KeyFactory.getInstance("RSA") + val priKey = keyFactory.generatePrivate(keySpec) + + val signature = Signature.getInstance(SIGN_ALGORITHMS) + signature.initSign(priKey) + signature.update(content.toByteArray(DEFAULT_CHARSET)) + + Base64.getEncoder().encodeToString(signature.sign()) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + // ========================= + // 验签 + // ========================= + fun verify(content: String, sign: String, publicKey: String): Boolean { + return try { + val keyFactory = KeyFactory.getInstance("RSA") + val pubKey = keyFactory.generatePublic( + X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)) + ) + + val signature = Signature.getInstance(SIGN_ALGORITHMS) + signature.initVerify(pubKey) + signature.update(content.toByteArray(DEFAULT_CHARSET)) + + signature.verify(Base64.getDecoder().decode(sign)) + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + // ========================= + // RSA 加密 + // ========================= + fun encrypt(content: String, publicKeyStr: String): String { + val cipher = Cipher.getInstance("RSA") + cipher.init(Cipher.ENCRYPT_MODE, getPublicKey(publicKeyStr)) + + val encrypted = cipher.doFinal(content.toByteArray(DEFAULT_CHARSET)) + return Base64.getEncoder().encodeToString(encrypted) + } + + // ========================= + // RSA 解密 + // ========================= + fun decrypt(content: String, privateKeyStr: String): String { + val cipher = Cipher.getInstance("RSA") + cipher.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKeyStr)) + + val decrypted = cipher.doFinal(Base64.getDecoder().decode(content)) + return String(decrypted, DEFAULT_CHARSET) + } + + // ========================= + // Key 转换 + // ========================= + fun getPublicKey(key: String): PublicKey { + val keyBytes = Base64.getDecoder().decode(key) + val keySpec = X509EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("RSA") + return keyFactory.generatePublic(keySpec) + } + + fun getPrivateKey(key: String): PrivateKey { + val keyBytes = Base64.getDecoder().decode(key) + val keySpec = PKCS8EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("RSA") + return keyFactory.generatePrivate(keySpec) + } + + fun getKeyString(key: Key): String { + return Base64.getEncoder().encodeToString(key.encoded) + } + + // ========================= + // 参数排序拼接(签名字符串) + // ========================= + fun getSignatureContent(params: Map): String { + return params + .filterValues { it != null } + .toSortedMap() + .map { (k, v) -> "$k=$v" } + .joinToString("&") + } + + // ========================= + // List 签名内容 + // ========================= + fun getListSignatureContent(mapList: List>?): String? { + if (mapList == null) return null + + return mapList + .mapNotNull { getSignatureContent(it) } + .sorted() + .toString() + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/net/SecurityUtil.kt b/server/src/main/kotlin/com/bbit/ticket/utils/net/SecurityUtil.kt new file mode 100644 index 0000000..3c44d64 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/utils/net/SecurityUtil.kt @@ -0,0 +1,65 @@ +package com.bbit.ticket.utils.net + +import java.nio.charset.Charset +import java.util.* +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +object SecurityUtil { + private const val ALGORITHM_3DES = "DESede" + val DEFAULT_CHARSET: Charset = Charset.forName("UTF-8") + + fun encrypt3DES(encryptPassword: String, encryptByte: ByteArray): ByteArray? { + try { + val cipher = init3DES(encryptPassword, 1) + val doFinal = cipher.doFinal(encryptByte) + return doFinal + } catch (var4: Exception) { + return null + } + } + + fun encrypt3DES(encryptPassword: String, encryptStr: String): String? { + try { + val cipher = init3DES(encryptPassword, 1) + val enBytes = cipher.doFinal(encryptStr.toByteArray(DEFAULT_CHARSET)) + return Base64.getEncoder().encodeToString(enBytes) + } catch (var4: Exception) { + return null + } + } + + fun decrypt3DES(decryptPassword: String, decryptByte: ByteArray): ByteArray? { + try { + val cipher = init3DES(decryptPassword, 2) + val doFinal = cipher.doFinal(decryptByte) + return doFinal + } catch (var4: Exception) { + return null + } + } + + fun decrypt3DES(decryptPassword: String, decryptString: String?): String? { + try { + val cipher = init3DES(decryptPassword, 2) + val deBytes = cipher.doFinal(Base64.getDecoder().decode(decryptString)) + return String(deBytes, DEFAULT_CHARSET) + } catch (var4: Exception) { + return null + } + } + + @Throws(Exception::class) + private fun init3DES(decryptPassword: String, cipherMode: Int): Cipher { + val deskey: SecretKey = SecretKeySpec(decryptPassword.toByteArray(), "DESede") + val cipher = Cipher.getInstance("DESede") + cipher.init(cipherMode, deskey) + return cipher + } +}