提升OpenAPI接口高并发能力

This commit is contained in:
BBIT-Kai
2026-05-25 14:41:24 +08:00
parent c2899ae64d
commit 62f9fd5b7f
15 changed files with 1454 additions and 43 deletions
+66
View File
@@ -221,6 +221,72 @@ export function openApiStatisticsApi(): Promise<OpenApiStatisticsItem[]> {
return http.get('/pt/openapi/statistics')
}
export interface OpenInvoiceTaskOverviewItem {
digitalAccountId: string
apiKey: string
account?: string | null
status: string
pauseCode?: string | null
reason?: string | null
pending: number
processing: number
success: number
failed: number
waitingAuth: number
total: number
lastCreatedAt?: string | null
}
export interface OpenInvoiceTaskItem {
id: string
digitalAccountId: string
apiKey: string
account?: string | null
taskType: string
sourceType: string
runMode: string
invoiceReqSerialNo: string
batchNo?: string | null
status: string
ptCode?: string | null
errorMessage?: string | null
attemptCount: number
maxAttemptCount: number
pollCount: number
maxPollCount: number
nextRunAt: string
createdAt: string
updatedAt?: string | null
startedAt?: string | null
finishedAt?: string | null
}
export function openInvoiceTaskOverviewApi(): Promise<OpenInvoiceTaskOverviewItem[]> {
return http.get('/pt/openapi/tasks/overview')
}
export function openInvoiceTaskPageApi(params: {
page: number
pageSize: number
digitalAccountId?: string
status?: string | null
sourceType?: string | null
runMode?: string | null
}): Promise<PageResult<OpenInvoiceTaskItem>> {
return http.get('/pt/openapi/tasks', { params })
}
export function pauseOpenInvoiceTaskQueueApi(
digitalAccountId: string,
reason?: string
): Promise<string> {
return http.post(`/pt/openapi/tasks/queues/${digitalAccountId}/pause`, { reason })
}
export function resumeOpenInvoiceTaskQueueApi(digitalAccountId: string): Promise<string> {
return http.post(`/pt/openapi/tasks/queues/${digitalAccountId}/resume`)
}
// =============================================
// 开票相关
// =============================================
+366 -18
View File
@@ -11,6 +11,25 @@
</div>
</div>
<div class="queue-summary">
<div class="metric">
<span>数电账号</span>
<strong>{{ rows.length }}</strong>
</div>
<div class="metric">
<span>待处理</span>
<strong>{{ totals.pending }}</strong>
</div>
<div class="metric">
<span>处理中</span>
<strong>{{ totals.processing }}</strong>
</div>
<div class="metric">
<span>需认证</span>
<strong>{{ totals.waitingAuth }}</strong>
</div>
</div>
<div class="card-body card-body-fill table-fill">
<n-data-table
flex-height
@@ -19,43 +38,372 @@
:data="rows"
:loading="loading"
:pagination="{ pageSize: 12 }"
:row-key="(row: OpenApiStatisticsItem) => `${row.digitalAccountId}-${row.interfaceCode}`"
:scroll-x="1040"
:row-key="(row: OpenInvoiceTaskOverviewItem) => row.digitalAccountId"
:row-props="rowProps"
:scroll-x="1160"
/>
</div>
</section>
<n-drawer v-model:show="drawerVisible" :width="760">
<n-drawer-content :title="drawerTitle" closable>
<div class="drawer-toolbar">
<n-select
v-model:value="taskStatus"
clearable
size="small"
placeholder="全部状态"
:options="statusOptions"
class="task-filter"
@update:value="loadTasks(1)"
/>
<n-select
v-model:value="taskSourceType"
clearable
size="small"
placeholder="全部来源"
:options="sourceTypeOptions"
class="task-filter"
@update:value="loadTasks(1)"
/>
<n-select
v-model:value="taskRunMode"
clearable
size="small"
placeholder="全部模式"
:options="runModeOptions"
class="task-filter"
@update:value="loadTasks(1)"
/>
<n-button size="small" :loading="taskLoading" @click="loadTasks(taskPage)">
<template #icon><n-icon :component="RefreshCw" /></template>
刷新
</n-button>
</div>
<n-data-table
size="small"
:columns="taskColumns"
:data="tasks"
:loading="taskLoading"
:pagination="taskPagination"
:scroll-x="980"
remote
@update:page="loadTasks"
@update:page-size="changeTaskPageSize"
/>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script setup lang="ts">
import { h, onMounted, ref } from 'vue'
import type { DataTableColumns } from 'naive-ui'
import { NButton, NDataTable, NIcon, NTag } from 'naive-ui'
import { RefreshCw } from 'lucide-vue-next'
import { openApiStatisticsApi } from '@/api/piaotong'
import type { OpenApiStatisticsItem } from '@/api/piaotong'
import { computed, h, onMounted, reactive, ref } from 'vue'
import type { DataTableColumns, PaginationProps } from 'naive-ui'
import { NButton, NDataTable, NDrawer, NDrawerContent, NIcon, NSelect, NTag } from 'naive-ui'
import { ListTree, Pause, Play, RefreshCw } from 'lucide-vue-next'
import {
openInvoiceTaskOverviewApi,
openInvoiceTaskPageApi,
pauseOpenInvoiceTaskQueueApi,
resumeOpenInvoiceTaskQueueApi
} from '@/api/piaotong'
import type { OpenInvoiceTaskItem, OpenInvoiceTaskOverviewItem } from '@/api/piaotong'
import { appMessage } from '@/utils/message'
const loading = ref(false)
const rows = ref<OpenApiStatisticsItem[]>([])
const rows = ref<OpenInvoiceTaskOverviewItem[]>([])
const selected = ref<OpenInvoiceTaskOverviewItem | null>(null)
const drawerVisible = ref(false)
const taskLoading = ref(false)
const tasks = ref<OpenInvoiceTaskItem[]>([])
const taskPage = ref(1)
const taskPageSize = ref(20)
const taskTotal = ref(0)
const taskStatus = ref<string | null>(null)
const taskSourceType = ref<string | null>(null)
const taskRunMode = ref<string | null>(null)
const columns: DataTableColumns<OpenApiStatisticsItem> = [
{ title: '数电账号ID', key: 'digitalAccountId', minWidth: 220 },
{ title: '接口', key: 'interfaceCode', minWidth: 180 },
{ title: '调用次数', key: 'total', width: 100 },
{ title: '成功', key: 'success', width: 100, render: (row) => h(NTag, { type: 'success' }, { default: () => row.success }) },
{ title: '失败', key: 'failed', width: 100, render: (row) => h(NTag, { type: row.failed > 0 ? 'error' : 'default' }, { default: () => row.failed }) },
{ title: '平均耗时(ms)', key: 'avgCostMs', width: 140 },
{ title: '最近调用', key: 'lastCalledAt', minWidth: 180 }
const statusOptions = [
{ label: '待处理', value: 'PENDING' },
{ label: '处理中', value: 'PROCESSING' },
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAILED' },
{ label: '需认证', value: 'WAITING_AUTH' }
]
const sourceTypeOptions = [
{ label: '单笔', value: 'SINGLE' },
{ label: '批量', value: 'BATCH' },
{ label: '测试', value: 'TEST' }
]
const runModeOptions = [
{ label: '生产', value: 'REAL' },
{ label: '模拟', value: 'SIMULATED' }
]
const totals = computed(() =>
rows.value.reduce(
(acc, row) => {
acc.pending += row.pending
acc.processing += row.processing
acc.waitingAuth += row.waitingAuth
return acc
},
{ pending: 0, processing: 0, waitingAuth: 0 }
)
)
const drawerTitle = computed(() => {
const row = selected.value
if (!row) return '任务明细'
return `${row.account || row.digitalAccountId} 任务明细`
})
const statusTagType = (status: string) => {
if (status === 'RUNNING' || status === 'SUCCESS') return 'success'
if (status === 'PAUSED' || status === 'WAITING_AUTH') return 'warning'
if (status === 'FAILED') return 'error'
if (status === 'PROCESSING') return 'info'
return 'default'
}
const columns: DataTableColumns<OpenInvoiceTaskOverviewItem> = [
{ title: '数电账号', key: 'account', minWidth: 160, render: (row) => row.account || '-' },
{ title: '数电账号ID', key: 'digitalAccountId', minWidth: 220 },
{
title: '队列',
key: 'status',
width: 110,
render: (row) =>
h(NTag, { type: statusTagType(row.status), size: 'small' }, { default: () => row.status })
},
{ title: '待处理', key: 'pending', width: 90 },
{ title: '处理中', key: 'processing', width: 90 },
{
title: '需认证',
key: 'waitingAuth',
width: 90,
render: (row) =>
h(
NTag,
{ type: row.waitingAuth > 0 ? 'warning' : 'default', size: 'small' },
{ default: () => row.waitingAuth }
)
},
{ title: '成功', key: 'success', width: 90 },
{
title: '失败',
key: 'failed',
width: 90,
render: (row) =>
h(
NTag,
{ type: row.failed > 0 ? 'error' : 'default', size: 'small' },
{ default: () => row.failed }
)
},
{ title: '总数', key: 'total', width: 90 },
{ title: '最近任务', key: 'lastCreatedAt', minWidth: 150 },
{
title: '操作',
key: 'actions',
width: 168,
align: 'center',
render: (row) =>
h(
'div',
{ class: 'action-buttons' },
[
h(
NButton,
{
size: 'small',
secondary: true,
title: '查看任务详情',
onClick: (event: MouseEvent) => {
event.stopPropagation()
openDrawer(row)
}
},
{
icon: () => h(NIcon, { component: ListTree }),
default: () => '详情'
}
),
h(
NButton,
{
size: 'small',
secondary: true,
type: row.status === 'PAUSED' ? 'success' : 'warning',
title: row.status === 'PAUSED' ? '恢复队列' : '暂停队列',
onClick: (event: MouseEvent) => {
event.stopPropagation()
row.status === 'PAUSED' ? resume(row) : pause(row)
}
},
{
icon: () => h(NIcon, { component: row.status === 'PAUSED' ? Play : Pause }),
default: () => (row.status === 'PAUSED' ? '恢复' : '暂停')
}
)
]
)
}
]
function rowProps(row: OpenInvoiceTaskOverviewItem) {
return {
style: 'cursor: pointer;',
onClick: () => {
openDrawer(row)
}
}
}
const taskColumns: DataTableColumns<OpenInvoiceTaskItem> = [
{ title: '票号', key: 'invoiceReqSerialNo', minWidth: 170 },
{ title: '任务', key: 'taskType', width: 120 },
{ title: '来源', key: 'sourceType', width: 90 },
{ title: '模式', key: 'runMode', width: 90 },
{
title: '状态',
key: 'status',
width: 110,
render: (row) =>
h(NTag, { type: statusTagType(row.status), size: 'small' }, { default: () => row.status })
},
{ title: 'PT码', key: 'ptCode', width: 90 },
{ 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 }
]
const taskPagination = reactive<PaginationProps>({
page: taskPage.value,
pageSize: taskPageSize.value,
itemCount: taskTotal.value,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
async function load() {
loading.value = true
try {
rows.value = await openApiStatisticsApi()
rows.value = await openInvoiceTaskOverviewApi()
} finally {
loading.value = false
}
}
async function loadTasks(page = 1) {
if (!selected.value) return
taskLoading.value = true
try {
const result = await openInvoiceTaskPageApi({
digitalAccountId: selected.value.digitalAccountId,
status: taskStatus.value,
sourceType: taskSourceType.value,
runMode: taskRunMode.value,
page,
pageSize: taskPageSize.value
})
tasks.value = result.items
taskPage.value = result.page
taskTotal.value = result.total
taskPagination.page = result.page
taskPagination.itemCount = result.total
} finally {
taskLoading.value = false
}
}
function openDrawer(row: OpenInvoiceTaskOverviewItem) {
selected.value = row
drawerVisible.value = true
taskStatus.value = null
taskSourceType.value = null
taskRunMode.value = null
loadTasks(1)
}
async function changeTaskPageSize(pageSize: number) {
taskPageSize.value = pageSize
taskPagination.pageSize = pageSize
await loadTasks(1)
}
async function pause(row: OpenInvoiceTaskOverviewItem) {
await pauseOpenInvoiceTaskQueueApi(row.digitalAccountId, '手动暂停')
appMessage.success('队列已暂停')
await load()
}
async function resume(row: OpenInvoiceTaskOverviewItem) {
await resumeOpenInvoiceTaskQueueApi(row.digitalAccountId)
appMessage.success('队列已恢复')
await load()
if (selected.value?.digitalAccountId === row.digitalAccountId) {
await loadTasks(taskPage.value)
}
}
onMounted(load)
</script>
<style scoped>
.queue-summary {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 12px;
padding: 0 16px 12px;
}
.metric {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
background: var(--card-color);
}
.metric span {
display: block;
color: var(--text-color-2);
font-size: 12px;
}
.metric strong {
display: block;
margin-top: 4px;
font-size: 22px;
font-weight: 700;
}
.drawer-toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.task-filter {
width: 140px;
}
.action-buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 144px;
}
@media (max-width: 720px) {
.queue-summary {
grid-template-columns: repeat(2, minmax(120px, 1fr));
}
}
</style>