init: 管理会计OS初始代码
包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
预警通知推送模块 — 管理会计OS
|
||||
支持渠道:企业微信 (群机器人/应用消息)、邮件
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger("cma.notifier")
|
||||
|
||||
# ============================================================
|
||||
# 企业微信机器人推送
|
||||
# ============================================================
|
||||
|
||||
def send_wecom_robot(webhook_url: str, title: str, content: str, alert_level: str = "yellow") -> dict:
|
||||
"""通过企业微信群机器人发送告警"""
|
||||
color_tag = {"red": "🔴", "yellow": "🟡", "green": "🟢"}.get(alert_level, "⚪")
|
||||
msg = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": f"## {color_tag} 管理会计OS预警通知\n"
|
||||
f"**{title}**\n\n"
|
||||
f"{content}\n\n"
|
||||
f"---\n"
|
||||
f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
||||
}
|
||||
}
|
||||
data = json.dumps(msg).encode("utf-8")
|
||||
req = urllib.request.Request(webhook_url, data=data,
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
result = json.loads(resp.read().decode())
|
||||
if result.get("errcode") == 0:
|
||||
return {"success": True, "message": "已推送至企业微信群"}
|
||||
else:
|
||||
return {"success": False, "message": f"推送失败: {result.get('errmsg', '未知错误')}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"推送异常: {str(e)}"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 企业微信应用消息推送(通过自建应用 message/send API)
|
||||
# ============================================================
|
||||
|
||||
def send_wecom_app(corp_id: str, corp_secret: str, agent_id: str,
|
||||
touser: str, title: str, content: str, alert_level: str = "yellow") -> dict:
|
||||
"""通过企微自建应用发送应用消息"""
|
||||
import requests
|
||||
try:
|
||||
token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corp_id}&corpsecret={corp_secret}"
|
||||
r = requests.get(token_url, timeout=10)
|
||||
token_data = r.json()
|
||||
if token_data.get("errcode") != 0:
|
||||
return {"success": False, "message": f"获取token失败: {token_data.get('errmsg', '')}"}
|
||||
access_token = token_data["access_token"]
|
||||
|
||||
color_tag = {"red": "🔴", "yellow": "🟡", "green": "🟢"}.get(alert_level, "⚪")
|
||||
md = "## " + color_tag + " 管理会计OS预警通知\n\n"
|
||||
md += "**" + title + "**\n\n"
|
||||
md += content + "\n\n---\n"
|
||||
md += "⏰ " + datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
payload = {
|
||||
"touser": touser,
|
||||
"msgtype": "markdown",
|
||||
"agentid": int(agent_id),
|
||||
"markdown": {"content": md},
|
||||
"safe": 0,
|
||||
}
|
||||
send_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + access_token
|
||||
r2 = requests.post(send_url, json=payload, timeout=10)
|
||||
send_data = r2.json()
|
||||
if send_data.get("errcode") == 0:
|
||||
return {"success": True, "message": f"已推送到企微用户 {touser}"}
|
||||
else:
|
||||
return {"success": False, "message": f"推送失败: {send_data.get('errmsg', '')}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"推送异常: {str(e)}"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 邮件推送
|
||||
# ============================================================
|
||||
|
||||
def send_mail(smtp_config: dict, to_addrs: list, title: str, content: str) -> dict:
|
||||
"""通过 SMTP 发送邮件告警"""
|
||||
try:
|
||||
msg = MIMEText(content, "plain", "utf-8")
|
||||
msg["Subject"] = Header(f"[管理会计OS预警] {title}", "utf-8")
|
||||
msg["From"] = smtp_config.get("from_addr", "")
|
||||
msg["To"] = ", ".join(to_addrs)
|
||||
|
||||
host = smtp_config.get("host", "smtp.qq.com")
|
||||
port = int(smtp_config.get("port", 465))
|
||||
user = smtp_config.get("user", "")
|
||||
password = smtp_config.get("password", "")
|
||||
use_ssl = smtp_config.get("use_ssl", True)
|
||||
|
||||
if use_ssl:
|
||||
server = smtplib.SMTP_SSL(host, port, timeout=10)
|
||||
else:
|
||||
server = smtplib.SMTP(host, port, timeout=10)
|
||||
server.starttls()
|
||||
|
||||
server.login(user, password)
|
||||
server.sendmail(user, to_addrs, msg.as_string())
|
||||
server.quit()
|
||||
return {"success": True, "message": f"已发送邮件至 {', '.join(to_addrs)}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"邮件发送失败: {str(e)}"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主推送函数
|
||||
# ============================================================
|
||||
|
||||
def push_alert(alert: dict, channels: list[dict]) -> list[dict]:
|
||||
"""向所有已启用渠道推送一条预警"""
|
||||
results = []
|
||||
alert_level = alert.get("alert_level", "yellow")
|
||||
title = alert.get("alert_message", "预警通知")
|
||||
content = _build_content(alert)
|
||||
|
||||
for ch in channels:
|
||||
if not ch.get("enabled", True):
|
||||
continue
|
||||
|
||||
ch_type = ch.get("channel_type", "")
|
||||
config = ch.get("config", {})
|
||||
result = {"channel": ch_type, "channel_name": ch.get("name", ""), "success": False}
|
||||
|
||||
if ch_type == "wecom":
|
||||
webhook = config.get("webhook_url", "")
|
||||
if webhook:
|
||||
result = send_wecom_robot(webhook, title, content, alert_level)
|
||||
result["channel"] = "wecom"
|
||||
|
||||
elif ch_type == "wecom_app":
|
||||
result = send_wecom_app(
|
||||
corp_id=config.get("corp_id", ""),
|
||||
corp_secret=config.get("corp_secret", ""),
|
||||
agent_id=config.get("agent_id", ""),
|
||||
touser=config.get("touser", ""),
|
||||
title=title, content=content, alert_level=alert_level,
|
||||
)
|
||||
result["channel"] = "wecom_app"
|
||||
|
||||
elif ch_type == "mail":
|
||||
to_list = config.get("to", [])
|
||||
if to_list:
|
||||
result = send_mail(config, to_list, title, content)
|
||||
result["channel"] = "mail"
|
||||
|
||||
results.append({**result, "channel_name": ch.get("name", "")})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _build_content(alert: dict) -> str:
|
||||
"""构建预警详情内容"""
|
||||
parts = [
|
||||
f"KPI: {alert.get('kpi_name', '未知')}",
|
||||
f"期间: {alert.get('period', '')}",
|
||||
f"实际值: {alert.get('actual_value', '-')}",
|
||||
f"目标值: {alert.get('target_value', '-')}",
|
||||
f"预警级别: {'🔴 紧急' if alert.get('alert_level') == 'red' else '🟡 警告'}",
|
||||
]
|
||||
if alert.get("resolution"):
|
||||
parts.append(f"处理建议: {alert['resolution']}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 从数据库加载渠道配置并推送待处理预警
|
||||
# ============================================================
|
||||
|
||||
def push_pending_alerts(db_session) -> int:
|
||||
"""推送所有待处理预警"""
|
||||
from app.models import NotificationChannel, NotificationLog, KPIAlert, KPIDefinition, KPIValue
|
||||
|
||||
# 加载已启用的通知渠道
|
||||
channels = db_session.query(NotificationChannel).filter(
|
||||
NotificationChannel.enabled == True
|
||||
).all()
|
||||
|
||||
if not channels:
|
||||
logger.info("无已启用的通知渠道,跳过推送")
|
||||
return 0
|
||||
|
||||
# 查待处理的预警
|
||||
alerts = db_session.query(KPIAlert).filter(
|
||||
KPIAlert.status == "pending"
|
||||
).all()
|
||||
|
||||
if not alerts:
|
||||
logger.info("无待处理预警")
|
||||
return 0
|
||||
|
||||
channel_configs = [json.loads(json.dumps({
|
||||
"name": c.name, "channel_type": c.channel_type,
|
||||
"config": c.config, "enabled": c.enabled
|
||||
})) for c in channels]
|
||||
|
||||
pushed = 0
|
||||
for alert in alerts:
|
||||
# 获取预警详情
|
||||
kpi = db_session.query(KPIDefinition).filter(
|
||||
KPIDefinition.id == alert.kpi_id
|
||||
).first()
|
||||
kpi_value = db_session.query(KPIValue).filter(
|
||||
KPIValue.id == alert.kpi_value_id
|
||||
).first()
|
||||
|
||||
alert_data = {
|
||||
"alert_level": alert.alert_level,
|
||||
"alert_message": alert.alert_message,
|
||||
"kpi_name": kpi.kpi_name if kpi else "未知",
|
||||
"period": kpi_value.period if kpi_value else "",
|
||||
"actual_value": kpi_value.actual_value if kpi_value else None,
|
||||
"target_value": kpi.target_value if kpi else None,
|
||||
}
|
||||
|
||||
results = push_alert(alert_data, channel_configs)
|
||||
|
||||
# 记录推送日志
|
||||
for r in results:
|
||||
log = NotificationLog(
|
||||
alert_id=alert.id,
|
||||
channel=r.get("channel", ""),
|
||||
recipient=r.get("channel_name", ""),
|
||||
title=alert.alert_message[:200],
|
||||
content=alert.alert_message,
|
||||
status="sent" if r.get("success") else "failed",
|
||||
error_msg=r.get("message") if not r.get("success") else None,
|
||||
sent_at=datetime.now(),
|
||||
)
|
||||
db_session.add(log)
|
||||
if r.get("success"):
|
||||
pushed += 1
|
||||
logger.info(f" 已推送预警 #{alert.id} -> {r.get('channel_name')}")
|
||||
|
||||
db_session.commit()
|
||||
return pushed
|
||||
Reference in New Issue
Block a user