Files
LoveACE-EndF/loveace/router/endpoint/jwc/utils/exam.py
Sibuxiangx bbc86b8330 ⚒️ 重大重构 LoveACE V2
引入了 mongodb
对数据库进行了一定程度的数据加密
性能改善
代码简化
统一错误模型和响应
使用 apifox 作为文档
2025-11-20 20:44:25 +08:00

338 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import time
from json import JSONDecodeError
from typing import List, Optional
from bs4 import BeautifulSoup
from loveace.router.endpoint.jwc.model.base import JWCConfig
from loveace.router.endpoint.jwc.model.exam import (
ExamInfoResponse,
ExamScheduleItem,
OtherExamRecord,
OtherExamResponse,
SeatInfo,
UnifiedExamInfo,
)
from loveace.service.remote.aufe import AUFEConnection
ENDPOINTS = {
"school_exam_pre_request": "/student/examinationManagement/examPlan/index",
"school_exam_request": "/student/examinationManagement/examPlan/detail",
"seat_info": "/student/examinationManagement/examPlan/index",
"other_exam_record": "/student/examinationManagement/othersExamPlan/queryScores?sf_request_type=ajax",
}
# +++++===== 考试信息前置方法 =====+++++ #
async def fetch_school_exam_schedule(
start_date: str, end_date: str, conn: AUFEConnection
) -> List[ExamScheduleItem]:
"""
获取校统考考试安排
Args:
start_date: 开始日期 (YYYY-MM-DD)
end_date: 结束日期 (YYYY-MM-DD)
Returns:
List[ExamScheduleItem]: 校统考列表
"""
try:
timestamp = int(time.time() * 1000)
headers = {
**conn.client.headers,
"Accept": "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest",
}
params = {
"start": start_date,
"end": end_date,
"_": str(timestamp),
"sf_request_type": "ajax",
}
await conn.client.get(
url=JWCConfig().to_full_url(ENDPOINTS["school_exam_pre_request"]),
follow_redirects=True,
headers=headers,
timeout=conn.timeout,
)
response = await conn.client.get(
url=JWCConfig().to_full_url(ENDPOINTS["school_exam_request"]),
headers=headers,
params=params,
follow_redirects=True,
timeout=conn.timeout,
)
if response.status_code != 200:
conn.logger.error(f"获取校统考信息失败: HTTP状态码 {response.status_code}")
return []
if "]" == response.text:
conn.logger.warning("获取校统考信息成功,但无数据")
return []
try:
json_data = response.json()
except JSONDecodeError as e:
conn.logger.error(f"解析校统考信息JSON失败: {str(e)}")
return []
# 解析为ExamScheduleItem列表
school_exams = []
if isinstance(json_data, list):
for item in json_data:
exam_item = ExamScheduleItem.model_validate(item)
school_exams.append(exam_item)
conn.logger.info(f"获取校统考信息成功,共 {len(school_exams)} 场考试")
return school_exams
except Exception as e:
conn.logger.error(f"获取校统考信息出现如下异常: {str(e)}")
return []
async def fetch_exam_seat_info(conn: AUFEConnection) -> List[SeatInfo]:
"""
获取考试座位号信息
conn: AUFEConnection
Returns:
List[SeatInfo]: 座位信息列表
"""
try:
headers = {
**conn.client.headers,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
}
response = await conn.client.get(
url=JWCConfig().to_full_url(ENDPOINTS["seat_info"]),
headers=headers,
follow_redirects=True,
timeout=conn.timeout,
)
if response.status_code != 200:
conn.logger.error(
f"获取考试座位号信息失败: HTTP状态码 {response.status_code}"
)
return []
soup = BeautifulSoup(response.text, "lxml")
seat_infos = []
# 查找所有考试信息区块
exam_blocks = soup.find_all("div", {"class": "widget-box"})
for block in exam_blocks:
course_name = ""
seat_number = ""
# 获取课程名
title = block.find("h5", {"class": "widget-title"}) # type: ignore
if title:
course_text = title.get_text(strip=True) # type: ignore
# 提取课程名,格式可能是: "(课程代码-班号)课程名"
if "" in course_text:
course_name = course_text.split("", 1)[1].strip()
else:
course_name = course_text.strip()
# 获取座位号
widget_main = block.find("div", {"class": "widget-main"}) # type: ignore
if widget_main:
content = widget_main.get_text() # type: ignore
for line in content.split("\n"):
if "座位号" in line:
try:
seat_number = line.split("座位号:")[1].strip()
except Exception:
try:
seat_number = line.split("座位号:")[1].strip()
except Exception:
pass
break
if course_name and seat_number:
seat_infos.append(
SeatInfo(course_name=course_name, seat_number=seat_number)
)
conn.logger.info(f"获取考试座位号信息成功,共 {len(seat_infos)} 条记录")
return seat_infos
except Exception as e:
conn.logger.error(f"获取考试座位号信息异常: {str(e)}")
return []
def convert_school_exam_to_unified(
exam: ExamScheduleItem, seat_infos: List[SeatInfo], conn: AUFEConnection
) -> Optional[UnifiedExamInfo]:
"""
将校统考数据转换为统一格式
Args:
exam: 校统考项目
seat_info: 座位号信息映射
Returns:
Optional[UnifiedExamInfo]: 统一格式的考试信息
"""
try:
# 解析title信息格式如: "新媒体导论\n08:30-10:30\n西校\n西校通慧楼\n通慧楼-308\n"
title_parts = exam.title.strip().split("\n")
if len(title_parts) < 2:
return None
course_name = title_parts[0]
exam_time = title_parts[1] if len(title_parts) > 1 else ""
# 拼接地点信息
location_parts = title_parts[2:] if len(title_parts) > 2 else []
exam_location = " ".join([part for part in location_parts if part.strip()])
# 添加座位号到备注
note = ""
for seat in seat_infos:
if seat.course_name == course_name:
note = f"座位号: {seat.seat_number}"
note = note.removesuffix("准考证号:")
break
return UnifiedExamInfo(
course_name=course_name,
exam_date=exam.start,
exam_time=exam_time,
exam_location=exam_location,
exam_type="校统考",
note=note,
)
except Exception as e:
conn.logger.error(f"转换校统考数据异常: {str(e)}")
return None
async def fetch_other_exam_records(
term_code: str, conn: AUFEConnection
) -> List[OtherExamRecord]:
"""
获取其他考试记录
Args:
term_code: 学期代码
conn: AUFEConnection
Returns:
List: 其他考试记录列表
"""
try:
headers = {
**conn.client.headers,
"Accept": "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
}
data = {"zxjxjhh": term_code, "tab": "0", "pageNum": "1", "pageSize": "30"}
response = await conn.client.post(
url=JWCConfig().to_full_url(ENDPOINTS["other_exam_record"]),
headers=headers,
data=data,
follow_redirects=True,
timeout=conn.timeout,
)
valid = OtherExamResponse.model_validate_json(response.text)
if valid.records:
conn.logger.info(f"获取其他考试信息成功,共 {len(valid.records)} 条记录")
return valid.records
else:
conn.logger.warning("获取其他考试信息成功,但无记录")
return []
except Exception as e:
conn.logger.error(f"获取其他考试信息出现如下异常: {str(e)}")
return []
def convert_other_exam_to_unified(
record: OtherExamRecord, conn: AUFEConnection
) -> Optional[UnifiedExamInfo]:
"""
将其他考试记录转换为统一格式
Args:
record: 其他考试记录
Returns:
Optional[UnifiedExamInfo]: 统一格式的考试信息
"""
try:
return UnifiedExamInfo(
course_name=record.course_name,
exam_date=record.exam_date,
exam_time=record.exam_time,
exam_location=record.exam_location,
exam_type="其他考试",
note=record.note,
)
except Exception as e:
conn.logger.error(f"转换其他考试数据异常: {str(e)}")
return None
async def fetch_unified_exam_info(
conn: AUFEConnection,
start_date: str,
end_date: str,
term_code: str = "2024-2025-2-1",
) -> ExamInfoResponse:
"""
获取统一的考试信息,包括校统考和其他考试
Args:
start_date: 开始日期 (YYYY-MM-DD)
end_date: 结束日期 (YYYY-MM-DD)
term_code: 学期代码,默认为当前学期
Returns:
ExamInfoResponse: 统一的考试信息响应
"""
try:
# 合并并转换为统一格式
unified_exams = []
# 获取校统考信息
if school_exams := await fetch_school_exam_schedule(start_date, end_date, conn):
# 获取座位号信息
seat_info = await fetch_exam_seat_info(conn)
# 处理校统考数据
for exam in school_exams:
unified_exam = convert_school_exam_to_unified(exam, seat_info, conn)
if unified_exam:
unified_exams.append(unified_exam)
# 获取其他考试信息
other_exams = await fetch_other_exam_records(term_code, conn)
# 处理其他考试数据
for record in other_exams:
unified_exam = convert_other_exam_to_unified(record, conn)
if unified_exam:
unified_exams.append(unified_exam)
# 按考试日期排序
def _sort_key(exam: UnifiedExamInfo) -> str:
return exam.exam_date + " " + exam.exam_time
unified_exams.sort(key=_sort_key)
return ExamInfoResponse(
exams=unified_exams,
total_count=len(unified_exams),
)
except Exception:
raise