449c6d4875
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS
## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS
## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)
## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code
## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)
## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过
## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务
## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记✅已实施
- 新增架构图/时序图/类图(mermaid)
## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
560 lines
20 KiB
Python
560 lines
20 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
|
||
|
||
# ==========================================================================
|
||
# 操作指南查询
|
||
# ==========================================================================
|
||
|
||
@staticmethod
|
||
async def get_guides(
|
||
db,
|
||
category: Optional[str] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
"""获取操作指南列表。
|
||
|
||
从数据库查询启用的操作指南,按 sort_order 排序。
|
||
可按设备类型过滤。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
category: 设备类型过滤(可选)
|
||
|
||
Returns:
|
||
List[Dict[str, Any]]: 指南列表
|
||
"""
|
||
from sqlalchemy import select as sa_select
|
||
from app.models.meetingroom_guide import MeetingroomGuide
|
||
|
||
stmt = (
|
||
sa_select(MeetingroomGuide)
|
||
.where(MeetingroomGuide.is_active == True) # noqa: E712
|
||
.order_by(MeetingroomGuide.sort_order, MeetingroomGuide.id)
|
||
)
|
||
if category:
|
||
stmt = stmt.where(MeetingroomGuide.category == category)
|
||
|
||
result = await db.execute(stmt)
|
||
guides = result.scalars().all()
|
||
|
||
return [
|
||
{
|
||
"id": g.id,
|
||
"category": g.category,
|
||
"title": g.title,
|
||
"brief": g.brief,
|
||
"detail_url": g.detail_url,
|
||
"icon": g.icon,
|
||
}
|
||
for g in guides
|
||
]
|