feat: add batch processing history and real-time status monitoring for team uploads

This commit is contained in:
2026-02-05 08:51:53 +08:00
parent 1718920250
commit 6e6dfded7b
3 changed files with 98 additions and 10 deletions

View File

@@ -80,6 +80,11 @@ func main() {
} }
fmt.Println() fmt.Println()
// 启动时清理遗留的 running 状态批次(避免多个运行中状态)
if affected, err := database.Instance.CleanupStuckBatchRuns(); err == nil && affected > 0 {
fmt.Printf("%s[清理]%s 已清理 %d 个遗留的运行中批次记录\n", colorYellow, colorReset, affected)
}
// 启动自动补号检查器(需在前端开启开关才会实际补号) // 启动自动补号检查器(需在前端开启开关才会实际补号)
api.StartAutoAddService() api.StartAutoAddService()

View File

@@ -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.Running = true
teamProcessState.StartedAt = time.Now() teamProcessState.StartedAt = time.Now()

View File

@@ -48,6 +48,25 @@ interface BatchHistoryResponse {
total_pages: number 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 { interface BatchDetailResponse {
batch: BatchRun batch: BatchRun
results: TeamResult[] results: TeamResult[]
@@ -172,11 +191,26 @@ function TeamResultRow({ result }: { result: TeamResult }) {
} }
// 批次卡片组件 // 批次卡片组件
function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id: number) => Promise<TeamResult[]> }) { function BatchCard({ batch, onLoadDetail, liveStatus }: {
batch: BatchRun;
onLoadDetail: (id: number) => Promise<TeamResult[]>;
liveStatus?: TeamProcessStatus | null;
}) {
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const [results, setResults] = useState<TeamResult[] | null>(null) const [results, setResults] = useState<TeamResult[] | null>(null)
const [loading, setLoading] = useState(false) 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 () => { const handleToggle = async () => {
if (!expanded && results === null) { if (!expanded && results === null) {
setLoading(true) setLoading(true)
@@ -214,11 +248,10 @@ function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id
<span className="font-medium text-slate-800 dark:text-slate-200"> <span className="font-medium text-slate-800 dark:text-slate-200">
#{batch.id} #{batch.id}
</span> </span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${ <span className={`px-2 py-0.5 rounded text-xs font-medium ${batch.status === 'completed'
batch.status === 'completed' ? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'
? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400' : 'bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400'
: 'bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400' }`}>
}`}>
{batch.status === 'completed' ? '已完成' : '运行中'} {batch.status === 'completed' ? '已完成' : '运行中'}
</span> </span>
</div> </div>
@@ -233,15 +266,17 @@ function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id
<div className="flex items-center gap-3 text-sm"> <div className="flex items-center gap-3 text-sm">
<div className="flex items-center gap-1 px-2 py-1 rounded bg-slate-100 dark:bg-slate-700"> <div className="flex items-center gap-1 px-2 py-1 rounded bg-slate-100 dark:bg-slate-700">
<Users className="h-3.5 w-3.5 text-slate-500" /> <Users className="h-3.5 w-3.5 text-slate-500" />
<span className="text-slate-600 dark:text-slate-300">{batch.total_owners}</span> <span className="text-slate-600 dark:text-slate-300">
{isRunning && displayCompleted > 0 ? `${displayCompleted}/${batch.total_owners}` : batch.total_owners}
</span>
</div> </div>
<div className="flex items-center gap-1 px-2 py-1 rounded bg-green-50 dark:bg-green-900/20"> <div className="flex items-center gap-1 px-2 py-1 rounded bg-green-50 dark:bg-green-900/20">
<CheckCircle className="h-3.5 w-3.5 text-green-500" /> <CheckCircle className="h-3.5 w-3.5 text-green-500" />
<span className="text-green-600 dark:text-green-400">{batch.total_registered}</span> <span className="text-green-600 dark:text-green-400">{displayRegistered}</span>
</div> </div>
<div className="flex items-center gap-1 px-2 py-1 rounded bg-purple-50 dark:bg-purple-900/20"> <div className="flex items-center gap-1 px-2 py-1 rounded bg-purple-50 dark:bg-purple-900/20">
<Settings className="h-3.5 w-3.5 text-purple-500" /> <Settings className="h-3.5 w-3.5 text-purple-500" />
<span className="text-purple-600 dark:text-purple-400">{batch.total_added_to_s2a}</span> <span className="text-purple-600 dark:text-purple-400">{displayAddedToS2A}</span>
</div> </div>
{expanded && errorCount > 0 && ( {expanded && errorCount > 0 && (
<div className="flex items-center gap-1 px-2 py-1 rounded bg-red-50 dark:bg-red-900/20"> <div className="flex items-center gap-1 px-2 py-1 rounded bg-red-50 dark:bg-red-900/20">
@@ -250,7 +285,7 @@ function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id
</div> </div>
)} )}
<span className="text-slate-400 text-xs"> <span className="text-slate-400 text-xs">
{formatDuration(batch.duration_seconds)} {formatDuration(displayElapsed)}
</span> </span>
</div> </div>
</div> </div>
@@ -326,6 +361,10 @@ export default function BatchResultHistory() {
const [totalPages, setTotalPages] = useState(1) const [totalPages, setTotalPages] = useState(1)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [liveStatus, setLiveStatus] = useState<TeamProcessStatus | null>(null)
// 检查是否有运行中的批次
const hasRunningBatch = batches.some(b => b.status === 'running')
// 加载批次列表 // 加载批次列表
const loadBatches = useCallback(async (p: number) => { const loadBatches = useCallback(async (p: number) => {
@@ -365,10 +404,46 @@ export default function BatchResultHistory() {
return [] 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(() => { useEffect(() => {
loadBatches(1) loadBatches(1)
}, [loadBatches]) }, [loadBatches])
// 当存在运行中批次时,轮询实时状态
useEffect(() => {
if (!hasRunningBatch) {
setLiveStatus(null)
return
}
// 立即获取一次
fetchLiveStatus()
// 每 2 秒轮询一次
const interval = setInterval(fetchLiveStatus, 2000)
return () => clearInterval(interval)
}, [hasRunningBatch, fetchLiveStatus])
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
loadBatches(newPage) loadBatches(newPage)
} }
@@ -417,6 +492,7 @@ export default function BatchResultHistory() {
key={batch.id} key={batch.id}
batch={batch} batch={batch}
onLoadDetail={loadBatchDetail} onLoadDetail={loadBatchDetail}
liveStatus={batch.status === 'running' ? liveStatus : null}
/> />
))} ))}
</div> </div>