1
0
forked from erp-dev/erp

feat: command for tiia(tencent)

This commit is contained in:
2026-01-20 15:26:20 +08:00
parent 90a70d6f6d
commit c170381383
2 changed files with 198 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
"""
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='处理某一天创建的 PlateOrderYYYY-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],
}
)
)