增加票通票样功能
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/dist/
|
web/dist/
|
||||||
|
log/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -10,11 +10,13 @@ import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable
|
|||||||
import com.bbit.ticket.entity.common.PageResult
|
import com.bbit.ticket.entity.common.PageResult
|
||||||
import com.bbit.ticket.entity.request.AskInvoiceRequest
|
import com.bbit.ticket.entity.request.AskInvoiceRequest
|
||||||
import com.bbit.ticket.entity.response.GetInvoiceInfoResponse
|
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.InvoiceDetailGoods
|
||||||
import com.bbit.ticket.entity.response.InvoiceDetailOrder
|
import com.bbit.ticket.entity.response.InvoiceDetailOrder
|
||||||
import com.bbit.ticket.entity.response.InvoiceDetailResponse
|
import com.bbit.ticket.entity.response.InvoiceDetailResponse
|
||||||
import com.bbit.ticket.entity.response.InvoiceDetailVoucher
|
import com.bbit.ticket.entity.response.InvoiceDetailVoucher
|
||||||
import com.bbit.ticket.entity.response.InvoiceHistoryItem
|
import com.bbit.ticket.entity.response.InvoiceHistoryItem
|
||||||
|
import com.bbit.ticket.utils.Base64TextUtil
|
||||||
import com.bbit.ticket.utils.formatDateTime
|
import com.bbit.ticket.utils.formatDateTime
|
||||||
import org.jetbrains.exposed.v1.core.Column
|
import org.jetbrains.exposed.v1.core.Column
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
import org.jetbrains.exposed.v1.core.Op
|
||||||
@@ -332,6 +334,7 @@ object BlueInvoiceDao {
|
|||||||
it.setIfNotNull(HistoryInvoiceBasicTable.definedData, req.definedData)
|
it.setIfNotNull(HistoryInvoiceBasicTable.definedData, req.definedData)
|
||||||
// 文件
|
// 文件
|
||||||
it.setIfNotNull(HistoryInvoiceBasicTable.invoiceLayoutFileType, req.invoiceLayoutFileType)
|
it.setIfNotNull(HistoryInvoiceBasicTable.invoiceLayoutFileType, req.invoiceLayoutFileType)
|
||||||
|
it.setIfNotNull(HistoryInvoiceBasicTable.downloadUrl, Base64TextUtil.decodeToText(req.downloadUrl))
|
||||||
// 删除标记
|
// 删除标记
|
||||||
it[HistoryInvoiceBasicTable.invDeletedFlag] = req.invDeletedFlag ?: "0"
|
it[HistoryInvoiceBasicTable.invDeletedFlag] = req.invDeletedFlag ?: "0"
|
||||||
}
|
}
|
||||||
@@ -348,6 +351,18 @@ object BlueInvoiceDao {
|
|||||||
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息")
|
?: 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(),
|
invoiceAmount = row[HistoryInvoiceGoodsTable.itemAmount].toPlainString(),
|
||||||
taxRateValue = row[HistoryInvoiceGoodsTable.taxRate].toPlainString(),
|
taxRateValue = row[HistoryInvoiceGoodsTable.taxRate].toPlainString(),
|
||||||
taxRateAmount = row[HistoryInvoiceGoodsTable.taxRateAmount].toPlainString(),
|
taxRateAmount = row[HistoryInvoiceGoodsTable.taxRateAmount].toPlainString(),
|
||||||
includeTaxFlag = false,
|
includeTaxFlag = row[HistoryInvoiceGoodsTable.includeTaxFlag] == "1",
|
||||||
zeroTaxFlag = row[HistoryInvoiceGoodsTable.zeroTaxFlag],
|
zeroTaxFlag = row[HistoryInvoiceGoodsTable.zeroTaxFlag],
|
||||||
preferentialPolicyFlag = row[HistoryInvoiceGoodsTable.preferentialPolicyFlag],
|
preferentialPolicyFlag = row[HistoryInvoiceGoodsTable.preferentialPolicyFlag],
|
||||||
vatSpecialManage = row[HistoryInvoiceGoodsTable.vatSpecialManage],
|
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.HistoryInvoiceBasicTable
|
||||||
import com.bbit.ticket.database.piaotong.HistoryInvoiceRedTable
|
import com.bbit.ticket.database.piaotong.HistoryInvoiceRedTable
|
||||||
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
|
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
|
||||||
|
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
|
||||||
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
|
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
@@ -46,6 +47,7 @@ object RedInvoiceDao {
|
|||||||
it[HistoryInvoiceBasicTable.invoiceKind] = req.invoiceKind
|
it[HistoryInvoiceBasicTable.invoiceKind] = req.invoiceKind
|
||||||
?: blueRow?.get(HistoryInvoiceBasicTable.invoiceKind) ?: "82"
|
?: blueRow?.get(HistoryInvoiceBasicTable.invoiceKind) ?: "82"
|
||||||
it[HistoryInvoiceBasicTable.invoiceType] = "2" // 红票
|
it[HistoryInvoiceBasicTable.invoiceType] = "2" // 红票
|
||||||
|
it[HistoryInvoiceBasicTable.redFlag] = "REDING"
|
||||||
|
|
||||||
// ---- 红冲关联 ----
|
// ---- 红冲关联 ----
|
||||||
it[HistoryInvoiceBasicTable.blueInvoiceCode] = blueRow?.get(HistoryInvoiceBasicTable.invoiceCode)
|
it[HistoryInvoiceBasicTable.blueInvoiceCode] = blueRow?.get(HistoryInvoiceBasicTable.invoiceCode)
|
||||||
@@ -115,4 +117,17 @@ object RedInvoiceDao {
|
|||||||
takerEmail = row[HistoryInvoiceRedTable.takerEmail],
|
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:已删除
|
* 1:已删除
|
||||||
*/
|
*/
|
||||||
val invDeletedFlag = varchar("inv_deleted_flag", 1)
|
val invDeletedFlag = varchar("inv_deleted_flag", 1)
|
||||||
|
val downloadUrl = text("download_url").nullable()
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// 时间字段
|
// 时间字段
|
||||||
|
|||||||
+5
-1
@@ -130,7 +130,11 @@ object HistoryInvoiceGoodsTable : Table("history_invoice_goods") {
|
|||||||
* 税额
|
* 税额
|
||||||
*/
|
*/
|
||||||
val taxRateAmount = decimal("tax_rate_amount", 18, 2)
|
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.PTBlueService
|
||||||
import com.bbit.ticket.service.piaotong.PTRedService
|
import com.bbit.ticket.service.piaotong.PTRedService
|
||||||
import com.bbit.ticket.utils.requireCurrentUser
|
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.request.receive
|
||||||
|
import io.ktor.server.response.header
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.response.respondBytes
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.server.routing.post
|
||||||
@@ -93,6 +97,86 @@ fun Route.registerPTiInvoiceRoutes() {
|
|||||||
call.respond(fail(code = "-1", message = e.message ?: "查询发票详情失败"))
|
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") {
|
get("/queryInvoice") {
|
||||||
try {
|
try {
|
||||||
val currentUser = call.requireCurrentUser()
|
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.AskInvoiceRequest
|
||||||
import com.bbit.ticket.entity.request.QueryInvoiceRequest
|
import com.bbit.ticket.entity.request.QueryInvoiceRequest
|
||||||
import com.bbit.ticket.entity.response.GetInvoiceInfoResponse
|
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.QueryInvoiceResult
|
||||||
import com.bbit.ticket.entity.response.InvoiceCreateResponse
|
import com.bbit.ticket.entity.response.InvoiceCreateResponse
|
||||||
import com.bbit.ticket.entity.response.InvoiceDetailResponse
|
import com.bbit.ticket.entity.response.InvoiceDetailResponse
|
||||||
import com.bbit.ticket.entity.response.InvoiceHistoryItem
|
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.plugins.dbQuery
|
||||||
import com.bbit.ticket.utils.net.PTClient
|
import com.bbit.ticket.utils.net.PTClient
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
@@ -70,6 +73,16 @@ object PTBlueService {
|
|||||||
suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? =
|
suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? =
|
||||||
dbQuery { BlueInvoiceDao.invoiceDetail(userId, invoiceReqSerialNo) }
|
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)
|
* 查询并更新发票状态(复用 syncInvoiceFromPT)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import com.bbit.ticket.dao.piaotong.HistoryDao
|
|||||||
import com.bbit.ticket.dao.piaotong.RedInvoiceDao
|
import com.bbit.ticket.dao.piaotong.RedInvoiceDao
|
||||||
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
|
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
|
||||||
import com.bbit.ticket.entity.request.RedCreateRequest
|
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.QuickRedInvoiceResponse
|
||||||
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
|
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
|
||||||
import com.bbit.ticket.utils.CurrentUser
|
import com.bbit.ticket.utils.CurrentUser
|
||||||
import com.bbit.ticket.utils.plugins.dbQuery
|
import com.bbit.ticket.utils.plugins.dbQuery
|
||||||
import com.bbit.ticket.utils.net.PTClient
|
import com.bbit.ticket.utils.net.PTClient
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsBytes
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,4 +62,13 @@ object PTRedService {
|
|||||||
suspend fun getRedInvoiceInfo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? =
|
suspend fun getRedInvoiceInfo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? =
|
||||||
dbQuery { RedInvoiceDao.findRedInfoBySerialNo(userId, invoiceReqSerialNo) }
|
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 {
|
object Global {
|
||||||
|
|
||||||
val isDev = false
|
val isDev = true
|
||||||
|
|
||||||
// 请求基础地址
|
// 请求基础地址
|
||||||
var baseUrl: String
|
var baseUrl: String
|
||||||
|
|||||||
@@ -705,6 +705,28 @@ export function invoiceDetailApi(invoiceReqSerialNo: string): Promise<InvoiceDet
|
|||||||
return http.get('/pt/invoiceDetail', { params: { invoiceReqSerialNo } })
|
return http.get('/pt/invoiceDetail', { params: { invoiceReqSerialNo } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function invoiceDownloadUrlApi(invoiceReqSerialNo: string): Promise<{ downloadUrl?: string }> {
|
||||||
|
return http.get('/pt/invoiceDownloadUrl', { params: { invoiceReqSerialNo } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invoicePreviewBlobApi(invoiceReqSerialNo: string): Promise<Blob> {
|
||||||
|
return http.get('/pt/invoicePreview', {
|
||||||
|
params: { invoiceReqSerialNo },
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redInvoiceDownloadUrlApi(invoiceReqSerialNo: string): Promise<{ downloadUrl?: string }> {
|
||||||
|
return http.get('/pt/redInvoiceDownloadUrl', { params: { invoiceReqSerialNo } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redInvoicePreviewBlobApi(invoiceReqSerialNo: string): Promise<Blob> {
|
||||||
|
return http.get('/pt/redInvoicePreview', {
|
||||||
|
params: { invoiceReqSerialNo },
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询并刷新发票状态
|
* 查询并刷新发票状态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -361,6 +361,53 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showSamplePreview"
|
||||||
|
preset="card"
|
||||||
|
title="查看票样"
|
||||||
|
:style="{ width: '980px', maxWidth: '94vw' }"
|
||||||
|
content-style="padding: 0"
|
||||||
|
>
|
||||||
|
<div class="sample-preview">
|
||||||
|
<div class="sample-toolbar">
|
||||||
|
<div class="sample-title">{{ sampleSerialNo }}</div>
|
||||||
|
<div class="sample-actions">
|
||||||
|
<n-button size="small" secondary @click="zoomOutSample">
|
||||||
|
<template #icon><ZoomOut :size="14" /></template>
|
||||||
|
缩小
|
||||||
|
</n-button>
|
||||||
|
<span class="sample-zoom">{{ sampleZoom }}%</span>
|
||||||
|
<n-button size="small" secondary @click="zoomInSample">
|
||||||
|
<template #icon><ZoomIn :size="14" /></template>
|
||||||
|
放大
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
tag="a"
|
||||||
|
:href="sampleBlobUrl || undefined"
|
||||||
|
:download="`${sampleSerialNo || 'invoice'}.pdf`"
|
||||||
|
:disabled="!sampleBlobUrl"
|
||||||
|
>
|
||||||
|
<template #icon><Download :size="14" /></template>
|
||||||
|
下载
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-spin :show="sampleLoading">
|
||||||
|
<div class="sample-frame-wrap">
|
||||||
|
<iframe
|
||||||
|
v-if="sampleBlobUrl"
|
||||||
|
class="sample-frame"
|
||||||
|
:src="sampleBlobUrl"
|
||||||
|
:style="{ transform: `scale(${sampleZoom / 100})` }"
|
||||||
|
/>
|
||||||
|
<n-empty v-else description="暂无票样地址" />
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="showRedForm"
|
v-model:show="showRedForm"
|
||||||
preset="card"
|
preset="card"
|
||||||
@@ -410,6 +457,7 @@ import type { Component } from 'vue'
|
|||||||
import {
|
import {
|
||||||
NButton,
|
NButton,
|
||||||
NDataTable,
|
NDataTable,
|
||||||
|
NEmpty,
|
||||||
NForm,
|
NForm,
|
||||||
NFormItem,
|
NFormItem,
|
||||||
NInput,
|
NInput,
|
||||||
@@ -426,16 +474,24 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
XCircle,
|
XCircle,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
|
FileSearch,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Download,
|
||||||
RotateCcw
|
RotateCcw
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
|
invoiceDownloadUrlApi,
|
||||||
|
invoicePreviewBlobApi,
|
||||||
invoiceDetailApi,
|
invoiceDetailApi,
|
||||||
invoiceHistoryApi,
|
invoiceHistoryApi,
|
||||||
invoiceKindMap,
|
invoiceKindMap,
|
||||||
invoiceStatusMap,
|
invoiceStatusMap,
|
||||||
queryInvoiceApi,
|
queryInvoiceApi,
|
||||||
redInvoiceCreateApi,
|
redInvoiceCreateApi,
|
||||||
|
redInvoiceDownloadUrlApi,
|
||||||
redInvoiceInfoApi,
|
redInvoiceInfoApi,
|
||||||
|
redInvoicePreviewBlobApi,
|
||||||
redReasonMap
|
redReasonMap
|
||||||
} from '@/api/piaotong'
|
} from '@/api/piaotong'
|
||||||
import type {
|
import type {
|
||||||
@@ -446,7 +502,7 @@ import type {
|
|||||||
RedCreateRequest,
|
RedCreateRequest,
|
||||||
RedInvoiceInfo
|
RedInvoiceInfo
|
||||||
} from '@/api/piaotong'
|
} from '@/api/piaotong'
|
||||||
import type { DataTableColumn } from 'naive-ui'
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
|
|
||||||
const invoiceTypeMap: Record<string, string> = {
|
const invoiceTypeMap: Record<string, string> = {
|
||||||
BLUE: '蓝票',
|
BLUE: '蓝票',
|
||||||
@@ -583,7 +639,7 @@ const pagination = reactive({
|
|||||||
showSizePicker: true,
|
showSizePicker: true,
|
||||||
pageSizes: [10, 20, 50, 100],
|
pageSizes: [10, 20, 50, 100],
|
||||||
pageSlot: 7,
|
pageSlot: 7,
|
||||||
prefix: ({ itemCount }: { itemCount: number }) => `共 ${itemCount} 条`
|
prefix: ({ itemCount }: { itemCount?: number }) => `共 ${itemCount ?? 0} 条`
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
@@ -613,12 +669,22 @@ function handlePageSizeChange(pageSize: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refreshingSet = reactive(new Set<string>())
|
const refreshingSet = reactive(new Set<string>())
|
||||||
|
const showSamplePreview = ref(false)
|
||||||
|
const sampleLoading = ref(false)
|
||||||
|
const sampleUrl = ref('')
|
||||||
|
const sampleBlobUrl = ref('')
|
||||||
|
const sampleSerialNo = ref('')
|
||||||
|
const sampleZoom = ref(100)
|
||||||
|
|
||||||
function getRowActions(row: InvoiceHistoryItem) {
|
function getRowActions(row: InvoiceHistoryItem) {
|
||||||
const actions: Array<{ label: string; icon?: Component; onClick: () => void }> = []
|
const actions: Array<{ label: string; icon?: Component; onClick: () => void }> = []
|
||||||
actions.push({ label: '详情', icon: Eye, onClick: () => showDetailInfo(row) })
|
actions.push({ label: '详情', icon: Eye, onClick: () => showDetailInfo(row) })
|
||||||
actions.push({ label: '刷新', icon: RefreshCw, onClick: () => refreshStatus(row) })
|
actions.push({ label: '刷新', icon: RefreshCw, onClick: () => refreshStatus(row) })
|
||||||
|
|
||||||
|
if ((activeTab.value === 'BLUE' || activeTab.value === 'RED') && row.status === 'SUCCESS') {
|
||||||
|
actions.push({ label: '查看票样', icon: FileSearch, onClick: () => openSamplePreview(row) })
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
activeTab.value === 'BLUE' &&
|
activeTab.value === 'BLUE' &&
|
||||||
row.status === 'SUCCESS' &&
|
row.status === 'SUCCESS' &&
|
||||||
@@ -630,7 +696,47 @@ function getRowActions(row: InvoiceHistoryItem) {
|
|||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = computed<DataTableColumn[]>(() => [
|
async function openSamplePreview(item: InvoiceHistoryItem) {
|
||||||
|
showSamplePreview.value = true
|
||||||
|
sampleLoading.value = true
|
||||||
|
if (sampleBlobUrl.value) {
|
||||||
|
URL.revokeObjectURL(sampleBlobUrl.value)
|
||||||
|
}
|
||||||
|
sampleUrl.value = ''
|
||||||
|
sampleBlobUrl.value = ''
|
||||||
|
sampleSerialNo.value = item.invoiceReqSerialNo
|
||||||
|
sampleZoom.value = 100
|
||||||
|
try {
|
||||||
|
const isRedInvoice = activeTab.value === 'RED' || item.invoiceType === 'RED'
|
||||||
|
const res = isRedInvoice
|
||||||
|
? await redInvoiceDownloadUrlApi(item.invoiceReqSerialNo)
|
||||||
|
: await invoiceDownloadUrlApi(item.invoiceReqSerialNo)
|
||||||
|
sampleUrl.value = res.downloadUrl || ''
|
||||||
|
if (!sampleUrl.value) {
|
||||||
|
message.warning('暂无票样地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const blob = isRedInvoice
|
||||||
|
? await redInvoicePreviewBlobApi(item.invoiceReqSerialNo)
|
||||||
|
: await invoicePreviewBlobApi(item.invoiceReqSerialNo)
|
||||||
|
sampleBlobUrl.value = URL.createObjectURL(new Blob([blob], { type: 'application/pdf' }))
|
||||||
|
} catch {
|
||||||
|
message.error('预览票样失败')
|
||||||
|
} finally {
|
||||||
|
sampleLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomInSample() {
|
||||||
|
sampleZoom.value = Math.min(200, sampleZoom.value + 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOutSample() {
|
||||||
|
sampleZoom.value = Math.max(50, sampleZoom.value - 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = computed<DataTableColumns<InvoiceHistoryItem>>(() => {
|
||||||
|
const tableColumns: DataTableColumns<InvoiceHistoryItem> = [
|
||||||
{
|
{
|
||||||
title: '流水号',
|
title: '流水号',
|
||||||
key: 'invoiceReqSerialNo',
|
key: 'invoiceReqSerialNo',
|
||||||
@@ -659,7 +765,17 @@ const columns = computed<DataTableColumn[]>(() => [
|
|||||||
key: 'redFlag',
|
key: 'redFlag',
|
||||||
width: 110,
|
width: 110,
|
||||||
render: (row: InvoiceHistoryItem) => {
|
render: (row: InvoiceHistoryItem) => {
|
||||||
if (!row.redFlag || row.redFlag === 'NOT_RED') {
|
const redFlag =
|
||||||
|
row.redFlag ||
|
||||||
|
(row.invoiceType === 'RED'
|
||||||
|
? {
|
||||||
|
SUCCESS: 'ALREADY_RED',
|
||||||
|
PROCESSING: 'REDING',
|
||||||
|
PENDING: 'REDING',
|
||||||
|
FAILED: 'RED_FAIL'
|
||||||
|
}[row.status]
|
||||||
|
: undefined)
|
||||||
|
if (!redFlag || redFlag === 'NOT_RED') {
|
||||||
return h(NTag, { size: 'small', round: true }, () => '未冲红')
|
return h(NTag, { size: 'small', round: true }, () => '未冲红')
|
||||||
}
|
}
|
||||||
const typeMap: Record<string, 'error' | 'warning' | 'default'> = {
|
const typeMap: Record<string, 'error' | 'warning' | 'default'> = {
|
||||||
@@ -670,8 +786,8 @@ const columns = computed<DataTableColumn[]>(() => [
|
|||||||
}
|
}
|
||||||
return h(
|
return h(
|
||||||
NTag,
|
NTag,
|
||||||
{ size: 'small', round: true, type: typeMap[row.redFlag] || 'default' },
|
{ size: 'small', round: true, type: typeMap[redFlag] || 'default' },
|
||||||
() => redFlagMap[row.redFlag] || row.redFlag
|
() => redFlagMap[redFlag] || redFlag
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -713,6 +829,7 @@ const columns = computed<DataTableColumn[]>(() => [
|
|||||||
{ style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap' },
|
{ style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap' },
|
||||||
actions.map((btn) => {
|
actions.map((btn) => {
|
||||||
const isLoading = btn.label === '刷新' && refreshingSet.has(row.invoiceReqSerialNo)
|
const isLoading = btn.label === '刷新' && refreshingSet.has(row.invoiceReqSerialNo)
|
||||||
|
const Icon = btn.icon
|
||||||
return h(
|
return h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
@@ -723,14 +840,19 @@ const columns = computed<DataTableColumn[]>(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => btn.label,
|
default: () => btn.label,
|
||||||
...(btn.icon && !isLoading ? { icon: () => h(btn.icon, { size: 13 }) } : {})
|
...(Icon && !isLoading ? { icon: () => h(Icon, { size: 13 }) } : {})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
|
|
||||||
|
return activeTab.value === 'RED'
|
||||||
|
? tableColumns.filter((column) => !('key' in column) || column.key !== 'redFlag')
|
||||||
|
: tableColumns
|
||||||
|
})
|
||||||
|
|
||||||
const showDetail = ref(false)
|
const showDetail = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
@@ -830,7 +952,7 @@ async function handleRedSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const goodsColumns: DataTableColumn[] = [
|
const goodsColumns: DataTableColumns<InvoiceDetailGoods> = [
|
||||||
{ title: '行号', key: 'lineNo', width: 60, align: 'center' },
|
{ title: '行号', key: 'lineNo', width: 60, align: 'center' },
|
||||||
{ title: '商品名称', key: 'goodsName', width: 140, ellipsis: { tooltip: true } },
|
{ title: '商品名称', key: 'goodsName', width: 140, ellipsis: { tooltip: true } },
|
||||||
{ title: '税收分类编码', key: 'taxClassificationCode', width: 120, ellipsis: { tooltip: true } },
|
{ title: '税收分类编码', key: 'taxClassificationCode', width: 120, ellipsis: { tooltip: true } },
|
||||||
@@ -931,7 +1053,7 @@ function goodsSummary() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const voucherColumns: DataTableColumn[] = [
|
const voucherColumns: DataTableColumns<InvoiceDetailVoucher> = [
|
||||||
{
|
{
|
||||||
title: '凭证类型',
|
title: '凭证类型',
|
||||||
key: 'proofType',
|
key: 'proofType',
|
||||||
@@ -1045,6 +1167,56 @@ onMounted(() => {
|
|||||||
background: #fafafa !important;
|
background: #fafafa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sample-preview {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-title {
|
||||||
|
color: #111827;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-zoom {
|
||||||
|
min-width: 46px;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-frame-wrap {
|
||||||
|
height: 70vh;
|
||||||
|
min-height: 420px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #eef1f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 680px;
|
||||||
|
border: 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-shell {
|
.detail-shell {
|
||||||
padding: 0 20px 20px;
|
padding: 0 20px 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user