feat: credit-based billing system
- New DB models: credit_packages, subscription_plans, user_credits, credit_consumptions, credit_purchases - CreditService: balance, deduct, add_credits, grant_free_trial, history - User API: /api/v1/credits/* (balance/history/packages/purchase/subscribe) - Admin API: /api/v1/admin/credit-* (CRUD packages/plans, user credits, consumptions) - PaymentService.create_credit_order + handle_callback for credit purchases - Credit deduction on: discovery, translate, marketing, ai_chat, followup - Free trial 30 credits on registration - Documentation: docs/CREDIT_SYSTEM.md
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
from app.services.credit import CreditService
|
||||
from app.services.payment import PaymentService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class PurchaseRequest(BaseModel):
|
||||
package_id: str
|
||||
pay_type: str = "alipay"
|
||||
|
||||
|
||||
class SubscribeRequest(BaseModel):
|
||||
plan_id: str
|
||||
pay_type: str = "alipay"
|
||||
|
||||
|
||||
@router.get("/balance")
|
||||
async def get_balance(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = CreditService(db)
|
||||
return await svc.get_balance(user_id)
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
async def get_history(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = CreditService(db)
|
||||
return await svc.get_history(user_id, page, size)
|
||||
|
||||
|
||||
@router.get("/packages")
|
||||
async def list_packages(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = CreditService(db)
|
||||
return await svc.get_packages()
|
||||
|
||||
|
||||
@router.get("/subscription-plans")
|
||||
async def list_subscription_plans(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = CreditService(db)
|
||||
return await svc.get_subscription_plans()
|
||||
|
||||
|
||||
@router.post("/purchase")
|
||||
async def purchase_package(
|
||||
req: PurchaseRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = CreditService(db)
|
||||
packages = await svc.get_packages()
|
||||
pkg = next((p for p in packages if p["id"] == req.package_id), None)
|
||||
if not pkg:
|
||||
raise HTTPException(status_code=404, detail="次数包不存在")
|
||||
|
||||
pay_svc = PaymentService(db)
|
||||
order = await pay_svc.create_credit_order(
|
||||
user_id=user_id,
|
||||
amount=pkg["price"],
|
||||
description=f"购买 {pkg['name']} ({pkg['credits']}次)",
|
||||
pay_type=req.pay_type,
|
||||
metadata={"credit_package_id": req.package_id, "credits": pkg["credits"]},
|
||||
)
|
||||
return order
|
||||
|
||||
|
||||
@router.post("/subscribe")
|
||||
async def subscribe_plan(
|
||||
req: SubscribeRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = CreditService(db)
|
||||
plans = await svc.get_subscription_plans()
|
||||
plan = next((p for p in plans if p["id"] == req.plan_id), None)
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="订阅套餐不存在")
|
||||
|
||||
pay_svc = PaymentService(db)
|
||||
order = await pay_svc.create_credit_order(
|
||||
user_id=user_id,
|
||||
amount=plan["price"],
|
||||
description=f"开通 {plan['name']} (每月{plan['credits_per_month']}次)",
|
||||
pay_type=req.pay_type,
|
||||
metadata={"subscription_plan_id": req.plan_id, "credits_per_month": plan["credits_per_month"]},
|
||||
)
|
||||
return order
|
||||
|
||||
|
||||
@router.post("/cancel-subscription")
|
||||
async def cancel_subscription(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
from app.models.user_credit import UserCredit
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db.execute(select(UserCredit).where(UserCredit.user_id == user_id))
|
||||
uc = result.scalar_one_or_none()
|
||||
if not uc or not uc.subscription_plan_id:
|
||||
raise HTTPException(status_code=400, detail="没有有效的订阅")
|
||||
|
||||
uc.subscription_auto_renew = False
|
||||
await db.flush()
|
||||
return {"success": True, "message": "已取消自动续费,当前订阅到期后不再续费"}
|
||||
Reference in New Issue
Block a user