# 问题 修复 文件 1 前端构建失败(引号错误) size="small type=" → size="small" type=" PosterHistoryPage.vue 2 migrate_014 ORM vs 缺失列 全部改为原始 SQL,不再引用 ORM 模型 migrate_014.py 3 cleanup 字段名错误 output_path → ppt_path cleanup.py 4 文案生成 case 越权 添加 case.user_id != user_id 校验 poster/service.py 5 存储路径未接通持久化卷 全部改用 get_storage_root()(默认 /app/api/storage/insurance) config.py, ppt/routes.py, poster/service.py, poster/tasks.py 高风险问题修复 # 问题 修复 文件 6 migrate_019 rollback 撤销成功字段 每个 ALTER 后立即 commit,失败只回滚当前语句 migrate_019.py 7 迁移锁 Windows 不兼容 + 句柄未持久化 全局变量保存锁句柄,支持 Windows msvcrt api/insurance/db/__init__.py 8 PDF 校验异常时放行 异常返回 False(文件损坏) security.py 9 健康检查始终返回成功 缺少关键资源时返回 503 + missing 列表 poster/routes.py 10 短密钥掩码泄露原值 ≤4 字符返回 **** ppt_admin_service.py 11 设置无键名白名单 添加 _ALLOWED_SETTING_KEYS 白名单 ppt_admin_service.py 12 容器重启任务永久 stuck 添加 recover_stale_tasks() 启动恢复函数 poster/tasks.py, ppt/parse_worker.py
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
|