545 lines
20 KiB
Python
545 lines
20 KiB
Python
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)
|