🎉初次提交

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=[]
)
]