学生资料设置、头像与自动备份恢复。

首页卡片支持修改/删除;详情页设置 Tab 可维护学校、年级与头像;系统设置新增数据备份下载与恢复;备份默认存 /root/grade-archive-backups,详见 docs/BACKUP.md。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 17:56:09 +08:00
parent 1cb3c7fad5
commit 530a8b70a1
25 changed files with 1230 additions and 194 deletions
+80
View File
@@ -0,0 +1,80 @@
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from app.core.deps import get_superuser
from app.models.user import User
from app.schemas import BackupInfoOut
from app.services import backup as backup_service
router = APIRouter(prefix="/admin/backups", tags=["admin-backups"])
@router.get("", response_model=list[BackupInfoOut])
def list_backups(_: User = Depends(get_superuser)):
return backup_service.list_backups()
@router.post("/run", response_model=BackupInfoOut)
def run_backup(_: User = Depends(get_superuser)):
try:
path = backup_service.create_backup()
except FileNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"备份失败: {exc}",
) from exc
stat = path.stat()
from datetime import datetime, timezone
return BackupInfoOut(
filename=path.name,
size_bytes=stat.st_size,
created_at=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc),
)
@router.get("/{filename}/download")
def download_backup(filename: str, _: User = Depends(get_superuser)):
try:
path = backup_service.resolve_backup_file(filename)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
except FileNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备份不存在") from exc
return FileResponse(
path,
media_type="application/gzip",
filename=filename,
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.post("/restore")
async def restore_backup(file: UploadFile = File(...), _: User = Depends(get_superuser)):
if not file.filename or not file.filename.endswith(".tar.gz"):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请上传 .tar.gz 备份包")
content = await file.read()
if len(content) > 512 * 1024 * 1024:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="备份文件过大(最大 512MB")
import tempfile
from pathlib import Path
tmp = Path(tempfile.mkdtemp(prefix="grade-archive-upload-"))
archive = tmp / "restore.tar.gz"
try:
archive.write_bytes(content)
backup_service.restore_backup(archive)
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"恢复失败: {exc}",
) from exc
finally:
import shutil
shutil.rmtree(tmp, ignore_errors=True)
return {"ok": True, "message": "数据已恢复,建议重启服务以确保缓存刷新"}