⚒️ 重大重构 LoveACE V2
引入了 mongodb 对数据库进行了一定程度的数据加密 性能改善 代码简化 统一错误模型和响应 使用 apifox 作为文档
This commit is contained in:
348
loveace/router/endpoint/jwc/model/plan.py
Normal file
348
loveace/router/endpoint/jwc/model/plan.py
Normal file
@@ -0,0 +1,348 @@
|
||||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PlanCompletionCourse(BaseModel):
|
||||
"""培养方案课程完成情况"""
|
||||
|
||||
flag_id: str = Field("", description="课程标识ID")
|
||||
flag_type: str = Field("", description="节点类型:001=分类, 002=子分类, kch=课程")
|
||||
course_code: str = Field("", description="课程代码,如 PDA2121005")
|
||||
course_name: str = Field("", description="课程名称")
|
||||
is_passed: bool = Field(False, description="是否通过(基于CSS图标解析)")
|
||||
status_description: str = Field("", description="状态描述:未修读/已通过/未通过")
|
||||
credits: Optional[float] = Field(None, description="学分")
|
||||
score: Optional[str] = Field(None, description="成绩")
|
||||
exam_date: Optional[str] = Field(None, description="考试日期")
|
||||
course_type: str = Field("", description="课程类型:必修/任选等")
|
||||
parent_id: str = Field("", description="父节点ID")
|
||||
level: int = Field(0, description="层级:0=根分类,1=子分类,2=课程")
|
||||
|
||||
@classmethod
|
||||
def from_ztree_node(cls, node: dict) -> "PlanCompletionCourse":
|
||||
"""从 zTree 节点数据创建课程对象"""
|
||||
# 解析name字段中的信息
|
||||
name = node.get("name", "")
|
||||
flag_id = node.get("flagId", "")
|
||||
flag_type = node.get("flagType", "")
|
||||
parent_id = node.get("pId", "")
|
||||
|
||||
# 根据CSS图标判断通过状态
|
||||
is_passed = False
|
||||
status_description = "未修读"
|
||||
|
||||
if "fa-smile-o fa-1x green" in name:
|
||||
is_passed = True
|
||||
status_description = "已通过"
|
||||
elif "fa-meh-o fa-1x light-grey" in name:
|
||||
is_passed = False
|
||||
status_description = "未修读"
|
||||
elif "fa-frown-o fa-1x red" in name:
|
||||
is_passed = False
|
||||
status_description = "未通过"
|
||||
|
||||
# 从name中提取纯文本内容
|
||||
# 移除HTML标签和图标
|
||||
clean_name = re.sub(r"<[^>]*>", "", name)
|
||||
clean_name = re.sub(r" ", " ", clean_name).strip()
|
||||
|
||||
# 解析课程信息
|
||||
course_code = ""
|
||||
course_name = ""
|
||||
credits = None
|
||||
score = None
|
||||
exam_date = None
|
||||
course_type = ""
|
||||
|
||||
if flag_type == "kch": # 课程节点
|
||||
# 解析课程代码:[PDA2121005]形势与政策
|
||||
code_match = re.search(r"\[([^\]]+)\]", clean_name)
|
||||
if code_match:
|
||||
course_code = code_match.group(1)
|
||||
remaining_text = clean_name.split("]", 1)[1].strip()
|
||||
|
||||
# 解析学分信息:[0.3学分]
|
||||
credit_match = re.search(r"\[([0-9.]+)学分\]", remaining_text)
|
||||
if credit_match:
|
||||
credits = float(credit_match.group(1))
|
||||
remaining_text = re.sub(
|
||||
r"\[[0-9.]+学分\]", "", remaining_text
|
||||
).strip()
|
||||
|
||||
# 处理复杂的括号内容
|
||||
# 例如:85.0(20250626 成绩,都没把日期解析上,中国近现代史纲要)
|
||||
# 或者:(任选,87.0(20250119))
|
||||
|
||||
# 找到最外层的括号
|
||||
paren_match = re.search(
|
||||
r"\(([^)]+(?:\([^)]*\)[^)]*)*)\)$", remaining_text
|
||||
)
|
||||
if paren_match:
|
||||
paren_content = paren_match.group(1)
|
||||
course_name_candidate = re.sub(
|
||||
r"\([^)]+(?:\([^)]*\)[^)]*)*\)$", "", remaining_text
|
||||
).strip()
|
||||
|
||||
# 检查括号内容的格式
|
||||
if "," in paren_content:
|
||||
# 处理包含中文逗号的复杂格式
|
||||
parts = paren_content.split(",")
|
||||
|
||||
# 最后一部分可能是课程名
|
||||
last_part = parts[-1].strip()
|
||||
if (
|
||||
re.search(r"[\u4e00-\u9fff]", last_part)
|
||||
and len(last_part) > 1
|
||||
):
|
||||
# 最后一部分包含中文,很可能是真正的课程名
|
||||
course_name = last_part
|
||||
|
||||
# 从前面的部分提取成绩和其他信息
|
||||
remaining_parts = ",".join(parts[:-1])
|
||||
|
||||
# 提取成绩
|
||||
score_match = re.search(r"([0-9.]+)", remaining_parts)
|
||||
if score_match:
|
||||
score = score_match.group(1)
|
||||
|
||||
# 提取日期
|
||||
date_match = re.search(r"(\d{8})", remaining_parts)
|
||||
if date_match:
|
||||
exam_date = date_match.group(1)
|
||||
|
||||
# 提取课程类型(如果有的话)
|
||||
if len(parts) > 2:
|
||||
potential_type = parts[0].strip()
|
||||
if not re.search(r"[0-9.]", potential_type):
|
||||
course_type = potential_type
|
||||
else:
|
||||
# 最后一部分不是课程名,使用括号外的内容
|
||||
course_name = (
|
||||
course_name_candidate
|
||||
if course_name_candidate
|
||||
else "未知课程"
|
||||
)
|
||||
|
||||
# 从整个括号内容提取信息
|
||||
score_match = re.search(r"([0-9.]+)", paren_content)
|
||||
if score_match:
|
||||
score = score_match.group(1)
|
||||
|
||||
date_match = re.search(r"(\d{8})", paren_content)
|
||||
if date_match:
|
||||
exam_date = date_match.group(1)
|
||||
|
||||
elif "," in paren_content:
|
||||
# 处理标准格式:(任选,87.0(20250119))
|
||||
type_score_parts = paren_content.split(",", 1)
|
||||
if len(type_score_parts) == 2:
|
||||
course_type = type_score_parts[0].strip()
|
||||
score_info = type_score_parts[1].strip()
|
||||
|
||||
# 解析成绩和日期
|
||||
score_date_match = re.search(
|
||||
r"([0-9.]+)\((\d{8})\)", score_info
|
||||
)
|
||||
if score_date_match:
|
||||
score = score_date_match.group(1)
|
||||
exam_date = score_date_match.group(2)
|
||||
else:
|
||||
score_match = re.search(r"([0-9.]+)", score_info)
|
||||
if score_match:
|
||||
score = score_match.group(1)
|
||||
|
||||
# 使用括号外的内容作为课程名
|
||||
course_name = (
|
||||
course_name_candidate
|
||||
if course_name_candidate
|
||||
else "未知课程"
|
||||
)
|
||||
|
||||
else:
|
||||
# 括号内只有简单内容
|
||||
course_name = (
|
||||
course_name_candidate
|
||||
if course_name_candidate
|
||||
else "未知课程"
|
||||
)
|
||||
|
||||
# 尝试从括号内容提取成绩
|
||||
score_match = re.search(r"([0-9.]+)", paren_content)
|
||||
if score_match:
|
||||
score = score_match.group(1)
|
||||
|
||||
# 尝试提取日期
|
||||
date_match = re.search(r"(\d{8})", paren_content)
|
||||
if date_match:
|
||||
exam_date = date_match.group(1)
|
||||
else:
|
||||
# 没有括号,直接使用剩余文本作为课程名
|
||||
course_name = remaining_text
|
||||
|
||||
# 清理课程名
|
||||
course_name = re.sub(r"\s+", " ", course_name).strip()
|
||||
course_name = course_name.strip(",,。.")
|
||||
|
||||
# 如果课程名为空或太短,尝试从原始名称提取
|
||||
if not course_name or len(course_name) < 2:
|
||||
chinese_match = re.search(
|
||||
r"[\u4e00-\u9fff]+(?:[\u4e00-\u9fff\s]*[\u4e00-\u9fff]+)*",
|
||||
clean_name,
|
||||
)
|
||||
if chinese_match:
|
||||
course_name = chinese_match.group(0).strip()
|
||||
else:
|
||||
course_name = clean_name
|
||||
else:
|
||||
# 分类节点
|
||||
course_name = clean_name
|
||||
|
||||
# 清理分类名称中的多余括号,但保留重要信息
|
||||
# 如果是包含学分信息的分类名,保留学分信息
|
||||
if not re.search(r"学分", course_name):
|
||||
# 删除分类名称中的统计信息括号,如 "通识通修(已完成20.0/需要20.0)"
|
||||
course_name = re.sub(r"\([^)]*完成[^)]*\)", "", course_name).strip()
|
||||
# 删除其他可能的统计括号
|
||||
course_name = re.sub(
|
||||
r"\([^)]*\d+\.\d+/[^)]*\)", "", course_name
|
||||
).strip()
|
||||
|
||||
# 清理多余的空格和空括号
|
||||
course_name = re.sub(r"\(\s*\)", "", course_name).strip()
|
||||
course_name = re.sub(r"\s+", " ", course_name).strip()
|
||||
|
||||
# 确定层级
|
||||
level = 0
|
||||
if flag_type == "002":
|
||||
level = 1
|
||||
elif flag_type == "kch":
|
||||
level = 2
|
||||
|
||||
return cls(
|
||||
flag_id=flag_id,
|
||||
flag_type=flag_type,
|
||||
course_code=course_code,
|
||||
course_name=course_name,
|
||||
is_passed=is_passed,
|
||||
status_description=status_description,
|
||||
credits=credits,
|
||||
score=score,
|
||||
exam_date=exam_date,
|
||||
course_type=course_type,
|
||||
parent_id=parent_id,
|
||||
level=level,
|
||||
)
|
||||
|
||||
|
||||
class PlanCompletionCategory(BaseModel):
|
||||
"""培养方案分类完成情况"""
|
||||
|
||||
category_id: str = Field("", description="分类ID")
|
||||
category_name: str = Field("", description="分类名称")
|
||||
min_credits: float = Field(0.0, description="最低修读学分")
|
||||
completed_credits: float = Field(0.0, description="通过学分")
|
||||
total_courses: int = Field(0, description="已修课程门数")
|
||||
passed_courses: int = Field(0, description="已及格课程门数")
|
||||
failed_courses: int = Field(0, description="未及格课程门数")
|
||||
missing_required_courses: int = Field(0, description="必修课缺修门数")
|
||||
subcategories: List["PlanCompletionCategory"] = Field(
|
||||
default_factory=list, description="子分类"
|
||||
)
|
||||
courses: List[PlanCompletionCourse] = Field(
|
||||
default_factory=list, description="课程列表"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ztree_node(cls, node: dict) -> "PlanCompletionCategory":
|
||||
"""从 zTree 节点创建分类对象"""
|
||||
name = node.get("name", "")
|
||||
flag_id = node.get("flagId", "")
|
||||
|
||||
# 移除HTML标签获取纯文本
|
||||
clean_name = re.sub(r"<[^>]*>", "", name)
|
||||
clean_name = re.sub(r" ", " ", clean_name).strip()
|
||||
|
||||
# 解析分类统计信息
|
||||
# 格式:通识通修(最低修读学分:68,通过学分:34.4,已修课程门数:26,已及格课程门数:26,未及格课程门数:0,必修课缺修门数:12)
|
||||
stats_match = re.search(
|
||||
r"([^(]+)\(最低修读学分:([0-9.]+),通过学分:([0-9.]+),已修课程门数:(\d+),已及格课程门数:(\d+),未及格课程门数:(\d+),必修课缺修门数:(\d+)\)",
|
||||
clean_name,
|
||||
)
|
||||
|
||||
if stats_match:
|
||||
category_name = stats_match.group(1)
|
||||
min_credits = float(stats_match.group(2))
|
||||
completed_credits = float(stats_match.group(3))
|
||||
total_courses = int(stats_match.group(4))
|
||||
passed_courses = int(stats_match.group(5))
|
||||
failed_courses = int(stats_match.group(6))
|
||||
missing_required_courses = int(stats_match.group(7))
|
||||
else:
|
||||
# 子分类可能没有完整的统计信息
|
||||
category_name = clean_name
|
||||
min_credits = 0.0
|
||||
completed_credits = 0.0
|
||||
total_courses = 0
|
||||
passed_courses = 0
|
||||
failed_courses = 0
|
||||
missing_required_courses = 0
|
||||
|
||||
return cls(
|
||||
category_id=flag_id,
|
||||
category_name=category_name,
|
||||
min_credits=min_credits,
|
||||
completed_credits=completed_credits,
|
||||
total_courses=total_courses,
|
||||
passed_courses=passed_courses,
|
||||
failed_courses=failed_courses,
|
||||
missing_required_courses=missing_required_courses,
|
||||
)
|
||||
|
||||
|
||||
class PlanCompletionInfo(BaseModel):
|
||||
"""培养方案完成情况总信息"""
|
||||
|
||||
plan_name: str = Field("", description="培养方案名称")
|
||||
major: str = Field("", description="专业名称")
|
||||
grade: str = Field("", description="年级")
|
||||
categories: List[PlanCompletionCategory] = Field(
|
||||
default_factory=list, description="分类列表"
|
||||
)
|
||||
total_categories: int = Field(0, description="总分类数")
|
||||
total_courses: int = Field(0, description="总课程数")
|
||||
passed_courses: int = Field(0, description="已通过课程数")
|
||||
failed_courses: int = Field(0, description="未通过课程数")
|
||||
unread_courses: int = Field(0, description="未修读课程数")
|
||||
|
||||
def calculate_statistics(self):
|
||||
"""计算统计信息"""
|
||||
total_courses = 0
|
||||
passed_courses = 0
|
||||
failed_courses = 0
|
||||
unread_courses = 0
|
||||
|
||||
def count_courses(categories: List[PlanCompletionCategory]):
|
||||
nonlocal total_courses, passed_courses, failed_courses, unread_courses
|
||||
|
||||
for category in categories:
|
||||
for course in category.courses:
|
||||
total_courses += 1
|
||||
if course.is_passed:
|
||||
passed_courses += 1
|
||||
elif course.status_description == "未通过":
|
||||
failed_courses += 1
|
||||
else:
|
||||
unread_courses += 1
|
||||
|
||||
# 递归处理子分类
|
||||
count_courses(category.subcategories)
|
||||
|
||||
count_courses(self.categories)
|
||||
|
||||
self.total_categories = len(self.categories)
|
||||
self.total_courses = total_courses
|
||||
self.passed_courses = passed_courses
|
||||
self.failed_courses = failed_courses
|
||||
self.unread_courses = unread_courses
|
||||
Reference in New Issue
Block a user