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