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,201 @@
"""
认证API路由示例代码
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db
from app.core.exceptions import UnauthorizedError, ConflictError
from app.schemas.auth import UserRegister, Token, PasswordReset
from app.schemas.base import ResponseModel
from app.services.auth_service import AuthService
router = APIRouter(prefix="/auth", tags=["认证"])
@router.post("/login", response_model=ResponseModel[Token], summary="用户登录")
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db)
):
"""
用户登录接口
- **username**: 用户名或邮箱
- **password**: 密码
返回访问令牌和刷新令牌
"""
auth_service = AuthService(db)
# 验证用户
user = await auth_service.authenticate_user(
username=form_data.username,
password=form_data.password
)
if not user:
raise UnauthorizedError("用户名或密码错误")
# 创建Token
token = await auth_service.create_tokens(user)
return ResponseModel(
code=200,
message="登录成功",
data=token
)
@router.post("/register", response_model=ResponseModel[Token], status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserRegister,
db: AsyncSession = Depends(get_db)
):
"""
用户注册接口
注册成功后自动登录返回Token
"""
# 验证密码一致性
if user_data.password != user_data.confirm_password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="两次输入的密码不一致"
)
auth_service = AuthService(db)
try:
# 创建用户
user = await auth_service.create_user(user_data)
# 自动登录
token = await auth_service.create_tokens(user)
return ResponseModel(
code=201,
message="注册成功",
data=token
)
except ConflictError as e:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=str(e)
)
@router.post("/logout", response_model=ResponseModel)
async def logout(
token: str = Depends(get_current_token),
db: AsyncSession = Depends(get_db)
):
"""
用户登出接口
将当前Token加入黑名单
"""
auth_service = AuthService(db)
await auth_service.logout(token)
return ResponseModel(
code=200,
message="登出成功"
)
@router.post("/refresh", response_model=ResponseModel[Token])
async def refresh_token(
refresh_token: str,
db: AsyncSession = Depends(get_db)
):
"""
刷新访问令牌
使用刷新令牌获取新的访问令牌
"""
auth_service = AuthService(db)
try:
token = await auth_service.refresh_access_token(refresh_token)
return ResponseModel(
code=200,
message="刷新成功",
data=token
)
except UnauthorizedError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="刷新令牌无效或已过期"
)
@router.post("/reset-password/request", response_model=ResponseModel)
async def request_password_reset(
email: str,
db: AsyncSession = Depends(get_db)
):
"""
请求重置密码
向用户邮箱发送重置链接
"""
auth_service = AuthService(db)
# 查找用户
user = await auth_service.get_user_by_email(email)
if not user:
# 为了安全,即使用户不存在也返回成功
return ResponseModel(
code=200,
message="如果邮箱存在,重置链接已发送"
)
# 生成重置令牌
reset_token = await auth_service.create_password_reset_token(user)
# TODO: 发送邮件
# await send_reset_email(email, reset_token)
return ResponseModel(
code=200,
message="如果邮箱存在,重置链接已发送"
)
@router.post("/reset-password/confirm", response_model=ResponseModel)
async def reset_password(
reset_data: PasswordReset,
db: AsyncSession = Depends(get_db)
):
"""
确认重置密码
使用重置令牌设置新密码
"""
auth_service = AuthService(db)
try:
await auth_service.reset_password(
token=reset_data.token,
new_password=reset_data.new_password
)
return ResponseModel(
code=200,
message="密码重置成功"
)
except UnauthorizedError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="重置令牌无效或已过期"
)
# 辅助函数
async def get_current_token(
authorization: str = Depends(oauth2_scheme)
) -> str:
"""获取当前请求的Token"""
return authorization

View File

@@ -0,0 +1,163 @@
"""
认证服务示例代码
"""
from typing import Optional
from datetime import datetime, timedelta
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import HTTPException, status
from app.core.security import verify_password, get_password_hash, create_access_token, create_refresh_token
from app.core.exceptions import UnauthorizedError, ConflictError, ForbiddenError
from app.core.logger import logger
from app.models.user import User
from app.schemas.auth import UserRegister, Token
class AuthService:
"""认证服务类"""
def __init__(self, db: AsyncSession):
self.db = db
async def authenticate_user(self, username: str, password: str) -> Optional[User]:
"""
验证用户身份
Args:
username: 用户名或邮箱
password: 密码
Returns:
验证成功返回User对象失败返回None
"""
# 查询用户(支持用户名或邮箱登录)
query = select(User).where(
(User.username == username) | (User.email == username)
)
result = await self.db.execute(query)
user = result.scalar_one_or_none()
if not user:
logger.warning("登录失败:用户不存在", username=username)
return None
# 验证密码
if not verify_password(password, user.password_hash):
logger.warning("登录失败:密码错误", user_id=user.id, username=username)
# TODO: 记录失败次数,实现账号锁定
return None
# 检查账号状态
if not user.is_active:
logger.warning("登录失败:账号已禁用", user_id=user.id, username=username)
raise ForbiddenError("账号已被禁用")
logger.info("用户登录成功", user_id=user.id, username=user.username)
return user
async def create_user(self, user_data: UserRegister) -> User:
"""
创建新用户
Args:
user_data: 用户注册数据
Returns:
创建的用户对象
Raises:
ConflictError: 用户名或邮箱已存在
"""
# 检查用户名是否存在
query = select(User).where(User.username == user_data.username)
result = await self.db.execute(query)
if result.scalar_one_or_none():
raise ConflictError("用户名已存在")
# 检查邮箱是否存在
query = select(User).where(User.email == user_data.email)
result = await self.db.execute(query)
if result.scalar_one_or_none():
raise ConflictError("邮箱已存在")
# 创建用户
user = User(
username=user_data.username,
email=user_data.email,
password_hash=get_password_hash(user_data.password),
is_active=True,
is_superuser=False,
role="trainee" # 默认角色为学员
)
self.db.add(user)
await self.db.commit()
await self.db.refresh(user)
logger.info("用户注册成功", user_id=user.id, username=user.username)
return user
async def create_tokens(self, user: User) -> Token:
"""
为用户创建访问令牌和刷新令牌
Args:
user: 用户对象
Returns:
Token对象
"""
# 创建访问令牌
access_token = create_access_token(
subject=user.id,
role=user.role,
username=user.username
)
# 创建刷新令牌
refresh_token = create_refresh_token(subject=user.id)
return Token(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=1800, # 30分钟
user={
"id": user.id,
"username": user.username,
"email": user.email,
"role": user.role
}
)
async def logout(self, token: str) -> None:
"""
用户登出将token加入黑名单
Args:
token: 要失效的token
"""
# TODO: 将token加入Redis黑名单
# redis_key = f"blacklist:{token}"
# await redis.setex(redis_key, 3600, "1") # 设置1小时过期
logger.info("用户登出成功")
async def refresh_access_token(self, refresh_token: str) -> Token:
"""
使用刷新令牌获取新的访问令牌
Args:
refresh_token: 刷新令牌
Returns:
新的Token对象
"""
# TODO: 验证刷新令牌
# TODO: 检查是否在黑名单
# TODO: 生成新的访问令牌
# TODO: 可选 - 轮换刷新令牌
pass