多数电账号管理

This commit is contained in:
BBIT-Kai
2026-05-22 15:37:45 +08:00
parent f718ff46da
commit d57ea3960c
63 changed files with 2421 additions and 2886 deletions
@@ -61,6 +61,8 @@ object BlueInvoiceDao {
*/
fun invoiceHistory(
userId: Uuid,
enterpriseId: Uuid?,
digitalAccountId: Uuid?,
page: Int,
pageSize: Int,
invoiceType: String? = null,
@@ -68,7 +70,13 @@ object BlueInvoiceDao {
batchNo: String? = null,
): PageResult<InvoiceHistoryItem> {
val conditions = mutableListOf<Op<Boolean>>()
conditions.add(HistoryInvoiceBasicTable.userId eq userId)
if (digitalAccountId != null) {
conditions.add(HistoryInvoiceBasicTable.digitalAccountId eq digitalAccountId)
} else if (enterpriseId != null) {
conditions.add(HistoryInvoiceBasicTable.enterpriseId eq enterpriseId)
} else {
conditions.add(HistoryInvoiceBasicTable.userId eq userId)
}
conditions.add(HistoryInvoiceBasicTable.deletedAt.isNull())
// 发票类型筛选:前端传 BLUE/RED,库中存 1/2
@@ -147,12 +155,14 @@ object BlueInvoiceDao {
return PageResult(items, page, pageSize, total)
}
fun addInvoice(userId: Uuid, req: AskInvoiceRequest) {
fun addInvoice(userId: Uuid, req: AskInvoiceRequest, enterpriseId: Uuid?, digitalAccountId: Uuid?) {
val now = OffsetDateTime.now()
// 1. 插入 HistoryInvoiceBasicTable(基本信息历史快照)
val basicRow = HistoryInvoiceBasicTable.insert {
it[HistoryInvoiceBasicTable.userId] = userId
it[HistoryInvoiceBasicTable.enterpriseId] = enterpriseId
it[HistoryInvoiceBasicTable.digitalAccountId] = digitalAccountId
it[HistoryInvoiceBasicTable.invoiceReqSerialNo] = req.invoiceReqSerialNo
// ---- 状态 ----
@@ -295,7 +305,7 @@ object BlueInvoiceDao {
* - 记录不存在 → 插入(由查询结果反推基础数据)
* - 商品明细采用先删后插策略
*/
fun upsertInvoiceInfo(userId: Uuid, req: GetInvoiceInfoResponse) {
fun upsertInvoiceInfo(userId: Uuid, req: GetInvoiceInfoResponse, enterpriseId: Uuid?, digitalAccountId: Uuid?) {
transaction {
val now = OffsetDateTime.now()
val existingId = HistoryInvoiceBasicTable
@@ -307,6 +317,8 @@ object BlueInvoiceDao {
val invoiceId = if (existingId != null) {
// 更新已有记录
HistoryInvoiceBasicTable.update({ HistoryInvoiceBasicTable.id eq existingId }) {
if (enterpriseId != null) it[HistoryInvoiceBasicTable.enterpriseId] = enterpriseId
if (digitalAccountId != null) it[HistoryInvoiceBasicTable.digitalAccountId] = digitalAccountId
applySyncFields(it, req)
it[HistoryInvoiceBasicTable.updatedAt] = now
}
@@ -315,6 +327,8 @@ object BlueInvoiceDao {
// 插入新记录
HistoryInvoiceBasicTable.insert {
it[HistoryInvoiceBasicTable.userId] = userId
it[HistoryInvoiceBasicTable.enterpriseId] = enterpriseId
it[HistoryInvoiceBasicTable.digitalAccountId] = digitalAccountId
it[HistoryInvoiceBasicTable.invoiceReqSerialNo] = req.invoiceReqSerialNo
applySyncFields(it, req)
it[HistoryInvoiceBasicTable.createdAt] = now
@@ -449,6 +463,25 @@ object BlueInvoiceDao {
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息")
}
fun findInvoiceScopeBySerialNo(invoiceReqSerialNo: String): InvoiceScope {
val row = HistoryInvoiceBasicTable.selectAll()
.where { HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo }
.singleOrNull()
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在")
return InvoiceScope(
userId = row[HistoryInvoiceBasicTable.userId]
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息"),
enterpriseId = row[HistoryInvoiceBasicTable.enterpriseId],
digitalAccountId = row[HistoryInvoiceBasicTable.digitalAccountId],
)
}
data class InvoiceScope(
val userId: Uuid,
val enterpriseId: Uuid?,
val digitalAccountId: Uuid?,
)
fun findRelatedInvoiceReqSerialNos(userId: Uuid, invoiceReqSerialNo: String): List<String> {
val basicRow = HistoryInvoiceBasicTable.selectAll()
.where {
@@ -0,0 +1,303 @@
@file:OptIn(ExperimentalUuidApi::class)
package com.bbit.ticket.dao.piaotong
import com.bbit.ticket.database.piaotong.PtDigitalAccountTable
import com.bbit.ticket.database.piaotong.PtEnterpriseTable
import com.bbit.ticket.database.system.SysApiAccessLogTable
import com.bbit.ticket.database.system.SysUserTable
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest
import com.bbit.ticket.entity.response.DigitalAccountInfo
import com.bbit.ticket.entity.response.DigitalAccountManageItem
import com.bbit.ticket.entity.response.EnterpriseManageResponse
import com.bbit.ticket.entity.response.OpenApiStatisticsItem
import com.bbit.ticket.entity.response.QueryEnterpriseInfoResponse
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.isNull
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import java.time.OffsetDateTime
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
object EnterpriseManageDao {
fun findEnterpriseByTaxpayerNum(taxpayerNum: String): ResultRow? =
PtEnterpriseTable.selectAll()
.where { (PtEnterpriseTable.taxpayerNum eq taxpayerNum) and PtEnterpriseTable.deletedAt.isNull() }
.singleOrNull()
fun createEnterprise(req: TaxRegisterInfo): Uuid {
val now = OffsetDateTime.now()
return PtEnterpriseTable.insert {
it[taxpayerNum] = req.taxpayerNum.trim()
it[enterpriseName] = req.enterpriseName.trim()
it[legalPersonName] = req.legalPersonName?.trim()?.ifBlank { null }
it[contactsName] = req.contactsName?.trim()?.ifBlank { null }
it[contactsEmail] = req.contactsEmail?.trim()?.ifBlank { null }
it[contactsPhone] = req.contactsPhone?.trim()?.ifBlank { null }
it[regionCode] = req.regionCode.trim().ifBlank { null }
it[cityName] = req.cityName.trim().ifBlank { null }
it[enterpriseAddress] = req.enterpriseAddress?.trim()?.ifBlank { null }
it[taxRegistrationCertificate] = req.taxRegistrationCertificate?.trim()?.ifBlank { null }
it[createdAt] = now
}[PtEnterpriseTable.id]
}
fun enterpriseDetail(enterpriseId: Uuid): EnterpriseManageResponse? =
PtEnterpriseTable.selectAll()
.where { (PtEnterpriseTable.id eq enterpriseId) and PtEnterpriseTable.deletedAt.isNull() }
.singleOrNull()
?.toEnterpriseResponse()
fun updateEnterpriseFromPt(enterpriseId: Uuid, res: QueryEnterpriseInfoResponse) {
PtEnterpriseTable.update({ PtEnterpriseTable.id eq enterpriseId }) {
it[taxpayerNum] = res.taxpayerNum
it[enterpriseName] = res.enterpriseName
it[regionCode] = res.regionCode
it[cityName] = res.cityName
it[enterpriseAddress] = res.enterpriseAddress
it[invitationCode] = res.invitationCode
it[reviewStatus] = res.reviewStatus
it[reviewOpinion] = res.reviewOpinion
it[invoiceKind] = res.invoiceKind
it[invoiceLayoutFileType] = res.invoiceLayoutFileType
it[serviceStatus] = res.serviceStatus
it[updatedAt] = OffsetDateTime.now()
}
}
fun updateInvoiceSetting(enterpriseId: Uuid, req: UpdateInvoiceSettingRequest) {
PtEnterpriseTable.update({ PtEnterpriseTable.id eq enterpriseId }) {
it[bankName] = req.bankName.trim().ifBlank { null }
it[bankAccount] = req.bankAccount.trim().ifBlank { null }
it[presetAddress] = req.address.trim().ifBlank { null }
it[presetPhone] = req.phone.trim().ifBlank { null }
it[updatedAt] = OffsetDateTime.now()
}
}
fun digitalAccountsForEnterprise(enterpriseId: Uuid): List<DigitalAccountManageItem> =
PtDigitalAccountTable.selectAll()
.where { (PtDigitalAccountTable.enterpriseId eq enterpriseId) and PtDigitalAccountTable.deletedAt.isNull() }
.orderBy(PtDigitalAccountTable.createdAt, SortOrder.DESC)
.map { it.toDigitalAccountItem() }
fun digitalAccount(id: Uuid): ResultRow? =
PtDigitalAccountTable.selectAll()
.where { (PtDigitalAccountTable.id eq id) and PtDigitalAccountTable.deletedAt.isNull() }
.singleOrNull()
fun digitalAccountByApiKey(apiKey: String): ResultRow? =
PtDigitalAccountTable.selectAll()
.where {
(PtDigitalAccountTable.apiKey eq apiKey) and
(PtDigitalAccountTable.status eq "ENABLED") and
PtDigitalAccountTable.deletedAt.isNull()
}
.singleOrNull()
fun digitalAccountForUser(userId: Uuid): ResultRow? =
PtDigitalAccountTable.selectAll()
.where {
(PtDigitalAccountTable.platformUserId eq userId) and
(PtDigitalAccountTable.status eq "ENABLED") and
PtDigitalAccountTable.deletedAt.isNull()
}
.singleOrNull()
fun upsertDigitalAccount(enterpriseId: Uuid, item: DigitalAccountInfo): Uuid {
val existing = PtDigitalAccountTable.selectAll()
.where {
(PtDigitalAccountTable.enterpriseId eq enterpriseId) and
(PtDigitalAccountTable.account eq item.account) and
PtDigitalAccountTable.deletedAt.isNull()
}
.singleOrNull()
val now = OffsetDateTime.now()
if (existing != null) {
val id = existing[PtDigitalAccountTable.id]
PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq id }) {
it[taxpayerNum] = item.taxpayerNum
it[name] = item.name
it[identityType] = item.identityType
it[operationProposed] = item.operationProposed
it[authStatus] = item.authStatus
it[switchable] = item.switchable
it[wechatUserBindStatus] = item.wechatUserBindStatus
it[lastAuthSuccTime] = item.lastAuthSuccTime
it[loginAuthStatus] = item.loginAuthStatus
it[lastLoginAuthTime] = item.lastLoginAuthTime
it[riskAuthStatus] = item.riskAuthStatus
it[lastRiskAuthTime] = item.lastRiskAuthTime
it[updatedAt] = now
}
return id
}
return PtDigitalAccountTable.insert {
it[PtDigitalAccountTable.enterpriseId] = enterpriseId
it[taxpayerNum] = item.taxpayerNum
it[account] = item.account
it[name] = item.name
it[identityType] = item.identityType
it[operationProposed] = item.operationProposed
it[authStatus] = item.authStatus
it[switchable] = item.switchable
it[wechatUserBindStatus] = item.wechatUserBindStatus
it[lastAuthSuccTime] = item.lastAuthSuccTime
it[loginAuthStatus] = item.loginAuthStatus
it[lastLoginAuthTime] = item.lastLoginAuthTime
it[riskAuthStatus] = item.riskAuthStatus
it[lastRiskAuthTime] = item.lastRiskAuthTime
it[createdAt] = now
}[PtDigitalAccountTable.id]
}
fun createDigitalAccount(
enterpriseId: Uuid,
taxpayerNum: String,
account: String,
name: String,
identityType: String,
platformUserId: Uuid,
apiKey: String,
): Uuid {
val existing = PtDigitalAccountTable.selectAll()
.where {
(PtDigitalAccountTable.enterpriseId eq enterpriseId) and
(PtDigitalAccountTable.account eq account) and
PtDigitalAccountTable.deletedAt.isNull()
}
.singleOrNull()
val now = OffsetDateTime.now()
if (existing != null) {
val id = existing[PtDigitalAccountTable.id]
PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq id }) {
it[PtDigitalAccountTable.name] = name
it[PtDigitalAccountTable.identityType] = identityType
it[PtDigitalAccountTable.platformUserId] = platformUserId
it[PtDigitalAccountTable.apiKey] = apiKey
it[updatedAt] = now
}
return id
}
return PtDigitalAccountTable.insert {
it[PtDigitalAccountTable.enterpriseId] = enterpriseId
it[PtDigitalAccountTable.taxpayerNum] = taxpayerNum
it[PtDigitalAccountTable.account] = account
it[PtDigitalAccountTable.name] = name
it[PtDigitalAccountTable.identityType] = identityType
it[PtDigitalAccountTable.platformUserId] = platformUserId
it[PtDigitalAccountTable.apiKey] = apiKey
it[PtDigitalAccountTable.createdAt] = now
}[PtDigitalAccountTable.id]
}
fun bindDigitalAccountUser(digitalAccountId: Uuid, userId: Uuid) {
PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq digitalAccountId }) {
it[platformUserId] = userId
it[updatedAt] = OffsetDateTime.now()
}
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.digitalAccountId] = digitalAccountId
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
fun bindDigitalAccountUserAndApiKey(digitalAccountId: Uuid, userId: Uuid, apiKey: String) {
PtDigitalAccountTable.update({ PtDigitalAccountTable.id eq digitalAccountId }) {
it[platformUserId] = userId
it[PtDigitalAccountTable.apiKey] = apiKey
it[updatedAt] = OffsetDateTime.now()
}
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.digitalAccountId] = digitalAccountId
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
fun openApiStatistics(enterpriseId: Uuid, digitalAccountId: Uuid?): List<OpenApiStatisticsItem> {
var where = SysApiAccessLogTable.enterpriseId eq enterpriseId
if (digitalAccountId != null) {
where = where and (SysApiAccessLogTable.digitalAccountId eq digitalAccountId)
}
return SysApiAccessLogTable
.selectAll()
.where { where }
.toList()
.groupBy { row -> row[SysApiAccessLogTable.digitalAccountId] to row[SysApiAccessLogTable.interfaceCode] }
.map { (key, rows) ->
OpenApiStatisticsItem(
digitalAccountId = key.first?.toString(),
interfaceCode = key.second,
total = rows.size.toLong(),
success = rows.count { it[SysApiAccessLogTable.status] == "SUCCESS" }.toLong(),
failed = rows.count { it[SysApiAccessLogTable.status] != "SUCCESS" }.toLong(),
avgCostMs = rows.map { it[SysApiAccessLogTable.costMs] }.average().toLong(),
lastCalledAt = rows.maxByOrNull { it[SysApiAccessLogTable.createdAt] }
?.get(SysApiAccessLogTable.createdAt)
?.toString(),
)
}
}
private fun ResultRow.toEnterpriseResponse() = EnterpriseManageResponse(
id = this[PtEnterpriseTable.id].toString(),
taxpayerNum = this[PtEnterpriseTable.taxpayerNum],
enterpriseName = this[PtEnterpriseTable.enterpriseName],
legalPersonName = this[PtEnterpriseTable.legalPersonName],
contactsName = this[PtEnterpriseTable.contactsName],
contactsEmail = this[PtEnterpriseTable.contactsEmail],
contactsPhone = this[PtEnterpriseTable.contactsPhone],
regionCode = this[PtEnterpriseTable.regionCode],
cityName = this[PtEnterpriseTable.cityName],
enterpriseAddress = this[PtEnterpriseTable.enterpriseAddress],
taxRegistrationCertificate = this[PtEnterpriseTable.taxRegistrationCertificate],
invitationCode = this[PtEnterpriseTable.invitationCode],
reviewStatus = this[PtEnterpriseTable.reviewStatus],
reviewOpinion = this[PtEnterpriseTable.reviewOpinion],
invoiceKind = this[PtEnterpriseTable.invoiceKind],
invoiceLayoutFileType = this[PtEnterpriseTable.invoiceLayoutFileType],
serviceStatus = this[PtEnterpriseTable.serviceStatus],
bankName = this[PtEnterpriseTable.bankName],
bankAccount = this[PtEnterpriseTable.bankAccount],
presetAddress = this[PtEnterpriseTable.presetAddress],
presetPhone = this[PtEnterpriseTable.presetPhone],
)
fun ResultRow.toDigitalAccountItem() = DigitalAccountManageItem(
id = this[PtDigitalAccountTable.id].toString(),
enterpriseId = this[PtDigitalAccountTable.enterpriseId].toString(),
taxpayerNum = this[PtDigitalAccountTable.taxpayerNum],
account = this[PtDigitalAccountTable.account],
name = this[PtDigitalAccountTable.name],
identityType = this[PtDigitalAccountTable.identityType],
operationProposed = this[PtDigitalAccountTable.operationProposed],
authStatus = this[PtDigitalAccountTable.authStatus],
switchable = this[PtDigitalAccountTable.switchable],
wechatUserBindStatus = this[PtDigitalAccountTable.wechatUserBindStatus],
lastAuthSuccTime = this[PtDigitalAccountTable.lastAuthSuccTime],
loginAuthStatus = this[PtDigitalAccountTable.loginAuthStatus],
lastLoginAuthTime = this[PtDigitalAccountTable.lastLoginAuthTime],
riskAuthStatus = this[PtDigitalAccountTable.riskAuthStatus],
lastRiskAuthTime = this[PtDigitalAccountTable.lastRiskAuthTime],
platformUserId = this[PtDigitalAccountTable.platformUserId]?.toString(),
platformUsername = this[PtDigitalAccountTable.platformUserId]?.let { platformUserId ->
SysUserTable.selectAll()
.where { (SysUserTable.id eq platformUserId) and SysUserTable.deletedAt.isNull() }
.singleOrNull()
?.get(SysUserTable.username)
},
apiKey = this[PtDigitalAccountTable.apiKey],
status = this[PtDigitalAccountTable.status],
)
}
@@ -1,144 +0,0 @@
@file:OptIn(ExperimentalUuidApi::class)
package com.bbit.ticket.dao.piaotong
import com.bbit.ticket.database.piaotong.HistoryInvoiceOrderTable
import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable
import com.bbit.ticket.database.system.SysUserTable
import com.bbit.ticket.entity.common.PageResult
import com.bbit.ticket.entity.request.AskInvoiceRequest
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.UpdateDigitalAccountRequest
import com.bbit.ticket.entity.request.UpdateEnterpriseInfoRequest
import com.bbit.ticket.entity.request.UpdatePresetDataRequest
import com.bbit.ticket.entity.response.DigitalAccountResponse
import com.bbit.ticket.entity.response.EnterpriseInfoResponse
import com.bbit.ticket.entity.response.InvoiceDetailGoods
import com.bbit.ticket.entity.response.InvoiceDetailOrder
import com.bbit.ticket.entity.response.InvoiceDetailResponse
import com.bbit.ticket.entity.response.InvoiceDetailVoucher
import com.bbit.ticket.entity.response.InvoiceHistoryItem
import com.bbit.ticket.entity.response.PresetDataResponse
import com.bbit.ticket.utils.formatDateTime
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import java.math.BigDecimal
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
object EnterpriseTaxDao {
// =============================================
// 企业信息
// =============================================
fun getEnterpriseInfo(userId: Uuid): EnterpriseInfoResponse? {
val row = SysUserTable.selectAll().where { SysUserTable.id eq userId }.singleOrNull() ?: return null
return EnterpriseInfoResponse(
taxpayerNum = row[SysUserTable.taxpayerNum],
enterpriseName = row[SysUserTable.taxEnterpriseName],
legalPersonName = row[SysUserTable.taxLegalPersonName],
contactsName = row[SysUserTable.taxContactName],
contactsEmail = row[SysUserTable.taxContactEmail],
contactsPhone = row[SysUserTable.taxContactPhone],
regionCode = row[SysUserTable.taxRegionCode],
cityName = row[SysUserTable.taxCityName],
enterpriseAddress = row[SysUserTable.taxEnterpriseAddress],
taxRegistrationCertificate = row[SysUserTable.taxRegistrationCertificate]
)
}
fun updateEnterpriseInfoLocal(userId: Uuid, req: UpdateEnterpriseInfoRequest) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = req.taxpayerNum.trim().ifBlank { null }
it[SysUserTable.taxEnterpriseName] = req.enterpriseName.trim().ifBlank { null }
it[SysUserTable.taxLegalPersonName] = req.legalPersonName.trim().ifBlank { null }
it[SysUserTable.taxContactName] = req.contactsName.trim().ifBlank { null }
it[SysUserTable.taxContactEmail] = req.contactsEmail.trim().ifBlank { null }
it[SysUserTable.taxContactPhone] = req.contactsPhone.trim().ifBlank { null }
it[SysUserTable.taxRegionCode] = req.regionCode.trim().ifBlank { null }
it[SysUserTable.taxCityName] = req.cityName.trim().ifBlank { null }
it[SysUserTable.taxEnterpriseAddress] = req.enterpriseAddress.trim().ifBlank { null }
it[SysUserTable.taxRegistrationCertificate] = req.taxRegistrationCertificate.ifBlank { null }
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
// =============================================
// 登记数电账号
// =============================================
fun getDigitalAccount(userId: Uuid): DigitalAccountResponse? {
val row = SysUserTable.selectAll().where { SysUserTable.id eq userId }.singleOrNull() ?: return null
return DigitalAccountResponse(
taxpayerNum = row[SysUserTable.taxpayerNum],
taxAccount = row[SysUserTable.taxAccount]
)
}
fun updateDigitalAccountLocal(userId: Uuid, req: UpdateDigitalAccountRequest) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = req.taxpayerNum.trim().ifBlank { null }
it[SysUserTable.taxAccount] = req.taxAccount.trim().ifBlank { null }
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
// =============================================
// 开票预设数据
// =============================================
fun getPresetData(userId: Uuid): PresetDataResponse? {
val row = SysUserTable.selectAll().where { SysUserTable.id eq userId }.singleOrNull() ?: return null
return PresetDataResponse(
bankName = row[SysUserTable.bankName],
bankAccount = row[SysUserTable.bankAccount],
address = row[SysUserTable.presetAddress],
phone = row[SysUserTable.presetPhone]
)
}
fun updatePresetData(userId: Uuid, req: UpdatePresetDataRequest) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.bankName] = req.bankName.trim().ifBlank { null }
it[SysUserTable.bankAccount] = req.bankAccount.trim().ifBlank { null }
it[SysUserTable.presetAddress] = req.address.trim().ifBlank { null }
it[SysUserTable.presetPhone] = req.phone.trim().ifBlank { null }
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
fun updateEnterpriseInfo(userId: Uuid, req: TaxRegisterInfo) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = req.taxpayerNum
it[SysUserTable.taxEnterpriseName] = req.enterpriseName
it[SysUserTable.taxContactName] = req.contactsName
it[SysUserTable.taxContactPhone] = req.contactsPhone
it[SysUserTable.taxContactEmail] = req.contactsEmail
it[SysUserTable.taxLegalPersonName] = req.legalPersonName
it[SysUserTable.taxRegionCode] = req.regionCode
it[SysUserTable.taxCityName] = req.cityName
it[SysUserTable.taxEnterpriseAddress] = req.enterpriseAddress
it[SysUserTable.taxRegistrationCertificate] = req.taxRegistrationCertificate
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
fun updateUserAccount(userId: Uuid, taxpayerNum: String, taxAccount: String) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[SysUserTable.taxpayerNum] = taxpayerNum
it[SysUserTable.taxAccount] = taxAccount
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
}
@@ -32,6 +32,8 @@ object RedInvoiceDao {
// 2. 插入红票历史记录(用于开票历史列表展示)
HistoryInvoiceBasicTable.insert {
it[HistoryInvoiceBasicTable.userId] = userId
it[HistoryInvoiceBasicTable.enterpriseId] = blueRow?.get(HistoryInvoiceBasicTable.enterpriseId)
it[HistoryInvoiceBasicTable.digitalAccountId] = blueRow?.get(HistoryInvoiceBasicTable.digitalAccountId)
it[HistoryInvoiceBasicTable.invoiceReqSerialNo] = req.invoiceReqSerialNo
// ---- 状态 ----
@@ -24,6 +24,7 @@ import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import java.time.OffsetDateTime
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
object LogDao {
private const val MAX_API_LOG_BODY_LENGTH = 20_000
@@ -132,6 +133,9 @@ object LogDao {
call: ApplicationCall,
appKey: String?,
appName: String?,
enterpriseId: Uuid?,
digitalAccountId: Uuid?,
interfaceCode: String?,
requestBody: String?,
responseCode: String?,
responseBody: String?,
@@ -143,6 +147,10 @@ object LogDao {
it[traceId] = call.traceIdOrNull()
it[SysApiAccessLogTable.appKey] = appKey?.take(100)
it[SysApiAccessLogTable.appName] = appName?.take(100)
it[SysApiAccessLogTable.enterpriseId] = enterpriseId
it[SysApiAccessLogTable.digitalAccountId] = digitalAccountId
it[SysApiAccessLogTable.apiKey] = appKey?.take(128)
it[SysApiAccessLogTable.interfaceCode] = interfaceCode?.take(100)
it[httpMethod] = call.request.httpMethod.value
it[requestPath] = call.request.path().take(255)
it[requestHeaders] = null
@@ -13,7 +13,6 @@ import com.bbit.ticket.entity.common.ErrorCode
import com.bbit.ticket.entity.common.PageResult
import com.bbit.ticket.entity.common.statusLabel
import com.bbit.ticket.entity.common.system.UserRoleBrief
import com.bbit.ticket.utils.ApiKeyUtil
import io.ktor.http.HttpStatusCode
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.ResultRow
@@ -82,13 +81,39 @@ object UserDao {
it[SysUserTable.avatar] = request.avatar.trimToNull()
it[SysUserTable.orgId] = orgId
it[SysUserTable.status] = request.status
it[SysUserTable.apiKey] = ApiKeyUtil.generate()
it[SysUserTable.tokenVersion] = 1
it[SysUserTable.createdAt] = now
}
return row[SysUserTable.id].toString()
}
fun createPlatformUser(
username: String,
passwordHash: String,
nickname: String?,
realName: String?,
phone: String?,
enterpriseId: Uuid?,
digitalAccountId: Uuid?,
userType: String,
): Uuid {
val now = OffsetDateTime.now()
val row = SysUserTable.insert {
it[SysUserTable.username] = username.trim()
it[SysUserTable.passwordHash] = passwordHash
it[SysUserTable.nickname] = nickname?.trimToNull()
it[SysUserTable.realName] = realName?.trimToNull()
it[SysUserTable.phone] = phone?.trimToNull()
it[SysUserTable.enterpriseId] = enterpriseId
it[SysUserTable.digitalAccountId] = digitalAccountId
it[SysUserTable.userType] = userType
it[SysUserTable.status] = "ENABLED"
it[SysUserTable.tokenVersion] = 1
it[SysUserTable.createdAt] = now
}
return row[SysUserTable.id]
}
fun detail(id: Uuid): UserDetailResponse {
val user = requireActive(id)
val roles = (SysUserRoleTable innerJoin SysRoleTable)
@@ -121,10 +146,6 @@ object UserDao {
it[SysUserTable.email] = request.email.trimToNull()
it[SysUserTable.avatar] = request.avatar.trimToNull()
it[SysUserTable.orgId] = orgId
it[SysUserTable.taxpayerNum] = request.taxpayerNum?.trimToNull()
it[SysUserTable.taxAccount] = request.account?.trimToNull()
it[SysUserTable.taxPassword] = request.taxPassword?.trimToNull()
it[SysUserTable.taxIdentityType] = request.taxIdentityType?.trimToNull()
it[SysUserTable.updatedAt] = OffsetDateTime.now()
}
}
@@ -168,6 +189,13 @@ object UserDao {
SysRoleTable.deletedAt.isNull()
}.count()
fun findEnabledRoleIdByCode(code: String): Uuid? =
SysRoleTable.selectAll().where {
(SysRoleTable.code eq code) and
(SysRoleTable.status eq "ENABLED") and
SysRoleTable.deletedAt.isNull()
}.singleOrNull()?.get(SysRoleTable.id)
fun orgExists(orgId: Uuid): Boolean =
SysOrgTable.selectAll().where {
(SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull()
@@ -240,7 +268,6 @@ object UserDao {
status = this[SysUserTable.status],
statusLabel = statusLabel(this[SysUserTable.status]),
roleCodes = roleCodes,
apiKey = this[SysUserTable.apiKey],
)
private fun ResultRow.toUserDetail(
@@ -262,7 +289,6 @@ object UserDao {
statusLabel = statusLabel(this[SysUserTable.status]),
roleIds = roleIds,
roles = roles,
apiKey = this[SysUserTable.apiKey],
tokenVersion = this[SysUserTable.tokenVersion],
lastLoginAt = this[SysUserTable.lastLoginAt]?.toString(),
lastLoginIp = this[SysUserTable.lastLoginIp],
@@ -273,21 +299,5 @@ object UserDao {
deletedAt = this[SysUserTable.deletedAt]?.toString(),
deletedBy = this[SysUserTable.deletedBy]?.toString(),
version = this[SysUserTable.version],
taxpayerNum = this[SysUserTable.taxpayerNum],
account = this[SysUserTable.taxAccount],
taxIdentityType = this[SysUserTable.taxIdentityType],
taxContactName = this[SysUserTable.taxContactName],
taxContactPhone = this[SysUserTable.taxContactPhone],
taxContactEmail = this[SysUserTable.taxContactEmail],
taxLegalPersonName = this[SysUserTable.taxLegalPersonName],
taxEnterpriseName = this[SysUserTable.taxEnterpriseName],
taxRegionCode = this[SysUserTable.taxRegionCode],
taxCityName = this[SysUserTable.taxCityName],
taxEnterpriseAddress = this[SysUserTable.taxEnterpriseAddress],
taxRegistrationCertificate = this[SysUserTable.taxRegistrationCertificate],
bankName = this[SysUserTable.bankName],
bankAccount = this[SysUserTable.bankAccount],
presetAddress = this[SysUserTable.presetAddress],
presetPhone = this[SysUserTable.presetPhone],
)
}
@@ -25,6 +25,12 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") {
val userId = uuid("user_id")
.nullable()
val enterpriseId = uuid("enterprise_id")
.nullable()
val digitalAccountId = uuid("digital_account_id")
.nullable()
// ----------------------------------------------------------------
// 发票请求信息
// ----------------------------------------------------------------
@@ -446,4 +452,4 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") {
val updatedAt = timestampWithTimeZone("updated_at").nullable()
override val primaryKey = PrimaryKey(id)
}
}
@@ -9,6 +9,8 @@ import kotlin.uuid.Uuid
object OpenInvoiceBatchTable : Table("open_invoice_batch") {
val id = uuid("id").clientDefault { Uuid.random() }
val userId = uuid("user_id")
val enterpriseId = uuid("enterprise_id").nullable()
val digitalAccountId = uuid("digital_account_id").nullable()
val batchNo = varchar("batch_no", 64)
val totalCount = integer("total_count").default(0)
val successCount = integer("success_count").default(0)
@@ -0,0 +1,42 @@
package com.bbit.ticket.database.piaotong
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalUuidApi::class)
object PtDigitalAccountTable : Table("pt_digital_account") {
val id = uuid("id").clientDefault { Uuid.random() }
val enterpriseId = uuid("enterprise_id").references(PtEnterpriseTable.id)
val taxpayerNum = varchar("taxpayer_num", 50)
val account = varchar("account", 100)
val name = varchar("name", 50).nullable()
val identityType = varchar("identity_type", 20).nullable()
val operationProposed = varchar("operation_proposed", 20).nullable()
val authStatus = varchar("auth_status", 20).nullable()
val switchable = varchar("switchable", 20).nullable()
val wechatUserBindStatus = varchar("wechat_user_bind_status", 20).nullable()
val lastAuthSuccTime = varchar("last_auth_succ_time", 32).nullable()
val loginAuthStatus = varchar("login_auth_status", 20).nullable()
val lastLoginAuthTime = varchar("last_login_auth_time", 32).nullable()
val riskAuthStatus = varchar("risk_auth_status", 20).nullable()
val lastRiskAuthTime = varchar("last_risk_auth_time", 32).nullable()
val platformUserId = uuid("platform_user_id").nullable()
val apiKey = varchar("api_key", 128).nullable().uniqueIndex()
val status = varchar("status", 20).default("ENABLED")
val createdAt = timestampWithTimeZone("created_at")
val createdBy = uuid("created_by").nullable()
val updatedAt = timestampWithTimeZone("updated_at").nullable()
val updatedBy = uuid("updated_by").nullable()
val deletedAt = timestampWithTimeZone("deleted_at").nullable()
val deletedBy = uuid("deleted_by").nullable()
val version = integer("version").default(1)
override val primaryKey = PrimaryKey(id)
init {
uniqueIndex(enterpriseId, account)
}
}
@@ -0,0 +1,43 @@
package com.bbit.ticket.database.piaotong
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.javatime.timestampWithTimeZone
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalUuidApi::class)
object PtEnterpriseTable : Table("pt_enterprise") {
val id = uuid("id").clientDefault { Uuid.random() }
val taxpayerNum = varchar("taxpayer_num", 50).uniqueIndex()
val enterpriseName = varchar("enterprise_name", 200)
val legalPersonName = varchar("legal_person_name", 50).nullable()
val contactsName = varchar("contacts_name", 50).nullable()
val contactsEmail = varchar("contacts_email", 100).nullable()
val contactsPhone = varchar("contacts_phone", 32).nullable()
val regionCode = varchar("region_code", 32).nullable()
val cityName = varchar("city_name", 50).nullable()
val enterpriseAddress = varchar("enterprise_address", 200).nullable()
val taxRegistrationCertificate = text("tax_registration_certificate").nullable()
val invitationCode = varchar("invitation_code", 20).nullable()
val reviewStatus = varchar("review_status", 20).nullable()
val reviewOpinion = varchar("review_opinion", 200).nullable()
val invoiceKind = varchar("invoice_kind", 100).nullable()
val invoiceLayoutFileType = varchar("invoice_layout_file_type", 20).nullable()
val serviceStatus = varchar("service_status", 20).nullable()
val bankName = varchar("bank_name", 100).nullable()
val bankAccount = varchar("bank_account", 50).nullable()
val presetAddress = varchar("preset_address", 200).nullable()
val presetPhone = varchar("preset_phone", 32).nullable()
val createdAt = timestampWithTimeZone("created_at")
val createdBy = uuid("created_by").nullable()
val updatedAt = timestampWithTimeZone("updated_at").nullable()
val updatedBy = uuid("updated_by").nullable()
val deletedAt = timestampWithTimeZone("deleted_at").nullable()
val deletedBy = uuid("deleted_by").nullable()
val version = integer("version").default(1)
override val primaryKey = PrimaryKey(id)
}
@@ -10,6 +10,10 @@ object SysApiAccessLogTable : Table("sys_api_access_log") {
val traceId = varchar("trace_id", 64).nullable()
val appKey = varchar("app_key", 100).nullable()
val appName = varchar("app_name", 100).nullable()
val enterpriseId = uuid("enterprise_id").nullable()
val digitalAccountId = uuid("digital_account_id").nullable()
val apiKey = varchar("api_key", 128).nullable()
val interfaceCode = varchar("interface_code", 100).nullable()
val httpMethod = varchar("http_method", 20)
val requestPath = varchar("request_path", 255)
val requestHeaders = text("request_headers").nullable()
@@ -15,7 +15,9 @@ object SysUserTable : Table("sys_user") {
val avatar = text("avatar").nullable()
val orgId = uuid("org_id").nullable()
val status = varchar("status", 20).default("ENABLED")
val apiKey = varchar("api_key", 128).nullable().uniqueIndex()
val enterpriseId = uuid("enterprise_id").nullable()
val digitalAccountId = uuid("digital_account_id").nullable()
val userType = varchar("user_type", 30).default("SYSTEM")
val tokenVersion = integer("token_version").default(1)
val lastLoginAt = timestampWithTimeZone("last_login_at").nullable()
val lastLoginIp = varchar("last_login_ip", 64).nullable()
@@ -29,24 +31,6 @@ object SysUserTable : Table("sys_user") {
val realName = varchar("real_name", 50).nullable()
val phone = varchar("phone", 32).nullable()
val taxpayerNum = varchar("tax_payer_num", 50).nullable()
val taxAccount = varchar("tax_account", 50).nullable()
val taxPassword = varchar("tax_password", 50).nullable()
val taxIdentityType = varchar("tax_identity_type", 50).nullable()
val taxContactName = varchar("tax_contact_name", 50).nullable()
val taxContactPhone = varchar("tax_contact_phone", 32).nullable()
val taxContactEmail = varchar("tax_contact_email", 100).nullable()
val taxLegalPersonName = varchar("tax_legal_person_name", 50).nullable()
val taxEnterpriseName = varchar("tax_enterprise_name", 200).nullable()
val taxRegionCode = varchar("tax_region_code", 32).nullable()
val taxCityName = varchar("tax_city_name", 50).nullable()
val taxEnterpriseAddress = varchar("tax_enterprise_address", 200).nullable()
val taxRegistrationCertificate = text("tax_registration_certificate").nullable()
val bankName = varchar("bank_name", 100).nullable()
val bankAccount = varchar("bank_account", 50).nullable()
val presetAddress = varchar("preset_address", 200).nullable()
val presetPhone = varchar("preset_phone", 32).nullable()
override val primaryKey = PrimaryKey(id)
}
@@ -34,12 +34,11 @@ data class CurrentUserProfile(
val phone: String? = null,
val email: String? = null,
val orgId: String? = null,
val enterpriseId: String? = null,
val digitalAccountId: String? = null,
val userType: String = "SYSTEM",
val status: String,
val createdAt: String? = null,
val taxpayerNum: String? = null,
val account: String? = null,
val taxPassword: String? = null,
val taxIdentityType: String? = null,
)
@Serializable
@@ -11,7 +11,6 @@ data class UserListItem(
val status: String,
val statusLabel: String,
val roleCodes: List<String>,
val apiKey: String? = null,
)
@Serializable
@@ -30,7 +29,6 @@ data class UserDetailResponse(
val statusLabel: String,
val roleIds: List<String>,
val roles: List<UserRoleBrief> = emptyList(),
val apiKey: String? = null,
val tokenVersion: Int,
val lastLoginAt: String? = null,
val lastLoginIp: String? = null,
@@ -41,22 +39,6 @@ data class UserDetailResponse(
val deletedAt: String? = null,
val deletedBy: String? = null,
val version: Int,
val taxpayerNum: String? = null,
val account: String? = null,
val taxIdentityType: String? = null,
val taxContactName: String? = null,
val taxContactPhone: String? = null,
val taxContactEmail: String? = null,
val taxLegalPersonName: String? = null,
val taxEnterpriseName: String? = null,
val taxRegionCode: String? = null,
val taxCityName: String? = null,
val taxEnterpriseAddress: String? = null,
val taxRegistrationCertificate: String? = null,
val bankName: String? = null,
val bankAccount: String? = null,
val presetAddress: String? = null,
val presetPhone: String? = null,
)
@Serializable
@@ -87,10 +69,6 @@ data class UpdateUserRequest(
val email: String? = null,
val avatar: String? = null,
val orgId: String? = null,
val taxpayerNum: String? = null,
val account: String? = null,
val taxPassword: String? = null,
val taxIdentityType: String? = null,
)
@Serializable
@@ -8,6 +8,11 @@ import kotlinx.serialization.Serializable
@Serializable
data class AskInvoiceRequest(
/**
* 平台数电账号 ID。企业管理员开票时用于指定开票员。
*/
val digitalAccountId: String? = null,
/**
* 销方纳税人识别号(销售方税号)
* 长度15~20,只允许大写字母和数字
@@ -448,4 +453,4 @@ data class OrderInfo(
* 业务单据号
*/
val orderNo: String
)
)
@@ -0,0 +1,51 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class EnterpriseRegisterRequest(
val taxpayerNum: String,
val enterpriseName: String,
val legalPersonName: String? = null,
val contactsName: String? = null,
val contactsEmail: String? = null,
val contactsPhone: String,
val regionCode: String,
val cityName: String = "",
val enterpriseAddress: String? = null,
val taxRegistrationCertificate: String? = null,
val password: String,
val confirmPassword: String,
)
fun EnterpriseRegisterRequest.toTaxRegisterInfo(): TaxRegisterInfo =
TaxRegisterInfo(
taxpayerNum = taxpayerNum.trim(),
enterpriseName = enterpriseName.trim(),
legalPersonName = legalPersonName?.trim()?.ifBlank { null },
contactsName = contactsName?.trim()?.ifBlank { null },
contactsEmail = contactsEmail?.trim()?.ifBlank { null },
contactsPhone = contactsPhone.trim(),
regionCode = regionCode.trim(),
cityName = cityName.trim(),
enterpriseAddress = enterpriseAddress?.trim()?.ifBlank { null },
taxRegistrationCertificate = taxRegistrationCertificate?.trim()?.ifBlank { null },
)
@Serializable
data class CreateDigitalAccountRequest(
val account: String,
val taxPassword: String,
val identityType: String,
val phoneNum: String,
val name: String,
val platformPassword: String,
)
@Serializable
data class UpdateInvoiceSettingRequest(
val bankName: String = "",
val bankAccount: String = "",
val address: String = "",
val phone: String = "",
)
@@ -11,16 +11,16 @@ data class TaxRegisterInfo(
val enterpriseName: String,
/** 法人姓名 */
val legalPersonName: String,
val legalPersonName: String?,
/** 联系人姓名 */
val contactsName: String,
val contactsName: String?,
/** 联系人邮箱 */
val contactsEmail: String,
val contactsEmail: String?,
/** 联系人手机号 */
val contactsPhone: String,
val contactsPhone: String?,
/** 区域编码(行政区划代码,如省/市级编码) */
val regionCode: String,
@@ -29,8 +29,8 @@ data class TaxRegisterInfo(
val cityName: String,
/** 企业详细地址 */
val enterpriseAddress: String,
val enterpriseAddress: String?,
/** 税务登记证编号 / 税务登记证明标识 */
val taxRegistrationCertificate: String
val taxRegistrationCertificate: String?
)
@@ -1,9 +0,0 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class TaxRegisterUserRequest(
val taxpayerNum: String,
val taxAccount: String
)
@@ -1,11 +0,0 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class UpdateDigitalAccountRequest(
/** 纳税人识别号 / 税号 */
val taxpayerNum: String = "",
/** 电子税局账号 */
val taxAccount: String = ""
)
@@ -1,27 +0,0 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class UpdateEnterpriseInfoRequest(
/** 纳税人识别号 / 税号 */
val taxpayerNum: String = "",
/** 企业名称 */
val enterpriseName: String = "",
/** 法人姓名 */
val legalPersonName: String = "",
/** 联系人姓名 */
val contactsName: String = "",
/** 联系人邮箱 */
val contactsEmail: String = "",
/** 联系人手机号 */
val contactsPhone: String = "",
/** 区域编码(行政区划代码,如省/市级编码) */
val regionCode: String = "",
/** 城市/区县名称 */
val cityName: String = "",
/** 企业详细地址 */
val enterpriseAddress: String = "",
/** 税务登记证图片 base64 */
val taxRegistrationCertificate: String = ""
)
@@ -1,11 +0,0 @@
package com.bbit.ticket.entity.request
import kotlinx.serialization.Serializable
@Serializable
data class UpdatePresetDataRequest(
val bankName: String = "",
val bankAccount: String = "",
val address: String = "",
val phone: String = ""
)
@@ -1,31 +0,0 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
@Serializable
data class EnterpriseInfoResponse(
val taxpayerNum: String?,
val enterpriseName: String?,
val legalPersonName: String?,
val contactsName: String?,
val contactsEmail: String?,
val contactsPhone: String?,
val regionCode: String?,
val cityName: String?,
val enterpriseAddress: String?,
val taxRegistrationCertificate: String?
)
@Serializable
data class DigitalAccountResponse(
val taxpayerNum: String?,
val taxAccount: String?
)
@Serializable
data class PresetDataResponse(
val bankName: String?,
val bankAccount: String?,
val address: String?,
val phone: String?
)
@@ -0,0 +1,63 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
@Serializable
data class EnterpriseManageResponse(
val id: String,
val taxpayerNum: String,
val enterpriseName: String,
val legalPersonName: String? = null,
val contactsName: String? = null,
val contactsEmail: String? = null,
val contactsPhone: String? = null,
val regionCode: String? = null,
val cityName: String? = null,
val enterpriseAddress: String? = null,
val taxRegistrationCertificate: String? = null,
val invitationCode: String? = null,
val reviewStatus: String? = null,
val reviewOpinion: String? = null,
val invoiceKind: String? = null,
val invoiceLayoutFileType: String? = null,
val serviceStatus: String? = null,
val bankName: String? = null,
val bankAccount: String? = null,
val presetAddress: String? = null,
val presetPhone: String? = null,
)
@Serializable
data class DigitalAccountManageItem(
val id: String,
val enterpriseId: String,
val taxpayerNum: String,
val account: String,
val name: String? = null,
val identityType: String? = null,
val operationProposed: String? = null,
val authStatus: String? = null,
val switchable: String? = null,
val wechatUserBindStatus: String? = null,
val lastAuthSuccTime: String? = null,
val loginAuthStatus: String? = null,
val lastLoginAuthTime: String? = null,
val riskAuthStatus: String? = null,
val lastRiskAuthTime: String? = null,
val platformUserId: String? = null,
val platformUsername: String? = null,
val apiKey: String? = null,
val status: String,
)
@Serializable
data class OpenApiStatisticsItem(
val digitalAccountId: String? = null,
val account: String? = null,
val interfaceCode: String? = null,
val total: Long,
val success: Long,
val failed: Long,
val avgCostMs: Long,
val lastCalledAt: String? = null,
)
@@ -2,144 +2,19 @@ package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
/**
* 查询数电账号列表响应
*/
@Serializable
data class QueryDigitalAccountListResponse(
/**
* 数电账号列表
*/
val list: List<DigitalAccountInfo>
)
/**
* 数电账号信息
*/
@Serializable
data class DigitalAccountInfo(
/**
* 销售方纳税人识别号
*
* 长度:15-20
* 只能包含大写英文字母或数字
*/
val taxpayerNum: String,
/**
* 电子税局登录账号
*/
val account: String,
/**
* 人员姓名
*/
val name: String,
/**
* 登录身份类型
*
* 01:法定代表人
* 02:财务负责人
* 03:办税员
* 04:涉税服务人员
* 05:管理员
* 07:领票人
* 09:开票员
* 99:其他人员
*/
val identityType: String,
/**
* 操作建议
*
* 注意:
* 该值是根据账号状态及不同地区登录方式综合判断,
* 并非与账号状态一一对应。
*
* 0:无需认证
* 1:需扫码认证
* 2:需扫码或短信认证
* 3:需短信认证
*/
val operationProposed: String,
/**
* 账号状态
*
* 0:无需认证
* 1:风险认证
* 2:登录认证
* 3:风险 + 登录认证
*
* 注:
* 风险认证通常需要在开票时才能得知。
*/
val authStatus: String,
/**
* 是否可切换企业状态
*
* 0:不可切换
* 1:可切换
*
* 表示该账号在当前地区其他企业已存在登录状态,
* 当前企业无需重复登录认证。
* 若发生开票,将自动调度切换至当前企业。
*/
val switchable: String,
/**
* 是否绑定微信公众号
*
* 是否绑定票通云服务微信公众号。
*
* 0:否
* 1:是
*/
val wechatUserBindStatus: String,
/**
* 最新认证成功时间
*
* 包括登录认证或风险认证成功时间。
*
* 时间格式:
* yyyy-MM-dd HH:mm:ss
*/
val lastAuthSuccTime: String? = null,
/**
* 登录认证状态
*
* 0:未登录
* 1:已登录
*/
val loginAuthStatus: String,
/**
* 最新登录认证时间
*
* 时间格式:
* yyyy-MM-dd HH:mm:ss
*/
val lastLoginAuthTime: String? = null,
/**
* 风险认证状态
*
* 0:未认证
* 1:已认证
*/
val riskAuthStatus: String,
/**
* 最新风险认证时间
*
* 时间格式:
* yyyy-MM-dd HH:mm:ss
*/
val lastRiskAuthTime: String? = null
)
)
@@ -11,6 +11,7 @@ import com.bbit.ticket.entity.openapi.OpenBlueInvoiceCreateRequest
import com.bbit.ticket.service.openapi.OpenBlueInvoiceService
import com.bbit.ticket.service.system.ApiAccessLogService
import com.bbit.ticket.utils.requireOpenApiPrincipal
import com.bbit.ticket.utils.OpenApiPrincipal
import com.bbit.ticket.utils.plugins.myJson
import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationCall
@@ -33,7 +34,7 @@ fun Route.registerOpenBlueInvoiceRoutes() {
post {
val principal = call.requireOpenApiPrincipal()
val request = call.receive<OpenBlueInvoiceCreateRequest>()
call.respondOpenApi(principal.apiKey, principal.username, myJson.encodeToString(request)) {
call.respondOpenApi(principal, "blue-invoice.create", myJson.encodeToString(request)) {
OpenBlueInvoiceService.createSingle(principal, request)
}
}
@@ -41,7 +42,7 @@ fun Route.registerOpenBlueInvoiceRoutes() {
get("/{invoiceReqSerialNo}") {
val principal = call.requireOpenApiPrincipal()
val invoiceReqSerialNo = call.parameters["invoiceReqSerialNo"].orEmpty()
call.respondOpenApi(principal.apiKey, principal.username, null) {
call.respondOpenApi(principal, "blue-invoice.query", null) {
OpenBlueInvoiceService.querySingle(principal, invoiceReqSerialNo)
}
}
@@ -49,7 +50,7 @@ fun Route.registerOpenBlueInvoiceRoutes() {
get("/sample/{invoiceReqSerialNo}") {
val principal = call.requireOpenApiPrincipal()
val invoiceReqSerialNo = call.parameters["invoiceReqSerialNo"].orEmpty()
call.respondOpenApi(principal.apiKey, principal.username, null) {
call.respondOpenApi(principal, "blue-invoice.sample", null) {
OpenBlueInvoiceService.sample(principal, invoiceReqSerialNo)
}
}
@@ -57,7 +58,7 @@ fun Route.registerOpenBlueInvoiceRoutes() {
post("/batches") {
val principal = call.requireOpenApiPrincipal()
val request = call.receive<OpenBlueInvoiceBatchCreateRequest>()
call.respondOpenApi(principal.apiKey, principal.username, myJson.encodeToString(request)) {
call.respondOpenApi(principal, "blue-invoice.batch-create", myJson.encodeToString(request)) {
val response = OpenBlueInvoiceService.createBatch(principal, request)
call.application.launch {
OpenBlueInvoiceService.processBatch(principal, request.batchNo)
@@ -69,7 +70,7 @@ fun Route.registerOpenBlueInvoiceRoutes() {
get("/batches/{batchNo}") {
val principal = call.requireOpenApiPrincipal()
val batchNo = call.parameters["batchNo"].orEmpty()
call.respondOpenApi(principal.apiKey, principal.username, null) {
call.respondOpenApi(principal, "blue-invoice.batch-query", null) {
OpenBlueInvoiceService.queryBatch(principal, batchNo)
}
}
@@ -84,8 +85,8 @@ fun Route.registerOpenBlueInvoiceRoutes() {
* @param block 当前接口要执行的业务逻辑。
*/
private suspend inline fun <reified T> ApplicationCall.respondOpenApi(
appKey: String?,
appName: String?,
principal: OpenApiPrincipal,
interfaceCode: String,
requestBody: String?,
crossinline block: suspend () -> T,
) {
@@ -94,22 +95,22 @@ private suspend inline fun <reified T> ApplicationCall.respondOpenApi(
val response = ok(block())
val responseBody = myJson.encodeToString(response)
respondText(responseBody, ContentType.Application.Json)
saveOpenApiLog(appKey, appName, requestBody, response.code, responseBody, "SUCCESS", null, start)
saveOpenApiLog(principal, interfaceCode, requestBody, response.code, responseBody, "SUCCESS", null, start)
} catch (e: PTException) {
val response = fail(code = e.code, message = e.message, traceId = e.serialNo)
val responseBody = myJson.encodeToString(response)
respondText(responseBody, ContentType.Application.Json)
saveOpenApiLog(appKey, appName, requestBody, e.code, responseBody, "FAILED", e.message, start)
saveOpenApiLog(principal, interfaceCode, requestBody, e.code, responseBody, "FAILED", e.message, start)
} catch (e: BizException) {
val response = fail(code = e.errorCode, message = e.message)
val responseBody = myJson.encodeToString(response)
respondText(responseBody, ContentType.Application.Json, e.status)
saveOpenApiLog(appKey, appName, requestBody, e.errorCode, responseBody, "FAILED", e.message, start)
saveOpenApiLog(principal, interfaceCode, requestBody, e.errorCode, responseBody, "FAILED", e.message, start)
} catch (e: Exception) {
val response = fail(code = "-1", message = e.message ?: "开放接口调用失败")
val responseBody = myJson.encodeToString(response)
respondText(responseBody, ContentType.Application.Json)
saveOpenApiLog(appKey, appName, requestBody, "-1", responseBody, "FAILED", e.message, start)
saveOpenApiLog(principal, interfaceCode, requestBody, "-1", responseBody, "FAILED", e.message, start)
}
}
@@ -126,8 +127,8 @@ private suspend inline fun <reified T> ApplicationCall.respondOpenApi(
* @param start 接口开始时间标记。
*/
private suspend fun ApplicationCall.saveOpenApiLog(
appKey: String?,
appName: String?,
principal: OpenApiPrincipal,
interfaceCode: String,
requestBody: String?,
responseCode: String?,
responseBody: String?,
@@ -137,8 +138,11 @@ private suspend fun ApplicationCall.saveOpenApiLog(
) {
ApiAccessLogService.save(
call = this,
appKey = appKey,
appName = appName,
appKey = principal.apiKey,
appName = principal.username,
enterpriseId = principal.enterpriseId,
digitalAccountId = principal.digitalAccountId,
interfaceCode = interfaceCode,
requestBody = requestBody,
responseCode = responseCode,
responseBody = responseBody,
@@ -2,22 +2,16 @@
package com.bbit.ticket.route.piaotong
import com.bbit.ticket.entity.common.BizException
import com.bbit.ticket.entity.request.AuthQrcodeRequest
import com.bbit.ticket.entity.request.CreateDigitalAccountRequest
import com.bbit.ticket.entity.request.GetLoginSmsCodeRequest
import com.bbit.ticket.entity.request.QueryRealNameAuthQrStatusRequest
import com.bbit.ticket.entity.request.SmsLoginRequest
import com.bbit.ticket.entity.request.TaxBureauAuthReq
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.TaxRegisterUserRequest
import com.bbit.ticket.entity.request.UpdateDigitalAccountRequest
import com.bbit.ticket.entity.request.UpdateEnterpriseInfoRequest
import com.bbit.ticket.entity.request.UpdatePresetDataRequest
import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest
import com.bbit.ticket.service.piaotong.PTAuthService
import com.bbit.ticket.service.piaotong.PTConfigService
import com.bbit.ticket.utils.requireCurrentUser
import com.bbit.ticket.utils.requirePtProfile
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
@@ -33,85 +27,80 @@ import kotlin.uuid.ExperimentalUuidApi
fun Route.registerPTAuthRoutes() {
get("/info") {
call.respondPt("查询票通认证状态失败") {
val profile = call.requireCurrentUser().requirePtProfile()
PTAuthService.getTaxBureauAccountAuthStatus(
TaxBureauAuthReq(profile.taxpayerNum, profile.taxAccount)
val account = PTConfigService.requireDigitalAccountForAction(
call.requireCurrentUser(),
call.request.queryParameters["digitalAccountId"],
)
}
}
post("/register") {
call.respondPt("企业注册失败") {
val currentUser = call.requireCurrentUser()
PTAuthService.registerEnterprise(call.receive<TaxRegisterInfo>(), currentUser.id)
}
}
post("/registerUser") {
call.respondPt("用户注册失败") {
PTAuthService.registerUserFromPayload(
call.receive<TaxRegisterUserRequest>(),
call.requireCurrentUser()
PTAuthService.getTaxBureauAccountAuthStatus(
TaxBureauAuthReq(account.taxpayerNum, account.account)
)
}
}
get("/enterprise") {
call.respondPtOrEmptyObject("查询企业信息失败") {
PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id)
PTConfigService.getEnterpriseInfo(call.requireCurrentUser())
}
}
put("/enterprise") {
call.respondPt("保存企业信息失败") {
PTConfigService.updateEnterpriseInfo(
call.requireCurrentUser().id,
call.receive<UpdateEnterpriseInfoRequest>()
)
post("/enterprise/refresh") {
call.respondPt("刷新企业信息失败") {
PTConfigService.refreshEnterpriseInfo(call.requireCurrentUser())
}
}
get("/digital-account") {
call.respondPtOrEmptyObject("查询数电账号失败") {
val currentUser = call.requireCurrentUser()
if (currentUser.taxPayerNum == null) {
throw BizException("-1", "请先完善用户信息", HttpStatusCode.OK)
}
PTConfigService.getDigitalAccount(currentUser.id)
get("/enterprise/bank-accounts") {
call.respondPt("查询企业开户行及账号失败") {
PTConfigService.queryEnterpriseBankAccounts(call.requireCurrentUser())
}
}
put("/digital-account") {
call.respondPt("保存数电账号失败") {
PTConfigService.updateDigitalAccount(
call.requireCurrentUser().id,
call.receive<UpdateDigitalAccountRequest>()
)
get("/digital-accounts") {
call.respondPt("查询数电账号失败") {
PTConfigService.listDigitalAccounts(call.requireCurrentUser())
}
}
post("/digital-accounts/refresh") {
call.respondPt("刷新数电账号失败") {
PTConfigService.refreshDigitalAccounts(call.requireCurrentUser())
}
}
post("/digital-accounts") {
call.respondPt("新增数电账号失败") {
PTConfigService.createDigitalAccount(call.requireCurrentUser(), call.receive<CreateDigitalAccountRequest>())
}
}
get("/preset") {
call.respondPtOrEmptyObject("查询预设数据失败") {
PTConfigService.getPresetData(call.requireCurrentUser().id)
PTConfigService.getEnterpriseInfo(call.requireCurrentUser())
}
}
put("/preset") {
call.respondPt("保存预设数据失败") {
PTConfigService.updatePresetData(
call.requireCurrentUser().id,
call.receive<UpdatePresetDataRequest>()
)
PTConfigService.updateInvoiceSetting(call.requireCurrentUser(), call.receive<UpdateInvoiceSettingRequest>())
}
}
get("/openapi/statistics") {
call.respondPt("查询 OpenAPI 统计失败") {
PTConfigService.openApiStatistics(call.requireCurrentUser())
}
}
get("/authentication") {
call.respondPt("获取认证二维码失败") {
val profile = call.requireCurrentUser().requirePtProfile()
val account = PTConfigService.requireDigitalAccountForAction(
call.requireCurrentUser(),
call.request.queryParameters["digitalAccountId"],
)
PTAuthService.getAuthenticationQrcode(
AuthQrcodeRequest(
taxpayerNum = profile.taxpayerNum,
account = profile.taxAccount,
taxpayerNum = account.taxpayerNum,
account = account.account,
qrcodeType = call.request.queryParameters["qrcodeType"]
)
)
@@ -9,7 +9,6 @@ import com.bbit.ticket.entity.request.RedCreateRequest
import com.bbit.ticket.service.piaotong.PTBlueService
import com.bbit.ticket.service.piaotong.PTRedService
import com.bbit.ticket.utils.requireCurrentUser
import com.bbit.ticket.utils.requirePtProfile
import io.ktor.server.request.receive
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
@@ -38,7 +37,7 @@ fun Route.registerPTInvoiceRoutes() {
call.respondPt("查询开票历史失败") {
val currentUser = call.requireCurrentUser()
PTBlueService.getInvoiceBlueHistory(
userId = currentUser.id,
user = currentUser,
page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1,
pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 20,
invoiceType = call.request.queryParameters["invoiceType"],
@@ -50,7 +49,7 @@ fun Route.registerPTInvoiceRoutes() {
get("/invoiceBatchNos") {
call.respondPt("查询批次号列表失败") {
PTBlueService.listBatchNos(call.requireCurrentUser().id)
PTBlueService.listBatchNos(call.requireCurrentUser())
}
}
@@ -103,9 +102,14 @@ fun Route.registerPTInvoiceRoutes() {
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
?: return@get
call.respondPt("刷新发票状态失败") {
val currentUser = call.requireCurrentUser()
val account = com.bbit.ticket.service.piaotong.PTConfigService.requireDigitalAccountForAction(
currentUser,
call.request.queryParameters["digitalAccountId"],
)
PTBlueService.queryInvoiceAllInfo(
QueryInvoiceRequest(
taxpayerNum = call.requireCurrentUser().requirePtProfile().taxpayerNum,
taxpayerNum = account.taxpayerNum,
invoiceReqSerialNo = invoiceReqSerialNo,
)
)
@@ -4,6 +4,7 @@ package com.bbit.ticket.route.system
import com.bbit.ticket.entity.common.ok
import com.bbit.ticket.service.system.AuthService
import com.bbit.ticket.entity.common.system.LoginRequest
import com.bbit.ticket.entity.request.EnterpriseRegisterRequest
import com.bbit.ticket.service.system.OperationLogService
import com.bbit.ticket.utils.requireCurrentUser
import io.ktor.server.auth.authenticate
@@ -31,6 +32,19 @@ fun Route.registerAuthRoutes() {
}
}
post("/register-enterprise") {
val start = TimeSource.Monotonic.markNow()
val request = call.receive<EnterpriseRegisterRequest>()
runCatching {
val response = AuthService.registerEnterprise(request, call.request.local.remoteHost)
call.respond(ok(response))
OperationLogService.success(call, null, "REGISTER_ENTERPRISE", "企业注册成功", start.elapsedNow().inWholeMilliseconds)
}.onFailure {
OperationLogService.fail(call, null, "REGISTER_ENTERPRISE", "企业注册失败", it.message, start.elapsedNow().inWholeMilliseconds)
throw it
}
}
authenticate("auth-jwt") {
post("/logout") {
val currentUser = call.requireCurrentUser()
@@ -44,7 +44,12 @@ object OpenBlueInvoiceService {
validateCreateRequest(request)
val createRequest = request.withGeneratedInvoiceReqSerialNo(principal.userId)
PTBlueService.createBlueInvoice(createRequest.toAskInvoiceRequest(principal), principal.userId)
PTBlueService.createBlueInvoice(
createRequest.toAskInvoiceRequest(principal),
principal.userId,
principal.enterpriseId,
principal.digitalAccountId,
)
val detail = PTBlueService.getInvoiceDetail(principal.userId, createRequest.requireInvoiceReqSerialNo())
return OpenBlueInvoiceCreateResponse(
requestNo = createRequest.requestNo,
@@ -59,7 +64,13 @@ object OpenBlueInvoiceService {
}
runCatching {
PTBlueService.syncInvoiceFromPT(principal.userId, invoiceReqSerialNo, principal.taxPayerNum)
PTBlueService.syncInvoiceFromPT(
principal.userId,
invoiceReqSerialNo,
principal.taxPayerNum,
principal.enterpriseId,
principal.digitalAccountId,
)
}
val detail = PTBlueService.getInvoiceDetail(principal.userId, invoiceReqSerialNo)
@@ -115,6 +126,8 @@ object OpenBlueInvoiceService {
dbQuery {
val batchId = OpenInvoiceBatchTable.insert {
it[userId] = principal.userId
it[enterpriseId] = principal.enterpriseId
it[digitalAccountId] = principal.digitalAccountId
it[batchNo] = request.batchNo
it[totalCount] = request.items.size
it[successCount] = 0
@@ -241,6 +254,8 @@ object OpenBlueInvoiceService {
principal.userId,
item.invoiceReqSerialNo,
principal.taxPayerNum,
principal.enterpriseId,
principal.digitalAccountId,
)
}
}
@@ -2,8 +2,6 @@
package com.bbit.ticket.service.piaotong
import com.bbit.ticket.dao.piaotong.EnterpriseTaxDao
import com.bbit.ticket.database.system.SysUserTable.taxpayerNum
import com.bbit.ticket.entity.request.AuthQrcodeRequest
import com.bbit.ticket.entity.request.GetLoginSmsCodeRequest
import com.bbit.ticket.entity.request.QueryDigitalAccountListRequest
@@ -12,23 +10,16 @@ import com.bbit.ticket.entity.request.QueryEnterpriseInfoRequest
import com.bbit.ticket.entity.request.QueryRealNameAuthQrStatusRequest
import com.bbit.ticket.entity.request.SmsLoginRequest
import com.bbit.ticket.entity.request.TaxBureauAuthReq
import com.bbit.ticket.entity.request.TaxRegister
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.TaxRegisterUserRequest
import com.bbit.ticket.entity.response.AuthQrcodeResponse
import com.bbit.ticket.entity.response.AuthQrcodeStatusResponse
import com.bbit.ticket.entity.response.DigitalAccountInfo
import com.bbit.ticket.entity.response.EnterpriseTaxInfo
import com.bbit.ticket.entity.response.EtaxRegisterResponse
import com.bbit.ticket.entity.response.LoginSmsCodeResponse
import com.bbit.ticket.entity.response.QueryDigitalAccountListResponse
import com.bbit.ticket.entity.response.QueryEnterpriseBankAccountResponse
import com.bbit.ticket.entity.response.QueryEnterpriseInfoResponse
import com.bbit.ticket.entity.response.SMSLoginResponse
import com.bbit.ticket.entity.response.TaxBureauAccountAuthContent
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.net.PTClient
import kotlin.uuid.Uuid
object PTAuthService {
@@ -41,34 +32,6 @@ object PTAuthService {
return res
}
/**
* 登记/删除 数电账号 2.3
*/
suspend fun registerUserFromPayload(req: TaxRegisterUserRequest, currentUser: CurrentUser): String {
val res = PTClient.ptPost<TaxRegister, EtaxRegisterResponse>(
"registerUser.pt", TaxRegister(
taxpayerNum = req.taxpayerNum,
account = req.taxAccount,
password = currentUser.taxPassword ?: "",
phoneNum = currentUser.phone ?: "",
name = currentUser.realName ?: "",
identityType = currentUser.taxIdentityType ?: "",
)
)
dbQuery { EnterpriseTaxDao.updateUserAccount(currentUser.id, req.taxpayerNum, req.taxAccount) }
return res.resultMsg
}
/**
* 注册企业(纳税人) 2.2
* 将企业信息注册到票通平台
*/
suspend fun registerEnterprise(req: TaxRegisterInfo, userId: Uuid): String {
PTClient.ptPost<TaxRegisterInfo, EnterpriseTaxInfo>("register.pt", req)
dbQuery { EnterpriseTaxDao.updateEnterpriseInfo(userId, req) }
return "操作成功"
}
/**
* 获取实名认证二维码 2.6
*
@@ -118,8 +81,8 @@ object PTAuthService {
/**
* 查询数电账号列表 2.45
*/
suspend fun getListTaxBureauAccount(req: QueryDigitalAccountListRequest): QueryDigitalAccountListResponse {
return PTClient.ptPost<QueryDigitalAccountListRequest, QueryDigitalAccountListResponse>(
suspend fun getListTaxBureauAccount(req: QueryDigitalAccountListRequest): List<DigitalAccountInfo> {
return PTClient.ptPost<QueryDigitalAccountListRequest, List<DigitalAccountInfo>>(
"listTaxBureauAccount.pt",
req
)
@@ -132,4 +95,4 @@ object PTAuthService {
return PTClient.ptPost<QueryEnterpriseBankAccountRequest, QueryEnterpriseBankAccountResponse>("queryEnterpriseBankInfo.pt", req)
}
}
}
@@ -13,17 +13,17 @@ import com.bbit.ticket.entity.response.InvoiceCreateResponse
import com.bbit.ticket.entity.response.InvoiceDetailResponse
import com.bbit.ticket.entity.response.InvoiceHistoryItem
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.requirePtProfile
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsBytes
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.parseUuid
import kotlin.uuid.Uuid
object PTBlueService {
suspend fun listBatchNos(userId: Uuid): List<String> =
dbQuery { BlueInvoiceDao.listBatchNos(userId) }
suspend fun listBatchNos(user: CurrentUser): List<String> =
dbQuery { BlueInvoiceDao.listBatchNos(user.id) }
/**
* 查询票通同步发票信息(支持插入和更新)
@@ -31,12 +31,18 @@ object PTBlueService {
* 既是蓝票/红票创建后的补充同步,
* 也是手动刷新的核心方法。
*/
suspend fun syncInvoiceFromPT(userId: Uuid, invoiceReqSerialNo: String, taxpayerNum: String): QueryInvoiceResult {
suspend fun syncInvoiceFromPT(
userId: Uuid,
invoiceReqSerialNo: String,
taxpayerNum: String,
enterpriseId: Uuid? = null,
digitalAccountId: Uuid? = null,
): QueryInvoiceResult {
val res = PTClient.ptPost<QueryInvoiceRequest, GetInvoiceInfoResponse>(
"queryInvoiceInfo.pt",
QueryInvoiceRequest(taxpayerNum = taxpayerNum, invoiceReqSerialNo = invoiceReqSerialNo)
)
dbQuery { BlueInvoiceDao.upsertInvoiceInfo(userId, res) }
dbQuery { BlueInvoiceDao.upsertInvoiceInfo(userId, res, enterpriseId, digitalAccountId) }
val newStatus = when (res.code) {
"0000" -> "SUCCESS"
"7777" -> "PROCESSING"
@@ -51,18 +57,25 @@ object PTBlueService {
* 蓝票接口调用
*/
suspend fun invoiceBlue(req: AskInvoiceRequest, user: CurrentUser): String {
val profile = user.requirePtProfile()
val account = PTConfigService.requireDigitalAccountForAction(user, req.digitalAccountId)
return createBlueInvoice(
req.copy(
taxpayerNum = profile.taxpayerNum,
account = req.account ?: profile.taxAccount,
taxpayerNum = account.taxpayerNum,
account = account.account,
),
user.id,
parseUuid(account.enterpriseId, "enterpriseId"),
parseUuid(account.id, "digitalAccountId"),
)
}
suspend fun createBlueInvoice(req: AskInvoiceRequest, userId: Uuid): String {
dbQuery { BlueInvoiceDao.addInvoice(userId, req) }
suspend fun createBlueInvoice(
req: AskInvoiceRequest,
userId: Uuid,
enterpriseId: Uuid? = null,
digitalAccountId: Uuid? = null,
): String {
dbQuery { BlueInvoiceDao.addInvoice(userId, req, enterpriseId, digitalAccountId) }
try {
PTClient.ptPost<AskInvoiceRequest, InvoiceCreateResponse>("invoiceBlue.pt", req)
} catch (e: Exception) {
@@ -71,7 +84,7 @@ object PTBlueService {
}
// 创建后立即同步一次(非关键,失败忽略)
try {
syncInvoiceFromPT(userId, req.invoiceReqSerialNo, req.taxpayerNum)
syncInvoiceFromPT(userId, req.invoiceReqSerialNo, req.taxpayerNum, enterpriseId, digitalAccountId)
} catch (_: Exception) { }
return "操作成功"
}
@@ -80,14 +93,25 @@ object PTBlueService {
* 分页查询开票历史(支持筛选)
*/
suspend fun getInvoiceBlueHistory(
userId: Uuid,
user: CurrentUser,
page: Int,
pageSize: Int,
invoiceType: String? = null,
isSuccess: Boolean? = null,
batchNo: String? = null,
): PageResult<InvoiceHistoryItem> =
dbQuery { BlueInvoiceDao.invoiceHistory(userId, page, pageSize, invoiceType, isSuccess, batchNo) }
dbQuery {
BlueInvoiceDao.invoiceHistory(
userId = user.id,
enterpriseId = user.enterpriseId,
digitalAccountId = if (user.isDigitalOperator) user.digitalAccountId else null,
page = page,
pageSize = pageSize,
invoiceType = invoiceType,
isSuccess = isSuccess,
batchNo = batchNo,
)
}
/**
* 查询发票完整详情
@@ -110,16 +134,16 @@ object PTBlueService {
*/
suspend fun queryInvoiceAllInfo(req: QueryInvoiceRequest): QueryInvoiceResult {
val invoiceReqSerialNo = req.invoiceReqSerialNo
val existing = dbQuery {
BlueInvoiceDao.findUserIdBySerialNo(invoiceReqSerialNo)
val scope = dbQuery {
BlueInvoiceDao.findInvoiceScopeBySerialNo(invoiceReqSerialNo)
}
val result = syncInvoiceFromPT(existing, invoiceReqSerialNo, req.taxpayerNum)
val result = syncInvoiceFromPT(scope.userId, invoiceReqSerialNo, req.taxpayerNum, scope.enterpriseId, scope.digitalAccountId)
val relatedSerialNos = dbQuery {
BlueInvoiceDao.findRelatedInvoiceReqSerialNos(existing, invoiceReqSerialNo)
BlueInvoiceDao.findRelatedInvoiceReqSerialNos(scope.userId, invoiceReqSerialNo)
}
relatedSerialNos.forEach { relatedSerialNo ->
runCatching {
syncInvoiceFromPT(existing, relatedSerialNo, req.taxpayerNum)
syncInvoiceFromPT(scope.userId, relatedSerialNo, req.taxpayerNum, scope.enterpriseId, scope.digitalAccountId)
}
}
return result
@@ -2,54 +2,224 @@
package com.bbit.ticket.service.piaotong
import com.bbit.ticket.dao.piaotong.EnterpriseTaxDao
import com.bbit.ticket.entity.request.UpdateDigitalAccountRequest
import com.bbit.ticket.entity.request.UpdateEnterpriseInfoRequest
import com.bbit.ticket.entity.request.UpdatePresetDataRequest
import com.bbit.ticket.entity.response.DigitalAccountResponse
import com.bbit.ticket.entity.response.EnterpriseInfoResponse
import com.bbit.ticket.entity.response.PresetDataResponse
import com.bbit.ticket.dao.piaotong.EnterpriseManageDao
import com.bbit.ticket.dao.system.UserDao
import com.bbit.ticket.database.piaotong.PtDigitalAccountTable
import com.bbit.ticket.database.system.SysUserTable
import com.bbit.ticket.entity.request.CreateDigitalAccountRequest
import com.bbit.ticket.entity.request.QueryDigitalAccountListRequest
import com.bbit.ticket.entity.request.QueryEnterpriseBankAccountRequest
import com.bbit.ticket.entity.request.QueryEnterpriseInfoRequest
import com.bbit.ticket.entity.request.TaxRegister
import com.bbit.ticket.entity.request.UpdateInvoiceSettingRequest
import com.bbit.ticket.entity.response.BankInfo
import com.bbit.ticket.entity.response.DigitalAccountInfo
import com.bbit.ticket.entity.response.DigitalAccountManageItem
import com.bbit.ticket.entity.response.EnterpriseManageResponse
import com.bbit.ticket.entity.response.EtaxRegisterResponse
import com.bbit.ticket.entity.response.OpenApiStatisticsItem
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.entity.common.BizException
import com.bbit.ticket.entity.common.ErrorCode
import com.bbit.ticket.service.system.PasswordService
import com.bbit.ticket.utils.ApiKeyUtil
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.parseUuid
import io.ktor.http.HttpStatusCode
import kotlin.uuid.Uuid
object PTConfigService {
private const val DIGITAL_OPERATOR_ROLE_CODE = "DIGITAL_OPERATOR"
private const val DEFAULT_OPERATOR_PASSWORD = "123456"
// =============================================
// 企业信息
// =============================================
suspend fun getEnterpriseInfo(userId: Uuid): EnterpriseInfoResponse? = dbQuery {
EnterpriseTaxDao.getEnterpriseInfo(userId)
suspend fun getEnterpriseInfo(user: CurrentUser): EnterpriseManageResponse? = dbQuery {
val enterpriseId = requireEnterpriseId(user)
EnterpriseManageDao.enterpriseDetail(enterpriseId)
}
suspend fun updateEnterpriseInfo(userId: Uuid, req: UpdateEnterpriseInfoRequest): String {
dbQuery { EnterpriseTaxDao.updateEnterpriseInfoLocal(userId, req) }
return "企业信息保存成功"
suspend fun refreshEnterpriseInfo(user: CurrentUser): EnterpriseManageResponse {
val enterpriseId = dbQuery { requireEnterpriseId(user) }
val taxpayerNum = dbQuery {
EnterpriseManageDao.enterpriseDetail(enterpriseId)?.taxpayerNum
?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest)
}
val res = PTAuthService.getEnterpriseInfo(QueryEnterpriseInfoRequest(taxpayerNum))
dbQuery { EnterpriseManageDao.updateEnterpriseFromPt(enterpriseId, res) }
return dbQuery { EnterpriseManageDao.enterpriseDetail(enterpriseId)!! }
}
// =============================================
// 登记数电账号
// =============================================
suspend fun getDigitalAccount(userId: Uuid): DigitalAccountResponse? = dbQuery {
EnterpriseTaxDao.getDigitalAccount(userId)
suspend fun updateInvoiceSetting(user: CurrentUser, req: UpdateInvoiceSettingRequest): String = dbQuery {
EnterpriseManageDao.updateInvoiceSetting(requireEnterpriseId(user), req)
"开票设置保存成功"
}
suspend fun updateDigitalAccount(userId: Uuid, req: UpdateDigitalAccountRequest): String {
dbQuery { EnterpriseTaxDao.updateDigitalAccountLocal(userId, req) }
return "账号信息保存成功"
suspend fun queryEnterpriseBankAccounts(user: CurrentUser): List<BankInfo> {
val taxpayerNum = dbQuery {
EnterpriseManageDao.enterpriseDetail(requireEnterpriseId(user))?.taxpayerNum
?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest)
}
return PTAuthService.queryEnterpriseBankInfo(QueryEnterpriseBankAccountRequest(taxpayerNum)).bankList
}
// =============================================
// 开票预设数据
// =============================================
suspend fun getPresetData(userId: Uuid): PresetDataResponse? = dbQuery {
EnterpriseTaxDao.getPresetData(userId)
suspend fun listDigitalAccounts(user: CurrentUser): List<DigitalAccountManageItem> = dbQuery {
when {
user.isSuperAdmin || user.isEnterpriseAdmin -> {
EnterpriseManageDao.digitalAccountsForEnterprise(requireEnterpriseId(user))
}
user.isDigitalOperator -> {
val id = user.digitalAccountId
?: throw BizException(ErrorCode.BAD_REQUEST.code, "当前账号未绑定数电账号")
EnterpriseManageDao.digitalAccount(id)?.let { listOf(EnterpriseManageDao.run { it.toDigitalAccountItem() }) }
?: emptyList()
}
else -> emptyList()
}
}
suspend fun updatePresetData(userId: Uuid, req: UpdatePresetDataRequest): String {
dbQuery { EnterpriseTaxDao.updatePresetData(userId, req) }
return "预设数据保存成功"
suspend fun refreshDigitalAccounts(user: CurrentUser): List<DigitalAccountManageItem> {
val enterprise = dbQuery {
val enterpriseId = requireEnterpriseId(user)
EnterpriseManageDao.enterpriseDetail(enterpriseId)
?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest)
}
val res = PTAuthService.getListTaxBureauAccount(QueryDigitalAccountListRequest(enterprise.taxpayerNum))
dbQuery {
val enterpriseId = parseUuid(enterprise.id, "enterpriseId")
res.forEach { item ->
val digitalAccountId = EnterpriseManageDao.upsertDigitalAccount(enterpriseId, item)
ensureDigitalOperatorAccount(
enterpriseId = enterpriseId,
taxpayerNum = enterprise.taxpayerNum,
item = item,
digitalAccountId = digitalAccountId,
platformPassword = DEFAULT_OPERATOR_PASSWORD,
)
}
}
return listDigitalAccounts(user)
}
suspend fun createDigitalAccount(user: CurrentUser, req: CreateDigitalAccountRequest): DigitalAccountManageItem {
if (!user.isSuperAdmin && !user.isEnterpriseAdmin) {
throw BizException(ErrorCode.FORBIDDEN.code, "无权新增数电账号", HttpStatusCode.Forbidden)
}
val enterprise = dbQuery {
EnterpriseManageDao.enterpriseDetail(requireEnterpriseId(user))
?: throw BizException(ErrorCode.BAD_REQUEST.code, "企业信息不存在", HttpStatusCode.BadRequest)
}
PTClient.ptPost<TaxRegister, EtaxRegisterResponse>(
"registerUser.pt",
TaxRegister(
taxpayerNum = enterprise.taxpayerNum,
account = req.account,
password = req.taxPassword,
phoneNum = req.phoneNum,
name = req.name,
identityType = req.identityType,
)
)
return dbQuery {
val enterpriseId = parseUuid(enterprise.id, "enterpriseId")
val userId = ensureDigitalOperatorAccount(
enterpriseId = enterpriseId,
taxpayerNum = enterprise.taxpayerNum,
account = req.account.trim(),
name = req.name.trim(),
phone = req.phoneNum.trim().ifBlank { null },
digitalAccountId = null,
platformPassword = req.platformPassword,
)
val digitalAccountId = EnterpriseManageDao.createDigitalAccount(
enterpriseId = enterpriseId,
taxpayerNum = enterprise.taxpayerNum,
account = req.account.trim(),
name = req.name.trim(),
identityType = req.identityType.trim(),
platformUserId = userId,
apiKey = ApiKeyUtil.generate(),
)
EnterpriseManageDao.bindDigitalAccountUser(digitalAccountId, userId)
EnterpriseManageDao.digitalAccount(digitalAccountId)!!.let { EnterpriseManageDao.run { it.toDigitalAccountItem() } }
}
}
private fun ensureDigitalOperatorAccount(
enterpriseId: Uuid,
taxpayerNum: String,
item: DigitalAccountInfo,
digitalAccountId: Uuid,
platformPassword: String,
): Uuid =
ensureDigitalOperatorAccount(
enterpriseId = enterpriseId,
taxpayerNum = taxpayerNum,
account = item.account,
name = item.name,
phone = null,
digitalAccountId = digitalAccountId,
platformPassword = platformPassword,
)
private fun ensureDigitalOperatorAccount(
enterpriseId: Uuid,
taxpayerNum: String,
account: String,
name: String?,
phone: String?,
digitalAccountId: Uuid?,
platformPassword: String,
): Uuid {
val username = "${taxpayerNum}_${account.trim()}"
val existing = UserDao.findByUsername(username)
val userId = existing?.get(SysUserTable.id)
?: UserDao.createPlatformUser(
username = username,
passwordHash = PasswordService.hash(platformPassword),
nickname = name,
realName = name,
phone = phone,
enterpriseId = enterpriseId,
digitalAccountId = null,
userType = "DIGITAL_OPERATOR",
).also { createdUserId ->
UserDao.findEnabledRoleIdByCode(DIGITAL_OPERATOR_ROLE_CODE)?.let { roleId ->
UserDao.replaceRoles(createdUserId, listOf(roleId))
}
}
if (digitalAccountId != null) {
val row = EnterpriseManageDao.digitalAccount(digitalAccountId)
val apiKey = row?.get(PtDigitalAccountTable.apiKey) ?: ApiKeyUtil.generate()
if (row?.get(PtDigitalAccountTable.platformUserId) != userId || row[PtDigitalAccountTable.apiKey].isNullOrBlank()) {
EnterpriseManageDao.bindDigitalAccountUserAndApiKey(digitalAccountId, userId, apiKey)
}
}
return userId
}
suspend fun openApiStatistics(user: CurrentUser): List<OpenApiStatisticsItem> = dbQuery {
EnterpriseManageDao.openApiStatistics(requireEnterpriseId(user), if (user.isDigitalOperator) user.digitalAccountId else null)
}
fun requireEnterpriseId(user: CurrentUser): Uuid =
user.enterpriseId ?: throw BizException(ErrorCode.BAD_REQUEST.code, "当前账号未绑定企业")
suspend fun requireDigitalAccountForAction(user: CurrentUser, digitalAccountId: String?): DigitalAccountManageItem = dbQuery {
val targetId = when {
user.isDigitalOperator -> user.digitalAccountId
!digitalAccountId.isNullOrBlank() -> parseUuid(digitalAccountId, "digitalAccountId")
else -> user.digitalAccountId
} ?: throw BizException(ErrorCode.BAD_REQUEST.code, "请选择数电账号")
val row = EnterpriseManageDao.digitalAccount(targetId)
?: throw BizException(ErrorCode.BAD_REQUEST.code, "数电账号不存在", HttpStatusCode.NotFound)
if (!user.isSuperAdmin && row[PtDigitalAccountTable.enterpriseId] != requireEnterpriseId(user)) {
throw BizException(ErrorCode.FORBIDDEN.code, "无权操作该数电账号", HttpStatusCode.Forbidden)
}
EnterpriseManageDao.run { row.toDigitalAccountItem() }
}
}
@@ -10,9 +10,9 @@ import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
import com.bbit.ticket.entity.response.QuickRedInvoiceResponse
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.requirePtProfile
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.utils.net.PTClient
import com.bbit.ticket.utils.parseUuid
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsBytes
import kotlin.uuid.Uuid
@@ -28,14 +28,14 @@ object PTRedService {
* 红票接口调用
*/
suspend fun invoiceRed(user: CurrentUser,req:RedCreateRequest): String {
val profile = user.requirePtProfile()
val account = PTConfigService.requireDigitalAccountForAction(user, null)
val invoiceReqSerialNo = PTClient.ptDate()
val historyId = Uuid.parse(req.historyId)
val his = dbQuery {
HistoryDao.findByHistory(historyId, user.id)
}
val req = QuickRedInvoiceRequest(
taxpayerNum = profile.taxpayerNum,
taxpayerNum = account.taxpayerNum,
invoiceReqSerialNo = invoiceReqSerialNo,
invoiceCode = his.invoiceCode,
invoiceNo = his.invoiceNo,
@@ -43,7 +43,7 @@ object PTRedService {
blueInvoiceDate = his.blueInvoiceDate,
redReason = req.redReason,
amount = his.totalAmount?.negate()?.toPlainString()?:"0.0",
account = profile.taxAccount,
account = account.account,
invoiceKind = his.invoiceKind,
takerName = req.takerName,
takerTel = req.takerTel,
@@ -51,8 +51,10 @@ object PTRedService {
)
PTClient.ptPost<QuickRedInvoiceRequest, QuickRedInvoiceResponse>("invoiceRed.pt", req)
dbQuery { RedInvoiceDao.addRedInvoice(user.id, historyId, req) }
PTBlueService.syncInvoiceFromPT(user.id, his.invoiceReqSerialNo, profile.taxpayerNum)
PTBlueService.syncInvoiceFromPT(user.id, invoiceReqSerialNo, profile.taxpayerNum)
val enterpriseId = parseUuid(account.enterpriseId, "enterpriseId")
val digitalAccountId = parseUuid(account.id, "digitalAccountId")
PTBlueService.syncInvoiceFromPT(user.id, his.invoiceReqSerialNo, account.taxpayerNum, enterpriseId, digitalAccountId)
PTBlueService.syncInvoiceFromPT(user.id, invoiceReqSerialNo, account.taxpayerNum, enterpriseId, digitalAccountId)
return "操作成功"
}
@@ -6,6 +6,7 @@ import com.bbit.ticket.dao.system.LogDao
import com.bbit.ticket.utils.plugins.dbQuery
import io.ktor.server.application.ApplicationCall
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
object ApiAccessLogService {
/**
@@ -25,6 +26,9 @@ object ApiAccessLogService {
call: ApplicationCall,
appKey: String?,
appName: String?,
enterpriseId: Uuid? = null,
digitalAccountId: Uuid? = null,
interfaceCode: String? = null,
requestBody: String?,
responseCode: String?,
responseBody: String?,
@@ -36,6 +40,9 @@ object ApiAccessLogService {
call = call,
appKey = appKey,
appName = appName,
enterpriseId = enterpriseId,
digitalAccountId = digitalAccountId,
interfaceCode = interfaceCode,
requestBody = requestBody,
responseCode = responseCode,
responseBody = responseBody,
@@ -3,6 +3,7 @@
package com.bbit.ticket.service.system
import com.bbit.ticket.dao.system.MenuDao
import com.bbit.ticket.dao.piaotong.EnterpriseManageDao
import com.bbit.ticket.dao.system.UserDao
import com.bbit.ticket.database.system.SysUserTable
import com.bbit.ticket.entity.common.BizException
@@ -11,12 +12,19 @@ import com.bbit.ticket.entity.common.system.CurrentUserProfile
import com.bbit.ticket.entity.common.system.LoginRequest
import com.bbit.ticket.entity.common.system.LoginResponse
import com.bbit.ticket.entity.common.system.MeResponse
import com.bbit.ticket.entity.request.EnterpriseRegisterRequest
import com.bbit.ticket.entity.request.TaxRegisterInfo
import com.bbit.ticket.entity.request.toTaxRegisterInfo
import com.bbit.ticket.entity.response.EnterpriseTaxInfo
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.net.PTClient
import io.ktor.http.HttpStatusCode
import kotlin.uuid.ExperimentalUuidApi
object AuthService {
private const val ENTERPRISE_ADMIN_ROLE_CODE = "ENTERPRISE_ADMIN"
suspend fun login(request: LoginRequest, loginIp: String?): LoginResponse {
val username = request.username.trim()
if (username.isBlank() || request.password.isBlank()) {
@@ -57,6 +65,55 @@ object AuthService {
return LoginResponse(accessToken = accessToken, expiresIn = expiresIn)
}
suspend fun registerEnterprise(request: EnterpriseRegisterRequest, loginIp: String?): LoginResponse {
val taxpayerNum = request.taxpayerNum.trim()
if (taxpayerNum.isBlank() || request.enterpriseName.isBlank()) {
throw BizException(ErrorCode.BAD_REQUEST.code, "纳税人识别号和企业名称不能为空", HttpStatusCode.BadRequest)
}
if (request.regionCode.isBlank()) {
throw BizException(ErrorCode.BAD_REQUEST.code, "地区编码不能为空", HttpStatusCode.BadRequest)
}
if (request.contactsPhone.isBlank()) {
throw BizException(ErrorCode.BAD_REQUEST.code, "联系人手机不能为空", HttpStatusCode.BadRequest)
}
if (request.password.isBlank()) {
throw BizException(ErrorCode.BAD_REQUEST.code, "密码不能为空", HttpStatusCode.BadRequest)
}
if (request.password != request.confirmPassword) {
throw BizException(ErrorCode.BAD_REQUEST.code, "两次输入的密码不一致", HttpStatusCode.BadRequest)
}
val registerInfo = request.toTaxRegisterInfo()
dbQuery {
if (UserDao.findByUsername(taxpayerNum) != null) {
throw BizException(ErrorCode.DATA_CONFLICT.code, "该纳税人识别号已注册", HttpStatusCode.Conflict)
}
if (EnterpriseManageDao.findEnterpriseByTaxpayerNum(taxpayerNum) != null) {
throw BizException(ErrorCode.DATA_CONFLICT.code, "企业已存在", HttpStatusCode.Conflict)
}
}
PTClient.ptPost<TaxRegisterInfo, EnterpriseTaxInfo>("register.pt", registerInfo)
dbQuery {
val enterpriseId = EnterpriseManageDao.createEnterprise(registerInfo)
val userId = UserDao.createPlatformUser(
username = taxpayerNum,
passwordHash = PasswordService.hash(request.password),
nickname = request.enterpriseName.trim(),
realName = request.legalPersonName?.trim()?.ifBlank { null },
phone = request.contactsPhone.trim(),
enterpriseId = enterpriseId,
digitalAccountId = null,
userType = "ENTERPRISE_ADMIN",
)
UserDao.findEnabledRoleIdByCode(ENTERPRISE_ADMIN_ROLE_CODE)?.let { roleId ->
UserDao.replaceRoles(userId, listOf(roleId))
}
}
return login(LoginRequest(username = taxpayerNum, password = request.password), loginIp)
}
suspend fun me(currentUser: CurrentUser): MeResponse {
val userRow = dbQuery { UserDao.requireActive(currentUser.id) }
@@ -73,12 +130,11 @@ object AuthService {
phone = userRow[SysUserTable.phone],
email = userRow[SysUserTable.email],
orgId = userRow[SysUserTable.orgId]?.toString(),
enterpriseId = userRow[SysUserTable.enterpriseId]?.toString(),
digitalAccountId = userRow[SysUserTable.digitalAccountId]?.toString(),
userType = userRow[SysUserTable.userType],
status = userRow[SysUserTable.status],
createdAt = userRow[SysUserTable.createdAt]?.toString(),
taxpayerNum = userRow[SysUserTable.taxpayerNum],
account = userRow[SysUserTable.taxAccount],
taxPassword = userRow[SysUserTable.taxPassword],
taxIdentityType = userRow[SysUserTable.taxIdentityType],
createdAt = userRow[SysUserTable.createdAt].toString(),
),
menus = menuTree,
permissions = permissions,
@@ -3,6 +3,8 @@
package com.bbit.ticket.utils
import com.bbit.ticket.database.system.SysUserTable
import com.bbit.ticket.dao.piaotong.EnterpriseManageDao
import com.bbit.ticket.database.piaotong.PtDigitalAccountTable
import com.bbit.ticket.entity.common.BizException
import com.bbit.ticket.entity.common.ErrorCode
import com.bbit.ticket.utils.plugins.dbQuery
@@ -21,6 +23,8 @@ data class OpenApiPrincipal(
val userId: Uuid,
val username: String,
val apiKey: String,
val enterpriseId: Uuid,
val digitalAccountId: Uuid,
val taxPayerNum: String,
val taxAccount: String,
)
@@ -32,47 +36,32 @@ suspend fun ApplicationCall.requireOpenApiPrincipal(): OpenApiPrincipal {
throw BizException(ErrorCode.UNAUTHORIZED.code, "缺少 X-Api-Key", HttpStatusCode.Unauthorized)
}
val row = dbQuery {
SysUserTable.selectAll()
.where {
(SysUserTable.apiKey eq apiKey) and
(SysUserTable.status eq "ENABLED") and
SysUserTable.deletedAt.isNull()
}
.singleOrNull()
} ?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 无效或已停用", HttpStatusCode.Unauthorized)
val accountRow = dbQuery { EnterpriseManageDao.digitalAccountByApiKey(apiKey) }
?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 无效或已停用", HttpStatusCode.Unauthorized)
val userId = accountRow[PtDigitalAccountTable.platformUserId]
?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 未绑定平台账号", HttpStatusCode.Unauthorized)
val taxpayerNum = row[SysUserTable.taxpayerNum]?.takeIf { it.isNotBlank() }
val taxAccount = row[SysUserTable.taxAccount]?.takeIf { it.isNotBlank() }
if (taxpayerNum == null || taxAccount == null) {
throw BizException(ErrorCode.BAD_REQUEST.code, "请先完善用户信息")
}
val row = dbQuery {
SysUserTable.selectAll().where {
(SysUserTable.id eq userId) and
(SysUserTable.status eq "ENABLED") and
SysUserTable.deletedAt.isNull()
}.singleOrNull()
} ?: throw BizException(ErrorCode.UNAUTHORIZED.code, "API Key 绑定账号无效或已停用", HttpStatusCode.Unauthorized)
dbQuery {
SysUserTable.update({ SysUserTable.id eq row[SysUserTable.id] }) {
SysUserTable.update({ SysUserTable.id eq userId }) {
it[updatedAt] = OffsetDateTime.now()
}
}
return OpenApiPrincipal(
userId = row[SysUserTable.id],
userId = userId,
username = row[SysUserTable.username],
apiKey = apiKey,
taxPayerNum = taxpayerNum,
taxAccount = taxAccount,
enterpriseId = accountRow[PtDigitalAccountTable.enterpriseId],
digitalAccountId = accountRow[PtDigitalAccountTable.id],
taxPayerNum = accountRow[PtDigitalAccountTable.taxpayerNum],
taxAccount = accountRow[PtDigitalAccountTable.account],
)
}
data class PtProfile(
val taxpayerNum: String,
val taxAccount: String,
)
fun CurrentUser.requirePtProfile(): PtProfile {
val taxpayerNum = taxPayerNum?.takeIf { it.isNotBlank() }
val account = taxAccount?.takeIf { it.isNotBlank() }
if (taxpayerNum == null || account == null) {
throw BizException(ErrorCode.BAD_REQUEST.code, "请先完善用户信息")
}
return PtProfile(taxpayerNum, account)
}
@@ -28,18 +28,23 @@ data class CurrentUser(
val id: Uuid,
val username: String,
val orgId: Uuid?,
val enterpriseId: Uuid?,
val digitalAccountId: Uuid?,
val userType: String,
val tokenVersion: Int,
val roleCodes: Set<String>,
val permissions: Set<String>,
val taxPayerNum: String?,
val taxAccount: String?,
val phone: String?,
val realName: String?,
val taxPassword: String?,
val taxIdentityType: String?,
) {
val isSuperAdmin: Boolean
get() = roleCodes.contains("SUPER_ADMIN")
val isEnterpriseAdmin: Boolean
get() = userType == "ENTERPRISE_ADMIN"
val isDigitalOperator: Boolean
get() = userType == "DIGITAL_OPERATOR"
}
private val CurrentUserKey = AttributeKey<CurrentUser>("currentUser")
@@ -138,13 +143,12 @@ suspend fun ApplicationCall.requireCurrentUser(): CurrentUser {
id = userRow[SysUserTable.id],
username = userRow[SysUserTable.username],
orgId = userRow[SysUserTable.orgId],
enterpriseId = userRow[SysUserTable.enterpriseId],
digitalAccountId = userRow[SysUserTable.digitalAccountId],
userType = userRow[SysUserTable.userType],
tokenVersion = userRow[SysUserTable.tokenVersion],
roleCodes = roleCodes,
permissions = permissions,
taxPayerNum = userRow[SysUserTable.taxpayerNum],
taxAccount = userRow[SysUserTable.taxAccount],
taxPassword = userRow[SysUserTable.taxPassword],
taxIdentityType = userRow[SysUserTable.taxIdentityType],
phone = userRow[SysUserTable.phone],
realName = userRow[SysUserTable.realName],
)
@@ -7,6 +7,8 @@ import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable
import com.bbit.ticket.database.piaotong.HistoryInvoiceRedTable
import com.bbit.ticket.database.piaotong.OpenInvoiceBatchItemTable
import com.bbit.ticket.database.piaotong.OpenInvoiceBatchTable
import com.bbit.ticket.database.piaotong.PtDigitalAccountTable
import com.bbit.ticket.database.piaotong.PtEnterpriseTable
import com.bbit.ticket.database.system.SysApiAccessLogTable
import com.bbit.ticket.database.system.SysDictItemTable
import com.bbit.ticket.database.system.SysDictTypeTable
@@ -36,6 +38,8 @@ object DatabaseInitializer {
SysDictItemTable,
SysOperationLogTable,
SysApiAccessLogTable,
PtEnterpriseTable,
PtDigitalAccountTable,
HistoryInvoiceRedTable,
HistoryInvoiceBasicTable,
HistoryInvoiceGoodsTable,
@@ -2,7 +2,7 @@ package com.bbit.ticket.utils.bootstrap
object Global {
val isDev = true
val isDev = false
// 请求基础地址
var baseUrl: String
@@ -5,7 +5,6 @@ package com.bbit.ticket.utils.bootstrap
import com.bbit.ticket.database.system.*
import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.service.system.PasswordService
import com.bbit.ticket.utils.net.SecurityUtil
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
@@ -25,10 +24,11 @@ object SeedData {
const val ADMIN_USERNAME = "admin"
const val ADMIN_INIT_PASSWORD = "Admin@123456"
const val ADMIN_INIT_API_KEY = "tk_admin_test_key_please_change"
private const val DEFAULT_ORG_CODE = "DEFAULT_ORG"
private const val SUPER_ADMIN_ROLE_CODE = "SUPER_ADMIN"
private const val ENTERPRISE_ADMIN_ROLE_CODE = "ENTERPRISE_ADMIN"
private const val DIGITAL_OPERATOR_ROLE_CODE = "DIGITAL_OPERATOR"
// =========================================================
// Main entry
@@ -38,10 +38,24 @@ object SeedData {
val now = OffsetDateTime.now()
val orgId = upsertDefaultOrg(now)
val roleId = upsertSuperAdminRole(now)
val enterpriseAdminRoleId = upsertBusinessRole(
code = ENTERPRISE_ADMIN_ROLE_CODE,
name = "企业管理员",
description = "外部注册企业默认管理员角色",
now = now,
)
val digitalOperatorRoleId = upsertBusinessRole(
code = DIGITAL_OPERATOR_ROLE_CODE,
name = "开票员",
description = "数电账号对应的平台开票员角色",
now = now,
)
val adminId = upsertAdminUser(orgId, now)
upsertUserRole(adminId, roleId)
val menuIds = upsertMenus(now)
bindRoleMenus(roleId, menuIds)
bindRoleMenus(roleId, menuIds.values.toList())
bindRoleMenus(enterpriseAdminRoleId, enterpriseAdminMenuIds(menuIds))
bindRoleMenus(digitalOperatorRoleId, digitalOperatorMenuIds(menuIds))
seedDicts(now)
logger.info("Seed data initialized, default admin username: {}", ADMIN_USERNAME)
}
@@ -105,6 +119,39 @@ object SeedData {
inserted[SysRoleTable.id]
}
private suspend fun upsertBusinessRole(
code: String,
name: String,
description: String,
now: OffsetDateTime,
): Uuid = dbQuery {
val existing = SysRoleTable.selectAll()
.where { (SysRoleTable.code eq code) and SysRoleTable.deletedAt.isNull() }
.singleOrNull()
if (existing != null) {
val id = existing[SysRoleTable.id]
SysRoleTable.update({ SysRoleTable.id eq id }) {
it[SysRoleTable.name] = name
it[SysRoleTable.description] = description
it[SysRoleTable.status] = "ENABLED"
it[SysRoleTable.dataScope] = "SELF"
it[SysRoleTable.updatedAt] = now
}
return@dbQuery id
}
val inserted = SysRoleTable.insert {
it[SysRoleTable.name] = name
it[SysRoleTable.code] = code
it[SysRoleTable.description] = description
it[SysRoleTable.status] = "ENABLED"
it[SysRoleTable.dataScope] = "SELF"
it[SysRoleTable.createdAt] = now
}
inserted[SysRoleTable.id]
}
// =========================================================
// Admin user
// =========================================================
@@ -120,14 +167,9 @@ object SeedData {
it[SysUserTable.nickname] = "系统管理员"
it[SysUserTable.orgId] = orgId
it[SysUserTable.status] = "ENABLED"
if (existing[SysUserTable.apiKey].isNullOrBlank()) {
it[SysUserTable.apiKey] = ADMIN_INIT_API_KEY
}
it[SysUserTable.userType] = "SYSTEM"
it[SysUserTable.updatedAt] = now
it[SysUserTable.taxpayerNum] = "500102201007206608"
it[SysUserTable.phone] = "13000000000"
it[SysUserTable.taxIdentityType] = "01"
it[SysUserTable.taxPassword] = SecurityUtil.encrypt3DES(Global.ptPassword, "ispassword")
it[SysUserTable.realName] = "测试"
}
return@dbQuery id
@@ -139,13 +181,9 @@ object SeedData {
it[SysUserTable.nickname] = "系统管理员"
it[SysUserTable.orgId] = orgId
it[SysUserTable.status] = "ENABLED"
it[SysUserTable.apiKey] = ADMIN_INIT_API_KEY
it[SysUserTable.userType] = "SYSTEM"
it[SysUserTable.tokenVersion] = 1
it[SysUserTable.taxpayerNum] = "500102201007206608"
it[SysUserTable.phone] = "13000000000"
it[SysUserTable.taxAccount] = "DEMOadmin"
it[SysUserTable.taxIdentityType] = "01"
it[SysUserTable.taxPassword] = SecurityUtil.encrypt3DES(Global.ptPassword, "ispassword")
it[SysUserTable.realName] = "测试"
it[SysUserTable.createdAt] = now
}
@@ -168,7 +206,7 @@ object SeedData {
// Menus & permissions
// =========================================================
private suspend fun upsertMenus(now: OffsetDateTime): List<Uuid> {
private suspend fun upsertMenus(now: OffsetDateTime): Map<String, Uuid> {
val seedMenus = listOf(
rootMenu("dashboard", "工作台", "Dashboard", "/dashboard", "dashboard/index", "LayoutDashboard", 10),
catalog("system", "系统管理", "SystemRoot", "Settings", 20),
@@ -196,10 +234,15 @@ object SeedData {
catalog("logs", "日志管理", "LogsRoot", "Logs", 30),
subMenu("logs_operation", "logs", "操作日志", "LogsOperation", "/logs/operation", "logs/operation/index", "ScrollText", "log:operation:view", 10),
subMenu("logs_api_access", "logs", "接口日志", "LogsApiAccess", "/logs/api-access", "logs/api-access/index", "Waypoints", "log:api-access:view", 20),
catalog("piaotong", "票通服务", "PiaoTongRoot", "Receipt", 40),
subMenu("piaotong_info", "piaotong", "基础信息", "PiaoTongInfo", "/piaotong/info", "piaotong/index", "User", "piaotong:info:view", 10),
subMenu("piaotong_invoice_issue", "piaotong", "开具蓝票", "PiaoTongInvoiceIssue", "/piaotong/invoice-issue", "piaotong/invoice-issue/index", "FilePlus", "piaotong:invoice-issue:view", 20),
subMenu("piaotong_invoice_history", "piaotong", "开票历史", "PiaoTongInvoiceHistory", "/piaotong/invoice-history", "piaotong/invoice-history/index", "History", "piaotong:invoice-history:view", 30),
catalog("basic_info", "基础信息", "BasicInfoRoot", "Building2", 40),
subMenu("enterprise_info", "basic_info", "企业信息", "EnterpriseInfo", "/enterprise/info", "piaotong/index", "Building2", "enterprise:info:view", 10),
subMenu("digital_account", "basic_info", "数电账号", "DigitalAccountManage", "/enterprise/digital-accounts", "piaotong/digital-accounts/index", "Users", "digital-account:view", 20),
subMenu("invoice_setting", "basic_info", "开票设置", "InvoiceSetting", "/enterprise/invoice-setting", "piaotong/invoice-setting/index", "SlidersHorizontal", "invoice-setting:view", 30),
catalog("invoice_service", "开票服务", "InvoiceServiceRoot", "Receipt", 50),
subMenu("piaotong_invoice_issue", "invoice_service", "开具蓝票", "PiaoTongInvoiceIssue", "/piaotong/invoice-issue", "piaotong/invoice-issue/index", "FilePlus", "piaotong:invoice-issue:view", 10),
subMenu("piaotong_invoice_history", "invoice_service", "开票历史", "PiaoTongInvoiceHistory", "/piaotong/invoice-history", "piaotong/invoice-history/index", "History", "piaotong:invoice-history:view", 20),
catalog("statistics_info", "统计信息", "StatisticsInfoRoot", "ChartNoAxesColumn", 60),
subMenu("openapi_statistics", "statistics_info", "OpenAPI", "OpenApiStatistics", "/statistics/openapi", "statistics/openapi/index", "Waypoints", "openapi:statistics:view", 10),
)
val idMap = mutableMapOf<String, Uuid>()
@@ -208,9 +251,35 @@ object SeedData {
val menuId = upsertMenu(menu, parentId, now)
idMap[menu.key] = menuId
}
return idMap.values.toList()
return idMap
}
private fun enterpriseAdminMenuIds(menuIds: Map<String, Uuid>): List<Uuid> =
listOf(
"dashboard",
"basic_info",
"enterprise_info",
"digital_account",
"invoice_setting",
"invoice_service",
"piaotong_invoice_issue",
"piaotong_invoice_history",
"statistics_info",
"openapi_statistics",
).mapNotNull { menuIds[it] }
private fun digitalOperatorMenuIds(menuIds: Map<String, Uuid>): List<Uuid> =
listOf(
"dashboard",
"basic_info",
"digital_account",
"invoice_service",
"piaotong_invoice_issue",
"piaotong_invoice_history",
"statistics_info",
"openapi_statistics",
).mapNotNull { menuIds[it] }
private suspend fun upsertMenu(seedMenu: SeedMenu, parentId: Uuid?, now: OffsetDateTime): Uuid = dbQuery {
val existing = SysMenuTable.selectAll()
.where { (SysMenuTable.name eq seedMenu.name) and SysMenuTable.deletedAt.isNull() }
@@ -12,19 +12,34 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URI
import java.text.SimpleDateFormat
import java.util.*
object PTClient {
val logger = LoggerFactory.getLogger(PTClient::class.java)
private const val CONNECT_TIMEOUT_MS = 10_000
private const val READ_TIMEOUT_MS = 60_000
val wireJson = Json {
explicitNulls = false
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
}
val client = HttpClient(CIO) {
@@ -123,14 +138,15 @@ object PTClient {
headers: Map<String, String> = emptyMap()
): Resp {
// req json
val reqJson = myJson.encodeToString(body)
val reqJson = wireJson.encodeToString(body)
val targetUrl = Global.baseUrl + url
val requestBody = buildRequestData(reqJson)
val startedAt = System.currentTimeMillis()
logger.info("POST url = $targetUrl")
logger.info("req = $reqJson")
val response = client.post(Global.baseUrl + url) {
contentType(ContentType.Application.Json)
headers.forEach { (k, v) -> header(k, v) }
setBody(buildRequestData(reqJson))
}.bodyAsText()
val response = postJsonRaw(targetUrl, requestBody, headers)
logger.info("raw response costMs=${System.currentTimeMillis() - startedAt} body=$response")
val decrypted = disposeResponse(response)
val result = myJson.decodeFromString<PTResponse<JsonElement>>(decrypted)
@@ -145,6 +161,54 @@ object PTClient {
return myJson.decodeFromJsonElement<Resp>(result.content!!)
}
suspend fun postJsonRaw(
url: String,
jsonBody: String,
headers: Map<String, String> = emptyMap(),
): String = withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
try {
val bytes = jsonBody.toByteArray(Charsets.UTF_8)
connection = (URI.create(url).toURL().openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
connectTimeout = CONNECT_TIMEOUT_MS
readTimeout = READ_TIMEOUT_MS
doOutput = true
useCaches = false
setRequestProperty("Content-Type", "application/json; charset=UTF-8")
setRequestProperty("Accept", "application/json")
setRequestProperty("Connection", "close")
headers.forEach { (k, v) -> setRequestProperty(k, v) }
setFixedLengthStreamingMode(bytes.size)
}
connection.outputStream.use { output ->
output.write(bytes)
output.flush()
}
val responseCode = connection.responseCode
val stream = if (responseCode in 200..299) connection.inputStream else connection.errorStream
val response = stream?.use { input ->
BufferedReader(InputStreamReader(input, Charsets.UTF_8)).use { reader ->
buildString {
while (true) {
val line = reader.readLine() ?: break
append(line)
}
}
}
}.orEmpty()
if (responseCode !in 200..299) {
throw IllegalStateException("票通接口 HTTP $responseCode: $response")
}
response
} finally {
connection?.disconnect()
}
}
/**
* 关闭
*/
@@ -166,7 +230,7 @@ object PTClient {
map["timestamp"] = sdf.format(Date())
map["serialNo"] = ptDate()
map["sign"] = RSAUtil.sign(RSAUtil.getSignatureContent(map), Global.ptPrivateKey) ?: ""
return myJson.encodeToString(map)
return wireJson.encodeToString(map)
}
fun disposeResponse(
@@ -229,4 +293,4 @@ object PTClient {
return str
}
}
}