Files
Django/jituan/management/commands/delete_ufo_public_catalog.py
XingQue d771add6e8 feat: 新增删除 UFO 公共商品与专区(含 OSS)管理命令
仅 club_id=ufo 且 dianpu 为空;默认 dry-run,--apply 需 --i-confirm-club=ufo。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 16:40:46 +08:00

179 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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'未动:其它俱乐部、店铺商品、全局类型、订单/转盘等。'
))