🎉初次提交
This commit is contained in:
287
provider/aufe/aac/__init__.py
Normal file
287
provider/aufe/aac/__init__.py
Normal 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("系统错误,已进行多次重试")
|
||||
|
||||
66
provider/aufe/aac/depends.py
Normal file
66
provider/aufe/aac/depends.py
Normal 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
105
provider/aufe/aac/model.py
Normal 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=[]
|
||||
)
|
||||
]
|
||||
Reference in New Issue
Block a user