From 4b23f3546ada6ef7196b20498fc0b8c9d2051ddb Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Mon, 11 May 2026 09:31:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=9F=BA=E7=A1=80=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E4=B8=8E=E5=BC=80=E7=A5=A8=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .codebuddy/rules/编码习惯.mdc | 11 + .../V20260508__alter_sys_user_tax_columns.sql | 8 + .../ticket/bootstrap/DatabaseInitializer.kt | 6 +- .../com/bbit/ticket/bootstrap/Global.kt | 14 +- .../com/bbit/ticket/bootstrap/SeedData.kt | 596 ++----- .../ticket/dao/piaotong/EnterpriseTaxDao.kt | 181 +++ .../com/bbit/ticket/dao/system/UserDao.kt | 8 + .../database/piaotong/InvoiceItemTable.kt | 106 ++ .../database/piaotong/InvoiceOrderTable.kt | 181 +++ .../ticket/database/system/SysUserTable.kt | 27 +- .../ticket/entity/request/InvoiceRequest.kt | 438 +++++ .../bbit/ticket/entity/request/TaxRegister.kt | 68 + .../ticket/entity/request/TaxRegisterInfo.kt | 36 + .../entity/request/TaxRegisterUserRequest.kt | 9 + .../request/UpdateDigitalAccountRequest.kt | 11 + .../request/UpdateEnterpriseInfoRequest.kt | 27 + .../entity/request/UpdatePresetDataRequest.kt | 11 + .../entity/response/EnterpriseInfoResponse.kt | 43 + .../entity/response/EnterpriseTaxInfo.kt | 12 + .../entity/response/EtaxRegisterResponse.kt | 26 + .../entity/response/InvoiceCreateResponse.kt | 39 + .../response/TaxBureauAccountAuthContent.kt | 118 +- .../com/bbit/ticket/entity/system/AuthDto.kt | 7 + .../com/bbit/ticket/entity/system/UserDto.kt | 8 + .../route/piaotong/registerPTTestRoutes.kt | 153 +- .../ticket/service/piaotong/PTAuthService.kt | 81 +- .../service/piaotong/PTConfigService.kt | 55 + .../bbit/ticket/service/system/AuthService.kt | 7 + .../bbit/ticket/utils/SecurityPrincipal.kt | 17 +- .../com/bbit/ticket/utils/net/PTClient.kt | 35 +- web/src/api/http.ts | 4 +- web/src/api/piaotong/index.ts | 337 ++++ web/src/features/piaotong/index.vue | 1360 ++++++++++++++++ .../piaotong/invoice-history/index.vue | 35 + .../features/piaotong/invoice-issue/index.vue | 1422 +++++++++++++++++ web/src/types/auth.ts | 8 + web/src/types/system/user.ts | 8 + 37 files changed, 4975 insertions(+), 538 deletions(-) create mode 100644 .codebuddy/rules/编码习惯.mdc create mode 100644 doc/migration/V20260508__alter_sys_user_tax_columns.sql create mode 100644 server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseTaxDao.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceOrderTable.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/InvoiceRequest.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegister.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterInfo.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterUserRequest.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateDigitalAccountRequest.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateEnterpriseInfoRequest.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/UpdatePresetDataRequest.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseInfoResponse.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseTaxInfo.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/EtaxRegisterResponse.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceCreateResponse.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTConfigService.kt create mode 100644 web/src/api/piaotong/index.ts create mode 100644 web/src/features/piaotong/index.vue create mode 100644 web/src/features/piaotong/invoice-history/index.vue create mode 100644 web/src/features/piaotong/invoice-issue/index.vue diff --git a/.codebuddy/rules/编码习惯.mdc b/.codebuddy/rules/编码习惯.mdc new file mode 100644 index 0000000..4bb8291 --- /dev/null +++ b/.codebuddy/rules/编码习惯.mdc @@ -0,0 +1,11 @@ +--- +description: +alwaysApply: true +enabled: true +updatedAt: 2026-05-08T02:44:45.758Z +provider: +--- + +1. 编码时要参考项目类似结构的代码,比如写接口要参考其他接口,写service要参考其他service,写dao要参考其他dao。 +2. 要保持代码干净、已读。 +3. 尽量不要出现重复代码,相同的逻辑需要提取出工具类到新文件中。 \ No newline at end of file diff --git a/doc/migration/V20260508__alter_sys_user_tax_columns.sql b/doc/migration/V20260508__alter_sys_user_tax_columns.sql new file mode 100644 index 0000000..5fe3c48 --- /dev/null +++ b/doc/migration/V20260508__alter_sys_user_tax_columns.sql @@ -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; diff --git a/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt index d86f25c..a1a5637 100644 --- a/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt @@ -1,5 +1,7 @@ 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.SysDictItemTable import com.bbit.ticket.database.system.SysDictTypeTable @@ -30,6 +32,8 @@ object DatabaseInitializer { SysDictItemTable, SysOperationLogTable, SysApiAccessLogTable, + InvoiceItemTable, + InvoiceOrderTable, ) // 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。 transaction { @@ -40,7 +44,7 @@ object DatabaseInitializer { if (statements.isNotEmpty()) { logger.info("Migrating database schema, statement count={}", statements.size) statements.forEach { - logger.debug("Executing migration SQL: {};", it) + logger.info("Executing migration SQL: {};", it) exec(it) } } diff --git a/server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt index 68b9099..81db646 100644 --- a/server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/Global.kt @@ -2,28 +2,24 @@ 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 diff --git a/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt index d7325b1..ee8f61c 100644 --- a/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/SeedData.kt @@ -2,16 +2,10 @@ package com.bbit.ticket.bootstrap -import com.bbit.ticket.database.system.SysDictItemTable -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.database.system.* import com.bbit.ticket.plugins.dbQuery 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.eq import org.jetbrains.exposed.v1.core.inList @@ -34,6 +28,10 @@ object SeedData { private const val DEFAULT_ORG_CODE = "DEFAULT_ORG" private const val SUPER_ADMIN_ROLE_CODE = "SUPER_ADMIN" + // ========================================================= + // Main entry + // ========================================================= + suspend fun seed() { val now = OffsetDateTime.now() val orgId = upsertDefaultOrg(now) @@ -46,6 +44,10 @@ object SeedData { logger.info("Seed data initialized, default admin username: {}", ADMIN_USERNAME) } + // ========================================================= + // Organization & Role + // ========================================================= + private suspend fun upsertDefaultOrg(now: OffsetDateTime): Uuid = dbQuery { val existing = SysOrgTable.selectAll() .where { (SysOrgTable.code eq DEFAULT_ORG_CODE) and SysOrgTable.deletedAt.isNull() } @@ -54,21 +56,21 @@ object SeedData { if (existing != null) { val id = existing[SysOrgTable.id] SysOrgTable.update({ SysOrgTable.id eq id }) { - it[name] = "默认组织" - it[sort] = 0 - it[status] = "ENABLED" - it[updatedAt] = now + it[SysOrgTable.name] = "默认组织" + it[SysOrgTable.sort] = 0 + it[SysOrgTable.status] = "ENABLED" + it[SysOrgTable.updatedAt] = now } return@dbQuery id } val inserted = SysOrgTable.insert { - it[parentId] = null - it[name] = "默认组织" - it[code] = DEFAULT_ORG_CODE - it[sort] = 0 - it[status] = "ENABLED" - it[createdAt] = now + it[SysOrgTable.parentId] = null + it[SysOrgTable.name] = "默认组织" + it[SysOrgTable.code] = DEFAULT_ORG_CODE + it[SysOrgTable.sort] = 0 + it[SysOrgTable.status] = "ENABLED" + it[SysOrgTable.createdAt] = now } inserted[SysOrgTable.id] } @@ -81,26 +83,30 @@ object SeedData { if (existing != null) { val id = existing[SysRoleTable.id] SysRoleTable.update({ SysRoleTable.id eq id }) { - it[name] = "超级管理员" - it[description] = "系统内置超级管理员角色" - it[status] = "ENABLED" - it[dataScope] = "ALL" - it[updatedAt] = now + it[SysRoleTable.name] = "超级管理员" + it[SysRoleTable.description] = "系统内置超级管理员角色" + it[SysRoleTable.status] = "ENABLED" + it[SysRoleTable.dataScope] = "ALL" + it[SysRoleTable.updatedAt] = now } return@dbQuery id } val inserted = SysRoleTable.insert { - it[name] = "超级管理员" - it[code] = SUPER_ADMIN_ROLE_CODE - it[description] = "系统内置超级管理员角色" - it[status] = "ENABLED" - it[dataScope] = "ALL" - it[createdAt] = now + it[SysRoleTable.name] = "超级管理员" + it[SysRoleTable.code] = SUPER_ADMIN_ROLE_CODE + it[SysRoleTable.description] = "系统内置超级管理员角色" + it[SysRoleTable.status] = "ENABLED" + it[SysRoleTable.dataScope] = "ALL" + it[SysRoleTable.createdAt] = now } inserted[SysRoleTable.id] } + // ========================================================= + // Admin user + // ========================================================= + private suspend fun upsertAdminUser(orgId: Uuid, now: OffsetDateTime): Uuid = dbQuery { val existing = SysUserTable.selectAll() .where { (SysUserTable.username eq ADMIN_USERNAME) and SysUserTable.deletedAt.isNull() } @@ -109,29 +115,34 @@ object SeedData { if (existing != null) { val id = existing[SysUserTable.id] SysUserTable.update({ SysUserTable.id eq id }) { - it[nickname] = "管理员" - it[realName] = "系统管理员" + it[SysUserTable.nickname] = "系统管理员" it[SysUserTable.orgId] = orgId - it[status] = "ENABLED" - it[updatedAt] = now - it[taxpayerNum] = "500102201007206608" - it[account] = "DEMOadmin" + it[SysUserTable.status] = "ENABLED" + it[SysUserTable.updatedAt] = now + it[SysUserTable.taxpayerNum] = "500102201007206608" + it[SysUserTable.phone] = "13000000000" + it[SysUserTable.taxIdentityType] = "01" + it[SysUserTable.taxPassword] = SecurityUtil.encrypt3DES(Global.ptPassword, "ispassword") + it[SysUserTable.realName] = "测试" } 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 { @@ -146,359 +157,42 @@ object SeedData { } } + // ========================================================= + // Menus & permissions + // ========================================================= + private suspend fun upsertMenus(now: OffsetDateTime): List { val seedMenus = listOf( - SeedMenu( - "dashboard", - null, - "MENU", - "工作台", - "Dashboard", - "/dashboard", - "dashboard/index", - "LayoutDashboard", - null, - 10, - true, - true - ), - SeedMenu( - "system", - null, - "CATALOG", - "系统管理", - "SystemRoot", - "/system", - null, - "Settings", - null, - 20, - true, - false - ), - SeedMenu( - "system_user", - "system", - "MENU", - "用户管理", - "SystemUsers", - "/system/users", - "system/users/index", - "Users", - "system:user:view", - 10, - true, - true - ), - SeedMenu( - "system_user_create", - "system_user", - "BUTTON", - "新增用户", - "SystemUserCreate", - null, - null, - null, - "system:user:create", - 1, - true, - false - ), - SeedMenu( - "system_user_update", - "system_user", - "BUTTON", - "修改用户", - "SystemUserUpdate", - null, - null, - null, - "system:user:update", - 2, - true, - false - ), - SeedMenu( - "system_user_delete", - "system_user", - "BUTTON", - "删除用户", - "SystemUserDelete", - null, - null, - null, - "system:user:delete", - 3, - true, - false - ), - SeedMenu( - "system_org", - "system", - "MENU", - "组织管理", - "SystemOrgs", - "/system/orgs", - "system/orgs/index", - "Building2", - "system:org:view", - 20, - true, - true - ), - SeedMenu( - "system_org_create", - "system_org", - "BUTTON", - "新增组织", - "SystemOrgCreate", - null, - null, - null, - "system:org:create", - 1, - true, - false - ), - SeedMenu( - "system_org_update", - "system_org", - "BUTTON", - "更新组织", - "SystemOrgUpdate", - null, - null, - null, - "system:org:update", - 2, - true, - false - ), - SeedMenu( - "system_org_delete", - "system_org", - "BUTTON", - "删除组织", - "SystemOrgDelete", - null, - null, - null, - "system:org:delete", - 3, - true, - false - ), - SeedMenu( - "system_role", - "system", - "MENU", - "角色管理", - "SystemRoles", - "/system/roles", - "system/roles/index", - "Shield", - "system:role:view", - 30, - true, - true - ), - SeedMenu( - "system_role_create", - "system_role", - "BUTTON", - "新增角色", - "SystemRoleCreate", - null, - null, - null, - "system:role:create", - 1, - true, - false - ), - SeedMenu( - "system_role_update", - "system_role", - "BUTTON", - "更新角色", - "SystemRoleUpdate", - null, - null, - null, - "system:role:update", - 2, - true, - false - ), - SeedMenu( - "system_role_delete", - "system_role", - "BUTTON", - "删除角色", - "SystemRoleDelete", - null, - null, - null, - "system:role:delete", - 3, - true, - false - ), - SeedMenu( - "system_role_assign", - "system_role", - "BUTTON", - "分配角色权限", - "SystemRoleAssign", - null, - null, - null, - "system:role:assign", - 4, - true, - false - ), - SeedMenu( - "system_menu", - "system", - "MENU", - "菜单管理", - "SystemMenus", - "/system/menus", - "system/menus/index", - "PanelLeft", - "system:menu:view", - 40, - true, - true - ), - SeedMenu( - "system_menu_create", - "system_menu", - "BUTTON", - "新增菜单", - "SystemMenuCreate", - null, - null, - null, - "system:menu:create", - 1, - true, - false - ), - SeedMenu( - "system_menu_update", - "system_menu", - "BUTTON", - "更新菜单", - "SystemMenuUpdate", - null, - null, - null, - "system:menu:update", - 2, - true, - false - ), - SeedMenu( - "system_menu_delete", - "system_menu", - "BUTTON", - "删除菜单", - "SystemMenuDelete", - null, - null, - null, - "system:menu:delete", - 3, - true, - false - ), - SeedMenu( - "system_dict", - "system", - "MENU", - "字典管理", - "SystemDict", - "/system/dicts", - "system/dicts/index", - "BookType", - "system:dict:view", - 50, - true, - true - ), - SeedMenu( - "system_dict_create", - "system_dict", - "BUTTON", - "新增字典", - "SystemDictCreate", - null, - null, - null, - "system:dict:create", - 1, - true, - false - ), - SeedMenu( - "system_dict_update", - "system_dict", - "BUTTON", - "更新字典", - "SystemDictUpdate", - null, - null, - null, - "system:dict:update", - 2, - true, - false - ), - SeedMenu( - "system_dict_delete", - "system_dict", - "BUTTON", - "删除字典", - "SystemDictDelete", - null, - null, - null, - "system:dict:delete", - 3, - true, - false - ), - SeedMenu("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 - ), + rootMenu("dashboard", "工作台", "Dashboard", "/dashboard", "dashboard/index", "LayoutDashboard", 10), + catalog("system", "系统管理", "SystemRoot", "Settings", 20), + subMenu("system_user", "system", "用户管理", "SystemUsers", "/system/users", "system/users/index", "Users", "system:user:view", 10), + button("system_user_create", "system_user", "新增用户", "SystemUserCreate", "system:user:create", 1), + button("system_user_update", "system_user", "修改用户", "SystemUserUpdate", "system:user:update", 2), + button("system_user_delete", "system_user", "删除用户", "SystemUserDelete", "system:user:delete", 3), + subMenu("system_org", "system", "组织管理", "SystemOrgs", "/system/orgs", "system/orgs/index", "Building2", "system:org:view", 20), + button("system_org_create", "system_org", "新增组织", "SystemOrgCreate", "system:org:create", 1), + button("system_org_update", "system_org", "更新组织", "SystemOrgUpdate", "system:org:update", 2), + button("system_org_delete", "system_org", "删除组织", "SystemOrgDelete", "system:org:delete", 3), + subMenu("system_role", "system", "角色管理", "SystemRoles", "/system/roles", "system/roles/index", "Shield", "system:role:view", 30), + button("system_role_create", "system_role", "新增角色", "SystemRoleCreate", "system:role:create", 1), + button("system_role_update", "system_role", "更新角色", "SystemRoleUpdate", "system:role:update", 2), + button("system_role_delete", "system_role", "删除角色", "SystemRoleDelete", "system:role:delete", 3), + button("system_role_assign", "system_role", "分配角色权限", "SystemRoleAssign", "system:role:assign", 4), + subMenu("system_menu", "system", "菜单管理", "SystemMenus", "/system/menus", "system/menus/index", "PanelLeft", "system:menu:view", 40), + button("system_menu_create", "system_menu", "新增菜单", "SystemMenuCreate", "system:menu:create", 1), + button("system_menu_update", "system_menu", "更新菜单", "SystemMenuUpdate", "system:menu:update", 2), + button("system_menu_delete", "system_menu", "删除菜单", "SystemMenuDelete", "system:menu:delete", 3), + subMenu("system_dict", "system", "字典管理", "SystemDict", "/system/dicts", "system/dicts/index", "BookType", "system:dict:view", 50), + button("system_dict_create", "system_dict", "新增字典", "SystemDictCreate", "system:dict:create", 1), + button("system_dict_update", "system_dict", "更新字典", "SystemDictUpdate", "system:dict:update", 2), + button("system_dict_delete", "system_dict", "删除字典", "SystemDictDelete", "system:dict:delete", 3), + catalog("logs", "日志管理", "LogsRoot", "Logs", 30), + subMenu("logs_operation", "logs", "操作日志", "LogsOperation", "/logs/operation", "logs/operation/index", "ScrollText", "log:operation:view", 10), + subMenu("logs_api_access", "logs", "接口日志", "LogsApiAccess", "/logs/api-access", "logs/api-access/index", "Waypoints", "log:api-access:view", 20), + catalog("piaotong", "票通服务", "PiaoTongRoot", "Receipt", 40), + subMenu("piaotong_info", "piaotong", "基础信息", "PiaoTongInfo", "/piaotong/info", "piaotong/index", "User", "piaotong:info:view", 10), + subMenu("piaotong_invoice_issue", "piaotong", "开票服务", "PiaoTongInvoiceIssue", "/piaotong/invoice-issue", "piaotong/invoice-issue/index", "FilePlus", "piaotong:invoice-issue:view", 20), + subMenu("piaotong_invoice_history", "piaotong", "开票历史", "PiaoTongInvoiceHistory", "/piaotong/invoice-history", "piaotong/invoice-history/index", "History", "piaotong:invoice-history:view", 30), ) val idMap = mutableMapOf() @@ -507,7 +201,6 @@ object SeedData { val menuId = upsertMenu(menu, parentId, now) idMap[menu.key] = menuId } - return idMap.values.toList() } @@ -558,16 +251,15 @@ object SeedData { } private suspend fun bindRoleMenus(roleId: Uuid, menuIds: List) = dbQuery { - if (menuIds.isEmpty()) { - return@dbQuery - } + if (menuIds.isEmpty()) return@dbQuery val existing = SysRoleMenuTable.selectAll() .where { SysRoleMenuTable.roleId eq roleId } .map { it[SysRoleMenuTable.menuId] } .toSet() - val toAdd = menuIds.filter { !existing.contains(it) } + val menuIdSet = menuIds.toSet() + val toAdd = menuIds.filter { it !in existing } toAdd.forEach { menuId -> SysRoleMenuTable.insert { it[SysRoleMenuTable.roleId] = roleId @@ -575,33 +267,45 @@ object SeedData { } } - val toRemove = existing.filter { !menuIds.contains(it) } + val toRemove = existing - menuIdSet 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) + private data class SeedDictItem(val label: String, val value: String, val color: String?, val sort: Int) + private suspend fun seedDicts(now: OffsetDateTime) { - val userStatusTypeId = upsertDictType("user_status", "用户状态", now) - upsertDictItem(userStatusTypeId, "启用", "ENABLED", "green", 1, now) - upsertDictItem(userStatusTypeId, "禁用", "DISABLED", "red", 2, now) + val statusItems = listOf( + SeedDictItem("启用", "ENABLED", "green", 1), + SeedDictItem("禁用", "DISABLED", "red", 2), + ) - val orgStatusTypeId = upsertDictType("org_status", "组织状态", now) - upsertDictItem(orgStatusTypeId, "启用", "ENABLED", "green", 1, now) - upsertDictItem(orgStatusTypeId, "禁用", "DISABLED", "red", 2, now) + val dicts = listOf( + SeedDict("user_status", "用户状态", statusItems), + 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) - upsertDictItem(roleStatusTypeId, "启用", "ENABLED", "green", 1, now) - upsertDictItem(roleStatusTypeId, "禁用", "DISABLED", "red", 2, 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) + for (dict in dicts) { + val typeId = upsertDictType(dict.code, dict.name, now) + for (item in dict.items) { + upsertDictItem(typeId, item.label, item.value, item.color, item.sort, now) + } + } } private suspend fun upsertDictType(code: String, name: String, now: OffsetDateTime): Uuid = dbQuery { @@ -613,8 +317,8 @@ object SeedData { val id = existing[SysDictTypeTable.id] SysDictTypeTable.update({ SysDictTypeTable.id eq id }) { it[SysDictTypeTable.name] = name - it[status] = "ENABLED" - it[updatedAt] = now + it[SysDictTypeTable.status] = "ENABLED" + it[SysDictTypeTable.updatedAt] = now } return@dbQuery id } @@ -622,19 +326,14 @@ object SeedData { val inserted = SysDictTypeTable.insert { it[SysDictTypeTable.code] = code it[SysDictTypeTable.name] = name - it[status] = "ENABLED" - it[createdAt] = now + it[SysDictTypeTable.status] = "ENABLED" + it[SysDictTypeTable.createdAt] = now } inserted[SysDictTypeTable.id] } private suspend fun upsertDictItem( - typeId: Uuid, - label: String, - value: String, - color: String?, - sort: Int, - now: OffsetDateTime, + typeId: Uuid, label: String, value: String, color: String?, sort: Int, now: OffsetDateTime, ) = dbQuery { val existing = SysDictItemTable.selectAll() .where { @@ -645,13 +344,12 @@ object SeedData { .singleOrNull() if (existing != null) { - val id = existing[SysDictItemTable.id] - SysDictItemTable.update({ SysDictItemTable.id eq id }) { + SysDictItemTable.update({ SysDictItemTable.id eq existing[SysDictItemTable.id] }) { it[SysDictItemTable.label] = label it[SysDictItemTable.color] = color it[SysDictItemTable.sort] = sort - it[status] = "ENABLED" - it[updatedAt] = now + it[SysDictItemTable.status] = "ENABLED" + it[SysDictItemTable.updatedAt] = now } return@dbQuery } @@ -662,18 +360,22 @@ object SeedData { it[SysDictItemTable.value] = value it[SysDictItemTable.color] = color it[SysDictItemTable.sort] = sort - it[status] = "ENABLED" - it[createdAt] = now + it[SysDictItemTable.status] = "ENABLED" + it[SysDictItemTable.createdAt] = now } } } +// ========================================================= +// Menu definition helpers +// ========================================================= + private data class SeedMenu( val key: String, val parentKey: String?, val type: String, val title: String, - val name: String, + val name: String?, val path: String?, val component: String?, val icon: String?, @@ -683,3 +385,15 @@ private data class SeedMenu( val keepAlive: Boolean, 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) diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseTaxDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseTaxDao.kt new file mode 100644 index 0000000..0e80059 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseTaxDao.kt @@ -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 + } + } + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt index 0ce821a..f549099 100644 --- a/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt @@ -102,6 +102,10 @@ object UserDao { it[SysUserTable.email] = request.email.trimToNull() it[SysUserTable.avatar] = request.avatar.trimToNull() 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() } } @@ -231,5 +235,9 @@ object UserDao { status = this[SysUserTable.status], statusLabel = statusLabel(this[SysUserTable.status]), roleIds = roleIds, + taxpayerNum = this[SysUserTable.taxpayerNum], + account = this[SysUserTable.taxAccount], + taxPassword = this[SysUserTable.taxPassword], + taxIdentityType = this[SysUserTable.taxIdentityType], ) } diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt new file mode 100644 index 0000000..8710ed3 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt @@ -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) +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceOrderTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceOrderTable.kt new file mode 100644 index 0000000..1bdb347 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceOrderTable.kt @@ -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) +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt index 8b65ce7..1f4b64d 100644 --- a/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysUserTable.kt @@ -11,8 +11,6 @@ object SysUserTable : Table("sys_user") { val username = varchar("username", 50).uniqueIndex() val passwordHash = varchar("password_hash", 255) 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 avatar = text("avatar").nullable() val orgId = uuid("org_id").nullable() @@ -28,8 +26,29 @@ 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() + // 核心业务逻辑字段================= + // 数电账号相关信息 + 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) } diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/InvoiceRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/InvoiceRequest.kt new file mode 100644 index 0000000..735688e --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/InvoiceRequest.kt @@ -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, + + /** + * 差额征税凭证明细 + */ + val variableLevyProofList: List? = null, + + /** + * 订单列表(支持合并开票) + */ + val orderList: List? = 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 +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegister.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegister.kt new file mode 100644 index 0000000..dadb371 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegister.kt @@ -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 +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterInfo.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterInfo.kt new file mode 100644 index 0000000..de4ef2e --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterInfo.kt @@ -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 +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterUserRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterUserRequest.kt new file mode 100644 index 0000000..ef6faad --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterUserRequest.kt @@ -0,0 +1,9 @@ +package com.bbit.ticket.entity.request + +import kotlinx.serialization.Serializable + +@Serializable +data class TaxRegisterUserRequest( + val taxpayerNum: String, + val taxAccount: String +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateDigitalAccountRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateDigitalAccountRequest.kt new file mode 100644 index 0000000..cacf8cb --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateDigitalAccountRequest.kt @@ -0,0 +1,11 @@ +package com.bbit.ticket.entity.request + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateDigitalAccountRequest( + /** 纳税人识别号 / 税号 */ + val taxpayerNum: String = "", + /** 电子税局账号 */ + val taxAccount: String = "" +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateEnterpriseInfoRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateEnterpriseInfoRequest.kt new file mode 100644 index 0000000..e4584e0 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateEnterpriseInfoRequest.kt @@ -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 = "" +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdatePresetDataRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdatePresetDataRequest.kt new file mode 100644 index 0000000..966bcdd --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdatePresetDataRequest.kt @@ -0,0 +1,11 @@ +package com.bbit.ticket.entity.request + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdatePresetDataRequest( + /** 开户银行 */ + val bankName: String = "", + /** 银行账号 */ + val bankAccount: String = "" +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseInfoResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseInfoResponse.kt new file mode 100644 index 0000000..5ac3c2e --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseInfoResponse.kt @@ -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? +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseTaxInfo.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseTaxInfo.kt new file mode 100644 index 0000000..1b7be09 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseTaxInfo.kt @@ -0,0 +1,12 @@ +package com.bbit.ticket.entity.response + +import kotlinx.serialization.Serializable + +@Serializable +data class EnterpriseTaxInfo( + // 纳税人识别号 / 税号(通常是企业在税务系统中的唯一标识) + val taxpayerNum: String, + + // 企业名称(工商注册名称) + val enterpriseName: String +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/EtaxRegisterResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/EtaxRegisterResponse.kt new file mode 100644 index 0000000..4ff1549 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/EtaxRegisterResponse.kt @@ -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 +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceCreateResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceCreateResponse.kt new file mode 100644 index 0000000..1d3b721 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceCreateResponse.kt @@ -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 +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt index 84268a8..fd51907 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/TaxBureauAccountAuthContent.kt @@ -4,72 +4,92 @@ 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, /** - * 纳税人识别号 + * 销售方纳税人识别号 (15-20位) */ 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, + /** + * 是否绑定微信公众号 + * 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, /** - * 电子税局账号 + * 最新风险认证时间 (yyyy-MM-dd HH:mm:ss) */ - val account: String -) \ No newline at end of file + val lastRiskAuthTime: String +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt index 55d9ac7..3588551 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/AuthDto.kt @@ -31,8 +31,15 @@ data class CurrentUserProfile( val username: String, val nickname: String? = null, val realName: String? = null, + val phone: String? = null, + val email: String? = null, val orgId: String? = null, val status: String, + val createdAt: String? = null, + val taxpayerNum: String? = null, + val account: String? = null, + val taxPassword: String? = null, + val taxIdentityType: String? = null, ) @Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt index 6f2a564..ee47923 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/system/UserDto.kt @@ -26,6 +26,10 @@ data class UserDetailResponse( val status: String, val statusLabel: String, val roleIds: List, + val taxpayerNum: String? = null, + val account: String? = null, + val taxPassword: String? = null, + val taxIdentityType: String? = null, ) @Serializable @@ -49,6 +53,10 @@ data class UpdateUserRequest( val email: String? = null, val avatar: String? = null, val orgId: String? = null, + val taxpayerNum: String? = null, + val account: String? = null, + val taxPassword: String? = null, + val taxIdentityType: String? = null, ) @Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt index ae6e6ec..cf38d3e 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt @@ -1,20 +1,33 @@ +@file:OptIn(ExperimentalUuidApi::class) + 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.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.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.PTConfigService import com.bbit.ticket.utils.requireCurrentUser +import io.ktor.http.HttpStatusCode import io.ktor.server.auth.authenticate +import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.put import io.ktor.server.routing.route -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromJsonElement +import kotlin.uuid.ExperimentalUuidApi fun Route.registerPTTestRoutes() { route("/pt") { @@ -22,8 +35,8 @@ fun Route.registerPTTestRoutes() { get("/info") { try { val currentUser = call.requireCurrentUser() - val taxpayerNum = currentUser.taxpayerNum ?: Global.testTaxpayerNum - val account = currentUser.account ?: Global.testAccount + val taxpayerNum = currentUser.taxPayerNum ?: "" + val account = currentUser.taxAccount ?: "" val response = PTAuthService.getTaxBureauAccountAuthStatus( TaxBureauAuthReq(taxpayerNum, account) ) @@ -38,6 +51,136 @@ fun Route.registerPTTestRoutes() { ) } } + post("/register") { + try { + val currentUser = call.requireCurrentUser() + val req = call.receive() + 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() + 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())) + } 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() + 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())) + } 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() + 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())) + } 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() + 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() + 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 + ) + ) + } + } } } } diff --git a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt index 62d7d4e..49a9ee7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTAuthService.kt @@ -1,24 +1,89 @@ +@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.InvoiceRequest 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.plugins.dbQuery +import com.bbit.ticket.utils.CurrentUser import com.bbit.ticket.utils.net.PTClient +import kotlin.uuid.Uuid object PTAuthService { /** * 查询数电账号认证状态 - * 此接口用来查询数电账号的认证状态,会返回当 - * - * @param taxpayerNum 纳税人识别号 - * @param account 账号 */ - suspend fun getTaxBureauAccountAuthStatus(req : TaxBureauAuthReq): String { - val res = PTClient.ptPost( + suspend fun getTaxBureauAccountAuthStatus(req: TaxBureauAuthReq): TaxBureauAccountAuthContent { + val res = PTClient.ptPost( url = "getTaxBureauAccountAuthStatus.pt", body = req ) - println("res = $res") - return res.taxpayerNum + return res + } + + /** + * 登记/删除 数电账号 + */ + /** + * 登记/删除 数电账号(前端弹窗传入 taxpayerNum + taxAccount) + */ + suspend fun registerUserFromPayload(req: TaxRegisterUserRequest, currentUser: CurrentUser): String { + val res = PTClient.ptPost( + 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( + url = "register.pt", + body = req + ) + dbQuery { EnterpriseTaxDao.updateEnterpriseInfo(userId, req) } + return "操作成功,企业状态为审核中(待审核)" + } + /** + * 蓝票接口调用 + */ + suspend fun invoiceBlue(req: InvoiceRequest, userId: Uuid): String { + PTClient.ptPost( + url = "invoiceBlue.pt", + body = req + ) + dbQuery { EnterpriseTaxDao.addInvoice(userId, req) } + return "操作成功,企业状态为审核中(待审核)" + } + /** + * 红票接口调用 + */ + suspend fun invoiceRed(req: InvoiceRequest, userId: Uuid): String { + PTClient.ptPost( + url = "invoiceBlue.pt", + body = req + ) + dbQuery { EnterpriseTaxDao.addInvoice(userId, req) } + return "操作成功,企业状态为审核中(待审核)" } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTConfigService.kt b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTConfigService.kt new file mode 100644 index 0000000..0907da3 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTConfigService.kt @@ -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 "预设数据保存成功" + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt index 89a61b0..176980e 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/AuthService.kt @@ -70,8 +70,15 @@ object AuthService { username = userRow[SysUserTable.username], nickname = userRow[SysUserTable.nickname], realName = userRow[SysUserTable.realName], + phone = userRow[SysUserTable.phone], + email = userRow[SysUserTable.email], orgId = userRow[SysUserTable.orgId]?.toString(), 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, permissions = permissions, diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt index 8625e29..bdcd407 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt @@ -9,7 +9,6 @@ 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 @@ -32,8 +31,12 @@ data class CurrentUser( val tokenVersion: Int, val roleCodes: Set, val permissions: Set, - val taxpayerNum: String?, - val account: String?, + val taxPayerNum: String?, + val taxAccount: String?, + val phone: String?, + val realName: String?, + val taxPassword: String?, + val taxIdentityType: String?, ) { val isSuperAdmin: Boolean get() = roleCodes.contains("SUPER_ADMIN") @@ -138,8 +141,12 @@ suspend fun ApplicationCall.requireCurrentUser(): CurrentUser { tokenVersion = userRow[SysUserTable.tokenVersion], roleCodes = roleCodes, permissions = permissions, - taxpayerNum = userRow[SysUserTable.taxpayerNum], - account = userRow[SysUserTable.account], + taxPayerNum = userRow[SysUserTable.taxpayerNum], + taxAccount = userRow[SysUserTable.taxAccount], + taxPassword = userRow[SysUserTable.taxPassword], + taxIdentityType = userRow[SysUserTable.taxIdentityType], + phone = userRow[SysUserTable.phone], + realName = userRow[SysUserTable.realName], ) attributes.put(CurrentUserKey, currentUser) diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt b/server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt index 659c900..5762ea3 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/net/PTClient.kt @@ -12,6 +12,7 @@ import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonObject @@ -25,6 +26,7 @@ object PTClient { install(ContentNegotiation) { json( Json { + explicitNulls = false ignoreUnknownKeys = true prettyPrint = true isLenient = true @@ -82,18 +84,12 @@ object PTClient { val response = client.post(Global.baseUrl + url) { contentType(ContentType.Application.Json) - - headers.forEach { (k, v) -> - header(k, v) - } - + headers.forEach { (k, v) -> header(k, v) } setBody(buildRequestData(Json.encodeToString(body))) }.bodyAsText() val decrypted = disposeResponse(response) - val result = Json.decodeFromString>(decrypted) - if (result.code != "0000") { throw PTException( code = result.code, @@ -102,7 +98,8 @@ object PTClient { ) } - return Json.decodeFromJsonElement(result.content!!) + println("res = $result.content") + return Json.decodeFromJsonElement(result.content!!) } /** @@ -124,7 +121,7 @@ object PTClient { map["version"] = "1.0" map["content"] = reqContent map["timestamp"] = sdf.format(Date()) - map["serialNo"] = ptDate(Global.ptPlatformAlias) + map["serialNo"] = ptDate() map["sign"] = RSAUtil.sign(RSAUtil.getSignatureContent(map), Global.ptPrivateKey) ?: "" return Json.encodeToString(map) } @@ -157,26 +154,34 @@ object PTClient { val encryptedContent = mutableMap["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 { json.forEach { (k, v) -> if (k == "content") { - put("content", Json.parseToJsonElement(plainContent)) + put("content", contentElement) } else { put(k, v) } } } - return resultJson.toString() } - fun ptDate(prefix: String?): String { + fun ptDate(): String { val date = Date() 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) return str } diff --git a/web/src/api/http.ts b/web/src/api/http.ts index b3bda0f..04f96be 100644 --- a/web/src/api/http.ts +++ b/web/src/api/http.ts @@ -30,7 +30,9 @@ http.interceptors.response.use( return response.data } 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 }, diff --git a/web/src/api/piaotong/index.ts b/web/src/api/piaotong/index.ts new file mode 100644 index 0000000..8de337c --- /dev/null +++ b/web/src/api/piaotong/index.ts @@ -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 { + 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 { + return http.post('/pt/register', payload) +} + +export interface TaxRegisterUserRequest { + taxpayerNum: string + taxAccount: string +} + +/** + * 登记账号 + */ +export function registerUserApi(payload: TaxRegisterUserRequest): Promise { + 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 { + return http.get('/pt/enterprise') +} + +/** 更新企业信息 */ +export function updateEnterpriseInfoApi(payload: Partial): Promise { + return http.put('/pt/enterprise', payload) +} + +/** 数电账号信息 */ +export interface DigitalAccountInfo { + taxpayerNum: string + taxAccount: string +} + +/** 获取数电账号信息 */ +export function getDigitalAccountApi(): Promise { + return http.get('/pt/digital-account') +} + +/** 更新数电账号信息 */ +export function updateDigitalAccountApi(payload: DigitalAccountInfo): Promise { + return http.put('/pt/digital-account', payload) +} + +/** 开票预设数据 */ +export interface PresetData { + bankName: string + bankAccount: string +} + +/** 获取开票预设数据 */ +export function getPresetDataApi(): Promise { + return http.get('/pt/preset') +} + +/** 更新开票预设数据 */ +export function updatePresetDataApi(payload: PresetData): Promise { + 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 { + return http.post('/pt/invoiceBlue', payload) +} diff --git a/web/src/features/piaotong/index.vue b/web/src/features/piaotong/index.vue new file mode 100644 index 0000000..b2087b0 --- /dev/null +++ b/web/src/features/piaotong/index.vue @@ -0,0 +1,1360 @@ + + + + + diff --git a/web/src/features/piaotong/invoice-history/index.vue b/web/src/features/piaotong/invoice-history/index.vue new file mode 100644 index 0000000..11d467b --- /dev/null +++ b/web/src/features/piaotong/invoice-history/index.vue @@ -0,0 +1,35 @@ + + + diff --git a/web/src/features/piaotong/invoice-issue/index.vue b/web/src/features/piaotong/invoice-issue/index.vue new file mode 100644 index 0000000..d215649 --- /dev/null +++ b/web/src/features/piaotong/invoice-issue/index.vue @@ -0,0 +1,1422 @@ + + + + + diff --git a/web/src/types/auth.ts b/web/src/types/auth.ts index 03ca33c..5df8ae9 100644 --- a/web/src/types/auth.ts +++ b/web/src/types/auth.ts @@ -16,6 +16,14 @@ export interface CurrentUserProfile { realName?: string | null orgId?: string | null 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 { diff --git a/web/src/types/system/user.ts b/web/src/types/system/user.ts index 3b11d2a..81708a1 100644 --- a/web/src/types/system/user.ts +++ b/web/src/types/system/user.ts @@ -23,6 +23,10 @@ export interface UserDetail { status: string statusLabel?: string roleIds: string[] + taxpayerNum?: string | null + account?: string | null + taxPassword?: string | null + taxIdentityType?: string | null } export interface UserQuery { @@ -55,6 +59,10 @@ export interface UpdateUserRequest { email?: string avatar?: string orgId?: string + taxpayerNum?: string + account?: string + taxPassword?: string + taxIdentityType?: string } export interface UpdateUserStatusRequest {