feat: 龙先生审核目录清空后从星阙复制

仅 lxs 审核专区/商品;先删后从 xq 镜像;dry-run + --i-confirm=lxs-from-xq。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-25 15:24:32 +08:00
parent 47939042e6
commit 59336424e3

View File

@@ -0,0 +1,312 @@
# -*- coding: utf-8 -*-
"""
龙先生(lxs)审核版目录重置:清空本俱乐部三个审核类型下的专区/商品,再从星阙(xq)原样复制。
范围(必须全部满足):
- 目标俱乐部固定 lxsname 含「龙先生」)
- 源俱乐部固定 xqname 含「星阙」)
- 仅全局 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)审核目录已与星阙对齐。'
))