feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
423
backend/app/services/user_service.py
Normal file
423
backend/app/services/user_service.py
Normal file
@@ -0,0 +1,423 @@
|
||||
"""
|
||||
用户服务
|
||||
"""
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user