1.6
This commit is contained in:
+4
-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
|
||||
from app.routers import auth, users, conversations, messages, friends, admin, uploads, moments, leaves, trees, capsules
|
||||
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["认证"])
|
||||
app.include_router(users.router, prefix="/api/v1/users", tags=["用户"])
|
||||
@@ -74,6 +74,9 @@ app.include_router(friends.router, prefix="/api/v1/friends", tags=["好友"])
|
||||
app.include_router(admin.router, prefix="/api/v1/admin", tags=["管理"])
|
||||
app.include_router(uploads.router, prefix="/api/v1/uploads", tags=["上传"])
|
||||
app.include_router(moments.router, prefix="/api/v1/moments", tags=["朋友圈"])
|
||||
app.include_router(leaves.router, prefix="/api/v1/leaves", tags=["心情叶"])
|
||||
app.include_router(trees.router, prefix="/api/v1/trees", tags=["好友之树"])
|
||||
app.include_router(capsules.router, prefix="/api/v1/capsules", tags=["时光胶囊"])
|
||||
|
||||
# WebSocket
|
||||
from app.websocket.router import websocket_router
|
||||
|
||||
@@ -8,6 +8,9 @@ from app.models.friend import Friend
|
||||
from app.models.friend_request import FriendRequest
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.models.moment import Moment, MomentLike, MomentComment
|
||||
from app.models.daily_leaf import DailyMoodLeaf
|
||||
from app.models.friendship_tree import FriendshipTree
|
||||
from app.models.time_capsule import TimeCapsule
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -20,4 +23,7 @@ __all__ = [
|
||||
"Moment",
|
||||
"MomentLike",
|
||||
"MomentComment",
|
||||
"DailyMoodLeaf",
|
||||
"FriendshipTree",
|
||||
"TimeCapsule",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""每日心情叶模型"""
|
||||
|
||||
from datetime import datetime, date
|
||||
|
||||
from sqlalchemy import String, Text, DateTime, Date, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class DailyMoodLeaf(Base):
|
||||
__tablename__ = "daily_mood_leaves"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "leaf_date", name="uq_user_daily_leaf"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
leaf_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
mood: Mapped[str | None] = mapped_column(String(20), nullable=True) # 预设心情标签
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True) # 心情备注
|
||||
leaf_seed: Mapped[str] = mapped_column(String(32), nullable=False) # 确定性生成种子
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
|
||||
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
@@ -0,0 +1,25 @@
|
||||
"""好友之树模型"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Integer, DateTime, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class FriendshipTree(Base):
|
||||
__tablename__ = "friendship_trees"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_a_id", "user_b_id", name="uq_friendship_tree"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
user_a_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
user_b_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
water_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
last_watered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
|
||||
|
||||
user_a = relationship("User", foreign_keys=[user_a_id])
|
||||
user_b = relationship("User", foreign_keys=[user_b_id])
|
||||
@@ -0,0 +1,25 @@
|
||||
"""时光胶囊模型"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Text, DateTime, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class TimeCapsule(Base):
|
||||
__tablename__ = "time_capsules"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
sender_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
recipient_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
title: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
unlock_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
mood: Mapped[str | None] = mapped_column(String(20), nullable=True) # 种子心情色
|
||||
is_opened: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
|
||||
|
||||
sender = relationship("User", foreign_keys=[sender_id])
|
||||
recipient = relationship("User", foreign_keys=[recipient_id])
|
||||
@@ -0,0 +1,51 @@
|
||||
"""时光胶囊路由"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.capsule import CapsuleCreate
|
||||
from app.services.capsule_service import CapsuleService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_capsules(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取我的时光胶囊"""
|
||||
service = CapsuleService(db)
|
||||
return await service.get_capsules(user.id)
|
||||
|
||||
|
||||
@router.get("/{capsule_id}")
|
||||
async def get_capsule(
|
||||
capsule_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取单个胶囊"""
|
||||
service = CapsuleService(db)
|
||||
try:
|
||||
return await service.get_capsule(user.id, capsule_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_capsule(
|
||||
req: CapsuleCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建时光胶囊"""
|
||||
service = CapsuleService(db)
|
||||
try:
|
||||
return await service.create_capsule(
|
||||
user.id, req.recipient_id, req.title, req.content, req.unlock_at, req.mood
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -0,0 +1,46 @@
|
||||
"""心情叶路由"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.leaf import LeafUpdate
|
||||
from app.services.leaf_service import LeafService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/today")
|
||||
async def get_today_leaf(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取或创建今日心情叶"""
|
||||
service = LeafService(db)
|
||||
return await service.get_or_create_today(user.id)
|
||||
|
||||
|
||||
@router.put("/{leaf_id}")
|
||||
async def update_leaf(
|
||||
leaf_id: str,
|
||||
req: LeafUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新今日叶子的心情和备注"""
|
||||
service = LeafService(db)
|
||||
try:
|
||||
return await service.update_leaf(user.id, leaf_id, req.mood, req.note)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/collection")
|
||||
async def get_collection(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取叶子收藏"""
|
||||
service = LeafService(db)
|
||||
return await service.get_collection(user.id)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""好友之树路由"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.tree_service import TreeService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_trees(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取所有好友的树"""
|
||||
service = TreeService(db)
|
||||
return await service.get_all_trees(user.id)
|
||||
|
||||
|
||||
@router.get("/{friend_id}")
|
||||
async def get_tree(
|
||||
friend_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取与某好友的树"""
|
||||
service = TreeService(db)
|
||||
return await service.get_tree(user.id, friend_id)
|
||||
|
||||
|
||||
@router.post("/{friend_id}/water")
|
||||
async def water_tree(
|
||||
friend_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""浇水"""
|
||||
service = TreeService(db)
|
||||
try:
|
||||
return await service.water(user.id, friend_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -0,0 +1,13 @@
|
||||
"""时光胶囊 Schema"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CapsuleCreate(BaseModel):
|
||||
recipient_id: str
|
||||
title: str = Field(..., min_length=1, max_length=100)
|
||||
content: str = Field(..., min_length=1, max_length=5000)
|
||||
unlock_at: datetime
|
||||
mood: str | None = Field(None, max_length=20)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""心情叶 Schema"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LeafUpdate(BaseModel):
|
||||
mood: str | None = Field(None, max_length=20)
|
||||
note: str | None = Field(None, max_length=500)
|
||||
@@ -0,0 +1,105 @@
|
||||
"""时光胶囊服务(懒解锁)"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.time_capsule import TimeCapsule
|
||||
from app.models.friend import Friend
|
||||
|
||||
|
||||
class CapsuleService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def create_capsule(self, sender_id: str, recipient_id: str, title: str,
|
||||
content: str, unlock_at: datetime,
|
||||
mood: str | None = None) -> dict:
|
||||
"""创建时光胶囊"""
|
||||
# 校验解锁时间是未来
|
||||
if unlock_at <= datetime.utcnow():
|
||||
raise ValueError("解锁时间必须是未来时间")
|
||||
|
||||
# 校验收件人:自己 或 好友
|
||||
if recipient_id != sender_id:
|
||||
friend_result = await self.db.execute(
|
||||
select(Friend).where(
|
||||
Friend.user_id == sender_id,
|
||||
Friend.friend_user_id == recipient_id,
|
||||
)
|
||||
)
|
||||
if not friend_result.scalars().first():
|
||||
raise ValueError("只能给好友或自己寄胶囊")
|
||||
|
||||
capsule = TimeCapsule(
|
||||
id=str(uuid.uuid4()),
|
||||
sender_id=sender_id,
|
||||
recipient_id=recipient_id,
|
||||
title=title,
|
||||
content=content,
|
||||
unlock_at=unlock_at,
|
||||
mood=mood,
|
||||
)
|
||||
self.db.add(capsule)
|
||||
await self.db.flush()
|
||||
return self._capsule_to_dict(capsule, sender_id)
|
||||
|
||||
async def get_capsules(self, user_id: str) -> list[dict]:
|
||||
"""获取我发的 + 我收的胶囊"""
|
||||
result = await self.db.execute(
|
||||
select(TimeCapsule).where(
|
||||
or_(
|
||||
TimeCapsule.sender_id == user_id,
|
||||
TimeCapsule.recipient_id == user_id,
|
||||
)
|
||||
).order_by(TimeCapsule.unlock_at.desc())
|
||||
)
|
||||
return [self._capsule_to_dict(c, user_id) for c in result.scalars().all()]
|
||||
|
||||
async def get_capsule(self, user_id: str, capsule_id: str) -> dict:
|
||||
"""获取单个胶囊"""
|
||||
result = await self.db.execute(
|
||||
select(TimeCapsule).where(TimeCapsule.id == capsule_id)
|
||||
)
|
||||
capsule = result.scalars().first()
|
||||
if not capsule:
|
||||
raise ValueError("胶囊不存在")
|
||||
if capsule.sender_id != user_id and capsule.recipient_id != user_id:
|
||||
raise ValueError("无权查看此胶囊")
|
||||
|
||||
# 若已解锁且收件人首次查看,标记 opened
|
||||
now = datetime.utcnow()
|
||||
if now >= capsule.unlock_at and capsule.recipient_id == user_id and not capsule.is_opened:
|
||||
capsule.is_opened = True
|
||||
await self.db.flush()
|
||||
return self._capsule_to_dict(capsule, user_id)
|
||||
|
||||
def _capsule_to_dict(self, capsule: TimeCapsule, viewer_id: str) -> dict:
|
||||
"""转字典,应用懒解锁:未到期则隐藏内容"""
|
||||
now = datetime.utcnow()
|
||||
locked = now < capsule.unlock_at
|
||||
seconds_left = int((capsule.unlock_at - now).total_seconds()) if locked else 0
|
||||
|
||||
# 内容可见性:发送人可看自己的(即使锁定);收件人锁定时不可看
|
||||
can_see_content = (
|
||||
capsule.sender_id == viewer_id # 发送人始终能看自己发的
|
||||
or not locked # 已解锁
|
||||
)
|
||||
|
||||
return {
|
||||
"id": capsule.id,
|
||||
"sender_id": capsule.sender_id,
|
||||
"recipient_id": capsule.recipient_id,
|
||||
"is_mine_sent": capsule.sender_id == viewer_id,
|
||||
"is_mine_received": capsule.recipient_id == viewer_id,
|
||||
"title": capsule.title,
|
||||
"content": capsule.content if can_see_content else None,
|
||||
"unlock_at": capsule.unlock_at.isoformat(),
|
||||
"mood": capsule.mood,
|
||||
"locked": locked,
|
||||
"seconds_left": seconds_left,
|
||||
"is_opened": capsule.is_opened,
|
||||
"created_at": capsule.created_at.isoformat(),
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"""每日心情叶服务"""
|
||||
|
||||
import hashlib
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.daily_leaf import DailyMoodLeaf
|
||||
|
||||
|
||||
class LeafService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_or_create_today(self, user_id: str) -> dict:
|
||||
"""获取或创建今日心情叶"""
|
||||
today = date.today()
|
||||
result = await self.db.execute(
|
||||
select(DailyMoodLeaf).where(
|
||||
DailyMoodLeaf.user_id == user_id,
|
||||
DailyMoodLeaf.leaf_date == today,
|
||||
)
|
||||
)
|
||||
leaf = result.scalars().first()
|
||||
if not leaf:
|
||||
seed = hashlib.md5(f"{user_id}:{today.isoformat()}".encode()).hexdigest()[:16]
|
||||
leaf = DailyMoodLeaf(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
leaf_date=today,
|
||||
leaf_seed=seed,
|
||||
)
|
||||
self.db.add(leaf)
|
||||
await self.db.flush()
|
||||
return self._leaf_to_dict(leaf)
|
||||
|
||||
async def update_leaf(self, user_id: str, leaf_id: str, mood: str | None,
|
||||
note: str | None) -> dict:
|
||||
"""更新今日叶子的心情和备注"""
|
||||
result = await self.db.execute(
|
||||
select(DailyMoodLeaf).where(DailyMoodLeaf.id == leaf_id)
|
||||
)
|
||||
leaf = result.scalars().first()
|
||||
if not leaf:
|
||||
raise ValueError("叶子不存在")
|
||||
if leaf.user_id != user_id:
|
||||
raise ValueError("无权操作此叶子")
|
||||
if mood is not None:
|
||||
leaf.mood = mood
|
||||
if note is not None:
|
||||
leaf.note = note
|
||||
await self.db.flush()
|
||||
return self._leaf_to_dict(leaf)
|
||||
|
||||
async def get_collection(self, user_id: str, limit: int = 60) -> list[dict]:
|
||||
"""获取叶子收藏(历史)"""
|
||||
result = await self.db.execute(
|
||||
select(DailyMoodLeaf)
|
||||
.where(DailyMoodLeaf.user_id == user_id)
|
||||
.order_by(DailyMoodLeaf.leaf_date.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return [self._leaf_to_dict(l) for l in result.scalars().all()]
|
||||
|
||||
def _leaf_to_dict(self, leaf: DailyMoodLeaf) -> dict:
|
||||
return {
|
||||
"id": leaf.id,
|
||||
"user_id": leaf.user_id,
|
||||
"leaf_date": leaf.leaf_date.isoformat() if leaf.leaf_date else None,
|
||||
"mood": leaf.mood,
|
||||
"note": leaf.note,
|
||||
"leaf_seed": leaf.leaf_seed,
|
||||
"created_at": leaf.created_at,
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
"""好友之树服务"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.friendship_tree import FriendshipTree
|
||||
from app.models.conversation import Conversation
|
||||
from app.models.conversation_member import ConversationMember
|
||||
from app.models.message import Message
|
||||
from app.models.friend import Friend
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
# 阶段定义:分数 -> (阶段索引, 名称, emoji)
|
||||
STAGES = [
|
||||
(0, "种子", "🌱"),
|
||||
(11, "萌芽", "🌿"),
|
||||
(41, "幼苗", "🪴"),
|
||||
(151, "小树", "🌲"),
|
||||
(401, "大树", "🌳"),
|
||||
(1001, "古树", "🌲"),
|
||||
]
|
||||
|
||||
|
||||
def stage_for_score(score: int) -> tuple[int, str, str]:
|
||||
"""根据分数返回 (阶段索引, 名称, emoji)"""
|
||||
idx = 0
|
||||
for i, (threshold, name, emoji) in enumerate(STAGES):
|
||||
if score >= threshold:
|
||||
idx = i
|
||||
return idx, STAGES[idx][1], STAGES[idx][2]
|
||||
|
||||
|
||||
class TreeService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def _get_or_create_tree_row(self, user_id: str, friend_id: str) -> FriendshipTree:
|
||||
"""规范化(a < b)后查/建树行"""
|
||||
a, b = (user_id, friend_id) if user_id < friend_id else (friend_id, user_id)
|
||||
result = await self.db.execute(
|
||||
select(FriendshipTree).where(
|
||||
FriendshipTree.user_a_id == a,
|
||||
FriendshipTree.user_b_id == b,
|
||||
)
|
||||
)
|
||||
tree = result.scalars().first()
|
||||
if not tree:
|
||||
tree = FriendshipTree(
|
||||
id=str(uuid.uuid4()),
|
||||
user_a_id=a,
|
||||
user_b_id=b,
|
||||
)
|
||||
self.db.add(tree)
|
||||
await self.db.flush()
|
||||
return tree
|
||||
|
||||
async def _count_messages_between(self, user_id: str, friend_id: str) -> int:
|
||||
"""统计两人私聊会话中的消息数(复用 get_or_create_private 的查找逻辑)"""
|
||||
# 找到两人共有的私聊会话
|
||||
result = await self.db.execute(
|
||||
select(Conversation).join(ConversationMember)
|
||||
.where(
|
||||
Conversation.type == "private",
|
||||
ConversationMember.user_id == user_id,
|
||||
)
|
||||
)
|
||||
conv_id = None
|
||||
for conv in result.scalars().all():
|
||||
member_result = await self.db.execute(
|
||||
select(ConversationMember).where(
|
||||
ConversationMember.conversation_id == conv.id,
|
||||
ConversationMember.user_id == friend_id,
|
||||
)
|
||||
)
|
||||
if member_result.scalars().first():
|
||||
conv_id = conv.id
|
||||
break
|
||||
if not conv_id:
|
||||
return 0
|
||||
count_result = await self.db.execute(
|
||||
select(func.count(Message.id)).where(
|
||||
Message.conversation_id == conv_id,
|
||||
Message.is_deleted == False,
|
||||
)
|
||||
)
|
||||
return count_result.scalar() or 0
|
||||
|
||||
async def get_tree(self, user_id: str, friend_id: str) -> dict:
|
||||
"""获取好友之树"""
|
||||
tree = await self._get_or_create_tree_row(user_id, friend_id)
|
||||
msg_count = await self._count_messages_between(user_id, friend_id)
|
||||
total_score = msg_count + tree.water_count * 5
|
||||
stage_idx, stage_name, stage_emoji = stage_for_score(total_score)
|
||||
|
||||
# 下个阶段的分数门槛
|
||||
next_threshold = STAGES[stage_idx + 1][0] if stage_idx + 1 < len(STAGES) else None
|
||||
|
||||
# 好友信息
|
||||
friend_result = await self.db.execute(select(User).where(User.id == friend_id))
|
||||
friend = friend_result.scalars().first()
|
||||
|
||||
return {
|
||||
"tree_id": tree.id,
|
||||
"friend_id": friend_id,
|
||||
"friend_name": friend.nickname or friend.username if friend else "未知",
|
||||
"friend_avatar": friend.avatar_url if friend else None,
|
||||
"message_count": msg_count,
|
||||
"water_count": tree.water_count,
|
||||
"total_score": total_score,
|
||||
"stage_index": stage_idx,
|
||||
"stage_name": stage_name,
|
||||
"stage_emoji": stage_emoji,
|
||||
"next_threshold": next_threshold,
|
||||
"last_watered_at": tree.last_watered_at,
|
||||
"seed": tree.id[:16], # 用于程序化树形态
|
||||
}
|
||||
|
||||
async def water(self, user_id: str, friend_id: str) -> dict:
|
||||
"""浇水,返回带 leveled_up 标志"""
|
||||
tree = await self._get_or_create_tree_row(user_id, friend_id)
|
||||
old_score = (await self._count_messages_between(user_id, friend_id)) + tree.water_count * 5
|
||||
old_stage_idx = stage_for_score(old_score)[0]
|
||||
|
||||
tree.water_count += 1
|
||||
tree.last_watered_at = datetime.utcnow()
|
||||
await self.db.flush()
|
||||
|
||||
result = await self.get_tree(user_id, friend_id)
|
||||
result["leveled_up"] = result["stage_index"] > old_stage_idx
|
||||
return result
|
||||
|
||||
async def get_all_trees(self, user_id: str) -> list[dict]:
|
||||
"""获取所有好友的树(花园概览用)"""
|
||||
# 获取好友列表
|
||||
friends_result = await self.db.execute(
|
||||
select(Friend.friend_user_id).where(Friend.user_id == user_id)
|
||||
)
|
||||
friend_ids = [r[0] for r in friends_result.all()]
|
||||
|
||||
trees = []
|
||||
for fid in friend_ids:
|
||||
trees.append(await self.get_tree(user_id, fid))
|
||||
# 按分数排序
|
||||
trees.sort(key=lambda t: t["total_score"], reverse=True)
|
||||
return trees
|
||||
Reference in New Issue
Block a user