baodan/api/insurance/utils/cleanup.py

143 lines
4.8 KiB
Python
Raw Normal View History

"""数据保留和清理任务。
根据 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