fix: improve retry feedback and deletion stability

This commit is contained in:
hectorzhao
2026-08-02 20:39:49 +08:00
parent ecc3d7a504
commit 94e997ec4d
9 changed files with 273 additions and 41 deletions
+122 -19
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
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 { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
@@ -42,6 +42,17 @@ const attemptStatusLabel: Record<string, string> = {
failed: '投递失败',
};
type RequeueTarget =
| { kind: 'single'; record: DownstreamDeliveryRecord }
| { kind: 'batch'; ids: string[] };
type RequeueResult = {
status: 'success' | 'partial' | 'failed';
title: string;
message: string;
failures?: string[];
};
function formatLocalDate(value: Date) {
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
}
@@ -149,6 +160,10 @@ export function AdminDownstreamDeliveriesPage() {
const [detail, setDetail] = useState<DownstreamDeliveryRecord | null>(null);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [dateRange, setDateRange] = useState<DateRangeValue>(recentSevenDays);
const [requeueTarget, setRequeueTarget] = useState<RequeueTarget | null>(null);
const [requeueBusy, setRequeueBusy] = useState(false);
const [requeueResult, setRequeueResult] = useState<RequeueResult | null>(null);
const requeueInFlightRef = useRef(false);
const loadData = useCallback(() => {
setLoading(true);
@@ -200,12 +215,61 @@ export function AdminDownstreamDeliveriesPage() {
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const replayRecord = (record: DownstreamDeliveryRecord) => {
const acknowledgedWarning = record.status === 'delivered' ? '该记录已收到客户端确认,' : '';
if (!window.confirm(`${acknowledgedWarning}重投可能导致下游业务重复处理,确认继续吗?`)) return;
adminApi.requeueDownstreamDelivery(record.id)
.then(() => loadData())
.catch((failure: Error) => setError(failure.message || '人工重投失败'));
const openSingleRequeue = (record: DownstreamDeliveryRecord) => {
setRequeueResult(null);
setRequeueTarget({ kind: 'single', record });
};
const openBatchRequeue = () => {
if (selectedIds.length === 0) return;
setRequeueResult(null);
setRequeueTarget({ kind: 'batch', ids: [...selectedIds] });
};
const closeRequeueDialog = () => {
if (requeueBusy) return;
setRequeueTarget(null);
setRequeueResult(null);
};
const confirmRequeue = async () => {
if (!requeueTarget || requeueInFlightRef.current) return;
requeueInFlightRef.current = true;
setRequeueBusy(true);
setRequeueResult(null);
setError('');
try {
if (requeueTarget.kind === 'single') {
const updated = await adminApi.requeueDownstreamDelivery(requeueTarget.record.id);
setRequeueResult({
status: 'success',
title: '重投请求已受理',
message: `${deliveryTypeLabel[updated.deliveryType] ?? updated.deliveryType}已进入人工重投流程,当前状态为“${deliveryStatusLabel(updated)}”,人工重投次数 ${updated.manualRetryCount} 次。`,
});
} else {
const result = await adminApi.batchRequeueDownstreamDeliveries(requeueTarget.ids);
const failures = result.results
.filter((item) => item.status === 'failed')
.map((item) => `${item.id}${item.errorMessage || '重投失败'}`);
setRequeueResult({
status: result.failedCount === 0 ? 'success' : result.successCount > 0 ? 'partial' : 'failed',
title: result.failedCount === 0 ? '批量重投已受理' : result.successCount > 0 ? '批量重投部分完成' : '批量重投失败',
message: `共处理 ${result.total} 条,成功 ${result.successCount} 条,失败 ${result.failedCount} 条。`,
failures,
});
setSelectedIds([]);
}
loadData();
} catch (failure) {
setRequeueResult({
status: 'failed',
title: '重投请求未确认成功',
message: failure instanceof Error && failure.message ? failure.message : '人工重投失败,请先检查列表状态和系统日志,避免重复提交。',
});
} finally {
requeueInFlightRef.current = false;
setRequeueBusy(false);
}
};
return (
@@ -400,16 +464,8 @@ export function AdminDownstreamDeliveriesPage() {
</Button>
<Button
icon={<RefreshCw size={14} />}
disabled={selectedIds.length === 0}
onClick={() => {
if (!window.confirm(`即将重投 ${selectedIds.length} 条记录,可能导致下游业务重复处理,确认继续吗?`)) return;
adminApi.batchRequeueDownstreamDeliveries(selectedIds)
.then((result) => {
setError(result.failedCount > 0 ? `批量重投完成,成功 ${result.successCount} 条,失败 ${result.failedCount}` : '');
loadData();
})
.catch((failure: Error) => setError(failure.message || '批量重投失败'));
}}
disabled={selectedIds.length === 0 || requeueBusy}
onClick={openBatchRequeue}
variant="secondary"
>
@@ -466,9 +522,9 @@ export function AdminDownstreamDeliveriesPage() {
<div className="downstream-delivery-list__actions" role="cell">
<Button icon={<Eye size={14} />} onClick={() => setDetail(record)} size="sm" variant="ghost"></Button>
<Button
disabled={!replayableStatuses.has(record.status)}
disabled={!replayableStatuses.has(record.status) || requeueBusy}
icon={<RefreshCw size={14} />}
onClick={() => replayRecord(record)}
onClick={() => openSingleRequeue(record)}
size="sm"
variant="secondary"
>
@@ -493,6 +549,53 @@ export function AdminDownstreamDeliveriesPage() {
</div>
{detail ? <DeliveryDetailModal record={detail} onClose={() => setDetail(null)} /> : null}
{requeueTarget ? (
<Modal
footer={requeueResult ? (
<Button onClick={closeRequeueDialog}></Button>
) : (
<>
<Button disabled={requeueBusy} onClick={closeRequeueDialog} variant="ghost"></Button>
<Button disabled={requeueBusy} icon={<RefreshCw size={15} />} onClick={() => void confirmRequeue()} variant="warning">
{requeueBusy ? '重投处理中…' : requeueTarget.kind === 'single' ? '确认重投' : `确认重投 ${requeueTarget.ids.length}`}
</Button>
</>
)}
onClose={closeRequeueDialog}
open
title={requeueResult?.title ?? (requeueTarget.kind === 'single' ? '确认人工重投' : '确认批量人工重投')}
>
{requeueResult ? (
<div className={`downstream-requeue-result downstream-requeue-result--${requeueResult.status}`} role={requeueResult.status === 'success' ? 'status' : 'alert'}>
<span aria-hidden="true">
{requeueResult.status === 'success' ? <CheckCircle2 size={28} /> : <AlertTriangle size={28} />}
</span>
<div>
<p>{requeueResult.message}</p>
{requeueResult.failures?.length ? (
<ul>
{requeueResult.failures.map((item, index) => <li key={`${index}-${item}`}>{item}</li>)}
</ul>
) : null}
{requeueResult.status === 'failed' ? <small></small> : null}
</div>
</div>
) : (
<div className="downstream-requeue-confirm">
<AlertTriangle aria-hidden="true" size={24} />
<div>
<p>
{requeueTarget.kind === 'single'
? `${requeueTarget.record.status === 'delivered' ? '该记录已收到客户端确认。' : ''}确认重投这条${deliveryTypeLabel[requeueTarget.record.deliveryType] ?? requeueTarget.record.deliveryType}吗?`
: `即将重投所选 ${requeueTarget.ids.length} 条下游投递记录。`}
</p>
{requeueTarget.kind === 'single' ? <small> ID{requeueTarget.record.messageId ?? '-'}</small> : null}
<strong></strong>
</div>
</div>
)}
</Modal>
) : null}
</section>
);
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Plus, Search, Trash2 } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Table, Textarea, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
type GlobalBlacklistItem = DictionaryItem & {
phoneNumber?: string;
@@ -36,7 +37,7 @@ export function AdminGlobalBlacklistPage() {
const columns = useMemo<Array<TableColumn<GlobalBlacklistItem>>>(() => [
{ key: 'phone', title: '手机号码', width: '180px', render: (record) => <strong>{record.phoneNumber}</strong> },
{ key: 'createdAt', title: '入库时间', width: '190px', render: (record) => record.createdAt ?? '-' },
{ key: 'createdAt', title: '入库时间', width: '190px', render: (record) => formatDateTime(record.createdAt) },
{ key: 'reason', title: '入库原因', render: (record) => record.reason ?? '-' },
{ key: 'status', title: '状态', width: '120px', render: (record) => record.status ?? '-' },
{
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Plus, Search, Trash2 } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
type SensitiveWordItem = DictionaryItem & {
word?: string;
@@ -59,7 +60,7 @@ export function AdminSensitiveWordsPage() {
{ key: 'word', title: '敏感词', width: '180px', render: (record) => <strong>{record.word}</strong> },
{ key: 'level', title: '级别', width: '120px', render: (record) => <Tag tone={levelToneMap[record.level ?? 'medium'] ?? 'warning'}>{levelLabelMap[record.level ?? 'medium'] ?? record.level}</Tag> },
{ key: 'status', title: '状态', width: '120px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'neutral'}>{record.status === 'active' ? '启用' : record.status === 'deleted' ? '已删除' : '停用'}</Tag> },
{ key: 'createdAt', title: '创建时间', width: '190px', render: (record) => record.createdAt ?? '-' },
{ key: 'createdAt', title: '创建时间', width: '190px', render: (record) => formatDateTime(record.createdAt) },
{
key: 'actions',
title: '操作',