""" 收回因 dddhq 每次 sync 错误补开通的会员权益(保守策略,优先不误伤正常付费)。 安全规则(按优先级): 1. 有任意未过期已付充值单支撑 → 绝不收回 2. 无充值单的老数据 → 不自动处理 3. 默认要求符合 bug repair 指纹(UpdateTime 写回 + 旧单原定期限已过) 4. 后台减会员:仅当仍有效且无未过期充值单时才 revoke 用法: python manage.py rollback_erroneous_member_restore --dry-run python manage.py rollback_erroneous_member_restore python manage.py rollback_erroneous_member_restore --since 2026-06-01 """ from django.core.management.base import BaseCommand from django.db import transaction from jituan.services.huiyuan_bundle import huiyuan_ids_match from jituan.services.member_recharge import ( expire_huiyuangoumai_record, revoke_member_entitlement_admin, should_rollback_erroneous_membership, user_has_active_czjilu_entitlement, ) from products.models import Huiyuangoumai from users.models import Xiugaijilu class Command(BaseCommand): help = '收回错误恢复的会员(保守:有未过期充值单绝不碰)' def add_arguments(self, parser): parser.add_argument( '--dry-run', action='store_true', help='仅统计,不写库', ) parser.add_argument( '--since', default='2026-06-01', help='repair 指纹 UpdateTime 起始日(默认 2026-06-01)', ) parser.add_argument( '--no-repair-signature', action='store_true', help='不要求 repair 指纹(更激进,不推荐)', ) def handle(self, *args, **options): dry_run = bool(options.get('dry_run')) since = options.get('since') require_sig = not bool(options.get('no_repair_signature')) admin_revoke = 0 expire_orphan = 0 skipped_safe = 0 samples = [] with transaction.atomic(): seen_pairs = set() logs = Xiugaijilu.query.filter( huiyuan_id__isnull=False, ).exclude(huiyuan_id='').order_by('-CreateTime') for log in logs: note = log.qitashuoming or '' if '移除' not in note and '减会员' not in note: continue if not log.yonghuid or not log.huiyuan_id: continue pair = (log.yonghuid, str(log.huiyuan_id).strip()) if pair in seen_pairs: continue seen_pairs.add(pair) for rec in Huiyuangoumai.query.filter(yonghu_id=log.yonghuid): if not huiyuan_ids_match(rec.huiyuan_id, log.huiyuan_id): continue if rec.jiance_shifou_daoqi() or rec.huiyuan_zhuangtai != 1: continue if user_has_active_czjilu_entitlement( rec.yonghu_id, rec.huiyuan_id, rec.club_id, ): skipped_safe += 1 continue ok, detail = should_rollback_erroneous_membership( rec, since=since, require_repair_signature=require_sig, ) if not ok: skipped_safe += 1 continue admin_revoke += 1 samples.append( f'[后台已减仍有效] {log.yonghuid} 会员{log.huiyuan_id} ' f'依据单{detail or "-"}', ) if not dry_run: revoke_member_entitlement_admin(log.yonghuid, log.huiyuan_id) break for rec in Huiyuangoumai.query.filter(huiyuan_zhuangtai=1): if rec.jiance_shifou_daoqi(): continue ok, detail = should_rollback_erroneous_membership( rec, since=since, require_repair_signature=require_sig, ) if not ok: if detail == '有未过期充值单支撑': skipped_safe += 1 continue expire_orphan += 1 samples.append( f'[充值单已过期仍有效] {rec.yonghu_id} 会员{rec.huiyuan_id} ' f'到期{rec.daoqi_time} 依据单{detail or "-"}', ) if not dry_run: expire_huiyuangoumai_record( rec, reason='rollback_erroneous_member_restore', ) if dry_run: transaction.set_rollback(True) mode = 'DRY-RUN' if dry_run else 'APPLIED' self.stdout.write(self.style.SUCCESS( f'[{mode}] 后台减会员仍有效收回 {admin_revoke} 条;' f'无有效充值单支撑过期 {expire_orphan} 条;' f'安全跳过 {skipped_safe} 条(含正常付费/不符合指纹)', )) if require_sig: self.stdout.write(' 已启用 repair 指纹校验(推荐)') else: self.stdout.write(self.style.WARNING(' 未启用 repair 指纹(激进模式,慎用)')) for line in samples[:50]: self.stdout.write(f' {line}') if len(samples) > 50: self.stdout.write(f' ... 另有 {len(samples) - 50} 条')