feat: 游玩记录多版本功能 - 支持多版本记录存储和回放 - 相同路径自动去重只保留最新 - 版本列表支持删除功能 - AI草稿箱游玩不记录历史 - iOS日期格式兼容修复
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
用户相关ORM模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Boolean, TIMESTAMP, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy import Column, Integer, String, Boolean, TIMESTAMP, ForeignKey, UniqueConstraint, JSON, Index
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
@@ -56,3 +56,21 @@ class UserEnding(Base):
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'story_id', 'ending_name', name='uk_user_ending'),
|
||||
)
|
||||
|
||||
|
||||
class PlayRecord(Base):
|
||||
"""游玩记录表 - 保存每次游玩的完整路径"""
|
||||
__tablename__ = "play_records"
|
||||
|
||||
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_type = Column(String(20), default="") # 结局类型 (good/bad/hidden/rewrite)
|
||||
path_history = Column(JSON, nullable=False) # 完整的选择路径
|
||||
play_duration = Column(Integer, default=0) # 游玩时长(秒)
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_user_story', 'user_id', 'story_id'),
|
||||
)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, func, text
|
||||
from sqlalchemy import select, update, func, text, delete
|
||||
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.user import User, UserProgress, UserEnding, PlayRecord
|
||||
from app.models.story import Story
|
||||
|
||||
router = APIRouter()
|
||||
@@ -46,6 +46,14 @@ class CollectRequest(BaseModel):
|
||||
isCollected: bool
|
||||
|
||||
|
||||
class PlayRecordRequest(BaseModel):
|
||||
userId: int
|
||||
storyId: int
|
||||
endingName: str
|
||||
endingType: str = ""
|
||||
pathHistory: list
|
||||
|
||||
|
||||
# ========== API接口 ==========
|
||||
|
||||
@router.post("/login")
|
||||
@@ -419,3 +427,147 @@ async def get_ai_quota(user_id: int = Query(..., alias="userId"), db: AsyncSessi
|
||||
"gift": 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ========== 游玩记录 API ==========
|
||||
|
||||
@router.post("/play-record")
|
||||
async def save_play_record(request: PlayRecordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""保存游玩记录(相同路径只保留最新)"""
|
||||
import json
|
||||
|
||||
# 查找该用户该故事的所有记录
|
||||
result = await db.execute(
|
||||
select(PlayRecord)
|
||||
.where(PlayRecord.user_id == request.userId, PlayRecord.story_id == request.storyId)
|
||||
)
|
||||
existing_records = result.scalars().all()
|
||||
|
||||
# 检查是否有相同路径的记录
|
||||
new_path_str = json.dumps(request.pathHistory, sort_keys=True, ensure_ascii=False)
|
||||
for old_record in existing_records:
|
||||
old_path_str = json.dumps(old_record.path_history, sort_keys=True, ensure_ascii=False)
|
||||
if old_path_str == new_path_str:
|
||||
# 相同路径,删除旧记录
|
||||
await db.delete(old_record)
|
||||
|
||||
# 创建新记录
|
||||
record = PlayRecord(
|
||||
user_id=request.userId,
|
||||
story_id=request.storyId,
|
||||
ending_name=request.endingName,
|
||||
ending_type=request.endingType,
|
||||
path_history=request.pathHistory
|
||||
)
|
||||
db.add(record)
|
||||
await db.commit()
|
||||
await db.refresh(record)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"recordId": record.id,
|
||||
"message": "记录保存成功"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/play-records")
|
||||
async def get_play_records(
|
||||
user_id: int = Query(..., alias="userId"),
|
||||
story_id: Optional[int] = Query(None, alias="storyId"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取游玩记录列表"""
|
||||
if story_id:
|
||||
# 获取指定故事的记录
|
||||
result = await db.execute(
|
||||
select(PlayRecord)
|
||||
.where(PlayRecord.user_id == user_id, PlayRecord.story_id == story_id)
|
||||
.order_by(PlayRecord.created_at.desc())
|
||||
)
|
||||
records = result.scalars().all()
|
||||
|
||||
data = [{
|
||||
"id": r.id,
|
||||
"endingName": r.ending_name,
|
||||
"endingType": r.ending_type,
|
||||
"createdAt": r.created_at.strftime("%Y-%m-%d %H:%M") if r.created_at else ""
|
||||
} for r in records]
|
||||
else:
|
||||
# 获取所有玩过的故事(按故事分组,取最新一条)
|
||||
result = await db.execute(
|
||||
select(PlayRecord, Story.title, Story.cover_url)
|
||||
.join(Story, PlayRecord.story_id == Story.id)
|
||||
.where(PlayRecord.user_id == user_id)
|
||||
.order_by(PlayRecord.created_at.desc())
|
||||
)
|
||||
rows = result.all()
|
||||
|
||||
# 按 story_id 分组,取每个故事的最新记录和记录数
|
||||
story_map = {}
|
||||
for row in rows:
|
||||
sid = row.PlayRecord.story_id
|
||||
if sid not in story_map:
|
||||
story_map[sid] = {
|
||||
"storyId": sid,
|
||||
"storyTitle": row.title,
|
||||
"coverUrl": row.cover_url,
|
||||
"latestEnding": row.PlayRecord.ending_name,
|
||||
"latestTime": row.PlayRecord.created_at.strftime("%Y-%m-%d %H:%M") if row.PlayRecord.created_at else "",
|
||||
"recordCount": 0
|
||||
}
|
||||
story_map[sid]["recordCount"] += 1
|
||||
|
||||
data = list(story_map.values())
|
||||
|
||||
return {"code": 0, "data": data}
|
||||
|
||||
|
||||
@router.get("/play-records/{record_id}")
|
||||
async def get_play_record_detail(
|
||||
record_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取单条记录详情"""
|
||||
result = await db.execute(
|
||||
select(PlayRecord, Story.title)
|
||||
.join(Story, PlayRecord.story_id == Story.id)
|
||||
.where(PlayRecord.id == record_id)
|
||||
)
|
||||
row = result.first()
|
||||
|
||||
if not row:
|
||||
return {"code": 404, "message": "记录不存在"}
|
||||
|
||||
record = row.PlayRecord
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": record.id,
|
||||
"storyId": record.story_id,
|
||||
"storyTitle": row.title,
|
||||
"endingName": record.ending_name,
|
||||
"endingType": record.ending_type,
|
||||
"pathHistory": record.path_history,
|
||||
"createdAt": record.created_at.strftime("%Y-%m-%d %H:%M") if record.created_at else ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/play-records/{record_id}")
|
||||
async def delete_play_record(
|
||||
record_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除游玩记录"""
|
||||
result = await db.execute(select(PlayRecord).where(PlayRecord.id == record_id))
|
||||
record = result.scalar_one_or_none()
|
||||
|
||||
if not record:
|
||||
return {"code": 404, "message": "记录不存在"}
|
||||
|
||||
await db.delete(record)
|
||||
await db.commit()
|
||||
|
||||
return {"code": 0, "message": "删除成功"}
|
||||
|
||||
Reference in New Issue
Block a user