This commit is contained in:
2026-06-14 10:01:47 +08:00
parent 6fbf610277
commit ca39190ad7
11 changed files with 556 additions and 13 deletions
+2 -1
View File
@@ -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
+2
View File
@@ -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",
]
+22
View File
@@ -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])
+20
View File
@@ -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)
+80
View File
@@ -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