1426 lines
52 KiB
Python
1426 lines
52 KiB
Python
import asyncio
|
||
import datetime
|
||
import hashlib
|
||
import json
|
||
import random
|
||
import re
|
||
from functools import wraps
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from bs4 import BeautifulSoup
|
||
from fastapi import Depends
|
||
|
||
from loveace.config.manager import config_manager
|
||
from loveace.router.endpoint.isim.model.isim import (
|
||
ElectricityBalance,
|
||
ElectricityUsageRecord,
|
||
PaymentRecord,
|
||
)
|
||
from loveace.router.endpoint.isim.model.room import (
|
||
BuildingInfo,
|
||
CacheBuildingData,
|
||
CacheFloorData,
|
||
CacheRoomsData,
|
||
FloorInfo,
|
||
RoomInfo,
|
||
)
|
||
from loveace.service.remote.aufe import AUFEConnection, SubClient
|
||
from loveace.service.remote.aufe.depends import get_aufe_conn
|
||
from loveace.utils.redis_client import get_redis_client
|
||
|
||
|
||
def ensure_session(func):
|
||
"""装饰器:确保在调用方法前已初始化会话"""
|
||
|
||
@wraps(func)
|
||
async def wrapper(self, *args, **kwargs):
|
||
if isinstance(self, ISIMClient):
|
||
await self._ensure_jsession()
|
||
return await func(self, *args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
|
||
class ISIMClient(SubClient):
|
||
"""ISIM系统客户端,用于获取楼栋、楼层和房间信息
|
||
|
||
该客户端会自动管理会话初始化,无需手动调用 get_jsession()
|
||
"""
|
||
|
||
DEFAULT_BASE_URL = config_manager.get_settings().isim.base_url.rstrip("/")
|
||
|
||
def __init__(self, aufe_connection: AUFEConnection, auto_init: bool = True):
|
||
"""
|
||
初始化ISIM客户端
|
||
|
||
Args:
|
||
aufe_connection: AUFE连接实例
|
||
auto_init: 是否自动初始化会话(默认为True,推荐保持默认)
|
||
"""
|
||
self.client = aufe_connection
|
||
self.config = config_manager.get_settings()
|
||
self._jsessionid: Optional[str] = None
|
||
self._session_initialized = False
|
||
self._auto_init = auto_init
|
||
self._init_lock = asyncio.Lock() # 防止并发初始化
|
||
self._jsession_bound = False # 标记是否已绑定寝室
|
||
|
||
def _generate_session_params(self) -> Dict[str, str]:
|
||
"""生成会话参数(openid和sn)"""
|
||
seed = self.client.userid if self.client.userid != "unknown" else "default"
|
||
|
||
# 生成openid - 基于学号的哈希值
|
||
openid_hash = hashlib.md5(f"{seed}_openid".encode()).hexdigest()
|
||
openid = openid_hash[:15] + str(random.randint(100, 999))
|
||
|
||
# 生成sn - 简单使用固定值
|
||
sn = "sn"
|
||
|
||
return {"openid": openid, "sn": sn}
|
||
|
||
async def _ensure_jsession(self) -> None:
|
||
"""
|
||
确保会话已初始化(内部方法)
|
||
使用锁机制防止并发初始化
|
||
"""
|
||
if self._session_initialized and self._jsessionid:
|
||
return
|
||
|
||
async with self._init_lock:
|
||
# 双重检查,避免重复初始化
|
||
if self._session_initialized and self._jsessionid:
|
||
return
|
||
|
||
if not self._auto_init:
|
||
raise RuntimeError(
|
||
"ISIM会话未初始化。请先调用 get_jsession() 或在创建实例时设置 auto_init=True"
|
||
)
|
||
|
||
success = await self.get_jsession()
|
||
if not success:
|
||
raise RuntimeError("ISIM会话初始化失败,请检查网络连接或认证状态")
|
||
|
||
async def get_jsession(self) -> bool:
|
||
"""
|
||
初始化ISIM会话,获取JSESSIONID
|
||
|
||
通常不需要手动调用此方法,客户端会在需要时自动初始化
|
||
|
||
Returns:
|
||
bool: 是否成功获取JSESSIONID
|
||
"""
|
||
try:
|
||
self.client.logger.info("开始初始化ISIM会话")
|
||
|
||
params = self._generate_session_params()
|
||
|
||
response = await self.client.client.get(
|
||
f"{self.DEFAULT_BASE_URL}/go",
|
||
params=params,
|
||
follow_redirects=False,
|
||
timeout=self.client.timeout,
|
||
)
|
||
|
||
# 检查是否收到302重定向响应
|
||
if response.status_code == 302:
|
||
set_cookie_header = response.headers.get("set-cookie", "")
|
||
if "JSESSIONID=" in set_cookie_header:
|
||
jsessionid_match = re.search(
|
||
r"JSESSIONID=([^;]+)", set_cookie_header
|
||
)
|
||
if jsessionid_match:
|
||
self._jsessionid = jsessionid_match.group(1)
|
||
self._session_initialized = True # 标记会话已初始化
|
||
self.client.logger.info(
|
||
f"成功获取JSESSIONID: {jsessionid_match.group(1)[:10]}***"
|
||
)
|
||
|
||
# 验证重定向位置
|
||
location = response.headers.get("location", "")
|
||
if "home" in location and "jsessionid" in location:
|
||
self.client.logger.info(
|
||
f"重定向位置正确: {location[:8]}****"
|
||
)
|
||
else:
|
||
self.client.logger.warning(f"重定向位置异常: {location}")
|
||
|
||
return True
|
||
|
||
self.client.logger.error("未能从Set-Cookie头中提取JSESSIONID")
|
||
return False
|
||
else:
|
||
self.client.logger.error(
|
||
f"期望302重定向,但收到状态码: {response.status_code}"
|
||
)
|
||
if response.text:
|
||
self.client.logger.debug(f"响应内容: {response.text[:200]}...")
|
||
return False
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"初始化ISIM会话异常: {str(e)}")
|
||
return False
|
||
|
||
@ensure_session
|
||
async def get_buildings(
|
||
self, jsessionid: Optional[str] = None
|
||
) -> List[BuildingInfo]:
|
||
"""
|
||
获取楼栋列表
|
||
|
||
Args:
|
||
jsessionid: 会话ID,通常不需要提供(自动使用实例中的ID)
|
||
|
||
Returns:
|
||
List[BuildingInfo]: 楼栋信息列表
|
||
"""
|
||
jsessionid = jsessionid or self._jsessionid
|
||
|
||
try:
|
||
headers = {
|
||
**self.config.aufe.default_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",
|
||
"Connection": "keep-alive",
|
||
"Upgrade-Insecure-Requests": "1",
|
||
"Cookie": f"JSESSIONID={jsessionid}; TWFID={self.client.twf_id}",
|
||
"Referer": f"{self.DEFAULT_BASE_URL}/home;jsessionid={jsessionid}",
|
||
}
|
||
|
||
response = await self.client.client.get(
|
||
f"{self.DEFAULT_BASE_URL}/about",
|
||
headers=headers,
|
||
follow_redirects=True,
|
||
timeout=self.client.timeout,
|
||
)
|
||
|
||
if response.status_code != 200:
|
||
raise Exception(f"请求失败,状态码: {response.status_code}")
|
||
|
||
# 解析HTML页面获取楼栋信息
|
||
soup = BeautifulSoup(response.text, "html.parser")
|
||
buildings = []
|
||
scripts = soup.find_all("script")
|
||
|
||
for script in scripts:
|
||
if script.string and "pickerBuilding" in script.string:
|
||
values_match = re.search(r"values:\s*\[(.*?)\]", script.string)
|
||
display_values_match = re.search(
|
||
r"displayValues:\s*\[(.*?)\]", script.string
|
||
)
|
||
|
||
if values_match and display_values_match:
|
||
values_str = values_match.group(1)
|
||
display_values_str = display_values_match.group(1)
|
||
|
||
values = [v.strip().strip('"') for v in values_str.split(",")]
|
||
display_values = [
|
||
v.strip().strip('"') for v in display_values_str.split(",")
|
||
]
|
||
|
||
for code, name in zip(values, display_values):
|
||
if code and code != '""' and name != "请选择":
|
||
buildings.append(BuildingInfo(code=code, name=name))
|
||
break
|
||
|
||
self.client.logger.info(f"成功获取{len(buildings)}个楼栋信息")
|
||
return buildings
|
||
|
||
except Exception as e:
|
||
self.client.logger.exception(e)
|
||
self.client.logger.error(f"获取楼栋列表异常: {str(e)}")
|
||
return []
|
||
|
||
@ensure_session
|
||
async def get_floors(
|
||
self, building_code: str, jsessionid: Optional[str] = None
|
||
) -> List[FloorInfo]:
|
||
"""
|
||
获取指定楼栋的楼层列表
|
||
|
||
Args:
|
||
building_code: 楼栋代码
|
||
jsessionid: 会话ID,通常不需要提供(自动使用实例中的ID)
|
||
|
||
Returns:
|
||
List[FloorInfo]: 楼层信息列表
|
||
"""
|
||
jsessionid = jsessionid or self._jsessionid
|
||
|
||
try:
|
||
self.client.logger.info(f"开始获取楼层列表,楼栋代码: {building_code}")
|
||
|
||
headers = {
|
||
**self.config.aufe.default_headers,
|
||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||
"Connection": "keep-alive",
|
||
"X-Requested-With": "XMLHttpRequest",
|
||
"Cookie": f"JSESSIONID={jsessionid}; TWFID={self.client.twf_id}",
|
||
"Referer": f"{self.DEFAULT_BASE_URL}/about;jsessionid={jsessionid}",
|
||
}
|
||
|
||
response = await self.client.client.get(
|
||
f"{self.DEFAULT_BASE_URL}/about/floors/{building_code}",
|
||
headers=headers,
|
||
follow_redirects=True,
|
||
timeout=self.client.timeout,
|
||
)
|
||
|
||
if response.status_code != 200:
|
||
raise Exception(f"请求失败,状态码: {response.status_code}")
|
||
|
||
# 解析响应
|
||
data_str = response.text.strip()
|
||
self.client.logger.debug(f"楼层响应原始数据: {data_str[:200]}...")
|
||
|
||
try:
|
||
json_data = response.json()
|
||
except Exception:
|
||
# 手动转换JavaScript对象字面量为JSON格式
|
||
json_str = re.sub(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*:", r'"\1":', data_str)
|
||
self.client.logger.debug(f"转换后的JSON字符串: {json_str[:200]}...")
|
||
json_data = json.loads(json_str)
|
||
|
||
floors = []
|
||
|
||
if isinstance(json_data, list) and len(json_data) > 0:
|
||
floor_data = json_data[0]
|
||
floor_codes = floor_data.get("floordm", [])
|
||
floor_names = floor_data.get("floorname", [])
|
||
|
||
# 跳过第一个空值("请选择")
|
||
for code, name in zip(floor_codes[1:], floor_names[1:]):
|
||
if code and name and name != "请选择":
|
||
floors.append(FloorInfo(code=code, name=name))
|
||
|
||
self.client.logger.info(f"成功获取{len(floors)}个楼层信息")
|
||
return floors
|
||
else:
|
||
self.client.logger.warning(f"楼层数据格式异常: {json_data}")
|
||
return []
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取楼层列表异常: {str(e)}")
|
||
return []
|
||
|
||
@ensure_session
|
||
async def get_rooms(
|
||
self, floor_code: str, jsessionid: Optional[str] = None
|
||
) -> List[RoomInfo]:
|
||
"""
|
||
获取指定楼层的房间列表
|
||
|
||
Args:
|
||
floor_code: 楼层代码
|
||
jsessionid: 会话ID,通常不需要提供(自动使用实例中的ID)
|
||
|
||
Returns:
|
||
List[RoomInfo]: 房间信息列表
|
||
"""
|
||
jsessionid = jsessionid or self._jsessionid
|
||
|
||
try:
|
||
self.client.logger.info(f"开始获取房间列表,楼层代码: {floor_code}")
|
||
|
||
headers = {
|
||
**self.config.aufe.default_headers,
|
||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||
"Connection": "keep-alive",
|
||
"X-Requested-With": "XMLHttpRequest",
|
||
"Cookie": f"JSESSIONID={jsessionid}; TWFID={self.client.twf_id}",
|
||
"Referer": f"{self.DEFAULT_BASE_URL}/about;jsessionid={jsessionid}",
|
||
}
|
||
|
||
response = await self.client.client.get(
|
||
f"{self.DEFAULT_BASE_URL}/about/rooms/{floor_code}",
|
||
headers=headers,
|
||
follow_redirects=True,
|
||
timeout=self.client.timeout,
|
||
)
|
||
|
||
if response.status_code != 200:
|
||
raise Exception(f"请求失败,状态码: {response.status_code}")
|
||
|
||
# 解析响应
|
||
data_str = response.text.strip()
|
||
self.client.logger.debug(f"房间响应原始数据: {data_str[:200]}...")
|
||
|
||
try:
|
||
json_data = response.json()
|
||
except Exception:
|
||
# 手动转换JavaScript对象字面量为JSON格式
|
||
json_str = re.sub(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*:", r'"\1":', data_str)
|
||
self.client.logger.debug(f"转换后的JSON字符串: {json_str[:200]}...")
|
||
json_data = json.loads(json_str)
|
||
|
||
rooms = []
|
||
|
||
if isinstance(json_data, list) and len(json_data) > 0:
|
||
room_data = json_data[0]
|
||
room_codes = room_data.get("roomdm", [])
|
||
room_names = room_data.get("roomname", [])
|
||
|
||
# 跳过第一个空值("请选择")
|
||
for code, name in zip(room_codes[1:], room_names[1:]):
|
||
if code and name and name != "请选择":
|
||
rooms.append(RoomInfo(code=code, name=name))
|
||
|
||
self.client.logger.info(f"成功获取{len(rooms)}个房间信息")
|
||
return rooms
|
||
else:
|
||
self.client.logger.warning(f"房间数据格式异常: {json_data}")
|
||
return []
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取房间列表异常: {str(e)}")
|
||
return []
|
||
|
||
@ensure_session
|
||
async def bind_room_to_jsession(self, room_code: str) -> bool:
|
||
"""
|
||
绑定房间到当前会话
|
||
|
||
Args:
|
||
room_code: 房间代码
|
||
|
||
Returns:
|
||
bool: 是否绑定成功
|
||
"""
|
||
if self._jsession_bound:
|
||
return True # 已绑定,直接返回成功
|
||
params = self._generate_session_params()
|
||
room_id = room_code
|
||
display_text = await self.get_room_display_text(room_id)
|
||
if not display_text:
|
||
self.client.logger.error(f"未找到房间名称: {room_id}")
|
||
return False
|
||
|
||
data = {
|
||
"sn": params["sn"],
|
||
"openid": params["openid"],
|
||
"roomdm": room_id,
|
||
"room": display_text,
|
||
"mode": "u", # u表示更新绑定
|
||
}
|
||
headers = {
|
||
**self.config.aufe.default_headers,
|
||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||
"Connection": "keep-alive",
|
||
"X-Requested-With": "XMLHttpRequest",
|
||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||
"Cookie": f"JSESSIONID={self._jsessionid}; TWFID={self.client.twf_id}",
|
||
"Referer": f"{self.DEFAULT_BASE_URL}/about;jsessionid={self._jsessionid}",
|
||
}
|
||
response = await self.client.client.post(
|
||
f"{self.DEFAULT_BASE_URL}/about/rebinding",
|
||
headers=headers,
|
||
data=data,
|
||
follow_redirects=True,
|
||
timeout=self.client.timeout,
|
||
)
|
||
if response.status_code != 200:
|
||
self.client.logger.error(
|
||
f"绑定寝室请求失败,状态码: {response.status_code}"
|
||
)
|
||
return False
|
||
self._jsession_bound = True
|
||
return True
|
||
|
||
@ensure_session
|
||
async def get_all_room_data(
|
||
self, max_retries: int = 2, retry_delay: float = 1.0
|
||
) -> CacheRoomsData:
|
||
"""
|
||
获取所有楼栋、楼层和房间的完整数据结构
|
||
|
||
支持失败节点自动重试机制:
|
||
- 第一轮并发请求所有数据
|
||
- 提取失败的楼层节点
|
||
- 单独重试失败节点
|
||
|
||
Args:
|
||
max_retries: 失败节点最大重试次数,默认2次
|
||
retry_delay: 重试延迟时间(秒),默认1秒
|
||
|
||
Returns:
|
||
CacheRoomsData: 完整的房间数据结构
|
||
"""
|
||
jsessionid = self._jsessionid
|
||
|
||
# 第一步:获取所有楼栋
|
||
self.client.logger.info("开始获取所有楼栋信息")
|
||
buildings = await self.get_buildings(jsessionid)
|
||
|
||
if not buildings:
|
||
self.client.logger.error("获取楼栋列表失败")
|
||
return CacheRoomsData(
|
||
datetime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
data=[],
|
||
)
|
||
|
||
# 第二步:并发获取所有楼层
|
||
self.client.logger.info(f"开始并发获取 {len(buildings)} 个楼栋的楼层信息")
|
||
floor_tasks = [
|
||
self.get_floors(building.code, jsessionid) for building in buildings
|
||
]
|
||
all_floors = await asyncio.gather(*floor_tasks, return_exceptions=True)
|
||
|
||
# 处理楼层获取结果,记录失败的楼栋
|
||
failed_buildings: List[BuildingInfo] = []
|
||
valid_floors: List[List[FloorInfo]] = []
|
||
|
||
for i, floors_result in enumerate(all_floors):
|
||
if isinstance(floors_result, Exception):
|
||
self.client.logger.warning(
|
||
f"楼栋 {buildings[i].code} ({buildings[i].name}) "
|
||
f"获取楼层失败: {str(floors_result)}"
|
||
)
|
||
failed_buildings.append(buildings[i])
|
||
valid_floors.append([])
|
||
elif not floors_result or not isinstance(floors_result, list):
|
||
self.client.logger.warning(
|
||
f"楼栋 {buildings[i].code} ({buildings[i].name}) 楼层数据为空"
|
||
)
|
||
valid_floors.append([])
|
||
else:
|
||
valid_floors.append(floors_result)
|
||
|
||
# 重试失败的楼栋
|
||
if failed_buildings and max_retries > 0:
|
||
self.client.logger.info(
|
||
f"开始重试 {len(failed_buildings)} 个失败的楼栋,最大重试次数: {max_retries}"
|
||
)
|
||
|
||
for retry_count in range(1, max_retries + 1):
|
||
if not failed_buildings:
|
||
break
|
||
|
||
self.client.logger.info(
|
||
f"第 {retry_count} 次重试,待重试楼栋数: {len(failed_buildings)}"
|
||
)
|
||
|
||
# 延迟后重试
|
||
if retry_delay > 0:
|
||
await asyncio.sleep(retry_delay)
|
||
|
||
retry_tasks = [
|
||
self.get_floors(building.code, jsessionid)
|
||
for building in failed_buildings
|
||
]
|
||
retry_results = await asyncio.gather(
|
||
*retry_tasks, return_exceptions=True
|
||
)
|
||
|
||
# 更新成功的结果
|
||
new_failed_buildings: List[BuildingInfo] = []
|
||
for i, result in enumerate(retry_results):
|
||
building = failed_buildings[i]
|
||
building_index = next(
|
||
(
|
||
idx
|
||
for idx, b in enumerate(buildings)
|
||
if b.code == building.code
|
||
),
|
||
None,
|
||
)
|
||
|
||
if isinstance(result, Exception):
|
||
self.client.logger.warning(
|
||
f"重试 {retry_count}: 楼栋 {building.code} 仍然失败: {str(result)}"
|
||
)
|
||
new_failed_buildings.append(building)
|
||
elif not result or not isinstance(result, list):
|
||
self.client.logger.warning(
|
||
f"重试 {retry_count}: 楼栋 {building.code} 数据为空"
|
||
)
|
||
else:
|
||
self.client.logger.info(
|
||
f"重试 {retry_count}: 楼栋 {building.code} 成功获取 {len(result)} 个楼层"
|
||
)
|
||
if building_index is not None:
|
||
valid_floors[building_index] = result
|
||
|
||
failed_buildings = new_failed_buildings
|
||
|
||
# 统计最终失败的楼栋
|
||
if failed_buildings:
|
||
self.client.logger.error(
|
||
f"最终仍有 {len(failed_buildings)} 个楼栋获取失败: "
|
||
f"{[f'{b.code}({b.name})' for b in failed_buildings]}"
|
||
)
|
||
|
||
# 第三步:并发获取所有房间
|
||
self.client.logger.info("开始并发获取所有房间信息")
|
||
|
||
# 收集所有楼层代码及其所属楼栋索引
|
||
floor_info_list: List[tuple[int, int, FloorInfo]] = (
|
||
[]
|
||
) # (building_idx, floor_idx, floor)
|
||
for building_idx, floors in enumerate(valid_floors):
|
||
for floor_idx, floor in enumerate(floors):
|
||
floor_info_list.append((building_idx, floor_idx, floor))
|
||
|
||
# 并发获取房间
|
||
room_tasks = [
|
||
self.get_rooms(floor.code, jsessionid) for _, _, floor in floor_info_list
|
||
]
|
||
all_rooms_results = await asyncio.gather(*room_tasks, return_exceptions=True)
|
||
|
||
# 处理房间获取结果
|
||
failed_floors: List[tuple[int, int, FloorInfo]] = []
|
||
room_results_map: Dict[str, List[RoomInfo]] = {}
|
||
|
||
for i, rooms_result in enumerate(all_rooms_results):
|
||
building_idx, floor_idx, floor = floor_info_list[i]
|
||
|
||
if isinstance(rooms_result, Exception):
|
||
self.client.logger.warning(
|
||
f"楼层 {floor.code} ({floor.name}) 获取房间失败: {str(rooms_result)}"
|
||
)
|
||
failed_floors.append((building_idx, floor_idx, floor))
|
||
room_results_map[floor.code] = []
|
||
elif not rooms_result or not isinstance(rooms_result, list):
|
||
self.client.logger.debug(
|
||
f"楼层 {floor.code} ({floor.name}) 房间数据为空"
|
||
)
|
||
room_results_map[floor.code] = []
|
||
else:
|
||
room_results_map[floor.code] = rooms_result
|
||
|
||
# 重试失败的楼层
|
||
if failed_floors and max_retries > 0:
|
||
self.client.logger.info(
|
||
f"开始重试 {len(failed_floors)} 个失败的楼层,最大重试次数: {max_retries}"
|
||
)
|
||
|
||
for retry_count in range(1, max_retries + 1):
|
||
if not failed_floors:
|
||
break
|
||
|
||
self.client.logger.info(
|
||
f"第 {retry_count} 次重试,待重试楼层数: {len(failed_floors)}"
|
||
)
|
||
|
||
# 延迟后重试
|
||
if retry_delay > 0:
|
||
await asyncio.sleep(retry_delay)
|
||
|
||
retry_tasks = [
|
||
self.get_rooms(floor.code, jsessionid)
|
||
for _, _, floor in failed_floors
|
||
]
|
||
retry_results = await asyncio.gather(
|
||
*retry_tasks, return_exceptions=True
|
||
)
|
||
|
||
# 更新成功的结果
|
||
new_failed_floors: List[tuple[int, int, FloorInfo]] = []
|
||
for i, result in enumerate(retry_results):
|
||
building_idx, floor_idx, floor = failed_floors[i]
|
||
|
||
if isinstance(result, Exception):
|
||
self.client.logger.warning(
|
||
f"重试 {retry_count}: 楼层 {floor.code} 仍然失败: {str(result)}"
|
||
)
|
||
new_failed_floors.append((building_idx, floor_idx, floor))
|
||
elif not result or not isinstance(result, list):
|
||
self.client.logger.warning(
|
||
f"重试 {retry_count}: 楼层 {floor.code} 数据为空"
|
||
)
|
||
room_results_map[floor.code] = []
|
||
else:
|
||
self.client.logger.info(
|
||
f"重试 {retry_count}: 楼层 {floor.code} 成功获取 {len(result)} 个房间"
|
||
)
|
||
room_results_map[floor.code] = result
|
||
|
||
failed_floors = new_failed_floors
|
||
|
||
# 统计最终失败的楼层
|
||
if failed_floors:
|
||
self.client.logger.error(
|
||
f"最终仍有 {len(failed_floors)} 个楼层获取失败: "
|
||
f"{[f'{floor.code}({floor.name})' for _, _, floor in failed_floors]}"
|
||
)
|
||
|
||
# 第四步:构建完整数据结构
|
||
self.client.logger.info("开始构建完整数据结构")
|
||
buildings.sort(key=lambda b: b.code)
|
||
|
||
data = []
|
||
for i, building in enumerate(buildings):
|
||
floors = valid_floors[i]
|
||
if not floors:
|
||
# 该楼栋没有楼层数据,跳过
|
||
continue
|
||
|
||
floors.sort(key=lambda f: f.code)
|
||
|
||
floor_data_list = []
|
||
for floor in floors:
|
||
rooms = room_results_map.get(floor.code, [])
|
||
floor_data_list.append(
|
||
CacheFloorData(
|
||
code=floor.code,
|
||
name=floor.name,
|
||
rooms=rooms,
|
||
)
|
||
)
|
||
|
||
data.append(
|
||
CacheBuildingData(
|
||
code=building.code,
|
||
name=building.name,
|
||
floors=floor_data_list,
|
||
)
|
||
)
|
||
|
||
result = CacheRoomsData(
|
||
datetime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
data=data,
|
||
)
|
||
|
||
# 统计信息
|
||
total_buildings = len(result.data)
|
||
total_floors = sum(len(b.floors) for b in result.data)
|
||
total_rooms = sum(
|
||
len(floor.rooms) for building in result.data for floor in building.floors
|
||
)
|
||
|
||
self.client.logger.info(
|
||
f"数据获取完成 - 楼栋: {total_buildings}, 楼层: {total_floors}, 房间: {total_rooms}"
|
||
)
|
||
|
||
return result
|
||
|
||
async def _cache_room_data_to_hash(self) -> None:
|
||
"""
|
||
将房间数据平铺缓存到Redis Hash中,过期时间为一周
|
||
|
||
Hash结构:
|
||
- key: isim:rooms:v1:data
|
||
- fields:
|
||
- meta: 元数据(更新时间、版本号)
|
||
- building:{code}: 楼栋信息 JSON
|
||
- floor:{code}: 楼层信息 JSON
|
||
- rooms:{floor_code}: 该楼层的所有房间列表 JSON
|
||
"""
|
||
try:
|
||
self.client.logger.info("开始构建Hash缓存结构")
|
||
data = await self.get_all_room_data()
|
||
redis_client = await get_redis_client()
|
||
|
||
# 构建Hash映射
|
||
hash_mapping: Dict[str, Any] = {}
|
||
|
||
# 添加元数据
|
||
meta = {
|
||
"datetime": data.datetime,
|
||
"version": "v1",
|
||
"building_count": len(data.data),
|
||
}
|
||
hash_mapping["meta"] = json.dumps(meta, ensure_ascii=False)
|
||
|
||
# 平铺楼栋、楼层、房间数据
|
||
floor_count = 0
|
||
room_count = 0
|
||
|
||
for building in data.data:
|
||
# 存储楼栋信息
|
||
building_key = f"building:{building.code}"
|
||
building_info = {
|
||
"code": building.code,
|
||
"name": building.name,
|
||
}
|
||
hash_mapping[building_key] = json.dumps(
|
||
building_info, ensure_ascii=False
|
||
)
|
||
|
||
# 存储楼层和房间信息
|
||
for floor in building.floors:
|
||
floor_count += 1
|
||
|
||
# 存储楼层信息
|
||
floor_key = f"floor:{floor.code}"
|
||
floor_info = {
|
||
"code": floor.code,
|
||
"name": floor.name,
|
||
"building_code": building.code,
|
||
}
|
||
hash_mapping[floor_key] = json.dumps(floor_info, ensure_ascii=False)
|
||
|
||
# 存储该楼层的所有房间
|
||
rooms_key = f"rooms:{floor.code}"
|
||
rooms_list = [
|
||
{"code": room.code, "name": room.name} for room in floor.rooms
|
||
]
|
||
room_count += len(rooms_list)
|
||
hash_mapping[rooms_key] = json.dumps(rooms_list, ensure_ascii=False)
|
||
|
||
# 批量写入Hash
|
||
HASH_KEY = "isim:rooms:v1:data"
|
||
await redis_client.hash_set(HASH_KEY, hash_mapping)
|
||
|
||
# 设置过期时间为一周
|
||
ONE_WEEK_SECONDS = 7 * 24 * 60 * 60
|
||
await redis_client.expire(HASH_KEY, ONE_WEEK_SECONDS)
|
||
|
||
self.client.logger.info(
|
||
f"房间数据已缓存到Redis Hash,"
|
||
f"楼栋数: {len(data.data)}, "
|
||
f"楼层数: {floor_count}, "
|
||
f"房间数: {room_count}, "
|
||
f"过期时间: 7天"
|
||
)
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"缓存房间数据到Redis Hash失败: {str(e)}")
|
||
self.client.logger.exception(e)
|
||
|
||
async def get_cached_rooms_from_hash(
|
||
self,
|
||
building_code: Optional[str] = None,
|
||
floor_code: Optional[str] = None,
|
||
) -> Optional[CacheRoomsData]:
|
||
"""
|
||
从Redis Hash中获取房间缓存数据
|
||
|
||
支持全量获取或按楼栋/楼层查询
|
||
|
||
Args:
|
||
building_code: 楼栋代码,为None则获取全部
|
||
floor_code: 楼层代码,为None则获取该楼栋全部楼层
|
||
|
||
Returns:
|
||
CacheRoomsData: 房间数据,不存在返回None
|
||
"""
|
||
try:
|
||
redis_client = await get_redis_client()
|
||
HASH_KEY = "isim:rooms:v1:data"
|
||
|
||
# 检查Hash是否存在
|
||
if not await redis_client.exists(HASH_KEY):
|
||
self.client.logger.info("Redis Hash缓存不存在")
|
||
return None
|
||
|
||
# 获取元数据
|
||
meta_str = await redis_client.hash_get(HASH_KEY, "meta")
|
||
if not meta_str:
|
||
self.client.logger.warning("Hash中缺少元数据")
|
||
return None
|
||
|
||
meta = json.loads(meta_str)
|
||
|
||
# 根据查询条件获取数据
|
||
if building_code:
|
||
# 按楼栋或楼层查询
|
||
return await self._get_rooms_by_building(
|
||
redis_client, HASH_KEY, building_code, floor_code, meta
|
||
)
|
||
else:
|
||
# 全量查询
|
||
return await self._get_all_rooms_from_hash(redis_client, HASH_KEY, meta)
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"从Hash获取房间缓存异常: {str(e)}")
|
||
self.client.logger.exception(e)
|
||
return None
|
||
|
||
async def _get_all_rooms_from_hash(
|
||
self,
|
||
redis_client: Any,
|
||
hash_key: str,
|
||
meta: Dict[str, Any],
|
||
) -> CacheRoomsData:
|
||
"""从Hash中获取所有房间数据"""
|
||
try:
|
||
# 获取所有Hash字段
|
||
all_data = await redis_client.hash_get_all(hash_key)
|
||
|
||
# 提取所有楼栋代码
|
||
building_codes = set()
|
||
for field in all_data.keys():
|
||
if field.startswith("building:"):
|
||
code = field.replace("building:", "")
|
||
building_codes.add(code)
|
||
|
||
# 构建完整数据结构
|
||
buildings = []
|
||
for building_code in sorted(building_codes):
|
||
building_key = f"building:{building_code}"
|
||
building_data = json.loads(all_data.get(building_key, "{}"))
|
||
|
||
if not building_data:
|
||
continue
|
||
|
||
# 查找该楼栋的所有楼层
|
||
floors = []
|
||
for field, value in all_data.items():
|
||
if field.startswith("floor:") and field.replace(
|
||
"floor:", ""
|
||
).startswith(building_code):
|
||
floor_data = json.loads(value)
|
||
floor_code = floor_data["code"]
|
||
|
||
# 获取该楼层的房间
|
||
rooms_key = f"rooms:{floor_code}"
|
||
rooms_data = json.loads(all_data.get(rooms_key, "[]"))
|
||
|
||
floors.append(
|
||
CacheFloorData(
|
||
code=floor_data["code"],
|
||
name=floor_data["name"],
|
||
rooms=[RoomInfo(**room) for room in rooms_data],
|
||
)
|
||
)
|
||
|
||
# 按楼层代码排序
|
||
floors.sort(key=lambda f: f.code)
|
||
|
||
buildings.append(
|
||
CacheBuildingData(
|
||
code=building_data["code"],
|
||
name=building_data["name"],
|
||
floors=floors,
|
||
)
|
||
)
|
||
|
||
return CacheRoomsData(
|
||
datetime=meta.get("datetime", ""),
|
||
data=buildings,
|
||
)
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"从Hash构建完整数据结构失败: {str(e)}")
|
||
raise
|
||
|
||
async def _get_rooms_by_building(
|
||
self,
|
||
redis_client: Any,
|
||
hash_key: str,
|
||
building_code: str,
|
||
floor_code: Optional[str],
|
||
meta: Dict[str, Any],
|
||
) -> CacheRoomsData:
|
||
"""从Hash中按楼栋获取房间数据"""
|
||
try:
|
||
# 获取楼栋信息
|
||
building_key = f"building:{building_code}"
|
||
building_str = await redis_client.hash_get(hash_key, building_key)
|
||
|
||
if not building_str:
|
||
self.client.logger.warning(f"楼栋 {building_code} 不存在")
|
||
return CacheRoomsData(datetime=meta.get("datetime", ""), data=[])
|
||
|
||
building_data = json.loads(building_str)
|
||
|
||
# 获取楼层列表
|
||
floors = []
|
||
|
||
if floor_code:
|
||
# 查询特定楼层
|
||
floor_key = f"floor:{floor_code}"
|
||
floor_str = await redis_client.hash_get(hash_key, floor_key)
|
||
|
||
if floor_str:
|
||
floor_data = json.loads(floor_str)
|
||
rooms_key = f"rooms:{floor_code}"
|
||
rooms_str = await redis_client.hash_get(hash_key, rooms_key)
|
||
rooms_data = json.loads(rooms_str) if rooms_str else []
|
||
|
||
floors.append(
|
||
CacheFloorData(
|
||
code=floor_data["code"],
|
||
name=floor_data["name"],
|
||
rooms=[RoomInfo(**room) for room in rooms_data],
|
||
)
|
||
)
|
||
else:
|
||
# 查询该楼栋的所有楼层
|
||
all_data = await redis_client.hash_get_all(hash_key)
|
||
|
||
for field, value in all_data.items():
|
||
if field.startswith("floor:") and field.replace(
|
||
"floor:", ""
|
||
).startswith(building_code):
|
||
floor_data = json.loads(value)
|
||
floor_code_item = floor_data["code"]
|
||
|
||
# 获取该楼层的房间
|
||
rooms_key = f"rooms:{floor_code_item}"
|
||
rooms_str = await redis_client.hash_get(hash_key, rooms_key)
|
||
rooms_data = json.loads(rooms_str) if rooms_str else []
|
||
|
||
floors.append(
|
||
CacheFloorData(
|
||
code=floor_data["code"],
|
||
name=floor_data["name"],
|
||
rooms=[RoomInfo(**room) for room in rooms_data],
|
||
)
|
||
)
|
||
|
||
# 按楼层代码排序
|
||
floors.sort(key=lambda f: f.code)
|
||
|
||
building = CacheBuildingData(
|
||
code=building_data["code"],
|
||
name=building_data["name"],
|
||
floors=floors,
|
||
)
|
||
|
||
return CacheRoomsData(
|
||
datetime=meta.get("datetime", ""),
|
||
data=[building],
|
||
)
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"从Hash按楼栋获取数据失败: {str(e)}")
|
||
raise
|
||
|
||
async def query_room_name(self, room_code: str) -> Optional[str]:
|
||
"""
|
||
根据房间代码查询房间名称
|
||
|
||
Args:
|
||
room_code: 房间代码
|
||
|
||
Returns:
|
||
Optional[str]: 房间名称,如果未找到则返回None
|
||
"""
|
||
bulding = room_code[:2]
|
||
floor = room_code[:4]
|
||
room = room_code
|
||
rooms_data = await self.get_cached_rooms()
|
||
for building in rooms_data.data:
|
||
if building.code == bulding:
|
||
for fl in building.floors:
|
||
if fl.code == floor:
|
||
for rm in fl.rooms:
|
||
if rm.code == room:
|
||
return rm.name
|
||
return None
|
||
|
||
async def query_room_name_online(self, room_code: str) -> Optional[str]:
|
||
"""
|
||
在线根据房间代码查询房间名称
|
||
|
||
Args:
|
||
room_code: 房间代码
|
||
|
||
Returns:
|
||
Optional[str]: 房间名称,如果未找到则返回None
|
||
"""
|
||
await self._ensure_jsession() # 确保会话已初始化
|
||
|
||
bulding = room_code[:2]
|
||
floor = room_code[:4]
|
||
room = room_code
|
||
jsessionid = self._jsessionid
|
||
|
||
floors = await self.get_floors(bulding, jsessionid)
|
||
for fl in floors:
|
||
if fl.code == floor:
|
||
rooms = await self.get_rooms(floor, jsessionid)
|
||
for rm in rooms:
|
||
if rm.code == room:
|
||
return rm.name
|
||
return None
|
||
|
||
async def get_room_display_text(self, room_code: str) -> Optional[str]:
|
||
"""
|
||
根据房间代码获取完整的房间显示名称
|
||
|
||
Args:
|
||
room_code: 房间代码
|
||
|
||
Returns:
|
||
Optional[str]: 完整的房间显示名称,如果未找到则返回None
|
||
"""
|
||
bulding = room_code[:2]
|
||
floor = room_code[:4]
|
||
room = room_code
|
||
rooms_data = await self.get_cached_rooms()
|
||
for building in rooms_data.data:
|
||
if building.code == bulding:
|
||
for fl in building.floors:
|
||
if fl.code == floor:
|
||
for rm in fl.rooms:
|
||
if rm.code == room:
|
||
return f"{building.name}/{fl.name}/{rm.name}"
|
||
return None
|
||
|
||
async def get_cached_rooms(self) -> CacheRoomsData:
|
||
"""
|
||
从Redis获取缓存的房间数据,如果缓存不存在则重新获取
|
||
|
||
使用Hash存储方案
|
||
|
||
Returns:
|
||
CacheRoomsData: 房间数据
|
||
"""
|
||
try:
|
||
# 从Hash获取缓存数据
|
||
cached_data = await self.get_cached_rooms_from_hash()
|
||
|
||
if cached_data is not None and cached_data.data:
|
||
self.client.logger.info("成功从Redis Hash获取房间缓存数据")
|
||
return cached_data
|
||
|
||
# 缓存不存在,重新获取数据
|
||
self.client.logger.info("Redis中房间缓存不存在,重新获取数据")
|
||
data = await self.get_all_room_data()
|
||
|
||
# 使用Hash方案缓存数据
|
||
await self._cache_room_data_to_hash()
|
||
|
||
self.client.logger.info("房间数据已缓存到Redis Hash,过期时间:7天")
|
||
return data
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取房间缓存异常: {str(e)}")
|
||
# 异常时返回空数据
|
||
return CacheRoomsData(datetime="", data=[])
|
||
|
||
async def refresh_expired_room_cache(self) -> None:
|
||
"""
|
||
刷新过期的房间缓存(该方法已弃用,改用Redis自动过期机制)
|
||
为了向后兼容,该方法仍然保留但不执行任何操作
|
||
Redis会自动在7天后失效缓存
|
||
"""
|
||
self.client.logger.info("房间缓存已完全迁移到Redis,使用Redis的自动过期机制")
|
||
|
||
async def force_refresh_room_cache(self) -> None:
|
||
"""
|
||
强制刷新房间缓存(重新从ISIM系统获取数据并缓存到Redis Hash)
|
||
"""
|
||
try:
|
||
self.client.logger.info("开始强制刷新房间缓存")
|
||
|
||
# 使用Hash方案缓存
|
||
await self._cache_room_data_to_hash()
|
||
|
||
self.client.logger.info(
|
||
"房间缓存已强制刷新并缓存到Redis Hash,过期时间:7天"
|
||
)
|
||
except Exception as e:
|
||
self.client.logger.error(f"强制刷新房间缓存失败: {str(e)}")
|
||
|
||
async def get_building_info(self, building_code: str) -> Optional[BuildingInfo]:
|
||
"""
|
||
从缓存中获取指定楼栋信息
|
||
|
||
Args:
|
||
building_code: 楼栋代码
|
||
|
||
Returns:
|
||
BuildingInfo: 楼栋信息,不存在返回None
|
||
"""
|
||
try:
|
||
redis_client = await get_redis_client()
|
||
HASH_KEY = "isim:rooms:v1:data"
|
||
|
||
building_key = f"building:{building_code}"
|
||
building_str = await redis_client.hash_get(HASH_KEY, building_key)
|
||
|
||
if not building_str:
|
||
return None
|
||
|
||
building_data = json.loads(building_str)
|
||
return BuildingInfo(**building_data)
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取楼栋信息失败: {str(e)}")
|
||
return None
|
||
|
||
async def get_floor_info(self, floor_code: str) -> Optional[FloorInfo]:
|
||
"""
|
||
从缓存中获取指定楼层信息
|
||
|
||
Args:
|
||
floor_code: 楼层代码
|
||
|
||
Returns:
|
||
FloorInfo: 楼层信息,不存在返回None
|
||
"""
|
||
try:
|
||
redis_client = await get_redis_client()
|
||
HASH_KEY = "isim:rooms:v1:data"
|
||
|
||
floor_key = f"floor:{floor_code}"
|
||
floor_str = await redis_client.hash_get(HASH_KEY, floor_key)
|
||
|
||
if not floor_str:
|
||
return None
|
||
|
||
floor_data = json.loads(floor_str)
|
||
return FloorInfo(code=floor_data["code"], name=floor_data["name"])
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取楼层信息失败: {str(e)}")
|
||
return None
|
||
|
||
async def get_rooms_by_floor(self, floor_code: str) -> List[RoomInfo]:
|
||
"""
|
||
从缓存中获取指定楼层的所有房间
|
||
|
||
Args:
|
||
floor_code: 楼层代码
|
||
|
||
Returns:
|
||
List[RoomInfo]: 房间列表
|
||
"""
|
||
try:
|
||
redis_client = await get_redis_client()
|
||
HASH_KEY = "isim:rooms:v1:data"
|
||
|
||
rooms_key = f"rooms:{floor_code}"
|
||
rooms_str = await redis_client.hash_get(HASH_KEY, rooms_key)
|
||
|
||
if not rooms_str:
|
||
return []
|
||
|
||
rooms_data = json.loads(rooms_str)
|
||
return [RoomInfo(**room) for room in rooms_data]
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取楼层房间列表失败: {str(e)}")
|
||
return []
|
||
|
||
async def get_building_with_floors(
|
||
self, building_code: str
|
||
) -> Optional[CacheBuildingData]:
|
||
"""
|
||
从缓存中获取指定楼栋及其所有楼层和房间
|
||
|
||
Args:
|
||
building_code: 楼栋代码
|
||
|
||
Returns:
|
||
CacheBuildingData: 楼栋完整数据,不存在返回None
|
||
"""
|
||
try:
|
||
data = await self.get_cached_rooms_from_hash(building_code=building_code)
|
||
if data and data.data:
|
||
return data.data[0]
|
||
return None
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取楼栋完整数据失败: {str(e)}")
|
||
return None
|
||
|
||
async def query_room_info_fast(self, room_code: str) -> Optional[RoomInfo]:
|
||
"""
|
||
快速查询房间信息(从Hash缓存)
|
||
|
||
Args:
|
||
room_code: 房间代码(如:010101)
|
||
|
||
Returns:
|
||
RoomInfo: 房间信息,不存在返回None
|
||
"""
|
||
try:
|
||
# 从房间代码提取楼层代码
|
||
if len(room_code) < 4:
|
||
return None
|
||
|
||
floor_code = room_code[:4]
|
||
rooms = await self.get_rooms_by_floor(floor_code)
|
||
|
||
# 在房间列表中查找
|
||
for room in rooms:
|
||
if room.code == room_code:
|
||
return room
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"快速查询房间信息失败: {str(e)}")
|
||
return None
|
||
|
||
async def get_headers(self) -> Dict[str, str]:
|
||
"""获取当前请求头信息"""
|
||
return {
|
||
**self.config.aufe.default_headers,
|
||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||
"Connection": "keep-alive",
|
||
"X-Requested-With": "XMLHttpRequest",
|
||
"Cookie": f"JSESSIONID={self._jsessionid}; TWFID={self.client.twf_id}",
|
||
"Referer": f"{self.DEFAULT_BASE_URL}/about;jsessionid={self._jsessionid}",
|
||
}
|
||
|
||
@ensure_session
|
||
async def get_electricity_info(self, room_code: str) -> Optional[Dict]:
|
||
"""
|
||
获取寝室电费信息,包括余额、用电记录和充值记录
|
||
|
||
Args:
|
||
room_code: 房间代码
|
||
|
||
Returns:
|
||
Optional[Dict]: 包含balance、usage_records、payments的字典,失败时返回None
|
||
"""
|
||
|
||
try:
|
||
# 绑定寝室到当前会话
|
||
if not await self.bind_room_to_jsession(room_code):
|
||
self.client.logger.error(f"绑定寝室失败: {room_code}")
|
||
return None
|
||
|
||
header = await self.get_headers()
|
||
|
||
# 获取用电记录和余额信息
|
||
url_usage = f"{self.DEFAULT_BASE_URL}/use/record"
|
||
response_usage_co = self.client.client.get(
|
||
url_usage, headers=header, timeout=10000, follow_redirects=True
|
||
)
|
||
url_payment = f"{self.DEFAULT_BASE_URL}/pay/record"
|
||
response_payment_co = self.client.client.get(
|
||
url_payment, headers=header, timeout=10000, follow_redirects=True
|
||
)
|
||
response_usage, response_payment = await asyncio.gather(
|
||
response_usage_co, response_payment_co
|
||
)
|
||
|
||
if response_usage.status_code != 200:
|
||
self.client.logger.error(
|
||
f"获取寝室电费信息失败,状态码: {response_usage.status_code}"
|
||
)
|
||
return None
|
||
|
||
soup = BeautifulSoup(response_usage.text, "lxml")
|
||
|
||
# 提取余额信息
|
||
balance_items = soup.find_all("li", class_="item-content")
|
||
remaining_purchased = 0.0
|
||
remaining_subsidy = 0.0
|
||
|
||
for item in balance_items:
|
||
title_div = item.find("div", class_="item-title")
|
||
after_div = item.find("div", class_="item-after")
|
||
|
||
if title_div and after_div:
|
||
title = title_div.get_text(strip=True)
|
||
value_text = after_div.get_text(strip=True)
|
||
|
||
# 提取数值
|
||
value_match = re.search(r"([\d.]+)", value_text)
|
||
if value_match:
|
||
value = float(value_match.group(1))
|
||
|
||
if "剩余购电" in title:
|
||
remaining_purchased = value
|
||
elif "剩余补助" in title:
|
||
remaining_subsidy = value
|
||
|
||
# 提取用电记录
|
||
usage_records = []
|
||
record_items = soup.select("#divRecord ul li")
|
||
|
||
for item in record_items:
|
||
title_div = item.find("div", class_="item-title")
|
||
after_div = item.find("div", class_="item-after")
|
||
subtitle_div = item.find("div", class_="item-subtitle")
|
||
|
||
if title_div and after_div and subtitle_div:
|
||
record_time = title_div.get_text(strip=True)
|
||
usage_text = after_div.get_text(strip=True)
|
||
meter_text = subtitle_div.get_text(strip=True)
|
||
|
||
# 提取用电量
|
||
usage_match = re.search(r"([\d.]+)度", usage_text)
|
||
if usage_match:
|
||
usage_amount = float(usage_match.group(1))
|
||
|
||
# 提取电表名称
|
||
meter_match = re.search(r"电表:\s*(.+)", meter_text)
|
||
meter_name = meter_match.group(1) if meter_match else meter_text
|
||
|
||
usage_records.append(
|
||
ElectricityUsageRecord(
|
||
record_time=record_time,
|
||
usage_amount=usage_amount,
|
||
meter_name=meter_name,
|
||
)
|
||
)
|
||
|
||
balance = ElectricityBalance(
|
||
remaining_purchased=remaining_purchased,
|
||
remaining_subsidy=remaining_subsidy,
|
||
)
|
||
|
||
# 获取充值记录
|
||
if response_payment.status_code != 200:
|
||
self.client.logger.error(
|
||
f"获取寝室电费充值记录失败,状态码: {response_payment.status_code}"
|
||
)
|
||
return None
|
||
|
||
soup = BeautifulSoup(response_payment.text, "lxml")
|
||
payment_records = []
|
||
record_items = soup.select("#divRecord ul li")
|
||
|
||
for item in record_items:
|
||
title_div = item.find("div", class_="item-title")
|
||
after_div = item.find("div", class_="item-after")
|
||
subtitle_div = item.find("div", class_="item-subtitle")
|
||
|
||
if title_div and after_div and subtitle_div:
|
||
payment_time = title_div.get_text(strip=True)
|
||
amount_text = after_div.get_text(strip=True)
|
||
type_text = subtitle_div.get_text(strip=True)
|
||
|
||
# 提取金额
|
||
amount_match = re.search(r"(-?[\d.]+)元", amount_text)
|
||
if amount_match:
|
||
amount = float(amount_match.group(1))
|
||
|
||
# 提取充值类型
|
||
type_match = re.search(r"类型:\s*(.+)", type_text)
|
||
payment_type = type_match.group(1) if type_match else type_text
|
||
|
||
payment_records.append(
|
||
PaymentRecord(
|
||
payment_time=payment_time,
|
||
amount=amount,
|
||
payment_type=payment_type,
|
||
)
|
||
)
|
||
|
||
self.client.logger.info(f"成功获取寝室 {room_code} 的电费信息")
|
||
return {
|
||
"balance": balance,
|
||
"usage_records": usage_records,
|
||
"payments": payment_records,
|
||
}
|
||
|
||
except Exception as e:
|
||
self.client.logger.error(f"获取寝室电费信息异常: {str(e)}")
|
||
self.client.logger.exception(e)
|
||
return None
|
||
|
||
async def aclose(self):
|
||
"""关闭客户端,释放资源"""
|
||
self.client.logger.info("正在关闭ISIM客户端")
|
||
# 目前没有额外资源需要释放
|
||
pass
|
||
|
||
|
||
async def get_isim_client(conn: AUFEConnection = Depends(get_aufe_conn)) -> ISIMClient:
|
||
"""
|
||
获取ISIM客户端实例
|
||
|
||
客户端会自动初始化会话和刷新过期的缓存,无需手动调用初始化方法
|
||
"""
|
||
if client := conn.get_subclient("isim", ISIMClient):
|
||
conn.logger.info("复用已存在的ISIM客户端实例")
|
||
return client
|
||
isim = ISIMClient(conn)
|
||
conn.logger.info("创建新的ISIM客户端实例")
|
||
conn.inject_subclient("isim", isim)
|
||
# 预先刷新过期的房间缓存(如果需要)
|
||
await isim.refresh_expired_room_cache()
|
||
return isim
|