整理代码

This commit is contained in:
BBIT-Kai
2026-05-22 18:00:14 +08:00
parent d57ea3960c
commit 01b63c4fe4
22 changed files with 969 additions and 218 deletions
@@ -46,9 +46,9 @@ import kotlin.uuid.Uuid
object BlueInvoiceDao {
fun listBatchNos(userId: Uuid): List<String> =
fun listBatchNos(userId: Uuid, enterpriseId: Uuid?, digitalAccountId: Uuid?): List<String> =
OpenInvoiceBatchTable.selectAll()
.where { OpenInvoiceBatchTable.userId eq userId }
.where { batchScopeWhere(userId, enterpriseId, digitalAccountId) }
.orderBy(OpenInvoiceBatchTable.createdAt, SortOrder.DESC)
.map { it[OpenInvoiceBatchTable.batchNo] }
@@ -69,14 +69,7 @@ object BlueInvoiceDao {
isSuccess: Boolean? = null,
batchNo: String? = null,
): PageResult<InvoiceHistoryItem> {
val conditions = mutableListOf<Op<Boolean>>()
if (digitalAccountId != null) {
conditions.add(HistoryInvoiceBasicTable.digitalAccountId eq digitalAccountId)
} else if (enterpriseId != null) {
conditions.add(HistoryInvoiceBasicTable.enterpriseId eq enterpriseId)
} else {
conditions.add(HistoryInvoiceBasicTable.userId eq userId)
}
val conditions = mutableListOf(invoiceScopeWhere(userId, enterpriseId, digitalAccountId))
conditions.add(HistoryInvoiceBasicTable.deletedAt.isNull())
// 发票类型筛选:前端传 BLUE/RED,库中存 1/2
@@ -105,7 +98,7 @@ object BlueInvoiceDao {
val matchedSerialNos = (OpenInvoiceBatchItemTable innerJoin OpenInvoiceBatchTable)
.selectAll()
.where {
(OpenInvoiceBatchTable.userId eq userId) and
batchScopeWhere(userId, enterpriseId, digitalAccountId) and
(OpenInvoiceBatchTable.batchNo like "%$normalizedBatchNo%")
}
.map { it[OpenInvoiceBatchItemTable.invoiceReqSerialNo] }
@@ -138,7 +131,7 @@ object BlueInvoiceDao {
(OpenInvoiceBatchItemTable innerJoin OpenInvoiceBatchTable)
.selectAll()
.where {
(OpenInvoiceBatchTable.userId eq userId) and
batchScopeWhere(userId, enterpriseId, digitalAccountId) and
(OpenInvoiceBatchItemTable.invoiceReqSerialNo inList serialNos)
}
.associate { row ->
@@ -451,18 +444,6 @@ object BlueInvoiceDao {
it[HistoryInvoiceBasicTable.invDeletedFlag] = req.invDeletedFlag ?: "0"
}
/**
* 根据流水号查询记录的 userId
*/
fun findUserIdBySerialNo(invoiceReqSerialNo: String): Uuid {
val row = HistoryInvoiceBasicTable.selectAll()
.where { HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo }
.singleOrNull()
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在")
return row[HistoryInvoiceBasicTable.userId]
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息")
}
fun findInvoiceScopeBySerialNo(invoiceReqSerialNo: String): InvoiceScope {
val row = HistoryInvoiceBasicTable.selectAll()
.where { HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo }
@@ -528,10 +509,15 @@ object BlueInvoiceDao {
.distinct()
}
fun invoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? {
fun invoiceDownloadUrl(
userId: Uuid,
enterpriseId: Uuid?,
digitalAccountId: Uuid?,
invoiceReqSerialNo: String,
): InvoiceDownloadUrlResponse? {
val row = HistoryInvoiceBasicTable.selectAll()
.where {
(HistoryInvoiceBasicTable.userId eq userId) and
invoiceScopeWhere(userId, enterpriseId, digitalAccountId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
HistoryInvoiceBasicTable.deletedAt.isNull()
}
@@ -543,14 +529,35 @@ object BlueInvoiceDao {
/**
* 查询发票完整详情(含商品明细、差额征税凭证、关联单据)
*/
fun invoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? {
fun invoiceDetail(
userId: Uuid,
enterpriseId: Uuid?,
digitalAccountId: Uuid?,
invoiceReqSerialNo: String,
): InvoiceDetailResponse? {
val basicRow = HistoryInvoiceBasicTable.selectAll().where {
(HistoryInvoiceBasicTable.userId eq userId) and (HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo)
invoiceScopeWhere(userId, enterpriseId, digitalAccountId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
HistoryInvoiceBasicTable.deletedAt.isNull()
}.singleOrNull() ?: return null
return buildInvoiceDetailResponse(basicRow)
}
private fun invoiceScopeWhere(userId: Uuid, enterpriseId: Uuid?, digitalAccountId: Uuid?): Op<Boolean> =
when {
digitalAccountId != null -> HistoryInvoiceBasicTable.digitalAccountId eq digitalAccountId
enterpriseId != null -> HistoryInvoiceBasicTable.enterpriseId eq enterpriseId
else -> HistoryInvoiceBasicTable.userId eq userId
}
private fun batchScopeWhere(userId: Uuid, enterpriseId: Uuid?, digitalAccountId: Uuid?): Op<Boolean> =
when {
digitalAccountId != null -> OpenInvoiceBatchTable.digitalAccountId eq digitalAccountId
enterpriseId != null -> OpenInvoiceBatchTable.enterpriseId eq enterpriseId
else -> OpenInvoiceBatchTable.userId eq userId
}
/**
* 根据主表行构建发票完整详情响应
*/
@@ -92,19 +92,19 @@ object EnterpriseManageDao {
.where { (PtDigitalAccountTable.id eq id) and PtDigitalAccountTable.deletedAt.isNull() }
.singleOrNull()
fun digitalAccountByApiKey(apiKey: String): ResultRow? =
fun digitalAccountByTaxpayerAndAccount(taxpayerNum: String, account: String): ResultRow? =
PtDigitalAccountTable.selectAll()
.where {
(PtDigitalAccountTable.apiKey eq apiKey) and
(PtDigitalAccountTable.status eq "ENABLED") and
(PtDigitalAccountTable.taxpayerNum eq taxpayerNum) and
(PtDigitalAccountTable.account eq account) and
PtDigitalAccountTable.deletedAt.isNull()
}
.singleOrNull()
fun digitalAccountForUser(userId: Uuid): ResultRow? =
fun digitalAccountByApiKey(apiKey: String): ResultRow? =
PtDigitalAccountTable.selectAll()
.where {
(PtDigitalAccountTable.platformUserId eq userId) and
(PtDigitalAccountTable.apiKey eq apiKey) and
(PtDigitalAccountTable.status eq "ENABLED") and
PtDigitalAccountTable.deletedAt.isNull()
}
@@ -183,7 +183,6 @@ object EnterpriseManageDao {
it[PtDigitalAccountTable.name] = name
it[PtDigitalAccountTable.identityType] = identityType
it[PtDigitalAccountTable.platformUserId] = platformUserId
it[PtDigitalAccountTable.apiKey] = apiKey
it[updatedAt] = now
}
return id
@@ -201,6 +200,13 @@ object EnterpriseManageDao {
}[PtDigitalAccountTable.id]
}
fun updateDigitalAccountStatus(digitalAccountId: Uuid, status: String) {
PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq digitalAccountId }) {
it[PtDigitalAccountTable.status] = status
it[updatedAt] = OffsetDateTime.now()
}
}
fun bindDigitalAccountUser(digitalAccountId: Uuid, userId: Uuid) {
PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq digitalAccountId }) {
it[platformUserId] = userId
@@ -3,6 +3,7 @@
package com.bbit.ticket.dao.piaotong
import com.bbit.ticket.database.piaotong.HistoryInvoiceBasicTable
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.isNull
@@ -41,14 +42,14 @@ object HistoryDao {
/**
* 根据历史记录 ID 查询蓝票基本信息
*/
fun findByHistory(historyId: Uuid?, userId: Uuid): HistoryRow {
fun findByHistory(historyId: Uuid?, userId: Uuid, enterpriseId: Uuid?, digitalAccountId: Uuid?): HistoryRow {
if (historyId == null) {
throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "历史记录 ID 为空")
}
val row = HistoryInvoiceBasicTable.selectAll()
.where {
(HistoryInvoiceBasicTable.id eq historyId) and
(HistoryInvoiceBasicTable.userId eq userId) and
scopeWhere(userId, enterpriseId, digitalAccountId) and
HistoryInvoiceBasicTable.deletedAt.isNull()
}
.singleOrNull()
@@ -79,4 +80,11 @@ object HistoryDao {
)
}
private fun scopeWhere(userId: Uuid, enterpriseId: Uuid?, digitalAccountId: Uuid?): Op<Boolean> =
when {
digitalAccountId != null -> HistoryInvoiceBasicTable.digitalAccountId eq digitalAccountId
enterpriseId != null -> HistoryInvoiceBasicTable.enterpriseId eq enterpriseId
else -> HistoryInvoiceBasicTable.userId eq userId
}
}
@@ -7,6 +7,7 @@ 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.Op
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.isNull
@@ -66,7 +67,7 @@ object RedInvoiceDao {
it[HistoryInvoiceBasicTable.buyerBankAccount] = blueRow?.get(HistoryInvoiceBasicTable.buyerBankAccount)
// ---- 金额 ----
val redAmount = req.amount?.toBigDecimalOrNull() ?: BigDecimal.ZERO
val redAmount = req.amount.toBigDecimalOrNull() ?: BigDecimal.ZERO
it[HistoryInvoiceBasicTable.noTaxAmount] = redAmount
it[HistoryInvoiceBasicTable.taxAmount] = BigDecimal.ZERO
it[HistoryInvoiceBasicTable.amountWithTax] = redAmount
@@ -89,7 +90,7 @@ object RedInvoiceDao {
it[HistoryInvoiceRedTable.historyId] = historyId
it[HistoryInvoiceRedTable.invoiceReqSerialNo] = req.invoiceReqSerialNo
it[HistoryInvoiceRedTable.redReason] = req.redReason
it[HistoryInvoiceRedTable.amount] = req.amount?.toBigDecimalOrNull()
it[HistoryInvoiceRedTable.amount] = req.amount.toBigDecimalOrNull()
it[HistoryInvoiceRedTable.invoiceCode] = req.invoiceCode
it[HistoryInvoiceRedTable.invoiceNo] = req.invoiceNo
it[HistoryInvoiceRedTable.invoiceKind] = req.invoiceKind
@@ -114,11 +115,24 @@ object RedInvoiceDao {
/**
* 根据流水号查询红票申请信息
*/
fun findRedInfoBySerialNo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? {
fun findRedInfoBySerialNo(
userId: Uuid,
enterpriseId: Uuid?,
digitalAccountId: Uuid?,
invoiceReqSerialNo: String,
): RedInvoiceInfoResponse? {
val canAccess = HistoryInvoiceBasicTable.selectAll()
.where {
scopeWhere(userId, enterpriseId, digitalAccountId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
HistoryInvoiceBasicTable.deletedAt.isNull()
}
.any()
if (!canAccess) return null
val row = HistoryInvoiceRedTable.selectAll()
.where {
(HistoryInvoiceRedTable.userId eq userId) and
(HistoryInvoiceRedTable.invoiceReqSerialNo eq invoiceReqSerialNo)
HistoryInvoiceRedTable.invoiceReqSerialNo eq invoiceReqSerialNo
}
.singleOrNull() ?: return null
@@ -130,10 +144,15 @@ object RedInvoiceDao {
)
}
fun invoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? {
fun invoiceDownloadUrl(
userId: Uuid,
enterpriseId: Uuid?,
digitalAccountId: Uuid?,
invoiceReqSerialNo: String,
): InvoiceDownloadUrlResponse? {
val row = HistoryInvoiceBasicTable.selectAll()
.where {
(HistoryInvoiceBasicTable.userId eq userId) and
scopeWhere(userId, enterpriseId, digitalAccountId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
(HistoryInvoiceBasicTable.invoiceType eq "2") and
HistoryInvoiceBasicTable.deletedAt.isNull()
@@ -142,4 +161,11 @@ object RedInvoiceDao {
return InvoiceDownloadUrlResponse(row[HistoryInvoiceBasicTable.downloadUrl])
}
private fun scopeWhere(userId: Uuid, enterpriseId: Uuid?, digitalAccountId: Uuid?): Op<Boolean> =
when {
digitalAccountId != null -> HistoryInvoiceBasicTable.digitalAccountId eq digitalAccountId
enterpriseId != null -> HistoryInvoiceBasicTable.enterpriseId eq enterpriseId
else -> HistoryInvoiceBasicTable.userId eq userId
}
}
@@ -49,3 +49,8 @@ data class UpdateInvoiceSettingRequest(
val address: String = "",
val phone: String = "",
)
@Serializable
data class UpdateDigitalAccountStatusRequest(
val status: String,
)
@@ -0,0 +1,155 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* 发票数据获取请求参数
*
* 用于查询企业开具/取得的发票信息
*/
@Serializable
data class InvoiceQueryRequest(
/**
* 纳税人识别号
*
* 长度:15-20
* 仅支持大写字母和数字
*/
@SerialName("taxpayerNum")
val taxpayerNum: String,
/**
* 查询类型
*
* 1:开具发票
* 2:取得发票
*/
@SerialName("queryType")
val queryType: String,
/**
* 发票来源
*
* 0:全部
* 1:增值税发票管理系统
* 2:电子发票服务平台
*/
@SerialName("invSource")
val invSource: String? = null,
/**
* 发票种类代码
*
* 81:电子发票(增值税专用发票)
* 82:电子发票(普通发票)
* 10:增值税电子普通发票
* 08:增值税电子专用发票
* 04:增值税普通发票
* 01:增值税专用发票
*/
@SerialName("invoiceKind")
val invoiceKind: String? = null,
/**
* 发票状态
*
* 空字符串:全部
* 01:正常
* 02:已作废
* 03:已红冲-全额
* 04:已红冲-部分
*/
@SerialName("invoiceState")
val invoiceState: String? = null,
/**
* 数电号码
*
* 查询数电发票时必填
* 与 invoiceCode 至少传一个
*/
@SerialName("electronicInvoiceNo")
val electronicInvoiceNo: String? = null,
/**
* 发票代码
*
* 长度:10 或 12
* 查询税控系统发票时必填
*/
@SerialName("invoiceCode")
val invoiceCode: String? = null,
/**
* 发票号码
*
* 长度:8
* 查询税控系统发票时必填
*/
@SerialName("invoiceNo")
val invoiceNo: String? = null,
/**
* 开票日期
*
* 格式:yyyyMMdd
* 示例:20221123
*/
@SerialName("invoiceDate")
val invoiceDate: String,
/**
* 对方纳税人税号
*/
@SerialName("reciprocalTaxpayerNum")
val reciprocalTaxpayerNum: String? = null,
/**
* 对方纳税人名称
*
* 长度:4-100
*/
@SerialName("reciprocalTaxpayerName")
val reciprocalTaxpayerName: String? = null,
/**
* 电子税局登录账号
*/
@SerialName("account")
val account: String? = null
)
@Serializable
data class InvoiceQuerySubmitRequest(
@SerialName("digitalAccountId")
val digitalAccountId: String? = null,
@SerialName("queryType")
val queryType: String,
@SerialName("invSource")
val invSource: String? = null,
@SerialName("invoiceKind")
val invoiceKind: String? = null,
@SerialName("invoiceState")
val invoiceState: String? = null,
@SerialName("electronicInvoiceNo")
val electronicInvoiceNo: String? = null,
@SerialName("invoiceCode")
val invoiceCode: String? = null,
@SerialName("invoiceDate")
val invoiceDate: String,
@SerialName("reciprocalTaxpayerNum")
val reciprocalTaxpayerNum: String? = null,
@SerialName("reciprocalTaxpayerName")
val reciprocalTaxpayerName: String? = null,
)
@@ -0,0 +1,348 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* 发票数据获取响应报文
*/
@Serializable
data class InvoiceQueryResponse(
/**
* 获取结果代码
*
* 0000:获取成功
* 其他:获取失败,失败原因见 resultMsg
*/
@SerialName("resultCode")
val resultCode: String,
/**
* 获取结果描述
*
* 成功或失败时返回的说明信息
*/
@SerialName("resultMsg")
val resultMsg: String? = null,
/**
* 发票列表
*
* 兼容后续批量查询场景
*/
@SerialName("invoiceList")
val invoiceList: List<InvoiceInfo> = emptyList()
)
/**
* 发票信息
*/
@Serializable
data class InvoiceInfo(
/**
* 购买方名称
*/
@SerialName("buyerName")
val buyerName: String,
/**
* 购买方纳税人识别号
*/
@SerialName("buyerTaxpayerNum")
val buyerTaxpayerNum: String? = null,
/**
* 购买方地址电话
*/
@SerialName("buyerAddress")
val buyerAddress: String? = null,
/**
* 购买方开户行及账号
*/
@SerialName("buyerBankName")
val buyerBankName: String? = null,
/**
* 销售方纳税人识别号
*/
@SerialName("sellerTaxpayerNum")
val sellerTaxpayerNum: String,
/**
* 销售方名称
*/
@SerialName("sellerName")
val sellerName: String,
/**
* 销售方地址电话
*/
@SerialName("sellerAddress")
val sellerAddress: String? = null,
/**
* 销售方开户行及账号
*/
@SerialName("sellerBankName")
val sellerBankName: String? = null,
/**
* 发票种类代码
*
* 81:电子发票(增值税专用发票)
* 82:电子发票(普通发票)
* 10:增值税电子普通发票
* 08:增值税电子专用发票
* 04:增值税普通发票
* 01:增值税专用发票
*/
@SerialName("invoiceKind")
val invoiceKind: String,
/**
* 开票日期
*
* 格式:yyyy-MM-dd HH:mm:ss
*/
@SerialName("invoiceDate")
val invoiceDate: String,
/**
* 发票代码
*
* 税控系统发票可能返回
*/
@SerialName("invoiceCode")
val invoiceCode: String? = null,
/**
* 发票号码
*
* 税控系统发票可能返回
*/
@SerialName("invoiceNo")
val invoiceNo: String? = null,
/**
* 数电发票号码
*/
@SerialName("electronicInvoiceNo")
val electronicInvoiceNo: String? = null,
/**
* 不含税金额
*
* 保留小数点后 2 位
*/
@SerialName("noTaxAmount")
val noTaxAmount: String,
/**
* 税额
*
* 保留小数点后 2 位
*/
@SerialName("taxAmount")
val taxAmount: String,
/**
* 价税合计
*
* 保留小数点后 2 位
*/
@SerialName("amountWithTax")
val amountWithTax: String,
/**
* 作废标志
*
* NOT_DESTROY:未作废
* ALREADY_DESTROY:已作废
* DESTROYING:作废中
* DESTROY_FAIL:作废失败
*/
@SerialName("invalidFlag")
val invalidFlag: String,
/**
* 冲红标志
*
* NOT_RED:未冲红
* ALREADY_RED:已冲红
* REDING:冲红中
* RED_FAIL:冲红失败
* PART_RED:部分冲红
*/
@SerialName("redFlag")
val redFlag: String,
/**
* 开票人名称
*/
@SerialName("drawerName")
val drawerName: String,
/**
* 收款人
*/
@SerialName("casherName")
val casherName: String? = null,
/**
* 复核人
*/
@SerialName("reviewerName")
val reviewerName: String? = null,
/**
* 备注
*
* 数电发票一般限制 200 字符
* 税控发票按 GBK 字节长度限制 240 字节
*/
@SerialName("remark")
val remark: String? = null,
/**
* 项目明细列表
*/
@SerialName("itemList")
val itemList: List<InvoiceItem> = emptyList()
)
/**
* 发票项目明细
*/
@Serializable
data class InvoiceItem(
/**
* 货物或服务名称
*/
@SerialName("goodsName")
val goodsName: String,
/**
* 税收分类编码
*/
@SerialName("taxClassificationCode")
val taxClassificationCode: String,
/**
* 规格型号
*/
@SerialName("specificationModel")
val specificationModel: String? = null,
/**
* 单位
*/
@SerialName("meteringUnit")
val meteringUnit: String? = null,
/**
* 数量
*
* 保留小数点后 8 位
*/
@SerialName("quantity")
val quantity: String? = null,
/**
* 单价
*
* 保留小数点后 8 位
*/
@SerialName("unitPrice")
val unitPrice: String? = null,
/**
* 含税标识
*
* 0:不含税
* 1:含税
*/
@SerialName("taxIncludeFlag")
val taxIncludeFlag: String,
/**
* 项目金额
*
* 保留小数点后 2 位
*/
@SerialName("itemAmount")
val itemAmount: String,
/**
* 税率
*
* 保留小数点后 2 位,例如 0.13
*/
@SerialName("taxRate")
val taxRate: String,
/**
* 项目税额
*
* 保留小数点后 2 位
*/
@SerialName("taxRateAmount")
val taxRateAmount: String,
/**
* 扣除额
*
* 差额发票有值,保留小数点后 2 位
*/
@SerialName("deduction")
val deduction: String? = null,
/**
* 优惠政策标识
*
* 0:不使用
* 1:使用
*/
@SerialName("preferentialPolicyFlag")
val preferentialPolicyFlag: String? = null,
/**
* 零税率标识
*
* 空:非零税率
* 1:免税
* 2:不征税
* 3:普通零税率
*/
@SerialName("zeroTaxFlag")
val zeroTaxFlag: String? = null,
/**
* 增值税特殊管理
*
* 例如:免税、不征税、简易征收等
*/
@SerialName("vatSpecialManage")
val vatSpecialManage: String? = null,
/**
* 发票行性质
*
* 0:正常行
* 1:折扣行
* 2:被折扣行
*/
@SerialName("itemProperty")
val itemProperty: String,
/**
* 项目序号
*
* 用于表示项目明细的先后顺序
*/
@SerialName("itemNo")
val itemNo: String
)
@@ -8,6 +8,7 @@ import com.bbit.ticket.entity.request.GetLoginSmsCodeRequest
import com.bbit.ticket.entity.request.QueryRealNameAuthQrStatusRequest
import com.bbit.ticket.entity.request.SmsLoginRequest
import com.bbit.ticket.entity.request.TaxBureauAuthReq
import com.bbit.ticket.entity.request.UpdateDigitalAccountStatusRequest
import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest
import com.bbit.ticket.service.piaotong.PTAuthService
import com.bbit.ticket.service.piaotong.PTConfigService
@@ -73,6 +74,17 @@ fun Route.registerPTAuthRoutes() {
}
}
put("/digital-accounts/{id}/status") {
call.respondPt("更新数电账号状态失败") {
val id = call.parameters["id"] ?: throw IllegalArgumentException("缺少数电账号ID")
PTConfigService.updateDigitalAccountStatus(
call.requireCurrentUser(),
id,
call.receive<UpdateDigitalAccountStatusRequest>(),
)
}
}
get("/preset") {
call.respondPtOrEmptyObject("查询预设数据失败") {
PTConfigService.getEnterpriseInfo(call.requireCurrentUser())
@@ -115,13 +127,17 @@ fun Route.registerPTAuthRoutes() {
post("/send-sms-code") {
call.respondPt("发送登录短信验证码失败") {
PTAuthService.sendLoginSmsCode(call.receive<GetLoginSmsCodeRequest>())
val req = call.receive<GetLoginSmsCodeRequest>()
PTConfigService.requireDigitalAccountForLogin(call.requireCurrentUser(), req.taxpayerNum, req.account)
PTAuthService.sendLoginSmsCode(req)
}
}
post("/sms-login") {
call.respondPt("短信验证码登录失败") {
PTAuthService.smsLogin(call.receive<SmsLoginRequest>())
val req = call.receive<SmsLoginRequest>()
PTConfigService.requireDigitalAccountForLogin(call.requireCurrentUser(), req.taxpayerNum, req.account)
PTAuthService.smsLogin(req)
}
}
}
@@ -4,6 +4,7 @@ package com.bbit.ticket.route.piaotong
import com.bbit.ticket.entity.common.BizException
import com.bbit.ticket.entity.request.AskInvoiceRequest
import com.bbit.ticket.entity.request.InvoiceQuerySubmitRequest
import com.bbit.ticket.entity.request.QueryInvoiceRequest
import com.bbit.ticket.entity.request.RedCreateRequest
import com.bbit.ticket.service.piaotong.PTBlueService
@@ -57,7 +58,7 @@ fun Route.registerPTInvoiceRoutes() {
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
?: return@get
call.respondPt("查询发票详情失败") {
PTBlueService.getInvoiceDetail(call.requireCurrentUser().id, invoiceReqSerialNo)
PTBlueService.getInvoiceDetail(call.requireCurrentUser(), invoiceReqSerialNo)
?: throw BizException("-1", "未找到该发票记录")
}
}
@@ -66,7 +67,7 @@ fun Route.registerPTInvoiceRoutes() {
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
?: return@get
call.respondPt("查询发票下载地址失败") {
PTBlueService.getInvoiceDownloadUrl(call.requireCurrentUser().id, invoiceReqSerialNo)
PTBlueService.getInvoiceDownloadUrl(call.requireCurrentUser(), invoiceReqSerialNo)
?: throw BizException("-1", "未找到该发票记录")
}
}
@@ -75,7 +76,7 @@ fun Route.registerPTInvoiceRoutes() {
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
?: return@get
call.respondPtPdf("预览票样失败", "$invoiceReqSerialNo.pdf") {
PTBlueService.getInvoicePreview(call.requireCurrentUser().id, invoiceReqSerialNo)
PTBlueService.getInvoicePreview(call.requireCurrentUser(), invoiceReqSerialNo)
?: throw BizException("-1", "未找到票样地址")
}
}
@@ -84,7 +85,7 @@ fun Route.registerPTInvoiceRoutes() {
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
?: return@get
call.respondPt("查询红票下载地址失败") {
PTRedService.getRedInvoiceDownloadUrl(call.requireCurrentUser().id, invoiceReqSerialNo)
PTRedService.getRedInvoiceDownloadUrl(call.requireCurrentUser(), invoiceReqSerialNo)
?: throw BizException("-1", "未找到该红票记录")
}
}
@@ -93,7 +94,7 @@ fun Route.registerPTInvoiceRoutes() {
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
?: return@get
call.respondPtPdf("预览红票票样失败", "$invoiceReqSerialNo.pdf") {
PTRedService.getRedInvoicePreview(call.requireCurrentUser().id, invoiceReqSerialNo)
PTRedService.getRedInvoicePreview(call.requireCurrentUser(), invoiceReqSerialNo)
?: throw BizException("-1", "未找到票样地址")
}
}
@@ -120,7 +121,7 @@ fun Route.registerPTInvoiceRoutes() {
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
?: return@get
call.respondPt("查询红票申请信息失败") {
PTRedService.getRedInvoiceInfo(call.requireCurrentUser().id, invoiceReqSerialNo)
PTRedService.getRedInvoiceInfo(call.requireCurrentUser(), invoiceReqSerialNo)
?: throw BizException("-1", "未找到红票申请信息")
}
}
@@ -13,86 +13,45 @@ import com.bbit.ticket.entity.request.TaxBureauAuthReq
import com.bbit.ticket.entity.response.AuthQrcodeResponse
import com.bbit.ticket.entity.response.AuthQrcodeStatusResponse
import com.bbit.ticket.entity.response.DigitalAccountInfo
import com.bbit.ticket.entity.response.EnterpriseTaxInfo
import com.bbit.ticket.entity.response.LoginSmsCodeResponse
import com.bbit.ticket.entity.response.QueryEnterpriseBankAccountResponse
import com.bbit.ticket.entity.response.QueryEnterpriseInfoResponse
import com.bbit.ticket.entity.response.SMSLoginResponse
import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.net.PTApi
object PTAuthService {
/**
* 查询数电账号认证状态 2.8
*/
suspend fun getTaxBureauAccountAuthStatus(req: TaxBureauAuthReq): TaxBureauAccountAuthContent {
val res =
PTClient.ptPost<TaxBureauAuthReq, TaxBureauAccountAuthContent>("getTaxBureauAccountAuthStatus.pt", req)
return res
return PTApi.getTaxBureauAccountAuthStatus(req)
}
/**
* 获取实名认证二维码 2.6
*
* @return AuthQrcodeResponse 包含 base64 二维码图片
*/
suspend fun getAuthenticationQrcode(req: AuthQrcodeRequest): AuthQrcodeResponse {
return PTClient.ptPost<AuthQrcodeRequest, AuthQrcodeResponse>("getAuthenticationQrcode.pt", req)
return PTApi.getAuthenticationQrcode(req)
}
/**
* 查询认证二维码扫码状态 2.7
*
* @return AuthQrcodeStatusResponse 包含 scanStatus、msg
*/
suspend fun queryAuthQrcodeScanStatus(req: QueryRealNameAuthQrStatusRequest): AuthQrcodeStatusResponse {
return PTClient.ptPost<QueryRealNameAuthQrStatusRequest, AuthQrcodeStatusResponse>(
"queryAuthQrcodeScanStatus.pt",
req
)
return PTApi.queryAuthQrcodeScanStatus(req)
}
/**
* 发送登录短信验证码 2.4
*
* @return LoginSmsCodeResponse 包含脱敏手机号、有效时长
*/
suspend fun sendLoginSmsCode(req: GetLoginSmsCodeRequest): LoginSmsCodeResponse {
return PTClient.ptPost<GetLoginSmsCodeRequest, LoginSmsCodeResponse>("sendLoginSmsCode.pt", req)
return PTApi.sendLoginSmsCode(req)
}
/**
* 短信验证码登录 2.5
*
* @return SMSLoginResponse 包含登录结果
*/
suspend fun smsLogin(req: SmsLoginRequest): SMSLoginResponse {
return PTClient.ptPost<SmsLoginRequest, SMSLoginResponse>("smsLogin.pt", req)
return PTApi.smsLogin(req)
}
/**
* 查询企业状态 2.47
*/
suspend fun getEnterpriseInfo(req: QueryEnterpriseInfoRequest): QueryEnterpriseInfoResponse {
return PTClient.ptPost<QueryEnterpriseInfoRequest, QueryEnterpriseInfoResponse>("getEnterpriseInfo.pt", req)
return PTApi.getEnterpriseInfo(req)
}
/**
* 查询数电账号列表 2.45
*/
suspend fun getListTaxBureauAccount(req: QueryDigitalAccountListRequest): List<DigitalAccountInfo> {
return PTClient.ptPost<QueryDigitalAccountListRequest, List<DigitalAccountInfo>>(
"listTaxBureauAccount.pt",
req
)
return PTApi.listTaxBureauAccount(req)
}
/**
* 查询企业开户行及账号 2.49
*/
suspend fun queryEnterpriseBankInfo(req: QueryEnterpriseBankAccountRequest): QueryEnterpriseBankAccountResponse {
return PTClient.ptPost<QueryEnterpriseBankAccountRequest, QueryEnterpriseBankAccountResponse>("queryEnterpriseBankInfo.pt", req)
return PTApi.queryEnterpriseBankInfo(req)
}
}
@@ -5,17 +5,19 @@ package com.bbit.ticket.service.piaotong
import com.bbit.ticket.dao.piaotong.BlueInvoiceDao
import com.bbit.ticket.entity.common.PageResult
import com.bbit.ticket.entity.request.AskInvoiceRequest
import com.bbit.ticket.entity.request.InvoiceQueryRequest
import com.bbit.ticket.entity.request.InvoiceQuerySubmitRequest
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 com.bbit.ticket.entity.response.InvoiceQueryResponse
import com.bbit.ticket.utils.CurrentUser
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.PTApi
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.parseUuid
import kotlin.uuid.Uuid
@@ -23,7 +25,13 @@ import kotlin.uuid.Uuid
object PTBlueService {
suspend fun listBatchNos(user: CurrentUser): List<String> =
dbQuery { BlueInvoiceDao.listBatchNos(user.id) }
dbQuery {
BlueInvoiceDao.listBatchNos(
user.id,
user.enterpriseId,
if (user.isDigitalOperator) user.digitalAccountId else null,
)
}
/**
* 查询票通同步发票信息(支持插入和更新)
@@ -38,8 +46,7 @@ object PTBlueService {
enterpriseId: Uuid? = null,
digitalAccountId: Uuid? = null,
): QueryInvoiceResult {
val res = PTClient.ptPost<QueryInvoiceRequest, GetInvoiceInfoResponse>(
"queryInvoiceInfo.pt",
val res = PTApi.queryInvoiceInfo(
QueryInvoiceRequest(taxpayerNum = taxpayerNum, invoiceReqSerialNo = invoiceReqSerialNo)
)
dbQuery { BlueInvoiceDao.upsertInvoiceInfo(userId, res, enterpriseId, digitalAccountId) }
@@ -77,7 +84,7 @@ object PTBlueService {
): String {
dbQuery { BlueInvoiceDao.addInvoice(userId, req, enterpriseId, digitalAccountId) }
try {
PTClient.ptPost<AskInvoiceRequest, InvoiceCreateResponse>("invoiceBlue.pt", req)
PTApi.invoiceBlue(req)
} catch (e: Exception) {
dbQuery { BlueInvoiceDao.markInvoiceFailed(userId, req.invoiceReqSerialNo, e.message) }
throw e
@@ -89,9 +96,6 @@ object PTBlueService {
return "操作成功"
}
/**
* 分页查询开票历史(支持筛选)
*/
suspend fun getInvoiceBlueHistory(
user: CurrentUser,
page: Int,
@@ -117,10 +121,30 @@ object PTBlueService {
* 查询发票完整详情
*/
suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? =
dbQuery { BlueInvoiceDao.invoiceDetail(userId, invoiceReqSerialNo) }
dbQuery { BlueInvoiceDao.invoiceDetail(userId, null, null, invoiceReqSerialNo) }
suspend fun getInvoiceDetail(user: CurrentUser, invoiceReqSerialNo: String): InvoiceDetailResponse? =
dbQuery {
BlueInvoiceDao.invoiceDetail(
user.id,
user.enterpriseId,
if (user.isDigitalOperator) user.digitalAccountId else null,
invoiceReqSerialNo,
)
}
suspend fun getInvoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery { BlueInvoiceDao.invoiceDownloadUrl(userId, invoiceReqSerialNo) }
dbQuery { BlueInvoiceDao.invoiceDownloadUrl(userId, null, null, invoiceReqSerialNo) }
suspend fun getInvoiceDownloadUrl(user: CurrentUser, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery {
BlueInvoiceDao.invoiceDownloadUrl(
user.id,
user.enterpriseId,
if (user.isDigitalOperator) user.digitalAccountId else null,
invoiceReqSerialNo,
)
}
suspend fun getInvoicePreview(userId: Uuid, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getInvoiceDownloadUrl(userId, invoiceReqSerialNo)?.downloadUrl
@@ -129,6 +153,13 @@ object PTBlueService {
return PTClient.client.get(downloadUrl).bodyAsBytes()
}
suspend fun getInvoicePreview(user: CurrentUser, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getInvoiceDownloadUrl(user, invoiceReqSerialNo)?.downloadUrl
?.takeIf { it.isNotBlank() }
?: return null
return PTClient.client.get(downloadUrl).bodyAsBytes()
}
/**
* 查询并更新发票状态(复用 syncInvoiceFromPT
*/
@@ -11,12 +11,12 @@ import com.bbit.ticket.entity.request.QueryDigitalAccountListRequest
import com.bbit.ticket.entity.request.QueryEnterpriseBankAccountRequest
import com.bbit.ticket.entity.request.QueryEnterpriseInfoRequest
import com.bbit.ticket.entity.request.TaxRegister
import com.bbit.ticket.entity.request.UpdateDigitalAccountStatusRequest
import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest
import com.bbit.ticket.entity.response.BankInfo
import com.bbit.ticket.entity.response.DigitalAccountInfo
import com.bbit.ticket.entity.response.DigitalAccountManageItem
import com.bbit.ticket.entity.response.EnterpriseManageResponse
import com.bbit.ticket.entity.response.EtaxRegisterResponse
import com.bbit.ticket.entity.response.OpenApiStatisticsItem
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.entity.common.BizException
@@ -24,7 +24,7 @@ import com.bbit.ticket.entity.common.ErrorCode
import com.bbit.ticket.service.system.PasswordService
import com.bbit.ticket.utils.ApiKeyUtil
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.net.PTApi
import com.bbit.ticket.utils.parseUuid
import io.ktor.http.HttpStatusCode
import kotlin.uuid.Uuid
@@ -109,8 +109,7 @@ object PTConfigService {
?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest)
}
PTClient.ptPost<TaxRegister, EtaxRegisterResponse>(
"registerUser.pt",
PTApi.registerDigitalAccount(
TaxRegister(
taxpayerNum = enterprise.taxpayerNum,
account = req.account,
@@ -147,6 +146,28 @@ object PTConfigService {
}
}
suspend fun updateDigitalAccountStatus(
user: CurrentUser,
digitalAccountId: String,
req: UpdateDigitalAccountStatusRequest,
): String = dbQuery {
if (!user.isSuperAdmin && !user.isEnterpriseAdmin) {
throw BizException(ErrorCode.FORBIDDEN.code, "无权操作数电账号", HttpStatusCode.Forbidden)
}
val status = req.status.trim().uppercase()
if (status !in setOf("ENABLED", "DISABLED")) {
throw BizException(ErrorCode.BAD_REQUEST.code, "账号状态必须是 ENABLED 或 DISABLED")
}
val targetId = parseUuid(digitalAccountId, "digitalAccountId")
val row = EnterpriseManageDao.digitalAccount(targetId)
?: throw BizException(ErrorCode.BAD_REQUEST.code, "数电账号不存在", HttpStatusCode.NotFound)
if (row[PtDigitalAccountTable.enterpriseId] != requireEnterpriseId(user)) {
throw BizException(ErrorCode.FORBIDDEN.code, "无权操作该数电账号", HttpStatusCode.Forbidden)
}
EnterpriseManageDao.updateDigitalAccountStatus(targetId, status)
if (status == "ENABLED") "数电账号已恢复" else "数电账号已禁用"
}
private fun ensureDigitalOperatorAccount(
enterpriseId: Uuid,
taxpayerNum: String,
@@ -220,6 +241,22 @@ object PTConfigService {
if (!user.isSuperAdmin && row[PtDigitalAccountTable.enterpriseId] != requireEnterpriseId(user)) {
throw BizException(ErrorCode.FORBIDDEN.code, "无权操作该数电账号", HttpStatusCode.Forbidden)
}
if (row[PtDigitalAccountTable.status] != "ENABLED") {
throw BizException(ErrorCode.FORBIDDEN.code, "数电账号已禁用", HttpStatusCode.Forbidden)
}
EnterpriseManageDao.run { row.toDigitalAccountItem() }
}
suspend fun requireDigitalAccountForLogin(user: CurrentUser, taxpayerNum: String, account: String): DigitalAccountManageItem =
dbQuery {
val row = EnterpriseManageDao.digitalAccountByTaxpayerAndAccount(taxpayerNum.trim(), account.trim())
?: throw BizException(ErrorCode.BAD_REQUEST.code, "数电账号不存在", HttpStatusCode.NotFound)
if (!user.isSuperAdmin && row[PtDigitalAccountTable.enterpriseId] != requireEnterpriseId(user)) {
throw BizException(ErrorCode.FORBIDDEN.code, "无权操作该数电账号", HttpStatusCode.Forbidden)
}
if (row[PtDigitalAccountTable.status] != "ENABLED") {
throw BizException(ErrorCode.FORBIDDEN.code, "数电账号已禁用", HttpStatusCode.Forbidden)
}
EnterpriseManageDao.run { row.toDigitalAccountItem() }
}
}
@@ -7,10 +7,10 @@ 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.PTApi
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.parseUuid
import io.ktor.client.request.get
@@ -25,14 +25,19 @@ import kotlin.uuid.Uuid
object PTRedService {
/**
* 红票接口调用
* 红票接口调用 2.10
*/
suspend fun invoiceRed(user: CurrentUser,req:RedCreateRequest): String {
suspend fun invoiceRed(user: CurrentUser, req: RedCreateRequest): String {
val account = PTConfigService.requireDigitalAccountForAction(user, null)
val invoiceReqSerialNo = PTClient.ptDate()
val historyId = Uuid.parse(req.historyId)
val his = dbQuery {
HistoryDao.findByHistory(historyId, user.id)
HistoryDao.findByHistory(
historyId,
user.id,
user.enterpriseId,
if (user.isDigitalOperator) user.digitalAccountId else null,
)
}
val req = QuickRedInvoiceRequest(
taxpayerNum = account.taxpayerNum,
@@ -42,33 +47,57 @@ object PTRedService {
blueAllEleInvNo = his.electronicInvoiceNo,
blueInvoiceDate = his.blueInvoiceDate,
redReason = req.redReason,
amount = his.totalAmount?.negate()?.toPlainString()?:"0.0",
amount = his.totalAmount?.negate()?.toPlainString() ?: "0.0",
account = account.account,
invoiceKind = his.invoiceKind,
takerName = req.takerName,
takerTel = req.takerTel,
takerEmail = req.takerEmail,
)
PTClient.ptPost<QuickRedInvoiceRequest, QuickRedInvoiceResponse>("invoiceRed.pt", req)
PTApi.invoiceRed(req)
dbQuery { RedInvoiceDao.addRedInvoice(user.id, historyId, req) }
val enterpriseId = parseUuid(account.enterpriseId, "enterpriseId")
val digitalAccountId = parseUuid(account.id, "digitalAccountId")
PTBlueService.syncInvoiceFromPT(user.id, his.invoiceReqSerialNo, account.taxpayerNum, enterpriseId, digitalAccountId)
PTBlueService.syncInvoiceFromPT(user.id, invoiceReqSerialNo, account.taxpayerNum, enterpriseId, digitalAccountId)
PTBlueService.syncInvoiceFromPT(
user.id,
his.invoiceReqSerialNo,
account.taxpayerNum,
enterpriseId,
digitalAccountId
)
PTBlueService.syncInvoiceFromPT(
user.id,
invoiceReqSerialNo,
account.taxpayerNum,
enterpriseId,
digitalAccountId
)
return "操作成功"
}
/**
* 查询红票申请信息
*/
suspend fun getRedInvoiceInfo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? =
dbQuery { RedInvoiceDao.findRedInfoBySerialNo(userId, invoiceReqSerialNo) }
suspend fun getRedInvoiceInfo(user: CurrentUser, invoiceReqSerialNo: String): RedInvoiceInfoResponse? =
dbQuery {
RedInvoiceDao.findRedInfoBySerialNo(
user.id, user.enterpriseId, if (user.isDigitalOperator) user.digitalAccountId else null,
invoiceReqSerialNo,
)
}
suspend fun getRedInvoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery { RedInvoiceDao.invoiceDownloadUrl(userId, invoiceReqSerialNo) }
suspend fun getRedInvoiceDownloadUrl(user: CurrentUser, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery {
RedInvoiceDao.invoiceDownloadUrl(
user.id,
user.enterpriseId,
if (user.isDigitalOperator) user.digitalAccountId else null,
invoiceReqSerialNo,
)
}
suspend fun getRedInvoicePreview(userId: Uuid, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getRedInvoiceDownloadUrl(userId, invoiceReqSerialNo)?.downloadUrl
suspend fun getRedInvoicePreview(user: CurrentUser, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getRedInvoiceDownloadUrl(user, invoiceReqSerialNo)?.downloadUrl
?.takeIf { it.isNotBlank() }
?: return null
return PTClient.client.get(downloadUrl).bodyAsBytes()
@@ -13,12 +13,10 @@ import com.bbit.ticket.entity.common.system.LoginRequest
import com.bbit.ticket.entity.common.system.LoginResponse
import com.bbit.ticket.entity.common.system.MeResponse
import com.bbit.ticket.entity.request.EnterpriseRegisterRequest
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.toTaxRegisterInfo
import com.bbit.ticket.entity.response.EnterpriseTaxInfo
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.net.PTApi
import io.ktor.http.HttpStatusCode
import kotlin.uuid.ExperimentalUuidApi
@@ -92,7 +90,7 @@ object AuthService {
throw BizException(ErrorCode.DATA_CONFLICT.code, "企业已存在", HttpStatusCode.Conflict)
}
}
PTClient.ptPost<TaxRegisterInfo, EnterpriseTaxInfo>("register.pt", registerInfo)
PTApi.registerEnterprise(registerInfo)
dbQuery {
val enterpriseId = EnterpriseManageDao.createEnterprise(registerInfo)
@@ -2,7 +2,7 @@ package com.bbit.ticket.utils.bootstrap
object Global {
val isDev = false
val isDev = true
// 请求基础地址
var baseUrl: String
@@ -2,9 +2,14 @@
package com.bbit.ticket.utils.bootstrap
import com.bbit.ticket.dao.piaotong.EnterpriseManageDao
import com.bbit.ticket.database.piaotong.PtEnterpriseTable
import com.bbit.ticket.database.system.*
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.entity.request.QueryEnterpriseInfoRequest
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.service.system.PasswordService
import com.bbit.ticket.utils.net.PTApi
import com.bbit.ticket.utils.plugins.dbQuery
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
@@ -26,6 +31,7 @@ object SeedData {
const val ADMIN_INIT_PASSWORD = "Admin@123456"
private const val DEFAULT_ORG_CODE = "DEFAULT_ORG"
private const val DEFAULT_ADMIN_TAXPAYER_NUM = "500102201007206608"
private const val SUPER_ADMIN_ROLE_CODE = "SUPER_ADMIN"
private const val ENTERPRISE_ADMIN_ROLE_CODE = "ENTERPRISE_ADMIN"
private const val DIGITAL_OPERATOR_ROLE_CODE = "DIGITAL_OPERATOR"
@@ -38,6 +44,7 @@ object SeedData {
val now = OffsetDateTime.now()
val orgId = upsertDefaultOrg(now)
val roleId = upsertSuperAdminRole(now)
val defaultEnterpriseId = ensureDefaultAdminEnterprise()
val enterpriseAdminRoleId = upsertBusinessRole(
code = ENTERPRISE_ADMIN_ROLE_CODE,
name = "企业管理员",
@@ -50,9 +57,10 @@ object SeedData {
description = "数电账号对应的平台开票员角色",
now = now,
)
val adminId = upsertAdminUser(orgId, now)
val adminId = upsertAdminUser(orgId, defaultEnterpriseId, now)
upsertUserRole(adminId, roleId)
val menuIds = upsertMenus(now)
disableObsoleteMenus(now)
bindRoleMenus(roleId, menuIds.values.toList())
bindRoleMenus(enterpriseAdminRoleId, enterpriseAdminMenuIds(menuIds))
bindRoleMenus(digitalOperatorRoleId, digitalOperatorMenuIds(menuIds))
@@ -156,7 +164,40 @@ object SeedData {
// Admin user
// =========================================================
private suspend fun upsertAdminUser(orgId: Uuid, now: OffsetDateTime): Uuid = dbQuery {
private suspend fun ensureDefaultAdminEnterprise(): Uuid {
val existingId = dbQuery {
EnterpriseManageDao.findEnterpriseByTaxpayerNum(DEFAULT_ADMIN_TAXPAYER_NUM)?.get(PtEnterpriseTable.id)
}
val ptEnterprise = PTApi.getEnterpriseInfo(QueryEnterpriseInfoRequest(DEFAULT_ADMIN_TAXPAYER_NUM))
if (existingId != null) {
dbQuery { EnterpriseManageDao.updateEnterpriseFromPt(existingId, ptEnterprise) }
return existingId
}
val enterpriseInfo = TaxRegisterInfo(
taxpayerNum = DEFAULT_ADMIN_TAXPAYER_NUM,
enterpriseName = ptEnterprise.enterpriseName.ifBlank { DEFAULT_ADMIN_TAXPAYER_NUM },
legalPersonName = null,
contactsName = null,
contactsEmail = null,
contactsPhone = null,
regionCode = ptEnterprise.regionCode,
cityName = ptEnterprise.cityName,
enterpriseAddress = ptEnterprise.enterpriseAddress,
taxRegistrationCertificate = null,
)
return dbQuery {
val enterpriseId = EnterpriseManageDao.findEnterpriseByTaxpayerNum(DEFAULT_ADMIN_TAXPAYER_NUM)
?.get(PtEnterpriseTable.id)
?: EnterpriseManageDao.createEnterprise(enterpriseInfo)
EnterpriseManageDao.updateEnterpriseFromPt(enterpriseId, ptEnterprise)
enterpriseId
}
}
private suspend fun upsertAdminUser(orgId: Uuid, enterpriseId: Uuid, now: OffsetDateTime): Uuid = dbQuery {
val existing = SysUserTable.selectAll()
.where { (SysUserTable.username eq ADMIN_USERNAME) and SysUserTable.deletedAt.isNull() }
.singleOrNull()
@@ -166,6 +207,7 @@ object SeedData {
SysUserTable.update({ SysUserTable.id eq id }) {
it[SysUserTable.nickname] = "系统管理员"
it[SysUserTable.orgId] = orgId
it[SysUserTable.enterpriseId] = enterpriseId
it[SysUserTable.status] = "ENABLED"
it[SysUserTable.userType] = "SYSTEM"
it[SysUserTable.updatedAt] = now
@@ -180,6 +222,7 @@ object SeedData {
it[SysUserTable.passwordHash] = PasswordService.hash(ADMIN_INIT_PASSWORD)
it[SysUserTable.nickname] = "系统管理员"
it[SysUserTable.orgId] = orgId
it[SysUserTable.enterpriseId] = enterpriseId
it[SysUserTable.status] = "ENABLED"
it[SysUserTable.userType] = "SYSTEM"
it[SysUserTable.tokenVersion] = 1
@@ -234,15 +277,15 @@ object SeedData {
catalog("logs", "日志管理", "LogsRoot", "Logs", 30),
subMenu("logs_operation", "logs", "操作日志", "LogsOperation", "/logs/operation", "logs/operation/index", "ScrollText", "log:operation:view", 10),
subMenu("logs_api_access", "logs", "接口日志", "LogsApiAccess", "/logs/api-access", "logs/api-access/index", "Waypoints", "log:api-access:view", 20),
subMenu("openapi_statistics", "logs", "OpenAPI", "OpenApiStatistics", "/logs/openapi", "statistics/openapi/index", "Waypoints", "openapi:statistics:view", 30),
catalog("basic_info", "基础信息", "BasicInfoRoot", "Building2", 40),
subMenu("enterprise_info", "basic_info", "企业信息", "EnterpriseInfo", "/enterprise/info", "piaotong/index", "Building2", "enterprise:info:view", 10),
subMenu("digital_account", "basic_info", "数电账号", "DigitalAccountManage", "/enterprise/digital-accounts", "piaotong/digital-accounts/index", "Users", "digital-account:view", 20),
button("digital_account_status", "digital_account", "禁用/恢复数电账号", "DigitalAccountStatus", "digital-account:status", 1),
subMenu("invoice_setting", "basic_info", "开票设置", "InvoiceSetting", "/enterprise/invoice-setting", "piaotong/invoice-setting/index", "SlidersHorizontal", "invoice-setting:view", 30),
catalog("invoice_service", "开票服务", "InvoiceServiceRoot", "Receipt", 50),
subMenu("piaotong_invoice_issue", "invoice_service", "开具蓝票", "PiaoTongInvoiceIssue", "/piaotong/invoice-issue", "piaotong/invoice-issue/index", "FilePlus", "piaotong:invoice-issue:view", 10),
subMenu("piaotong_invoice_history", "invoice_service", "开票历史", "PiaoTongInvoiceHistory", "/piaotong/invoice-history", "piaotong/invoice-history/index", "History", "piaotong:invoice-history:view", 20),
catalog("statistics_info", "统计信息", "StatisticsInfoRoot", "ChartNoAxesColumn", 60),
subMenu("openapi_statistics", "statistics_info", "OpenAPI", "OpenApiStatistics", "/statistics/openapi", "statistics/openapi/index", "Waypoints", "openapi:statistics:view", 10),
)
val idMap = mutableMapOf<String, Uuid>()
@@ -260,11 +303,11 @@ object SeedData {
"basic_info",
"enterprise_info",
"digital_account",
"digital_account_status",
"invoice_setting",
"invoice_service",
"piaotong_invoice_issue",
"piaotong_invoice_history",
"statistics_info",
"openapi_statistics",
).mapNotNull { menuIds[it] }
@@ -276,7 +319,6 @@ object SeedData {
"invoice_service",
"piaotong_invoice_issue",
"piaotong_invoice_history",
"statistics_info",
"openapi_statistics",
).mapNotNull { menuIds[it] }
@@ -351,6 +393,16 @@ object SeedData {
}
}
private suspend fun disableObsoleteMenus(now: OffsetDateTime) = dbQuery {
SysMenuTable.update({
(SysMenuTable.name inList listOf("StatisticsInfoRoot")) and SysMenuTable.deletedAt.isNull()
}) {
it[SysMenuTable.visible] = false
it[SysMenuTable.status] = "DISABLED"
it[SysMenuTable.updatedAt] = now
}
}
// =========================================================
// Dicts
// =========================================================
@@ -0,0 +1,89 @@
package com.bbit.ticket.utils.net
import com.bbit.ticket.entity.request.AskInvoiceRequest
import com.bbit.ticket.entity.request.AuthQrcodeRequest
import com.bbit.ticket.entity.request.GetLoginSmsCodeRequest
import com.bbit.ticket.entity.request.InvoiceQueryRequest
import com.bbit.ticket.entity.request.QueryDigitalAccountListRequest
import com.bbit.ticket.entity.request.QueryEnterpriseBankAccountRequest
import com.bbit.ticket.entity.request.QueryEnterpriseInfoRequest
import com.bbit.ticket.entity.request.QueryInvoiceRequest
import com.bbit.ticket.entity.request.QueryRealNameAuthQrStatusRequest
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
import com.bbit.ticket.entity.request.SmsLoginRequest
import com.bbit.ticket.entity.request.TaxBureauAuthReq
import com.bbit.ticket.entity.request.TaxRegister
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.response.AuthQrcodeResponse
import com.bbit.ticket.entity.response.AuthQrcodeStatusResponse
import com.bbit.ticket.entity.response.DigitalAccountInfo
import com.bbit.ticket.entity.response.EnterpriseTaxInfo
import com.bbit.ticket.entity.response.EtaxRegisterResponse
import com.bbit.ticket.entity.response.GetInvoiceInfoResponse
import com.bbit.ticket.entity.response.InvoiceCreateResponse
import com.bbit.ticket.entity.response.InvoiceQueryResponse
import com.bbit.ticket.entity.response.LoginSmsCodeResponse
import com.bbit.ticket.entity.response.QueryEnterpriseBankAccountResponse
import com.bbit.ticket.entity.response.QueryEnterpriseInfoResponse
import com.bbit.ticket.entity.response.QuickRedInvoiceResponse
import com.bbit.ticket.entity.response.SMSLoginResponse
import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent
object PTApi {
/** 企业注册 2.2*/
suspend fun registerEnterprise(req: TaxRegisterInfo): EnterpriseTaxInfo =
PTClient.ptPost("register.pt", req)
/** 登记数电账号 2.3*/
suspend fun registerDigitalAccount(req: TaxRegister): EtaxRegisterResponse =
PTClient.ptPost("registerUser.pt", req)
/** 发送登录短信验证码 2.4 */
suspend fun sendLoginSmsCode(req: GetLoginSmsCodeRequest): LoginSmsCodeResponse =
PTClient.ptPost("sendLoginSmsCode.pt", req)
/** 短信验证码登录 2.5 */
suspend fun smsLogin(req: SmsLoginRequest): SMSLoginResponse =
PTClient.ptPost("smsLogin.pt", req)
/** 获取实名认证二维码 2.6 */
suspend fun getAuthenticationQrcode(req: AuthQrcodeRequest): AuthQrcodeResponse =
PTClient.ptPost("getAuthenticationQrcode.pt", req)
/** 查询认证二维码扫码状态 2.7 */
suspend fun queryAuthQrcodeScanStatus(req: QueryRealNameAuthQrStatusRequest): AuthQrcodeStatusResponse =
PTClient.ptPost("queryAuthQrcodeScanStatus.pt", req)
/** 查询数电账号认证状态 2.8 */
suspend fun getTaxBureauAccountAuthStatus(req: TaxBureauAuthReq): TaxBureauAccountAuthContent =
PTClient.ptPost("getTaxBureauAccountAuthStatus.pt", req)
/** 蓝票接口调用 2.9 */
suspend fun invoiceBlue(req: AskInvoiceRequest): InvoiceCreateResponse =
PTClient.ptPost("invoiceBlue.pt", req)
/** 红票接口调用 2.10 */
suspend fun invoiceRed(req: QuickRedInvoiceRequest): QuickRedInvoiceResponse =
PTClient.ptPost("invoiceRed.pt", req)
/** 开票统计及授信额度查询 2.20 */
suspend fun queryAllEleBlueInvStatistics(req: InvoiceQueryRequest): InvoiceQueryResponse =
PTClient.ptPost("queryAllEleBlueInvStatistics.pt", req)
/** 查询票通同步发票信息 2.12*/
suspend fun queryInvoiceInfo(req: QueryInvoiceRequest): GetInvoiceInfoResponse =
PTClient.ptPost("queryInvoiceInfo.pt", req)
/** 查询数电账号列表 2.45 */
suspend fun listTaxBureauAccount(req: QueryDigitalAccountListRequest): List<DigitalAccountInfo> =
PTClient.ptPost("listTaxBureauAccount.pt", req)
/** 查询企业状态 2.47 */
suspend fun getEnterpriseInfo(req: QueryEnterpriseInfoRequest): QueryEnterpriseInfoResponse =
PTClient.ptPost("getEnterpriseInfo.pt", req)
/** 查询企业开户行及账号 2.49 */
suspend fun queryEnterpriseBankInfo(req: QueryEnterpriseBankAccountRequest): QueryEnterpriseBankAccountResponse =
PTClient.ptPost("queryEnterpriseBankInfo.pt", req)
}
+4
View File
@@ -172,6 +172,10 @@ export function createDigitalAccountApi(
return http.post('/pt/digital-accounts', payload)
}
export function updateDigitalAccountStatusApi(id: string, status: string): Promise<string> {
return http.put(`/pt/digital-accounts/${id}/status`, { status })
}
export interface PresetData {
bankName: string
bankAccount: string
@@ -110,6 +110,7 @@ import {
NIcon,
NInput,
NModal,
NPopconfirm,
NSelect,
NSpace,
NTag,
@@ -122,7 +123,8 @@ import {
listDigitalAccountsApi,
refreshDigitalAccountsApi,
sendLoginSmsCodeApi,
smsLoginApi
smsLoginApi,
updateDigitalAccountStatusApi
} from '@/api/piaotong'
import type { DigitalAccountItem } from '@/api/piaotong'
import { useAuthStore } from '@/stores/auth'
@@ -144,6 +146,7 @@ const selected = ref<DigitalAccountItem | null>(null)
const createFormRef = ref<FormInst | null>(null)
const canManage = computed(() => authStore.user?.userType !== 'DIGITAL_OPERATOR')
const canUpdateStatus = computed(() => authStore.hasPermission('digital-account:status'))
const createForm = reactive({
account: '',
@@ -197,6 +200,17 @@ const columns: DataTableColumns<DigitalAccountItem> = [
)
},
{ title: '最近认证', key: 'lastAuthSuccTime', minWidth: 160 },
{
title: '平台状态',
key: 'status',
width: 110,
render: (row) =>
h(
NTag,
{ type: row.status === 'ENABLED' ? 'success' : 'warning' },
{ default: () => (row.status === 'ENABLED' ? '可用' : '禁用') }
)
},
{
title: 'API Key',
key: 'apiKey',
@@ -207,19 +221,49 @@ const columns: DataTableColumns<DigitalAccountItem> = [
title: '操作',
key: 'actions',
fixed: 'right',
width: 220,
width: 300,
render: (row) =>
h('div', { class: 'row-actions' }, [
h(
NButton,
{ size: 'small', secondary: true, onClick: () => openSms(row) },
{
size: 'small',
secondary: true,
disabled: row.status !== 'ENABLED',
onClick: () => openSms(row)
},
{ icon: () => h(NIcon, { component: KeyRound }), default: () => '短信登录' }
),
h(
NButton,
{ size: 'small', tertiary: true, onClick: () => openQrcode(row) },
{
size: 'small',
tertiary: true,
disabled: row.status !== 'ENABLED',
onClick: () => openQrcode(row)
},
{ icon: () => h(NIcon, { component: ShieldCheck }), default: () => '认证' }
),
canUpdateStatus.value
? h(
NPopconfirm,
{
onPositiveClick: () => updateAccountStatus(row)
},
{
trigger: () =>
h(
NButton,
{ size: 'small', tertiary: true, type: row.status === 'ENABLED' ? 'warning' : 'success' },
{ default: () => (row.status === 'ENABLED' ? '禁用' : '恢复') }
),
default: () =>
row.status === 'ENABLED'
? `确认禁用数电账号 ${row.account} 吗?`
: `确认恢复数电账号 ${row.account} 吗?`
}
)
: null
])
}
]
@@ -270,6 +314,13 @@ async function createAccount() {
}
}
async function updateAccountStatus(row: DigitalAccountItem) {
const nextStatus = row.status === 'ENABLED' ? 'DISABLED' : 'ENABLED'
await updateDigitalAccountStatusApi(row.id, nextStatus)
message.success(nextStatus === 'ENABLED' ? '数电账号已恢复' : '数电账号已禁用')
await load()
}
async function openSms(row: DigitalAccountItem) {
selected.value = row
smsCode.value = ''
@@ -400,7 +400,6 @@
style="color: #e74c3c; font-size: 12px"
>农产品开票销方信息代表实际的购方信息</span
>
<span v-else class="section-hint">不填则使用平台默认配置</span>
</div>
<div class="section-body">
<n-grid item-responsive responsive="screen" cols="2 s:2 m:3 l:4 xl:5 2xl:6" :x-gap="12" :y-gap="4">
-50
View File
@@ -102,26 +102,6 @@
</div>
</div>
<div class="detail-section">
<div class="detail-section-title">税务信息</div>
<div class="detail-grid">
<div v-for="item in taxDetailItems" :key="item.label" class="detail-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-section-title">银行与预设信息</div>
<div class="detail-grid">
<div v-for="item in presetDetailItems" :key="item.label" class="detail-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-section-title">系统信息</div>
<div class="detail-grid">
@@ -342,36 +322,6 @@ const baseDetailItems = computed(() => {
]
})
const taxDetailItems = computed(() => {
const user = detailUser.value
if (!user) return []
return [
{ label: '纳税人识别号', value: displayValue(user.taxpayerNum) },
{ label: '数电账号', value: displayValue(user.account) },
{ label: '身份类型', value: displayValue(user.taxIdentityType) },
{ label: '联系人', value: displayValue(user.taxContactName) },
{ label: '联系人电话', value: displayValue(user.taxContactPhone) },
{ label: '联系人邮箱', value: displayValue(user.taxContactEmail) },
{ label: '法定代表人', value: displayValue(user.taxLegalPersonName) },
{ label: '企业名称', value: displayValue(user.taxEnterpriseName) },
{ label: '地区编码', value: displayValue(user.taxRegionCode) },
{ label: '城市', value: displayValue(user.taxCityName) },
{ label: '企业地址', value: displayValue(user.taxEnterpriseAddress) },
{ label: '税务登记证', value: displayValue(user.taxRegistrationCertificate) }
]
})
const presetDetailItems = computed(() => {
const user = detailUser.value
if (!user) return []
return [
{ label: '开户行', value: displayValue(user.bankName) },
{ label: '银行账号', value: displayValue(user.bankAccount) },
{ label: '预设地址', value: displayValue(user.presetAddress) },
{ label: '预设电话', value: displayValue(user.presetPhone) }
]
})
const systemDetailItems = computed(() => {
const user = detailUser.value
if (!user) return []
-20
View File
@@ -36,22 +36,6 @@ export interface UserDetail {
deletedAt?: string | null
deletedBy?: string | null
version: number
taxpayerNum?: string | null
account?: string | null
taxIdentityType?: string | null
taxContactName?: string | null
taxContactPhone?: string | null
taxContactEmail?: string | null
taxLegalPersonName?: string | null
taxEnterpriseName?: string | null
taxRegionCode?: string | null
taxCityName?: string | null
taxEnterpriseAddress?: string | null
taxRegistrationCertificate?: string | null
bankName?: string | null
bankAccount?: string | null
presetAddress?: string | null
presetPhone?: string | null
}
export interface UserRoleBrief {
@@ -90,10 +74,6 @@ export interface UpdateUserRequest {
email?: string
avatar?: string
orgId?: string
taxpayerNum?: string
account?: string
taxPassword?: string
taxIdentityType?: string
}
export interface UpdateUserStatusRequest {