feat: 增强下游重投与签名质量检测

This commit is contained in:
hectorzhao
2026-08-12 17:05:53 +08:00
parent 1d8d6701a6
commit 4994841709
23 changed files with 960 additions and 67 deletions
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AlertTriangle, BarChart3, CheckCircle2, Eye, RefreshCw, Search, TimerReset } from 'lucide-react';
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type EnterpriseApplication } from '@/api/adminApi';
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type EnterpriseApplication } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
@@ -153,7 +153,7 @@ export function AdminDownstreamDeliveriesPage() {
const [deliveryType, setDeliveryType] = useState('all');
const [applicationId, setApplicationId] = useState('all');
const [page, setPage] = useState(1);
const [pageSize] = useState(10);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
@@ -164,6 +164,28 @@ export function AdminDownstreamDeliveriesPage() {
const [requeueBusy, setRequeueBusy] = useState(false);
const [requeueResult, setRequeueResult] = useState<RequeueResult | null>(null);
const requeueInFlightRef = useRef(false);
const [taskPreview, setTaskPreview] = useState<DownstreamRequeuePreview | null>(null);
const [taskPreviewBusy, setTaskPreviewBusy] = useState(false);
const [taskReason, setTaskReason] = useState('');
const [taskRate, setTaskRate] = useState(10);
const [taskCreateBusy, setTaskCreateBusy] = useState(false);
const [requeueTasks, setRequeueTasks] = useState<DownstreamRequeueTask[]>([]);
const [selectedTask, setSelectedTask] = useState<DownstreamRequeueTask | null>(null);
const currentTaskFilter = useCallback(() => ({
keyword: keyword || undefined,
status,
deliveryType,
applicationId,
createdAtFrom: dateRange.start,
createdAtTo: dateRange.end,
}), [applicationId, dateRange.end, dateRange.start, deliveryType, keyword, status]);
const loadRequeueTasks = useCallback(() => {
adminApi.listDownstreamRequeueTasks({ page: 1, pageSize: 10 })
.then((response) => setRequeueTasks(response.items))
.catch(() => undefined);
}, []);
const loadData = useCallback(() => {
setLoading(true);
@@ -202,6 +224,39 @@ export function AdminDownstreamDeliveriesPage() {
loadData();
}, [loadData]);
useEffect(() => {
loadRequeueTasks();
const timer = window.setInterval(loadRequeueTasks, 3000);
return () => window.clearInterval(timer);
}, [loadRequeueTasks]);
const openTaskPreview = async () => {
setTaskPreviewBusy(true);
setError('');
try {
setTaskPreview(await adminApi.previewDownstreamRequeueTask(currentTaskFilter()));
setTaskReason('');
} catch (failure) {
setError(failure instanceof Error ? failure.message : '后台重投范围预检失败');
} finally {
setTaskPreviewBusy(false);
}
};
const createRequeueTask = async () => {
if (!taskPreview || taskReason.trim().length < 5) return;
setTaskCreateBusy(true);
try {
await adminApi.createDownstreamRequeueTask({ filter: taskPreview.filter, snapshotAt: taskPreview.snapshotAt, reason: taskReason.trim(), ratePerSecond: taskRate, consecutiveFailureLimit: 10 });
setTaskPreview(null);
loadRequeueTasks();
} catch (failure) {
setError(failure instanceof Error ? failure.message : '后台重投任务创建失败');
} finally {
setTaskCreateBusy(false);
}
};
const replayableStatuses = useMemo(() => new Set(['pending', 'delivered', 'failed', 'unconfirmed', 'rejected']), []);
const selectableIds = useMemo(
() => records.filter((item) => replayableStatuses.has(item.status)).map((item) => item.id),
@@ -455,6 +510,17 @@ export function AdminDownstreamDeliveriesPage() {
<strong>{selectedIds.length}</strong>
</p>
<div>
<label className="downstream-page-size">
<select value={pageSize} onChange={(event) => { setPageSize(Number(event.target.value)); setPage(1); setSelectedIds([]); }}>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</label>
<Button disabled={taskPreviewBusy} onClick={() => void openTaskPreview()} variant="secondary">
{taskPreviewBusy ? '范围预检中…' : '按筛选条件重投'}
</Button>
<Button
disabled={selectableIds.length === 0}
onClick={() => setSelectedIds(allSelected ? [] : selectableIds)}
@@ -548,7 +614,62 @@ export function AdminDownstreamDeliveriesPage() {
/>
</div>
<div className="surface admin-task-table-card report-task-table-card">
<div className="section-heading">
<div><h2></h2><p className="muted"></p></div>
<Button onClick={loadRequeueTasks} variant="ghost"></Button>
</div>
<div className="downstream-requeue-task-list">
{requeueTasks.map((task) => (
<article key={task.id}>
<div><strong>{task.taskNo}</strong><span>{formatDateTime(task.createdAt)}</span></div>
<div><span>{task.application?.name ?? '多个应用'}</span><small>{task.reason}</small></div>
<div><Tag tone={task.status === 'completed' ? 'success' : task.status === 'paused' ? 'warning' : task.status === 'partial_completed' ? 'danger' : 'info'}>{task.status}</Tag><span>{task.successCount + task.failedCount + task.skippedCount}/{task.totalCount}</span></div>
<div className="downstream-requeue-task-list__actions">
<Button onClick={() => void adminApi.getDownstreamRequeueTask(task.id).then(setSelectedTask)} size="sm" variant="ghost"></Button>
{task.status === 'running' || task.status === 'queued' ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'pause').then(loadRequeueTasks)} size="sm" variant="secondary"></Button> : null}
{task.status === 'paused' ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'resume').then(loadRequeueTasks)} size="sm" variant="secondary"></Button> : null}
{['queued', 'running', 'paused'].includes(task.status) ? <Button onClick={() => void adminApi.changeDownstreamRequeueTaskStatus(task.id, 'terminate').then(loadRequeueTasks)} size="sm" variant="warning"></Button> : null}
</div>
</article>
))}
{requeueTasks.length === 0 ? <p className="muted"></p> : null}
</div>
</div>
{detail ? <DeliveryDetailModal record={detail} onClose={() => setDetail(null)} /> : null}
{taskPreview ? (
<Modal open onClose={() => !taskCreateBusy && setTaskPreview(null)} title="创建下游后台重投任务" size="xl" footer={<><Button disabled={taskCreateBusy} onClick={() => setTaskPreview(null)} variant="ghost"></Button><Button disabled={taskCreateBusy || taskPreview.replayableCount === 0 || taskReason.trim().length < 5} onClick={() => void createRequeueTask()} variant="warning">{taskCreateBusy ? '创建中…' : '确认创建任务'}</Button></>}>
<div className="downstream-requeue-preview">
<div className="detail-grid">
<div><span></span><strong>{taskPreview.matchedCount}</strong></div>
<div><span></span><strong>{taskPreview.replayableCount}</strong></div>
<div><span></span><strong>{taskPreview.skippedCount}</strong></div>
<div><span></span><strong>{taskPreview.applicationCount}</strong></div>
<div><span></span><strong>{taskPreview.oldestCreatedAt ? formatDateTime(taskPreview.oldestCreatedAt) : '-'}</strong></div>
<div><span></span><strong>{formatDateTime(taskPreview.snapshotAt)}</strong></div>
</div>
<p className="muted">{Object.entries(taskPreview.statusCounts).map(([key, value]) => `${statusLabel[key] ?? key} ${value}`).join('') || '无'}</p>
<Select label="执行速度" value={String(taskRate)} onChange={(event) => setTaskRate(Number(event.target.value))} options={[{ label: '平稳(每应用10条/秒)', value: '10' }, { label: '快速(每应用20条/秒)', value: '20' }, { label: '低速(每应用5条/秒)', value: '5' }]} />
<label className="field"><span> *</span><textarea value={taskReason} onChange={(event) => setTaskReason(event.target.value)} placeholder="请填写事故原因、工单号或处理说明(至少5个字)" rows={3} /></label>
<div className="downstream-requeue-warning"><AlertTriangle size={20} /><strong></strong></div>
</div>
</Modal>
) : null}
{selectedTask ? (
<Modal open onClose={() => setSelectedTask(null)} title={`后台重投任务 ${selectedTask.taskNo}`} size="xl" footer={<Button onClick={() => setSelectedTask(null)}></Button>}>
<div className="report-record-detail">
<div className="detail-grid">
<div><span></span><strong>{selectedTask.status}</strong></div><div><span></span><strong>{selectedTask.reason}</strong></div>
<div><span></span><strong>{selectedTask.totalCount}</strong></div><div><span></span><strong>{selectedTask.successCount}</strong></div>
<div><span></span><strong>{selectedTask.failedCount}</strong></div><div><span></span><strong>{selectedTask.skippedCount}</strong></div>
<div><span></span><strong>{selectedTask.waitingCount}</strong></div><div><span></span><strong>{selectedTask.ratePerSecond}/</strong></div>
</div>
<h3></h3>
<div className="downstream-requeue-detail-items">{(selectedTask.recentItems ?? []).map((item) => <div key={item.id}><span>{item.delivery.messageId ?? '-'}</span><Tag tone={item.status === 'success' ? 'success' : item.status === 'failed' ? 'danger' : item.status === 'skipped' ? 'warning' : 'info'}>{item.status}</Tag><span>{item.skipReason ?? item.errorMessage ?? '-'}</span></div>)}</div>
</div>
</Modal>
) : null}
{requeueTarget ? (
<Modal
footer={requeueResult ? (