247 lines
10 KiB
Python
247 lines
10 KiB
Python
|
|
import asyncio
|
|||
|
|
import re
|
|||
|
|
|
|||
|
|
from bs4 import BeautifulSoup
|
|||
|
|
from fastapi import APIRouter, Depends
|
|||
|
|
from fastapi.responses import JSONResponse
|
|||
|
|
from pydantic import ValidationError
|
|||
|
|
|
|||
|
|
from loveace.router.endpoint.jwc.model.base import JWCConfig
|
|||
|
|
from loveace.router.endpoint.jwc.model.schedule import ScheduleData
|
|||
|
|
from loveace.router.schemas.error import ProtectRouterErrorToCode
|
|||
|
|
from loveace.router.schemas.uniresponse import UniResponseModel
|
|||
|
|
from loveace.service.remote.aufe import AUFEConnection
|
|||
|
|
from loveace.service.remote.aufe.depends import get_aufe_conn
|
|||
|
|
|
|||
|
|
jwc_schedules_router = APIRouter(
|
|||
|
|
prefix="/schedule",
|
|||
|
|
responses=ProtectRouterErrorToCode.gen_code_table(),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
ENDPOINTS = {
|
|||
|
|
"student_schedule_pre": "/student/courseSelect/calendarSemesterCurriculum/index",
|
|||
|
|
"student_schedule": "/student/courseSelect/thisSemesterCurriculum/{dynamic_path}/ajaxStudentSchedule/past/callback",
|
|||
|
|
"section_and_time": "/ajax/getSectionAndTime",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@jwc_schedules_router.get(
|
|||
|
|
"/{term_code}/table",
|
|||
|
|
summary="获取课表信息",
|
|||
|
|
response_model=UniResponseModel[ScheduleData],
|
|||
|
|
)
|
|||
|
|
async def get_schedule_table(
|
|||
|
|
term_code: str, conn: AUFEConnection = Depends(get_aufe_conn)
|
|||
|
|
) -> UniResponseModel[ScheduleData] | JSONResponse:
|
|||
|
|
"""
|
|||
|
|
获取指定学期的课程表
|
|||
|
|
|
|||
|
|
✅ 功能特性:
|
|||
|
|
- 获取指定学期的完整课程表
|
|||
|
|
- 显示课程名称、教室、时间、教师等信息
|
|||
|
|
- 支持按周查询
|
|||
|
|
|
|||
|
|
💡 使用场景:
|
|||
|
|
- 查看本周课程安排
|
|||
|
|
- 了解完整学期课程表
|
|||
|
|
- 课表分享和导出
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
term_code: 学期代码(如:2023-2024-1)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
ScheduleData: 包含课程表数据和课程详情
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
conn.logger.info(f"开始获取学期 {term_code} 的课表信息")
|
|||
|
|
# 第一步:访问课表预备页面,获取动态路径
|
|||
|
|
|
|||
|
|
dynamic_page = JWCConfig().to_full_url(ENDPOINTS["student_schedule_pre"])
|
|||
|
|
dynamic_page_response = await conn.client.get(
|
|||
|
|
dynamic_page, follow_redirects=True, timeout=conn.timeout
|
|||
|
|
)
|
|||
|
|
if dynamic_page_response.status_code != 200:
|
|||
|
|
conn.logger.error(
|
|||
|
|
f"获取课表预备页面失败,状态码: {dynamic_page_response.status_code}"
|
|||
|
|
)
|
|||
|
|
return ProtectRouterErrorToCode.remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
soup = BeautifulSoup(dynamic_page_response.text, "lxml")
|
|||
|
|
|
|||
|
|
# 尝试从页面中提取动态路径
|
|||
|
|
scripts = soup.find_all("script")
|
|||
|
|
dynamic_path = "B2RMNJkT95" # 默认值
|
|||
|
|
for script in scripts:
|
|||
|
|
try:
|
|||
|
|
script_text = script.string # type: ignore
|
|||
|
|
if script_text and "ajaxStudentSchedule" in script_text:
|
|||
|
|
# 使用正则表达式提取路径
|
|||
|
|
match = re.search(
|
|||
|
|
r"/([A-Za-z0-9]+)/ajaxStudentSchedule", script_text
|
|||
|
|
)
|
|||
|
|
if match:
|
|||
|
|
dynamic_path = match.group(1)
|
|||
|
|
break
|
|||
|
|
except AttributeError:
|
|||
|
|
continue
|
|||
|
|
section_and_time_headers = {
|
|||
|
|
**conn.client.headers,
|
|||
|
|
"Referer": JWCConfig().to_full_url(ENDPOINTS["student_schedule"]),
|
|||
|
|
}
|
|||
|
|
select_and_time_url = JWCConfig().to_full_url(ENDPOINTS["section_and_time"])
|
|||
|
|
select_and_time_data = {
|
|||
|
|
"planNumber": "",
|
|||
|
|
"ff": "f",
|
|||
|
|
"sf_request_type": "ajax",
|
|||
|
|
}
|
|||
|
|
section_and_time_response_coro = conn.client.post(
|
|||
|
|
select_and_time_url,
|
|||
|
|
data=select_and_time_data,
|
|||
|
|
headers=section_and_time_headers,
|
|||
|
|
follow_redirects=True,
|
|||
|
|
timeout=conn.timeout,
|
|||
|
|
)
|
|||
|
|
student_schedule_url = JWCConfig().to_full_url(
|
|||
|
|
ENDPOINTS["student_schedule"].format(dynamic_path=dynamic_path)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
schedule_params = {
|
|||
|
|
"planCode": term_code,
|
|||
|
|
"sf_request_type": "ajax",
|
|||
|
|
}
|
|||
|
|
student_schedule_response_coro = conn.client.get(
|
|||
|
|
student_schedule_url,
|
|||
|
|
params=schedule_params,
|
|||
|
|
follow_redirects=True,
|
|||
|
|
timeout=conn.timeout,
|
|||
|
|
)
|
|||
|
|
section_and_time_response, student_schedule_response = await asyncio.gather(
|
|||
|
|
section_and_time_response_coro, student_schedule_response_coro
|
|||
|
|
)
|
|||
|
|
if section_and_time_response.status_code != 200:
|
|||
|
|
conn.logger.error(
|
|||
|
|
f"获取节次时间信息失败,状态码: {section_and_time_response.status_code}"
|
|||
|
|
)
|
|||
|
|
return ProtectRouterErrorToCode.remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id, message="无法获取节次时间信息,请稍后再试"
|
|||
|
|
)
|
|||
|
|
if student_schedule_response.status_code != 200:
|
|||
|
|
conn.logger.error(
|
|||
|
|
f"获取课表信息失败,状态码: {student_schedule_response.status_code}"
|
|||
|
|
)
|
|||
|
|
return ProtectRouterErrorToCode.remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id, message="无法获取课表信息,请稍后再试"
|
|||
|
|
)
|
|||
|
|
time_data = section_and_time_response.json()
|
|||
|
|
schedule_data = student_schedule_response.json()
|
|||
|
|
|
|||
|
|
# 处理时间段信息
|
|||
|
|
time_slots = []
|
|||
|
|
section_time = time_data.get("sectionTime", [])
|
|||
|
|
for time_slot in section_time:
|
|||
|
|
time_slots.append(
|
|||
|
|
{
|
|||
|
|
"session": time_slot.get("id", {}).get("session", 0),
|
|||
|
|
"session_name": time_slot.get("sessionName", ""),
|
|||
|
|
"start_time": time_slot.get("startTime", ""),
|
|||
|
|
"end_time": time_slot.get("endTime", ""),
|
|||
|
|
"time_length": time_slot.get("timeLength", ""),
|
|||
|
|
"djjc": time_slot.get("djjc", 0),
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 处理课程信息
|
|||
|
|
courses = []
|
|||
|
|
xkxx_list = schedule_data.get("xkxx", [])
|
|||
|
|
|
|||
|
|
for xkxx_item in xkxx_list:
|
|||
|
|
if isinstance(xkxx_item, dict):
|
|||
|
|
for course_key, course_data in xkxx_item.items():
|
|||
|
|
if isinstance(course_data, dict):
|
|||
|
|
# 提取基本课程信息
|
|||
|
|
course_name = course_data.get("courseName", "")
|
|||
|
|
course_code = course_data.get("id", {}).get("coureNumber", "")
|
|||
|
|
course_sequence = course_data.get("id", {}).get(
|
|||
|
|
"coureSequenceNumber", ""
|
|||
|
|
)
|
|||
|
|
teacher_name = (
|
|||
|
|
course_data.get("attendClassTeacher", "")
|
|||
|
|
.replace("* ", "")
|
|||
|
|
.strip()
|
|||
|
|
)
|
|||
|
|
course_properties = course_data.get("coursePropertiesName", "")
|
|||
|
|
exam_type = course_data.get("examTypeName", "")
|
|||
|
|
unit = float(course_data.get("unit", 0))
|
|||
|
|
|
|||
|
|
# 处理时间地点列表
|
|||
|
|
time_locations = []
|
|||
|
|
time_place_list = course_data.get("timeAndPlaceList", [])
|
|||
|
|
|
|||
|
|
# 检查是否有具体时间安排
|
|||
|
|
is_no_schedule = len(time_place_list) == 0
|
|||
|
|
|
|||
|
|
for time_place in time_place_list:
|
|||
|
|
# 过滤掉无用的字段,只保留关键信息
|
|||
|
|
time_location = {
|
|||
|
|
"class_day": time_place.get("classDay", 0),
|
|||
|
|
"class_sessions": time_place.get("classSessions", 0),
|
|||
|
|
"continuing_session": time_place.get(
|
|||
|
|
"continuingSession", 0
|
|||
|
|
),
|
|||
|
|
"class_week": time_place.get("classWeek", ""),
|
|||
|
|
"week_description": time_place.get(
|
|||
|
|
"weekDescription", ""
|
|||
|
|
),
|
|||
|
|
"campus_name": time_place.get("campusName", ""),
|
|||
|
|
"teaching_building_name": time_place.get(
|
|||
|
|
"teachingBuildingName", ""
|
|||
|
|
),
|
|||
|
|
"classroom_name": time_place.get("classroomName", ""),
|
|||
|
|
}
|
|||
|
|
time_locations.append(time_location)
|
|||
|
|
|
|||
|
|
# 只保留有效的课程(有课程名称的)
|
|||
|
|
if course_name:
|
|||
|
|
course = {
|
|||
|
|
"course_name": course_name,
|
|||
|
|
"course_code": course_code,
|
|||
|
|
"course_sequence": course_sequence,
|
|||
|
|
"teacher_name": teacher_name,
|
|||
|
|
"course_properties": course_properties,
|
|||
|
|
"exam_type": exam_type,
|
|||
|
|
"unit": unit,
|
|||
|
|
"time_locations": time_locations,
|
|||
|
|
"is_no_schedule": is_no_schedule,
|
|||
|
|
}
|
|||
|
|
courses.append(course)
|
|||
|
|
# 构建最终数据
|
|||
|
|
processed_data = {
|
|||
|
|
"total_units": float(schedule_data.get("allUnits", 0)),
|
|||
|
|
"time_slots": time_slots,
|
|||
|
|
"courses": courses,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
conn.logger.info(
|
|||
|
|
f"成功处理课表数据:共{len(courses)}门课程,{len(time_slots)}个时间段"
|
|||
|
|
)
|
|||
|
|
result = ScheduleData.model_validate(processed_data)
|
|||
|
|
return UniResponseModel[ScheduleData](
|
|||
|
|
success=True,
|
|||
|
|
data=result,
|
|||
|
|
message="获取课表信息成功",
|
|||
|
|
error=None,
|
|||
|
|
)
|
|||
|
|
except ValidationError as ve:
|
|||
|
|
conn.logger.error(f"数据验证错误: {ve}")
|
|||
|
|
return ProtectRouterErrorToCode().validation_error.to_json_response(
|
|||
|
|
conn.logger.trace_id, "数据验证错误"
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
conn.logger.exception(e)
|
|||
|
|
return ProtectRouterErrorToCode().server_error.to_json_response(
|
|||
|
|
conn.logger.trace_id,
|
|||
|
|
)
|