Files
dreamweaver/backend/app/api/reading_events.py
zhangtuo e9d7f8832a 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
2026-01-20 18:20:03 +08:00

121 lines
3.5 KiB
Python

"""Reading event APIs."""
from datetime import datetime, timezone
from typing import Literal
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, MemoryItem, ReadingEvent, Story, User
router = APIRouter()
EVENT_WEIGHTS: dict[str, float] = {
"completed": 1.0,
"replayed": 1.5,
"started": 0.1,
"skipped": -0.5,
}
class ReadingEventCreate(BaseModel):
"""Reading event payload."""
child_profile_id: str
story_id: int | None = None
event_type: Literal["started", "completed", "skipped", "replayed"]
reading_time: int = Field(default=0, ge=0)
class ReadingEventResponse(BaseModel):
"""Reading event response."""
id: int
child_profile_id: str
story_id: int | None
event_type: str
reading_time: int
created_at: datetime
class Config:
from_attributes = True
@router.post("/reading-events", response_model=ReadingEventResponse, status_code=status.HTTP_201_CREATED)
async def create_reading_event(
payload: ReadingEventCreate,
user: User = Depends(require_user),
db: AsyncSession = Depends(get_db),
):
"""Create a reading event and update profile stats/memory."""
result = await db.execute(
select(ChildProfile).where(
ChildProfile.id == payload.child_profile_id,
ChildProfile.user_id == user.id,
)
)
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(status_code=404, detail="孩子档案不存在")
story = None
if payload.story_id is not None:
result = await db.execute(
select(Story).where(
Story.id == payload.story_id,
Story.user_id == user.id,
)
)
story = result.scalar_one_or_none()
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
if payload.reading_time:
profile.total_reading_time = (profile.total_reading_time or 0) + payload.reading_time
if payload.event_type in {"completed", "replayed"} and payload.story_id is not None:
existing = await db.scalar(
select(ReadingEvent.id).where(
ReadingEvent.child_profile_id == payload.child_profile_id,
ReadingEvent.story_id == payload.story_id,
ReadingEvent.event_type.in_(["completed", "replayed"]),
)
)
if existing is None:
profile.stories_count = (profile.stories_count or 0) + 1
event = ReadingEvent(
child_profile_id=payload.child_profile_id,
story_id=payload.story_id,
event_type=payload.event_type,
reading_time=payload.reading_time,
)
db.add(event)
weight = EVENT_WEIGHTS.get(payload.event_type, 0.0)
if story and weight > 0:
db.add(
MemoryItem(
child_profile_id=payload.child_profile_id,
universe_id=story.universe_id,
type="recent_story",
value={
"story_id": story.id,
"title": story.title,
"event_type": payload.event_type,
},
base_weight=weight,
last_used_at=datetime.now(timezone.utc),
ttl_days=90,
)
)
await db.commit()
await db.refresh(event)
return event