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

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)
}