Files

111 lines
3.6 KiB
Python
Raw Permalink 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.
"""套餐与计费 API(批次 C1)。
- GET /api/v1/billing/plans/ 套餐列表(官网价格页数据源)
- GET /api/v1/billing/subscription/ 我的套餐 + 用量
- POST /api/v1/billing/subscribe/ 变更套餐(真实支付接入后由回调驱动)
"""
from asgiref.sync import sync_to_async
from adrf.decorators import api_view
from rest_framework import status
from rest_framework.decorators import authentication_classes, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from apps.core.viewset import resolve_tenant
from .models import Plan, Subscription, upgrade, get_subscription, seed_plans
from . import quota as billing_quota
async def _tenant(request):
code = request.META.get("HTTP_X_TENANT_ID", "")
tenant = await sync_to_async(resolve_tenant)(code)
if tenant is None:
raise ValidationError({"tenant": "无法识别租户"})
return tenant
@api_view(["GET"])
@authentication_classes([])
@permission_classes([AllowAny])
async def plan_list(request):
"""套餐列表(价格 + 功能对比)。公开:官网价格页/登录页都需要。"""
def _load():
seed_plans()
return list(Plan.objects.filter(is_active=True).order_by("sort_order", "code"))
plans = await sync_to_async(_load)()
return Response([
{
"code": p.code,
"name": p.name,
"price_monthly": str(p.price_monthly),
"description": p.description,
"limits": p.limits,
}
for p in plans
])
@api_view(["GET"])
async def my_subscription(request):
"""我的套餐状态 + 各项用量(升级引导弹窗数据源)。"""
tenant = await _tenant(request)
snap = await sync_to_async(billing_quota.usage_snapshot)(tenant)
return Response(snap)
@api_view(["POST"])
async def change_plan(request):
"""变更套餐:POST {plan_code, months?}。
真实支付接入前:直接切换(演示/内部开通用);接入支付后应改为
"创建订单 → 支付回调 → upgrade",此接口仅保留给管理员。
"""
tenant = await _tenant(request)
payload = request.data or {}
plan_code = (payload.get("plan_code") or "").strip()
if not plan_code:
raise ValidationError({"detail": "plan_code 必填"})
months = int(payload.get("months") or 1)
def _do():
sub = upgrade(tenant, plan_code, months=months)
return sub
try:
sub = await sync_to_async(_do)()
except ValueError as exc:
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
return Response({
"ok": True,
"plan_code": sub.plan.code,
"plan_name": sub.plan.name,
"status": sub.status,
"period_end": sub.period_end.isoformat() if sub.period_end else None,
})
@api_view(["GET"])
async def quota_check(request):
"""预检某配额项:?kind=products(前端按钮置灰/拦截提示用)。
语义是"还能不能再加一个":delta=1,因此 used 已达上限即返回 403。
"""
tenant = await _tenant(request)
kind = request.query_params.get("kind") or ""
if not kind:
raise ValidationError({"detail": "kind 必填(products/bills_monthly/ai_parse_order/...)"})
def _check():
return billing_quota.check_and_count(tenant, kind, delta=1)
try:
out = await sync_to_async(_check)()
except billing_quota.QuotaExceeded as exc:
return Response(exc.as_dict(), status=status.HTTP_403_FORBIDDEN)
return Response(out)