From 3506efe894ede964d3639c255408c9a6bfc35e40 Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Thu, 21 May 2026 14:55:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dapiresult=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9B=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E6=9F=A5=E7=9C=8B=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/bbit/ticket/dao/system/UserDao.kt | 58 +++- .../ticket/entity/common/system/UserDto.kt | 34 +- .../openapi/registerOpenBlueInvoiceRoutes.kt | 20 +- .../ticket/route/piaotong/PTRouteSupport.kt | 60 +++- .../route/piaotong/registerPTAuthRoutes.kt | 12 +- web/src/features/system/users/index.vue | 319 +++++++++++++++++- web/src/types/system/user.ts | 33 +- 7 files changed, 503 insertions(+), 33 deletions(-) diff --git a/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt index 42f81ee..c317637 100644 --- a/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt +++ b/server/src/main/kotlin/com/bbit/ticket/dao/system/UserDao.kt @@ -12,6 +12,7 @@ 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 com.bbit.ticket.entity.common.system.UserRoleBrief import com.bbit.ticket.utils.ApiKeyUtil import io.ktor.http.HttpStatusCode import org.jetbrains.exposed.v1.core.Op @@ -90,10 +91,26 @@ object UserDao { fun detail(id: Uuid): UserDetailResponse { val user = requireActive(id) - val roleIds = SysUserRoleTable.selectAll() - .where { SysUserRoleTable.userId eq id } - .map { it[SysUserRoleTable.roleId].toString() } - return user.toUserDetail(roleIds) + val roles = (SysUserRoleTable innerJoin SysRoleTable) + .selectAll() + .where { + (SysUserRoleTable.userId eq id) and + SysRoleTable.deletedAt.isNull() + } + .map { + UserRoleBrief( + id = it[SysRoleTable.id].toString(), + name = it[SysRoleTable.name], + code = it[SysRoleTable.code], + ) + } + 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) } fun updateProfile(id: Uuid, request: UpdateUserRequest, orgId: Uuid?) { @@ -226,7 +243,11 @@ object UserDao { apiKey = this[SysUserTable.apiKey], ) - private fun ResultRow.toUserDetail(roleIds: List) = UserDetailResponse( + private fun ResultRow.toUserDetail( + roleIds: List, + roles: List, + org: ResultRow?, + ) = UserDetailResponse( id = this[SysUserTable.id].toString(), username = this[SysUserTable.username], nickname = this[SysUserTable.nickname], @@ -235,13 +256,38 @@ object UserDao { email = this[SysUserTable.email], avatar = this[SysUserTable.avatar], orgId = this[SysUserTable.orgId]?.toString(), + orgName = org?.get(SysOrgTable.name), + orgCode = org?.get(SysOrgTable.code), status = this[SysUserTable.status], statusLabel = statusLabel(this[SysUserTable.status]), roleIds = roleIds, + roles = roles, apiKey = this[SysUserTable.apiKey], + tokenVersion = this[SysUserTable.tokenVersion], + lastLoginAt = this[SysUserTable.lastLoginAt]?.toString(), + lastLoginIp = this[SysUserTable.lastLoginIp], + createdAt = this[SysUserTable.createdAt].toString(), + createdBy = this[SysUserTable.createdBy]?.toString(), + updatedAt = this[SysUserTable.updatedAt]?.toString(), + updatedBy = this[SysUserTable.updatedBy]?.toString(), + deletedAt = this[SysUserTable.deletedAt]?.toString(), + deletedBy = this[SysUserTable.deletedBy]?.toString(), + version = this[SysUserTable.version], taxpayerNum = this[SysUserTable.taxpayerNum], account = this[SysUserTable.taxAccount], - taxPassword = this[SysUserTable.taxPassword], taxIdentityType = this[SysUserTable.taxIdentityType], + taxContactName = this[SysUserTable.taxContactName], + taxContactPhone = this[SysUserTable.taxContactPhone], + taxContactEmail = this[SysUserTable.taxContactEmail], + taxLegalPersonName = this[SysUserTable.taxLegalPersonName], + taxEnterpriseName = this[SysUserTable.taxEnterpriseName], + taxRegionCode = this[SysUserTable.taxRegionCode], + taxCityName = this[SysUserTable.taxCityName], + taxEnterpriseAddress = this[SysUserTable.taxEnterpriseAddress], + taxRegistrationCertificate = this[SysUserTable.taxRegistrationCertificate], + bankName = this[SysUserTable.bankName], + bankAccount = this[SysUserTable.bankAccount], + presetAddress = this[SysUserTable.presetAddress], + presetPhone = this[SysUserTable.presetPhone], ) } diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/common/system/UserDto.kt b/server/src/main/kotlin/com/bbit/ticket/entity/common/system/UserDto.kt index 758db50..0cc42b8 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/common/system/UserDto.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/common/system/UserDto.kt @@ -24,14 +24,46 @@ data class UserDetailResponse( val email: String? = null, val avatar: String? = null, val orgId: String? = null, + val orgName: String? = null, + val orgCode: String? = null, val status: String, val statusLabel: String, val roleIds: List, + val roles: List = emptyList(), val apiKey: String? = null, + val tokenVersion: Int, + val lastLoginAt: String? = null, + val lastLoginIp: String? = null, + val createdAt: String? = null, + val createdBy: String? = null, + val updatedAt: String? = null, + val updatedBy: String? = null, + val deletedAt: String? = null, + val deletedBy: String? = null, + val version: Int, val taxpayerNum: String? = null, val account: String? = null, - val taxPassword: String? = null, val taxIdentityType: String? = null, + val taxContactName: String? = null, + val taxContactPhone: String? = null, + val taxContactEmail: String? = null, + val taxLegalPersonName: String? = null, + val taxEnterpriseName: String? = null, + val taxRegionCode: String? = null, + val taxCityName: String? = null, + val taxEnterpriseAddress: String? = null, + val taxRegistrationCertificate: String? = null, + val bankName: String? = null, + val bankAccount: String? = null, + val presetAddress: String? = null, + val presetPhone: String? = null, +) + +@Serializable +data class UserRoleBrief( + val id: String, + val name: String, + val code: String, ) @Serializable diff --git a/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt index 79b9c10..9d2f95a 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/openapi/registerOpenBlueInvoiceRoutes.kt @@ -12,9 +12,10 @@ import com.bbit.ticket.service.openapi.OpenBlueInvoiceService import com.bbit.ticket.service.system.ApiAccessLogService import com.bbit.ticket.utils.requireOpenApiPrincipal import com.bbit.ticket.utils.plugins.myJson +import io.ktor.http.ContentType import io.ktor.server.application.ApplicationCall import io.ktor.server.request.receive -import io.ktor.server.response.respond +import io.ktor.server.response.respondText import io.ktor.server.routing.Route import io.ktor.server.routing.get import io.ktor.server.routing.post @@ -92,20 +93,23 @@ private suspend inline fun ApplicationCall.respondOpenApi( try { val response = ok(block()) val responseBody = myJson.encodeToString(response) - respond(response) + respondText(responseBody, ContentType.Application.Json) saveOpenApiLog(appKey, appName, requestBody, response.code, responseBody, "SUCCESS", null, start) } catch (e: PTException) { val response = fail(code = e.code, message = e.message, traceId = e.serialNo) - respond(response) - saveOpenApiLog(appKey, appName, requestBody, e.code, myJson.encodeToString(response), "FAILED", e.message, start) + val responseBody = myJson.encodeToString(response) + respondText(responseBody, ContentType.Application.Json) + saveOpenApiLog(appKey, appName, requestBody, e.code, responseBody, "FAILED", e.message, start) } catch (e: BizException) { val response = fail(code = e.errorCode, message = e.message) - respond(e.status, response) - saveOpenApiLog(appKey, appName, requestBody, e.errorCode, myJson.encodeToString(response), "FAILED", e.message, start) + val responseBody = myJson.encodeToString(response) + respondText(responseBody, ContentType.Application.Json, e.status) + saveOpenApiLog(appKey, appName, requestBody, e.errorCode, responseBody, "FAILED", e.message, start) } catch (e: Exception) { val response = fail(code = "-1", message = e.message ?: "开放接口调用失败") - respond(response) - saveOpenApiLog(appKey, appName, requestBody, "-1", myJson.encodeToString(response), "FAILED", e.message, start) + val responseBody = myJson.encodeToString(response) + respondText(responseBody, ContentType.Application.Json) + saveOpenApiLog(appKey, appName, requestBody, "-1", responseBody, "FAILED", e.message, start) } } diff --git a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/PTRouteSupport.kt b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/PTRouteSupport.kt index 8ab97e9..8e2a9b5 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/PTRouteSupport.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/PTRouteSupport.kt @@ -4,12 +4,15 @@ 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.utils.plugins.myJson import io.ktor.http.ContentType import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall import io.ktor.server.response.header -import io.ktor.server.response.respond import io.ktor.server.response.respondBytes +import io.ktor.server.response.respondText +import kotlinx.serialization.encodeToString /** * 使用统一票通响应格式执行接口逻辑。 @@ -22,13 +25,39 @@ suspend inline fun ApplicationCall.respondPt( crossinline block: suspend () -> T, ) { try { - respond(ok(block())) + respondJson(ok(block())) } catch (e: PTException) { - respond(fail(code = e.code, message = e.message, traceId = e.serialNo)) + respondJson(fail(code = e.code, message = e.message, traceId = e.serialNo)) } catch (e: BizException) { - respond(e.status, fail(code = e.errorCode, message = e.message)) + respondJson(fail(code = e.errorCode, message = e.message), e.status) } catch (e: Exception) { - respond(fail(code = "-1", message = e.message ?: fallbackMessage)) + respondJson(fail(code = "-1", message = e.message ?: fallbackMessage)) + } +} + +/** + * 使用统一票通响应格式执行可空查询,空结果按空对象返回。 + * + * @param fallbackMessage 未知异常时返回给前端的兜底提示。 + * @param block 当前接口要执行的查询逻辑。 + */ +suspend inline fun ApplicationCall.respondPtOrEmptyObject( + fallbackMessage: String, + crossinline block: suspend () -> T?, +) { + try { + val data = block() + if (data == null) { + respondJson(ok(emptyMap())) + } else { + respondJson(ok(data)) + } + } catch (e: PTException) { + respondJson(fail(code = e.code, message = e.message, traceId = e.serialNo)) + } catch (e: BizException) { + respondJson(fail(code = e.errorCode, message = e.message), e.status) + } catch (e: Exception) { + respondJson(fail(code = "-1", message = e.message ?: fallbackMessage)) } } @@ -41,7 +70,7 @@ suspend inline fun ApplicationCall.respondPt( suspend fun ApplicationCall.requiredQueryParameter(name: String, message: String): String? { val value = request.queryParameters[name]?.trim()?.takeIf { it.isNotEmpty() } if (value == null) { - respond(fail(code = "-1", message = message)) + respondJson(fail(code = "-1", message = message)) } return value } @@ -62,10 +91,23 @@ suspend fun ApplicationCall.respondPtPdf( response.header(HttpHeaders.ContentDisposition, "inline; filename=\"$filename\"") respondBytes(block(), ContentType.Application.Pdf) } catch (e: PTException) { - respond(fail(code = e.code, message = e.message, traceId = e.serialNo)) + respondJson(fail(code = e.code, message = e.message, traceId = e.serialNo)) } catch (e: BizException) { - respond(e.status, fail(code = e.errorCode, message = e.message)) + respondJson(fail(code = e.errorCode, message = e.message), e.status) } catch (e: Exception) { - respond(fail(code = "-1", message = e.message ?: fallbackMessage)) + respondJson(fail(code = "-1", message = e.message ?: fallbackMessage)) } } + +/** + * 显式序列化 JSON 响应,避免 Ktor 在泛型 helper 中丢失 ApiResult 类型信息。 + * + * @param data 要返回的响应对象。 + * @param status HTTP 状态码。 + */ +suspend inline fun ApplicationCall.respondJson( + data: T, + status: HttpStatusCode = HttpStatusCode.OK, +) { + respondText(myJson.encodeToString(data), ContentType.Application.Json, status) +} diff --git a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt index e070070..35401b7 100644 --- a/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt +++ b/server/src/main/kotlin/com/bbit/ticket/route/piaotong/registerPTAuthRoutes.kt @@ -57,8 +57,8 @@ fun Route.registerPTAuthRoutes() { } get("/enterprise") { - call.respondPt("查询企业信息失败") { - PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id) ?: emptyMap() + call.respondPtOrEmptyObject("查询企业信息失败") { + PTConfigService.getEnterpriseInfo(call.requireCurrentUser().id) } } @@ -72,12 +72,12 @@ fun Route.registerPTAuthRoutes() { } get("/digital-account") { - call.respondPt("查询数电账号失败") { + call.respondPtOrEmptyObject("查询数电账号失败") { val currentUser = call.requireCurrentUser() if (currentUser.taxPayerNum == null) { throw BizException("-1", "请先完善用户信息", HttpStatusCode.OK) } - PTConfigService.getDigitalAccount(currentUser.id) ?: emptyMap() + PTConfigService.getDigitalAccount(currentUser.id) } } @@ -91,8 +91,8 @@ fun Route.registerPTAuthRoutes() { } get("/preset") { - call.respondPt("查询预设数据失败") { - PTConfigService.getPresetData(call.requireCurrentUser().id) ?: emptyMap() + call.respondPtOrEmptyObject("查询预设数据失败") { + PTConfigService.getPresetData(call.requireCurrentUser().id) } } diff --git a/web/src/features/system/users/index.vue b/web/src/features/system/users/index.vue index 15692f6..aa4ecea 100644 --- a/web/src/features/system/users/index.vue +++ b/web/src/features/system/users/index.vue @@ -45,6 +45,96 @@ + + + + + + ({ })) const editFormRef = ref(null) +const detailModal = reactive({ + visible: false, + loading: false +}) +const detailUser = ref(null) + +const baseDetailItems = computed(() => { + const user = detailUser.value + if (!user) return [] + return [ + { label: '用户ID', value: displayValue(user.id) }, + { label: '用户名', value: displayValue(user.username) }, + { label: '昵称', value: displayValue(user.nickname) }, + { label: '姓名', value: displayValue(user.realName) }, + { label: '手机号', value: displayValue(user.phone) }, + { label: '邮箱', value: displayValue(user.email) }, + { label: '组织', value: formatOrg(user) }, + { label: '状态', value: user.statusLabel || statusLabel(user.status) }, + { label: '头像', value: displayValue(user.avatar) }, + { label: 'API Key', value: displayValue(user.apiKey) } + ] +}) + +const taxDetailItems = computed(() => { + const user = detailUser.value + if (!user) return [] + return [ + { label: '纳税人识别号', value: displayValue(user.taxpayerNum) }, + { label: '数电账号', value: displayValue(user.account) }, + { label: '身份类型', value: displayValue(user.taxIdentityType) }, + { label: '联系人', value: displayValue(user.taxContactName) }, + { label: '联系人电话', value: displayValue(user.taxContactPhone) }, + { label: '联系人邮箱', value: displayValue(user.taxContactEmail) }, + { label: '法定代表人', value: displayValue(user.taxLegalPersonName) }, + { label: '企业名称', value: displayValue(user.taxEnterpriseName) }, + { label: '地区编码', value: displayValue(user.taxRegionCode) }, + { label: '城市', value: displayValue(user.taxCityName) }, + { label: '企业地址', value: displayValue(user.taxEnterpriseAddress) }, + { label: '税务登记证', value: displayValue(user.taxRegistrationCertificate) } + ] +}) + +const presetDetailItems = computed(() => { + const user = detailUser.value + if (!user) return [] + return [ + { label: '开户行', value: displayValue(user.bankName) }, + { label: '银行账号', value: displayValue(user.bankAccount) }, + { label: '预设地址', value: displayValue(user.presetAddress) }, + { label: '预设电话', value: displayValue(user.presetPhone) } + ] +}) + +const systemDetailItems = computed(() => { + const user = detailUser.value + if (!user) return [] + return [ + { label: 'Token版本', value: displayValue(user.tokenVersion) }, + { label: '数据版本', value: displayValue(user.version) }, + { label: '最后登录时间', value: displayValue(user.lastLoginAt) }, + { label: '最后登录IP', value: displayValue(user.lastLoginIp) }, + { label: '创建时间', value: displayValue(user.createdAt) }, + { label: '创建人', value: displayValue(user.createdBy) }, + { label: '更新时间', value: displayValue(user.updatedAt) }, + { label: '更新人', value: displayValue(user.updatedBy) }, + { label: '删除时间', value: displayValue(user.deletedAt) }, + { label: '删除人', value: displayValue(user.deletedBy) } + ] +}) + const editModal = reactive({ visible: false, mode: 'create' as 'create' | 'edit', @@ -373,6 +538,29 @@ function openCreate() { editModal.visible = true } +function displayValue(value: unknown) { + if (value === null || value === undefined || value === '') return '-' + return String(value) +} + +function formatOrg(user: UserDetail) { + if (user.orgName && user.orgCode) return `${user.orgName}(${user.orgCode})` + return user.orgName || user.orgCode || user.orgId || '-' +} + +async function openDetail(row: UserListItem) { + detailModal.visible = true + detailModal.loading = true + detailUser.value = null + try { + detailUser.value = await getUserDetailApi(row.id) + } catch { + message.error('查询用户详情失败') + } finally { + detailModal.loading = false + } +} + async function openEdit(row: UserListItem) { editModal.mode = 'edit' editModal.title = `编辑用户 - ${row.username}` @@ -485,13 +673,24 @@ const columns = computed>(() => [ { title: '操作', key: 'actions', - width: 360, + width: 430, render: (row) => h( NSpace, { size: 6 }, { default: () => [ + h( + NButton, + { + size: 'small', + tertiary: true, + type: 'info', + class: 'action-btn', + onClick: () => openDetail(row) + }, + { default: () => '详情' } + ), authStore.hasPermission('system:user:update') ? h( NButton, @@ -560,3 +759,119 @@ const columns = computed>(() => [ } ]) + + diff --git a/web/src/types/system/user.ts b/web/src/types/system/user.ts index f92d7f6..c7ff706 100644 --- a/web/src/types/system/user.ts +++ b/web/src/types/system/user.ts @@ -21,14 +21,45 @@ export interface UserDetail { email?: string | null avatar?: string | null orgId?: string | null + orgName?: string | null + orgCode?: string | null status: string statusLabel?: string roleIds: string[] + roles: UserRoleBrief[] apiKey?: string | null + tokenVersion: number + lastLoginAt?: string | null + lastLoginIp?: string | null + createdAt?: string | null + createdBy?: string | null + updatedAt?: string | null + updatedBy?: string | null + deletedAt?: string | null + deletedBy?: string | null + version: number taxpayerNum?: string | null account?: string | null - taxPassword?: string | null taxIdentityType?: string | null + taxContactName?: string | null + taxContactPhone?: string | null + taxContactEmail?: string | null + taxLegalPersonName?: string | null + taxEnterpriseName?: string | null + taxRegionCode?: string | null + taxCityName?: string | null + taxEnterpriseAddress?: string | null + taxRegistrationCertificate?: string | null + bankName?: string | null + bankAccount?: string | null + presetAddress?: string | null + presetPhone?: string | null +} + +export interface UserRoleBrief { + id: string + name: string + code: string } export interface UserQuery {