修复企业管理员角色无法红冲的问题;修复操作列按钮高低不齐的问题

This commit is contained in:
BBIT-Kai
2026-05-27 09:29:06 +08:00
parent 9fd80980e7
commit a716c481da
10 changed files with 118 additions and 22 deletions
@@ -6,6 +6,10 @@ import kotlinx.serialization.Serializable
data class RedCreateRequest( data class RedCreateRequest(
val historyId: String, val historyId: String,
/**
* 平台数电账号 ID。企业管理员发起冲红时用于指定开票员。
*/
val digitalAccountId: String? = null,
/** /**
* 冲红原因 * 冲红原因
* *
@@ -222,12 +222,28 @@ object OpenInvoiceTaskService {
.where { accountScope(user) } .where { accountScope(user) }
.associateBy { it[PtDigitalAccountTable.id] } .associateBy { it[PtDigitalAccountTable.id] }
val total = OpenInvoiceTaskTable.selectAll().where { where }.count() val total = OpenInvoiceTaskTable.selectAll().where { where }.count()
val items = OpenInvoiceTaskTable.selectAll() val taskRows = OpenInvoiceTaskTable.selectAll()
.where { where } .where { where }
.orderBy(OpenInvoiceTaskTable.createdAt, SortOrder.DESC) .orderBy(OpenInvoiceTaskTable.createdAt, SortOrder.DESC)
.limit(pageSize) .limit(pageSize)
.offset(((page - 1).coerceAtLeast(0) * pageSize).toLong()) .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) PageResult(items, page, pageSize, total)
} }
@@ -302,9 +318,9 @@ object OpenInvoiceTaskService {
val runMode = task[OpenInvoiceTaskTable.runMode] val runMode = task[OpenInvoiceTaskTable.runMode]
val invoiceReqSerialNo = task[OpenInvoiceTaskTable.invoiceReqSerialNo] val invoiceReqSerialNo = task[OpenInvoiceTaskTable.invoiceReqSerialNo]
try { try {
val code = if (runMode == MODE_SIMULATED) { val (code, message) = if (runMode == MODE_SIMULATED) {
delay(2000) delay(2000)
simulatedQueryCode(invoiceReqSerialNo, task[OpenInvoiceTaskTable.pollCount]) simulatedQueryCode(invoiceReqSerialNo, task[OpenInvoiceTaskTable.pollCount]) to null
} else { } else {
val res = PTApi.queryInvoiceInfo( val res = PTApi.queryInvoiceInfo(
QueryInvoiceRequest( QueryInvoiceRequest(
@@ -320,9 +336,9 @@ object OpenInvoiceTaskService {
task[OpenInvoiceTaskTable.digitalAccountId], task[OpenInvoiceTaskTable.digitalAccountId],
) )
} }
res.code res.code to res.msg
} }
handleQueryCode(task, code) handleQueryCode(task, code, message)
} catch (e: PTException) { } catch (e: PTException) {
retryOrFail(task, e.code, e.message) retryOrFail(task, e.code, e.message)
} catch (e: Exception) { } 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) { when (code) {
"0000" -> finishQueryTask(task, STATUS_SUCCESS, code, null) "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 -> { AUTH_REQUIRED_CODE -> {
val message = "需要登录/风险认证" val authMessage = message?.takeIf { it.isNotBlank() } ?: "需要登录/风险认证"
dbQuery { dbQuery {
OpenInvoiceTaskTable.update({ OpenInvoiceTaskTable.id eq task[OpenInvoiceTaskTable.id] }) { OpenInvoiceTaskTable.update({ OpenInvoiceTaskTable.id eq task[OpenInvoiceTaskTable.id] }) {
it[status] = STATUS_WAITING_AUTH it[status] = STATUS_WAITING_AUTH
it[ptCode] = code it[ptCode] = code
it[errorMessage] = message it[errorMessage] = authMessage
it[updatedAt] = OffsetDateTime.now() it[updatedAt] = OffsetDateTime.now()
it[lockedBy] = null it[lockedBy] = null
it[lockedAt] = null it[lockedAt] = null
} }
} }
pauseApiKey(task[OpenInvoiceTaskTable.apiKey], code, message) pauseApiKey(task[OpenInvoiceTaskTable.apiKey], code, authMessage)
} }
"7777", "6666" -> requeueQueryTask(task, code) "7777", "6666" -> requeueQueryTask(task, code)
else -> requeueQueryTask(task, code) else -> requeueQueryTask(task, code)
@@ -693,7 +709,7 @@ object OpenInvoiceTaskService {
runMode = this[OpenInvoiceTaskTable.runMode], runMode = this[OpenInvoiceTaskTable.runMode],
) )
private fun ResultRow.toTaskItem(account: String?): OpenInvoiceTaskItem = private fun ResultRow.toTaskItem(account: String?, historyMessage: String?): OpenInvoiceTaskItem =
OpenInvoiceTaskItem( OpenInvoiceTaskItem(
id = this[OpenInvoiceTaskTable.id].toString(), id = this[OpenInvoiceTaskTable.id].toString(),
digitalAccountId = this[OpenInvoiceTaskTable.digitalAccountId].toString(), digitalAccountId = this[OpenInvoiceTaskTable.digitalAccountId].toString(),
@@ -706,7 +722,8 @@ object OpenInvoiceTaskService {
batchNo = this[OpenInvoiceTaskTable.batchNo], batchNo = this[OpenInvoiceTaskTable.batchNo],
status = this[OpenInvoiceTaskTable.status], status = this[OpenInvoiceTaskTable.status],
ptCode = this[OpenInvoiceTaskTable.ptCode], ptCode = this[OpenInvoiceTaskTable.ptCode],
errorMessage = this[OpenInvoiceTaskTable.errorMessage], errorMessage = this[OpenInvoiceTaskTable.errorMessage]
?: historyMessage?.takeIf { this[OpenInvoiceTaskTable.status] == STATUS_FAILED },
attemptCount = this[OpenInvoiceTaskTable.attemptCount], attemptCount = this[OpenInvoiceTaskTable.attemptCount],
maxAttemptCount = this[OpenInvoiceTaskTable.maxAttemptCount], maxAttemptCount = this[OpenInvoiceTaskTable.maxAttemptCount],
pollCount = this[OpenInvoiceTaskTable.pollCount], pollCount = this[OpenInvoiceTaskTable.pollCount],
@@ -28,7 +28,7 @@ object PTRedService {
* 红票接口调用 2.10 * 红票接口调用 2.10
*/ */
suspend fun invoiceRed(user: CurrentUser, req: RedCreateRequest): String { 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 invoiceReqSerialNo = PTClient.ptDate()
val historyId = Uuid.parse(req.historyId) val historyId = Uuid.parse(req.historyId)
val his = dbQuery { val his = dbQuery {
+2
View File
@@ -458,6 +458,8 @@ export function invoiceIssueApi(payload: InvoiceRequest): Promise<string> {
export interface RedCreateRequest { export interface RedCreateRequest {
/** 蓝票历史记录 ID */ /** 蓝票历史记录 ID */
historyId: string historyId: string
/** 平台数电账号 ID。企业管理员冲红时用于指定开票员 */
digitalAccountId?: string | null
/** 冲红原因:01开票有误 02销货退回 03服务中止 04销售折让 */ /** 冲红原因:01开票有误 02销货退回 03服务中止 04销售折让 */
redReason: string redReason: string
/** 收票人名称 */ /** 收票人名称 */
@@ -431,6 +431,16 @@
label-placement="top" label-placement="top"
require-mark-placement="right-hanging" require-mark-placement="right-hanging"
> >
<n-form-item label="开票员" path="digitalAccountId">
<n-select
v-model:value="redForm.digitalAccountId"
:options="digitalAccountOptions"
:loading="digitalAccountLoading"
placeholder="请选择开票员"
filterable
clearable
/>
</n-form-item>
<n-form-item label="冲红原因" path="redReason"> <n-form-item label="冲红原因" path="redReason">
<n-select <n-select
v-model:value="redForm.redReason" v-model:value="redForm.redReason"
@@ -496,6 +506,7 @@ import {
invoiceHistoryApi, invoiceHistoryApi,
invoiceKindMap, invoiceKindMap,
invoiceStatusMap, invoiceStatusMap,
listDigitalAccountsApi,
queryInvoiceApi, queryInvoiceApi,
redInvoiceCreateApi, redInvoiceCreateApi,
redInvoiceDownloadUrlApi, redInvoiceDownloadUrlApi,
@@ -507,6 +518,7 @@ import type {
InvoiceDetailGoods, InvoiceDetailGoods,
InvoiceDetailResponse, InvoiceDetailResponse,
InvoiceDetailVoucher, InvoiceDetailVoucher,
DigitalAccountItem,
InvoiceHistoryItem, InvoiceHistoryItem,
RedCreateRequest, RedCreateRequest,
RedInvoiceInfo RedInvoiceInfo
@@ -973,9 +985,12 @@ async function refreshStatus(item: InvoiceHistoryItem) {
const showRedForm = ref(false) const showRedForm = ref(false)
const redSubmitting = ref(false) const redSubmitting = ref(false)
const redFormRef = ref() const redFormRef = ref()
const digitalAccountLoading = ref(false)
const digitalAccounts = ref<DigitalAccountItem[]>([])
const redForm = reactive({ const redForm = reactive({
historyId: '', historyId: '',
digitalAccountId: null as string | null,
redReason: '01', redReason: '01',
takerName: '', takerName: '',
takerTel: '', takerTel: '',
@@ -989,17 +1004,40 @@ const redReasonOptions = [
{ label: '销售折让', value: '04' } { label: '销售折让', value: '04' }
] ]
const digitalAccountOptions = computed(() =>
digitalAccounts.value.map((item) => ({
label: `${item.account}${item.name ? `${item.name}` : ''}`,
value: item.id
}))
)
const redFormRules = { const redFormRules = {
digitalAccountId: [{ required: true, message: '请选择开票员', trigger: 'change' }],
redReason: [{ 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.historyId = item.id
redForm.digitalAccountId = null
redForm.redReason = '01' redForm.redReason = '01'
redForm.takerName = '' redForm.takerName = ''
redForm.takerTel = '' redForm.takerTel = ''
redForm.takerEmail = '' redForm.takerEmail = ''
showRedForm.value = true showRedForm.value = true
await ensureDigitalAccountsLoaded()
if (!redForm.digitalAccountId && digitalAccounts.value.length === 1) {
redForm.digitalAccountId = digitalAccounts.value[0].id
}
} }
async function handleRedSubmit() { async function handleRedSubmit() {
@@ -1012,6 +1050,7 @@ async function handleRedSubmit() {
try { try {
const payload: RedCreateRequest = { const payload: RedCreateRequest = {
historyId: redForm.historyId, historyId: redForm.historyId,
digitalAccountId: redForm.digitalAccountId,
redReason: redForm.redReason, redReason: redForm.redReason,
takerName: redForm.takerName || undefined, takerName: redForm.takerName || undefined,
takerTel: redForm.takerTel || undefined, takerTel: redForm.takerTel || undefined,
@@ -45,7 +45,7 @@
</div> </div>
</section> </section>
<n-drawer v-model:show="drawerVisible" :width="760"> <n-drawer v-model:show="drawerVisible" :width="980">
<n-drawer-content :title="drawerTitle" closable> <n-drawer-content :title="drawerTitle" closable>
<div class="drawer-toolbar"> <div class="drawer-toolbar">
<n-select <n-select
@@ -87,7 +87,7 @@
:data="tasks" :data="tasks"
:loading="taskLoading" :loading="taskLoading"
:pagination="taskPagination" :pagination="taskPagination"
:scroll-x="980" :scroll-x="1280"
remote remote
@update:page="loadTasks" @update:page="loadTasks"
@update:page-size="changeTaskPageSize" @update:page-size="changeTaskPageSize"
@@ -276,10 +276,10 @@ const taskColumns: DataTableColumns<OpenInvoiceTaskItem> = [
h(NTag, { type: statusTagType(row.status), size: 'small' }, { default: () => row.status }) h(NTag, { type: statusTagType(row.status), size: 'small' }, { default: () => row.status })
}, },
{ title: 'PT码', key: 'ptCode', width: 90 }, { 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: 'pollCount', width: 90, render: (row) => `${row.pollCount}/${row.maxPollCount}` },
{ title: '重试', key: 'attemptCount', width: 90, render: (row) => `${row.attemptCount}/${row.maxAttemptCount}` }, { title: '重试', key: 'attemptCount', width: 90, render: (row) => `${row.attemptCount}/${row.maxAttemptCount}` },
{ title: '下次执行', key: 'nextRunAt', minWidth: 150 }, { title: '下次执行', key: 'nextRunAt', minWidth: 150 }
{ title: '错误', key: 'errorMessage', minWidth: 180 }
] ]
const taskPagination = reactive<PaginationProps>({ const taskPagination = reactive<PaginationProps>({
+1 -1
View File
@@ -262,7 +262,7 @@ function renderLabel(payload: { option: unknown }) {
? h(NTag, { size: 'small', type: 'primary' }, { default: () => '基础内置' }) ? h(NTag, { size: 'small', type: 'primary' }, { default: () => '基础内置' })
: null : null
]), ]),
h(NSpace, { size: 6, class: 'menu-tree-actions' }, () => [ h(NSpace, { size: 6, align: 'center', class: 'menu-tree-actions table-action-buttons' }, () => [
h( h(
NButton, NButton,
{ size: 'small', tertiary: true, class: 'action-btn', onClick: () => openCreate(node.id) }, { size: 'small', tertiary: true, class: 'action-btn', onClick: () => openCreate(node.id) },
+1 -1
View File
@@ -256,7 +256,7 @@ const columns = computed<DataTableColumns<RoleItem>>(() => [
title: '操作', title: '操作',
key: 'actions', key: 'actions',
render: (r) => render: (r) =>
h(NSpace, { size: 6 }, () => [ h(NSpace, { size: 6, align: 'center', class: 'table-action-buttons' }, () => [
h( h(
NButton, NButton,
{ {
+1 -1
View File
@@ -660,7 +660,7 @@ const columns = computed<DataTableColumns<UserListItem>>(() => [
render: (row) => render: (row) =>
h( h(
NSpace, NSpace,
{ size: 6 }, { size: 6, align: 'center', class: 'table-action-buttons' },
{ {
default: () => [ default: () => [
h( h(
+34
View File
@@ -213,6 +213,40 @@ select {
min-width: 68px; 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 { .soft-stat {
border: 1px solid var(--app-border); border: 1px solid var(--app-border);
border-radius: 12px; border-radius: 12px;