大改三角色逻辑:超级管理员、企业管理员、开票员

This commit is contained in:
BBIT-Kai
2026-05-27 09:10:54 +08:00
parent 59fa2beb03
commit 9fd80980e7
67 changed files with 1141 additions and 2230 deletions
@@ -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)
@@ -25,4 +25,4 @@ object SysOperationLogTable : Table("sys_operation_log") {
val createdAt = timestampWithTimeZone("created_at")
override val primaryKey = PrimaryKey(id)
}
}
@@ -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,
)
}
}
/**
* 使用统一票通响应格式执行可空查询,空结果按空对象返回。
*
@@ -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")
@@ -32,4 +30,4 @@ object JwtService {
return token to AppConfig.jwt.accessTokenTtlMinutes * 60
}
}
}
@@ -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
View File
@@ -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 })
}
-61
View File
@@ -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}`)
}
+6
View File
@@ -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 })
}
-18
View File
@@ -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}`)
}
+6 -1
View File
@@ -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),
+9 -7
View File
@@ -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 {
+103 -2
View File
@@ -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>
-142
View File
@@ -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 {
-237
View File
@@ -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>
-1
View File
@@ -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' },
-238
View File
@@ -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>
+2 -2
View File
@@ -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({
+55 -9
View File
@@ -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',
+16 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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;
-1
View File
@@ -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
-15
View File
@@ -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>
-25
View File
@@ -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>
+29
View File
@@ -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>
-25
View File
@@ -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
}
+9 -7
View File
@@ -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 {
+2 -2
View File
@@ -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) : '-'