⚒️ 重大重构 LoveACE V2
引入了 mongodb 对数据库进行了一定程度的数据加密 性能改善 代码简化 统一错误模型和响应 使用 apifox 作为文档
This commit is contained in:
12
loveace/router/endpoint/isim/__init__.py
Normal file
12
loveace/router/endpoint/isim/__init__.py
Normal 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)
|
||||
74
loveace/router/endpoint/isim/elec.py
Normal file
74
loveace/router/endpoint/isim/elec.py
Normal 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
|
||||
)
|
||||
42
loveace/router/endpoint/isim/model/isim.py
Normal file
42
loveace/router/endpoint/isim/model/isim.py
Normal 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="充值记录")
|
||||
18
loveace/router/endpoint/isim/model/protect_router.py
Normal file
18
loveace/router/endpoint/isim/model/protect_router.py
Normal 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="房间缓存已过期,请稍后重新获取房间列表",
|
||||
)
|
||||
172
loveace/router/endpoint/isim/model/room.py
Normal file
172
loveace/router/endpoint/isim/model/room.py
Normal 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="数据更新时间")
|
||||
544
loveace/router/endpoint/isim/room.py
Normal file
544
loveace/router/endpoint/isim/room.py
Normal 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)
|
||||
1425
loveace/router/endpoint/isim/utils/isim.py
Normal file
1425
loveace/router/endpoint/isim/utils/isim.py
Normal file
File diff suppressed because it is too large
Load Diff
109
loveace/router/endpoint/isim/utils/lock_manager.py
Normal file
109
loveace/router/endpoint/isim/utils/lock_manager.py
Normal 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
|
||||
24
loveace/router/endpoint/isim/utils/room.py
Normal file
24
loveace/router/endpoint/isim/utils/room.py
Normal 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
|
||||
Reference in New Issue
Block a user