"""每日花园状态服务:连续天数 + 树渴了提醒""" 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