1
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
from app.models.base import Base, TimestampMixin, engine, async_session, init_database, close_database
|
||||
from app.models.user import User
|
||||
from app.models.project import Project
|
||||
from app.models.generation_record import GenerationRecord
|
||||
from app.models.credit_record import CreditRecord
|
||||
from app.models.model_config import ModelConfig
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.models.notification import Notification
|
||||
from app.models.notification_read import NotificationRead
|
||||
from app.models.payment_order import PaymentOrder
|
||||
from app.models.token_usage import TokenUsage
|
||||
from app.models.industry_config import IndustryConfig
|
||||
from app.models.video_engine import VideoEngine
|
||||
from app.models.credit_ratio import CreditRatio
|
||||
from app.models.menu_config import MenuConfig
|
||||
from app.models.recharge_package import RechargePackage
|
||||
from app.models.operation_log import OperationLog
|
||||
|
||||
__all__ = [
|
||||
"Base", "TimestampMixin", "engine", "async_session",
|
||||
"init_database", "close_database",
|
||||
"User", "Project", "GenerationRecord", "CreditRecord",
|
||||
"ModelConfig", "SystemConfig", "Notification", "PaymentOrder",
|
||||
"TokenUsage", "IndustryConfig", "VideoEngine", "CreditRatio",
|
||||
"MenuConfig", "RechargePackage", "OperationLog",
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, func
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
from app.config import settings
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG)
|
||||
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
async def init_database() -> None:
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def close_database() -> None:
|
||||
await engine.dispose()
|
||||
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class CreditRatio(Base, TimestampMixin):
|
||||
__tablename__ = "credit_ratios"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
model_config_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("model_configs.id")
|
||||
)
|
||||
gen_type: Mapped[str] = mapped_column(String(16), default="video")
|
||||
resolution: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
ratio: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
base_credits: Mapped[float] = mapped_column(Float, default=80.0)
|
||||
per_second_credits: Mapped[float] = mapped_column(Float, default=2.0)
|
||||
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class CreditRecord(Base, TimestampMixin):
|
||||
__tablename__ = "credit_records"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
type: Mapped[str] = mapped_column(String(16))
|
||||
amount: Mapped[float] = mapped_column(Float)
|
||||
balance_after: Mapped[float] = mapped_column(Float)
|
||||
description: Mapped[str] = mapped_column(String(256))
|
||||
related_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
@@ -0,0 +1,46 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class GenerationRecord(Base, TimestampMixin):
|
||||
__tablename__ = "generation_records"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
project_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("projects.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
original_prompt: Mapped[str] = mapped_column(Text)
|
||||
optimized_prompt: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
gen_type: Mapped[str] = mapped_column(String(16), default="video")
|
||||
duration: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
aspect_ratio: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
||||
resolution: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
||||
image_size: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
||||
image_proportion: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
||||
image_px: Mapped[str | None] = mapped_column(String(10), nullable=True)
|
||||
|
||||
status: Mapped[str] = mapped_column(String(32), default="prompt_optimized")
|
||||
video_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
image_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
media_references: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
video_url_expires_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
seedance_task_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
credits_cost: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
text_credits_cost: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
text_tokens_used: Mapped[int] = mapped_column(Integer, default=0)
|
||||
video_tokens_used: Mapped[int] = mapped_column(Integer, default=0)
|
||||
image_tokens_used: Mapped[int] = mapped_column(Integer, default=0)
|
||||
generated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
idempotency_key: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||||
@@ -0,0 +1,22 @@
|
||||
from sqlalchemy import Boolean, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class ImageEngine(Base, TimestampMixin):
|
||||
__tablename__ = "image_engines"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
api_base: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
api_key: Mapped[str] = mapped_column(String(256), default="")
|
||||
model_name: Mapped[str] = mapped_column(String(128), default="")
|
||||
supported_models: Mapped[str] = mapped_column(Text, default='["doubao-seedream-5-0-260128"]')
|
||||
# {"2K":{"1:1":"2048×2048",...}, "4K":{"1:1":"4096×4096",...}}
|
||||
supported_sizes: Mapped[str] = mapped_column(Text, default='{}')
|
||||
default_size: Mapped[str] = mapped_column(String(32), default="2K")
|
||||
generate_url: Mapped[str | None] = mapped_column(String(512), nullable=True, default="")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
priority: Mapped[int] = mapped_column(Integer, default=0)
|
||||
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Boolean, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class IndustryConfig(Base, TimestampMixin):
|
||||
__tablename__ = "industry_configs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
key: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
||||
label: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
icon: Mapped[str] = mapped_column(String(64), default="")
|
||||
description: Mapped[str] = mapped_column(String(256), default="")
|
||||
skills: Mapped[str] = mapped_column(Text, default="[]")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import Boolean, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class MenuConfig(Base, TimestampMixin):
|
||||
__tablename__ = "menu_configs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
label: Mapped[str] = mapped_column(String(64))
|
||||
path: Mapped[str] = mapped_column(String(128), default="")
|
||||
icon: Mapped[str] = mapped_column(String(64), default="")
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
parent_id: Mapped[str | None] = mapped_column(String(32), ForeignKey("menu_configs.id"), nullable=True)
|
||||
menu_type: Mapped[str] = mapped_column(String(16), default="page") # page / group
|
||||
menu_target: Mapped[str] = mapped_column(String(16), default="frontend") # frontend / admin / both
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False) # default show for new users
|
||||
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy import Boolean, Float, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class ModelConfig(Base, TimestampMixin):
|
||||
__tablename__ = "model_configs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(64))
|
||||
provider: Mapped[str] = mapped_column(String(32))
|
||||
api_base: Mapped[str] = mapped_column(String(512))
|
||||
api_key: Mapped[str] = mapped_column(String(256))
|
||||
model_name: Mapped[str] = mapped_column(String(128))
|
||||
weight: Mapped[int] = mapped_column(Integer, default=1)
|
||||
max_tokens: Mapped[int] = mapped_column(Integer, default=4096)
|
||||
temperature: Mapped[float] = mapped_column(Float, default=0.7)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
priority: Mapped[int] = mapped_column(Integer, default=0)
|
||||
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Boolean, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class Notification(Base, TimestampMixin):
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=True, index=True
|
||||
)
|
||||
title: Mapped[str] = mapped_column(String(128))
|
||||
content: Mapped[str] = mapped_column(Text)
|
||||
type: Mapped[str] = mapped_column(String(32), default="system")
|
||||
is_read: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
related_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy import String, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class NotificationRead(Base, TimestampMixin):
|
||||
__tablename__ = "notification_reads"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
notification_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("notifications.id"), index=True
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(String(32), index=True)
|
||||
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class OperationLog(Base, TimestampMixin):
|
||||
__tablename__ = "operation_logs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(String(32), index=True)
|
||||
username: Mapped[str] = mapped_column(String(64))
|
||||
action: Mapped[str] = mapped_column(String(128)) # e.g. "创建用户", "修改菜单"
|
||||
method: Mapped[str] = mapped_column(String(10)) # POST/PUT/DELETE
|
||||
path: Mapped[str] = mapped_column(String(256))
|
||||
detail: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON detail
|
||||
ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
@@ -0,0 +1,24 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class PaymentOrder(Base, TimestampMixin):
|
||||
__tablename__ = "payment_orders"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
order_no: Mapped[str] = mapped_column(String(64), unique=True)
|
||||
amount: Mapped[float] = mapped_column(Float)
|
||||
credits: Mapped[float] = mapped_column(Float)
|
||||
payment_method: Mapped[str] = mapped_column(String(16))
|
||||
status: Mapped[str] = mapped_column(String(16), default="pending")
|
||||
paid_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
trade_no: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class Project(Base, TimestampMixin):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(128))
|
||||
industry: Mapped[str] = mapped_column(String(32))
|
||||
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import Boolean, Integer, String, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class RechargePackage(Base, TimestampMixin):
|
||||
__tablename__ = "recharge_packages"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(64))
|
||||
credits: Mapped[float] = mapped_column(Float)
|
||||
price: Mapped[float] = mapped_column(Float)
|
||||
bonus_credits: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
description: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
package_type: Mapped[str] = mapped_column(String(32), default="normal")
|
||||
is_gift: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class SystemConfig(Base, TimestampMixin):
|
||||
__tablename__ = "system_configs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
key: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
value: Mapped[str] = mapped_column(Text)
|
||||
description: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class TokenUsage(Base, TimestampMixin):
|
||||
__tablename__ = "token_usage"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
model_config_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("model_configs.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
user_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
)
|
||||
input_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||
output_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||
total_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||
@@ -0,0 +1,25 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Float, Integer, String, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class User(Base, TimestampMixin):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
email: Mapped[str | None] = mapped_column(String(255), unique=True, nullable=True)
|
||||
phone: Mapped[str | None] = mapped_column(String(20), unique=True, nullable=True)
|
||||
hashed_password: Mapped[str] = mapped_column(String(128))
|
||||
avatar: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
credits: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
user_type: Mapped[str] = mapped_column(String(16), default="frontend")
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
allowed_menus: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import Boolean, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class VideoEngine(Base, TimestampMixin):
|
||||
__tablename__ = "video_engines"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
provider: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
api_base: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
api_key: Mapped[str] = mapped_column(String(256), default="")
|
||||
model_name: Mapped[str] = mapped_column(String(128), default="")
|
||||
supported_ratios: Mapped[str] = mapped_column(String(256), default='["16:9","4:3","1:1","3:4","9:16","21:9"]')
|
||||
supported_resolutions: Mapped[str] = mapped_column(String(256), default='["480p","720p","1080p"]')
|
||||
supported_durations: Mapped[str] = mapped_column(String(256), nullable=True, default='[4,5,6,7,8,9,10,11,12,13,14,15]')
|
||||
max_duration: Mapped[int] = mapped_column(Integer, default=15)
|
||||
generate_url: Mapped[str | None] = mapped_column(String(512), nullable=True, default="")
|
||||
query_url: Mapped[str | None] = mapped_column(String(512), nullable=True, default="")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
priority: Mapped[int] = mapped_column(Integer, default=0)
|
||||
Reference in New Issue
Block a user