"""企业微信回调 API(URL验证 + 消息接收)""" import logging from fastapi import APIRouter, Request, Response from app.services.wechat_service import get_wechat_service from app.core.config import settings logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/wechat", tags=["企业微信回调"]) @router.post("/menu/create") async def create_menu(): """创建/更新企业微信应用菜单""" svc = get_wechat_service() menu = { "button": [ { "name": "设备查询", "sub_button": [ {"type": "click", "name": "在线统计", "key": "online"}, {"type": "click", "name": "全离线学校", "key": "offline_schools"}, {"type": "click", "name": "MAC查询", "key": "status"}, ] }, {"type": "click", "name": "帮助", "key": "help"}, ] } ok = svc.create_menu(menu) return {"success": ok} @router.get("/callback") async def wechat_callback_get(request: Request): """企业微信 URL 验证(GET)""" params = request.query_params msg_signature = params.get("msg_signature", "") timestamp = params.get("timestamp", "") nonce = params.get("nonce", "") echostr = params.get("echostr", "") svc = get_wechat_service() result = svc.verify_url(msg_signature, timestamp, nonce, echostr) if result: return Response(content=result, media_type="text/plain") return Response(content="验证失败", status_code=403) @router.post("/callback") async def wechat_callback_post(request: Request): """企业微信消息接收(POST)""" params = request.query_params msg_signature = params.get("msg_signature", "") timestamp = params.get("timestamp", "") nonce = params.get("nonce", "") xml_data = await request.body() if not xml_data: return Response(content="", media_type="text/plain") svc = get_wechat_service() msg = svc.parse_message(xml_data) if not msg: return Response(content="", media_type="text/plain") msg_type = msg.get("MsgType", "") from_user = msg.get("FromUserName", "") logger.info(f"收到企微消息: type={msg_type}, from={from_user}, content={msg.get('Content', '')}") if msg_type == "text": content = msg.get("Content", "").strip() if content.lower() in ("online", "在线", "在线统计"): _handle_online_cmd(svc, from_user) elif content.lower() in ("全离线", "离线学校", "offline"): _handle_offline_schools_cmd(svc, from_user) elif content.startswith("#状态+") or content.startswith("#status+"): _handle_status_cmd(svc, from_user, content) elif content.lower() in ("help", "帮助", "#帮助", "#help"): _handle_help_cmd(svc, from_user) else: # 尝试作为 MAC 后缀查询 _handle_status_cmd(svc, from_user, f"#状态+{content}") elif msg_type == "event": event = msg.get("Event", "") event_key = msg.get("EventKey", "") if event == "click": if event_key == "online": _handle_online_cmd(svc, from_user) elif event_key == "offline_schools": _handle_offline_schools_cmd(svc, from_user) elif event_key == "help": _handle_help_cmd(svc, from_user) return Response(content="", media_type="text/plain") # ── 命令处理 ──────────────────────────────────────────────────────────────── def _handle_online_cmd(svc, from_user: str): """处理在线统计命令""" try: from app.core.database import SessionLocal from app.models.device import ONUDevice, DeviceStatusHistory from sqlalchemy import func, case db = SessionLocal() try: latest_subq = ( db.query(DeviceStatusHistory.onu_device_id, func.max(DeviceStatusHistory.checked_at).label("max_checked_at")) .group_by(DeviceStatusHistory.onu_device_id).subquery() ) latest_status_subq = ( db.query(DeviceStatusHistory.onu_device_id, DeviceStatusHistory.status) .join(latest_subq, (DeviceStatusHistory.onu_device_id == latest_subq.c.onu_device_id) & (DeviceStatusHistory.checked_at == latest_subq.c.max_checked_at)).subquery() ) total = db.query(func.count(ONUDevice.id)).scalar() or 0 online = ( db.query(func.count(ONUDevice.id)) .join(latest_status_subq, ONUDevice.id == latest_status_subq.c.onu_device_id, isouter=True) .filter(latest_status_subq.c.status == 'online').scalar() or 0 ) rate = (online / total * 100) if total > 0 else 0 svc.send_text_message( f"📊 设备在线统计\n总设备数: {total}\n在线: {online}\n离线: {total - online}\n在线率: {rate:.1f}%", to_user=from_user ) finally: db.close() except Exception as e: logger.error(f"在线统计失败: {e}") svc.send_text_message("查询失败,请稍后重试", to_user=from_user) def _handle_status_cmd(svc, from_user: str, content: str): """处理设备状态查询命令""" try: mac_suffix = content.split('+')[1].strip().upper() from app.core.database import SessionLocal from app.models.device import ONUDevice, DeviceStatusHistory from sqlalchemy import func db = SessionLocal() try: devices = db.query(ONUDevice).filter( ONUDevice.mac_address.ilike(f"%{mac_suffix}") ).limit(10).all() if not devices: svc.send_text_message("未找到匹配的设备", to_user=from_user) return lines = [f"🔍 找到 {len(devices)} 个设备(MAC 含 {mac_suffix}):", ""] for d in devices[:8]: # 查最新状态 latest = ( db.query(DeviceStatusHistory.status, func.max(DeviceStatusHistory.checked_at)) .filter(DeviceStatusHistory.onu_device_id == d.id) .group_by(DeviceStatusHistory.status) .order_by(func.max(DeviceStatusHistory.checked_at).desc()) .first() ) status_text = latest[0] if latest else "未知" emoji = "🟢" if status_text == "online" else "🔴" school = d.school_name or "未知" lines.append(f"{emoji} {d.mac_address} | {school} | {d.region or ''}") svc.send_text_message("\n".join(lines), to_user=from_user) finally: db.close() except Exception as e: logger.error(f"设备查询失败: {e}") svc.send_text_message("查询失败,请稍后重试", to_user=from_user) def _handle_offline_schools_cmd(svc, from_user: str): """查询全离线学校""" try: from app.core.database import SessionLocal from app.models.device import ONUDevice, DeviceStatusHistory from sqlalchemy import func, case db = SessionLocal() try: latest_subq = ( db.query(DeviceStatusHistory.onu_device_id, func.max(DeviceStatusHistory.checked_at).label("max_checked_at")) .group_by(DeviceStatusHistory.onu_device_id).subquery() ) latest_status_subq = ( db.query(DeviceStatusHistory.onu_device_id, DeviceStatusHistory.status) .join(latest_subq, (DeviceStatusHistory.onu_device_id == latest_subq.c.onu_device_id) & (DeviceStatusHistory.checked_at == latest_subq.c.max_checked_at)).subquery() ) rows = ( db.query( ONUDevice.school_name, ONUDevice.region, func.count().label("total"), func.sum(case((latest_status_subq.c.status == 'online', 1), else_=0)).label("online"), ) .outerjoin(latest_status_subq, ONUDevice.id == latest_status_subq.c.onu_device_id) .group_by(ONUDevice.school_name, ONUDevice.region) .having(func.sum(case((latest_status_subq.c.status == 'online', 1), else_=0)) == 0) .all() ) if not rows: svc.send_text_message("✅ 当前没有全离线的学校", to_user=from_user) return lines = [f"🔴 全离线学校 ({len(rows)} 所):", ""] for r in rows: school = r.school_name or "未知" region = r.region or "未知" total = int(r.total or 0) if total > 0: lines.append(f"• {school}({region}): {total}台全离线") svc.send_text_message("\n".join(lines), to_user=from_user) finally: db.close() except Exception as e: logger.error(f"全离线查询失败: {e}") svc.send_text_message("查询失败,请稍后重试", to_user=from_user) def _handle_help_cmd(svc, from_user: str): """处理帮助命令""" svc.send_text_message( "📋 H3C ONU 管理助手\n\n" "🔍 设备查询:\n" "• 发送「在线」查看设备在线统计\n" "• 发送「全离线」查看全离线学校\n" "• 发送 MAC 地址后四位查询设备\n\n" "💡 发送「帮助」显示此信息\n" f"💻 完整功能: {settings.FRONTEND_URL}", to_user=from_user )