From 2401b6e512f6a253a0b6a83739e7786494adee97 Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Tue, 12 May 2026 09:33:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E7=A5=A8=E5=8E=86=E5=8F=B2=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/bootstrap/DatabaseInitializer.kt | 10 +- .../ticket/dao/piaotong/EnterpriseTaxDao.kt | 320 +++++++++++-- ...erTable.kt => HistoryInvoiceBasicTable.kt} | 80 +--- .../piaotong/HistoryInvoiceGoodsTable.kt | 108 +++++ .../piaotong/HistoryInvoiceOrderTable.kt | 33 ++ .../piaotong/HistoryInvoiceVoucherTable.kt | 81 ++++ .../database/piaotong/InvoiceItemTable.kt | 3 +- ...iceRequest.kt => AskBlueInvoiceRequest.kt} | 3 +- .../entity/request/QueryInvoiceRequest.kt | 24 + .../entity/response/InvoiceDetailResponse.kt | 104 +++++ .../entity/response/InvoiceHistoryItem.kt | 77 +++ .../entity/response/QueryInvoiceResponse.kt | 227 +++++++++ .../entity/response/QueryInvoiceResult.kt | 14 + .../com/bbit/ticket/plugins/JsonPlugin.kt | 11 + .../route/piaotong/registerPTTestRoutes.kt | 62 ++- .../ticket/route/system/registerDictRoutes.kt | 2 +- .../ticket/service/piaotong/PTAuthService.kt | 70 ++- .../com/bbit/ticket/utils/net/PTClient.kt | 30 +- web/src/api/piaotong/index.ts | 190 ++++++++ .../piaotong/invoice-history/index.vue | 438 +++++++++++++++++- .../features/piaotong/invoice-issue/index.vue | 64 +-- 21 files changed, 1773 insertions(+), 178 deletions(-) rename server/src/main/kotlin/com/bbit/ticket/database/piaotong/{InvoiceOrderTable.kt => HistoryInvoiceBasicTable.kt} (71%) create mode 100644 server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceGoodsTable.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceOrderTable.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceVoucherTable.kt rename server/src/main/kotlin/com/bbit/ticket/entity/request/{InvoiceRequest.kt => AskBlueInvoiceRequest.kt} (99%) create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/request/QueryInvoiceRequest.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceDetailResponse.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceHistoryItem.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResponse.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResult.kt create mode 100644 server/src/main/kotlin/com/bbit/ticket/plugins/JsonPlugin.kt diff --git a/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt index a1a5637..619b3a8 100644 --- a/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt +++ b/server/src/main/kotlin/com/bbit/ticket/bootstrap/DatabaseInitializer.kt @@ -1,7 +1,10 @@ package com.bbit.ticket.bootstrap +import com.bbit.ticket.database.piaotong.HistoryInvoiceBasicTable +import com.bbit.ticket.database.piaotong.HistoryInvoiceGoodsTable +import com.bbit.ticket.database.piaotong.HistoryInvoiceOrderTable +import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable import com.bbit.ticket.database.piaotong.InvoiceItemTable -import com.bbit.ticket.database.piaotong.InvoiceOrderTable import com.bbit.ticket.database.system.SysApiAccessLogTable import com.bbit.ticket.database.system.SysDictItemTable import com.bbit.ticket.database.system.SysDictTypeTable @@ -33,7 +36,10 @@ object DatabaseInitializer { SysOperationLogTable, SysApiAccessLogTable, InvoiceItemTable, - InvoiceOrderTable, + HistoryInvoiceBasicTable, + HistoryInvoiceGoodsTable, + HistoryInvoiceVoucherTable, + HistoryInvoiceOrderTable, ) // 先通过 Exposed 生成迁移 SQL,再逐条执行,避免启动时静默跳过缺失表或字段。 transaction { 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 index 0e80059..e6e8b5f 100644 --- a/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseTaxDao.kt +++ b/server/src/main/kotlin/com/bbit/ticket/dao/piaotong/EnterpriseTaxDao.kt @@ -2,31 +2,43 @@ package com.bbit.ticket.dao.piaotong +import com.bbit.ticket.database.piaotong.HistoryInvoiceBasicTable +import com.bbit.ticket.database.piaotong.HistoryInvoiceGoodsTable +import com.bbit.ticket.database.piaotong.HistoryInvoiceOrderTable +import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable import com.bbit.ticket.database.piaotong.InvoiceItemTable -import com.bbit.ticket.database.piaotong.InvoiceOrderTable import com.bbit.ticket.database.system.SysUserTable -import com.bbit.ticket.entity.request.InvoiceRequest +import com.bbit.ticket.entity.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 kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import com.bbit.ticket.utils.formatDateTime +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.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.insertAndGetId -import org.jetbrains.exposed.v1.jdbc.insertReturning import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.update import java.math.BigDecimal -import java.text.DecimalFormat +import java.time.LocalDateTime import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +private fun pageOffset(page: Int, pageSize: Int): Long = ((page - 1) * pageSize).toLong() + object EnterpriseTaxDao { // ============================================= @@ -129,36 +141,119 @@ object EnterpriseTaxDao { } } - fun addInvoice(userId: Uuid, req: InvoiceRequest) { + // ============================================= + // 开票历史查询 + // ============================================= + + /** + * 分页查询开票历史 + */ + fun invoiceHistory(userId: Uuid, page: Int, pageSize: Int): PageResult { + val where = HistoryInvoiceBasicTable.userId eq userId + val total = HistoryInvoiceBasicTable.selectAll().where { where }.count() + val rows = HistoryInvoiceBasicTable.selectAll().where { where } + .orderBy(HistoryInvoiceBasicTable.createdAt, SortOrder.DESC) + .limit(pageSize) + .offset(pageOffset(page, pageSize)) + .toList() + return PageResult( + items = rows.map { row -> + InvoiceHistoryItem( + id = row[HistoryInvoiceBasicTable.id], + invoiceReqSerialNo = row[HistoryInvoiceBasicTable.invoiceReqSerialNo], + taxpayerNum = row[HistoryInvoiceBasicTable.taxpayerNum], + invoiceKindCode = row[HistoryInvoiceBasicTable.invoiceKindCode], + buyerName = row[HistoryInvoiceBasicTable.buyerName], + buyerTaxpayerNum = row[HistoryInvoiceBasicTable.buyerTaxpayerNum], + buyerAddress = row[HistoryInvoiceBasicTable.buyerAddress], + buyerTel = row[HistoryInvoiceBasicTable.buyerTel], + buyerBankName = row[HistoryInvoiceBasicTable.buyerBankName], + buyerBankAccount = row[HistoryInvoiceBasicTable.buyerBankAccount], + amount = row[HistoryInvoiceBasicTable.amount].toPlainString(), + taxAmount = row[HistoryInvoiceBasicTable.taxAmount].toPlainString(), + totalAmount = row[HistoryInvoiceBasicTable.totalAmount].toPlainString(), + invoiceNo = row[HistoryInvoiceBasicTable.invoiceNo], + invoiceCode = row[HistoryInvoiceBasicTable.invoiceCode], + electronicInvoiceNo = row[HistoryInvoiceBasicTable.electronicInvoiceNo], + issuedAt = formatDateTime(row[HistoryInvoiceBasicTable.issuedAt]), + status = row[HistoryInvoiceBasicTable.status], + pdfUrl = row[HistoryInvoiceBasicTable.pdfUrl], + ofdUrl = row[HistoryInvoiceBasicTable.ofdUrl], + xmlUrl = row[HistoryInvoiceBasicTable.xmlUrl], + tradeNo = row[HistoryInvoiceBasicTable.tradeNo], + remark = row[HistoryInvoiceBasicTable.remark], + definedData = row[HistoryInvoiceBasicTable.definedData], + createdAt = formatDateTime(row[HistoryInvoiceBasicTable.createdAt]) ?: "", + errorMessage = row[HistoryInvoiceBasicTable.errorMessage], + ) + }, + page = page, + pageSize = pageSize, + total = total, + ) + } + + fun addInvoice(userId: Uuid, req: AskInvoiceRequest) { val now = OffsetDateTime.now() - val row = InvoiceOrderTable.insert { - it[InvoiceOrderTable.userId] = userId - it[InvoiceOrderTable.invoiceReqSerialNo] = req.invoiceReqSerialNo - it[InvoiceOrderTable.taxpayerNum] = req.taxpayerNum - it[InvoiceOrderTable.invoiceKindCode] = req.invoiceIssueKindCode - it[InvoiceOrderTable.buyerName] = req.buyerName - it[InvoiceOrderTable.buyerTaxpayerNum] = req.buyerTaxpayerNum - it[InvoiceOrderTable.buyerAddress] = req.buyerAddress - it[InvoiceOrderTable.buyerTel] = req.buyerTel - it[InvoiceOrderTable.buyerBankName] = req.buyerBankName - it[InvoiceOrderTable.buyerBankAccount] = req.buyerBankAccount - it[InvoiceOrderTable.remark] = req.remark - it[InvoiceOrderTable.definedData] = req.definedData - it[InvoiceOrderTable.tradeNo] = req.tradeNo - it[InvoiceOrderTable.taxAmount] = BigDecimal.ZERO - it[InvoiceOrderTable.amount] = BigDecimal.ZERO - it[InvoiceOrderTable.totalAmount] = BigDecimal.ZERO - it[InvoiceOrderTable.requestJson] = Json.encodeToString(req) - it[InvoiceOrderTable.status] = "PENDING" - it[InvoiceOrderTable.createdAt] = now - it[InvoiceOrderTable.createdBy] = userId + + // 1. 插入 HistoryInvoiceBasicTable(基本信息历史快照) + val basicRow = HistoryInvoiceBasicTable.insert { + it[HistoryInvoiceBasicTable.userId] = userId + it[HistoryInvoiceBasicTable.invoiceReqSerialNo] = req.invoiceReqSerialNo + it[HistoryInvoiceBasicTable.taxpayerNum] = req.taxpayerNum + it[HistoryInvoiceBasicTable.invoiceKindCode] = req.invoiceIssueKindCode + it[HistoryInvoiceBasicTable.invoiceType] = "BLUE" + it[HistoryInvoiceBasicTable.buyerName] = req.buyerName + it[HistoryInvoiceBasicTable.buyerTaxpayerNum] = req.buyerTaxpayerNum + it[HistoryInvoiceBasicTable.buyerAddress] = req.buyerAddress + it[HistoryInvoiceBasicTable.buyerTel] = req.buyerTel + it[HistoryInvoiceBasicTable.buyerBankName] = req.buyerBankName + it[HistoryInvoiceBasicTable.buyerBankAccount] = req.buyerBankAccount + it[HistoryInvoiceBasicTable.remark] = req.remark + it[HistoryInvoiceBasicTable.definedData] = req.definedData + it[HistoryInvoiceBasicTable.tradeNo] = req.tradeNo + it[HistoryInvoiceBasicTable.amount] = req.itemList.sumOf { it.invoiceAmount.toBigDecimal() } + it[HistoryInvoiceBasicTable.taxAmount] = req.itemList.sumOf { it.taxRateAmount?.toBigDecimalOrNull() ?: BigDecimal.ZERO } + it[HistoryInvoiceBasicTable.totalAmount] = req.itemList.sumOf { item -> + val amount = item.invoiceAmount.toBigDecimal() + val tax = item.taxRateAmount?.toBigDecimalOrNull() ?: BigDecimal.ZERO + amount + tax + } + it[HistoryInvoiceBasicTable.status] = "PENDING" + it[HistoryInvoiceBasicTable.createdAt] = now + it[HistoryInvoiceBasicTable.createdBy] = userId } - val invoiceId = row[InvoiceOrderTable.id] + val historyBasicId = basicRow[HistoryInvoiceBasicTable.id] + + // 2. 插入商品明细(HistoryInvoiceGoodsTable + InvoiceItemTable) var lineNo = 1 for (item in req.itemList) { + // 历史商品明细 + HistoryInvoiceGoodsTable.insert { + it[HistoryInvoiceGoodsTable.basicId] = historyBasicId + it[HistoryInvoiceGoodsTable.lineNo] = lineNo + it[HistoryInvoiceGoodsTable.goodsName] = item.goodsName + it[HistoryInvoiceGoodsTable.taxClassificationCode] = item.taxClassificationCode + it[HistoryInvoiceGoodsTable.specificationModel] = item.specificationModel + it[HistoryInvoiceGoodsTable.meteringUnit] = item.meteringUnit + it[HistoryInvoiceGoodsTable.quantity] = item.quantity?.toBigDecimalOrNull() + it[HistoryInvoiceGoodsTable.unitPrice] = item.unitPrice?.toBigDecimalOrNull() + it[HistoryInvoiceGoodsTable.invoiceAmount] = item.invoiceAmount.toBigDecimal() + it[HistoryInvoiceGoodsTable.taxRateValue] = item.taxRateValue.toBigDecimal() + it[HistoryInvoiceGoodsTable.taxRateAmount] = item.taxRateAmount?.toBigDecimalOrNull() + it[HistoryInvoiceGoodsTable.includeTaxFlag] = item.includeTaxFlag == "1" + it[HistoryInvoiceGoodsTable.discountAmount] = item.discountAmount?.toBigDecimalOrNull() + it[HistoryInvoiceGoodsTable.zeroTaxFlag] = item.zeroTaxFlag + it[HistoryInvoiceGoodsTable.preferentialPolicyFlag] = item.preferentialPolicyFlag + it[HistoryInvoiceGoodsTable.vatSpecialManage] = item.vatSpecialManage + it[HistoryInvoiceGoodsTable.deductionAmount] = item.deductionAmount?.toBigDecimalOrNull() + it[HistoryInvoiceGoodsTable.createdAt] = now + } + + // 当前商品明细 InvoiceItemTable.insert { - it[InvoiceItemTable.invoiceId] = invoiceId - it[InvoiceItemTable.lineNo] = lineNo++ + it[InvoiceItemTable.invoiceId] = historyBasicId + it[InvoiceItemTable.lineNo] = lineNo it[InvoiceItemTable.goodsName] = item.goodsName it[InvoiceItemTable.taxClassificationCode] = item.taxClassificationCode it[InvoiceItemTable.specificationModel] = item.specificationModel @@ -176,6 +271,167 @@ object EnterpriseTaxDao { it[InvoiceItemTable.deductionAmount] = item.deductionAmount?.toBigDecimalOrNull() it[InvoiceItemTable.createdAt] = now } + lineNo++ + } + + // 3. 插入差额征税凭证明细(HistoryInvoiceVoucherTable) + if (!req.variableLevyProofList.isNullOrEmpty()) { + for (proof in req.variableLevyProofList) { + HistoryInvoiceVoucherTable.insert { + it[HistoryInvoiceVoucherTable.basicId] = historyBasicId + it[HistoryInvoiceVoucherTable.proofType] = proof.proofType + it[HistoryInvoiceVoucherTable.electronicInvoiceNo] = proof.electronicInvoiceNo + it[HistoryInvoiceVoucherTable.invoiceCode] = proof.invoiceCode + it[HistoryInvoiceVoucherTable.invoiceNo] = proof.invoiceNo + it[HistoryInvoiceVoucherTable.proofNo] = proof.proofNo + it[HistoryInvoiceVoucherTable.issueDate] = proof.issueDate + it[HistoryInvoiceVoucherTable.proofAmount] = proof.proofAmount.toBigDecimal() + it[HistoryInvoiceVoucherTable.deductionAmount] = proof.deductionAmount.toBigDecimal() + it[HistoryInvoiceVoucherTable.proofRemark] = proof.proofRemark + it[HistoryInvoiceVoucherTable.his_source] = proof.source + it[HistoryInvoiceVoucherTable.createdAt] = now + } + } + } + + // 4. 插入关联单据(HistoryInvoiceOrderTable) + if (!req.orderList.isNullOrEmpty()) { + for (order in req.orderList) { + HistoryInvoiceOrderTable.insert { + it[HistoryInvoiceOrderTable.basicId] = historyBasicId + it[HistoryInvoiceOrderTable.orderNo] = order.orderNo + it[HistoryInvoiceOrderTable.createdAt] = now + } + } } } + + /** + * 根据票通查询结果更新发票状态 + */ + fun updateInvoiceStatus( + invoiceReqSerialNo: String, + code: String, + msg: String, + invoiceNo: String?, + invoiceCode: String?, + electronicInvoiceNo: String?, + invoiceDate: String?, + tradeNo: String?, + ) { + HistoryInvoiceBasicTable.update({ + HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo + }) { + val status = when (code) { + "0000" -> "SUCCESS" + "7777" -> "PROCESSING" + "9999" -> "FAILED" + "6666" -> "PENDING" + else -> "PROCESSING" + } + it[HistoryInvoiceBasicTable.status] = status + // 失败时将 msg 写入错误信息,成功时清空 + it[HistoryInvoiceBasicTable.errorMessage] = if (status == "FAILED") msg else null + if (!invoiceNo.isNullOrBlank()) it[HistoryInvoiceBasicTable.invoiceNo] = invoiceNo + if (!invoiceCode.isNullOrBlank()) it[HistoryInvoiceBasicTable.invoiceCode] = invoiceCode + if (!electronicInvoiceNo.isNullOrBlank()) it[HistoryInvoiceBasicTable.electronicInvoiceNo] = electronicInvoiceNo + if (!invoiceDate.isNullOrBlank()) { + val localDt = LocalDateTime.parse(invoiceDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + it[HistoryInvoiceBasicTable.issuedAt] = localDt.atZone(ZoneId.systemDefault()).toOffsetDateTime() + } + if (!tradeNo.isNullOrBlank()) it[HistoryInvoiceBasicTable.tradeNo] = tradeNo + it[HistoryInvoiceBasicTable.updatedAt] = OffsetDateTime.now() + } + } + + /** + * 查询发票完整详情(含商品明细、差额征税凭证、关联单据) + */ + fun invoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? { + val basicRow = HistoryInvoiceBasicTable.selectAll().where { + (HistoryInvoiceBasicTable.userId eq userId) and + (HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) + }.singleOrNull() ?: return null + + val basicId = basicRow[HistoryInvoiceBasicTable.id] + + // 查询商品明细 + val goodsRows = HistoryInvoiceGoodsTable.selectAll().where { + HistoryInvoiceGoodsTable.basicId eq basicId + }.orderBy(HistoryInvoiceGoodsTable.lineNo).toList() + + // 查询差额征税凭证 + val voucherRows = HistoryInvoiceVoucherTable.selectAll().where { + HistoryInvoiceVoucherTable.basicId eq basicId + }.toList() + + // 查询关联单据 + val orderRows = HistoryInvoiceOrderTable.selectAll().where { + HistoryInvoiceOrderTable.basicId eq basicId + }.toList() + + return InvoiceDetailResponse( + id = basicRow[HistoryInvoiceBasicTable.id], + invoiceReqSerialNo = basicRow[HistoryInvoiceBasicTable.invoiceReqSerialNo], + taxpayerNum = basicRow[HistoryInvoiceBasicTable.taxpayerNum], + invoiceKindCode = basicRow[HistoryInvoiceBasicTable.invoiceKindCode], + invoiceType = basicRow[HistoryInvoiceBasicTable.invoiceType], + buyerName = basicRow[HistoryInvoiceBasicTable.buyerName], + buyerTaxpayerNum = basicRow[HistoryInvoiceBasicTable.buyerTaxpayerNum], + buyerAddress = basicRow[HistoryInvoiceBasicTable.buyerAddress], + buyerTel = basicRow[HistoryInvoiceBasicTable.buyerTel], + buyerBankName = basicRow[HistoryInvoiceBasicTable.buyerBankName], + buyerBankAccount = basicRow[HistoryInvoiceBasicTable.buyerBankAccount], + amount = basicRow[HistoryInvoiceBasicTable.amount].toPlainString(), + taxAmount = basicRow[HistoryInvoiceBasicTable.taxAmount].toPlainString(), + totalAmount = basicRow[HistoryInvoiceBasicTable.totalAmount].toPlainString(), + invoiceNo = basicRow[HistoryInvoiceBasicTable.invoiceNo], + invoiceCode = basicRow[HistoryInvoiceBasicTable.invoiceCode], + electronicInvoiceNo = basicRow[HistoryInvoiceBasicTable.electronicInvoiceNo], + issuedAt = formatDateTime(basicRow[HistoryInvoiceBasicTable.issuedAt]), + status = basicRow[HistoryInvoiceBasicTable.status], + errorMessage = basicRow[HistoryInvoiceBasicTable.errorMessage], + pdfUrl = basicRow[HistoryInvoiceBasicTable.pdfUrl], + ofdUrl = basicRow[HistoryInvoiceBasicTable.ofdUrl], + xmlUrl = basicRow[HistoryInvoiceBasicTable.xmlUrl], + tradeNo = basicRow[HistoryInvoiceBasicTable.tradeNo], + remark = basicRow[HistoryInvoiceBasicTable.remark], + definedData = basicRow[HistoryInvoiceBasicTable.definedData], + createdAt = formatDateTime(basicRow[HistoryInvoiceBasicTable.createdAt]) ?: "", + goodsList = goodsRows.map { row -> + InvoiceDetailGoods( + lineNo = row[HistoryInvoiceGoodsTable.lineNo], + goodsName = row[HistoryInvoiceGoodsTable.goodsName], + taxClassificationCode = row[HistoryInvoiceGoodsTable.taxClassificationCode], + specificationModel = row[HistoryInvoiceGoodsTable.specificationModel], + meteringUnit = row[HistoryInvoiceGoodsTable.meteringUnit], + quantity = row[HistoryInvoiceGoodsTable.quantity]?.toPlainString(), + unitPrice = row[HistoryInvoiceGoodsTable.unitPrice]?.toPlainString(), + invoiceAmount = row[HistoryInvoiceGoodsTable.invoiceAmount].toPlainString(), + taxRateValue = row[HistoryInvoiceGoodsTable.taxRateValue].toPlainString(), + taxRateAmount = row[HistoryInvoiceGoodsTable.taxRateAmount]?.toPlainString(), + includeTaxFlag = row[HistoryInvoiceGoodsTable.includeTaxFlag], + ) + }, + voucherList = voucherRows.map { row -> + InvoiceDetailVoucher( + proofType = row[HistoryInvoiceVoucherTable.proofType], + electronicInvoiceNo = row[HistoryInvoiceVoucherTable.electronicInvoiceNo], + invoiceCode = row[HistoryInvoiceVoucherTable.invoiceCode], + invoiceNo = row[HistoryInvoiceVoucherTable.invoiceNo], + proofNo = row[HistoryInvoiceVoucherTable.proofNo], + issueDate = row[HistoryInvoiceVoucherTable.issueDate], + proofAmount = row[HistoryInvoiceVoucherTable.proofAmount].toPlainString(), + deductionAmount = row[HistoryInvoiceVoucherTable.deductionAmount].toPlainString(), + proofRemark = row[HistoryInvoiceVoucherTable.proofRemark], + source = row[HistoryInvoiceVoucherTable.his_source], + ) + }, + orderList = orderRows.map { row -> + InvoiceDetailOrder( + orderNo = row[HistoryInvoiceOrderTable.orderNo], + ) + }, + ) + } } diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceOrderTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceBasicTable.kt similarity index 71% rename from server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceOrderTable.kt rename to server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceBasicTable.kt index 1bdb347..9972ca4 100644 --- a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceOrderTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceBasicTable.kt @@ -5,8 +5,12 @@ import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +/** + * 发票基本信息表(历史) + * 存储发票开具时的基本信息快照 + */ @OptIn(ExperimentalUuidApi::class) -object InvoiceOrderTable : Table("invoice_order") { +object HistoryInvoiceBasicTable : Table("history_invoice_basic") { val id = uuid("id").clientDefault { Uuid.Companion.random() } @@ -30,29 +34,23 @@ object InvoiceOrderTable : Table("invoice_order") { * 发票种类 * 81:电子发票(增值税专用发票) * 82:电子发票(普通发票) - * 87:数电纸质发票(机动车销售统一发票) - * 10:增值税电子普通发票 - * 08:增值税电子专用发票 - * 04:增值税普通发票 - * 01:增值税专用发票 - * */ val invoiceKindCode = varchar("invoice_kind_code", 8) + /** + * 发票类型 + * BLUE:蓝票 + * RED:红票 + */ + val invoiceType = varchar("invoice_type", 8).default("BLUE") + // ========================= - // 购买方信息(历史快照) + // 购买方信息 // ========================= - /** - * 购买方名称 - */ val buyerName = varchar("buyer_name", 200) - /** - * 购买方税号 - */ - val buyerTaxpayerNum = varchar("buyer_taxpayer_num", 32) - .nullable() + val buyerTaxpayerNum = varchar("buyer_taxpayer_num", 32).nullable() val buyerAddress = varchar("buyer_address", 255).nullable() @@ -85,82 +83,36 @@ object InvoiceOrderTable : Table("invoice_order") { // 开票结果 // ========================= - /** - * 发票号码 - */ val invoiceNo = varchar("invoice_no", 64).nullable() - /** - * 发票代码 - */ val invoiceCode = varchar("invoice_code", 64).nullable() - /** - * 数电票号码 - */ val electronicInvoiceNo = varchar("electronic_invoice_no", 64).nullable() - /** - * 开票时间 - */ val issuedAt = timestampWithTimeZone("issued_at").nullable() - /** - * 开票状态 - */ - val status = varchar("status", 32) .default("PENDING") + val status = varchar("status", 32).default("PENDING") - /** - * 第三方平台返回错误 - */ val errorMessage = text("error_message").nullable() - /** - * 原始请求报文 - */ - val requestJson = text("request_json").nullable() - - /** - * 原始响应报文 - */ - val responseJson = text("response_json").nullable() - // ========================= // 文件 // ========================= - /** - * PDF地址 - */ val pdfUrl = text("pdf_url").nullable() - /** - * OFD地址 - */ val ofdUrl = text("ofd_url").nullable() - /** - * XML地址 - */ val xmlUrl = text("xml_url").nullable() // ========================= // 业务字段 // ========================= - /** - * 订单号 - */ val tradeNo = varchar("trade_no", 128).nullable() - /** - * 备注 - */ val remark = text("remark").nullable() - /** - * 自定义透传数据 - */ val definedData = text("defined_data").nullable() // ========================= @@ -178,4 +130,4 @@ object InvoiceOrderTable : Table("invoice_order") { val deletedAt = timestampWithTimeZone("deleted_at").nullable() override val primaryKey = PrimaryKey(id) -} \ No newline at end of file +} diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceGoodsTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceGoodsTable.kt new file mode 100644 index 0000000..c10a229 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceGoodsTable.kt @@ -0,0 +1,108 @@ +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 HistoryInvoiceGoodsTable : Table("history_invoice_goods") { + + val id = uuid("id").clientDefault { Uuid.Companion.random() } + + /** + * 关联历史发票基本信息ID + */ + val basicId = uuid("basic_id").references(HistoryInvoiceBasicTable.id) + + /** + * 行号 + */ + val lineNo = integer("line_no") + + /** + * 商品名称 + */ + val goodsName = varchar("goods_name", 200) + + /** + * 税收分类编码 + */ + val taxClassificationCode = varchar("tax_classification_code", 64) + + /** + * 规格型号 + */ + val specificationModel = varchar("specification_model", 100).nullable() + + /** + * 单位 + */ + val meteringUnit = varchar("metering_unit", 32).nullable() + + /** + * 数量 + */ + val quantity = decimal("quantity", 18, 8).nullable() + + /** + * 单价 + */ + val unitPrice = decimal("unit_price", 18, 8).nullable() + + /** + * 金额 + */ + val invoiceAmount = decimal("invoice_amount", 18, 2) + + /** + * 税率 + */ + val taxRateValue = decimal("tax_rate_value", 8, 4) + + /** + * 税额 + */ + val taxRateAmount = decimal("tax_rate_amount", 18, 2).nullable() + + /** + * 是否含税 + */ + val includeTaxFlag = bool("include_tax_flag").default(false) + + /** + * 折扣金额 + */ + val discountAmount = decimal("discount_amount", 18, 2).nullable() + + /** + * 零税率标识 + */ + val zeroTaxFlag = varchar("zero_tax_flag", 8).nullable() + + /** + * 优惠政策标识 + */ + val preferentialPolicyFlag = varchar("preferential_policy_flag", 8).nullable() + + /** + * 增值税特殊管理 + */ + val vatSpecialManage = varchar("vat_special_manage", 100).nullable() + + /** + * 差额扣除金额 + */ + val deductionAmount = decimal("deduction_amount", 18, 2).nullable() + + /** + * 创建时间 + */ + val createdAt = timestampWithTimeZone("created_at") + + override val primaryKey = PrimaryKey(id) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceOrderTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceOrderTable.kt new file mode 100644 index 0000000..698e835 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceOrderTable.kt @@ -0,0 +1,33 @@ +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 HistoryInvoiceOrderTable : Table("history_invoice_order") { + + val id = uuid("id").clientDefault { Uuid.Companion.random() } + + /** + * 关联历史发票基本信息ID + */ + val basicId = uuid("basic_id").references(HistoryInvoiceBasicTable.id) + + /** + * 业务单据号 + */ + val orderNo = varchar("order_no", 128) + + /** + * 创建时间 + */ + val createdAt = timestampWithTimeZone("created_at") + + override val primaryKey = PrimaryKey(id) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceVoucherTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceVoucherTable.kt new file mode 100644 index 0000000..f0959a5 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/HistoryInvoiceVoucherTable.kt @@ -0,0 +1,81 @@ +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 HistoryInvoiceVoucherTable : Table("history_invoice_voucher") { + + val id = uuid("id").clientDefault { Uuid.Companion.random() } + + /** + * 关联历史发票基本信息ID + */ + val basicId = uuid("basic_id").references(HistoryInvoiceBasicTable.id) + + /** + * 凭证类型 + * 01:数电票;02:增值税专用发票;03:增值税普通发票 + * 04:营业税发票;05:财政票据;06:法院裁决书 + * 07:契税完税凭证;08:其他发票类;09:其他扣除凭证 + */ + val proofType = varchar("proof_type", 8) + + /** + * 数电票号码(凭证类型为01时必填) + */ + val electronicInvoiceNo = varchar("electronic_invoice_no", 64).nullable() + + /** + * 发票代码(凭证类型02/03/04时必填) + */ + val invoiceCode = varchar("invoice_code", 64).nullable() + + /** + * 发票号码(凭证类型02/03/04时必填) + */ + val invoiceNo = varchar("invoice_no", 64).nullable() + + /** + * 凭证号码 + */ + val proofNo = varchar("proof_no", 64).nullable() + + /** + * 开具日期(格式yyyy-MM-dd) + */ + val issueDate = varchar("issue_date", 16).nullable() + + /** + * 凭证合计金额 + */ + val proofAmount = decimal("proof_amount", 18, 2) + + /** + * 本次扣除金额 + */ + val deductionAmount = decimal("deduction_amount", 18, 2) + + /** + * 备注 + */ + val proofRemark = text("proof_remark").nullable() + + /** + * 来源 + */ + val his_source = varchar("source", 32).nullable() + + /** + * 创建时间 + */ + val createdAt = timestampWithTimeZone("created_at") + + override val primaryKey = PrimaryKey(id) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt index 8710ed3..d4dc05a 100644 --- a/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt +++ b/server/src/main/kotlin/com/bbit/ticket/database/piaotong/InvoiceItemTable.kt @@ -1,5 +1,6 @@ package com.bbit.ticket.database.piaotong +import com.bbit.ticket.database.piaotong.HistoryInvoiceBasicTable import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone import kotlin.uuid.ExperimentalUuidApi @@ -13,7 +14,7 @@ object InvoiceItemTable : Table("invoice_item") { /** * 发票ID */ - val invoiceId = uuid("invoice_id").references(InvoiceOrderTable.id) + val invoiceId = uuid("invoice_id").references(HistoryInvoiceBasicTable.id) /** * 行号 diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/InvoiceRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/AskBlueInvoiceRequest.kt similarity index 99% rename from server/src/main/kotlin/com/bbit/ticket/entity/request/InvoiceRequest.kt rename to server/src/main/kotlin/com/bbit/ticket/entity/request/AskBlueInvoiceRequest.kt index 735688e..d321e07 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/InvoiceRequest.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/AskBlueInvoiceRequest.kt @@ -1,13 +1,12 @@ package com.bbit.ticket.entity.request -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** * 数电发票开票请求 */ @Serializable -data class InvoiceRequest( +data class AskInvoiceRequest( /** * 销方纳税人识别号(销售方税号) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/QueryInvoiceRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/QueryInvoiceRequest.kt new file mode 100644 index 0000000..9bd9354 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/QueryInvoiceRequest.kt @@ -0,0 +1,24 @@ +package com.bbit.ticket.entity.request + +import kotlinx.serialization.Serializable + +/** + * 发票申请请求 + */ +@Serializable +data class QueryInvoiceRequest( + + /** + * 纳税人识别号 + * + * 一般为企业税号、统一社会信用代码 + */ + val taxpayerNum: String, + + /** + * 发票申请流水号 + * + * 用于标识一次唯一的开票申请请求 + */ + val invoiceReqSerialNo: String +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceDetailResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceDetailResponse.kt new file mode 100644 index 0000000..031e99e --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceDetailResponse.kt @@ -0,0 +1,104 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package com.bbit.ticket.entity.response + +import kotlinx.serialization.Serializable +import kotlin.uuid.Uuid + +/** + * 发票完整详情(含商品明细、差额征税凭证、关联单据) + */ +@Serializable +data class InvoiceDetailResponse( + // ===== 基本信息 ===== + val id: Uuid, + val invoiceReqSerialNo: String, + val taxpayerNum: String, + val invoiceKindCode: String, + val invoiceType: String, + + // ===== 购买方信息 ===== + val buyerName: String, + val buyerTaxpayerNum: String? = null, + val buyerAddress: String? = null, + val buyerTel: String? = null, + val buyerBankName: String? = null, + val buyerBankAccount: String? = null, + + // ===== 金额 ===== + val amount: String, + val taxAmount: String, + val totalAmount: String, + + // ===== 开票结果 ===== + val invoiceNo: String? = null, + val invoiceCode: String? = null, + val electronicInvoiceNo: String? = null, + val issuedAt: String? = null, + val status: String, + val errorMessage: String? = null, + + // ===== 文件 ===== + val pdfUrl: String? = null, + val ofdUrl: String? = null, + val xmlUrl: String? = null, + + // ===== 业务字段 ===== + val tradeNo: String? = null, + val remark: String? = null, + val definedData: String? = null, + + // ===== 审计 ===== + val createdAt: String, + + // ===== 子表数据 ===== + /** 商品明细 */ + val goodsList: List, + /** 差额征税凭证明细 */ + val voucherList: List, + /** 关联单据 */ + val orderList: List, +) + +/** + * 发票商品明细(详情) + */ +@Serializable +data class InvoiceDetailGoods( + val lineNo: Int, + val goodsName: String, + val taxClassificationCode: String, + val specificationModel: String? = null, + val meteringUnit: String? = null, + val quantity: String? = null, + val unitPrice: String? = null, + val invoiceAmount: String, + val taxRateValue: String, + val taxRateAmount: String? = null, + val includeTaxFlag: Boolean = false, +) + +/** + * 差额征税凭证明细(详情) + */ +@Serializable +data class InvoiceDetailVoucher( + val proofType: String, + val electronicInvoiceNo: String? = null, + val invoiceCode: String? = null, + val invoiceNo: String? = null, + val proofNo: String? = null, + val issueDate: String? = null, + val proofAmount: String, + val deductionAmount: String, + val proofRemark: String? = null, + val source: String? = null, +) + +/** + * 关联单据(详情) + */ +@Serializable +data class InvoiceDetailOrder( + val orderNo: String, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceHistoryItem.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceHistoryItem.kt new file mode 100644 index 0000000..e47e47f --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/InvoiceHistoryItem.kt @@ -0,0 +1,77 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package com.bbit.ticket.entity.response + +import kotlinx.serialization.Serializable +import kotlin.uuid.Uuid + +/** + * 发票历史记录(列表页展示项) + */ +@Serializable +data class InvoiceHistoryItem( + val id: Uuid, + + /** 发票请求流水号 */ + val invoiceReqSerialNo: String, + /** 销方税号 */ + val taxpayerNum: String, + /** 发票种类 */ + val invoiceKindCode: String, + + // ===== 购买方信息 ===== + /** 购买方名称 */ + val buyerName: String, + /** 购买方税号 */ + val buyerTaxpayerNum: String? = null, + /** 购买方地址 */ + val buyerAddress: String? = null, + /** 购买方电话 */ + val buyerTel: String? = null, + /** 购买方开户银行 */ + val buyerBankName: String? = null, + /** 购买方银行账号 */ + val buyerBankAccount: String? = null, + + // ===== 金额 ===== + /** 不含税金额 */ + val amount: String, + /** 税额 */ + val taxAmount: String, + /** 含税总金额 */ + val totalAmount: String, + + // ===== 开票结果 ===== + /** 发票号码 */ + val invoiceNo: String? = null, + /** 发票代码 */ + val invoiceCode: String? = null, + /** 数电票号码 */ + val electronicInvoiceNo: String? = null, + /** 开票时间 */ + val issuedAt: String? = null, + /** 开票状态 */ + val status: String, + + // ===== 文件 ===== + /** PDF 地址 */ + val pdfUrl: String? = null, + /** OFD 地址 */ + val ofdUrl: String? = null, + /** XML 地址 */ + val xmlUrl: String? = null, + + // ===== 业务字段 ===== + /** 订单号 */ + val tradeNo: String? = null, + /** 备注 */ + val remark: String? = null, + /** 自定义透传数据 */ + val definedData: String? = null, + + // ===== 审计字段 ===== + /** 创建时间 */ + val createdAt: String, + /** 错误信息 */ + val errorMessage: String? = null, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResponse.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResponse.kt new file mode 100644 index 0000000..b131067 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResponse.kt @@ -0,0 +1,227 @@ +package com.bbit.ticket.entity.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * 发票开具结果响应 + */ + +@Serializable +data class QueryInvoiceResponse( + + /** + * 销售方纳税人识别号 + */ + @SerialName("taxpayerNum") + val taxpayerNum: String, + + /** + * 发票请求流水号 + * 用于关联本次开票请求 + */ + @SerialName("invoiceReqSerialNo") + val invoiceReqSerialNo: String, + + /** + * 开票类型 + * + * 1: 蓝票 + * 2: 红票 + */ + @SerialName("invoiceType") + val invoiceType: String? = null, + + + /** + * 发票种类代码 + * + * 81: 数电票(增值税专用发票) + * 82: 数电票(普通发票) + * 83: 数电票(机动车销售统一发票) + * 10: 增值税电子普通发票 + * 08: 增值税电子专用发票 + */ + @SerialName("invoiceKind") + val invoiceKind: String, + + /** + * 发票状态码 + * + * 0000: 开票成功 + * 6666: 未开票 + * 7777: 开票中 + * 9999: 开票失败 + * 3999: 开票失败,需要扫码或短信认证 + * 4999: 红字发票确认单申请中 + * 5999: 红字发票确认单审核中 + */ + @SerialName("code") + val code: String, + + /** + * 发票状态描述 + * 一般为成功提示或失败原因 + */ + @SerialName("msg") + val msg: String, + + /** + * 数电票开票通道 + * + * 0: RPA电子税局开具 + * 1: 乐企自用 + * 4: 乐企联用(腾讯) + * 5: 乐企联用(支付宝) + */ + @SerialName("invIssueChannel") + val invIssueChannel: String? = null, + + /** + * 开票人税局账号 + * 一般为手机号或身份证号 + */ + @SerialName("account") + val account: String? = null, + + /** + * 实名认证二维码图片 + * Base64字符串 + */ + @SerialName("authenticationQrcode") + val authenticationQrcode: String? = null, + + /** + * 实名认证二维码认证ID + */ + @SerialName("authId") + val authId: String? = null, + + /** + * 订单号 + * 开票成功时通常会返回 + */ + @SerialName("tradeNo") + val tradeNo: String? = null, + + /** + * 自定义透传数据 + * 通常用于业务侧关联 + */ + @SerialName("definedData") + val definedData: String? = null, + + /** + * 发票二维码内容 + */ + @SerialName("qrCode") + val qrCode: String? = null, + + /** + * 发票代码 + */ + @SerialName("invoiceCode") + val invoiceCode: String? = null, + + /** + * 发票号码 + */ + @SerialName("invoiceNo") + val invoiceNo: String? = null, + + /** + * 数电发票号码 + * 通常 = 发票代码 + 发票号码 + */ + @SerialName("electronicInvoiceNo") + val electronicInvoiceNo: String? = null, + + /** + * 开票日期 + * 格式: yyyy-MM-dd HH:mm:ss + */ + @SerialName("invoiceDate") + val invoiceDate: String? = null, + + /** + * 不含税金额 + * 单位:元 + * 保留两位小数 + */ + @SerialName("noTaxAmount") + val noTaxAmount: String? = null, + + /** + * 税额 + * 单位:元 + * 保留两位小数 + */ + @SerialName("taxAmount") + val taxAmount: String? = null, + + /** + * 发票版式文件类型 + * + * pdf: PDF文件 + * ofd: OFD文件 + */ + @SerialName("invoiceLayoutFileType") + val invoiceLayoutFileType: String? = null, + + /** + * 发票版式文件 + * Base64编码后的文件流 + */ + @SerialName("invoicePdf") + val invoicePdf: String? = null, + + /** + * 发票XML文件 + * Base64字符串 + */ + @SerialName("invoiceXml") + val invoiceXml: String? = null, + + /** + * 发票下载地址 + */ + @SerialName("downloadUrl") + val downloadUrl: String? = null, + + /** + * 收费明细PDF文件 + * 常用于门诊/住院发票 + * Base64字符串 + */ + @SerialName("chargeDetailPdf") + val chargeDetailPdf: String? = null, + + /** + * 收费明细PDF下载地址 + */ + @SerialName("chargeDetailDownloadUrl") + val chargeDetailDownloadUrl: String? = null, + + /** + * 电子发票预览二维码URL + * 用于扫码查看电子发票 + */ + @SerialName("invPreviewQrcodePath") + val invPreviewQrcodePath: String? = null, + + /** + * 电子发票预览二维码图片 + * Base64字符串 + */ + @SerialName("invPreviewQrcode") + val invPreviewQrcode: String? = null, + + /** + * 发票删除标志 + * + * 0: 未删除 + * 1: 已删除 + */ + @SerialName("invDeletedFlag") + val invDeletedFlag: String? = null, +) \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResult.kt b/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResult.kt new file mode 100644 index 0000000..eafedd3 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/entity/response/QueryInvoiceResult.kt @@ -0,0 +1,14 @@ +package com.bbit.ticket.entity.response + +import kotlinx.serialization.Serializable + +/** + * 查询并刷新发票状态的结果 + */ +@Serializable +data class QueryInvoiceResult( + /** 发票请求流水号 */ + val invoiceReqSerialNo: String, + /** 刷新后的状态 */ + val status: String, +) diff --git a/server/src/main/kotlin/com/bbit/ticket/plugins/JsonPlugin.kt b/server/src/main/kotlin/com/bbit/ticket/plugins/JsonPlugin.kt new file mode 100644 index 0000000..3a87109 --- /dev/null +++ b/server/src/main/kotlin/com/bbit/ticket/plugins/JsonPlugin.kt @@ -0,0 +1,11 @@ +package com.bbit.ticket.plugins + +import kotlinx.serialization.json.Json + + +val myJson = Json { + explicitNulls = false + ignoreUnknownKeys = true + prettyPrint = true + isLenient = true +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt index cf38d3e..2ad023a 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTTestRoutes.kt @@ -2,13 +2,11 @@ package com.bbit.ticket.route.piaotong -import com.bbit.ticket.bootstrap.Global import com.bbit.ticket.entity.common.PTException import com.bbit.ticket.entity.common.fail import com.bbit.ticket.entity.common.ok -import com.bbit.ticket.entity.common.BizException -import com.bbit.ticket.entity.common.ErrorCode -import com.bbit.ticket.entity.request.InvoiceRequest +import com.bbit.ticket.entity.request.AskInvoiceRequest +import com.bbit.ticket.entity.request.QueryInvoiceRequest import com.bbit.ticket.entity.request.TaxBureauAuthReq import com.bbit.ticket.entity.request.TaxRegisterInfo import com.bbit.ticket.entity.request.TaxRegisterUserRequest @@ -18,7 +16,6 @@ import com.bbit.ticket.entity.request.UpdatePresetDataRequest import com.bbit.ticket.service.piaotong.PTAuthService import com.bbit.ticket.service.piaotong.PTConfigService import com.bbit.ticket.utils.requireCurrentUser -import io.ktor.http.HttpStatusCode import io.ktor.server.auth.authenticate import io.ktor.server.request.receive import io.ktor.server.response.respond @@ -168,7 +165,7 @@ fun Route.registerPTTestRoutes() { post("/invoiceBlue") { try { val currentUser = call.requireCurrentUser() - val req = call.receive() + val req = call.receive() val response = PTAuthService.invoiceBlue(req, currentUser.id) call.respond(ok(response)) } catch (e: PTException) { @@ -181,6 +178,59 @@ fun Route.registerPTTestRoutes() { ) } } + get("/invoiceBlueHistory") { + try { + val currentUser = call.requireCurrentUser() + val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1 + val pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 20 + val response = PTAuthService.getInvoiceBlueHistory(currentUser.id, page, pageSize) + call.respond(ok(response)) + } catch (e: Exception) { + call.respond(fail(code = "-1", message = e.message ?: "查询开票历史失败")) + } + } + + get("/invoiceDetail") { + try { + val currentUser = call.requireCurrentUser() + val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"] + if (invoiceReqSerialNo.isNullOrBlank()) { + call.respond(fail(code = "-1", message = "请传入发票请求流水号")) + return@get + } + val response = PTAuthService.getInvoiceDetail(currentUser.id, invoiceReqSerialNo) + if (response == null) { + call.respond(fail(code = "-1", message = "未找到该发票记录")) + return@get + } + call.respond(ok(response)) + } catch (e: Exception) { + call.respond(fail(code = "-1", message = e.message ?: "查询发票详情失败")) + } + } + get("/queryInvoice"){ + try { + val currentUser = call.requireCurrentUser() + val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"] + if (invoiceReqSerialNo.isNullOrBlank()) { + call.respond(fail(code = "-1", message = "请传入发票请求流水号")) + return@get + } + val response = PTAuthService.queryInvoice(QueryInvoiceRequest( + taxpayerNum = currentUser.taxPayerNum ?: "", + invoiceReqSerialNo = invoiceReqSerialNo + )) + call.respond(ok(response)) + } catch (e: PTException) { + call.respond( + fail( + code = e.code, + message = e.message, + traceId = e.serialNo + ) + ) + } + } } } } diff --git a/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt index 92509f2..d4ecb2b 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/system/registerDictRoutes.kt @@ -75,7 +75,7 @@ fun Route.registerDictRoutes() { } } } - route("/api/system/dict-items") { + route("/system/dict-items") { get { call.requirePermission("system:dict:view") val page = call.queryInt("page", 1) 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 49a9ee7..d8e9c1a 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 @@ -3,18 +3,25 @@ package com.bbit.ticket.service.piaotong import com.bbit.ticket.dao.piaotong.EnterpriseTaxDao -import com.bbit.ticket.entity.request.InvoiceRequest +import com.bbit.ticket.entity.common.PageResult +import com.bbit.ticket.entity.request.AskInvoiceRequest +import com.bbit.ticket.entity.request.QueryInvoiceRequest 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.QueryInvoiceResult import com.bbit.ticket.entity.response.EnterpriseTaxInfo import com.bbit.ticket.entity.response.EtaxRegisterResponse import com.bbit.ticket.entity.response.InvoiceCreateResponse +import com.bbit.ticket.entity.response.InvoiceDetailResponse +import com.bbit.ticket.entity.response.InvoiceHistoryItem +import com.bbit.ticket.entity.response.QueryInvoiceResponse import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent import com.bbit.ticket.plugins.dbQuery import com.bbit.ticket.utils.CurrentUser import com.bbit.ticket.utils.net.PTClient +import io.ktor.server.util.url import kotlin.uuid.Uuid object PTAuthService { @@ -62,28 +69,73 @@ object PTAuthService { body = req ) dbQuery { EnterpriseTaxDao.updateEnterpriseInfo(userId, req) } - return "操作成功,企业状态为审核中(待审核)" + return "操作成功" } /** * 蓝票接口调用 */ - suspend fun invoiceBlue(req: InvoiceRequest, userId: Uuid): String { - PTClient.ptPost( + suspend fun invoiceBlue(req: AskInvoiceRequest, userId: Uuid): String { + PTClient.ptPost( url = "invoiceBlue.pt", body = req ) dbQuery { EnterpriseTaxDao.addInvoice(userId, req) } - return "操作成功,企业状态为审核中(待审核)" + return "操作成功" } /** * 红票接口调用 */ - suspend fun invoiceRed(req: InvoiceRequest, userId: Uuid): String { - PTClient.ptPost( + suspend fun invoiceRed(req: AskInvoiceRequest, userId: Uuid): String { + PTClient.ptPost( url = "invoiceBlue.pt", body = req ) - dbQuery { EnterpriseTaxDao.addInvoice(userId, req) } - return "操作成功,企业状态为审核中(待审核)" + return "操作成功" + } + + /** + * 分页查询蓝票开票历史 + */ + suspend fun getInvoiceBlueHistory(userId: Uuid, page: Int, pageSize: Int): PageResult = + dbQuery { EnterpriseTaxDao.invoiceHistory(userId, page, pageSize) } + + /** + * 查询发票完整详情 + */ + suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? = + dbQuery { EnterpriseTaxDao.invoiceDetail(userId, invoiceReqSerialNo) } + + /** + * 查询并更新发票状态 + * 调用票通平台查询实时状态,将结果更新到本地数据库 + */ + suspend fun queryInvoice(req: QueryInvoiceRequest): QueryInvoiceResult { + val res = PTClient.ptPost( + url = "queryInvoice.pt", + body = req + ) + dbQuery { + EnterpriseTaxDao.updateInvoiceStatus( + invoiceReqSerialNo = req.invoiceReqSerialNo, + code = res.code, + msg = res.msg, + invoiceNo = res.invoiceNo, + invoiceCode = res.invoiceCode, + electronicInvoiceNo = res.electronicInvoiceNo, + invoiceDate = res.invoiceDate, + tradeNo = res.tradeNo, + ) + } + val newStatus = when (res.code) { + "0000" -> "SUCCESS" + "7777" -> "PROCESSING" + "9999" -> "FAILED" + "6666" -> "PENDING" + else -> "PROCESSING" + } + return QueryInvoiceResult( + invoiceReqSerialNo = req.invoiceReqSerialNo, + status = newStatus + ) } } \ No newline at end of file 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 5762ea3..ffb225d 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 @@ -3,6 +3,7 @@ package com.bbit.ticket.utils.net import com.bbit.ticket.bootstrap.Global import com.bbit.ticket.entity.common.PTException import com.bbit.ticket.entity.response.PTResponse +import com.bbit.ticket.plugins.myJson import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.contentnegotiation.* @@ -24,14 +25,7 @@ object PTClient { val client = HttpClient(CIO) { install(ContentNegotiation) { - json( - Json { - explicitNulls = false - ignoreUnknownKeys = true - prettyPrint = true - isLenient = true - } - ) + json(myJson) } } @@ -48,8 +42,8 @@ object PTClient { url { // ⚠️ 把 Req 转成 JSON 再拆成 query(统一协议口径) - val json = Json.encodeToString(queryParams) - val element = Json.parseToJsonElement(json).jsonObject + val json = myJson.encodeToString(queryParams) + val element = myJson.parseToJsonElement(json).jsonObject element.forEach { (k, v) -> parameters.append(k, v.toString().trim('"')) @@ -63,7 +57,7 @@ object PTClient { val decrypted = disposeResponse(response) - val result = Json.decodeFromString>(decrypted) + val result = myJson.decodeFromString>(decrypted) if (result.code != "0000") { throw PTException( @@ -73,7 +67,7 @@ object PTClient { ) } - return Json.decodeFromJsonElement(result.content!!) + return myJson.decodeFromJsonElement(result.content!!) } suspend inline fun ptPost( @@ -85,11 +79,11 @@ object PTClient { val response = client.post(Global.baseUrl + url) { contentType(ContentType.Application.Json) headers.forEach { (k, v) -> header(k, v) } - setBody(buildRequestData(Json.encodeToString(body))) + setBody(buildRequestData(myJson.encodeToString(body))) }.bodyAsText() val decrypted = disposeResponse(response) - val result = Json.decodeFromString>(decrypted) + val result = myJson.decodeFromString>(decrypted) if (result.code != "0000") { throw PTException( code = result.code, @@ -99,7 +93,7 @@ object PTClient { } println("res = $result.content") - return Json.decodeFromJsonElement(result.content!!) + return myJson.decodeFromJsonElement(result.content!!) } /** @@ -123,14 +117,14 @@ object PTClient { map["timestamp"] = sdf.format(Date()) map["serialNo"] = ptDate() map["sign"] = RSAUtil.sign(RSAUtil.getSignatureContent(map), Global.ptPrivateKey) ?: "" - return Json.encodeToString(map) + return myJson.encodeToString(map) } fun disposeResponse( jsonStr: String, ): String { - val json = Json.parseToJsonElement(jsonStr).jsonObject + val json = myJson.parseToJsonElement(jsonStr).jsonObject // 1. 转 Map(用于签名验证) val mutableMap = json .toMutableMap() @@ -161,7 +155,7 @@ object PTClient { ?: "{}" val contentElement = runCatching { - Json.parseToJsonElement(plainContent) + myJson.parseToJsonElement(plainContent) }.getOrElse { JsonObject(emptyMap()) } diff --git a/web/src/api/piaotong/index.ts b/web/src/api/piaotong/index.ts index 8de337c..9356cb1 100644 --- a/web/src/api/piaotong/index.ts +++ b/web/src/api/piaotong/index.ts @@ -335,3 +335,193 @@ export interface InvoiceRequest { export function invoiceIssueApi(payload: InvoiceRequest): Promise { return http.post('/pt/invoiceBlue', payload) } + +// ============================================= +// 开票历史 +// ============================================= + +/** 分页结果 */ +export interface PageResult { + items: T[] + page: number + pageSize: number + total: number +} + +/** 发票历史记录 */ +export interface InvoiceHistoryItem { + id: string + /** 发票请求流水号 */ + invoiceReqSerialNo: string + /** 销方税号 */ + taxpayerNum: string + /** 发票种类 */ + invoiceKindCode: string + /** 购买方名称 */ + buyerName: string + /** 购买方税号 */ + buyerTaxpayerNum?: string + /** 购买方地址 */ + buyerAddress?: string + /** 购买方电话 */ + buyerTel?: string + /** 购买方开户银行 */ + buyerBankName?: string + /** 购买方银行账号 */ + buyerBankAccount?: string + /** 不含税金额 */ + amount: string + /** 税额 */ + taxAmount: string + /** 含税总金额 */ + totalAmount: string + /** 发票号码 */ + invoiceNo?: string + /** 发票代码 */ + invoiceCode?: string + /** 数电票号码 */ + electronicInvoiceNo?: string + /** 开票时间 */ + issuedAt?: string + /** 开票状态 */ + status: string + /** PDF 地址 */ + pdfUrl?: string + /** OFD 地址 */ + ofdUrl?: string + /** XML 地址 */ + xmlUrl?: string + /** 订单号 */ + tradeNo?: string + /** 备注 */ + remark?: string + /** 自定义透传数据 */ + definedData?: string + /** 创建时间 */ + createdAt: string + /** 错误信息 */ + errorMessage?: string +} + +/** 发票种类映射 */ +export const invoiceKindMap: Record = { + '81': '数电专票', + '82': '数电普票', + '87': '机动车发票', + '10': '电子普票', + '08': '电子专票', + '04': '增值税普票', + '01': '增值税专票' +} + +/** 开票状态映射 */ +export const invoiceStatusMap: Record = { + 'PENDING': '待处理', + 'PROCESSING': '处理中', + 'SUCCESS': '开票成功', + 'FAILED': '开票失败' +} + +/** 开票状态颜色映射 */ +export const invoiceStatusColorMap: Record = { + 'PENDING': '#faad14', + 'PROCESSING': '#409eff', + 'SUCCESS': '#52c41a', + 'FAILED': '#f56c6c' +} + +/** + * 分页查询蓝票开票历史 + */ +export function invoiceHistoryApi(page: number, pageSize: number): Promise> { + return http.get('/pt/invoiceBlueHistory', { params: { page, pageSize } }) +} + +// ============================================= +// 发票详情 +// ============================================= + +/** 发票商品明细(详情) */ +export interface InvoiceDetailGoods { + lineNo: number + goodsName: string + taxClassificationCode: string + specificationModel?: string + meteringUnit?: string + quantity?: string + unitPrice?: string + invoiceAmount: string + taxRateValue: string + taxRateAmount?: string + includeTaxFlag: boolean +} + +/** 差额征税凭证明细(详情) */ +export interface InvoiceDetailVoucher { + proofType: string + electronicInvoiceNo?: string + invoiceCode?: string + invoiceNo?: string + proofNo?: string + issueDate?: string + proofAmount: string + deductionAmount: string + proofRemark?: string + source?: string +} + +/** 关联单据(详情) */ +export interface InvoiceDetailOrder { + orderNo: string +} + +/** 发票完整详情 */ +export interface InvoiceDetailResponse { + id: string + invoiceReqSerialNo: string + taxpayerNum: string + invoiceKindCode: string + invoiceType: string + buyerName: string + buyerTaxpayerNum?: string + buyerAddress?: string + buyerTel?: string + buyerBankName?: string + buyerBankAccount?: string + amount: string + taxAmount: string + totalAmount: string + invoiceNo?: string + invoiceCode?: string + electronicInvoiceNo?: string + issuedAt?: string + status: string + errorMessage?: string + pdfUrl?: string + ofdUrl?: string + xmlUrl?: string + tradeNo?: string + remark?: string + definedData?: string + createdAt: string + /** 商品明细 */ + goodsList: InvoiceDetailGoods[] + /** 差额征税凭证明细 */ + voucherList: InvoiceDetailVoucher[] + /** 关联单据 */ + orderList: InvoiceDetailOrder[] +} + +/** + * 查询发票完整详情 + */ +export function invoiceDetailApi(invoiceReqSerialNo: string): Promise { + return http.get('/pt/invoiceDetail', { params: { invoiceReqSerialNo } }) +} + +/** + * 查询并刷新发票状态 + */ +export function queryInvoiceApi(invoiceReqSerialNo: string): Promise<{ invoiceReqSerialNo: string; status: string }> { + return http.get('/pt/queryInvoice', { params: { invoiceReqSerialNo } }) +} diff --git a/web/src/features/piaotong/invoice-history/index.vue b/web/src/features/piaotong/invoice-history/index.vue index 11d467b..6035fc3 100644 --- a/web/src/features/piaotong/invoice-history/index.vue +++ b/web/src/features/piaotong/invoice-history/index.vue @@ -1,35 +1,447 @@ + + diff --git a/web/src/features/piaotong/invoice-issue/index.vue b/web/src/features/piaotong/invoice-issue/index.vue index d215649..5186d84 100644 --- a/web/src/features/piaotong/invoice-issue/index.vue +++ b/web/src/features/piaotong/invoice-issue/index.vue @@ -448,13 +448,13 @@ - - + + - - + + @@ -538,18 +538,18 @@ - - - - - - + + + + + +