提升OpenAPI接口高并发能力
This commit is contained in:
@@ -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`)
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 开票相关
|
||||
// =============================================
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user