跑通PT
This commit is contained in:
@@ -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. 解密 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user