fix: 禁止过期充值单补开通 + 管理命令收回错误恢复会员
rollback_erroneous_member_restore:后台已减仍有效、充值单已过期仍显示有效。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
117
jituan/management/commands/rollback_erroneous_member_restore.py
Normal file
117
jituan/management/commands/rollback_erroneous_member_restore.py
Normal file
@@ -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} 条')
|
||||||
@@ -634,6 +634,60 @@ def _czjilu_entitlement_revoked(order):
|
|||||||
return ENTITLEMENT_REVOKED_MARKER in (getattr(order, 'shuoming', None) or '')
|
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):
|
def repair_member_entitlement_if_paid(dingdan_id):
|
||||||
order = Czjilu.query.filter(dingdan_id=dingdan_id, leixing=MEMBER_LEIXING).first()
|
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:
|
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:
|
if days <= 0:
|
||||||
_, _, days = club_huiyuan_sellable(club_id, order.huiyuan_id, is_trial=is_trial)
|
_, _, days = club_huiyuan_sellable(club_id, order.huiyuan_id, is_trial=is_trial)
|
||||||
days = max(int(days or 0), 1)
|
days = max(int(days or 0), 1)
|
||||||
|
if not czjilu_entitlement_active(order, club_id):
|
||||||
|
return False
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
_apply_huiyuan_entitlement(
|
_apply_huiyuan_entitlement(
|
||||||
order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, days,
|
order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, days,
|
||||||
|
|||||||
Reference in New Issue
Block a user