from collections.abc import Callable from django.db.models import Q from printing.models import PrintingJob def extract_request_name(job: PrintingJob) -> str: raw = job.external_raw or {} if isinstance(raw, dict): request_name = str(raw.get("YanSe") or "").strip() if request_name: return request_name return str(job.external_product_name or "").strip() def run_external_product_image_backfill( *, fetch_image: Callable[[str], dict], upload_image: Callable[[object, dict], None], merchant_id: int | None = None, product_id: int | None = None, limit: int | None = None, dry_run: bool = False, on_success: Callable[[str], None] | None = None, on_error: Callable[[str], None] | None = None, on_info: Callable[[str], None] | None = None, ) -> dict: queryset = ( PrintingJob.objects.filter( printing_order__external_order_id__isnull=False, ) .exclude(printing_order__external_order_id="") .filter(Q(product__image__isnull=True) | Q(product__image="")) .select_related("product", "printing_order", "product__merchant") .order_by("-id") ) if merchant_id: queryset = queryset.filter(product__merchant_id=merchant_id) if product_id: queryset = queryset.filter(product_id=product_id) if limit is not None: limit = int(limit) if limit <= 0: raise ValueError("limit must be greater than 0") success_count = 0 failed_count = 0 skipped_with_image_count = 0 skipped_without_request_name_count = 0 skipped_duplicate_product_count = 0 selected_count = 0 scanned_count = 0 seen_product_ids = set() for job in queryset.iterator(chunk_size=1000): scanned_count += 1 product = job.product if product.image: skipped_with_image_count += 1 continue if product.id in seen_product_ids: skipped_duplicate_product_count += 1 continue request_name = extract_request_name(job) if not request_name: skipped_without_request_name_count += 1 continue seen_product_ids.add(product.id) selected_count += 1 if dry_run: if on_info: on_info( "[DRY-RUN] " f"job_id={job.id} " f"product_id={product.id} " f"merchant_id={product.merchant_id} " f"product_name={product.name} " f"request_name={request_name}" ) else: try: image_payload = fetch_image(request_name) upload_image(product, image_payload) except Exception as exc: failed_count += 1 if on_error: on_error( "补图失败: " f"job_id={job.id} " f"product_id={product.id} " f"merchant_id={product.merchant_id} " f"product_name={product.name} " f"request_name={request_name} " f"error={exc}" ) else: success_count += 1 if on_success: on_success( "补图成功: " f"job_id={job.id} " f"product_id={product.id} " f"merchant_id={product.merchant_id} " f"product_name={product.name} " f"request_name={request_name}" ) if limit is not None and selected_count >= limit: break payload = { "scanned": scanned_count, "selected": selected_count, "success": success_count, "failed": failed_count, "skipped_with_image": skipped_with_image_count, "skipped_without_request_name": skipped_without_request_name_count, "skipped_duplicate_product": skipped_duplicate_product_count, "merchant_id": merchant_id, "product_id": product_id, "limit": limit, "dry_run": dry_run, } if dry_run and on_info: on_info("dry-run 完成") return payload