feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View 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