feat: 龙先生审核「娱乐护航」专区商品复制到其它审核类型
仅 lxs;只增不减;默认 dry-run,--apply 需 --i-confirm-club=lxs。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
将龙先生(lxs)审核类型「娱乐护航」下的专区+商品,复制到其它审核专用商品类型。
|
||||
|
||||
背景:全局通常有 3 个 shenhezhuangtai=2 的商品类型;其中「娱乐护航」已有专区/商品,
|
||||
另外两个往往为空。本命令只增不减,把源类型目录镜像到目标类型。
|
||||
|
||||
范围:
|
||||
- 仅 club_id=lxs(Club.name 须含「龙先生」)
|
||||
- 仅 shenhezhuangtai=2(审核专用)
|
||||
- 仅公共目录 dianpu IS NULL
|
||||
- 源类型:名称精确「娱乐护航」(可用 --source-name 覆盖)
|
||||
- 目标:除源以外的全部审核类型(或 --target-id 指定)
|
||||
|
||||
不碰:正常类型/商品、其它俱乐部、店铺商品、源数据、全局类型表本身。
|
||||
|
||||
图片:复用源相对路径(同俱乐部审核素材,不另拷 OSS)。
|
||||
|
||||
用法:
|
||||
python manage.py copy_lxs_shenhe_huhang_to_other_types
|
||||
python manage.py copy_lxs_shenhe_huhang_to_other_types --list-leixing
|
||||
python manage.py copy_lxs_shenhe_huhang_to_other_types --apply --i-confirm-club=lxs
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
ALLOWED_CLUB_ID = 'lxs'
|
||||
NAME_HINT = '龙先生'
|
||||
SHENHE = 2
|
||||
DEFAULT_SOURCE_NAME = '娱乐护航'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '龙先生:把审核类型「娱乐护航」的专区/商品复制到其它审核类型(默认 dry-run)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--apply', action='store_true', help='真正写库')
|
||||
parser.add_argument(
|
||||
'--i-confirm-club',
|
||||
type=str,
|
||||
default='',
|
||||
help='必须等于 lxs 才允许 --apply',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--club-id',
|
||||
type=str,
|
||||
default=ALLOWED_CLUB_ID,
|
||||
help='仅允许 lxs',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--source-name',
|
||||
type=str,
|
||||
default=DEFAULT_SOURCE_NAME,
|
||||
help=f'源审核类型全名精确匹配,默认「{DEFAULT_SOURCE_NAME}」',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--source-id',
|
||||
type=int,
|
||||
default=0,
|
||||
help='可选:直接指定源 ShangpinLeixing.id(须为审核专用且名称校验)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--target-id',
|
||||
type=int,
|
||||
action='append',
|
||||
default=[],
|
||||
help='可选:只复制到指定审核类型 id,可多次传入;默认=除源外全部审核类型',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--list-leixing',
|
||||
action='store_true',
|
||||
help='只列出审核类型及 lxs 下专区/商品数量',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from jituan.models import Club
|
||||
from products.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu
|
||||
|
||||
club_id = (options.get('club_id') or '').strip()
|
||||
apply = bool(options.get('apply'))
|
||||
confirm = (options.get('i_confirm_club') or '').strip()
|
||||
source_name = (options.get('source_name') or DEFAULT_SOURCE_NAME).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 not in (club.name or ''):
|
||||
raise CommandError(
|
||||
f'安全拦截:{ALLOWED_CLUB_ID} 的 name={club.name!r} 不含「{NAME_HINT}」'
|
||||
)
|
||||
|
||||
audit_types = list(
|
||||
ShangpinLeixing.query.filter(shenhezhuangtai=SHENHE).order_by('id')
|
||||
)
|
||||
if not audit_types:
|
||||
raise CommandError('库中没有任何审核专用商品类型(shenhezhuangtai=2)')
|
||||
|
||||
def count_for(leixing_id):
|
||||
z = ShangpinZhuanqu.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=leixing_id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
p = Shangpin.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=leixing_id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
return z, p
|
||||
|
||||
if options.get('list_leixing'):
|
||||
self.stdout.write(self.style.NOTICE(
|
||||
f'审核类型清单(club={ALLOWED_CLUB_ID} 下专区/商品数):'
|
||||
))
|
||||
for lx in audit_types:
|
||||
zc, pc = count_for(lx.id)
|
||||
mark = ' <<-- 源候选' if (lx.jieshao or '') == source_name else ''
|
||||
self.stdout.write(
|
||||
f' id={lx.id} name={lx.jieshao!r} zones={zc} products={pc}{mark}'
|
||||
)
|
||||
return
|
||||
|
||||
if apply and confirm != ALLOWED_CLUB_ID:
|
||||
raise CommandError(
|
||||
'安全拦截:--apply 必须带 --i-confirm-club=lxs\n'
|
||||
' python manage.py copy_lxs_shenhe_huhang_to_other_types '
|
||||
'--apply --i-confirm-club=lxs'
|
||||
)
|
||||
|
||||
# 解析源类型
|
||||
source_id = int(options.get('source_id') or 0)
|
||||
if source_id:
|
||||
source = ShangpinLeixing.query.filter(id=source_id).first()
|
||||
if not source:
|
||||
raise CommandError(f'找不到类型 id={source_id}')
|
||||
if source.shenhezhuangtai != SHENHE:
|
||||
raise CommandError(f'类型 id={source_id} 不是审核专用')
|
||||
if (source.jieshao or '') != source_name:
|
||||
raise CommandError(
|
||||
f'安全拦截:id={source_id} 名称是「{source.jieshao}」,'
|
||||
f'与要求的「{source_name}」不一致'
|
||||
)
|
||||
else:
|
||||
matches = [lx for lx in audit_types if (lx.jieshao or '') == source_name]
|
||||
if not matches:
|
||||
# 宽松:名称包含「娱乐护」且含「航」或等于常见别名
|
||||
soft = [
|
||||
lx for lx in audit_types
|
||||
if '娱乐护' in (lx.jieshao or '')
|
||||
]
|
||||
hint = '\n'.join(
|
||||
f' id={lx.id} name={lx.jieshao!r}' for lx in audit_types
|
||||
)
|
||||
raise CommandError(
|
||||
f'找不到审核类型全名「{source_name}」。当前审核类型:\n{hint}\n'
|
||||
f'可用 --source-id=... 或 --source-name=... 指定。'
|
||||
+ (f'\n名称含「娱乐护」的有: {[s.jieshao for s in soft]}' if soft else '')
|
||||
)
|
||||
if len(matches) > 1:
|
||||
raise CommandError(
|
||||
f'「{source_name}」匹配到多条 {[m.id for m in matches]},请 --source-id='
|
||||
)
|
||||
source = matches[0]
|
||||
|
||||
src_zones_n, src_prods_n = count_for(source.id)
|
||||
if src_zones_n == 0 and src_prods_n == 0:
|
||||
raise CommandError(
|
||||
f'源类型 id={source.id}「{source.jieshao}」在 {ALLOWED_CLUB_ID} 下'
|
||||
f'没有任何审核专区/商品,无需复制'
|
||||
)
|
||||
|
||||
# 目标类型
|
||||
target_ids = [int(x) for x in (options.get('target_id') or [])]
|
||||
if target_ids:
|
||||
targets = []
|
||||
for tid in target_ids:
|
||||
lx = ShangpinLeixing.query.filter(id=tid).first()
|
||||
if not lx:
|
||||
raise CommandError(f'目标类型 id={tid} 不存在')
|
||||
if lx.shenhezhuangtai != SHENHE:
|
||||
raise CommandError(f'目标类型 id={tid} 不是审核专用,拒绝')
|
||||
if lx.id == source.id:
|
||||
raise CommandError('目标不能与源相同')
|
||||
targets.append(lx)
|
||||
else:
|
||||
targets = [lx for lx in audit_types if lx.id != source.id]
|
||||
|
||||
if not targets:
|
||||
raise CommandError('没有可复制的目标审核类型(除源外为空)')
|
||||
|
||||
src_zones = list(
|
||||
ShangpinZhuanqu.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=source.id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).order_by('paixu', 'id')
|
||||
)
|
||||
src_products = list(
|
||||
Shangpin.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=source.id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).order_by('paixu', 'id')
|
||||
)
|
||||
|
||||
mode = 'APPLY' if apply else 'DRY-RUN'
|
||||
self.stdout.write(self.style.NOTICE(
|
||||
f'\n===== {mode} 复制审核目录 =====\n'
|
||||
f'club={ALLOWED_CLUB_ID} ({club.name})\n'
|
||||
f'源类型: id={source.id}「{source.jieshao}」 '
|
||||
f'专区={len(src_zones)} 商品={len(src_products)}\n'
|
||||
f'目标类型({len(targets)}): '
|
||||
+ ', '.join(f'id={t.id}「{t.jieshao}」' for t in targets)
|
||||
+ '\n'
|
||||
))
|
||||
|
||||
for t in targets:
|
||||
zc, pc = count_for(t.id)
|
||||
self.stdout.write(f' 目标当前存量 id={t.id}「{t.jieshao}」 zones={zc} products={pc}')
|
||||
|
||||
self.stdout.write('—— 源专区 ——')
|
||||
for z in src_zones:
|
||||
self.stdout.write(f' id={z.id} {z.mingzi!r} paixu={z.paixu}')
|
||||
self.stdout.write('—— 源商品样例 ——')
|
||||
for p in src_products[:8]:
|
||||
self.stdout.write(f' id={p.id} {p.biaoqian!r} ¥{p.jiage} zq={p.zhuanqu_id}')
|
||||
if len(src_products) > 8:
|
||||
self.stdout.write(f' ... 其余 {len(src_products) - 8} 个省略')
|
||||
|
||||
# 预计算每个目标的 create/skip
|
||||
preview = []
|
||||
for t in targets:
|
||||
z_create = z_skip = p_create = p_skip = 0
|
||||
for z in src_zones:
|
||||
exist_z = ShangpinZhuanqu.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=t.id,
|
||||
mingzi=z.mingzi,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).first()
|
||||
if exist_z:
|
||||
z_skip += 1
|
||||
else:
|
||||
z_create += 1
|
||||
# 商品去重按:目标类型下 同标题+同专区名(专区可能尚未建,用名推断)
|
||||
for p in src_products:
|
||||
src_z = next((x for x in src_zones if x.id == p.zhuanqu_id), None)
|
||||
zname = src_z.mingzi if src_z else None
|
||||
exist_zq = None
|
||||
if zname:
|
||||
exist_zq = ShangpinZhuanqu.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=t.id,
|
||||
mingzi=zname,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).first()
|
||||
if exist_zq:
|
||||
exist_p = Shangpin.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=t.id,
|
||||
zhuanqu_id=exist_zq.id,
|
||||
biaoqian=p.biaoqian,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).first()
|
||||
if exist_p:
|
||||
p_skip += 1
|
||||
else:
|
||||
p_create += 1
|
||||
else:
|
||||
p_create += 1
|
||||
preview.append((t, z_create, z_skip, p_create, p_skip))
|
||||
self.stdout.write(
|
||||
f' 计划 →「{t.jieshao}」: zones +{z_create}/skip{z_skip}, '
|
||||
f'products +{p_create}/skip{p_skip}'
|
||||
)
|
||||
|
||||
if not apply:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n以上仅计划,未写库。确认后:\n'
|
||||
' python manage.py copy_lxs_shenhe_huhang_to_other_types '
|
||||
'--apply --i-confirm-club=lxs\n'
|
||||
))
|
||||
return
|
||||
|
||||
stats = {
|
||||
'targets': 0,
|
||||
'zones_created': 0,
|
||||
'zones_skipped': 0,
|
||||
'products_created': 0,
|
||||
'products_skipped': 0,
|
||||
'products_skip_no_zone': 0,
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
for t in targets:
|
||||
stats['targets'] += 1
|
||||
zone_map = {} # src_zone_id -> dst_zone_id
|
||||
|
||||
for z in src_zones:
|
||||
exist_z = ShangpinZhuanqu.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=t.id,
|
||||
mingzi=z.mingzi,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).first()
|
||||
if exist_z:
|
||||
zone_map[z.id] = exist_z.id
|
||||
stats['zones_skipped'] += 1
|
||||
continue
|
||||
row = ShangpinZhuanqu.query.create(
|
||||
mingzi=z.mingzi,
|
||||
leixing_id=t.id,
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
tupian_url=z.tupian_url or '',
|
||||
shenhezhuangtai=SHENHE,
|
||||
paixu=int(z.paixu or 0),
|
||||
shangjia_zhuangtai=bool(getattr(z, 'shangjia_zhuangtai', True)),
|
||||
fengjin_zhuangtai=bool(getattr(z, 'fengjin_zhuangtai', True)),
|
||||
dianpu=None,
|
||||
dianpu_leixing=None,
|
||||
)
|
||||
zone_map[z.id] = row.id
|
||||
stats['zones_created'] += 1
|
||||
|
||||
for p in src_products:
|
||||
dst_zq = zone_map.get(p.zhuanqu_id)
|
||||
if not dst_zq:
|
||||
stats['products_skip_no_zone'] += 1
|
||||
self.stderr.write(
|
||||
f'跳过无专区映射商品: {p.biaoqian!r} src_zq={p.zhuanqu_id} →「{t.jieshao}」'
|
||||
)
|
||||
continue
|
||||
exist_p = Shangpin.query.filter(
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
leixing_id=t.id,
|
||||
zhuanqu_id=dst_zq,
|
||||
biaoqian=p.biaoqian,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).first()
|
||||
if exist_p:
|
||||
stats['products_skipped'] += 1
|
||||
continue
|
||||
Shangpin.query.create(
|
||||
biaoqian=p.biaoqian,
|
||||
jiage=p.jiage,
|
||||
kucun=p.kucun,
|
||||
leixing_id=t.id,
|
||||
zhuanqu_id=dst_zq,
|
||||
zhenshi_xiaoliang=0,
|
||||
duiwai_xiaoliang=0,
|
||||
jieshao=p.jieshao,
|
||||
xiadan_xuzhi=p.xiadan_xuzhi,
|
||||
guize_tupian=p.guize_tupian or '',
|
||||
tupian_url=p.tupian_url or '',
|
||||
yaoqiuleixing=p.yaoqiuleixing,
|
||||
huiyuan_id=p.huiyuan_id,
|
||||
yongjin=p.yongjin,
|
||||
kaioi_ewai_dashou_fencheng=bool(p.kaioi_ewai_dashou_fencheng),
|
||||
ewai_dashou_fencheng=p.ewai_dashou_fencheng,
|
||||
paixu=int(p.paixu or 0),
|
||||
shenhezhuangtai=SHENHE,
|
||||
shangjia_zhuangtai=bool(p.shangjia_zhuangtai),
|
||||
fengjin_zhuangtai=bool(p.fengjin_zhuangtai),
|
||||
shenhe_zhuangtai=bool(getattr(p, 'shenhe_zhuangtai', True)),
|
||||
club_id=ALLOWED_CLUB_ID,
|
||||
shi_zhuanpan=False,
|
||||
dianpu=None,
|
||||
dianpu_leixing=None,
|
||||
)
|
||||
stats['products_created'] += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n完成: {stats}\n'
|
||||
f'源「{source.jieshao}」未改动;目标均为审核专用且仅 lxs。'
|
||||
))
|
||||
Reference in New Issue
Block a user