feat: 游玩记录多版本功能 - 支持多版本记录存储和回放 - 相同路径自动去重只保留最新 - 版本列表支持删除功能 - AI草稿箱游玩不记录历史 - iOS日期格式兼容修复

This commit is contained in:
wangwuww111
2026-03-10 12:44:55 +08:00
parent 9948ccba8f
commit baf7dd1e2b
8 changed files with 693 additions and 35 deletions

View File

@@ -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'),
)

View File

@@ -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": "删除成功"}