From 93580a6d637d05fad4fd73bc4c015070f5848635 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 22 Jul 2026 23:26:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20UFO=20=E5=AF=BC=E5=85=A5=E5=BF=85?= =?UTF-8?q?=E9=A1=BB=E6=98=BE=E5=BC=8F=20--leixing-id=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E4=B8=89=E8=A7=92=E6=B4=B2=E7=B1=BB=E5=9E=8B=E6=8C=82?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 --list-leixing;名称不含「三角洲」直接拦截;禁止模糊匹配。 Co-authored-by: Cursor --- .../management/commands/import_ufo_catalog.py | 165 +++++++++++------- 1 file changed, 105 insertions(+), 60 deletions(-) diff --git a/jituan/management/commands/import_ufo_catalog.py b/jituan/management/commands/import_ufo_catalog.py index 160f3ca..e9ca175 100644 --- a/jituan/management/commands/import_ufo_catalog.py +++ b/jituan/management/commands/import_ufo_catalog.py @@ -3,18 +3,20 @@ 将原 UFO 站点商品目录灌入星阙,且只写 club_id=ufo。 默认 dry-run(只打印计划,不写库)。真正写入必须加 --apply。 +【必须】用 --leixing-id 指定三角洲类型主键,禁止靠模糊名字瞎挂。 -服务器上(复制整段即可): +服务器上(复制整段): cd /opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django - ./venv/bin/python manage.py import_ufo_catalog - ./venv/bin/python manage.py import_ufo_catalog --apply + git pull origin main -可选: - --leixing-name 三角洲 - --with-banners 同时写入 accept_order / accept_order_banner2 轮播 - --catalog /path/to.json 自定义目录文件(默认用内置 jituan/data/ufo_catalog.json) - --skip-oss 不上传图,直接用原 CDN 完整 URL 写入(仅应急) + # 先看全部类型 id(核对哪一行是三角洲) + ./venv/bin/python manage.py import_ufo_catalog --list-leixing + + # 假设核对后 id=3(示例!以你库里为准) + ./venv/bin/python manage.py import_ufo_catalog --leixing-id=3 + ./venv/bin/python manage.py import_ufo_catalog --leixing-id=3 --apply + ./venv/bin/python manage.py import_ufo_catalog --leixing-id=3 --apply --with-banners """ from __future__ import annotations @@ -33,54 +35,59 @@ from django.core.management.base import BaseCommand, CommandError from django.db import transaction TARGET_CLUB = 'ufo' -DEFAULT_LEIXING = '三角洲' +EXPECTED_NAME_HINT = '三角洲' DEFAULT_CATALOG = Path(__file__).resolve().parents[2] / 'data' / 'ufo_catalog.json' class Command(BaseCommand): - help = '导入 UFO 商品/专区到 club_id=ufo(默认 dry-run,加 --apply 才写库)' + help = '导入 UFO 商品/专区到 club_id=ufo(默认 dry-run;必须 --leixing-id)' def add_arguments(self, parser): + parser.add_argument('--apply', action='store_true', help='真正写库(仅 ufo)') + parser.add_argument('--catalog', type=str, default='', help='catalog.json 路径') parser.add_argument( - '--apply', + '--list-leixing', action='store_true', - help='真正写库(仅 ufo)。不加则只预览。', + help='只列出全局商品类型 id/名称,用来核对三角洲真实 id', ) parser.add_argument( - '--catalog', - type=str, - default='', - help='catalog.json 路径,默认 jituan/data/ufo_catalog.json', + '--leixing-id', + type=int, + default=0, + help='【必填,推荐】全局商品类型主键。先 --list-leixing 核对', ) parser.add_argument( '--leixing-name', type=str, - default=DEFAULT_LEIXING, - help='商品类型名称(全局 ShangpinLeixing.jieshao),默认:三角洲', - ) - parser.add_argument( - '--with-banners', - action='store_true', - help='同时导入主轮播/第二横幅到 Lunbo(page_key=accept_order / accept_order_banner2)', - ) - parser.add_argument( - '--skip-oss', - action='store_true', - help='不上传 OSS,直接把原站图片 URL 写入(应急;正式建议走 OSS)', - ) - parser.add_argument( - '--club-id', - type=str, - default=TARGET_CLUB, - help='禁止改成其它俱乐部;仅允许 ufo', + default='', + help='未传 id 时:按 jieshao「全名精确匹配」且必须唯一;禁止模糊匹配', ) + parser.add_argument('--with-banners', action='store_true', help='同时导入点单主轮播+第二横幅') + parser.add_argument('--skip-oss', action='store_true', help='不上传 OSS,保留原 URL(应急)') + parser.add_argument('--club-id', type=str, default=TARGET_CLUB, help='仅允许 ufo') def handle(self, *args, **options): + from products.models import ShangpinLeixing + + if options.get('list_leixing'): + self.stdout.write('全局商品类型清单:') + for lx in ShangpinLeixing.query.order_by('id'): + mark = ' <<-- 名称含三角洲' if EXPECTED_NAME_HINT in (lx.jieshao or '') else '' + self.stdout.write( + f' id={lx.id} name={lx.jieshao!r} ' + f'shenhezhuangtai={lx.shenhezhuangtai} paixu={lx.paixu}{mark}' + ) + self.stdout.write(self.style.NOTICE( + '请人工确认「三角洲」那一行的 id,再执行:\n' + ' ./venv/bin/python manage.py import_ufo_catalog --leixing-id=\n' + ' ./venv/bin/python manage.py import_ufo_catalog --leixing-id= --apply' + )) + return + club_id = (options.get('club_id') or '').strip() if club_id != TARGET_CLUB: raise CommandError( - f'拒绝执行:本命令只允许 club_id={TARGET_CLUB},你传的是 {club_id!r}。' - '禁止影响 xq/xzj/其它俱乐部。' + f'拒绝执行:只允许 club_id={TARGET_CLUB},你传的是 {club_id!r}' ) catalog_path = Path(options['catalog']).expanduser() if options['catalog'] else DEFAULT_CATALOG @@ -92,35 +99,38 @@ class Command(BaseCommand): zones = data.get('zones') or [] banners = data.get('banners') or [] banners2 = data.get('banners2') or [] - leixing_name = (options['leixing_name'] or DEFAULT_LEIXING).strip() apply = bool(options['apply']) with_banners = bool(options['with_banners']) skip_oss = bool(options['skip_oss']) from jituan.models import Club, ClubShangpinLeixingConfig - from products.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu + from products.models import Shangpin, ShangpinZhuanqu if not Club.query.filter(club_id=TARGET_CLUB).exists(): raise CommandError(f'俱乐部 {TARGET_CLUB} 不存在,请先 create_club ufo') - leixing = ( - ShangpinLeixing.query.filter(jieshao__icontains=leixing_name) - .exclude(shenhezhuangtai=2) - .order_by('id') - .first() + leixing = self._resolve_leixing( + ShangpinLeixing, + leixing_id=int(options.get('leixing_id') or 0), + leixing_name=(options.get('leixing_name') or '').strip(), ) - if not leixing: - raise CommandError( - f'找不到全局商品类型「{leixing_name}」。请先在后台建好该类型(集团共享),再跑本命令。' - ) self.stdout.write(self.style.NOTICE( - f'目标俱乐部={TARGET_CLUB} 类型={leixing.jieshao}(id={leixing.id}) ' + f'目标俱乐部={TARGET_CLUB} 类型={leixing.jieshao!r}(id={leixing.id}) ' + f'shenhezhuangtai={leixing.shenhezhuangtai} ' f'专区={len(zones)} 商品={len(products)} banners={len(banners)}+{len(banners2)} ' f'mode={"APPLY" if apply else "DRY-RUN"}' )) + if leixing.shenhezhuangtai == 2: + raise CommandError( + f'类型 id={leixing.id}「{leixing.jieshao}」是审核专用,拒绝挂载' + ) + if EXPECTED_NAME_HINT not in (leixing.jieshao or ''): + raise CommandError( + f'安全拦截:类型 id={leixing.id} 名称是「{leixing.jieshao}」,' + f'不含「{EXPECTED_NAME_HINT}」。请重新 --list-leixing 核对后再传正确 --leixing-id。' + ) - # 计划摘要 for z in zones: self.stdout.write(f' 专区: {z.get("name")} goods={len(z.get("goods_ids") or [])}') for p in products[:5]: @@ -132,9 +142,9 @@ class Command(BaseCommand): if not apply: self.stdout.write(self.style.WARNING( - '当前为 dry-run,未写库。确认无误后执行:\n' - ' ./venv/bin/python manage.py import_ufo_catalog --apply\n' - '需要轮播再加: --with-banners' + '当前 dry-run,未写库。确认类型 id 无误后:\n' + f' ./venv/bin/python manage.py import_ufo_catalog --leixing-id={leixing.id} --apply\n' + f' ./venv/bin/python manage.py import_ufo_catalog --leixing-id={leixing.id} --apply --with-banners' )) return @@ -150,12 +160,10 @@ class Command(BaseCommand): } def to_oss(url: str, prefix: str) -> str: - """下载原图并上传 OSS,返回相对路径;失败返回空串。""" if not url: return '' if skip_oss: return url - # 已是相对路径 if not str(url).startswith('http'): return url try: @@ -172,7 +180,6 @@ class Command(BaseCommand): return url with transaction.atomic(): - # 1) 上架三角洲到 ufo cfg, created = ClubShangpinLeixingConfig.query.get_or_create( club_id=TARGET_CLUB, leixing_id=leixing.id, @@ -189,8 +196,7 @@ class Command(BaseCommand): elif created: stats['leixing_enabled'] += 1 - # 2) 专区 - zone_id_map = {} # source_cat2_id -> ShangpinZhuanqu.id + zone_id_map = {} for idx, z in enumerate(zones): name = (z.get('name') or '').strip() if not name: @@ -225,7 +231,6 @@ class Command(BaseCommand): zone_id_map[src_id] = row.id zone_id_map[name] = row.id - # 3) 商品 for p in products: title = (p.get('title') or '').strip() if not title: @@ -295,7 +300,6 @@ class Command(BaseCommand): ) stats['products_created'] += 1 - # 4) 轮播(可选) if with_banners: from config.models import Lunbo from jituan.services.display_config import ( @@ -306,7 +310,6 @@ class Command(BaseCommand): def upsert_lunbo(page_key, items): image_type = lunbo_image_type_for_page(page_key) - # 只清本俱乐部本 page_key,不动其它 Lunbo.query.filter( club_id=TARGET_CLUB, page_key=page_key, @@ -336,6 +339,48 @@ class Command(BaseCommand): f"OSS成功={stats['oss_ok']} 失败={stats['oss_fail']}" )) + def _resolve_leixing(self, ShangpinLeixing, leixing_id: int, leixing_name: str): + """必须用 id;名字仅作精确匹配兜底,且必须唯一。""" + if leixing_id: + try: + return ShangpinLeixing.query.get(id=leixing_id) + except ShangpinLeixing.DoesNotExist as exc: + raise CommandError(f'类型 id={leixing_id} 不存在') from exc + + name = (leixing_name or '').strip() + if not name: + raise CommandError( + '必须指定类型,先执行:\n' + ' ./venv/bin/python manage.py import_ufo_catalog --list-leixing\n' + '再:\n' + ' ./venv/bin/python manage.py import_ufo_catalog --leixing-id=<三角洲的id>' + ) + + rows = list(ShangpinLeixing.query.filter(jieshao=name).order_by('id')) + if not rows: + # 列出接近的,方便人工选 + near = list( + ShangpinLeixing.query.filter(jieshao__icontains=name).order_by('id')[:20] + ) + hint = '\n'.join( + f' id={x.id} name={x.jieshao!r} shenhezhuangtai={x.shenhezhuangtai}' + for x in near + ) or ' (无近似匹配)' + raise CommandError( + f'找不到全名精确等于「{name}」的类型。近似:\n{hint}\n' + '请用 --leixing-id=... 明确指定。' + ) + if len(rows) > 1: + detail = '\n'.join( + f' id={x.id} name={x.jieshao!r} shenhezhuangtai={x.shenhezhuangtai}' + for x in rows + ) + raise CommandError( + f'名称「{name}」匹配到多条,拒绝自动选择,避免挂错:\n{detail}\n' + '请用 --leixing-id=... 指定其中一条。' + ) + return rows[0] + @staticmethod def _decimal(v) -> Decimal: try: