From a716c481daa5df54e97eb1799dfe37ddf7078187 Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Wed, 27 May 2026 09:29:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=81=E4=B8=9A=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E8=A7=92=E8=89=B2=E6=97=A0=E6=B3=95=E7=BA=A2?= =?UTF-8?q?=E5=86=B2=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E5=88=97=E6=8C=89=E9=92=AE=E9=AB=98=E4=BD=8E?= =?UTF-8?q?=E4=B8=8D=E9=BD=90=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/entity/request/RedCreateRequest.kt | 4 ++ .../service/openapi/OpenInvoiceTaskService.kt | 43 +++++++++++++------ .../ticket/service/piaotong/PTRedService.kt | 2 +- web/src/api/piaotong/index.ts | 2 + .../piaotong/invoice-history/index.vue | 41 +++++++++++++++++- web/src/features/statistics/openapi/index.vue | 8 ++-- web/src/features/system/menus/index.vue | 2 +- web/src/features/system/roles/index.vue | 2 +- web/src/features/system/users/index.vue | 2 +- web/src/style.css | 34 +++++++++++++++ 10 files changed, 118 insertions(+), 22 deletions(-) diff --git a/server/src/main/kotlin/com/bbit/ticket/entity/request/RedCreateRequest.kt b/server/src/main/kotlin/com/bbit/ticket/entity/request/RedCreateRequest.kt index cf5fd8e..11812de 100644 --- a/server/src/main/kotlin/com/bbit/ticket/entity/request/RedCreateRequest.kt +++ b/server/src/main/kotlin/com/bbit/ticket/entity/request/RedCreateRequest.kt @@ -6,6 +6,10 @@ import kotlinx.serialization.Serializable data class RedCreateRequest( val historyId: String, + /** + * 平台数电账号 ID。企业管理员发起冲红时用于指定开票员。 + */ + val digitalAccountId: String? = null, /** * 冲红原因 * diff --git a/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenInvoiceTaskService.kt b/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenInvoiceTaskService.kt index c0c8419..3fd0e75 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenInvoiceTaskService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/openapi/OpenInvoiceTaskService.kt @@ -222,12 +222,28 @@ object OpenInvoiceTaskService { .where { accountScope(user) } .associateBy { it[PtDigitalAccountTable.id] } val total = OpenInvoiceTaskTable.selectAll().where { where }.count() - val items = OpenInvoiceTaskTable.selectAll() + val taskRows = OpenInvoiceTaskTable.selectAll() .where { where } .orderBy(OpenInvoiceTaskTable.createdAt, SortOrder.DESC) .limit(pageSize) .offset(((page - 1).coerceAtLeast(0) * pageSize).toLong()) - .map { it.toTaskItem(accountRows[it[OpenInvoiceTaskTable.digitalAccountId]]?.get(PtDigitalAccountTable.account)) } + .toList() + val historyMessages = taskRows + .map { it[OpenInvoiceTaskTable.invoiceReqSerialNo] } + .distinct() + .takeIf { it.isNotEmpty() } + ?.let { serialNos -> + HistoryInvoiceBasicTable.selectAll() + .where { HistoryInvoiceBasicTable.invoiceReqSerialNo inList serialNos } + .associate { it[HistoryInvoiceBasicTable.invoiceReqSerialNo] to it[HistoryInvoiceBasicTable.msg] } + } + ?: emptyMap() + val items = taskRows.map { + it.toTaskItem( + accountRows[it[OpenInvoiceTaskTable.digitalAccountId]]?.get(PtDigitalAccountTable.account), + historyMessages[it[OpenInvoiceTaskTable.invoiceReqSerialNo]], + ) + } PageResult(items, page, pageSize, total) } @@ -302,9 +318,9 @@ object OpenInvoiceTaskService { val runMode = task[OpenInvoiceTaskTable.runMode] val invoiceReqSerialNo = task[OpenInvoiceTaskTable.invoiceReqSerialNo] try { - val code = if (runMode == MODE_SIMULATED) { + val (code, message) = if (runMode == MODE_SIMULATED) { delay(2000) - simulatedQueryCode(invoiceReqSerialNo, task[OpenInvoiceTaskTable.pollCount]) + simulatedQueryCode(invoiceReqSerialNo, task[OpenInvoiceTaskTable.pollCount]) to null } else { val res = PTApi.queryInvoiceInfo( QueryInvoiceRequest( @@ -320,9 +336,9 @@ object OpenInvoiceTaskService { task[OpenInvoiceTaskTable.digitalAccountId], ) } - res.code + res.code to res.msg } - handleQueryCode(task, code) + handleQueryCode(task, code, message) } catch (e: PTException) { retryOrFail(task, e.code, e.message) } catch (e: Exception) { @@ -365,23 +381,23 @@ object OpenInvoiceTaskService { } } - private suspend fun handleQueryCode(task: ResultRow, code: String) { + private suspend fun handleQueryCode(task: ResultRow, code: String, message: String?) { when (code) { "0000" -> finishQueryTask(task, STATUS_SUCCESS, code, null) - "9999" -> finishQueryTask(task, STATUS_FAILED, code, "开票失败") + "9999" -> finishQueryTask(task, STATUS_FAILED, code, message?.takeIf { it.isNotBlank() } ?: "开票失败") AUTH_REQUIRED_CODE -> { - val message = "需要登录/风险认证" + val authMessage = message?.takeIf { it.isNotBlank() } ?: "需要登录/风险认证" dbQuery { OpenInvoiceTaskTable.update({ OpenInvoiceTaskTable.id eq task[OpenInvoiceTaskTable.id] }) { it[status] = STATUS_WAITING_AUTH it[ptCode] = code - it[errorMessage] = message + it[errorMessage] = authMessage it[updatedAt] = OffsetDateTime.now() it[lockedBy] = null it[lockedAt] = null } } - pauseApiKey(task[OpenInvoiceTaskTable.apiKey], code, message) + pauseApiKey(task[OpenInvoiceTaskTable.apiKey], code, authMessage) } "7777", "6666" -> requeueQueryTask(task, code) else -> requeueQueryTask(task, code) @@ -693,7 +709,7 @@ object OpenInvoiceTaskService { runMode = this[OpenInvoiceTaskTable.runMode], ) - private fun ResultRow.toTaskItem(account: String?): OpenInvoiceTaskItem = + private fun ResultRow.toTaskItem(account: String?, historyMessage: String?): OpenInvoiceTaskItem = OpenInvoiceTaskItem( id = this[OpenInvoiceTaskTable.id].toString(), digitalAccountId = this[OpenInvoiceTaskTable.digitalAccountId].toString(), @@ -706,7 +722,8 @@ object OpenInvoiceTaskService { batchNo = this[OpenInvoiceTaskTable.batchNo], status = this[OpenInvoiceTaskTable.status], ptCode = this[OpenInvoiceTaskTable.ptCode], - errorMessage = this[OpenInvoiceTaskTable.errorMessage], + errorMessage = this[OpenInvoiceTaskTable.errorMessage] + ?: historyMessage?.takeIf { this[OpenInvoiceTaskTable.status] == STATUS_FAILED }, attemptCount = this[OpenInvoiceTaskTable.attemptCount], maxAttemptCount = this[OpenInvoiceTaskTable.maxAttemptCount], pollCount = this[OpenInvoiceTaskTable.pollCount], diff --git a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt index 4684d4f..edad957 100644 --- a/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt +++ b/server/src/main/kotlin/com/bbit/ticket/service/piaotong/PTRedService.kt @@ -28,7 +28,7 @@ object PTRedService { * 红票接口调用 2.10 */ suspend fun invoiceRed(user: CurrentUser, req: RedCreateRequest): String { - val account = PTConfigService.requireDigitalAccountForAction(user, null) + val account = PTConfigService.requireDigitalAccountForAction(user, req.digitalAccountId) val invoiceReqSerialNo = PTClient.ptDate() val historyId = Uuid.parse(req.historyId) val his = dbQuery { diff --git a/web/src/api/piaotong/index.ts b/web/src/api/piaotong/index.ts index 500a63f..6b34832 100644 --- a/web/src/api/piaotong/index.ts +++ b/web/src/api/piaotong/index.ts @@ -458,6 +458,8 @@ export function invoiceIssueApi(payload: InvoiceRequest): Promise { export interface RedCreateRequest { /** 蓝票历史记录 ID */ historyId: string + /** 平台数电账号 ID。企业管理员冲红时用于指定开票员 */ + digitalAccountId?: string | null /** 冲红原因:01开票有误 02销货退回 03服务中止 04销售折让 */ redReason: string /** 收票人名称 */ diff --git a/web/src/features/piaotong/invoice-history/index.vue b/web/src/features/piaotong/invoice-history/index.vue index b5790f4..db65aa5 100644 --- a/web/src/features/piaotong/invoice-history/index.vue +++ b/web/src/features/piaotong/invoice-history/index.vue @@ -431,6 +431,16 @@ label-placement="top" require-mark-placement="right-hanging" > + + + ([]) const redForm = reactive({ historyId: '', + digitalAccountId: null as string | null, redReason: '01', takerName: '', takerTel: '', @@ -989,17 +1004,40 @@ const redReasonOptions = [ { label: '销售折让', value: '04' } ] +const digitalAccountOptions = computed(() => + digitalAccounts.value.map((item) => ({ + label: `${item.account}${item.name ? `(${item.name})` : ''}`, + value: item.id + })) +) + const redFormRules = { + digitalAccountId: [{ required: true, message: '请选择开票员', trigger: 'change' }], redReason: [{ required: true, message: '请选择冲红原因', trigger: 'change' }] } -function startRedTask(item: InvoiceHistoryItem) { +async function ensureDigitalAccountsLoaded() { + if (digitalAccounts.value.length > 0) return + digitalAccountLoading.value = true + try { + digitalAccounts.value = await listDigitalAccountsApi() + } finally { + digitalAccountLoading.value = false + } +} + +async function startRedTask(item: InvoiceHistoryItem) { redForm.historyId = item.id + redForm.digitalAccountId = null redForm.redReason = '01' redForm.takerName = '' redForm.takerTel = '' redForm.takerEmail = '' showRedForm.value = true + await ensureDigitalAccountsLoaded() + if (!redForm.digitalAccountId && digitalAccounts.value.length === 1) { + redForm.digitalAccountId = digitalAccounts.value[0].id + } } async function handleRedSubmit() { @@ -1012,6 +1050,7 @@ async function handleRedSubmit() { try { const payload: RedCreateRequest = { historyId: redForm.historyId, + digitalAccountId: redForm.digitalAccountId, redReason: redForm.redReason, takerName: redForm.takerName || undefined, takerTel: redForm.takerTel || undefined, diff --git a/web/src/features/statistics/openapi/index.vue b/web/src/features/statistics/openapi/index.vue index 6813ebe..a03c649 100644 --- a/web/src/features/statistics/openapi/index.vue +++ b/web/src/features/statistics/openapi/index.vue @@ -45,7 +45,7 @@ - +
= [ h(NTag, { type: statusTagType(row.status), size: 'small' }, { default: () => row.status }) }, { title: 'PT码', key: 'ptCode', width: 90 }, + { title: '错误', key: 'errorMessage', minWidth: 260, ellipsis: { tooltip: true } }, { title: '查询次数', key: 'pollCount', width: 90, render: (row) => `${row.pollCount}/${row.maxPollCount}` }, { title: '重试', key: 'attemptCount', width: 90, render: (row) => `${row.attemptCount}/${row.maxAttemptCount}` }, - { title: '下次执行', key: 'nextRunAt', minWidth: 150 }, - { title: '错误', key: 'errorMessage', minWidth: 180 } + { title: '下次执行', key: 'nextRunAt', minWidth: 150 } ] const taskPagination = reactive({ diff --git a/web/src/features/system/menus/index.vue b/web/src/features/system/menus/index.vue index fe670cf..6e40f17 100644 --- a/web/src/features/system/menus/index.vue +++ b/web/src/features/system/menus/index.vue @@ -262,7 +262,7 @@ function renderLabel(payload: { option: unknown }) { ? h(NTag, { size: 'small', type: 'primary' }, { default: () => '基础内置' }) : null ]), - h(NSpace, { size: 6, class: 'menu-tree-actions' }, () => [ + h(NSpace, { size: 6, align: 'center', class: 'menu-tree-actions table-action-buttons' }, () => [ h( NButton, { size: 'small', tertiary: true, class: 'action-btn', onClick: () => openCreate(node.id) }, diff --git a/web/src/features/system/roles/index.vue b/web/src/features/system/roles/index.vue index ef117b6..5ec93d5 100644 --- a/web/src/features/system/roles/index.vue +++ b/web/src/features/system/roles/index.vue @@ -256,7 +256,7 @@ const columns = computed>(() => [ title: '操作', key: 'actions', render: (r) => - h(NSpace, { size: 6 }, () => [ + h(NSpace, { size: 6, align: 'center', class: 'table-action-buttons' }, () => [ h( NButton, { diff --git a/web/src/features/system/users/index.vue b/web/src/features/system/users/index.vue index edd16f8..899516e 100644 --- a/web/src/features/system/users/index.vue +++ b/web/src/features/system/users/index.vue @@ -660,7 +660,7 @@ const columns = computed>(() => [ render: (row) => h( NSpace, - { size: 6 }, + { size: 6, align: 'center', class: 'table-action-buttons' }, { default: () => [ h( diff --git a/web/src/style.css b/web/src/style.css index 4d36aa1..52f8448 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -213,6 +213,40 @@ select { min-width: 68px; } +.table-action-buttons, +.row-actions, +.table-actions, +.action-buttons { + display: flex; + align-items: center; + gap: 6px; + line-height: 1; + white-space: nowrap; +} + +.table-action-buttons { + flex-wrap: nowrap; +} + +.table-action-buttons .n-button, +.row-actions .n-button, +.table-actions .n-button, +.action-buttons .n-button { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + vertical-align: middle; +} + +.table-action-buttons .n-button__content, +.row-actions .n-button__content, +.table-actions .n-button__content, +.action-buttons .n-button__content { + display: inline-flex; + align-items: center; +} + .soft-stat { border: 1px solid var(--app-border); border-radius: 12px;