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:
@@ -143,6 +143,39 @@ class PaymentService:
|
||||
**gw_result,
|
||||
}
|
||||
|
||||
async def create_credit_order(self, user_id: str, amount: float,
|
||||
description: str, pay_type: str = "alipay",
|
||||
metadata: dict = None) -> Dict[str, Any]:
|
||||
order_no = gen_order_no(user_id)
|
||||
gw = get_gateway(pay_type)
|
||||
|
||||
meta_remark = {"uid": user_id, "oid": order_no, "type": "credit_purchase"}
|
||||
if metadata:
|
||||
meta_remark.update(metadata)
|
||||
|
||||
gw_result = await gw.create_order(order_no, int(amount * 100),
|
||||
description, pay_type=pay_type,
|
||||
remark=json.dumps(meta_remark, separators=(",", ":")))
|
||||
|
||||
txn = PaymentTransaction(
|
||||
user_id=user_id, order_no=order_no, plan="credit_purchase",
|
||||
amount=amount, gateway="unified", pay_type=pay_type,
|
||||
status="pending", description=json.dumps(metadata or {}, ensure_ascii=False),
|
||||
gateway_order_no=gw_result.get("gateway_order_id", ""),
|
||||
)
|
||||
self.db.add(txn)
|
||||
await self.db.flush()
|
||||
return {
|
||||
"status": "pending",
|
||||
"order_id": order_no,
|
||||
"amount": amount,
|
||||
"currency": "CNY",
|
||||
"gateway": "unified",
|
||||
"pay_type": pay_type,
|
||||
"metadata": metadata or {},
|
||||
**gw_result,
|
||||
}
|
||||
|
||||
async def handle_callback(self, order_no: str, gateway_order_id: str,
|
||||
gateway_order_no: str, success: bool,
|
||||
amount: float = 0, notify_raw: str = "") -> bool:
|
||||
@@ -162,30 +195,52 @@ class PaymentService:
|
||||
txn.paid_at = datetime.utcnow()
|
||||
txn.notify_raw = notify_raw
|
||||
|
||||
sub_result = await self.db.execute(
|
||||
select(Subscription).where(Subscription.payment_id == order_no)
|
||||
)
|
||||
sub = sub_result.scalar_one_or_none()
|
||||
if sub:
|
||||
sub.status = "active"
|
||||
sub.started_at = datetime.utcnow()
|
||||
if PLANS[sub.plan]["duration_days"]:
|
||||
sub.expires_at = datetime.utcnow() + timedelta(days=PLANS[sub.plan]["duration_days"])
|
||||
if txn.plan == "credit_purchase":
|
||||
from app.services.credit import CreditService
|
||||
credit_svc = CreditService(self.db)
|
||||
|
||||
user_result = await self.db.execute(select(User).where(User.id == txn.user_id))
|
||||
user = user_result.scalar_one_or_none()
|
||||
if user:
|
||||
user.tier = txn.plan
|
||||
if txn.description:
|
||||
try:
|
||||
meta = json.loads(txn.description)
|
||||
credits = meta.get("credits", 0)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
credits = 0
|
||||
else:
|
||||
credits = 0
|
||||
|
||||
if not credits:
|
||||
credits = max(1, int(txn.amount / 0.79))
|
||||
|
||||
await credit_svc.add_credits(
|
||||
txn.user_id, credits, "package",
|
||||
f"支付完成 - 获得 {credits} 次信用额度"
|
||||
)
|
||||
else:
|
||||
sub_result = await self.db.execute(
|
||||
select(Subscription).where(Subscription.payment_id == order_no)
|
||||
)
|
||||
sub = sub_result.scalar_one_or_none()
|
||||
if sub:
|
||||
sub.status = "active"
|
||||
sub.started_at = datetime.utcnow()
|
||||
if PLANS[sub.plan]["duration_days"]:
|
||||
sub.expires_at = datetime.utcnow() + timedelta(days=PLANS[sub.plan]["duration_days"])
|
||||
|
||||
user_result = await self.db.execute(select(User).where(User.id == txn.user_id))
|
||||
user = user_result.scalar_one_or_none()
|
||||
if user:
|
||||
user.tier = txn.plan
|
||||
else:
|
||||
txn.status = "failed"
|
||||
txn.notify_raw = notify_raw
|
||||
|
||||
sub_result = await self.db.execute(
|
||||
select(Subscription).where(Subscription.payment_id == order_no)
|
||||
)
|
||||
sub = sub_result.scalar_one_or_none()
|
||||
if sub:
|
||||
sub.status = "failed"
|
||||
if txn.plan != "credit_purchase":
|
||||
sub_result = await self.db.execute(
|
||||
select(Subscription).where(Subscription.payment_id == order_no)
|
||||
)
|
||||
sub = sub_result.scalar_one_or_none()
|
||||
if sub:
|
||||
sub.status = "failed"
|
||||
|
||||
await self.db.flush()
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user