338 lines
11 KiB
Python
338 lines
11 KiB
Python
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
|