feat: 按商户号彻底删除俱乐部及绑定配置 delete_club_by_mch_id
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
286
jituan/management/commands/delete_club_by_mch_id.py
Normal file
286
jituan/management/commands/delete_club_by_mch_id.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""按微信商户号彻底删除俱乐部及其绑定配置(不删业务订单/真实用户)。
|
||||
|
||||
用法:
|
||||
python manage.py delete_club_by_mch_id 17480975501
|
||||
python manage.py delete_club_by_mch_id 17480975501 --apply
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Q
|
||||
|
||||
from jituan.models import (
|
||||
AdminAssignment,
|
||||
Club,
|
||||
ClubDashouExamConfig,
|
||||
ClubDashouExamPass,
|
||||
ClubDashouExamQuestion,
|
||||
ClubDashouExamQuestionImage,
|
||||
ClubFadanFenhongLilv,
|
||||
ClubHuiyuanBundleInclude,
|
||||
ClubHuiyuanPrice,
|
||||
ClubKaoheChenghaoConfig,
|
||||
ClubLilubiao,
|
||||
ClubPaymentChannel,
|
||||
ClubShangpinLeixingConfig,
|
||||
ClubTixianQuota,
|
||||
ClubWithdrawConfig,
|
||||
IdentityTag,
|
||||
IdentityTagBind,
|
||||
PayChannelLedger,
|
||||
UserWxOpenid,
|
||||
)
|
||||
from jituan.services.club_cert_upload import club_wx_cert_root
|
||||
|
||||
try:
|
||||
from jituan.models import AdminAuditLog
|
||||
except Exception:
|
||||
AdminAuditLog = None
|
||||
|
||||
# 展示/运营配置(config app)
|
||||
try:
|
||||
from config.models import (
|
||||
ClubMiniappIcon,
|
||||
ClubPindao,
|
||||
ClubPindaoImage,
|
||||
Gonggao,
|
||||
Lunbo,
|
||||
MiniappScriptAutoReply,
|
||||
MiniappScriptScene,
|
||||
PopupPage,
|
||||
Szjilu,
|
||||
Tupianpeizhi,
|
||||
)
|
||||
except Exception: # pragma: no cover
|
||||
Lunbo = Gonggao = Tupianpeizhi = PopupPage = Szjilu = None
|
||||
ClubMiniappIcon = ClubPindao = ClubPindaoImage = None
|
||||
MiniappScriptScene = MiniappScriptAutoReply = None
|
||||
|
||||
def _delete_qs(model, club_id, stdout, label=None):
|
||||
if model is None:
|
||||
return 0
|
||||
name = label or model.__name__
|
||||
try:
|
||||
qs = model.query.filter(club_id=club_id)
|
||||
n = qs.count()
|
||||
if n:
|
||||
qs.delete()
|
||||
stdout.write(f' - {name}: 删除 {n} 条')
|
||||
else:
|
||||
stdout.write(f' - {name}: 0')
|
||||
return n
|
||||
except Exception as exc:
|
||||
stdout.write(f' - {name}: 跳过 ({exc})')
|
||||
return 0
|
||||
|
||||
|
||||
def _find_clubs_by_mch(mch_id: str):
|
||||
"""club.mch_id / withdraw_mch_id / payment_channel.mch_id / extra_json 命中。"""
|
||||
clubs = list(
|
||||
Club.query.filter(
|
||||
Q(mch_id=mch_id) | Q(withdraw_mch_id=mch_id)
|
||||
)
|
||||
)
|
||||
found_ids = {c.club_id for c in clubs}
|
||||
|
||||
for ch in ClubPaymentChannel.query.filter(mch_id=mch_id):
|
||||
if ch.club_id not in found_ids:
|
||||
c = Club.query.filter(club_id=ch.club_id).first()
|
||||
if c:
|
||||
clubs.append(c)
|
||||
found_ids.add(c.club_id)
|
||||
|
||||
# extra_json 里可能写 merchant_id
|
||||
for ch in ClubPaymentChannel.query.all().only('id', 'club_id', 'mch_id', 'extra_json'):
|
||||
extra = ch.extra_json or {}
|
||||
mid = str(extra.get('merchant_id') or extra.get('mch_id') or '').strip()
|
||||
if mid == mch_id and ch.club_id not in found_ids:
|
||||
c = Club.query.filter(club_id=ch.club_id).first()
|
||||
if c:
|
||||
clubs.append(c)
|
||||
found_ids.add(c.club_id)
|
||||
|
||||
return clubs
|
||||
|
||||
|
||||
def _purge_club(club: Club, stdout, style, apply: bool, remove_cert_dir: bool):
|
||||
cid = club.club_id
|
||||
stdout.write(style.WARNING(f'\n>>> 俱乐部 {cid} ({club.name})'))
|
||||
stdout.write(f' mch_id={club.mch_id or "(空)"} withdraw_mch_id={getattr(club, "withdraw_mch_id", "") or "(空)"}')
|
||||
stdout.write(f' wx_appid={club.wx_appid or "(空)"}')
|
||||
stdout.write(f' pay_key_path={club.pay_key_path or "(空)"}')
|
||||
stdout.write(f' pay_cert_path={club.pay_cert_path or "(空)"}')
|
||||
stdout.write(f' private_key_path={club.private_key_path or "(空)"}')
|
||||
|
||||
channel_n = ClubPaymentChannel.query.filter(club_id=cid).count()
|
||||
stdout.write(f' payment_channel 条数={channel_n}')
|
||||
|
||||
if not apply:
|
||||
stdout.write(' (dry-run,未删除)')
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
# jituan 配置表
|
||||
for model in (
|
||||
ClubPaymentChannel,
|
||||
ClubHuiyuanPrice,
|
||||
ClubHuiyuanBundleInclude,
|
||||
ClubLilubiao,
|
||||
ClubWithdrawConfig,
|
||||
ClubTixianQuota,
|
||||
ClubShangpinLeixingConfig,
|
||||
ClubKaoheChenghaoConfig,
|
||||
ClubFadanFenhongLilv,
|
||||
ClubDashouExamConfig,
|
||||
ClubDashouExamQuestionImage,
|
||||
ClubDashouExamQuestion,
|
||||
ClubDashouExamPass,
|
||||
IdentityTagBind,
|
||||
IdentityTag,
|
||||
PayChannelLedger,
|
||||
UserWxOpenid,
|
||||
AdminAssignment,
|
||||
AdminAuditLog,
|
||||
):
|
||||
_delete_qs(model, cid, stdout)
|
||||
|
||||
# 展示配置
|
||||
for model in (
|
||||
Lunbo, Gonggao, Tupianpeizhi, PopupPage, Szjilu,
|
||||
ClubMiniappIcon, ClubPindaoImage, ClubPindao,
|
||||
MiniappScriptScene, MiniappScriptAutoReply,
|
||||
):
|
||||
_delete_qs(model, cid, stdout)
|
||||
|
||||
# 兜底:扫一遍已知配置表(防 ORM 漏删)
|
||||
with connection.cursor() as cursor:
|
||||
tables = [
|
||||
'club_payment_channel',
|
||||
'club_huiyuan_price',
|
||||
'club_huiyuan_bundle_include',
|
||||
'club_lilubiao',
|
||||
'club_withdraw_config',
|
||||
'club_tixian_quota',
|
||||
'club_shangpin_leixing_config',
|
||||
'club_kaohe_chenghao_config',
|
||||
'club_fadan_fenhong_lilv',
|
||||
'club_dashou_exam_config',
|
||||
'club_dashou_exam_question',
|
||||
'club_dashou_exam_question_image',
|
||||
'club_dashou_exam_pass',
|
||||
'identity_tag_bind',
|
||||
'identity_tag',
|
||||
'pay_channel_ledger',
|
||||
'user_wx_openid',
|
||||
'admin_assignment',
|
||||
'admin_audit_log',
|
||||
'lunbo',
|
||||
'gonggao',
|
||||
'tupianpeizhi',
|
||||
'popup_page',
|
||||
'szjilu',
|
||||
'club_miniapp_icon',
|
||||
'club_pindao',
|
||||
'club_pindao_image',
|
||||
'miniapp_script_scene',
|
||||
'miniapp_script_auto_reply',
|
||||
'daily_income_stat',
|
||||
'daily_payout_stat',
|
||||
'platform_income_log',
|
||||
]
|
||||
for table in tables:
|
||||
try:
|
||||
cursor.execute(f'SELECT COUNT(*) FROM `{table}` WHERE club_id=%s', [cid])
|
||||
n = cursor.fetchone()[0]
|
||||
if n:
|
||||
cursor.execute(f'DELETE FROM `{table}` WHERE club_id=%s', [cid])
|
||||
stdout.write(f' - SQL {table}: 删除 {n} 条')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Club.query.filter(club_id=cid).delete()
|
||||
stdout.write(style.SUCCESS(f' - Club 主表已删除: {cid}'))
|
||||
|
||||
if remove_cert_dir:
|
||||
root = club_wx_cert_root()
|
||||
cert_dir = os.path.join(root, cid)
|
||||
if os.path.isdir(cert_dir):
|
||||
if apply:
|
||||
shutil.rmtree(cert_dir, ignore_errors=True)
|
||||
stdout.write(style.SUCCESS(f' - 已删除证书目录: {cert_dir}'))
|
||||
else:
|
||||
stdout.write(f' - 将删除证书目录: {cert_dir}')
|
||||
else:
|
||||
stdout.write(f' - 证书目录不存在: {cert_dir}')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '按商户号删除俱乐部及其全部绑定配置(默认 dry-run)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'mch_id',
|
||||
nargs='?',
|
||||
default='17480975501',
|
||||
help='微信商户号(默认 17480975501)',
|
||||
)
|
||||
parser.add_argument('--apply', action='store_true', help='真正删除')
|
||||
parser.add_argument(
|
||||
'--keep-cert-dir',
|
||||
action='store_true',
|
||||
help='不删磁盘上 wx_certs/{club_id} 目录',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
mch_id = str(options.get('mch_id') or '').strip()
|
||||
if not mch_id:
|
||||
raise CommandError('请提供商户号')
|
||||
apply = bool(options.get('apply'))
|
||||
remove_cert = not bool(options.get('keep_cert_dir'))
|
||||
|
||||
clubs = _find_clubs_by_mch(mch_id)
|
||||
# 也列出仅有通道命中、主表已无的 club_id
|
||||
orphan_channels = list(
|
||||
ClubPaymentChannel.query.filter(mch_id=mch_id).exclude(
|
||||
club_id__in=[c.club_id for c in clubs]
|
||||
)
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('=' * 60))
|
||||
self.stdout.write(f'目标商户号 mch_id = {mch_id}')
|
||||
self.stdout.write(f'命中俱乐部: {len(clubs)} 个')
|
||||
if not clubs and not orphan_channels:
|
||||
self.stdout.write(self.style.WARNING('未找到绑定该商户号的俱乐部/支付通道,无需删除。'))
|
||||
return
|
||||
|
||||
for club in clubs:
|
||||
_purge_club(club, self.stdout, self.style, apply, remove_cert)
|
||||
|
||||
if orphan_channels:
|
||||
self.stdout.write(self.style.WARNING('\n仅有支付通道、无 Club 主表的残留:'))
|
||||
for ch in orphan_channels:
|
||||
self.stdout.write(
|
||||
f' channel id={ch.id} club_id={ch.club_id} name={ch.name} mch_id={ch.mch_id}'
|
||||
)
|
||||
if apply:
|
||||
cid = ch.club_id
|
||||
with transaction.atomic():
|
||||
ClubPaymentChannel.query.filter(id=ch.id).delete()
|
||||
# 顺带清同 club_id 残留配置
|
||||
for model in (
|
||||
ClubPaymentChannel, PayChannelLedger, AdminAssignment, UserWxOpenid,
|
||||
):
|
||||
_delete_qs(model, cid, self.stdout)
|
||||
self.stdout.write(self.style.SUCCESS(f' 已删通道 id={ch.id}'))
|
||||
|
||||
if not apply:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
f'\n当前为预览。确认后执行:\n'
|
||||
f' python manage.py delete_club_by_mch_id {mch_id} --apply'
|
||||
))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS('\n删除完成。'))
|
||||
Reference in New Issue
Block a user