diff --git a/doc/migration/V20260508__alter_sys_user_tax_columns.sql b/doc/migration/V20260508__alter_sys_user_tax_columns.sql deleted file mode 100644 index 5fe3c48..0000000 --- a/doc/migration/V20260508__alter_sys_user_tax_columns.sql +++ /dev/null @@ -1,8 +0,0 @@ --- 扩展现有字段长度,适配票通接口校验规则和 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/doc/migration/V20260519__add_sys_user_preset_contact_columns.sql b/doc/migration/V20260519__add_sys_user_preset_contact_columns.sql deleted file mode 100644 index 8042812..0000000 --- a/doc/migration/V20260519__add_sys_user_preset_contact_columns.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE sys_user - ADD COLUMN IF NOT EXISTS preset_address VARCHAR(200), - ADD COLUMN IF NOT EXISTS preset_phone VARCHAR(32); diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/BlueInvoiceDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/BlueInvoiceDao.kt index 99d13c2..08523c9 100644 --- a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/BlueInvoiceDao.kt +++ b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/BlueInvoiceDao.kt @@ -61,6 +61,8 @@ object BlueInvoiceDao { */ fun invoiceHistory( userId: Uuid, + enterpriseId: Uuid?, + digitalAccountId: Uuid?, page: Int, pageSize: Int, invoiceType: String? = null, @@ -68,7 +70,13 @@ object BlueInvoiceDao { batchNo: String? = null, ): PageResult { val conditions = mutableListOf>() - conditions.add(HistoryInvoiceBasicTable.userId eq userId) + if (digitalAccountId != null) { + conditions.add(HistoryInvoiceBasicTable.digitalAccountId eq digitalAccountId) + } else if (enterpriseId != null) { + conditions.add(HistoryInvoiceBasicTable.enterpriseId eq enterpriseId) + } else { + conditions.add(HistoryInvoiceBasicTable.userId eq userId) + } conditions.add(HistoryInvoiceBasicTable.deletedAt.isNull()) // 发票类型筛选:前端传 BLUE/RED,库中存 1/2 @@ -147,12 +155,14 @@ object BlueInvoiceDao { return PageResult(items, page, pageSize, total) } - fun addInvoice(userId: Uuid, req: AskInvoiceRequest) { + fun addInvoice(userId: Uuid, req: AskInvoiceRequest, enterpriseId: Uuid?, digitalAccountId: Uuid?) { val now = OffsetDateTime.now() // 1. 插入 HistoryInvoiceBasicTable(基本信息历史快照) val basicRow = HistoryInvoiceBasicTable.insert { it[HistoryInvoiceBasicTable.userId] = userId + it[HistoryInvoiceBasicTable.enterpriseId] = enterpriseId + it[HistoryInvoiceBasicTable.digitalAccountId] = digitalAccountId it[HistoryInvoiceBasicTable.invoiceReqSerialNo] = req.invoiceReqSerialNo // ---- 状态 ---- @@ -295,7 +305,7 @@ object BlueInvoiceDao { * - 记录不存在 → 插入(由查询结果反推基础数据) * - 商品明细采用先删后插策略 */ - fun upsertInvoiceInfo(userId: Uuid, req: GetInvoiceInfoResponse) { + fun upsertInvoiceInfo(userId: Uuid, req: GetInvoiceInfoResponse, enterpriseId: Uuid?, digitalAccountId: Uuid?) { transaction { val now = OffsetDateTime.now() val existingId = HistoryInvoiceBasicTable @@ -307,6 +317,8 @@ object BlueInvoiceDao { val invoiceId = if (existingId != null) { // 更新已有记录 HistoryInvoiceBasicTable.update({ HistoryInvoiceBasicTable.id eq existingId }) { + if (enterpriseId != null) it[HistoryInvoiceBasicTable.enterpriseId] = enterpriseId + if (digitalAccountId != null) it[HistoryInvoiceBasicTable.digitalAccountId] = digitalAccountId applySyncFields(it, req) it[HistoryInvoiceBasicTable.updatedAt] = now } @@ -315,6 +327,8 @@ object BlueInvoiceDao { // 插入新记录 HistoryInvoiceBasicTable.insert { it[HistoryInvoiceBasicTable.userId] = userId + it[HistoryInvoiceBasicTable.enterpriseId] = enterpriseId + it[HistoryInvoiceBasicTable.digitalAccountId] = digitalAccountId it[HistoryInvoiceBasicTable.invoiceReqSerialNo] = req.invoiceReqSerialNo applySyncFields(it, req) it[HistoryInvoiceBasicTable.createdAt] = now @@ -449,6 +463,25 @@ object BlueInvoiceDao { ?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息") } + fun findInvoiceScopeBySerialNo(invoiceReqSerialNo: String): InvoiceScope { + val row = HistoryInvoiceBasicTable.selectAll() + .where { HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo } + .singleOrNull() + ?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在") + return InvoiceScope( + userId = row[HistoryInvoiceBasicTable.userId] + ?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息"), + enterpriseId = row[HistoryInvoiceBasicTable.enterpriseId], + digitalAccountId = row[HistoryInvoiceBasicTable.digitalAccountId], + ) + } + + data class InvoiceScope( + val userId: Uuid, + val enterpriseId: Uuid?, + val digitalAccountId: Uuid?, + ) + fun findRelatedInvoiceReqSerialNos(userId: Uuid, invoiceReqSerialNo: String): List { val basicRow = HistoryInvoiceBasicTable.selectAll() .where { diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseManageDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseManageDao.kt new file mode 100644 index 0000000..1fa0bad --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseManageDao.kt @@ -0,0 +1,303 @@ +@file:OptIn(ExperimentalUuidApi::class) + +package com.bbit.ticket.dao.piaotong + +import com.bbit.ticket.database.piaotong.PtDigitalAccountTable +import com.bbit.ticket.database.piaotong.PtEnterpriseTable +import com.bbit.ticket.database.system.SysApiAccessLogTable +import com.bbit.ticket.database.system.SysUserTable +import com.bbit.ticket.entity.request.TaxRegisterInfo +import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest +import com.bbit.ticket.entity.response.DigitalAccountInfo +import com.bbit.ticket.entity.response.DigitalAccountManageItem +import com.bbit.ticket.entity.response.EnterpriseManageResponse +import com.bbit.ticket.entity.response.OpenApiStatisticsItem +import com.bbit.ticket.entity.response.QueryEnterpriseInfoResponse +import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.SortOrder +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.isNull +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.update +import java.time.OffsetDateTime +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object EnterpriseManageDao { + fun findEnterpriseByTaxpayerNum(taxpayerNum: String): ResultRow? = + PtEnterpriseTable.selectAll() + .where { (PtEnterpriseTable.taxpayerNum eq taxpayerNum) and PtEnterpriseTable.deletedAt.isNull() } + .singleOrNull() + + fun createEnterprise(req: TaxRegisterInfo): Uuid { + val now = OffsetDateTime.now() + return PtEnterpriseTable.insert { + it[taxpayerNum] = req.taxpayerNum.trim() + it[enterpriseName] = req.enterpriseName.trim() + it[legalPersonName] = req.legalPersonName?.trim()?.ifBlank { null } + it[contactsName] = req.contactsName?.trim()?.ifBlank { null } + it[contactsEmail] = req.contactsEmail?.trim()?.ifBlank { null } + it[contactsPhone] = req.contactsPhone?.trim()?.ifBlank { null } + it[regionCode] = req.regionCode.trim().ifBlank { null } + it[cityName] = req.cityName.trim().ifBlank { null } + it[enterpriseAddress] = req.enterpriseAddress?.trim()?.ifBlank { null } + it[taxRegistrationCertificate] = req.taxRegistrationCertificate?.trim()?.ifBlank { null } + it[createdAt] = now + }[PtEnterpriseTable.id] + } + + fun enterpriseDetail(enterpriseId: Uuid): EnterpriseManageResponse? = + PtEnterpriseTable.selectAll() + .where { (PtEnterpriseTable.id eq enterpriseId) and PtEnterpriseTable.deletedAt.isNull() } + .singleOrNull() + ?.toEnterpriseResponse() + + fun updateEnterpriseFromPt(enterpriseId: Uuid, res: QueryEnterpriseInfoResponse) { + PtEnterpriseTable.update({ PtEnterpriseTable.id eq enterpriseId }) { + it[taxpayerNum] = res.taxpayerNum + it[enterpriseName] = res.enterpriseName + it[regionCode] = res.regionCode + it[cityName] = res.cityName + it[enterpriseAddress] = res.enterpriseAddress + it[invitationCode] = res.invitationCode + it[reviewStatus] = res.reviewStatus + it[reviewOpinion] = res.reviewOpinion + it[invoiceKind] = res.invoiceKind + it[invoiceLayoutFileType] = res.invoiceLayoutFileType + it[serviceStatus] = res.serviceStatus + it[updatedAt] = OffsetDateTime.now() + } + } + + fun updateInvoiceSetting(enterpriseId: Uuid, req: UpdateInvoiceSettingRequest) { + PtEnterpriseTable.update({ PtEnterpriseTable.id eq enterpriseId }) { + it[bankName] = req.bankName.trim().ifBlank { null } + it[bankAccount] = req.bankAccount.trim().ifBlank { null } + it[presetAddress] = req.address.trim().ifBlank { null } + it[presetPhone] = req.phone.trim().ifBlank { null } + it[updatedAt] = OffsetDateTime.now() + } + } + + fun digitalAccountsForEnterprise(enterpriseId: Uuid): List = + PtDigitalAccountTable.selectAll() + .where { (PtDigitalAccountTable.enterpriseId eq enterpriseId) and PtDigitalAccountTable.deletedAt.isNull() } + .orderBy(PtDigitalAccountTable.createdAt, SortOrder.DESC) + .map { it.toDigitalAccountItem() } + + fun digitalAccount(id: Uuid): ResultRow? = + PtDigitalAccountTable.selectAll() + .where { (PtDigitalAccountTable.id eq id) and PtDigitalAccountTable.deletedAt.isNull() } + .singleOrNull() + + fun digitalAccountByApiKey(apiKey: String): ResultRow? = + PtDigitalAccountTable.selectAll() + .where { + (PtDigitalAccountTable.apiKey eq apiKey) and + (PtDigitalAccountTable.status eq "ENABLED") and + PtDigitalAccountTable.deletedAt.isNull() + } + .singleOrNull() + + fun digitalAccountForUser(userId: Uuid): ResultRow? = + PtDigitalAccountTable.selectAll() + .where { + (PtDigitalAccountTable.platformUserId eq userId) and + (PtDigitalAccountTable.status eq "ENABLED") and + PtDigitalAccountTable.deletedAt.isNull() + } + .singleOrNull() + + fun upsertDigitalAccount(enterpriseId: Uuid, item: DigitalAccountInfo): Uuid { + val existing = PtDigitalAccountTable.selectAll() + .where { + (PtDigitalAccountTable.enterpriseId eq enterpriseId) and + (PtDigitalAccountTable.account eq item.account) and + PtDigitalAccountTable.deletedAt.isNull() + } + .singleOrNull() + + val now = OffsetDateTime.now() + if (existing != null) { + val id = existing[PtDigitalAccountTable.id] + PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq id }) { + it[taxpayerNum] = item.taxpayerNum + it[name] = item.name + it[identityType] = item.identityType + it[operationProposed] = item.operationProposed + it[authStatus] = item.authStatus + it[switchable] = item.switchable + it[wechatUserBindStatus] = item.wechatUserBindStatus + it[lastAuthSuccTime] = item.lastAuthSuccTime + it[loginAuthStatus] = item.loginAuthStatus + it[lastLoginAuthTime] = item.lastLoginAuthTime + it[riskAuthStatus] = item.riskAuthStatus + it[lastRiskAuthTime] = item.lastRiskAuthTime + it[updatedAt] = now + } + return id + } + + return PtDigitalAccountTable.insert { + it[PtDigitalAccountTable.enterpriseId] = enterpriseId + it[taxpayerNum] = item.taxpayerNum + it[account] = item.account + it[name] = item.name + it[identityType] = item.identityType + it[operationProposed] = item.operationProposed + it[authStatus] = item.authStatus + it[switchable] = item.switchable + it[wechatUserBindStatus] = item.wechatUserBindStatus + it[lastAuthSuccTime] = item.lastAuthSuccTime + it[loginAuthStatus] = item.loginAuthStatus + it[lastLoginAuthTime] = item.lastLoginAuthTime + it[riskAuthStatus] = item.riskAuthStatus + it[lastRiskAuthTime] = item.lastRiskAuthTime + it[createdAt] = now + }[PtDigitalAccountTable.id] + } + + fun createDigitalAccount( + enterpriseId: Uuid, + taxpayerNum: String, + account: String, + name: String, + identityType: String, + platformUserId: Uuid, + apiKey: String, + ): Uuid { + val existing = PtDigitalAccountTable.selectAll() + .where { + (PtDigitalAccountTable.enterpriseId eq enterpriseId) and + (PtDigitalAccountTable.account eq account) and + PtDigitalAccountTable.deletedAt.isNull() + } + .singleOrNull() + + val now = OffsetDateTime.now() + if (existing != null) { + val id = existing[PtDigitalAccountTable.id] + PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq id }) { + it[PtDigitalAccountTable.name] = name + it[PtDigitalAccountTable.identityType] = identityType + it[PtDigitalAccountTable.platformUserId] = platformUserId + it[PtDigitalAccountTable.apiKey] = apiKey + it[updatedAt] = now + } + return id + } + + return PtDigitalAccountTable.insert { + it[PtDigitalAccountTable.enterpriseId] = enterpriseId + it[PtDigitalAccountTable.taxpayerNum] = taxpayerNum + it[PtDigitalAccountTable.account] = account + it[PtDigitalAccountTable.name] = name + it[PtDigitalAccountTable.identityType] = identityType + it[PtDigitalAccountTable.platformUserId] = platformUserId + it[PtDigitalAccountTable.apiKey] = apiKey + it[PtDigitalAccountTable.createdAt] = now + }[PtDigitalAccountTable.id] + } + + fun bindDigitalAccountUser(digitalAccountId: Uuid, userId: Uuid) { + PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq digitalAccountId }) { + it[platformUserId] = userId + it[updatedAt] = OffsetDateTime.now() + } + SysUserTable.update({ SysUserTable.id eq userId }) { + it[SysUserTable.digitalAccountId] = digitalAccountId + it[SysUserTable.updatedAt] = OffsetDateTime.now() + } + } + + fun bindDigitalAccountUserAndApiKey(digitalAccountId: Uuid, userId: Uuid, apiKey: String) { + PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq digitalAccountId }) { + it[platformUserId] = userId + it[PtDigitalAccountTable.apiKey] = apiKey + it[updatedAt] = OffsetDateTime.now() + } + SysUserTable.update({ SysUserTable.id eq userId }) { + it[SysUserTable.digitalAccountId] = digitalAccountId + it[SysUserTable.updatedAt] = OffsetDateTime.now() + } + } + + fun openApiStatistics(enterpriseId: Uuid, digitalAccountId: Uuid?): List { + var where = SysApiAccessLogTable.enterpriseId eq enterpriseId + if (digitalAccountId != null) { + where = where and (SysApiAccessLogTable.digitalAccountId eq digitalAccountId) + } + + return SysApiAccessLogTable + .selectAll() + .where { where } + .toList() + .groupBy { row -> row[SysApiAccessLogTable.digitalAccountId] to row[SysApiAccessLogTable.interfaceCode] } + .map { (key, rows) -> + OpenApiStatisticsItem( + digitalAccountId = key.first?.toString(), + interfaceCode = key.second, + total = rows.size.toLong(), + success = rows.count { it[SysApiAccessLogTable.status] == "SUCCESS" }.toLong(), + failed = rows.count { it[SysApiAccessLogTable.status] != "SUCCESS" }.toLong(), + avgCostMs = rows.map { it[SysApiAccessLogTable.costMs] }.average().toLong(), + lastCalledAt = rows.maxByOrNull { it[SysApiAccessLogTable.createdAt] } + ?.get(SysApiAccessLogTable.createdAt) + ?.toString(), + ) + } + } + + private fun ResultRow.toEnterpriseResponse() = EnterpriseManageResponse( + id = this[PtEnterpriseTable.id].toString(), + taxpayerNum = this[PtEnterpriseTable.taxpayerNum], + enterpriseName = this[PtEnterpriseTable.enterpriseName], + legalPersonName = this[PtEnterpriseTable.legalPersonName], + contactsName = this[PtEnterpriseTable.contactsName], + contactsEmail = this[PtEnterpriseTable.contactsEmail], + contactsPhone = this[PtEnterpriseTable.contactsPhone], + regionCode = this[PtEnterpriseTable.regionCode], + cityName = this[PtEnterpriseTable.cityName], + enterpriseAddress = this[PtEnterpriseTable.enterpriseAddress], + taxRegistrationCertificate = this[PtEnterpriseTable.taxRegistrationCertificate], + invitationCode = this[PtEnterpriseTable.invitationCode], + reviewStatus = this[PtEnterpriseTable.reviewStatus], + reviewOpinion = this[PtEnterpriseTable.reviewOpinion], + invoiceKind = this[PtEnterpriseTable.invoiceKind], + invoiceLayoutFileType = this[PtEnterpriseTable.invoiceLayoutFileType], + serviceStatus = this[PtEnterpriseTable.serviceStatus], + bankName = this[PtEnterpriseTable.bankName], + bankAccount = this[PtEnterpriseTable.bankAccount], + presetAddress = this[PtEnterpriseTable.presetAddress], + presetPhone = this[PtEnterpriseTable.presetPhone], + ) + + fun ResultRow.toDigitalAccountItem() = DigitalAccountManageItem( + id = this[PtDigitalAccountTable.id].toString(), + enterpriseId = this[PtDigitalAccountTable.enterpriseId].toString(), + taxpayerNum = this[PtDigitalAccountTable.taxpayerNum], + account = this[PtDigitalAccountTable.account], + name = this[PtDigitalAccountTable.name], + identityType = this[PtDigitalAccountTable.identityType], + operationProposed = this[PtDigitalAccountTable.operationProposed], + authStatus = this[PtDigitalAccountTable.authStatus], + switchable = this[PtDigitalAccountTable.switchable], + wechatUserBindStatus = this[PtDigitalAccountTable.wechatUserBindStatus], + lastAuthSuccTime = this[PtDigitalAccountTable.lastAuthSuccTime], + loginAuthStatus = this[PtDigitalAccountTable.loginAuthStatus], + lastLoginAuthTime = this[PtDigitalAccountTable.lastLoginAuthTime], + riskAuthStatus = this[PtDigitalAccountTable.riskAuthStatus], + lastRiskAuthTime = this[PtDigitalAccountTable.lastRiskAuthTime], + platformUserId = this[PtDigitalAccountTable.platformUserId]?.toString(), + platformUsername = this[PtDigitalAccountTable.platformUserId]?.let { platformUserId -> + SysUserTable.selectAll() + .where { (SysUserTable.id eq platformUserId) and SysUserTable.deletedAt.isNull() } + .singleOrNull() + ?.get(SysUserTable.username) + }, + apiKey = this[PtDigitalAccountTable.apiKey], + status = this[PtDigitalAccountTable.status], + ) +} 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 deleted file mode 100644 index 569dc03..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseTaxDao.kt +++ /dev/null @@ -1,144 +0,0 @@ -@file:OptIn(ExperimentalUuidApi::class) - -package com.bbit.ticket.dao.piaotong - -import com.bbit.ticket.database.piaotong.HistoryInvoiceOrderTable -import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable -import com.bbit.ticket.database.system.SysUserTable -import com.bbit.ticket.entity.common.PageResult -import com.bbit.ticket.entity.request.AskInvoiceRequest -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.InvoiceDetailGoods -import com.bbit.ticket.entity.response.InvoiceDetailOrder -import com.bbit.ticket.entity.response.InvoiceDetailResponse -import com.bbit.ticket.entity.response.InvoiceDetailVoucher -import com.bbit.ticket.entity.response.InvoiceHistoryItem -import com.bbit.ticket.entity.response.PresetDataResponse -import com.bbit.ticket.utils.formatDateTime -import org.jetbrains.exposed.v1.core.SortOrder -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update -import java.math.BigDecimal -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter -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], - address = row[SysUserTable.presetAddress], - phone = row[SysUserTable.presetPhone] - ) - } - - 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.presetAddress] = req.address.trim().ifBlank { null } - it[SysUserTable.presetPhone] = req.phone.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() - } - } - -} diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/RedInvoiceDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/RedInvoiceDao.kt index 719c300..f06ccb4 100644 --- a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/RedInvoiceDao.kt +++ b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/RedInvoiceDao.kt @@ -32,6 +32,8 @@ object RedInvoiceDao { // 2. 插入红票历史记录(用于开票历史列表展示) HistoryInvoiceBasicTable.insert { it[HistoryInvoiceBasicTable.userId] = userId + it[HistoryInvoiceBasicTable.enterpriseId] = blueRow?.get(HistoryInvoiceBasicTable.enterpriseId) + it[HistoryInvoiceBasicTable.digitalAccountId] = blueRow?.get(HistoryInvoiceBasicTable.digitalAccountId) it[HistoryInvoiceBasicTable.invoiceReqSerialNo] = req.invoiceReqSerialNo // ---- 状态 ---- diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/LogDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/LogDao.kt index 9ac9011..ee4047e 100644 --- a/server/src/main/kotlin/com/bbit/ticket/dao/system/LogDao.kt +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/LogDao.kt @@ -24,6 +24,7 @@ import org.jetbrains.exposed.v1.jdbc.insert import org.jetbrains.exposed.v1.jdbc.selectAll import java.time.OffsetDateTime import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid object LogDao { private const val MAX_API_LOG_BODY_LENGTH = 20_000 @@ -132,6 +133,9 @@ object LogDao { call: ApplicationCall, appKey: String?, appName: String?, + enterpriseId: Uuid?, + digitalAccountId: Uuid?, + interfaceCode: String?, requestBody: String?, responseCode: String?, responseBody: String?, @@ -143,6 +147,10 @@ object LogDao { it[traceId] = call.traceIdOrNull() it[SysApiAccessLogTable.appKey] = appKey?.take(100) it[SysApiAccessLogTable.appName] = appName?.take(100) + it[SysApiAccessLogTable.enterpriseId] = enterpriseId + it[SysApiAccessLogTable.digitalAccountId] = digitalAccountId + it[SysApiAccessLogTable.apiKey] = appKey?.take(128) + it[SysApiAccessLogTable.interfaceCode] = interfaceCode?.take(100) it[httpMethod] = call.request.httpMethod.value it[requestPath] = call.request.path().take(255) it[requestHeaders] = null 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 c317637..ad7fb31 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 @@ -13,7 +13,6 @@ import com.bbit.ticket.entity.common.ErrorCode import com.bbit.ticket.entity.common.PageResult import com.bbit.ticket.entity.common.statusLabel import com.bbit.ticket.entity.common.system.UserRoleBrief -import com.bbit.ticket.utils.ApiKeyUtil import io.ktor.http.HttpStatusCode import org.jetbrains.exposed.v1.core.Op import org.jetbrains.exposed.v1.core.ResultRow @@ -82,13 +81,39 @@ object UserDao { it[SysUserTable.avatar] = request.avatar.trimToNull() it[SysUserTable.orgId] = orgId it[SysUserTable.status] = request.status - it[SysUserTable.apiKey] = ApiKeyUtil.generate() it[SysUserTable.tokenVersion] = 1 it[SysUserTable.createdAt] = now } return row[SysUserTable.id].toString() } + fun createPlatformUser( + username: String, + passwordHash: String, + nickname: String?, + realName: String?, + phone: String?, + enterpriseId: Uuid?, + digitalAccountId: Uuid?, + userType: String, + ): Uuid { + val now = OffsetDateTime.now() + val row = SysUserTable.insert { + it[SysUserTable.username] = username.trim() + it[SysUserTable.passwordHash] = passwordHash + it[SysUserTable.nickname] = nickname?.trimToNull() + it[SysUserTable.realName] = realName?.trimToNull() + it[SysUserTable.phone] = phone?.trimToNull() + it[SysUserTable.enterpriseId] = enterpriseId + it[SysUserTable.digitalAccountId] = digitalAccountId + it[SysUserTable.userType] = userType + it[SysUserTable.status] = "ENABLED" + it[SysUserTable.tokenVersion] = 1 + it[SysUserTable.createdAt] = now + } + return row[SysUserTable.id] + } + fun detail(id: Uuid): UserDetailResponse { val user = requireActive(id) val roles = (SysUserRoleTable innerJoin SysRoleTable) @@ -121,10 +146,6 @@ 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() } } @@ -168,6 +189,13 @@ object UserDao { SysRoleTable.deletedAt.isNull() }.count() + fun findEnabledRoleIdByCode(code: String): Uuid? = + SysRoleTable.selectAll().where { + (SysRoleTable.code eq code) and + (SysRoleTable.status eq "ENABLED") and + SysRoleTable.deletedAt.isNull() + }.singleOrNull()?.get(SysRoleTable.id) + fun orgExists(orgId: Uuid): Boolean = SysOrgTable.selectAll().where { (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() @@ -240,7 +268,6 @@ object UserDao { status = this[SysUserTable.status], statusLabel = statusLabel(this[SysUserTable.status]), roleCodes = roleCodes, - apiKey = this[SysUserTable.apiKey], ) private fun ResultRow.toUserDetail( @@ -262,7 +289,6 @@ object UserDao { statusLabel = statusLabel(this[SysUserTable.status]), roleIds = roleIds, roles = roles, - apiKey = this[SysUserTable.apiKey], tokenVersion = this[SysUserTable.tokenVersion], lastLoginAt = this[SysUserTable.lastLoginAt]?.toString(), lastLoginIp = this[SysUserTable.lastLoginIp], @@ -273,21 +299,5 @@ object UserDao { deletedAt = this[SysUserTable.deletedAt]?.toString(), deletedBy = this[SysUserTable.deletedBy]?.toString(), version = this[SysUserTable.version], - taxpayerNum = this[SysUserTable.taxpayerNum], - account = this[SysUserTable.taxAccount], - taxIdentityType = this[SysUserTable.taxIdentityType], - taxContactName = this[SysUserTable.taxContactName], - taxContactPhone = this[SysUserTable.taxContactPhone], - taxContactEmail = this[SysUserTable.taxContactEmail], - taxLegalPersonName = this[SysUserTable.taxLegalPersonName], - taxEnterpriseName = this[SysUserTable.taxEnterpriseName], - taxRegionCode = this[SysUserTable.taxRegionCode], - taxCityName = this[SysUserTable.taxCityName], - taxEnterpriseAddress = this[SysUserTable.taxEnterpriseAddress], - taxRegistrationCertificate = this[SysUserTable.taxRegistrationCertificate], - bankName = this[SysUserTable.bankName], - bankAccount = this[SysUserTable.bankAccount], - presetAddress = this[SysUserTable.presetAddress], - presetPhone = this[SysUserTable.presetPhone], ) } diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceBasicTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceBasicTable.kt index 6f8f403..7d341c3 100644 --- a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceBasicTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceBasicTable.kt @@ -25,6 +25,12 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") { val userId = uuid("user_id") .nullable() + val enterpriseId = uuid("enterprise_id") + .nullable() + + val digitalAccountId = uuid("digital_account_id") + .nullable() + // ---------------------------------------------------------------- // 发票请求信息 // ---------------------------------------------------------------- @@ -446,4 +452,4 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") { val updatedAt = timestampWithTimeZone("updated_at").nullable() override val primaryKey = PrimaryKey(id) -} \ No newline at end of file +} diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/OpenInvoiceBatchTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/OpenInvoiceBatchTable.kt index 710574a..c7d9844 100644 --- a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/OpenInvoiceBatchTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/OpenInvoiceBatchTable.kt @@ -9,6 +9,8 @@ import kotlin.uuid.Uuid object OpenInvoiceBatchTable : Table("open_invoice_batch") { val id = uuid("id").clientDefault { Uuid.random() } val userId = uuid("user_id") + val enterpriseId = uuid("enterprise_id").nullable() + val digitalAccountId = uuid("digital_account_id").nullable() val batchNo = varchar("batch_no", 64) val totalCount = integer("total_count").default(0) val successCount = integer("success_count").default(0) diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/PtDigitalAccountTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/PtDigitalAccountTable.kt new file mode 100644 index 0000000..d23d2a5 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/PtDigitalAccountTable.kt @@ -0,0 +1,42 @@ +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 PtDigitalAccountTable : Table("pt_digital_account") { + val id = uuid("id").clientDefault { Uuid.random() } + val enterpriseId = uuid("enterprise_id").references(PtEnterpriseTable.id) + val taxpayerNum = varchar("taxpayer_num", 50) + val account = varchar("account", 100) + val name = varchar("name", 50).nullable() + val identityType = varchar("identity_type", 20).nullable() + val operationProposed = varchar("operation_proposed", 20).nullable() + val authStatus = varchar("auth_status", 20).nullable() + val switchable = varchar("switchable", 20).nullable() + val wechatUserBindStatus = varchar("wechat_user_bind_status", 20).nullable() + val lastAuthSuccTime = varchar("last_auth_succ_time", 32).nullable() + val loginAuthStatus = varchar("login_auth_status", 20).nullable() + val lastLoginAuthTime = varchar("last_login_auth_time", 32).nullable() + val riskAuthStatus = varchar("risk_auth_status", 20).nullable() + val lastRiskAuthTime = varchar("last_risk_auth_time", 32).nullable() + + val platformUserId = uuid("platform_user_id").nullable() + val apiKey = varchar("api_key", 128).nullable().uniqueIndex() + val status = varchar("status", 20).default("ENABLED") + 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() + val deletedBy = uuid("deleted_by").nullable() + val version = integer("version").default(1) + + override val primaryKey = PrimaryKey(id) + + init { + uniqueIndex(enterpriseId, account) + } +} diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/PtEnterpriseTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/PtEnterpriseTable.kt new file mode 100644 index 0000000..890a8f0 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/PtEnterpriseTable.kt @@ -0,0 +1,43 @@ +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 PtEnterpriseTable : Table("pt_enterprise") { + val id = uuid("id").clientDefault { Uuid.random() } + val taxpayerNum = varchar("taxpayer_num", 50).uniqueIndex() + val enterpriseName = varchar("enterprise_name", 200) + val legalPersonName = varchar("legal_person_name", 50).nullable() + val contactsName = varchar("contacts_name", 50).nullable() + val contactsEmail = varchar("contacts_email", 100).nullable() + val contactsPhone = varchar("contacts_phone", 32).nullable() + val regionCode = varchar("region_code", 32).nullable() + val cityName = varchar("city_name", 50).nullable() + val enterpriseAddress = varchar("enterprise_address", 200).nullable() + val taxRegistrationCertificate = text("tax_registration_certificate").nullable() + + val invitationCode = varchar("invitation_code", 20).nullable() + val reviewStatus = varchar("review_status", 20).nullable() + val reviewOpinion = varchar("review_opinion", 200).nullable() + val invoiceKind = varchar("invoice_kind", 100).nullable() + val invoiceLayoutFileType = varchar("invoice_layout_file_type", 20).nullable() + val serviceStatus = varchar("service_status", 20).nullable() + + val bankName = varchar("bank_name", 100).nullable() + val bankAccount = varchar("bank_account", 50).nullable() + val presetAddress = varchar("preset_address", 200).nullable() + val presetPhone = varchar("preset_phone", 32).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() + val deletedBy = uuid("deleted_by").nullable() + val version = integer("version").default(1) + + override val primaryKey = PrimaryKey(id) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/database/system/SysApiAccessLogTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/system/SysApiAccessLogTable.kt index a8ae394..6d7525f 100644 --- a/server/src/main/kotlin/com/bbit/ticket/database/system/SysApiAccessLogTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/system/SysApiAccessLogTable.kt @@ -10,6 +10,10 @@ object SysApiAccessLogTable : Table("sys_api_access_log") { val traceId = varchar("trace_id", 64).nullable() val appKey = varchar("app_key", 100).nullable() val appName = varchar("app_name", 100).nullable() + val enterpriseId = uuid("enterprise_id").nullable() + val digitalAccountId = uuid("digital_account_id").nullable() + val apiKey = varchar("api_key", 128).nullable() + val interfaceCode = varchar("interface_code", 100).nullable() val httpMethod = varchar("http_method", 20) val requestPath = varchar("request_path", 255) val requestHeaders = text("request_headers").nullable() 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 2cb54fd..6c5675b 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 @@ -15,7 +15,9 @@ object SysUserTable : Table("sys_user") { val avatar = text("avatar").nullable() val orgId = uuid("org_id").nullable() val status = varchar("status", 20).default("ENABLED") - val apiKey = varchar("api_key", 128).nullable().uniqueIndex() + val enterpriseId = uuid("enterprise_id").nullable() + val digitalAccountId = uuid("digital_account_id").nullable() + val userType = varchar("user_type", 30).default("SYSTEM") val tokenVersion = integer("token_version").default(1) val lastLoginAt = timestampWithTimeZone("last_login_at").nullable() val lastLoginIp = varchar("last_login_ip", 64).nullable() @@ -29,24 +31,6 @@ object SysUserTable : Table("sys_user") { 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() - - val bankName = varchar("bank_name", 100).nullable() - val bankAccount = varchar("bank_account", 50).nullable() - val presetAddress = varchar("preset_address", 200).nullable() - val presetPhone = varchar("preset_phone", 32).nullable() override val primaryKey = PrimaryKey(id) } diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/common/system/AuthDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/system/AuthDto.kt index 77b9659..30e7966 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/common/system/AuthDto.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/system/AuthDto.kt @@ -34,12 +34,11 @@ data class CurrentUserProfile( val phone: String? = null, val email: String? = null, val orgId: String? = null, + val enterpriseId: String? = null, + val digitalAccountId: String? = null, + val userType: String = "SYSTEM", 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/common/system/UserDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/system/UserDto.kt index 0cc42b8..176aa21 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/common/system/UserDto.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/system/UserDto.kt @@ -11,7 +11,6 @@ data class UserListItem( val status: String, val statusLabel: String, val roleCodes: List, - val apiKey: String? = null, ) @Serializable @@ -30,7 +29,6 @@ data class UserDetailResponse( val statusLabel: String, val roleIds: List, val roles: List = emptyList(), - val apiKey: String? = null, val tokenVersion: Int, val lastLoginAt: String? = null, val lastLoginIp: String? = null, @@ -41,22 +39,6 @@ data class UserDetailResponse( val deletedAt: String? = null, val deletedBy: String? = null, val version: Int, - val taxpayerNum: String? = null, - val account: String? = null, - val taxIdentityType: String? = null, - val taxContactName: String? = null, - val taxContactPhone: String? = null, - val taxContactEmail: String? = null, - val taxLegalPersonName: String? = null, - val taxEnterpriseName: String? = null, - val taxRegionCode: String? = null, - val taxCityName: String? = null, - val taxEnterpriseAddress: String? = null, - val taxRegistrationCertificate: String? = null, - val bankName: String? = null, - val bankAccount: String? = null, - val presetAddress: String? = null, - val presetPhone: String? = null, ) @Serializable @@ -87,10 +69,6 @@ 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/entity/request/AskBlueInvoiceRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/AskBlueInvoiceRequest.kt index c6e6a44..e7de4d1 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/AskBlueInvoiceRequest.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/AskBlueInvoiceRequest.kt @@ -8,6 +8,11 @@ import kotlinx.serialization.Serializable @Serializable data class AskInvoiceRequest( + /** + * 平台数电账号 ID。企业管理员开票时用于指定开票员。 + */ + val digitalAccountId: String? = null, + /** * 销方纳税人识别号(销售方税号) * 长度15~20,只允许大写字母和数字 @@ -448,4 +453,4 @@ data class OrderInfo( * 业务单据号 */ val orderNo: String -) \ No newline at end of file +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/EnterpriseRegisterRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/EnterpriseRegisterRequest.kt new file mode 100644 index 0000000..89fd25d --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/EnterpriseRegisterRequest.kt @@ -0,0 +1,51 @@ +package com.bbit.ticket.entity.request + +import kotlinx.serialization.Serializable + +@Serializable +data class EnterpriseRegisterRequest( + val taxpayerNum: String, + val enterpriseName: String, + val legalPersonName: String? = null, + val contactsName: String? = null, + val contactsEmail: String? = null, + val contactsPhone: String, + val regionCode: String, + val cityName: String = "", + val enterpriseAddress: String? = null, + val taxRegistrationCertificate: String? = null, + val password: String, + val confirmPassword: String, +) + +fun EnterpriseRegisterRequest.toTaxRegisterInfo(): TaxRegisterInfo = + TaxRegisterInfo( + taxpayerNum = taxpayerNum.trim(), + enterpriseName = enterpriseName.trim(), + legalPersonName = legalPersonName?.trim()?.ifBlank { null }, + contactsName = contactsName?.trim()?.ifBlank { null }, + contactsEmail = contactsEmail?.trim()?.ifBlank { null }, + contactsPhone = contactsPhone.trim(), + regionCode = regionCode.trim(), + cityName = cityName.trim(), + enterpriseAddress = enterpriseAddress?.trim()?.ifBlank { null }, + taxRegistrationCertificate = taxRegistrationCertificate?.trim()?.ifBlank { null }, + ) + +@Serializable +data class CreateDigitalAccountRequest( + val account: String, + val taxPassword: String, + val identityType: String, + val phoneNum: String, + val name: String, + val platformPassword: String, +) + +@Serializable +data class UpdateInvoiceSettingRequest( + val bankName: String = "", + val bankAccount: String = "", + val address: String = "", + val phone: String = "", +) 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 index de4ef2e..7265d7f 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterInfo.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterInfo.kt @@ -11,16 +11,16 @@ data class TaxRegisterInfo( val enterpriseName: String, /** 法人姓名 */ - val legalPersonName: String, + val legalPersonName: String?, /** 联系人姓名 */ - val contactsName: String, + val contactsName: String?, /** 联系人邮箱 */ - val contactsEmail: String, + val contactsEmail: String?, /** 联系人手机号 */ - val contactsPhone: String, + val contactsPhone: String?, /** 区域编码(行政区划代码,如省/市级编码) */ val regionCode: String, @@ -29,8 +29,8 @@ data class TaxRegisterInfo( val cityName: String, /** 企业详细地址 */ - val enterpriseAddress: String, + val enterpriseAddress: String?, /** 税务登记证编号 / 税务登记证明标识 */ - val taxRegistrationCertificate: 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 deleted file mode 100644 index ef6faad..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/TaxRegisterUserRequest.kt +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index cacf8cb..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateDigitalAccountRequest.kt +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index e4584e0..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdateEnterpriseInfoRequest.kt +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 1779569..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/UpdatePresetDataRequest.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.bbit.ticket.entity.request - -import kotlinx.serialization.Serializable - -@Serializable -data class UpdatePresetDataRequest( - val bankName: String = "", - val bankAccount: String = "", - val address: String = "", - val phone: 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 deleted file mode 100644 index f2cce58..0000000 --- a/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseInfoResponse.kt +++ /dev/null @@ -1,31 +0,0 @@ -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?, - val taxRegistrationCertificate: String? -) - -@Serializable -data class DigitalAccountResponse( - val taxpayerNum: String?, - val taxAccount: String? -) - -@Serializable -data class PresetDataResponse( - val bankName: String?, - val bankAccount: String?, - val address: String?, - val phone: String? -) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseManageResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseManageResponse.kt new file mode 100644 index 0000000..f841a36 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/EnterpriseManageResponse.kt @@ -0,0 +1,63 @@ +package com.bbit.ticket.entity.response + +import kotlinx.serialization.Serializable + +@Serializable +data class EnterpriseManageResponse( + val id: String, + val taxpayerNum: String, + val enterpriseName: String, + val legalPersonName: String? = null, + val contactsName: String? = null, + val contactsEmail: String? = null, + val contactsPhone: String? = null, + val regionCode: String? = null, + val cityName: String? = null, + val enterpriseAddress: String? = null, + val taxRegistrationCertificate: String? = null, + val invitationCode: String? = null, + val reviewStatus: String? = null, + val reviewOpinion: String? = null, + val invoiceKind: String? = null, + val invoiceLayoutFileType: String? = null, + val serviceStatus: String? = null, + val bankName: String? = null, + val bankAccount: String? = null, + val presetAddress: String? = null, + val presetPhone: String? = null, +) + +@Serializable +data class DigitalAccountManageItem( + val id: String, + val enterpriseId: String, + val taxpayerNum: String, + val account: String, + val name: String? = null, + val identityType: String? = null, + val operationProposed: String? = null, + val authStatus: String? = null, + val switchable: String? = null, + val wechatUserBindStatus: String? = null, + val lastAuthSuccTime: String? = null, + val loginAuthStatus: String? = null, + val lastLoginAuthTime: String? = null, + val riskAuthStatus: String? = null, + val lastRiskAuthTime: String? = null, + val platformUserId: String? = null, + val platformUsername: String? = null, + val apiKey: String? = null, + val status: String, +) + +@Serializable +data class OpenApiStatisticsItem( + val digitalAccountId: String? = null, + val account: String? = null, + val interfaceCode: String? = null, + val total: Long, + val success: Long, + val failed: Long, + val avgCostMs: Long, + val lastCalledAt: String? = null, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryDigitalAccountListResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryDigitalAccountListResponse.kt index f324123..4ee2af5 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryDigitalAccountListResponse.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryDigitalAccountListResponse.kt @@ -2,144 +2,19 @@ package com.bbit.ticket.entity.response import kotlinx.serialization.Serializable -/** - * 查询数电账号列表响应 - */ -@Serializable -data class QueryDigitalAccountListResponse( - - /** - * 数电账号列表 - */ - val list: List -) - -/** - * 数电账号信息 - */ @Serializable data class DigitalAccountInfo( - - /** - * 销售方纳税人识别号 - * - * 长度:15-20 - * 只能包含大写英文字母或数字 - */ val taxpayerNum: String, - - /** - * 电子税局登录账号 - */ val account: String, - - /** - * 人员姓名 - */ val name: 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? = null, - - /** - * 登录认证状态 - * - * 0:未登录 - * 1:已登录 - */ val loginAuthStatus: String, - - /** - * 最新登录认证时间 - * - * 时间格式: - * yyyy-MM-dd HH:mm:ss - */ val lastLoginAuthTime: String? = null, - - /** - * 风险认证状态 - * - * 0:未认证 - * 1:已认证 - */ val riskAuthStatus: String, - - /** - * 最新风险认证时间 - * - * 时间格式: - * yyyy-MM-dd HH:mm:ss - */ val lastRiskAuthTime: String? = null -) \ No newline at end of file +) diff --git a/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt index 9d2f95a..b0c2fb4 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt @@ -11,6 +11,7 @@ import com.bbit.ticket.entity.openapi.OpenBlueInvoiceCreateRequest import com.bbit.ticket.service.openapi.OpenBlueInvoiceService import com.bbit.ticket.service.system.ApiAccessLogService import com.bbit.ticket.utils.requireOpenApiPrincipal +import com.bbit.ticket.utils.OpenApiPrincipal import com.bbit.ticket.utils.plugins.myJson import io.ktor.http.ContentType import io.ktor.server.application.ApplicationCall @@ -33,7 +34,7 @@ fun Route.registerOpenBlueInvoiceRoutes() { post { val principal = call.requireOpenApiPrincipal() val request = call.receive() - call.respondOpenApi(principal.apiKey, principal.username, myJson.encodeToString(request)) { + call.respondOpenApi(principal, "blue-invoice.create", myJson.encodeToString(request)) { OpenBlueInvoiceService.createSingle(principal, request) } } @@ -41,7 +42,7 @@ fun Route.registerOpenBlueInvoiceRoutes() { get("/{invoiceReqSerialNo}") { val principal = call.requireOpenApiPrincipal() val invoiceReqSerialNo = call.parameters["invoiceReqSerialNo"].orEmpty() - call.respondOpenApi(principal.apiKey, principal.username, null) { + call.respondOpenApi(principal, "blue-invoice.query", null) { OpenBlueInvoiceService.querySingle(principal, invoiceReqSerialNo) } } @@ -49,7 +50,7 @@ fun Route.registerOpenBlueInvoiceRoutes() { get("/sample/{invoiceReqSerialNo}") { val principal = call.requireOpenApiPrincipal() val invoiceReqSerialNo = call.parameters["invoiceReqSerialNo"].orEmpty() - call.respondOpenApi(principal.apiKey, principal.username, null) { + call.respondOpenApi(principal, "blue-invoice.sample", null) { OpenBlueInvoiceService.sample(principal, invoiceReqSerialNo) } } @@ -57,7 +58,7 @@ fun Route.registerOpenBlueInvoiceRoutes() { post("/batches") { val principal = call.requireOpenApiPrincipal() val request = call.receive() - call.respondOpenApi(principal.apiKey, principal.username, myJson.encodeToString(request)) { + call.respondOpenApi(principal, "blue-invoice.batch-create", myJson.encodeToString(request)) { val response = OpenBlueInvoiceService.createBatch(principal, request) call.application.launch { OpenBlueInvoiceService.processBatch(principal, request.batchNo) @@ -69,7 +70,7 @@ fun Route.registerOpenBlueInvoiceRoutes() { get("/batches/{batchNo}") { val principal = call.requireOpenApiPrincipal() val batchNo = call.parameters["batchNo"].orEmpty() - call.respondOpenApi(principal.apiKey, principal.username, null) { + call.respondOpenApi(principal, "blue-invoice.batch-query", null) { OpenBlueInvoiceService.queryBatch(principal, batchNo) } } @@ -84,8 +85,8 @@ fun Route.registerOpenBlueInvoiceRoutes() { * @param block 当前接口要执行的业务逻辑。 */ private suspend inline fun ApplicationCall.respondOpenApi( - appKey: String?, - appName: String?, + principal: OpenApiPrincipal, + interfaceCode: String, requestBody: String?, crossinline block: suspend () -> T, ) { @@ -94,22 +95,22 @@ private suspend inline fun ApplicationCall.respondOpenApi( val response = ok(block()) val responseBody = myJson.encodeToString(response) respondText(responseBody, ContentType.Application.Json) - saveOpenApiLog(appKey, appName, requestBody, response.code, responseBody, "SUCCESS", null, start) + saveOpenApiLog(principal, interfaceCode, requestBody, response.code, responseBody, "SUCCESS", null, start) } catch (e: PTException) { val response = fail(code = e.code, message = e.message, traceId = e.serialNo) val responseBody = myJson.encodeToString(response) respondText(responseBody, ContentType.Application.Json) - saveOpenApiLog(appKey, appName, requestBody, e.code, responseBody, "FAILED", e.message, start) + saveOpenApiLog(principal, interfaceCode, requestBody, e.code, responseBody, "FAILED", e.message, start) } catch (e: BizException) { val response = fail(code = e.errorCode, message = e.message) val responseBody = myJson.encodeToString(response) respondText(responseBody, ContentType.Application.Json, e.status) - saveOpenApiLog(appKey, appName, requestBody, e.errorCode, responseBody, "FAILED", e.message, start) + saveOpenApiLog(principal, interfaceCode, requestBody, e.errorCode, responseBody, "FAILED", e.message, start) } catch (e: Exception) { val response = fail(code = "-1", message = e.message ?: "开放接口调用失败") val responseBody = myJson.encodeToString(response) respondText(responseBody, ContentType.Application.Json) - saveOpenApiLog(appKey, appName, requestBody, "-1", responseBody, "FAILED", e.message, start) + saveOpenApiLog(principal, interfaceCode, requestBody, "-1", responseBody, "FAILED", e.message, start) } } @@ -126,8 +127,8 @@ private suspend inline fun ApplicationCall.respondOpenApi( * @param start 接口开始时间标记。 */ private suspend fun ApplicationCall.saveOpenApiLog( - appKey: String?, - appName: String?, + principal: OpenApiPrincipal, + interfaceCode: String, requestBody: String?, responseCode: String?, responseBody: String?, @@ -137,8 +138,11 @@ private suspend fun ApplicationCall.saveOpenApiLog( ) { ApiAccessLogService.save( call = this, - appKey = appKey, - appName = appName, + appKey = principal.apiKey, + appName = principal.username, + enterpriseId = principal.enterpriseId, + digitalAccountId = principal.digitalAccountId, + interfaceCode = interfaceCode, requestBody = requestBody, responseCode = responseCode, responseBody = responseBody, diff --git a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt index 35401b7..d02e0e0 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt @@ -2,22 +2,16 @@ package com.bbit.ticket.route.piaotong -import com.bbit.ticket.entity.common.BizException import com.bbit.ticket.entity.request.AuthQrcodeRequest +import com.bbit.ticket.entity.request.CreateDigitalAccountRequest import com.bbit.ticket.entity.request.GetLoginSmsCodeRequest import com.bbit.ticket.entity.request.QueryRealNameAuthQrStatusRequest import com.bbit.ticket.entity.request.SmsLoginRequest import com.bbit.ticket.entity.request.TaxBureauAuthReq -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.entity.request.UpdateInvoiceSettingRequest 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.requirePtProfile -import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.routing.Route import io.ktor.server.routing.get @@ -33,85 +27,80 @@ import kotlin.uuid.ExperimentalUuidApi fun Route.registerPTAuthRoutes() { get("/info") { call.respondPt("查询票通认证状态失败") { - val profile = call.requireCurrentUser().requirePtProfile() - PTAuthService.getTaxBureauAccountAuthStatus( - TaxBureauAuthReq(profile.taxpayerNum, profile.taxAccount) + val account = PTConfigService.requireDigitalAccountForAction( + call.requireCurrentUser(), + call.request.queryParameters["digitalAccountId"], ) - } - } - - post("/register") { - call.respondPt("企业注册失败") { - val currentUser = call.requireCurrentUser() - PTAuthService.registerEnterprise(call.receive(), currentUser.id) - } - } - - post("/registerUser") { - call.respondPt("用户注册失败") { - PTAuthService.registerUserFromPayload( - call.receive(), - call.requireCurrentUser() + PTAuthService.getTaxBureauAccountAuthStatus( + TaxBureauAuthReq(account.taxpayerNum, account.account) ) } } get("/enterprise") { call.respondPtOrEmptyObject("查询企业信息失败") { - PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id) + PTConfigService.getEnterpriseInfo(call.requireCurrentUser()) } } - put("/enterprise") { - call.respondPt("保存企业信息失败") { - PTConfigService.updateEnterpriseInfo( - call.requireCurrentUser().id, - call.receive() - ) + post("/enterprise/refresh") { + call.respondPt("刷新企业信息失败") { + PTConfigService.refreshEnterpriseInfo(call.requireCurrentUser()) } } - get("/digital-account") { - call.respondPtOrEmptyObject("查询数电账号失败") { - val currentUser = call.requireCurrentUser() - if (currentUser.taxPayerNum == null) { - throw BizException("-1", "请先完善用户信息", HttpStatusCode.OK) - } - PTConfigService.getDigitalAccount(currentUser.id) + get("/enterprise/bank-accounts") { + call.respondPt("查询企业开户行及账号失败") { + PTConfigService.queryEnterpriseBankAccounts(call.requireCurrentUser()) } } - put("/digital-account") { - call.respondPt("保存数电账号失败") { - PTConfigService.updateDigitalAccount( - call.requireCurrentUser().id, - call.receive() - ) + get("/digital-accounts") { + call.respondPt("查询数电账号失败") { + PTConfigService.listDigitalAccounts(call.requireCurrentUser()) + } + } + + post("/digital-accounts/refresh") { + call.respondPt("刷新数电账号失败") { + PTConfigService.refreshDigitalAccounts(call.requireCurrentUser()) + } + } + + post("/digital-accounts") { + call.respondPt("新增数电账号失败") { + PTConfigService.createDigitalAccount(call.requireCurrentUser(), call.receive()) } } get("/preset") { call.respondPtOrEmptyObject("查询预设数据失败") { - PTConfigService.getPresetData(call.requireCurrentUser().id) + PTConfigService.getEnterpriseInfo(call.requireCurrentUser()) } } put("/preset") { call.respondPt("保存预设数据失败") { - PTConfigService.updatePresetData( - call.requireCurrentUser().id, - call.receive() - ) + PTConfigService.updateInvoiceSetting(call.requireCurrentUser(), call.receive()) + } + } + + get("/openapi/statistics") { + call.respondPt("查询 OpenAPI 统计失败") { + PTConfigService.openApiStatistics(call.requireCurrentUser()) } } get("/authentication") { call.respondPt("获取认证二维码失败") { - val profile = call.requireCurrentUser().requirePtProfile() + val account = PTConfigService.requireDigitalAccountForAction( + call.requireCurrentUser(), + call.request.queryParameters["digitalAccountId"], + ) PTAuthService.getAuthenticationQrcode( AuthQrcodeRequest( - taxpayerNum = profile.taxpayerNum, - account = profile.taxAccount, + taxpayerNum = account.taxpayerNum, + account = account.account, qrcodeType = call.request.queryParameters["qrcodeType"] ) ) diff --git a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTInvoiceRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTInvoiceRoutes.kt index 2e17c12..6733cab 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTInvoiceRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTInvoiceRoutes.kt @@ -9,7 +9,6 @@ import com.bbit.ticket.entity.request.RedCreateRequest import com.bbit.ticket.service.piaotong.PTBlueService import com.bbit.ticket.service.piaotong.PTRedService import com.bbit.ticket.utils.requireCurrentUser -import com.bbit.ticket.utils.requirePtProfile import io.ktor.server.request.receive import io.ktor.server.routing.Route import io.ktor.server.routing.get @@ -38,7 +37,7 @@ fun Route.registerPTInvoiceRoutes() { call.respondPt("查询开票历史失败") { val currentUser = call.requireCurrentUser() PTBlueService.getInvoiceBlueHistory( - userId = currentUser.id, + user = currentUser, page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1, pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 20, invoiceType = call.request.queryParameters["invoiceType"], @@ -50,7 +49,7 @@ fun Route.registerPTInvoiceRoutes() { get("/invoiceBatchNos") { call.respondPt("查询批次号列表失败") { - PTBlueService.listBatchNos(call.requireCurrentUser().id) + PTBlueService.listBatchNos(call.requireCurrentUser()) } } @@ -103,9 +102,14 @@ fun Route.registerPTInvoiceRoutes() { val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号") ?: return@get call.respondPt("刷新发票状态失败") { + val currentUser = call.requireCurrentUser() + val account = com.bbit.ticket.service.piaotong.PTConfigService.requireDigitalAccountForAction( + currentUser, + call.request.queryParameters["digitalAccountId"], + ) PTBlueService.queryInvoiceAllInfo( QueryInvoiceRequest( - taxpayerNum = call.requireCurrentUser().requirePtProfile().taxpayerNum, + taxpayerNum = account.taxpayerNum, invoiceReqSerialNo = invoiceReqSerialNo, ) ) diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt index ca0eba8..70ce9f4 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerAuthRoutes.kt @@ -4,6 +4,7 @@ package com.bbit.ticket.route.system import com.bbit.ticket.entity.common.ok import com.bbit.ticket.service.system.AuthService import com.bbit.ticket.entity.common.system.LoginRequest +import com.bbit.ticket.entity.request.EnterpriseRegisterRequest import com.bbit.ticket.service.system.OperationLogService import com.bbit.ticket.utils.requireCurrentUser import io.ktor.server.auth.authenticate @@ -31,6 +32,19 @@ fun Route.registerAuthRoutes() { } } + post("/register-enterprise") { + val start = TimeSource.Monotonic.markNow() + val request = call.receive() + runCatching { + val response = AuthService.registerEnterprise(request, call.request.local.remoteHost) + call.respond(ok(response)) + OperationLogService.success(call, null, "REGISTER_ENTERPRISE", "企业注册成功", start.elapsedNow().inWholeMilliseconds) + }.onFailure { + OperationLogService.fail(call, null, "REGISTER_ENTERPRISE", "企业注册失败", it.message, start.elapsedNow().inWholeMilliseconds) + throw it + } + } + authenticate("auth-jwt") { post("/logout") { val currentUser = call.requireCurrentUser() diff --git a/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenBlueInvoiceService.kt b/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenBlueInvoiceService.kt index e95f046..ca44fa5 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenBlueInvoiceService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenBlueInvoiceService.kt @@ -44,7 +44,12 @@ object OpenBlueInvoiceService { validateCreateRequest(request) val createRequest = request.withGeneratedInvoiceReqSerialNo(principal.userId) - PTBlueService.createBlueInvoice(createRequest.toAskInvoiceRequest(principal), principal.userId) + PTBlueService.createBlueInvoice( + createRequest.toAskInvoiceRequest(principal), + principal.userId, + principal.enterpriseId, + principal.digitalAccountId, + ) val detail = PTBlueService.getInvoiceDetail(principal.userId, createRequest.requireInvoiceReqSerialNo()) return OpenBlueInvoiceCreateResponse( requestNo = createRequest.requestNo, @@ -59,7 +64,13 @@ object OpenBlueInvoiceService { } runCatching { - PTBlueService.syncInvoiceFromPT(principal.userId, invoiceReqSerialNo, principal.taxPayerNum) + PTBlueService.syncInvoiceFromPT( + principal.userId, + invoiceReqSerialNo, + principal.taxPayerNum, + principal.enterpriseId, + principal.digitalAccountId, + ) } val detail = PTBlueService.getInvoiceDetail(principal.userId, invoiceReqSerialNo) @@ -115,6 +126,8 @@ object OpenBlueInvoiceService { dbQuery { val batchId = OpenInvoiceBatchTable.insert { it[userId] = principal.userId + it[enterpriseId] = principal.enterpriseId + it[digitalAccountId] = principal.digitalAccountId it[batchNo] = request.batchNo it[totalCount] = request.items.size it[successCount] = 0 @@ -241,6 +254,8 @@ object OpenBlueInvoiceService { principal.userId, item.invoiceReqSerialNo, principal.taxPayerNum, + principal.enterpriseId, + principal.digitalAccountId, ) } } 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 45abed9..5c14dfd 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 @@ -2,8 +2,6 @@ package com.bbit.ticket.service.piaotong -import com.bbit.ticket.dao.piaotong.EnterpriseTaxDao -import com.bbit.ticket.database.system.SysUserTable.taxpayerNum import com.bbit.ticket.entity.request.AuthQrcodeRequest import com.bbit.ticket.entity.request.GetLoginSmsCodeRequest import com.bbit.ticket.entity.request.QueryDigitalAccountListRequest @@ -12,23 +10,16 @@ import com.bbit.ticket.entity.request.QueryEnterpriseInfoRequest import com.bbit.ticket.entity.request.QueryRealNameAuthQrStatusRequest import com.bbit.ticket.entity.request.SmsLoginRequest 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.AuthQrcodeResponse import com.bbit.ticket.entity.response.AuthQrcodeStatusResponse +import com.bbit.ticket.entity.response.DigitalAccountInfo import com.bbit.ticket.entity.response.EnterpriseTaxInfo -import com.bbit.ticket.entity.response.EtaxRegisterResponse import com.bbit.ticket.entity.response.LoginSmsCodeResponse -import com.bbit.ticket.entity.response.QueryDigitalAccountListResponse import com.bbit.ticket.entity.response.QueryEnterpriseBankAccountResponse import com.bbit.ticket.entity.response.QueryEnterpriseInfoResponse import com.bbit.ticket.entity.response.SMSLoginResponse import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent -import com.bbit.ticket.utils.plugins.dbQuery -import com.bbit.ticket.utils.CurrentUser import com.bbit.ticket.utils.net.PTClient -import kotlin.uuid.Uuid object PTAuthService { @@ -41,34 +32,6 @@ object PTAuthService { return res } - /** - * 登记/删除 数电账号 2.3 - */ - suspend fun registerUserFromPayload(req: TaxRegisterUserRequest, currentUser: CurrentUser): String { - val res = PTClient.ptPost( - "registerUser.pt", 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 - } - - /** - * 注册企业(纳税人) 2.2 - * 将企业信息注册到票通平台 - */ - suspend fun registerEnterprise(req: TaxRegisterInfo, userId: Uuid): String { - PTClient.ptPost("register.pt", req) - dbQuery { EnterpriseTaxDao.updateEnterpriseInfo(userId, req) } - return "操作成功" - } - /** * 获取实名认证二维码 2.6 * @@ -118,8 +81,8 @@ object PTAuthService { /** * 查询数电账号列表 2.45 */ - suspend fun getListTaxBureauAccount(req: QueryDigitalAccountListRequest): QueryDigitalAccountListResponse { - return PTClient.ptPost( + suspend fun getListTaxBureauAccount(req: QueryDigitalAccountListRequest): List { + return PTClient.ptPost>( "listTaxBureauAccount.pt", req ) @@ -132,4 +95,4 @@ object PTAuthService { return PTClient.ptPost("queryEnterpriseBankInfo.pt", req) } -} \ No newline at end of file +} diff --git a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTBlueService.kt b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTBlueService.kt index 10da011..2af8135 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTBlueService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTBlueService.kt @@ -13,17 +13,17 @@ import com.bbit.ticket.entity.response.InvoiceCreateResponse import com.bbit.ticket.entity.response.InvoiceDetailResponse import com.bbit.ticket.entity.response.InvoiceHistoryItem import com.bbit.ticket.utils.CurrentUser -import com.bbit.ticket.utils.requirePtProfile import io.ktor.client.request.get import io.ktor.client.statement.bodyAsBytes import com.bbit.ticket.utils.plugins.dbQuery import com.bbit.ticket.utils.net.PTClient +import com.bbit.ticket.utils.parseUuid import kotlin.uuid.Uuid object PTBlueService { - suspend fun listBatchNos(userId: Uuid): List = - dbQuery { BlueInvoiceDao.listBatchNos(userId) } + suspend fun listBatchNos(user: CurrentUser): List = + dbQuery { BlueInvoiceDao.listBatchNos(user.id) } /** * 查询票通同步发票信息(支持插入和更新) @@ -31,12 +31,18 @@ object PTBlueService { * 既是蓝票/红票创建后的补充同步, * 也是手动刷新的核心方法。 */ - suspend fun syncInvoiceFromPT(userId: Uuid, invoiceReqSerialNo: String, taxpayerNum: String): QueryInvoiceResult { + suspend fun syncInvoiceFromPT( + userId: Uuid, + invoiceReqSerialNo: String, + taxpayerNum: String, + enterpriseId: Uuid? = null, + digitalAccountId: Uuid? = null, + ): QueryInvoiceResult { val res = PTClient.ptPost( "queryInvoiceInfo.pt", QueryInvoiceRequest(taxpayerNum = taxpayerNum, invoiceReqSerialNo = invoiceReqSerialNo) ) - dbQuery { BlueInvoiceDao.upsertInvoiceInfo(userId, res) } + dbQuery { BlueInvoiceDao.upsertInvoiceInfo(userId, res, enterpriseId, digitalAccountId) } val newStatus = when (res.code) { "0000" -> "SUCCESS" "7777" -> "PROCESSING" @@ -51,18 +57,25 @@ object PTBlueService { * 蓝票接口调用 */ suspend fun invoiceBlue(req: AskInvoiceRequest, user: CurrentUser): String { - val profile = user.requirePtProfile() + val account = PTConfigService.requireDigitalAccountForAction(user, req.digitalAccountId) return createBlueInvoice( req.copy( - taxpayerNum = profile.taxpayerNum, - account = req.account ?: profile.taxAccount, + taxpayerNum = account.taxpayerNum, + account = account.account, ), user.id, + parseUuid(account.enterpriseId, "enterpriseId"), + parseUuid(account.id, "digitalAccountId"), ) } - suspend fun createBlueInvoice(req: AskInvoiceRequest, userId: Uuid): String { - dbQuery { BlueInvoiceDao.addInvoice(userId, req) } + suspend fun createBlueInvoice( + req: AskInvoiceRequest, + userId: Uuid, + enterpriseId: Uuid? = null, + digitalAccountId: Uuid? = null, + ): String { + dbQuery { BlueInvoiceDao.addInvoice(userId, req, enterpriseId, digitalAccountId) } try { PTClient.ptPost("invoiceBlue.pt", req) } catch (e: Exception) { @@ -71,7 +84,7 @@ object PTBlueService { } // 创建后立即同步一次(非关键,失败忽略) try { - syncInvoiceFromPT(userId, req.invoiceReqSerialNo, req.taxpayerNum) + syncInvoiceFromPT(userId, req.invoiceReqSerialNo, req.taxpayerNum, enterpriseId, digitalAccountId) } catch (_: Exception) { } return "操作成功" } @@ -80,14 +93,25 @@ object PTBlueService { * 分页查询开票历史(支持筛选) */ suspend fun getInvoiceBlueHistory( - userId: Uuid, + user: CurrentUser, page: Int, pageSize: Int, invoiceType: String? = null, isSuccess: Boolean? = null, batchNo: String? = null, ): PageResult = - dbQuery { BlueInvoiceDao.invoiceHistory(userId, page, pageSize, invoiceType, isSuccess, batchNo) } + dbQuery { + BlueInvoiceDao.invoiceHistory( + userId = user.id, + enterpriseId = user.enterpriseId, + digitalAccountId = if (user.isDigitalOperator) user.digitalAccountId else null, + page = page, + pageSize = pageSize, + invoiceType = invoiceType, + isSuccess = isSuccess, + batchNo = batchNo, + ) + } /** * 查询发票完整详情 @@ -110,16 +134,16 @@ object PTBlueService { */ suspend fun queryInvoiceAllInfo(req: QueryInvoiceRequest): QueryInvoiceResult { val invoiceReqSerialNo = req.invoiceReqSerialNo - val existing = dbQuery { - BlueInvoiceDao.findUserIdBySerialNo(invoiceReqSerialNo) + val scope = dbQuery { + BlueInvoiceDao.findInvoiceScopeBySerialNo(invoiceReqSerialNo) } - val result = syncInvoiceFromPT(existing, invoiceReqSerialNo, req.taxpayerNum) + val result = syncInvoiceFromPT(scope.userId, invoiceReqSerialNo, req.taxpayerNum, scope.enterpriseId, scope.digitalAccountId) val relatedSerialNos = dbQuery { - BlueInvoiceDao.findRelatedInvoiceReqSerialNos(existing, invoiceReqSerialNo) + BlueInvoiceDao.findRelatedInvoiceReqSerialNos(scope.userId, invoiceReqSerialNo) } relatedSerialNos.forEach { relatedSerialNo -> runCatching { - syncInvoiceFromPT(existing, relatedSerialNo, req.taxpayerNum) + syncInvoiceFromPT(scope.userId, relatedSerialNo, req.taxpayerNum, scope.enterpriseId, scope.digitalAccountId) } } return result 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 index e7e4dc0..e51b698 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTConfigService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTConfigService.kt @@ -2,54 +2,224 @@ 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.dao.piaotong.EnterpriseManageDao +import com.bbit.ticket.dao.system.UserDao +import com.bbit.ticket.database.piaotong.PtDigitalAccountTable +import com.bbit.ticket.database.system.SysUserTable +import com.bbit.ticket.entity.request.CreateDigitalAccountRequest +import com.bbit.ticket.entity.request.QueryDigitalAccountListRequest +import com.bbit.ticket.entity.request.QueryEnterpriseBankAccountRequest +import com.bbit.ticket.entity.request.QueryEnterpriseInfoRequest +import com.bbit.ticket.entity.request.TaxRegister +import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest +import com.bbit.ticket.entity.response.BankInfo +import com.bbit.ticket.entity.response.DigitalAccountInfo +import com.bbit.ticket.entity.response.DigitalAccountManageItem +import com.bbit.ticket.entity.response.EnterpriseManageResponse +import com.bbit.ticket.entity.response.EtaxRegisterResponse +import com.bbit.ticket.entity.response.OpenApiStatisticsItem import com.bbit.ticket.utils.plugins.dbQuery +import com.bbit.ticket.entity.common.BizException +import com.bbit.ticket.entity.common.ErrorCode +import com.bbit.ticket.service.system.PasswordService +import com.bbit.ticket.utils.ApiKeyUtil +import com.bbit.ticket.utils.CurrentUser +import com.bbit.ticket.utils.net.PTClient +import com.bbit.ticket.utils.parseUuid +import io.ktor.http.HttpStatusCode import kotlin.uuid.Uuid object PTConfigService { + private const val DIGITAL_OPERATOR_ROLE_CODE = "DIGITAL_OPERATOR" + private const val DEFAULT_OPERATOR_PASSWORD = "123456" - // ============================================= - // 企业信息 - // ============================================= - - suspend fun getEnterpriseInfo(userId: Uuid): EnterpriseInfoResponse? = dbQuery { - EnterpriseTaxDao.getEnterpriseInfo(userId) + suspend fun getEnterpriseInfo(user: CurrentUser): EnterpriseManageResponse? = dbQuery { + val enterpriseId = requireEnterpriseId(user) + EnterpriseManageDao.enterpriseDetail(enterpriseId) } - suspend fun updateEnterpriseInfo(userId: Uuid, req: UpdateEnterpriseInfoRequest): String { - dbQuery { EnterpriseTaxDao.updateEnterpriseInfoLocal(userId, req) } - return "企业信息保存成功" + suspend fun refreshEnterpriseInfo(user: CurrentUser): EnterpriseManageResponse { + val enterpriseId = dbQuery { requireEnterpriseId(user) } + val taxpayerNum = dbQuery { + EnterpriseManageDao.enterpriseDetail(enterpriseId)?.taxpayerNum + ?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest) + } + val res = PTAuthService.getEnterpriseInfo(QueryEnterpriseInfoRequest(taxpayerNum)) + dbQuery { EnterpriseManageDao.updateEnterpriseFromPt(enterpriseId, res) } + return dbQuery { EnterpriseManageDao.enterpriseDetail(enterpriseId)!! } } - // ============================================= - // 登记数电账号 - // ============================================= - - suspend fun getDigitalAccount(userId: Uuid): DigitalAccountResponse? = dbQuery { - EnterpriseTaxDao.getDigitalAccount(userId) + suspend fun updateInvoiceSetting(user: CurrentUser, req: UpdateInvoiceSettingRequest): String = dbQuery { + EnterpriseManageDao.updateInvoiceSetting(requireEnterpriseId(user), req) + "开票设置保存成功" } - suspend fun updateDigitalAccount(userId: Uuid, req: UpdateDigitalAccountRequest): String { - dbQuery { EnterpriseTaxDao.updateDigitalAccountLocal(userId, req) } - return "账号信息保存成功" + suspend fun queryEnterpriseBankAccounts(user: CurrentUser): List { + val taxpayerNum = dbQuery { + EnterpriseManageDao.enterpriseDetail(requireEnterpriseId(user))?.taxpayerNum + ?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest) + } + return PTAuthService.queryEnterpriseBankInfo(QueryEnterpriseBankAccountRequest(taxpayerNum)).bankList } - // ============================================= - // 开票预设数据 - // ============================================= - - suspend fun getPresetData(userId: Uuid): PresetDataResponse? = dbQuery { - EnterpriseTaxDao.getPresetData(userId) + suspend fun listDigitalAccounts(user: CurrentUser): List = dbQuery { + when { + user.isSuperAdmin || user.isEnterpriseAdmin -> { + EnterpriseManageDao.digitalAccountsForEnterprise(requireEnterpriseId(user)) + } + user.isDigitalOperator -> { + val id = user.digitalAccountId + ?: throw BizException(ErrorCode.BAD_REQUEST.code, "当前账号未绑定数电账号") + EnterpriseManageDao.digitalAccount(id)?.let { listOf(EnterpriseManageDao.run { it.toDigitalAccountItem() }) } + ?: emptyList() + } + else -> emptyList() + } } - suspend fun updatePresetData(userId: Uuid, req: UpdatePresetDataRequest): String { - dbQuery { EnterpriseTaxDao.updatePresetData(userId, req) } - return "预设数据保存成功" + suspend fun refreshDigitalAccounts(user: CurrentUser): List { + val enterprise = dbQuery { + val enterpriseId = requireEnterpriseId(user) + EnterpriseManageDao.enterpriseDetail(enterpriseId) + ?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest) + } + val res = PTAuthService.getListTaxBureauAccount(QueryDigitalAccountListRequest(enterprise.taxpayerNum)) + dbQuery { + val enterpriseId = parseUuid(enterprise.id, "enterpriseId") + res.forEach { item -> + val digitalAccountId = EnterpriseManageDao.upsertDigitalAccount(enterpriseId, item) + ensureDigitalOperatorAccount( + enterpriseId = enterpriseId, + taxpayerNum = enterprise.taxpayerNum, + item = item, + digitalAccountId = digitalAccountId, + platformPassword = DEFAULT_OPERATOR_PASSWORD, + ) + } + } + return listDigitalAccounts(user) + } + + suspend fun createDigitalAccount(user: CurrentUser, req: CreateDigitalAccountRequest): DigitalAccountManageItem { + if (!user.isSuperAdmin && !user.isEnterpriseAdmin) { + throw BizException(ErrorCode.FORBIDDEN.code, "无权新增数电账号", HttpStatusCode.Forbidden) + } + val enterprise = dbQuery { + EnterpriseManageDao.enterpriseDetail(requireEnterpriseId(user)) + ?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest) + } + + PTClient.ptPost( + "registerUser.pt", + TaxRegister( + taxpayerNum = enterprise.taxpayerNum, + account = req.account, + password = req.taxPassword, + phoneNum = req.phoneNum, + name = req.name, + identityType = req.identityType, + ) + ) + + return dbQuery { + val enterpriseId = parseUuid(enterprise.id, "enterpriseId") + val userId = ensureDigitalOperatorAccount( + enterpriseId = enterpriseId, + taxpayerNum = enterprise.taxpayerNum, + account = req.account.trim(), + name = req.name.trim(), + phone = req.phoneNum.trim().ifBlank { null }, + digitalAccountId = null, + platformPassword = req.platformPassword, + ) + + val digitalAccountId = EnterpriseManageDao.createDigitalAccount( + enterpriseId = enterpriseId, + taxpayerNum = enterprise.taxpayerNum, + account = req.account.trim(), + name = req.name.trim(), + identityType = req.identityType.trim(), + platformUserId = userId, + apiKey = ApiKeyUtil.generate(), + ) + EnterpriseManageDao.bindDigitalAccountUser(digitalAccountId, userId) + EnterpriseManageDao.digitalAccount(digitalAccountId)!!.let { EnterpriseManageDao.run { it.toDigitalAccountItem() } } + } + } + + private fun ensureDigitalOperatorAccount( + enterpriseId: Uuid, + taxpayerNum: String, + item: DigitalAccountInfo, + digitalAccountId: Uuid, + platformPassword: String, + ): Uuid = + ensureDigitalOperatorAccount( + enterpriseId = enterpriseId, + taxpayerNum = taxpayerNum, + account = item.account, + name = item.name, + phone = null, + digitalAccountId = digitalAccountId, + platformPassword = platformPassword, + ) + + private fun ensureDigitalOperatorAccount( + enterpriseId: Uuid, + taxpayerNum: String, + account: String, + name: String?, + phone: String?, + digitalAccountId: Uuid?, + platformPassword: String, + ): Uuid { + val username = "${taxpayerNum}_${account.trim()}" + val existing = UserDao.findByUsername(username) + val userId = existing?.get(SysUserTable.id) + ?: UserDao.createPlatformUser( + username = username, + passwordHash = PasswordService.hash(platformPassword), + nickname = name, + realName = name, + phone = phone, + enterpriseId = enterpriseId, + digitalAccountId = null, + userType = "DIGITAL_OPERATOR", + ).also { createdUserId -> + UserDao.findEnabledRoleIdByCode(DIGITAL_OPERATOR_ROLE_CODE)?.let { roleId -> + UserDao.replaceRoles(createdUserId, listOf(roleId)) + } + } + + if (digitalAccountId != null) { + val row = EnterpriseManageDao.digitalAccount(digitalAccountId) + val apiKey = row?.get(PtDigitalAccountTable.apiKey) ?: ApiKeyUtil.generate() + if (row?.get(PtDigitalAccountTable.platformUserId) != userId || row[PtDigitalAccountTable.apiKey].isNullOrBlank()) { + EnterpriseManageDao.bindDigitalAccountUserAndApiKey(digitalAccountId, userId, apiKey) + } + } + return userId + } + + suspend fun openApiStatistics(user: CurrentUser): List = dbQuery { + EnterpriseManageDao.openApiStatistics(requireEnterpriseId(user), if (user.isDigitalOperator) user.digitalAccountId else null) + } + + fun requireEnterpriseId(user: CurrentUser): Uuid = + user.enterpriseId ?: throw BizException(ErrorCode.BAD_REQUEST.code, "当前账号未绑定企业") + + suspend fun requireDigitalAccountForAction(user: CurrentUser, digitalAccountId: String?): DigitalAccountManageItem = dbQuery { + val targetId = when { + user.isDigitalOperator -> user.digitalAccountId + !digitalAccountId.isNullOrBlank() -> parseUuid(digitalAccountId, "digitalAccountId") + else -> user.digitalAccountId + } ?: throw BizException(ErrorCode.BAD_REQUEST.code, "请选择数电账号") + + val row = EnterpriseManageDao.digitalAccount(targetId) + ?: throw BizException(ErrorCode.BAD_REQUEST.code, "数电账号不存在", HttpStatusCode.NotFound) + if (!user.isSuperAdmin && row[PtDigitalAccountTable.enterpriseId] != requireEnterpriseId(user)) { + throw BizException(ErrorCode.FORBIDDEN.code, "无权操作该数电账号", HttpStatusCode.Forbidden) + } + EnterpriseManageDao.run { row.toDigitalAccountItem() } } } diff --git a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt index a78b700..d21c446 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt @@ -10,9 +10,9 @@ import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse import com.bbit.ticket.entity.response.QuickRedInvoiceResponse import com.bbit.ticket.entity.response.RedInvoiceInfoResponse import com.bbit.ticket.utils.CurrentUser -import com.bbit.ticket.utils.requirePtProfile import com.bbit.ticket.utils.plugins.dbQuery import com.bbit.ticket.utils.net.PTClient +import com.bbit.ticket.utils.parseUuid import io.ktor.client.request.get import io.ktor.client.statement.bodyAsBytes import kotlin.uuid.Uuid @@ -28,14 +28,14 @@ object PTRedService { * 红票接口调用 */ suspend fun invoiceRed(user: CurrentUser,req:RedCreateRequest): String { - val profile = user.requirePtProfile() + val account = PTConfigService.requireDigitalAccountForAction(user, null) val invoiceReqSerialNo = PTClient.ptDate() val historyId = Uuid.parse(req.historyId) val his = dbQuery { HistoryDao.findByHistory(historyId, user.id) } val req = QuickRedInvoiceRequest( - taxpayerNum = profile.taxpayerNum, + taxpayerNum = account.taxpayerNum, invoiceReqSerialNo = invoiceReqSerialNo, invoiceCode = his.invoiceCode, invoiceNo = his.invoiceNo, @@ -43,7 +43,7 @@ object PTRedService { blueInvoiceDate = his.blueInvoiceDate, redReason = req.redReason, amount = his.totalAmount?.negate()?.toPlainString()?:"0.0", - account = profile.taxAccount, + account = account.account, invoiceKind = his.invoiceKind, takerName = req.takerName, takerTel = req.takerTel, @@ -51,8 +51,10 @@ object PTRedService { ) PTClient.ptPost("invoiceRed.pt", req) dbQuery { RedInvoiceDao.addRedInvoice(user.id, historyId, req) } - PTBlueService.syncInvoiceFromPT(user.id, his.invoiceReqSerialNo, profile.taxpayerNum) - PTBlueService.syncInvoiceFromPT(user.id, invoiceReqSerialNo, profile.taxpayerNum) + val enterpriseId = parseUuid(account.enterpriseId, "enterpriseId") + val digitalAccountId = parseUuid(account.id, "digitalAccountId") + PTBlueService.syncInvoiceFromPT(user.id, his.invoiceReqSerialNo, account.taxpayerNum, enterpriseId, digitalAccountId) + PTBlueService.syncInvoiceFromPT(user.id, invoiceReqSerialNo, account.taxpayerNum, enterpriseId, digitalAccountId) return "操作成功" } diff --git a/server/src/main/kotlin/com/bbit/ticket/service/system/ApiAccessLogService.kt b/server/src/main/kotlin/com/bbit/ticket/service/system/ApiAccessLogService.kt index 09f1644..117fc31 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/system/ApiAccessLogService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/system/ApiAccessLogService.kt @@ -6,6 +6,7 @@ import com.bbit.ticket.dao.system.LogDao import com.bbit.ticket.utils.plugins.dbQuery import io.ktor.server.application.ApplicationCall import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid object ApiAccessLogService { /** @@ -25,6 +26,9 @@ object ApiAccessLogService { call: ApplicationCall, appKey: String?, appName: String?, + enterpriseId: Uuid? = null, + digitalAccountId: Uuid? = null, + interfaceCode: String? = null, requestBody: String?, responseCode: String?, responseBody: String?, @@ -36,6 +40,9 @@ object ApiAccessLogService { call = call, appKey = appKey, appName = appName, + enterpriseId = enterpriseId, + digitalAccountId = digitalAccountId, + interfaceCode = interfaceCode, requestBody = requestBody, responseCode = responseCode, responseBody = responseBody, 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 646db8b..f28e1be 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 @@ -3,6 +3,7 @@ package com.bbit.ticket.service.system import com.bbit.ticket.dao.system.MenuDao +import com.bbit.ticket.dao.piaotong.EnterpriseManageDao import com.bbit.ticket.dao.system.UserDao import com.bbit.ticket.database.system.SysUserTable import com.bbit.ticket.entity.common.BizException @@ -11,12 +12,19 @@ import com.bbit.ticket.entity.common.system.CurrentUserProfile import com.bbit.ticket.entity.common.system.LoginRequest import com.bbit.ticket.entity.common.system.LoginResponse import com.bbit.ticket.entity.common.system.MeResponse +import com.bbit.ticket.entity.request.EnterpriseRegisterRequest +import com.bbit.ticket.entity.request.TaxRegisterInfo +import com.bbit.ticket.entity.request.toTaxRegisterInfo +import com.bbit.ticket.entity.response.EnterpriseTaxInfo import com.bbit.ticket.utils.plugins.dbQuery import com.bbit.ticket.utils.CurrentUser +import com.bbit.ticket.utils.net.PTClient import io.ktor.http.HttpStatusCode import kotlin.uuid.ExperimentalUuidApi object AuthService { + private const val ENTERPRISE_ADMIN_ROLE_CODE = "ENTERPRISE_ADMIN" + suspend fun login(request: LoginRequest, loginIp: String?): LoginResponse { val username = request.username.trim() if (username.isBlank() || request.password.isBlank()) { @@ -57,6 +65,55 @@ object AuthService { return LoginResponse(accessToken = accessToken, expiresIn = expiresIn) } + suspend fun registerEnterprise(request: EnterpriseRegisterRequest, loginIp: String?): LoginResponse { + val taxpayerNum = request.taxpayerNum.trim() + if (taxpayerNum.isBlank() || request.enterpriseName.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "纳税人识别号和企业名称不能为空", HttpStatusCode.BadRequest) + } + if (request.regionCode.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "地区编码不能为空", HttpStatusCode.BadRequest) + } + if (request.contactsPhone.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "联系人手机不能为空", HttpStatusCode.BadRequest) + } + if (request.password.isBlank()) { + throw BizException(ErrorCode.BAD_REQUEST.code, "密码不能为空", HttpStatusCode.BadRequest) + } + if (request.password != request.confirmPassword) { + throw BizException(ErrorCode.BAD_REQUEST.code, "两次输入的密码不一致", HttpStatusCode.BadRequest) + } + + val registerInfo = request.toTaxRegisterInfo() + dbQuery { + if (UserDao.findByUsername(taxpayerNum) != null) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "该纳税人识别号已注册", HttpStatusCode.Conflict) + } + if (EnterpriseManageDao.findEnterpriseByTaxpayerNum(taxpayerNum) != null) { + throw BizException(ErrorCode.DATA_CONFLICT.code, "企业已存在", HttpStatusCode.Conflict) + } + } + PTClient.ptPost("register.pt", registerInfo) + + dbQuery { + val enterpriseId = EnterpriseManageDao.createEnterprise(registerInfo) + val userId = UserDao.createPlatformUser( + username = taxpayerNum, + passwordHash = PasswordService.hash(request.password), + nickname = request.enterpriseName.trim(), + realName = request.legalPersonName?.trim()?.ifBlank { null }, + phone = request.contactsPhone.trim(), + enterpriseId = enterpriseId, + digitalAccountId = null, + userType = "ENTERPRISE_ADMIN", + ) + UserDao.findEnabledRoleIdByCode(ENTERPRISE_ADMIN_ROLE_CODE)?.let { roleId -> + UserDao.replaceRoles(userId, listOf(roleId)) + } + } + + return login(LoginRequest(username = taxpayerNum, password = request.password), loginIp) + } + suspend fun me(currentUser: CurrentUser): MeResponse { val userRow = dbQuery { UserDao.requireActive(currentUser.id) } @@ -73,12 +130,11 @@ object AuthService { phone = userRow[SysUserTable.phone], email = userRow[SysUserTable.email], orgId = userRow[SysUserTable.orgId]?.toString(), + enterpriseId = userRow[SysUserTable.enterpriseId]?.toString(), + digitalAccountId = userRow[SysUserTable.digitalAccountId]?.toString(), + userType = userRow[SysUserTable.userType], 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], + createdAt = userRow[SysUserTable.createdAt].toString(), ), menus = menuTree, permissions = permissions, diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/OpenApiPrincipal.kt b/server/src/main/kotlin/com/bbit/ticket/utils/OpenApiPrincipal.kt index 38f5db0..7a2d3c7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/OpenApiPrincipal.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/OpenApiPrincipal.kt @@ -3,6 +3,8 @@ package com.bbit.ticket.utils import com.bbit.ticket.database.system.SysUserTable +import com.bbit.ticket.dao.piaotong.EnterpriseManageDao +import com.bbit.ticket.database.piaotong.PtDigitalAccountTable import com.bbit.ticket.entity.common.BizException import com.bbit.ticket.entity.common.ErrorCode import com.bbit.ticket.utils.plugins.dbQuery @@ -21,6 +23,8 @@ data class OpenApiPrincipal( val userId: Uuid, val username: String, val apiKey: String, + val enterpriseId: Uuid, + val digitalAccountId: Uuid, val taxPayerNum: String, val taxAccount: String, ) @@ -32,47 +36,32 @@ suspend fun ApplicationCall.requireOpenApiPrincipal(): OpenApiPrincipal { throw BizException(ErrorCode.UNAUTHORIZED.code, "缺少 X-Api-Key", HttpStatusCode.Unauthorized) } - val row = dbQuery { - SysUserTable.selectAll() - .where { - (SysUserTable.apiKey eq apiKey) and - (SysUserTable.status eq "ENABLED") and - SysUserTable.deletedAt.isNull() - } - .singleOrNull() - } ?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 无效或已停用", HttpStatusCode.Unauthorized) + val accountRow = dbQuery { EnterpriseManageDao.digitalAccountByApiKey(apiKey) } + ?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 无效或已停用", HttpStatusCode.Unauthorized) + val userId = accountRow[PtDigitalAccountTable.platformUserId] + ?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 未绑定平台账号", HttpStatusCode.Unauthorized) - val taxpayerNum = row[SysUserTable.taxpayerNum]?.takeIf { it.isNotBlank() } - val taxAccount = row[SysUserTable.taxAccount]?.takeIf { it.isNotBlank() } - if (taxpayerNum == null || taxAccount == null) { - throw BizException(ErrorCode.BAD_REQUEST.code, "请先完善用户信息") - } + val row = dbQuery { + SysUserTable.selectAll().where { + (SysUserTable.id eq userId) and + (SysUserTable.status eq "ENABLED") and + SysUserTable.deletedAt.isNull() + }.singleOrNull() + } ?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 绑定账号无效或已停用", HttpStatusCode.Unauthorized) dbQuery { - SysUserTable.update({ SysUserTable.id eq row[SysUserTable.id] }) { + SysUserTable.update({ SysUserTable.id eq userId }) { it[updatedAt] = OffsetDateTime.now() } } return OpenApiPrincipal( - userId = row[SysUserTable.id], + userId = userId, username = row[SysUserTable.username], apiKey = apiKey, - taxPayerNum = taxpayerNum, - taxAccount = taxAccount, + enterpriseId = accountRow[PtDigitalAccountTable.enterpriseId], + digitalAccountId = accountRow[PtDigitalAccountTable.id], + taxPayerNum = accountRow[PtDigitalAccountTable.taxpayerNum], + taxAccount = accountRow[PtDigitalAccountTable.account], ) } - -data class PtProfile( - val taxpayerNum: String, - val taxAccount: String, -) - -fun CurrentUser.requirePtProfile(): PtProfile { - val taxpayerNum = taxPayerNum?.takeIf { it.isNotBlank() } - val account = taxAccount?.takeIf { it.isNotBlank() } - if (taxpayerNum == null || account == null) { - throw BizException(ErrorCode.BAD_REQUEST.code, "请先完善用户信息") - } - return PtProfile(taxpayerNum, account) -} 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 e0f63cb..9371fa7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/SecurityPrincipal.kt @@ -28,18 +28,23 @@ data class CurrentUser( val id: Uuid, val username: String, val orgId: Uuid?, + val enterpriseId: Uuid?, + val digitalAccountId: Uuid?, + val userType: String, val tokenVersion: Int, val roleCodes: Set, val permissions: Set, - 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") + + val isEnterpriseAdmin: Boolean + get() = userType == "ENTERPRISE_ADMIN" + + val isDigitalOperator: Boolean + get() = userType == "DIGITAL_OPERATOR" } private val CurrentUserKey = AttributeKey("currentUser") @@ -138,13 +143,12 @@ suspend fun ApplicationCall.requireCurrentUser(): CurrentUser { id = userRow[SysUserTable.id], username = userRow[SysUserTable.username], orgId = userRow[SysUserTable.orgId], + enterpriseId = userRow[SysUserTable.enterpriseId], + digitalAccountId = userRow[SysUserTable.digitalAccountId], + userType = userRow[SysUserTable.userType], tokenVersion = userRow[SysUserTable.tokenVersion], roleCodes = roleCodes, permissions = permissions, - taxPayerNum = userRow[SysUserTable.taxpayerNum], - taxAccount = userRow[SysUserTable.taxAccount], - taxPassword = userRow[SysUserTable.taxPassword], - taxIdentityType = userRow[SysUserTable.taxIdentityType], phone = userRow[SysUserTable.phone], realName = userRow[SysUserTable.realName], ) diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/DatabaseInitializer.kt b/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/DatabaseInitializer.kt index 7f0670b..27ce51b 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/DatabaseInitializer.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/DatabaseInitializer.kt @@ -7,6 +7,8 @@ import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable import com.bbit.ticket.database.piaotong.HistoryInvoiceRedTable import com.bbit.ticket.database.piaotong.OpenInvoiceBatchItemTable import com.bbit.ticket.database.piaotong.OpenInvoiceBatchTable +import com.bbit.ticket.database.piaotong.PtDigitalAccountTable +import com.bbit.ticket.database.piaotong.PtEnterpriseTable import com.bbit.ticket.database.system.SysApiAccessLogTable import com.bbit.ticket.database.system.SysDictItemTable import com.bbit.ticket.database.system.SysDictTypeTable @@ -36,6 +38,8 @@ object DatabaseInitializer { SysDictItemTable, SysOperationLogTable, SysApiAccessLogTable, + PtEnterpriseTable, + PtDigitalAccountTable, HistoryInvoiceRedTable, HistoryInvoiceBasicTable, HistoryInvoiceGoodsTable, diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/Global.kt b/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/Global.kt index bea4805..0ad6d38 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/Global.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/Global.kt @@ -2,7 +2,7 @@ package com.bbit.ticket.utils.bootstrap object Global { - val isDev = true + val isDev = false // 请求基础地址 var baseUrl: String diff --git a/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/SeedData.kt b/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/SeedData.kt index 38d61b9..8f0c62d 100644 --- a/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/SeedData.kt +++ b/server/src/main/kotlin/com/bbit/ticket/utils/bootstrap/SeedData.kt @@ -5,7 +5,6 @@ package com.bbit.ticket.utils.bootstrap import com.bbit.ticket.database.system.* import com.bbit.ticket.utils.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 @@ -25,10 +24,11 @@ object SeedData { const val ADMIN_USERNAME = "admin" const val ADMIN_INIT_PASSWORD = "Admin@123456" - const val ADMIN_INIT_API_KEY = "tk_admin_test_key_please_change" private const val DEFAULT_ORG_CODE = "DEFAULT_ORG" private const val SUPER_ADMIN_ROLE_CODE = "SUPER_ADMIN" + private const val ENTERPRISE_ADMIN_ROLE_CODE = "ENTERPRISE_ADMIN" + private const val DIGITAL_OPERATOR_ROLE_CODE = "DIGITAL_OPERATOR" // ========================================================= // Main entry @@ -38,10 +38,24 @@ object SeedData { val now = OffsetDateTime.now() val orgId = upsertDefaultOrg(now) val roleId = upsertSuperAdminRole(now) + val enterpriseAdminRoleId = upsertBusinessRole( + code = ENTERPRISE_ADMIN_ROLE_CODE, + name = "企业管理员", + description = "外部注册企业默认管理员角色", + now = now, + ) + val digitalOperatorRoleId = upsertBusinessRole( + code = DIGITAL_OPERATOR_ROLE_CODE, + name = "开票员", + description = "数电账号对应的平台开票员角色", + now = now, + ) val adminId = upsertAdminUser(orgId, now) upsertUserRole(adminId, roleId) val menuIds = upsertMenus(now) - bindRoleMenus(roleId, menuIds) + bindRoleMenus(roleId, menuIds.values.toList()) + bindRoleMenus(enterpriseAdminRoleId, enterpriseAdminMenuIds(menuIds)) + bindRoleMenus(digitalOperatorRoleId, digitalOperatorMenuIds(menuIds)) seedDicts(now) logger.info("Seed data initialized, default admin username: {}", ADMIN_USERNAME) } @@ -105,6 +119,39 @@ object SeedData { inserted[SysRoleTable.id] } + private suspend fun upsertBusinessRole( + code: String, + name: String, + description: String, + now: OffsetDateTime, + ): Uuid = dbQuery { + val existing = SysRoleTable.selectAll() + .where { (SysRoleTable.code eq code) and SysRoleTable.deletedAt.isNull() } + .singleOrNull() + + if (existing != null) { + val id = existing[SysRoleTable.id] + SysRoleTable.update({ SysRoleTable.id eq id }) { + it[SysRoleTable.name] = name + it[SysRoleTable.description] = description + it[SysRoleTable.status] = "ENABLED" + it[SysRoleTable.dataScope] = "SELF" + it[SysRoleTable.updatedAt] = now + } + return@dbQuery id + } + + val inserted = SysRoleTable.insert { + it[SysRoleTable.name] = name + it[SysRoleTable.code] = code + it[SysRoleTable.description] = description + it[SysRoleTable.status] = "ENABLED" + it[SysRoleTable.dataScope] = "SELF" + it[SysRoleTable.createdAt] = now + } + inserted[SysRoleTable.id] + } + // ========================================================= // Admin user // ========================================================= @@ -120,14 +167,9 @@ object SeedData { it[SysUserTable.nickname] = "系统管理员" it[SysUserTable.orgId] = orgId it[SysUserTable.status] = "ENABLED" - if (existing[SysUserTable.apiKey].isNullOrBlank()) { - it[SysUserTable.apiKey] = ADMIN_INIT_API_KEY - } + it[SysUserTable.userType] = "SYSTEM" 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 @@ -139,13 +181,9 @@ object SeedData { it[SysUserTable.nickname] = "系统管理员" it[SysUserTable.orgId] = orgId it[SysUserTable.status] = "ENABLED" - it[SysUserTable.apiKey] = ADMIN_INIT_API_KEY + it[SysUserTable.userType] = "SYSTEM" 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] = "测试" it[SysUserTable.createdAt] = now } @@ -168,7 +206,7 @@ object SeedData { // Menus & permissions // ========================================================= - private suspend fun upsertMenus(now: OffsetDateTime): List { + private suspend fun upsertMenus(now: OffsetDateTime): Map { val seedMenus = listOf( rootMenu("dashboard", "工作台", "Dashboard", "/dashboard", "dashboard/index", "LayoutDashboard", 10), catalog("system", "系统管理", "SystemRoot", "Settings", 20), @@ -196,10 +234,15 @@ object SeedData { 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), + catalog("basic_info", "基础信息", "BasicInfoRoot", "Building2", 40), + subMenu("enterprise_info", "basic_info", "企业信息", "EnterpriseInfo", "/enterprise/info", "piaotong/index", "Building2", "enterprise:info:view", 10), + subMenu("digital_account", "basic_info", "数电账号", "DigitalAccountManage", "/enterprise/digital-accounts", "piaotong/digital-accounts/index", "Users", "digital-account:view", 20), + subMenu("invoice_setting", "basic_info", "开票设置", "InvoiceSetting", "/enterprise/invoice-setting", "piaotong/invoice-setting/index", "SlidersHorizontal", "invoice-setting:view", 30), + catalog("invoice_service", "开票服务", "InvoiceServiceRoot", "Receipt", 50), + subMenu("piaotong_invoice_issue", "invoice_service", "开具蓝票", "PiaoTongInvoiceIssue", "/piaotong/invoice-issue", "piaotong/invoice-issue/index", "FilePlus", "piaotong:invoice-issue:view", 10), + subMenu("piaotong_invoice_history", "invoice_service", "开票历史", "PiaoTongInvoiceHistory", "/piaotong/invoice-history", "piaotong/invoice-history/index", "History", "piaotong:invoice-history:view", 20), + catalog("statistics_info", "统计信息", "StatisticsInfoRoot", "ChartNoAxesColumn", 60), + subMenu("openapi_statistics", "statistics_info", "OpenAPI", "OpenApiStatistics", "/statistics/openapi", "statistics/openapi/index", "Waypoints", "openapi:statistics:view", 10), ) val idMap = mutableMapOf() @@ -208,9 +251,35 @@ object SeedData { val menuId = upsertMenu(menu, parentId, now) idMap[menu.key] = menuId } - return idMap.values.toList() + return idMap } + private fun enterpriseAdminMenuIds(menuIds: Map): List = + listOf( + "dashboard", + "basic_info", + "enterprise_info", + "digital_account", + "invoice_setting", + "invoice_service", + "piaotong_invoice_issue", + "piaotong_invoice_history", + "statistics_info", + "openapi_statistics", + ).mapNotNull { menuIds[it] } + + private fun digitalOperatorMenuIds(menuIds: Map): List = + listOf( + "dashboard", + "basic_info", + "digital_account", + "invoice_service", + "piaotong_invoice_issue", + "piaotong_invoice_history", + "statistics_info", + "openapi_statistics", + ).mapNotNull { menuIds[it] } + private suspend fun upsertMenu(seedMenu: SeedMenu, parentId: Uuid?, now: OffsetDateTime): Uuid = dbQuery { val existing = SysMenuTable.selectAll() .where { (SysMenuTable.name eq seedMenu.name) and SysMenuTable.deletedAt.isNull() } 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 4138fa0..1dfb0a9 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,19 +12,34 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory +import java.io.BufferedReader +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.URI import java.text.SimpleDateFormat import java.util.* object PTClient { val logger = LoggerFactory.getLogger(PTClient::class.java) + private const val CONNECT_TIMEOUT_MS = 10_000 + private const val READ_TIMEOUT_MS = 60_000 + val wireJson = Json { + explicitNulls = false + ignoreUnknownKeys = true + isLenient = true + encodeDefaults = true + } val client = HttpClient(CIO) { @@ -123,14 +138,15 @@ object PTClient { headers: Map = emptyMap() ): Resp { // req json - val reqJson = myJson.encodeToString(body) + val reqJson = wireJson.encodeToString(body) + val targetUrl = Global.baseUrl + url + val requestBody = buildRequestData(reqJson) + val startedAt = System.currentTimeMillis() + logger.info("POST url = $targetUrl") logger.info("req = $reqJson") - val response = client.post(Global.baseUrl + url) { - contentType(ContentType.Application.Json) - headers.forEach { (k, v) -> header(k, v) } - setBody(buildRequestData(reqJson)) - }.bodyAsText() + val response = postJsonRaw(targetUrl, requestBody, headers) + logger.info("raw response costMs=${System.currentTimeMillis() - startedAt} body=$response") val decrypted = disposeResponse(response) val result = myJson.decodeFromString>(decrypted) @@ -145,6 +161,54 @@ object PTClient { return myJson.decodeFromJsonElement(result.content!!) } + suspend fun postJsonRaw( + url: String, + jsonBody: String, + headers: Map = emptyMap(), + ): String = withContext(Dispatchers.IO) { + var connection: HttpURLConnection? = null + try { + val bytes = jsonBody.toByteArray(Charsets.UTF_8) + connection = (URI.create(url).toURL().openConnection() as HttpURLConnection).apply { + requestMethod = "POST" + connectTimeout = CONNECT_TIMEOUT_MS + readTimeout = READ_TIMEOUT_MS + doOutput = true + useCaches = false + setRequestProperty("Content-Type", "application/json; charset=UTF-8") + setRequestProperty("Accept", "application/json") + setRequestProperty("Connection", "close") + headers.forEach { (k, v) -> setRequestProperty(k, v) } + setFixedLengthStreamingMode(bytes.size) + } + + connection.outputStream.use { output -> + output.write(bytes) + output.flush() + } + + val responseCode = connection.responseCode + val stream = if (responseCode in 200..299) connection.inputStream else connection.errorStream + val response = stream?.use { input -> + BufferedReader(InputStreamReader(input, Charsets.UTF_8)).use { reader -> + buildString { + while (true) { + val line = reader.readLine() ?: break + append(line) + } + } + } + }.orEmpty() + + if (responseCode !in 200..299) { + throw IllegalStateException("票通接口 HTTP $responseCode: $response") + } + response + } finally { + connection?.disconnect() + } + } + /** * 关闭 */ @@ -166,7 +230,7 @@ object PTClient { map["timestamp"] = sdf.format(Date()) map["serialNo"] = ptDate() map["sign"] = RSAUtil.sign(RSAUtil.getSignatureContent(map), Global.ptPrivateKey) ?: "" - return myJson.encodeToString(map) + return wireJson.encodeToString(map) } fun disposeResponse( @@ -229,4 +293,4 @@ object PTClient { return str } -} \ No newline at end of file +} diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index fe98582..dedf7db 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -1,10 +1,14 @@ import http from '@/api/http' -import type { LoginRequest, LoginResponse, MeResponse } from '@/types/auth' +import type { EnterpriseRegisterRequest, LoginRequest, LoginResponse, MeResponse } from '@/types/auth' export function loginApi(payload: LoginRequest) { return http.post('/auth/login', payload) } +export function registerEnterpriseApi(payload: EnterpriseRegisterRequest) { + return http.post('/auth/register-enterprise', payload) +} + export function logoutApi() { return http.post('/auth/logout') } diff --git a/web/src/api/piaotong/index.ts b/web/src/api/piaotong/index.ts index e85d6c4..cca492f 100644 --- a/web/src/api/piaotong/index.ts +++ b/web/src/api/piaotong/index.ts @@ -95,83 +95,83 @@ 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 { + id: string taxpayerNum: string enterpriseName: string - legalPersonName: string - contactsName: string - contactsEmail: string - contactsPhone: string - regionCode: string - cityName: string - enterpriseAddress: string - taxRegistrationCertificate: string + legalPersonName?: string | null + contactsName?: string | null + contactsEmail?: string | null + contactsPhone?: string | null + regionCode?: string | null + cityName?: string | null + enterpriseAddress?: string | null + taxRegistrationCertificate?: string | null + invitationCode?: string | null + reviewStatus?: string | null + reviewOpinion?: string | null + invoiceKind?: string | null + invoiceLayoutFileType?: string | null + serviceStatus?: string | null + bankName?: string | null + bankAccount?: string | null + presetAddress?: string | null + presetPhone?: string | null } -/** 获取企业信息 */ export function getEnterpriseInfoApi(): Promise { return http.get('/pt/enterprise') } -/** 更新企业信息 */ -export function updateEnterpriseInfoApi(payload: Partial): Promise { - return http.put('/pt/enterprise', payload) +export function refreshEnterpriseInfoApi(): Promise { + return http.post('/pt/enterprise/refresh') } -/** 数电账号信息 */ -export interface DigitalAccountInfo { +export interface DigitalAccountItem { + id: string + enterpriseId: string taxpayerNum: string - taxAccount: string + account: string + name?: string | null + identityType?: string | null + operationProposed?: string | null + authStatus?: string | null + switchable?: string | null + wechatUserBindStatus?: string | null + lastAuthSuccTime?: string | null + loginAuthStatus?: string | null + lastLoginAuthTime?: string | null + riskAuthStatus?: string | null + lastRiskAuthTime?: string | null + platformUserId?: string | null + platformUsername?: string | null + apiKey?: string | null + status: string } -/** 获取数电账号信息 */ -export function getDigitalAccountApi(): Promise { - return http.get('/pt/digital-account') +export interface CreateDigitalAccountRequest { + account: string + taxPassword: string + identityType: string + phoneNum: string + name: string + platformPassword: string } -/** 更新数电账号信息 */ -export function updateDigitalAccountApi(payload: DigitalAccountInfo): Promise { - return http.put('/pt/digital-account', payload) +export function listDigitalAccountsApi(): Promise { + return http.get('/pt/digital-accounts') +} + +export function refreshDigitalAccountsApi(): Promise { + return http.post('/pt/digital-accounts/refresh') +} + +export function createDigitalAccountApi( + payload: CreateDigitalAccountRequest +): Promise { + return http.post('/pt/digital-accounts', payload) } -/** 开票预设数据 */ export interface PresetData { bankName: string bankAccount: string @@ -179,16 +179,44 @@ export interface PresetData { phone: string } -/** 获取开票预设数据 */ -export function getPresetDataApi(): Promise { - return http.get('/pt/preset') +export interface EnterpriseBankAccount { + bankName: string + bankAccount?: string | null + source: string +} + +export function getPresetDataApi(): Promise { + return getEnterpriseInfoApi().then((info) => ({ + bankName: info.bankName ?? '', + bankAccount: info.bankAccount ?? '', + address: info.presetAddress ?? '', + phone: info.presetPhone ?? '' + })) } -/** 更新开票预设数据 */ export function updatePresetDataApi(payload: PresetData): Promise { return http.put('/pt/preset', payload) } +export function queryEnterpriseBankAccountsApi(): Promise { + return http.get('/pt/enterprise/bank-accounts') +} + +export interface OpenApiStatisticsItem { + digitalAccountId?: string | null + account?: string | null + interfaceCode?: string | null + total: number + success: number + failed: number + avgCostMs: number + lastCalledAt?: string | null +} + +export function openApiStatisticsApi(): Promise { + return http.get('/pt/openapi/statistics') +} + // ============================================= // 开票相关 // ============================================= @@ -269,6 +297,7 @@ export interface OrderInfo { * 数电发票开票请求 */ export interface InvoiceRequest { + digitalAccountId?: string | null /** 销方纳税人识别号 */ taxpayerNum: string /** 发票请求流水号 */ @@ -850,8 +879,11 @@ export interface AuthQrcodeResponse { * 获取实名认证二维码 * @param qrcodeType 1: 电子税务局 APP, 2: 国家网络身份认证 APP */ -export function getAuthQrcodeApi(qrcodeType: string): Promise { - return http.get('/pt/authentication', { params: { qrcodeType } }) +export function getAuthQrcodeApi( + qrcodeType: string, + digitalAccountId?: string +): Promise { + return http.get('/pt/authentication', { params: { qrcodeType, digitalAccountId } }) } /** 查询认证二维码扫码状态响应 */ diff --git a/web/src/components/AppMenu.vue b/web/src/components/AppMenu.vue index f9fa4c2..825f4ef 100644 --- a/web/src/components/AppMenu.vue +++ b/web/src/components/AppMenu.vue @@ -60,6 +60,7 @@ function handleSelect(key: string) { diff --git a/web/src/features/piaotong/index.vue b/web/src/features/piaotong/index.vue index e1f2aec..7f2f82c 100644 --- a/web/src/features/piaotong/index.vue +++ b/web/src/features/piaotong/index.vue @@ -1,2106 +1,112 @@ diff --git a/web/src/features/piaotong/invoice-history/index.vue b/web/src/features/piaotong/invoice-history/index.vue index b5df69c..b5790f4 100644 --- a/web/src/features/piaotong/invoice-history/index.vue +++ b/web/src/features/piaotong/invoice-history/index.vue @@ -44,6 +44,7 @@
+ + + + + ('basic') const currentUser = computed(() => authStore.user) +const digitalAccounts = ref([]) +const isOperator = computed(() => currentUser.value?.userType === 'DIGITAL_OPERATOR') +const digitalAccountOptions = computed(() => + digitalAccounts.value.map((item) => ({ + label: `${item.name || item.account}(${item.account})`, + value: item.id + })) +) const currentItem = computed({ get: () => form.itemList[activeSidebar.value as number] ?? form.itemList[0], set: (val) => { @@ -1144,6 +1168,7 @@ function createEmptyVariableLevyProof(): VariableLevyProof { } const form = reactive({ + digitalAccountId: null, taxpayerNum: '', invoiceReqSerialNo: '', invoiceIssueKindCode: '82', @@ -1231,6 +1256,13 @@ function generateSerialNo() { autoSerial.value = true } +function applySelectedDigitalAccount() { + const selected = digitalAccounts.value.find((item) => item.id === form.digitalAccountId) + if (!selected) return + form.taxpayerNum = selected.taxpayerNum + form.account = selected.account +} + function confirmRemove(index: number) { const item = form.itemList[index] const itemName = item?.goodsName || `商品 #${item?.lineNo || index + 1}` @@ -1533,20 +1565,24 @@ async function handleSubmit() { } onMounted(async () => { - if (currentUser.value?.taxpayerNum) { - form.taxpayerNum = currentUser.value.taxpayerNum - } - if (currentUser.value?.account) { - form.account = currentUser.value.account - } generateSerialNo() // 加载企业信息和预设数据,自动填入销方信息 try { - const [enterpriseInfo, presetData] = await Promise.all([ + const [enterpriseInfo, presetData, accounts] = await Promise.all([ getEnterpriseInfoApi(), - getPresetDataApi() + getPresetDataApi(), + listDigitalAccountsApi() ]) + digitalAccounts.value = accounts + const defaultAccount = + accounts.find((item) => item.id === currentUser.value?.digitalAccountId) || accounts[0] + if (defaultAccount) { + form.digitalAccountId = defaultAccount.id + applySelectedDigitalAccount() + } else if (enterpriseInfo?.taxpayerNum) { + form.taxpayerNum = enterpriseInfo.taxpayerNum + } if (enterpriseInfo?.enterpriseAddress) { form.sellerAddress = enterpriseInfo.enterpriseAddress } @@ -1650,7 +1686,7 @@ watch( /** 重置表单到初始状态 */ function resetForm() { - form.taxpayerNum = currentUser.value?.taxpayerNum || '' + form.taxpayerNum = '' form.invoiceReqSerialNo = '' form.buyerName = '' form.buyerTaxpayerNum = '' @@ -1670,7 +1706,7 @@ function resetForm() { form.showSellerBank = '0' form.showBuyerAddrTel = '0' form.showSellerAddrTel = '0' - form.account = currentUser.value?.account || '' + form.account = '' form.variableLevyFlag = '' form.casherName = '' form.reviewerName = '' @@ -1687,6 +1723,7 @@ function resetForm() { form.variableLevyProofList = [] form.orderList = [] activeSidebar.value = 'basic' + applySelectedDigitalAccount() generateSerialNo() } diff --git a/web/src/features/piaotong/invoice-setting/index.vue b/web/src/features/piaotong/invoice-setting/index.vue new file mode 100644 index 0000000..fa52dce --- /dev/null +++ b/web/src/features/piaotong/invoice-setting/index.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/web/src/features/statistics/openapi/index.vue b/web/src/features/statistics/openapi/index.vue new file mode 100644 index 0000000..7a477f0 --- /dev/null +++ b/web/src/features/statistics/openapi/index.vue @@ -0,0 +1,61 @@ + + + diff --git a/web/src/features/system/dicts/index.vue b/web/src/features/system/dicts/index.vue index d675610..cd42ada 100644 --- a/web/src/features/system/dicts/index.vue +++ b/web/src/features/system/dicts/index.vue @@ -8,6 +8,7 @@
- - - 启用 @@ -340,8 +338,7 @@ const baseDetailItems = computed(() => { { label: '邮箱', value: displayValue(user.email) }, { label: '组织', value: formatOrg(user) }, { label: '状态', value: user.statusLabel || statusLabel(user.status) }, - { label: '头像', value: displayValue(user.avatar) }, - { label: 'API Key', value: displayValue(user.apiKey) } + { label: '头像', value: displayValue(user.avatar) } ] }) @@ -405,7 +402,6 @@ const editForm = reactive({ realName: '', phone: '', email: '', - apiKey: '', status: 'ENABLED' }) @@ -526,7 +522,6 @@ function resetEditForm() { editForm.realName = '' editForm.phone = '' editForm.email = '' - editForm.apiKey = '' editForm.status = 'ENABLED' } @@ -572,7 +567,6 @@ async function openEdit(row: UserListItem) { editForm.realName = detail.realName ?? '' editForm.phone = detail.phone ?? '' editForm.email = detail.email ?? '' - editForm.apiKey = detail.apiKey ?? '' editForm.status = detail.status editModal.visible = true } @@ -639,13 +633,6 @@ const columns = computed>(() => [ minWidth: 180, render: (row) => (row.roleCodes.length > 0 ? row.roleCodes.join(', ') : '-') }, - { - title: 'API Key', - key: 'apiKey', - minWidth: 220, - ellipsis: { tooltip: true }, - render: (row) => row.apiKey || '-' - }, { title: '状态', key: 'status', diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index d87c81c..adaf283 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -275,11 +275,20 @@ async function handleLogout() { height: 0; } +.layout-sider :deep(.n-layout-sider-scroll-container) { + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + .layout-sider-hidden { border-right: 0; } .brand-mark { + flex: 0 0 auto; display: flex; align-items: center; gap: 12px; @@ -321,9 +330,11 @@ async function handleLogout() { } .menu-wrap { - height: calc(100% - 64px); + flex: 1 1 auto; + min-height: 0; padding: 10px 8px 14px; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; } diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts index 76205c8..f6bf36b 100644 --- a/web/src/stores/auth.ts +++ b/web/src/stores/auth.ts @@ -1,8 +1,14 @@ import { computed, ref } from 'vue' import { defineStore } from 'pinia' -import { loginApi, logoutApi, meApi } from '@/api/auth' +import { loginApi, logoutApi, meApi, registerEnterpriseApi } from '@/api/auth' import { listDictItemsApi } from '@/api/system/dict' -import type { CurrentUserProfile, LoginRequest, MeResponse, MenuNode } from '@/types/auth' +import type { + CurrentUserProfile, + EnterpriseRegisterRequest, + LoginRequest, + MeResponse, + MenuNode +} from '@/types/auth' import type { DictItem } from '@/types/system/dict' const TOKEN_KEY = 'platform.token' @@ -59,6 +65,12 @@ export const useAuthStore = defineStore('auth', () => { await loadProfile() } + async function registerEnterprise(payload: EnterpriseRegisterRequest) { + const result = await registerEnterpriseApi(payload) + setToken(result.accessToken) + await loadProfile() + } + async function logout() { try { if (token.value) { @@ -168,6 +180,7 @@ export const useAuthStore = defineStore('auth', () => { setToken, clearAuth, login, + registerEnterprise, logout, loadProfile, hasPermission, diff --git a/web/src/style.css b/web/src/style.css index 2f22f4c..454d093 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -100,6 +100,22 @@ select { background: #ffffff; } +.page-toolbar-title { + margin: 0; + color: #111827; + font-size: 16px; + font-weight: 600; + line-height: 1.4; +} + +.page-toolbar-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-shrink: 0; +} + .toolbar-form { flex: 1; } @@ -220,4 +236,9 @@ select { flex-direction: column; align-items: stretch; } + + .page-toolbar-actions { + justify-content: flex-start; + flex-wrap: wrap; + } } diff --git a/web/src/types/auth.ts b/web/src/types/auth.ts index 5df8ae9..ba5976f 100644 --- a/web/src/types/auth.ts +++ b/web/src/types/auth.ts @@ -9,21 +9,35 @@ export interface LoginResponse { expiresIn: number } +export interface EnterpriseRegisterRequest { + taxpayerNum: string + enterpriseName: string + legalPersonName?: string | null + contactsName?: string | null + contactsEmail?: string | null + contactsPhone: string + regionCode: string + cityName?: string + enterpriseAddress?: string | null + taxRegistrationCertificate?: string | null + password: string + confirmPassword: string +} + export interface CurrentUserProfile { id: string username: string nickname?: string | null realName?: string | null orgId?: string | null + enterpriseId?: string | null + digitalAccountId?: string | null + userType: 'SYSTEM' | 'ENTERPRISE_ADMIN' | 'DIGITAL_OPERATOR' | 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 { diff --git a/web/src/types/system/user.ts b/web/src/types/system/user.ts index c7ff706..802cf0c 100644 --- a/web/src/types/system/user.ts +++ b/web/src/types/system/user.ts @@ -9,7 +9,6 @@ export interface UserListItem { status: string statusLabel?: string roleCodes: string[] - apiKey?: string | null } export interface UserDetail { @@ -27,7 +26,6 @@ export interface UserDetail { statusLabel?: string roleIds: string[] roles: UserRoleBrief[] - apiKey?: string | null tokenVersion: number lastLoginAt?: string | null lastLoginIp?: string | null diff --git a/web/src/views/auth/LoginView.vue b/web/src/views/auth/LoginView.vue index fa310f6..653a68d 100644 --- a/web/src/views/auth/LoginView.vue +++ b/web/src/views/auth/LoginView.vue @@ -4,7 +4,9 @@ @@ -12,13 +14,20 @@ 未登录或登录已失效,请重新登录。 - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ 选择图片 + + {{ registerForm.taxRegistrationCertificate ? '已选择图片' : '未选择,可不上传' }} + +
+ + 企业注册证书预览 +
+
+ + + + + + + + + + + + + + + + 注册并登录 + +
+ + + {{ isRegister ? '已有账号,返回登录' : '注册企业' }} +
@@ -35,7 +151,18 @@