Files
dreamweaver/backend/app/api/universes.py
torin b8d3cb4644
Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
wip: snapshot full local workspace state
2026-04-17 18:58:11 +08:00

202 lines
5.8 KiB
Python

"""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