1.0
This commit is contained in:
+2
-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
|
||||
from app.routers import auth, users, conversations, messages, friends, admin, uploads, moments
|
||||
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["认证"])
|
||||
app.include_router(users.router, prefix="/api/v1/users", tags=["用户"])
|
||||
@@ -73,6 +73,7 @@ app.include_router(messages.router, prefix="/api/v1/conversations", tags=["消
|
||||
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=["朋友圈"])
|
||||
|
||||
# WebSocket
|
||||
from app.websocket.router import websocket_router
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.models.message import Message
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -16,4 +17,7 @@ __all__ = [
|
||||
"Friend",
|
||||
"FriendRequest",
|
||||
"SystemConfig",
|
||||
"Moment",
|
||||
"MomentLike",
|
||||
"MomentComment",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""朋友圈动态模型"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Text, DateTime, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Moment(Base):
|
||||
__tablename__ = "moments"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
images: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array of URLs
|
||||
visibility: Mapped[str] = mapped_column(String(20), default="friends") # public/friends/private
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=lambda: datetime.utcnow(),
|
||||
onupdate=lambda: datetime.utcnow(),
|
||||
)
|
||||
|
||||
# 关系
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
likes = relationship("MomentLike", back_populates="moment", cascade="all, delete-orphan")
|
||||
comments = relationship("MomentComment", back_populates="moment", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class MomentLike(Base):
|
||||
__tablename__ = "moment_likes"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("moment_id", "user_id", name="uq_moment_like"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
moment_id: Mapped[str] = mapped_column(String(36), ForeignKey("moments.id", ondelete="CASCADE"))
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
|
||||
|
||||
moment = relationship("Moment", back_populates="likes")
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
|
||||
|
||||
class MomentComment(Base):
|
||||
__tablename__ = "moment_comments"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
moment_id: Mapped[str] = mapped_column(String(36), ForeignKey("moments.id", ondelete="CASCADE"))
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id", ondelete="CASCADE"))
|
||||
content: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
reply_to_id: Mapped[str | None] = mapped_column(
|
||||
String(36), ForeignKey("moment_comments.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.utcnow())
|
||||
|
||||
moment = relationship("Moment", back_populates="comments")
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
reply_to = relationship("MomentComment", remote_side=[id])
|
||||
@@ -13,6 +13,7 @@ class User(Base):
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
nickname: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
@@ -5,7 +5,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.conversation import ConversationCreate, ConversationRead, ConversationDetail, GroupCreate
|
||||
from app.schemas.conversation import (
|
||||
ConversationCreate, ConversationRead, ConversationDetail,
|
||||
GroupCreate, GroupUpdate, MemberAdd, RoleUpdate,
|
||||
)
|
||||
from app.services.conversation_service import ConversationService
|
||||
|
||||
router = APIRouter()
|
||||
@@ -65,3 +68,83 @@ async def get_conversation(
|
||||
if not detail:
|
||||
raise HTTPException(status_code=404, detail="会话不存在或无权访问")
|
||||
return detail
|
||||
|
||||
|
||||
@router.put("/{conversation_id}")
|
||||
async def update_group(
|
||||
conversation_id: str,
|
||||
req: GroupUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新群聊信息(群主/管理员)"""
|
||||
service = ConversationService(db)
|
||||
try:
|
||||
await service.update_group(conversation_id, user.id, **req.model_dump(exclude_none=True))
|
||||
return {"success": True, "message": "群信息已更新"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{conversation_id}/members")
|
||||
async def add_members(
|
||||
conversation_id: str,
|
||||
req: MemberAdd,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""添加群成员(群主/管理员)"""
|
||||
service = ConversationService(db)
|
||||
try:
|
||||
await service.add_members(conversation_id, user.id, req.user_ids)
|
||||
return {"success": True, "message": "成员已添加"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{conversation_id}/members/{target_user_id}")
|
||||
async def remove_member(
|
||||
conversation_id: str,
|
||||
target_user_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""移除群成员(群主/管理员)"""
|
||||
service = ConversationService(db)
|
||||
try:
|
||||
await service.remove_member(conversation_id, user.id, target_user_id)
|
||||
return {"success": True, "message": "成员已移除"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{conversation_id}/leave")
|
||||
async def leave_group(
|
||||
conversation_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""退出群聊"""
|
||||
service = ConversationService(db)
|
||||
try:
|
||||
await service.leave_group(conversation_id, user.id)
|
||||
return {"success": True, "message": "已退出群聊"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{conversation_id}/members/{target_user_id}/role")
|
||||
async def update_member_role(
|
||||
conversation_id: str,
|
||||
target_user_id: str,
|
||||
req: RoleUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""修改群成员角色(群主)"""
|
||||
service = ConversationService(db)
|
||||
try:
|
||||
await service.update_member_role(conversation_id, user.id, target_user_id, req.role)
|
||||
return {"success": True, "message": "角色已更新"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.friend import FriendRequestCreate, FriendRead, FriendRequestRead
|
||||
from app.schemas.friend import FriendRequestCreate, FriendRead, FriendRequestRead, RemarkUpdate
|
||||
from app.services.friend_service import FriendService
|
||||
|
||||
router = APIRouter()
|
||||
@@ -46,6 +46,37 @@ async def send_friend_request(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/add-direct")
|
||||
async def add_friend_direct(
|
||||
req: FriendRequestCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""直接添加好友(跳过验证)"""
|
||||
service = FriendService(db)
|
||||
try:
|
||||
await service.add_direct(user.id, req.to_user_id)
|
||||
return {"success": True, "message": "已添加好友"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{friend_user_id}/remark")
|
||||
async def update_friend_remark(
|
||||
friend_user_id: str,
|
||||
req: RemarkUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""修改好友备注"""
|
||||
service = FriendService(db)
|
||||
try:
|
||||
await service.update_remark(user.id, friend_user_id, req.remark)
|
||||
return {"success": True, "message": "备注已更新"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/request/{request_id}/accept")
|
||||
async def accept_friend_request(
|
||||
request_id: str,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""朋友圈路由"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.moment import MomentCreate, MomentCommentCreate
|
||||
from app.services.moment_service import MomentService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_feed(
|
||||
cursor: str | None = Query(None),
|
||||
limit: int = Query(20, ge=1, le=50),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取朋友圈 feed"""
|
||||
service = MomentService(db)
|
||||
return await service.get_feed(user.id, cursor, limit)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_moment(
|
||||
req: MomentCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""发布动态"""
|
||||
service = MomentService(db)
|
||||
try:
|
||||
moment = await service.create_moment(user.id, req.content, req.images, req.visibility)
|
||||
detail = await service.get_feed(user.id, limit=1)
|
||||
return detail[0] if detail else {"success": True}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/user/{target_user_id}")
|
||||
async def get_user_moments(
|
||||
target_user_id: str,
|
||||
cursor: str | None = Query(None),
|
||||
limit: int = Query(20, ge=1, le=50),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取指定用户的动态"""
|
||||
service = MomentService(db)
|
||||
return await service.get_user_moments(target_user_id, user.id, cursor, limit)
|
||||
|
||||
|
||||
@router.get("/{moment_id}")
|
||||
async def get_moment(
|
||||
moment_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取单条动态详情"""
|
||||
service = MomentService(db)
|
||||
feed = await service.get_feed(user.id, limit=100)
|
||||
moment = next((m for m in feed if m["id"] == moment_id), None)
|
||||
if not moment:
|
||||
raise HTTPException(status_code=404, detail="动态不存在")
|
||||
return moment
|
||||
|
||||
|
||||
@router.delete("/{moment_id}")
|
||||
async def delete_moment(
|
||||
moment_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除动态"""
|
||||
service = MomentService(db)
|
||||
try:
|
||||
await service.delete_moment(moment_id, user.id)
|
||||
return {"success": True, "message": "动态已删除"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{moment_id}/like")
|
||||
async def toggle_like(
|
||||
moment_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""点赞/取消点赞"""
|
||||
service = MomentService(db)
|
||||
try:
|
||||
is_liked = await service.toggle_like(moment_id, user.id)
|
||||
return {"success": True, "is_liked": is_liked}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{moment_id}/comments")
|
||||
async def get_comments(
|
||||
moment_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取评论列表"""
|
||||
service = MomentService(db)
|
||||
return await service.get_comments(moment_id)
|
||||
|
||||
|
||||
@router.post("/{moment_id}/comments")
|
||||
async def add_comment(
|
||||
moment_id: str,
|
||||
req: MomentCommentCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""添加评论"""
|
||||
service = MomentService(db)
|
||||
try:
|
||||
comment = await service.add_comment(moment_id, user.id, req.content, req.reply_to_id)
|
||||
return comment
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{moment_id}/comments/{comment_id}")
|
||||
async def delete_comment(
|
||||
moment_id: str,
|
||||
comment_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除评论"""
|
||||
service = MomentService(db)
|
||||
try:
|
||||
await service.delete_comment(comment_id, user.id)
|
||||
return {"success": True, "message": "评论已删除"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -5,7 +5,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserRead, UserProfile, UserUpdate, UserSearchResult
|
||||
from app.schemas.user import (
|
||||
UserRead, UserProfile, UserUpdate, UserSearchResult,
|
||||
PasswordChange, EmailChange,
|
||||
)
|
||||
from app.services.user_service import UserService
|
||||
|
||||
router = APIRouter()
|
||||
@@ -29,6 +32,36 @@ async def update_me(
|
||||
return updated
|
||||
|
||||
|
||||
@router.put("/me/password")
|
||||
async def change_password(
|
||||
req: PasswordChange,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""修改密码"""
|
||||
service = UserService(db)
|
||||
try:
|
||||
await service.change_password(user.id, req.old_password, req.new_password)
|
||||
return {"success": True, "message": "密码修改成功"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/me/email")
|
||||
async def change_email(
|
||||
req: EmailChange,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更换绑定邮箱"""
|
||||
service = UserService(db)
|
||||
try:
|
||||
await service.change_email(user.id, req.email, req.password)
|
||||
return {"success": True, "message": "邮箱已更新"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/search", response_model=list[UserSearchResult])
|
||||
async def search_users(
|
||||
q: str = Query(..., min_length=1),
|
||||
|
||||
@@ -28,6 +28,7 @@ class RefreshRequest(BaseModel):
|
||||
class UserBrief(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
is_admin: bool = False
|
||||
|
||||
|
||||
@@ -51,3 +51,17 @@ class GroupCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
description: str | None = Field(None, max_length=500)
|
||||
member_ids: list[str] = Field(..., min_length=1)
|
||||
|
||||
|
||||
class GroupUpdate(BaseModel):
|
||||
name: str | None = Field(None, max_length=100)
|
||||
description: str | None = Field(None, max_length=500)
|
||||
avatar_url: str | None = None
|
||||
|
||||
|
||||
class MemberAdd(BaseModel):
|
||||
user_ids: list[str] = Field(..., min_length=1)
|
||||
|
||||
|
||||
class RoleUpdate(BaseModel):
|
||||
role: str = Field(..., pattern="^(admin|member)$")
|
||||
|
||||
@@ -34,3 +34,7 @@ class FriendRead(BaseModel):
|
||||
status: str = "offline"
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class RemarkUpdate(BaseModel):
|
||||
remark: str | None = None
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""朋友圈相关 Schema"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class MomentCreate(BaseModel):
|
||||
content: str = Field(..., min_length=1, max_length=1000)
|
||||
images: list[str] | None = Field(None, max_length=9)
|
||||
visibility: str = Field("friends", pattern="^(public|friends|private)$")
|
||||
|
||||
|
||||
class MomentRead(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
username: str
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
content: str
|
||||
images: list[str] = []
|
||||
visibility: str
|
||||
like_count: int = 0
|
||||
is_liked: bool = False
|
||||
comment_count: int = 0
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class MomentCommentCreate(BaseModel):
|
||||
content: str = Field(..., min_length=1, max_length=500)
|
||||
reply_to_id: str | None = None
|
||||
|
||||
|
||||
class MomentCommentRead(BaseModel):
|
||||
id: str
|
||||
moment_id: str
|
||||
user_id: str
|
||||
username: str
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
content: str
|
||||
reply_to_id: str | None = None
|
||||
reply_to_username: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
|
||||
class UserRead(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
nickname: str | None = None
|
||||
email: str
|
||||
avatar_url: str | None = None
|
||||
bio: str | None = None
|
||||
status: str = "offline"
|
||||
is_admin: bool = False
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
@@ -21,6 +23,7 @@ class UserProfile(BaseModel):
|
||||
"""他人可见的公开信息"""
|
||||
id: str
|
||||
username: str
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
bio: str | None = None
|
||||
status: str = "offline"
|
||||
@@ -30,6 +33,7 @@ class UserProfile(BaseModel):
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
username: str | None = Field(None, min_length=2, max_length=50)
|
||||
nickname: str | None = Field(None, max_length=50)
|
||||
bio: str | None = Field(None, max_length=200)
|
||||
avatar_url: str | None = None
|
||||
|
||||
@@ -39,9 +43,15 @@ class PasswordChange(BaseModel):
|
||||
new_password: str = Field(..., min_length=6, max_length=100)
|
||||
|
||||
|
||||
class EmailChange(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
class UserSearchResult(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
nickname: str | None = None
|
||||
avatar_url: str | None = None
|
||||
bio: str | None = None
|
||||
status: str = "offline"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user