diff --git a/apps/web-antd/src/views/log/queue-job-business/index.vue b/apps/web-antd/src/views/log/queue-job-business/index.vue new file mode 100644 index 00000000..7e5d0cf9 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job-business/index.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/apps/web-antd/src/views/log/queue-job-infra/index.vue b/apps/web-antd/src/views/log/queue-job-infra/index.vue new file mode 100644 index 00000000..cd0251fc --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job-infra/index.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/apps/web-antd/src/views/log/queue-job-monitor/api/index.ts b/apps/web-antd/src/views/log/queue-job-monitor/api/index.ts new file mode 100644 index 00000000..03de4237 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job-monitor/api/index.ts @@ -0,0 +1,85 @@ +/** + * 队列任务监控大屏 API + */ +import { requestClient } from '#/api/request'; + +const prefix = 'queue-job/'; + +export type QueueJobMonitorSummary = { + total: number; + pending: number; + pending_total: number; + delayed: number; + running: number; + success: number; + failed: number; + cancelled: number; + fail_rate: number | null; +}; + +export type QueueJobMonitorTrendItem = { + label: string; + start_at: number; + created: number; + success: number; + failed: number; +}; + +export type QueueJobMonitorResult = { + now: number; + hours: number; + summary: QueueJobMonitorSummary; + by_category: Array<{ + category: number; + category_txt: string; + total: number; + pending: number; + running: number; + success: number; + failed: number; + }>; + trend: QueueJobMonitorTrendItem[]; + top_failed_jobs: Array<{ + job_name: string; + label: string; + count: number; + }>; + recent_failures: Array<{ + id: number; + title: string; + job_name: string; + job_name_txt: string; + category: number; + category_txt: string; + error_message: string; + updated_at: string | number; + created_at: string | number; + }>; +}; + +/** + * 拉取监控统计;hours 为趋势窗口(默认 24) + */ +export async function getQueueJobStats(hours = 24) { + return requestClient.get(`${prefix}stats`, { + params: { hours }, + }); +} + +export type QueueJobProbeResult = { + normal_id: number; + delayed_id: number; + fail_id: number; + delay_seconds: number; + available_at: number; +}; + +/** + * 超管测试探针:普通 / 延迟 / 失败 各投一条 + */ +export async function createQueueJobProbe(data: { + delay_amount: number; + delay_unit: 'second' | 'minute' | 'hour' | 'day'; +}) { + return requestClient.post(`${prefix}create-probe`, data); +} diff --git a/apps/web-antd/src/views/log/queue-job-monitor/components/CategoryPieChart.vue b/apps/web-antd/src/views/log/queue-job-monitor/components/CategoryPieChart.vue new file mode 100644 index 00000000..b6514567 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job-monitor/components/CategoryPieChart.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/apps/web-antd/src/views/log/queue-job-monitor/components/ProbeModal.vue b/apps/web-antd/src/views/log/queue-job-monitor/components/ProbeModal.vue new file mode 100644 index 00000000..0235e396 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job-monitor/components/ProbeModal.vue @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-antd/src/views/log/queue-job-monitor/components/StatusTrendChart.vue b/apps/web-antd/src/views/log/queue-job-monitor/components/StatusTrendChart.vue new file mode 100644 index 00000000..4a78a4ce --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job-monitor/components/StatusTrendChart.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/apps/web-antd/src/views/log/queue-job-monitor/index.vue b/apps/web-antd/src/views/log/queue-job-monitor/index.vue new file mode 100644 index 00000000..235767e6 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job-monitor/index.vue @@ -0,0 +1,272 @@ + + + + + + + 实时快照 · 每 30 秒自动刷新 + + ({{ new Date(Number(data.now) * 1000).toLocaleString() }}) + + + + + + 测试任务 + + + 刷新 + + + + + + + + + + {{ card.title }} + + {{ Number(card.value || 0).toLocaleString() }} + + {{ card.tip }} + + + + + + + + + + + + + + + + 暂无失败 + + + {{ item.label }} + {{ item.count }} + + + 业务失败列表 + 基础设施失败列表 + + + + + + + + + + + diff --git a/apps/web-antd/src/views/log/queue-job/api/index.ts b/apps/web-antd/src/views/log/queue-job/api/index.ts new file mode 100644 index 00000000..0ea30e87 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job/api/index.ts @@ -0,0 +1,48 @@ +/** + * 队列任务管理 API(业务/基础设施共用,靠 category 区分) + */ +import { requestClient } from '#/api/request'; + +const prefix = 'queue-job/'; + +export async function getQueueJobList(data: any) { + return requestClient.get(`${prefix}list`, { params: data }); +} + +export async function getQueueJobDetail(id: number) { + return requestClient.get(`${prefix}detail`, { params: { id } }); +} + +/** + * 某任务的取消/重跑操作记录 + */ +export async function getQueueJobOpLogList(id: number) { + return requestClient.get(`${prefix}op-log-list`, { params: { id } }); +} + +export async function getQueueJobStatusOption() { + return requestClient.get<{ value: number; label: string }[]>( + `${prefix}status-option`, + ); +} + +export async function getQueueJobNameOption(category: number) { + return requestClient.get<{ value: string; label: string }[]>( + `${prefix}job-name-option`, + { params: { category } }, + ); +} + +/** + * 取消排队中任务 + */ +export async function cancelQueueJob(id: number) { + return requestClient.post(`${prefix}cancel`, { id }); +} + +/** + * 失败/取消后重跑 + */ +export async function retryQueueJob(id: number) { + return requestClient.post(`${prefix}retry`, { id }); +} diff --git a/apps/web-antd/src/views/log/queue-job/components/QueueJobList.vue b/apps/web-antd/src/views/log/queue-job/components/QueueJobList.vue new file mode 100644 index 00000000..9c02c91c --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job/components/QueueJobList.vue @@ -0,0 +1,240 @@ + + + + + + + + + + + {{ row.title || '-' }} + + + + + {{ row.delay_type_txt || (Number(row.is_delayed) === 1 ? '延迟' : '普通') }} + + + + + {{ row.status_txt || row.status }} + + + + {{ + previewError(row.error_message) + }} + + + + {{ row.available_at || row.created_at || '-' }} + + 延迟 + {{ + Number(row.delay_seconds) > 0 + ? formatDelaySeconds(Number(row.delay_seconds)) + : '' + }} + + + + + + + + + diff --git a/apps/web-antd/src/views/log/queue-job/components/detail-modal.vue b/apps/web-antd/src/views/log/queue-job/components/detail-modal.vue new file mode 100644 index 00000000..d03250d4 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job/components/detail-modal.vue @@ -0,0 +1,157 @@ + + + + + + {{ data.id }} + + + {{ data.status_txt || data.status }} + + + + {{ data.title || '-' }} + + + {{ data.job_name_txt || '-' }} + + + {{ data.category_txt || data.category }} + + + {{ data.user_name || '-' }}(ID: {{ data.user_id || 0 }}) + + + {{ data.time_difference || '-' }} + + + {{ data.created_at || '-' }} + + + {{ data.available_at || data.created_at || '-' }} + + 延迟 + + + (相对创建 {{ data.delay_type_txt || '延迟' }}) + + + + {{ data.updated_at || '-' }} + + + {{ data.detail || '-' }} + + + {{ data.job_name || '-' }} + + + {{ data.error_message || '(无)' }} + + + {{ formatJson(data.payload) }} + + + + + #{{ item.id }} + + {{ item.status_txt }} + + {{ item.title }} + {{ item.created_at }} + + (来自 #{{ item.retry_from_id }}) + + + + - + + + + + + diff --git a/apps/web-antd/src/views/log/queue-job/components/op-log-modal.vue b/apps/web-antd/src/views/log/queue-job/components/op-log-modal.vue new file mode 100644 index 00000000..90791f6d --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job/components/op-log-modal.vue @@ -0,0 +1,89 @@ + + + + + + + + + + {{ row.op_type_txt || row.op_type }} + + {{ row.created_at || '-' }} + + + 操作人:{{ row.operator_name || '-' }} + (ID: {{ row.operator_id || 0 }}) + + + 关联任务:#{{ row.related_job_id }} + + + {{ row.remark }} + + + + + diff --git a/apps/web-antd/src/views/log/queue-job/config/constants.ts b/apps/web-antd/src/views/log/queue-job/config/constants.ts new file mode 100644 index 00000000..24896e26 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job/config/constants.ts @@ -0,0 +1,54 @@ +/** + * 队列任务状态/分类常量(与后端 Enum 对齐,接口失败时作兜底) + */ +export const QUEUE_JOB_STATUS_OPTIONS = [ + { label: '排队中', value: 0 }, + { label: '正在执行', value: 1 }, + { label: '已完成', value: 2 }, + { label: '队列异常', value: 3 }, + { label: '已取消', value: 4 }, +]; + +/** 普通 / 延迟 筛选(与后端 delay_type 对齐) */ +export const QUEUE_JOB_DELAY_TYPE_OPTIONS = [ + { label: '普通队列', value: 0 }, + { label: '延迟队列', value: 1 }, +]; + +/** 1 业务 2 基础设施 */ +export const QUEUE_JOB_CATEGORY = { + BUSINESS: 1, + INFRA: 2, +} as const; + +export function statusTagColor(status: number): string { + if (status === 0) return 'default'; + if (status === 1) return 'processing'; + if (status === 2) return 'success'; + if (status === 3) return 'error'; + if (status === 4) return 'warning'; + return 'default'; +} + +/** + * 延迟秒数可读文案(列表「延迟」标签旁展示) + */ +export function formatDelaySeconds(seconds: number): string { + const s = Math.max(0, Math.floor(Number(seconds) || 0)); + if (s < 60) return `${s}秒`; + if (s < 3600) return `${Math.round(s / 60)}分钟`; + if (s < 86400) { + const h = Math.floor(s / 3600); + const m = Math.round((s % 3600) / 60); + return m > 0 ? `${h}小时${m}分` : `${h}小时`; + } + const d = Math.floor(s / 86400); + const h = Math.round((s % 86400) / 3600); + return h > 0 ? `${d}天${h}小时` : `${d}天`; +} + +export function opTypeTagColor(opType: number): string { + if (opType === 1) return 'warning'; + if (opType === 2) return 'processing'; + return 'default'; +} diff --git a/apps/web-antd/src/views/log/queue-job/config/search.ts b/apps/web-antd/src/views/log/queue-job/config/search.ts new file mode 100644 index 00000000..191e9d24 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job/config/search.ts @@ -0,0 +1,106 @@ +import type { VbenFormProps } from '#/adapter/form'; + +import dayjs from 'dayjs'; + +import { + QUEUE_JOB_DELAY_TYPE_OPTIONS, + QUEUE_JOB_STATUS_OPTIONS, +} from './constants'; + +/** + * 队列任务搜索表单 + * 业务队列额外提供「普通/延迟」筛选 + */ +export function createFormOptions(options?: { + showDelayFilter?: boolean; +}): VbenFormProps { + const schema: VbenFormProps['schema'] = [ + { + component: 'VbenInput', + componentProps: { + placeholder: '标题 / 简介 / 错误 / 任务类', + allowClear: true, + }, + fieldName: 'keyword', + label: '关键词', + }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [...QUEUE_JOB_STATUS_OPTIONS], + placeholder: '全部', + }, + fieldName: 'status', + label: '状态', + }, + { + component: 'Select', + componentProps: { + allowClear: true, + options: [], + placeholder: '全部', + showSearch: true, + optionFilterProp: 'label', + }, + fieldName: 'job_name', + label: '任务类型', + }, + ]; + // 业务队列:普通 / 延迟 筛选 + if (options?.showDelayFilter) { + schema.push({ + component: 'Select', + componentProps: { + allowClear: true, + options: [...QUEUE_JOB_DELAY_TYPE_OPTIONS], + placeholder: '全部', + }, + fieldName: 'delay_type', + label: '队列类型', + }); + } + schema.push({ + component: 'RangePicker', + componentProps: { + format: 'YYYY-MM-DD', + valueFormat: 'YYYY-MM-DD', + }, + // 默认近 7 天 + defaultValue: [ + dayjs().subtract(6, 'day').format('YYYY-MM-DD'), + dayjs().format('YYYY-MM-DD'), + ], + fieldName: 'search_time', + label: '时间范围', + }); + return { + collapsed: false, + schema, + showCollapseButton: true, + submitButtonOptions: { + content: '查询', + }, + submitOnChange: false, + submitOnEnter: true, + }; +} + +/** + * search_time → start_time/end_time 时间戳 + */ +export function normalizeQueueJobFilters(formValues: Record = {}) { + const params: Record = { ...formValues }; + const range = params.search_time; + delete params.search_time; + if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) { + params.start_time = dayjs(range[0]).startOf('day').unix(); + params.end_time = dayjs(range[1]).endOf('day').unix(); + } + Object.keys(params).forEach((k) => { + if (params[k] === '' || params[k] === undefined || params[k] === null) { + delete params[k]; + } + }); + return params; +} diff --git a/apps/web-antd/src/views/log/queue-job/config/table.ts b/apps/web-antd/src/views/log/queue-job/config/table.ts new file mode 100644 index 00000000..c9230a15 --- /dev/null +++ b/apps/web-antd/src/views/log/queue-job/config/table.ts @@ -0,0 +1,84 @@ +import type { VxeGridProps } from '#/adapter/vxe-table'; + +import { getQueueJobList } from '../api'; +import { normalizeQueueJobFilters } from './search'; + +/** + * 按分类生成表格配置(业务/基础设施共用列) + * 标题可点开操作记录;执行时间列展示延迟标记 + */ +export function createGridOptions(category: number): VxeGridProps { + return { + checkboxConfig: { + highlight: true, + labelField: '', + }, + columns: [ + { field: 'id', title: 'ID', width: 80 }, + { + field: 'title', + title: '标题', + minWidth: 160, + slots: { default: 'title' }, + }, + { field: 'job_name_txt', title: '任务类型', minWidth: 140 }, + { + field: 'delay_type_txt', + title: '队列类型', + width: 100, + slots: { default: 'delay_type' }, + }, + { + field: 'status_txt', + title: '状态', + width: 100, + slots: { default: 'status' }, + }, + { field: 'time_difference', title: '耗时', width: 90 }, + { field: 'user_name', title: '操作人', width: 100 }, + { + field: 'error_message', + title: '错误摘要', + minWidth: 160, + slots: { default: 'error' }, + }, + { field: 'retry_from_id', title: '重跑自', width: 80 }, + { field: 'created_at', title: '创建时间', width: 170 }, + { + field: 'available_at', + title: '执行时间', + width: 200, + slots: { default: 'available_at' }, + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'action' }, + }, + ], + keepSource: true, + pagerConfig: {}, + proxyConfig: { + ajax: { + query: async ({ page }, formValues) => { + return await getQueueJobList({ + page: page.currentPage, + pageSize: page.pageSize, + category, + ...normalizeQueueJobFilters(formValues || {}), + }); + }, + }, + }, + height: 'auto', + toolbarConfig: { + search: true, + refresh: true, + zoom: true, + slots: { + buttons: 'toolbar-buttons', + }, + }, + }; +}
{{ data.job_name || '-' }}
{{ data.error_message || '(无)' }}
{{ formatJson(data.payload) }}