import logging import subprocess from datetime import datetime, timedelta from pathlib import Path from sqlalchemy import select, update from sqlalchemy.orm import Session from app.core.config import settings from app.core.paths import data_path from app.db.session import get_db from app.models import Certificate logger = logging.getLogger(__name__) def cleanup_expired_pdfs() -> dict: """ 清理超过指定天数未访问的PDF文件 Returns: dict: 清理结果统计 """ logger.info("[PDF清理] 开始清理过期PDF文件...") # 使用配置中的清理天数 cleanup_days = settings.pdf_cleanup_days # 计算截止时间 cutoff_time = datetime.utcnow() - timedelta(days=cleanup_days) # 统计结果 result = { "total_checked": 0, "cleaned_files": 0, "cleaned_records": 0, "cleanup_days": cleanup_days, "errors": [], } try: # 获取数据库会话 db = next(get_db()) # 查询需要清理的证书 # 条件:PDF已生成,且最后访问时间早于截止时间,或从未访问过 stmt = select(Certificate).where( Certificate.pdf_status == "generated", Certificate.pdf_file_path.isnot(None), ( (Certificate.pdf_last_accessed_at < cutoff_time) | (Certificate.pdf_last_accessed_at.is_(None)) ) ) certificates = db.execute(stmt).scalars().all() result["total_checked"] = len(certificates) for cert in certificates: try: # 删除PDF文件 if cert.pdf_file_path: pdf_path = Path(cert.pdf_file_path) if pdf_path.exists(): subprocess.run(["rm", "-f", str(pdf_path)], check=False) result["cleaned_files"] += 1 logger.info(f"[PDF清理] 已删除文件: {cert.pdf_file_path}") # 更新数据库记录 cert.pdf_status = "cleaned" cert.pdf_file_path = None cert.pdf_last_accessed_at = None result["cleaned_records"] += 1 except Exception as e: error_msg = f"清理证书 {cert.certificate_no} 失败: {str(e)}" result["errors"].append(error_msg) logger.error(f"[PDF清理] {error_msg}") # 提交数据库更改 db.commit() logger.info(f"[PDF清理] 清理完成: 检查 {result['total_checked']} 个, " f"删除 {result['cleaned_files']} 个文件, " f"更新 {result['cleaned_records']} 条记录") except Exception as e: logger.error(f"[PDF清理] 清理过程发生错误: {str(e)}") result["errors"].append(f"清理过程发生错误: {str(e)}") return result def get_pdf_cleanup_stats() -> dict: """ 获取PDF清理统计信息 Returns: dict: 统计信息 """ try: db = next(get_db()) # 使用配置中的清理天数 cleanup_days = settings.pdf_cleanup_days # 统计总数 total_stmt = select(Certificate).where(Certificate.pdf_status == "generated") total_count = len(db.execute(total_stmt).scalars().all()) # 统计需要清理的 cutoff_time = datetime.utcnow() - timedelta(days=cleanup_days) cleanup_stmt = select(Certificate).where( Certificate.pdf_status == "generated", Certificate.pdf_file_path.isnot(None), ( (Certificate.pdf_last_accessed_at < cutoff_time) | (Certificate.pdf_last_accessed_at.is_(None)) ) ) cleanup_count = len(db.execute(cleanup_stmt).scalars().all()) # 统计PDF文件大小 total_size = 0 pdf_cache_dir = data_path("pdf-cache") if pdf_cache_dir.exists(): for pdf_file in pdf_cache_dir.rglob("*.pdf"): total_size += pdf_file.stat().st_size return { "total_pdfs": total_count, "need_cleanup": cleanup_count, "total_size_mb": round(total_size / (1024 * 1024), 2), "cleanup_days": cleanup_days, } except Exception as e: logger.error(f"[PDF统计] 获取统计信息失败: {str(e)}") return { "total_pdfs": 0, "need_cleanup": 0, "total_size_mb": 0, "cleanup_days": settings.pdf_cleanup_days, "error": str(e), } def manual_cleanup_certificate(certificate_no: str) -> bool: """ 手动清理指定证书的PDF文件 Args: certificate_no: 证书编号 Returns: bool: 是否成功清理 """ try: db = next(get_db()) # 查询证书 stmt = select(Certificate).where(Certificate.certificate_no == certificate_no) cert = db.execute(stmt).scalar_one_or_none() if not cert: logger.warning(f"[手动清理] 证书不存在: {certificate_no}") return False # 删除PDF文件 if cert.pdf_file_path: pdf_path = Path(cert.pdf_file_path) if pdf_path.exists(): subprocess.run(["rm", "-f", str(pdf_path)], check=False) logger.info(f"[手动清理] 已删除文件: {cert.pdf_file_path}") # 更新数据库记录 cert.pdf_status = "cleaned" cert.pdf_file_path = None cert.pdf_last_accessed_at = None db.commit() logger.info(f"[手动清理] 清理完成: {certificate_no}") return True except Exception as e: logger.error(f"[手动清理] 清理失败: {certificate_no}, 错误: {str(e)}") return False