""" TIIA manual uploader (management command). This command is intended for one-off backfill / verification: - Iterate PlateOrder(s) - Upload all image URLs in plate_image JSON field to Tencent Cloud TIIA gallery (CreateImage) - Respect QPS limit - Record failures into PlateOrderTiiaUploadFailure (and continue) """ from __future__ import annotations from datetime import date, datetime, time, timedelta from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.utils import timezone from printing.models import PlateOrder, PlateOrderTiiaUploadFailure def _get_day_range(day: date) -> tuple[datetime, datetime]: tz = timezone.get_current_timezone() start = datetime.combine(day, time.min) end = start + timedelta(days=1) if timezone.is_naive(start): start = timezone.make_aware(start, tz) if timezone.is_naive(end): end = timezone.make_aware(end, tz) return start, end @transaction.atomic def _record_failure(*, run_date: date, plate_order_id: int, error: str, details): now = timezone.now() obj, created = PlateOrderTiiaUploadFailure.objects.select_for_update().get_or_create( run_date=run_date, plate_order_id=plate_order_id, defaults={ 'error': error or '', 'details': details or [], 'attempts': 1, 'last_attempt_at': now, }, ) if not created: obj.error = error or obj.error or '' obj.details = details or obj.details or [] obj.attempts = (obj.attempts or 0) + 1 obj.last_attempt_at = now obj.save(update_fields=['error', 'details', 'attempts', 'last_attempt_at', 'updated_at']) return obj class Command(BaseCommand): help = '手动上传 PlateOrder.plate_image 中的图片到腾讯云 TIIA 图库(CreateImage),带限速与失败落库' def add_arguments(self, parser): parser.add_argument( '--all', action='store_true', help='处理所有 PlateOrder(建议仅用于一次性跑批/补数据)', ) parser.add_argument( '--yesterday', action='store_true', help='处理“昨天创建”的 PlateOrder(同定时任务筛选口径)', ) parser.add_argument( '--date', type=str, help='处理某一天创建的 PlateOrder(YYYY-MM-DD,本地时区)', ) parser.add_argument( '--plate-order-id', type=int, help='仅处理指定 plate_order_id', ) parser.add_argument( '--qps', type=float, default=None, help='覆盖 settings.TENCENTCLOUD_TIIA_QPS(默认 10)', ) parser.add_argument( '--dry-run', action='store_true', help='仅展示将要处理的 PlateOrder 数量,不执行上传', ) parser.add_argument( '--limit', type=int, default=None, help='最多处理多少个 PlateOrder(按 id 升序),用于小批量验证', ) def handle(self, *args, **options): from api_v1.utils.tencentcloud_tiia import SimpleRateLimiter, upload_plate_order_images_to_tencent_tiia do_all = bool(options['all']) yesterday = bool(options['yesterday']) date_str = options.get('date') plate_order_id = options.get('plate_order_id') dry_run = bool(options['dry_run']) limit = options.get('limit') chosen = sum([1 if do_all else 0, 1 if yesterday else 0, 1 if bool(date_str) else 0, 1 if plate_order_id else 0]) if chosen != 1: raise CommandError('必须且只能指定一种模式:--all / --yesterday / --date / --plate-order-id') qps_opt = options.get('qps') qps = float(qps_opt) if qps_opt is not None else float(getattr(settings, 'TENCENTCLOUD_TIIA_QPS', 10) or 10) limiter = SimpleRateLimiter(qps=qps) run_date = timezone.localdate() qs = PlateOrder.objects.all().only('id', 'plate_image').order_by('id') mode = 'all' if yesterday: mode = 'yesterday' day = timezone.localdate() - timedelta(days=1) start, end = _get_day_range(day) qs = qs.filter(created_at__gte=start, created_at__lt=end) elif date_str: mode = f'date={date_str}' try: day = date.fromisoformat(date_str) except Exception as e: raise CommandError('--date 必须为 YYYY-MM-DD') from e start, end = _get_day_range(day) qs = qs.filter(created_at__gte=start, created_at__lt=end) elif plate_order_id: mode = f'plate_order_id={plate_order_id}' qs = qs.filter(id=int(plate_order_id)) if limit is not None: qs = qs[: int(limit)] total_orders = qs.count() if dry_run else 0 if dry_run: self.stdout.write(f'[TIIA] dry-run: mode={mode} qps={qps} run_date={run_date} total_orders={total_orders}') return total_orders = 0 processed_images = 0 failed_ids: list[int] = [] self.stdout.write(f'[TIIA] start: mode={mode} qps={qps} run_date={run_date}') for po in qs.iterator(chunk_size=200): total_orders += 1 try: results = upload_plate_order_images_to_tencent_tiia( plate_order_id=po.id, rate_limiter=limiter, ) except Exception as e: failed_ids.append(po.id) _record_failure(run_date=run_date, plate_order_id=po.id, error=str(e), details=[{'error': str(e)}]) self.stderr.write(f'[TIIA] PlateOrder {po.id} 上传异常(已记录,继续):{e}') continue processed_images += len(results) bad = [r for r in results if not r.get('ok')] if bad: failed_ids.append(po.id) summary = '; '.join([(b.get('error') or '') for b in bad][:5]) _record_failure(run_date=run_date, plate_order_id=po.id, error=summary, details=bad) self.stderr.write(f'[TIIA] PlateOrder {po.id} 部分失败(已记录,继续):{summary}') self.stdout.write(self.style.SUCCESS('[TIIA] done')) self.stdout.write( str( { 'run_date': str(run_date), 'mode': mode, 'qps': qps, 'total_orders': total_orders, 'processed_images': processed_images, 'failed_count': len(failed_ids), 'failed_ids_sample': failed_ids[:50], } ) )