refactor: 后端从Node.js重写为Python FastAPI
This commit is contained in:
0
server/app/__init__.py
Normal file
0
server/app/__init__.py
Normal file
BIN
server/app/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
server/app/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
server/app/__pycache__/config.cpython-310.pyc
Normal file
BIN
server/app/__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
BIN
server/app/__pycache__/database.cpython-310.pyc
Normal file
BIN
server/app/__pycache__/database.cpython-310.pyc
Normal file
Binary file not shown.
BIN
server/app/__pycache__/main.cpython-310.pyc
Normal file
BIN
server/app/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
37
server/app/config.py
Normal file
37
server/app/config.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
配置管理
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置"""
|
||||
# 数据库配置
|
||||
db_host: str = "localhost"
|
||||
db_port: int = 3306
|
||||
db_user: str = "root"
|
||||
db_password: str = ""
|
||||
db_name: str = "stardom_story"
|
||||
|
||||
# 服务配置
|
||||
server_host: str = "0.0.0.0"
|
||||
server_port: int = 3000
|
||||
debug: bool = True
|
||||
|
||||
# AI服务配置(预留)
|
||||
openai_api_key: str = ""
|
||||
openai_base_url: str = "https://api.openai.com/v1"
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
return f"mysql+aiomysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
36
server/app/database.py
Normal file
36
server/app/database.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
数据库连接
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from app.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# 创建异步引擎
|
||||
engine = create_async_engine(
|
||||
settings.database_url,
|
||||
echo=settings.debug,
|
||||
pool_pre_ping=True,
|
||||
pool_size=10,
|
||||
max_overflow=20
|
||||
)
|
||||
|
||||
# 创建异步会话
|
||||
AsyncSessionLocal = sessionmaker(
|
||||
bind=engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
# 基类
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
async def get_db():
|
||||
"""获取数据库会话"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
51
server/app/main.py
Normal file
51
server/app/main.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
星域故事汇 - Python后端服务
|
||||
"""
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.config import get_settings
|
||||
from app.routers import story, user
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# 创建应用
|
||||
app = FastAPI(
|
||||
title="星域故事汇",
|
||||
description="互动故事小游戏后端服务",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 跨域配置
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册路由
|
||||
app.include_router(story.router, prefix="/api/stories", tags=["故事"])
|
||||
app.include_router(user.router, prefix="/api/user", tags=["用户"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "星域故事汇后端服务运行中", "version": "1.0.0"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"星域故事汇服务器运行在 http://localhost:{settings.server_port}")
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host=settings.server_host,
|
||||
port=settings.server_port,
|
||||
reload=settings.debug
|
||||
)
|
||||
2
server/app/models/__init__.py
Normal file
2
server/app/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from app.models.story import Story, StoryNode, StoryChoice
|
||||
from app.models.user import User, UserProgress, UserEnding
|
||||
BIN
server/app/models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
server/app/models/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
server/app/models/__pycache__/story.cpython-310.pyc
Normal file
BIN
server/app/models/__pycache__/story.cpython-310.pyc
Normal file
Binary file not shown.
BIN
server/app/models/__pycache__/user.cpython-310.pyc
Normal file
BIN
server/app/models/__pycache__/user.cpython-310.pyc
Normal file
Binary file not shown.
66
server/app/models/story.py
Normal file
66
server/app/models/story.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
故事相关ORM模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, TIMESTAMP, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Story(Base):
|
||||
"""故事主表"""
|
||||
__tablename__ = "stories"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
title = Column(String(100), nullable=False)
|
||||
cover_url = Column(String(255), default="")
|
||||
description = Column(Text)
|
||||
author_id = Column(Integer, default=0)
|
||||
category = Column(String(50), nullable=False)
|
||||
play_count = Column(Integer, default=0)
|
||||
like_count = Column(Integer, default=0)
|
||||
is_featured = Column(Boolean, default=False)
|
||||
status = Column(Integer, default=1)
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
nodes = relationship("StoryNode", back_populates="story", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class StoryNode(Base):
|
||||
"""故事节点表"""
|
||||
__tablename__ = "story_nodes"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
story_id = Column(Integer, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False)
|
||||
node_key = Column(String(50), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
speaker = Column(String(50), default="")
|
||||
background_image = Column(String(255), default="")
|
||||
character_image = Column(String(255), default="")
|
||||
bgm = Column(String(255), default="")
|
||||
is_ending = Column(Boolean, default=False)
|
||||
ending_name = Column(String(100), default="")
|
||||
ending_score = Column(Integer, default=0)
|
||||
ending_type = Column(String(20), default="")
|
||||
sort_order = Column(Integer, default=0)
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
|
||||
story = relationship("Story", back_populates="nodes")
|
||||
choices = relationship("StoryChoice", back_populates="node", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class StoryChoice(Base):
|
||||
"""故事选项表"""
|
||||
__tablename__ = "story_choices"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
node_id = Column(Integer, ForeignKey("story_nodes.id", ondelete="CASCADE"), nullable=False)
|
||||
story_id = Column(Integer, nullable=False)
|
||||
text = Column(String(200), nullable=False)
|
||||
next_node_key = Column(String(50), nullable=False)
|
||||
sort_order = Column(Integer, default=0)
|
||||
is_locked = Column(Boolean, default=False)
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
|
||||
node = relationship("StoryNode", back_populates="choices")
|
||||
58
server/app/models/user.py
Normal file
58
server/app/models/user.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
用户相关ORM模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Boolean, TIMESTAMP, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户表"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
openid = Column(String(100), unique=True, nullable=False)
|
||||
nickname = Column(String(100), default="")
|
||||
avatar_url = Column(String(255), default="")
|
||||
gender = Column(Integer, default=0)
|
||||
total_play_count = Column(Integer, default=0)
|
||||
total_endings = Column(Integer, default=0)
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class UserProgress(Base):
|
||||
"""用户进度表"""
|
||||
__tablename__ = "user_progress"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
story_id = Column(Integer, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False)
|
||||
current_node_key = Column(String(50), default="start")
|
||||
is_completed = Column(Boolean, default=False)
|
||||
ending_reached = Column(String(100), default="")
|
||||
is_liked = Column(Boolean, default=False)
|
||||
is_collected = Column(Boolean, default=False)
|
||||
play_count = Column(Integer, default=1)
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'story_id', name='uk_user_story'),
|
||||
)
|
||||
|
||||
|
||||
class UserEnding(Base):
|
||||
"""用户结局收集表"""
|
||||
__tablename__ = "user_endings"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
story_id = Column(Integer, ForeignKey("stories.id", ondelete="CASCADE"), nullable=False)
|
||||
ending_name = Column(String(100), nullable=False)
|
||||
ending_score = Column(Integer, default=0)
|
||||
unlocked_at = Column(TIMESTAMP, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'story_id', 'ending_name', name='uk_user_ending'),
|
||||
)
|
||||
1
server/app/routers/__init__.py
Normal file
1
server/app/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from app.routers import story, user
|
||||
BIN
server/app/routers/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
server/app/routers/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
server/app/routers/__pycache__/story.cpython-310.pyc
Normal file
BIN
server/app/routers/__pycache__/story.cpython-310.pyc
Normal file
Binary file not shown.
BIN
server/app/routers/__pycache__/user.cpython-310.pyc
Normal file
BIN
server/app/routers/__pycache__/user.cpython-310.pyc
Normal file
Binary file not shown.
221
server/app/routers/story.py
Normal file
221
server/app/routers/story.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
故事相关API路由
|
||||
"""
|
||||
import random
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, func, distinct
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.story import Story, StoryNode, StoryChoice
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ========== 请求/响应模型 ==========
|
||||
class LikeRequest(BaseModel):
|
||||
like: bool
|
||||
|
||||
|
||||
class RewriteRequest(BaseModel):
|
||||
ending_name: str
|
||||
ending_content: str
|
||||
prompt: str
|
||||
|
||||
|
||||
# ========== API接口 ==========
|
||||
|
||||
@router.get("")
|
||||
async def get_stories(
|
||||
category: Optional[str] = Query(None),
|
||||
featured: bool = Query(False),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取故事列表"""
|
||||
query = select(Story).where(Story.status == 1)
|
||||
|
||||
if category:
|
||||
query = query.where(Story.category == category)
|
||||
if featured:
|
||||
query = query.where(Story.is_featured == True)
|
||||
|
||||
query = query.order_by(Story.is_featured.desc(), Story.play_count.desc())
|
||||
query = query.limit(limit).offset(offset)
|
||||
|
||||
result = await db.execute(query)
|
||||
stories = result.scalars().all()
|
||||
|
||||
data = [{
|
||||
"id": s.id,
|
||||
"title": s.title,
|
||||
"cover_url": s.cover_url,
|
||||
"description": s.description,
|
||||
"category": s.category,
|
||||
"play_count": s.play_count,
|
||||
"like_count": s.like_count,
|
||||
"is_featured": s.is_featured
|
||||
} for s in stories]
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.get("/hot")
|
||||
async def get_hot_stories(
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取热门故事"""
|
||||
query = select(Story).where(Story.status == 1).order_by(Story.play_count.desc()).limit(limit)
|
||||
result = await db.execute(query)
|
||||
stories = result.scalars().all()
|
||||
|
||||
data = [{
|
||||
"id": s.id,
|
||||
"title": s.title,
|
||||
"cover_url": s.cover_url,
|
||||
"description": s.description,
|
||||
"category": s.category,
|
||||
"play_count": s.play_count,
|
||||
"like_count": s.like_count
|
||||
} for s in stories]
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_categories(db: AsyncSession = Depends(get_db)):
|
||||
"""获取分类列表"""
|
||||
query = select(distinct(Story.category)).where(Story.status == 1)
|
||||
result = await db.execute(query)
|
||||
categories = [row[0] for row in result.all()]
|
||||
|
||||
return {"code": 0, "data": categories}
|
||||
|
||||
|
||||
@router.get("/{story_id}")
|
||||
async def get_story_detail(story_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""获取故事详情(含节点和选项)"""
|
||||
# 获取故事
|
||||
result = await db.execute(select(Story).where(Story.id == story_id, Story.status == 1))
|
||||
story = result.scalar_one_or_none()
|
||||
|
||||
if not story:
|
||||
raise HTTPException(status_code=404, detail="故事不存在")
|
||||
|
||||
# 获取节点
|
||||
nodes_result = await db.execute(
|
||||
select(StoryNode).where(StoryNode.story_id == story_id).order_by(StoryNode.sort_order)
|
||||
)
|
||||
nodes = nodes_result.scalars().all()
|
||||
|
||||
# 获取选项
|
||||
choices_result = await db.execute(
|
||||
select(StoryChoice).where(StoryChoice.story_id == story_id).order_by(StoryChoice.sort_order)
|
||||
)
|
||||
choices = choices_result.scalars().all()
|
||||
|
||||
# 组装节点和选项
|
||||
nodes_map = {}
|
||||
for node in nodes:
|
||||
nodes_map[node.node_key] = {
|
||||
"id": node.id,
|
||||
"node_key": node.node_key,
|
||||
"content": node.content,
|
||||
"speaker": node.speaker,
|
||||
"background_image": node.background_image,
|
||||
"character_image": node.character_image,
|
||||
"bgm": node.bgm,
|
||||
"is_ending": node.is_ending,
|
||||
"ending_name": node.ending_name,
|
||||
"ending_score": node.ending_score,
|
||||
"ending_type": node.ending_type,
|
||||
"choices": []
|
||||
}
|
||||
|
||||
for choice in choices:
|
||||
# 找到对应的节点
|
||||
for node in nodes:
|
||||
if node.id == choice.node_id and node.node_key in nodes_map:
|
||||
nodes_map[node.node_key]["choices"].append({
|
||||
"text": choice.text,
|
||||
"nextNodeKey": choice.next_node_key,
|
||||
"isLocked": choice.is_locked
|
||||
})
|
||||
break
|
||||
|
||||
data = {
|
||||
"id": story.id,
|
||||
"title": story.title,
|
||||
"cover_url": story.cover_url,
|
||||
"description": story.description,
|
||||
"category": story.category,
|
||||
"author_id": story.author_id,
|
||||
"play_count": story.play_count,
|
||||
"like_count": story.like_count,
|
||||
"is_featured": story.is_featured,
|
||||
"nodes": nodes_map
|
||||
}
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.post("/{story_id}/play")
|
||||
async def record_play(story_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""记录游玩"""
|
||||
await db.execute(
|
||||
update(Story).where(Story.id == story_id).values(play_count=Story.play_count + 1)
|
||||
)
|
||||
await db.commit()
|
||||
return {"code": 0, "message": "记录成功"}
|
||||
|
||||
|
||||
@router.post("/{story_id}/like")
|
||||
async def toggle_like(story_id: int, request: LikeRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""点赞/取消点赞"""
|
||||
delta = 1 if request.like else -1
|
||||
await db.execute(
|
||||
update(Story).where(Story.id == story_id).values(like_count=Story.like_count + delta)
|
||||
)
|
||||
await db.commit()
|
||||
return {"code": 0, "message": "点赞成功" if request.like else "取消点赞成功"}
|
||||
|
||||
|
||||
@router.post("/{story_id}/rewrite")
|
||||
async def ai_rewrite_ending(story_id: int, request: RewriteRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""AI改写结局"""
|
||||
if not request.prompt:
|
||||
raise HTTPException(status_code=400, detail="请输入改写指令")
|
||||
|
||||
# 获取故事信息
|
||||
result = await db.execute(select(Story).where(Story.id == story_id))
|
||||
story = result.scalar_one_or_none()
|
||||
|
||||
# 模拟AI生成(后续替换为真实API调用)
|
||||
templates = [
|
||||
f"根据你的愿望「{request.prompt}」,故事有了新的发展...\n\n",
|
||||
f"命运的齿轮开始转动,{request.prompt}...\n\n",
|
||||
f"在另一个平行世界里,{request.prompt}成为了现实...\n\n"
|
||||
]
|
||||
|
||||
template = random.choice(templates)
|
||||
new_content = (
|
||||
template +
|
||||
"原本的结局被改写,新的故事在这里展开。\n\n" +
|
||||
f"【AI改写提示】这是基于「{request.prompt}」生成的新结局。\n" +
|
||||
"实际部署时,这里将由AI大模型根据上下文生成更精彩的内容。"
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"content": new_content,
|
||||
"speaker": "旁白",
|
||||
"is_ending": True,
|
||||
"ending_name": f"{request.ending_name}(改写版)",
|
||||
"ending_type": "rewrite"
|
||||
}
|
||||
}
|
||||
421
server/app/routers/user.py
Normal file
421
server/app/routers/user.py
Normal file
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
用户相关API路由
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, func, text
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User, UserProgress, UserEnding
|
||||
from app.models.story import Story
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ========== 请求/响应模型 ==========
|
||||
class LoginRequest(BaseModel):
|
||||
code: str
|
||||
userInfo: Optional[dict] = None
|
||||
|
||||
|
||||
class ProfileRequest(BaseModel):
|
||||
nickname: str
|
||||
avatarUrl: str
|
||||
gender: int = 0
|
||||
|
||||
|
||||
class ProgressRequest(BaseModel):
|
||||
userId: int
|
||||
storyId: int
|
||||
currentNodeKey: str
|
||||
isCompleted: bool = False
|
||||
endingReached: str = ""
|
||||
|
||||
|
||||
class LikeRequest(BaseModel):
|
||||
userId: int
|
||||
storyId: int
|
||||
isLiked: bool
|
||||
|
||||
|
||||
class CollectRequest(BaseModel):
|
||||
userId: int
|
||||
storyId: int
|
||||
isCollected: bool
|
||||
|
||||
|
||||
# ========== API接口 ==========
|
||||
|
||||
@router.post("/login")
|
||||
async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""微信登录"""
|
||||
# 实际部署时需要调用微信API获取openid
|
||||
# 这里简化处理:用code作为openid
|
||||
openid = request.code
|
||||
|
||||
# 查找或创建用户
|
||||
result = await db.execute(select(User).where(User.openid == openid))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
user_info = request.userInfo or {}
|
||||
user = User(
|
||||
openid=openid,
|
||||
nickname=user_info.get("nickname", ""),
|
||||
avatar_url=user_info.get("avatarUrl", ""),
|
||||
gender=user_info.get("gender", 0)
|
||||
)
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"userId": user.id,
|
||||
"openid": user.openid,
|
||||
"nickname": user.nickname,
|
||||
"avatarUrl": user.avatar_url,
|
||||
"gender": user.gender,
|
||||
"total_play_count": user.total_play_count,
|
||||
"total_endings": user.total_endings
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/profile")
|
||||
async def update_profile(request: ProfileRequest, user_id: int = Query(..., alias="userId"), db: AsyncSession = Depends(get_db)):
|
||||
"""更新用户信息"""
|
||||
await db.execute(
|
||||
update(User).where(User.id == user_id).values(
|
||||
nickname=request.nickname,
|
||||
avatar_url=request.avatarUrl,
|
||||
gender=request.gender
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
return {"code": 0, "message": "更新成功"}
|
||||
|
||||
|
||||
@router.get("/progress")
|
||||
async def get_progress(
|
||||
user_id: int = Query(..., alias="userId"),
|
||||
story_id: Optional[str] = Query(None, alias="storyId"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取用户进度"""
|
||||
# 处理 storyId 为 "null" 字符串的情况
|
||||
story_id_int = None
|
||||
if story_id and story_id != "null":
|
||||
try:
|
||||
story_id_int = int(story_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
query = (
|
||||
select(UserProgress, Story.title.label("story_title"), Story.cover_url)
|
||||
.join(Story, UserProgress.story_id == Story.id)
|
||||
.where(UserProgress.user_id == user_id)
|
||||
)
|
||||
|
||||
if story_id_int:
|
||||
query = query.where(UserProgress.story_id == story_id_int)
|
||||
|
||||
query = query.order_by(UserProgress.updated_at.desc())
|
||||
result = await db.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
data = [{
|
||||
"id": row.UserProgress.id,
|
||||
"user_id": row.UserProgress.user_id,
|
||||
"story_id": row.UserProgress.story_id,
|
||||
"story_title": row.story_title,
|
||||
"cover_url": row.cover_url,
|
||||
"current_node_key": row.UserProgress.current_node_key,
|
||||
"is_completed": row.UserProgress.is_completed,
|
||||
"ending_reached": row.UserProgress.ending_reached,
|
||||
"is_liked": row.UserProgress.is_liked,
|
||||
"is_collected": row.UserProgress.is_collected,
|
||||
"play_count": row.UserProgress.play_count
|
||||
} for row in rows]
|
||||
|
||||
if story_id_int:
|
||||
return {"code": 0, "data": data[0] if data else None}
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.post("/progress")
|
||||
async def save_progress(request: ProgressRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""保存用户进度"""
|
||||
user_id = request.userId
|
||||
story_id = request.storyId
|
||||
|
||||
# 查找是否存在
|
||||
result = await db.execute(
|
||||
select(UserProgress).where(
|
||||
UserProgress.user_id == user_id,
|
||||
UserProgress.story_id == story_id
|
||||
)
|
||||
)
|
||||
progress = result.scalar_one_or_none()
|
||||
|
||||
if progress:
|
||||
# 更新
|
||||
await db.execute(
|
||||
update(UserProgress).where(UserProgress.id == progress.id).values(
|
||||
current_node_key=request.currentNodeKey,
|
||||
is_completed=request.isCompleted,
|
||||
ending_reached=request.endingReached,
|
||||
play_count=UserProgress.play_count + 1
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 新建
|
||||
progress = UserProgress(
|
||||
user_id=user_id,
|
||||
story_id=story_id,
|
||||
current_node_key=request.currentNodeKey,
|
||||
is_completed=request.isCompleted,
|
||||
ending_reached=request.endingReached
|
||||
)
|
||||
db.add(progress)
|
||||
|
||||
# 如果完成,记录结局
|
||||
if request.isCompleted and request.endingReached:
|
||||
# 检查是否已存在
|
||||
ending_result = await db.execute(
|
||||
select(UserEnding).where(
|
||||
UserEnding.user_id == user_id,
|
||||
UserEnding.story_id == story_id,
|
||||
UserEnding.ending_name == request.endingReached
|
||||
)
|
||||
)
|
||||
if not ending_result.scalar_one_or_none():
|
||||
ending = UserEnding(
|
||||
user_id=user_id,
|
||||
story_id=story_id,
|
||||
ending_name=request.endingReached
|
||||
)
|
||||
db.add(ending)
|
||||
|
||||
# 更新用户统计
|
||||
await db.execute(
|
||||
update(User).where(User.id == user_id).values(
|
||||
total_play_count=User.total_play_count + 1
|
||||
)
|
||||
)
|
||||
# 更新结局总数
|
||||
count_result = await db.execute(
|
||||
select(func.count()).select_from(UserEnding).where(UserEnding.user_id == user_id)
|
||||
)
|
||||
count = count_result.scalar()
|
||||
await db.execute(
|
||||
update(User).where(User.id == user_id).values(total_endings=count)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
return {"code": 0, "message": "保存成功"}
|
||||
|
||||
|
||||
@router.post("/like")
|
||||
async def toggle_like(request: LikeRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""点赞/取消点赞"""
|
||||
user_id = request.userId
|
||||
story_id = request.storyId
|
||||
|
||||
result = await db.execute(
|
||||
select(UserProgress).where(
|
||||
UserProgress.user_id == user_id,
|
||||
UserProgress.story_id == story_id
|
||||
)
|
||||
)
|
||||
progress = result.scalar_one_or_none()
|
||||
|
||||
if progress:
|
||||
await db.execute(
|
||||
update(UserProgress).where(UserProgress.id == progress.id).values(is_liked=request.isLiked)
|
||||
)
|
||||
else:
|
||||
progress = UserProgress(
|
||||
user_id=user_id,
|
||||
story_id=story_id,
|
||||
is_liked=request.isLiked
|
||||
)
|
||||
db.add(progress)
|
||||
|
||||
await db.commit()
|
||||
return {"code": 0, "message": "点赞成功" if request.isLiked else "取消点赞成功"}
|
||||
|
||||
|
||||
@router.post("/collect")
|
||||
async def toggle_collect(request: CollectRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""收藏/取消收藏"""
|
||||
user_id = request.userId
|
||||
story_id = request.storyId
|
||||
|
||||
result = await db.execute(
|
||||
select(UserProgress).where(
|
||||
UserProgress.user_id == user_id,
|
||||
UserProgress.story_id == story_id
|
||||
)
|
||||
)
|
||||
progress = result.scalar_one_or_none()
|
||||
|
||||
if progress:
|
||||
await db.execute(
|
||||
update(UserProgress).where(UserProgress.id == progress.id).values(is_collected=request.isCollected)
|
||||
)
|
||||
else:
|
||||
progress = UserProgress(
|
||||
user_id=user_id,
|
||||
story_id=story_id,
|
||||
is_collected=request.isCollected
|
||||
)
|
||||
db.add(progress)
|
||||
|
||||
await db.commit()
|
||||
return {"code": 0, "message": "收藏成功" if request.isCollected else "取消收藏成功"}
|
||||
|
||||
|
||||
@router.get("/collections")
|
||||
async def get_collections(user_id: int = Query(..., alias="userId"), db: AsyncSession = Depends(get_db)):
|
||||
"""获取收藏列表"""
|
||||
result = await db.execute(
|
||||
select(Story)
|
||||
.join(UserProgress, Story.id == UserProgress.story_id)
|
||||
.where(UserProgress.user_id == user_id, UserProgress.is_collected == True)
|
||||
.order_by(UserProgress.updated_at.desc())
|
||||
)
|
||||
stories = result.scalars().all()
|
||||
|
||||
data = [{
|
||||
"id": s.id,
|
||||
"title": s.title,
|
||||
"cover_url": s.cover_url,
|
||||
"description": s.description,
|
||||
"category": s.category,
|
||||
"play_count": s.play_count,
|
||||
"like_count": s.like_count
|
||||
} for s in stories]
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.get("/endings")
|
||||
async def get_unlocked_endings(
|
||||
user_id: int = Query(..., alias="userId"),
|
||||
story_id: Optional[str] = Query(None, alias="storyId"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取已解锁结局"""
|
||||
# 处理 storyId 为 "null" 字符串的情况
|
||||
story_id_int = None
|
||||
if story_id and story_id != "null":
|
||||
try:
|
||||
story_id_int = int(story_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
query = select(UserEnding).where(UserEnding.user_id == user_id)
|
||||
if story_id_int:
|
||||
query = query.where(UserEnding.story_id == story_id_int)
|
||||
|
||||
result = await db.execute(query)
|
||||
endings = result.scalars().all()
|
||||
|
||||
data = [{
|
||||
"id": e.id,
|
||||
"story_id": e.story_id,
|
||||
"ending_name": e.ending_name,
|
||||
"ending_score": e.ending_score,
|
||||
"unlocked_at": str(e.unlocked_at) if e.unlocked_at else None
|
||||
} for e in endings]
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.get("/my-works")
|
||||
async def get_my_works(user_id: int = Query(..., alias="userId"), db: AsyncSession = Depends(get_db)):
|
||||
"""获取我的作品"""
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Story).where(Story.author_id == user_id).order_by(Story.created_at.desc())
|
||||
)
|
||||
works = result.scalars().all()
|
||||
|
||||
data = [{
|
||||
"id": w.id,
|
||||
"title": w.title,
|
||||
"description": w.description,
|
||||
"category": w.category,
|
||||
"cover_url": w.cover_url,
|
||||
"play_count": w.play_count,
|
||||
"like_count": w.like_count,
|
||||
"status": w.status,
|
||||
"created_at": str(w.created_at) if w.created_at else None,
|
||||
"updated_at": str(w.updated_at) if w.updated_at else None
|
||||
} for w in works]
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
except Exception:
|
||||
return {"code": 0, "data": []}
|
||||
|
||||
|
||||
@router.get("/drafts")
|
||||
async def get_drafts(user_id: int = Query(..., alias="userId"), db: AsyncSession = Depends(get_db)):
|
||||
"""获取草稿箱(预留)"""
|
||||
# story_drafts表可能不存在,返回空
|
||||
return {"code": 0, "data": []}
|
||||
|
||||
|
||||
@router.get("/recent-played")
|
||||
async def get_recent_played(
|
||||
user_id: int = Query(..., alias="userId"),
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取最近游玩"""
|
||||
result = await db.execute(
|
||||
select(Story, UserProgress)
|
||||
.join(UserProgress, Story.id == UserProgress.story_id)
|
||||
.where(UserProgress.user_id == user_id)
|
||||
.order_by(UserProgress.updated_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
rows = result.all()
|
||||
|
||||
data = [{
|
||||
"id": row.Story.id,
|
||||
"title": row.Story.title,
|
||||
"category": row.Story.category,
|
||||
"description": row.Story.description,
|
||||
"cover_url": row.Story.cover_url,
|
||||
"current_node_key": row.UserProgress.current_node_key,
|
||||
"is_completed": row.UserProgress.is_completed,
|
||||
"progress": "已完成" if row.UserProgress.is_completed else "进行中"
|
||||
} for row in rows]
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.get("/ai-history")
|
||||
async def get_ai_history(user_id: int = Query(..., alias="userId"), limit: int = Query(20), db: AsyncSession = Depends(get_db)):
|
||||
"""获取AI创作历史(预留)"""
|
||||
return {"code": 0, "data": []}
|
||||
|
||||
|
||||
@router.get("/ai-quota")
|
||||
async def get_ai_quota(user_id: int = Query(..., alias="userId"), db: AsyncSession = Depends(get_db)):
|
||||
"""获取AI配额"""
|
||||
# 返回默认值
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"daily": 3,
|
||||
"used": 0,
|
||||
"purchased": 0,
|
||||
"gift": 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user