import logging from datetime import datetime, time, timedelta from celery import shared_task from django.conf import settings from django.db import transaction from django.utils import timezone from printing.models import PlateOrder, PlateOrderTiiaUploadFailure logger = logging.getLogger(__name__) def _get_day_range(day) -> tuple[datetime, datetime]: """ Build [start, end) datetimes for a local date. """ 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, plate_order_id: int, error: str, details): """ Upsert failure record for (run_date, plate_order_id). """ 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 @shared_task(bind=True) def upload_yesterday_plate_order_images_to_tencent_tiia(self): """ 每天凌晨 03:00 运行: - 扫描“昨天创建”的 PlateOrder - 将 plate_image(JSONField) 中的所有图片 URL 上传到腾讯云图库 - 遇错:记录失败的 plate_order_id(落库),继续处理下一个 注意:腾讯云调用限速 10 QPS(每秒 10 次)。 """ from api_v1.utils.tencentcloud_tiia import ( SimpleRateLimiter, upload_plate_order_images_to_tencent_tiia, ) today = timezone.localdate() run_date = today - timedelta(days=1) start, end = _get_day_range(run_date) qps = int(getattr(settings, 'TENCENTCLOUD_TIIA_QPS', 10) or 10) limiter = SimpleRateLimiter(qps=float(qps)) qs = ( PlateOrder.objects.filter(created_at__gte=start, created_at__lt=end) .only('id', 'plate_image') .order_by('id') ) total_orders = 0 failed_ids: list[int] = [] processed_images = 0 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)}]) logger.exception('[TIIA] PlateOrder %s 上传异常(任务继续)', po.id) 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) logger.warning('[TIIA] PlateOrder %s 部分图片上传失败: %s', po.id, summary) payload = { 'task_id': getattr(getattr(self, 'request', None), 'id', None), 'run_date': str(run_date), 'created_range': {'start': start.isoformat(), 'end': end.isoformat()}, 'total_orders': total_orders, 'processed_images': processed_images, 'failed_count': len(failed_ids), 'failed_ids': failed_ids, } logger.info('[TIIA] 昨日 PlateOrder 图片上传任务完成: %s', payload) return payload