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,101 @@
|
||||
"""add credit system tables (packages, plans, user credits, consumptions, purchases)
|
||||
|
||||
Revision ID: add_credit_system
|
||||
Revises: add_perf_indexes
|
||||
Create Date: 2026-06-12
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision = "add_credit_system"
|
||||
down_revision = "add_perf_indexes"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"credit_packages",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("name_en", sa.String(100), nullable=False),
|
||||
sa.Column("credits", sa.Integer, nullable=False),
|
||||
sa.Column("price", sa.Float, nullable=False),
|
||||
sa.Column("price_usd", sa.Float, nullable=True),
|
||||
sa.Column("original_price", sa.Float, nullable=True),
|
||||
sa.Column("is_active", sa.Boolean, default=True),
|
||||
sa.Column("sort_order", sa.Integer, default=0),
|
||||
sa.Column("created_at", sa.DateTime, default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, default=sa.func.now()),
|
||||
)
|
||||
op.create_table(
|
||||
"subscription_plans",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("name_en", sa.String(100), nullable=False),
|
||||
sa.Column("credits_per_month", sa.Integer, nullable=False),
|
||||
sa.Column("price", sa.Float, nullable=False),
|
||||
sa.Column("price_usd", sa.Float, nullable=True),
|
||||
sa.Column("duration_days", sa.Integer, default=30),
|
||||
sa.Column("is_active", sa.Boolean, default=True),
|
||||
sa.Column("sort_order", sa.Integer, default=0),
|
||||
sa.Column("created_at", sa.DateTime, default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, default=sa.func.now()),
|
||||
)
|
||||
op.create_table(
|
||||
"user_credits",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False, unique=True, index=True),
|
||||
sa.Column("balance", sa.Float, default=0),
|
||||
sa.Column("total_purchased", sa.Float, default=0),
|
||||
sa.Column("total_used", sa.Float, default=0),
|
||||
sa.Column("subscription_plan_id", UUID(as_uuid=True), sa.ForeignKey("subscription_plans.id"), nullable=True),
|
||||
sa.Column("subscription_expires_at", sa.DateTime, nullable=True),
|
||||
sa.Column("subscription_auto_renew", sa.Boolean, default=False),
|
||||
sa.Column("free_trial_used", sa.Boolean, default=False),
|
||||
sa.Column("daily_translate_chars", sa.Integer, default=0),
|
||||
sa.Column("daily_translate_date", sa.Date, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, default=sa.func.now()),
|
||||
)
|
||||
op.create_table(
|
||||
"credit_consumptions",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False, index=True),
|
||||
sa.Column("result_type", sa.String(50), nullable=False),
|
||||
sa.Column("reference_id", UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("credits_change", sa.Float, nullable=False),
|
||||
sa.Column("balance_after", sa.Float, nullable=False),
|
||||
sa.Column("source", sa.String(30), nullable=False),
|
||||
sa.Column("description", sa.String(500), nullable=True),
|
||||
sa.Column("metadata", JSONB, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, default=sa.func.now()),
|
||||
)
|
||||
op.create_index("idx_credit_consumptions_user", "credit_consumptions", ["user_id", sa.text("created_at DESC")])
|
||||
op.create_index("idx_credit_consumptions_type", "credit_consumptions", ["result_type"])
|
||||
op.create_table(
|
||||
"credit_purchases",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False, index=True),
|
||||
sa.Column("package_id", UUID(as_uuid=True), sa.ForeignKey("credit_packages.id"), nullable=True),
|
||||
sa.Column("subscription_plan_id", UUID(as_uuid=True), sa.ForeignKey("subscription_plans.id"), nullable=True),
|
||||
sa.Column("credits", sa.Integer, nullable=False),
|
||||
sa.Column("amount", sa.Float, nullable=False),
|
||||
sa.Column("currency", sa.String(3), default="CNY"),
|
||||
sa.Column("payment_method", sa.String(20), nullable=True),
|
||||
sa.Column("status", sa.String(20), default="pending"),
|
||||
sa.Column("payment_transaction_id", UUID(as_uuid=True), sa.ForeignKey("payment_transactions.id"), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, default=sa.func.now()),
|
||||
sa.Column("paid_at", sa.DateTime, nullable=True),
|
||||
)
|
||||
op.create_index("idx_credit_purchases_user", "credit_purchases", ["user_id"])
|
||||
op.create_index("idx_credit_purchases_status", "credit_purchases", ["status"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("credit_purchases")
|
||||
op.drop_table("credit_consumptions")
|
||||
op.drop_table("user_credits")
|
||||
op.drop_table("subscription_plans")
|
||||
op.drop_table("credit_packages")
|
||||
Reference in New Issue
Block a user