feat: 新增删除 UFO 公共商品与专区(含 OSS)管理命令
仅 club_id=ufo 且 dianpu 为空;默认 dry-run,--apply 需 --i-confirm-club=ufo。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
178
jituan/management/commands/delete_ufo_public_catalog.py
Normal file
178
jituan/management/commands/delete_ufo_public_catalog.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# -*- 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'未动:其它俱乐部、店铺商品、全局类型、订单/转盘等。'
|
||||
))
|
||||
Reference in New Issue
Block a user