From 59336424e332371b3d88139ce183dc5b574bd2d6 Mon Sep 17 00:00:00 2001 From: XingQue Date: Sat, 25 Jul 2026 15:24:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=BE=99=E5=85=88=E7=94=9F=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E7=9B=AE=E5=BD=95=E6=B8=85=E7=A9=BA=E5=90=8E=E4=BB=8E?= =?UTF-8?q?=E6=98=9F=E9=98=99=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅 lxs 审核专区/商品;先删后从 xq 镜像;dry-run + --i-confirm=lxs-from-xq。 Co-authored-by: Cursor --- .../reset_lxs_shenhe_catalog_from_xq.py | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 jituan/management/commands/reset_lxs_shenhe_catalog_from_xq.py diff --git a/jituan/management/commands/reset_lxs_shenhe_catalog_from_xq.py b/jituan/management/commands/reset_lxs_shenhe_catalog_from_xq.py new file mode 100644 index 0000000..dbabc84 --- /dev/null +++ b/jituan/management/commands/reset_lxs_shenhe_catalog_from_xq.py @@ -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)审核目录已与星阙对齐。' + ))