⚒️ 重大重构 LoveACE V2

引入了 mongodb
对数据库进行了一定程度的数据加密
性能改善
代码简化
统一错误模型和响应
使用 apifox 作为文档
This commit is contained in:
2025-11-20 20:44:25 +08:00
parent 6b90c6d7bb
commit bbc86b8330
168 changed files with 14264 additions and 19152 deletions

View File

@@ -0,0 +1,12 @@
from fastapi import APIRouter
from loveace.router.endpoint.isim.elec import isim_elec_router
from loveace.router.endpoint.isim.room import isim_room_router
isim_base_router = APIRouter(
prefix="/isim",
tags=["电费"],
)
isim_base_router.include_router(isim_room_router)
isim_base_router.include_router(isim_elec_router)

View File

@@ -0,0 +1,74 @@
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from loveace.database.isim.room import RoomBind
from loveace.router.endpoint.isim.model.isim import (
UniISIMInfoResponse,
)
from loveace.router.endpoint.isim.model.protect_router import ISIMRouterErrorToCode
from loveace.router.endpoint.isim.utils.isim import ISIMClient, get_isim_client
from loveace.router.endpoint.isim.utils.room import get_bound_room
from loveace.router.schemas.uniresponse import UniResponseModel
isim_elec_router = APIRouter(
prefix="/elec",
responses=ISIMRouterErrorToCode().gen_code_table(),
)
@isim_elec_router.get(
"/info",
summary="获取寝室电费信息",
response_model=UniResponseModel[UniISIMInfoResponse],
)
async def get_isim_info(
isim: ISIMClient = Depends(get_isim_client),
room: RoomBind = Depends(get_bound_room),
) -> UniResponseModel[UniISIMInfoResponse] | JSONResponse:
"""
获取用户绑定宿舍的电费信息
✅ 功能特性:
- 获取当前电费余额
- 获取用电记录历史
- 获取缴费记录
💡 使用场景:
- 个人中心查看宿舍电费
- 监测用电情况
- 查看缴费历史
Returns:
UniISIMInfoResponse: 包含房间信息、电费余额、用电记录、缴费记录
"""
try:
# 使用 ISIMClient 的集成方法获取电费信息
result = await isim.get_electricity_info(room.roomid)
if result is None:
isim.client.logger.error(f"获取寝室 {room.roomid} 电费信息失败")
return ISIMRouterErrorToCode().remote_service_error.to_json_response(
isim.client.logger.trace_id
)
room_display = await isim.get_room_display_text(room.roomid)
room_display = "" if room_display is None else room_display
return UniResponseModel[UniISIMInfoResponse](
success=True,
data=UniISIMInfoResponse(
room_code=room.roomid,
room_display=room_display,
room_text=room.roomtext,
balance=result["balance"],
usage_records=result["usage_records"],
payments=result["payments"],
),
message="获取寝室电费信息成功",
error=None,
)
except Exception as e:
isim.client.logger.error("获取寝室电费信息异常")
isim.client.logger.exception(e)
return ISIMRouterErrorToCode().server_error.to_json_response(
isim.client.logger.trace_id
)

View File

@@ -0,0 +1,42 @@
from typing import List
from pydantic import BaseModel, Field
# ==================== 电费相关模型 ====================
class ElectricityBalance(BaseModel):
"""电费余额信息"""
remaining_purchased: float = Field(..., description="剩余购电(度)")
remaining_subsidy: float = Field(..., description="剩余补助(度)")
class ElectricityUsageRecord(BaseModel):
"""用电记录"""
record_time: str = Field(..., description="记录时间2025-08-29 00:04:58")
usage_amount: float = Field(..., description="用电量(度)")
meter_name: str = Field(..., description="电表名称1-101 或 1-101空调")
# ==================== 充值相关模型 ====================
class PaymentRecord(BaseModel):
"""充值记录"""
payment_time: str = Field(..., description="充值时间2025-02-21 11:30:08")
amount: float = Field(..., description="充值金额(元)")
payment_type: str = Field(..., description="充值类型,如:下发补助、一卡通充值")
class UniISIMInfoResponse(BaseModel):
"""寝室电费信息"""
room_code: str = Field(..., description="寝室代码")
room_text: str = Field(..., description="寝室显示名称")
room_display: str = Field(..., description="寝室显示名称")
balance: ElectricityBalance = Field(..., description="电费余额")
usage_records: List[ElectricityUsageRecord] = Field(..., description="用电记录")
payments: List[PaymentRecord] = Field(..., description="充值记录")

View File

@@ -0,0 +1,18 @@
from fastapi import status
from loveace.router.schemas.error import ErrorToCodeNode, ProtectRouterErrorToCode
class ISIMRouterErrorToCode(ProtectRouterErrorToCode):
"""ISIM 统一错误码"""
UNBOUNDROOM: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_400_BAD_REQUEST,
code="UNBOUND_ROOM",
message="房间未绑定",
)
CACHEDROOMSEXPIRED: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_400_BAD_REQUEST,
code="CACHED_ROOMS_EXPIRED",
message="房间缓存已过期,请稍后重新获取房间列表",
)

View File

@@ -0,0 +1,172 @@
from typing import List
from pydantic import BaseModel, Field
##############################################################
# * 寝室绑定请求模型 *#
##############################################################
class BindRoomRequest(BaseModel):
"""绑定寝室请求模型"""
room_id: str = Field(..., description="寝室ID")
##############################################################
# * 寝室绑定响应模型 *#
##############################################################
class BindRoomResponse(BaseModel):
"""绑定寝室响应模型"""
success: bool = Field(..., description="是否绑定成功")
##############################################################
# * 楼栋信息模型 *#
##############################################################
class BuildingInfo(BaseModel):
"""楼栋信息"""
code: str = Field(..., description="楼栋代码")
name: str = Field(..., description="楼栋名称")
##############################################################
# * 楼层信息模型 *#
##############################################################
class FloorInfo(BaseModel):
"""楼层信息"""
code: str = Field(..., description="楼层代码")
name: str = Field(..., description="楼层名称")
##############################################################
# * 房间信息模型 *#
##############################################################
class RoomInfo(BaseModel):
"""房间信息"""
code: str = Field(..., description="房间代码")
name: str = Field(..., description="房间名称")
###############################################################
# * 楼栋-楼层-房间信息模型 *#
###############################################################
class CacheFloorData(BaseModel):
"""缓存的楼层信息"""
code: str = Field(..., description="楼层代码")
name: str = Field(..., description="楼层名称")
rooms: List[RoomInfo] = Field(..., description="房间列表")
class CacheBuildingData(BaseModel):
"""缓存的楼栋信息"""
code: str = Field(..., description="楼栋代码")
name: str = Field(..., description="楼栋名称")
floors: List[CacheFloorData] = Field(..., description="楼层列表")
class CacheRoomsData(BaseModel):
"""缓存的寝室信息"""
datetime: str = Field(..., description="数据更新时间格式YYYY-MM-DD HH:MM:SS")
data: List[CacheBuildingData] = Field(..., description="楼栋列表")
class RoomBindingInfo(BaseModel):
"""房间绑定信息"""
building: BuildingInfo
floor: FloorInfo
room: RoomInfo
room_id: str = Field(..., description="完整房间ID")
display_text: str = Field(
..., description="显示文本北苑11号学生公寓/11-6层/11-627"
)
##############################################################
# * 获取当前宿舍响应模型 *#
##############################################################
class CurrentRoomResponse(BaseModel):
"""获取当前宿舍响应模型"""
room_code: str = Field(..., description="房间代码")
display_text: str = Field(
..., description="显示文本北苑11号学生公寓/11-6层/11-627"
)
##############################################################
# * 强制刷新响应模型 *#
##############################################################
class ForceRefreshResponse(BaseModel):
"""强制刷新响应模型"""
success: bool = Field(..., description="是否刷新成功")
message: str = Field(..., description="响应消息")
remaining_cooldown: float = Field(
default=0.0, description="剩余冷却时间0表示无冷却"
)
##############################################################
# * 楼层房间查询响应模型 *#
##############################################################
class FloorRoomsResponse(BaseModel):
"""楼层房间查询响应模型"""
floor_code: str = Field(..., description="楼层代码")
floor_name: str = Field(..., description="楼层名称")
building_code: str = Field(..., description="所属楼栋代码")
rooms: List[RoomInfo] = Field(..., description="房间列表")
room_count: int = Field(..., description="房间数量")
##############################################################
# * 房间详情查询响应模型 *#
##############################################################
class RoomDetailResponse(BaseModel):
"""房间详情查询响应模型"""
room_code: str = Field(..., description="房间代码")
room_name: str = Field(..., description="房间名称")
floor_code: str = Field(..., description="所属楼层代码")
floor_name: str = Field(..., description="所属楼层名称")
building_code: str = Field(..., description="所属楼栋代码")
building_name: str = Field(..., description="所属楼栋名称")
display_text: str = Field(..., description="完整显示文本")
##############################################################
# * 楼栋列表响应模型 *#
##############################################################
class BuildingListResponse(BaseModel):
"""楼栋列表响应模型"""
buildings: List[BuildingInfo] = Field(..., description="楼栋列表")
building_count: int = Field(..., description="楼栋数量")
datetime: str = Field(..., description="数据更新时间")

View File

@@ -0,0 +1,544 @@
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from loveace.database.auth.user import ACEUser
from loveace.database.creator import get_db_session
from loveace.database.isim.room import RoomBind
from loveace.router.dependencies.auth import get_user_by_token
from loveace.router.endpoint.isim.model.protect_router import ISIMRouterErrorToCode
from loveace.router.endpoint.isim.model.room import (
BindRoomRequest,
BindRoomResponse,
BuildingInfo,
BuildingListResponse,
CacheRoomsData,
CurrentRoomResponse,
FloorRoomsResponse,
ForceRefreshResponse,
RoomDetailResponse,
)
from loveace.router.endpoint.isim.utils.isim import ISIMClient, get_isim_client
from loveace.router.endpoint.isim.utils.lock_manager import get_refresh_lock_manager
from loveace.router.schemas.uniresponse import UniResponseModel
isim_room_router = APIRouter(
prefix="/room",
responses=ISIMRouterErrorToCode.gen_code_table(),
)
@isim_room_router.get(
"/list",
summary="[完整数据] 获取所有楼栋、楼层、房间的完整树形结构",
response_model=UniResponseModel[CacheRoomsData],
)
async def get_rooms(
isim_conn: ISIMClient = Depends(get_isim_client),
) -> UniResponseModel[CacheRoomsData] | JSONResponse:
"""
获取完整的寝室列表(所有楼栋、楼层、房间的树形结构)
⚠️ 数据量大:包含所有楼栋的完整数据,适合需要完整数据的场景
💡 建议:移动端或需要部分数据的场景,请使用其他精细化查询接口
"""
try:
rooms = await isim_conn.get_cached_rooms()
if not rooms:
return ISIMRouterErrorToCode().null_response.to_json_response(
isim_conn.client.logger.trace_id
)
return UniResponseModel[CacheRoomsData](
success=True,
data=rooms,
message="获取寝室列表成功",
error=None,
)
except Exception as e:
isim_conn.client.logger.error(f"获取寝室列表异常: {str(e)}")
return ISIMRouterErrorToCode().server_error.to_json_response(
isim_conn.client.logger.trace_id
)
@isim_room_router.get(
"/list/buildings",
summary="[轻量级] 获取所有楼栋列表(仅楼栋信息,不含楼层和房间)",
response_model=UniResponseModel[BuildingListResponse],
)
async def get_all_buildings(
isim_conn: ISIMClient = Depends(get_isim_client),
) -> UniResponseModel[BuildingListResponse] | JSONResponse:
"""
获取所有楼栋列表(仅楼栋的代码和名称)
✅ 数据量小:只返回楼栋列表,不包含楼层和房间
💡 使用场景:
- 楼栋选择器
- 第一级导航菜单
- 需要快速获取楼栋列表的场景
"""
logger = isim_conn.client.logger
try:
# 从Hash缓存获取完整数据
full_data = await isim_conn.get_cached_rooms()
if not full_data or not full_data.data:
logger.warning("楼栋数据不存在")
return ISIMRouterErrorToCode().null_response.to_json_response(
logger.trace_id
)
# 提取楼栋信息
buildings = [
{"code": building.code, "name": building.name}
for building in full_data.data
]
result = BuildingListResponse(
buildings=[BuildingInfo(**b) for b in buildings],
building_count=len(buildings),
datetime=full_data.datetime,
)
logger.info(f"成功获取楼栋列表,共 {len(buildings)} 个楼栋")
return UniResponseModel[BuildingListResponse](
success=True,
data=result,
message=f"获取楼栋列表成功,共 {len(buildings)} 个楼栋",
error=None,
)
except Exception as e:
logger.error(f"获取楼栋列表异常: {str(e)}")
return ISIMRouterErrorToCode().server_error.to_json_response(logger.trace_id)
@isim_room_router.get(
"/list/building/{building_code}",
summary="[按楼栋查询] 获取指定楼栋的所有楼层和房间",
response_model=UniResponseModel[CacheRoomsData],
)
async def get_building_rooms(
building_code: str, isim_conn: ISIMClient = Depends(get_isim_client)
) -> UniResponseModel[CacheRoomsData] | JSONResponse:
"""
获取指定楼栋及其所有楼层和房间的完整数据
✅ 数据量适中只返回单个楼栋的数据比完整列表小90%+
💡 使用场景:
- 用户选择楼栋后,展示该楼栋的所有楼层和房间
- 楼栋详情页
- 减少移动端流量消耗
Args:
building_code: 楼栋代码01, 02, 11等
"""
logger = isim_conn.client.logger
try:
# 使用Hash精细化查询只获取指定楼栋
building_data = await isim_conn.get_building_with_floors(building_code)
if not building_data:
logger.warning(f"楼栋 {building_code} 不存在或无数据")
return ISIMRouterErrorToCode().null_response.to_json_response(
logger.trace_id
)
# 构造响应数据
import datetime
result = CacheRoomsData(
datetime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
data=[building_data],
)
logger.info(
f"成功获取楼栋 {building_code} 信息,"
f"楼层数: {len(building_data.floors)}, "
f"房间数: {sum(len(f.rooms) for f in building_data.floors)}"
)
return UniResponseModel[CacheRoomsData](
success=True,
data=result,
message=f"获取楼栋 {building_code} 信息成功",
error=None,
)
except Exception as e:
logger.error(f"获取楼栋 {building_code} 信息异常: {str(e)}")
return ISIMRouterErrorToCode().server_error.to_json_response(logger.trace_id)
@isim_room_router.get(
"/list/floor/{floor_code}",
summary="[按楼层查询] 获取指定楼层的所有房间列表",
response_model=UniResponseModel[FloorRoomsResponse],
)
async def get_floor_rooms(
floor_code: str, isim_conn: ISIMClient = Depends(get_isim_client)
) -> UniResponseModel[FloorRoomsResponse] | JSONResponse:
"""
获取指定楼层的所有房间信息
✅ 数据量最小:只返回单个楼层的房间列表,极小数据量
💡 使用场景:
- 用户选择楼层后,展示该楼层的所有房间
- 房间选择器的第三级
- 移动端分页加载
- 需要最快响应速度的场景
Args:
floor_code: 楼层代码0101, 0102, 1101等
"""
logger = isim_conn.client.logger
try:
# 获取楼层信息
floor_info = await isim_conn.get_floor_info(floor_code)
if not floor_info:
logger.warning(f"楼层 {floor_code} 不存在")
return ISIMRouterErrorToCode().null_response.to_json_response(
logger.trace_id
)
# 获取房间列表从Hash直接查询非常快速
rooms = await isim_conn.get_rooms_by_floor(floor_code)
# 从楼层代码提取楼栋代码前2位
building_code = floor_code[:2] if len(floor_code) >= 2 else ""
result = FloorRoomsResponse(
floor_code=floor_info.code,
floor_name=floor_info.name,
building_code=building_code,
rooms=rooms,
room_count=len(rooms),
)
logger.info(
f"成功获取楼层 {floor_code} ({floor_info.name}) 的房间信息,共 {len(rooms)} 个房间"
)
return UniResponseModel[FloorRoomsResponse](
success=True,
data=result,
message=f"获取楼层 {floor_code} 的房间信息成功,共 {len(rooms)} 个房间",
error=None,
)
except Exception as e:
logger.error(f"获取楼层 {floor_code} 房间信息异常: {str(e)}")
return ISIMRouterErrorToCode().server_error.to_json_response(logger.trace_id)
@isim_room_router.get(
"/info/{room_code}",
summary="[房间详情] 获取单个房间的完整层级信息",
response_model=UniResponseModel[RoomDetailResponse],
)
async def get_room_info(
room_code: str, isim_conn: ISIMClient = Depends(get_isim_client)
) -> UniResponseModel[RoomDetailResponse] | JSONResponse:
"""
获取指定房间的完整信息(包括楼栋、楼层、房间的完整层级结构)
✅ 功能强大:一次性返回房间的完整上下文信息
💡 使用场景:
- 房间详情页展示
- 显示完整的 "楼栋/楼层/房间" 路径
- 房间搜索结果展示
- 需要房间完整信息的场景
Args:
room_code: 房间代码010101, 110627等
"""
logger = isim_conn.client.logger
try:
# 提取层级代码
if len(room_code) < 4:
logger.warning(f"房间代码 {room_code} 格式错误")
return ISIMRouterErrorToCode().null_response.to_json_response(
logger.trace_id
)
building_code = room_code[:2]
floor_code = room_code[:4]
# 并发获取所有需要的信息
import asyncio
building_info, floor_info, room_info = await asyncio.gather(
isim_conn.get_building_info(building_code),
isim_conn.get_floor_info(floor_code),
isim_conn.query_room_info_fast(room_code),
)
if not room_info:
logger.warning(f"房间 {room_code} 不存在")
return ISIMRouterErrorToCode().null_response.to_json_response(
logger.trace_id
)
# 构造显示文本
building_name = building_info.name if building_info else "未知楼栋"
floor_name = floor_info.name if floor_info else "未知楼层"
display_text = f"{building_name}/{floor_name}/{room_info.name}"
result = RoomDetailResponse(
room_code=room_info.code,
room_name=room_info.name,
floor_code=floor_code,
floor_name=floor_name,
building_code=building_code,
building_name=building_name,
display_text=display_text,
)
logger.info(f"成功获取房间 {room_code} 的详细信息: {display_text}")
return UniResponseModel[RoomDetailResponse](
success=True,
data=result,
message=f"获取房间 {room_code} 的详细信息成功",
error=None,
)
except Exception as e:
logger.error(f"获取房间 {room_code} 详细信息异常: {str(e)}")
return ISIMRouterErrorToCode().server_error.to_json_response(logger.trace_id)
@isim_room_router.post(
"/bind",
summary="[用户操作] 绑定寝室到当前用户",
response_model=UniResponseModel[BindRoomResponse],
)
async def bind_room(
bind_request: BindRoomRequest,
isim_conn: ISIMClient = Depends(get_isim_client),
db: AsyncSession = Depends(get_db_session),
) -> UniResponseModel[BindRoomResponse] | JSONResponse:
"""
绑定寝室到当前用户(存在即更新)
💡 使用场景:
- 用户首次绑定寝室
- 用户更换寝室
- 修改绑定信息
"""
logger = isim_conn.client.logger
try:
exist = await db.execute(
select(RoomBind).where(RoomBind.user_id == isim_conn.client.userid)
)
exist = exist.scalars().first()
if exist:
if exist.roomid == bind_request.room_id:
return UniResponseModel[BindRoomResponse](
success=True,
data=BindRoomResponse(success=True),
message="宿舍绑定成功",
error=None,
)
else:
# 使用快速查询方法从Hash直接获取无需遍历完整树
room_info = await isim_conn.query_room_info_fast(bind_request.room_id)
roomtext = room_info.name if room_info else None
# 如果Hash中没有回退到完整查询
if not roomtext:
roomtext = await isim_conn.query_room_name(bind_request.room_id)
await db.execute(
update(RoomBind)
.where(RoomBind.user_id == isim_conn.client.userid)
.values(roomid=bind_request.room_id, roomtext=roomtext)
)
await db.commit()
logger.info(f"更新寝室绑定成功: {roomtext}({bind_request.room_id})")
return UniResponseModel[BindRoomResponse](
success=True,
data=BindRoomResponse(success=True),
message="宿舍绑定成功",
error=None,
)
else:
# 使用快速查询方法从Hash直接获取无需遍历完整树
room_info = await isim_conn.query_room_info_fast(bind_request.room_id)
roomtext = room_info.name if room_info else None
# 如果Hash中没有回退到完整查询
if not roomtext:
roomtext = await isim_conn.query_room_name(bind_request.room_id)
new_bind = RoomBind(
user_id=isim_conn.client.userid,
roomid=bind_request.room_id,
roomtext=roomtext,
)
db.add(new_bind)
await db.commit()
logger.info(f"新增寝室绑定成功: {roomtext}({bind_request.room_id})")
return UniResponseModel[BindRoomResponse](
success=True,
data=BindRoomResponse(success=True),
message="宿舍绑定成功",
error=None,
)
except Exception as e:
logger.error(f"绑定寝室异常: {str(e)}")
return ISIMRouterErrorToCode().server_error.to_json_response(
isim_conn.client.logger.trace_id
)
@isim_room_router.get(
"/current",
summary="[用户查询] 获取当前用户绑定的宿舍信息",
response_model=UniResponseModel[CurrentRoomResponse],
)
async def get_current_room(
user: ACEUser = Depends(get_user_by_token),
isim_conn: ISIMClient = Depends(get_isim_client),
db: AsyncSession = Depends(get_db_session),
) -> UniResponseModel[CurrentRoomResponse] | JSONResponse:
"""
获取当前用户绑定的宿舍信息,返回 room_code 和 display_text
💡 使用场景:
- 个人中心显示已绑定宿舍
- 查询当前用户的寝室信息
- 验证用户是否已绑定寝室
"""
logger = isim_conn.client.logger
try:
# 查询用户绑定的房间
result = await db.execute(
select(RoomBind).where(RoomBind.user_id == user.userid)
)
room_bind = result.scalars().first()
if not room_bind:
logger.warning(f"用户 {user.userid} 未绑定宿舍")
return UniResponseModel[CurrentRoomResponse](
success=True,
data=CurrentRoomResponse(
room_code="",
display_text="",
),
message="获取宿舍信息成功,用户未绑定宿舍",
error=None,
)
# 优先从Hash缓存快速获取房间显示文本
display_text = await isim_conn.get_room_display_text(room_bind.roomid)
if not display_text:
# 如果缓存中没有,使用数据库中存储的文本
display_text = room_bind.roomtext
logger.debug(
f"Hash缓存中未找到房间 {room_bind.roomid},使用数据库存储的文本"
)
logger.info(f"成功获取用户 {user.userid} 的宿舍信息: {display_text}")
return UniResponseModel[CurrentRoomResponse](
success=True,
data=CurrentRoomResponse(
room_code=room_bind.roomid,
display_text=display_text,
),
message="获取宿舍信息成功",
error=None,
)
except Exception as e:
logger.error(f"获取当前宿舍异常: {str(e)}")
return ISIMRouterErrorToCode().server_error.to_json_response(logger.trace_id)
@isim_room_router.post(
"/refresh",
summary="[管理操作] 强制刷新房间列表缓存",
response_model=UniResponseModel[ForceRefreshResponse],
)
async def force_refresh_rooms(
isim_conn: ISIMClient = Depends(get_isim_client),
) -> UniResponseModel[ForceRefreshResponse] | JSONResponse:
"""
强制刷新房间列表缓存从ISIM系统重新获取数据
⚠️ 限制:
- 使用全局锁确保同一时间只有一个请求在执行刷新操作
- 刷新完成后有30分钟的冷却时间
💡 使用场景:
- 发现数据不准确时手动刷新
- 管理员更新缓存数据
- 调试和测试
"""
logger = isim_conn.client.logger
lock_manager = get_refresh_lock_manager()
try:
# 尝试获取锁
acquired, remaining_cooldown = await lock_manager.try_acquire()
if not acquired:
if remaining_cooldown is not None:
# 在冷却期内
minutes = int(remaining_cooldown // 60)
seconds = int(remaining_cooldown % 60)
message = f"刷新操作冷却中,请在 {minutes}{seconds} 秒后重试"
logger.warning(f"刷新请求被拒绝: {message}")
return UniResponseModel[ForceRefreshResponse](
success=False,
data=ForceRefreshResponse(
success=False,
message=message,
remaining_cooldown=remaining_cooldown,
),
message=message,
error=None,
)
else:
# 有其他人正在刷新
message = "其他用户正在刷新房间列表,请稍后再试"
logger.warning(message)
return UniResponseModel[ForceRefreshResponse](
success=False,
data=ForceRefreshResponse(
success=False,
message=message,
remaining_cooldown=0.0,
),
message=message,
error=None,
)
# 成功获取锁,执行刷新操作
try:
logger.info("开始强制刷新房间列表缓存")
await isim_conn.force_refresh_room_cache()
logger.info("房间列表缓存刷新完成")
return UniResponseModel[ForceRefreshResponse](
success=True,
data=ForceRefreshResponse(
success=True,
message="房间列表刷新成功",
remaining_cooldown=0.0,
),
message="房间列表刷新成功",
error=None,
)
finally:
# 释放锁并设置冷却时间
lock_manager.release()
logger.info("刷新锁已释放,冷却时间已设置")
except Exception as e:
logger.error(f"强制刷新房间列表异常: {str(e)}")
# 确保异常时也释放锁
if lock_manager.is_refreshing():
lock_manager.release()
return ISIMRouterErrorToCode().server_error.to_json_response(logger.trace_id)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
"""
全局锁管理器模块
用于管理需要冷却时间(CD)的操作锁
"""
import asyncio
import time
from typing import Optional
class RefreshLockManager:
"""刷新操作锁管理器确保一次只能执行一个刷新操作且有30分钟的冷却时间"""
def __init__(self, cooldown_seconds: int = 1800): # 默认30分钟 = 1800秒
"""
初始化锁管理器
Args:
cooldown_seconds: 冷却时间默认为1800秒30分钟
"""
self._lock = asyncio.Lock()
self._last_refresh_time: Optional[float] = None
self._cooldown_seconds = cooldown_seconds
self._is_refreshing = False
async def try_acquire(self) -> tuple[bool, Optional[float]]:
"""
尝试获取锁并检查冷却时间
Returns:
tuple[bool, Optional[float]]:
- bool: 是否成功获取锁(未在冷却期且未被占用)
- Optional[float]: 如果在冷却期返回剩余冷却时间否则为None
"""
# 检查是否有其他人正在刷新
if self._is_refreshing:
return False, None
# 检查冷却时间
if self._last_refresh_time is not None:
elapsed = time.time() - self._last_refresh_time
if elapsed < self._cooldown_seconds:
remaining_cooldown = self._cooldown_seconds - elapsed
return False, remaining_cooldown
# 尝试获取锁(非阻塞)
acquired = not self._lock.locked()
if acquired:
await self._lock.acquire()
self._is_refreshing = True
return acquired, None
def release(self):
"""
释放锁并更新最后刷新时间
"""
self._last_refresh_time = time.time()
self._is_refreshing = False
if self._lock.locked():
self._lock.release()
def get_remaining_cooldown(self) -> Optional[float]:
"""
获取剩余冷却时间
Returns:
Optional[float]: 剩余冷却时间如果不在冷却期则返回None
"""
if self._last_refresh_time is None:
return None
elapsed = time.time() - self._last_refresh_time
if elapsed < self._cooldown_seconds:
return self._cooldown_seconds - elapsed
return None
def is_in_cooldown(self) -> bool:
"""
检查是否在冷却期内
Returns:
bool: 是否在冷却期内
"""
return self.get_remaining_cooldown() is not None
def is_refreshing(self) -> bool:
"""
检查是否正在刷新
Returns:
bool: 是否正在刷新
"""
return self._is_refreshing
# 全局单例实例
_refresh_lock_manager = RefreshLockManager(cooldown_seconds=1800) # 30分钟
def get_refresh_lock_manager() -> RefreshLockManager:
"""
获取全局刷新锁管理器实例
Returns:
RefreshLockManager: 全局锁管理器实例
"""
return _refresh_lock_manager

View File

@@ -0,0 +1,24 @@
from fastapi import Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from loveace.database.creator import get_db_session
from loveace.database.isim.room import RoomBind
from loveace.router.endpoint.isim.model.protect_router import ISIMRouterErrorToCode
from loveace.router.endpoint.isim.utils.isim import ISIMClient, get_isim_client
async def get_bound_room(
isim_conn: ISIMClient = Depends(get_isim_client),
db: AsyncSession = Depends(get_db_session),
) -> RoomBind:
"""获取已绑定的寝室"""
result = await db.execute(
select(RoomBind).where(RoomBind.user_id == isim_conn.client.userid)
)
bound_room = result.scalars().first()
if not bound_room:
raise ISIMRouterErrorToCode.UNBOUNDROOM.to_http_exception(
isim_conn.client.logger.trace_id
)
return bound_room