"""数据保留和清理任务。 根据 system_settings 中的 retention_days 配置,清理过期的海报和 PPT 文件及记录。 支持手动触发和定时调用。 """ import logging import os from datetime import datetime, timedelta from sqlalchemy import text logger = logging.getLogger(__name__) def cleanup_expired_data(dry_run: bool = False) -> dict: """清理过期数据。 参数: dry_run: True 时只统计不实际删除 返回: {"poster_deleted": N, "ppt_deleted": N, "files_deleted": N, "errors": [...]} """ from insurance.db.compat import db from insurance.models.system_setting import SystemSetting stats = {"poster_deleted": 0, "ppt_deleted": 0, "files_deleted": 0, "errors": []} # 读取保留天数配置 settings = {s.key: s.value for s in SystemSetting.query.filter( SystemSetting.key.in_(["poster_history_retention_days", "ppt_history_retention_days"]) ).all()} poster_days = _parse_int(settings.get("poster_history_retention_days"), 365) ppt_days = _parse_int(settings.get("ppt_history_retention_days"), 365) # 清理过期海报记录 if poster_days > 0: cutoff = datetime.now() - timedelta(days=poster_days) try: _cleanup_poster_records(db, cutoff, dry_run, stats) except Exception as e: stats["errors"].append(f"poster cleanup error: {e}") logger.error(f"海报清理失败: {e}") # 清理过期 PPT 历史 if ppt_days > 0: cutoff = datetime.now() - timedelta(days=ppt_days) try: _cleanup_ppt_history(db, cutoff, dry_run, stats) except Exception as e: stats["errors"].append(f"ppt cleanup error: {e}") logger.error(f"PPT 历史清理失败: {e}") logger.info(f"数据清理完成: {stats}") return stats def _cleanup_poster_records(db, cutoff: datetime, dry_run: bool, stats: dict): """清理过期海报记录和文件。""" from insurance.models.poster_record import PosterRecord expired = PosterRecord.query.filter( PosterRecord.created_at < cutoff, PosterRecord.task_status.in_(["done", "failed"]), ).all() for record in expired: # 删除物理文件 if record.export_url and os.path.exists(record.export_url): if not dry_run: try: os.remove(record.export_url) stats["files_deleted"] += 1 except Exception as e: stats["errors"].append(f"file delete error {record.export_url}: {e}") else: stats["files_deleted"] += 1 # 匿名化数据库记录(保留合规留痕,删除用户标识) if not dry_run: record.user_id = "deleted" record.export_url = None # 保留 prompt_used, ai_raw_content 等合规字段 stats["poster_deleted"] += 1 if not dry_run and expired: db.session.commit() def _cleanup_ppt_history(db, cutoff: datetime, dry_run: bool, stats: dict): """清理过期 PPT 历史记录和文件。""" rows = db.session.execute(text( "SELECT id, file_url FROM insurance_ppt_history WHERE created_at < :cutoff" ), {"cutoff": cutoff}).fetchall() for row in rows: record_id, file_url = row # 删除物理文件 if file_url and os.path.exists(file_url): if not dry_run: try: os.remove(file_url) stats["files_deleted"] += 1 except Exception as e: stats["errors"].append(f"file delete error {file_url}: {e}") else: stats["files_deleted"] += 1 # 匿名化记录 if not dry_run: db.session.execute(text( "UPDATE insurance_ppt_history SET user_id = 'deleted', file_url = NULL WHERE id = :id" ), {"id": record_id}) stats["ppt_deleted"] += 1 if not dry_run and rows: db.session.commit() # 清理过期 PPT 会话文件 sessions = db.session.execute(text( "SELECT id, ppt_path FROM insurance_ppt_sessions " "WHERE created_at < :cutoff AND status = 'done'" ), {"cutoff": cutoff}).fetchall() for session_id, ppt_path in sessions: if ppt_path and os.path.exists(ppt_path): if not dry_run: try: os.remove(ppt_path) stats["files_deleted"] += 1 except Exception as e: stats["errors"].append(f"session file delete error: {e}") else: stats["files_deleted"] += 1 def _parse_int(value: str, default: int) -> int: try: return int(str(value or "").strip()) except (TypeError, ValueError): return default