1
0

Add complete ai.card Rust implementation

- Implement complete Rust API server with axum framework
- Add database abstraction supporting PostgreSQL and SQLite
- Implement comprehensive gacha system with probability calculations
- Add JWT authentication with atproto DID integration
- Create card master data system with rarities (Normal, Rare, SuperRare, Kira, Unique)
- Implement draw history tracking and collection management
- Add API endpoints for authentication, card drawing, and collection viewing
- Include database migrations for both PostgreSQL and SQLite
- Maintain full compatibility with Python API implementation
- Add comprehensive documentation and development guide

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-06-07 17:43:10 +09:00
parent ef907660cc
commit 0b34568585
57 changed files with 3469 additions and 422 deletions

View File

@@ -0,0 +1 @@
# Database Package

53
python/api/app/db/base.py Normal file
View File

@@ -0,0 +1,53 @@
"""Database base configuration"""
import os
from pathlib import Path
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings
# Create base class for models
Base = declarative_base()
# Ensure database directory exists
db_path = Path.home() / ".config" / "syui" / "ai" / "card"
db_path.mkdir(parents=True, exist_ok=True)
# Select database URL based on configuration
database_url = settings.database_url_supabase if settings.use_supabase else settings.database_url
# Expand ~ in database URL
if database_url.startswith("sqlite"):
database_url = database_url.replace("~", str(Path.home()))
# Create async engine (SQLite-optimized settings)
if "sqlite" in database_url:
engine = create_async_engine(
database_url,
echo=settings.debug,
future=True,
# SQLite-specific optimizations
connect_args={"check_same_thread": False}
)
else:
# PostgreSQL settings (fallback)
engine = create_async_engine(
database_url,
echo=settings.debug,
future=True,
pool_pre_ping=True,
pool_size=5,
max_overflow=10
)
# Create async session factory
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)
async def get_session() -> AsyncSession:
"""Dependency to get database session"""
async with async_session() as session:
yield session

121
python/api/app/db/models.py Normal file
View File

@@ -0,0 +1,121 @@
"""Database models"""
from datetime import datetime
from sqlalchemy import (
Column, Integer, String, DateTime, Boolean,
Float, ForeignKey, UniqueConstraint, Index,
Enum as SQLEnum
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid
import enum
from app.db.base import Base
from app.models.card import CardRarity
class User(Base):
"""ユーザーモデル"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
did = Column(String, unique=True, nullable=False, index=True)
handle = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
cards = relationship("UserCard", back_populates="owner")
draws = relationship("DrawHistory", back_populates="user")
class CardMaster(Base):
"""カードマスタデータ"""
__tablename__ = "card_master"
id = Column(Integer, primary_key=True) # 0-15
name = Column(String, nullable=False)
base_cp_min = Column(Integer, nullable=False)
base_cp_max = Column(Integer, nullable=False)
color = Column(String, nullable=False)
description = Column(String)
# Relationships
user_cards = relationship("UserCard", back_populates="card_info")
class UserCard(Base):
"""ユーザー所有カード"""
__tablename__ = "user_cards"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
card_id = Column(Integer, ForeignKey("card_master.id"), nullable=False)
cp = Column(Integer, nullable=False)
status = Column(SQLEnum(CardRarity), nullable=False)
skill = Column(String, nullable=True)
obtained_at = Column(DateTime, default=datetime.utcnow)
is_unique = Column(Boolean, default=False)
unique_id = Column(UUID(as_uuid=True), nullable=True, unique=True)
# Relationships
owner = relationship("User", back_populates="cards")
card_info = relationship("CardMaster", back_populates="user_cards")
# Indexes
__table_args__ = (
Index('idx_user_cards', 'user_id', 'card_id'),
)
class UniqueCardRegistry(Base):
"""uniqueカードのグローバルレジストリ"""
__tablename__ = "unique_card_registry"
id = Column(Integer, primary_key=True)
unique_id = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
card_id = Column(Integer, ForeignKey("card_master.id"), nullable=False)
owner_did = Column(String, ForeignKey("users.did"), nullable=False)
obtained_at = Column(DateTime, default=datetime.utcnow)
verse_skill_id = Column(String, nullable=True) # ai.verse連携用
# Unique constraint: 各card_idは1人のみ所有可能
__table_args__ = (
UniqueConstraint('card_id', name='unique_card_per_type'),
)
class DrawHistory(Base):
"""ガチャ履歴"""
__tablename__ = "draw_history"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
card_id = Column(Integer, nullable=False)
status = Column(SQLEnum(CardRarity), nullable=False)
cp = Column(Integer, nullable=False)
is_paid = Column(Boolean, default=False)
drawn_at = Column(DateTime, default=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="draws")
# Indexes
__table_args__ = (
Index('idx_draw_history_user', 'user_id', 'drawn_at'),
)
class GachaPool(Base):
"""ガチャプール(ピックアップ管理)"""
__tablename__ = "gacha_pools"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
description = Column(String)
is_active = Column(Boolean, default=True)
start_at = Column(DateTime, nullable=False)
end_at = Column(DateTime, nullable=True)
pickup_card_ids = Column(String) # JSON array of card IDs
rate_up_multiplier = Column(Float, default=1.0)
created_at = Column(DateTime, default=datetime.utcnow)