Files
Nexus/server/infrastructure/database/server_batch_job_repo.py
T
Nexus Agent 08a0157c95 feat: 调度表单重构、登录门控、批量任务与页面缓存对齐
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航;
服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 11:17:21 +08:00

129 lines
4.0 KiB
Python

"""Server batch job persistence (MySQL)."""
from __future__ import annotations
import json
from datetime import datetime, timezone
from typing import Any, Optional
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from server.domain.models import ServerBatchJob
class ServerBatchJobRepositoryImpl:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_id(self, job_id: int) -> Optional[ServerBatchJob]:
result = await self.session.execute(
select(ServerBatchJob).where(ServerBatchJob.id == job_id)
)
return result.scalar_one_or_none()
async def create(self, job: ServerBatchJob) -> ServerBatchJob:
self.session.add(job)
await self.session.commit()
return job
async def upsert_running(
self,
*,
job_id: int,
op: str,
label: str,
server_ids: list[int],
operator: str,
params: Optional[dict[str, Any]] = None,
) -> ServerBatchJob:
row = await self.get_by_id(job_id)
if row:
return row
job = ServerBatchJob(
id=job_id,
op=op,
label=label,
server_ids=json.dumps(server_ids, ensure_ascii=False),
status="running",
results=json.dumps([], ensure_ascii=False),
operator=operator,
params=json.dumps(params or {}, ensure_ascii=False),
)
return await self.create(job)
async def finalize(
self,
job_id: int,
*,
status: str,
results: list[dict[str, Any]],
) -> Optional[ServerBatchJob]:
row = await self.get_by_id(job_id)
if not row:
return None
row.status = status
row.results = json.dumps(results, ensure_ascii=False)
row.completed_at = datetime.now(timezone.utc)
await self.session.commit()
return row
async def list_recent(
self,
*,
limit: int = 50,
offset: int = 0,
op: Optional[str] = None,
) -> tuple[list[ServerBatchJob], int]:
filters = []
if op:
filters.append(ServerBatchJob.op == op)
count_stmt = select(func.count(ServerBatchJob.id))
if filters:
count_stmt = count_stmt.where(*filters)
total = int((await self.session.execute(count_stmt)).scalar() or 0)
stmt = select(ServerBatchJob).order_by(ServerBatchJob.started_at.desc())
if filters:
stmt = stmt.where(*filters)
result = await self.session.execute(stmt.limit(limit).offset(offset))
return list(result.scalars().all()), total
def job_row_to_api(row: ServerBatchJob) -> dict[str, Any]:
"""Convert MySQL row to BatchJobStatus-shaped dict."""
try:
server_ids = json.loads(row.server_ids or "[]")
except json.JSONDecodeError:
server_ids = []
try:
results = json.loads(row.results or "[]")
except json.JSONDecodeError:
results = []
if isinstance(results, dict):
ordered = []
for sid in server_ids:
entry = results.get(str(sid))
if entry:
ordered.append(entry)
results = ordered
total = len(server_ids)
success = sum(1 for r in results if r.get("success"))
failed = sum(1 for r in results if not r.get("success"))
done = len(results)
return {
"job_id": int(row.id),
"op": row.op or "",
"label": row.label or "",
"status": row.status or "completed",
"operator": row.operator or "",
"total": total,
"done": done,
"remaining": max(0, total - done),
"success": success,
"failed": failed,
"progress": f"{success}/{total}" if total else "—",
"results": results,
"started_at": row.started_at.isoformat() if row.started_at else None,
"completed_at": row.completed_at.isoformat() if row.completed_at else None,
}