This commit is contained in:
2026-06-13 17:57:43 +08:00
parent 68678304ff
commit a0f441d8ae
28 changed files with 1933 additions and 2 deletions
+105
View File
@@ -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(),
}
+76
View File
@@ -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,
}
+149
View File
@@ -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