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:
TradeMate Dev
2026-06-12 10:39:45 +08:00
parent 5d895ae12c
commit 2a107a42f3
21 changed files with 1528 additions and 33 deletions
+8
View File
@@ -19,6 +19,10 @@ from .search_provider import SearchProvider
from .discovery_record import DiscoveryRecord
from .ai_provider import AIProvider
from .payment_transaction import PaymentTransaction
from .credit_package import CreditPackage, SubscriptionPlan
from .user_credit import UserCredit
from .credit_consumption import CreditConsumption
from .credit_purchase import CreditPurchase
__all__ = [
"User", "Product",
@@ -37,4 +41,8 @@ __all__ = [
"DiscoveryRecord",
"AIProvider",
"PaymentTransaction",
"CreditPackage", "SubscriptionPlan",
"UserCredit",
"CreditConsumption",
"CreditPurchase",
]
+20
View File
@@ -0,0 +1,20 @@
from sqlalchemy import Column, String, Float, DateTime, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID, JSONB
from datetime import datetime
from app.database import Base
import uuid
class CreditConsumption(Base):
__tablename__ = "credit_consumptions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True)
result_type = Column(String(50), nullable=False)
reference_id = Column(UUID(as_uuid=True), nullable=True)
credits_change = Column(Float, nullable=False)
balance_after = Column(Float, nullable=False)
source = Column(String(30), nullable=False)
description = Column(String(500))
metadata_ = Column("metadata", JSONB)
created_at = Column(DateTime, default=datetime.utcnow)
+37
View File
@@ -0,0 +1,37 @@
from sqlalchemy import Column, String, Integer, Float, Boolean, DateTime
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime
from app.database import Base
import uuid
class CreditPackage(Base):
__tablename__ = "credit_packages"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(100), nullable=False)
name_en = Column(String(100), nullable=False)
credits = Column(Integer, nullable=False)
price = Column(Float, nullable=False)
price_usd = Column(Float)
original_price = Column(Float)
is_active = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class SubscriptionPlan(Base):
__tablename__ = "subscription_plans"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(100), nullable=False)
name_en = Column(String(100), nullable=False)
credits_per_month = Column(Integer, nullable=False)
price = Column(Float, nullable=False)
price_usd = Column(Float)
duration_days = Column(Integer, default=30)
is_active = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+22
View File
@@ -0,0 +1,22 @@
from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime
from app.database import Base
import uuid
class CreditPurchase(Base):
__tablename__ = "credit_purchases"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True)
package_id = Column(UUID(as_uuid=True), ForeignKey("credit_packages.id"), nullable=True)
subscription_plan_id = Column(UUID(as_uuid=True), ForeignKey("subscription_plans.id"), nullable=True)
credits = Column(Integer, nullable=False)
amount = Column(Float, nullable=False)
currency = Column(String(3), default="CNY")
payment_method = Column(String(20))
status = Column(String(20), default="pending")
payment_transaction_id = Column(UUID(as_uuid=True), ForeignKey("payment_transactions.id"), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
paid_at = Column(DateTime, nullable=True)
+26
View File
@@ -0,0 +1,26 @@
from sqlalchemy import Column, String, Float, Boolean, DateTime, ForeignKey, Integer, Date
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime
from app.database import Base
import uuid
class UserCredit(Base):
__tablename__ = "user_credits"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, unique=True, index=True)
balance = Column(Float, default=0)
total_purchased = Column(Float, default=0)
total_used = Column(Float, default=0)
subscription_plan_id = Column(UUID(as_uuid=True), ForeignKey("subscription_plans.id"), nullable=True)
subscription_expires_at = Column(DateTime, nullable=True)
subscription_auto_renew = Column(Boolean, default=False)
free_trial_used = Column(Boolean, default=False)
daily_translate_chars = Column(Integer, default=0)
daily_translate_date = Column(Date, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)