Files
012-kaopeilian/backend/app/services/user_service.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

424 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
用户服务
"""
from datetime import datetime
from typing import Any, Dict, List, Optional
from sqlalchemy import and_, or_, select, func
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.core.exceptions import ConflictError, NotFoundError
from app.core.logger import logger
from app.core.security import get_password_hash, verify_password
from app.models.user import Team, User, user_teams
from app.schemas.user import UserCreate, UserFilter, UserUpdate
from app.services.base_service import BaseService
class UserService(BaseService[User]):
"""用户服务"""
def __init__(self, db: AsyncSession):
super().__init__(User)
self.db = db
async def get_by_id(self, user_id: int) -> Optional[User]:
"""根据ID获取用户"""
result = await self.db.execute(
select(User).where(User.id == user_id, User.is_deleted == False)
)
return result.scalar_one_or_none()
async def get_by_username(self, username: str) -> Optional[User]:
"""根据用户名获取用户"""
result = await self.db.execute(
select(User).where(
User.username == username,
User.is_deleted == False,
)
)
return result.scalar_one_or_none()
async def get_by_email(self, email: str) -> Optional[User]:
"""根据邮箱获取用户"""
result = await self.db.execute(
select(User).where(
User.email == email,
User.is_deleted == False,
)
)
return result.scalar_one_or_none()
async def get_by_phone(self, phone: str) -> Optional[User]:
"""根据手机号获取用户"""
result = await self.db.execute(
select(User).where(
User.phone == phone,
User.is_deleted == False,
)
)
return result.scalar_one_or_none()
async def _check_username_exists_all(self, username: str) -> Optional[User]:
"""
检查用户名是否已存在(包括已删除的用户)
用于创建用户时检查唯一性约束
"""
result = await self.db.execute(
select(User).where(User.username == username)
)
return result.scalar_one_or_none()
async def _check_email_exists_all(self, email: str) -> Optional[User]:
"""
检查邮箱是否已存在(包括已删除的用户)
用于创建用户时检查唯一性约束
"""
result = await self.db.execute(
select(User).where(User.email == email)
)
return result.scalar_one_or_none()
async def _check_phone_exists_all(self, phone: str) -> Optional[User]:
"""
检查手机号是否已存在(包括已删除的用户)
用于创建用户时检查唯一性约束
"""
result = await self.db.execute(
select(User).where(User.phone == phone)
)
return result.scalar_one_or_none()
async def create_user(
self,
*,
obj_in: UserCreate,
created_by: Optional[int] = None,
) -> User:
"""创建用户"""
# 检查用户名是否已存在(包括已删除的用户,防止唯一键冲突)
existing_user = await self._check_username_exists_all(obj_in.username)
if existing_user:
if existing_user.is_deleted:
raise ConflictError(f"用户名 {obj_in.username} 已被使用(历史用户),请更换其他用户名")
else:
raise ConflictError(f"用户名 {obj_in.username} 已存在")
# 检查邮箱是否已存在(包括已删除的用户)
if obj_in.email:
existing_email = await self._check_email_exists_all(obj_in.email)
if existing_email:
if existing_email.is_deleted:
raise ConflictError(f"邮箱 {obj_in.email} 已被使用(历史用户),请更换其他邮箱")
else:
raise ConflictError(f"邮箱 {obj_in.email} 已存在")
# 检查手机号是否已存在(包括已删除的用户)
if obj_in.phone:
existing_phone = await self._check_phone_exists_all(obj_in.phone)
if existing_phone:
if existing_phone.is_deleted:
raise ConflictError(f"手机号 {obj_in.phone} 已被使用(历史用户),请更换其他手机号")
else:
raise ConflictError(f"手机号 {obj_in.phone} 已存在")
# 创建用户数据
user_data = obj_in.model_dump(exclude={"password"})
user_data["hashed_password"] = get_password_hash(obj_in.password)
# 注意User模型不包含created_by字段该信息记录在日志中
# user_data["created_by"] = created_by
try:
# 创建用户
user = await self.create(db=self.db, obj_in=user_data)
except IntegrityError as e:
# 捕获数据库唯一键冲突异常,返回友好错误信息
await self.db.rollback()
error_msg = str(e.orig) if e.orig else str(e)
logger.warning(
"创建用户时发生唯一键冲突",
username=obj_in.username,
email=obj_in.email,
error=error_msg,
)
if "username" in error_msg.lower():
raise ConflictError(f"用户名 {obj_in.username} 已被占用,请更换其他用户名")
elif "email" in error_msg.lower():
raise ConflictError(f"邮箱 {obj_in.email} 已被占用,请更换其他邮箱")
elif "phone" in error_msg.lower():
raise ConflictError(f"手机号 {obj_in.phone} 已被占用,请更换其他手机号")
else:
raise ConflictError(f"创建用户失败:数据冲突,请检查用户名、邮箱或手机号是否重复")
# 记录日志
logger.info(
"用户创建成功",
user_id=user.id,
username=user.username,
role=user.role,
created_by=created_by,
)
return user
async def update_user(
self,
*,
user_id: int,
obj_in: UserUpdate,
updated_by: Optional[int] = None,
) -> User:
"""更新用户"""
user = await self.get_by_id(user_id)
if not user:
raise NotFoundError("用户不存在")
# 如果更新邮箱,检查是否已存在
if obj_in.email and obj_in.email != user.email:
if await self.get_by_email(obj_in.email):
raise ConflictError(f"邮箱 {obj_in.email} 已存在")
# 如果更新手机号,检查是否已存在
if obj_in.phone and obj_in.phone != user.phone:
if await self.get_by_phone(obj_in.phone):
raise ConflictError(f"手机号 {obj_in.phone} 已存在")
# 更新用户数据
update_data = obj_in.model_dump(exclude_unset=True)
update_data["updated_by"] = updated_by
user = await self.update(db=self.db, db_obj=user, obj_in=update_data)
# 记录日志
logger.info(
"用户更新成功",
user_id=user.id,
username=user.username,
updated_fields=list(update_data.keys()),
updated_by=updated_by,
)
return user
async def update_password(
self,
*,
user_id: int,
old_password: str,
new_password: str,
) -> User:
"""更新密码"""
user = await self.get_by_id(user_id)
if not user:
raise NotFoundError("用户不存在")
# 验证旧密码
if not verify_password(old_password, user.hashed_password):
raise ConflictError("旧密码错误")
# 更新密码
update_data = {
"hashed_password": get_password_hash(new_password),
"password_changed_at": datetime.now(),
}
user = await self.update(db=self.db, db_obj=user, obj_in=update_data)
# 记录日志
logger.info(
"用户密码更新成功",
user_id=user.id,
username=user.username,
)
return user
async def update_last_login(self, user_id: int) -> None:
"""更新最后登录时间"""
user = await self.get_by_id(user_id)
if user:
await self.update(
db=self.db,
db_obj=user,
obj_in={"last_login_at": datetime.now()},
)
async def get_users_with_filter(
self,
*,
skip: int = 0,
limit: int = 100,
filter_params: UserFilter,
) -> tuple[List[User], int]:
"""根据筛选条件获取用户列表"""
# 构建筛选条件
filters = [User.is_deleted == False]
if filter_params.role:
filters.append(User.role == filter_params.role)
if filter_params.is_active is not None:
filters.append(User.is_active == filter_params.is_active)
if filter_params.keyword:
keyword = f"%{filter_params.keyword}%"
filters.append(
or_(
User.username.like(keyword),
User.email.like(keyword),
User.full_name.like(keyword),
)
)
if filter_params.team_id:
# 通过团队ID筛选用户
subquery = select(user_teams.c.user_id).where(
user_teams.c.team_id == filter_params.team_id
)
filters.append(User.id.in_(subquery))
# 构建查询
query = select(User).where(and_(*filters))
# 获取用户列表
users = await self.get_multi(self.db, skip=skip, limit=limit, query=query)
# 获取总数
count_query = select(func.count(User.id)).where(and_(*filters))
count_result = await self.db.execute(count_query)
total = count_result.scalar()
return users, total
async def add_user_to_team(
self,
*,
user_id: int,
team_id: int,
role: str = "member",
) -> None:
"""将用户添加到团队"""
# 检查用户是否存在
user = await self.get_by_id(user_id)
if not user:
raise NotFoundError("用户不存在")
# 检查团队是否存在
team_result = await self.db.execute(
select(Team).where(Team.id == team_id, Team.is_deleted == False)
)
team = team_result.scalar_one_or_none()
if not team:
raise NotFoundError("团队不存在")
# 检查是否已在团队中
existing = await self.db.execute(
select(user_teams).where(
user_teams.c.user_id == user_id,
user_teams.c.team_id == team_id,
)
)
if existing.first():
raise ConflictError("用户已在该团队中")
# 添加到团队
await self.db.execute(
user_teams.insert().values(
user_id=user_id,
team_id=team_id,
role=role,
joined_at=datetime.now(),
)
)
await self.db.commit()
# 记录日志
logger.info(
"用户加入团队",
user_id=user_id,
username=user.username,
team_id=team_id,
team_name=team.name,
role=role,
)
async def remove_user_from_team(
self,
*,
user_id: int,
team_id: int,
) -> None:
"""从团队中移除用户"""
# 删除关联
result = await self.db.execute(
user_teams.delete().where(
user_teams.c.user_id == user_id,
user_teams.c.team_id == team_id,
)
)
if result.rowcount == 0:
raise NotFoundError("用户不在该团队中")
await self.db.commit()
# 记录日志
logger.info(
"用户离开团队",
user_id=user_id,
team_id=team_id,
)
async def soft_delete(self, *, db_obj: User) -> User:
"""
软删除用户
Args:
db_obj: 用户对象
Returns:
软删除后的用户对象
"""
db_obj.is_deleted = True
db_obj.deleted_at = datetime.now()
self.db.add(db_obj)
await self.db.commit()
await self.db.refresh(db_obj)
logger.info(
"用户软删除成功",
user_id=db_obj.id,
username=db_obj.username,
)
return db_obj
async def authenticate(
self,
*,
username: str,
password: str,
) -> Optional[User]:
"""用户认证"""
# 尝试用户名登录
user = await self.get_by_username(username)
# 尝试邮箱登录
if not user:
user = await self.get_by_email(username)
# 尝试手机号登录
if not user:
user = await self.get_by_phone(username)
if not user:
return None
# 验证密码
if not verify_password(password, user.hashed_password):
return None
return user