from __future__ import annotations import argparse import asyncio import json import sys from app.models.base import async_session from app.enums.upload_resource import UploadResourceEventEnum from app.services.upload_resource.backfill_service import BackfillOptions, run_upload_resource_backfill from app.services.upload_resource.log_service import log_upload_resource_exception, safe_rollback_with_log VALID_MODULES = {"common", "hot_opening_replicate", "shot_replicate"} def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="补录 storage/uploads 下的上传资源账本,并可重绑模块归属。") parser.add_argument("--root", default="storage/uploads", help="扫描根目录,默认 storage/uploads") parser.add_argument("--batch-size", type=int, default=500, help="每批处理数量,默认 500") parser.add_argument("--dry-run", action="store_true", help="试跑,不写数据库") parser.add_argument("--commit", action="store_true", help="真实执行写入") parser.add_argument("--include-legacy", action="store_true", help="补录 gen_{user_id}_* 等旧版散落文件") parser.add_argument("--rebind-modules", action="store_true", help="根据 hot/shot 业务表中保存的 URL 重新绑定模块归属") parser.add_argument("--rebuild-stats", action="store_true", help="补录完成后按资源账本重算容量统计") parser.add_argument("--only-user-id", default=None, help="只处理指定用户ID") parser.add_argument("--only-module", choices=sorted(VALID_MODULES), default=None, help="只处理指定模块") parser.add_argument("--cleanup-pending-files", action="store_true", help="清理已软删但真实文件待删除/删除失败的 UploadResource 文件") parser.add_argument("--cleanup-limit", type=int, default=500, help="每次最多清理待删除真实文件数量,默认 500") return parser async def amain(argv: list[str]) -> int: parser = build_parser() args = parser.parse_args(argv) if args.dry_run and args.commit: parser.error("--dry-run 和 --commit 不能同时使用") if not args.dry_run and not args.commit: args.dry_run = True if args.batch_size <= 0: parser.error("--batch-size 必须大于 0") options = BackfillOptions( root=args.root, batch_size=args.batch_size, dry_run=not args.commit, include_legacy=args.include_legacy, rebind_modules=args.rebind_modules, rebuild_stats=args.rebuild_stats, only_user_id=args.only_user_id, only_module=args.only_module, cleanup_pending_files=args.cleanup_pending_files, cleanup_limit=args.cleanup_limit, ) async with async_session() as db: try: result = await run_upload_resource_backfill(db, options) if options.dry_run: await db.rollback() else: await db.commit() except Exception as exc: # noqa: BLE001 await safe_rollback_with_log( db, event_type=UploadResourceEventEnum.UPLOAD_RESOURCE_BACKFILL_CLI_ROLLBACK_FAILED.value, message="UploadResource 补录/清理 CLI 回滚失败", detail={"argv": argv, "options": vars(args)}, original_exc=exc, ) log_upload_resource_exception( event_type=UploadResourceEventEnum.UPLOAD_RESOURCE_BACKFILL_CLI_FAILED.value, message=f"UploadResource 补录/清理 CLI 执行失败: {exc}", detail={"argv": argv, "options": vars(args)}, exc=exc, ) raise print(json.dumps(result.to_dict(), ensure_ascii=False, indent=2, default=str)) return 0 def main() -> int: return asyncio.run(amain(sys.argv[1:])) if __name__ == "__main__": raise SystemExit(main())