fix: 不再自动创建 lsx 空俱乐部;新增按无 key 路径清理重复配置
cleanup_lsx_empty_club 只删无证书路径的龙先生相关俱乐部,保留已上传 key 的配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
188
jituan/management/commands/cleanup_lsx_empty_club.py
Normal file
188
jituan/management/commands/cleanup_lsx_empty_club.py
Normal file
@@ -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'
|
||||
))
|
||||
@@ -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} 检查完成。'
|
||||
))
|
||||
|
||||
@@ -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)。'
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user