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