🎉初次提交
This commit is contained in:
77
router/aac/__init__.py
Normal file
77
router/aac/__init__.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from provider.aufe.aac import AACClient
|
||||
from provider.aufe.aac.depends import get_aac_client
|
||||
from provider.loveac.authme import AuthmeResponse
|
||||
from router.aac.model import ScoreInfoResponse, ScoreListResponse
|
||||
from router.common_model import ErrorResponse
|
||||
|
||||
|
||||
aac_router = APIRouter(prefix="/api/v1/aac")
|
||||
|
||||
|
||||
@aac_router.post(
|
||||
"/fetch_score_info",
|
||||
summary="获取爱安财总分信息",
|
||||
response_model=ScoreInfoResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_score_info(client: AACClient = Depends(get_aac_client)):
|
||||
"""获取爱安财系统的总分信息"""
|
||||
try:
|
||||
result = await client.fetch_score_info()
|
||||
|
||||
# 检查是否是AuthmeResponse(认证错误)
|
||||
if isinstance(result, AuthmeResponse):
|
||||
return result
|
||||
|
||||
# 使用新的错误检测机制
|
||||
response = ScoreInfoResponse.from_data(
|
||||
data=result,
|
||||
success_message="爱安财总分信息获取成功",
|
||||
error_message="获取爱安财总分信息失败,网络请求多次重试后仍无法连接服务器,请稍后重试或联系管理员",
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return ErrorResponse(
|
||||
message=f"获取爱安财总分信息时发生系统错误:{str(e)}", code=500
|
||||
)
|
||||
|
||||
|
||||
@aac_router.post(
|
||||
"/fetch_score_list",
|
||||
summary="获取爱安财分数明细列表",
|
||||
response_model=ScoreListResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_score_list(
|
||||
client: AACClient = Depends(get_aac_client),
|
||||
):
|
||||
"""获取爱安财系统的分数明细列表"""
|
||||
try:
|
||||
result = await client.fetch_score_list()
|
||||
|
||||
# 检查是否是AuthmeResponse(认证错误)
|
||||
if isinstance(result, AuthmeResponse):
|
||||
return result
|
||||
|
||||
# 检查分数列表数据
|
||||
if result and hasattr(result, "data") and result.data:
|
||||
# 使用新的错误检测机制检查列表数据
|
||||
response = ScoreListResponse.from_data(
|
||||
data=result.data,
|
||||
success_message="爱安财分数明细获取成功",
|
||||
error_message="获取爱安财分数明细失败,网络请求多次重试后仍无法连接服务器,请稍后重试或联系管理员",
|
||||
)
|
||||
return response
|
||||
else:
|
||||
# 没有数据的情况
|
||||
return ScoreListResponse.error(
|
||||
message="暂无爱安财分数数据,请确认您的账户状态或稍后再试",
|
||||
code=404,
|
||||
data=[],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ErrorResponse(
|
||||
message=f"获取爱安财分数明细时发生系统错误:{str(e)}", code=500
|
||||
)
|
||||
16
router/aac/model.py
Normal file
16
router/aac/model.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from router.common_model import BaseResponse
|
||||
from provider.aufe.aac.model import LoveACScoreInfo, LoveACScoreCategory
|
||||
from typing import List
|
||||
|
||||
|
||||
# 统一响应模型
|
||||
class ScoreInfoResponse(BaseResponse[LoveACScoreInfo]):
|
||||
"""爱安财总分信息响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ScoreListResponse(BaseResponse[List[LoveACScoreCategory]]):
|
||||
"""爱安财分数明细列表响应"""
|
||||
|
||||
pass
|
||||
100
router/common_model.py
Normal file
100
router/common_model.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from typing import Generic, Optional, TypeVar, Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class BaseResponse(BaseModel, Generic[T]):
|
||||
"""通用响应模型基类"""
|
||||
|
||||
code: int = Field(200, description="状态码")
|
||||
message: str = Field("成功", description="提示信息")
|
||||
data: Optional[T] = Field(None, description="响应数据")
|
||||
|
||||
@classmethod
|
||||
def success(cls, data: T, message: str = "获取成功") -> "BaseResponse[T]":
|
||||
"""创建成功响应"""
|
||||
return cls(code=200, message=message, data=data)
|
||||
|
||||
@classmethod
|
||||
def error(
|
||||
cls, message: str = "请求失败", code: int = 500, data: Optional[T] = None
|
||||
) -> "BaseResponse[T]":
|
||||
"""创建错误响应"""
|
||||
return cls(code=code, message=message, data=data)
|
||||
|
||||
@classmethod
|
||||
def from_data(
|
||||
cls,
|
||||
data: Any,
|
||||
success_message: str = "获取成功",
|
||||
error_message: str = "网络请求失败,已进行多次重试",
|
||||
) -> "BaseResponse[T]":
|
||||
"""
|
||||
根据数据自动判断是否为错误模型并生成相应响应
|
||||
|
||||
Args:
|
||||
data: 要检查的数据
|
||||
success_message: 成功时的消息
|
||||
error_message: 失败时的消息
|
||||
|
||||
Returns:
|
||||
BaseResponse: 相应的响应模型
|
||||
"""
|
||||
if cls._is_error_data(data):
|
||||
return cls.error(message=error_message, code=500, data=data)
|
||||
else:
|
||||
return cls.success(data=data, message=success_message)
|
||||
|
||||
@staticmethod
|
||||
def _is_error_data(data: Any) -> bool:
|
||||
"""
|
||||
检测数据是否为错误模型
|
||||
|
||||
Args:
|
||||
data: 要检查的数据
|
||||
|
||||
Returns:
|
||||
bool: 如果是错误数据返回True
|
||||
"""
|
||||
if data is None:
|
||||
return True
|
||||
|
||||
# 检查是否有错误指示符
|
||||
if hasattr(data, "total_score") and data.total_score == -1.0:
|
||||
return True
|
||||
if hasattr(data, "completed_courses") and data.completed_courses == -1:
|
||||
return True
|
||||
if hasattr(data, "gpa") and data.gpa == -1.0:
|
||||
return True
|
||||
if hasattr(data, "plan_name") and data.plan_name == "请求失败,请稍后重试":
|
||||
return True
|
||||
if hasattr(data, "code") and data.code == -1:
|
||||
return True
|
||||
if hasattr(data, "total_count") and data.total_count == -1:
|
||||
return True
|
||||
if hasattr(data, "result") and data.result == "failed":
|
||||
return True
|
||||
if (
|
||||
hasattr(data, "can_select")
|
||||
and hasattr(data, "start_time")
|
||||
and data.start_time == "请求失败"
|
||||
):
|
||||
return True
|
||||
|
||||
# 检查列表类型的错误数据
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
first_item = data[0]
|
||||
if hasattr(first_item, "id") and first_item.id == "error":
|
||||
return True
|
||||
if hasattr(first_item, "type_name") and first_item.type_name == "请求失败":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ErrorResponse(BaseResponse[None]):
|
||||
"""专用错误响应模型"""
|
||||
|
||||
def __init__(self, message: str = "请求失败,请稍后重试", code: int = 500):
|
||||
super().__init__(code=code, message=message, data=None)
|
||||
118
router/invite/__init__.py
Normal file
118
router/invite/__init__.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from database.user import Invite, User
|
||||
from database.creator import get_db_session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from router.invite.model import (
|
||||
InviteRequest,
|
||||
RegisterRequest,
|
||||
InviteResponse,
|
||||
RegisterResponse,
|
||||
InviteTokenData,
|
||||
AuthMeData,
|
||||
)
|
||||
from provider.aufe.client import AUFEConnection
|
||||
from database.user import AuthME
|
||||
import secrets
|
||||
|
||||
invite_router = APIRouter(prefix="/api/v1/user")
|
||||
invite_tokens = []
|
||||
|
||||
|
||||
@invite_router.post("/veryfy_invite_code", summary="验证邀请码")
|
||||
async def verify_invite_code(
|
||||
data: InviteRequest,
|
||||
asyncsession: AsyncSession = Depends(get_db_session),
|
||||
) -> InviteResponse:
|
||||
"""
|
||||
验证邀请码
|
||||
:param data: InviteRequest
|
||||
:return: InviteResponse
|
||||
"""
|
||||
async with asyncsession as session:
|
||||
invite_code = data.invite_code
|
||||
invite = select(Invite).where(Invite.invite_code == invite_code)
|
||||
result = await session.execute(invite)
|
||||
invite_data = result.scalars().first()
|
||||
if invite_data:
|
||||
invite_token = secrets.token_urlsafe(128)
|
||||
invite_tokens.append(invite_token)
|
||||
return InviteResponse(
|
||||
code=200,
|
||||
message="邀请码验证成功",
|
||||
data=InviteTokenData(invite_token=invite_token),
|
||||
)
|
||||
else:
|
||||
return InviteResponse(
|
||||
code=400,
|
||||
message="邀请码无效或已过期",
|
||||
data=None,
|
||||
)
|
||||
|
||||
|
||||
@invite_router.post("/register", summary="注册新用户")
|
||||
async def register_user(
|
||||
data: RegisterRequest,
|
||||
asyncsession: AsyncSession = Depends(get_db_session),
|
||||
) -> RegisterResponse:
|
||||
"""
|
||||
注册新用户
|
||||
:param data: RegisterRequest
|
||||
:return: RegisterResponse
|
||||
"""
|
||||
async with asyncsession as session:
|
||||
userid = data.userid
|
||||
password = data.password
|
||||
easyconnect_password = data.easyconnect_password
|
||||
invite_token = data.invite_token
|
||||
if invite_token not in invite_tokens:
|
||||
return RegisterResponse(
|
||||
code=400,
|
||||
message="无效的邀请令牌",
|
||||
data=None,
|
||||
)
|
||||
|
||||
# 检查用户是否已存在
|
||||
existing_user = await session.execute(select(User).where(User.userid == userid))
|
||||
if existing_user.scalars().first():
|
||||
return RegisterResponse(
|
||||
code=400,
|
||||
message="用户已存在",
|
||||
data=None,
|
||||
)
|
||||
|
||||
# 检查连接
|
||||
vpn = AUFEConnection.create_or_get_connection("vpn.aufe.edu.cn", userid)
|
||||
if not await vpn.login(userid, easyconnect_password):
|
||||
return RegisterResponse(
|
||||
code=400,
|
||||
message="VPN登录失败,请检查用户名和密码",
|
||||
data=None,
|
||||
)
|
||||
|
||||
if not await vpn.uaap_login(userid, password):
|
||||
return RegisterResponse(
|
||||
code=400,
|
||||
message="大学登录失败,请检查用户名和密码",
|
||||
data=None,
|
||||
)
|
||||
# 创建新用户
|
||||
|
||||
new_user = User(
|
||||
userid=userid,
|
||||
password=password,
|
||||
easyconnect_password=easyconnect_password,
|
||||
)
|
||||
session.add(new_user)
|
||||
await session.commit()
|
||||
authme_token = secrets.token_urlsafe(128)
|
||||
new_authme = AuthME(userid=userid, authme_token=authme_token)
|
||||
session.add(new_authme)
|
||||
await session.commit()
|
||||
invite_tokens.remove(invite_token)
|
||||
return RegisterResponse(
|
||||
code=200,
|
||||
message="注册成功",
|
||||
data=AuthMeData(authme_token=authme_token),
|
||||
)
|
||||
39
router/invite/model.py
Normal file
39
router/invite/model.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from router.common_model import BaseResponse
|
||||
|
||||
|
||||
class InviteRequest(BaseModel):
|
||||
invite_code: str = Field(..., description="邀请码")
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
userid: str = Field(..., description="学号")
|
||||
password: str = Field(..., description="密码")
|
||||
easyconnect_password: str = Field(..., description="易联密码")
|
||||
invite_token: str = Field(..., description="邀请码")
|
||||
|
||||
|
||||
# 邀请相关响应数据模型
|
||||
class InviteTokenData(BaseModel):
|
||||
"""邀请令牌数据"""
|
||||
|
||||
invite_token: str = Field(..., description="邀请密钥")
|
||||
|
||||
|
||||
class AuthMeData(BaseModel):
|
||||
"""认证令牌数据"""
|
||||
|
||||
authme_token: str = Field(..., description="AuthMe Token")
|
||||
|
||||
|
||||
# 统一响应模型
|
||||
class InviteResponse(BaseResponse[InviteTokenData]):
|
||||
"""邀请响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RegisterResponse(BaseResponse[AuthMeData]):
|
||||
"""注册响应"""
|
||||
|
||||
pass
|
||||
605
router/jwc/__init__.py
Normal file
605
router/jwc/__init__.py
Normal file
@@ -0,0 +1,605 @@
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from provider.aufe.jwc import JWCClient
|
||||
from provider.aufe.jwc.depends import get_jwc_client
|
||||
from provider.loveac.authme import AuthmeResponse
|
||||
from router.jwc.model import (
|
||||
AcademicInfoResponse,
|
||||
TrainingPlanInfoResponse,
|
||||
CourseListResponse,
|
||||
ExamInfoAPIResponse,
|
||||
AllTermsResponse,
|
||||
TermScoreAPIResponse,
|
||||
FetchTermScoreRequest,
|
||||
ScheduleResponse,
|
||||
FetchScheduleRequest,
|
||||
)
|
||||
from router.common_model import ErrorResponse
|
||||
from .evaluate_model import (
|
||||
EvaluationStatsResponse,
|
||||
CurrentCourseInfoResponse,
|
||||
TaskOperationResponse,
|
||||
InitializeResponse,
|
||||
CourseInfo,
|
||||
TaskStatusEnum,
|
||||
EvaluationStatsData,
|
||||
CurrentCourseInfoData,
|
||||
TaskOperationData,
|
||||
InitializeData,
|
||||
)
|
||||
from .evaluate import (
|
||||
get_task_manager,
|
||||
remove_task_manager,
|
||||
)
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
jwc_router = APIRouter(prefix="/api/v1/jwc")
|
||||
invite_tokens = []
|
||||
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/fetch_academic_info",
|
||||
summary="获取学业信息",
|
||||
response_model=AcademicInfoResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_academic_info(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""获取学术信息(课程数量、绩点等)"""
|
||||
try:
|
||||
result = await client.fetch_academic_info()
|
||||
|
||||
# 检查是否是AuthmeResponse(认证错误)
|
||||
if isinstance(result, AuthmeResponse):
|
||||
return result
|
||||
|
||||
# 使用新的错误检测机制
|
||||
response = AcademicInfoResponse.from_data(
|
||||
data=result,
|
||||
success_message="学业信息获取成功",
|
||||
error_message="获取学业信息失败,网络请求多次重试后仍无法连接教务系统,请稍后重试或联系管理员",
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return ErrorResponse(message=f"获取学业信息时发生系统错误:{str(e)}", code=500)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/fetch_education_plan_info",
|
||||
summary="获取培养方案信息",
|
||||
response_model=TrainingPlanInfoResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_education_plan_info(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""获取培养方案信息"""
|
||||
try:
|
||||
result = await client.fetch_training_plan_info()
|
||||
|
||||
# 检查是否是AuthmeResponse(认证错误)
|
||||
if isinstance(result, AuthmeResponse):
|
||||
return result
|
||||
|
||||
# 使用新的错误检测机制
|
||||
response = TrainingPlanInfoResponse.from_data(
|
||||
data=result,
|
||||
success_message="培养方案信息获取成功",
|
||||
error_message="获取培养方案信息失败,网络请求多次重试后仍无法连接教务系统,请稍后重试或联系管理员",
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return ErrorResponse(
|
||||
message=f"获取培养方案信息时发生系统错误:{str(e)}", code=500
|
||||
)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/fetch_evaluation_course_list",
|
||||
summary="获取评教课程列表",
|
||||
response_model=CourseListResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_evaluation_course_list(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""获取评教课程列表"""
|
||||
try:
|
||||
result = await client.fetch_evaluation_course_list()
|
||||
|
||||
# 检查是否是AuthmeResponse(认证错误)
|
||||
if isinstance(result, AuthmeResponse):
|
||||
return result
|
||||
|
||||
# 对于列表类型,使用特殊的检查逻辑
|
||||
if result and len(result) > 0:
|
||||
# 检查第一个元素是否是错误数据
|
||||
first_course = result[0]
|
||||
if (
|
||||
hasattr(first_course, "evaluated_people")
|
||||
and first_course.evaluated_people == "请求失败"
|
||||
):
|
||||
return CourseListResponse.error(
|
||||
message="获取评教课程列表失败,网络请求多次重试后仍无法连接教务系统,请稍后重试或联系管理员",
|
||||
code=500,
|
||||
data=[],
|
||||
)
|
||||
else:
|
||||
return CourseListResponse.success(
|
||||
data=result, message="评教课程列表获取成功"
|
||||
)
|
||||
else:
|
||||
return CourseListResponse.success(data=[], message="暂无需要评教的课程")
|
||||
|
||||
except Exception as e:
|
||||
return ErrorResponse(
|
||||
message=f"获取评教课程列表时发生系统错误:{str(e)}", code=500
|
||||
)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/fetch_exam_info",
|
||||
summary="获取考试信息",
|
||||
response_model=ExamInfoAPIResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_exam_info(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""获取考试信息,包括校统考和其他考试"""
|
||||
try:
|
||||
train_plan_info = await client.fetch_training_plan_info()
|
||||
|
||||
# 检查培养方案信息是否获取失败
|
||||
if not train_plan_info or (
|
||||
hasattr(train_plan_info, "plan_name")
|
||||
and train_plan_info.plan_name == "请求失败,请稍后重试"
|
||||
):
|
||||
return ErrorResponse(
|
||||
message="无法获取培养方案信息,导致考试信息获取失败。网络请求多次重试后仍无法连接教务系统,请稍后重试或联系管理员",
|
||||
code=500,
|
||||
)
|
||||
|
||||
# 检查是否是AuthmeResponse
|
||||
if isinstance(train_plan_info, AuthmeResponse):
|
||||
return train_plan_info
|
||||
|
||||
_term_code = train_plan_info.current_term
|
||||
# _term_code -> term_code: "2024-2025春季学期" 转换为 "2024-2025-2-1" "2024-2025秋季学期" 转换为 "2024-2025-1-1"
|
||||
# 进行转换
|
||||
term_code = f"{_term_code[:4]}-{_term_code[5:9]}-{"1" if _term_code[10] == "秋" else "2"}-1"
|
||||
print(f"当前学期代码: {term_code}")
|
||||
start_date = datetime.now()
|
||||
# termcode 结尾为 1 为秋季学期,考试应在3月之前,2为春季学期,考试应在9月之前
|
||||
end_date = datetime(
|
||||
year=start_date.year + (1 if term_code.endswith("1") else 0),
|
||||
month=3 if term_code.endswith("1") else 9,
|
||||
day=30,
|
||||
)
|
||||
|
||||
result = await client.fetch_unified_exam_info(
|
||||
start_date=start_date.strftime("%Y-%m-%d"),
|
||||
end_date=end_date.strftime("%Y-%m-%d"),
|
||||
term_code=term_code,
|
||||
)
|
||||
|
||||
# 检查是否是AuthmeResponse(认证错误)
|
||||
if isinstance(result, AuthmeResponse):
|
||||
return result
|
||||
|
||||
# 使用新的错误检测机制
|
||||
response = ExamInfoAPIResponse.from_data(
|
||||
data=result,
|
||||
success_message="考试信息获取成功",
|
||||
error_message="获取考试信息失败,网络请求多次重试后仍无法连接教务系统,请稍后重试或联系管理员",
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return ErrorResponse(message=f"获取考试信息时发生系统错误:{str(e)}", code=500)
|
||||
|
||||
|
||||
# ==================== 评价系统API ====================
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/evaluation/initialize",
|
||||
summary="初始化评价任务",
|
||||
response_model=InitializeResponse | AuthmeResponse,
|
||||
)
|
||||
async def initialize_evaluation_task(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""初始化评价任务,获取课程列表"""
|
||||
try:
|
||||
# 获取用户ID (从JWC客户端获取)
|
||||
user_id = getattr(client, "user_id", "unknown")
|
||||
|
||||
# 检查是否已有活跃的任务管理器
|
||||
existing_manager = get_task_manager(user_id)
|
||||
if existing_manager:
|
||||
current_status = existing_manager.get_task_status().status
|
||||
if current_status in [
|
||||
TaskStatusEnum.RUNNING,
|
||||
TaskStatusEnum.PAUSED,
|
||||
TaskStatusEnum.INITIALIZING,
|
||||
]:
|
||||
return InitializeResponse(
|
||||
code=400,
|
||||
message="您已有一个评价任务在进行中,请先完成或终止当前任务",
|
||||
data=None,
|
||||
)
|
||||
# 如果任务已完成、失败或终止,移除旧的任务管理器
|
||||
elif current_status in [
|
||||
TaskStatusEnum.COMPLETED,
|
||||
TaskStatusEnum.FAILED,
|
||||
TaskStatusEnum.TERMINATED,
|
||||
]:
|
||||
remove_task_manager(user_id)
|
||||
|
||||
# 获取或创建任务管理器
|
||||
task_manager = get_task_manager(user_id, client)
|
||||
if not task_manager:
|
||||
return InitializeResponse(code=400, message="创建任务管理器失败", data=None)
|
||||
|
||||
# 执行初始化
|
||||
success = await task_manager.initialize()
|
||||
stats = task_manager.get_task_status()
|
||||
|
||||
# 转换课程列表格式
|
||||
course_list = []
|
||||
for course in stats.course_list:
|
||||
course_info = CourseInfo(
|
||||
course_id=(
|
||||
getattr(course.id, "coure_sequence_number", "") if course.id else ""
|
||||
),
|
||||
course_name=course.evaluation_content,
|
||||
teacher_name=course.evaluated_people,
|
||||
is_evaluated=course.is_evaluated,
|
||||
evaluation_content=course.evaluation_content,
|
||||
)
|
||||
course_list.append(course_info)
|
||||
|
||||
initialize_data = InitializeData(
|
||||
total_courses=stats.total_courses,
|
||||
pending_courses=stats.pending_courses,
|
||||
course_list=course_list,
|
||||
)
|
||||
|
||||
return InitializeResponse(
|
||||
code=200 if success else 400, message=stats.message, data=initialize_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return InitializeResponse(code=500, message=f"初始化失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/evaluation/start",
|
||||
summary="开始评价任务",
|
||||
response_model=TaskOperationResponse | AuthmeResponse,
|
||||
)
|
||||
async def start_evaluation_task(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""开始评价任务"""
|
||||
try:
|
||||
user_id = getattr(client, "user_id", "unknown")
|
||||
|
||||
# 检查是否已有运行中的任务
|
||||
existing_manager = get_task_manager(user_id)
|
||||
if existing_manager:
|
||||
current_status = existing_manager.get_task_status().status
|
||||
if current_status.value in [
|
||||
TaskStatusEnum.RUNNING.value,
|
||||
TaskStatusEnum.PAUSED.value,
|
||||
]:
|
||||
task_data = TaskOperationData(
|
||||
task_status=TaskStatusEnum(current_status.value)
|
||||
)
|
||||
return TaskOperationResponse(
|
||||
code=400,
|
||||
message="您已有一个评价任务在运行中,请先完成或终止当前任务",
|
||||
data=task_data,
|
||||
)
|
||||
|
||||
task_manager = get_task_manager(user_id, client)
|
||||
if not task_manager:
|
||||
task_data = TaskOperationData(task_status=TaskStatusEnum.FAILED)
|
||||
return TaskOperationResponse(
|
||||
code=400, message="任务管理器不存在,请先初始化", data=task_data
|
||||
)
|
||||
|
||||
success = await task_manager.start_evaluation_task()
|
||||
stats = task_manager.get_task_status()
|
||||
|
||||
task_data = TaskOperationData(task_status=TaskStatusEnum(stats.status.value))
|
||||
|
||||
return TaskOperationResponse(
|
||||
code=200 if success else 400,
|
||||
message="任务已启动" if success else "任务启动失败,可能已有任务在运行",
|
||||
data=task_data,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
task_data = TaskOperationData(task_status=TaskStatusEnum.FAILED)
|
||||
return TaskOperationResponse(
|
||||
code=500, message=f"启动任务失败: {str(e)}", data=task_data
|
||||
)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/evaluation/terminate",
|
||||
summary="终止评价任务",
|
||||
response_model=TaskOperationResponse | AuthmeResponse,
|
||||
)
|
||||
async def terminate_evaluation_task(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""终止评价任务"""
|
||||
try:
|
||||
user_id = getattr(client, "user_id", "unknown")
|
||||
task_manager = get_task_manager(user_id)
|
||||
|
||||
if not task_manager:
|
||||
task_data = TaskOperationData(task_status=TaskStatusEnum.IDLE)
|
||||
return TaskOperationResponse(
|
||||
code=400, message="任务管理器不存在", data=task_data
|
||||
)
|
||||
|
||||
success = await task_manager.terminate_task()
|
||||
stats = task_manager.get_task_status()
|
||||
|
||||
# 移除任务管理器
|
||||
remove_task_manager(user_id)
|
||||
|
||||
task_data = TaskOperationData(task_status=TaskStatusEnum(stats.status.value))
|
||||
|
||||
return TaskOperationResponse(
|
||||
code=200 if success else 400,
|
||||
message="任务已终止" if success else "终止失败",
|
||||
data=task_data,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
task_data = TaskOperationData(task_status=TaskStatusEnum.FAILED)
|
||||
return TaskOperationResponse(
|
||||
code=500, message=f"终止任务失败: {str(e)}", data=task_data
|
||||
)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/evaluation/status",
|
||||
summary="获取评价任务状态",
|
||||
response_model=EvaluationStatsResponse | AuthmeResponse,
|
||||
)
|
||||
async def get_evaluation_task_status(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""获取评价任务状态"""
|
||||
try:
|
||||
user_id = getattr(client, "user_id", "unknown")
|
||||
task_manager = get_task_manager(user_id)
|
||||
|
||||
if not task_manager:
|
||||
return EvaluationStatsResponse(code=200, message="无活跃任务", data=None)
|
||||
|
||||
stats = task_manager.get_task_status()
|
||||
|
||||
# 转换课程列表格式
|
||||
course_list = []
|
||||
for course in stats.course_list:
|
||||
course_info = CourseInfo(
|
||||
course_id=(
|
||||
getattr(course.id, "coure_sequence_number", "") if course.id else ""
|
||||
),
|
||||
course_name=course.evaluation_content,
|
||||
teacher_name=course.evaluated_people,
|
||||
is_evaluated=course.is_evaluated,
|
||||
evaluation_content=course.evaluation_content,
|
||||
)
|
||||
course_list.append(course_info)
|
||||
|
||||
stats_data = EvaluationStatsData(
|
||||
total_courses=stats.total_courses,
|
||||
pending_courses=stats.pending_courses,
|
||||
success_count=stats.success_count,
|
||||
fail_count=stats.fail_count,
|
||||
current_index=stats.current_index,
|
||||
status=TaskStatusEnum(stats.status.value),
|
||||
current_countdown=stats.current_countdown,
|
||||
start_time=stats.start_time,
|
||||
end_time=stats.end_time,
|
||||
error_message=stats.error_message,
|
||||
course_list=course_list,
|
||||
)
|
||||
|
||||
return EvaluationStatsResponse(code=200, message=stats.message, data=stats_data)
|
||||
|
||||
except Exception as e:
|
||||
return EvaluationStatsResponse(
|
||||
code=500, message=f"获取状态失败: {str(e)}", data=None
|
||||
)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/evaluation/current",
|
||||
summary="获取当前评价课程信息",
|
||||
response_model=CurrentCourseInfoResponse | AuthmeResponse,
|
||||
)
|
||||
async def get_current_course_info(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""获取当前评价课程信息"""
|
||||
try:
|
||||
user_id = getattr(client, "user_id", "unknown")
|
||||
task_manager = get_task_manager(user_id)
|
||||
|
||||
if not task_manager:
|
||||
return CurrentCourseInfoResponse(code=200, message="无活跃任务", data=None)
|
||||
|
||||
current_info = task_manager.get_current_course_info()
|
||||
|
||||
course_info_data = CurrentCourseInfoData(
|
||||
is_evaluating=current_info.is_evaluating,
|
||||
course_name=current_info.course_name,
|
||||
teacher_name=current_info.teacher_name,
|
||||
progress_text=current_info.progress_text,
|
||||
countdown_seconds=current_info.countdown_seconds,
|
||||
current_index=current_info.current_index,
|
||||
total_pending=current_info.total_pending,
|
||||
)
|
||||
|
||||
return CurrentCourseInfoResponse(
|
||||
code=200, message="获取成功", data=course_info_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CurrentCourseInfoResponse(
|
||||
code=500, message=f"获取信息失败: {str(e)}", data=None
|
||||
)
|
||||
|
||||
|
||||
# ==================== 学期和成绩相关API ====================
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/fetch_all_terms",
|
||||
summary="获取所有学期信息",
|
||||
response_model=AllTermsResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_all_terms(client: JWCClient = Depends(get_jwc_client)):
|
||||
"""获取所有可查询的学期信息"""
|
||||
try:
|
||||
result = await client.fetch_all_terms()
|
||||
|
||||
# 检查结果
|
||||
if result and len(result) > 0:
|
||||
return AllTermsResponse.success(data=result, message="学期信息获取成功")
|
||||
else:
|
||||
return AllTermsResponse.error(
|
||||
message="获取学期信息失败,网络请求多次重试后仍无法连接教务系统,请稍后重试或联系管理员",
|
||||
code=500,
|
||||
data={},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ErrorResponse(message=f"获取学期信息时发生系统错误:{str(e)}", code=500)
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/fetch_term_score",
|
||||
summary="获取指定学期成绩",
|
||||
response_model=TermScoreAPIResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_term_score(
|
||||
request: FetchTermScoreRequest,
|
||||
client: JWCClient = Depends(get_jwc_client),
|
||||
):
|
||||
"""
|
||||
获取指定学期的成绩信息
|
||||
"""
|
||||
try:
|
||||
raw_result = await client.fetch_term_score(
|
||||
term_id=request.term_id,
|
||||
course_code=request.course_code,
|
||||
course_name=request.course_name,
|
||||
page_num=request.page_num,
|
||||
page_size=request.page_size,
|
||||
)
|
||||
|
||||
if not raw_result:
|
||||
return TermScoreAPIResponse.error(
|
||||
message="获取成绩信息失败,网络请求多次重试后仍无法连接教务系统,请稍后重试或联系管理员",
|
||||
code=500,
|
||||
data=None,
|
||||
)
|
||||
|
||||
try:
|
||||
# 解析原始数据为结构化数据
|
||||
from provider.aufe.jwc.model import TermScoreResponse, ScoreRecord
|
||||
|
||||
list_data = raw_result.get("list", {})
|
||||
page_context = list_data.get("pageContext", {})
|
||||
records_raw = list_data.get("records", [])
|
||||
|
||||
# 转换记录格式
|
||||
score_records = []
|
||||
for record in records_raw:
|
||||
if len(record) >= 13: # 确保数据完整
|
||||
score_record = ScoreRecord(
|
||||
sequence=record[0] if record[0] else 0,
|
||||
term_id=record[1] if record[1] else "",
|
||||
course_code=record[2] if record[2] else "",
|
||||
course_class=record[3] if record[3] else "",
|
||||
course_name_cn=record[4] if record[4] else "",
|
||||
course_name_en=record[5] if record[5] else "",
|
||||
credits=record[6] if record[6] else "",
|
||||
hours=record[7] if record[7] else 0,
|
||||
course_type=record[8] if record[8] else "",
|
||||
exam_type=record[9] if record[9] else "",
|
||||
score=record[10] if record[10] else "",
|
||||
retake_score=(
|
||||
record[11] if len(record) > 11 and record[11] else None
|
||||
),
|
||||
makeup_score=(
|
||||
record[12] if len(record) > 12 and record[12] else None
|
||||
),
|
||||
)
|
||||
score_records.append(score_record)
|
||||
|
||||
result = TermScoreResponse(
|
||||
page_size=list_data.get("pageSize", 50),
|
||||
page_num=list_data.get("pageNum", 1),
|
||||
total_count=page_context.get("totalCount", 0),
|
||||
records=score_records,
|
||||
)
|
||||
|
||||
return TermScoreAPIResponse(
|
||||
code=200,
|
||||
message="success",
|
||||
data=result,
|
||||
)
|
||||
|
||||
except Exception as parse_error:
|
||||
return TermScoreAPIResponse.error(
|
||||
message=f"解析成绩数据失败:{str(parse_error)}", code=500, data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取学期成绩失败: {str(e)}")
|
||||
return ErrorResponse(code=1, message=f"获取学期成绩失败: {str(e)}")
|
||||
|
||||
|
||||
@jwc_router.post(
|
||||
"/fetch_course_schedule",
|
||||
summary="获取课表信息",
|
||||
response_model=ScheduleResponse | AuthmeResponse | ErrorResponse,
|
||||
)
|
||||
async def fetch_course_schedule(
|
||||
request: FetchScheduleRequest,
|
||||
client: JWCClient = Depends(get_jwc_client)
|
||||
):
|
||||
"""
|
||||
获取聚合的课表信息,包含:
|
||||
- 课程基本信息(课程名、教师、学分等)
|
||||
- 上课时间和地点信息
|
||||
- 时间段详情
|
||||
- 学期信息
|
||||
|
||||
特殊处理:
|
||||
- 自动过滤无用字段
|
||||
- 标记没有具体时间安排的课程
|
||||
- 清理教师姓名中的特殊字符
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取课表请求: plan_code={request.plan_code}")
|
||||
|
||||
# 检查环境和Cookie有效性
|
||||
is_valid = await client.validate_environment_and_cookie()
|
||||
if not is_valid:
|
||||
return AuthmeResponse(
|
||||
code=401,
|
||||
message="Cookie已失效或不在VPN/校园网环境,请重新登录",
|
||||
)
|
||||
|
||||
# 获取处理后的课表数据
|
||||
schedule_data = await client.get_processed_schedule(request.plan_code)
|
||||
|
||||
if not schedule_data:
|
||||
return ErrorResponse(
|
||||
code=1,
|
||||
message="获取课表信息失败,请稍后重试"
|
||||
)
|
||||
|
||||
return ScheduleResponse(
|
||||
code=0,
|
||||
message="success",
|
||||
data=schedule_data,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"获取课表信息失败: {str(e)}")
|
||||
return ErrorResponse(code=1, message=f"获取课表信息失败: {str(e)}")
|
||||
670
router/jwc/evaluate.py
Normal file
670
router/jwc/evaluate.py
Normal file
@@ -0,0 +1,670 @@
|
||||
from provider.aufe.jwc import JWCClient
|
||||
from provider.aufe.jwc.model import Course, EvaluationRequestParam
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class TaskStatus(Enum):
|
||||
"""任务状态枚举"""
|
||||
|
||||
IDLE = "idle" # 空闲
|
||||
INITIALIZING = "initializing" # 初始化中
|
||||
RUNNING = "running" # 运行中
|
||||
PAUSED = "paused" # 暂停
|
||||
COMPLETED = "completed" # 完成
|
||||
FAILED = "failed" # 失败
|
||||
TERMINATED = "terminated" # 已终止
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvaluationStats:
|
||||
"""评价统计信息"""
|
||||
|
||||
total_courses: int = 0
|
||||
pending_courses: int = 0
|
||||
success_count: int = 0
|
||||
fail_count: int = 0
|
||||
current_index: int = 0
|
||||
status: TaskStatus = TaskStatus.IDLE
|
||||
message: str = ""
|
||||
course_list: List[Course] = field(default_factory=list)
|
||||
current_countdown: int = 0
|
||||
current_course: Optional[Course] = None
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
error_message: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CurrentCourseInfo:
|
||||
"""当前评价课程信息"""
|
||||
|
||||
is_evaluating: bool = False
|
||||
course_name: str = ""
|
||||
teacher_name: str = ""
|
||||
progress_text: str = ""
|
||||
countdown_seconds: int = 0
|
||||
current_index: int = -1
|
||||
total_pending: int = 0
|
||||
|
||||
|
||||
class Constants:
|
||||
"""常量定义"""
|
||||
|
||||
# 等待评价的冷却时间(秒)
|
||||
COUNTDOWN_SECONDS = 140 # 2分20秒
|
||||
|
||||
# 随机评价文案 - 总体评价文案
|
||||
ZGPGS = [
|
||||
"老师授课生动形象,课堂氛围活跃。",
|
||||
"教学方法新颖,能够激发学习兴趣。",
|
||||
"讲解耐心细致,知识点清晰易懂。",
|
||||
"对待学生公平公正,很有亲和力。",
|
||||
"课堂管理有序,效率高。",
|
||||
"能理论联系实际,深入浅出。",
|
||||
"作业布置合理,有助于巩固知识。",
|
||||
"教学经验丰富,讲解深入浅出。",
|
||||
"关注学生反馈,及时调整教学。",
|
||||
"教学资源丰富,便于学习。",
|
||||
"课堂互动性强,能充分调动积极性。",
|
||||
"教学重点突出,难点突破到位。",
|
||||
"性格开朗,课堂充满活力。",
|
||||
"批改作业认真,评语有指导性。",
|
||||
"教学目标明确,条理清晰。",
|
||||
]
|
||||
|
||||
# 额外描述性文案
|
||||
NICE_0000000200 = [
|
||||
"常把晦涩理论生活化,知识瞬间亲近起来。",
|
||||
"总用类比解难点,复杂概念秒懂。",
|
||||
"引入行业前沿案例,打开视野新窗口。",
|
||||
"设问巧妙引深思,激发自主探寻答案。",
|
||||
"常分享学科冷知识,拓宽知识边界。",
|
||||
"用跨学科视角解题,思维更灵动。",
|
||||
"鼓励尝试多元解法,创新思维被激活。",
|
||||
"常分享科研趣事,点燃学术热情。",
|
||||
"用思维导图梳理知识,结构一目了然。",
|
||||
"常把学习方法倾囊相授,效率直线提升。",
|
||||
"用历史事件类比,知识记忆更深刻。",
|
||||
"常鼓励跨学科学习,综合素养渐涨。",
|
||||
"分享行业大咖故事,奋斗动力满满。",
|
||||
"总能挖掘知识背后的趣味,学习味十足。",
|
||||
"常组织知识竞赛,学习热情被点燃。",
|
||||
]
|
||||
|
||||
# 建议文案
|
||||
NICE_0000000201 = [
|
||||
"无",
|
||||
"没有",
|
||||
"没有什么建议,老师很好",
|
||||
"继续保持这么好的教学风格",
|
||||
"希望老师继续分享更多精彩案例",
|
||||
"感谢老师的悉心指导",
|
||||
]
|
||||
|
||||
|
||||
class EvaluationTaskManager:
|
||||
"""评价任务管理器 - 基于学号管理"""
|
||||
|
||||
def __init__(self, jwc_client: JWCClient, user_id: str):
|
||||
"""
|
||||
初始化评价任务管理器
|
||||
|
||||
Args:
|
||||
jwc_client: JWC客户端实例
|
||||
user_id: 用户学号
|
||||
"""
|
||||
self.jwc_client = jwc_client
|
||||
self.user_id = user_id
|
||||
self.stats = EvaluationStats()
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._stop_event = asyncio.Event()
|
||||
self._progress_callbacks: List[Callable[[EvaluationStats], None]] = []
|
||||
|
||||
logger.info(f"初始化评价任务管理器,用户ID: {user_id}")
|
||||
|
||||
def add_progress_callback(self, callback: Callable[[EvaluationStats], None]):
|
||||
"""添加进度回调函数"""
|
||||
self._progress_callbacks.append(callback)
|
||||
|
||||
def _notify_progress(self):
|
||||
"""通知所有进度回调"""
|
||||
for callback in self._progress_callbacks:
|
||||
try:
|
||||
callback(self.stats)
|
||||
except Exception as e:
|
||||
logger.error(f"进度回调执行失败: {str(e)}")
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""
|
||||
初始化评价环境
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
self.stats.status = TaskStatus.INITIALIZING
|
||||
self.stats.message = "正在检查网络..."
|
||||
self._notify_progress()
|
||||
|
||||
# 检查网络连接
|
||||
if not await self.jwc_client.check_network_connection():
|
||||
self.stats.status = TaskStatus.FAILED
|
||||
self.stats.message = "网络连接失败,请确保连接到校园网或VPN"
|
||||
self.stats.error_message = "网络连接失败"
|
||||
self._notify_progress()
|
||||
return False
|
||||
|
||||
# 验证环境和Cookie
|
||||
self.stats.message = "正在验证登录状态..."
|
||||
self._notify_progress()
|
||||
|
||||
if not await self.jwc_client.validate_environment_and_cookie():
|
||||
self.stats.status = TaskStatus.FAILED
|
||||
self.stats.message = "登录状态失效,请重新登录"
|
||||
self.stats.error_message = "Cookie验证失败"
|
||||
self._notify_progress()
|
||||
return False
|
||||
|
||||
# 获取Token
|
||||
self.stats.message = "正在获取Token..."
|
||||
self._notify_progress()
|
||||
|
||||
token = await self.jwc_client.get_token()
|
||||
if not token:
|
||||
self.stats.status = TaskStatus.FAILED
|
||||
self.stats.message = "获取Token失败,可能是评教系统未开放"
|
||||
self.stats.error_message = "Token获取失败"
|
||||
self._notify_progress()
|
||||
return False
|
||||
|
||||
# 获取课程列表
|
||||
self.stats.message = "正在获取课程列表..."
|
||||
self._notify_progress()
|
||||
|
||||
courses = await self.jwc_client.fetch_evaluation_course_list()
|
||||
if not courses:
|
||||
self.stats.status = TaskStatus.FAILED
|
||||
self.stats.message = "未获取到课程列表,请稍后再试"
|
||||
self.stats.error_message = "课程列表获取失败"
|
||||
self._notify_progress()
|
||||
return False
|
||||
|
||||
# 更新统计信息
|
||||
pending_courses = [
|
||||
course
|
||||
for course in courses
|
||||
if getattr(course, "is_evaluated", "否") != "是"
|
||||
]
|
||||
self.stats.course_list = courses
|
||||
self.stats.total_courses = len(courses)
|
||||
self.stats.pending_courses = len(pending_courses)
|
||||
self.stats.status = TaskStatus.IDLE
|
||||
self.stats.message = (
|
||||
f"初始化完成,找到 {self.stats.pending_courses} 门待评价课程"
|
||||
)
|
||||
self.stats.current_course = None
|
||||
|
||||
logger.info(
|
||||
f"用户 {self.user_id} 初始化完成,待评价课程: {self.stats.pending_courses}"
|
||||
)
|
||||
self._notify_progress()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.stats.status = TaskStatus.FAILED
|
||||
self.stats.message = f"初始化异常: {str(e)}"
|
||||
self.stats.error_message = str(e)
|
||||
logger.error(f"用户 {self.user_id} 初始化失败: {str(e)}")
|
||||
self._notify_progress()
|
||||
return False
|
||||
|
||||
async def evaluate_course(self, course: Course, token: str) -> bool:
|
||||
"""
|
||||
评价单门课程
|
||||
|
||||
Args:
|
||||
course: 课程信息
|
||||
token: CSRF Token
|
||||
|
||||
Returns:
|
||||
bool: 评价是否成功
|
||||
"""
|
||||
try:
|
||||
# 设置当前课程
|
||||
self.stats.current_course = course
|
||||
|
||||
# 如果课程已评价,则跳过
|
||||
if getattr(course, "is_evaluated", "否") == "是":
|
||||
logger.info(f"课程已评价,跳过: {course.evaluation_content}")
|
||||
return True
|
||||
|
||||
# 第一步:访问评价页面
|
||||
if not await self.jwc_client.access_evaluation_page(token, course):
|
||||
return False
|
||||
|
||||
course_name = course.evaluation_content
|
||||
logger.info(f"正在准备评价: {course_name}")
|
||||
|
||||
self.stats.message = "已访问评价页面,等待服务器倒计时完成后提交评价..."
|
||||
self._notify_progress()
|
||||
|
||||
# 等待服务器倒计时
|
||||
server_wait_time = Constants.COUNTDOWN_SECONDS
|
||||
|
||||
# 显示倒计时
|
||||
for second in range(server_wait_time, 0, -1):
|
||||
# 检查是否被终止
|
||||
if self._stop_event.is_set():
|
||||
self.stats.status = TaskStatus.TERMINATED
|
||||
self.stats.message = "任务已被终止"
|
||||
self._notify_progress()
|
||||
return False
|
||||
|
||||
self.stats.current_countdown = second
|
||||
self.stats.message = f"服务器倒计时: {second} 秒,然后提交评价..."
|
||||
self._notify_progress()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
self.stats.current_countdown = 0
|
||||
self.stats.message = "倒计时结束,正在提交评价..."
|
||||
self._notify_progress()
|
||||
|
||||
# 生成评价数据
|
||||
evaluation_ratings = {}
|
||||
for i in range(180, 202):
|
||||
key = f"0000000{i}"
|
||||
if i == 200:
|
||||
evaluation_ratings[key] = random.choice(Constants.NICE_0000000200)
|
||||
elif i == 201:
|
||||
evaluation_ratings[key] = random.choice(Constants.NICE_0000000201)
|
||||
else:
|
||||
evaluation_ratings[key] = f"5_{random.choice(['0.8', '1'])}"
|
||||
|
||||
# 创建评价请求参数
|
||||
evaluation_param = EvaluationRequestParam(
|
||||
token_value=token,
|
||||
questionnaire_code=(
|
||||
course.questionnaire.questionnaire_number
|
||||
if course.questionnaire
|
||||
else ""
|
||||
),
|
||||
evaluation_content=(
|
||||
course.id.evaluation_content_number if course.id else ""
|
||||
),
|
||||
evaluated_people_number=course.id.evaluated_people if course.id else "",
|
||||
zgpj=random.choice(Constants.ZGPGS),
|
||||
rating_items=evaluation_ratings,
|
||||
)
|
||||
|
||||
# 提交评价
|
||||
response = await self.jwc_client.submit_evaluation(evaluation_param)
|
||||
success = response.result == "success"
|
||||
|
||||
if success:
|
||||
logger.info(f"课程评价成功: {course_name}")
|
||||
else:
|
||||
logger.error(f"课程评价失败: {course_name}, 错误: {response.msg}")
|
||||
|
||||
# 清除当前课程信息
|
||||
self.stats.current_course = None
|
||||
self.stats.current_countdown = 0
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"评价课程异常: {str(e)}")
|
||||
return False
|
||||
|
||||
async def start_evaluation_task(self) -> bool:
|
||||
"""
|
||||
开始评价任务
|
||||
确保一个用户只能有一个运行中的任务
|
||||
|
||||
Returns:
|
||||
bool: 任务是否成功启动
|
||||
"""
|
||||
# 检查当前状态
|
||||
if self.stats.status == TaskStatus.RUNNING:
|
||||
logger.warning(f"用户 {self.user_id} 的评价任务已在运行中")
|
||||
return False
|
||||
|
||||
if self.stats.status == TaskStatus.INITIALIZING:
|
||||
logger.warning(f"用户 {self.user_id} 的评价任务正在初始化中")
|
||||
return False
|
||||
|
||||
# 检查是否有未完成的异步任务
|
||||
if self._task and not self._task.done():
|
||||
logger.warning(f"用户 {self.user_id} 已有任务在执行")
|
||||
return False
|
||||
|
||||
# 确保任务已经初始化
|
||||
if self.stats.status == TaskStatus.IDLE and len(self.stats.course_list) == 0:
|
||||
logger.warning(f"用户 {self.user_id} 任务未初始化,请先调用initialize")
|
||||
return False
|
||||
|
||||
# 重置停止事件
|
||||
self._stop_event.clear()
|
||||
|
||||
# 创建新任务
|
||||
self._task = asyncio.create_task(self._evaluate_all_courses())
|
||||
|
||||
logger.info(f"用户 {self.user_id} 开始评价任务")
|
||||
return True
|
||||
|
||||
async def _evaluate_all_courses(self):
|
||||
"""批量评价所有课程(内部方法)"""
|
||||
try:
|
||||
# 获取Token
|
||||
token = await self.jwc_client.get_token()
|
||||
if not token:
|
||||
self.stats.status = TaskStatus.FAILED
|
||||
self.stats.message = "获取Token失败"
|
||||
self._notify_progress()
|
||||
return
|
||||
|
||||
# 获取待评价课程
|
||||
pending_courses = [
|
||||
course
|
||||
for course in self.stats.course_list
|
||||
if getattr(course, "is_evaluated", "否") != "是"
|
||||
]
|
||||
|
||||
if not pending_courses:
|
||||
self.stats.status = TaskStatus.COMPLETED
|
||||
self.stats.message = "所有课程已评价完成!"
|
||||
self._notify_progress()
|
||||
return
|
||||
|
||||
# 开始评价流程
|
||||
self.stats.status = TaskStatus.RUNNING
|
||||
self.stats.success_count = 0
|
||||
self.stats.fail_count = 0
|
||||
self.stats.current_course = None
|
||||
self.stats.start_time = datetime.now()
|
||||
|
||||
index = 0
|
||||
while index < len(pending_courses):
|
||||
# 检查是否被终止
|
||||
if self._stop_event.is_set():
|
||||
self.stats.status = TaskStatus.TERMINATED
|
||||
self.stats.message = "任务已被终止"
|
||||
self.stats.end_time = datetime.now()
|
||||
self._notify_progress()
|
||||
return
|
||||
|
||||
course = pending_courses[index]
|
||||
self.stats.current_index = index
|
||||
self.stats.current_course = course
|
||||
|
||||
course_name = getattr(
|
||||
course.questionnaire,
|
||||
"questionnaire_name",
|
||||
course.evaluation_content,
|
||||
)
|
||||
self.stats.message = f"正在处理第 {index + 1}/{len(pending_courses)} 门课程: {course_name}"
|
||||
self._notify_progress()
|
||||
|
||||
# 评价当前课程
|
||||
success = await self.evaluate_course(course, token)
|
||||
|
||||
if success:
|
||||
self.stats.success_count += 1
|
||||
self.stats.message = f"课程评价成功: {course_name}"
|
||||
else:
|
||||
self.stats.fail_count += 1
|
||||
self.stats.message = f"课程评价失败: {course_name}"
|
||||
|
||||
self._notify_progress()
|
||||
|
||||
# 评价完一门课程后,重新获取课程列表
|
||||
self.stats.message = "正在更新课程列表..."
|
||||
self._notify_progress()
|
||||
|
||||
# 重新获取课程列表
|
||||
updated_courses = await self.jwc_client.fetch_evaluation_course_list()
|
||||
if updated_courses:
|
||||
self.stats.course_list = updated_courses
|
||||
pending_courses = [
|
||||
course
|
||||
for course in updated_courses
|
||||
if getattr(course, "is_evaluated", "否") != "是"
|
||||
]
|
||||
self.stats.total_courses = len(updated_courses)
|
||||
self.stats.pending_courses = len(pending_courses)
|
||||
self.stats.message = (
|
||||
f"课程列表已更新,剩余待评价课程: {self.stats.pending_courses}"
|
||||
)
|
||||
self._notify_progress()
|
||||
|
||||
# 给服务器一些处理时间
|
||||
if pending_courses and index < len(pending_courses) - 1:
|
||||
self.stats.message = "准备处理下一门课程..."
|
||||
self._notify_progress()
|
||||
await asyncio.sleep(3)
|
||||
|
||||
index += 1
|
||||
|
||||
# 评价完成
|
||||
self.stats.status = TaskStatus.COMPLETED
|
||||
self.stats.current_course = None
|
||||
self.stats.end_time = datetime.now()
|
||||
self.stats.message = f"评价完成!成功: {self.stats.success_count},失败: {self.stats.fail_count}"
|
||||
|
||||
logger.info(
|
||||
f"用户 {self.user_id} 评价任务完成,成功: {self.stats.success_count},失败: {self.stats.fail_count}"
|
||||
)
|
||||
self._notify_progress()
|
||||
|
||||
except Exception as e:
|
||||
self.stats.status = TaskStatus.FAILED
|
||||
self.stats.error_message = str(e)
|
||||
self.stats.message = f"评价任务异常: {str(e)}"
|
||||
self.stats.end_time = datetime.now()
|
||||
logger.error(f"用户 {self.user_id} 评价任务异常: {str(e)}")
|
||||
self._notify_progress()
|
||||
|
||||
async def pause_task(self) -> bool:
|
||||
"""
|
||||
暂停任务
|
||||
|
||||
Returns:
|
||||
bool: 是否成功暂停
|
||||
"""
|
||||
if self.stats.status != TaskStatus.RUNNING:
|
||||
return False
|
||||
|
||||
self.stats.status = TaskStatus.PAUSED
|
||||
self.stats.message = "任务已暂停"
|
||||
logger.info(f"用户 {self.user_id} 任务已暂停")
|
||||
self._notify_progress()
|
||||
return True
|
||||
|
||||
async def resume_task(self) -> bool:
|
||||
"""
|
||||
恢复任务
|
||||
|
||||
Returns:
|
||||
bool: 是否成功恢复
|
||||
"""
|
||||
if self.stats.status != TaskStatus.PAUSED:
|
||||
return False
|
||||
|
||||
self.stats.status = TaskStatus.RUNNING
|
||||
self.stats.message = "任务已恢复"
|
||||
logger.info(f"用户 {self.user_id} 任务已恢复")
|
||||
self._notify_progress()
|
||||
return True
|
||||
|
||||
async def terminate_task(self) -> bool:
|
||||
"""
|
||||
终止任务
|
||||
|
||||
Returns:
|
||||
bool: 是否成功终止
|
||||
"""
|
||||
if self.stats.status not in [TaskStatus.RUNNING, TaskStatus.PAUSED]:
|
||||
return False
|
||||
|
||||
# 设置停止事件
|
||||
self._stop_event.set()
|
||||
|
||||
# 如果有运行中的任务,等待其完成
|
||||
if self._task and not self._task.done():
|
||||
try:
|
||||
await asyncio.wait_for(self._task, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
self.stats.status = TaskStatus.TERMINATED
|
||||
self.stats.message = "任务已终止"
|
||||
self.stats.end_time = datetime.now()
|
||||
logger.info(f"用户 {self.user_id} 任务已终止")
|
||||
self._notify_progress()
|
||||
return True
|
||||
|
||||
def get_current_course_info(self) -> CurrentCourseInfo:
|
||||
"""
|
||||
获取当前评价课程信息
|
||||
|
||||
Returns:
|
||||
CurrentCourseInfo: 当前课程信息
|
||||
"""
|
||||
# 如果没有运行评价任务
|
||||
if self.stats.status != TaskStatus.RUNNING:
|
||||
return CurrentCourseInfo(
|
||||
is_evaluating=False, progress_text="当前无评价任务"
|
||||
)
|
||||
|
||||
# 正在评价但还没有确定是哪门课程
|
||||
if (
|
||||
self.stats.current_index < 0
|
||||
or self.stats.current_index >= len(self.stats.course_list)
|
||||
or self.stats.current_course is None
|
||||
):
|
||||
return CurrentCourseInfo(
|
||||
is_evaluating=True,
|
||||
progress_text="准备中...",
|
||||
total_pending=self.stats.pending_courses,
|
||||
)
|
||||
|
||||
# 正在评价特定课程
|
||||
course = self.stats.current_course
|
||||
pending_courses = [
|
||||
c
|
||||
for c in self.stats.course_list
|
||||
if getattr(c, "is_evaluated", "否") != "是"
|
||||
]
|
||||
index = self.stats.current_index + 1
|
||||
total = len(pending_courses)
|
||||
|
||||
countdown_text = (
|
||||
f" (倒计时: {self.stats.current_countdown}秒)"
|
||||
if self.stats.current_countdown > 0
|
||||
else ""
|
||||
)
|
||||
|
||||
course_name = course.evaluation_content[:20]
|
||||
if len(course.evaluation_content) > 20:
|
||||
course_name += "..."
|
||||
|
||||
return CurrentCourseInfo(
|
||||
is_evaluating=True,
|
||||
course_name=course_name,
|
||||
teacher_name=course.evaluated_people,
|
||||
progress_text=f"正在评价({index}/{total}): {course_name} - {course.evaluated_people}{countdown_text}",
|
||||
countdown_seconds=self.stats.current_countdown,
|
||||
current_index=self.stats.current_index,
|
||||
total_pending=total,
|
||||
)
|
||||
|
||||
def get_task_status(self) -> EvaluationStats:
|
||||
"""
|
||||
获取任务状态
|
||||
|
||||
Returns:
|
||||
EvaluationStats: 任务统计信息
|
||||
"""
|
||||
return self.stats
|
||||
|
||||
def get_user_id(self) -> str:
|
||||
"""获取用户ID"""
|
||||
return self.user_id
|
||||
|
||||
|
||||
# 全局任务管理器字典,以学号为键
|
||||
_task_managers: Dict[str, EvaluationTaskManager] = {}
|
||||
|
||||
|
||||
def get_task_manager(
|
||||
user_id: str, jwc_client: Optional[JWCClient] = None
|
||||
) -> Optional[EvaluationTaskManager]:
|
||||
"""
|
||||
获取或创建任务管理器
|
||||
一个用户只能有一个活跃的任务管理器
|
||||
|
||||
Args:
|
||||
user_id: 用户学号
|
||||
jwc_client: JWC客户端(创建新管理器时需要)
|
||||
|
||||
Returns:
|
||||
Optional[EvaluationTaskManager]: 任务管理器实例
|
||||
"""
|
||||
if user_id in _task_managers:
|
||||
existing_manager = _task_managers[user_id]
|
||||
# 检查现有任务的状态
|
||||
current_status = existing_manager.get_task_status().status
|
||||
|
||||
# 如果任务已完成、失败或终止,自动清理
|
||||
if current_status in [
|
||||
TaskStatus.COMPLETED,
|
||||
TaskStatus.FAILED,
|
||||
TaskStatus.TERMINATED,
|
||||
]:
|
||||
logger.info(f"自动清理用户 {user_id} 的已完成任务")
|
||||
del _task_managers[user_id]
|
||||
else:
|
||||
# 返回现有的管理器
|
||||
return existing_manager
|
||||
|
||||
# 创建新的管理器
|
||||
if jwc_client is None:
|
||||
return None
|
||||
|
||||
manager = EvaluationTaskManager(jwc_client, user_id)
|
||||
_task_managers[user_id] = manager
|
||||
logger.info(f"为用户 {user_id} 创建新的任务管理器")
|
||||
return manager
|
||||
|
||||
|
||||
def remove_task_manager(user_id: str) -> bool:
|
||||
"""
|
||||
移除任务管理器
|
||||
|
||||
Args:
|
||||
user_id: 用户学号
|
||||
|
||||
Returns:
|
||||
bool: 是否成功移除
|
||||
"""
|
||||
if user_id in _task_managers:
|
||||
del _task_managers[user_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_all_task_managers() -> Dict[str, EvaluationTaskManager]:
|
||||
"""获取所有任务管理器"""
|
||||
return _task_managers.copy()
|
||||
95
router/jwc/evaluate_model.py
Normal file
95
router/jwc/evaluate_model.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from router.common_model import BaseResponse
|
||||
|
||||
|
||||
class TaskStatusEnum(str, Enum):
|
||||
"""任务状态枚举"""
|
||||
|
||||
IDLE = "idle"
|
||||
INITIALIZING = "initializing"
|
||||
RUNNING = "running"
|
||||
PAUSED = "paused"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
TERMINATED = "terminated"
|
||||
|
||||
|
||||
class CourseInfo(BaseModel):
|
||||
"""课程信息响应模型"""
|
||||
|
||||
course_id: str = Field("", description="课程ID")
|
||||
course_name: str = Field("", description="课程名称")
|
||||
teacher_name: str = Field("", description="教师姓名")
|
||||
is_evaluated: str = Field("", description="是否已评价")
|
||||
evaluation_content: str = Field("", description="评价内容")
|
||||
|
||||
|
||||
# 统一响应数据模型
|
||||
class EvaluationStatsData(BaseModel):
|
||||
"""评价统计信息数据模型"""
|
||||
|
||||
total_courses: int = Field(0, description="总课程数")
|
||||
pending_courses: int = Field(0, description="待评价课程数")
|
||||
success_count: int = Field(0, description="成功评价数")
|
||||
fail_count: int = Field(0, description="失败评价数")
|
||||
current_index: int = Field(0, description="当前评价索引")
|
||||
status: TaskStatusEnum = Field(TaskStatusEnum.IDLE, description="任务状态")
|
||||
current_countdown: int = Field(0, description="当前倒计时")
|
||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
||||
error_message: str = Field("", description="错误消息")
|
||||
course_list: List[CourseInfo] = Field(default_factory=list, description="课程列表")
|
||||
|
||||
|
||||
class CurrentCourseInfoData(BaseModel):
|
||||
"""当前评价课程信息数据模型"""
|
||||
|
||||
is_evaluating: bool = Field(False, description="是否正在评价")
|
||||
course_name: str = Field("", description="课程名称")
|
||||
teacher_name: str = Field("", description="教师姓名")
|
||||
progress_text: str = Field("", description="进度文本")
|
||||
countdown_seconds: int = Field(0, description="倒计时秒数")
|
||||
current_index: int = Field(-1, description="当前索引")
|
||||
total_pending: int = Field(0, description="总待评价数")
|
||||
|
||||
|
||||
class TaskOperationData(BaseModel):
|
||||
"""任务操作数据模型"""
|
||||
|
||||
task_status: TaskStatusEnum = Field(TaskStatusEnum.IDLE, description="任务状态")
|
||||
|
||||
|
||||
class InitializeData(BaseModel):
|
||||
"""初始化数据模型"""
|
||||
|
||||
total_courses: int = Field(0, description="总课程数")
|
||||
pending_courses: int = Field(0, description="待评价课程数")
|
||||
course_list: List[CourseInfo] = Field(default_factory=list, description="课程列表")
|
||||
|
||||
|
||||
# 统一响应模型
|
||||
class EvaluationStatsResponse(BaseResponse[EvaluationStatsData]):
|
||||
"""评价统计信息响应模型"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CurrentCourseInfoResponse(BaseResponse[CurrentCourseInfoData]):
|
||||
"""当前评价课程信息响应模型"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TaskOperationResponse(BaseResponse[TaskOperationData]):
|
||||
"""任务操作响应模型"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InitializeResponse(BaseResponse[InitializeData]):
|
||||
"""初始化响应模型"""
|
||||
|
||||
pass
|
||||
122
router/jwc/model.py
Normal file
122
router/jwc/model.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from router.common_model import BaseResponse
|
||||
from provider.aufe.jwc.model import (
|
||||
AcademicInfo,
|
||||
TrainingPlanInfo,
|
||||
Course,
|
||||
ExamInfoResponse,
|
||||
TermScoreResponse,
|
||||
)
|
||||
from typing import List, Dict, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# 统一响应模型
|
||||
class AcademicInfoResponse(BaseResponse[AcademicInfo]):
|
||||
"""学业信息响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TrainingPlanInfoResponse(BaseResponse[TrainingPlanInfo]):
|
||||
"""培养方案信息响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CourseListResponse(BaseResponse[List[Course]]):
|
||||
"""评教课程列表响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ExamInfoAPIResponse(BaseResponse[ExamInfoResponse]):
|
||||
"""考试信息响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# ==================== 学期和成绩相关响应模型 ====================
|
||||
|
||||
|
||||
class FetchTermScoreRequest(BaseModel):
|
||||
"""获取学期成绩请求模型"""
|
||||
|
||||
term_id: str = Field(..., description="学期ID,如:2024-2025-2-1")
|
||||
course_code: str = Field("", description="课程代码(可选,用于筛选)")
|
||||
course_name: str = Field("", description="课程名称(可选,用于筛选)")
|
||||
page_num: int = Field(1, description="页码,默认为1", ge=1)
|
||||
page_size: int = Field(50, description="每页大小,默认为50", ge=1, le=100)
|
||||
|
||||
|
||||
class AllTermsResponse(BaseResponse[Dict[str, str]]):
|
||||
"""所有学期信息响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TermScoreAPIResponse(BaseResponse[TermScoreResponse]):
|
||||
"""学期成绩响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# ==================== 课表相关响应模型 ====================
|
||||
|
||||
|
||||
class TimeSlot(BaseModel):
|
||||
"""时间段模型"""
|
||||
|
||||
session: int = Field(..., description="节次")
|
||||
session_name: str = Field(..., description="节次名称")
|
||||
start_time: str = Field(..., description="开始时间,格式:HHMM")
|
||||
end_time: str = Field(..., description="结束时间,格式:HHMM")
|
||||
time_length: str = Field(..., description="时长(分钟)")
|
||||
djjc: int = Field(..., description="大节节次")
|
||||
|
||||
|
||||
class CourseTimeLocation(BaseModel):
|
||||
"""课程时间地点模型"""
|
||||
|
||||
class_day: int = Field(..., description="上课星期几(1-7)")
|
||||
class_sessions: int = Field(..., description="上课节次")
|
||||
continuing_session: int = Field(..., description="持续节次数")
|
||||
class_week: str = Field(..., description="上课周次(24位二进制字符串)")
|
||||
week_description: str = Field(..., description="上课周次描述")
|
||||
campus_name: str = Field(..., description="校区名称")
|
||||
teaching_building_name: str = Field(..., description="教学楼名称")
|
||||
classroom_name: str = Field(..., description="教室名称")
|
||||
|
||||
|
||||
class ScheduleCourse(BaseModel):
|
||||
"""课表课程模型"""
|
||||
|
||||
course_name: str = Field(..., description="课程名称")
|
||||
course_code: str = Field(..., description="课程代码")
|
||||
course_sequence: str = Field(..., description="课程序号")
|
||||
teacher_name: str = Field(..., description="授课教师")
|
||||
course_properties: str = Field(..., description="课程性质")
|
||||
exam_type: str = Field(..., description="考试类型")
|
||||
unit: float = Field(..., description="学分")
|
||||
time_locations: List[CourseTimeLocation] = Field(..., description="时间地点列表")
|
||||
is_no_schedule: bool = Field(False, description="是否无具体时间安排")
|
||||
|
||||
|
||||
class ScheduleData(BaseModel):
|
||||
"""课表数据模型"""
|
||||
|
||||
total_units: float = Field(..., description="总学分")
|
||||
time_slots: List[TimeSlot] = Field(..., description="时间段列表")
|
||||
courses: List[ScheduleCourse] = Field(..., description="课程列表")
|
||||
semester_info: Dict[str, str] = Field(..., description="学期信息")
|
||||
|
||||
|
||||
class ScheduleResponse(BaseResponse[ScheduleData]):
|
||||
"""课表响应模型"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FetchScheduleRequest(BaseModel):
|
||||
"""获取课表请求模型"""
|
||||
|
||||
plan_code: str = Field(..., description="培养方案代码,如:2024-2025-2-1")
|
||||
109
router/login/__init__.py
Normal file
109
router/login/__init__.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from database.user import User
|
||||
from database.creator import get_db_session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from router.login.model import (
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
AuthmeResponse,
|
||||
AuthmeStatusData,
|
||||
)
|
||||
from router.invite.model import AuthMeData
|
||||
from provider.aufe.client import AUFEConnection
|
||||
from provider.loveac.authme import manage_user_tokens, generate_device_id, fetch_user_by_token, AuthmeRequest
|
||||
import secrets
|
||||
|
||||
login_router = APIRouter(prefix="/api/v1/user")
|
||||
|
||||
|
||||
@login_router.post("/login", summary="用户登录")
|
||||
async def login_user(
|
||||
data: LoginRequest, asyncsession: AsyncSession = Depends(get_db_session)
|
||||
) -> LoginResponse:
|
||||
"""
|
||||
用户登录
|
||||
:param data: LoginRequest
|
||||
:return: LoginResponse
|
||||
"""
|
||||
async with asyncsession as session:
|
||||
userid = data.userid
|
||||
password = data.password
|
||||
easyconnect_password = data.easyconnect_password
|
||||
|
||||
# 检查用户是否存在
|
||||
existing_user = await session.execute(select(User).where(User.userid == userid))
|
||||
user = existing_user.scalars().first()
|
||||
if not user:
|
||||
return LoginResponse(
|
||||
code=400,
|
||||
message="用户不存在",
|
||||
data=None,
|
||||
)
|
||||
|
||||
# 检查连接
|
||||
vpn = AUFEConnection.create_or_get_connection("vpn.aufe.edu.cn", userid)
|
||||
# 检查连接是否已经存在,避免重复登录
|
||||
if not vpn.login_status():
|
||||
if not await vpn.login(userid, easyconnect_password):
|
||||
return LoginResponse(
|
||||
code=400,
|
||||
message="VPN登录失败,请检查用户名和密码",
|
||||
data=None,
|
||||
)
|
||||
if not vpn.uaap_login_status():
|
||||
if not await vpn.uaap_login(userid, password):
|
||||
return LoginResponse(
|
||||
code=400,
|
||||
message="大学登录失败,请检查用户名和密码",
|
||||
data=None,
|
||||
)
|
||||
|
||||
# 生成新的token和设备ID
|
||||
authme_token = secrets.token_urlsafe(128)
|
||||
device_id = generate_device_id()
|
||||
|
||||
# 使用新的token管理系统
|
||||
await manage_user_tokens(userid, authme_token, device_id, session)
|
||||
|
||||
return LoginResponse(
|
||||
code=200,
|
||||
message="登录成功",
|
||||
data=AuthMeData(authme_token=authme_token),
|
||||
)
|
||||
|
||||
|
||||
@login_router.post("/authme", summary="验证登录状态")
|
||||
async def check_auth_status(
|
||||
data: AuthmeRequest, asyncsession: AsyncSession = Depends(get_db_session)
|
||||
) -> AuthmeResponse:
|
||||
"""
|
||||
验证token是否有效,返回登录状态
|
||||
:param data: AuthmeRequest
|
||||
:return: AuthmeResponse
|
||||
"""
|
||||
try:
|
||||
# 使用已有的fetch_user_by_token函数验证token
|
||||
user = await fetch_user_by_token(data, asyncsession)
|
||||
|
||||
return AuthmeResponse(
|
||||
code=200,
|
||||
message="验证成功",
|
||||
data=AuthmeStatusData(
|
||||
is_logged_in=True,
|
||||
userid=user.userid
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
# token无效或其他错误
|
||||
return AuthmeResponse(
|
||||
code=401,
|
||||
message="token无效或已过期",
|
||||
data=AuthmeStatusData(
|
||||
is_logged_in=False,
|
||||
userid=None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
31
router/login/model.py
Normal file
31
router/login/model.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from router.common_model import BaseResponse
|
||||
from router.invite.model import AuthMeData
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
userid: str = Field(..., description="学号")
|
||||
password: str = Field(..., description="密码")
|
||||
easyconnect_password: str = Field(..., description="VPN密码")
|
||||
|
||||
|
||||
# 统一响应模型
|
||||
class LoginResponse(BaseResponse[AuthMeData]):
|
||||
"""登录响应"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Authme相关模型
|
||||
class AuthmeStatusData(BaseModel):
|
||||
"""认证状态数据"""
|
||||
|
||||
is_logged_in: bool = Field(..., description="是否处于登录状态")
|
||||
userid: Optional[str] = Field(None, description="用户ID")
|
||||
|
||||
|
||||
class AuthmeResponse(BaseResponse[AuthmeStatusData]):
|
||||
"""AuthMe验证响应"""
|
||||
|
||||
pass
|
||||
242
router/user/__init__.py
Normal file
242
router/user/__init__.py
Normal file
@@ -0,0 +1,242 @@
|
||||
import base64
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database.creator import get_db_session
|
||||
from database.user import UserProfile, User
|
||||
from provider.loveac.authme import fetch_user_by_token, AuthmeRequest
|
||||
from utils.file_manager import file_manager
|
||||
from .model import (
|
||||
UserProfileResponse,
|
||||
GetUserProfileRequest,
|
||||
UpdateUserProfileRequest,
|
||||
UserProfileData,
|
||||
UserSettings,
|
||||
)
|
||||
|
||||
user_router = APIRouter(prefix="/api/v1/user")
|
||||
|
||||
|
||||
@user_router.post("/profile/get", summary="获取用户资料")
|
||||
async def get_user_profile(
|
||||
data: GetUserProfileRequest,
|
||||
asyncsession: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
获取用户资料
|
||||
:param data: GetUserProfileRequest
|
||||
:return: UserProfileResponse
|
||||
"""
|
||||
try:
|
||||
# 使用token验证获取用户
|
||||
authme_request = AuthmeRequest(token=data.token)
|
||||
user = await fetch_user_by_token(authme_request, asyncsession)
|
||||
|
||||
async with asyncsession as session:
|
||||
result = await session.execute(
|
||||
select(UserProfile).where(UserProfile.userid == user.userid)
|
||||
)
|
||||
profile = result.scalars().first()
|
||||
|
||||
if not profile:
|
||||
# 如果用户资料不存在,创建默认资料
|
||||
profile = UserProfile(
|
||||
userid=user.userid,
|
||||
avatar_filename=None,
|
||||
background_filename=None,
|
||||
nickname=None,
|
||||
settings_filename=None
|
||||
)
|
||||
session.add(profile)
|
||||
await session.commit()
|
||||
|
||||
# 获取头像数据
|
||||
avatar_data = None
|
||||
if profile.avatar_filename:
|
||||
avatar_bytes = await file_manager.get_avatar(profile.avatar_filename)
|
||||
if avatar_bytes:
|
||||
# 转换为base64
|
||||
avatar_data = base64.b64encode(avatar_bytes).decode('utf-8')
|
||||
# 根据文件扩展名添加data URI前缀
|
||||
if profile.avatar_filename.endswith('.png'):
|
||||
avatar_data = f"data:image/png;base64,{avatar_data}"
|
||||
elif profile.avatar_filename.endswith(('.jpg', '.jpeg')):
|
||||
avatar_data = f"data:image/jpeg;base64,{avatar_data}"
|
||||
elif profile.avatar_filename.endswith('.gif'):
|
||||
avatar_data = f"data:image/gif;base64,{avatar_data}"
|
||||
|
||||
# 获取背景数据
|
||||
background_data = None
|
||||
if profile.background_filename:
|
||||
background_bytes = await file_manager.get_background(profile.background_filename)
|
||||
if background_bytes:
|
||||
# 转换为base64
|
||||
background_data = base64.b64encode(background_bytes).decode('utf-8')
|
||||
# 根据文件扩展名添加data URI前缀
|
||||
if profile.background_filename.endswith('.png'):
|
||||
background_data = f"data:image/png;base64,{background_data}"
|
||||
elif profile.background_filename.endswith(('.jpg', '.jpeg')):
|
||||
background_data = f"data:image/jpeg;base64,{background_data}"
|
||||
elif profile.background_filename.endswith('.gif'):
|
||||
background_data = f"data:image/gif;base64,{background_data}"
|
||||
elif profile.background_filename.endswith('.webp'):
|
||||
background_data = f"data:image/webp;base64,{background_data}"
|
||||
|
||||
# 获取设置数据
|
||||
settings_data = None
|
||||
if profile.settings_filename:
|
||||
settings_dict = await file_manager.get_settings(profile.settings_filename)
|
||||
if settings_dict:
|
||||
settings_data = UserSettings(**settings_dict)
|
||||
|
||||
profile_data = UserProfileData(
|
||||
userid=profile.userid,
|
||||
avatar=avatar_data,
|
||||
background=background_data,
|
||||
nickname=profile.nickname,
|
||||
settings=settings_data,
|
||||
)
|
||||
|
||||
return UserProfileResponse(
|
||||
code=200,
|
||||
message="获取用户资料成功",
|
||||
data=profile_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return UserProfileResponse(
|
||||
code=500,
|
||||
message=f"获取用户资料失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
|
||||
@user_router.post("/profile/update", summary="更新用户资料")
|
||||
async def update_user_profile(
|
||||
data: UpdateUserProfileRequest,
|
||||
asyncsession: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
更新用户资料
|
||||
:param data: UpdateUserProfileRequest
|
||||
:return: UserProfileResponse
|
||||
"""
|
||||
try:
|
||||
# 使用token验证获取用户
|
||||
authme_request = AuthmeRequest(token=data.token)
|
||||
user = await fetch_user_by_token(authme_request, asyncsession)
|
||||
|
||||
async with asyncsession as session:
|
||||
result = await session.execute(
|
||||
select(UserProfile).where(UserProfile.userid == user.userid)
|
||||
)
|
||||
profile = result.scalars().first()
|
||||
|
||||
if not profile:
|
||||
# 如果用户资料不存在,创建新的
|
||||
profile = UserProfile(
|
||||
userid=user.userid,
|
||||
avatar_filename=None,
|
||||
background_filename=None,
|
||||
nickname=data.nickname,
|
||||
settings_filename=None
|
||||
)
|
||||
session.add(profile)
|
||||
else:
|
||||
# 更新昵称
|
||||
if data.nickname is not None:
|
||||
profile.nickname = data.nickname
|
||||
|
||||
# 处理头像更新
|
||||
if data.avatar is not None:
|
||||
if data.avatar: # 如果头像不为空
|
||||
new_avatar_filename = await file_manager.save_avatar(user.userid, data.avatar)
|
||||
profile.avatar_filename = new_avatar_filename
|
||||
else: # 如果头像为空,表示删除头像
|
||||
if profile.avatar_filename:
|
||||
await file_manager.delete_avatar(profile.avatar_filename)
|
||||
profile.avatar_filename = None
|
||||
|
||||
# 处理背景更新
|
||||
if data.background is not None:
|
||||
if data.background: # 如果背景不为空
|
||||
new_background_filename = await file_manager.save_background(user.userid, data.background)
|
||||
profile.background_filename = new_background_filename
|
||||
else: # 如果背景为空,表示删除背景
|
||||
if profile.background_filename:
|
||||
await file_manager.delete_background(profile.background_filename)
|
||||
profile.background_filename = None
|
||||
|
||||
# 处理设置更新
|
||||
if data.settings is not None:
|
||||
if data.settings: # 如果设置不为空
|
||||
# data.settings在model验证时已经被转换为UserSettings对象
|
||||
if isinstance(data.settings, UserSettings):
|
||||
settings_dict = data.settings.model_dump()
|
||||
new_settings_filename = await file_manager.save_settings(user.userid, settings_dict)
|
||||
profile.settings_filename = new_settings_filename
|
||||
else:
|
||||
# 如果不是UserSettings对象,说明验证有问题
|
||||
raise ValueError(f"Settings对象类型错误: {type(data.settings)}")
|
||||
else: # 如果设置为空,表示删除设置
|
||||
if profile.settings_filename:
|
||||
await file_manager.delete_settings(profile.settings_filename)
|
||||
profile.settings_filename = None
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(profile)
|
||||
|
||||
# 获取更新后的数据
|
||||
avatar_data = None
|
||||
if profile.avatar_filename:
|
||||
avatar_bytes = await file_manager.get_avatar(profile.avatar_filename)
|
||||
if avatar_bytes:
|
||||
avatar_data = base64.b64encode(avatar_bytes).decode('utf-8')
|
||||
if profile.avatar_filename.endswith('.png'):
|
||||
avatar_data = f"data:image/png;base64,{avatar_data}"
|
||||
elif profile.avatar_filename.endswith(('.jpg', '.jpeg')):
|
||||
avatar_data = f"data:image/jpeg;base64,{avatar_data}"
|
||||
elif profile.avatar_filename.endswith('.gif'):
|
||||
avatar_data = f"data:image/gif;base64,{avatar_data}"
|
||||
|
||||
background_data = None
|
||||
if profile.background_filename:
|
||||
background_bytes = await file_manager.get_background(profile.background_filename)
|
||||
if background_bytes:
|
||||
background_data = base64.b64encode(background_bytes).decode('utf-8')
|
||||
if profile.background_filename.endswith('.png'):
|
||||
background_data = f"data:image/png;base64,{background_data}"
|
||||
elif profile.background_filename.endswith(('.jpg', '.jpeg')):
|
||||
background_data = f"data:image/jpeg;base64,{background_data}"
|
||||
elif profile.background_filename.endswith('.gif'):
|
||||
background_data = f"data:image/gif;base64,{background_data}"
|
||||
elif profile.background_filename.endswith('.webp'):
|
||||
background_data = f"data:image/webp;base64,{background_data}"
|
||||
|
||||
settings_data = None
|
||||
if profile.settings_filename:
|
||||
settings_dict = await file_manager.get_settings(profile.settings_filename)
|
||||
if settings_dict:
|
||||
settings_data = UserSettings(**settings_dict)
|
||||
|
||||
profile_data = UserProfileData(
|
||||
userid=profile.userid,
|
||||
avatar=avatar_data,
|
||||
background=background_data,
|
||||
nickname=profile.nickname,
|
||||
settings=settings_data,
|
||||
)
|
||||
|
||||
return UserProfileResponse(
|
||||
code=200,
|
||||
message="更新用户资料成功",
|
||||
data=profile_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return UserProfileResponse(
|
||||
code=500,
|
||||
message=f"更新用户资料失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
79
router/user/model.py
Normal file
79
router/user/model.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from router.common_model import BaseResponse
|
||||
from typing import Optional, Dict, Any, Union
|
||||
import json
|
||||
|
||||
|
||||
class UserSettings(BaseModel):
|
||||
"""用户设置模型"""
|
||||
theme: str = Field(..., description="主题模式")
|
||||
lightModeOpacity: float = Field(..., description="浅色模式透明度", ge=0.0, le=1.0)
|
||||
lightModeBrightness: float = Field(..., description="浅色模式亮度", ge=0.0, le=1.0)
|
||||
darkModeOpacity: float = Field(..., description="深色模式透明度", ge=0.0, le=1.0)
|
||||
darkModeBrightness: float = Field(..., description="深色模式亮度", ge=0.0, le=1.0)
|
||||
backgroundBlur: float = Field(..., description="背景模糊强度", ge=0.0, le=1.0)
|
||||
|
||||
@field_validator('theme')
|
||||
def validate_theme(cls, v):
|
||||
"""验证主题值"""
|
||||
valid_themes = ['light', 'dark', 'system', 'ThemeMode.light', 'ThemeMode.dark', 'ThemeMode.system']
|
||||
if v not in valid_themes:
|
||||
raise ValueError(f"无效的主题值: {v},有效值: {valid_themes}")
|
||||
return v
|
||||
|
||||
|
||||
class UserProfileData(BaseModel):
|
||||
"""用户资料数据模型"""
|
||||
userid: str = Field(..., description="用户ID")
|
||||
avatar: Optional[str] = Field(None, description="用户头像base64数据")
|
||||
background: Optional[str] = Field(None, description="用户背景base64数据")
|
||||
nickname: Optional[str] = Field(None, description="用户昵称")
|
||||
settings: Optional[UserSettings] = Field(None, description="用户设置对象")
|
||||
|
||||
|
||||
class GetUserProfileRequest(BaseModel):
|
||||
"""获取用户资料请求模型"""
|
||||
token: str = Field(..., description="用户认证token")
|
||||
|
||||
|
||||
class UpdateUserProfileRequest(BaseModel):
|
||||
"""更新用户资料请求模型"""
|
||||
token: str = Field(..., description="用户认证token")
|
||||
avatar: Optional[str] = Field(None, description="用户头像base64编码数据")
|
||||
background: Optional[str] = Field(None, description="用户背景base64编码数据")
|
||||
nickname: Optional[str] = Field(None, description="用户昵称")
|
||||
settings: Optional[Union[UserSettings, str]] = Field(None, description="用户设置对象或JSON字符串")
|
||||
|
||||
@field_validator('settings')
|
||||
def parse_settings(cls, v):
|
||||
"""解析settings字段,支持字符串和对象两种格式"""
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
# 如果已经是UserSettings对象,直接返回
|
||||
if isinstance(v, UserSettings):
|
||||
return v
|
||||
|
||||
# 如果是字符串,尝试解析为JSON然后创建UserSettings对象
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
settings_dict = json.loads(v)
|
||||
return UserSettings(**settings_dict)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"settings字段JSON格式错误: {str(e)}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"settings字段验证失败: {str(e)}")
|
||||
|
||||
# 如果是字典,直接创建UserSettings对象
|
||||
if isinstance(v, dict):
|
||||
try:
|
||||
return UserSettings(**v)
|
||||
except Exception as e:
|
||||
raise ValueError(f"settings字段验证失败: {str(e)}")
|
||||
|
||||
raise ValueError("settings字段必须是JSON字符串、字典或UserSettings对象")
|
||||
|
||||
|
||||
class UserProfileResponse(BaseResponse[UserProfileData]):
|
||||
"""用户资料响应模型"""
|
||||
pass
|
||||
Reference in New Issue
Block a user