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

307 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 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
)