feat: add batch processing history and real-time status monitoring for team uploads
This commit is contained in:
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user