Files
H3ConuMS-v2/backend/app/api/v1/wechat.py
T
v6ole b2c20ec43d feat(alerts): 添加微信告警、WebSocket实时推送和地图功能
- 新增微信告警服务(wechat_service)和告警任务(alert_tasks)
- 新增 WebSocket 实时推送端点
- 新增监控管理模块(monitor)
- 增强统计仪表板:趋势图、区域分布、光功率历史
- 设备管理:添加坐标信息、标签系统、复合索引优化
- 前端:重构Dashboard/Charts页面,新增业务组件
- 新增5个数据库迁移(坐标、复合索引、标签、光功率历史、显示名)
- 更新部署配置和脚本
- 新增测试框架基础结构
2026-06-11 15:16:29 +08:00

241 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""企业微信回调 APIURL验证 + 消息接收)"""
import logging
from fastapi import APIRouter, Request, Response
from app.services.wechat_service import get_wechat_service
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"
"💻 完整功能: https://onu.dhdx.fun",
to_user=from_user
)