Files
trade-assistant/backend/app/api/v1/credits.py
T

179 lines
5.4 KiB
Python

import json
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 StripePurchaseRequest(BaseModel):
package_id: str
gateway: str = "stripe"
success_url: str = "https://trade.yuzhiran.com/workspace/credits?pay=success"
cancel_url: str = "https://trade.yuzhiran.com/workspace/credits?pay=cancel"
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("/stripe-purchase")
async def stripe_purchase(
req: StripePurchaseRequest,
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="次数包不存在")
from app.services.payment import get_gateway, gen_order_no
price_usd = pkg.get("price_usd") or round(pkg["price"] / 7, 2)
amount_cents = int(price_usd * 100)
order_no = gen_order_no(user_id)
gw = get_gateway(req.gateway)
sep = '&' if '?' in req.success_url else '?'
success_url = f"{req.success_url}{sep}order_id={order_no}"
gw_result = await gw.create_order(
order_no, amount_cents, f"{pkg['name_en']} ({pkg['credits']} credits)",
pay_type=req.gateway,
success_url=success_url,
cancel_url=req.cancel_url,
)
from app.models.payment_transaction import PaymentTransaction
txn = PaymentTransaction(
user_id=user_id, order_no=order_no, plan="credit_purchase",
amount=price_usd, gateway=req.gateway, pay_type=req.gateway,
status="pending", description=json.dumps({"credits": pkg["credits"]}),
gateway_order_no=gw_result.get("session_id", ""),
)
db.add(txn)
await db.flush()
return {
"status": "pending",
"order_id": order_no,
"session_url": gw_result.get("session_url"),
"session_id": gw_result.get("session_id"),
"amount": price_usd,
"currency": "USD",
"gateway": req.gateway,
}
@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": "已取消自动续费,当前订阅到期后不再续费"}