# -*- coding: utf-8 -*- """ 删除 UFO 俱乐部「正常/公共」商品目录(专区 + 商品)及对应 OSS 图片。 范围(必须全部满足,缺一不删): - club_id 固定为 ufo(不可改目标) - dianpu IS NULL(公共目录;店铺商品一律不碰) - 表:shangpin、shangpin_zhuanqu - OSS:商品 tupian_url / guize_tupian、专区 tupian_url 不碰: - 其它俱乐部(xq / lxs / xzj 等) - 店铺商品(dianpu 非空) - 全局 ShangpinLeixing - ClubShangpinLeixingConfig、订单、轮播、转盘、统计等 用法: python manage.py delete_ufo_public_catalog python manage.py delete_ufo_public_catalog --apply --i-confirm-club=ufo """ from __future__ import annotations from django.core.management.base import BaseCommand, CommandError from django.db import transaction ALLOWED_CLUB_ID = 'ufo' NAME_HINT = 'UFO' # Club.name 须含此字样(大小写不敏感) class Command(BaseCommand): help = '删除 UFO 公共商品+专区及 OSS 图(默认 dry-run;仅 ufo)' def add_arguments(self, parser): parser.add_argument( '--apply', action='store_true', help='真正删库 + 删 OSS(默认仅打印清单)', ) parser.add_argument( '--i-confirm-club', type=str, default='', help='必须等于 ufo,才允许 --apply', ) parser.add_argument( '--club-id', type=str, default=ALLOWED_CLUB_ID, help='仅允许 ufo;传其它值会直接拒绝', ) def handle(self, *args, **options): from jituan.models import Club from products.models import Shangpin, ShangpinZhuanqu from utils.oss_utils import delete_from_oss club_id = (options.get('club_id') or '').strip() apply = bool(options.get('apply')) confirm = (options.get('i_confirm_club') or '').strip() if club_id != ALLOWED_CLUB_ID: raise CommandError( f'安全拦截:本命令只允许 club_id={ALLOWED_CLUB_ID!r},你传的是 {club_id!r}' ) club = Club.query.filter(club_id=ALLOWED_CLUB_ID).first() if not club: raise CommandError(f'俱乐部 {ALLOWED_CLUB_ID} 不存在,无需删除') if NAME_HINT.lower() not in (club.name or '').lower(): raise CommandError( f'安全拦截:club_id={ALLOWED_CLUB_ID!r} 的 name={club.name!r} ' f'不含「{NAME_HINT}」,拒绝执行。' ) if apply and confirm != ALLOWED_CLUB_ID: raise CommandError( '安全拦截:--apply 必须同时传 --i-confirm-club=ufo\n' ' 例:python manage.py delete_ufo_public_catalog --apply --i-confirm-club=ufo' ) zones = list( ShangpinZhuanqu.query.filter( club_id=ALLOWED_CLUB_ID, dianpu__isnull=True, ).order_by('id') ) products = list( Shangpin.query.filter( club_id=ALLOWED_CLUB_ID, dianpu__isnull=True, ).order_by('id') ) oss_urls = [] for z in zones: if z.tupian_url: oss_urls.append(('zone', z.id, z.tupian_url)) for p in products: if p.tupian_url: oss_urls.append(('product', p.id, p.tupian_url)) if p.guize_tupian: oss_urls.append(('product_guize', p.id, p.guize_tupian)) mode = 'APPLY' if apply else 'DRY-RUN' self.stdout.write(self.style.NOTICE( f'\n===== {mode} 删除 UFO 公共商品目录 =====\n' f'club_id={ALLOWED_CLUB_ID} name={club.name!r}\n' f'公共专区={len(zones)} 公共商品={len(products)} ' f'待删 OSS 路径={len(oss_urls)}\n' )) self.stdout.write('—— 专区 ——') for z in zones[:50]: self.stdout.write( f' id={z.id} mingzi={z.mingzi!r} leixing_id={z.leixing_id} ' f'tupian={z.tupian_url or "-"}' ) if len(zones) > 50: self.stdout.write(f' ... 其余 {len(zones) - 50} 个专区省略') self.stdout.write('—— 商品 ——') for p in products[:80]: self.stdout.write( f' id={p.id} {p.biaoqian!r} ¥{p.jiage} ' f'zhuanqu_id={p.zhuanqu_id} tupian={p.tupian_url or "-"}' ) if len(products) > 80: self.stdout.write(f' ... 其余 {len(products) - 80} 个商品省略') self.stdout.write(f'—— OSS 将删 {len(oss_urls)} 个对象(相对路径)——') for kind, oid, url in oss_urls[:30]: self.stdout.write(f' [{kind}#{oid}] {url}') if len(oss_urls) > 30: self.stdout.write(f' ... 其余 {len(oss_urls) - 30} 个省略') if not apply: self.stdout.write(self.style.WARNING( '\n以上仅计划,未删库、未删 OSS。\n' '确认无误后:\n' ' python manage.py delete_ufo_public_catalog --apply --i-confirm-club=ufo\n' )) return if not zones and not products: self.stdout.write(self.style.SUCCESS('无公共专区/商品,无需删除。')) return with transaction.atomic(): # 再按条件删一遍,避免误伤其它 club / 店铺商品 n_prod, _ = Shangpin.query.filter( club_id=ALLOWED_CLUB_ID, dianpu__isnull=True, ).delete() n_zone, _ = ShangpinZhuanqu.query.filter( club_id=ALLOWED_CLUB_ID, dianpu__isnull=True, ).delete() oss_ok = 0 oss_fail = 0 seen = set() for _, _, url in oss_urls: key = (url or '').strip() if not key or key in seen: continue seen.add(key) if delete_from_oss(key): oss_ok += 1 else: oss_fail += 1 self.stderr.write(f'OSS 删除失败(已记日志,继续): {key}') self.stdout.write(self.style.SUCCESS( f'\n删除完成: products={n_prod} zones={n_zone} ' f'oss_ok={oss_ok} oss_fail={oss_fail}\n' f'未动:其它俱乐部、店铺商品、全局类型、订单/转盘等。' ))