From 6e6dfded7b443474dfea4fbb975ea16cb89a8222 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Thu, 5 Feb 2026 08:51:53 +0800 Subject: [PATCH] feat: add batch processing history and real-time status monitoring for team uploads --- backend/cmd/main.go | 5 + backend/internal/api/team_process.go | 7 ++ .../components/upload/BatchResultHistory.tsx | 96 +++++++++++++++++-- 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index afe43a9..37fd683 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -80,6 +80,11 @@ func main() { } fmt.Println() + // 启动时清理遗留的 running 状态批次(避免多个运行中状态) + if affected, err := database.Instance.CleanupStuckBatchRuns(); err == nil && affected > 0 { + fmt.Printf("%s[清理]%s 已清理 %d 个遗留的运行中批次记录\n", colorYellow, colorReset, affected) + } + // 启动自动补号检查器(需在前端开启开关才会实际补号) api.StartAutoAddService() diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go index 394d9b0..23a7c81 100644 --- a/backend/internal/api/team_process.go +++ b/backend/internal/api/team_process.go @@ -167,6 +167,13 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) { } } + // 启动新任务前,先清理数据库中遗留的 running 状态批次 + if database.Instance != nil { + if affected, err := database.Instance.CleanupStuckBatchRuns(); err == nil && affected > 0 { + logger.Warning(fmt.Sprintf("清理了 %d 个遗留的运行中批次记录", affected), "", "team") + } + } + // 初始化状态 teamProcessState.Running = true teamProcessState.StartedAt = time.Now() diff --git a/frontend/src/components/upload/BatchResultHistory.tsx b/frontend/src/components/upload/BatchResultHistory.tsx index 70b0836..2fde563 100644 --- a/frontend/src/components/upload/BatchResultHistory.tsx +++ b/frontend/src/components/upload/BatchResultHistory.tsx @@ -48,6 +48,25 @@ interface BatchHistoryResponse { total_pages: number } +// 实时处理状态(来自 /api/team/status) +interface TeamProcessStatus { + running: boolean + started_at: string + total_teams: number + completed: number + results: { + team_index: number + owner_email: string + team_id: string + registered: number + added_to_s2a: number + member_emails: string[] + errors: string[] + duration_ms: number + }[] + elapsed_ms: number +} + interface BatchDetailResponse { batch: BatchRun results: TeamResult[] @@ -172,11 +191,26 @@ function TeamResultRow({ result }: { result: TeamResult }) { } // 批次卡片组件 -function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id: number) => Promise }) { +function BatchCard({ batch, onLoadDetail, liveStatus }: { + batch: BatchRun; + onLoadDetail: (id: number) => Promise; + liveStatus?: TeamProcessStatus | null; +}) { const [expanded, setExpanded] = useState(false) const [results, setResults] = useState(null) const [loading, setLoading] = useState(false) + // 如果是运行中的批次,使用实时状态数据 + const isRunning = batch.status === 'running' + const displayRegistered = isRunning && liveStatus ? + liveStatus.results.reduce((sum, r) => sum + r.registered, 0) : + batch.total_registered + const displayAddedToS2A = isRunning && liveStatus ? + liveStatus.results.reduce((sum, r) => sum + r.added_to_s2a, 0) : + batch.total_added_to_s2a + const displayCompleted = isRunning && liveStatus ? liveStatus.completed : 0 + const displayElapsed = isRunning && liveStatus ? Math.floor(liveStatus.elapsed_ms / 1000) : batch.duration_seconds + const handleToggle = async () => { if (!expanded && results === null) { setLoading(true) @@ -214,11 +248,10 @@ function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id 批次 #{batch.id} - + {batch.status === 'completed' ? '已完成' : '运行中'} @@ -233,15 +266,17 @@ function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id
- {batch.total_owners} + + {isRunning && displayCompleted > 0 ? `${displayCompleted}/${batch.total_owners}` : batch.total_owners} +
- {batch.total_registered} + {displayRegistered}
- {batch.total_added_to_s2a} + {displayAddedToS2A}
{expanded && errorCount > 0 && (
@@ -250,7 +285,7 @@ function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id
)} - {formatDuration(batch.duration_seconds)} + {formatDuration(displayElapsed)}
@@ -326,6 +361,10 @@ export default function BatchResultHistory() { const [totalPages, setTotalPages] = useState(1) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) + const [liveStatus, setLiveStatus] = useState(null) + + // 检查是否有运行中的批次 + const hasRunningBatch = batches.some(b => b.status === 'running') // 加载批次列表 const loadBatches = useCallback(async (p: number) => { @@ -365,10 +404,46 @@ export default function BatchResultHistory() { return [] }, []) + // 获取实时处理状态 + const fetchLiveStatus = useCallback(async () => { + try { + const res = await fetch('/api/team/status') + if (res.ok) { + const data = await res.json() + if (data.code === 0) { + const status = data.data as TeamProcessStatus + setLiveStatus(status) + // 如果任务刚完成(running 变为 false),刷新批次列表 + if (!status.running) { + setLiveStatus(null) + loadBatches(page) + } + } + } + } catch (e) { + console.error('获取实时状态失败:', e) + } + }, [loadBatches, page]) + useEffect(() => { loadBatches(1) }, [loadBatches]) + // 当存在运行中批次时,轮询实时状态 + useEffect(() => { + if (!hasRunningBatch) { + setLiveStatus(null) + return + } + + // 立即获取一次 + fetchLiveStatus() + + // 每 2 秒轮询一次 + const interval = setInterval(fetchLiveStatus, 2000) + return () => clearInterval(interval) + }, [hasRunningBatch, fetchLiveStatus]) + const handlePageChange = (newPage: number) => { loadBatches(newPage) } @@ -417,6 +492,7 @@ export default function BatchResultHistory() { key={batch.id} batch={batch} onLoadDetail={loadBatchDetail} + liveStatus={batch.status === 'running' ? liveStatus : null} /> ))}