Files
Django/jituan/management/commands/delete_club_by_mch_id.py

428 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""仅清理「龙先生电竞」(club_id=lsx) 的配置;绝对不碰其他俱乐部。
安全规则(硬约束):
1. 只允许操作 club_id=lsx或显式传入且必须等于 lsx
2. 若商户号命中了非 lsx 俱乐部:只打印警告,绝不删除
3. 默认 dry-run / --precheck只列出将删内容与条数不写库
4. --apply 必须再带 --i-understand-lsx-only 才真正删除
用法:
# 预检(推荐先跑这个,把输出发给你确认)
python manage.py delete_club_by_mch_id 17480975501 --precheck
# 确认无误后再删(双确认)
python manage.py delete_club_by_mch_id 17480975501 --apply --i-understand-lsx-only
"""
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
try:
from config.models import (
ClubMiniappIcon,
ClubPindao,
ClubPindaoImage,
Gonggao,
Lunbo,
MiniappScriptAutoReply,
MiniappScriptScene,
PopupPage,
Szjilu,
Tupianpeizhi,
)
except Exception:
Lunbo = Gonggao = Tupianpeizhi = PopupPage = Szjilu = None
ClubMiniappIcon = ClubPindao = ClubPindaoImage = None
MiniappScriptScene = MiniappScriptAutoReply = None
# 本命令唯一允许删除的俱乐部
ALLOWED_CLUB_ID = 'lsx'
ALLOWED_CLUB_NAME_HINT = '龙先生'
# ORM 配置模型(均按 club_id 过滤)
ORM_MODELS = [
('支付通道 ClubPaymentChannel', ClubPaymentChannel),
('会员价 ClubHuiyuanPrice', ClubHuiyuanPrice),
('会员套餐 ClubHuiyuanBundleInclude', ClubHuiyuanBundleInclude),
('利率 ClubLilubiao', ClubLilubiao),
('提现配置 ClubWithdrawConfig', ClubWithdrawConfig),
('提现配额 ClubTixianQuota', ClubTixianQuota),
('商品类型上架 ClubShangpinLeixingConfig', ClubShangpinLeixingConfig),
('考核称号 ClubKaoheChenghaoConfig', ClubKaoheChenghaoConfig),
('发单分红利率 ClubFadanFenhongLilv', ClubFadanFenhongLilv),
('接单考试配置 ClubDashouExamConfig', ClubDashouExamConfig),
('接单考试题图 ClubDashouExamQuestionImage', ClubDashouExamQuestionImage),
('接单考试题 ClubDashouExamQuestion', ClubDashouExamQuestion),
('接单考试通过 ClubDashouExamPass', ClubDashouExamPass),
('身份标签绑定 IdentityTagBind', IdentityTagBind),
('身份标签 IdentityTag', IdentityTag),
('支付通道流水 PayChannelLedger', PayChannelLedger),
('OpenID绑定 UserWxOpenid', UserWxOpenid),
('客服任职 AdminAssignment', AdminAssignment),
('审计日志 AdminAuditLog', AdminAuditLog),
('轮播 Lunbo', Lunbo),
('公告 Gonggao', Gonggao),
('图片配置 Tupianpeizhi', Tupianpeizhi),
('弹窗页 PopupPage', PopupPage),
('收支记录配置 Szjilu', Szjilu),
('小程序图标 ClubMiniappIcon', ClubMiniappIcon),
('频道图 ClubPindaoImage', ClubPindaoImage),
('频道 ClubPindao', ClubPindao),
('话术场景 MiniappScriptScene', MiniappScriptScene),
('话术自动回复 MiniappScriptAutoReply', MiniappScriptAutoReply),
]
# SQL 兜底表(仅 DELETE WHERE club_id=%s绝不会无 club_id 条件)
SQL_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',
]
# 明确不删(业务数据)
NEVER_DELETE_NOTE = [
'User 用户主表 / 各身份扩展表(打手商家等)',
'订单 Order / 退款 / 处罚等业务流水',
'商品 Shangpin若 club_id=lsx 有商品也不会在本命令删;如需另开命令)',
'其他俱乐部 xq / xzj / ufo 等任何数据',
]
def _count_model(model, club_id):
if model is None:
return None
try:
return model.query.filter(club_id=club_id).count()
except Exception:
return None
def _assert_lsx_only(club_id: str):
cid = (club_id or '').strip()
if cid != ALLOWED_CLUB_ID:
raise CommandError(
f'安全拦截:本命令只允许操作 club_id={ALLOWED_CLUB_ID}(龙先生电竞),'
f'拒绝 club_id={cid!r}'
)
def _club_snapshot(club: Club):
return {
'club_id': club.club_id,
'name': club.name,
'status': club.status,
'wx_appid': club.wx_appid or '',
'official_appid': club.official_appid or '',
'official_secret_set': bool((club.official_secret or '').strip()),
'official_token_set': bool((club.official_token or '').strip()),
'encoding_aes_key_set': bool((club.encoding_aes_key or '').strip()),
'template_id': club.template_id or '',
'mch_id': club.mch_id or '',
'withdraw_mch_id': getattr(club, 'withdraw_mch_id', '') or '',
'pay_app_id': club.pay_app_id or '',
'pay_key_path': club.pay_key_path or '',
'pay_cert_path': club.pay_cert_path or '',
'private_key_path': club.private_key_path or '',
'platform_cert_dir': club.platform_cert_dir or '',
'goeasy_appkey_set': bool((club.goeasy_appkey or '').strip()),
'h5_domain': club.h5_domain or '',
'oss_prefix': club.oss_prefix or '',
'mini_pay_channel': getattr(club, 'mini_pay_channel', '') or '',
}
def _find_other_clubs_with_mch(mch_id: str, exclude_club_id: str):
"""商户号撞到其他俱乐部时列出,绝不删除它们。"""
others = list(
Club.query.filter(
Q(mch_id=mch_id) | Q(withdraw_mch_id=mch_id)
).exclude(club_id=exclude_club_id)
)
ch_club_ids = (
ClubPaymentChannel.query.filter(mch_id=mch_id)
.exclude(club_id=exclude_club_id)
.values_list('club_id', flat=True)
.distinct()
)
for cid in ch_club_ids:
if not any(c.club_id == cid for c in others):
c = Club.query.filter(club_id=cid).first()
if c:
others.append(c)
return others
def _precheck_report(stdout, style, club: Club, mch_id: str):
snap = _club_snapshot(club)
stdout.write(style.SUCCESS('=' * 64))
stdout.write(style.SUCCESS('【预检】仅针对龙先生电竞 club_id=lsx'))
stdout.write(style.SUCCESS('=' * 64))
stdout.write(f'请求商户号: {mch_id or "(未指定,仅按 lsx 清理)"}')
stdout.write('')
stdout.write('—— 俱乐部主表字段 ——')
for k, v in snap.items():
stdout.write(f' {k}: {v if v != "" else "(空)"}')
if mch_id:
club_mch = (club.mch_id or '').strip()
club_wm = (getattr(club, 'withdraw_mch_id', '') or '').strip()
ch_hit = ClubPaymentChannel.query.filter(club_id=ALLOWED_CLUB_ID, mch_id=mch_id).count()
matched = club_mch == mch_id or club_wm == mch_id or ch_hit > 0
if not matched:
stdout.write(style.WARNING(
f'\n注意lsx 当前 mch_id={club_mch or "(空)"} / withdraw={club_wm or "(空)"} / '
f'通道命中={ch_hit},与请求商户号 {mch_id} 不完全一致。'
f'本命令仍只清理 lsx不会动其他俱乐部。'
))
else:
stdout.write(style.SUCCESS(f'\n商户号匹配确认lsx 绑定了 {mch_id}'))
others = _find_other_clubs_with_mch(mch_id, ALLOWED_CLUB_ID)
if others:
stdout.write(style.ERROR(
f'\n!! 其他俱乐部也配置了商户号 {mch_id}(只警告,绝对不删):'
))
for o in others:
stdout.write(
f' - KEEP {o.club_id} ({o.name}) mch={o.mch_id} withdraw={getattr(o, "withdraw_mch_id", "")}'
)
else:
stdout.write('其他俱乐部未发现同商户号(安全)。')
stdout.write('\n—— 将删除的配置表(仅 club_id=lsx——')
total = 0
for label, model in ORM_MODELS:
n = _count_model(model, ALLOWED_CLUB_ID)
if n is None:
stdout.write(f' [跳过不可用] {label}')
continue
mark = '将删' if n else ''
stdout.write(f' [{mark}] {label}: {n}')
total += n or 0
stdout.write('\n—— SQL 兜底表计数(仅 WHERE club_id=lsx——')
with connection.cursor() as cursor:
for table in SQL_TABLES:
try:
cursor.execute(
f'SELECT COUNT(*) FROM `{table}` WHERE club_id=%s',
[ALLOWED_CLUB_ID],
)
n = cursor.fetchone()[0]
stdout.write(f' [{("将删" if n else "")}] {table}: {n}')
total += n
except Exception as exc:
stdout.write(f' [无此表/跳过] {table}: {exc}')
cert_dir = os.path.join(club_wx_cert_root(), ALLOWED_CLUB_ID)
stdout.write('\n—— 证书目录 ——')
if os.path.isdir(cert_dir):
file_n = 0
for _root, _dirs, files in os.walk(cert_dir):
file_n += len(files)
stdout.write(f' [将删目录] {cert_dir} (约 {file_n} 个文件)')
else:
stdout.write(f' [无目录] {cert_dir}')
stdout.write('\n—— 明确不删 ——')
for tip in NEVER_DELETE_NOTE:
stdout.write(f' · {tip}')
stdout.write('')
stdout.write(style.SUCCESS(f'预检合计(配置行粗略合计,含重复计数可能):约 {total}'))
stdout.write(style.WARNING(
'确认无误后再执行:\n'
f' python manage.py delete_club_by_mch_id {mch_id or "17480975501"} '
'--apply --i-understand-lsx-only'
))
stdout.write(style.SUCCESS('=' * 64))
def _delete_lsx(stdout, style, remove_cert_dir: bool):
_assert_lsx_only(ALLOWED_CLUB_ID)
club = Club.query.filter(club_id=ALLOWED_CLUB_ID).first()
if not club:
raise CommandError(f'俱乐部 {ALLOWED_CLUB_ID} 不存在,无需删除')
# 名称再确认一层(非强制阻断,只警告)
if ALLOWED_CLUB_NAME_HINT not in (club.name or ''):
stdout.write(style.WARNING(
f'警告:{ALLOWED_CLUB_ID} 的 name={club.name!r} 不含「{ALLOWED_CLUB_NAME_HINT}」,'
f'但仍按 club_id=lsx 删除(主键约束)。'
))
with transaction.atomic():
for label, model in ORM_MODELS:
if model is None:
continue
n = model.query.filter(club_id=ALLOWED_CLUB_ID).count()
if n:
model.query.filter(club_id=ALLOWED_CLUB_ID).delete()
stdout.write(f' - 已删 {label}: {n}')
with connection.cursor() as cursor:
for table in SQL_TABLES:
try:
cursor.execute(
f'SELECT COUNT(*) FROM `{table}` WHERE club_id=%s',
[ALLOWED_CLUB_ID],
)
n = cursor.fetchone()[0]
if n:
cursor.execute(
f'DELETE FROM `{table}` WHERE club_id=%s',
[ALLOWED_CLUB_ID],
)
stdout.write(f' - SQL 已删 {table}: {n}')
except Exception:
pass
# 最后删主表(再次校验主键)
deleted, _ = Club.query.filter(club_id=ALLOWED_CLUB_ID).delete()
stdout.write(style.SUCCESS(f' - 已删 Club 主表 lsx (affected={deleted})'))
if remove_cert_dir:
cert_dir = os.path.join(club_wx_cert_root(), ALLOWED_CLUB_ID)
if os.path.isdir(cert_dir):
shutil.rmtree(cert_dir, ignore_errors=True)
stdout.write(style.SUCCESS(f' - 已删证书目录 {cert_dir}'))
class Command(BaseCommand):
help = '【仅 lsx/龙先生】按商户号预检/删除俱乐部配置;绝不碰其他俱乐部'
def add_arguments(self, parser):
parser.add_argument(
'mch_id',
nargs='?',
default='17480975501',
help='用于核对的商户号(默认 17480975501删除范围仍固定为 lsx',
)
parser.add_argument(
'--precheck',
action='store_true',
help='只预检列出将删内容(默认行为也是预检)',
)
parser.add_argument('--apply', action='store_true', help='真正删除(需配合确认开关)')
parser.add_argument(
'--i-understand-lsx-only',
action='store_true',
help='确认仅删除龙先生 lsx不涉及其他俱乐部',
)
parser.add_argument(
'--keep-cert-dir',
action='store_true',
help='保留磁盘 wx_certs/lsx 目录',
)
parser.add_argument(
'--club-id',
type=str,
default=ALLOWED_CLUB_ID,
help='必须为 lsx传其他值会直接拒绝',
)
def handle(self, *args, **options):
club_id = (options.get('club_id') or ALLOWED_CLUB_ID).strip()
_assert_lsx_only(club_id)
mch_id = str(options.get('mch_id') or '').strip()
apply = bool(options.get('apply'))
confirm = bool(options.get('i_understand_lsx_only'))
# 默认/显式 precheck 都走预检;只有 apply+确认才删除
do_delete = apply and confirm
club = Club.query.filter(club_id=ALLOWED_CLUB_ID).first()
if not club:
raise CommandError(
f'未找到 club_id={ALLOWED_CLUB_ID}。不会删除任何其他俱乐部。'
)
_precheck_report(self.stdout, self.style, club, mch_id)
if not do_delete:
if apply and not confirm:
self.stdout.write(self.style.ERROR(
'\n已拒绝删除:缺少 --i-understand-lsx-only防止误删'
))
self.stdout.write(self.style.WARNING('\n当前仅预检,数据库未改动。'))
return
# 删除前再拦一道:其他俱乐部同商户号只警告
others = _find_other_clubs_with_mch(mch_id, ALLOWED_CLUB_ID) if mch_id else []
if others:
self.stdout.write(self.style.ERROR(
'其他俱乐部存在同商户号配置,本命令仍只删 lsx下列俱乐部保持不动'
))
for o in others:
self.stdout.write(f' KEEP {o.club_id}')
self.stdout.write(self.style.WARNING('\n开始删除(仅 lsx...'))
_delete_lsx(
self.stdout,
self.style,
remove_cert_dir=not bool(options.get('keep_cert_dir')),
)
self.stdout.write(self.style.SUCCESS('\n完成:仅龙先生 lsx 配置已清理。'))