⚒️ 重大重构 LoveACE V2

引入了 mongodb
对数据库进行了一定程度的数据加密
性能改善
代码简化
统一错误模型和响应
使用 apifox 作为文档
This commit is contained in:
2025-11-20 20:44:25 +08:00
parent 6b90c6d7bb
commit bbc86b8330
168 changed files with 14264 additions and 19152 deletions

View File

@@ -0,0 +1,67 @@
from typing import List
from pydantic import BaseModel, Field
from loveace.router.endpoint.jwc.utils.zxjxjhh_to_term_format import (
convert_zxjxjhh_to_term_format,
)
class AcademicInfoTransformer(BaseModel):
"""学术信息数据项"""
completed_courses: int = Field(0, alias="courseNum")
failed_courses: int = Field(0, alias="coursePas")
gpa: float = Field(0, alias="gpa")
current_term: str = Field("", alias="zxjxjhh")
pending_courses: int = Field(0, alias="courseNum_bxqyxd")
def to_academic_info(self) -> "AcademicInfo":
"""转换为 AcademicInfo"""
return AcademicInfo(
completed_courses=self.completed_courses,
failed_courses=self.failed_courses,
pending_courses=self.pending_courses,
gpa=self.gpa,
current_term=self.current_term,
current_term_name=convert_zxjxjhh_to_term_format(self.current_term),
)
class AcademicInfo(BaseModel):
"""学术信息数据模型"""
completed_courses: int = Field(0, description="已修课程数")
failed_courses: int = Field(0, description="不及格课程数")
pending_courses: int = Field(0, description="本学期待修课程数")
gpa: float = Field(0, description="绩点")
current_term: str = Field("", description="当前学期")
current_term_name: str = Field("", description="当前学期名称")
class TrainingPlanInfoTransformer(BaseModel):
"""培养方案响应模型"""
count: int = 0
data: List[List[str]] = []
class TrainingPlanInfo(BaseModel):
"""培养方案信息模型"""
plan_name: str = Field("", description="培养方案名称")
major_name: str = Field("", description="专业名称")
grade: str = Field("", description="年级")
class CourseSelectionStatusTransformer(BaseModel):
"""选课状态响应模型新格式"""
term_name: str = Field("", alias="zxjxjhm")
status_code: str = Field("", alias="retString")
class CourseSelectionStatus(BaseModel):
"""选课状态信息"""
can_select: bool = Field(False, description="是否可以选课")

View File

@@ -0,0 +1,10 @@
class JWCConfig:
"""教务系统配置常量"""
DEFAULT_BASE_URL = "http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118/"
def to_full_url(self, path: str) -> str:
"""将路径转换为完整URL"""
if path.startswith("http://") or path.startswith("https://"):
return path
return self.DEFAULT_BASE_URL.rstrip("/") + "/" + path.lstrip("/")

View File

@@ -0,0 +1,84 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class AwardProject(BaseModel):
"""
获奖项目信息模型
表示用户通过创新创业管理平台申报的单个获奖项目
"""
project_id: str = Field("", description="申报ID唯一标识符")
project_name: str = Field("", description="项目名称/赛事名称")
level: str = Field("", description="级别(校级/省部级/国家级等)")
grade: str = Field("", description="等级/奖项等级(一等奖/二等奖等)")
award_date: str = Field("", description="获奖日期,格式为 YYYY/M/D")
applicant_id: str = Field("", description="主持人姓名")
applicant_name: str = Field("", description="参与人姓名(作为用户)")
order: int = Field(0, description="顺序号(多人项目的排序)")
credits: float = Field(0.0, description="获奖学分")
bonus: float = Field(0.0, description="奖励金额")
status: str = Field("", description="申报状态(提交/审核中/已审核等)")
verification_status: str = Field(
"", description="学校审核状态(通过/未通过/待审核等)"
)
class CreditsSummary(BaseModel):
"""
学分汇总信息模型
存储用户在创新创业管理平台的各类学分统计
"""
discipline_competition_credits: Optional[float] = Field(
None, description="学科竞赛学分"
)
scientific_research_credits: Optional[float] = Field(
None, description="科研项目学分"
)
transferable_competition_credits: Optional[float] = Field(
None, description="可转竞赛类学分"
)
innovation_practice_credits: Optional[float] = Field(
None, description="创新创业实践学分"
)
ability_certification_credits: Optional[float] = Field(
None, description="能力资格认证学分"
)
other_project_credits: Optional[float] = Field(None, description="其他项目学分")
class CompetitionAwardsResponse(BaseModel):
"""
获奖项目列表响应模型
"""
student_id: str = Field("", description="学生ID/工号")
total_count: int = Field(0, description="获奖项目总数")
awards: List[AwardProject] = Field(default_factory=list, description="获奖项目列表")
class CompetitionCreditsSummaryResponse(BaseModel):
"""
学分汇总响应模型
"""
student_id: str = Field("", description="学生ID/工号")
credits_summary: Optional[CreditsSummary] = Field(None, description="学分汇总详情")
class CompetitionFullResponse(BaseModel):
"""
学科竞赛完整信息响应模型
整合了获奖项目列表和学分汇总信息减少网络IO调用
在单次请求中返回所有竞赛相关数据
"""
student_id: str = Field("", description="学生ID/工号")
total_awards_count: int = Field(0, description="获奖项目总数")
awards: List[AwardProject] = Field(default_factory=list, description="获奖项目列表")
credits_summary: Optional[CreditsSummary] = Field(None, description="学分汇总详情")

View File

@@ -0,0 +1,65 @@
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
class ExamScheduleItem(BaseModel):
"""考试安排项目 - 校统考格式"""
title: str = "" # 考试标题,包含课程名、时间、地点等信息
start: str = "" # 考试日期 (YYYY-MM-DD)
color: str = "" # 显示颜色
class OtherExamRecord(BaseModel):
"""其他考试记录"""
term_code: str = Field("", alias="ZXJXJHH") # 学期代码
term_name: str = Field("", alias="ZXJXJHM") # 学期名称
exam_name: str = Field("", alias="KSMC") # 考试名称
course_code: str = Field("", alias="KCH") # 课程代码
course_name: str = Field("", alias="KCM") # 课程名称
class_number: str = Field("", alias="KXH") # 课序号
student_id: str = Field("", alias="XH") # 学号
student_name: str = Field("", alias="XM") # 姓名
exam_location: str = Field("", alias="KSDD") # 考试地点
exam_date: str = Field("", alias="KSRQ") # 考试日期
exam_time: str = Field("", alias="KSSJ") # 考试时间
note: str = Field("", alias="BZ") # 备注
row_number: str = Field("", alias="RN") # 行号
class OtherExamResponse(BaseModel):
"""其他考试查询响应"""
page_size: int = Field(0, alias="pageSize")
page_num: int = Field(0, alias="pageNum")
page_context: Dict[str, int] = Field(default_factory=dict, alias="pageContext")
records: Optional[List[OtherExamRecord]] = Field(alias="records")
class UnifiedExamInfo(BaseModel):
"""统一考试信息模型 - 对外提供的统一格式"""
course_name: str = Field("", description="课程名称")
exam_date: str = Field("", description="考试日期")
exam_time: str = Field("", description="考试时间")
exam_location: str = Field("", description="考试地点")
exam_type: str = Field("", description="考试类型")
note: str = Field("", description="备注")
class ExamInfoResponse(BaseModel):
"""考试信息统一响应模型"""
exams: List[UnifiedExamInfo] = Field(
default_factory=list, description="考试信息列表"
)
total_count: int = Field(0, description="考试总数")
class SeatInfo(BaseModel):
"""座位信息模型"""
course_name: str = Field("", description="课程名称")
seat_number: str = Field("", description="座位号")

View 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"&nbsp;", " ", 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"&nbsp;", " ", 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

View File

@@ -0,0 +1,49 @@
from typing import List
from pydantic import BaseModel, Field
class TimeSlot(BaseModel):
"""时间段模型"""
session: int = Field(..., description="节次")
session_name: str = Field(..., description="节次名称")
start_time: str = Field(..., description="开始时间格式HHMM")
end_time: str = Field(..., description="结束时间格式HHMM")
time_length: str = Field(..., description="时长(分钟)")
djjc: int = Field(..., description="大节节次")
class CourseTimeLocation(BaseModel):
"""课程时间地点模型"""
class_day: int = Field(..., description="上课星期几1-7")
class_sessions: int = Field(..., description="上课节次")
continuing_session: int = Field(..., description="持续节次数")
class_week: str = Field(..., description="上课周次24位二进制字符串")
week_description: str = Field(..., description="上课周次描述")
campus_name: str = Field(..., description="校区名称")
teaching_building_name: str = Field(..., description="教学楼名称")
classroom_name: str = Field(..., description="教室名称")
class ScheduleCourse(BaseModel):
"""课表课程模型"""
course_name: str = Field(..., description="课程名称")
course_code: str = Field(..., description="课程代码")
course_sequence: str = Field(..., description="课程序号")
teacher_name: str = Field(..., description="授课教师")
course_properties: str = Field(..., description="课程性质")
exam_type: str = Field(..., description="考试类型")
unit: float = Field(..., description="学分")
time_locations: List[CourseTimeLocation] = Field(..., description="时间地点列表")
is_no_schedule: bool = Field(False, description="是否无具体时间安排")
class ScheduleData(BaseModel):
"""课表数据模型"""
total_units: float = Field(..., description="总学分")
time_slots: List[TimeSlot] = Field(..., description="时间段列表")
courses: List[ScheduleCourse] = Field(..., description="课程列表")

View File

@@ -0,0 +1,28 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class ScoreRecord(BaseModel):
"""成绩记录模型"""
sequence: int = Field(0, description="序号")
term_id: str = Field("", description="学期ID")
course_code: str = Field("", description="课程代码")
course_class: str = Field("", description="课程班级")
course_name_cn: str = Field("", description="课程名称(中文)")
course_name_en: str = Field("", description="课程名称(英文)")
credits: str = Field("", description="学分")
hours: int = Field(0, description="学时")
course_type: Optional[str] = Field(None, description="课程性质")
exam_type: Optional[str] = Field(None, description="考试性质")
score: str = Field("", description="成绩")
retake_score: Optional[str] = Field(None, description="重修成绩")
makeup_score: Optional[str] = Field(None, description="补考成绩")
class TermScoreResponse(BaseModel):
"""学期成绩响应模型"""
total_count: int = Field(0, description="总记录数")
records: List[ScoreRecord] = Field(default_factory=list, description="成绩记录列表")

View File

@@ -0,0 +1,20 @@
from pydantic import BaseModel, Field
class TermItem(BaseModel):
"""学期信息项"""
term_code: str = Field(..., description="学期代码")
term_name: str = Field(..., description="学期名称")
is_current: bool = Field(..., description="是否为当前学期")
class CurrentTermInfo(BaseModel):
"""学期周数信息"""
academic_year: str = Field("", description="学年,如 2025-2026")
current_term_name: str = Field("", description="学期,如 秋、春")
week_number: int = Field(0, description="当前周数")
start_at: str = Field("", description="学期开始时间,格式 YYYY-MM-DD")
is_end: bool = Field(False, description="是否为学期结束")
weekday: int = Field(0, description="星期几")