🎉初次提交

This commit is contained in:
2025-08-03 16:50:56 +08:00
commit 56bdf5388d
67 changed files with 18379 additions and 0 deletions

605
router/jwc/__init__.py Normal file
View 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
View 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()

View 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
View 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="学期ID2024-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")