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()
// 启动时清理遗留的 running 状态批次(避免多个运行中状态)
if affected, err := database.Instance.CleanupStuckBatchRuns(); err == nil && affected > 0 {
fmt.Printf("%s[清理]%s 已清理 %d 个遗留的运行中批次记录\n", colorYellow, colorReset, affected)
}
// 启动自动补号检查器(需在前端开启开关才会实际补号)
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.StartedAt = time.Now()

View File

@@ -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<TeamResult[]> }) {
function BatchCard({ batch, onLoadDetail, liveStatus }: {
batch: BatchRun;
onLoadDetail: (id: number) => Promise<TeamResult[]>;
liveStatus?: TeamProcessStatus | null;
}) {
const [expanded, setExpanded] = useState(false)
const [results, setResults] = useState<TeamResult[] | null>(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,8 +248,7 @@ function BatchCard({ batch, onLoadDetail }: { batch: BatchRun; onLoadDetail: (id
<span className="font-medium text-slate-800 dark:text-slate-200">
#{batch.id}
</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
batch.status === 'completed'
<span className={`px-2 py-0.5 rounded text-xs font-medium ${batch.status === 'completed'
? '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'
}`}>
@@ -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-1 px-2 py-1 rounded bg-slate-100 dark:bg-slate-700">
<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 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" />
<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 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" />
<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>
{expanded && errorCount > 0 && (
<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>
)}
<span className="text-slate-400 text-xs">
{formatDuration(batch.duration_seconds)}
{formatDuration(displayElapsed)}
</span>
</div>
</div>
@@ -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<TeamProcessStatus | null>(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}
/>
))}
</div>