大改三角色逻辑:超级管理员、企业管理员、开票员
This commit is contained in:
@@ -8,7 +8,6 @@ import com.bbit.ticket.route.system.registerAuthRoutes
|
||||
import com.bbit.ticket.utils.plugins.configureCors
|
||||
import com.bbit.ticket.utils.plugins.configureDatabase
|
||||
import com.bbit.ticket.utils.plugins.configureLogging
|
||||
import com.bbit.ticket.utils.plugins.configureApiAccessLog
|
||||
import com.bbit.ticket.utils.plugins.configureRedis
|
||||
import com.bbit.ticket.utils.plugins.configureSecurity
|
||||
import com.bbit.ticket.utils.plugins.configureSerialization
|
||||
@@ -19,10 +18,9 @@ import com.bbit.ticket.route.piaotong.registerPTInvoiceRoutes
|
||||
import com.bbit.ticket.route.openapi.registerOpenBlueInvoiceRoutes
|
||||
import com.bbit.ticket.route.openapi.registerOpenInvoiceTaskRoutes
|
||||
import com.bbit.ticket.route.piaotong.registerOpenInvoiceTaskManageRoutes
|
||||
import com.bbit.ticket.route.system.registerDictRoutes
|
||||
import com.bbit.ticket.route.system.registerEnterpriseManageRoutes
|
||||
import com.bbit.ticket.route.system.registerLogsQueryRoutes
|
||||
import com.bbit.ticket.route.system.registerMenuRoutes
|
||||
import com.bbit.ticket.route.system.registerOrgRoutes
|
||||
import com.bbit.ticket.route.system.registerRoleRoutes
|
||||
import com.bbit.ticket.route.system.registerUserRoutes
|
||||
import com.bbit.ticket.service.openapi.OpenInvoiceTaskWorker
|
||||
@@ -46,7 +44,6 @@ fun Application.module() {
|
||||
configureSerialization()
|
||||
configureStatusPages()
|
||||
configureLogging()
|
||||
configureApiAccessLog()
|
||||
configureCors()
|
||||
configureSecurity()
|
||||
configureDatabase()
|
||||
@@ -64,10 +61,9 @@ fun Application.module() {
|
||||
route("/api") {
|
||||
registerAuthRoutes()
|
||||
registerUserRoutes()
|
||||
registerOrgRoutes()
|
||||
registerEnterpriseManageRoutes()
|
||||
registerRoleRoutes()
|
||||
registerMenuRoutes()
|
||||
registerDictRoutes()
|
||||
registerLogsQueryRoutes()
|
||||
route("/open/v1") {
|
||||
route("/blue-invoices") {
|
||||
|
||||
@@ -6,18 +6,23 @@ 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.common.PageResult
|
||||
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.EnterpriseManageListItem
|
||||
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.Op
|
||||
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.core.like
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
@@ -26,6 +31,25 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
object EnterpriseManageDao {
|
||||
fun list(keyword: String?, page: Int, pageSize: Int): PageResult<EnterpriseManageListItem> {
|
||||
var where: Op<Boolean> = PtEnterpriseTable.deletedAt.isNull()
|
||||
if (!keyword.isNullOrBlank()) {
|
||||
val pattern = "%${keyword.trim()}%"
|
||||
where = where and (
|
||||
(PtEnterpriseTable.taxpayerNum like pattern) or
|
||||
(PtEnterpriseTable.enterpriseName like pattern)
|
||||
)
|
||||
}
|
||||
val total = PtEnterpriseTable.selectAll().where { where }.count()
|
||||
val rows = PtEnterpriseTable.selectAll()
|
||||
.where { where }
|
||||
.orderBy(PtEnterpriseTable.createdAt, SortOrder.DESC)
|
||||
.limit(pageSize)
|
||||
.offset(pageOffset(page, pageSize))
|
||||
.toList()
|
||||
return PageResult(rows.map { it.toEnterpriseManageListItem() }, page, pageSize, total)
|
||||
}
|
||||
|
||||
fun findEnterpriseByTaxpayerNum(taxpayerNum: String): ResultRow? =
|
||||
PtEnterpriseTable.selectAll()
|
||||
.where { (PtEnterpriseTable.taxpayerNum eq taxpayerNum) and PtEnterpriseTable.deletedAt.isNull() }
|
||||
@@ -280,6 +304,26 @@ object EnterpriseManageDao {
|
||||
presetPhone = this[PtEnterpriseTable.presetPhone],
|
||||
)
|
||||
|
||||
private fun ResultRow.toEnterpriseManageListItem() = EnterpriseManageListItem(
|
||||
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],
|
||||
reviewStatus = this[PtEnterpriseTable.reviewStatus],
|
||||
reviewOpinion = this[PtEnterpriseTable.reviewOpinion],
|
||||
invoiceKind = this[PtEnterpriseTable.invoiceKind],
|
||||
invoiceLayoutFileType = this[PtEnterpriseTable.invoiceLayoutFileType],
|
||||
serviceStatus = this[PtEnterpriseTable.serviceStatus],
|
||||
createdAt = this[PtEnterpriseTable.createdAt].toString(),
|
||||
updatedAt = this[PtEnterpriseTable.updatedAt]?.toString(),
|
||||
)
|
||||
|
||||
fun ResultRow.toDigitalAccountItem() = DigitalAccountManageItem(
|
||||
id = this[PtDigitalAccountTable.id].toString(),
|
||||
enterpriseId = this[PtDigitalAccountTable.enterpriseId].toString(),
|
||||
@@ -306,4 +350,6 @@ object EnterpriseManageDao {
|
||||
apiKey = this[PtDigitalAccountTable.apiKey],
|
||||
status = this[PtDigitalAccountTable.status],
|
||||
)
|
||||
|
||||
private fun pageOffset(page: Int, pageSize: Int): Long = ((page - 1) * pageSize).toLong()
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.dao.system
|
||||
|
||||
import com.bbit.ticket.database.system.SysDictItemTable
|
||||
import com.bbit.ticket.database.system.SysDictTypeTable
|
||||
import com.bbit.ticket.entity.common.system.CreateDictItemRequest
|
||||
import com.bbit.ticket.entity.common.system.CreateDictTypeRequest
|
||||
import com.bbit.ticket.entity.common.system.DictItem
|
||||
import com.bbit.ticket.entity.common.system.DictTypeItem
|
||||
import com.bbit.ticket.entity.common.system.UpdateDictItemRequest
|
||||
import com.bbit.ticket.entity.common.system.UpdateDictTypeRequest
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.PageResult
|
||||
import com.bbit.ticket.entity.common.statusLabel
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
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.core.isNull
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
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 DictDao {
|
||||
fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult<DictTypeItem> {
|
||||
val where = buildTypeWhere(keyword)
|
||||
val total = SysDictTypeTable.selectAll().where { where }.count()
|
||||
val rows = SysDictTypeTable.selectAll().where { where }
|
||||
.orderBy(SysDictTypeTable.createdAt)
|
||||
.limit(pageSize)
|
||||
.offset(pageOffset(page, pageSize))
|
||||
.toList()
|
||||
return PageResult(
|
||||
items = rows.map { it.toDictTypeItem() },
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
total = total,
|
||||
)
|
||||
}
|
||||
|
||||
fun typeCodeExists(code: String): Boolean =
|
||||
SysDictTypeTable.selectAll().where {
|
||||
(SysDictTypeTable.code eq code) and SysDictTypeTable.deletedAt.isNull()
|
||||
}.any()
|
||||
|
||||
fun createType(request: CreateDictTypeRequest): String {
|
||||
val inserted = SysDictTypeTable.insert {
|
||||
it[SysDictTypeTable.code] = request.code.trim()
|
||||
it[SysDictTypeTable.name] = request.name.trim()
|
||||
it[SysDictTypeTable.status] = request.status
|
||||
it[SysDictTypeTable.remark] = request.remark.trimToNull()
|
||||
it[SysDictTypeTable.createdAt] = OffsetDateTime.now()
|
||||
}
|
||||
return inserted[SysDictTypeTable.id].toString()
|
||||
}
|
||||
|
||||
fun updateType(id: Uuid, request: UpdateDictTypeRequest) {
|
||||
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
|
||||
it[SysDictTypeTable.name] = request.name.trim()
|
||||
it[SysDictTypeTable.status] = request.status
|
||||
it[SysDictTypeTable.remark] = request.remark.trimToNull()
|
||||
it[SysDictTypeTable.updatedAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
fun requireType(id: Uuid): ResultRow =
|
||||
SysDictTypeTable.selectAll().where {
|
||||
(SysDictTypeTable.id eq id) and SysDictTypeTable.deletedAt.isNull()
|
||||
}.singleOrNull() ?: throw BizException(
|
||||
ErrorCode.DICT_TYPE_NOT_FOUND.code,
|
||||
ErrorCode.DICT_TYPE_NOT_FOUND.message,
|
||||
HttpStatusCode.NotFound,
|
||||
)
|
||||
|
||||
fun typeHasItems(id: Uuid): Boolean =
|
||||
SysDictItemTable.selectAll().where {
|
||||
(SysDictItemTable.typeId eq id) and SysDictItemTable.deletedAt.isNull()
|
||||
}.any()
|
||||
|
||||
fun softDeleteType(id: Uuid) {
|
||||
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
|
||||
it[SysDictTypeTable.deletedAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult<DictItem> {
|
||||
var where: Op<Boolean> = SysDictItemTable.deletedAt.isNull()
|
||||
if (typeId != null) where = where and (SysDictItemTable.typeId eq typeId)
|
||||
val total = SysDictItemTable.selectAll().where { where }.count()
|
||||
val rows = SysDictItemTable.selectAll().where { where }
|
||||
.orderBy(SysDictItemTable.sort)
|
||||
.limit(pageSize)
|
||||
.offset(pageOffset(page, pageSize))
|
||||
.toList()
|
||||
return PageResult(
|
||||
items = rows.map { it.toDictItem() },
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
total = total,
|
||||
)
|
||||
}
|
||||
|
||||
fun createItem(request: CreateDictItemRequest, typeId: Uuid): String {
|
||||
val inserted = SysDictItemTable.insert {
|
||||
it[SysDictItemTable.typeId] = typeId
|
||||
it[SysDictItemTable.label] = request.label.trim()
|
||||
it[SysDictItemTable.value] = request.value.trim()
|
||||
it[SysDictItemTable.color] = request.color.trimToNull()
|
||||
it[SysDictItemTable.sort] = request.sort
|
||||
it[SysDictItemTable.status] = request.status
|
||||
it[SysDictItemTable.remark] = request.remark.trimToNull()
|
||||
it[SysDictItemTable.createdAt] = OffsetDateTime.now()
|
||||
}
|
||||
return inserted[SysDictItemTable.id].toString()
|
||||
}
|
||||
|
||||
fun requireItem(id: Uuid): ResultRow =
|
||||
SysDictItemTable.selectAll().where {
|
||||
(SysDictItemTable.id eq id) and SysDictItemTable.deletedAt.isNull()
|
||||
}.singleOrNull() ?: throw BizException(
|
||||
ErrorCode.DICT_ITEM_NOT_FOUND.code,
|
||||
ErrorCode.DICT_ITEM_NOT_FOUND.message,
|
||||
HttpStatusCode.NotFound,
|
||||
)
|
||||
|
||||
fun updateItem(id: Uuid, request: UpdateDictItemRequest, typeId: Uuid) {
|
||||
SysDictItemTable.update({ SysDictItemTable.id eq id }) {
|
||||
it[SysDictItemTable.typeId] = typeId
|
||||
it[SysDictItemTable.label] = request.label.trim()
|
||||
it[SysDictItemTable.value] = request.value.trim()
|
||||
it[SysDictItemTable.color] = request.color.trimToNull()
|
||||
it[SysDictItemTable.sort] = request.sort
|
||||
it[SysDictItemTable.status] = request.status
|
||||
it[SysDictItemTable.remark] = request.remark.trimToNull()
|
||||
it[SysDictItemTable.updatedAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
fun softDeleteItem(id: Uuid) {
|
||||
SysDictItemTable.update({ SysDictItemTable.id eq id }) {
|
||||
it[SysDictItemTable.deletedAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildTypeWhere(keyword: String?): Op<Boolean> {
|
||||
var where: Op<Boolean> = SysDictTypeTable.deletedAt.isNull()
|
||||
if (!keyword.isNullOrBlank()) {
|
||||
where = where and ((SysDictTypeTable.code like "%$keyword%") or (SysDictTypeTable.name like "%$keyword%"))
|
||||
}
|
||||
return where
|
||||
}
|
||||
|
||||
private fun ResultRow.toDictTypeItem() = DictTypeItem(
|
||||
id = this[SysDictTypeTable.id].toString(),
|
||||
code = this[SysDictTypeTable.code],
|
||||
name = this[SysDictTypeTable.name],
|
||||
status = this[SysDictTypeTable.status],
|
||||
statusLabel = statusLabel(this[SysDictTypeTable.status]),
|
||||
remark = this[SysDictTypeTable.remark],
|
||||
)
|
||||
|
||||
private fun ResultRow.toDictItem() = DictItem(
|
||||
id = this[SysDictItemTable.id].toString(),
|
||||
typeId = this[SysDictItemTable.typeId].toString(),
|
||||
label = this[SysDictItemTable.label],
|
||||
value = this[SysDictItemTable.value],
|
||||
color = this[SysDictItemTable.color],
|
||||
sort = this[SysDictItemTable.sort],
|
||||
status = this[SysDictItemTable.status],
|
||||
statusLabel = statusLabel(this[SysDictItemTable.status]),
|
||||
remark = this[SysDictItemTable.remark],
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,6 @@ package com.bbit.ticket.dao.system
|
||||
|
||||
import com.bbit.ticket.database.system.SysApiAccessLogTable
|
||||
import com.bbit.ticket.database.system.SysOperationLogTable
|
||||
import com.bbit.ticket.entity.common.system.ApiAccessLogItem
|
||||
import com.bbit.ticket.entity.common.system.OperationLogItem
|
||||
import com.bbit.ticket.entity.common.PageResult
|
||||
import com.bbit.ticket.utils.CurrentUser
|
||||
@@ -65,42 +64,6 @@ object LogDao {
|
||||
)
|
||||
}
|
||||
|
||||
fun apiAccessLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<ApiAccessLogItem> {
|
||||
var where: Op<Boolean> = Op.TRUE
|
||||
if (!keyword.isNullOrBlank()) {
|
||||
where = where and ((SysApiAccessLogTable.appName like "%$keyword%") or (SysApiAccessLogTable.requestPath like "%$keyword%"))
|
||||
}
|
||||
if (!status.isNullOrBlank()) {
|
||||
where = where and (SysApiAccessLogTable.status eq status)
|
||||
}
|
||||
val total = SysApiAccessLogTable.selectAll().where { where }.count()
|
||||
val rows = SysApiAccessLogTable.selectAll().where { where }
|
||||
.orderBy(SysApiAccessLogTable.createdAt, SortOrder.DESC)
|
||||
.limit(pageSize)
|
||||
.offset(pageOffset(page, pageSize))
|
||||
.toList()
|
||||
return PageResult(
|
||||
items = rows.map { row ->
|
||||
ApiAccessLogItem(
|
||||
id = row[SysApiAccessLogTable.id].toString(),
|
||||
traceId = row[SysApiAccessLogTable.traceId],
|
||||
appKey = row[SysApiAccessLogTable.appKey],
|
||||
appName = row[SysApiAccessLogTable.appName],
|
||||
httpMethod = row[SysApiAccessLogTable.httpMethod],
|
||||
requestPath = row[SysApiAccessLogTable.requestPath],
|
||||
responseCode = row[SysApiAccessLogTable.responseCode],
|
||||
status = row[SysApiAccessLogTable.status],
|
||||
errorMessage = row[SysApiAccessLogTable.errorMessage],
|
||||
costMs = row[SysApiAccessLogTable.costMs],
|
||||
createdAt = formatDateTime(row[SysApiAccessLogTable.createdAt]) ?: "",
|
||||
)
|
||||
},
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
total = total,
|
||||
)
|
||||
}
|
||||
|
||||
fun saveOperationLog(
|
||||
call: ApplicationCall,
|
||||
currentUser: CurrentUser?,
|
||||
@@ -114,7 +77,7 @@ object LogDao {
|
||||
it[traceId] = call.traceIdOrNull()
|
||||
it[userId] = currentUser?.id
|
||||
it[SysOperationLogTable.username] = currentUser?.username
|
||||
it[orgId] = currentUser?.orgId
|
||||
it[enterpriseId] = currentUser?.enterpriseId
|
||||
it[SysOperationLogTable.operationType] = operationType
|
||||
it[SysOperationLogTable.operationName] = operationName
|
||||
it[httpMethod] = call.request.httpMethod.value
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.dao.system
|
||||
|
||||
import com.bbit.ticket.database.system.SysOrgTable
|
||||
import com.bbit.ticket.database.system.SysUserTable
|
||||
import com.bbit.ticket.entity.common.system.CreateOrgRequest
|
||||
import com.bbit.ticket.entity.common.system.OrgTreeNode
|
||||
import com.bbit.ticket.entity.common.system.UpdateOrgRequest
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.statusLabel
|
||||
import io.ktor.http.HttpStatusCode
|
||||
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.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 OrgDao {
|
||||
fun tree(): List<OrgTreeNode> {
|
||||
val rows = SysOrgTable.selectAll()
|
||||
.where { SysOrgTable.deletedAt.isNull() }
|
||||
.orderBy(SysOrgTable.sort)
|
||||
.toList()
|
||||
val nodes = rows.map(::toFlatNode)
|
||||
return buildTree(nodes)
|
||||
}
|
||||
|
||||
fun codeExists(code: String): Boolean =
|
||||
SysOrgTable.selectAll().where {
|
||||
(SysOrgTable.code eq code) and SysOrgTable.deletedAt.isNull()
|
||||
}.any()
|
||||
|
||||
fun requireActive(id: Uuid): ResultRow =
|
||||
SysOrgTable.selectAll().where {
|
||||
(SysOrgTable.id eq id) and SysOrgTable.deletedAt.isNull()
|
||||
}.singleOrNull() ?: throw BizException(
|
||||
ErrorCode.ORG_NOT_FOUND.code,
|
||||
ErrorCode.ORG_NOT_FOUND.message,
|
||||
HttpStatusCode.NotFound,
|
||||
)
|
||||
|
||||
fun create(request: CreateOrgRequest, parentId: Uuid?): String {
|
||||
val inserted = SysOrgTable.insert {
|
||||
it[SysOrgTable.parentId] = parentId
|
||||
it[SysOrgTable.name] = request.name.trim()
|
||||
it[SysOrgTable.code] = request.code.trim()
|
||||
it[SysOrgTable.sort] = request.sort
|
||||
it[SysOrgTable.status] = request.status
|
||||
it[SysOrgTable.createdAt] = OffsetDateTime.now()
|
||||
}
|
||||
return inserted[SysOrgTable.id].toString()
|
||||
}
|
||||
|
||||
fun update(id: Uuid, request: UpdateOrgRequest, parentId: Uuid?) {
|
||||
SysOrgTable.update({ SysOrgTable.id eq id }) {
|
||||
it[SysOrgTable.parentId] = parentId
|
||||
it[SysOrgTable.name] = request.name.trim()
|
||||
it[SysOrgTable.sort] = request.sort
|
||||
it[SysOrgTable.status] = request.status
|
||||
it[SysOrgTable.updatedAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
fun hasChildren(id: Uuid): Boolean =
|
||||
SysOrgTable.selectAll().where {
|
||||
(SysOrgTable.parentId eq id) and SysOrgTable.deletedAt.isNull()
|
||||
}.any()
|
||||
|
||||
fun hasUsers(id: Uuid): Boolean =
|
||||
SysUserTable.selectAll().where {
|
||||
(SysUserTable.orgId eq id) and SysUserTable.deletedAt.isNull()
|
||||
}.any()
|
||||
|
||||
fun softDelete(id: Uuid) {
|
||||
SysOrgTable.update({ SysOrgTable.id eq id }) {
|
||||
it[SysOrgTable.deletedAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toFlatNode(row: ResultRow) = OrgNodeFlat(
|
||||
id = row[SysOrgTable.id],
|
||||
parentId = row[SysOrgTable.parentId],
|
||||
name = row[SysOrgTable.name],
|
||||
code = row[SysOrgTable.code],
|
||||
sort = row[SysOrgTable.sort],
|
||||
status = row[SysOrgTable.status],
|
||||
)
|
||||
|
||||
private fun buildTree(nodes: List<OrgNodeFlat>): List<OrgTreeNode> {
|
||||
val byParent = nodes.groupBy { it.parentId }
|
||||
fun children(parentId: Uuid?): List<OrgTreeNode> =
|
||||
(byParent[parentId] ?: emptyList()).sortedBy { it.sort }.map { item ->
|
||||
OrgTreeNode(
|
||||
id = item.id.toString(),
|
||||
parentId = item.parentId?.toString(),
|
||||
name = item.name,
|
||||
code = item.code,
|
||||
sort = item.sort,
|
||||
status = item.status,
|
||||
statusLabel = statusLabel(item.status),
|
||||
children = children(item.id),
|
||||
)
|
||||
}
|
||||
return children(null)
|
||||
}
|
||||
|
||||
private data class OrgNodeFlat(
|
||||
val id: Uuid,
|
||||
val parentId: Uuid?,
|
||||
val name: String,
|
||||
val code: String,
|
||||
val sort: Int,
|
||||
val status: String,
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.bbit.ticket.dao.system
|
||||
|
||||
import com.bbit.ticket.database.system.SysOrgTable
|
||||
import com.bbit.ticket.database.piaotong.PtEnterpriseTable
|
||||
import com.bbit.ticket.database.system.SysRoleTable
|
||||
import com.bbit.ticket.database.system.SysUserRoleTable
|
||||
import com.bbit.ticket.database.system.SysUserTable
|
||||
@@ -38,9 +38,9 @@ object UserDao {
|
||||
username: String?,
|
||||
nickname: String?,
|
||||
status: String?,
|
||||
orgId: Uuid?,
|
||||
enterpriseId: Uuid?,
|
||||
): PageResult<UserListItem> {
|
||||
val where = buildWhere(username, nickname, status, orgId)
|
||||
val where = buildWhere(username, nickname, status, enterpriseId)
|
||||
val total = SysUserTable.selectAll().where { where }.count()
|
||||
val rows = SysUserTable.selectAll()
|
||||
.where { where }
|
||||
@@ -51,7 +51,12 @@ object UserDao {
|
||||
|
||||
val roleMap = findRoleCodesByUserIds(rows.map { it[SysUserTable.id] })
|
||||
return PageResult(
|
||||
items = rows.map { row -> row.toUserListItem(roleMap[row[SysUserTable.id]] ?: emptyList()) },
|
||||
items = rows.map { row ->
|
||||
row.toUserListItem(
|
||||
roleCodes = roleMap[row[SysUserTable.id]] ?: emptyList(),
|
||||
enterprise = row[SysUserTable.enterpriseId]?.let(::findEnterprise),
|
||||
)
|
||||
},
|
||||
page = page,
|
||||
pageSize = pageSize,
|
||||
total = total,
|
||||
@@ -69,7 +74,7 @@ object UserDao {
|
||||
HttpStatusCode.NotFound,
|
||||
)
|
||||
|
||||
fun create(request: CreateUserRequest, passwordHash: String, orgId: Uuid?): String {
|
||||
fun create(request: CreateUserRequest, passwordHash: String, enterpriseId: Uuid?): String {
|
||||
val now = OffsetDateTime.now()
|
||||
val row = SysUserTable.insert {
|
||||
it[SysUserTable.username] = request.username.trim()
|
||||
@@ -79,7 +84,7 @@ object UserDao {
|
||||
it[SysUserTable.phone] = request.phone.trimToNull()
|
||||
it[SysUserTable.email] = request.email.trimToNull()
|
||||
it[SysUserTable.avatar] = request.avatar.trimToNull()
|
||||
it[SysUserTable.orgId] = orgId
|
||||
it[SysUserTable.enterpriseId] = enterpriseId
|
||||
it[SysUserTable.status] = request.status
|
||||
it[SysUserTable.tokenVersion] = 1
|
||||
it[SysUserTable.createdAt] = now
|
||||
@@ -130,22 +135,17 @@ object UserDao {
|
||||
)
|
||||
}
|
||||
val roleIds = roles.map { it.id }
|
||||
val org = user[SysUserTable.orgId]?.let { orgId ->
|
||||
SysOrgTable.selectAll()
|
||||
.where { (SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull() }
|
||||
.singleOrNull()
|
||||
}
|
||||
return user.toUserDetail(roleIds, roles, org)
|
||||
return user.toUserDetail(roleIds, roles, user[SysUserTable.enterpriseId]?.let(::findEnterprise))
|
||||
}
|
||||
|
||||
fun updateProfile(id: Uuid, request: UpdateUserRequest, orgId: Uuid?) {
|
||||
fun updateProfile(id: Uuid, request: UpdateUserRequest, enterpriseId: Uuid?) {
|
||||
SysUserTable.update({ SysUserTable.id eq id }) {
|
||||
it[SysUserTable.nickname] = request.nickname.trimToNull()
|
||||
it[SysUserTable.realName] = request.realName.trimToNull()
|
||||
it[SysUserTable.phone] = request.phone.trimToNull()
|
||||
it[SysUserTable.email] = request.email.trimToNull()
|
||||
it[SysUserTable.avatar] = request.avatar.trimToNull()
|
||||
it[SysUserTable.orgId] = orgId
|
||||
it[SysUserTable.enterpriseId] = enterpriseId
|
||||
it[SysUserTable.updatedAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
@@ -196,9 +196,9 @@ object UserDao {
|
||||
SysRoleTable.deletedAt.isNull()
|
||||
}.singleOrNull()?.get(SysRoleTable.id)
|
||||
|
||||
fun orgExists(orgId: Uuid): Boolean =
|
||||
SysOrgTable.selectAll().where {
|
||||
(SysOrgTable.id eq orgId) and SysOrgTable.deletedAt.isNull()
|
||||
fun enterpriseExists(enterpriseId: Uuid): Boolean =
|
||||
PtEnterpriseTable.selectAll().where {
|
||||
(PtEnterpriseTable.id eq enterpriseId) and PtEnterpriseTable.deletedAt.isNull()
|
||||
}.any()
|
||||
|
||||
fun findEnabledRoleCodes(userId: Uuid): List<String> =
|
||||
@@ -231,7 +231,7 @@ object UserDao {
|
||||
|
||||
private fun activeWhere(): Op<Boolean> = SysUserTable.deletedAt.isNull()
|
||||
|
||||
private fun buildWhere(username: String?, nickname: String?, status: String?, orgId: Uuid?): Op<Boolean> {
|
||||
private fun buildWhere(username: String?, nickname: String?, status: String?, enterpriseId: Uuid?): Op<Boolean> {
|
||||
var where: Op<Boolean> = activeWhere()
|
||||
if (!username.isNullOrBlank()) {
|
||||
where = where and (SysUserTable.username like "%$username%")
|
||||
@@ -242,12 +242,17 @@ object UserDao {
|
||||
if (!status.isNullOrBlank()) {
|
||||
where = where and (SysUserTable.status eq status)
|
||||
}
|
||||
if (orgId != null) {
|
||||
where = where and (SysUserTable.orgId eq orgId)
|
||||
if (enterpriseId != null) {
|
||||
where = where and (SysUserTable.enterpriseId eq enterpriseId)
|
||||
}
|
||||
return where
|
||||
}
|
||||
|
||||
private fun findEnterprise(enterpriseId: Uuid): ResultRow? =
|
||||
PtEnterpriseTable.selectAll()
|
||||
.where { (PtEnterpriseTable.id eq enterpriseId) and PtEnterpriseTable.deletedAt.isNull() }
|
||||
.singleOrNull()
|
||||
|
||||
private fun findRoleCodesByUserIds(userIds: List<Uuid>): Map<Uuid, List<String>> {
|
||||
if (userIds.isEmpty()) return emptyMap()
|
||||
return (SysUserRoleTable innerJoin SysRoleTable).selectAll()
|
||||
@@ -259,12 +264,14 @@ object UserDao {
|
||||
.mapValues { entry -> entry.value.map { row -> row[SysRoleTable.code] }.distinct() }
|
||||
}
|
||||
|
||||
private fun ResultRow.toUserListItem(roleCodes: List<String>) = UserListItem(
|
||||
private fun ResultRow.toUserListItem(roleCodes: List<String>, enterprise: ResultRow?) = UserListItem(
|
||||
id = this[SysUserTable.id].toString(),
|
||||
username = this[SysUserTable.username],
|
||||
nickname = this[SysUserTable.nickname],
|
||||
realName = this[SysUserTable.realName],
|
||||
orgId = this[SysUserTable.orgId]?.toString(),
|
||||
enterpriseId = this[SysUserTable.enterpriseId]?.toString(),
|
||||
enterpriseName = enterprise?.get(PtEnterpriseTable.enterpriseName),
|
||||
taxpayerNum = enterprise?.get(PtEnterpriseTable.taxpayerNum),
|
||||
status = this[SysUserTable.status],
|
||||
statusLabel = statusLabel(this[SysUserTable.status]),
|
||||
roleCodes = roleCodes,
|
||||
@@ -273,7 +280,7 @@ object UserDao {
|
||||
private fun ResultRow.toUserDetail(
|
||||
roleIds: List<String>,
|
||||
roles: List<UserRoleBrief>,
|
||||
org: ResultRow?,
|
||||
enterprise: ResultRow?,
|
||||
) = UserDetailResponse(
|
||||
id = this[SysUserTable.id].toString(),
|
||||
username = this[SysUserTable.username],
|
||||
@@ -282,9 +289,9 @@ object UserDao {
|
||||
phone = this[SysUserTable.phone],
|
||||
email = this[SysUserTable.email],
|
||||
avatar = this[SysUserTable.avatar],
|
||||
orgId = this[SysUserTable.orgId]?.toString(),
|
||||
orgName = org?.get(SysOrgTable.name),
|
||||
orgCode = org?.get(SysOrgTable.code),
|
||||
enterpriseId = this[SysUserTable.enterpriseId]?.toString(),
|
||||
enterpriseName = enterprise?.get(PtEnterpriseTable.enterpriseName),
|
||||
taxpayerNum = enterprise?.get(PtEnterpriseTable.taxpayerNum),
|
||||
status = this[SysUserTable.status],
|
||||
statusLabel = statusLabel(this[SysUserTable.status]),
|
||||
roleIds = roleIds,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.bbit.ticket.database.system
|
||||
|
||||
|
||||
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 SysDictItemTable : Table("sys_dict_item") {
|
||||
val id = uuid("id").clientDefault { Uuid.random() }
|
||||
val typeId = uuid("type_id").references(SysDictTypeTable.id)
|
||||
val label = varchar("label", 100)
|
||||
val value = varchar("value", 100)
|
||||
val color = varchar("color", 30).nullable()
|
||||
val sort = integer("sort").default(0)
|
||||
val status = varchar("status", 20).default("ENABLED")
|
||||
val remark = varchar("remark", 255).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)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.bbit.ticket.database.system
|
||||
|
||||
|
||||
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 SysDictTypeTable : Table("sys_dict_type") {
|
||||
val id = uuid("id").clientDefault { Uuid.random() }
|
||||
val code = varchar("code", 80).uniqueIndex()
|
||||
val name = varchar("name", 100)
|
||||
val status = varchar("status", 20).default("ENABLED")
|
||||
val remark = varchar("remark", 255).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)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ object SysOperationLogTable : Table("sys_operation_log") {
|
||||
val traceId = varchar("trace_id", 64).nullable()
|
||||
val userId = uuid("user_id").nullable()
|
||||
val username = varchar("username", 50).nullable()
|
||||
val orgId = uuid("org_id").nullable()
|
||||
val enterpriseId = uuid("enterprise_id").nullable()
|
||||
val operationType = varchar("operation_type", 50)
|
||||
val operationName = varchar("operation_name", 100)
|
||||
val httpMethod = varchar("http_method", 20)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.bbit.ticket.database.system
|
||||
|
||||
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 SysOrgTable : Table("sys_org") {
|
||||
val id = uuid("id").clientDefault { Uuid.random() }
|
||||
val parentId = uuid("parent_id").nullable()
|
||||
val name = varchar("name", 100)
|
||||
val code = varchar("code", 50).uniqueIndex()
|
||||
val sort = integer("sort").default(0)
|
||||
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)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ object SysUserTable : Table("sys_user") {
|
||||
val nickname = varchar("nickname", 50).nullable()
|
||||
val email = varchar("email", 100).nullable()
|
||||
val avatar = text("avatar").nullable()
|
||||
val orgId = uuid("org_id").nullable()
|
||||
val status = varchar("status", 20).default("ENABLED")
|
||||
val enterpriseId = uuid("enterprise_id").nullable()
|
||||
val digitalAccountId = uuid("digital_account_id").nullable()
|
||||
|
||||
@@ -17,8 +17,8 @@ fun menuTypeLabel(type: String): String = when (type) {
|
||||
|
||||
fun dataScopeLabel(scope: String): String = when (scope) {
|
||||
"ALL" -> "全部数据"
|
||||
"DEPT" -> "本组织及下级"
|
||||
"DEPT_ONLY" -> "本组织"
|
||||
"DEPT" -> "本企业"
|
||||
"DEPT_ONLY" -> "本企业"
|
||||
"SELF" -> "仅本人"
|
||||
else -> scope
|
||||
}
|
||||
|
||||
@@ -8,11 +8,8 @@ enum class ErrorCode(val code: String, val message: String) {
|
||||
USERNAME_OR_PASSWORD_INVALID("AUTH.USERNAME_OR_PASSWORD_INVALID", "用户名或密码错误"),
|
||||
USER_DISABLED("AUTH.USER_DISABLED", "用户已禁用"),
|
||||
USER_NOT_FOUND("SYSTEM.USER_NOT_FOUND", "用户不存在"),
|
||||
ORG_NOT_FOUND("SYSTEM.ORG_NOT_FOUND", "组织不存在"),
|
||||
ROLE_NOT_FOUND("SYSTEM.ROLE_NOT_FOUND", "角色不存在"),
|
||||
MENU_NOT_FOUND("SYSTEM.MENU_NOT_FOUND", "菜单不存在"),
|
||||
DICT_TYPE_NOT_FOUND("SYSTEM.DICT_TYPE_NOT_FOUND", "字典类型不存在"),
|
||||
DICT_ITEM_NOT_FOUND("SYSTEM.DICT_ITEM_NOT_FOUND", "字典项不存在"),
|
||||
TOKEN_VERSION_INVALID("AUTH.TOKEN_VERSION_INVALID", "登录状态已失效,请重新登录"),
|
||||
INTERNAL_SERVER_ERROR("COMMON.INTERNAL_SERVER_ERROR", "服务器内部错误"),
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ data class CurrentUserProfile(
|
||||
val realName: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val orgId: String? = null,
|
||||
val enterpriseId: String? = null,
|
||||
val digitalAccountId: String? = null,
|
||||
val userType: String = "SYSTEM",
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.bbit.ticket.entity.common.system
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DictTypeItem(
|
||||
val id: String,
|
||||
val code: String,
|
||||
val name: String,
|
||||
val status: String,
|
||||
val statusLabel: String,
|
||||
val remark: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DictItem(
|
||||
val id: String,
|
||||
val typeId: String,
|
||||
val label: String,
|
||||
val value: String,
|
||||
val color: String? = null,
|
||||
val sort: Int,
|
||||
val status: String,
|
||||
val statusLabel: String,
|
||||
val remark: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateDictTypeRequest(val code: String, val name: String, val status: String = "ENABLED", val remark: String? = null)
|
||||
|
||||
@Serializable
|
||||
data class UpdateDictTypeRequest(val name: String, val status: String = "ENABLED", val remark: String? = null)
|
||||
|
||||
@Serializable
|
||||
data class CreateDictItemRequest(
|
||||
val typeId: String,
|
||||
val label: String,
|
||||
val value: String,
|
||||
val color: String? = null,
|
||||
val sort: Int = 0,
|
||||
val status: String = "ENABLED",
|
||||
val remark: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateDictItemRequest(
|
||||
val typeId: String,
|
||||
val label: String,
|
||||
val value: String,
|
||||
val color: String? = null,
|
||||
val sort: Int = 0,
|
||||
val status: String = "ENABLED",
|
||||
val remark: String? = null,
|
||||
)
|
||||
@@ -15,18 +15,3 @@ data class OperationLogItem(
|
||||
val costMs: Long,
|
||||
val createdAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiAccessLogItem(
|
||||
val id: String,
|
||||
val traceId: String? = null,
|
||||
val appKey: String? = null,
|
||||
val appName: String? = null,
|
||||
val httpMethod: String,
|
||||
val requestPath: String,
|
||||
val responseCode: String? = null,
|
||||
val status: String,
|
||||
val errorMessage: String? = null,
|
||||
val costMs: Long,
|
||||
val createdAt: String,
|
||||
)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.bbit.ticket.entity.common.system
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OrgTreeNode(
|
||||
val id: String,
|
||||
val parentId: String? = null,
|
||||
val name: String,
|
||||
val code: String,
|
||||
val sort: Int,
|
||||
val status: String,
|
||||
val statusLabel: String,
|
||||
val children: List<OrgTreeNode> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateOrgRequest(
|
||||
val parentId: String? = null,
|
||||
val name: String,
|
||||
val code: String,
|
||||
val sort: Int = 0,
|
||||
val status: String = "ENABLED",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateOrgRequest(
|
||||
val parentId: String? = null,
|
||||
val name: String,
|
||||
val sort: Int = 0,
|
||||
val status: String = "ENABLED",
|
||||
)
|
||||
@@ -7,7 +7,9 @@ data class UserListItem(
|
||||
val username: String,
|
||||
val nickname: String? = null,
|
||||
val realName: String? = null,
|
||||
val orgId: String? = null,
|
||||
val enterpriseId: String? = null,
|
||||
val enterpriseName: String? = null,
|
||||
val taxpayerNum: String? = null,
|
||||
val status: String,
|
||||
val statusLabel: String,
|
||||
val roleCodes: List<String>,
|
||||
@@ -22,9 +24,9 @@ data class UserDetailResponse(
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val avatar: String? = null,
|
||||
val orgId: String? = null,
|
||||
val orgName: String? = null,
|
||||
val orgCode: String? = null,
|
||||
val enterpriseId: String? = null,
|
||||
val enterpriseName: String? = null,
|
||||
val taxpayerNum: String? = null,
|
||||
val status: String,
|
||||
val statusLabel: String,
|
||||
val roleIds: List<String>,
|
||||
@@ -57,7 +59,7 @@ data class CreateUserRequest(
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val avatar: String? = null,
|
||||
val orgId: String? = null,
|
||||
val enterpriseId: String? = null,
|
||||
val status: String = "ENABLED",
|
||||
)
|
||||
|
||||
@@ -68,7 +70,7 @@ data class UpdateUserRequest(
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val avatar: String? = null,
|
||||
val orgId: String? = null,
|
||||
val enterpriseId: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -61,3 +61,24 @@ data class OpenApiStatisticsItem(
|
||||
val avgCostMs: Long,
|
||||
val lastCalledAt: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EnterpriseManageListItem(
|
||||
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 reviewStatus: String? = null,
|
||||
val reviewOpinion: String? = null,
|
||||
val invoiceKind: String? = null,
|
||||
val invoiceLayoutFileType: String? = null,
|
||||
val serviceStatus: String? = null,
|
||||
val createdAt: String,
|
||||
val updatedAt: String? = null,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.PTException
|
||||
import com.bbit.ticket.entity.common.fail
|
||||
import com.bbit.ticket.entity.common.ok
|
||||
import com.bbit.ticket.service.system.OperationLogService
|
||||
import com.bbit.ticket.utils.CurrentUser
|
||||
import com.bbit.ticket.utils.plugins.myJson
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
@@ -13,6 +15,7 @@ import io.ktor.server.response.header
|
||||
import io.ktor.server.response.respondBytes
|
||||
import io.ktor.server.response.respondText
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
/**
|
||||
* 使用统一票通响应格式执行接口逻辑。
|
||||
@@ -35,6 +38,57 @@ suspend inline fun <reified T> ApplicationCall.respondPt(
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <reified T> ApplicationCall.respondPtWithOperationLog(
|
||||
fallbackMessage: String,
|
||||
currentUser: CurrentUser,
|
||||
operationType: String,
|
||||
operationName: String,
|
||||
crossinline block: suspend () -> T,
|
||||
) {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
try {
|
||||
respondJson(ok(block()))
|
||||
OperationLogService.success(
|
||||
this,
|
||||
currentUser,
|
||||
operationType,
|
||||
operationName,
|
||||
start.elapsedNow().inWholeMilliseconds,
|
||||
)
|
||||
} catch (e: PTException) {
|
||||
respondJson(fail(code = e.code, message = e.message, traceId = e.serialNo))
|
||||
OperationLogService.fail(
|
||||
this,
|
||||
currentUser,
|
||||
operationType,
|
||||
operationName,
|
||||
e.message,
|
||||
start.elapsedNow().inWholeMilliseconds,
|
||||
)
|
||||
} catch (e: BizException) {
|
||||
respondJson(fail(code = e.errorCode, message = e.message), e.status)
|
||||
OperationLogService.fail(
|
||||
this,
|
||||
currentUser,
|
||||
operationType,
|
||||
operationName,
|
||||
e.message,
|
||||
start.elapsedNow().inWholeMilliseconds,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
val message = e.message ?: fallbackMessage
|
||||
respondJson(fail(code = "-1", message = message))
|
||||
OperationLogService.fail(
|
||||
this,
|
||||
currentUser,
|
||||
operationType,
|
||||
operationName,
|
||||
message,
|
||||
start.elapsedNow().inWholeMilliseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用统一票通响应格式执行可空查询,空结果按空对象返回。
|
||||
*
|
||||
|
||||
+6
-4
@@ -31,9 +31,10 @@ fun Route.registerOpenInvoiceTaskManageRoutes() {
|
||||
post("/openapi/tasks/queues/{digitalAccountId}/pause") {
|
||||
val digitalAccountId = call.parameters["digitalAccountId"].orEmpty()
|
||||
val body = call.receiveNullable<Map<String, String>>() ?: emptyMap()
|
||||
call.respondPt("暂停 OpenAPI 队列失败") {
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("暂停 OpenAPI 队列失败", currentUser, "PAUSE_QUEUE", "暂停 OpenAPI 队列") {
|
||||
OpenInvoiceTaskService.pauseQueue(
|
||||
user = call.requireCurrentUser(),
|
||||
user = currentUser,
|
||||
digitalAccountId = digitalAccountId,
|
||||
reason = body["reason"],
|
||||
)
|
||||
@@ -42,8 +43,9 @@ fun Route.registerOpenInvoiceTaskManageRoutes() {
|
||||
|
||||
post("/openapi/tasks/queues/{digitalAccountId}/resume") {
|
||||
val digitalAccountId = call.parameters["digitalAccountId"].orEmpty()
|
||||
call.respondPt("恢复 OpenAPI 队列失败") {
|
||||
OpenInvoiceTaskService.resumeQueue(call.requireCurrentUser(), digitalAccountId)
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("恢复 OpenAPI 队列失败", currentUser, "RESUME_QUEUE", "恢复 OpenAPI 队列") {
|
||||
OpenInvoiceTaskService.resumeQueue(currentUser, digitalAccountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,9 @@ fun Route.registerPTAuthRoutes() {
|
||||
}
|
||||
|
||||
post("/enterprise/refresh") {
|
||||
call.respondPt("刷新企业信息失败") {
|
||||
PTConfigService.refreshEnterpriseInfo(call.requireCurrentUser())
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("刷新企业信息失败", currentUser, "REFRESH", "刷新企业信息") {
|
||||
PTConfigService.refreshEnterpriseInfo(currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,22 +64,25 @@ fun Route.registerPTAuthRoutes() {
|
||||
}
|
||||
|
||||
post("/digital-accounts/refresh") {
|
||||
call.respondPt("刷新数电账号失败") {
|
||||
PTConfigService.refreshDigitalAccounts(call.requireCurrentUser())
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("刷新数电账号失败", currentUser, "REFRESH", "刷新数电账号") {
|
||||
PTConfigService.refreshDigitalAccounts(currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
post("/digital-accounts") {
|
||||
call.respondPt("新增数电账号失败") {
|
||||
PTConfigService.createDigitalAccount(call.requireCurrentUser(), call.receive<CreateDigitalAccountRequest>())
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("新增数电账号失败", currentUser, "CREATE", "新增数电账号") {
|
||||
PTConfigService.createDigitalAccount(currentUser, call.receive<CreateDigitalAccountRequest>())
|
||||
}
|
||||
}
|
||||
|
||||
put("/digital-accounts/{id}/status") {
|
||||
call.respondPt("更新数电账号状态失败") {
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("更新数电账号状态失败", currentUser, "UPDATE_STATUS", "更新数电账号状态") {
|
||||
val id = call.parameters["id"] ?: throw IllegalArgumentException("缺少数电账号ID")
|
||||
PTConfigService.updateDigitalAccountStatus(
|
||||
call.requireCurrentUser(),
|
||||
currentUser,
|
||||
id,
|
||||
call.receive<UpdateDigitalAccountStatusRequest>(),
|
||||
)
|
||||
@@ -92,8 +96,9 @@ fun Route.registerPTAuthRoutes() {
|
||||
}
|
||||
|
||||
put("/preset") {
|
||||
call.respondPt("保存预设数据失败") {
|
||||
PTConfigService.updateInvoiceSetting(call.requireCurrentUser(), call.receive<UpdateInvoiceSettingRequest>())
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("保存预设数据失败", currentUser, "UPDATE", "保存开票预设数据") {
|
||||
PTConfigService.updateInvoiceSetting(currentUser, call.receive<UpdateInvoiceSettingRequest>())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,9 +109,10 @@ fun Route.registerPTAuthRoutes() {
|
||||
}
|
||||
|
||||
get("/authentication") {
|
||||
call.respondPt("获取认证二维码失败") {
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("获取认证二维码失败", currentUser, "AUTH_QRCODE", "获取认证二维码") {
|
||||
val account = PTConfigService.requireDigitalAccountForAction(
|
||||
call.requireCurrentUser(),
|
||||
currentUser,
|
||||
call.request.queryParameters["digitalAccountId"],
|
||||
)
|
||||
PTAuthService.getAuthenticationQrcode(
|
||||
@@ -120,23 +126,26 @@ fun Route.registerPTAuthRoutes() {
|
||||
}
|
||||
|
||||
post("/query-auth-status") {
|
||||
call.respondPt("查询认证二维码扫码状态失败") {
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("查询认证二维码扫码状态失败", currentUser, "AUTH_STATUS", "查询认证扫码状态") {
|
||||
PTAuthService.queryAuthQrcodeScanStatus(call.receive<QueryRealNameAuthQrStatusRequest>())
|
||||
}
|
||||
}
|
||||
|
||||
post("/send-sms-code") {
|
||||
call.respondPt("发送登录短信验证码失败") {
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("发送登录短信验证码失败", currentUser, "SEND_SMS_CODE", "发送登录短信验证码") {
|
||||
val req = call.receive<GetLoginSmsCodeRequest>()
|
||||
PTConfigService.requireDigitalAccountForLogin(call.requireCurrentUser(), req.taxpayerNum, req.account)
|
||||
PTConfigService.requireDigitalAccountForLogin(currentUser, req.taxpayerNum, req.account)
|
||||
PTAuthService.sendLoginSmsCode(req)
|
||||
}
|
||||
}
|
||||
|
||||
post("/sms-login") {
|
||||
call.respondPt("短信验证码登录失败") {
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("短信验证码登录失败", currentUser, "SMS_LOGIN", "短信验证码登录") {
|
||||
val req = call.receive<SmsLoginRequest>()
|
||||
PTConfigService.requireDigitalAccountForLogin(call.requireCurrentUser(), req.taxpayerNum, req.account)
|
||||
PTConfigService.requireDigitalAccountForLogin(currentUser, req.taxpayerNum, req.account)
|
||||
PTAuthService.smsLogin(req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,16 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||
*/
|
||||
fun Route.registerPTInvoiceRoutes() {
|
||||
post("/invoiceRed") {
|
||||
call.respondPt("红字任务创建失败") {
|
||||
PTRedService.invoiceRed(call.requireCurrentUser(), call.receive<RedCreateRequest>())
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("红字任务创建失败", currentUser, "CREATE_RED_INVOICE", "创建红字任务") {
|
||||
PTRedService.invoiceRed(currentUser, call.receive<RedCreateRequest>())
|
||||
}
|
||||
}
|
||||
|
||||
post("/invoiceBlue") {
|
||||
call.respondPt("蓝票任务创建失败") {
|
||||
PTBlueService.invoiceBlue(call.receive<AskInvoiceRequest>(), call.requireCurrentUser())
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("蓝票任务创建失败", currentUser, "CREATE_BLUE_INVOICE", "创建蓝票任务") {
|
||||
PTBlueService.invoiceBlue(call.receive<AskInvoiceRequest>(), currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +102,9 @@ fun Route.registerPTInvoiceRoutes() {
|
||||
get("/queryInvoice") {
|
||||
val invoiceReqSerialNo = call.requiredQueryParameter("invoiceReqSerialNo", "请传入发票请求流水号")
|
||||
?: return@get
|
||||
call.respondPt("刷新发票状态失败") {
|
||||
PTBlueService.queryInvoiceAllInfo(call.requireCurrentUser(), invoiceReqSerialNo)
|
||||
val currentUser = call.requireCurrentUser()
|
||||
call.respondPtWithOperationLog("刷新发票状态失败", currentUser, "REFRESH_INVOICE_STATUS", "刷新发票状态") {
|
||||
PTBlueService.queryInvoiceAllInfo(currentUser, invoiceReqSerialNo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.route.system
|
||||
|
||||
import com.bbit.ticket.entity.common.ok
|
||||
import com.bbit.ticket.utils.parseUuid
|
||||
import com.bbit.ticket.utils.queryInt
|
||||
import com.bbit.ticket.utils.queryString
|
||||
import com.bbit.ticket.entity.common.system.CreateDictItemRequest
|
||||
import com.bbit.ticket.entity.common.system.CreateDictTypeRequest
|
||||
import com.bbit.ticket.entity.common.system.UpdateDictItemRequest
|
||||
import com.bbit.ticket.entity.common.system.UpdateDictTypeRequest
|
||||
import com.bbit.ticket.service.system.OperationLogService
|
||||
import com.bbit.ticket.utils.requirePermission
|
||||
import com.bbit.ticket.service.system.DictService
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.delete
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.put
|
||||
import io.ktor.server.routing.route
|
||||
import kotlin.time.TimeSource
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
fun Route.registerDictRoutes() {
|
||||
authenticate("auth-jwt") {
|
||||
route("/system/dict-types") {
|
||||
get {
|
||||
call.requirePermission("system:dict:view")
|
||||
val page = call.queryInt("page", 1)
|
||||
val pageSize = call.queryInt("pageSize", 20)
|
||||
call.respond(ok(DictService.listTypes(page, pageSize, call.queryString("keyword"))))
|
||||
}
|
||||
post {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:dict:create")
|
||||
val request = call.receive<CreateDictTypeRequest>()
|
||||
runCatching {
|
||||
val id = DictService.createType(request)
|
||||
call.respond(ok(mapOf("id" to id)))
|
||||
OperationLogService.success(call, currentUser, "CREATE", "新增字典类型", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "CREATE", "新增字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
put("/{id}") {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:dict:update")
|
||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||
val request = call.receive<UpdateDictTypeRequest>()
|
||||
runCatching {
|
||||
DictService.updateType(id, request)
|
||||
call.respond(ok<Unit>(message = "更新成功"))
|
||||
OperationLogService.success(call, currentUser, "UPDATE", "更新字典类型", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
delete("/{id}") {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:dict:delete")
|
||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||
runCatching {
|
||||
DictService.deleteType(id)
|
||||
call.respond(ok<Unit>(message = "删除成功"))
|
||||
OperationLogService.success(call, currentUser, "DELETE", "删除字典类型", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "DELETE", "删除字典类型", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
route("/system/dict-items") {
|
||||
get {
|
||||
call.requirePermission("system:dict:view")
|
||||
val page = call.queryInt("page", 1)
|
||||
val pageSize = call.queryInt("pageSize", 20)
|
||||
val typeId = call.queryString("typeId")?.let { parseUuid(it, "typeId") }
|
||||
call.respond(ok(DictService.listItems(page, pageSize, typeId)))
|
||||
}
|
||||
post {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:dict:create")
|
||||
val request = call.receive<CreateDictItemRequest>()
|
||||
runCatching {
|
||||
val id = DictService.createItem(request)
|
||||
call.respond(ok(mapOf("id" to id)))
|
||||
OperationLogService.success(call, currentUser, "CREATE", "新增字典项", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "CREATE", "新增字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
put("/{id}") {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:dict:update")
|
||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||
val request = call.receive<UpdateDictItemRequest>()
|
||||
runCatching {
|
||||
DictService.updateItem(id, request)
|
||||
call.respond(ok<Unit>(message = "更新成功"))
|
||||
OperationLogService.success(call, currentUser, "UPDATE", "更新字典项", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
delete("/{id}") {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:dict:delete")
|
||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||
runCatching {
|
||||
DictService.deleteItem(id)
|
||||
call.respond(ok<Unit>(message = "删除成功"))
|
||||
OperationLogService.success(call, currentUser, "DELETE", "删除字典项", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "DELETE", "删除字典项", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.bbit.ticket.route.system
|
||||
|
||||
import com.bbit.ticket.entity.common.ok
|
||||
import com.bbit.ticket.service.system.EnterpriseManageService
|
||||
import com.bbit.ticket.utils.queryInt
|
||||
import com.bbit.ticket.utils.queryString
|
||||
import com.bbit.ticket.utils.requirePermission
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Route.registerEnterpriseManageRoutes() {
|
||||
authenticate("auth-jwt") {
|
||||
route("/system/enterprises") {
|
||||
get {
|
||||
call.requirePermission("system:enterprise:view")
|
||||
call.respond(
|
||||
ok(
|
||||
EnterpriseManageService.list(
|
||||
keyword = call.queryString("keyword"),
|
||||
page = call.queryInt("page", 1),
|
||||
pageSize = call.queryInt("pageSize", 20),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,6 @@ fun Route.registerLogsQueryRoutes() {
|
||||
val pageSize = call.queryInt("pageSize", 20)
|
||||
call.respond(ok(LogsQueryService.operationLogs(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
||||
}
|
||||
get("/api-access") {
|
||||
call.requirePermission("log:api-access:view")
|
||||
val page = call.queryInt("page", 1)
|
||||
val pageSize = call.queryInt("pageSize", 20)
|
||||
call.respond(ok(LogsQueryService.apiAccessLogs(page, pageSize, call.queryString("keyword"), call.queryString("status"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.route.system
|
||||
|
||||
import com.bbit.ticket.entity.common.ok
|
||||
import com.bbit.ticket.entity.common.system.CreateOrgRequest
|
||||
import com.bbit.ticket.entity.common.system.UpdateOrgRequest
|
||||
import com.bbit.ticket.utils.parseUuid
|
||||
import com.bbit.ticket.utils.requirePermission
|
||||
import com.bbit.ticket.service.system.OperationLogService
|
||||
import com.bbit.ticket.service.system.OrgService
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.delete
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.put
|
||||
import io.ktor.server.routing.route
|
||||
import kotlin.time.TimeSource
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
fun Route.registerOrgRoutes() {
|
||||
authenticate("auth-jwt") {
|
||||
route("/system/orgs") {
|
||||
get {
|
||||
call.requirePermission("system:org:view")
|
||||
call.respond(ok(OrgService.tree()))
|
||||
}
|
||||
post {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:org:create")
|
||||
val request = call.receive<CreateOrgRequest>()
|
||||
runCatching {
|
||||
val id = OrgService.create(request)
|
||||
call.respond(ok(mapOf("id" to id)))
|
||||
OperationLogService.success(call, currentUser, "CREATE", "新增组织", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "CREATE", "新增组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
put("/{id}") {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:org:update")
|
||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||
val request = call.receive<UpdateOrgRequest>()
|
||||
runCatching {
|
||||
OrgService.update(id, request)
|
||||
call.respond(ok<Unit>(message = "更新成功"))
|
||||
OperationLogService.success(call, currentUser, "UPDATE", "更新组织", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "UPDATE", "更新组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
delete("/{id}") {
|
||||
val start = TimeSource.Monotonic.markNow()
|
||||
val currentUser = call.requirePermission("system:org:delete")
|
||||
val id = parseUuid(call.parameters["id"] ?: "", "id")
|
||||
runCatching {
|
||||
OrgService.delete(id)
|
||||
call.respond(ok<Unit>(message = "删除成功"))
|
||||
OperationLogService.success(call, currentUser, "DELETE", "删除组织", start.elapsedNow().inWholeMilliseconds)
|
||||
}.onFailure {
|
||||
OperationLogService.fail(call, currentUser, "DELETE", "删除组织", it.message, start.elapsedNow().inWholeMilliseconds)
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ fun Route.registerUserRoutes() {
|
||||
username = call.queryString("username"),
|
||||
nickname = call.queryString("nickname"),
|
||||
status = call.queryString("status"),
|
||||
orgId = call.queryString("orgId")?.let { parseUuid(it, "orgId") },
|
||||
enterpriseId = call.queryString("enterpriseId")?.let { parseUuid(it, "enterpriseId") },
|
||||
)
|
||||
call.respond(ok(result))
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.jetbrains.exposed.v1.core.notInList
|
||||
import org.jetbrains.exposed.v1.core.or
|
||||
import org.jetbrains.exposed.v1.jdbc.Query
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.insertIgnore
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import java.time.OffsetDateTime
|
||||
@@ -67,6 +68,9 @@ object OpenInvoiceTaskService {
|
||||
private const val QUEUE_RUNNING = "RUNNING"
|
||||
private const val QUEUE_PAUSED = "PAUSED"
|
||||
private const val AUTH_REQUIRED_CODE = "3999"
|
||||
private const val HISTORY_FAILED_CODE = "9999"
|
||||
private const val EXCEPTION_CODE = "EXCEPTION"
|
||||
private const val STALE_EXCEPTION_LOCK_SECONDS = 300L
|
||||
|
||||
suspend fun createIssueTask(
|
||||
principal: OpenApiPrincipal,
|
||||
@@ -113,8 +117,10 @@ object OpenInvoiceTaskService {
|
||||
|
||||
ensureQueueCanAccept(principal.apiKey)
|
||||
|
||||
val taskId = dbQuery {
|
||||
OpenInvoiceTaskTable.insert {
|
||||
val createdTaskId = Uuid.random()
|
||||
val task = dbQuery {
|
||||
OpenInvoiceTaskTable.insertIgnore {
|
||||
it[id] = createdTaskId
|
||||
it[apiKey] = principal.apiKey
|
||||
it[userId] = principal.userId
|
||||
it[enterpriseId] = principal.enterpriseId
|
||||
@@ -132,13 +138,25 @@ object OpenInvoiceTaskService {
|
||||
it[maxPollCount] = com.bbit.ticket.utils.bootstrap.AppConfig.openApiQueue.maxQueryPollCount
|
||||
it[nextRunAt] = now
|
||||
it[createdAt] = now
|
||||
}[OpenInvoiceTaskTable.id]
|
||||
}
|
||||
OpenInvoiceTaskTable.selectAll()
|
||||
.where {
|
||||
(OpenInvoiceTaskTable.taskType eq TASK_ISSUE_BLUE) and
|
||||
(OpenInvoiceTaskTable.invoiceReqSerialNo eq invoiceReqSerialNo)
|
||||
}
|
||||
.single()
|
||||
}
|
||||
if (task[OpenInvoiceTaskTable.runMode] != runMode) {
|
||||
throw BizException(
|
||||
ErrorCode.BAD_REQUEST.code,
|
||||
"invoiceReqSerialNo 已存在 ${task[OpenInvoiceTaskTable.runMode]} 任务,不能重复创建 $runMode 任务",
|
||||
)
|
||||
}
|
||||
|
||||
return OpenInvoiceTaskSubmitResponse(
|
||||
taskId = taskId.toString(),
|
||||
taskId = task[OpenInvoiceTaskTable.id].toString(),
|
||||
invoiceReqSerialNo = invoiceReqSerialNo,
|
||||
status = STATUS_PENDING,
|
||||
status = task[OpenInvoiceTaskTable.status],
|
||||
taskType = TASK_ISSUE_BLUE,
|
||||
runMode = runMode,
|
||||
)
|
||||
@@ -275,7 +293,7 @@ object OpenInvoiceTaskService {
|
||||
pauseApiKey(task[OpenInvoiceTaskTable.apiKey], e.code, e.message)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
retryOrFail(task, "EXCEPTION", e.message ?: "开票任务执行失败")
|
||||
retryOrFail(task, EXCEPTION_CODE, e.message ?: "开票任务执行失败")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +326,7 @@ object OpenInvoiceTaskService {
|
||||
} catch (e: PTException) {
|
||||
retryOrFail(task, e.code, e.message)
|
||||
} catch (e: Exception) {
|
||||
retryOrFail(task, "EXCEPTION", e.message ?: "查询任务执行失败")
|
||||
retryOrFail(task, EXCEPTION_CODE, e.message ?: "查询任务执行失败")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,32 +343,24 @@ object OpenInvoiceTaskService {
|
||||
it[lockedBy] = null
|
||||
it[lockedAt] = null
|
||||
}
|
||||
val exists = OpenInvoiceTaskTable.selectAll()
|
||||
.where {
|
||||
(OpenInvoiceTaskTable.taskType eq TASK_QUERY_BLUE) and
|
||||
(OpenInvoiceTaskTable.invoiceReqSerialNo eq invoiceReqSerialNo)
|
||||
}
|
||||
.singleOrNull() != null
|
||||
if (!exists) {
|
||||
OpenInvoiceTaskTable.insert {
|
||||
it[apiKey] = task[OpenInvoiceTaskTable.apiKey]
|
||||
it[userId] = task[OpenInvoiceTaskTable.userId]
|
||||
it[enterpriseId] = task[OpenInvoiceTaskTable.enterpriseId]
|
||||
it[digitalAccountId] = task[OpenInvoiceTaskTable.digitalAccountId]
|
||||
it[taxpayerNum] = task[OpenInvoiceTaskTable.taxpayerNum]
|
||||
it[taxAccount] = task[OpenInvoiceTaskTable.taxAccount]
|
||||
it[taskType] = TASK_QUERY_BLUE
|
||||
it[sourceType] = task[OpenInvoiceTaskTable.sourceType]
|
||||
it[runMode] = task[OpenInvoiceTaskTable.runMode]
|
||||
it[OpenInvoiceTaskTable.invoiceReqSerialNo] = invoiceReqSerialNo
|
||||
it[batchNo] = task[OpenInvoiceTaskTable.batchNo]
|
||||
it[status] = STATUS_PENDING
|
||||
it[requestBody] = task[OpenInvoiceTaskTable.taxpayerNum]
|
||||
it[maxAttemptCount] = task[OpenInvoiceTaskTable.maxAttemptCount]
|
||||
it[maxPollCount] = task[OpenInvoiceTaskTable.maxPollCount]
|
||||
it[nextRunAt] = now.plusSeconds(com.bbit.ticket.utils.bootstrap.AppConfig.openApiQueue.queryDelaySeconds)
|
||||
it[createdAt] = now
|
||||
}
|
||||
OpenInvoiceTaskTable.insertIgnore {
|
||||
it[apiKey] = task[OpenInvoiceTaskTable.apiKey]
|
||||
it[userId] = task[OpenInvoiceTaskTable.userId]
|
||||
it[enterpriseId] = task[OpenInvoiceTaskTable.enterpriseId]
|
||||
it[digitalAccountId] = task[OpenInvoiceTaskTable.digitalAccountId]
|
||||
it[taxpayerNum] = task[OpenInvoiceTaskTable.taxpayerNum]
|
||||
it[taxAccount] = task[OpenInvoiceTaskTable.taxAccount]
|
||||
it[taskType] = TASK_QUERY_BLUE
|
||||
it[sourceType] = task[OpenInvoiceTaskTable.sourceType]
|
||||
it[runMode] = task[OpenInvoiceTaskTable.runMode]
|
||||
it[OpenInvoiceTaskTable.invoiceReqSerialNo] = invoiceReqSerialNo
|
||||
it[batchNo] = task[OpenInvoiceTaskTable.batchNo]
|
||||
it[status] = STATUS_PENDING
|
||||
it[requestBody] = task[OpenInvoiceTaskTable.taxpayerNum]
|
||||
it[maxAttemptCount] = task[OpenInvoiceTaskTable.maxAttemptCount]
|
||||
it[maxPollCount] = task[OpenInvoiceTaskTable.maxPollCount]
|
||||
it[nextRunAt] = now.plusSeconds(com.bbit.ticket.utils.bootstrap.AppConfig.openApiQueue.queryDelaySeconds)
|
||||
it[createdAt] = now
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,8 +403,8 @@ object OpenInvoiceTaskService {
|
||||
HistoryInvoiceBasicTable.update({
|
||||
HistoryInvoiceBasicTable.invoiceReqSerialNo eq task[OpenInvoiceTaskTable.invoiceReqSerialNo]
|
||||
}) {
|
||||
it[HistoryInvoiceBasicTable.code] = code
|
||||
it[msg] = message ?: if (code == "0000") "开票成功" else "开票失败"
|
||||
it[HistoryInvoiceBasicTable.code] = historyCode(code)
|
||||
it[msg] = historyMessage(message ?: if (code == "0000") "开票成功" else "开票失败")
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
@@ -420,8 +430,8 @@ object OpenInvoiceTaskService {
|
||||
HistoryInvoiceBasicTable.update({
|
||||
HistoryInvoiceBasicTable.invoiceReqSerialNo eq task[OpenInvoiceTaskTable.invoiceReqSerialNo]
|
||||
}) {
|
||||
it[HistoryInvoiceBasicTable.code] = code
|
||||
it[msg] = if (code == "6666") "未开票" else "开票中..."
|
||||
it[HistoryInvoiceBasicTable.code] = historyCode(code)
|
||||
it[msg] = historyMessage(if (code == "6666") "未开票" else "开票中...")
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
@@ -467,8 +477,8 @@ object OpenInvoiceTaskService {
|
||||
HistoryInvoiceBasicTable.update({
|
||||
HistoryInvoiceBasicTable.invoiceReqSerialNo eq task[OpenInvoiceTaskTable.invoiceReqSerialNo]
|
||||
}) {
|
||||
it[HistoryInvoiceBasicTable.code] = code
|
||||
it[msg] = message
|
||||
it[HistoryInvoiceBasicTable.code] = historyCode(code)
|
||||
it[msg] = historyMessage(message)
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
@@ -477,13 +487,19 @@ object OpenInvoiceTaskService {
|
||||
|
||||
private suspend fun claimTasks(taskType: String, workerId: String, limit: Int): List<ResultRow> {
|
||||
val now = OffsetDateTime.now()
|
||||
val staleLockAt = now.minusSeconds(STALE_EXCEPTION_LOCK_SECONDS)
|
||||
val candidateIds = dbQuery {
|
||||
val pausedApiKeys = OpenInvoiceQueueControlTable.selectAll()
|
||||
.where { OpenInvoiceQueueControlTable.status eq QUEUE_PAUSED }
|
||||
.map { it[OpenInvoiceQueueControlTable.apiKey] }
|
||||
var where = (OpenInvoiceTaskTable.taskType eq taskType) and
|
||||
(OpenInvoiceTaskTable.status eq STATUS_PENDING) and
|
||||
(OpenInvoiceTaskTable.nextRunAt lessEq now)
|
||||
var where = (OpenInvoiceTaskTable.taskType eq taskType) and (
|
||||
((OpenInvoiceTaskTable.status eq STATUS_PENDING) and (OpenInvoiceTaskTable.nextRunAt lessEq now)) or
|
||||
(
|
||||
(OpenInvoiceTaskTable.status eq STATUS_PROCESSING) and
|
||||
(OpenInvoiceTaskTable.ptCode eq EXCEPTION_CODE) and
|
||||
(OpenInvoiceTaskTable.lockedAt lessEq staleLockAt)
|
||||
)
|
||||
)
|
||||
if (pausedApiKeys.isNotEmpty()) {
|
||||
where = where and (OpenInvoiceTaskTable.apiKey notInList pausedApiKeys)
|
||||
}
|
||||
@@ -499,7 +515,14 @@ object OpenInvoiceTaskService {
|
||||
return dbQuery {
|
||||
candidateIds.mapNotNull { id ->
|
||||
val updated = OpenInvoiceTaskTable.update({
|
||||
(OpenInvoiceTaskTable.id eq id) and (OpenInvoiceTaskTable.status eq STATUS_PENDING)
|
||||
(OpenInvoiceTaskTable.id eq id) and (
|
||||
(OpenInvoiceTaskTable.status eq STATUS_PENDING) or
|
||||
(
|
||||
(OpenInvoiceTaskTable.status eq STATUS_PROCESSING) and
|
||||
(OpenInvoiceTaskTable.ptCode eq EXCEPTION_CODE) and
|
||||
(OpenInvoiceTaskTable.lockedAt lessEq staleLockAt)
|
||||
)
|
||||
)
|
||||
}) {
|
||||
it[status] = STATUS_PROCESSING
|
||||
it[lockedBy] = workerId
|
||||
@@ -517,24 +540,19 @@ object OpenInvoiceTaskService {
|
||||
}
|
||||
|
||||
private suspend fun createHistoryPlaceholder(task: ResultRow, request: AskInvoiceRequest) = dbQuery {
|
||||
val exists = HistoryInvoiceBasicTable.selectAll()
|
||||
.where { HistoryInvoiceBasicTable.invoiceReqSerialNo eq request.invoiceReqSerialNo }
|
||||
.singleOrNull() != null
|
||||
if (!exists) {
|
||||
val now = OffsetDateTime.now()
|
||||
HistoryInvoiceBasicTable.insert {
|
||||
it[userId] = task[OpenInvoiceTaskTable.userId]
|
||||
it[enterpriseId] = task[OpenInvoiceTaskTable.enterpriseId]
|
||||
it[digitalAccountId] = task[OpenInvoiceTaskTable.digitalAccountId]
|
||||
it[invoiceReqSerialNo] = request.invoiceReqSerialNo
|
||||
it[code] = "7777"
|
||||
it[msg] = "开票中..."
|
||||
it[sellerTaxpayerNum] = request.taxpayerNum
|
||||
it[invoiceKind] = request.invoiceIssueKindCode
|
||||
it[invoiceType] = "1"
|
||||
it[invDeletedFlag] = "0"
|
||||
it[createdAt] = now
|
||||
}
|
||||
val now = OffsetDateTime.now()
|
||||
HistoryInvoiceBasicTable.insertIgnore {
|
||||
it[userId] = task[OpenInvoiceTaskTable.userId]
|
||||
it[enterpriseId] = task[OpenInvoiceTaskTable.enterpriseId]
|
||||
it[digitalAccountId] = task[OpenInvoiceTaskTable.digitalAccountId]
|
||||
it[invoiceReqSerialNo] = request.invoiceReqSerialNo
|
||||
it[code] = "7777"
|
||||
it[msg] = "开票中..."
|
||||
it[sellerTaxpayerNum] = request.taxpayerNum
|
||||
it[invoiceKind] = request.invoiceIssueKindCode
|
||||
it[invoiceType] = "1"
|
||||
it[invDeletedFlag] = "0"
|
||||
it[createdAt] = now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,6 +653,12 @@ object OpenInvoiceTaskService {
|
||||
else -> "0000"
|
||||
}
|
||||
|
||||
private fun historyCode(code: String): String =
|
||||
if (code.length <= 8) code else HISTORY_FAILED_CODE
|
||||
|
||||
private fun historyMessage(message: String): String =
|
||||
message.take(200)
|
||||
|
||||
private fun scopedTaskQuery(user: CurrentUser): Query =
|
||||
OpenInvoiceTaskTable.selectAll().where { taskScope(user) }
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ object AuthService {
|
||||
val (accessToken, expiresIn) = JwtService.issueAccessToken(
|
||||
userId = userId.toString(),
|
||||
username = user[com.bbit.ticket.database.system.SysUserTable.username],
|
||||
orgId = user[com.bbit.ticket.database.system.SysUserTable.orgId]?.toString(),
|
||||
roles = roleCodes,
|
||||
tokenVersion = user[com.bbit.ticket.database.system.SysUserTable.tokenVersion],
|
||||
)
|
||||
@@ -127,7 +126,6 @@ object AuthService {
|
||||
realName = userRow[SysUserTable.realName],
|
||||
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],
|
||||
@@ -140,10 +138,6 @@ object AuthService {
|
||||
}
|
||||
|
||||
private suspend fun loadMenusForUser(currentUser: CurrentUser) = dbQuery {
|
||||
if (currentUser.isSuperAdmin) {
|
||||
MenuDao.enabledMenusForSuperAdmin()
|
||||
} else {
|
||||
MenuDao.enabledMenusForRoleIds(UserDao.findEnabledRoleIds(currentUser.id))
|
||||
}
|
||||
MenuDao.enabledMenusForRoleIds(UserDao.findEnabledRoleIds(currentUser.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.service.system
|
||||
|
||||
import com.bbit.ticket.dao.system.DictDao
|
||||
import com.bbit.ticket.entity.common.system.CreateDictItemRequest
|
||||
import com.bbit.ticket.entity.common.system.CreateDictTypeRequest
|
||||
import com.bbit.ticket.entity.common.system.DictItem
|
||||
import com.bbit.ticket.entity.common.system.DictTypeItem
|
||||
import com.bbit.ticket.entity.common.system.UpdateDictItemRequest
|
||||
import com.bbit.ticket.entity.common.system.UpdateDictTypeRequest
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.entity.common.PageResult
|
||||
import com.bbit.ticket.utils.plugins.dbQuery
|
||||
import com.bbit.ticket.utils.parseUuid
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
object DictService {
|
||||
suspend fun listTypes(page: Int, pageSize: Int, keyword: String?): PageResult<DictTypeItem> =
|
||||
dbQuery { DictDao.listTypes(page, pageSize, keyword) }
|
||||
|
||||
suspend fun createType(request: CreateDictTypeRequest): String = dbQuery {
|
||||
if (request.code.trim().isBlank() || request.name.trim().isBlank()) {
|
||||
throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型编码和名称不能为空")
|
||||
}
|
||||
if (DictDao.typeCodeExists(request.code.trim())) {
|
||||
throw BizException(ErrorCode.DATA_CONFLICT.code, "字典类型编码已存在")
|
||||
}
|
||||
DictDao.createType(request)
|
||||
}
|
||||
|
||||
suspend fun updateType(id: Uuid, request: UpdateDictTypeRequest) = dbQuery {
|
||||
DictDao.requireType(id)
|
||||
DictDao.updateType(id, request)
|
||||
}
|
||||
|
||||
suspend fun deleteType(id: Uuid) = dbQuery {
|
||||
DictDao.requireType(id)
|
||||
if (DictDao.typeHasItems(id)) throw BizException(ErrorCode.BAD_REQUEST.code, "字典类型下存在字典项,不能删除")
|
||||
DictDao.softDeleteType(id)
|
||||
}
|
||||
|
||||
suspend fun listItems(page: Int, pageSize: Int, typeId: Uuid?): PageResult<DictItem> =
|
||||
dbQuery { DictDao.listItems(page, pageSize, typeId) }
|
||||
|
||||
suspend fun createItem(request: CreateDictItemRequest): String = dbQuery {
|
||||
val typeId = parseUuid(request.typeId, "typeId")
|
||||
DictDao.requireType(typeId)
|
||||
DictDao.createItem(request, typeId)
|
||||
}
|
||||
|
||||
suspend fun updateItem(id: Uuid, request: UpdateDictItemRequest) = dbQuery {
|
||||
DictDao.requireItem(id)
|
||||
val typeId = parseUuid(request.typeId, "typeId")
|
||||
DictDao.requireType(typeId)
|
||||
DictDao.updateItem(id, request, typeId)
|
||||
}
|
||||
|
||||
suspend fun deleteItem(id: Uuid) = dbQuery {
|
||||
DictDao.requireItem(id)
|
||||
DictDao.softDeleteItem(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.bbit.ticket.service.system
|
||||
|
||||
import com.bbit.ticket.dao.piaotong.EnterpriseManageDao
|
||||
import com.bbit.ticket.entity.common.PageResult
|
||||
import com.bbit.ticket.entity.response.EnterpriseManageListItem
|
||||
import com.bbit.ticket.utils.plugins.dbQuery
|
||||
|
||||
object EnterpriseManageService {
|
||||
suspend fun list(keyword: String?, page: Int, pageSize: Int): PageResult<EnterpriseManageListItem> =
|
||||
dbQuery { EnterpriseManageDao.list(keyword, page, pageSize) }
|
||||
}
|
||||
@@ -10,7 +10,6 @@ object JwtService {
|
||||
fun issueAccessToken(
|
||||
userId: String,
|
||||
username: String,
|
||||
orgId: String?,
|
||||
roles: List<String>,
|
||||
tokenVersion: Int,
|
||||
): Pair<String, Long> {
|
||||
@@ -24,7 +23,6 @@ object JwtService {
|
||||
.withExpiresAt(expiresAt)
|
||||
.withSubject(userId)
|
||||
.withClaim("username", username)
|
||||
.withClaim("orgId", orgId)
|
||||
.withArrayClaim("roles", roles.toTypedArray())
|
||||
.withClaim("tokenVersion", tokenVersion)
|
||||
.withClaim("token_type", "access_token")
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package com.bbit.ticket.service.system
|
||||
|
||||
import com.bbit.ticket.dao.system.LogDao
|
||||
import com.bbit.ticket.entity.common.system.ApiAccessLogItem
|
||||
import com.bbit.ticket.entity.common.system.OperationLogItem
|
||||
import com.bbit.ticket.entity.common.PageResult
|
||||
import com.bbit.ticket.utils.plugins.dbQuery
|
||||
@@ -12,7 +11,4 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||
object LogsQueryService {
|
||||
suspend fun operationLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<OperationLogItem> =
|
||||
dbQuery { LogDao.operationLogs(page, pageSize, keyword, status) }
|
||||
|
||||
suspend fun apiAccessLogs(page: Int, pageSize: Int, keyword: String?, status: String?): PageResult<ApiAccessLogItem> =
|
||||
dbQuery { LogDao.apiAccessLogs(page, pageSize, keyword, status) }
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package com.bbit.ticket.service.system
|
||||
|
||||
import com.bbit.ticket.dao.system.OrgDao
|
||||
import com.bbit.ticket.database.system.SysOrgTable
|
||||
import com.bbit.ticket.entity.common.system.CreateOrgRequest
|
||||
import com.bbit.ticket.entity.common.system.OrgTreeNode
|
||||
import com.bbit.ticket.entity.common.system.UpdateOrgRequest
|
||||
import com.bbit.ticket.entity.common.BizException
|
||||
import com.bbit.ticket.entity.common.ErrorCode
|
||||
import com.bbit.ticket.utils.plugins.dbQuery
|
||||
import com.bbit.ticket.utils.parseUuid
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
object OrgService {
|
||||
suspend fun tree(): List<OrgTreeNode> = dbQuery { OrgDao.tree() }
|
||||
|
||||
suspend fun create(request: CreateOrgRequest): String = dbQuery {
|
||||
val code = request.code.trim()
|
||||
if (code.isBlank() || request.name.trim().isBlank()) {
|
||||
throw BizException(ErrorCode.BAD_REQUEST.code, "组织名称和编码不能为空")
|
||||
}
|
||||
if (OrgDao.codeExists(code)) {
|
||||
throw BizException(ErrorCode.DATA_CONFLICT.code, "组织编码已存在")
|
||||
}
|
||||
val parentId = request.parentId?.let { parseUuid(it, "parentId") }
|
||||
if (parentId != null) OrgDao.requireActive(parentId)
|
||||
OrgDao.create(request, parentId)
|
||||
}
|
||||
|
||||
suspend fun update(id: Uuid, request: UpdateOrgRequest) = dbQuery {
|
||||
OrgDao.requireActive(id)
|
||||
val parentId = request.parentId?.let { parseUuid(it, "parentId") }
|
||||
if (parentId == id) {
|
||||
throw BizException(ErrorCode.BAD_REQUEST.code, "上级组织不能选择自身")
|
||||
}
|
||||
if (parentId != null) OrgDao.requireActive(parentId)
|
||||
OrgDao.update(id, request, parentId)
|
||||
}
|
||||
|
||||
suspend fun delete(id: Uuid) = dbQuery {
|
||||
val org = OrgDao.requireActive(id)
|
||||
if (org[SysOrgTable.code] == "DEFAULT_ORG") {
|
||||
throw BizException(ErrorCode.BAD_REQUEST.code, "默认组织不可删除")
|
||||
}
|
||||
if (OrgDao.hasChildren(id)) {
|
||||
throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在子组织,不能删除")
|
||||
}
|
||||
if (OrgDao.hasUsers(id)) {
|
||||
throw BizException(ErrorCode.BAD_REQUEST.code, "当前组织存在用户,不能删除")
|
||||
}
|
||||
OrgDao.softDelete(id)
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ object UserService {
|
||||
username: String?,
|
||||
nickname: String?,
|
||||
status: String?,
|
||||
orgId: Uuid?,
|
||||
): PageResult<UserListItem> = dbQuery { UserDao.list(page, pageSize, username, nickname, status, orgId) }
|
||||
enterpriseId: Uuid?,
|
||||
): PageResult<UserListItem> = dbQuery { UserDao.list(page, pageSize, username, nickname, status, enterpriseId) }
|
||||
|
||||
suspend fun create(request: CreateUserRequest): String = dbQuery {
|
||||
val username = request.username.trim()
|
||||
@@ -37,22 +37,22 @@ object UserService {
|
||||
throw BizException(ErrorCode.DATA_CONFLICT.code, "用户名已存在")
|
||||
}
|
||||
|
||||
val orgUuid = request.orgId?.let { parseUuid(it, "orgId") }
|
||||
if (orgUuid != null) {
|
||||
ensureOrgExists(orgUuid)
|
||||
val enterpriseUuid = request.enterpriseId?.let { parseUuid(it, "enterpriseId") }
|
||||
if (enterpriseUuid != null) {
|
||||
ensureEnterpriseExists(enterpriseUuid)
|
||||
}
|
||||
UserDao.create(request, PasswordService.hash(request.password), orgUuid)
|
||||
UserDao.create(request, PasswordService.hash(request.password), enterpriseUuid)
|
||||
}
|
||||
|
||||
suspend fun detail(id: Uuid): UserDetailResponse = dbQuery { UserDao.detail(id) }
|
||||
|
||||
suspend fun update(id: Uuid, request: UpdateUserRequest) = dbQuery {
|
||||
UserDao.requireActive(id)
|
||||
val orgUuid = request.orgId?.let { parseUuid(it, "orgId") }
|
||||
if (orgUuid != null) {
|
||||
ensureOrgExists(orgUuid)
|
||||
val enterpriseUuid = request.enterpriseId?.let { parseUuid(it, "enterpriseId") }
|
||||
if (enterpriseUuid != null) {
|
||||
ensureEnterpriseExists(enterpriseUuid)
|
||||
}
|
||||
UserDao.updateProfile(id, request, orgUuid)
|
||||
UserDao.updateProfile(id, request, enterpriseUuid)
|
||||
}
|
||||
|
||||
suspend fun softDelete(id: Uuid) = dbQuery {
|
||||
@@ -91,9 +91,9 @@ object UserService {
|
||||
UserDao.replaceRoles(id, roleIds)
|
||||
}
|
||||
|
||||
private fun ensureOrgExists(orgId: Uuid) {
|
||||
if (!UserDao.orgExists(orgId)) {
|
||||
throw BizException(ErrorCode.ORG_NOT_FOUND.code, ErrorCode.ORG_NOT_FOUND.message)
|
||||
private fun ensureEnterpriseExists(enterpriseId: Uuid) {
|
||||
if (!UserDao.enterpriseExists(enterpriseId)) {
|
||||
throw BizException(ErrorCode.BAD_REQUEST.code, "企业不存在")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ import io.ktor.server.application.ApplicationCall
|
||||
|
||||
suspend fun ApplicationCall.requirePermission(permission: String): CurrentUser {
|
||||
val currentUser = requireCurrentUser()
|
||||
if (currentUser.isSuperAdmin || currentUser.permissions.contains(permission)) {
|
||||
if (currentUser.permissions.contains(permission)) {
|
||||
return currentUser
|
||||
}
|
||||
|
||||
throw BizException(ErrorCode.FORBIDDEN.code, ErrorCode.FORBIDDEN.message, HttpStatusCode.Forbidden)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import kotlin.uuid.Uuid
|
||||
data class CurrentUser(
|
||||
val id: Uuid,
|
||||
val username: String,
|
||||
val orgId: Uuid?,
|
||||
val enterpriseId: Uuid?,
|
||||
val digitalAccountId: Uuid?,
|
||||
val userType: String,
|
||||
@@ -99,50 +98,36 @@ suspend fun ApplicationCall.requireCurrentUser(): CurrentUser {
|
||||
.toSet()
|
||||
}
|
||||
|
||||
val permissions = if (roleCodes.contains("SUPER_ADMIN")) {
|
||||
val roleIds = dbQuery {
|
||||
(SysUserRoleTable innerJoin SysRoleTable)
|
||||
.selectAll()
|
||||
.where {
|
||||
(SysUserRoleTable.userId eq userUuid) and
|
||||
SysRoleTable.deletedAt.isNull() and
|
||||
(SysRoleTable.status eq "ENABLED")
|
||||
}
|
||||
.map { it[SysRoleTable.id] }
|
||||
}
|
||||
val permissions = if (roleIds.isEmpty()) {
|
||||
emptySet()
|
||||
} else {
|
||||
dbQuery {
|
||||
SysMenuTable.selectAll()
|
||||
(SysRoleMenuTable innerJoin SysMenuTable)
|
||||
.selectAll()
|
||||
.where {
|
||||
SysMenuTable.deletedAt.isNull() and
|
||||
(SysRoleMenuTable.roleId inList roleIds) and
|
||||
SysMenuTable.deletedAt.isNull() and
|
||||
(SysMenuTable.status eq "ENABLED") and
|
||||
SysMenuTable.permission.isNotNull()
|
||||
}
|
||||
.mapNotNull { it[SysMenuTable.permission] }
|
||||
.toSet()
|
||||
}
|
||||
} else {
|
||||
val roleIds = dbQuery {
|
||||
(SysUserRoleTable innerJoin SysRoleTable)
|
||||
.selectAll()
|
||||
.where {
|
||||
(SysUserRoleTable.userId eq userUuid) and
|
||||
SysRoleTable.deletedAt.isNull() and
|
||||
(SysRoleTable.status eq "ENABLED")
|
||||
}
|
||||
.map { it[SysRoleTable.id] }
|
||||
}
|
||||
if (roleIds.isEmpty()) {
|
||||
emptySet()
|
||||
} else {
|
||||
dbQuery {
|
||||
(SysRoleMenuTable innerJoin SysMenuTable)
|
||||
.selectAll()
|
||||
.where {
|
||||
(SysRoleMenuTable.roleId inList roleIds) and
|
||||
SysMenuTable.deletedAt.isNull() and
|
||||
(SysMenuTable.status eq "ENABLED") and
|
||||
SysMenuTable.permission.isNotNull()
|
||||
}
|
||||
.mapNotNull { it[SysMenuTable.permission] }
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val currentUser = 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],
|
||||
|
||||
@@ -12,11 +12,8 @@ import com.bbit.ticket.database.piaotong.OpenInvoiceTaskTable
|
||||
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
|
||||
import com.bbit.ticket.database.system.SysMenuTable
|
||||
import com.bbit.ticket.database.system.SysOperationLogTable
|
||||
import com.bbit.ticket.database.system.SysOrgTable
|
||||
import com.bbit.ticket.database.system.SysRoleMenuTable
|
||||
import com.bbit.ticket.database.system.SysRoleTable
|
||||
import com.bbit.ticket.database.system.SysUserRoleTable
|
||||
@@ -30,14 +27,11 @@ object DatabaseInitializer {
|
||||
|
||||
suspend fun initialize() {
|
||||
val tables = arrayOf(
|
||||
SysOrgTable,
|
||||
SysUserTable,
|
||||
SysRoleTable,
|
||||
SysMenuTable,
|
||||
SysUserRoleTable,
|
||||
SysRoleMenuTable,
|
||||
SysDictTypeTable,
|
||||
SysDictItemTable,
|
||||
SysOperationLogTable,
|
||||
SysApiAccessLogTable,
|
||||
PtEnterpriseTable,
|
||||
|
||||
@@ -30,8 +30,16 @@ object SeedData {
|
||||
const val ADMIN_USERNAME = "admin"
|
||||
const val ADMIN_INIT_PASSWORD = "Admin@123456"
|
||||
|
||||
private const val DEFAULT_ORG_CODE = "DEFAULT_ORG"
|
||||
private const val DEFAULT_ADMIN_TAXPAYER_NUM = "500102201007206608"
|
||||
private const val ADMIN_NICKNAME = "超级管理员"
|
||||
private const val ENTERPRISE_ADMIN_INIT_PASSWORD = "123456"
|
||||
private const val DEV_ENTERPRISE_TAXPAYER_NUM = "500102201007206608"
|
||||
private const val PROD_ENTERPRISE_TAXPAYER_NUM = "91510106MA68JJHN7C"
|
||||
private const val PROD_ENTERPRISE_NAME = "成都主干智慧云信息技术有限公司"
|
||||
private const val PROD_ENTERPRISE_LEGAL_PERSON_NAME = "范鸿才"
|
||||
private const val PROD_ENTERPRISE_REGION_CODE = "51"
|
||||
private const val PROD_ENTERPRISE_CITY_NAME = "成都市"
|
||||
private const val PROD_ENTERPRISE_ADDRESS = "成都金牛区蜀西路42号5栋3单元5层2号"
|
||||
private const val PROD_ENTERPRISE_PHONE = "18981977117"
|
||||
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"
|
||||
@@ -42,9 +50,7 @@ object SeedData {
|
||||
|
||||
suspend fun seed() {
|
||||
val now = OffsetDateTime.now()
|
||||
val orgId = upsertDefaultOrg(now)
|
||||
val roleId = upsertSuperAdminRole(now)
|
||||
val defaultEnterpriseId = ensureDefaultAdminEnterprise()
|
||||
val enterpriseAdminRoleId = upsertBusinessRole(
|
||||
code = ENTERPRISE_ADMIN_ROLE_CODE,
|
||||
name = "企业管理员",
|
||||
@@ -57,48 +63,24 @@ object SeedData {
|
||||
description = "数电账号对应的平台开票员角色",
|
||||
now = now,
|
||||
)
|
||||
val adminId = upsertAdminUser(orgId, defaultEnterpriseId, now)
|
||||
val adminId = upsertAdminUser(now)
|
||||
upsertUserRole(adminId, roleId)
|
||||
val enterpriseSeed = enterpriseSeed()
|
||||
val enterpriseId = ensureSeedEnterprise(enterpriseSeed)
|
||||
val enterpriseAdminId = upsertEnterpriseAdminUser(enterpriseId, enterpriseSeed, now)
|
||||
upsertUserRole(enterpriseAdminId, enterpriseAdminRoleId)
|
||||
val menuIds = upsertMenus(now)
|
||||
disableObsoleteMenus(now)
|
||||
bindRoleMenus(roleId, menuIds.values.toList())
|
||||
bindRoleMenus(roleId, superAdminMenuIds(menuIds))
|
||||
bindRoleMenus(enterpriseAdminRoleId, enterpriseAdminMenuIds(menuIds))
|
||||
bindRoleMenus(digitalOperatorRoleId, digitalOperatorMenuIds(menuIds))
|
||||
seedDicts(now)
|
||||
logger.info("Seed data initialized, default admin username: {}", ADMIN_USERNAME)
|
||||
logger.info("Seed data initialized, super admin username: {}", ADMIN_USERNAME)
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Organization & Role
|
||||
// Role
|
||||
// =========================================================
|
||||
|
||||
private suspend fun upsertDefaultOrg(now: OffsetDateTime): Uuid = dbQuery {
|
||||
val existing = SysOrgTable.selectAll()
|
||||
.where { (SysOrgTable.code eq DEFAULT_ORG_CODE) and SysOrgTable.deletedAt.isNull() }
|
||||
.singleOrNull()
|
||||
|
||||
if (existing != null) {
|
||||
val id = existing[SysOrgTable.id]
|
||||
SysOrgTable.update({ SysOrgTable.id eq id }) {
|
||||
it[SysOrgTable.name] = "默认组织"
|
||||
it[SysOrgTable.sort] = 0
|
||||
it[SysOrgTable.status] = "ENABLED"
|
||||
it[SysOrgTable.updatedAt] = now
|
||||
}
|
||||
return@dbQuery id
|
||||
}
|
||||
|
||||
val inserted = SysOrgTable.insert {
|
||||
it[SysOrgTable.parentId] = null
|
||||
it[SysOrgTable.name] = "默认组织"
|
||||
it[SysOrgTable.code] = DEFAULT_ORG_CODE
|
||||
it[SysOrgTable.sort] = 0
|
||||
it[SysOrgTable.status] = "ENABLED"
|
||||
it[SysOrgTable.createdAt] = now
|
||||
}
|
||||
inserted[SysOrgTable.id]
|
||||
}
|
||||
|
||||
private suspend fun upsertSuperAdminRole(now: OffsetDateTime): Uuid = dbQuery {
|
||||
val existing = SysRoleTable.selectAll()
|
||||
.where { (SysRoleTable.code eq SUPER_ADMIN_ROLE_CODE) and SysRoleTable.deletedAt.isNull() }
|
||||
@@ -161,43 +143,38 @@ object SeedData {
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Admin user
|
||||
// Users & Enterprise
|
||||
// =========================================================
|
||||
|
||||
private suspend fun ensureDefaultAdminEnterprise(): Uuid {
|
||||
private suspend fun ensureSeedEnterprise(seed: EnterpriseSeed): Uuid {
|
||||
val existingId = dbQuery {
|
||||
EnterpriseManageDao.findEnterpriseByTaxpayerNum(DEFAULT_ADMIN_TAXPAYER_NUM)?.get(PtEnterpriseTable.id)
|
||||
EnterpriseManageDao.findEnterpriseByTaxpayerNum(seed.taxpayerNum)?.get(PtEnterpriseTable.id)
|
||||
}
|
||||
val ptEnterprise = PTApi.getEnterpriseInfo(QueryEnterpriseInfoRequest(DEFAULT_ADMIN_TAXPAYER_NUM))
|
||||
if (existingId != null) return existingId
|
||||
|
||||
if (existingId != null) {
|
||||
dbQuery { EnterpriseManageDao.updateEnterpriseFromPt(existingId, ptEnterprise) }
|
||||
return existingId
|
||||
}
|
||||
val ptEnterprise = PTApi.getEnterpriseInfo(QueryEnterpriseInfoRequest(seed.taxpayerNum))
|
||||
|
||||
val enterpriseInfo = TaxRegisterInfo(
|
||||
taxpayerNum = DEFAULT_ADMIN_TAXPAYER_NUM,
|
||||
enterpriseName = ptEnterprise.enterpriseName.ifBlank { DEFAULT_ADMIN_TAXPAYER_NUM },
|
||||
legalPersonName = null,
|
||||
contactsName = null,
|
||||
contactsEmail = null,
|
||||
contactsPhone = null,
|
||||
regionCode = ptEnterprise.regionCode,
|
||||
cityName = ptEnterprise.cityName,
|
||||
enterpriseAddress = ptEnterprise.enterpriseAddress,
|
||||
taxRegistrationCertificate = null,
|
||||
taxpayerNum = seed.taxpayerNum,
|
||||
enterpriseName = ptEnterprise.enterpriseName.ifBlank { seed.enterpriseName },
|
||||
legalPersonName = seed.legalPersonName,
|
||||
contactsName = seed.contactsName,
|
||||
contactsEmail = seed.contactsEmail,
|
||||
contactsPhone = seed.contactsPhone,
|
||||
regionCode = ptEnterprise.regionCode.ifBlank { seed.regionCode },
|
||||
cityName = ptEnterprise.cityName.ifBlank { seed.cityName },
|
||||
enterpriseAddress = ptEnterprise.enterpriseAddress.ifBlank { seed.enterpriseAddress.orEmpty() },
|
||||
taxRegistrationCertificate = seed.taxRegistrationCertificate,
|
||||
)
|
||||
|
||||
return dbQuery {
|
||||
val enterpriseId = EnterpriseManageDao.findEnterpriseByTaxpayerNum(DEFAULT_ADMIN_TAXPAYER_NUM)
|
||||
?.get(PtEnterpriseTable.id)
|
||||
?: EnterpriseManageDao.createEnterprise(enterpriseInfo)
|
||||
val enterpriseId = EnterpriseManageDao.createEnterprise(enterpriseInfo)
|
||||
EnterpriseManageDao.updateEnterpriseFromPt(enterpriseId, ptEnterprise)
|
||||
enterpriseId
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun upsertAdminUser(orgId: Uuid, enterpriseId: Uuid, now: OffsetDateTime): Uuid = dbQuery {
|
||||
private suspend fun upsertAdminUser(now: OffsetDateTime): Uuid = dbQuery {
|
||||
val existing = SysUserTable.selectAll()
|
||||
.where { (SysUserTable.username eq ADMIN_USERNAME) and SysUserTable.deletedAt.isNull() }
|
||||
.singleOrNull()
|
||||
@@ -205,14 +182,13 @@ object SeedData {
|
||||
if (existing != null) {
|
||||
val id = existing[SysUserTable.id]
|
||||
SysUserTable.update({ SysUserTable.id eq id }) {
|
||||
it[SysUserTable.nickname] = "系统管理员"
|
||||
it[SysUserTable.orgId] = orgId
|
||||
it[SysUserTable.enterpriseId] = enterpriseId
|
||||
it[SysUserTable.nickname] = ADMIN_NICKNAME
|
||||
it[SysUserTable.enterpriseId] = null
|
||||
it[SysUserTable.status] = "ENABLED"
|
||||
it[SysUserTable.userType] = "SYSTEM"
|
||||
it[SysUserTable.updatedAt] = now
|
||||
it[SysUserTable.phone] = "13000000000"
|
||||
it[SysUserTable.realName] = "测试"
|
||||
it[SysUserTable.phone] = null
|
||||
it[SysUserTable.realName] = null
|
||||
}
|
||||
return@dbQuery id
|
||||
}
|
||||
@@ -220,19 +196,85 @@ object SeedData {
|
||||
val inserted = SysUserTable.insert {
|
||||
it[SysUserTable.username] = ADMIN_USERNAME
|
||||
it[SysUserTable.passwordHash] = PasswordService.hash(ADMIN_INIT_PASSWORD)
|
||||
it[SysUserTable.nickname] = "系统管理员"
|
||||
it[SysUserTable.orgId] = orgId
|
||||
it[SysUserTable.enterpriseId] = enterpriseId
|
||||
it[SysUserTable.nickname] = ADMIN_NICKNAME
|
||||
it[SysUserTable.enterpriseId] = null
|
||||
it[SysUserTable.status] = "ENABLED"
|
||||
it[SysUserTable.userType] = "SYSTEM"
|
||||
it[SysUserTable.tokenVersion] = 1
|
||||
it[SysUserTable.phone] = "13000000000"
|
||||
it[SysUserTable.realName] = "测试"
|
||||
it[SysUserTable.phone] = null
|
||||
it[SysUserTable.realName] = null
|
||||
it[SysUserTable.createdAt] = now
|
||||
}
|
||||
inserted[SysUserTable.id]
|
||||
}
|
||||
|
||||
private suspend fun upsertEnterpriseAdminUser(
|
||||
enterpriseId: Uuid,
|
||||
seed: EnterpriseSeed,
|
||||
now: OffsetDateTime,
|
||||
): Uuid = dbQuery {
|
||||
val existing = SysUserTable.selectAll()
|
||||
.where { (SysUserTable.username eq seed.taxpayerNum) and SysUserTable.deletedAt.isNull() }
|
||||
.singleOrNull()
|
||||
|
||||
if (existing != null) {
|
||||
val id = existing[SysUserTable.id]
|
||||
SysUserTable.update({ SysUserTable.id eq id }) {
|
||||
it[SysUserTable.nickname] = seed.enterpriseName
|
||||
it[SysUserTable.enterpriseId] = enterpriseId
|
||||
it[SysUserTable.status] = "ENABLED"
|
||||
it[SysUserTable.userType] = "ENTERPRISE_ADMIN"
|
||||
it[SysUserTable.updatedAt] = now
|
||||
it[SysUserTable.phone] = seed.contactsPhone
|
||||
it[SysUserTable.realName] = seed.legalPersonName
|
||||
}
|
||||
return@dbQuery id
|
||||
}
|
||||
|
||||
val inserted = SysUserTable.insert {
|
||||
it[SysUserTable.username] = seed.taxpayerNum
|
||||
it[SysUserTable.passwordHash] = PasswordService.hash(ENTERPRISE_ADMIN_INIT_PASSWORD)
|
||||
it[SysUserTable.nickname] = seed.enterpriseName
|
||||
it[SysUserTable.enterpriseId] = enterpriseId
|
||||
it[SysUserTable.status] = "ENABLED"
|
||||
it[SysUserTable.userType] = "ENTERPRISE_ADMIN"
|
||||
it[SysUserTable.tokenVersion] = 1
|
||||
it[SysUserTable.phone] = seed.contactsPhone
|
||||
it[SysUserTable.realName] = seed.legalPersonName
|
||||
it[SysUserTable.createdAt] = now
|
||||
}
|
||||
inserted[SysUserTable.id]
|
||||
}
|
||||
|
||||
private fun enterpriseSeed(): EnterpriseSeed =
|
||||
if (Global.isDev) {
|
||||
EnterpriseSeed(
|
||||
taxpayerNum = DEV_ENTERPRISE_TAXPAYER_NUM,
|
||||
enterpriseName = DEV_ENTERPRISE_TAXPAYER_NUM,
|
||||
legalPersonName = null,
|
||||
contactsName = null,
|
||||
contactsEmail = null,
|
||||
contactsPhone = null,
|
||||
regionCode = "",
|
||||
cityName = "",
|
||||
enterpriseAddress = null,
|
||||
taxRegistrationCertificate = null,
|
||||
)
|
||||
} else {
|
||||
EnterpriseSeed(
|
||||
taxpayerNum = PROD_ENTERPRISE_TAXPAYER_NUM,
|
||||
enterpriseName = PROD_ENTERPRISE_NAME,
|
||||
legalPersonName = PROD_ENTERPRISE_LEGAL_PERSON_NAME,
|
||||
contactsName = PROD_ENTERPRISE_LEGAL_PERSON_NAME,
|
||||
contactsEmail = null,
|
||||
contactsPhone = PROD_ENTERPRISE_PHONE,
|
||||
regionCode = PROD_ENTERPRISE_REGION_CODE,
|
||||
cityName = PROD_ENTERPRISE_CITY_NAME,
|
||||
enterpriseAddress = PROD_ENTERPRISE_ADDRESS,
|
||||
taxRegistrationCertificate = null,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun upsertUserRole(userId: Uuid, roleId: Uuid) = dbQuery {
|
||||
val exists = SysUserRoleTable.selectAll()
|
||||
.where { (SysUserRoleTable.userId eq userId) and (SysUserRoleTable.roleId eq roleId) }
|
||||
@@ -257,10 +299,7 @@ object SeedData {
|
||||
button("system_user_create", "system_user", "新增用户", "SystemUserCreate", "system:user:create", 1),
|
||||
button("system_user_update", "system_user", "修改用户", "SystemUserUpdate", "system:user:update", 2),
|
||||
button("system_user_delete", "system_user", "删除用户", "SystemUserDelete", "system:user:delete", 3),
|
||||
subMenu("system_org", "system", "组织管理", "SystemOrgs", "/system/orgs", "system/orgs/index", "Building2", "system:org:view", 20),
|
||||
button("system_org_create", "system_org", "新增组织", "SystemOrgCreate", "system:org:create", 1),
|
||||
button("system_org_update", "system_org", "更新组织", "SystemOrgUpdate", "system:org:update", 2),
|
||||
button("system_org_delete", "system_org", "删除组织", "SystemOrgDelete", "system:org:delete", 3),
|
||||
subMenu("system_enterprise", "system", "企业管理", "SystemEnterprises", "/system/enterprises", "system/enterprises/index", "Building2", "system:enterprise:view", 20),
|
||||
subMenu("system_role", "system", "角色管理", "SystemRoles", "/system/roles", "system/roles/index", "Shield", "system:role:view", 30),
|
||||
button("system_role_create", "system_role", "新增角色", "SystemRoleCreate", "system:role:create", 1),
|
||||
button("system_role_update", "system_role", "更新角色", "SystemRoleUpdate", "system:role:update", 2),
|
||||
@@ -270,13 +309,8 @@ object SeedData {
|
||||
button("system_menu_create", "system_menu", "新增菜单", "SystemMenuCreate", "system:menu:create", 1),
|
||||
button("system_menu_update", "system_menu", "更新菜单", "SystemMenuUpdate", "system:menu:update", 2),
|
||||
button("system_menu_delete", "system_menu", "删除菜单", "SystemMenuDelete", "system:menu:delete", 3),
|
||||
subMenu("system_dict", "system", "字典管理", "SystemDict", "/system/dicts", "system/dicts/index", "BookType", "system:dict:view", 50),
|
||||
button("system_dict_create", "system_dict", "新增字典", "SystemDictCreate", "system:dict:create", 1),
|
||||
button("system_dict_update", "system_dict", "更新字典", "SystemDictUpdate", "system:dict:update", 2),
|
||||
button("system_dict_delete", "system_dict", "删除字典", "SystemDictDelete", "system:dict:delete", 3),
|
||||
catalog("logs", "日志管理", "LogsRoot", "Logs", 30),
|
||||
subMenu("logs_operation", "logs", "操作日志", "LogsOperation", "/logs/operation", "logs/operation/index", "ScrollText", "log:operation:view", 10),
|
||||
subMenu("logs_api_access", "logs", "接口日志", "LogsApiAccess", "/logs/api-access", "logs/api-access/index", "Waypoints", "log:api-access:view", 20),
|
||||
subMenu("openapi_statistics", "logs", "OpenAPI", "OpenApiStatistics", "/logs/openapi", "statistics/openapi/index", "Waypoints", "openapi:statistics:view", 30),
|
||||
catalog("basic_info", "基础信息", "BasicInfoRoot", "Building2", 40),
|
||||
subMenu("enterprise_info", "basic_info", "企业信息", "EnterpriseInfo", "/enterprise/info", "piaotong/index", "Building2", "enterprise:info:view", 10),
|
||||
@@ -297,6 +331,29 @@ object SeedData {
|
||||
return idMap
|
||||
}
|
||||
|
||||
private fun superAdminMenuIds(menuIds: Map<String, Uuid>): List<Uuid> =
|
||||
listOf(
|
||||
"dashboard",
|
||||
"system",
|
||||
"system_user",
|
||||
"system_user_create",
|
||||
"system_user_update",
|
||||
"system_user_delete",
|
||||
"system_enterprise",
|
||||
"system_role",
|
||||
"system_role_create",
|
||||
"system_role_update",
|
||||
"system_role_delete",
|
||||
"system_role_assign",
|
||||
"system_menu",
|
||||
"system_menu_create",
|
||||
"system_menu_update",
|
||||
"system_menu_delete",
|
||||
"logs",
|
||||
"logs_operation",
|
||||
"openapi_statistics",
|
||||
).mapNotNull { menuIds[it] }
|
||||
|
||||
private fun enterpriseAdminMenuIds(menuIds: Map<String, Uuid>): List<Uuid> =
|
||||
listOf(
|
||||
"dashboard",
|
||||
@@ -308,6 +365,7 @@ object SeedData {
|
||||
"invoice_service",
|
||||
"piaotong_invoice_issue",
|
||||
"piaotong_invoice_history",
|
||||
"logs",
|
||||
"openapi_statistics",
|
||||
).mapNotNull { menuIds[it] }
|
||||
|
||||
@@ -319,6 +377,7 @@ object SeedData {
|
||||
"invoice_service",
|
||||
"piaotong_invoice_issue",
|
||||
"piaotong_invoice_history",
|
||||
"logs",
|
||||
"openapi_statistics",
|
||||
).mapNotNull { menuIds[it] }
|
||||
|
||||
@@ -395,7 +454,7 @@ object SeedData {
|
||||
|
||||
private suspend fun disableObsoleteMenus(now: OffsetDateTime) = dbQuery {
|
||||
SysMenuTable.update({
|
||||
(SysMenuTable.name inList listOf("StatisticsInfoRoot")) and SysMenuTable.deletedAt.isNull()
|
||||
(SysMenuTable.name inList listOf("StatisticsInfoRoot", "SystemOrgs", "SystemDict", "LogsApiAccess")) and SysMenuTable.deletedAt.isNull()
|
||||
}) {
|
||||
it[SysMenuTable.visible] = false
|
||||
it[SysMenuTable.status] = "DISABLED"
|
||||
@@ -403,95 +462,6 @@ object SeedData {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Dicts
|
||||
// =========================================================
|
||||
|
||||
private data class SeedDict(val code: String, val name: String, val items: List<SeedDictItem>)
|
||||
private data class SeedDictItem(val label: String, val value: String, val color: String?, val sort: Int)
|
||||
|
||||
private suspend fun seedDicts(now: OffsetDateTime) {
|
||||
val statusItems = listOf(
|
||||
SeedDictItem("启用", "ENABLED", "green", 1),
|
||||
SeedDictItem("禁用", "DISABLED", "red", 2),
|
||||
)
|
||||
|
||||
val dicts = listOf(
|
||||
SeedDict("user_status", "用户状态", statusItems),
|
||||
SeedDict("org_status", "组织状态", statusItems),
|
||||
SeedDict("role_status", "角色状态", statusItems),
|
||||
SeedDict("menu_type", "菜单类型", listOf(
|
||||
SeedDictItem("目录", "CATALOG", "default", 1),
|
||||
SeedDictItem("菜单", "MENU", "blue", 2),
|
||||
SeedDictItem("按钮", "BUTTON", "orange", 3),
|
||||
)),
|
||||
SeedDict("log_status", "日志状态", statusItems),
|
||||
)
|
||||
|
||||
for (dict in dicts) {
|
||||
val typeId = upsertDictType(dict.code, dict.name, now)
|
||||
for (item in dict.items) {
|
||||
upsertDictItem(typeId, item.label, item.value, item.color, item.sort, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun upsertDictType(code: String, name: String, now: OffsetDateTime): Uuid = dbQuery {
|
||||
val existing = SysDictTypeTable.selectAll()
|
||||
.where { (SysDictTypeTable.code eq code) and SysDictTypeTable.deletedAt.isNull() }
|
||||
.singleOrNull()
|
||||
|
||||
if (existing != null) {
|
||||
val id = existing[SysDictTypeTable.id]
|
||||
SysDictTypeTable.update({ SysDictTypeTable.id eq id }) {
|
||||
it[SysDictTypeTable.name] = name
|
||||
it[SysDictTypeTable.status] = "ENABLED"
|
||||
it[SysDictTypeTable.updatedAt] = now
|
||||
}
|
||||
return@dbQuery id
|
||||
}
|
||||
|
||||
val inserted = SysDictTypeTable.insert {
|
||||
it[SysDictTypeTable.code] = code
|
||||
it[SysDictTypeTable.name] = name
|
||||
it[SysDictTypeTable.status] = "ENABLED"
|
||||
it[SysDictTypeTable.createdAt] = now
|
||||
}
|
||||
inserted[SysDictTypeTable.id]
|
||||
}
|
||||
|
||||
private suspend fun upsertDictItem(
|
||||
typeId: Uuid, label: String, value: String, color: String?, sort: Int, now: OffsetDateTime,
|
||||
) = dbQuery {
|
||||
val existing = SysDictItemTable.selectAll()
|
||||
.where {
|
||||
(SysDictItemTable.typeId eq typeId) and
|
||||
(SysDictItemTable.value eq value) and
|
||||
SysDictItemTable.deletedAt.isNull()
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
if (existing != null) {
|
||||
SysDictItemTable.update({ SysDictItemTable.id eq existing[SysDictItemTable.id] }) {
|
||||
it[SysDictItemTable.label] = label
|
||||
it[SysDictItemTable.color] = color
|
||||
it[SysDictItemTable.sort] = sort
|
||||
it[SysDictItemTable.status] = "ENABLED"
|
||||
it[SysDictItemTable.updatedAt] = now
|
||||
}
|
||||
return@dbQuery
|
||||
}
|
||||
|
||||
SysDictItemTable.insert {
|
||||
it[SysDictItemTable.typeId] = typeId
|
||||
it[SysDictItemTable.label] = label
|
||||
it[SysDictItemTable.value] = value
|
||||
it[SysDictItemTable.color] = color
|
||||
it[SysDictItemTable.sort] = sort
|
||||
it[SysDictItemTable.status] = "ENABLED"
|
||||
it[SysDictItemTable.createdAt] = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
@@ -514,6 +484,19 @@ private data class SeedMenu(
|
||||
val builtIn: Boolean = true,
|
||||
)
|
||||
|
||||
private data class EnterpriseSeed(
|
||||
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?,
|
||||
)
|
||||
|
||||
private fun rootMenu(key: String, title: String, name: String, path: String, component: String, icon: String, sort: Int): SeedMenu =
|
||||
SeedMenu(key, null, "MENU", title, name, path, component, icon, null, sort, visible = true, keepAlive = true)
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.bbit.ticket.utils.plugins
|
||||
|
||||
import com.bbit.ticket.utils.TraceIdKey
|
||||
import com.bbit.ticket.database.system.SysApiAccessLogTable
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.application.createApplicationPlugin
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.jwt.JWTPrincipal
|
||||
import io.ktor.server.auth.principal
|
||||
import io.ktor.server.request.httpMethod
|
||||
import io.ktor.server.request.path
|
||||
import io.ktor.util.AttributeKey
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
private val accessLogStartMarkKey = AttributeKey<TimeSource.Monotonic.ValueTimeMark>("api-access-start")
|
||||
private val accessLogWrittenKey = io.ktor.util.AttributeKey<Boolean>("api-access-written")
|
||||
|
||||
fun Application.configureApiAccessLog() {
|
||||
install(
|
||||
createApplicationPlugin("ApiAccessLogPlugin") {
|
||||
onCall { call ->
|
||||
if (!call.request.path().startsWith("/api/")) return@onCall
|
||||
call.attributes.put(accessLogStartMarkKey, TimeSource.Monotonic.markNow())
|
||||
}
|
||||
|
||||
// 统一记录 API 访问日志,业务操作日志由各模块在写操作成功/失败后补充。
|
||||
onCallRespond { call, _ ->
|
||||
if (!call.request.path().startsWith("/api/")) return@onCallRespond
|
||||
if (call.attributes.getOrNull(accessLogWrittenKey) == true) return@onCallRespond
|
||||
writeAccessLog(call, null)
|
||||
call.attributes.put(accessLogWrittenKey, true)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun writeAccessLog(
|
||||
call: ApplicationCall,
|
||||
errorMessage: String?,
|
||||
) = dbQuery {
|
||||
val startedAt = call.attributes.getOrNull(accessLogStartMarkKey)
|
||||
val costMs = startedAt?.elapsedNow()?.inWholeMilliseconds ?: 0L
|
||||
val traceId = call.attributes.getOrNull(TraceIdKey)
|
||||
val requestPath = call.request.path().take(255)
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val appKeyFromHeader = call.request.headers["X-App-Key"]?.take(100)
|
||||
val appNameFromHeader = call.request.headers["X-App-Name"]?.take(100)
|
||||
val appNameFromUser = principal?.payload?.getClaim("username")?.asString()?.take(100)
|
||||
val responseCode = call.response.status()?.value?.toString()
|
||||
val statusCode = call.response.status()?.value ?: 200
|
||||
val statusForStore = if (statusCode >= 400) "FAIL" else "SUCCESS"
|
||||
|
||||
SysApiAccessLogTable.insert {
|
||||
it[SysApiAccessLogTable.traceId] = traceId?.take(64)
|
||||
it[appKey] = appKeyFromHeader
|
||||
it[appName] = appNameFromHeader ?: appNameFromUser
|
||||
it[httpMethod] = call.request.httpMethod.value.take(20)
|
||||
it[SysApiAccessLogTable.requestPath] = requestPath
|
||||
it[requestHeaders] = maskedHeaders(call.request.headers.entries())
|
||||
it[requestBody] = null
|
||||
it[SysApiAccessLogTable.responseCode] = responseCode?.take(50)
|
||||
it[responseBody] = null
|
||||
it[ip] = call.request.local.remoteHost.take(64)
|
||||
it[SysApiAccessLogTable.status] = statusForStore
|
||||
it[SysApiAccessLogTable.errorMessage] = errorMessage?.take(500)
|
||||
it[SysApiAccessLogTable.costMs] = costMs
|
||||
it[createdAt] = OffsetDateTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
private fun maskedHeaders(entries: Set<Map.Entry<String, List<String>>>): String {
|
||||
if (entries.isEmpty()) return ""
|
||||
val content = entries.joinToString("&") { (key, values) ->
|
||||
val value = if (key.equals("Authorization", ignoreCase = true)) "***" else values.joinToString(",")
|
||||
"${key.lowercase()}=${value.take(120)}"
|
||||
}
|
||||
return content.take(2000)
|
||||
}
|
||||
+1
-10
@@ -1,5 +1,5 @@
|
||||
import http from '@/api/http'
|
||||
import type { ApiAccessLogPage, OperationLogPage } from '@/types/logs'
|
||||
import type { OperationLogPage } from '@/types/logs'
|
||||
|
||||
export function listOperationLogsApi(params: {
|
||||
page: number
|
||||
@@ -9,12 +9,3 @@ export function listOperationLogsApi(params: {
|
||||
}) {
|
||||
return http.get<never, OperationLogPage>('/logs/operation', { params })
|
||||
}
|
||||
|
||||
export function listApiAccessLogsApi(params: {
|
||||
page: number
|
||||
pageSize: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
}) {
|
||||
return http.get<never, ApiAccessLogPage>('/logs/api-access', { params })
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import http from '@/api/http'
|
||||
import type { DictItemPage, DictTypePage } from '@/types/system/dict'
|
||||
|
||||
export function listDictTypesApi(params: { page: number; pageSize: number; keyword?: string }) {
|
||||
return http.get<never, DictTypePage>('/system/dict-types', { params })
|
||||
}
|
||||
|
||||
export function createDictTypeApi(payload: {
|
||||
code: string
|
||||
name: string
|
||||
status: string
|
||||
remark?: string
|
||||
}) {
|
||||
return http.post<never, { id: string }>('/system/dict-types', payload)
|
||||
}
|
||||
|
||||
export function updateDictTypeApi(
|
||||
id: string,
|
||||
payload: { name: string; status: string; remark?: string }
|
||||
) {
|
||||
return http.put<never, void>(`/system/dict-types/${id}`, payload)
|
||||
}
|
||||
|
||||
export function deleteDictTypeApi(id: string) {
|
||||
return http.delete<never, void>(`/system/dict-types/${id}`)
|
||||
}
|
||||
|
||||
export function listDictItemsApi(params: { page: number; pageSize: number; typeId?: string }) {
|
||||
return http.get<never, DictItemPage>('/system/dict-items', { params })
|
||||
}
|
||||
|
||||
export function createDictItemApi(payload: {
|
||||
typeId: string
|
||||
label: string
|
||||
value: string
|
||||
color?: string
|
||||
sort: number
|
||||
status: string
|
||||
remark?: string
|
||||
}) {
|
||||
return http.post<never, { id: string }>('/system/dict-items', payload)
|
||||
}
|
||||
|
||||
export function updateDictItemApi(
|
||||
id: string,
|
||||
payload: {
|
||||
typeId: string
|
||||
label: string
|
||||
value: string
|
||||
color?: string
|
||||
sort: number
|
||||
status: string
|
||||
remark?: string
|
||||
}
|
||||
) {
|
||||
return http.put<never, void>(`/system/dict-items/${id}`, payload)
|
||||
}
|
||||
|
||||
export function deleteDictItemApi(id: string) {
|
||||
return http.delete<never, void>(`/system/dict-items/${id}`)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import http from '@/api/http'
|
||||
import type { EnterpriseManagePage, EnterpriseQuery } from '@/types/system/enterprise'
|
||||
|
||||
export function listEnterprisesApi(params: EnterpriseQuery) {
|
||||
return http.get<never, EnterpriseManagePage>('/system/enterprises', { params })
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import http from '@/api/http'
|
||||
import type { CreateOrgRequest, OrgTreeNode, UpdateOrgRequest } from '@/types/system/org'
|
||||
|
||||
export function listOrgsApi() {
|
||||
return http.get<never, OrgTreeNode[]>('/system/orgs')
|
||||
}
|
||||
|
||||
export function createOrgApi(payload: CreateOrgRequest) {
|
||||
return http.post<never, { id: string }>('/system/orgs', payload)
|
||||
}
|
||||
|
||||
export function updateOrgApi(id: string, payload: UpdateOrgRequest) {
|
||||
return http.put<never, void>(`/system/orgs/${id}`, payload)
|
||||
}
|
||||
|
||||
export function deleteOrgApi(id: string) {
|
||||
return http.delete<never, void>(`/system/orgs/${id}`)
|
||||
}
|
||||
@@ -108,12 +108,17 @@ function handleSelect(key: string) {
|
||||
}
|
||||
|
||||
:deep(.n-menu-item-content--selected) {
|
||||
color: var(--app-primary);
|
||||
font-weight: 600;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.n-menu-item-content--selected::before) {
|
||||
background: transparent;
|
||||
background: var(--app-primary-soft);
|
||||
}
|
||||
|
||||
:deep(.n-menu-item-content:hover::before) {
|
||||
background: var(--app-primary-subtle);
|
||||
}
|
||||
|
||||
:deep(.n-menu--collapsed .n-menu-item-content),
|
||||
|
||||
@@ -41,7 +41,7 @@ function onClose(path: string) {
|
||||
.tabs-shell {
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eef1f5;
|
||||
border-top: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.tabs-track {
|
||||
@@ -64,9 +64,9 @@ function onClose(path: string) {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
color: var(--app-text-muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.16s ease,
|
||||
@@ -75,13 +75,15 @@ function onClose(path: string) {
|
||||
}
|
||||
|
||||
.tab-chip:hover {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
background: var(--app-primary-soft);
|
||||
color: var(--app-primary);
|
||||
}
|
||||
|
||||
.tab-chip-active {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
background: var(--app-primary-soft);
|
||||
color: var(--app-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div class="page-shell dashboard-page">
|
||||
<div class="dashboard-grid">
|
||||
<div class="soft-stat">
|
||||
<n-statistic label="可见菜单" :value="visibleMenuTotal" />
|
||||
@@ -17,33 +17,134 @@
|
||||
<n-statistic label="已打开页签" :value="authStore.tabs.length" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-shortcut-grid">
|
||||
<button
|
||||
v-for="menu in shortcutMenus"
|
||||
:key="menu.id"
|
||||
class="soft-stat menu-shortcut"
|
||||
type="button"
|
||||
@click="openMenu(menu)"
|
||||
>
|
||||
<span class="shortcut-icon">
|
||||
<n-icon :size="22">
|
||||
<component :is="resolveIcon(menu.icon)" />
|
||||
</n-icon>
|
||||
</span>
|
||||
<span class="shortcut-content">
|
||||
<span class="shortcut-title">{{ menu.title }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, type Component } from 'vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import * as LucideIcons from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { statusLabel } from '@/utils/display'
|
||||
import type { MenuNode } from '@/types/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
function flattenMenus(nodes: MenuNode[]): MenuNode[] {
|
||||
return nodes.flatMap((item) => [item, ...flattenMenus(item.children ?? [])])
|
||||
}
|
||||
|
||||
function resolveIcon(icon?: string | null) {
|
||||
return (
|
||||
(icon ? (LucideIcons[icon as keyof typeof LucideIcons] as unknown as Component | undefined) : undefined) ??
|
||||
LucideIcons.PanelRightOpen
|
||||
)
|
||||
}
|
||||
|
||||
function openMenu(menu: MenuNode) {
|
||||
if (menu.path) {
|
||||
router.push(menu.path)
|
||||
}
|
||||
}
|
||||
|
||||
const visibleMenus = computed(() => flattenMenus(authStore.menus).filter((item) => item.visible))
|
||||
const visibleMenuTotal = computed(() => visibleMenus.value.length)
|
||||
const visiblePageTotal = computed(
|
||||
() => visibleMenus.value.filter((item) => item.type === 'MENU' && item.path).length
|
||||
)
|
||||
const shortcutMenus = computed(() =>
|
||||
visibleMenus.value.filter((item) => item.type === 'MENU' && Boolean(item.path))
|
||||
)
|
||||
const currentStatus = computed(() => statusLabel(authStore.user?.status))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-page {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.menu-shortcut-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.menu-shortcut {
|
||||
width: 100%;
|
||||
min-height: 92px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
color: #111827;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.16s ease,
|
||||
box-shadow 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
}
|
||||
|
||||
.menu-shortcut:hover {
|
||||
border-color: #94a3b8;
|
||||
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.menu-shortcut:focus-visible {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.shortcut-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex: 0 0 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
color: #2563eb;
|
||||
background: var(--app-primary-soft);
|
||||
}
|
||||
|
||||
.shortcut-content {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shortcut-title {
|
||||
overflow: hidden;
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<n-form inline :show-label="false" class="toolbar-form">
|
||||
<n-form-item>
|
||||
<n-input v-model:value="query.keyword" clearable placeholder="应用/路径" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-select
|
||||
v-model:value="query.status"
|
||||
clearable
|
||||
placeholder="状态"
|
||||
:options="statusOptions"
|
||||
style="width: 140px"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="refetch">查询</n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="apiAccessLogQuery.isLoading.value"
|
||||
:pagination="pagination"
|
||||
:row-key="(row: ApiAccessLogItem) => row.id"
|
||||
@update:page="onPage"
|
||||
@update:page-size="onPageSize"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, reactive } from 'vue'
|
||||
import { NTag, type DataTableColumns } from 'naive-ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { listApiAccessLogsApi } from '@/api/logs'
|
||||
import type { ApiAccessLogItem } from '@/types/logs'
|
||||
import { statusLabel, statusTagType } from '@/utils/display'
|
||||
import { renderPagePrefix } from '@/utils/pagination'
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
status: null as string | null
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '成功', value: 'SUCCESS' },
|
||||
{ label: '失败', value: 'FAIL' }
|
||||
]
|
||||
|
||||
const apiAccessLogQuery = useQuery({
|
||||
queryKey: computed(() => ['logs', 'api-access', { ...query }]),
|
||||
queryFn: () =>
|
||||
listApiAccessLogsApi({
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
keyword: query.keyword || undefined,
|
||||
status: query.status || undefined
|
||||
})
|
||||
})
|
||||
|
||||
const rows = computed(() => apiAccessLogQuery.data.value?.items ?? [])
|
||||
|
||||
const pagination = computed(() => ({
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
itemCount: apiAccessLogQuery.data.value?.total ?? 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50],
|
||||
prefix: renderPagePrefix
|
||||
}))
|
||||
|
||||
function refetch() {
|
||||
query.page = 1
|
||||
apiAccessLogQuery.refetch()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
query.keyword = ''
|
||||
query.status = null
|
||||
query.page = 1
|
||||
apiAccessLogQuery.refetch()
|
||||
}
|
||||
|
||||
function onPage(page: number) {
|
||||
query.page = page
|
||||
}
|
||||
|
||||
function onPageSize(pageSize: number) {
|
||||
query.pageSize = pageSize
|
||||
query.page = 1
|
||||
}
|
||||
|
||||
const columns = computed<DataTableColumns<ApiAccessLogItem>>(() => [
|
||||
{ title: '时间', key: 'createdAt', width: 170 },
|
||||
{
|
||||
title: '应用',
|
||||
key: 'appName',
|
||||
minWidth: 160,
|
||||
render: (row) =>
|
||||
h('div', { class: 'leading-5' }, [
|
||||
h('div', { class: 'font-medium text-slate-900' }, row.appName || '-'),
|
||||
h('div', { class: 'text-xs text-slate-500' }, row.appKey || '未提供 AppKey')
|
||||
])
|
||||
},
|
||||
{ title: '方法', key: 'httpMethod', width: 90 },
|
||||
{ title: '路径', key: 'requestPath', minWidth: 240 },
|
||||
{ title: '响应码', key: 'responseCode', width: 100, render: (row) => row.responseCode || '-' },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 90,
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: statusTagType(row.status), size: 'small' },
|
||||
{ default: () => statusLabel(row.status) }
|
||||
)
|
||||
},
|
||||
{ title: '耗时', key: 'costMs', width: 90, render: (row) => `${row.costMs} ms` },
|
||||
{
|
||||
title: '错误信息',
|
||||
key: 'errorMessage',
|
||||
minWidth: 180,
|
||||
render: (row) => row.errorMessage || '-'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -74,10 +74,23 @@
|
||||
</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="请输入短信验证码" />
|
||||
<n-modal
|
||||
v-model:show="showSms"
|
||||
preset="card"
|
||||
title="短信登录"
|
||||
:style="{ width: '360px', maxWidth: '88vw' }"
|
||||
content-style="padding: 14px 18px 18px"
|
||||
>
|
||||
<n-space vertical size="small" class="auth-panel">
|
||||
<div class="auth-account">
|
||||
<span>税局账号</span>
|
||||
<strong>{{ selected?.account || '-' }}</strong>
|
||||
</div>
|
||||
<n-alert v-if="smsTip" type="info" :show-icon="false">{{ smsTip }}</n-alert>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="smsCode" placeholder="请输入短信验证码" />
|
||||
<n-button :loading="smsCodeLoading" @click="sendSmsCode">发送验证码</n-button>
|
||||
</n-input-group>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="showSms = false">取消</n-button>
|
||||
<n-button type="primary" :loading="smsLoading" @click="submitSms">确认登录</n-button>
|
||||
@@ -85,12 +98,36 @@
|
||||
</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
|
||||
v-model:show="showQrcode"
|
||||
preset="card"
|
||||
title="风险认证"
|
||||
:style="{ width: '400px', maxWidth: '88vw' }"
|
||||
content-style="padding: 14px 18px 18px"
|
||||
>
|
||||
<n-space vertical size="small" class="auth-panel">
|
||||
<div class="auth-account">
|
||||
<span>税局账号</span>
|
||||
<strong>{{ selected?.account || '-' }}</strong>
|
||||
</div>
|
||||
<n-radio-group v-model:value="qrcodeType" size="small" @update:value="loadQrcode">
|
||||
<n-radio-button value="1">电子税务局 APP</n-radio-button>
|
||||
<n-radio-button value="2">国家网络身份认证 APP</n-radio-button>
|
||||
</n-radio-group>
|
||||
<div class="qrcode-wrap">
|
||||
<img v-if="qrcodeImg" :src="qrcodeImg" :alt="qrcodeTypeLabel" />
|
||||
<n-empty v-else description="暂无二维码" />
|
||||
</div>
|
||||
<div class="qrcode-meta">
|
||||
<span>{{ qrcodeTypeLabel }}</span>
|
||||
<small v-if="qrcodeGenTime">生成时间:{{ qrcodeGenTime }}</small>
|
||||
<small v-if="qrcodeTip">{{ qrcodeTip }}</small>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="showQrcode = false">关闭</n-button>
|
||||
<n-button type="primary" :loading="qrLoading" @click="loadQrcode">刷新二维码</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -109,11 +146,15 @@ import {
|
||||
NGrid,
|
||||
NIcon,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NModal,
|
||||
NPopconfirm,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NSpace,
|
||||
NTag,
|
||||
NTooltip,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { KeyRound, Plus, RefreshCw, ShieldCheck } from 'lucide-vue-next'
|
||||
@@ -138,15 +179,22 @@ const showCreate = ref(false)
|
||||
const showSms = ref(false)
|
||||
const showQrcode = ref(false)
|
||||
const smsLoading = ref(false)
|
||||
const smsCodeLoading = ref(false)
|
||||
const qrLoading = ref(false)
|
||||
const smsCode = ref('')
|
||||
const smsTip = ref('')
|
||||
const qrcodeImg = ref('')
|
||||
const qrcodeType = ref('1')
|
||||
const qrcodeGenTime = ref('')
|
||||
const qrcodeTip = ref('')
|
||||
const selected = ref<DigitalAccountItem | null>(null)
|
||||
const createFormRef = ref<FormInst | null>(null)
|
||||
|
||||
const canManage = computed(() => authStore.user?.userType !== 'DIGITAL_OPERATOR')
|
||||
const canUpdateStatus = computed(() => authStore.hasPermission('digital-account:status'))
|
||||
const qrcodeTypeLabel = computed(() =>
|
||||
qrcodeType.value === '2' ? '国家网络身份认证 APP' : '电子税务局 APP'
|
||||
)
|
||||
|
||||
const createForm = reactive({
|
||||
account: '',
|
||||
@@ -175,7 +223,13 @@ const identityOptions = [
|
||||
|
||||
const columns: DataTableColumns<DigitalAccountItem> = [
|
||||
{ title: '税局账号', key: 'account', minWidth: 140 },
|
||||
{ title: '平台登录账号', key: 'platformUsername', minWidth: 180, render: (row) => row.platformUsername || '-' },
|
||||
{
|
||||
title: '平台登录账号',
|
||||
key: 'platformUsername',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (row) => renderEllipsisText(row.platformUsername)
|
||||
},
|
||||
{ title: '姓名', key: 'name', minWidth: 100 },
|
||||
{ title: '登录身份', key: 'identityType', render: (row) => identityLabel(row.identityType) },
|
||||
{ title: '账号状态', key: 'authStatus', render: (row) => authStatusLabel(row.authStatus) },
|
||||
@@ -214,8 +268,9 @@ const columns: DataTableColumns<DigitalAccountItem> = [
|
||||
{
|
||||
title: 'API Key',
|
||||
key: 'apiKey',
|
||||
minWidth: 180,
|
||||
render: (row) => row.apiKey || '-'
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (row) => renderEllipsisText(row.apiKey)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -282,6 +337,18 @@ function authStatusLabel(value?: string | null) {
|
||||
return value ? map[value] || value : '-'
|
||||
}
|
||||
|
||||
function renderEllipsisText(value?: string | null) {
|
||||
if (!value) return '-'
|
||||
return h(
|
||||
NTooltip,
|
||||
{ trigger: 'hover' },
|
||||
{
|
||||
trigger: () => h('span', { class: 'cell-ellipsis' }, value),
|
||||
default: () => value
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -324,13 +391,21 @@ async function updateAccountStatus(row: DigitalAccountItem) {
|
||||
async function openSms(row: DigitalAccountItem) {
|
||||
selected.value = row
|
||||
smsCode.value = ''
|
||||
smsLoading.value = true
|
||||
smsTip.value = ''
|
||||
showSms.value = true
|
||||
}
|
||||
|
||||
async function sendSmsCode() {
|
||||
if (!selected.value) return
|
||||
smsCodeLoading.value = true
|
||||
try {
|
||||
const res = await sendLoginSmsCodeApi({ taxpayerNum: row.taxpayerNum, account: row.account })
|
||||
const res = await sendLoginSmsCodeApi({
|
||||
taxpayerNum: selected.value.taxpayerNum,
|
||||
account: selected.value.account
|
||||
})
|
||||
smsTip.value = res.phoneNum ? `验证码已发送至 ${res.phoneNum}` : res.resultMsg
|
||||
} finally {
|
||||
smsLoading.value = false
|
||||
smsCodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +428,10 @@ async function submitSms() {
|
||||
|
||||
async function openQrcode(row: DigitalAccountItem) {
|
||||
selected.value = row
|
||||
qrcodeType.value = '1'
|
||||
qrcodeImg.value = ''
|
||||
qrcodeGenTime.value = ''
|
||||
qrcodeTip.value = ''
|
||||
showQrcode.value = true
|
||||
await loadQrcode()
|
||||
}
|
||||
@@ -361,8 +440,11 @@ async function loadQrcode() {
|
||||
if (!selected.value) return
|
||||
qrLoading.value = true
|
||||
try {
|
||||
const res = await getAuthQrcodeApi('1', selected.value.id)
|
||||
const res = await getAuthQrcodeApi(qrcodeType.value, selected.value.id)
|
||||
qrcodeImg.value = res.qrcodeImg ? `data:image/png;base64,${res.qrcodeImg}` : ''
|
||||
qrcodeType.value = res.qrcodeType || qrcodeType.value
|
||||
qrcodeGenTime.value = res.genTime || ''
|
||||
qrcodeTip.value = res.resultMsg || ''
|
||||
} finally {
|
||||
qrLoading.value = false
|
||||
}
|
||||
@@ -388,27 +470,81 @@ onMounted(load)
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cell-ellipsis {
|
||||
display: inline-block;
|
||||
width: 148px;
|
||||
max-width: 148px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: min(720px, 92vw);
|
||||
}
|
||||
|
||||
.sms-card {
|
||||
width: min(420px, 92vw);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eef1f5;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #eef1f5;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.auth-account span {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.auth-account strong {
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.qrcode-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
min-height: 236px;
|
||||
border: 1px solid #eef1f5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.qrcode-wrap img {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.qrcode-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-meta span {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<!-- ===== 顶部工具栏:操作按钮 ===== -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-actions">
|
||||
<div class="page-shell invoice-issue-page">
|
||||
<section class="page-card invoice-issue-card">
|
||||
<!-- ===== 顶部工具栏:操作按钮 ===== -->
|
||||
<div class="page-toolbar invoice-toolbar">
|
||||
<h2 class="page-toolbar-title">开具蓝票</h2>
|
||||
<div class="page-toolbar-actions toolbar-actions">
|
||||
<n-button type="primary" @click="addItem">新增商品</n-button>
|
||||
<n-button type="primary" @click="addVariableLevyProof">新增差额征税凭证明细</n-button>
|
||||
<n-button type="primary" @click="showOrderNoDialog">新增单据号</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ===== 主体:左栏 + 右栏 ===== -->
|
||||
<main class="main">
|
||||
<!-- ===== 主体:左栏 + 右栏 ===== -->
|
||||
<main class="main card-body card-body-fill">
|
||||
<!-- 左栏:导航列表 -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-head">
|
||||
@@ -879,7 +881,8 @@
|
||||
</section>
|
||||
</n-form>
|
||||
</section>
|
||||
</main>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<!-- ===== 新增单据号弹窗 ===== -->
|
||||
<n-modal
|
||||
@@ -1728,41 +1731,38 @@ function resetForm() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ===== 全屏布局 ===== */
|
||||
.page {
|
||||
height: 100%;
|
||||
.invoice-issue-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.invoice-issue-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f7f8fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===== 主体 ===== */
|
||||
.main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background: var(--app-surface);
|
||||
}
|
||||
|
||||
/* ===== 左栏 ===== */
|
||||
.sidebar {
|
||||
width: 204px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
background: var(--app-surface);
|
||||
border-right: 1px solid var(--app-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ===== 顶部工具栏 ===== */
|
||||
.toolbar {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 5px 14px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
.invoice-toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
@@ -1775,17 +1775,17 @@ function resetForm() {
|
||||
.sidebar-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
min-height: 44px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #999;
|
||||
color: var(--app-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.sidebar-body {
|
||||
@@ -1796,15 +1796,15 @@ function resetForm() {
|
||||
|
||||
.sidebar-foot {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 7px 12px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
border-left: 3px solid transparent;
|
||||
@@ -1812,12 +1812,12 @@ function resetForm() {
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #f5f5f5;
|
||||
background: var(--app-primary-subtle);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #f0f5ff;
|
||||
border-left-color: #409eff;
|
||||
background: var(--app-primary-soft);
|
||||
border-left-color: var(--app-primary);
|
||||
}
|
||||
|
||||
.nav-item-body {
|
||||
@@ -1830,25 +1830,25 @@ function resetForm() {
|
||||
|
||||
.nav-icon {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
color: var(--app-text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-icon {
|
||||
color: #409eff;
|
||||
color: var(--app-primary);
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
color: var(--app-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-label {
|
||||
color: #409eff;
|
||||
color: var(--app-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1877,7 +1877,7 @@ function resetForm() {
|
||||
|
||||
.nav-divider {
|
||||
height: 1px;
|
||||
background: #eee;
|
||||
background: var(--app-border);
|
||||
margin: 4px 12px;
|
||||
}
|
||||
|
||||
@@ -1885,25 +1885,27 @@ function resetForm() {
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* ===== Section 卡片 ===== */
|
||||
.section {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 12px;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
background: #fbfcfe;
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
@@ -1924,20 +1926,20 @@ function resetForm() {
|
||||
}
|
||||
|
||||
.section-badge.optional {
|
||||
color: #096dd9;
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #bae7ff;
|
||||
color: var(--app-primary);
|
||||
background: var(--app-primary-soft);
|
||||
border: 1px solid var(--app-border-strong);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 620;
|
||||
color: #222;
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.section-hint {
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
color: var(--app-text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -1989,7 +1991,7 @@ function resetForm() {
|
||||
width: 100%;
|
||||
max-height: 190px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.sidebar-body {
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<span class="text-sm font-semibold text-slate-700">字典类型</span>
|
||||
<n-button size="small" type="primary" @click="openTypeCreate">新增类型</n-button>
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="typeColumns"
|
||||
:data="typeRows"
|
||||
:pagination="typePagination"
|
||||
remote
|
||||
@update:page="onTypePage"
|
||||
@update:page-size="onTypePageSize"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<span class="text-sm font-semibold text-slate-700">字典项</span>
|
||||
<n-button size="small" type="primary" :disabled="!selectedTypeId" @click="openItemCreate"
|
||||
>新增字典项</n-button
|
||||
>
|
||||
</div>
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
:columns="itemColumns"
|
||||
:data="itemRows"
|
||||
:pagination="itemPagination"
|
||||
remote
|
||||
@update:page="onItemPage"
|
||||
@update:page-size="onItemPageSize"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, reactive, ref, watch } from 'vue'
|
||||
import { NButton, NPopconfirm, NTag, NSpace, useMessage, type DataTableColumns } from 'naive-ui'
|
||||
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||
import {
|
||||
createDictItemApi,
|
||||
createDictTypeApi,
|
||||
deleteDictItemApi,
|
||||
deleteDictTypeApi,
|
||||
listDictItemsApi,
|
||||
listDictTypesApi
|
||||
} from '@/api/system/dict'
|
||||
import type { DictItem, DictTypeItem } from '@/types/system/dict'
|
||||
import { statusLabel, statusTagType } from '@/utils/display'
|
||||
import { renderPagePrefix } from '@/utils/pagination'
|
||||
|
||||
const message = useMessage()
|
||||
const selectedTypeId = ref<string>('')
|
||||
const typeQuery = reactive({ page: 1, pageSize: 10, keyword: '' })
|
||||
const itemQuery = reactive({ page: 1, pageSize: 10 })
|
||||
const typesReq = useQuery({
|
||||
queryKey: computed(() => ['dict-types', { ...typeQuery }]),
|
||||
queryFn: () => listDictTypesApi({ ...typeQuery, keyword: typeQuery.keyword || undefined })
|
||||
})
|
||||
const itemsReq = useQuery({
|
||||
queryKey: computed(() => ['dict-items', selectedTypeId.value, { ...itemQuery }]),
|
||||
queryFn: () => listDictItemsApi({ ...itemQuery, typeId: selectedTypeId.value || undefined })
|
||||
})
|
||||
const typeRows = computed(() => typesReq.data.value?.items ?? [])
|
||||
const itemRows = computed(() => itemsReq.data.value?.items ?? [])
|
||||
const typePagination = computed(() => ({
|
||||
page: typeQuery.page,
|
||||
pageSize: typeQuery.pageSize,
|
||||
itemCount: typesReq.data.value?.total ?? 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20],
|
||||
prefix: renderPagePrefix
|
||||
}))
|
||||
const itemPagination = computed(() => ({
|
||||
page: itemQuery.page,
|
||||
pageSize: itemQuery.pageSize,
|
||||
itemCount: itemsReq.data.value?.total ?? 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20],
|
||||
prefix: renderPagePrefix
|
||||
}))
|
||||
watch(
|
||||
typeRows,
|
||||
(v) => {
|
||||
if (!selectedTypeId.value && v.length > 0) selectedTypeId.value = v[0].id
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const createTypeMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createDictTypeApi({
|
||||
code: `TYPE_${Date.now()}`,
|
||||
name: `新类型${Date.now().toString().slice(-4)}`,
|
||||
status: 'ENABLED'
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
message.success('新增类型成功')
|
||||
await typesReq.refetch()
|
||||
}
|
||||
})
|
||||
const createItemMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createDictItemApi({
|
||||
typeId: selectedTypeId.value,
|
||||
label: `新项${Date.now().toString().slice(-4)}`,
|
||||
value: `${Date.now()}`,
|
||||
sort: 0,
|
||||
status: 'ENABLED'
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
message.success('新增字典项成功')
|
||||
await itemsReq.refetch()
|
||||
}
|
||||
})
|
||||
const deleteTypeMutation = useMutation({
|
||||
mutationFn: deleteDictTypeApi,
|
||||
onSuccess: async () => {
|
||||
message.success('删除成功')
|
||||
selectedTypeId.value = ''
|
||||
await typesReq.refetch()
|
||||
await itemsReq.refetch()
|
||||
}
|
||||
})
|
||||
const deleteItemMutation = useMutation({
|
||||
mutationFn: deleteDictItemApi,
|
||||
onSuccess: async () => {
|
||||
message.success('删除成功')
|
||||
await itemsReq.refetch()
|
||||
}
|
||||
})
|
||||
|
||||
function openTypeCreate() {
|
||||
createTypeMutation.mutate()
|
||||
}
|
||||
function openItemCreate() {
|
||||
if (!selectedTypeId.value) return
|
||||
createItemMutation.mutate()
|
||||
}
|
||||
function onTypePage(p: number) {
|
||||
typeQuery.page = p
|
||||
}
|
||||
function onTypePageSize(s: number) {
|
||||
typeQuery.pageSize = s
|
||||
typeQuery.page = 1
|
||||
}
|
||||
function onItemPage(p: number) {
|
||||
itemQuery.page = p
|
||||
}
|
||||
function onItemPageSize(s: number) {
|
||||
itemQuery.pageSize = s
|
||||
itemQuery.page = 1
|
||||
}
|
||||
|
||||
const typeColumns = computed<DataTableColumns<DictTypeItem>>(() => [
|
||||
{ title: '编码', key: 'code' },
|
||||
{
|
||||
title: '名称',
|
||||
key: 'name',
|
||||
render: (r) => h('a', { onClick: () => (selectedTypeId.value = r.id) }, r.name)
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
render: (r) =>
|
||||
h(
|
||||
NTag,
|
||||
{ size: 'small', type: statusTagType(r.status) },
|
||||
{ default: () => statusLabel(r.status) }
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (r) =>
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => deleteTypeMutation.mutate(r.id) },
|
||||
{
|
||||
trigger: () =>
|
||||
h(NButton, { size: 'tiny', tertiary: true, type: 'error' }, { default: () => '删除' }),
|
||||
default: () => '确认删除?'
|
||||
}
|
||||
)
|
||||
}
|
||||
])
|
||||
const itemColumns = computed<DataTableColumns<DictItem>>(() => [
|
||||
{ title: '标签', key: 'label' },
|
||||
{ title: '值', key: 'value' },
|
||||
{
|
||||
title: '颜色',
|
||||
key: 'color',
|
||||
render: (r) =>
|
||||
r.color
|
||||
? h(NTag, { size: 'small', color: { color: r.color } }, { default: () => r.color })
|
||||
: '-'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
render: (r) =>
|
||||
h(
|
||||
NTag,
|
||||
{ size: 'small', type: statusTagType(r.status) },
|
||||
{ default: () => statusLabel(r.status) }
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (r) =>
|
||||
h(NSpace, { size: 6 }, () => [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => deleteItemMutation.mutate(r.id) },
|
||||
{
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'tiny', tertiary: true, type: 'error' },
|
||||
{ default: () => '删除' }
|
||||
),
|
||||
default: () => '确认删除?'
|
||||
}
|
||||
)
|
||||
])
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<n-form inline :show-label="false" class="toolbar-form">
|
||||
<n-form-item>
|
||||
<n-input v-model:value="queryForm.keyword" clearable placeholder="企业名称 / 税号" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">查询</n-button>
|
||||
<n-button @click="handleReset">重置</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
flex-height
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="tableRows"
|
||||
:loading="enterpriseQuery.isLoading.value"
|
||||
:pagination="pagination"
|
||||
:row-key="(row: EnterpriseManageItem) => row.id"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, reactive } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NSpace, NTag } from 'naive-ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { listEnterprisesApi } from '@/api/system/enterprise'
|
||||
import type { EnterpriseManageItem } from '@/types/system/enterprise'
|
||||
import { renderPagePrefix } from '@/utils/pagination'
|
||||
|
||||
const queryForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const pager = reactive({
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
|
||||
const enterpriseQuery = useQuery({
|
||||
queryKey: computed(() => ['system', 'enterprises', { ...queryForm, ...pager }]),
|
||||
queryFn: () =>
|
||||
listEnterprisesApi({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
keyword: queryForm.keyword || undefined
|
||||
})
|
||||
})
|
||||
|
||||
const tableRows = computed(() => enterpriseQuery.data.value?.items ?? [])
|
||||
|
||||
const pagination = computed(() => ({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
itemCount: enterpriseQuery.data.value?.total ?? 0,
|
||||
pageSizes: [10, 20, 50],
|
||||
showSizePicker: true,
|
||||
prefix: renderPagePrefix
|
||||
}))
|
||||
|
||||
const columns: DataTableColumns<EnterpriseManageItem> = [
|
||||
{
|
||||
title: '企业信息',
|
||||
key: 'enterpriseName',
|
||||
minWidth: 260,
|
||||
render: (row) =>
|
||||
h('div', { class: 'leading-5' }, [
|
||||
h('div', { class: 'font-medium text-slate-900' }, row.enterpriseName),
|
||||
h('div', { class: 'text-xs text-slate-500' }, row.taxpayerNum)
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
key: 'contactsName',
|
||||
minWidth: 160,
|
||||
render: (row) => row.contactsName || row.legalPersonName || '-'
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
key: 'contactsPhone',
|
||||
minWidth: 140,
|
||||
render: (row) => row.contactsPhone || '-'
|
||||
},
|
||||
{
|
||||
title: '地区',
|
||||
key: 'cityName',
|
||||
minWidth: 160,
|
||||
render: (row) => [row.regionCode, row.cityName].filter(Boolean).join(' / ') || '-'
|
||||
},
|
||||
{
|
||||
title: '票通状态',
|
||||
key: 'serviceStatus',
|
||||
width: 140,
|
||||
render: (row) =>
|
||||
h(NSpace, { size: 6 }, () => [
|
||||
h(NTag, { size: 'small' }, { default: () => row.reviewStatus || '-' }),
|
||||
h(NTag, { size: 'small' }, { default: () => row.serviceStatus || '-' })
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
key: 'createdAt',
|
||||
minWidth: 190,
|
||||
render: (row) => row.createdAt
|
||||
}
|
||||
]
|
||||
|
||||
function onPageChange(page: number) {
|
||||
pager.page = page
|
||||
}
|
||||
|
||||
function onPageSizeChange(pageSize: number) {
|
||||
pager.pageSize = pageSize
|
||||
pager.page = 1
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pager.page = 1
|
||||
enterpriseQuery.refetch()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryForm.keyword = ''
|
||||
pager.page = 1
|
||||
enterpriseQuery.refetch()
|
||||
}
|
||||
</script>
|
||||
@@ -126,7 +126,6 @@ const iconOptions = [
|
||||
{ label: '组织', value: 'Building2' },
|
||||
{ label: '盾牌', value: 'Shield' },
|
||||
{ label: '侧栏', value: 'PanelLeft' },
|
||||
{ label: '字典', value: 'BookType' },
|
||||
{ label: '日志', value: 'Logs' },
|
||||
{ label: '文本日志', value: 'ScrollText' },
|
||||
{ label: '接口链路', value: 'Waypoints' },
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<span class="text-sm font-semibold text-slate-700">组织架构</span>
|
||||
<n-button type="primary" @click="openCreate()">新增组织</n-button>
|
||||
</div>
|
||||
<div class="card-body card-body-fill tree-card-body">
|
||||
<n-tree
|
||||
block-line
|
||||
expand-on-click
|
||||
:data="treeData"
|
||||
key-field="id"
|
||||
label-field="name"
|
||||
children-field="children"
|
||||
:render-switcher-icon="renderSwitcherIcon"
|
||||
:render-label="renderLabel"
|
||||
:expanded-keys="expandedKeys"
|
||||
@update:expanded-keys="onExpanded"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-modal
|
||||
v-model:show="modal.visible"
|
||||
preset="card"
|
||||
:title="modal.mode === 'create' ? '新增组织' : '编辑组织'"
|
||||
class="w-[520px]"
|
||||
>
|
||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="left" label-width="90">
|
||||
<n-form-item label="上级组织">
|
||||
<n-tree-select
|
||||
v-model:value="form.parentId"
|
||||
clearable
|
||||
:options="treeSelectOptions"
|
||||
placeholder="可选"
|
||||
key-field="id"
|
||||
label-field="name"
|
||||
children-field="children"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="组织名称" path="name"
|
||||
><n-input v-model:value="form.name"
|
||||
/></n-form-item>
|
||||
<n-form-item label="组织编码" path="code"
|
||||
><n-input v-model:value="form.code" :disabled="modal.mode === 'edit'"
|
||||
/></n-form-item>
|
||||
<n-form-item label="排序"
|
||||
><n-input-number v-model:value="form.sort" :min="0"
|
||||
/></n-form-item>
|
||||
<n-form-item label="状态">
|
||||
<n-radio-group v-model:value="form.status">
|
||||
<n-radio-button value="ENABLED">启用</n-radio-button>
|
||||
<n-radio-button value="DISABLED">禁用</n-radio-button>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<n-button @click="modal.visible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saveMutation.isPending.value" @click="save"
|
||||
>保存</n-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
import type { FormInst, FormRules, TreeOption } from 'naive-ui'
|
||||
import { NButton, NPopconfirm, NSpace, NTag, useMessage } from 'naive-ui'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||
import { createOrgApi, deleteOrgApi, listOrgsApi, updateOrgApi } from '@/api/system/org'
|
||||
import type { OrgTreeNode } from '@/types/system/org'
|
||||
import { statusLabel, statusTagType } from '@/utils/display'
|
||||
|
||||
const message = useMessage()
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const expandedKeys = ref<string[]>([])
|
||||
|
||||
const modal = reactive({ visible: false, mode: 'create' as 'create' | 'edit', id: '' })
|
||||
const form = reactive({
|
||||
parentId: null as string | null,
|
||||
name: '',
|
||||
code: '',
|
||||
sort: 0,
|
||||
status: 'ENABLED'
|
||||
})
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '请输入组织名称', trigger: ['blur', 'input'] }],
|
||||
code: [{ required: true, message: '请输入组织编码', trigger: ['blur', 'input'] }]
|
||||
}
|
||||
|
||||
const orgQuery = useQuery({
|
||||
queryKey: ['system', 'orgs'],
|
||||
queryFn: listOrgsApi
|
||||
})
|
||||
|
||||
const treeData = computed(() => orgQuery.data.value ?? [])
|
||||
const treeSelectOptions = computed(() => treeData.value as unknown as TreeOption[])
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const payload = {
|
||||
parentId: form.parentId ?? undefined,
|
||||
name: form.name.trim(),
|
||||
code: form.code.trim(),
|
||||
sort: form.sort,
|
||||
status: form.status
|
||||
}
|
||||
if (modal.mode === 'create') return createOrgApi(payload)
|
||||
return updateOrgApi(modal.id, payload)
|
||||
},
|
||||
onSuccess: async () => {
|
||||
message.success('保存成功')
|
||||
modal.visible = false
|
||||
await orgQuery.refetch()
|
||||
expandedKeys.value = collectAllKeys(treeData.value)
|
||||
}
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: deleteOrgApi,
|
||||
onSuccess: async () => {
|
||||
message.success('删除成功')
|
||||
await orgQuery.refetch()
|
||||
}
|
||||
})
|
||||
|
||||
function openCreate(parentId?: string) {
|
||||
modal.mode = 'create'
|
||||
modal.id = ''
|
||||
form.parentId = parentId ?? null
|
||||
form.name = ''
|
||||
form.code = ''
|
||||
form.sort = 0
|
||||
form.status = 'ENABLED'
|
||||
modal.visible = true
|
||||
}
|
||||
|
||||
function openEdit(node: OrgTreeNode) {
|
||||
modal.mode = 'edit'
|
||||
modal.id = node.id
|
||||
form.parentId = node.parentId ?? null
|
||||
form.name = node.name
|
||||
form.code = node.code
|
||||
form.sort = node.sort
|
||||
form.status = node.status
|
||||
modal.visible = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
await formRef.value?.validate()
|
||||
await saveMutation.mutateAsync()
|
||||
}
|
||||
|
||||
function renderLabel(payload: { option: unknown }) {
|
||||
const node = payload.option as OrgTreeNode
|
||||
return h('div', { class: 'flex w-full items-center justify-between py-2' }, [
|
||||
h('div', { class: 'flex items-center gap-2' }, [
|
||||
h('span', node.name),
|
||||
h(
|
||||
NTag,
|
||||
{ size: 'small', type: statusTagType(node.status) },
|
||||
{ default: () => statusLabel(node.status) }
|
||||
)
|
||||
]),
|
||||
h(NSpace, { size: 6 }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', tertiary: true, onClick: () => openCreate(node.id) },
|
||||
{ default: () => '新增子级' }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', tertiary: true, type: 'primary', onClick: () => openEdit(node) },
|
||||
{ default: () => '编辑' }
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => deleteMutation.mutate(node.id) },
|
||||
{
|
||||
trigger: () =>
|
||||
h(NButton, { size: 'small', tertiary: true, type: 'error' }, { default: () => '删除' }),
|
||||
default: () => '确认删除该组织吗?'
|
||||
}
|
||||
)
|
||||
])
|
||||
])
|
||||
}
|
||||
function onExpanded(keys: string[]) {
|
||||
expandedKeys.value = keys
|
||||
}
|
||||
|
||||
function collectAllKeys(nodes: OrgTreeNode[]): string[] {
|
||||
return nodes.flatMap((node) => [node.id, ...collectAllKeys(node.children ?? [])])
|
||||
}
|
||||
|
||||
function renderSwitcherIcon() {
|
||||
return h(ChevronRight, { size: 16, strokeWidth: 2.25, class: 'tree-switcher-icon' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-card-body {
|
||||
padding: 8px 10px 12px;
|
||||
}
|
||||
|
||||
:deep(.n-tree-node-switcher) {
|
||||
width: 28px;
|
||||
height: 42px;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
:deep(.tree-switcher-icon) {
|
||||
display: block;
|
||||
color: #0f8f86;
|
||||
}
|
||||
|
||||
:deep(.n-tree-node-content) {
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
:deep(.n-tree-node-content__text) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -119,8 +119,8 @@ const statusOptions = [
|
||||
]
|
||||
const dataScopeOptions = [
|
||||
{ label: '全部数据', value: 'ALL' },
|
||||
{ label: '本组织及下级', value: 'DEPT' },
|
||||
{ label: '本组织', value: 'DEPT_ONLY' },
|
||||
{ label: '本企业', value: 'DEPT' },
|
||||
{ label: '本企业', value: 'DEPT_ONLY' },
|
||||
{ label: '仅本人', value: 'SELF' }
|
||||
]
|
||||
const roleQuery = useQuery({
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
style="width: 140px"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-select
|
||||
v-model:value="queryForm.enterpriseId"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="企业"
|
||||
:options="enterpriseOptions"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">查询</n-button>
|
||||
@@ -65,8 +75,8 @@
|
||||
<n-tag :type="statusTagType(detailUser.status)" size="small" round>
|
||||
{{ detailUser.statusLabel || statusLabel(detailUser.status) }}
|
||||
</n-tag>
|
||||
<n-tag v-if="detailUser.orgName || detailUser.orgCode" size="small" round>
|
||||
{{ detailUser.orgName || detailUser.orgCode }}
|
||||
<n-tag v-if="detailUser.enterpriseName || detailUser.taxpayerNum" size="small" round>
|
||||
{{ detailUser.enterpriseName || detailUser.taxpayerNum }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,6 +157,15 @@
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="editForm.email" />
|
||||
</n-form-item>
|
||||
<n-form-item label="企业" path="enterpriseId">
|
||||
<n-select
|
||||
v-model:value="editForm.enterpriseId"
|
||||
clearable
|
||||
filterable
|
||||
:options="enterpriseOptions"
|
||||
placeholder="可选"
|
||||
/>
|
||||
</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>
|
||||
@@ -237,6 +256,7 @@ import {
|
||||
updateUserStatusApi
|
||||
} from '@/api/system/user'
|
||||
import { listRolesApi } from '@/api/system/role'
|
||||
import { listEnterprisesApi } from '@/api/system/enterprise'
|
||||
import type {
|
||||
CreateUserRequest,
|
||||
UpdateUserRequest,
|
||||
@@ -251,7 +271,8 @@ const queryClient = useQueryClient()
|
||||
const queryForm = reactive({
|
||||
username: '',
|
||||
nickname: '',
|
||||
status: null as string | null
|
||||
status: null as string | null,
|
||||
enterpriseId: null as string | null
|
||||
})
|
||||
|
||||
const pager = reactive({
|
||||
@@ -272,7 +293,8 @@ const usersQuery = useQuery({
|
||||
pageSize: pager.pageSize,
|
||||
username: queryForm.username || undefined,
|
||||
nickname: queryForm.nickname || undefined,
|
||||
status: queryForm.status || undefined
|
||||
status: queryForm.status || undefined,
|
||||
enterpriseId: queryForm.enterpriseId || undefined
|
||||
})
|
||||
})
|
||||
|
||||
@@ -281,6 +303,11 @@ const rolesQuery = useQuery({
|
||||
queryFn: () => listRolesApi({ page: 1, pageSize: 200, status: 'ENABLED' })
|
||||
})
|
||||
|
||||
const enterprisesQuery = useQuery({
|
||||
queryKey: ['system', 'enterprises', 'options'],
|
||||
queryFn: () => listEnterprisesApi({ page: 1, pageSize: 200 })
|
||||
})
|
||||
|
||||
const roleOptions = computed(() =>
|
||||
(rolesQuery.data.value?.items ?? []).map((role) => ({
|
||||
label: `${role.name} (${role.code})`,
|
||||
@@ -288,6 +315,13 @@ const roleOptions = computed(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const enterpriseOptions = computed(() =>
|
||||
(enterprisesQuery.data.value?.items ?? []).map((enterprise) => ({
|
||||
label: `${enterprise.enterpriseName}(${enterprise.taxpayerNum})`,
|
||||
value: enterprise.id
|
||||
}))
|
||||
)
|
||||
|
||||
const tableRows = computed(() => usersQuery.data.value?.items ?? [])
|
||||
|
||||
const pagination = computed(() => ({
|
||||
@@ -316,7 +350,7 @@ const baseDetailItems = computed(() => {
|
||||
{ label: '姓名', value: displayValue(user.realName) },
|
||||
{ label: '手机号', value: displayValue(user.phone) },
|
||||
{ label: '邮箱', value: displayValue(user.email) },
|
||||
{ label: '组织', value: formatOrg(user) },
|
||||
{ label: '企业', value: formatEnterprise(user) },
|
||||
{ label: '状态', value: user.statusLabel || statusLabel(user.status) },
|
||||
{ label: '头像', value: displayValue(user.avatar) }
|
||||
]
|
||||
@@ -352,6 +386,7 @@ const editForm = reactive({
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
enterpriseId: null as string | null,
|
||||
status: 'ENABLED'
|
||||
})
|
||||
|
||||
@@ -370,6 +405,7 @@ const saveMutation = useMutation({
|
||||
realName: editForm.realName.trim() || undefined,
|
||||
phone: editForm.phone.trim() || undefined,
|
||||
email: editForm.email.trim() || undefined,
|
||||
enterpriseId: editForm.enterpriseId || undefined,
|
||||
status: editForm.status
|
||||
}
|
||||
return createUserApi(payload)
|
||||
@@ -378,7 +414,8 @@ const saveMutation = useMutation({
|
||||
nickname: editForm.nickname.trim() || undefined,
|
||||
realName: editForm.realName.trim() || undefined,
|
||||
phone: editForm.phone.trim() || undefined,
|
||||
email: editForm.email.trim() || undefined
|
||||
email: editForm.email.trim() || undefined,
|
||||
enterpriseId: editForm.enterpriseId || undefined
|
||||
}
|
||||
return updateUserApi(editModal.id, payload)
|
||||
},
|
||||
@@ -461,6 +498,7 @@ function handleReset() {
|
||||
queryForm.username = ''
|
||||
queryForm.nickname = ''
|
||||
queryForm.status = null
|
||||
queryForm.enterpriseId = null
|
||||
pager.page = 1
|
||||
usersQuery.refetch()
|
||||
}
|
||||
@@ -472,6 +510,7 @@ function resetEditForm() {
|
||||
editForm.realName = ''
|
||||
editForm.phone = ''
|
||||
editForm.email = ''
|
||||
editForm.enterpriseId = null
|
||||
editForm.status = 'ENABLED'
|
||||
}
|
||||
|
||||
@@ -488,9 +527,9 @@ function displayValue(value: unknown) {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function formatOrg(user: UserDetail) {
|
||||
if (user.orgName && user.orgCode) return `${user.orgName}(${user.orgCode})`
|
||||
return user.orgName || user.orgCode || user.orgId || '-'
|
||||
function formatEnterprise(user: UserDetail) {
|
||||
if (user.enterpriseName && user.taxpayerNum) return `${user.enterpriseName}(${user.taxpayerNum})`
|
||||
return user.enterpriseName || user.taxpayerNum || user.enterpriseId || '-'
|
||||
}
|
||||
|
||||
async function openDetail(row: UserListItem) {
|
||||
@@ -517,6 +556,7 @@ async function openEdit(row: UserListItem) {
|
||||
editForm.realName = detail.realName ?? ''
|
||||
editForm.phone = detail.phone ?? ''
|
||||
editForm.email = detail.email ?? ''
|
||||
editForm.enterpriseId = detail.enterpriseId ?? null
|
||||
editForm.status = detail.status
|
||||
editModal.visible = true
|
||||
}
|
||||
@@ -583,6 +623,12 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
|
||||
minWidth: 180,
|
||||
render: (row) => (row.roleCodes.length > 0 ? row.roleCodes.join(', ') : '-')
|
||||
},
|
||||
{
|
||||
title: '企业',
|
||||
key: 'enterpriseName',
|
||||
minWidth: 220,
|
||||
render: (row) => row.enterpriseName || row.taxpayerNum || '-'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
|
||||
@@ -249,13 +249,13 @@ async function handleLogout() {
|
||||
.app-shell {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #f5f6f8;
|
||||
background: var(--app-bg);
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e7eaf0;
|
||||
background: var(--app-surface);
|
||||
border-right: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.layout-sider :deep(.n-layout-sider-scroll-container),
|
||||
@@ -294,7 +294,7 @@ async function handleLogout() {
|
||||
gap: 12px;
|
||||
min-height: 64px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #eef1f5;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.brand-seal {
|
||||
@@ -303,9 +303,10 @@ async function handleLogout() {
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
border-radius: 11px;
|
||||
background: var(--app-primary);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.brand-glyph {
|
||||
@@ -322,7 +323,7 @@ async function handleLogout() {
|
||||
.brand-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #111827;
|
||||
color: var(--app-text);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
@@ -367,7 +368,7 @@ async function handleLogout() {
|
||||
|
||||
.sider-resizer:hover::after,
|
||||
:global(.sider-resizing) .sider-resizer::after {
|
||||
background: #d1d5db;
|
||||
background: var(--app-border-strong);
|
||||
}
|
||||
|
||||
:global(.sider-resizing) {
|
||||
@@ -397,8 +398,9 @@ async function handleLogout() {
|
||||
.app-header {
|
||||
flex: 0 0 auto;
|
||||
padding: 16px 20px 10px;
|
||||
border-bottom: 1px solid #e7eaf0;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.header-main {
|
||||
@@ -416,7 +418,7 @@ async function handleLogout() {
|
||||
}
|
||||
|
||||
.header-icon-btn {
|
||||
color: #4b5563;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
@@ -425,7 +427,7 @@ async function handleLogout() {
|
||||
|
||||
.header-title {
|
||||
margin: 0 0 4px;
|
||||
color: #111827;
|
||||
color: var(--app-text);
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -437,12 +439,12 @@ async function handleLogout() {
|
||||
}
|
||||
|
||||
.identity-name {
|
||||
color: #374151;
|
||||
color: var(--app-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
color: #374151;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
|
||||
+1
-27
@@ -1,7 +1,6 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { loginApi, logoutApi, meApi, registerEnterpriseApi } from '@/api/auth'
|
||||
import { listDictItemsApi } from '@/api/system/dict'
|
||||
import type {
|
||||
CurrentUserProfile,
|
||||
EnterpriseRegisterRequest,
|
||||
@@ -9,7 +8,6 @@ import type {
|
||||
MeResponse,
|
||||
MenuNode
|
||||
} from '@/types/auth'
|
||||
import type { DictItem } from '@/types/system/dict'
|
||||
|
||||
const TOKEN_KEY = 'platform.token'
|
||||
|
||||
@@ -35,7 +33,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const menus = ref<MenuNode[]>([])
|
||||
const permissions = ref<string[]>([])
|
||||
const layoutCollapsed = ref(false)
|
||||
const dictCache = ref<Record<string, DictItem[]>>({})
|
||||
const tabs = ref<TabItem[]>(createDefaultTabs())
|
||||
const tabRefreshMarks = ref<Record<string, number>>({})
|
||||
const loaded = ref(false)
|
||||
@@ -52,7 +49,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
user.value = null
|
||||
menus.value = []
|
||||
permissions.value = []
|
||||
dictCache.value = {}
|
||||
loaded.value = false
|
||||
tabs.value = createDefaultTabs()
|
||||
tabRefreshMarks.value = {}
|
||||
@@ -103,24 +99,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return permissions.value.includes(permission)
|
||||
}
|
||||
|
||||
async function loadDictByTypeId(typeId: string, force = false) {
|
||||
const cached = dictCache.value[typeId]
|
||||
if (cached && !force) return cached
|
||||
const result = await listDictItemsApi({ typeId, page: 1, pageSize: 200 })
|
||||
dictCache.value[typeId] = result.items.filter((item) => item.status === 'ENABLED')
|
||||
return dictCache.value[typeId]
|
||||
}
|
||||
|
||||
function getDictLabel(typeId: string, value: string) {
|
||||
const item = dictCache.value[typeId]?.find((entry) => entry.value === value)
|
||||
return item?.label ?? value
|
||||
}
|
||||
|
||||
function getDictColor(typeId: string, value: string) {
|
||||
const item = dictCache.value[typeId]?.find((entry) => entry.value === value)
|
||||
return item?.color ?? undefined
|
||||
}
|
||||
|
||||
function addTab(tab: TabItem) {
|
||||
const normalizedPath = normalizeTabPath(tab.path)
|
||||
if (tabs.value.some((item) => item.path === normalizedPath)) return
|
||||
@@ -172,7 +150,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
menus,
|
||||
permissions,
|
||||
layoutCollapsed,
|
||||
dictCache,
|
||||
tabs,
|
||||
tabRefreshMarks,
|
||||
loaded,
|
||||
@@ -188,9 +165,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
closeTab,
|
||||
closeOtherTabs,
|
||||
closeAllTabs,
|
||||
refreshTab,
|
||||
loadDictByTypeId,
|
||||
getDictLabel,
|
||||
getDictColor
|
||||
refreshTab
|
||||
}
|
||||
})
|
||||
|
||||
+29
-17
@@ -4,17 +4,20 @@
|
||||
|
||||
:root {
|
||||
color: #111827;
|
||||
background: #f5f6f8;
|
||||
background: #f6f7fb;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
|
||||
--app-bg: #f5f6f8;
|
||||
--app-bg: #f6f7fb;
|
||||
--app-surface: #ffffff;
|
||||
--app-border: #e7eaf0;
|
||||
--app-border-strong: #d7dce5;
|
||||
--app-primary: #111827;
|
||||
--app-primary-soft: #f3f4f6;
|
||||
--app-surface-tint: #fafbff;
|
||||
--app-border: #e5e7eb;
|
||||
--app-border-strong: #cbd5e1;
|
||||
--app-primary: #2563eb;
|
||||
--app-primary-deep: #1d4ed8;
|
||||
--app-primary-soft: #edf4ff;
|
||||
--app-primary-subtle: #f7faff;
|
||||
--app-text: #111827;
|
||||
--app-text-muted: #6b7280;
|
||||
--app-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--app-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -46,13 +49,13 @@ select {
|
||||
|
||||
.app-panel {
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 14px;
|
||||
border-radius: 12px;
|
||||
background: var(--app-surface);
|
||||
box-shadow: var(--app-shadow);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #111827;
|
||||
color: var(--app-text);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -78,7 +81,7 @@ select {
|
||||
|
||||
.page-title-xl {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
color: var(--app-text);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -97,12 +100,12 @@ select {
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
background: #ffffff;
|
||||
background: var(--app-surface);
|
||||
}
|
||||
|
||||
.page-toolbar-title {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
color: var(--app-text);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
@@ -195,15 +198,15 @@ select {
|
||||
|
||||
.page-card .n-data-table-th {
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
||||
.page-card .n-data-table-td {
|
||||
color: #111827;
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.page-card .n-data-table-tr:hover .n-data-table-td {
|
||||
background: #fafafa;
|
||||
background: var(--app-primary-subtle);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
@@ -212,8 +215,8 @@ select {
|
||||
|
||||
.soft-stat {
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
background: var(--app-surface);
|
||||
box-shadow: var(--app-shadow);
|
||||
padding: 18px;
|
||||
}
|
||||
@@ -222,6 +225,15 @@ select {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.n-button.n-button--primary-type {
|
||||
--n-color: var(--app-primary) !important;
|
||||
--n-color-hover: var(--app-primary-deep) !important;
|
||||
--n-color-pressed: #1e40af !important;
|
||||
--n-border: 1px solid var(--app-primary) !important;
|
||||
--n-border-hover: 1px solid var(--app-primary-deep) !important;
|
||||
--n-border-pressed: 1px solid #1e40af !important;
|
||||
}
|
||||
|
||||
.n-input,
|
||||
.n-base-selection {
|
||||
font-size: 13px;
|
||||
|
||||
@@ -29,7 +29,6 @@ export interface CurrentUserProfile {
|
||||
username: string
|
||||
nickname?: string | null
|
||||
realName?: string | null
|
||||
orgId?: string | null
|
||||
enterpriseId?: string | null
|
||||
digitalAccountId?: string | null
|
||||
userType: 'SYSTEM' | 'ENTERPRISE_ADMIN' | 'DIGITAL_OPERATOR' | string
|
||||
|
||||
@@ -14,19 +14,4 @@ export interface OperationLogItem {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ApiAccessLogItem {
|
||||
id: string
|
||||
traceId?: string | null
|
||||
appKey?: string | null
|
||||
appName?: string | null
|
||||
httpMethod: string
|
||||
requestPath: string
|
||||
responseCode?: string | null
|
||||
status: string
|
||||
errorMessage?: string | null
|
||||
costMs: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type OperationLogPage = PageResult<OperationLogItem>
|
||||
export type ApiAccessLogPage = PageResult<ApiAccessLogItem>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { PageResult } from '@/types/http'
|
||||
|
||||
export interface DictTypeItem {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
status: string
|
||||
statusLabel?: string
|
||||
remark?: string | null
|
||||
}
|
||||
|
||||
export interface DictItem {
|
||||
id: string
|
||||
typeId: string
|
||||
label: string
|
||||
value: string
|
||||
color?: string | null
|
||||
sort: number
|
||||
status: string
|
||||
statusLabel?: string
|
||||
remark?: string | null
|
||||
}
|
||||
|
||||
export type DictTypePage = PageResult<DictTypeItem>
|
||||
export type DictItemPage = PageResult<DictItem>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { PageResult } from '@/types/http'
|
||||
|
||||
export interface EnterpriseManageItem {
|
||||
id: string
|
||||
taxpayerNum: string
|
||||
enterpriseName: string
|
||||
legalPersonName?: string | null
|
||||
contactsName?: string | null
|
||||
contactsEmail?: string | null
|
||||
contactsPhone?: string | null
|
||||
regionCode?: string | null
|
||||
cityName?: string | null
|
||||
enterpriseAddress?: string | null
|
||||
reviewStatus?: string | null
|
||||
reviewOpinion?: string | null
|
||||
invoiceKind?: string | null
|
||||
invoiceLayoutFileType?: string | null
|
||||
serviceStatus?: string | null
|
||||
createdAt: string
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
export interface EnterpriseQuery {
|
||||
keyword?: string
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type EnterpriseManagePage = PageResult<EnterpriseManageItem>
|
||||
@@ -1,25 +0,0 @@
|
||||
export interface OrgTreeNode {
|
||||
id: string
|
||||
parentId?: string | null
|
||||
name: string
|
||||
code: string
|
||||
sort: number
|
||||
status: string
|
||||
statusLabel?: string
|
||||
children: OrgTreeNode[]
|
||||
}
|
||||
|
||||
export interface CreateOrgRequest {
|
||||
parentId?: string
|
||||
name: string
|
||||
code: string
|
||||
sort: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface UpdateOrgRequest {
|
||||
parentId?: string
|
||||
name: string
|
||||
sort: number
|
||||
status: string
|
||||
}
|
||||
@@ -5,7 +5,9 @@ export interface UserListItem {
|
||||
username: string
|
||||
nickname?: string | null
|
||||
realName?: string | null
|
||||
orgId?: string | null
|
||||
enterpriseId?: string | null
|
||||
enterpriseName?: string | null
|
||||
taxpayerNum?: string | null
|
||||
status: string
|
||||
statusLabel?: string
|
||||
roleCodes: string[]
|
||||
@@ -19,9 +21,9 @@ export interface UserDetail {
|
||||
phone?: string | null
|
||||
email?: string | null
|
||||
avatar?: string | null
|
||||
orgId?: string | null
|
||||
orgName?: string | null
|
||||
orgCode?: string | null
|
||||
enterpriseId?: string | null
|
||||
enterpriseName?: string | null
|
||||
taxpayerNum?: string | null
|
||||
status: string
|
||||
statusLabel?: string
|
||||
roleIds: string[]
|
||||
@@ -48,7 +50,7 @@ export interface UserQuery {
|
||||
username?: string
|
||||
nickname?: string
|
||||
status?: string
|
||||
orgId?: string
|
||||
enterpriseId?: string
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
@@ -63,7 +65,7 @@ export interface CreateUserRequest {
|
||||
phone?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
orgId?: string
|
||||
enterpriseId?: string
|
||||
status: string
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ export interface UpdateUserRequest {
|
||||
phone?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
orgId?: string
|
||||
enterpriseId?: string
|
||||
}
|
||||
|
||||
export interface UpdateUserStatusRequest {
|
||||
|
||||
@@ -29,8 +29,8 @@ export function menuTypeLabel(type?: string | null) {
|
||||
export function dataScopeLabel(scope?: string | null) {
|
||||
const map: Record<string, string> = {
|
||||
ALL: '全部数据',
|
||||
DEPT: '本组织及下级',
|
||||
DEPT_ONLY: '本组织',
|
||||
DEPT: '本企业',
|
||||
DEPT_ONLY: '本企业',
|
||||
SELF: '仅本人'
|
||||
}
|
||||
return scope ? (map[scope] ?? scope) : '-'
|
||||
|
||||
Reference in New Issue
Block a user