Files
Django/jituan/management/commands/rollback_erroneous_member_restore.py
XingQue c464c31140 fix: 加固会员回滚命令,有未过期充值单绝不收回
增加 repair 指纹校验;减会员后重新付费不再误收。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:20:33 +08:00

140 lines
5.4 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.
"""
收回因 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}')