This commit is contained in:
BBIT-Kai
2026-05-07 16:47:17 +08:00
parent 9fa7f89666
commit 3bbd8941a9
22 changed files with 999 additions and 59 deletions
@@ -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<String>) {
@@ -52,6 +55,7 @@ fun Application.module() {
get("/health") {
call.respond(ok(mapOf("status" to "UP", "service" to AppConfig.app.name)))
}
route("/api"){
registerAuthRoutes()
registerUserRoutes()
registerOrgRoutes()
@@ -59,5 +63,8 @@ fun Application.module() {
registerMenuRoutes()
registerDictRoutes()
registerLogsQueryRoutes()
registerPTTestRoutes()
}
}
}
@@ -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 = ""
}
}
}
@@ -114,10 +114,11 @@ 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)
@@ -126,10 +127,12 @@ object SeedData {
it[SysUserTable.orgId] = orgId
it[status] = "ENABLED"
it[tokenVersion] = 1
it[createdAt] = now
it[taxpayerNum] = "500102201007206608"
it[account] = "DEMOadmin"
}
inserted[SysUserTable.id]
}
}
private suspend fun upsertUserRole(userId: Uuid, roleId: Uuid) = dbQuery {
val exists = SysUserRoleTable.selectAll()
@@ -145,32 +148,357 @@ object SeedData {
private suspend fun upsertMenus(now: OffsetDateTime): List<Uuid> {
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<String, Uuid>()
@@ -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<Boolean> = SysUserTable.deletedAt.isNull()
private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op<Boolean> {
var where: Op<Boolean> = SysUserTable.deletedAt.isNull()
var where: Op<Boolean> = activeWhere()
if (!username.isNullOrBlank()) {
where = where and (SysUserTable.username like "%$username%")
}
@@ -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)
}
@@ -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)
@@ -0,0 +1,9 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class TaxBureauAuthReq(
val taxpayerNum: String,
val account: String
)
@@ -0,0 +1,20 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
@Serializable
data class PTResponse<T>(
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
)
@@ -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
)
@@ -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
)
)
}
}
}
}
}
@@ -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<LoginRequest>()
@@ -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)
@@ -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)
@@ -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()))
@@ -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()))
@@ -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)
@@ -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)
@@ -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<TaxBureauAuthReq, TaxBureauAccountAuthContent>(
url = "getTaxBureauAccountAuthStatus.pt",
body = req
)
println("res = $res")
return res.taxpayerNum
}
}
@@ -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<String>,
val permissions: Set<String>,
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)
@@ -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 <reified Req, reified Resp> ptGet(
url: String,
queryParams: Req,
headers: Map<String, String> = 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<PTResponse<JsonElement>>(decrypted)
if (result.code != "0000") {
throw PTException(
code = result.code,
message = result.msg,
serialNo = result.serialNo
)
}
return Json.decodeFromJsonElement(result.content!!)
}
suspend inline fun <reified Req, reified Resp> ptPost(
url: String,
body: Req,
headers: Map<String, String> = 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<PTResponse<JsonElement>>(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<String, String>()
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. 解密 content3DES
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
}
}
@@ -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, Any?>): String {
return params
.filterValues { it != null }
.toSortedMap()
.map { (k, v) -> "$k=$v" }
.joinToString("&")
}
// =========================
// List<Map> 签名内容
// =========================
fun getListSignatureContent(mapList: List<Map<String, Any?>>?): String? {
if (mapList == null) return null
return mapList
.mapNotNull { getSignatureContent(it) }
.sorted()
.toString()
}
}
@@ -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
}
}