⚒️ 重大重构 LoveACE V2
引入了 mongodb 对数据库进行了一定程度的数据加密 性能改善 代码简化 统一错误模型和响应 使用 apifox 作为文档
This commit is contained in:
306
loveace/router/endpoint/jwc/term.py
Normal file
306
loveace/router/endpoint/jwc/term.py
Normal file
@@ -0,0 +1,306 @@
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user