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

247 lines
10 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 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,
)