This commit is contained in:
2026-06-13 07:33:46 +08:00
parent e2da13bc5c
commit 24017e7454
40 changed files with 3135 additions and 108 deletions
+85 -17
View File
@@ -1,11 +1,10 @@
"""会话服务"""
import uuid
from datetime import datetime, timezone
from datetime import datetime
from sqlalchemy import select, and_
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.conversation import Conversation
from app.models.conversation_member import ConversationMember
@@ -18,7 +17,6 @@ class ConversationService:
async def get_or_create_private(self, user1_id: str, user2_id: str) -> Conversation:
"""获取或创建私聊会话"""
# 查找已有的私聊
result = await self.db.execute(
select(Conversation).join(ConversationMember)
.where(
@@ -36,12 +34,10 @@ class ConversationService:
if member_result.scalars().first():
return conv
# 创建新私聊
conv = Conversation(id=str(uuid.uuid4()), type="private")
self.db.add(conv)
await self.db.flush()
# 添加两个成员
self.db.add(ConversationMember(
id=str(uuid.uuid4()), conversation_id=conv.id, user_id=user1_id, role="member"
))
@@ -63,12 +59,10 @@ class ConversationService:
self.db.add(conv)
await self.db.flush()
# 创建者为 owner
self.db.add(ConversationMember(
id=str(uuid.uuid4()), conversation_id=conv.id,
user_id=creator_id, role="owner"
))
# 其他成员
for mid in member_ids:
if mid != creator_id:
self.db.add(ConversationMember(
@@ -77,6 +71,86 @@ class ConversationService:
))
return conv
async def update_group(self, conv_id: str, user_id: str, **kwargs):
"""更新群聊信息(仅群主/管理员)"""
conv = await self._get_conv_if_admin(conv_id, user_id)
for key, value in kwargs.items():
if value is not None and hasattr(conv, key):
setattr(conv, key, value)
async def add_members(self, conv_id: str, user_id: str, new_member_ids: list[str]):
"""添加群成员(仅群主/管理员)"""
await self._get_conv_if_admin(conv_id, user_id)
for mid in new_member_ids:
# 检查是否已在群中
existing = await self.db.execute(
select(ConversationMember).where(
ConversationMember.conversation_id == conv_id,
ConversationMember.user_id == mid,
ConversationMember.left_at.is_(None),
)
)
if not existing.scalars().first():
self.db.add(ConversationMember(
id=str(uuid.uuid4()), conversation_id=conv_id,
user_id=mid, role="member"
))
async def remove_member(self, conv_id: str, user_id: str, target_user_id: str):
"""移除群成员(仅群主/管理员,不能移除群主)"""
await self._get_conv_if_admin(conv_id, user_id)
member = await self._get_member(conv_id, target_user_id)
if not member:
raise ValueError("该用户不在群中")
if member.role == "owner":
raise ValueError("不能移除群主")
member.left_at = datetime.utcnow()
async def leave_group(self, conv_id: str, user_id: str):
"""退出群聊"""
member = await self._get_member(conv_id, user_id)
if not member:
raise ValueError("你不在该群中")
if member.role == "owner":
raise ValueError("群主不能退出,请先转让群主身份")
member.left_at = datetime.utcnow()
async def update_member_role(self, conv_id: str, user_id: str, target_user_id: str, role: str):
"""修改成员角色(仅群主)"""
member = await self._get_member(conv_id, user_id)
if not member or member.role != "owner":
raise ValueError("只有群主可以修改角色")
target = await self._get_member(conv_id, target_user_id)
if not target:
raise ValueError("目标用户不在群中")
target.role = role
async def _get_conv_if_admin(self, conv_id: str, user_id: str) -> Conversation:
"""获取会话并验证管理员权限"""
conv_result = await self.db.execute(
select(Conversation).where(Conversation.id == conv_id)
)
conv = conv_result.scalars().first()
if not conv:
raise ValueError("会话不存在")
if conv.type != "group":
raise ValueError("仅群聊支持此操作")
member = await self._get_member(conv_id, user_id)
if not member or member.role not in ("owner", "admin"):
raise ValueError("仅群主或管理员可执行此操作")
return conv
async def _get_member(self, conv_id: str, user_id: str) -> ConversationMember | None:
"""获取成员记录"""
result = await self.db.execute(
select(ConversationMember).where(
ConversationMember.conversation_id == conv_id,
ConversationMember.user_id == user_id,
ConversationMember.left_at.is_(None),
)
)
return result.scalars().first()
async def get_user_conversations(self, user_id: str) -> list[dict]:
"""获取用户的会话列表"""
result = await self.db.execute(
@@ -95,17 +169,15 @@ class ConversationService:
if not conv:
continue
# 获取未读数
unread = await self._get_unread_count(conv.id, member.last_read_message_id)
# 获取显示信息
display_name = conv.name
display_avatar = conv.avatar_url
if conv.type == "private":
other = await self._get_other_member(conv.id, user_id)
if other:
display_name = other.username
display_name = other.nickname or other.username
display_avatar = other.avatar_url
conversations.append({
@@ -120,7 +192,6 @@ class ConversationService:
"created_at": conv.created_at,
})
# 按最后消息时间排序
conversations.sort(key=lambda x: x["last_message_at"] or x["created_at"], reverse=True)
return conversations
@@ -133,7 +204,6 @@ class ConversationService:
if not conv:
return None
# 验证成员身份
member_result = await self.db.execute(
select(ConversationMember).where(
ConversationMember.conversation_id == conv_id,
@@ -144,7 +214,6 @@ class ConversationService:
if not member_result.scalars().first():
return None
# 获取所有成员
members_result = await self.db.execute(
select(ConversationMember).where(
ConversationMember.conversation_id == conv_id,
@@ -160,7 +229,7 @@ class ConversationService:
"id": m.id,
"user_id": user.id,
"username": user.username,
"nickname": user.bio,
"nickname": user.nickname,
"avatar_url": user.avatar_url,
"role": m.role,
"joined_at": m.joined_at,
@@ -196,12 +265,11 @@ class ConversationService:
async def _get_unread_count(self, conv_id: str, last_read_id: str | None) -> int:
"""计算未读消息数"""
from app.models.message import Message
query = select(func := __import__("sqlalchemy").func).count(Message.id).where(
query = select(func.count(Message.id)).where(
Message.conversation_id == conv_id,
Message.is_deleted == False,
)
if last_read_id:
# 获取 last_read 消息的时间
lr = await self.db.execute(select(Message).where(Message.id == last_read_id))
lr_msg = lr.scalars().first()
if lr_msg:
+44 -1
View File
@@ -113,7 +113,7 @@ class FriendService:
"id": friendship.id,
"friend_user_id": user.id,
"username": user.username,
"nickname": user.bio,
"nickname": user.nickname,
"avatar_url": user.avatar_url,
"remark": friendship.remark,
"status": user.status,
@@ -160,3 +160,46 @@ class FriendService:
(Friend.user_id == friend_id) & (Friend.friend_user_id == user_id)
)
)
async def add_direct(self, from_user_id: str, to_user_id: str):
"""直接添加好友(跳过验证)"""
if from_user_id == to_user_id:
raise ValueError("不能添加自己为好友")
# 检查目标用户是否存在
target = await self.db.execute(select(User).where(User.id == to_user_id))
if not target.scalars().first():
raise ValueError("目标用户不存在")
# 检查是否已是好友
existing = await self.db.execute(
select(Friend).where(
Friend.user_id == from_user_id,
Friend.friend_user_id == to_user_id,
)
)
if existing.scalars().first():
raise ValueError("已经是好友了")
# 创建双向好友关系
self.db.add(Friend(
id=str(uuid.uuid4()), user_id=from_user_id,
friend_user_id=to_user_id,
))
self.db.add(Friend(
id=str(uuid.uuid4()), user_id=to_user_id,
friend_user_id=from_user_id,
))
async def update_remark(self, user_id: str, friend_user_id: str, remark: str | None):
"""修改好友备注"""
result = await self.db.execute(
select(Friend).where(
Friend.user_id == user_id,
Friend.friend_user_id == friend_user_id,
)
)
friendship = result.scalars().first()
if not friendship:
raise ValueError("好友关系不存在")
friendship.remark = remark
+312
View File
@@ -0,0 +1,312 @@
"""朋友圈服务"""
import json
import uuid
from datetime import datetime
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.moment import Moment, MomentLike, MomentComment
from app.models.friend import Friend
from app.models.user import User
class MomentService:
def __init__(self, db: AsyncSession):
self.db = db
async def create_moment(self, user_id: str, content: str,
images: list[str] | None = None,
visibility: str = "friends") -> Moment:
"""发布动态"""
moment = Moment(
id=str(uuid.uuid4()),
user_id=user_id,
content=content,
images=json.dumps(images) if images else None,
visibility=visibility,
)
self.db.add(moment)
await self.db.flush()
return moment
async def get_feed(self, user_id: str, cursor: str | None = None,
limit: int = 20) -> list[dict]:
"""获取朋友圈 feed(自己 + 好友的动态)"""
# 获取好友ID列表
friend_ids = await self._get_friend_ids(user_id)
# 可以看到的人:自己 + 好友
visible_user_ids = [user_id] + friend_ids
query = (
select(Moment)
.where(
Moment.user_id.in_(visible_user_ids),
Moment.visibility != "private", # 私密动态只有自己能看到(下面单独处理)
)
.order_by(Moment.created_at.desc())
.limit(limit)
)
# 也获取自己的私密动态
own_private_query = (
select(Moment)
.where(Moment.user_id == user_id, Moment.visibility == "private")
.order_by(Moment.created_at.desc())
.limit(limit)
)
if cursor:
cursor_result = await self.db.execute(
select(Moment.created_at).where(Moment.id == cursor)
)
cursor_time = cursor_result.scalar()
if cursor_time:
query = query.where(Moment.created_at < cursor_time)
own_private_query = own_private_query.where(Moment.created_at < cursor_time)
result = await self.db.execute(query)
own_private_result = await self.db.execute(own_private_query)
moments = list(result.scalars().all())
moments.extend(own_private_result.scalars().all())
# 合并并去重、排序
seen_ids = set()
unique = []
for m in moments:
if m.id not in seen_ids:
seen_ids.add(m.id)
unique.append(m)
unique.sort(key=lambda x: x.created_at, reverse=True)
unique = unique[:limit]
return [await self._moment_to_dict(m, user_id) for m in unique]
async def get_user_moments(self, user_id: str, viewer_id: str | None = None,
cursor: str | None = None, limit: int = 20) -> list[dict]:
"""获取指定用户的动态"""
query = (
select(Moment)
.where(Moment.user_id == user_id)
.order_by(Moment.created_at.desc())
.limit(limit)
)
if cursor:
cursor_result = await self.db.execute(
select(Moment.created_at).where(Moment.id == cursor)
)
cursor_time = cursor_result.scalar()
if cursor_time:
query = query.where(Moment.created_at < cursor_time)
result = await self.db.execute(query)
moments = list(result.scalars().all())
# 过滤可见性
filtered = []
for m in moments:
if m.visibility == "public":
filtered.append(m)
elif m.visibility == "friends":
if viewer_id and (viewer_id == user_id or await self._are_friends(viewer_id, user_id)):
filtered.append(m)
elif m.visibility == "private":
if viewer_id == user_id:
filtered.append(m)
return [await self._moment_to_dict(m, viewer_id) for m in filtered]
async def delete_moment(self, moment_id: str, user_id: str):
"""删除动态(仅作者)"""
result = await self.db.execute(select(Moment).where(Moment.id == moment_id))
moment = result.scalars().first()
if not moment:
raise ValueError("动态不存在")
if moment.user_id != user_id:
raise ValueError("只能删除自己的动态")
await self.db.delete(moment)
async def toggle_like(self, moment_id: str, user_id: str) -> bool:
"""点赞/取消点赞,返回是否已点赞"""
result = await self.db.execute(
select(MomentLike).where(
MomentLike.moment_id == moment_id,
MomentLike.user_id == user_id,
)
)
existing = result.scalars().first()
if existing:
await self.db.delete(existing)
return False
else:
self.db.add(MomentLike(
id=str(uuid.uuid4()),
moment_id=moment_id,
user_id=user_id,
))
return True
async def add_comment(self, moment_id: str, user_id: str, content: str,
reply_to_id: str | None = None) -> dict:
"""添加评论"""
# 验证动态存在
moment_result = await self.db.execute(select(Moment).where(Moment.id == moment_id))
if not moment_result.scalars().first():
raise ValueError("动态不存在")
comment = MomentComment(
id=str(uuid.uuid4()),
moment_id=moment_id,
user_id=user_id,
content=content,
reply_to_id=reply_to_id,
)
self.db.add(comment)
await self.db.flush()
# 返回带用户信息的评论
user_result = await self.db.execute(select(User).where(User.id == user_id))
user = user_result.scalars().first()
reply_to_username = None
if reply_to_id:
rt_result = await self.db.execute(select(User).where(User.id == comment.reply_to_id))
# reply_to_id 是评论 ID,需要找到评论者的 user
rt_comment = await self.db.execute(
select(MomentComment).where(MomentComment.id == reply_to_id)
)
rt_c = rt_comment.scalars().first()
if rt_c:
rt_user = await self.db.execute(select(User).where(User.id == rt_c.user_id))
rt_u = rt_user.scalars().first()
reply_to_username = rt_u.username if rt_u else None
return {
"id": comment.id,
"moment_id": moment_id,
"user_id": user_id,
"username": user.username if user else "未知",
"nickname": user.nickname if user else None,
"avatar_url": user.avatar_url if user else None,
"content": content,
"reply_to_id": reply_to_id,
"reply_to_username": reply_to_username,
"created_at": comment.created_at,
}
async def get_comments(self, moment_id: str) -> list[dict]:
"""获取评论列表"""
result = await self.db.execute(
select(MomentComment).where(
MomentComment.moment_id == moment_id
).order_by(MomentComment.created_at.asc())
)
comments = []
for c in result.scalars().all():
user_result = await self.db.execute(select(User).where(User.id == c.user_id))
user = user_result.scalars().first()
reply_to_username = None
if c.reply_to_id:
rt_comment = await self.db.execute(
select(MomentComment).where(MomentComment.id == c.reply_to_id)
)
rt_c = rt_comment.scalars().first()
if rt_c:
rt_user = await self.db.execute(select(User).where(User.id == rt_c.user_id))
rt_u = rt_user.scalars().first()
reply_to_username = rt_u.username if rt_u else None
comments.append({
"id": c.id,
"moment_id": moment_id,
"user_id": c.user_id,
"username": user.username if user else "未知",
"nickname": user.nickname if user else None,
"avatar_url": user.avatar_url if user else None,
"content": c.content,
"reply_to_id": c.reply_to_id,
"reply_to_username": reply_to_username,
"created_at": c.created_at,
})
return comments
async def delete_comment(self, comment_id: str, user_id: str):
"""删除评论(仅作者)"""
result = await self.db.execute(select(MomentComment).where(MomentComment.id == comment_id))
comment = result.scalars().first()
if not comment:
raise ValueError("评论不存在")
if comment.user_id != user_id:
raise ValueError("只能删除自己的评论")
await self.db.delete(comment)
async def _moment_to_dict(self, moment: Moment, viewer_id: str | None) -> dict:
"""将 Moment ORM 对象转为前端需要的字典"""
user_result = await self.db.execute(select(User).where(User.id == moment.user_id))
user = user_result.scalars().first()
# 点赞数
like_count_result = await self.db.execute(
select(func.count(MomentLike.id)).where(MomentLike.moment_id == moment.id)
)
like_count = like_count_result.scalar() or 0
# 是否已点赞
is_liked = False
if viewer_id:
like_result = await self.db.execute(
select(MomentLike).where(
MomentLike.moment_id == moment.id,
MomentLike.user_id == viewer_id,
)
)
is_liked = like_result.scalars().first() is not None
# 评论数
comment_count_result = await self.db.execute(
select(func.count(MomentComment.id)).where(MomentComment.moment_id == moment.id)
)
comment_count = comment_count_result.scalar() or 0
# 解析图片
images = []
if moment.images:
try:
images = json.loads(moment.images)
except:
pass
return {
"id": moment.id,
"user_id": moment.user_id,
"username": user.username if user else "未知",
"nickname": user.nickname if user else None,
"avatar_url": user.avatar_url if user else None,
"content": moment.content,
"images": images,
"visibility": moment.visibility,
"like_count": like_count,
"is_liked": is_liked,
"comment_count": comment_count,
"created_at": moment.created_at,
}
async def _get_friend_ids(self, user_id: str) -> list[str]:
"""获取好友ID列表"""
result = await self.db.execute(
select(Friend.friend_user_id).where(Friend.user_id == user_id)
)
return [r[0] for r in result.all()]
async def _are_friends(self, user1_id: str, user2_id: str) -> bool:
"""检查两人是否是好友"""
result = await self.db.execute(
select(Friend).where(
Friend.user_id == user1_id,
Friend.friend_user_id == user2_id,
)
)
return result.scalars().first() is not None
+29 -1
View File
@@ -6,6 +6,7 @@ from sqlalchemy import select, or_, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.utils.security import hash_password, verify_password
class UserService:
@@ -23,11 +24,12 @@ class UserService:
return result.scalars().first()
async def search_users(self, query: str, current_user_id: str, limit: int = 20) -> list[User]:
"""搜索用户"""
"""搜索用户(支持用户名、昵称、邮箱)"""
result = await self.db.execute(
select(User).where(
or_(
User.username.ilike(f"%{query}%"),
User.nickname.ilike(f"%{query}%"),
User.email.ilike(f"%{query}%"),
),
User.id != current_user_id,
@@ -49,6 +51,32 @@ class UserService:
user.updated_at = datetime.utcnow()
return user
async def change_password(self, user_id: str, old_password: str, new_password: str):
"""修改密码"""
user = await self.get_by_id(user_id)
if not user:
raise ValueError("用户不存在")
if not verify_password(old_password, user.password_hash):
raise ValueError("原密码错误")
user.password_hash = hash_password(new_password)
user.updated_at = datetime.utcnow()
async def change_email(self, user_id: str, new_email: str, password: str):
"""更换绑定邮箱"""
user = await self.get_by_id(user_id)
if not user:
raise ValueError("用户不存在")
if not verify_password(password, user.password_hash):
raise ValueError("密码错误")
# 检查邮箱是否已被使用
result = await self.db.execute(
select(User).where(User.email == new_email, User.id != user_id)
)
if result.scalars().first():
raise ValueError("该邮箱已被其他账号使用")
user.email = new_email
user.updated_at = datetime.utcnow()
async def update_status(self, user_id: str, status: str):
"""更新用户在线状态"""
user = await self.get_by_id(user_id)