🎉初次提交

This commit is contained in:
2025-08-03 16:50:56 +08:00
commit 56bdf5388d
67 changed files with 18379 additions and 0 deletions

View File

@@ -0,0 +1,287 @@
from typing import Optional
from urllib.parse import unquote
from loguru import logger
from provider.aufe.aac.model import (
LoveACScoreInfo,
LoveACScoreInfoResponse,
LoveACScoreListResponse,
SimpleResponse,
ErrorLoveACScoreInfo,
ErrorLoveACScoreInfoResponse,
ErrorLoveACScoreListResponse,
ErrorLoveACScoreCategory,
)
from provider.aufe.client import (
AUFEConnection,
AUFEConfig,
activity_tracker,
retry_async,
AUFEConnectionError,
AUFELoginError,
AUFEParseError,
RetryConfig
)
class AACConfig:
"""AAC 模块配置常量"""
BASE_URL = "http://api-dekt-ac-acxk-net.vpn2.aufe.edu.cn:8118"
WEB_URL = "http://dekt-ac-acxk-net.vpn2.aufe.edu.cn:8118"
LOGIN_SERVICE_URL = "http://uaap-aufe-edu-cn.vpn2.aufe.edu.cn:8118/cas/login?service=http%3a%2f%2fapi.dekt.ac.acxk.net%2fUser%2fIndex%2fCoreLoginCallback%3fisCASGateway%3dtrue"
@retry_async()
async def get_system_token(vpn_connection: AUFEConnection) -> Optional[str]:
"""
获取系统令牌 (sys_token)
Args:
vpn_connection: VPN连接实例
Returns:
Optional[str]: 系统令牌失败时返回None
Raises:
AUFEConnectionError: 连接失败
AUFEParseError: 令牌解析失败
"""
try:
next_location = AACConfig.LOGIN_SERVICE_URL
max_redirects = 10 # 防止无限重定向
redirect_count = 0
while redirect_count < max_redirects:
response = await vpn_connection.requester().get(
next_location, follow_redirects=False
)
# 如果是重定向,继续跟踪
if response.status_code in (301, 302, 303, 307, 308):
next_location = response.headers.get("Location")
if not next_location:
raise AUFEParseError("重定向响应中缺少Location头")
logger.debug(f"重定向到: {next_location}")
redirect_count += 1
if "register?ticket=" in next_location:
logger.info(f"重定向到爱安财注册页面: {next_location}")
try:
sys_token = next_location.split("ticket=")[-1]
# URL编码转为正常字符串
sys_token = unquote(sys_token)
if sys_token:
logger.info(f"获取到系统令牌: {sys_token[:10]}...")
return sys_token
else:
raise AUFEParseError("提取的系统令牌为空")
except Exception as e:
raise AUFEParseError(f"解析系统令牌失败: {str(e)}") from e
else:
break
if redirect_count >= max_redirects:
raise AUFEConnectionError(f"重定向次数过多 ({max_redirects})")
raise AUFEParseError("未能从重定向中获取到系统令牌")
except (AUFEConnectionError, AUFEParseError):
raise
except Exception as e:
logger.error(f"获取系统令牌异常: {str(e)}")
raise AUFEConnectionError(f"获取系统令牌失败: {str(e)}") from e
class AACClient:
"""爱安财系统客户端"""
def __init__(
self,
vpn_connection: AUFEConnection,
ticket: Optional[str] = None,
retry_config: Optional[RetryConfig] = None
):
"""
初始化爱安财系统客户端
Args:
vpn_connection: VPN连接实例
ticket: 系统令牌
retry_config: 重试配置
"""
self.vpn_connection = vpn_connection
self.base_url = AACConfig.BASE_URL.rstrip("/")
self.web_url = AACConfig.WEB_URL.rstrip("/")
self.twfid = vpn_connection.get_twfid()
self.system_token: Optional[str] = ticket
self.retry_config = retry_config or RetryConfig()
logger.info(
f"爱安财系统客户端初始化: base_url={self.base_url}, web_url={self.web_url}"
)
def _get_default_headers(self) -> dict:
"""获取默认请求头"""
return {
**AUFEConfig.DEFAULT_HEADERS,
"ticket": self.system_token or "",
"sdp-app-session": self.twfid or "",
}
@activity_tracker
@retry_async()
async def validate_connection(self) -> bool:
"""
验证爱安财系统连接
Returns:
bool: 连接是否有效
Raises:
AUFEConnectionError: 连接失败
"""
try:
headers = AUFEConfig.DEFAULT_HEADERS.copy()
response = await self.vpn_connection.requester().get(
f"{self.web_url}/", headers=headers
)
is_valid = response.status_code == 200
logger.info(
f"爱安财系统连接验证结果: {'有效' if is_valid else '无效'} (HTTP状态码: {response.status_code})"
)
if not is_valid:
raise AUFEConnectionError(f"爱安财系统连接验证失败,状态码: {response.status_code}")
return is_valid
except AUFEConnectionError:
raise
except Exception as e:
logger.error(f"验证爱安财系统连接异常: {str(e)}")
raise AUFEConnectionError(f"验证连接失败: {str(e)}") from e
@activity_tracker
async def fetch_score_info(self) -> LoveACScoreInfo:
"""
获取爱安财总分信息,使用重试机制
Returns:
LoveACScoreInfo: 总分信息,失败时返回错误模型
"""
try:
logger.info("开始获取爱安财总分信息")
headers = self._get_default_headers()
# 使用新的重试机制
score_response = await self.vpn_connection.model_request(
model=LoveACScoreInfoResponse,
url=f"{self.base_url}/User/Center/DoGetScoreInfo?sf_request_type=ajax",
method="POST",
headers=headers,
data={}, # 空的POST请求体
follow_redirects=True,
)
if score_response and score_response.code == 0 and score_response.data:
logger.info(
f"爱安财总分信息获取成功: {score_response.data.total_score}"
)
return score_response.data
else:
error_msg = score_response.msg if score_response else '未知错误'
logger.error(f"获取爱安财总分信息失败: {error_msg}")
# 返回错误模型
return ErrorLoveACScoreInfo(
TotalScore=-1.0,
IsTypeAdopt=False,
TypeAdoptResult=f"请求失败: {error_msg}",
)
except (AUFEConnectionError, AUFEParseError) as e:
logger.error(f"获取爱安财总分信息失败: {str(e)}")
return ErrorLoveACScoreInfo(
TotalScore=-1.0,
IsTypeAdopt=False,
TypeAdoptResult=f"请求失败: {str(e)}",
)
except Exception as e:
logger.error(f"获取爱安财总分信息异常: {str(e)}")
# 返回错误模型
return ErrorLoveACScoreInfo(
TotalScore=-1.0,
IsTypeAdopt=False,
TypeAdoptResult="系统错误,请稍后重试",
)
@activity_tracker
async def fetch_score_list(
self, page_index: int = 1, page_size: int = 10
) -> LoveACScoreListResponse:
"""
获取爱安财分数列表,使用重试机制
Args:
page_index: 页码默认为1
page_size: 每页大小默认为10
Returns:
LoveACScoreListResponse: 分数列表响应,失败时返回错误模型
"""
def _create_error_response(error_msg: str) -> ErrorLoveACScoreListResponse:
"""创建错误响应模型"""
return ErrorLoveACScoreListResponse(
code=-1,
msg=error_msg,
data=[
ErrorLoveACScoreCategory(
ID="error",
ShowNum=-1,
TypeName="请求失败",
TotalScore=-1.0,
children=[],
)
],
)
try:
logger.info(
f"开始获取爱安财分数列表,页码: {page_index}, 每页大小: {page_size}"
)
headers = self._get_default_headers()
data = {"pageIndex": str(page_index), "pageSize": str(page_size)}
# 使用新的重试机制
score_list_response = await self.vpn_connection.model_request(
model=LoveACScoreListResponse,
url=f"{self.base_url}/User/Center/DoGetScoreList?sf_request_type=ajax",
method="POST",
headers=headers,
data=data,
follow_redirects=True,
)
if (
score_list_response
and score_list_response.code == 0
and score_list_response.data
):
logger.info(
f"爱安财分数列表获取成功,分类数量: {len(score_list_response.data)}"
)
return score_list_response
else:
error_msg = score_list_response.msg if score_list_response else '未知错误'
logger.error(f"获取爱安财分数列表失败: {error_msg}")
return _create_error_response(f"请求失败: {error_msg}")
except (AUFEConnectionError, AUFEParseError) as e:
logger.error(f"获取爱安财分数列表失败: {str(e)}")
return _create_error_response(f"请求失败: {str(e)}")
except Exception as e:
logger.error(f"获取爱安财分数列表异常: {str(e)}")
return _create_error_response("系统错误,已进行多次重试")

View File

@@ -0,0 +1,66 @@
from fastapi import Depends, HTTPException
from loguru import logger
from provider.loveac.authme import fetch_user_by_token
from provider.aufe.aac import AACClient, get_system_token
from provider.aufe.client import AUFEConnection
from database.user import User, AACTicket
from sqlalchemy.ext.asyncio import AsyncSession
from database.creator import get_db_session
from sqlalchemy import select
async def get_aac_client(
user: User = Depends(fetch_user_by_token),
db: AsyncSession = Depends(get_db_session),
) -> AACClient:
"""
获取AAC客户端
:param user: 用户信息
:return: AACClient
:raises HTTPException: 如果用户无效或登录失败
"""
if not user:
raise HTTPException(status_code=400, detail="无效的令牌或用户不存在")
aufe = AUFEConnection.create_or_get_connection("vpn.aufe.edu.cn", user.userid)
if not aufe.login_status():
userid = user.userid
easyconnect_password = user.easyconnect_password
if not await aufe.login(userid, easyconnect_password):
raise HTTPException(
status_code=400,
detail="VPN登录失败请检查用户名和密码",
)
if not aufe.uaap_login_status():
userid = user.userid
password = user.password
if not await aufe.uaap_login(userid, password):
raise HTTPException(
status_code=400,
detail="大学登录失败,请检查用户名和密码",
)
# 检查AAC Ticket是否存在
async with db as session:
result = await session.execute(
select(AACTicket).where(AACTicket.userid == user.userid)
)
aac_ticket = result.scalars().first()
if not aac_ticket:
# 如果不存在尝试获取新的AAC Ticket
logger.info(f"用户 {user.userid} 的 AAC Ticket 不存在,正在获取新的 Ticket")
aac_ticket = await get_system_token(aufe)
if not aac_ticket:
logger.error(f"用户 {user.userid} 获取 AAC Ticket 失败")
raise HTTPException(
status_code=400,
detail="获取AAC Ticket失败请稍后再试",
)
# 保存到数据库
async with db as session:
session.add(AACTicket(userid=user.userid, aac_token=aac_ticket))
await session.commit()
logger.success(f"用户 {user.userid} 成功获取并保存新的 AAC Ticket")
else:
logger.info(f"用户 {user.userid} 使用现有的 AAC Ticket")
aac_ticket = aac_ticket.aac_token
return AACClient(aufe, aac_ticket)

105
provider/aufe/aac/model.py Normal file
View File

@@ -0,0 +1,105 @@
from typing import List, Optional, Any
from pydantic import BaseModel, Field
class LoveACScoreInfo(BaseModel):
"""爱安财总分信息"""
total_score: float = Field(0.0, alias="TotalScore")
is_type_adopt: bool = Field(False, alias="IsTypeAdopt")
type_adopt_result: str = Field("", alias="TypeAdoptResult")
class LoveACScoreItem(BaseModel):
"""爱安财分数明细条目"""
id: str = Field("", alias="ID")
title: str = Field("", alias="Title")
type_name: str = Field("", alias="TypeName")
user_no: str = Field("", alias="UserNo")
score: float = Field(0.0, alias="Score")
add_time: str = Field("", alias="AddTime")
class LoveACScoreCategory(BaseModel):
"""爱安财分数类别"""
id: str = Field("", alias="ID")
show_num: int = Field(0, alias="ShowNum")
type_name: str = Field("", alias="TypeName")
total_score: float = Field(0.0, alias="TotalScore")
children: List[LoveACScoreItem] = Field([], alias="children")
class LoveACBaseResponse(BaseModel):
"""爱安财系统响应基础模型"""
code: int = 0
msg: str = ""
data: Any = None
class LoveACScoreInfoResponse(LoveACBaseResponse):
"""爱安财总分响应"""
data: Optional[LoveACScoreInfo] = None
class LoveACScoreListResponse(LoveACBaseResponse):
"""爱安财分数列表响应"""
data: Optional[List[LoveACScoreCategory]] = None
class SimpleResponse(BaseModel):
"""简单响应类用于解析基本的JSON结构"""
code: int = 0
msg: str = ""
data: Any = None
class ErrorLoveACScoreInfo(LoveACScoreInfo):
"""错误的爱安财总分信息模型,用于重试失败时返回"""
total_score: float = Field(-1.0, alias="TotalScore")
is_type_adopt: bool = Field(False, alias="IsTypeAdopt")
type_adopt_result: str = Field("请求失败,请稍后重试", alias="TypeAdoptResult")
class ErrorLoveACScoreCategory(BaseModel):
"""错误的爱安财分数类别模型"""
id: str = Field("error", alias="ID")
show_num: int = Field(-1, alias="ShowNum")
type_name: str = Field("请求失败", alias="TypeName")
total_score: float = Field(-1.0, alias="TotalScore")
children: List[LoveACScoreItem] = Field([], alias="children")
class ErrorLoveACBaseResponse(BaseModel):
"""错误的爱安财系统响应基础模型"""
code: int = -1
msg: str = "网络请求失败,已进行多次重试"
data: Any = None
class ErrorLoveACScoreInfoResponse(ErrorLoveACBaseResponse):
"""错误的爱安财总分响应"""
data: Optional[ErrorLoveACScoreInfo] = ErrorLoveACScoreInfo(
TotalScore=-1.0, IsTypeAdopt=False, TypeAdoptResult="请求失败,请稍后重试"
)
class ErrorLoveACScoreListResponse(LoveACScoreListResponse):
"""错误的爱安财分数列表响应"""
code: int = -1
msg: str = "网络请求失败,已进行多次重试"
data: Optional[List[ErrorLoveACScoreCategory]] = [
ErrorLoveACScoreCategory(
ID="error", ShowNum=-1, TypeName="请求失败", TotalScore=-1.0, children=[]
)
]

1176
provider/aufe/client.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
from fastapi import Depends, HTTPException
from provider.loveac.authme import fetch_user_by_token
from provider.aufe.jwc import JWCClient
from provider.aufe.client import AUFEConnection
from database.user import User
async def get_jwc_client(
user: User = Depends(fetch_user_by_token),
) -> JWCClient:
"""
获取教务处客户端
:param authme_request: AuthmeRequest
:return: JWCClient
"""
if not user:
raise HTTPException(status_code=400, detail="无效的令牌或用户不存在")
aufe = AUFEConnection.create_or_get_connection("vpn.aufe.edu.cn", user.userid)
if not aufe.login_status():
userid = user.userid
easyconnect_password = user.easyconnect_password
if not await aufe.login(userid, easyconnect_password):
raise HTTPException(
status_code=400,
detail="VPN登录失败请检查用户名和密码",
)
if not aufe.uaap_login_status():
userid = user.userid
password = user.password
if not await aufe.uaap_login(userid, password):
raise HTTPException(
status_code=400,
detail="大学登录失败,请检查用户名和密码",
)
return JWCClient(aufe)

296
provider/aufe/jwc/model.py Normal file
View File

@@ -0,0 +1,296 @@
from typing import Dict, List, Optional, Any
from pydantic import BaseModel, Field
class AcademicDataItem(BaseModel):
"""学术信息数据项用于直接反序列化JSON数组中的元素"""
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")
class AcademicInfo(BaseModel):
"""学术信息数据模型 - 兼容旧版API"""
completed_courses: int = Field(0, alias="count")
failed_courses: int = Field(0, alias="countNotPass")
gpa: float = Field(0, alias="gpa")
# ==================== 学期和成绩相关模型 ====================
class TermInfo(BaseModel):
"""学期信息模型"""
term_id: str = Field("", description="学期ID2024-2025-2-1")
term_name: str = Field("", description="学期名称2024-2025春季学期")
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: str = Field("", description="课程性质")
exam_type: str = Field("", description="考试性质")
score: str = Field("", description="成绩")
retake_score: Optional[str] = Field(None, description="重修成绩")
makeup_score: Optional[str] = Field(None, description="补考成绩")
class TermScoreResponse(BaseModel):
"""学期成绩响应模型"""
page_size: int = Field(50, description="每页大小")
page_num: int = Field(1, description="页码")
total_count: int = Field(0, description="总记录数")
records: List[ScoreRecord] = Field(default_factory=list, description="成绩记录列表")
# ==================== 原有模型继续 ====================
class TrainingPlanDataItem(BaseModel):
"""培养方案数据项"""
plan_name: str = "" # 第一项为培养方案名称
plan_id: str = "" # 第二项为培养方案ID
class TrainingPlanResponseWrapper(BaseModel):
"""培养方案响应模型"""
count: int = 0
data: List[List[str]] = []
class TrainingPlanInfo(BaseModel):
"""培养方案信息模型 - 兼容旧版API"""
plan_name: str = Field("", alias="pyfa")
current_term: str = Field("", alias="term")
pending_courses: int = Field(0, alias="courseCount")
major_name: str = Field("", alias="major")
grade: str = Field("", alias="grade")
class CourseSelectionStatusDirectResponse(BaseModel):
"""选课状态响应模型新格式"""
term_name: str = Field("", alias="zxjxjhm")
status_code: str = Field("", alias="retString")
class CourseSelectionStatus(BaseModel):
"""选课状态信息"""
can_select: bool = Field(False, alias="isCanSelect")
start_time: str = Field("", alias="startTime")
end_time: str = Field("", alias="endTime")
class CourseId(BaseModel):
"""课程ID信息"""
evaluated_people: str = Field("", alias="evaluatedPeople")
coure_sequence_number: str = Field("", alias="coureSequenceNumber")
evaluation_content_number: str = Field("", alias="evaluationContentNumber")
class Questionnaire(BaseModel):
"""问卷信息"""
questionnaire_number: str = Field("", alias="questionnaireNumber")
questionnaire_name: str = Field("", alias="questionnaireName")
class Course(BaseModel):
"""课程基本信息"""
id: Optional[CourseId] = None
questionnaire: Optional[Questionnaire] = Field(None, alias="questionnaire")
evaluated_people: str = Field("", alias="evaluatedPeople")
is_evaluated: str = Field("", alias="isEvaluated")
evaluation_content: str = Field("", alias="evaluationContent")
class CourseListResponse(BaseModel):
"""课程列表响应"""
not_finished_num: int = Field(0, alias="notFinishedNum")
evaluation_num: int = Field(0, alias="evaluationNum")
data: List[Course] = Field(default_factory=list, alias="data")
msg: str = Field("", alias="msg")
result: str = "success" # 设置默认值
class EvaluationResponse(BaseModel):
"""评价提交响应"""
result: str = ""
msg: str = ""
data: Any = None
class EvaluationRequestParam(BaseModel):
"""评价请求参数"""
opt_type: str = "submit"
token_value: str = ""
questionnaire_code: str = ""
evaluation_content: str = ""
evaluated_people_number: str = ""
count: str = ""
zgpj: str = ""
rating_items: Dict[str, str] = {}
def to_form_data(self) -> Dict[str, str]:
"""将对象转换为表单数据映射"""
form_data = {
"optType": self.opt_type,
"tokenValue": self.token_value,
"questionnaireCode": self.questionnaire_code,
"evaluationContent": self.evaluation_content,
"evaluatedPeopleNumber": self.evaluated_people_number,
"count": self.count,
"zgpj": self.zgpj,
}
# 添加评分项
form_data.update(self.rating_items)
return form_data
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: List[OtherExamRecord] = Field(default_factory=list, alias="records")
class UnifiedExamInfo(BaseModel):
"""统一考试信息模型 - 对外提供的统一格式"""
course_name: str = "" # 课程名称
exam_date: str = "" # 考试日期 (YYYY-MM-DD)
exam_time: str = "" # 考试时间
exam_location: str = "" # 考试地点
exam_type: str = "" # 考试类型 (校统考/其他考试)
note: str = "" # 备注信息
class ExamInfoResponse(BaseModel):
"""考试信息统一响应模型"""
exams: List[UnifiedExamInfo] = Field(default_factory=list)
total_count: int = 0
# ==================== 错误响应模型 ====================
class ErrorAcademicInfo(AcademicInfo):
"""错误的学术信息数据模型"""
completed_courses: int = Field(-1, alias="count")
failed_courses: int = Field(-1, alias="countNotPass")
gpa: float = Field(-1.0, alias="gpa")
class ErrorTrainingPlanInfo(TrainingPlanInfo):
"""错误的培养方案信息模型"""
plan_name: str = Field("请求失败,请稍后重试", alias="pyfa")
current_term: str = Field("", alias="term")
pending_courses: int = Field(-1, alias="courseCount")
major_name: str = Field("请求失败", alias="major")
grade: str = Field("", alias="grade")
class ErrorCourseSelectionStatus(CourseSelectionStatus):
"""错误的选课状态信息"""
can_select: bool = Field(False, alias="isCanSelect")
start_time: str = Field("请求失败", alias="startTime")
end_time: str = Field("请求失败", alias="endTime")
class ErrorCourse(Course):
"""错误的课程基本信息"""
id: Optional[CourseId] = None
questionnaire: Optional[Questionnaire] = None
evaluated_people: str = Field("请求失败", alias="evaluatedPeople")
is_evaluated: str = Field("", alias="isEvaluated")
evaluation_content: str = Field("请求失败,请稍后重试", alias="evaluationContent")
class ErrorCourseListResponse(CourseListResponse):
"""错误的课程列表响应"""
not_finished_num: int = Field(-1, alias="notFinishedNum")
evaluation_num: int = Field(-1, alias="evaluationNum")
data: List[Course] = Field(default_factory=list, alias="data")
msg: str = Field("网络请求失败,已进行多次重试", alias="msg")
result: str = "failed"
class ErrorEvaluationResponse(EvaluationResponse):
"""错误的评价提交响应"""
result: str = "failed"
msg: str = "网络请求失败,已进行多次重试"
data: Any = None
class ErrorExamInfoResponse(ExamInfoResponse):
"""错误的考试信息响应模型"""
exams: List[UnifiedExamInfo] = Field(default_factory=list)
total_count: int = -1
class ErrorTermScoreResponse(BaseModel):
"""错误的学期成绩响应模型"""
page_size: int = Field(-1, description="每页大小")
page_num: int = Field(-1, description="页码")
total_count: int = Field(-1, description="总记录数")
records: List[ScoreRecord] = Field(default_factory=list, description="成绩记录列表")

View File

View File

View File