from datetime import date from django.core.management.base import BaseCommand, CommandError from django.utils import timezone from api_v1 import models as api_models from api_v1.tasks import ( _get_printing_sync_product_category, _get_printing_sync_user, _sync_external_printing_records_batch, ) class Command(BaseCommand): help = '基于失败记录重试外部印染 records 同步' def add_arguments(self, parser): parser.add_argument('--limit', type=int, default=None, help='最多重试多少条失败记录;不传则重试全部') parser.add_argument( '--run-date', type=str, default=None, help='仅重试指定任务日期的失败记录,格式 YYYY-MM-DD', ) parser.add_argument( '--record-id', action='append', type=int, default=None, help='仅重试指定 external_record_id,可多次传入', ) def handle(self, *args, **options): limit = options.get('limit') if limit is not None: limit = max(1, int(limit)) run_date_text = options.get('run_date') record_ids = options.get('record_id') or [] sync_user = _get_printing_sync_user() merchant = sync_user.employee.merchant category = _get_printing_sync_product_category(merchant) failures_qs = api_models.PrintingExternalSyncFailure.objects.all().order_by( 'external_record_id', '-created_at', '-id', ) if run_date_text: try: failures_qs = failures_qs.filter(run_date=date.fromisoformat(run_date_text)) except ValueError as exc: raise CommandError(f'--run-date 格式非法: {run_date_text}') from exc if record_ids: failures_qs = failures_qs.filter(external_record_id__in=record_ids) selected_failures = [] selected_record_ids = set() for failure in failures_qs: if failure.external_record_id in selected_record_ids: continue selected_failures.append(failure) selected_record_ids.add(failure.external_record_id) if limit is not None and len(selected_failures) >= limit: break if not selected_failures: self.stdout.write(self.style.SUCCESS(str({'retried_records': 0, 'message': '没有可重试的失败记录'}))) return retry_records: list[dict] = [] failure_by_record_id: dict[int, api_models.PrintingExternalSyncFailure] = {} skipped_records = 0 for failure in selected_failures: raw = failure.raw or {} if not isinstance(raw, dict) or not raw: failure.error = '失败记录缺少原始 raw 数据,无法重试' failure.attempts = int(failure.attempts or 0) + 1 failure.last_attempt_at = timezone.now() failure.save(update_fields=['error', 'attempts', 'last_attempt_at', 'updated_at']) skipped_records += 1 continue retry_records.append(raw) failure_by_record_id[failure.external_record_id] = failure def _update_failure(*, record: dict, error: str): external_record_id = int(record.get('ID') or 0) failure = failure_by_record_id.get(external_record_id) if not failure: return failure.error = error or '' failure.raw = record failure.attempts = int(failure.attempts or 0) + 1 failure.last_attempt_at = timezone.now() failure.save(update_fields=['error', 'raw', 'attempts', 'last_attempt_at', 'updated_at']) batch_result = _sync_external_printing_records_batch( records=retry_records, merchant=merchant, sync_user=sync_user, category=category, record_failure=_update_failure, ) failed_ids = set(batch_result['failed_record_ids']) succeeded_ids = [record_id for record_id in failure_by_record_id.keys() if record_id not in failed_ids] deleted_failures = 0 if succeeded_ids: deleted_failures, _deleted_detail = api_models.PrintingExternalSyncFailure.objects.filter( external_record_id__in=succeeded_ids ).delete() remaining_failures = api_models.PrintingExternalSyncFailure.objects.count() result = { 'limit': limit, 'retried_records': len(retry_records), 'skipped_records': skipped_records, 'orders_created': batch_result['orders_created'], 'orders_updated': batch_result['orders_updated'], 'jobs_created': batch_result['jobs_created'], 'jobs_updated': batch_result['jobs_updated'], 'failed_records': batch_result['failed_records'], 'failed_record_ids': batch_result['failed_record_ids'], 'deleted_failures': deleted_failures, 'remaining_failures': remaining_failures, } self.stdout.write(self.style.SUCCESS(str(result)))