Files
dealerhub/backend/apps/billing/tasks.py
T

82 lines
2.7 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.
"""订阅到期处理(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()