"""订阅到期处理(Dramatiq actor + cron 每日调用)。""" from __future__ import annotations from datetime import date, timedelta import dramatiq from django.utils import timezone from .models import Plan, Subscription def expire_trials() -> dict: """扫描到期试用 → 降级 free 并把状态置为 expired,同时发通知。""" from django.db.models import Q from apps.notify.services import send_notification now = timezone.now() qs = Subscription.objects.filter( status=Subscription.STATUS_TRIAL, trial_ends_at__isnull=False, trial_ends_at__lt=now, ).select_related("tenant", "plan") free = Plan.objects.filter(code="free", is_active=True).first() downgraded = 0 for sub in qs: sub.status = Subscription.STATUS_EXPIRED if free is not None: sub.plan = free sub.save(update_fields=["status", "plan", "updated_at"]) try: send_notification( tenant=sub.tenant, title="【套餐】免费试用已到期", content=( "30 天全功能试用已结束,当前已回落至免费版配额" "(1 用户 / 100 商品 / 50 单据每月)。" "升级基础版 ¥998/月 可解除限制,历史数据全部保留。" ), category="warning", link="/#/pricing", extra_data={"kind": "trial_expired"}, ) except Exception: pass downgraded += 1 # 付费到期(未续费)→ 同样降级 today = date.today() paid_expired = Subscription.objects.filter( status=Subscription.STATUS_ACTIVE, period_end__isnull=False, period_end__lt=today, ).select_related("tenant", "plan") for sub in paid_expired: sub.status = Subscription.STATUS_EXPIRED if free is not None: sub.plan = free sub.save(update_fields=["status", "plan", "updated_at"]) try: send_notification( tenant=sub.tenant, title="【套餐】订阅已到期", content=f"套餐已于 {sub.period_end} 到期,当前回落至免费版配额,请及时续费。", category="warning", link="/#/pricing", extra_data={"kind": "subscription_expired"}, ) except Exception: pass downgraded += 1 return {"expired_count": downgraded, "checked_at": now.isoformat()} @dramatiq.actor(max_retries=2, time_limit=300_000) def expire_trials_and_notify(): """Dramatiq actor 包装(供 config/dramatiq.py 的 cron 调用/手动 send)。""" return expire_trials()