多数电账号管理
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
-- 扩展现有字段长度,适配票通接口校验规则和 base64 图片存储
|
||||
ALTER TABLE sys_user
|
||||
ALTER COLUMN tax_contact_name TYPE varchar(50),
|
||||
ALTER COLUMN tax_contact_email TYPE varchar(100),
|
||||
ALTER COLUMN tax_legal_person_name TYPE varchar(50),
|
||||
ALTER COLUMN tax_city_name TYPE varchar(50),
|
||||
ALTER COLUMN tax_enterprise_address TYPE varchar(200),
|
||||
ALTER COLUMN tax_registration_certificate TYPE text;
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE sys_user
|
||||
ADD COLUMN IF NOT EXISTS preset_address VARCHAR(200),
|
||||
ADD COLUMN IF NOT EXISTS preset_phone VARCHAR(32);
|
||||
@@ -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],
|
||||
)
|
||||
}
|
||||
|
||||
+7
-1
@@ -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,
|
||||
)
|
||||
+1
-126
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
+19
-15
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,10 +1,14 @@
|
||||
import http from '@/api/http'
|
||||
import type { LoginRequest, LoginResponse, MeResponse } from '@/types/auth'
|
||||
import type { EnterpriseRegisterRequest, LoginRequest, LoginResponse, MeResponse } from '@/types/auth'
|
||||
|
||||
export function loginApi(payload: LoginRequest) {
|
||||
return http.post<never, LoginResponse>('/auth/login', payload)
|
||||
}
|
||||
|
||||
export function registerEnterpriseApi(payload: EnterpriseRegisterRequest) {
|
||||
return http.post<never, LoginResponse>('/auth/register-enterprise', payload)
|
||||
}
|
||||
|
||||
export function logoutApi() {
|
||||
return http.post<never, void>('/auth/logout')
|
||||
}
|
||||
|
||||
@@ -95,83 +95,83 @@ export function getPTInfoApi(): Promise<TaxBureauAccountAuth> {
|
||||
return http.get('/pt/info')
|
||||
}
|
||||
|
||||
export interface TaxEnterpriseRegisterRequest {
|
||||
taxpayerNum: string
|
||||
enterpriseName: string
|
||||
legalPersonName: string
|
||||
contactsName: string
|
||||
contactsEmail: string
|
||||
contactsPhone: string
|
||||
regionCode: string
|
||||
cityName: string
|
||||
enterpriseAddress: string
|
||||
taxRegistrationCertificate: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册企业(纳税人)
|
||||
*/
|
||||
export function registerEnterpriseApi(payload: TaxEnterpriseRegisterRequest): Promise<string> {
|
||||
return http.post('/pt/register', payload)
|
||||
}
|
||||
|
||||
export interface TaxRegisterUserRequest {
|
||||
taxpayerNum: string
|
||||
taxAccount: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 登记账号
|
||||
*/
|
||||
export function registerUserApi(payload: TaxRegisterUserRequest): Promise<string> {
|
||||
return http.post('/pt/registerUser', payload)
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 基础信息配置(本地 CRUD)
|
||||
// =============================================
|
||||
|
||||
/** 企业信息 */
|
||||
export interface EnterpriseInfo {
|
||||
id: string
|
||||
taxpayerNum: string
|
||||
enterpriseName: string
|
||||
legalPersonName: string
|
||||
contactsName: string
|
||||
contactsEmail: string
|
||||
contactsPhone: string
|
||||
regionCode: string
|
||||
cityName: string
|
||||
enterpriseAddress: string
|
||||
taxRegistrationCertificate: string
|
||||
legalPersonName?: string | null
|
||||
contactsName?: string | null
|
||||
contactsEmail?: string | null
|
||||
contactsPhone?: string | null
|
||||
regionCode?: string | null
|
||||
cityName?: string | null
|
||||
enterpriseAddress?: string | null
|
||||
taxRegistrationCertificate?: string | null
|
||||
invitationCode?: string | null
|
||||
reviewStatus?: string | null
|
||||
reviewOpinion?: string | null
|
||||
invoiceKind?: string | null
|
||||
invoiceLayoutFileType?: string | null
|
||||
serviceStatus?: string | null
|
||||
bankName?: string | null
|
||||
bankAccount?: string | null
|
||||
presetAddress?: string | null
|
||||
presetPhone?: string | null
|
||||
}
|
||||
|
||||
/** 获取企业信息 */
|
||||
export function getEnterpriseInfoApi(): Promise<EnterpriseInfo> {
|
||||
return http.get('/pt/enterprise')
|
||||
}
|
||||
|
||||
/** 更新企业信息 */
|
||||
export function updateEnterpriseInfoApi(payload: Partial<EnterpriseInfo>): Promise<string> {
|
||||
return http.put('/pt/enterprise', payload)
|
||||
export function refreshEnterpriseInfoApi(): Promise<EnterpriseInfo> {
|
||||
return http.post('/pt/enterprise/refresh')
|
||||
}
|
||||
|
||||
/** 数电账号信息 */
|
||||
export interface DigitalAccountInfo {
|
||||
export interface DigitalAccountItem {
|
||||
id: string
|
||||
enterpriseId: string
|
||||
taxpayerNum: string
|
||||
taxAccount: string
|
||||
account: string
|
||||
name?: string | null
|
||||
identityType?: string | null
|
||||
operationProposed?: string | null
|
||||
authStatus?: string | null
|
||||
switchable?: string | null
|
||||
wechatUserBindStatus?: string | null
|
||||
lastAuthSuccTime?: string | null
|
||||
loginAuthStatus?: string | null
|
||||
lastLoginAuthTime?: string | null
|
||||
riskAuthStatus?: string | null
|
||||
lastRiskAuthTime?: string | null
|
||||
platformUserId?: string | null
|
||||
platformUsername?: string | null
|
||||
apiKey?: string | null
|
||||
status: string
|
||||
}
|
||||
|
||||
/** 获取数电账号信息 */
|
||||
export function getDigitalAccountApi(): Promise<DigitalAccountInfo> {
|
||||
return http.get('/pt/digital-account')
|
||||
export interface CreateDigitalAccountRequest {
|
||||
account: string
|
||||
taxPassword: string
|
||||
identityType: string
|
||||
phoneNum: string
|
||||
name: string
|
||||
platformPassword: string
|
||||
}
|
||||
|
||||
/** 更新数电账号信息 */
|
||||
export function updateDigitalAccountApi(payload: DigitalAccountInfo): Promise<string> {
|
||||
return http.put('/pt/digital-account', payload)
|
||||
export function listDigitalAccountsApi(): Promise<DigitalAccountItem[]> {
|
||||
return http.get('/pt/digital-accounts')
|
||||
}
|
||||
|
||||
export function refreshDigitalAccountsApi(): Promise<DigitalAccountItem[]> {
|
||||
return http.post('/pt/digital-accounts/refresh')
|
||||
}
|
||||
|
||||
export function createDigitalAccountApi(
|
||||
payload: CreateDigitalAccountRequest
|
||||
): Promise<DigitalAccountItem> {
|
||||
return http.post('/pt/digital-accounts', payload)
|
||||
}
|
||||
|
||||
/** 开票预设数据 */
|
||||
export interface PresetData {
|
||||
bankName: string
|
||||
bankAccount: string
|
||||
@@ -179,16 +179,44 @@ export interface PresetData {
|
||||
phone: string
|
||||
}
|
||||
|
||||
/** 获取开票预设数据 */
|
||||
export interface EnterpriseBankAccount {
|
||||
bankName: string
|
||||
bankAccount?: string | null
|
||||
source: string
|
||||
}
|
||||
|
||||
export function getPresetDataApi(): Promise<PresetData> {
|
||||
return http.get('/pt/preset')
|
||||
return getEnterpriseInfoApi().then((info) => ({
|
||||
bankName: info.bankName ?? '',
|
||||
bankAccount: info.bankAccount ?? '',
|
||||
address: info.presetAddress ?? '',
|
||||
phone: info.presetPhone ?? ''
|
||||
}))
|
||||
}
|
||||
|
||||
/** 更新开票预设数据 */
|
||||
export function updatePresetDataApi(payload: PresetData): Promise<string> {
|
||||
return http.put('/pt/preset', payload)
|
||||
}
|
||||
|
||||
export function queryEnterpriseBankAccountsApi(): Promise<EnterpriseBankAccount[]> {
|
||||
return http.get('/pt/enterprise/bank-accounts')
|
||||
}
|
||||
|
||||
export interface OpenApiStatisticsItem {
|
||||
digitalAccountId?: string | null
|
||||
account?: string | null
|
||||
interfaceCode?: string | null
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
avgCostMs: number
|
||||
lastCalledAt?: string | null
|
||||
}
|
||||
|
||||
export function openApiStatisticsApi(): Promise<OpenApiStatisticsItem[]> {
|
||||
return http.get('/pt/openapi/statistics')
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 开票相关
|
||||
// =============================================
|
||||
@@ -269,6 +297,7 @@ export interface OrderInfo {
|
||||
* 数电发票开票请求
|
||||
*/
|
||||
export interface InvoiceRequest {
|
||||
digitalAccountId?: string | null
|
||||
/** 销方纳税人识别号 */
|
||||
taxpayerNum: string
|
||||
/** 发票请求流水号 */
|
||||
@@ -850,8 +879,11 @@ export interface AuthQrcodeResponse {
|
||||
* 获取实名认证二维码
|
||||
* @param qrcodeType 1: 电子税务局 APP, 2: 国家网络身份认证 APP
|
||||
*/
|
||||
export function getAuthQrcodeApi(qrcodeType: string): Promise<AuthQrcodeResponse> {
|
||||
return http.get('/pt/authentication', { params: { qrcodeType } })
|
||||
export function getAuthQrcodeApi(
|
||||
qrcodeType: string,
|
||||
digitalAccountId?: string
|
||||
): Promise<AuthQrcodeResponse> {
|
||||
return http.get('/pt/authentication', { params: { qrcodeType, digitalAccountId } })
|
||||
}
|
||||
|
||||
/** 查询认证二维码扫码状态响应 */
|
||||
|
||||
@@ -60,6 +60,7 @@ function handleSelect(key: string) {
|
||||
<style scoped>
|
||||
:deep(.n-menu) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<h2 class="page-toolbar-title">数电账号管理</h2>
|
||||
<div class="page-toolbar-actions">
|
||||
<n-button :loading="loading" @click="refreshAccounts">
|
||||
<template #icon><n-icon :component="RefreshCw" /></template>
|
||||
刷新
|
||||
</n-button>
|
||||
<n-button v-if="canManage" type="primary" @click="showCreate = true">
|
||||
<template #icon><n-icon :component="Plus" /></template>
|
||||
新增
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="accounts"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:row-key="(row: DigitalAccountItem) => row.id"
|
||||
:scroll-x="1460"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-modal v-model:show="showCreate" preset="card" title="新增数电账号" class="modal-card">
|
||||
<n-form ref="createFormRef" :model="createForm" :rules="createRules" label-placement="top">
|
||||
<n-grid responsive="screen" :cols="2" :x-gap="12">
|
||||
<n-gi>
|
||||
<n-form-item label="税局账号" path="account">
|
||||
<n-input v-model:value="createForm.account" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="税局密码" path="taxPassword">
|
||||
<n-input v-model:value="createForm.taxPassword" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="姓名" path="name">
|
||||
<n-input v-model:value="createForm.name" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="手机号" path="phoneNum">
|
||||
<n-input v-model:value="createForm.phoneNum" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="身份类型" path="identityType">
|
||||
<n-select v-model:value="createForm.identityType" :options="identityOptions" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="平台密码" path="platformPassword">
|
||||
<n-input
|
||||
v-model:value="createForm.platformPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="showCreate = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="createAccount">保存</n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showSms" preset="card" title="短信登录" class="sms-card">
|
||||
<n-space vertical>
|
||||
<n-alert v-if="smsTip" type="info">{{ smsTip }}</n-alert>
|
||||
<n-input v-model:value="smsCode" placeholder="请输入短信验证码" />
|
||||
<div class="modal-actions">
|
||||
<n-button @click="showSms = false">取消</n-button>
|
||||
<n-button type="primary" :loading="smsLoading" @click="submitSms">确认登录</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showQrcode" preset="card" title="风险认证" class="sms-card">
|
||||
<div class="qrcode-wrap">
|
||||
<img v-if="qrcodeImg" :src="qrcodeImg" alt="认证二维码" />
|
||||
<n-empty v-else description="暂无二维码" />
|
||||
<n-button size="small" :loading="qrLoading" @click="loadQrcode">刷新二维码</n-button>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NDataTable,
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGi,
|
||||
NGrid,
|
||||
NIcon,
|
||||
NInput,
|
||||
NModal,
|
||||
NSelect,
|
||||
NSpace,
|
||||
NTag,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { KeyRound, Plus, RefreshCw, ShieldCheck } from 'lucide-vue-next'
|
||||
import {
|
||||
createDigitalAccountApi,
|
||||
getAuthQrcodeApi,
|
||||
listDigitalAccountsApi,
|
||||
refreshDigitalAccountsApi,
|
||||
sendLoginSmsCodeApi,
|
||||
smsLoginApi
|
||||
} from '@/api/piaotong'
|
||||
import type { DigitalAccountItem } from '@/api/piaotong'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const accounts = ref<DigitalAccountItem[]>([])
|
||||
const showCreate = ref(false)
|
||||
const showSms = ref(false)
|
||||
const showQrcode = ref(false)
|
||||
const smsLoading = ref(false)
|
||||
const qrLoading = ref(false)
|
||||
const smsCode = ref('')
|
||||
const smsTip = ref('')
|
||||
const qrcodeImg = ref('')
|
||||
const selected = ref<DigitalAccountItem | null>(null)
|
||||
const createFormRef = ref<FormInst | null>(null)
|
||||
|
||||
const canManage = computed(() => authStore.user?.userType !== 'DIGITAL_OPERATOR')
|
||||
|
||||
const createForm = reactive({
|
||||
account: '',
|
||||
taxPassword: '',
|
||||
identityType: '09',
|
||||
phoneNum: '',
|
||||
name: '',
|
||||
platformPassword: ''
|
||||
})
|
||||
|
||||
const createRules: FormRules = {
|
||||
account: [{ required: true, message: '请输入税局账号', trigger: ['blur', 'input'] }],
|
||||
taxPassword: [{ required: true, message: '请输入税局密码', trigger: ['blur', 'input'] }],
|
||||
name: [{ required: true, message: '请输入姓名', trigger: ['blur', 'input'] }],
|
||||
phoneNum: [{ required: true, message: '请输入手机号', trigger: ['blur', 'input'] }],
|
||||
platformPassword: [{ required: true, message: '请输入平台密码', trigger: ['blur', 'input'] }]
|
||||
}
|
||||
|
||||
const identityOptions = [
|
||||
{ label: '法定代表人', value: '01' },
|
||||
{ label: '财务负责人', value: '02' },
|
||||
{ label: '办税员', value: '03' },
|
||||
{ label: '管理员', value: '05' },
|
||||
{ label: '开票员', value: '09' }
|
||||
]
|
||||
|
||||
const columns: DataTableColumns<DigitalAccountItem> = [
|
||||
{ title: '税局账号', key: 'account', minWidth: 140 },
|
||||
{ title: '平台登录账号', key: 'platformUsername', minWidth: 180, render: (row) => row.platformUsername || '-' },
|
||||
{ title: '姓名', key: 'name', minWidth: 100 },
|
||||
{ title: '登录身份', key: 'identityType', render: (row) => identityLabel(row.identityType) },
|
||||
{ title: '账号状态', key: 'authStatus', render: (row) => authStatusLabel(row.authStatus) },
|
||||
{
|
||||
title: '登录认证',
|
||||
key: 'loginAuthStatus',
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: row.loginAuthStatus === '1' ? 'success' : 'warning' },
|
||||
{ default: () => (row.loginAuthStatus === '1' ? '已登录' : '未登录') }
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '风险认证',
|
||||
key: 'riskAuthStatus',
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: row.riskAuthStatus === '1' ? 'success' : 'warning' },
|
||||
{ default: () => (row.riskAuthStatus === '1' ? '已认证' : '未认证') }
|
||||
)
|
||||
},
|
||||
{ title: '最近认证', key: 'lastAuthSuccTime', minWidth: 160 },
|
||||
{
|
||||
title: 'API Key',
|
||||
key: 'apiKey',
|
||||
minWidth: 180,
|
||||
render: (row) => row.apiKey || '-'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
fixed: 'right',
|
||||
width: 220,
|
||||
render: (row) =>
|
||||
h('div', { class: 'row-actions' }, [
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', secondary: true, onClick: () => openSms(row) },
|
||||
{ icon: () => h(NIcon, { component: KeyRound }), default: () => '短信登录' }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', tertiary: true, onClick: () => openQrcode(row) },
|
||||
{ icon: () => h(NIcon, { component: ShieldCheck }), default: () => '认证' }
|
||||
)
|
||||
])
|
||||
}
|
||||
]
|
||||
|
||||
function identityLabel(value?: string | null) {
|
||||
return identityOptions.find((item) => item.value === value)?.label || value || '-'
|
||||
}
|
||||
|
||||
function authStatusLabel(value?: string | null) {
|
||||
const map: Record<string, string> = {
|
||||
'0': '无需认证',
|
||||
'1': '风险认证',
|
||||
'2': '登录认证',
|
||||
'3': '风险+登录认证'
|
||||
}
|
||||
return value ? map[value] || value : '-'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
accounts.value = await listDigitalAccountsApi()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAccounts() {
|
||||
loading.value = true
|
||||
try {
|
||||
accounts.value = await refreshDigitalAccountsApi()
|
||||
message.success('数电账号已刷新')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createAccount() {
|
||||
await createFormRef.value?.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
await createDigitalAccountApi({ ...createForm })
|
||||
message.success('数电账号已创建')
|
||||
showCreate.value = false
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openSms(row: DigitalAccountItem) {
|
||||
selected.value = row
|
||||
smsCode.value = ''
|
||||
smsLoading.value = true
|
||||
showSms.value = true
|
||||
try {
|
||||
const res = await sendLoginSmsCodeApi({ taxpayerNum: row.taxpayerNum, account: row.account })
|
||||
smsTip.value = res.phoneNum ? `验证码已发送至 ${res.phoneNum}` : res.resultMsg
|
||||
} finally {
|
||||
smsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSms() {
|
||||
if (!selected.value || !smsCode.value.trim()) return
|
||||
smsLoading.value = true
|
||||
try {
|
||||
await smsLoginApi({
|
||||
taxpayerNum: selected.value.taxpayerNum,
|
||||
account: selected.value.account,
|
||||
smsCode: smsCode.value.trim()
|
||||
})
|
||||
message.success('短信登录成功')
|
||||
showSms.value = false
|
||||
await load()
|
||||
} finally {
|
||||
smsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openQrcode(row: DigitalAccountItem) {
|
||||
selected.value = row
|
||||
showQrcode.value = true
|
||||
await loadQrcode()
|
||||
}
|
||||
|
||||
async function loadQrcode() {
|
||||
if (!selected.value) return
|
||||
qrLoading.value = true
|
||||
try {
|
||||
const res = await getAuthQrcodeApi('1', selected.value.id)
|
||||
qrcodeImg.value = res.qrcodeImg ? `data:image/png;base64,${res.qrcodeImg}` : ''
|
||||
} finally {
|
||||
qrLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-actions,
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row-actions :deep(.n-button) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: min(720px, 92vw);
|
||||
}
|
||||
|
||||
.sms-card {
|
||||
width: min(420px, 92vw);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.qrcode-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.qrcode-wrap img {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
</style>
|
||||
+69
-2063
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
:columns="columns"
|
||||
:data="dataSource"
|
||||
|
||||
@@ -190,6 +190,17 @@
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<n-grid item-responsive responsive="screen" cols="2 s:2 m:3 l:4 xl:5 2xl:6" :x-gap="12" :y-gap="4">
|
||||
<n-gi>
|
||||
<n-form-item label="数电账号" path="digitalAccountId">
|
||||
<n-select
|
||||
v-model:value="form.digitalAccountId"
|
||||
:options="digitalAccountOptions"
|
||||
:disabled="isOperator"
|
||||
placeholder="请选择开票员"
|
||||
@update:value="applySelectedDigitalAccount"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="销方纳税人识别号" path="taxpayerNum">
|
||||
<n-input
|
||||
@@ -914,8 +925,13 @@ import {
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { invoiceIssueApi, getEnterpriseInfoApi, getPresetDataApi } from '@/api/piaotong'
|
||||
import type { InvoiceRequest, InvoiceItem, VariableLevyProof } from '@/api/piaotong'
|
||||
import {
|
||||
invoiceIssueApi,
|
||||
getEnterpriseInfoApi,
|
||||
getPresetDataApi,
|
||||
listDigitalAccountsApi
|
||||
} from '@/api/piaotong'
|
||||
import type { DigitalAccountItem, InvoiceRequest, InvoiceItem, VariableLevyProof } from '@/api/piaotong'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
|
||||
const message = useMessage()
|
||||
@@ -931,6 +947,14 @@ const orderInputValue = ref('')
|
||||
const activeSidebar = ref<string | number>('basic')
|
||||
|
||||
const currentUser = computed(() => authStore.user)
|
||||
const digitalAccounts = ref<DigitalAccountItem[]>([])
|
||||
const isOperator = computed(() => currentUser.value?.userType === 'DIGITAL_OPERATOR')
|
||||
const digitalAccountOptions = computed(() =>
|
||||
digitalAccounts.value.map((item) => ({
|
||||
label: `${item.name || item.account}(${item.account})`,
|
||||
value: item.id
|
||||
}))
|
||||
)
|
||||
const currentItem = computed({
|
||||
get: () => form.itemList[activeSidebar.value as number] ?? form.itemList[0],
|
||||
set: (val) => {
|
||||
@@ -1144,6 +1168,7 @@ function createEmptyVariableLevyProof(): VariableLevyProof {
|
||||
}
|
||||
|
||||
const form = reactive<InvoiceRequest>({
|
||||
digitalAccountId: null,
|
||||
taxpayerNum: '',
|
||||
invoiceReqSerialNo: '',
|
||||
invoiceIssueKindCode: '82',
|
||||
@@ -1231,6 +1256,13 @@ function generateSerialNo() {
|
||||
autoSerial.value = true
|
||||
}
|
||||
|
||||
function applySelectedDigitalAccount() {
|
||||
const selected = digitalAccounts.value.find((item) => item.id === form.digitalAccountId)
|
||||
if (!selected) return
|
||||
form.taxpayerNum = selected.taxpayerNum
|
||||
form.account = selected.account
|
||||
}
|
||||
|
||||
function confirmRemove(index: number) {
|
||||
const item = form.itemList[index]
|
||||
const itemName = item?.goodsName || `商品 #${item?.lineNo || index + 1}`
|
||||
@@ -1533,20 +1565,24 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (currentUser.value?.taxpayerNum) {
|
||||
form.taxpayerNum = currentUser.value.taxpayerNum
|
||||
}
|
||||
if (currentUser.value?.account) {
|
||||
form.account = currentUser.value.account
|
||||
}
|
||||
generateSerialNo()
|
||||
|
||||
// 加载企业信息和预设数据,自动填入销方信息
|
||||
try {
|
||||
const [enterpriseInfo, presetData] = await Promise.all([
|
||||
const [enterpriseInfo, presetData, accounts] = await Promise.all([
|
||||
getEnterpriseInfoApi(),
|
||||
getPresetDataApi()
|
||||
getPresetDataApi(),
|
||||
listDigitalAccountsApi()
|
||||
])
|
||||
digitalAccounts.value = accounts
|
||||
const defaultAccount =
|
||||
accounts.find((item) => item.id === currentUser.value?.digitalAccountId) || accounts[0]
|
||||
if (defaultAccount) {
|
||||
form.digitalAccountId = defaultAccount.id
|
||||
applySelectedDigitalAccount()
|
||||
} else if (enterpriseInfo?.taxpayerNum) {
|
||||
form.taxpayerNum = enterpriseInfo.taxpayerNum
|
||||
}
|
||||
if (enterpriseInfo?.enterpriseAddress) {
|
||||
form.sellerAddress = enterpriseInfo.enterpriseAddress
|
||||
}
|
||||
@@ -1650,7 +1686,7 @@ watch(
|
||||
|
||||
/** 重置表单到初始状态 */
|
||||
function resetForm() {
|
||||
form.taxpayerNum = currentUser.value?.taxpayerNum || ''
|
||||
form.taxpayerNum = ''
|
||||
form.invoiceReqSerialNo = ''
|
||||
form.buyerName = ''
|
||||
form.buyerTaxpayerNum = ''
|
||||
@@ -1670,7 +1706,7 @@ function resetForm() {
|
||||
form.showSellerBank = '0'
|
||||
form.showBuyerAddrTel = '0'
|
||||
form.showSellerAddrTel = '0'
|
||||
form.account = currentUser.value?.account || ''
|
||||
form.account = ''
|
||||
form.variableLevyFlag = ''
|
||||
form.casherName = ''
|
||||
form.reviewerName = ''
|
||||
@@ -1687,6 +1723,7 @@ function resetForm() {
|
||||
form.variableLevyProofList = []
|
||||
form.orderList = []
|
||||
activeSidebar.value = 'basic'
|
||||
applySelectedDigitalAccount()
|
||||
generateSerialNo()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card">
|
||||
<div class="page-toolbar">
|
||||
<h2 class="page-toolbar-title">开票设置</h2>
|
||||
<div class="page-toolbar-actions">
|
||||
<n-button :loading="bankLoading" @click="openBankPicker">快捷选择银行</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="save">保存</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<n-form :model="form" label-placement="top" class="form">
|
||||
<n-grid responsive="screen" :cols="2" :x-gap="16">
|
||||
<n-gi>
|
||||
<n-form-item label="开户行">
|
||||
<n-input v-model:value="form.bankName" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="银行账号">
|
||||
<n-input v-model:value="form.bankAccount" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="销售方地址">
|
||||
<n-input v-model:value="form.address" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item label="销售方电话">
|
||||
<n-input v-model:value="form.phone" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-modal
|
||||
v-model:show="bankModalVisible"
|
||||
preset="card"
|
||||
title="快捷选择银行"
|
||||
:style="{ width: '520px', maxWidth: '92vw' }"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<n-space vertical>
|
||||
<n-select
|
||||
v-model:value="selectedBankKey"
|
||||
:options="bankOptions"
|
||||
:loading="bankLoading"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择开户行及账号"
|
||||
/>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="bankModalVisible = false">取消</n-button>
|
||||
<n-button type="primary" :disabled="!selectedBankKey" :loading="saving" @click="confirmBank">
|
||||
确定
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { NButton, NForm, NFormItem, NGi, NGrid, NInput, NModal, NSelect, NSpace, useMessage } from 'naive-ui'
|
||||
import {
|
||||
getPresetDataApi,
|
||||
queryEnterpriseBankAccountsApi,
|
||||
updatePresetDataApi
|
||||
} from '@/api/piaotong'
|
||||
import type { EnterpriseBankAccount } from '@/api/piaotong'
|
||||
|
||||
const message = useMessage()
|
||||
const saving = ref(false)
|
||||
const bankLoading = ref(false)
|
||||
const bankModalVisible = ref(false)
|
||||
const selectedBankKey = ref<string | null>(null)
|
||||
const bankAccounts = ref<EnterpriseBankAccount[]>([])
|
||||
const form = reactive({ bankName: '', bankAccount: '', address: '', phone: '' })
|
||||
|
||||
const bankOptions = computed(() =>
|
||||
bankAccounts.value.map((item, index) => ({
|
||||
label: `${item.bankName}${item.bankAccount ? ` / ${item.bankAccount}` : ''}`,
|
||||
value: String(index)
|
||||
}))
|
||||
)
|
||||
|
||||
async function load() {
|
||||
Object.assign(form, await getPresetDataApi())
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await updatePresetDataApi({ ...form })
|
||||
message.success('开票设置已保存')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openBankPicker() {
|
||||
bankModalVisible.value = true
|
||||
bankLoading.value = true
|
||||
try {
|
||||
bankAccounts.value = await queryEnterpriseBankAccountsApi()
|
||||
selectedBankKey.value = bankAccounts.value.length === 1 ? '0' : null
|
||||
if (bankAccounts.value.length === 0) {
|
||||
message.info('未查询到可选择的开户行及账号')
|
||||
}
|
||||
} finally {
|
||||
bankLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmBank() {
|
||||
if (selectedBankKey.value === null) return
|
||||
const selected = bankAccounts.value[Number(selectedBankKey.value)]
|
||||
if (!selected) return
|
||||
form.bankName = selected.bankName
|
||||
form.bankAccount = selected.bankAccount ?? ''
|
||||
await save()
|
||||
bankModalVisible.value = false
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form {
|
||||
max-width: 880px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<h2 class="page-toolbar-title">OpenAPI</h2>
|
||||
<div class="page-toolbar-actions">
|
||||
<n-button :loading="loading" @click="load">
|
||||
<template #icon><n-icon :component="RefreshCw" /></template>
|
||||
刷新
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 12 }"
|
||||
:row-key="(row: OpenApiStatisticsItem) => `${row.digitalAccountId}-${row.interfaceCode}`"
|
||||
:scroll-x="1040"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NDataTable, NIcon, NTag } from 'naive-ui'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { openApiStatisticsApi } from '@/api/piaotong'
|
||||
import type { OpenApiStatisticsItem } from '@/api/piaotong'
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<OpenApiStatisticsItem[]>([])
|
||||
|
||||
const columns: DataTableColumns<OpenApiStatisticsItem> = [
|
||||
{ title: '数电账号ID', key: 'digitalAccountId', minWidth: 220 },
|
||||
{ title: '接口', key: 'interfaceCode', minWidth: 180 },
|
||||
{ title: '调用次数', key: 'total', width: 100 },
|
||||
{ title: '成功', key: 'success', width: 100, render: (row) => h(NTag, { type: 'success' }, { default: () => row.success }) },
|
||||
{ title: '失败', key: 'failed', width: 100, render: (row) => h(NTag, { type: row.failed > 0 ? 'error' : 'default' }, { default: () => row.failed }) },
|
||||
{ title: '平均耗时(ms)', key: 'avgCostMs', width: 140 },
|
||||
{ title: '最近调用', key: 'lastCalledAt', minWidth: 180 }
|
||||
]
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = await openApiStatisticsApi()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="typeColumns"
|
||||
:data="typeRows"
|
||||
:pagination="typePagination"
|
||||
@@ -26,6 +27,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="itemColumns"
|
||||
:data="itemRows"
|
||||
:pagination="itemPagination"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:pagination="pagination"
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
@@ -166,9 +167,6 @@
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="editForm.email" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="editModal.mode === 'edit'" label="API Key">
|
||||
<n-input v-model:value="editForm.apiKey" readonly />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="editModal.mode === 'create'" label="状态" path="status">
|
||||
<n-radio-group v-model:value="editForm.status">
|
||||
<n-radio-button value="ENABLED">启用</n-radio-button>
|
||||
@@ -340,8 +338,7 @@ const baseDetailItems = computed(() => {
|
||||
{ label: '邮箱', value: displayValue(user.email) },
|
||||
{ label: '组织', value: formatOrg(user) },
|
||||
{ label: '状态', value: user.statusLabel || statusLabel(user.status) },
|
||||
{ label: '头像', value: displayValue(user.avatar) },
|
||||
{ label: 'API Key', value: displayValue(user.apiKey) }
|
||||
{ label: '头像', value: displayValue(user.avatar) }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -405,7 +402,6 @@ const editForm = reactive({
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
apiKey: '',
|
||||
status: 'ENABLED'
|
||||
})
|
||||
|
||||
@@ -526,7 +522,6 @@ function resetEditForm() {
|
||||
editForm.realName = ''
|
||||
editForm.phone = ''
|
||||
editForm.email = ''
|
||||
editForm.apiKey = ''
|
||||
editForm.status = 'ENABLED'
|
||||
}
|
||||
|
||||
@@ -572,7 +567,6 @@ async function openEdit(row: UserListItem) {
|
||||
editForm.realName = detail.realName ?? ''
|
||||
editForm.phone = detail.phone ?? ''
|
||||
editForm.email = detail.email ?? ''
|
||||
editForm.apiKey = detail.apiKey ?? ''
|
||||
editForm.status = detail.status
|
||||
editModal.visible = true
|
||||
}
|
||||
@@ -639,13 +633,6 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
|
||||
minWidth: 180,
|
||||
render: (row) => (row.roleCodes.length > 0 ? row.roleCodes.join(', ') : '-')
|
||||
},
|
||||
{
|
||||
title: 'API Key',
|
||||
key: 'apiKey',
|
||||
minWidth: 220,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => row.apiKey || '-'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
|
||||
@@ -275,11 +275,20 @@ async function handleLogout() {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.layout-sider :deep(.n-layout-sider-scroll-container) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-sider-hidden {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -321,9 +330,11 @@ async function handleLogout() {
|
||||
}
|
||||
|
||||
.menu-wrap {
|
||||
height: calc(100% - 64px);
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 10px 8px 14px;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
+15
-2
@@ -1,8 +1,14 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { loginApi, logoutApi, meApi } from '@/api/auth'
|
||||
import { loginApi, logoutApi, meApi, registerEnterpriseApi } from '@/api/auth'
|
||||
import { listDictItemsApi } from '@/api/system/dict'
|
||||
import type { CurrentUserProfile, LoginRequest, MeResponse, MenuNode } from '@/types/auth'
|
||||
import type {
|
||||
CurrentUserProfile,
|
||||
EnterpriseRegisterRequest,
|
||||
LoginRequest,
|
||||
MeResponse,
|
||||
MenuNode
|
||||
} from '@/types/auth'
|
||||
import type { DictItem } from '@/types/system/dict'
|
||||
|
||||
const TOKEN_KEY = 'platform.token'
|
||||
@@ -59,6 +65,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
await loadProfile()
|
||||
}
|
||||
|
||||
async function registerEnterprise(payload: EnterpriseRegisterRequest) {
|
||||
const result = await registerEnterpriseApi(payload)
|
||||
setToken(result.accessToken)
|
||||
await loadProfile()
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
if (token.value) {
|
||||
@@ -168,6 +180,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
setToken,
|
||||
clearAuth,
|
||||
login,
|
||||
registerEnterprise,
|
||||
logout,
|
||||
loadProfile,
|
||||
hasPermission,
|
||||
|
||||
@@ -100,6 +100,22 @@ select {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.page-toolbar-title {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.page-toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-form {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -220,4 +236,9 @@ select {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.page-toolbar-actions {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-4
@@ -9,21 +9,35 @@ export interface LoginResponse {
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
export interface EnterpriseRegisterRequest {
|
||||
taxpayerNum: string
|
||||
enterpriseName: string
|
||||
legalPersonName?: string | null
|
||||
contactsName?: string | null
|
||||
contactsEmail?: string | null
|
||||
contactsPhone: string
|
||||
regionCode: string
|
||||
cityName?: string
|
||||
enterpriseAddress?: string | null
|
||||
taxRegistrationCertificate?: string | null
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
export interface CurrentUserProfile {
|
||||
id: string
|
||||
username: string
|
||||
nickname?: string | null
|
||||
realName?: string | null
|
||||
orgId?: string | null
|
||||
enterpriseId?: string | null
|
||||
digitalAccountId?: string | null
|
||||
userType: 'SYSTEM' | 'ENTERPRISE_ADMIN' | 'DIGITAL_OPERATOR' | string
|
||||
status: string
|
||||
avatar?: string | null
|
||||
email?: string | null
|
||||
phone?: string | null
|
||||
createdAt?: string | null
|
||||
taxpayerNum?: string | null
|
||||
account?: string | null
|
||||
taxPassword?: string | null
|
||||
taxIdentityType?: string | null
|
||||
}
|
||||
|
||||
export interface MenuNode {
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface UserListItem {
|
||||
status: string
|
||||
statusLabel?: string
|
||||
roleCodes: string[]
|
||||
apiKey?: string | null
|
||||
}
|
||||
|
||||
export interface UserDetail {
|
||||
@@ -27,7 +26,6 @@ export interface UserDetail {
|
||||
statusLabel?: string
|
||||
roleIds: string[]
|
||||
roles: UserRoleBrief[]
|
||||
apiKey?: string | null
|
||||
tokenVersion: number
|
||||
lastLoginAt?: string | null
|
||||
lastLoginIp?: string | null
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<template #header>
|
||||
<div class="login-card-header">
|
||||
<div class="login-card-title">{{ appTitle }}</div>
|
||||
<div class="login-card-subtitle">登录后继续使用</div>
|
||||
<div class="login-card-subtitle">
|
||||
{{ isRegister ? '注册企业后直接进入系统' : '登录后继续使用' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,13 +14,20 @@
|
||||
未登录或登录已失效,请重新登录。
|
||||
</n-alert>
|
||||
|
||||
<n-form ref="formRef" :model="form" :rules="rules" size="large" @submit.prevent="onSubmit">
|
||||
<n-form
|
||||
v-if="!isRegister"
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
@submit.prevent="onLogin"
|
||||
>
|
||||
<n-form-item path="username" label="用户名">
|
||||
<n-input v-model:value="form.username" placeholder="请输入用户名" />
|
||||
<n-input v-model:value="loginForm.username" placeholder="请输入用户名" />
|
||||
</n-form-item>
|
||||
<n-form-item path="password" label="密码">
|
||||
<n-input
|
||||
v-model:value="form.password"
|
||||
v-model:value="loginForm.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请输入密码"
|
||||
@@ -28,6 +37,113 @@
|
||||
登录
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<n-form
|
||||
v-else
|
||||
ref="registerFormRef"
|
||||
:model="registerForm"
|
||||
:rules="registerRules"
|
||||
size="medium"
|
||||
label-placement="top"
|
||||
@submit.prevent="onRegister"
|
||||
>
|
||||
<n-grid responsive="screen" :cols="2" :x-gap="12">
|
||||
<n-gi>
|
||||
<n-form-item path="taxpayerNum" label="纳税人识别号">
|
||||
<n-input v-model:value="registerForm.taxpayerNum" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="enterpriseName" label="企业名称">
|
||||
<n-input v-model:value="registerForm.enterpriseName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="legalPersonName" label="法人姓名">
|
||||
<n-input v-model:value="registerForm.legalPersonName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="contactsName" label="联系人">
|
||||
<n-input v-model:value="registerForm.contactsName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="contactsPhone" label="联系人手机">
|
||||
<n-input v-model:value="registerForm.contactsPhone" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="contactsEmail" label="联系邮箱">
|
||||
<n-input v-model:value="registerForm.contactsEmail" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="regionCode" label="省份">
|
||||
<n-select
|
||||
v-model:value="registerForm.regionCode"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择省份"
|
||||
:options="regionCodeOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="cityName" label="城市">
|
||||
<n-input v-model:value="registerForm.cityName" clearable />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-form-item path="enterpriseAddress" label="企业地址">
|
||||
<n-input v-model:value="registerForm.enterpriseAddress" clearable />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="taxRegistrationCertificate" label="企业注册证书">
|
||||
<div class="cert-upload">
|
||||
<div class="cert-actions">
|
||||
<n-button secondary @click="triggerCertInput">选择图片</n-button>
|
||||
<span class="cert-state">
|
||||
{{ registerForm.taxRegistrationCertificate ? '已选择图片' : '未选择,可不上传' }}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
ref="certInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
@change="onCertFileChange"
|
||||
/>
|
||||
<img v-if="certPreview" class="cert-preview" :src="certPreview" alt="企业注册证书预览" />
|
||||
</div>
|
||||
</n-form-item>
|
||||
|
||||
<n-grid responsive="screen" :cols="2" :x-gap="12">
|
||||
<n-gi>
|
||||
<n-form-item path="password" label="登录密码">
|
||||
<n-input v-model:value="registerForm.password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item path="confirmPassword" label="确认密码">
|
||||
<n-input
|
||||
v-model:value="registerForm.confirmPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-button type="primary" block size="large" :loading="loading" attr-type="submit">
|
||||
注册并登录
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<n-button text block class="switch-btn" @click="toggleMode">
|
||||
{{ isRegister ? '已有账号,返回登录' : '注册企业' }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,7 +151,18 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGi,
|
||||
NGrid,
|
||||
NInput,
|
||||
NSelect,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { appEnv } from '@/config/env'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -47,25 +174,142 @@ const authStore = useAuthStore()
|
||||
const appTitle = appEnv.appTitle
|
||||
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
const isRegister = ref(false)
|
||||
const loginFormRef = ref<FormInst | null>(null)
|
||||
const registerFormRef = ref<FormInst | null>(null)
|
||||
const certInputRef = ref<HTMLInputElement | null>(null)
|
||||
const certPreview = ref('')
|
||||
|
||||
const loginForm = reactive({ username: '', password: '' })
|
||||
const registerForm = reactive({
|
||||
taxpayerNum: '',
|
||||
enterpriseName: '',
|
||||
legalPersonName: '',
|
||||
contactsName: '',
|
||||
contactsEmail: '',
|
||||
contactsPhone: '',
|
||||
regionCode: '',
|
||||
cityName: '',
|
||||
enterpriseAddress: '',
|
||||
taxRegistrationCertificate: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
const loginRules: FormRules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: ['blur', 'input'] }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: ['blur', 'input'] }]
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
await formRef.value?.validate()
|
||||
const registerRules: FormRules = {
|
||||
taxpayerNum: [{ required: true, message: '请输入纳税人识别号', trigger: ['blur', 'input'] }],
|
||||
enterpriseName: [{ required: true, message: '请输入企业名称', trigger: ['blur', 'input'] }],
|
||||
contactsPhone: [{ required: true, message: '请输入联系人手机', trigger: ['blur', 'input'] }],
|
||||
regionCode: [{ required: true, message: '请选择省份', trigger: ['blur', 'change'] }],
|
||||
password: [{ required: true, message: '请输入登录密码', trigger: ['blur', 'input'] }],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认登录密码', trigger: ['blur', 'input'] },
|
||||
{
|
||||
validator: () => registerForm.password === registerForm.confirmPassword,
|
||||
message: '两次输入的密码不一致',
|
||||
trigger: ['blur', 'input']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const regionCodeOptions = [
|
||||
{ label: '北京市', value: '11' },
|
||||
{ label: '上海市', value: '31' },
|
||||
{ label: '天津市', value: '12' },
|
||||
{ label: '河北省', value: '13' },
|
||||
{ label: '山西省', value: '14' },
|
||||
{ label: '内蒙古自治区', value: '15' },
|
||||
{ label: '辽宁省', value: '21' },
|
||||
{ label: '吉林省', value: '22' },
|
||||
{ label: '黑龙江省', value: '23' },
|
||||
{ label: '江苏省', value: '32' },
|
||||
{ label: '浙江省', value: '33' },
|
||||
{ label: '安徽省', value: '34' },
|
||||
{ label: '福建省', value: '35' },
|
||||
{ label: '江西省', value: '36' },
|
||||
{ label: '山东省', value: '37' },
|
||||
{ label: '河南省', value: '41' },
|
||||
{ label: '湖北省', value: '42' },
|
||||
{ label: '湖南省', value: '43' },
|
||||
{ label: '广东省', value: '44' },
|
||||
{ label: '广西壮族自治区', value: '45' },
|
||||
{ label: '海南省', value: '46' },
|
||||
{ label: '重庆市', value: '50' },
|
||||
{ label: '四川省', value: '51' },
|
||||
{ label: '贵州省', value: '52' },
|
||||
{ label: '云南省', value: '53' },
|
||||
{ label: '西藏自治区', value: '54' },
|
||||
{ label: '陕西省', value: '61' },
|
||||
{ label: '甘肃省', value: '62' },
|
||||
{ label: '青海省', value: '63' },
|
||||
{ label: '宁夏回族自治区', value: '64' },
|
||||
{ label: '新疆维吾尔自治区', value: '65' }
|
||||
]
|
||||
|
||||
function toggleMode() {
|
||||
isRegister.value = !isRegister.value
|
||||
}
|
||||
|
||||
function triggerCertInput() {
|
||||
certInputRef.value?.click()
|
||||
}
|
||||
|
||||
function onCertFileChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const base64 = String(reader.result || '')
|
||||
registerForm.taxRegistrationCertificate = base64
|
||||
certPreview.value = base64
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
target.value = ''
|
||||
}
|
||||
|
||||
async function enterSystem() {
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
|
||||
await router.replace(redirect)
|
||||
}
|
||||
|
||||
async function onLogin() {
|
||||
await loginFormRef.value?.validate()
|
||||
loading.value = true
|
||||
try {
|
||||
await authStore.login(form)
|
||||
await authStore.login(loginForm)
|
||||
message.success('登录成功')
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
|
||||
await router.replace(redirect)
|
||||
await enterSystem()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRegister() {
|
||||
await registerFormRef.value?.validate()
|
||||
loading.value = true
|
||||
try {
|
||||
await authStore.registerEnterprise({
|
||||
taxpayerNum: registerForm.taxpayerNum.trim(),
|
||||
enterpriseName: registerForm.enterpriseName.trim(),
|
||||
legalPersonName: registerForm.legalPersonName.trim() || null,
|
||||
contactsName: registerForm.contactsName.trim() || null,
|
||||
contactsEmail: registerForm.contactsEmail.trim() || null,
|
||||
contactsPhone: registerForm.contactsPhone.trim(),
|
||||
regionCode: registerForm.regionCode,
|
||||
cityName: registerForm.cityName.trim(),
|
||||
enterpriseAddress: registerForm.enterpriseAddress.trim() || null,
|
||||
taxRegistrationCertificate: registerForm.taxRegistrationCertificate || null,
|
||||
password: registerForm.password,
|
||||
confirmPassword: registerForm.confirmPassword
|
||||
})
|
||||
message.success('注册成功')
|
||||
await enterSystem()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -74,21 +318,25 @@ async function onSubmit() {
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
padding: 32px 24px;
|
||||
overflow-y: auto;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border-radius: 16px;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.login-card :deep(.n-card__content) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -109,4 +357,52 @@ async function onSubmit() {
|
||||
.login-alert {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.cert-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cert-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cert-state {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cert-preview {
|
||||
display: block;
|
||||
max-width: 220px;
|
||||
max-height: 132px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-page {
|
||||
padding: 18px 14px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user