diff --git a/cqwu/client.py b/cqwu/client.py
index fd790b1..638c9ab 100644
--- a/cqwu/client.py
+++ b/cqwu/client.py
@@ -15,7 +15,7 @@ class Client(Methods):
password: str = None,
cookie: str = None,
cookie_file_path: str = "cookie.txt",
- timeout: int = 10,
+ timeout: int = 60,
):
self.username = username
self.password = password
diff --git a/cqwu/enums/webvpn.py b/cqwu/enums/webvpn.py
index 1f378d3..b700ffb 100644
--- a/cqwu/enums/webvpn.py
+++ b/cqwu/enums/webvpn.py
@@ -8,3 +8,13 @@ class ExamRound(str, Enum):
""" 分散考试 """
Concentration = "3"
""" 集中考试 """
+
+
+class ScoreSearchType(str, Enum):
+ """ 成绩查询类型 """
+ All = "1"
+ """入学以来"""
+ XUENIAN = "2"
+ """学年"""
+ XUEQI = "3"
+ """学期"""
diff --git a/cqwu/errors/webvpn.py b/cqwu/errors/webvpn.py
index abb0019..fe1fca5 100644
--- a/cqwu/errors/webvpn.py
+++ b/cqwu/errors/webvpn.py
@@ -7,3 +7,7 @@ class CQWUWebVPNError(CQWUEhallError):
class NoExamData(CQWUWebVPNError):
""" 没有检索到对应的考试记录 """
+
+
+class NoScoreDetailData(CQWUWebVPNError):
+ """ 没有检索到对应的成绩明细记录 """
diff --git a/cqwu/methods/webvpn/__init__.py b/cqwu/methods/webvpn/__init__.py
index a62a236..e05cfee 100644
--- a/cqwu/methods/webvpn/__init__.py
+++ b/cqwu/methods/webvpn/__init__.py
@@ -3,6 +3,7 @@ from httpx import URL
from .get_calendar import GetCalendar
from .get_calendar_change import GetCalendarChange
from .get_exam_calendar import GetExamCalendar
+from .get_score_detail import GetScoreDetail
from .get_selected_courses import GetSelectedCourses
from .login_jwmis import LoginJwmis
from .login_webvpn import LoginWebVPN
@@ -12,6 +13,7 @@ class WebVPN(
GetCalendar,
GetCalendarChange,
GetExamCalendar,
+ GetScoreDetail,
GetSelectedCourses,
LoginJwmis,
LoginWebVPN,
diff --git a/cqwu/methods/webvpn/get_score_detail.py b/cqwu/methods/webvpn/get_score_detail.py
new file mode 100644
index 0000000..394ed78
--- /dev/null
+++ b/cqwu/methods/webvpn/get_score_detail.py
@@ -0,0 +1,136 @@
+from typing import Union, List
+
+from bs4 import BeautifulSoup, Tag
+
+import cqwu
+from cqwu.enums import ScoreSearchType
+from cqwu.errors import NoScoreDetailData
+from cqwu.types import ScoreDetail, ScoreDetailInfo, ScoreDetailCourse, ScoreDetailTotal
+
+
+class GetScoreDetail:
+ async def get_score_detail(
+ self: "cqwu.Client",
+ search_type: Union[str, ScoreSearchType] = ScoreSearchType.XUEQI,
+ xue_nian: int = None,
+ xue_qi: int = None,
+ use_model: bool = False,
+ ) -> Union[str, ScoreDetail]:
+ """ 获取学业成绩 """
+ xue_nian = xue_nian or self.xue_nian
+ xue_qi = xue_qi or self.xue_qi
+ search_type = ScoreSearchType(search_type)
+ jw_html = await self.login_jwmis()
+ jw_host = self.get_web_vpn_host(jw_html.url, https=True)
+ jw_url = f"{jw_host}/cqwljw/student/xscj.stuckcj_data.jsp"
+ headers = {
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'Accept-Language': 'zh-CN,zh;q=0.9,zh-Hans;q=0.8,und;q=0.7,en;q=0.6,zh-Hant;q=0.5,ja;q=0.4',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'DNT': '1',
+ 'Pragma': 'no-cache',
+ 'Referer': f'{jw_host}/cqwljw/student/ksap.ksapb.html?menucode=S20403',
+ 'Sec-Fetch-Dest': 'iframe',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-Site': 'same-origin',
+ 'Sec-Fetch-User': '?1',
+ 'Upgrade-Insecure-Requests': '1',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
+ 'sec-ch-ua': '"Chromium";v="112", "Not:A-Brand";v="99"',
+ 'sec-ch-ua-mobile': '?0',
+ 'sec-ch-ua-platform': '"Windows"',
+ }
+ data = {
+ "sjxz": f"sjxz{search_type.value}",
+ "ysyx": "yxcj",
+ "zx": "1",
+ "fx": "1",
+ "btnExport": "%B5%BC%B3%F6",
+ "rxnj": str(xue_nian),
+ "xn": str(xue_nian),
+ 'xn1': str(xue_nian + 1),
+ "xq": str(xue_qi),
+ "ysyxS": "on",
+ "sjxzS": "on",
+ "zxC": "on",
+ "fxC": "on",
+ "xsjd": "1",
+ }
+ jw_html = await self.request.post(jw_url, data=data, headers=headers, timeout=60, follow_redirects=True)
+ if "没有检索到记录!" in jw_html.text:
+ raise NoScoreDetailData("没有检索到记录!")
+ jw_html = jw_html.text.replace("""""", "")
+ jw_html = jw_html.replace("""""", "")
+ jw_html = jw_html.replace("charset=GBK", 'charset=UTF-8')
+ if not use_model:
+ return jw_html
+ return parse_html(jw_html)
+
+
+def parse_info(tag: Tag) -> ScoreDetailInfo:
+ divs = tag.find_all("div")
+
+ def get_text(_tag) -> str:
+ return (":".join(_tag.text.split(":")[1:])).strip()
+
+ return ScoreDetailInfo(
+ college=get_text(divs[0]),
+ major=get_text(divs[1]),
+ level=get_text(divs[2]),
+ class_name=get_text(divs[3]),
+ student_id=get_text(divs[4]),
+ student_name=get_text(divs[5]),
+ date=get_text(divs[6]),
+ )
+
+
+def parse_course(tag: Tag) -> List[ScoreDetailCourse]:
+ trs = tag.find_all("tr")
+ courses = []
+ for tr in trs:
+ tds = tr.find_all("td")
+ course = ScoreDetailCourse(
+ id=tds[0].text,
+ name=tds[1].text,
+ credit=tds[2].text,
+ period=tds[3].text,
+ type=tds[4].text,
+ nature=tds[5].text,
+ exam_method=tds[6].text,
+ score=tds[7].text,
+ score_point=tds[8].text,
+ grade_point=tds[9].text,
+ gp=tds[10].text,
+ remark=tds[11].text,
+ )
+ courses.append(course)
+ return courses
+
+
+def parse_total(tag: Tag) -> ScoreDetailTotal:
+ tr = tag.find_all("tr")[-1]
+ tds = tr.find_all("td")
+ return ScoreDetailTotal(
+ num=tds[1].text,
+ credit=tds[2].text,
+ get_credit=tds[3].text,
+ grade_point=tds[4].text,
+ gp=tds[5].text,
+ gpa=tds[6].text,
+ score_avg=tds[7].text,
+ weighted_grade_avg=tds[8].text,
+ )
+
+
+def parse_html(html: str) -> ScoreDetail:
+ soup = BeautifulSoup(html, "lxml")
+ group_div = soup.find("div", {"group": "group"})
+ info = parse_info(group_div)
+ tbody_s = soup.find_all("tbody")
+ courses = []
+ for tbody in tbody_s[:-1]:
+ courses += parse_course(tbody)
+ total = parse_total(tbody_s[-1])
+ return ScoreDetail(info=info, courses=courses, total=total)
diff --git a/cqwu/types/__init__.py b/cqwu/types/__init__.py
index 569f229..a2d5139 100644
--- a/cqwu/types/__init__.py
+++ b/cqwu/types/__init__.py
@@ -4,4 +4,5 @@ from .epay import PayBill, PayBillPage
from .exam import AiExam
from .pay import PayProject, PayProjectDetail, PayUser
from .score import Score
+from .score_detail import ScoreDetail, ScoreDetailInfo, ScoreDetailCourse, ScoreDetailTotal
from .user import User
diff --git a/cqwu/types/score_detail.py b/cqwu/types/score_detail.py
new file mode 100644
index 0000000..4441aff
--- /dev/null
+++ b/cqwu/types/score_detail.py
@@ -0,0 +1,75 @@
+from typing import List
+
+from pydantic import BaseModel
+
+
+class ScoreDetailInfo(BaseModel):
+ college: str = ""
+ """院(系)/部"""
+ major: str = ""
+ """专业"""
+ level: str = ""
+ """培养层次"""
+ class_name: str = ""
+ """行政班级"""
+ student_id: str = ""
+ """学号"""
+ student_name: str = ""
+ """姓名"""
+ date: str = ""
+ """打印时间"""
+
+
+class ScoreDetailCourse(BaseModel):
+ id: int = 0
+ """序号"""
+ name: str = ""
+ """课程/环节"""
+ credit: float = 0.0
+ """学分"""
+ period: int = 0
+ """总学时"""
+ type: str = ""
+ """类别"""
+ nature: str = ""
+ """修读性质"""
+ exam_method: str = ""
+ """考核方式"""
+ score: float = 0.0
+ """成绩"""
+ score_point: float = 0.0
+ """获得学分"""
+ grade_point: float = 0.0
+ """绩点"""
+ gp: float = 0.0
+ """学分绩点"""
+ remark: str = ""
+ """备注"""
+
+
+class ScoreDetailTotal(BaseModel):
+ num: int = 0
+ """修读课程环节数"""
+ credit: float = 0.0
+ """学分"""
+ get_credit: float = 0.0
+ """获得学分"""
+ grade_point: float = 0.0
+ """获得绩点"""
+ gp: float = 0.0
+ """获得学分绩点"""
+ gpa: float = 0.0
+ """平均学分绩点"""
+ score_avg: float = 0.0
+ """平均成绩"""
+ weighted_grade_avg: float = 0.0
+ """加权平均成绩"""
+
+
+class ScoreDetail(BaseModel):
+ info: ScoreDetailInfo
+ """基本信息"""
+ courses: List[ScoreDetailCourse]
+ """课程信息"""
+ total: ScoreDetailTotal
+ """总计"""
diff --git a/setup.py b/setup.py
index 2faa281..ccaef98 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
setuptools.setup(
name="cqwu", # 用自己的名替换其中的YOUR_USERNAME_
- version="0.0.16", # 包版本号,便于维护版本
+ version="0.0.17", # 包版本号,便于维护版本
author="omg-xtao", # 作者,可以写自己的姓名
author_email="xtao@xtaolink.cn", # 作者联系方式,可写自己的邮箱地址
description="A cqwu ehall client.", # 包的简述