diff --git a/jituan/management/commands/cleanup_lsx_empty_club.py b/jituan/management/commands/cleanup_lsx_empty_club.py new file mode 100644 index 0000000..342a166 --- /dev/null +++ b/jituan/management/commands/cleanup_lsx_empty_club.py @@ -0,0 +1,188 @@ +"""清理龙先生重复/空证书俱乐部。 + +规则: +- 候选:club_id=lsx / 名称含「龙先生」 / wx_appid=龙先生小程序 +- 「有 key」:pay_key_path / pay_cert_path / private_key_path / platform_cert_dir 任一非空 +- 默认 dry-run;--apply 才删除「完全没有 key 路径」的俱乐部行及相关配置表 +- 若只剩一条且无 key,不会删(避免误删唯一配置);除非 --force-delete-empty-only + +用法: + python manage.py cleanup_lsx_empty_club + python manage.py cleanup_lsx_empty_club --apply + python manage.py cleanup_lsx_empty_club --apply --remove-seed-users +""" +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q + +from jituan.models import ( + Club, + ClubDashouExamConfig, + ClubDashouExamPass, + ClubDashouExamQuestion, + ClubDashouExamQuestionImage, + ClubFadanFenhongLilv, + ClubHuiyuanBundleInclude, + ClubHuiyuanPrice, + ClubKaoheChenghaoConfig, + ClubLilubiao, + ClubPaymentChannel, + ClubShangpinLeixingConfig, + ClubTixianQuota, + ClubWithdrawConfig, +) +from users.business_models import User +from users.models import UserBoss, UserGuanshi, UserZuzhang + +LSX_APPID = 'wx7ff90e9d024fcdb8' +SEED_UIDS = ('9910001', '9910002') + +# 删除俱乐部时一并清掉的配置表(按 club_id) +RELATED_MODELS = ( + ClubPaymentChannel, + ClubHuiyuanPrice, + ClubHuiyuanBundleInclude, + ClubLilubiao, + ClubWithdrawConfig, + ClubTixianQuota, + ClubShangpinLeixingConfig, + ClubKaoheChenghaoConfig, + ClubFadanFenhongLilv, + ClubDashouExamConfig, + ClubDashouExamQuestion, + ClubDashouExamQuestionImage, + ClubDashouExamPass, +) + + +def _has_key_paths(club): + fields = ( + getattr(club, 'pay_key_path', '') or '', + getattr(club, 'pay_cert_path', '') or '', + getattr(club, 'private_key_path', '') or '', + getattr(club, 'platform_cert_dir', '') or '', + ) + return any(str(x).strip() for x in fields) + + +def _candidates(): + return Club.query.filter( + Q(club_id__iexact='lsx') + | Q(club_id__icontains='lsx') + | Q(name__icontains='龙先生') + | Q(wx_appid=LSX_APPID) + ).order_by('club_id') + + +def _purge_club_related(club_id, stdout, style): + for model in RELATED_MODELS: + try: + n = model.query.filter(club_id=club_id).count() + if n: + model.query.filter(club_id=club_id).delete() + stdout.write(f' - 已删 {model.__name__} x{n}') + except Exception as exc: + stdout.write(style.WARNING(f' - 跳过 {model.__name__}: {exc}')) + + +class Command(BaseCommand): + help = '列出并删除龙先生相关「无证书 key 路径」的重复俱乐部(默认 dry-run)' + + def add_arguments(self, parser): + parser.add_argument('--apply', action='store_true', help='真正写入删除') + parser.add_argument( + '--force-delete-empty-only', + action='store_true', + help='即使没有「有 key」的对照俱乐部,也允许删除无 key 的候选', + ) + parser.add_argument( + '--remove-seed-users', + action='store_true', + help='同时删除 seed_lsx_invite_data 种子用户 9910001/9910002', + ) + parser.add_argument( + '--club-id', + type=str, + default='', + help='只处理指定 club_id(精确匹配)', + ) + + def handle(self, *args, **options): + apply = bool(options.get('apply')) + force = bool(options.get('force_delete_empty_only')) + only_id = (options.get('club_id') or '').strip() + + qs = _candidates() + if only_id: + qs = Club.query.filter(club_id=only_id) + + clubs = list(qs) + if not clubs: + self.stdout.write(self.style.WARNING('未找到龙先生/lsx 相关俱乐部')) + return + + with_keys = [] + without_keys = [] + self.stdout.write(self.style.SUCCESS('=' * 60)) + self.stdout.write('龙先生相关俱乐部扫描结果:') + for c in clubs: + keyed = _has_key_paths(c) + (with_keys if keyed else without_keys).append(c) + self.stdout.write('-' * 40) + self.stdout.write(f'club_id={c.club_id} name={c.name} status={c.status}') + self.stdout.write(f' wx_appid={c.wx_appid or "(空)"}') + self.stdout.write(f' pay_key_path={c.pay_key_path or "(空)"}') + self.stdout.write(f' pay_cert_path={c.pay_cert_path or "(空)"}') + self.stdout.write(f' private_key_path={c.private_key_path or "(空)"}') + self.stdout.write(f' platform_cert_dir={c.platform_cert_dir or "(空)"}') + self.stdout.write(f' >>> {"有 key 路径(保留)" if keyed else "无 key 路径(可删)"}') + + self.stdout.write(self.style.SUCCESS('=' * 60)) + self.stdout.write(f'有 key: {len(with_keys)} 条;无 key: {len(without_keys)} 条') + + if not without_keys: + self.stdout.write(self.style.SUCCESS('没有「无 key 路径」的俱乐部,无需删除。')) + elif not with_keys and not force: + self.stdout.write(self.style.ERROR( + '全部候选都没有 key 路径。为安全起见不自动删。' + '若确认要删,加 --force-delete-empty-only --apply' + )) + else: + for c in without_keys: + self.stdout.write(self.style.WARNING(f'将删除无 key 俱乐部: {c.club_id} ({c.name})')) + if not apply: + self.stdout.write(' (dry-run,未真正删除;加 --apply 执行)') + continue + with transaction.atomic(): + _purge_club_related(c.club_id, self.stdout, self.style) + Club.query.filter(club_id=c.club_id).delete() + self.stdout.write(self.style.SUCCESS(f' 已删除俱乐部 {c.club_id}')) + + if options.get('remove_seed_users'): + self.stdout.write('-' * 40) + self.stdout.write('处理种子用户 9910001 / 9910002 ...') + for uid in SEED_UIDS: + user = User.query.filter(UserUID=uid).first() + if not user: + self.stdout.write(f' {uid}: 不存在') + continue + self.stdout.write( + f' {uid}: ClubID={user.ClubID} Phone={user.Phone} OpenID={user.OpenID}' + ) + if not apply: + self.stdout.write(' (dry-run)') + continue + with transaction.atomic(): + UserZuzhang.query.filter(user=user).delete() + UserGuanshi.query.filter(user=user).delete() + UserBoss.query.filter(user=user).delete() + user.delete() + self.stdout.write(self.style.SUCCESS(f' 已删除用户 {uid}')) + + if not apply: + self.stdout.write(self.style.WARNING( + '\n当前为预览。确认后执行:\n' + ' python manage.py cleanup_lsx_empty_club --apply\n' + '如需顺带删种子账号:\n' + ' python manage.py cleanup_lsx_empty_club --apply --remove-seed-users' + )) diff --git a/jituan/management/commands/seed_club_lsx.py b/jituan/management/commands/seed_club_lsx.py index c562865..761cbba 100644 --- a/jituan/management/commands/seed_club_lsx.py +++ b/jituan/management/commands/seed_club_lsx.py @@ -26,21 +26,23 @@ class Command(BaseCommand): club_id = 'lsx' name = '龙先生电竞' - if Club.query.filter(club_id=club_id).exists(): - self.stdout.write(self.style.WARNING(f'俱乐部 {club_id} 已存在,跳过 create_club')) - club = Club.query.filter(club_id=club_id).first() - if club and wx_appid and (club.wx_appid or '') != wx_appid: - club.wx_appid = wx_appid - club.save(update_fields=['wx_appid']) - self.stdout.write(f'已更新 {club_id}.wx_appid = {wx_appid}') + # 俱乐部应由后台「俱乐部配置」创建并上传证书;本命令不再自动 create_club + club = Club.query.filter(club_id=club_id).first() + if club is None: + self.stdout.write(self.style.ERROR( + f'俱乐部 {club_id} 不存在。请先在后台创建并上传支付 key,' + f'不要用脚本自动建俱乐部(避免生成无证书空配置)。' + )) + return + + if wx_appid and (club.wx_appid or '') != wx_appid: + club.wx_appid = wx_appid + club.save(update_fields=['wx_appid']) + self.stdout.write(f'已更新 {club_id}.wx_appid = {wx_appid}') else: - self.stdout.write(f'创建俱乐部 {club_id} ...') - call_command( - 'create_club', - club_id, - name=name, - wx_appid=wx_appid, - ) + self.stdout.write(self.style.SUCCESS( + f'俱乐部 {club_id} 已存在(name={club.name}),跳过创建。' + )) if options.get('assignments'): phone = (options.get('phone') or '').strip() @@ -61,6 +63,18 @@ class Command(BaseCommand): 'python manage.py seed_admin_assignments --phone 手机号 --club-id lsx' ) + keyed = any([ + (club.pay_key_path or '').strip(), + (club.pay_cert_path or '').strip(), + (club.private_key_path or '').strip(), + (club.platform_cert_dir or '').strip(), + ]) + if not keyed: + self.stdout.write(self.style.WARNING( + f'{club_id} 当前无证书路径,请在后台上传 key;' + f'若存在重复空俱乐部可用:python manage.py cleanup_lsx_empty_club' + )) + self.stdout.write(self.style.SUCCESS( - f'龙先生电竞 {club_id} 就绪。请在后台「俱乐部配置」检查支付密钥与 wx_appid。' + f'龙先生电竞 {club_id} 检查完成。' )) diff --git a/jituan/management/commands/seed_lsx_invite_data.py b/jituan/management/commands/seed_lsx_invite_data.py index 2ec1a1d..35cea23 100644 --- a/jituan/management/commands/seed_lsx_invite_data.py +++ b/jituan/management/commands/seed_lsx_invite_data.py @@ -1,5 +1,4 @@ """龙先生电竞(lsx)邀请链种子:自动插入组长+管事(含邀请码),无需先登录小程序。""" -from django.core.management import call_command from django.core.management.base import BaseCommand from django.db import transaction @@ -29,13 +28,12 @@ SEED_GUANSHI = { def _ensure_club(): + """仅检查俱乐部是否已由后台创建;不自动建空俱乐部。""" if Club.query.filter(club_id=CLUB_ID).exists(): return - call_command( - 'create_club', - CLUB_ID, - name='龙先生电竞', - wx_appid=WX_APPID, + raise SystemExit( + f'俱乐部 {CLUB_ID} 不存在。请先在后台创建龙先生俱乐部并上传证书,' + f'再执行 seed_lsx_invite_data(本命令不再自动 create_club)。' )