From 7c3f04e7f3d1e10b6afc758f94689f1b6e9c3cd3 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 8 Jul 2026 15:56:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A6=81=E6=AD=A2=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E5=85=85=E5=80=BC=E5=8D=95=E8=A1=A5=E5=BC=80=E9=80=9A=20+=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=BD=E4=BB=A4=E6=94=B6=E5=9B=9E=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=81=A2=E5=A4=8D=E4=BC=9A=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rollback_erroneous_member_restore:后台已减仍有效、充值单已过期仍显示有效。 Co-authored-by: Cursor --- .../rollback_erroneous_member_restore.py | 117 ++++++++++++++++++ jituan/services/member_recharge.py | 56 +++++++++ 2 files changed, 173 insertions(+) create mode 100644 jituan/management/commands/rollback_erroneous_member_restore.py diff --git a/jituan/management/commands/rollback_erroneous_member_restore.py b/jituan/management/commands/rollback_erroneous_member_restore.py new file mode 100644 index 0000000..ebe4bf5 --- /dev/null +++ b/jituan/management/commands/rollback_erroneous_member_restore.py @@ -0,0 +1,117 @@ +""" +收回因 dddhq 每次 sync 错误补开通的会员权益。 + +两类处理: +1. 后台「减会员」日志存在但权益仍有效 → 再次 revoke(删记录+标记充值单撤销) +2. 当前仍显示有效,但已无任何未过期充值单支撑 → 置为过期 + +用法: + python manage.py rollback_erroneous_member_restore --dry-run + python manage.py rollback_erroneous_member_restore +""" +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 ( + ENTITLEMENT_REVOKED_MARKER, + expire_huiyuangoumai_record, + revoke_member_entitlement_admin, + 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='仅统计,不写库', + ) + + def handle(self, *args, **options): + dry_run = bool(options.get('dry_run')) + admin_revoke = 0 + expire_orphan = 0 + samples = [] + + with transaction.atomic(): + # 1) 后台减会员日志 → 仍有效的再 revoke 一遍 + logs = Xiugaijilu.query.filter( + huiyuan_id__isnull=False, + ).exclude(huiyuan_id='').order_by('-CreateTime') + seen_pairs = set() + for log in logs: + note = (log.qitashuoming or '') + (log.huiyuan_id or '') + if '移除' not in (log.qitashuoming or '') and '减会员' not in (log.qitashuoming or ''): + 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) + + active = False + for rec in Huiyuangoumai.query.filter(yonghu_id=log.yonghuid): + if not huiyuan_ids_match(rec.huiyuan_id, log.huiyuan_id): + continue + if not rec.jiance_shifou_daoqi() and rec.huiyuan_zhuangtai == 1: + active = True + break + if not active: + continue + + admin_revoke += 1 + samples.append(f'[后台已减仍有效] {log.yonghuid} 会员{log.huiyuan_id}') + if not dry_run: + revoke_member_entitlement_admin(log.yonghuid, log.huiyuan_id) + + # 2) 仍显示有效,但没有任何未过期充值单支撑 → 过期收回 + for rec in Huiyuangoumai.query.filter(huiyuan_zhuangtai=1): + if rec.jiance_shifou_daoqi(): + continue + from products.models import Czjilu + from jituan.services.member_recharge import MEMBER_LEIXING, MEMBER_PAID_STATUS + + paid_orders = [ + o for o in Czjilu.query.filter( + yonghuid=rec.yonghu_id, + leixing=MEMBER_LEIXING, + zhuangtai=MEMBER_PAID_STATUS, + ) + if huiyuan_ids_match(o.huiyuan_id, rec.huiyuan_id) + ] + if not paid_orders: + continue + if user_has_active_czjilu_entitlement( + rec.yonghu_id, rec.huiyuan_id, rec.club_id, + ): + continue + expire_orphan += 1 + samples.append( + f'[充值单已过期仍有效] {rec.yonghu_id} 会员{rec.huiyuan_id} ' + f'到期{rec.daoqi_time}', + ) + 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} 条', + )) + for line in samples[:50]: + self.stdout.write(f' {line}') + if len(samples) > 50: + self.stdout.write(f' ... 另有 {len(samples) - 50} 条') diff --git a/jituan/services/member_recharge.py b/jituan/services/member_recharge.py index ea382c0..2ef2091 100644 --- a/jituan/services/member_recharge.py +++ b/jituan/services/member_recharge.py @@ -634,6 +634,60 @@ def _czjilu_entitlement_revoked(order): return ENTITLEMENT_REVOKED_MARKER in (getattr(order, 'shuoming', None) or '') +def _czjilu_entitlement_end(order, club_id=None): + """充值单对应权益的原定到期时刻(CreateTime + purchase_days)。""" + from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare + + cid = getattr(order, 'club_id', None) or club_id or CLUB_ID_DEFAULT + is_trial = _order_is_trial_like(order, cid) + days = int(getattr(order, 'purchase_days', None) or 0) + if days <= 0: + _, _, days = club_huiyuan_sellable(cid, order.huiyuan_id, is_trial=is_trial) + days = max(int(days or 0), 1) + start = _normalize_datetime_for_compare(order.CreateTime or timezone.now()) + return start + timedelta(days=days) + + +def czjilu_entitlement_active(order, club_id=None): + """充值单权益是否仍在原定期限内(未撤销且未过期)。""" + if not order or _czjilu_entitlement_revoked(order): + return False + from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare + now = _normalize_datetime_for_compare(timezone.now()) + return _czjilu_entitlement_end(order, club_id) > now + + +def user_has_active_czjilu_entitlement(yonghu_id, huiyuan_id, club_id=None): + """用户是否仍有未过期的已付充值单支撑该会员类型。""" + from jituan.services.huiyuan_bundle import huiyuan_ids_match + + for order in Czjilu.query.filter( + yonghuid=yonghu_id, + leixing=MEMBER_LEIXING, + zhuangtai=MEMBER_PAID_STATUS, + ).order_by('-CreateTime'): + if not huiyuan_ids_match(order.huiyuan_id, huiyuan_id): + continue + if czjilu_entitlement_active(order, club_id): + return True + return False + + +def expire_huiyuangoumai_record(rec, reason=''): + """将会员记录置为已过期(不删行,便于审计)。""" + from jituan.services.huiyuan_bundle import _normalize_datetime_for_compare + + now = _normalize_datetime_for_compare(timezone.now()) + rec.daoqi_time = now - timedelta(seconds=1) + rec.huiyuan_zhuangtai = 0 + rec.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai', 'UpdateTime']) + logger.warning( + '会员权益已收回 yonghu=%s huiyuan=%s reason=%s', + rec.yonghu_id, rec.huiyuan_id, reason or 'cleanup', + ) + return True + + def repair_member_entitlement_if_paid(dingdan_id): order = Czjilu.query.filter(dingdan_id=dingdan_id, leixing=MEMBER_LEIXING).first() if not order or order.zhuangtai != MEMBER_PAID_STATUS or not order.huiyuan_id: @@ -659,6 +713,8 @@ def repair_member_entitlement_if_paid(dingdan_id): if days <= 0: _, _, days = club_huiyuan_sellable(club_id, order.huiyuan_id, is_trial=is_trial) days = max(int(days or 0), 1) + if not czjilu_entitlement_active(order, club_id): + return False with transaction.atomic(): _apply_huiyuan_entitlement( order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, days,