1.8
This commit is contained in:
+2
-1
@@ -64,7 +64,7 @@ app.add_middleware(
|
||||
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
|
||||
|
||||
# 注册路由
|
||||
from app.routers import auth, users, conversations, messages, friends, admin, uploads, moments, leaves, trees, capsules, echoes, sync, climates, flash
|
||||
from app.routers import auth, users, conversations, messages, friends, admin, uploads, moments, leaves, trees, capsules, echoes, sync, climates, flash, garden
|
||||
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["认证"])
|
||||
app.include_router(users.router, prefix="/api/v1/users", tags=["用户"])
|
||||
@@ -81,6 +81,7 @@ app.include_router(echoes.router, prefix="/api/v1/echoes", tags=["念念回音"]
|
||||
app.include_router(sync.router, prefix="/api/v1/sync", tags=["默契种子"])
|
||||
app.include_router(climates.router, prefix="/api/v1/climates", tags=["聊天气候"])
|
||||
app.include_router(flash.router, prefix="/api/v1/flash", tags=["萤火虫时刻"])
|
||||
app.include_router(garden.router, prefix="/api/v1/garden", tags=["花园状态"])
|
||||
|
||||
# WebSocket
|
||||
from app.websocket.router import websocket_router
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.models.miss_echo import MissEcho
|
||||
from app.models.sync_seed import SyncQuestion, SyncSeed
|
||||
from app.models.chat_climate import ChatClimate
|
||||
from app.models.flash_event import FlashEvent, FlashParticipation
|
||||
from app.models.user_streak import UserStreak
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -36,4 +37,5 @@ __all__ = [
|
||||
"ChatClimate",
|
||||
"FlashEvent",
|
||||
"FlashParticipation",
|
||||
"UserStreak",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""用户每日签到/连续天数模型"""
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import String, Integer, Date, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class UserStreak(Base):
|
||||
__tablename__ = "user_streaks"
|
||||
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
|
||||
streak: Mapped[int] = mapped_column(Integer, default=0) # 连续打开天数
|
||||
last_open_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
total_days: Mapped[int] = mapped_column(Integer, default=0) # 累计打开天数
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=lambda: datetime.utcnow(), onupdate=lambda: datetime.utcnow()
|
||||
)
|
||||
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
@@ -0,0 +1,20 @@
|
||||
"""花园综合状态路由"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.daily_service import DailyService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/daily-status")
|
||||
async def daily_status(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""今日花园状态:连续天数 + 口渴的树"""
|
||||
service = DailyService(db)
|
||||
return await service.get_daily_status(user.id)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""每日花园状态服务:连续天数 + 树渴了提醒"""
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from sqlalchemy import select, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.user_streak import UserStreak
|
||||
from app.models.friendship_tree import FriendshipTree
|
||||
from app.models.friend import Friend
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class DailyService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_daily_status(self, user_id: str) -> dict:
|
||||
"""获取/更新今日花园状态"""
|
||||
today = date.today()
|
||||
result = await self.db.execute(
|
||||
select(UserStreak).where(UserStreak.user_id == user_id)
|
||||
)
|
||||
streak_row = result.scalars().first()
|
||||
|
||||
is_new_day_open = False
|
||||
if not streak_row:
|
||||
streak_row = UserStreak(
|
||||
user_id=user_id, streak=1, last_open_date=today, total_days=1,
|
||||
)
|
||||
self.db.add(streak_row)
|
||||
is_new_day_open = True
|
||||
elif streak_row.last_open_date != today:
|
||||
# 计算连续天数
|
||||
if streak_row.last_open_date == today - timedelta(days=1):
|
||||
streak_row.streak += 1 # 连续
|
||||
else:
|
||||
streak_row.streak = 1 # 断了,重新计数
|
||||
streak_row.last_open_date = today
|
||||
streak_row.total_days += 1
|
||||
is_new_day_open = True
|
||||
await self.db.flush()
|
||||
|
||||
# 找口渴的树(24 小时没浇水的)
|
||||
thirsty_trees = await self._get_thirsty_trees(user_id)
|
||||
|
||||
return {
|
||||
"streak": streak_row.streak,
|
||||
"total_days": streak_row.total_days,
|
||||
"is_new_day_open": is_new_day_open,
|
||||
"thirsty_count": len(thirsty_trees),
|
||||
"thirsty_trees": thirsty_trees[:3], # 最多提示 3 棵
|
||||
}
|
||||
|
||||
async def _get_thirsty_trees(self, user_id: str) -> list[dict]:
|
||||
"""获取口渴的好友之树(24h 未浇水)"""
|
||||
cutoff = datetime.utcnow() - timedelta(hours=24)
|
||||
# 查该用户参与的所有树
|
||||
result = await self.db.execute(
|
||||
select(FriendshipTree).where(
|
||||
or_(
|
||||
FriendshipTree.user_a_id == user_id,
|
||||
FriendshipTree.user_b_id == user_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
thirsty = []
|
||||
for tree in result.scalars().all():
|
||||
if tree.last_watered_at is None or tree.last_watered_at < cutoff:
|
||||
friend_id = tree.user_b_id if tree.user_a_id == user_id else tree.user_a_id
|
||||
# 取好友信息
|
||||
fr = await self.db.execute(select(User).where(User.id == friend_id))
|
||||
friend = fr.scalars().first()
|
||||
if friend:
|
||||
thirsty.append({
|
||||
"friend_id": friend_id,
|
||||
"friend_name": friend.nickname or friend.username,
|
||||
"hours_since": int((datetime.utcnow() - (tree.last_watered_at or tree.created_at)).total_seconds() / 3600),
|
||||
})
|
||||
return thirsty
|
||||
Reference in New Issue
Block a user