Initial commit: clean project structure
- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+) - Frontend: Vue 3 + TypeScript + Pinia + Tailwind - Admin Frontend: separate Vue 3 app for management - Docker Compose: 9 services orchestration - Specs: design prototypes, memory system PRD, product roadmap Cleanup performed: - Removed temporary debug scripts from backend root - Removed deprecated admin_app.py (embedded UI) - Removed duplicate docs from admin-frontend - Updated .gitignore for Vite cache and egg-info
This commit is contained in:
201
backend/app/api/universes.py
Normal file
201
backend/app/api/universes.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""Story universe APIs."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import require_user
|
||||
from app.db.database import get_db
|
||||
from app.db.models import ChildProfile, StoryUniverse, User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class StoryUniverseCreate(BaseModel):
|
||||
"""Create universe payload."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
protagonist: dict[str, Any]
|
||||
recurring_characters: list[dict[str, Any]] = Field(default_factory=list)
|
||||
world_settings: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class StoryUniverseUpdate(BaseModel):
|
||||
"""Update universe payload."""
|
||||
|
||||
name: str | None = Field(default=None, min_length=1, max_length=100)
|
||||
protagonist: dict[str, Any] | None = None
|
||||
recurring_characters: list[dict[str, Any]] | None = None
|
||||
world_settings: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class AchievementCreate(BaseModel):
|
||||
"""Achievement payload."""
|
||||
|
||||
type: str = Field(..., min_length=1, max_length=50)
|
||||
description: str = Field(..., min_length=1, max_length=200)
|
||||
|
||||
|
||||
class StoryUniverseResponse(BaseModel):
|
||||
"""Universe response."""
|
||||
|
||||
id: str
|
||||
child_profile_id: str
|
||||
name: str
|
||||
protagonist: dict[str, Any]
|
||||
recurring_characters: list[dict[str, Any]]
|
||||
world_settings: dict[str, Any]
|
||||
achievements: list[dict[str, Any]]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class StoryUniverseListResponse(BaseModel):
|
||||
"""Universe list response."""
|
||||
|
||||
universes: list[StoryUniverseResponse]
|
||||
total: int
|
||||
|
||||
|
||||
async def _get_profile_or_404(
|
||||
profile_id: str,
|
||||
user: User,
|
||||
db: AsyncSession,
|
||||
) -> ChildProfile:
|
||||
result = await db.execute(
|
||||
select(ChildProfile).where(
|
||||
ChildProfile.id == profile_id,
|
||||
ChildProfile.user_id == user.id,
|
||||
)
|
||||
)
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
raise HTTPException(status_code=404, detail="档案不存在")
|
||||
return profile
|
||||
|
||||
|
||||
async def _get_universe_or_404(
|
||||
universe_id: str,
|
||||
user: User,
|
||||
db: AsyncSession,
|
||||
) -> StoryUniverse:
|
||||
result = await db.execute(
|
||||
select(StoryUniverse)
|
||||
.join(ChildProfile, StoryUniverse.child_profile_id == ChildProfile.id)
|
||||
.where(
|
||||
StoryUniverse.id == universe_id,
|
||||
ChildProfile.user_id == user.id,
|
||||
)
|
||||
)
|
||||
universe = result.scalar_one_or_none()
|
||||
if not universe:
|
||||
raise HTTPException(status_code=404, detail="宇宙不存在")
|
||||
return universe
|
||||
|
||||
|
||||
@router.get("/profiles/{profile_id}/universes", response_model=StoryUniverseListResponse)
|
||||
async def list_universes(
|
||||
profile_id: str,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List universes for a child profile."""
|
||||
await _get_profile_or_404(profile_id, user, db)
|
||||
result = await db.execute(
|
||||
select(StoryUniverse)
|
||||
.where(StoryUniverse.child_profile_id == profile_id)
|
||||
.order_by(StoryUniverse.updated_at.desc())
|
||||
)
|
||||
universes = result.scalars().all()
|
||||
return StoryUniverseListResponse(universes=universes, total=len(universes))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/profiles/{profile_id}/universes",
|
||||
response_model=StoryUniverseResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_universe(
|
||||
profile_id: str,
|
||||
payload: StoryUniverseCreate,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create a story universe."""
|
||||
await _get_profile_or_404(profile_id, user, db)
|
||||
universe = StoryUniverse(child_profile_id=profile_id, **payload.model_dump())
|
||||
db.add(universe)
|
||||
await db.commit()
|
||||
await db.refresh(universe)
|
||||
return universe
|
||||
|
||||
|
||||
@router.get("/universes/{universe_id}", response_model=StoryUniverseResponse)
|
||||
async def get_universe(
|
||||
universe_id: str,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get one universe."""
|
||||
universe = await _get_universe_or_404(universe_id, user, db)
|
||||
return universe
|
||||
|
||||
|
||||
@router.put("/universes/{universe_id}", response_model=StoryUniverseResponse)
|
||||
async def update_universe(
|
||||
universe_id: str,
|
||||
payload: StoryUniverseUpdate,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update a story universe."""
|
||||
universe = await _get_universe_or_404(universe_id, user, db)
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
for key, value in updates.items():
|
||||
setattr(universe, key, value)
|
||||
await db.commit()
|
||||
await db.refresh(universe)
|
||||
return universe
|
||||
|
||||
|
||||
@router.delete("/universes/{universe_id}")
|
||||
async def delete_universe(
|
||||
universe_id: str,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete a story universe."""
|
||||
universe = await _get_universe_or_404(universe_id, user, db)
|
||||
await db.delete(universe)
|
||||
await db.commit()
|
||||
return {"message": "Deleted"}
|
||||
|
||||
|
||||
@router.post("/universes/{universe_id}/achievements", response_model=StoryUniverseResponse)
|
||||
async def add_achievement(
|
||||
universe_id: str,
|
||||
payload: AchievementCreate,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Add an achievement to a universe."""
|
||||
universe = await _get_universe_or_404(universe_id, user, db)
|
||||
|
||||
achievements = list(universe.achievements or [])
|
||||
key = (payload.type.strip(), payload.description.strip())
|
||||
existing = {
|
||||
(str(item.get("type", "")).strip(), str(item.get("description", "")).strip())
|
||||
for item in achievements
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
if key not in existing:
|
||||
achievements.append({"type": key[0], "description": key[1]})
|
||||
universe.achievements = achievements
|
||||
await db.commit()
|
||||
await db.refresh(universe)
|
||||
|
||||
return universe
|
||||
Reference in New Issue
Block a user