import os from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from .database import get_db, init_db FRONTEND_DIR = os.environ.get("FRONTEND_DIR", "../frontend/dist") @asynccontextmanager async def lifespan(app: FastAPI): await init_db() yield app = FastAPI(lifespan=lifespan) class PlayerCreate(BaseModel): name: str class ProgressSave(BaseModel): player_id: int level_id: int stars: int code: str = "" @app.get("/api/health") async def health(): return {"status": "ok"} @app.post("/api/players") async def create_or_get_player(data: PlayerCreate): name = data.name.strip() if not name: raise HTTPException(400, "名字不能为空") db = await get_db() try: row = await db.execute_fetchall( "SELECT id, name FROM players WHERE name = ?", (name,) ) if row: player_id = row[0][0] else: cursor = await db.execute( "INSERT INTO players (name) VALUES (?)", (name,) ) await db.commit() player_id = cursor.lastrowid progress = await db.execute_fetchall( "SELECT level_id, stars, code FROM progress WHERE player_id = ?", (player_id,), ) progress_dict = { r[0]: {"stars": r[1], "code": r[2], "completed": True} for r in progress } return {"id": player_id, "name": name, "progress": progress_dict} finally: await db.close() @app.post("/api/progress") async def save_progress(data: ProgressSave): db = await get_db() try: await db.execute( """INSERT INTO progress (player_id, level_id, stars, code) VALUES (?, ?, ?, ?) ON CONFLICT (player_id, level_id) DO UPDATE SET stars = MAX(stars, excluded.stars), code = excluded.code, completed_at = CURRENT_TIMESTAMP""", (data.player_id, data.level_id, data.stars, data.code), ) await db.commit() return {"success": True} finally: await db.close() @app.get("/api/leaderboard") async def leaderboard(): db = await get_db() try: rows = await db.execute_fetchall(""" SELECT p.name, COALESCE(SUM(pr.stars), 0) as total_stars, COUNT(pr.id) as levels_completed FROM players p LEFT JOIN progress pr ON p.id = pr.player_id GROUP BY p.id ORDER BY total_stars DESC, levels_completed DESC LIMIT 50 """) return [ {"name": r[0], "total_stars": r[1], "levels_completed": r[2]} for r in rows ] finally: await db.close() # Serve frontend static files if os.path.isdir(FRONTEND_DIR): @app.get("/{full_path:path}") async def serve_spa(full_path: str): file_path = os.path.join(FRONTEND_DIR, full_path) if full_path and os.path.isfile(file_path): return FileResponse(file_path) index = os.path.join(FRONTEND_DIR, "index.html") return FileResponse(index)