增加票通票样功能

This commit is contained in:
BBIT-Kai
2026-05-19 17:43:39 +08:00
parent 74fdddd113
commit cdcfaa192c
14 changed files with 376 additions and 466 deletions
@@ -10,11 +10,13 @@ import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable
import com.bbit.ticket.entity.common.PageResult
import com.bbit.ticket.entity.request.AskInvoiceRequest
import com.bbit.ticket.entity.response.GetInvoiceInfoResponse
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
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.utils.Base64TextUtil
import com.bbit.ticket.utils.formatDateTime
import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Op
@@ -332,6 +334,7 @@ object BlueInvoiceDao {
it.setIfNotNull(HistoryInvoiceBasicTable.definedData, req.definedData)
// 文件
it.setIfNotNull(HistoryInvoiceBasicTable.invoiceLayoutFileType, req.invoiceLayoutFileType)
it.setIfNotNull(HistoryInvoiceBasicTable.downloadUrl, Base64TextUtil.decodeToText(req.downloadUrl))
// 删除标记
it[HistoryInvoiceBasicTable.invDeletedFlag] = req.invDeletedFlag ?: "0"
}
@@ -348,6 +351,18 @@ object BlueInvoiceDao {
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息")
}
fun invoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? {
val row = HistoryInvoiceBasicTable.selectAll()
.where {
(HistoryInvoiceBasicTable.userId eq userId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
HistoryInvoiceBasicTable.deletedAt.isNull()
}
.singleOrNull() ?: return null
return InvoiceDownloadUrlResponse(row[HistoryInvoiceBasicTable.downloadUrl])
}
/**
* 查询发票完整详情(含商品明细、差额征税凭证、关联单据)
*/
@@ -381,7 +396,7 @@ object BlueInvoiceDao {
invoiceAmount = row[HistoryInvoiceGoodsTable.itemAmount].toPlainString(),
taxRateValue = row[HistoryInvoiceGoodsTable.taxRate].toPlainString(),
taxRateAmount = row[HistoryInvoiceGoodsTable.taxRateAmount].toPlainString(),
includeTaxFlag = false,
includeTaxFlag = row[HistoryInvoiceGoodsTable.includeTaxFlag] == "1",
zeroTaxFlag = row[HistoryInvoiceGoodsTable.zeroTaxFlag],
preferentialPolicyFlag = row[HistoryInvoiceGoodsTable.preferentialPolicyFlag],
vatSpecialManage = row[HistoryInvoiceGoodsTable.vatSpecialManage],
@@ -5,6 +5,7 @@ package com.bbit.ticket.dao.piaotong
import com.bbit.ticket.database.piaotong.HistoryInvoiceBasicTable
import com.bbit.ticket.database.piaotong.HistoryInvoiceRedTable
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
@@ -46,6 +47,7 @@ object RedInvoiceDao {
it[HistoryInvoiceBasicTable.invoiceKind] = req.invoiceKind
?: blueRow?.get(HistoryInvoiceBasicTable.invoiceKind) ?: "82"
it[HistoryInvoiceBasicTable.invoiceType] = "2" // 红票
it[HistoryInvoiceBasicTable.redFlag] = "REDING"
// ---- 红冲关联 ----
it[HistoryInvoiceBasicTable.blueInvoiceCode] = blueRow?.get(HistoryInvoiceBasicTable.invoiceCode)
@@ -115,4 +117,17 @@ object RedInvoiceDao {
takerEmail = row[HistoryInvoiceRedTable.takerEmail],
)
}
fun invoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? {
val row = HistoryInvoiceBasicTable.selectAll()
.where {
(HistoryInvoiceBasicTable.userId eq userId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
(HistoryInvoiceBasicTable.invoiceType eq "2") and
HistoryInvoiceBasicTable.deletedAt.isNull()
}
.singleOrNull() ?: return null
return InvoiceDownloadUrlResponse(row[HistoryInvoiceBasicTable.downloadUrl])
}
}
@@ -423,6 +423,7 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") {
* 1:已删除
*/
val invDeletedFlag = varchar("inv_deleted_flag", 1)
val downloadUrl = text("download_url").nullable()
// ----------------------------------------------------------------
// 时间字段
@@ -130,7 +130,11 @@ object HistoryInvoiceGoodsTable : Table("history_invoice_goods") {
* 税额
*/
val taxRateAmount = decimal("tax_rate_amount", 18, 2)
/**
* 税额
*/
val includeTaxFlag = varchar("include_tax_flag", 1)
.nullable()
/**
* 扣除额
*
@@ -0,0 +1,8 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
@Serializable
data class InvoiceDownloadUrlResponse(
val downloadUrl: String? = null
)
@@ -12,8 +12,12 @@ 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 io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.server.request.receive
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.post
@@ -93,6 +97,86 @@ fun Route.registerPTiInvoiceRoutes() {
call.respond(fail(code = "-1", message = e.message ?: "查询发票详情失败"))
}
}
get("/invoiceDownloadUrl") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "璇蜂紶鍏ュ彂绁ㄨ姹傛祦姘村彿"))
return@get
}
val response = PTBlueService.getInvoiceDownloadUrl(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("/invoicePreview") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "璇蜂紶鍏ュ彂绁ㄨ姹傛祦姘村彿"))
return@get
}
val bytes = PTBlueService.getInvoicePreview(currentUser.id, invoiceReqSerialNo)
if (bytes == null) {
call.respond(fail(code = "-1", message = "鏈壘鍒扮エ鏍峰湴鍧€"))
return@get
}
call.response.header(
HttpHeaders.ContentDisposition,
"inline; filename=\"${invoiceReqSerialNo}.pdf\""
)
call.respondBytes(bytes, ContentType.Application.Pdf)
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "棰勮绁ㄦ牱澶辫触"))
}
}
get("/redInvoiceDownloadUrl") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "请传入发票请求流水号"))
return@get
}
val response = PTRedService.getRedInvoiceDownloadUrl(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("/redInvoicePreview") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "请传入发票请求流水号"))
return@get
}
val bytes = PTRedService.getRedInvoicePreview(currentUser.id, invoiceReqSerialNo)
if (bytes == null) {
call.respond(fail(code = "-1", message = "未找到票样地址"))
return@get
}
call.response.header(
HttpHeaders.ContentDisposition,
"inline; filename=\"${invoiceReqSerialNo}.pdf\""
)
call.respondBytes(bytes, ContentType.Application.Pdf)
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "预览红票票样失败"))
}
}
get("/queryInvoice") {
try {
val currentUser = call.requireCurrentUser()
@@ -7,10 +7,13 @@ 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.response.GetInvoiceInfoResponse
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
import com.bbit.ticket.entity.response.QueryInvoiceResult
import com.bbit.ticket.entity.response.InvoiceCreateResponse
import com.bbit.ticket.entity.response.InvoiceDetailResponse
import com.bbit.ticket.entity.response.InvoiceHistoryItem
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 kotlin.uuid.Uuid
@@ -70,6 +73,16 @@ object PTBlueService {
suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? =
dbQuery { BlueInvoiceDao.invoiceDetail(userId, invoiceReqSerialNo) }
suspend fun getInvoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery { BlueInvoiceDao.invoiceDownloadUrl(userId, invoiceReqSerialNo) }
suspend fun getInvoicePreview(userId: Uuid, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getInvoiceDownloadUrl(userId, invoiceReqSerialNo)?.downloadUrl
?.takeIf { it.isNotBlank() }
?: return null
return PTClient.client.get(downloadUrl).bodyAsBytes()
}
/**
* 查询并更新发票状态(复用 syncInvoiceFromPT
*/
@@ -81,4 +94,4 @@ object PTBlueService {
return syncInvoiceFromPT(existing, invoiceReqSerialNo, req.taxpayerNum)
}
}
}
@@ -6,11 +6,14 @@ import com.bbit.ticket.dao.piaotong.HistoryDao
import com.bbit.ticket.dao.piaotong.RedInvoiceDao
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
import com.bbit.ticket.entity.request.RedCreateRequest
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.plugins.dbQuery
import com.bbit.ticket.utils.net.PTClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsBytes
import kotlin.uuid.Uuid
/**
@@ -59,4 +62,13 @@ object PTRedService {
suspend fun getRedInvoiceInfo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? =
dbQuery { RedInvoiceDao.findRedInfoBySerialNo(userId, invoiceReqSerialNo) }
suspend fun getRedInvoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery { RedInvoiceDao.invoiceDownloadUrl(userId, invoiceReqSerialNo) }
suspend fun getRedInvoicePreview(userId: Uuid, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getRedInvoiceDownloadUrl(userId, invoiceReqSerialNo)?.downloadUrl
?.takeIf { it.isNotBlank() }
?: return null
return PTClient.client.get(downloadUrl).bodyAsBytes()
}
}
@@ -0,0 +1,15 @@
package com.bbit.ticket.utils
import java.nio.charset.StandardCharsets
import java.util.Base64
object Base64TextUtil {
fun decodeToText(value: String?): String? {
val text = value?.trim() ?: return null
if (text.isEmpty()) return text
return runCatching {
String(Base64.getDecoder().decode(text), StandardCharsets.UTF_8)
}.getOrElse { text }
}
}
@@ -2,7 +2,7 @@ package com.bbit.ticket.utils.bootstrap
object Global {
val isDev = false
val isDev = true
// 请求基础地址
var baseUrl: String