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