完成基础信息与开票的逻辑

This commit is contained in:
BBIT-Kai
2026-05-11 09:31:23 +08:00
parent 3bbd8941a9
commit 4b23f3546a
37 changed files with 4975 additions and 538 deletions
+11
View File
@@ -0,0 +1,11 @@
---
description:
alwaysApply: true
enabled: true
updatedAt: 2026-05-08T02:44:45.758Z
provider:
---
1. 编码时要参考项目类似结构的代码,比如写接口要参考其他接口,写service要参考其他service,写dao要参考其他dao。
2. 要保持代码干净、已读。
3. 尽量不要出现重复代码,相同的逻辑需要提取出工具类到新文件中。
@@ -0,0 +1,8 @@
-- 扩展现有字段长度,适配票通接口校验规则和 base64 图片存储
ALTER TABLE sys_user
ALTER COLUMN tax_contact_name TYPE varchar(50),
ALTER COLUMN tax_contact_email TYPE varchar(100),
ALTER COLUMN tax_legal_person_name TYPE varchar(50),
ALTER COLUMN tax_city_name TYPE varchar(50),
ALTER COLUMN tax_enterprise_address TYPE varchar(200),
ALTER COLUMN tax_registration_certificate TYPE text;
@@ -1,5 +1,7 @@
package com.bbit.ticket.bootstrap package com.bbit.ticket.bootstrap
import com.bbit.ticket.database.piaotong.InvoiceItemTable
import com.bbit.ticket.database.piaotong.InvoiceOrderTable
import com.bbit.ticket.database.system.SysApiAccessLogTable import com.bbit.ticket.database.system.SysApiAccessLogTable
import com.bbit.ticket.database.system.SysDictItemTable import com.bbit.ticket.database.system.SysDictItemTable
import com.bbit.ticket.database.system.SysDictTypeTable import com.bbit.ticket.database.system.SysDictTypeTable
@@ -30,6 +32,8 @@ object DatabaseInitializer {
SysDictItemTable, SysDictItemTable,
SysOperationLogTable, SysOperationLogTable,
SysApiAccessLogTable, SysApiAccessLogTable,
InvoiceItemTable,
InvoiceOrderTable,
) )
// 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。 // 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。
transaction { transaction {
@@ -40,7 +44,7 @@ object DatabaseInitializer {
if (statements.isNotEmpty()) { if (statements.isNotEmpty()) {
logger.info("Migrating database schema, statement count={}", statements.size) logger.info("Migrating database schema, statement count={}", statements.size)
statements.forEach { statements.forEach {
logger.debug("Executing migration SQL: {};", it) logger.info("Executing migration SQL: {};", it)
exec(it) exec(it)
} }
} }
@@ -2,28 +2,24 @@ package com.bbit.ticket.bootstrap
object Global { object Global {
// 测试账号 销售方税号
const val testTaxpayerNum = "500102201007206608"
const val testAccount = "DEMOadmin"
val isDev = true val isDev = true
// 请求基础地址 // 请求基础地址
var baseUrl: String var baseUrl: String
// 票通私钥 // 票通私钥 私钥(与发给票通的公钥为一对)
var ptPrivateKey: String var ptPrivateKey: String
// 票通公钥 // 票通公钥 (票通提供)
var ptPublicKey: String var ptPublicKey: String
// 票通密码 // 票通密码 (票通提供)
var ptPassword: String var ptPassword: String
// 票通平台简称 // 票通平台简称 (票通提供)
var ptPlatformAlias: String var ptPlatformAlias: String
// 票通编码 // 票通编码 (票通提供)
var ptPlatformCode: String var ptPlatformCode: String
@@ -2,16 +2,10 @@
package com.bbit.ticket.bootstrap package com.bbit.ticket.bootstrap
import com.bbit.ticket.database.system.SysDictItemTable import com.bbit.ticket.database.system.*
import com.bbit.ticket.database.system.SysDictTypeTable
import com.bbit.ticket.database.system.SysMenuTable
import com.bbit.ticket.database.system.SysOrgTable
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.plugins.dbQuery import com.bbit.ticket.plugins.dbQuery
import com.bbit.ticket.service.system.PasswordService import com.bbit.ticket.service.system.PasswordService
import com.bbit.ticket.utils.net.SecurityUtil
import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList import org.jetbrains.exposed.v1.core.inList
@@ -34,6 +28,10 @@ object SeedData {
private const val DEFAULT_ORG_CODE = "DEFAULT_ORG" private const val DEFAULT_ORG_CODE = "DEFAULT_ORG"
private const val SUPER_ADMIN_ROLE_CODE = "SUPER_ADMIN" private const val SUPER_ADMIN_ROLE_CODE = "SUPER_ADMIN"
// =========================================================
// Main entry
// =========================================================
suspend fun seed() { suspend fun seed() {
val now = OffsetDateTime.now() val now = OffsetDateTime.now()
val orgId = upsertDefaultOrg(now) val orgId = upsertDefaultOrg(now)
@@ -46,6 +44,10 @@ object SeedData {
logger.info("Seed data initialized, default admin username: {}", ADMIN_USERNAME) logger.info("Seed data initialized, default admin username: {}", ADMIN_USERNAME)
} }
// =========================================================
// Organization & Role
// =========================================================
private suspend fun upsertDefaultOrg(now: OffsetDateTime): Uuid = dbQuery { private suspend fun upsertDefaultOrg(now: OffsetDateTime): Uuid = dbQuery {
val existing = SysOrgTable.selectAll() val existing = SysOrgTable.selectAll()
.where { (SysOrgTable.code eq DEFAULT_ORG_CODE) and SysOrgTable.deletedAt.isNull() } .where { (SysOrgTable.code eq DEFAULT_ORG_CODE) and SysOrgTable.deletedAt.isNull() }
@@ -54,21 +56,21 @@ object SeedData {
if (existing != null) { if (existing != null) {
val id = existing[SysOrgTable.id] val id = existing[SysOrgTable.id]
SysOrgTable.update({ SysOrgTable.id eq id }) { SysOrgTable.update({ SysOrgTable.id eq id }) {
it[name] = "默认组织" it[SysOrgTable.name] = "默认组织"
it[sort] = 0 it[SysOrgTable.sort] = 0
it[status] = "ENABLED" it[SysOrgTable.status] = "ENABLED"
it[updatedAt] = now it[SysOrgTable.updatedAt] = now
} }
return@dbQuery id return@dbQuery id
} }
val inserted = SysOrgTable.insert { val inserted = SysOrgTable.insert {
it[parentId] = null it[SysOrgTable.parentId] = null
it[name] = "默认组织" it[SysOrgTable.name] = "默认组织"
it[code] = DEFAULT_ORG_CODE it[SysOrgTable.code] = DEFAULT_ORG_CODE
it[sort] = 0 it[SysOrgTable.sort] = 0
it[status] = "ENABLED" it[SysOrgTable.status] = "ENABLED"
it[createdAt] = now it[SysOrgTable.createdAt] = now
} }
inserted[SysOrgTable.id] inserted[SysOrgTable.id]
} }
@@ -81,26 +83,30 @@ object SeedData {
if (existing != null) { if (existing != null) {
val id = existing[SysRoleTable.id] val id = existing[SysRoleTable.id]
SysRoleTable.update({ SysRoleTable.id eq id }) { SysRoleTable.update({ SysRoleTable.id eq id }) {
it[name] = "超级管理员" it[SysRoleTable.name] = "超级管理员"
it[description] = "系统内置超级管理员角色" it[SysRoleTable.description] = "系统内置超级管理员角色"
it[status] = "ENABLED" it[SysRoleTable.status] = "ENABLED"
it[dataScope] = "ALL" it[SysRoleTable.dataScope] = "ALL"
it[updatedAt] = now it[SysRoleTable.updatedAt] = now
} }
return@dbQuery id return@dbQuery id
} }
val inserted = SysRoleTable.insert { val inserted = SysRoleTable.insert {
it[name] = "超级管理员" it[SysRoleTable.name] = "超级管理员"
it[code] = SUPER_ADMIN_ROLE_CODE it[SysRoleTable.code] = SUPER_ADMIN_ROLE_CODE
it[description] = "系统内置超级管理员角色" it[SysRoleTable.description] = "系统内置超级管理员角色"
it[status] = "ENABLED" it[SysRoleTable.status] = "ENABLED"
it[dataScope] = "ALL" it[SysRoleTable.dataScope] = "ALL"
it[createdAt] = now it[SysRoleTable.createdAt] = now
} }
inserted[SysRoleTable.id] inserted[SysRoleTable.id]
} }
// =========================================================
// Admin user
// =========================================================
private suspend fun upsertAdminUser(orgId: Uuid, now: OffsetDateTime): Uuid = dbQuery { private suspend fun upsertAdminUser(orgId: Uuid, now: OffsetDateTime): Uuid = dbQuery {
val existing = SysUserTable.selectAll() val existing = SysUserTable.selectAll()
.where { (SysUserTable.username eq ADMIN_USERNAME) and SysUserTable.deletedAt.isNull() } .where { (SysUserTable.username eq ADMIN_USERNAME) and SysUserTable.deletedAt.isNull() }
@@ -109,29 +115,34 @@ object SeedData {
if (existing != null) { if (existing != null) {
val id = existing[SysUserTable.id] val id = existing[SysUserTable.id]
SysUserTable.update({ SysUserTable.id eq id }) { SysUserTable.update({ SysUserTable.id eq id }) {
it[nickname] = "管理员" it[SysUserTable.nickname] = "系统管理员"
it[realName] = "系统管理员"
it[SysUserTable.orgId] = orgId it[SysUserTable.orgId] = orgId
it[status] = "ENABLED" it[SysUserTable.status] = "ENABLED"
it[updatedAt] = now it[SysUserTable.updatedAt] = now
it[taxpayerNum] = "500102201007206608" it[SysUserTable.taxpayerNum] = "500102201007206608"
it[account] = "DEMOadmin" it[SysUserTable.phone] = "13000000000"
it[SysUserTable.taxIdentityType] = "01"
it[SysUserTable.taxPassword] = SecurityUtil.encrypt3DES(Global.ptPassword, "ispassword")
it[SysUserTable.realName] = "测试"
} }
return@dbQuery id 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[SysUserTable.username] = ADMIN_USERNAME
it[SysUserTable.passwordHash] = PasswordService.hash(ADMIN_INIT_PASSWORD)
it[SysUserTable.nickname] = "系统管理员"
it[SysUserTable.orgId] = orgId
it[SysUserTable.status] = "ENABLED"
it[SysUserTable.tokenVersion] = 1
it[SysUserTable.taxpayerNum] = "500102201007206608"
it[SysUserTable.phone] = "13000000000"
it[SysUserTable.taxAccount] = "DEMOadmin"
it[SysUserTable.taxIdentityType] = "01"
it[SysUserTable.taxPassword] = SecurityUtil.encrypt3DES(Global.ptPassword, "ispassword")
it[SysUserTable.realName] = "测试"
}
inserted[SysUserTable.id]
} }
private suspend fun upsertUserRole(userId: Uuid, roleId: Uuid) = dbQuery { private suspend fun upsertUserRole(userId: Uuid, roleId: Uuid) = dbQuery {
@@ -146,359 +157,42 @@ object SeedData {
} }
} }
// =========================================================
// Menus & permissions
// =========================================================
private suspend fun upsertMenus(now: OffsetDateTime): List<Uuid> { private suspend fun upsertMenus(now: OffsetDateTime): List<Uuid> {
val seedMenus = listOf( val seedMenus = listOf(
SeedMenu( rootMenu("dashboard", "工作台", "Dashboard", "/dashboard", "dashboard/index", "LayoutDashboard", 10),
"dashboard", catalog("system", "系统管理", "SystemRoot", "Settings", 20),
null, subMenu("system_user", "system", "用户管理", "SystemUsers", "/system/users", "system/users/index", "Users", "system:user:view", 10),
"MENU", button("system_user_create", "system_user", "新增用户", "SystemUserCreate", "system:user:create", 1),
"工作台", button("system_user_update", "system_user", "修改用户", "SystemUserUpdate", "system:user:update", 2),
"Dashboard", button("system_user_delete", "system_user", "删除用户", "SystemUserDelete", "system:user:delete", 3),
"/dashboard", subMenu("system_org", "system", "组织管理", "SystemOrgs", "/system/orgs", "system/orgs/index", "Building2", "system:org:view", 20),
"dashboard/index", button("system_org_create", "system_org", "新增组织", "SystemOrgCreate", "system:org:create", 1),
"LayoutDashboard", button("system_org_update", "system_org", "更新组织", "SystemOrgUpdate", "system:org:update", 2),
null, button("system_org_delete", "system_org", "删除组织", "SystemOrgDelete", "system:org:delete", 3),
10, subMenu("system_role", "system", "角色管理", "SystemRoles", "/system/roles", "system/roles/index", "Shield", "system:role:view", 30),
true, button("system_role_create", "system_role", "新增角色", "SystemRoleCreate", "system:role:create", 1),
true button("system_role_update", "system_role", "更新角色", "SystemRoleUpdate", "system:role:update", 2),
), button("system_role_delete", "system_role", "删除角色", "SystemRoleDelete", "system:role:delete", 3),
SeedMenu( button("system_role_assign", "system_role", "分配角色权限", "SystemRoleAssign", "system:role:assign", 4),
"system", subMenu("system_menu", "system", "菜单管理", "SystemMenus", "/system/menus", "system/menus/index", "PanelLeft", "system:menu:view", 40),
null, button("system_menu_create", "system_menu", "新增菜单", "SystemMenuCreate", "system:menu:create", 1),
"CATALOG", button("system_menu_update", "system_menu", "更新菜单", "SystemMenuUpdate", "system:menu:update", 2),
"系统管理", button("system_menu_delete", "system_menu", "删除菜单", "SystemMenuDelete", "system:menu:delete", 3),
"SystemRoot", subMenu("system_dict", "system", "字典管理", "SystemDict", "/system/dicts", "system/dicts/index", "BookType", "system:dict:view", 50),
"/system", button("system_dict_create", "system_dict", "新增字典", "SystemDictCreate", "system:dict:create", 1),
null, button("system_dict_update", "system_dict", "更新字典", "SystemDictUpdate", "system:dict:update", 2),
"Settings", button("system_dict_delete", "system_dict", "删除字典", "SystemDictDelete", "system:dict:delete", 3),
null, catalog("logs", "日志管理", "LogsRoot", "Logs", 30),
20, subMenu("logs_operation", "logs", "操作日志", "LogsOperation", "/logs/operation", "logs/operation/index", "ScrollText", "log:operation:view", 10),
true, subMenu("logs_api_access", "logs", "接口日志", "LogsApiAccess", "/logs/api-access", "logs/api-access/index", "Waypoints", "log:api-access:view", 20),
false catalog("piaotong", "票通服务", "PiaoTongRoot", "Receipt", 40),
), subMenu("piaotong_info", "piaotong", "基础信息", "PiaoTongInfo", "/piaotong/info", "piaotong/index", "User", "piaotong:info:view", 10),
SeedMenu( subMenu("piaotong_invoice_issue", "piaotong", "开票服务", "PiaoTongInvoiceIssue", "/piaotong/invoice-issue", "piaotong/invoice-issue/index", "FilePlus", "piaotong:invoice-issue:view", 20),
"system_user", subMenu("piaotong_invoice_history", "piaotong", "开票历史", "PiaoTongInvoiceHistory", "/piaotong/invoice-history", "piaotong/invoice-history/index", "History", "piaotong:invoice-history:view", 30),
"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
),
) )
val idMap = mutableMapOf<String, Uuid>() val idMap = mutableMapOf<String, Uuid>()
@@ -507,7 +201,6 @@ object SeedData {
val menuId = upsertMenu(menu, parentId, now) val menuId = upsertMenu(menu, parentId, now)
idMap[menu.key] = menuId idMap[menu.key] = menuId
} }
return idMap.values.toList() return idMap.values.toList()
} }
@@ -558,16 +251,15 @@ object SeedData {
} }
private suspend fun bindRoleMenus(roleId: Uuid, menuIds: List<Uuid>) = dbQuery { private suspend fun bindRoleMenus(roleId: Uuid, menuIds: List<Uuid>) = dbQuery {
if (menuIds.isEmpty()) { if (menuIds.isEmpty()) return@dbQuery
return@dbQuery
}
val existing = SysRoleMenuTable.selectAll() val existing = SysRoleMenuTable.selectAll()
.where { SysRoleMenuTable.roleId eq roleId } .where { SysRoleMenuTable.roleId eq roleId }
.map { it[SysRoleMenuTable.menuId] } .map { it[SysRoleMenuTable.menuId] }
.toSet() .toSet()
val toAdd = menuIds.filter { !existing.contains(it) } val menuIdSet = menuIds.toSet()
val toAdd = menuIds.filter { it !in existing }
toAdd.forEach { menuId -> toAdd.forEach { menuId ->
SysRoleMenuTable.insert { SysRoleMenuTable.insert {
it[SysRoleMenuTable.roleId] = roleId it[SysRoleMenuTable.roleId] = roleId
@@ -575,33 +267,45 @@ object SeedData {
} }
} }
val toRemove = existing.filter { !menuIds.contains(it) } val toRemove = existing - menuIdSet
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
SysRoleMenuTable.deleteWhere { (SysRoleMenuTable.roleId eq roleId) and (SysRoleMenuTable.menuId inList toRemove) } SysRoleMenuTable.deleteWhere {
(SysRoleMenuTable.roleId eq roleId) and (SysRoleMenuTable.menuId inList toRemove.toList())
}
} }
} }
// =========================================================
// Dicts
// =========================================================
private data class SeedDict(val code: String, val name: String, val items: List<SeedDictItem>)
private data class SeedDictItem(val label: String, val value: String, val color: String?, val sort: Int)
private suspend fun seedDicts(now: OffsetDateTime) { private suspend fun seedDicts(now: OffsetDateTime) {
val userStatusTypeId = upsertDictType("user_status", "用户状态", now) val statusItems = listOf(
upsertDictItem(userStatusTypeId, "启用", "ENABLED", "green", 1, now) SeedDictItem("启用", "ENABLED", "green", 1),
upsertDictItem(userStatusTypeId, "禁用", "DISABLED", "red", 2, now) SeedDictItem("禁用", "DISABLED", "red", 2),
)
val orgStatusTypeId = upsertDictType("org_status", "组织状态", now) val dicts = listOf(
upsertDictItem(orgStatusTypeId, "启用", "ENABLED", "green", 1, now) SeedDict("user_status", "用户状态", statusItems),
upsertDictItem(orgStatusTypeId, "禁用", "DISABLED", "red", 2, now) SeedDict("org_status", "组织状态", statusItems),
SeedDict("role_status", "角色状态", statusItems),
SeedDict("menu_type", "菜单类型", listOf(
SeedDictItem("目录", "CATALOG", "default", 1),
SeedDictItem("菜单", "MENU", "blue", 2),
SeedDictItem("按钮", "BUTTON", "orange", 3),
)),
SeedDict("log_status", "日志状态", statusItems),
)
val roleStatusTypeId = upsertDictType("role_status", "角色状态", now) for (dict in dicts) {
upsertDictItem(roleStatusTypeId, "启用", "ENABLED", "green", 1, now) val typeId = upsertDictType(dict.code, dict.name, now)
upsertDictItem(roleStatusTypeId, "禁用", "DISABLED", "red", 2, now) for (item in dict.items) {
upsertDictItem(typeId, item.label, item.value, item.color, item.sort, now)
val menuTypeId = upsertDictType("menu_type", "菜单类型", now) }
upsertDictItem(menuTypeId, "目录", "CATALOG", "default", 1, now) }
upsertDictItem(menuTypeId, "菜单", "MENU", "blue", 2, now)
upsertDictItem(menuTypeId, "按钮", "BUTTON", "orange", 3, now)
val logStatusTypeId = upsertDictType("log_status", "日志状态", now)
upsertDictItem(logStatusTypeId, "成功", "SUCCESS", "green", 1, now)
upsertDictItem(logStatusTypeId, "失败", "FAIL", "red", 2, now)
} }
private suspend fun upsertDictType(code: String, name: String, now: OffsetDateTime): Uuid = dbQuery { private suspend fun upsertDictType(code: String, name: String, now: OffsetDateTime): Uuid = dbQuery {
@@ -613,8 +317,8 @@ object SeedData {
val id = existing[SysDictTypeTable.id] val id = existing[SysDictTypeTable.id]
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
it[SysDictTypeTable.name] = name it[SysDictTypeTable.name] = name
it[status] = "ENABLED" it[SysDictTypeTable.status] = "ENABLED"
it[updatedAt] = now it[SysDictTypeTable.updatedAt] = now
} }
return@dbQuery id return@dbQuery id
} }
@@ -622,19 +326,14 @@ object SeedData {
val inserted = SysDictTypeTable.insert { val inserted = SysDictTypeTable.insert {
it[SysDictTypeTable.code] = code it[SysDictTypeTable.code] = code
it[SysDictTypeTable.name] = name it[SysDictTypeTable.name] = name
it[status] = "ENABLED" it[SysDictTypeTable.status] = "ENABLED"
it[createdAt] = now it[SysDictTypeTable.createdAt] = now
} }
inserted[SysDictTypeTable.id] inserted[SysDictTypeTable.id]
} }
private suspend fun upsertDictItem( private suspend fun upsertDictItem(
typeId: Uuid, typeId: Uuid, label: String, value: String, color: String?, sort: Int, now: OffsetDateTime,
label: String,
value: String,
color: String?,
sort: Int,
now: OffsetDateTime,
) = dbQuery { ) = dbQuery {
val existing = SysDictItemTable.selectAll() val existing = SysDictItemTable.selectAll()
.where { .where {
@@ -645,13 +344,12 @@ object SeedData {
.singleOrNull() .singleOrNull()
if (existing != null) { if (existing != null) {
val id = existing[SysDictItemTable.id] SysDictItemTable.update({ SysDictItemTable.id eq existing[SysDictItemTable.id] }) {
SysDictItemTable.update({ SysDictItemTable.id eq id }) {
it[SysDictItemTable.label] = label it[SysDictItemTable.label] = label
it[SysDictItemTable.color] = color it[SysDictItemTable.color] = color
it[SysDictItemTable.sort] = sort it[SysDictItemTable.sort] = sort
it[status] = "ENABLED" it[SysDictItemTable.status] = "ENABLED"
it[updatedAt] = now it[SysDictItemTable.updatedAt] = now
} }
return@dbQuery return@dbQuery
} }
@@ -662,18 +360,22 @@ object SeedData {
it[SysDictItemTable.value] = value it[SysDictItemTable.value] = value
it[SysDictItemTable.color] = color it[SysDictItemTable.color] = color
it[SysDictItemTable.sort] = sort it[SysDictItemTable.sort] = sort
it[status] = "ENABLED" it[SysDictItemTable.status] = "ENABLED"
it[createdAt] = now it[SysDictItemTable.createdAt] = now
} }
} }
} }
// =========================================================
// Menu definition helpers
// =========================================================
private data class SeedMenu( private data class SeedMenu(
val key: String, val key: String,
val parentKey: String?, val parentKey: String?,
val type: String, val type: String,
val title: String, val title: String,
val name: String, val name: String?,
val path: String?, val path: String?,
val component: String?, val component: String?,
val icon: String?, val icon: String?,
@@ -683,3 +385,15 @@ private data class SeedMenu(
val keepAlive: Boolean, val keepAlive: Boolean,
val builtIn: Boolean = true, val builtIn: Boolean = true,
) )
private fun rootMenu(key: String, title: String, name: String, path: String, component: String, icon: String, sort: Int): SeedMenu =
SeedMenu(key, null, "MENU", title, name, path, component, icon, null, sort, visible = true, keepAlive = true)
private fun catalog(key: String, title: String, name: String, icon: String, sort: Int): SeedMenu =
SeedMenu(key, null, "CATALOG", title, name, "/$key", null, icon, null, sort, visible = true, keepAlive = false)
private fun subMenu(key: String, parentKey: String, title: String, name: String, path: String, component: String, icon: String, permission: String, sort: Int): SeedMenu =
SeedMenu(key, parentKey, "MENU", title, name, path, component, icon, permission, sort, visible = true, keepAlive = true)
private fun button(key: String, parentKey: String, title: String, name: String, permission: String, sort: Int): SeedMenu =
SeedMenu(key, parentKey, "BUTTON", title, name, null, null, null, permission, sort, visible = true, keepAlive = false)
@@ -0,0 +1,181 @@
@file:OptIn(ExperimentalUuidApi::class)
package com.bbit.ticket.dao.piaotong
import com.bbit.ticket.database.piaotong.InvoiceItemTable
import com.bbit.ticket.database.piaotong.InvoiceOrderTable
import com.bbit.ticket.database.system.SysUserTable
import com.bbit.ticket.entity.request.InvoiceRequest
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.UpdateDigitalAccountRequest
import com.bbit.ticket.entity.request.UpdateEnterpriseInfoRequest
import com.bbit.ticket.entity.request.UpdatePresetDataRequest
import com.bbit.ticket.entity.response.DigitalAccountResponse
import com.bbit.ticket.entity.response.EnterpriseInfoResponse
import com.bbit.ticket.entity.response.PresetDataResponse
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.insertAndGetId
import org.jetbrains.exposed.v1.jdbc.insertReturning
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import java.math.BigDecimal
import java.text.DecimalFormat
import java.time.OffsetDateTime
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
object EnterpriseTaxDao {
// =============================================
// 企业信息
// =============================================
fun getEnterpriseInfo(userId: Uuid): EnterpriseInfoResponse? {
val row = SysUserTable.selectAll().where { SysUserTable.id eq userId }.singleOrNull() ?: return null
return EnterpriseInfoResponse(
taxpayerNum = row[SysUserTable.taxpayerNum],
enterpriseName = row[SysUserTable.taxEnterpriseName],
legalPersonName = row[SysUserTable.taxLegalPersonName],
contactsName = row[SysUserTable.taxContactName],
contactsEmail = row[SysUserTable.taxContactEmail],
contactsPhone = row[SysUserTable.taxContactPhone],
regionCode = row[SysUserTable.taxRegionCode],
cityName = row[SysUserTable.taxCityName],
enterpriseAddress = row[SysUserTable.taxEnterpriseAddress],
taxRegistrationCertificate = row[SysUserTable.taxRegistrationCertificate]
)
}
fun updateEnterpriseInfoLocal(userId: Uuid, req: UpdateEnterpriseInfoRequest) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = req.taxpayerNum.trim().ifBlank { null }
it[SysUserTable.taxEnterpriseName] = req.enterpriseName.trim().ifBlank { null }
it[SysUserTable.taxLegalPersonName] = req.legalPersonName.trim().ifBlank { null }
it[SysUserTable.taxContactName] = req.contactsName.trim().ifBlank { null }
it[SysUserTable.taxContactEmail] = req.contactsEmail.trim().ifBlank { null }
it[SysUserTable.taxContactPhone] = req.contactsPhone.trim().ifBlank { null }
it[SysUserTable.taxRegionCode] = req.regionCode.trim().ifBlank { null }
it[SysUserTable.taxCityName] = req.cityName.trim().ifBlank { null }
it[SysUserTable.taxEnterpriseAddress] = req.enterpriseAddress.trim().ifBlank { null }
it[SysUserTable.taxRegistrationCertificate] = req.taxRegistrationCertificate.ifBlank { null }
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
// =============================================
// 登记数电账号
// =============================================
fun getDigitalAccount(userId: Uuid): DigitalAccountResponse? {
val row = SysUserTable.selectAll().where { SysUserTable.id eq userId }.singleOrNull() ?: return null
return DigitalAccountResponse(
taxpayerNum = row[SysUserTable.taxpayerNum],
taxAccount = row[SysUserTable.taxAccount]
)
}
fun updateDigitalAccountLocal(userId: Uuid, req: UpdateDigitalAccountRequest) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = req.taxpayerNum.trim().ifBlank { null }
it[SysUserTable.taxAccount] = req.taxAccount.trim().ifBlank { null }
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
// =============================================
// 开票预设数据
// =============================================
fun getPresetData(userId: Uuid): PresetDataResponse? {
val row = SysUserTable.selectAll().where { SysUserTable.id eq userId }.singleOrNull() ?: return null
return PresetDataResponse(
bankName = row[SysUserTable.bankName],
bankAccount = row[SysUserTable.bankAccount]
)
}
fun updatePresetData(userId: Uuid, req: UpdatePresetDataRequest) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.bankName] = req.bankName.trim().ifBlank { null }
it[SysUserTable.bankAccount] = req.bankAccount.trim().ifBlank { null }
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
fun updateEnterpriseInfo(userId: Uuid, req: TaxRegisterInfo) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = req.taxpayerNum
it[SysUserTable.taxEnterpriseName] = req.enterpriseName
it[SysUserTable.taxContactName] = req.contactsName
it[SysUserTable.taxContactPhone] = req.contactsPhone
it[SysUserTable.taxContactEmail] = req.contactsEmail
it[SysUserTable.taxLegalPersonName] = req.legalPersonName
it[SysUserTable.taxRegionCode] = req.regionCode
it[SysUserTable.taxCityName] = req.cityName
it[SysUserTable.taxEnterpriseAddress] = req.enterpriseAddress
it[SysUserTable.taxRegistrationCertificate] = req.taxRegistrationCertificate
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
fun updateUserAccount(userId: Uuid, taxpayerNum: String, taxAccount: String) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = taxpayerNum
it[SysUserTable.taxAccount] = taxAccount
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
fun addInvoice(userId: Uuid, req: InvoiceRequest) {
val now = OffsetDateTime.now()
val row = InvoiceOrderTable.insert {
it[InvoiceOrderTable.userId] = userId
it[InvoiceOrderTable.invoiceReqSerialNo] = req.invoiceReqSerialNo
it[InvoiceOrderTable.taxpayerNum] = req.taxpayerNum
it[InvoiceOrderTable.invoiceKindCode] = req.invoiceIssueKindCode
it[InvoiceOrderTable.buyerName] = req.buyerName
it[InvoiceOrderTable.buyerTaxpayerNum] = req.buyerTaxpayerNum
it[InvoiceOrderTable.buyerAddress] = req.buyerAddress
it[InvoiceOrderTable.buyerTel] = req.buyerTel
it[InvoiceOrderTable.buyerBankName] = req.buyerBankName
it[InvoiceOrderTable.buyerBankAccount] = req.buyerBankAccount
it[InvoiceOrderTable.remark] = req.remark
it[InvoiceOrderTable.definedData] = req.definedData
it[InvoiceOrderTable.tradeNo] = req.tradeNo
it[InvoiceOrderTable.taxAmount] = BigDecimal.ZERO
it[InvoiceOrderTable.amount] = BigDecimal.ZERO
it[InvoiceOrderTable.totalAmount] = BigDecimal.ZERO
it[InvoiceOrderTable.requestJson] = Json.encodeToString(req)
it[InvoiceOrderTable.status] = "PENDING"
it[InvoiceOrderTable.createdAt] = now
it[InvoiceOrderTable.createdBy] = userId
}
val invoiceId = row[InvoiceOrderTable.id]
var lineNo = 1
for (item in req.itemList) {
InvoiceItemTable.insert {
it[InvoiceItemTable.invoiceId] = invoiceId
it[InvoiceItemTable.lineNo] = lineNo++
it[InvoiceItemTable.goodsName] = item.goodsName
it[InvoiceItemTable.taxClassificationCode] = item.taxClassificationCode
it[InvoiceItemTable.specificationModel] = item.specificationModel
it[InvoiceItemTable.meteringUnit] = item.meteringUnit
it[InvoiceItemTable.quantity] = item.quantity?.toBigDecimalOrNull()
it[InvoiceItemTable.unitPrice] = item.unitPrice?.toBigDecimalOrNull()
it[InvoiceItemTable.invoiceAmount] = item.invoiceAmount.toBigDecimal()
it[InvoiceItemTable.taxRateValue] = item.taxRateValue.toBigDecimal()
it[InvoiceItemTable.taxRateAmount] = item.taxRateAmount?.toBigDecimalOrNull()
it[InvoiceItemTable.includeTaxFlag] = item.includeTaxFlag == "1"
it[InvoiceItemTable.discountAmount] = item.discountAmount?.toBigDecimalOrNull()
it[InvoiceItemTable.zeroTaxFlag] = item.zeroTaxFlag
it[InvoiceItemTable.preferentialPolicyFlag] = item.preferentialPolicyFlag
it[InvoiceItemTable.vatSpecialManage] = item.vatSpecialManage
it[InvoiceItemTable.deductionAmount] = item.deductionAmount?.toBigDecimalOrNull()
it[InvoiceItemTable.createdAt] = now
}
}
}
}
@@ -102,6 +102,10 @@ object UserDao {
it[SysUserTable.email] = request.email.trimToNull() it[SysUserTable.email] = request.email.trimToNull()
it[SysUserTable.avatar] = request.avatar.trimToNull() it[SysUserTable.avatar] = request.avatar.trimToNull()
it[SysUserTable.orgId] = orgId it[SysUserTable.orgId] = orgId
it[SysUserTable.taxpayerNum] = request.taxpayerNum?.trimToNull()
it[SysUserTable.taxAccount] = request.account?.trimToNull()
it[SysUserTable.taxPassword] = request.taxPassword?.trimToNull()
it[SysUserTable.taxIdentityType] = request.taxIdentityType?.trimToNull()
it[SysUserTable.updatedAt] = OffsetDateTime.now() it[SysUserTable.updatedAt] = OffsetDateTime.now()
} }
} }
@@ -231,5 +235,9 @@ object UserDao {
status = this[SysUserTable.status], status = this[SysUserTable.status],
statusLabel = statusLabel(this[SysUserTable.status]), statusLabel = statusLabel(this[SysUserTable.status]),
roleIds = roleIds, roleIds = roleIds,
taxpayerNum = this[SysUserTable.taxpayerNum],
account = this[SysUserTable.taxAccount],
taxPassword = this[SysUserTable.taxPassword],
taxIdentityType = this[SysUserTable.taxIdentityType],
) )
} }
@@ -0,0 +1,106 @@
package com.bbit.ticket.database.piaotong
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalUuidApi::class)
object InvoiceItemTable : Table("invoice_item") {
val id = uuid("id").clientDefault { Uuid.Companion.random() }
/**
* 发票ID
*/
val invoiceId = uuid("invoice_id").references(InvoiceOrderTable.id)
/**
* 行号
*/
val lineNo = integer("line_no")
/**
* 商品名称
*/
val goodsName = varchar("goods_name", 200)
/**
* 税收分类编码
*/
val taxClassificationCode = varchar("tax_classification_code", 64)
/**
* 规格型号
*/
val specificationModel =
varchar("specification_model", 100).nullable()
/**
* 单位
*/
val meteringUnit =
varchar("metering_unit", 32).nullable()
/**
* 数量
*/
val quantity = decimal("quantity", 18, 8).nullable()
/**
* 单价
*/
val unitPrice = decimal("unit_price", 18, 8).nullable()
/**
* 金额
*/
val invoiceAmount = decimal("invoice_amount", 18, 2)
/**
* 税率
*/
val taxRateValue = decimal("tax_rate_value", 8, 4)
/**
* 税额
*/
val taxRateAmount = decimal("tax_rate_amount", 18, 2).nullable()
/**
* 是否含税
*/
val includeTaxFlag = bool("include_tax_flag").default(false)
/**
* 折扣金额
*/
val discountAmount = decimal("discount_amount", 18, 2).nullable()
/**
* 零税率标识
*/
val zeroTaxFlag = varchar("zero_tax_flag", 8).nullable()
/**
* 优惠政策标识
*/
val preferentialPolicyFlag = varchar("preferential_policy_flag", 8).nullable()
/**
* 增值税特殊管理
*/
val vatSpecialManage = varchar("vat_special_manage", 100).nullable()
/**
* 差额扣除金额
*/
val deductionAmount = decimal("deduction_amount", 18, 2).nullable()
/**
* 创建时间
*/
val createdAt = timestampWithTimeZone("created_at")
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,181 @@
package com.bbit.ticket.database.piaotong
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalUuidApi::class)
object InvoiceOrderTable : Table("invoice_order") {
val id = uuid("id").clientDefault { Uuid.Companion.random() }
/**
* 租户/组织ID
*/
val userId = uuid("user_id").nullable()
/**
* 发票请求流水号
*/
val invoiceReqSerialNo = varchar("invoice_req_serial_no", 64)
.uniqueIndex()
/**
* 销方税号
*/
val taxpayerNum = varchar("taxpayer_num", 32)
/**
* 发票种类
* 81:电子发票(增值税专用发票)
* 82:电子发票(普通发票)
* 87:数电纸质发票(机动车销售统一发票)
* 10:增值税电子普通发票
* 08:增值税电子专用发票
* 04:增值税普通发票
* 01:增值税专用发票
*
*/
val invoiceKindCode = varchar("invoice_kind_code", 8)
// =========================
// 购买方信息(历史快照)
// =========================
/**
* 购买方名称
*/
val buyerName = varchar("buyer_name", 200)
/**
* 购买方税号
*/
val buyerTaxpayerNum = varchar("buyer_taxpayer_num", 32)
.nullable()
val buyerAddress = varchar("buyer_address", 255).nullable()
val buyerTel = varchar("buyer_tel", 50).nullable()
val buyerBankName = varchar("buyer_bank_name", 100).nullable()
val buyerBankAccount = varchar("buyer_bank_account", 100).nullable()
// =========================
// 金额
// =========================
/**
* 不含税金额
*/
val amount = decimal("amount", 18, 2)
/**
* 税额
*/
val taxAmount = decimal("tax_amount", 18, 2)
/**
* 含税总金额
*/
val totalAmount = decimal("total_amount", 18, 2)
// =========================
// 开票结果
// =========================
/**
* 发票号码
*/
val invoiceNo = varchar("invoice_no", 64).nullable()
/**
* 发票代码
*/
val invoiceCode = varchar("invoice_code", 64).nullable()
/**
* 数电票号码
*/
val electronicInvoiceNo = varchar("electronic_invoice_no", 64).nullable()
/**
* 开票时间
*/
val issuedAt = timestampWithTimeZone("issued_at").nullable()
/**
* 开票状态
*/
val status = varchar("status", 32) .default("PENDING")
/**
* 第三方平台返回错误
*/
val errorMessage = text("error_message").nullable()
/**
* 原始请求报文
*/
val requestJson = text("request_json").nullable()
/**
* 原始响应报文
*/
val responseJson = text("response_json").nullable()
// =========================
// 文件
// =========================
/**
* PDF地址
*/
val pdfUrl = text("pdf_url").nullable()
/**
* OFD地址
*/
val ofdUrl = text("ofd_url").nullable()
/**
* XML地址
*/
val xmlUrl = text("xml_url").nullable()
// =========================
// 业务字段
// =========================
/**
* 订单号
*/
val tradeNo = varchar("trade_no", 128).nullable()
/**
* 备注
*/
val remark = text("remark").nullable()
/**
* 自定义透传数据
*/
val definedData = text("defined_data").nullable()
// =========================
// 审计字段
// =========================
val createdAt = timestampWithTimeZone("created_at")
val createdBy = uuid("created_by").nullable()
val updatedAt = timestampWithTimeZone("updated_at").nullable()
val updatedBy = uuid("updated_by").nullable()
val deletedAt = timestampWithTimeZone("deleted_at").nullable()
override val primaryKey = PrimaryKey(id)
}
@@ -11,8 +11,6 @@ object SysUserTable : Table("sys_user") {
val username = varchar("username", 50).uniqueIndex() val username = varchar("username", 50).uniqueIndex()
val passwordHash = varchar("password_hash", 255) val passwordHash = varchar("password_hash", 255)
val nickname = varchar("nickname", 50).nullable() val nickname = varchar("nickname", 50).nullable()
val realName = varchar("real_name", 50).nullable()
val phone = varchar("phone", 32).nullable()
val email = varchar("email", 100).nullable() val email = varchar("email", 100).nullable()
val avatar = text("avatar").nullable() val avatar = text("avatar").nullable()
val orgId = uuid("org_id").nullable() val orgId = uuid("org_id").nullable()
@@ -28,8 +26,29 @@ object SysUserTable : Table("sys_user") {
val deletedBy = uuid("deleted_by").nullable() val deletedBy = uuid("deleted_by").nullable()
val version = integer("version").default(1) val version = integer("version").default(1)
val taxpayerNum = varchar("taxpayer_num", 50).nullable() // 核心业务逻辑字段=================
val account = varchar("account", 50).nullable() // 数电账号相关信息
val realName = varchar("real_name", 50).nullable()
val phone = varchar("phone", 32).nullable()
val taxpayerNum = varchar("tax_payer_num", 50).nullable()
val taxAccount = varchar("tax_account", 50).nullable()
val taxPassword = varchar("tax_password", 50).nullable()
val taxIdentityType = varchar("tax_identity_type", 50).nullable()
// 联系人
val taxContactName = varchar("tax_contact_name", 50).nullable()
val taxContactPhone = varchar("tax_contact_phone", 32).nullable()
val taxContactEmail = varchar("tax_contact_email", 100).nullable()
// 公司信息
val taxLegalPersonName = varchar("tax_legal_person_name", 50).nullable() // 法人
val taxEnterpriseName = varchar("tax_enterprise_name", 200).nullable() // 企业名称
val taxRegionCode = varchar("tax_region_code", 32).nullable()//地区编码
val taxCityName = varchar("tax_city_name", 50).nullable() // 城市
val taxEnterpriseAddress = varchar("tax_enterprise_address", 200).nullable() // 企业地址
val taxRegistrationCertificate = text("tax_registration_certificate").nullable() // 企业注册证书 证件图片base64
val bankName = varchar("bank_name", 100).nullable()
val bankAccount = varchar("bank_account", 50).nullable()
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
} }
@@ -0,0 +1,438 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* 数电发票开票请求
*/
@Serializable
data class InvoiceRequest(
/**
* 销方纳税人识别号(销售方税号)
* 长度15~20,只允许大写字母和数字
*/
val taxpayerNum: String,
/**
* 发票请求流水号
* 一般格式:4位平台简称 + 16位随机数
* 必须唯一
*/
val invoiceReqSerialNo: String,
/**
* 开票种类
*
* 81:数电专票
* 82:数电普通发票
* 10:增值税电子普通发票
* 08:增值税电子专用发票
*/
val invoiceIssueKindCode: String = "82",
/**
* 购买方名称(发票抬头)
*/
val buyerName: String,
/**
* 农产品收购发票销售方证件类型
*
* 证件类型代码证件类型名称
* 201居民身份证
* 208外国护照
* 210港澳居民来往内地通行证
* 213台湾居民来往大陆通行证
* 215外国人居留证
* 219香港永久性居民身份证
* 220台湾身份证
* 221澳门特别行政区永久性居民身份证
* 233外国人永久居留身份证(外国人永久居留证)
* 103税务登记证
* 299其他个人证件
*/
val purchaseInvSellerIdType: String? = null,
/**
* 购买方纳税人识别号
* 个人开票时通常为空
*/
val buyerTaxpayerNum: String? = null,
/**
* 是否开具给自然人
*
* 0:否
* 1:是 电子税局勾选“是”时的提示:请您确认受票方为自然人,并在纳税人识别号档次填入“自然人纳税人识别号”
* (自然人受票方可登录个人所得税APP查看“自然人纳税人识别号”),该张发票将在受票方自然人个人票夹中展示。
*
*/
val naturalPersonFlag: String? = null,
/**
* 购买方地址
*/
val buyerAddress: String? = null,
/**
* 购买方电话
*/
val buyerTel: String? = null,
/**
* 购买方开户银行
*/
val buyerBankName: String? = null,
/**
* 购买方银行账号
*/
val buyerBankAccount: String? = null,
/**
* 销方地址
* 不传时平台自动取默认配置
*/
val sellerAddress: String? = null,
/**
* 销方电话
*/
val sellerTel: String? = null,
/**
* 销方开户银行
*/
val sellerBankName: String? = null,
/**
* 销方银行账号
*/
val sellerBankAccount: String? = null,
/**
* 是否在备注中显示购买方银行信息
*
* 0:不显示
* 1:显示
*/
val showBuyerBank: String? = null,
/**
* 是否在备注中显示销售方开户行及账号到发票备注
*
* 0:不显示
* 1:显示
*/
val showSellerBank: String? = null,
/**
* 是否在备注中显示购买方地址电话
*/
val showBuyerAddrTel: String? = null,
/**
* 是否在备注中显示销售方地址电话
*/
val showSellerAddrTel: String? = null,
/**
* 开票员税局账号
* 不传则平台随机选择已登记开票员
*/
val account: String? = null,
/**
* 数电发票差额征税标识。只有差额征税时需要填写。
*
* 1:全额开票
* 2:差额开票
*/
val variableLevyFlag: String? = null,
/**
* 收款人名称
*/
val casherName: String? = null,
/**
* 复核人名称
*/
val reviewerName: String? = null,
/**
* 收票人姓名
*/
val takerName: String? = null,
/**
* 收票人手机号
*/
val takerTel: String? = null,
/**
* 收票人邮箱
* 填写后系统可自动发送发票邮件
*/
val takerEmail: String? = null,
/**
* 特殊票种
*
* 08:成品油发票
* 02:农产品收购发票
* 12:自产农产品销售发票
*/
val specialInvoiceKind: String? = null,
/**
* 发票备注
*/
val remark: String? = null,
/**
* 自定义数据
* 回调推送时原样返回
*/
val definedData: String? = null,
/**
* 业务订单号
* 不传时默认使用 invoiceReqSerialNo
*/
val tradeNo: String? = null,
/**
* 门店编号(集团版)
*/
val shopNum: String? = null,
/**
* 发票商品明细
*/
val itemList: List<InvoiceItem>,
/**
* 差额征税凭证明细
*/
val variableLevyProofList: List<VariableLevyProof>? = null,
/**
* 订单列表(支持合并开票)
*/
val orderList: List<OrderInfo>? = null
)
/**
* 发票商品项目
*/
@Serializable
data class InvoiceItem(
/**
* 商品/服务名称
*/
val goodsName: String,
/**
* 税收分类编码
* 使用国家统一税编
*/
val taxClassificationCode: String,
/**
* 规格型号
*/
val specificationModel: String? = null,
/**
* 计量单位
*/
val meteringUnit: String? = null,
/**
* 数量
*/
val quantity: String? = null,
/**
* 含税标识
*
* 0:不含税
* 1:含税
*/
val includeTaxFlag: String? = "0",
/**
* 单价
*/
val unitPrice: String? = null,
/**
* 金额
*/
val invoiceAmount: String,
/**
* 税率
* 例如:
* 0.13 = 13%
*/
val taxRateValue: String,
/**
* 税额
* 不传则系统自动计算
*/
val taxRateAmount: String? = null,
/**
* 折扣金额(负数)
*/
val discountAmount: String? = null,
/**
* 折扣税额
*/
val discountTaxRateAmount: String? = null,
/**
* 差额征税扣除金额
*/
val deductionAmount: String? = null,
/**
* 优惠政策标识
*
* 1:使用优惠政策
*/
val preferentialPolicyFlag: String? = null,
/**
* 零税率标识
*
* 1:免税
* 2:不征税
* 3:普通零税率
*/
val zeroTaxFlag: String? = null,
/**
* 增值税特殊管理说明
*
* 如:
* 免税
* 不征税
* 简易征收
*/
val vatSpecialManage: String? = null,
/**
* 指定开票员账号
*/
val account: String? = null
)
/**
* 差额征税凭证明细
*/
@Serializable
data class VariableLevyProof(
/**
* 凭证类型。
* 01:数电票;
* 02:增值税专用发票;
* 03:增值税普通发票;
* 04:营业税发票;
* 05:财政票据;
* 06:法院裁决书;
* 07:契税完税凭证;
* 08:其他发票类;
* 09:其他扣除凭证。
*/
val proofType: String,
/**
* 数电票号码。
* 凭证类型为01数电票时必填。
*/
val electronicInvoiceNo: String? = null,
/**
* 发票代码。
* 凭证类型为02增值税专用发票时必填。
* 凭证类型为03增值税普通发票时必填。
* 凭证类型为04营业税发票时必填。
*/
val invoiceCode: String? = null,
/**
* 发票号码。
* 凭证类型为02增值税专用发票时必填。
* 凭证类型为03增值税普通发票时必填。
* 凭证类型为04营业税发票时必填。
*/
val invoiceNo: String? = null,
/**
* 凭证号码
*/
val proofNo: String? = null,
/**
* 开具日期
* 开具日期。
* 格式yyyy-MM-dd。
* 凭证类型为01数电票时必填。
* 凭证类型为02增值税专用发票时必填。
* 凭证类型为03增值税普通发票时必填。
* 凭证类型为04营业税发票时必填。
*/
val issueDate: String? = null,
/**
* 凭证合计金额。
* 不能等于0。
*/
val proofAmount: String,
/**
* 本次扣除金额。
* 不能等于0。所有凭证的扣除金额要等于itemList中的 deductionAmount。
*/
val deductionAmount: String,
/**
* 备注。
* 凭证类型为08其他发票类时必填。
* 凭证类型为09其他扣除凭证时必填。
*/
val proofRemark: String? = null,
/**
* 来源。默认手工录入。
* 手工录入;
* 勾选录入;
* 模板录入。
*/
val source: String? = null
)
/**
* 订单列表
* 合并订单开票可以使用该字段传值
*/
@Serializable
data class OrderInfo(
/**
* 业务单据号
*/
val orderNo: String
)
@@ -0,0 +1,68 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class TaxRegister(
/**
* 销方税号
*/
val taxpayerNum: String,
/**
* 登录方式
* 1:用户名(居民身份证号码/手机号码/用户名)+密码
*/
val loginMethod: String = "1",
/**
* 电子税局登录账号
* (手机号或身份证号)
*/
val account: String,
/**
* 电子税局登录密码
* 需要 3DES 加密
*/
val password: String,
/**
* 登录身份类型
*
* 01:法定代表人
* 02:财务负责人
* 03:办税员
* 04:涉税服务人员
* 05:管理员
* 07:领票人
* 09:开票员
* 99:其他人员
*/
val identityType: String,
/**
* 登录身份密码
* 多数地区不需要
*/
val identityPwd: String? = null,
/**
* 手机号码
*/
val phoneNum: String,
/**
* 姓名
*/
val name: String,
/**
* 操作类型
*
* 1:登记
* 2:删除
*/
val operationType: String? = null
)
@@ -0,0 +1,36 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class TaxRegisterInfo(
/** 纳税人识别号 / 税号 */
val taxpayerNum: String,
/** 企业名称 */
val enterpriseName: String,
/** 法人姓名 */
val legalPersonName: String,
/** 联系人姓名 */
val contactsName: String,
/** 联系人邮箱 */
val contactsEmail: String,
/** 联系人手机号 */
val contactsPhone: String,
/** 区域编码(行政区划代码,如省/市级编码) */
val regionCode: String,
/** 城市/区县名称(示例:海淀区) */
val cityName: String,
/** 企业详细地址 */
val enterpriseAddress: String,
/** 税务登记证编号 / 税务登记证明标识 */
val taxRegistrationCertificate: String
)
@@ -0,0 +1,9 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class TaxRegisterUserRequest(
val taxpayerNum: String,
val taxAccount: String
)
@@ -0,0 +1,11 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class UpdateDigitalAccountRequest(
/** 纳税人识别号 / 税号 */
val taxpayerNum: String = "",
/** 电子税局账号 */
val taxAccount: String = ""
)
@@ -0,0 +1,27 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class UpdateEnterpriseInfoRequest(
/** 纳税人识别号 / 税号 */
val taxpayerNum: String = "",
/** 企业名称 */
val enterpriseName: String = "",
/** 法人姓名 */
val legalPersonName: String = "",
/** 联系人姓名 */
val contactsName: String = "",
/** 联系人邮箱 */
val contactsEmail: String = "",
/** 联系人手机号 */
val contactsPhone: String = "",
/** 区域编码(行政区划代码,如省/市级编码) */
val regionCode: String = "",
/** 城市/区县名称 */
val cityName: String = "",
/** 企业详细地址 */
val enterpriseAddress: String = "",
/** 税务登记证图片 base64 */
val taxRegistrationCertificate: String = ""
)
@@ -0,0 +1,11 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class UpdatePresetDataRequest(
/** 开户银行 */
val bankName: String = "",
/** 银行账号 */
val bankAccount: String = ""
)
@@ -0,0 +1,43 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
@Serializable
data class EnterpriseInfoResponse(
/** 纳税人识别号 */
val taxpayerNum: String?,
/** 企业名称 */
val enterpriseName: String?,
/** 法人姓名 */
val legalPersonName: String?,
/** 联系人姓名 */
val contactsName: String?,
/** 联系人邮箱 */
val contactsEmail: String?,
/** 联系人手机号 */
val contactsPhone: String?,
/** 区域编码 */
val regionCode: String?,
/** 城市名称 */
val cityName: String?,
/** 企业地址 */
val enterpriseAddress: String?,
/** 注册证书 base64 */
val taxRegistrationCertificate: String?
)
@Serializable
data class DigitalAccountResponse(
/** 纳税人识别号 */
val taxpayerNum: String?,
/** 电子税局账号 */
val taxAccount: String?
)
@Serializable
data class PresetDataResponse(
/** 开户银行 */
val bankName: String?,
/** 银行账号 */
val bankAccount: String?
)
@@ -0,0 +1,12 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
@Serializable
data class EnterpriseTaxInfo(
// 纳税人识别号 / 税号(通常是企业在税务系统中的唯一标识)
val taxpayerNum: String,
// 企业名称(工商注册名称)
val enterpriseName: String
)
@@ -0,0 +1,26 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class EtaxRegisterResponse(
@SerialName("resultCode")
val resultCode: String,
@SerialName("resultMsg")
val resultMsg: String,
@SerialName("resultData")
val resultData: String? = null,
@SerialName("qrcodePath")
val qrcodePath: String? = null,
@SerialName("qrcodeImgUrl")
val qrcodeImgUrl: String? = null,
@SerialName("failureTime")
val failureTime: String? = null
)
@@ -0,0 +1,39 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
/**
* 发票开具响应结果
*/
@Serializable
data class InvoiceCreateResponse(
/**
* 发票请求流水号
*
* 用于关联本次开票请求。
* 通常格式:
* 4位平台简称 + 16位随机数
*/
val invoiceReqSerialNo: String,
/**
* 发票状态二维码访问地址
*
* 一般是 Base64 字符串或二维码链接内容。
* 用户扫码后可查看电子发票状态。
*
* 电子发票场景必传。
*/
val qrCodePath: String? = null,
/**
* 二维码图片 Base64 数据
*
* 二维码内容通常就是 qrCodePath。
* 前端可直接转图片展示。
*
* 电子发票场景必传。
*/
val qrCode: String? = null
)
@@ -4,72 +4,92 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class TaxBureauAccountAuthContent( 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 name: String,
/** /**
* 纳税人识别号 * 销售方纳税人识别号 (15-20位)
*/ */
val taxpayerNum: String, val taxpayerNum: String,
/** /**
* 是否允许切换 * 电子税局登录账号
*/
val account: String,
/**
* 登录身份类型
* 01:法定代表人
* 02:财务负责人
* 03:办税员
* 04:涉税服务人员
* 05:管理员
* 07:领票人
* 09:开票员
* 99:其他人员
*/
val identityType: String,
/**
* 操作建议(根据账号状态和不同地区的登录方式区分)
* 0:无需认证
* 1:需扫码认证
* 2:需扫码或短信认证
* 3:需短信认证
*/
val operationProposed: String,
/**
* 账号状态
* 0:无需认证
* 1:风险认证
* 2:登录认证
* 3:风险+登录认证
*/
val authStatus: String,
/**
* 当前企业是否可切换
* 0:不可切换
* 1:可切换,代表该数电账号在该地区的其他企业有登录状态
*/ */
val switchable: String, val switchable: String,
/**
* 是否绑定微信公众号
* 0:否
* 1:是
*/
val wechatUserBindStatus: String,
/**
* 最新认证成功时间 (yyyy-MM-dd HH:mm:ss)
*/
val lastAuthSuccTime: String,
/**
* 登录认证状态
* 0:未登录
* 1:已登录
*/
val loginAuthStatus: String,
/**
* 最新登录认证时间 (yyyy-MM-dd HH:mm:ss)
*/
val lastLoginAuthTime: String,
/** /**
* 风险认证状态 * 风险认证状态
* 0:未认证
* 1:已认证
*/ */
val riskAuthStatus: String, val riskAuthStatus: String,
/** /**
* 电子税局账号 * 最新风险认证时间 (yyyy-MM-dd HH:mm:ss)
*/ */
val account: String val lastRiskAuthTime: String
) )
@@ -31,8 +31,15 @@ data class CurrentUserProfile(
val username: String, val username: String,
val nickname: String? = null, val nickname: String? = null,
val realName: String? = null, val realName: String? = null,
val phone: String? = null,
val email: String? = null,
val orgId: String? = null, val orgId: String? = null,
val status: String, val status: String,
val createdAt: String? = null,
val taxpayerNum: String? = null,
val account: String? = null,
val taxPassword: String? = null,
val taxIdentityType: String? = null,
) )
@Serializable @Serializable
@@ -26,6 +26,10 @@ data class UserDetailResponse(
val status: String, val status: String,
val statusLabel: String, val statusLabel: String,
val roleIds: List<String>, val roleIds: List<String>,
val taxpayerNum: String? = null,
val account: String? = null,
val taxPassword: String? = null,
val taxIdentityType: String? = null,
) )
@Serializable @Serializable
@@ -49,6 +53,10 @@ data class UpdateUserRequest(
val email: String? = null, val email: String? = null,
val avatar: String? = null, val avatar: String? = null,
val orgId: String? = null, val orgId: String? = null,
val taxpayerNum: String? = null,
val account: String? = null,
val taxPassword: String? = null,
val taxIdentityType: String? = null,
) )
@Serializable @Serializable
@@ -1,20 +1,33 @@
@file:OptIn(ExperimentalUuidApi::class)
package com.bbit.ticket.route.piaotong package com.bbit.ticket.route.piaotong
import com.bbit.ticket.bootstrap.Global import com.bbit.ticket.bootstrap.Global
import com.bbit.ticket.entity.common.PTException import com.bbit.ticket.entity.common.PTException
import com.bbit.ticket.entity.common.fail import com.bbit.ticket.entity.common.fail
import com.bbit.ticket.entity.common.ok import com.bbit.ticket.entity.common.ok
import com.bbit.ticket.entity.common.BizException
import com.bbit.ticket.entity.common.ErrorCode
import com.bbit.ticket.entity.request.InvoiceRequest
import com.bbit.ticket.entity.request.TaxBureauAuthReq import com.bbit.ticket.entity.request.TaxBureauAuthReq
import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.TaxRegisterUserRequest
import com.bbit.ticket.entity.request.UpdateDigitalAccountRequest
import com.bbit.ticket.entity.request.UpdateEnterpriseInfoRequest
import com.bbit.ticket.entity.request.UpdatePresetDataRequest
import com.bbit.ticket.service.piaotong.PTAuthService import com.bbit.ticket.service.piaotong.PTAuthService
import com.bbit.ticket.service.piaotong.PTConfigService
import com.bbit.ticket.utils.requireCurrentUser import com.bbit.ticket.utils.requireCurrentUser
import io.ktor.http.HttpStatusCode
import io.ktor.server.auth.authenticate import io.ktor.server.auth.authenticate
import io.ktor.server.request.receive
import io.ktor.server.response.respond import io.ktor.server.response.respond
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.put
import io.ktor.server.routing.route import io.ktor.server.routing.route
import kotlinx.serialization.json.Json import kotlin.uuid.ExperimentalUuidApi
import kotlinx.serialization.json.decodeFromJsonElement
fun Route.registerPTTestRoutes() { fun Route.registerPTTestRoutes() {
route("/pt") { route("/pt") {
@@ -22,8 +35,8 @@ fun Route.registerPTTestRoutes() {
get("/info") { get("/info") {
try { try {
val currentUser = call.requireCurrentUser() val currentUser = call.requireCurrentUser()
val taxpayerNum = currentUser.taxpayerNum ?: Global.testTaxpayerNum val taxpayerNum = currentUser.taxPayerNum ?: ""
val account = currentUser.account ?: Global.testAccount val account = currentUser.taxAccount ?: ""
val response = PTAuthService.getTaxBureauAccountAuthStatus( val response = PTAuthService.getTaxBureauAccountAuthStatus(
TaxBureauAuthReq(taxpayerNum, account) TaxBureauAuthReq(taxpayerNum, account)
) )
@@ -38,6 +51,136 @@ fun Route.registerPTTestRoutes() {
) )
} }
} }
post("/register") {
try {
val currentUser = call.requireCurrentUser()
val req = call.receive<TaxRegisterInfo>()
val response = PTAuthService.registerEnterprise(req, currentUser.id)
call.respond(ok(response))
} catch (e: PTException) {
call.respond(
fail(
code = e.code,
message = e.message,
traceId = e.serialNo
)
)
}
}
post("/registerUser") {
try {
val currentUser = call.requireCurrentUser()
val req = call.receive<TaxRegisterUserRequest>()
val response = PTAuthService.registerUserFromPayload(req, currentUser)
call.respond(ok(response))
} catch (e: PTException) {
call.respond(
fail(
code = e.code,
message = e.message,
traceId = e.serialNo
)
)
}
}
// =============================================
// 基础信息配置(本地 CRUD
// =============================================
// 1. 企业信息
get("/enterprise") {
try {
val currentUser = call.requireCurrentUser()
val response = PTConfigService.getEnterpriseInfo(currentUser.id)
if (response == null) {
call.respond(ok(emptyMap<String, String>()))
} else {
call.respond(ok(response))
}
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "查询企业信息失败"))
}
}
put("/enterprise") {
try {
val currentUser = call.requireCurrentUser()
val req = call.receive<UpdateEnterpriseInfoRequest>()
val response = PTConfigService.updateEnterpriseInfo(currentUser.id, req)
call.respond(ok(response))
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "保存企业信息失败"))
}
}
// 2. 登记数电账号
get("/digital-account") {
try {
val currentUser = call.requireCurrentUser()
val response = PTConfigService.getDigitalAccount(currentUser.id)
if (response == null) {
call.respond(ok(emptyMap<String, String>()))
} else {
call.respond(ok(response))
}
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "查询数电账号失败"))
}
}
put("/digital-account") {
try {
val currentUser = call.requireCurrentUser()
val req = call.receive<UpdateDigitalAccountRequest>()
val response = PTConfigService.updateDigitalAccount(currentUser.id, req)
call.respond(ok(response))
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "保存数电账号失败"))
}
}
// 3. 开票预设数据
get("/preset") {
try {
val currentUser = call.requireCurrentUser()
val response = PTConfigService.getPresetData(currentUser.id)
if (response == null) {
call.respond(ok(emptyMap<String, String>()))
} else {
call.respond(ok(response))
}
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "查询预设数据失败"))
}
}
put("/preset") {
try {
val currentUser = call.requireCurrentUser()
val req = call.receive<UpdatePresetDataRequest>()
val response = PTConfigService.updatePresetData(currentUser.id, req)
call.respond(ok(response))
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "保存预设数据失败"))
}
}
post("/invoiceBlue") {
try {
val currentUser = call.requireCurrentUser()
val req = call.receive<InvoiceRequest>()
val response = PTAuthService.invoiceBlue(req, currentUser.id)
call.respond(ok(response))
} catch (e: PTException) {
call.respond(
fail(
code = e.code,
message = e.message,
traceId = e.serialNo
)
)
}
}
} }
} }
} }
@@ -1,24 +1,89 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package com.bbit.ticket.service.piaotong package com.bbit.ticket.service.piaotong
import com.bbit.ticket.dao.piaotong.EnterpriseTaxDao
import com.bbit.ticket.entity.request.InvoiceRequest
import com.bbit.ticket.entity.request.TaxBureauAuthReq import com.bbit.ticket.entity.request.TaxBureauAuthReq
import com.bbit.ticket.entity.request.TaxRegister
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.TaxRegisterUserRequest
import com.bbit.ticket.entity.response.EnterpriseTaxInfo
import com.bbit.ticket.entity.response.EtaxRegisterResponse
import com.bbit.ticket.entity.response.InvoiceCreateResponse
import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent
import com.bbit.ticket.plugins.dbQuery
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.net.PTClient import com.bbit.ticket.utils.net.PTClient
import kotlin.uuid.Uuid
object PTAuthService { object PTAuthService {
/** /**
* 查询数电账号认证状态 * 查询数电账号认证状态
* 此接口用来查询数电账号的认证状态,会返回当
*
* @param taxpayerNum 纳税人识别号
* @param account 账号
*/ */
suspend fun getTaxBureauAccountAuthStatus(req : TaxBureauAuthReq): String { suspend fun getTaxBureauAccountAuthStatus(req: TaxBureauAuthReq): TaxBureauAccountAuthContent {
val res = PTClient.ptPost<TaxBureauAuthReq, TaxBureauAccountAuthContent>( val res = PTClient.ptPost<TaxBureauAuthReq, TaxBureauAccountAuthContent>(
url = "getTaxBureauAccountAuthStatus.pt", url = "getTaxBureauAccountAuthStatus.pt",
body = req body = req
) )
println("res = $res") return res
return res.taxpayerNum }
/**
* 登记/删除 数电账号
*/
/**
* 登记/删除 数电账号(前端弹窗传入 taxpayerNum + taxAccount
*/
suspend fun registerUserFromPayload(req: TaxRegisterUserRequest, currentUser: CurrentUser): String {
val res = PTClient.ptPost<TaxRegister, EtaxRegisterResponse>(
url = "registerUser.pt",
body = TaxRegister(
taxpayerNum = req.taxpayerNum,
account = req.taxAccount,
password = currentUser.taxPassword ?: "",
phoneNum = currentUser.phone ?: "",
name = currentUser.realName ?: "",
identityType = currentUser.taxIdentityType ?: "",
)
)
dbQuery { EnterpriseTaxDao.updateUserAccount(currentUser.id, req.taxpayerNum, req.taxAccount) }
return res.resultMsg
}
/**
* 注册企业(纳税人)
* 将企业信息注册到票通平台
*/
suspend fun registerEnterprise(req: TaxRegisterInfo, userId: Uuid): String {
PTClient.ptPost<TaxRegisterInfo, EnterpriseTaxInfo>(
url = "register.pt",
body = req
)
dbQuery { EnterpriseTaxDao.updateEnterpriseInfo(userId, req) }
return "操作成功,企业状态为审核中(待审核)"
}
/**
* 蓝票接口调用
*/
suspend fun invoiceBlue(req: InvoiceRequest, userId: Uuid): String {
PTClient.ptPost<InvoiceRequest, InvoiceCreateResponse>(
url = "invoiceBlue.pt",
body = req
)
dbQuery { EnterpriseTaxDao.addInvoice(userId, req) }
return "操作成功,企业状态为审核中(待审核)"
}
/**
* 红票接口调用
*/
suspend fun invoiceRed(req: InvoiceRequest, userId: Uuid): String {
PTClient.ptPost<InvoiceRequest, InvoiceCreateResponse>(
url = "invoiceBlue.pt",
body = req
)
dbQuery { EnterpriseTaxDao.addInvoice(userId, req) }
return "操作成功,企业状态为审核中(待审核)"
} }
} }
@@ -0,0 +1,55 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package com.bbit.ticket.service.piaotong
import com.bbit.ticket.dao.piaotong.EnterpriseTaxDao
import com.bbit.ticket.entity.request.UpdateDigitalAccountRequest
import com.bbit.ticket.entity.request.UpdateEnterpriseInfoRequest
import com.bbit.ticket.entity.request.UpdatePresetDataRequest
import com.bbit.ticket.entity.response.DigitalAccountResponse
import com.bbit.ticket.entity.response.EnterpriseInfoResponse
import com.bbit.ticket.entity.response.PresetDataResponse
import com.bbit.ticket.plugins.dbQuery
import kotlin.uuid.Uuid
object PTConfigService {
// =============================================
// 企业信息
// =============================================
suspend fun getEnterpriseInfo(userId: Uuid): EnterpriseInfoResponse? = dbQuery {
EnterpriseTaxDao.getEnterpriseInfo(userId)
}
suspend fun updateEnterpriseInfo(userId: Uuid, req: UpdateEnterpriseInfoRequest): String {
dbQuery { EnterpriseTaxDao.updateEnterpriseInfoLocal(userId, req) }
return "企业信息保存成功"
}
// =============================================
// 登记数电账号
// =============================================
suspend fun getDigitalAccount(userId: Uuid): DigitalAccountResponse? = dbQuery {
EnterpriseTaxDao.getDigitalAccount(userId)
}
suspend fun updateDigitalAccount(userId: Uuid, req: UpdateDigitalAccountRequest): String {
dbQuery { EnterpriseTaxDao.updateDigitalAccountLocal(userId, req) }
return "账号信息保存成功"
}
// =============================================
// 开票预设数据
// =============================================
suspend fun getPresetData(userId: Uuid): PresetDataResponse? = dbQuery {
EnterpriseTaxDao.getPresetData(userId)
}
suspend fun updatePresetData(userId: Uuid, req: UpdatePresetDataRequest): String {
dbQuery { EnterpriseTaxDao.updatePresetData(userId, req) }
return "预设数据保存成功"
}
}
@@ -70,8 +70,15 @@ object AuthService {
username = userRow[SysUserTable.username], username = userRow[SysUserTable.username],
nickname = userRow[SysUserTable.nickname], nickname = userRow[SysUserTable.nickname],
realName = userRow[SysUserTable.realName], realName = userRow[SysUserTable.realName],
phone = userRow[SysUserTable.phone],
email = userRow[SysUserTable.email],
orgId = userRow[SysUserTable.orgId]?.toString(), orgId = userRow[SysUserTable.orgId]?.toString(),
status = userRow[SysUserTable.status], status = userRow[SysUserTable.status],
createdAt = userRow[SysUserTable.createdAt]?.toString(),
taxpayerNum = userRow[SysUserTable.taxpayerNum],
account = userRow[SysUserTable.taxAccount],
taxPassword = userRow[SysUserTable.taxPassword],
taxIdentityType = userRow[SysUserTable.taxIdentityType],
), ),
menus = menuTree, menus = menuTree,
permissions = permissions, permissions = permissions,
@@ -9,7 +9,6 @@ import com.bbit.ticket.database.system.SysRoleMenuTable
import com.bbit.ticket.database.system.SysRoleTable import com.bbit.ticket.database.system.SysRoleTable
import com.bbit.ticket.database.system.SysUserRoleTable import com.bbit.ticket.database.system.SysUserRoleTable
import com.bbit.ticket.database.system.SysUserTable import com.bbit.ticket.database.system.SysUserTable
import com.bbit.ticket.database.system.SysUserTable.account
import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.plugins.dbQuery
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCall
@@ -32,8 +31,12 @@ data class CurrentUser(
val tokenVersion: Int, val tokenVersion: Int,
val roleCodes: Set<String>, val roleCodes: Set<String>,
val permissions: Set<String>, val permissions: Set<String>,
val taxpayerNum: String?, val taxPayerNum: String?,
val account: String?, val taxAccount: String?,
val phone: String?,
val realName: String?,
val taxPassword: String?,
val taxIdentityType: String?,
) { ) {
val isSuperAdmin: Boolean val isSuperAdmin: Boolean
get() = roleCodes.contains("SUPER_ADMIN") get() = roleCodes.contains("SUPER_ADMIN")
@@ -138,8 +141,12 @@ suspend fun ApplicationCall.requireCurrentUser(): CurrentUser {
tokenVersion = userRow[SysUserTable.tokenVersion], tokenVersion = userRow[SysUserTable.tokenVersion],
roleCodes = roleCodes, roleCodes = roleCodes,
permissions = permissions, permissions = permissions,
taxpayerNum = userRow[SysUserTable.taxpayerNum], taxPayerNum = userRow[SysUserTable.taxpayerNum],
account = userRow[SysUserTable.account], taxAccount = userRow[SysUserTable.taxAccount],
taxPassword = userRow[SysUserTable.taxPassword],
taxIdentityType = userRow[SysUserTable.taxIdentityType],
phone = userRow[SysUserTable.phone],
realName = userRow[SysUserTable.realName],
) )
attributes.put(CurrentUserKey, currentUser) attributes.put(CurrentUserKey, currentUser)
@@ -12,6 +12,7 @@ import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@@ -25,6 +26,7 @@ object PTClient {
install(ContentNegotiation) { install(ContentNegotiation) {
json( json(
Json { Json {
explicitNulls = false
ignoreUnknownKeys = true ignoreUnknownKeys = true
prettyPrint = true prettyPrint = true
isLenient = true isLenient = true
@@ -82,18 +84,12 @@ object PTClient {
val response = client.post(Global.baseUrl + url) { val response = client.post(Global.baseUrl + url) {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
headers.forEach { (k, v) -> header(k, v) }
headers.forEach { (k, v) ->
header(k, v)
}
setBody(buildRequestData(Json.encodeToString(body))) setBody(buildRequestData(Json.encodeToString(body)))
}.bodyAsText() }.bodyAsText()
val decrypted = disposeResponse(response) val decrypted = disposeResponse(response)
val result = Json.decodeFromString<PTResponse<JsonElement>>(decrypted) val result = Json.decodeFromString<PTResponse<JsonElement>>(decrypted)
if (result.code != "0000") { if (result.code != "0000") {
throw PTException( throw PTException(
code = result.code, code = result.code,
@@ -102,7 +98,8 @@ object PTClient {
) )
} }
return Json.decodeFromJsonElement(result.content!!) println("res = $result.content")
return Json.decodeFromJsonElement<Resp>(result.content!!)
} }
/** /**
@@ -124,7 +121,7 @@ object PTClient {
map["version"] = "1.0" map["version"] = "1.0"
map["content"] = reqContent map["content"] = reqContent
map["timestamp"] = sdf.format(Date()) map["timestamp"] = sdf.format(Date())
map["serialNo"] = ptDate(Global.ptPlatformAlias) map["serialNo"] = ptDate()
map["sign"] = RSAUtil.sign(RSAUtil.getSignatureContent(map), Global.ptPrivateKey) ?: "" map["sign"] = RSAUtil.sign(RSAUtil.getSignatureContent(map), Global.ptPrivateKey) ?: ""
return Json.encodeToString(map) return Json.encodeToString(map)
} }
@@ -157,26 +154,34 @@ object PTClient {
val encryptedContent = mutableMap["content"] val encryptedContent = mutableMap["content"]
?: throw IllegalStateException("content 为空") ?: throw IllegalStateException("content 为空")
val plainContent = SecurityUtil.decrypt3DES(Global.ptPassword, encryptedContent) ?: "" val plainContent =
SecurityUtil.decrypt3DES(Global.ptPassword, encryptedContent)
?.trim()
?.takeIf { it.isNotEmpty() }
?: "{}"
val contentElement = runCatching {
Json.parseToJsonElement(plainContent)
}.getOrElse {
JsonObject(emptyMap())
}
// 5. 替换 content
val resultJson = buildJsonObject { val resultJson = buildJsonObject {
json.forEach { (k, v) -> json.forEach { (k, v) ->
if (k == "content") { if (k == "content") {
put("content", Json.parseToJsonElement(plainContent)) put("content", contentElement)
} else { } else {
put(k, v) put(k, v)
} }
} }
} }
return resultJson.toString() return resultJson.toString()
} }
fun ptDate(prefix: String?): String { fun ptDate(): String {
val date = Date() val date = Date()
val sdf = SimpleDateFormat("YYYYMMddHHmmss") val sdf = SimpleDateFormat("YYYYMMddHHmmss")
val str = prefix + sdf.format(date) + (Math.random() * 90 + 10).toInt() val str = Global.ptPlatformAlias + sdf.format(date) + (Math.random() * 90 + 10).toInt()
println(str) println(str)
return str return str
} }
+3 -1
View File
@@ -30,7 +30,9 @@ http.interceptors.response.use(
return response.data return response.data
} }
if (payload.code !== '0') { if (payload.code !== '0') {
throw new BizError(payload.code, payload.message || '请求失败', payload.traceId ?? traceId) const errMsg = payload.message || '请求失败'
message.error(payload.traceId ?? traceId ? `${errMsg}(追踪ID${payload.traceId ?? traceId}` : errMsg)
throw new BizError(payload.code, errMsg, payload.traceId ?? traceId)
} }
return payload.data return payload.data
}, },
+337
View File
@@ -0,0 +1,337 @@
import http from '@/api/http'
/**
* 账号状态
* 0:无需认证
* 1:风险认证
* 2:登录认证
* 3:风险+登录认证
*/
export type AuthStatus = '0' | '1' | '2' | '3'
/**
* 登录身份类型
* 01:法定代表人
* 02:财务负责人
* 03:办税员
* 04:涉税服务人员
* 05:管理员
* 07:领票人
* 09:开票员
* 99:其他人员
*/
export type IdentityType = '01' | '02' | '03' | '04' | '05' | '07' | '09' | '99'
/**
* 操作建议
* 0:无需认证
* 1:需扫码认证
* 2:需扫码或短信认证
* 3:需短信认证
*/
export type OperationProposed = '0' | '1' | '2' | '3'
/**
* 可切换状态
* 0:不可切换
* 1:可切换,代表该数电账号在该地区的其他企业有登录状态
*/
export type Switchable = '0' | '1'
/**
* 微信绑定状态
* 0:否
* 1:是
*/
export type WechatBindStatus = '0' | '1'
/**
* 登录认证状态
* 0:未登录
* 1:已登录
*/
export type LoginAuthStatus = '0' | '1'
/**
* 风险认证状态
* 0:未认证
* 1:已认证
*/
export type RiskAuthStatus = '0' | '1'
export interface TaxBureauAccountAuth {
/** 姓名 */
name: string
/** 销售方纳税人识别号 (15-20位) */
taxpayerNum: string
/** 电子税局登录账号 */
account: string
/** 登录身份类型: 01法定代表人, 02财务负责人, 03办税员, 04涉税服务人员, 05管理员, 07领票人, 09开票员, 99其他人员 */
identityType: string
/** 操作建议: 0无需认证, 1需扫码认证, 2需扫码或短信认证, 3需短信认证 */
operationProposed: string
/** 账号状态: 0无需认证, 1风险认证, 2登录认证, 3风险+登录认证 */
authStatus: string
/** 当前企业是否可切换: 0不可切换, 1可切换 */
switchable: string
/** 是否绑定微信公众号: 0否, 1是 */
wechatUserBindStatus: string
/** 最新认证成功时间 (yyyy-MM-dd HH:mm:ss) */
lastAuthSuccTime: string
/** 登录认证状态: 0未登录, 1已登录 */
loginAuthStatus: string
/** 最新登录认证时间 (yyyy-MM-dd HH:mm:ss) */
lastLoginAuthTime: string
/** 风险认证状态: 0未认证, 1已认证 */
riskAuthStatus: string
/** 最新风险认证时间 (yyyy-MM-dd HH:mm:ss) */
lastRiskAuthTime: string
}
/**
* 获取票通账号认证状态信息
*/
export function getPTInfoApi(): Promise<TaxBureauAccountAuth> {
return http.get('/pt/info')
}
export interface TaxEnterpriseRegisterRequest {
taxpayerNum: string
enterpriseName: string
legalPersonName: string
contactsName: string
contactsEmail: string
contactsPhone: string
regionCode: string
cityName: string
enterpriseAddress: string
taxRegistrationCertificate: string
}
/**
* 注册企业(纳税人)
*/
export function registerEnterpriseApi(payload: TaxEnterpriseRegisterRequest): Promise<string> {
return http.post('/pt/register', payload)
}
export interface TaxRegisterUserRequest {
taxpayerNum: string
taxAccount: string
}
/**
* 登记账号
*/
export function registerUserApi(payload: TaxRegisterUserRequest): Promise<string> {
return http.post('/pt/registerUser', payload)
}
// =============================================
// 基础信息配置(本地 CRUD
// =============================================
/** 企业信息 */
export interface EnterpriseInfo {
taxpayerNum: string
enterpriseName: string
legalPersonName: string
contactsName: string
contactsEmail: string
contactsPhone: string
regionCode: string
cityName: string
enterpriseAddress: string
taxRegistrationCertificate: string
}
/** 获取企业信息 */
export function getEnterpriseInfoApi(): Promise<EnterpriseInfo> {
return http.get('/pt/enterprise')
}
/** 更新企业信息 */
export function updateEnterpriseInfoApi(payload: Partial<EnterpriseInfo>): Promise<string> {
return http.put('/pt/enterprise', payload)
}
/** 数电账号信息 */
export interface DigitalAccountInfo {
taxpayerNum: string
taxAccount: string
}
/** 获取数电账号信息 */
export function getDigitalAccountApi(): Promise<DigitalAccountInfo> {
return http.get('/pt/digital-account')
}
/** 更新数电账号信息 */
export function updateDigitalAccountApi(payload: DigitalAccountInfo): Promise<string> {
return http.put('/pt/digital-account', payload)
}
/** 开票预设数据 */
export interface PresetData {
bankName: string
bankAccount: string
}
/** 获取开票预设数据 */
export function getPresetDataApi(): Promise<PresetData> {
return http.get('/pt/preset')
}
/** 更新开票预设数据 */
export function updatePresetDataApi(payload: PresetData): Promise<string> {
return http.put('/pt/preset', payload)
}
// =============================================
// 开票相关
// =============================================
/** 发票商品项目 */
export interface InvoiceItem {
/** 商品/服务名称 */
goodsName: string
/** 税收分类编码 */
taxClassificationCode: string
/** 规格型号 */
specificationModel?: string
/** 计量单位 */
meteringUnit?: string
/** 数量 */
quantity?: string
/** 含税标识:0不含税,1含税 */
includeTaxFlag?: string
/** 单价 */
unitPrice?: string
/** 金额 */
invoiceAmount: string
/** 税率 如 0.13 = 13% */
taxRateValue: string
/** 税额 */
taxRateAmount?: string
/** 折扣金额(负数) */
discountAmount?: string
/** 折扣税额 */
discountTaxRateAmount?: string
/** 优惠政策标识:1使用优惠政策 */
preferentialPolicyFlag?: string
/** 零税率标识:1免税,2不征税,3普通零税率 */
zeroTaxFlag?: string
/** 增值税特殊管理说明 */
vatSpecialManage?: string
}
/** 差额征税凭证明细 */
export interface VariableLevyProof {
/** 凭证类型:01数电票,02增值税专票,03增值税普票 */
proofType: string
/** 数电票号码 */
electronicInvoiceNo?: string
/** 发票代码 */
invoiceCode?: string
/** 发票号码 */
invoiceNo?: string
/** 凭证号码 */
proofNo?: string
/** 开具日期 yyyy-MM-dd */
issueDate?: string
/** 凭证总金额 */
proofAmount: string
/** 本次扣除金额 */
deductionAmount: string
/** 备注 */
proofRemark?: string
/** 来源 */
source?: string
}
/** 订单信息 */
export interface OrderInfo {
/** 业务单据号 */
orderNo: string
}
/**
* 数电发票开票请求
*/
export interface InvoiceRequest {
/** 销方纳税人识别号 */
taxpayerNum: string
/** 发票请求流水号 */
invoiceReqSerialNo: string
/** 开票种类:81数电专票,82数电普票,10电子普票,08电子专票 */
invoiceIssueKindCode?: string
/** 购买方名称(发票抬头) */
buyerName: string
/** 农产品收购发票销售方证件类型:201居民身份证, 208外国护照, 210港澳居民来往内地通行证, 213台湾居民来往大陆通行证, 215外国人居留证, 219香港永久性居民身份证, 220台湾身份证, 221澳门特别行政区永久性居民身份证, 233外国人永久居留身份证, 103税务登记证, 299其他个人证件 */
purchaseInvSellerIdType?: string
/** 购买方纳税人识别号 */
buyerTaxpayerNum?: string
/** 是否自然人:0否,1是 */
naturalPersonFlag?: string
/** 购买方地址 */
buyerAddress?: string
/** 购买方电话 */
buyerTel?: string
/** 购买方开户银行 */
buyerBankName?: string
/** 购买方银行账号 */
buyerBankAccount?: string
/** 销方地址 */
sellerAddress?: string
/** 销方电话 */
sellerTel?: string
/** 销方开户银行 */
sellerBankName?: string
/** 销方银行账号 */
sellerBankAccount?: string
/** 备注显示购买方银行:0不显示,1显示 */
showBuyerBank?: string
/** 备注显示销售方银行:0不显示,1显示 */
showSellerBank?: string
/** 备注显示购买方地址电话 */
showBuyerAddrTel?: string
/** 备注显示销售方地址电话 */
showSellerAddrTel?: string
/** 开票员税局账号 */
account?: string
/** 差额征税标识:1全额,2差额 */
variableLevyFlag?: string
/** 收款人名称 */
casherName?: string
/** 复核人名称 */
reviewerName?: string
/** 收票人姓名 */
takerName?: string
/** 收票人手机号 */
takerTel?: string
/** 收票人邮箱 */
takerEmail?: string
/** 特殊票种:08成品油,02农产品收购,12自产农产品 */
specialInvoiceKind?: string
/** 发票备注 */
remark?: string
/** 自定义数据 */
definedData?: string
/** 业务订单号 */
tradeNo?: string
/** 门店编号(集团版) */
shopNum?: string
/** 发票商品明细 */
itemList: InvoiceItem[]
/** 差额征税凭证明细 */
variableLevyProofList?: VariableLevyProof[]
/** 订单列表(支持合并开票) */
orderList?: OrderInfo[]
}
/**
* 提交开票
*/
export function invoiceIssueApi(payload: InvoiceRequest): Promise<string> {
return http.post('/pt/invoiceBlue', payload)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
<template>
<div class="page">
<div class="placeholder">
<h2>开票历史</h2>
<p>功能开发中敬请期待</p>
</div>
</div>
</template>
<style scoped>
.page {
min-height: 100%;
background: #f7f8fa;
display: flex;
align-items: center;
justify-content: center;
}
.placeholder {
text-align: center;
color: #bbb;
}
.placeholder h2 {
margin: 0 0 8px;
font-size: 18px;
font-weight: 600;
color: #999;
}
.placeholder p {
margin: 0;
font-size: 14px;
}
</style>
File diff suppressed because it is too large Load Diff
+8
View File
@@ -16,6 +16,14 @@ export interface CurrentUserProfile {
realName?: string | null realName?: string | null
orgId?: string | null orgId?: string | null
status: string status: string
avatar?: string | null
email?: string | null
phone?: string | null
createdAt?: string | null
taxpayerNum?: string | null
account?: string | null
taxPassword?: string | null
taxIdentityType?: string | null
} }
export interface MenuNode { export interface MenuNode {
+8
View File
@@ -23,6 +23,10 @@ export interface UserDetail {
status: string status: string
statusLabel?: string statusLabel?: string
roleIds: string[] roleIds: string[]
taxpayerNum?: string | null
account?: string | null
taxPassword?: string | null
taxIdentityType?: string | null
} }
export interface UserQuery { export interface UserQuery {
@@ -55,6 +59,10 @@ export interface UpdateUserRequest {
email?: string email?: string
avatar?: string avatar?: string
orgId?: string orgId?: string
taxpayerNum?: string
account?: string
taxPassword?: string
taxIdentityType?: string
} }
export interface UpdateUserStatusRequest { export interface UpdateUserStatusRequest {