307 lines
11 KiB
Python
307 lines
11 KiB
Python
|
|
import re
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
from bs4 import BeautifulSoup
|
|||
|
|
from fastapi import APIRouter, Depends
|
|||
|
|
from fastapi.responses import JSONResponse
|
|||
|
|
from httpx import HTTPError
|
|||
|
|
from pydantic import ValidationError
|
|||
|
|
|
|||
|
|
from loveace.router.endpoint.jwc.model.base import JWCConfig
|
|||
|
|
from loveace.router.endpoint.jwc.model.term import CurrentTermInfo, TermItem
|
|||
|
|
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_term_router = APIRouter(
|
|||
|
|
prefix="/term",
|
|||
|
|
responses=ProtectRouterErrorToCode().gen_code_table(),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
ENDPOINT = {
|
|||
|
|
"all_terms": "/student/courseSelect/calendarSemesterCurriculum/index",
|
|||
|
|
"calendar": "/indexCalendar",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@jwc_term_router.get(
|
|||
|
|
"/all",
|
|||
|
|
summary="获取所有学期信息",
|
|||
|
|
response_model=UniResponseModel[list[TermItem]],
|
|||
|
|
)
|
|||
|
|
async def get_all_terms(
|
|||
|
|
conn: AUFEConnection = Depends(get_aufe_conn),
|
|||
|
|
) -> UniResponseModel[list[TermItem]] | JSONResponse:
|
|||
|
|
"""
|
|||
|
|
获取用户可选的所有学期列表
|
|||
|
|
|
|||
|
|
✅ 功能特性:
|
|||
|
|
- 获取从入学至今的所有学期
|
|||
|
|
- 标记当前学期
|
|||
|
|
- 学期名称格式统一处理
|
|||
|
|
|
|||
|
|
💡 使用场景:
|
|||
|
|
- 选课系统的学期选择菜单
|
|||
|
|
- 成绩查询的学期选择
|
|||
|
|
- 课程表查询的学期选择
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
list[TermItem]: 学期列表,包含学期代码、名称、是否为当前学期
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
all_terms = []
|
|||
|
|
response = await conn.client.get(
|
|||
|
|
JWCConfig().to_full_url(ENDPOINT["all_terms"]),
|
|||
|
|
follow_redirects=True,
|
|||
|
|
timeout=conn.timeout,
|
|||
|
|
)
|
|||
|
|
if response.status_code != 200:
|
|||
|
|
conn.logger.error(f"获取学期信息失败,状态码: {response.status_code}")
|
|||
|
|
return ProtectRouterErrorToCode().remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
# 解析HTML获取学期选项
|
|||
|
|
soup = BeautifulSoup(response.text, "lxml")
|
|||
|
|
|
|||
|
|
# 查找学期选择下拉框
|
|||
|
|
select_element = soup.find("select", {"id": "planCode"})
|
|||
|
|
if not select_element:
|
|||
|
|
conn.logger.error("未找到学期选择框")
|
|||
|
|
return UniResponseModel[list[TermItem]](
|
|||
|
|
success=False,
|
|||
|
|
data=[],
|
|||
|
|
message="未找到学期选择框",
|
|||
|
|
error=None,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
terms = {}
|
|||
|
|
# 使用更安全的方式处理选项
|
|||
|
|
try:
|
|||
|
|
options = select_element.find_all("option") # type: ignore
|
|||
|
|
for option in options:
|
|||
|
|
value = option.get("value") # type: ignore
|
|||
|
|
text = option.get_text(strip=True) # type: ignore
|
|||
|
|
|
|||
|
|
# 跳过空值选项(如"全部")
|
|||
|
|
if value and str(value).strip() and text != "全部":
|
|||
|
|
terms[str(value)] = text
|
|||
|
|
except AttributeError:
|
|||
|
|
conn.logger.error("解析学期选项失败")
|
|||
|
|
return UniResponseModel[list[TermItem]](
|
|||
|
|
success=False,
|
|||
|
|
data=[],
|
|||
|
|
message="解析学期选项失败",
|
|||
|
|
error=None,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
conn.logger.info(f"成功获取{len(terms)}个学期信息")
|
|||
|
|
counter = 0
|
|||
|
|
# 遍历学期选项,提取学期代码和名称
|
|||
|
|
# 将学期中的 "春" 替换为 "下" , "秋" 替换为 "上"
|
|||
|
|
for key, value in terms.items():
|
|||
|
|
counter += 1
|
|||
|
|
value = value.replace("春", "下").replace("秋", "上")
|
|||
|
|
all_terms.append(
|
|||
|
|
TermItem(term_code=key, term_name=value, is_current=counter == 1)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return UniResponseModel[list[TermItem]](
|
|||
|
|
success=True,
|
|||
|
|
data=all_terms,
|
|||
|
|
message="获取学期信息成功",
|
|||
|
|
error=None,
|
|||
|
|
)
|
|||
|
|
except ValidationError as ve:
|
|||
|
|
conn.logger.error(f"数据验证错误: {ve}")
|
|||
|
|
return ProtectRouterErrorToCode().validation_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
except HTTPError as he:
|
|||
|
|
conn.logger.error(f"HTTP请求错误: {he}")
|
|||
|
|
return ProtectRouterErrorToCode().remote_service_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
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@jwc_term_router.get(
|
|||
|
|
"/current",
|
|||
|
|
summary="获取当前学期信息",
|
|||
|
|
response_model=UniResponseModel[CurrentTermInfo],
|
|||
|
|
)
|
|||
|
|
async def get_current_term(
|
|||
|
|
conn: AUFEConnection = Depends(get_aufe_conn),
|
|||
|
|
) -> UniResponseModel[CurrentTermInfo] | JSONResponse:
|
|||
|
|
"""
|
|||
|
|
获取当前学期的详细信息
|
|||
|
|
|
|||
|
|
✅ 功能特性:
|
|||
|
|
- 获取当前学期的开始和结束日期
|
|||
|
|
- 获取学期周数信息
|
|||
|
|
- 实时从教务系统获取
|
|||
|
|
|
|||
|
|
💡 使用场景:
|
|||
|
|
- 显示当前学期进度
|
|||
|
|
- 课程表的周次显示参考
|
|||
|
|
- 学期时间提醒
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
CurrentTermInfo: 包含学期代码、名称、开始日期、结束日期等
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
info_response = await conn.client.get(
|
|||
|
|
JWCConfig().DEFAULT_BASE_URL, follow_redirects=True, timeout=conn.timeout
|
|||
|
|
)
|
|||
|
|
if info_response.status_code != 200:
|
|||
|
|
conn.logger.error(
|
|||
|
|
f"获取学期信息页面失败,状态码: {info_response.status_code}"
|
|||
|
|
)
|
|||
|
|
return ProtectRouterErrorToCode().remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
start_response = await conn.client.get(
|
|||
|
|
JWCConfig().to_full_url(ENDPOINT["calendar"]),
|
|||
|
|
follow_redirects=True,
|
|||
|
|
timeout=conn.timeout,
|
|||
|
|
)
|
|||
|
|
if start_response.status_code != 200:
|
|||
|
|
conn.logger.error(
|
|||
|
|
f"获取学期开始时间失败,状态码: {start_response.status_code}"
|
|||
|
|
)
|
|||
|
|
return ProtectRouterErrorToCode().remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
# 提取学期开始时间
|
|||
|
|
flexible_pattern = r'var\s+rq\s*=\s*"(\d{8})";\s*//.*'
|
|||
|
|
match = re.findall(flexible_pattern, start_response.text)
|
|||
|
|
if not match:
|
|||
|
|
conn.logger.error("未找到学期开始时间")
|
|||
|
|
return ProtectRouterErrorToCode().remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
start_date_str = match[0]
|
|||
|
|
try:
|
|||
|
|
start_date = datetime.strptime(start_date_str, "%Y%m%d").date()
|
|||
|
|
except ValueError:
|
|||
|
|
conn.logger.error(f"学期开始时间格式错误: {start_date_str}")
|
|||
|
|
return ProtectRouterErrorToCode().remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
start_date = datetime.strptime(start_date_str, "%Y%m%d").date()
|
|||
|
|
|
|||
|
|
html_content = info_response.text
|
|||
|
|
|
|||
|
|
# 使用BeautifulSoup解析HTML
|
|||
|
|
soup = BeautifulSoup(html_content, "html.parser")
|
|||
|
|
|
|||
|
|
# 查找包含学期周数信息的元素
|
|||
|
|
# 使用CSS选择器查找
|
|||
|
|
calendar_element = soup.select_one(
|
|||
|
|
"#navbar-container > div.navbar-buttons.navbar-header.pull-right > ul > li.light-red > a"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if not calendar_element:
|
|||
|
|
# 如果CSS选择器失败,尝试其他方法
|
|||
|
|
# 查找包含"第X周"的元素
|
|||
|
|
potential_elements = soup.find_all("a", class_="dropdown-toggle")
|
|||
|
|
calendar_element = None
|
|||
|
|
|
|||
|
|
for element in potential_elements:
|
|||
|
|
text = element.get_text(strip=True) if element else ""
|
|||
|
|
if "第" in text and "周" in text:
|
|||
|
|
calendar_element = element
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# 如果还是找不到,尝试查找任何包含学期信息的元素
|
|||
|
|
if not calendar_element:
|
|||
|
|
all_elements = soup.find_all(text=re.compile(r"\d{4}-\d{4}.*第\d+周"))
|
|||
|
|
if all_elements:
|
|||
|
|
# 找到包含学期信息的文本,查找其父元素
|
|||
|
|
for text_node in all_elements:
|
|||
|
|
parent = text_node.parent
|
|||
|
|
if parent:
|
|||
|
|
calendar_element = parent
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not calendar_element:
|
|||
|
|
conn.logger.warning("未找到学期周数信息元素")
|
|||
|
|
|
|||
|
|
# 尝试在整个页面中搜索学期信息模式
|
|||
|
|
semester_pattern = re.search(
|
|||
|
|
r"(\d{4}-\d{4})\s*(春|秋|夏)?\s*第(\d+)周\s*(星期[一二三四五六日天])?",
|
|||
|
|
html_content,
|
|||
|
|
)
|
|||
|
|
if semester_pattern:
|
|||
|
|
calendar_text = semester_pattern.group(0)
|
|||
|
|
conn.logger.info(f"通过正则表达式找到学期信息: {calendar_text}")
|
|||
|
|
else:
|
|||
|
|
conn.logger.debug(f"HTML内容长度: {len(html_content)}")
|
|||
|
|
conn.logger.debug(
|
|||
|
|
"未检测到学期周数相关内容,可能需要重新登录或检查访问权限"
|
|||
|
|
)
|
|||
|
|
return ProtectRouterErrorToCode().remote_service_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
# 提取文本内容
|
|||
|
|
calendar_text = calendar_element.get_text(strip=True)
|
|||
|
|
conn.logger.info(f"找到学期周数信息: {calendar_text}")
|
|||
|
|
clean_text = re.sub(r"\s+", " ", calendar_text.strip())
|
|||
|
|
|
|||
|
|
# 初始化默认值
|
|||
|
|
academic_year = ""
|
|||
|
|
term = ""
|
|||
|
|
week_number = 0
|
|||
|
|
is_end = False
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 解析学年:2025-2026
|
|||
|
|
year_match = re.search(r"(\d{4}-\d{4})", clean_text)
|
|||
|
|
if year_match:
|
|||
|
|
academic_year = year_match.group(1)
|
|||
|
|
|
|||
|
|
# 解析学期:秋、春
|
|||
|
|
semester_match = re.search(r"(春|秋)", clean_text)
|
|||
|
|
if semester_match:
|
|||
|
|
term = semester_match.group(1)
|
|||
|
|
|
|||
|
|
# 解析周数:第1周、第15周等
|
|||
|
|
week_match = re.search(r"第(\d+)周", clean_text)
|
|||
|
|
if week_match:
|
|||
|
|
week_number = int(week_match.group(1))
|
|||
|
|
|
|||
|
|
# 判断是否为学期结束(通常第16周以后或包含"结束"等关键词)
|
|||
|
|
if week_number >= 16 or "结束" in clean_text or "考试" in clean_text:
|
|||
|
|
is_end = True
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
conn.logger.warning(f"解析学期周数信息时出错: {str(e)}")
|
|||
|
|
return ProtectRouterErrorToCode().server_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|
|||
|
|
result = CurrentTermInfo(
|
|||
|
|
academic_year=academic_year,
|
|||
|
|
current_term_name=term,
|
|||
|
|
week_number=week_number,
|
|||
|
|
start_at=start_date.strftime("%Y-%m-%d"),
|
|||
|
|
is_end=is_end,
|
|||
|
|
weekday=datetime.now().weekday(),
|
|||
|
|
)
|
|||
|
|
return UniResponseModel[CurrentTermInfo](
|
|||
|
|
success=True,
|
|||
|
|
data=result,
|
|||
|
|
message="获取当前学期信息成功",
|
|||
|
|
error=None,
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
conn.logger.exception(e)
|
|||
|
|
return ProtectRouterErrorToCode().server_error.to_json_response(
|
|||
|
|
conn.logger.trace_id
|
|||
|
|
)
|