feat: 龙先生审核目录清空后从星阙复制
仅 lxs 审核专区/商品;先删后从 xq 镜像;dry-run + --i-confirm=lxs-from-xq。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
312
jituan/management/commands/reset_lxs_shenhe_catalog_from_xq.py
Normal file
312
jituan/management/commands/reset_lxs_shenhe_catalog_from_xq.py
Normal file
@@ -0,0 +1,312 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
龙先生(lxs)审核版目录重置:清空本俱乐部三个审核类型下的专区/商品,再从星阙(xq)原样复制。
|
||||
|
||||
范围(必须全部满足):
|
||||
- 目标俱乐部固定 lxs(name 含「龙先生」)
|
||||
- 源俱乐部固定 xq(name 含「星阙」)
|
||||
- 仅全局 ShangpinLeixing.shenhezhuangtai=2 的类型(通常 3 个)
|
||||
- 仅公共目录 dianpu IS NULL
|
||||
- 专区/商品均要求 shenhezhuangtai=2
|
||||
|
||||
步骤:
|
||||
1) 删除 lxs 下上述类型的专区+商品(只删 DB,不删 OSS,避免误伤星阙共用图)
|
||||
2) 从 xq 复制同类型专区+商品到 lxs(图片复用相对路径;销量归零)
|
||||
|
||||
不碰:正常商品(shenhezhuangtai=1)、其它俱乐部、店铺商品、全局类型表、订单等。
|
||||
|
||||
用法:
|
||||
python manage.py reset_lxs_shenhe_catalog_from_xq
|
||||
python manage.py reset_lxs_shenhe_catalog_from_xq --list
|
||||
python manage.py reset_lxs_shenhe_catalog_from_xq --apply --i-confirm=lxs-from-xq
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
TARGET_CLUB = 'lxs'
|
||||
TARGET_NAME_HINT = '龙先生'
|
||||
SOURCE_CLUB = 'xq'
|
||||
SOURCE_NAME_HINT = '星阙'
|
||||
SHENHE = 2
|
||||
CONFIRM_TOKEN = 'lxs-from-xq'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '清空龙先生审核专区/商品,并从星阙复制三个审核类型目录(默认 dry-run)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--apply', action='store_true', help='真正删+写')
|
||||
parser.add_argument(
|
||||
'--i-confirm',
|
||||
type=str,
|
||||
default='',
|
||||
help=f'必须等于 {CONFIRM_TOKEN} 才允许 --apply',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--list',
|
||||
action='store_true',
|
||||
help='只列出审核类型及 xq/lxs 下专区商品数量',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from jituan.models import Club
|
||||
from products.models import Shangpin, ShangpinLeixing, ShangpinZhuanqu
|
||||
|
||||
apply = bool(options.get('apply'))
|
||||
confirm = (options.get('i_confirm') or '').strip()
|
||||
|
||||
src_club = Club.query.filter(club_id=SOURCE_CLUB).first()
|
||||
if not src_club:
|
||||
raise CommandError(f'源俱乐部 {SOURCE_CLUB} 不存在')
|
||||
if SOURCE_NAME_HINT not in (src_club.name or ''):
|
||||
raise CommandError(
|
||||
f'安全拦截:{SOURCE_CLUB} 的 name={src_club.name!r} 不含「{SOURCE_NAME_HINT}」'
|
||||
)
|
||||
|
||||
dst_club = Club.query.filter(club_id=TARGET_CLUB).first()
|
||||
if not dst_club:
|
||||
raise CommandError(f'目标俱乐部 {TARGET_CLUB} 不存在')
|
||||
if TARGET_NAME_HINT not in (dst_club.name or ''):
|
||||
raise CommandError(
|
||||
f'安全拦截:{TARGET_CLUB} 的 name={dst_club.name!r} 不含「{TARGET_NAME_HINT}」'
|
||||
)
|
||||
|
||||
audit_types = list(
|
||||
ShangpinLeixing.query.filter(shenhezhuangtai=SHENHE).order_by('id')
|
||||
)
|
||||
if not audit_types:
|
||||
raise CommandError('没有任何审核专用商品类型(shenhezhuangtai=2)')
|
||||
if len(audit_types) != 3:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f'注意:当前审核类型数量={len(audit_types)}(预期常为 3),将按实际全部处理。'
|
||||
))
|
||||
|
||||
leixing_ids = [lx.id for lx in audit_types]
|
||||
|
||||
def counts(club_id):
|
||||
z = ShangpinZhuanqu.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
p = Shangpin.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
return z, p
|
||||
|
||||
def per_type(club_id):
|
||||
rows = []
|
||||
for lx in audit_types:
|
||||
z = ShangpinZhuanqu.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
p = Shangpin.query.filter(
|
||||
club_id=club_id,
|
||||
leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).count()
|
||||
rows.append((lx, z, p))
|
||||
return rows
|
||||
|
||||
if options.get('list'):
|
||||
self.stdout.write(self.style.NOTICE('审核类型及两侧存量:'))
|
||||
for lx in audit_types:
|
||||
sz, sp = (
|
||||
ShangpinZhuanqu.query.filter(
|
||||
club_id=SOURCE_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
Shangpin.query.filter(
|
||||
club_id=SOURCE_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
)
|
||||
dz, dp = (
|
||||
ShangpinZhuanqu.query.filter(
|
||||
club_id=TARGET_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
Shangpin.query.filter(
|
||||
club_id=TARGET_CLUB, leixing_id=lx.id,
|
||||
shenhezhuangtai=SHENHE, dianpu__isnull=True,
|
||||
).count(),
|
||||
)
|
||||
self.stdout.write(
|
||||
f' id={lx.id}「{lx.jieshao}」 '
|
||||
f'xq: zones={sz} products={sp} | '
|
||||
f'lxs: zones={dz} products={dp}'
|
||||
)
|
||||
return
|
||||
|
||||
if apply and confirm != CONFIRM_TOKEN:
|
||||
raise CommandError(
|
||||
f'安全拦截:--apply 必须带 --i-confirm={CONFIRM_TOKEN}\n'
|
||||
f' python manage.py reset_lxs_shenhe_catalog_from_xq '
|
||||
f'--apply --i-confirm={CONFIRM_TOKEN}'
|
||||
)
|
||||
|
||||
src_z_total, src_p_total = counts(SOURCE_CLUB)
|
||||
dst_z_total, dst_p_total = counts(TARGET_CLUB)
|
||||
|
||||
src_zones = list(
|
||||
ShangpinZhuanqu.query.filter(
|
||||
club_id=SOURCE_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).order_by('leixing_id', 'paixu', 'id')
|
||||
)
|
||||
src_products = list(
|
||||
Shangpin.query.filter(
|
||||
club_id=SOURCE_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).order_by('leixing_id', 'paixu', 'id')
|
||||
)
|
||||
|
||||
# 源商品的专区必须落在本批源专区内,否则拒绝(防串数据)
|
||||
src_zone_ids = {z.id for z in src_zones}
|
||||
orphan = [p for p in src_products if p.zhuanqu_id not in src_zone_ids]
|
||||
if orphan:
|
||||
sample = ', '.join(f'{p.id}:{p.biaoqian}' for p in orphan[:5])
|
||||
raise CommandError(
|
||||
f'源商品有 {len(orphan)} 条专区不在本批审核专区内,已中止。样例: {sample}'
|
||||
)
|
||||
|
||||
mode = 'APPLY' if apply else 'DRY-RUN'
|
||||
self.stdout.write(self.style.NOTICE(
|
||||
f'\n===== {mode} 龙先生审核目录 ← 星阙 =====\n'
|
||||
f'源: {SOURCE_CLUB} ({src_club.name}) '
|
||||
f'专区={src_z_total} 商品={src_p_total}\n'
|
||||
f'目标: {TARGET_CLUB} ({dst_club.name}) '
|
||||
f'将先删除 专区={dst_z_total} 商品={dst_p_total},再复制源数据\n'
|
||||
f'审核类型数={len(audit_types)} ids={leixing_ids}\n'
|
||||
f'OSS: 不删文件;复制时复用星阙相对路径\n'
|
||||
))
|
||||
|
||||
self.stdout.write('—— 分类型存量 ——')
|
||||
for lx, sz, sp in per_type(SOURCE_CLUB):
|
||||
self.stdout.write(f' [xq] id={lx.id}「{lx.jieshao}」 zones={sz} products={sp}')
|
||||
for lx, dz, dp in per_type(TARGET_CLUB):
|
||||
self.stdout.write(f' [lxs] id={lx.id}「{lx.jieshao}」 zones={dz} products={dp}')
|
||||
|
||||
self.stdout.write('—— 将从 xq 复制的专区 ——')
|
||||
for z in src_zones[:40]:
|
||||
self.stdout.write(
|
||||
f' zq#{z.id} leixing={z.leixing_id} {z.mingzi!r}'
|
||||
)
|
||||
if len(src_zones) > 40:
|
||||
self.stdout.write(f' ... 其余 {len(src_zones) - 40} 个省略')
|
||||
|
||||
self.stdout.write('—— 将从 xq 复制的商品样例 ——')
|
||||
for p in src_products[:15]:
|
||||
self.stdout.write(
|
||||
f' sp#{p.id} leixing={p.leixing_id} {p.biaoqian!r} ¥{p.jiage}'
|
||||
)
|
||||
if len(src_products) > 15:
|
||||
self.stdout.write(f' ... 其余 {len(src_products) - 15} 个省略')
|
||||
|
||||
if not apply:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'\n以上仅计划:未删库、未写库。确认无误后:\n'
|
||||
f' python manage.py reset_lxs_shenhe_catalog_from_xq '
|
||||
f'--apply --i-confirm={CONFIRM_TOKEN}\n'
|
||||
))
|
||||
return
|
||||
|
||||
stats = {
|
||||
'deleted_products': 0,
|
||||
'deleted_zones': 0,
|
||||
'zones_created': 0,
|
||||
'products_created': 0,
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
# 1) 只删 lxs + 审核类型 + 公共
|
||||
n_prod, _ = Shangpin.query.filter(
|
||||
club_id=TARGET_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).delete()
|
||||
n_zone, _ = ShangpinZhuanqu.query.filter(
|
||||
club_id=TARGET_CLUB,
|
||||
leixing_id__in=leixing_ids,
|
||||
shenhezhuangtai=SHENHE,
|
||||
dianpu__isnull=True,
|
||||
).delete()
|
||||
stats['deleted_products'] = n_prod
|
||||
stats['deleted_zones'] = n_zone
|
||||
|
||||
# 2) 按源专区插入,建立 id 映射
|
||||
zone_id_map = {}
|
||||
for z in src_zones:
|
||||
if z.leixing_id not in leixing_ids:
|
||||
raise CommandError(f'专区 {z.id} 类型 {z.leixing_id} 不在审核列表')
|
||||
row = ShangpinZhuanqu.query.create(
|
||||
mingzi=z.mingzi,
|
||||
leixing_id=z.leixing_id,
|
||||
club_id=TARGET_CLUB,
|
||||
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_id_map[z.id] = row.id
|
||||
stats['zones_created'] += 1
|
||||
|
||||
for p in src_products:
|
||||
if p.leixing_id not in leixing_ids:
|
||||
raise CommandError(f'商品 {p.id} 类型 {p.leixing_id} 不在审核列表')
|
||||
dst_zq = zone_id_map.get(p.zhuanqu_id)
|
||||
if not dst_zq:
|
||||
raise CommandError(
|
||||
f'商品「{p.biaoqian}」id={p.id} 无法映射 zhuanqu_id={p.zhuanqu_id}'
|
||||
)
|
||||
Shangpin.query.create(
|
||||
biaoqian=p.biaoqian,
|
||||
jiage=p.jiage,
|
||||
kucun=p.kucun,
|
||||
leixing_id=p.leixing_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=TARGET_CLUB,
|
||||
shi_zhuanpan=False,
|
||||
dianpu=None,
|
||||
dianpu_leixing=None,
|
||||
)
|
||||
stats['products_created'] += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n完成: {stats}\n'
|
||||
f'星阙(xq)未改动;龙先生(lxs)审核目录已与星阙对齐。'
|
||||
))
|
||||
Reference in New Issue
Block a user