513 lines
19 KiB
Python
513 lines
19 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微IT智能服务台 — 会议室预定业务服务
|
|||
|
|
# =============================================================================
|
|||
|
|
# 说明:作为企微会议室API的薄代理层,提供:
|
|||
|
|
# 1. Redis缓存管理(减少企微API调用)
|
|||
|
|
# 2. 企微API代理(列表/预定状态/预定/取消/详情)
|
|||
|
|
# 3. 实时状态计算(综合判断空闲/使用中/即将开始)
|
|||
|
|
# 4. 状态变更后WebSocket通知终端
|
|||
|
|
#
|
|||
|
|
# 设计决策:
|
|||
|
|
# - 企微API为唯一数据源,不在本地维护预定数据
|
|||
|
|
# - 读操作加Redis短缓存(30秒),写操作直接转发企微API
|
|||
|
|
# - 预定/取消成功后立即清除缓存并推送WS通知
|
|||
|
|
# =============================================================================
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from typing import Any, Dict, List, Optional
|
|||
|
|
|
|||
|
|
import redis.asyncio as aioredis
|
|||
|
|
|
|||
|
|
from app.services.wecom_service import WecomService
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MeetingroomService:
|
|||
|
|
"""会议室预定业务服务 — 企微API代理 + Redis缓存。
|
|||
|
|
|
|||
|
|
所有预定数据以企微API为唯一数据源,本服务仅做代理+缓存+鉴权。
|
|||
|
|
terminal_room_binding 表仅存终端↔会议室映射关系,不存预定数据。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# Redis 缓存 key 前缀与 TTL
|
|||
|
|
CACHE_KEY_ROOM_LIST = "meetingroom:room_list" # 会议室列表 (TTL=600s)
|
|||
|
|
CACHE_KEY_BOOKING_INFO = "meetingroom:booking:{room_id}:{date}" # 预定状态 (TTL=30s)
|
|||
|
|
CACHE_KEY_BOOKING_DETAIL = "meetingroom:detail:{booking_id}" # 预定详情 (TTL=300s)
|
|||
|
|
CACHE_KEY_STATUS = "meetingroom:status:{room_id}" # 实时状态 (TTL=10s)
|
|||
|
|
|
|||
|
|
# 缓存 TTL(秒)
|
|||
|
|
TTL_ROOM_LIST = 600
|
|||
|
|
TTL_BOOKING_INFO = 30
|
|||
|
|
TTL_BOOKING_DETAIL = 300
|
|||
|
|
TTL_STATUS = 10
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
wecom_service: WecomService,
|
|||
|
|
redis_client: Optional[aioredis.Redis] = None,
|
|||
|
|
) -> None:
|
|||
|
|
"""初始化会议室预定服务。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
wecom_service: 企微API服务实例
|
|||
|
|
redis_client: Redis 异步客户端(可为 None)
|
|||
|
|
"""
|
|||
|
|
self.wecom = wecom_service
|
|||
|
|
self.redis = redis_client
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 会议室列表
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def get_room_list(
|
|||
|
|
self,
|
|||
|
|
city: Optional[str] = None,
|
|||
|
|
building: Optional[str] = None,
|
|||
|
|
floor: Optional[str] = None,
|
|||
|
|
) -> List[Dict[str, Any]]:
|
|||
|
|
"""获取会议室列表(缓存10分钟)。
|
|||
|
|
|
|||
|
|
代理调用企微API获取会议室列表,结果缓存到Redis。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
city: 城市名称(可选过滤)
|
|||
|
|
building: 楼宇名称(可选过滤)
|
|||
|
|
floor: 楼层名称(可选过滤)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
List[Dict[str, Any]]: 会议室列表
|
|||
|
|
"""
|
|||
|
|
# 构建缓存key(含过滤条件)
|
|||
|
|
filter_parts = []
|
|||
|
|
if city:
|
|||
|
|
filter_parts.append(f"city={city}")
|
|||
|
|
if building:
|
|||
|
|
filter_parts.append(f"building={building}")
|
|||
|
|
if floor:
|
|||
|
|
filter_parts.append(f"floor={floor}")
|
|||
|
|
filter_str = ":".join(filter_parts) if filter_parts else "all"
|
|||
|
|
cache_key = f"{self.CACHE_KEY_ROOM_LIST}:{filter_str}"
|
|||
|
|
|
|||
|
|
# 1. 尝试从缓存获取
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
cached = await self.redis.get(cache_key)
|
|||
|
|
if cached:
|
|||
|
|
logger.debug(f"从缓存获取会议室列表: filter={filter_str}")
|
|||
|
|
return json.loads(cached)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 读取会议室列表缓存失败: {e}")
|
|||
|
|
|
|||
|
|
# 2. 调用企微API
|
|||
|
|
try:
|
|||
|
|
room_list = await self.wecom.get_meetingroom_list(city, building, floor)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"获取会议室列表失败: {e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
# 3. 缓存到Redis
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
await self.redis.setex(cache_key, self.TTL_ROOM_LIST, json.dumps(room_list, ensure_ascii=False))
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 写入会议室列表缓存失败: {e}")
|
|||
|
|
|
|||
|
|
return room_list
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 预定状态查询
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def get_room_status(
|
|||
|
|
self,
|
|||
|
|
meetingroom_id: int,
|
|||
|
|
date: Optional[str] = None,
|
|||
|
|
) -> List[Dict[str, Any]]:
|
|||
|
|
"""获取指定日期的预定状态(缓存30秒)。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
meetingroom_id: 企微会议室ID
|
|||
|
|
date: 查询日期(YYYY-MM-DD格式,默认今天)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
List[Dict[str, Any]]: 预定记录列表
|
|||
|
|
"""
|
|||
|
|
# 默认今天
|
|||
|
|
if not date:
|
|||
|
|
date = datetime.now().strftime("%Y-%m-%d")
|
|||
|
|
|
|||
|
|
cache_key = self.CACHE_KEY_BOOKING_INFO.format(room_id=meetingroom_id, date=date)
|
|||
|
|
|
|||
|
|
# 1. 尝试从缓存获取
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
cached = await self.redis.get(cache_key)
|
|||
|
|
if cached:
|
|||
|
|
logger.debug(f"从缓存获取预定状态: room_id={meetingroom_id}, date={date}")
|
|||
|
|
return json.loads(cached)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 读取预定状态缓存失败: {e}")
|
|||
|
|
|
|||
|
|
# 2. 构建查询时间范围(当天 00:00 ~ 23:59,带时区)
|
|||
|
|
from app.config import settings
|
|||
|
|
tz_offset = "+08:00"
|
|||
|
|
start_time = f"{date}T00:00:00{tz_offset}"
|
|||
|
|
end_time = f"{date}T23:59:59{tz_offset}"
|
|||
|
|
|
|||
|
|
# 3. 调用企微API
|
|||
|
|
try:
|
|||
|
|
booking_list = await self.wecom.get_booking_info(meetingroom_id, start_time, end_time)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"获取预定状态失败: room_id={meetingroom_id}, date={date}, error={e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
# 4. 缓存到Redis
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
await self.redis.setex(cache_key, self.TTL_BOOKING_INFO, json.dumps(booking_list, ensure_ascii=False))
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 写入预定状态缓存失败: {e}")
|
|||
|
|
|
|||
|
|
return booking_list
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 实时状态计算
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def get_current_status(self, meetingroom_id: int) -> Dict[str, Any]:
|
|||
|
|
"""获取当前实时状态(综合判断空闲/使用中/即将开始)。
|
|||
|
|
|
|||
|
|
缓存10秒(极短缓存防刷),过期后重新计算。
|
|||
|
|
|
|||
|
|
状态判断逻辑:
|
|||
|
|
- busy: 当前时间在某个预定的 start_time ~ end_time 之间
|
|||
|
|
- starting_soon: 当前时间距下一个预定开始时间 ≤ 15分钟
|
|||
|
|
- free: 其他情况
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
meetingroom_id: 企微会议室ID
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Dict[str, Any]: 包含 status/current_meeting/next_meeting/minutes_to_next/bookings
|
|||
|
|
"""
|
|||
|
|
cache_key = self.CACHE_KEY_STATUS.format(room_id=meetingroom_id)
|
|||
|
|
|
|||
|
|
# 1. 尝试从缓存获取
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
cached = await self.redis.get(cache_key)
|
|||
|
|
if cached:
|
|||
|
|
logger.debug(f"从缓存获取实时状态: room_id={meetingroom_id}")
|
|||
|
|
return json.loads(cached)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 读取实时状态缓存失败: {e}")
|
|||
|
|
|
|||
|
|
# 2. 获取当日预定列表
|
|||
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|||
|
|
bookings = await self.get_room_status(meetingroom_id, today)
|
|||
|
|
|
|||
|
|
# 3. 计算当前状态
|
|||
|
|
now = datetime.now()
|
|||
|
|
current_meeting: Optional[Dict[str, Any]] = None
|
|||
|
|
next_meeting: Optional[Dict[str, Any]] = None
|
|||
|
|
status = "free"
|
|||
|
|
minutes_to_next: Optional[int] = None
|
|||
|
|
|
|||
|
|
# 遍历预定记录,查找当前进行中的会议和下一个会议
|
|||
|
|
active_bookings = []
|
|||
|
|
for booking in bookings:
|
|||
|
|
# 跳过已取消的预定
|
|||
|
|
if booking.get("status", 0) != 0:
|
|||
|
|
continue
|
|||
|
|
# 解析时间(企微返回的时间戳或ISO字符串)
|
|||
|
|
start_time = self._parse_booking_time(booking.get("start_time"))
|
|||
|
|
end_time = self._parse_booking_time(booking.get("end_time"))
|
|||
|
|
if not start_time or not end_time:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
active_bookings.append(booking)
|
|||
|
|
|
|||
|
|
# 检查是否当前进行中
|
|||
|
|
if start_time <= now <= end_time:
|
|||
|
|
current_meeting = self._format_booking(booking)
|
|||
|
|
status = "busy"
|
|||
|
|
# 检查是否是下一个即将开始的
|
|||
|
|
elif start_time > now:
|
|||
|
|
if next_meeting is None or start_time < self._parse_booking_time(next_meeting.get("start_time")):
|
|||
|
|
next_meeting = self._format_booking(booking)
|
|||
|
|
minutes_to_next = int((start_time - now).total_seconds() / 60)
|
|||
|
|
|
|||
|
|
# 如果当前没有进行中的会议,但下一个会议在15分钟内开始
|
|||
|
|
if status == "free" and next_meeting and minutes_to_next is not None and minutes_to_next <= 15:
|
|||
|
|
status = "starting_soon"
|
|||
|
|
|
|||
|
|
result: Dict[str, Any] = {
|
|||
|
|
"status": status,
|
|||
|
|
"current_meeting": current_meeting,
|
|||
|
|
"next_meeting": next_meeting,
|
|||
|
|
"minutes_to_next": minutes_to_next,
|
|||
|
|
"bookings": [self._format_booking(b) for b in active_bookings],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 4. 缓存到Redis(极短TTL)
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
await self.redis.setex(cache_key, self.TTL_STATUS, json.dumps(result, ensure_ascii=False))
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 写入实时状态缓存失败: {e}")
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 预定操作
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def book_room(
|
|||
|
|
self,
|
|||
|
|
meetingroom_id: int,
|
|||
|
|
subject: str,
|
|||
|
|
start_time: str,
|
|||
|
|
end_time: str,
|
|||
|
|
booker: str,
|
|||
|
|
attendees: Optional[List[str]] = None,
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""预定会议室(成功后清除该会议室的预定状态缓存)。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
meetingroom_id: 企微会议室ID
|
|||
|
|
subject: 会议主题
|
|||
|
|
start_time: 开始时间(ISO 8601)
|
|||
|
|
end_time: 结束时间(ISO 8601)
|
|||
|
|
booker: 预定人userid
|
|||
|
|
attendees: 参与人userid列表(可选)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Dict[str, Any]: 预定结果,含 booking_id
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
Exception: 预定失败
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
result = await self.wecom.book_meetingroom(
|
|||
|
|
meetingroom_id=meetingroom_id,
|
|||
|
|
subject=subject,
|
|||
|
|
start_time=start_time,
|
|||
|
|
end_time=end_time,
|
|||
|
|
booker=booker,
|
|||
|
|
attendees=attendees,
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"预定会议室失败: room_id={meetingroom_id}, error={e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
# 预定成功后清除缓存
|
|||
|
|
await self.invalidate_room_cache(meetingroom_id)
|
|||
|
|
|
|||
|
|
logger.info(f"预定成功: room_id={meetingroom_id}, booking_id={result.get('booking_id')}")
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 取消预定
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def cancel_booking(self, booking_id: str, meetingroom_id: int) -> Dict[str, Any]:
|
|||
|
|
"""取消预定(成功后清除缓存)。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
booking_id: 企微预定ID
|
|||
|
|
meetingroom_id: 企微会议室ID
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Dict[str, Any]: 取消结果
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
Exception: 取消失败
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
result = await self.wecom.cancel_booking(booking_id, meetingroom_id)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"取消预定失败: booking_id={booking_id}, error={e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
# 取消成功后清除缓存
|
|||
|
|
await self.invalidate_room_cache(meetingroom_id)
|
|||
|
|
|
|||
|
|
logger.info(f"取消成功: booking_id={booking_id}, room_id={meetingroom_id}")
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 预定详情
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def get_booking_detail(self, meetingroom_id: int, booking_id: str) -> Dict[str, Any]:
|
|||
|
|
"""获取预定详情(缓存5分钟)。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
meetingroom_id: 企微会议室ID
|
|||
|
|
booking_id: 企微预定ID
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Dict[str, Any]: 预定详情
|
|||
|
|
"""
|
|||
|
|
cache_key = self.CACHE_KEY_BOOKING_DETAIL.format(booking_id=booking_id)
|
|||
|
|
|
|||
|
|
# 1. 尝试从缓存获取
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
cached = await self.redis.get(cache_key)
|
|||
|
|
if cached:
|
|||
|
|
logger.debug(f"从缓存获取预定详情: booking_id={booking_id}")
|
|||
|
|
return json.loads(cached)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 读取预定详情缓存失败: {e}")
|
|||
|
|
|
|||
|
|
# 2. 调用企微API
|
|||
|
|
try:
|
|||
|
|
detail = await self.wecom.get_booking_detail(meetingroom_id, booking_id)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"获取预定详情失败: booking_id={booking_id}, error={e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
# 3. 缓存到Redis
|
|||
|
|
if self.redis:
|
|||
|
|
try:
|
|||
|
|
await self.redis.setex(cache_key, self.TTL_BOOKING_DETAIL, json.dumps(detail, ensure_ascii=False))
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"Redis 写入预定详情缓存失败: {e}")
|
|||
|
|
|
|||
|
|
return detail
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 缓存管理
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def invalidate_room_cache(
|
|||
|
|
self,
|
|||
|
|
meetingroom_id: int,
|
|||
|
|
date: Optional[str] = None,
|
|||
|
|
) -> None:
|
|||
|
|
"""清除指定会议室的缓存(预定/取消后调用)。
|
|||
|
|
|
|||
|
|
清除以下缓存:
|
|||
|
|
- meetingroom:booking:{room_id}:{date} — 预定状态
|
|||
|
|
- meetingroom:status:{room_id} — 实时状态
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
meetingroom_id: 企微会议室ID
|
|||
|
|
date: 日期(默认今天)
|
|||
|
|
"""
|
|||
|
|
if not self.redis:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if not date:
|
|||
|
|
date = datetime.now().strftime("%Y-%m-%d")
|
|||
|
|
|
|||
|
|
keys_to_delete = [
|
|||
|
|
self.CACHE_KEY_BOOKING_INFO.format(room_id=meetingroom_id, date=date),
|
|||
|
|
self.CACHE_KEY_STATUS.format(room_id=meetingroom_id),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
for key in keys_to_delete:
|
|||
|
|
await self.redis.delete(key)
|
|||
|
|
logger.info(f"已清除会议室缓存: room_id={meetingroom_id}, date={date}")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"清除会议室缓存失败: {e}")
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# WebSocket 通知
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
async def notify_terminal_update(
|
|||
|
|
self,
|
|||
|
|
terminal_sn: str,
|
|||
|
|
meetingroom_id: int,
|
|||
|
|
) -> None:
|
|||
|
|
"""状态变更后通过WebSocket通知终端。
|
|||
|
|
|
|||
|
|
获取最新状态后,通过ConnectionManager推送给绑定的终端。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
terminal_sn: 终端序列号
|
|||
|
|
meetingroom_id: 企微会议室ID
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
from app.services.ws_manager import manager as ws_manager
|
|||
|
|
|
|||
|
|
# 获取最新状态
|
|||
|
|
status_data = await self.get_current_status(meetingroom_id)
|
|||
|
|
|
|||
|
|
# 构建推送消息
|
|||
|
|
message = {
|
|||
|
|
"type": "room_status_update",
|
|||
|
|
"data": {
|
|||
|
|
"meetingroom_id": meetingroom_id,
|
|||
|
|
"status": status_data.get("status"),
|
|||
|
|
"current_meeting": status_data.get("current_meeting"),
|
|||
|
|
"next_meeting": status_data.get("next_meeting"),
|
|||
|
|
"minutes_to_next": status_data.get("minutes_to_next"),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 推送给终端
|
|||
|
|
await ws_manager.send_to_terminal(terminal_sn, message)
|
|||
|
|
logger.info(f"已推送状态更新到终端: sn={terminal_sn}, room_id={meetingroom_id}")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"推送终端状态更新失败: sn={terminal_sn}, error={e}")
|
|||
|
|
|
|||
|
|
# ==========================================================================
|
|||
|
|
# 辅助方法
|
|||
|
|
# ==========================================================================
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _parse_booking_time(time_value: Any) -> Optional[datetime]:
|
|||
|
|
"""解析预定时间(支持时间戳和ISO字符串两种格式)。
|
|||
|
|
|
|||
|
|
企微API返回的时间可能是:
|
|||
|
|
- 整数时间戳(秒)
|
|||
|
|
- ISO 8601 字符串
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
time_value: 时间值
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Optional[datetime]: 解析后的datetime对象
|
|||
|
|
"""
|
|||
|
|
if not time_value:
|
|||
|
|
return None
|
|||
|
|
try:
|
|||
|
|
if isinstance(time_value, (int, float)):
|
|||
|
|
return datetime.fromtimestamp(time_value)
|
|||
|
|
if isinstance(time_value, str):
|
|||
|
|
# 尝试解析ISO格式
|
|||
|
|
if "T" in time_value:
|
|||
|
|
return datetime.fromisoformat(time_value)
|
|||
|
|
# 尝试解析时间戳字符串
|
|||
|
|
return datetime.fromtimestamp(int(time_value))
|
|||
|
|
except (ValueError, TypeError) as e:
|
|||
|
|
logger.warning(f"解析预定时间失败: value={time_value}, error={e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _format_booking(booking: Dict[str, Any]) -> Dict[str, Any]:
|
|||
|
|
"""格式化预定记录,统一时间格式为ISO字符串。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
booking: 原始预定记录
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Dict[str, Any]: 格式化后的预定记录
|
|||
|
|
"""
|
|||
|
|
result = dict(booking)
|
|||
|
|
# 转换时间戳为ISO字符串
|
|||
|
|
for field in ("start_time", "end_time"):
|
|||
|
|
value = booking.get(field)
|
|||
|
|
dt = MeetingroomService._parse_booking_time(value)
|
|||
|
|
if dt:
|
|||
|
|
result[field] = dt.isoformat()
|
|||
|
|
return result
|