import { useCallback, useEffect, useMemo, useState } from 'react'; import { AlertTriangle, RefreshCw, RotateCcw, Search } from 'lucide-react'; import type { BillingSettlement } from '@easyai-ai-gateway/contracts'; import { listBillingSettlements, retryBillingSettlement } from '../../api'; import { Badge, Button, Card, CardContent, Checkbox, ConfirmDialog, Input, Select, Table, TableCell, TableHead, TableRow, } from '../../components/ui'; export function BillingSettlementsPanel(props: { token: string }) { const [items, setItems] = useState([]); const [query, setQuery] = useState(''); const [status, setStatus] = useState(''); const [loading, setLoading] = useState(false); const [retrying, setRetrying] = useState(''); const [batchRetrying, setBatchRetrying] = useState(false); const [confirming, setConfirming] = useState(null); const [batchConfirming, setBatchConfirming] = useState(false); const [selectedIds, setSelectedIds] = useState>(() => new Set()); const [message, setMessage] = useState(''); const load = useCallback(async (signal?: AbortSignal) => { setLoading(true); setMessage(''); try { const result = await listBillingSettlements(props.token, { status, pageSize: 100, signal }); setItems(result.items); } catch (error) { if (signal?.aborted) return; setMessage(error instanceof Error ? error.message : '结算队列加载失败'); } finally { if (!signal?.aborted) setLoading(false); } }, [props.token, status]); useEffect(() => { const controller = new AbortController(); void load(controller.signal); return () => controller.abort(); }, [load]); const filteredItems = useMemo(() => { const keyword = query.trim().toLowerCase(); if (!keyword) return items; return items.filter((item) => [ item.id, item.taskId, item.action, item.status, item.lastErrorCode, item.manualReviewReason, ].filter(Boolean).join(' ').toLowerCase().includes(keyword)); }, [items, query]); const retryableItems = useMemo(() => items.filter(canRetry), [items]); const visibleRetryableItems = useMemo(() => filteredItems.filter(canRetry), [filteredItems]); const selectedItems = useMemo( () => retryableItems.filter((item) => selectedIds.has(item.id)), [retryableItems, selectedIds], ); const allVisibleSelected = visibleRetryableItems.length > 0 && visibleRetryableItems.every((item) => selectedIds.has(item.id)); const someVisibleSelected = visibleRetryableItems.some((item) => selectedIds.has(item.id)); const busy = batchRetrying || Boolean(retrying); useEffect(() => { const retryableIds = new Set(retryableItems.map((item) => item.id)); setSelectedIds((current) => { const next = new Set(Array.from(current).filter((id) => retryableIds.has(id))); return next.size === current.size ? current : next; }); }, [retryableItems]); async function retry(item: BillingSettlement) { setRetrying(item.id); setMessage(''); try { await retryBillingSettlement(props.token, item.id); await load(); setMessage(`结算 ${shortId(item.id)} 已重新进入队列`); } catch (error) { setMessage(error instanceof Error ? error.message : '结算重试失败'); } finally { setRetrying(''); } } function toggleItem(itemId: string, checked: boolean) { setSelectedIds((current) => { const next = new Set(current); if (checked) next.add(itemId); else next.delete(itemId); return next; }); } function toggleVisible(checked: boolean) { setSelectedIds((current) => { const next = new Set(current); visibleRetryableItems.forEach((item) => { if (checked) next.add(item.id); else next.delete(item.id); }); return next; }); } async function retrySelected() { if (!selectedItems.length) return; setBatchRetrying(true); setMessage(''); const processingItems = [...selectedItems]; try { const { failedIds, succeeded } = await retrySettlementBatch( processingItems, (item) => retryBillingSettlement(props.token, item.id), ); setSelectedIds(new Set(failedIds)); await load(); setMessage(failedIds.length ? `批量结算完成:成功 ${succeeded} 条,失败 ${failedIds.length} 条;失败记录已保留选择。` : `已批量结算 ${succeeded} 条记录。`); } catch (error) { setMessage(error instanceof Error ? error.message : '批量结算处理失败'); } finally { setBatchRetrying(false); } } return (
setQuery(event.target.value)} />
{message &&

{message}

} {filteredItems.length ? (
toggleVisible(checked === true)} /> 任务 / 动作 状态 金额 尝试 异常分类 操作 {filteredItems.map((item) => ( toggleItem(item.id, checked === true)} /> {shortId(item.taskId)} {item.action === 'settle' ? '扣费' : '释放冻结'} · {shortId(item.id)} {statusLabel(item.status)} {formatAmount(item.amount)} {item.currency} {item.attempts} / 20 {item.lastErrorCode || item.manualReviewReason || '-'} {canRetry(item) ? ( ) : '-'} ))}
) : (
{loading ? '正在加载结算队列' : '暂无匹配的结算记录'} 可调整搜索或状态筛选查看其他记录。
)}
setConfirming(null)} onConfirm={() => { if (!confirming) return; const item = confirming; void retry(item).finally(() => setConfirming(null)); }} /> setBatchConfirming(false)} onConfirm={() => void retrySelected().finally(() => setBatchConfirming(false))} />
); } function canRetry(item: BillingSettlement) { return item.status === 'retryable_failed' || item.status === 'manual_review'; } export async function retrySettlementBatch( items: T[], retryOne: (item: T) => Promise, concurrency = 5, ) { const failedIds: string[] = []; let succeeded = 0; const batchSize = Math.max(1, Math.floor(concurrency)); for (let index = 0; index < items.length; index += batchSize) { const batch = items.slice(index, index + batchSize); const results = await Promise.allSettled(batch.map(retryOne)); results.forEach((result, resultIndex) => { if (result.status === 'fulfilled') succeeded += 1; else failedIds.push(batch[resultIndex].id); }); } return { failedIds, succeeded }; } function statusLabel(status: string) { return ({ pending: '待处理', processing: '处理中', retryable_failed: '等待重试', manual_review: '人工复核', completed: '已完成' } as Record)[status] ?? status; } function statusVariant(status: string): 'secondary' | 'warning' | 'destructive' | 'success' | 'outline' { if (status === 'completed') return 'success'; if (status === 'manual_review') return 'destructive'; if (status === 'retryable_failed') return 'warning'; if (status === 'processing') return 'outline'; return 'secondary'; } function formatAmount(value: number) { return new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 9 }).format(value); } function retryActionLabel(item: BillingSettlement) { if (item.status !== 'manual_review') return '重试'; if (item.manualReviewReason === 'upstream_submission_unknown' && item.action === 'release') return '确认释放'; if (item.manualReviewReason === 'historical_success_without_charge' && item.action === 'settle') return '确认结算'; return '重新入队'; } function retryActionDescription(item: BillingSettlement) { if (item.manualReviewReason === 'upstream_submission_unknown' && item.action === 'release') { return '上游提交结果不明。确认后只会释放当前冻结,不会再次调用上游;请先完成外部核查。'; } if (item.manualReviewReason === 'historical_success_without_charge' && item.action === 'settle') { return `确认后将按历史快照结算 ${formatAmount(item.amount)} ${item.currency},不会重新调用上游。`; } return '确认后会将该记录重新放入结算队列,不会重新调用上游。'; } function shortId(value: string) { return value.length > 12 ? `${value.slice(0, 8)}…${value.slice(-4)}` : value; }