包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
202 lines
7.0 KiB
Python
202 lines
7.0 KiB
Python
"""
|
||
定时AI经营简报 — 管理会计OS
|
||
每天凌晨自动生成经营分析报告,推送至企微
|
||
在 daily_sync.py 之后运行
|
||
"""
|
||
import sys
|
||
import os
|
||
import json
|
||
import logging
|
||
from datetime import datetime
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
from dotenv import load_dotenv
|
||
load_dotenv('/root/cma-management/backend/.env')
|
||
|
||
from app.database import get_session_local
|
||
from app.models import KPIDefinition, KPIValue, KPIAlert, ActionPlan
|
||
import httpx
|
||
|
||
logger = logging.getLogger("cma.ai_brief")
|
||
|
||
|
||
async def call_deepseek(prompt: str, system_prompt: str = None) -> str:
|
||
"""调用DeepSeek API生成分析内容"""
|
||
api_key = os.getenv("DEEPSEEK_API_KEY", "")
|
||
api_url = "https://api.deepseek.com/v1/chat/completions"
|
||
|
||
if not system_prompt:
|
||
system_prompt = "你是一名CMA管理会计师,擅长用数据驱动的方式分析企业经营状况,给出专业的财务分析和管理建议。回答要简洁、专业、有数据支撑。"
|
||
|
||
if not api_key:
|
||
logger.warning("DEEPSEEK_API_KEY 未配置,跳过AI调用")
|
||
return "(AI简报暂不可用:API Key未配置)"
|
||
|
||
async with httpx.AsyncClient(timeout=60) as client:
|
||
resp = await client.post(
|
||
api_url,
|
||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||
json={
|
||
"model": "deepseek-chat",
|
||
"messages": [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
"stream": False,
|
||
"temperature": 0.3,
|
||
}
|
||
)
|
||
data = resp.json()
|
||
if resp.status_code != 200:
|
||
logger.error(f"DeepSeek API错误: {resp.status_code} {data}")
|
||
return ""
|
||
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||
|
||
|
||
def generate_brief(db_session) -> dict:
|
||
"""生成经营简报"""
|
||
period = datetime.now().strftime("%Y-%m")
|
||
|
||
kpis = db_session.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||
kpi_lines = []
|
||
for k in kpis:
|
||
latest = db_session.query(KPIValue).filter(
|
||
KPIValue.kpi_id == k.id,
|
||
).order_by(KPIValue.period.desc()).first()
|
||
|
||
prev_month = f"{int(period[:4])}-{int(period[5:7])-1:02d}" if int(period[5:7]) > 1 else f"{int(period[:4])-1}-12"
|
||
prev = db_session.query(KPIValue).filter(
|
||
KPIValue.kpi_id == k.id, KPIValue.period == prev_month
|
||
).first()
|
||
|
||
alert = db_session.query(KPIAlert).filter(
|
||
KPIAlert.kpi_id == k.id, KPIAlert.status == "pending"
|
||
).first()
|
||
|
||
if latest and latest.actual_value is not None:
|
||
line = f"- {k.kpi_name}({k.kpi_code}): {latest.actual_value}{k.unit or ''}"
|
||
if k.target_value:
|
||
line += f" | 目标: {k.target_value}"
|
||
if prev and prev.actual_value:
|
||
diff = latest.actual_value - prev.actual_value
|
||
direction = "↑" if diff > 0 else "↓"
|
||
line += f" | 环比: {direction}{abs(diff):.1f}"
|
||
if alert:
|
||
line += f" | ⚠️ {alert.alert_level}预警"
|
||
kpi_lines.append(line)
|
||
|
||
plans = db_session.query(ActionPlan).order_by(ActionPlan.created_at.desc()).all()
|
||
plan_lines = []
|
||
for p in plans:
|
||
plan_lines.append(f"- {p.title} | 负责人: {p.assignee} | 状态: {p.status} | 进度: {p.progress}%")
|
||
|
||
kpi_text = "\n".join(kpi_lines) if kpi_lines else "暂无KPI数据"
|
||
plan_text = "\n".join(plan_lines) if plan_lines else "暂无行动计划"
|
||
|
||
red_count = db_session.query(KPIAlert).filter(
|
||
KPIAlert.status == "pending", KPIAlert.alert_level == "red"
|
||
).count()
|
||
yellow_count = db_session.query(KPIAlert).filter(
|
||
KPIAlert.status == "pending", KPIAlert.alert_level == "yellow"
|
||
).count()
|
||
|
||
today_str = datetime.now().strftime('%Y-%m-%d')
|
||
prompt = f"""请为管理层生成一份今日经营简报(日期:{today_str})。
|
||
|
||
## 本月KPI数据
|
||
{kpi_text}
|
||
|
||
## 待处理预警
|
||
- 红色(紧急): {red_count}条
|
||
- 黄色(预警): {yellow_count}条
|
||
|
||
## 正在执行的改善行动
|
||
{plan_text}
|
||
|
||
请按以下结构生成简报(不超过800字):
|
||
1. 📊 **经营概览**:一句话总结本月经营状况
|
||
2. 🔍 **关键发现**:最重要的3个发现(数据驱动)
|
||
3. ⚠️ **预警聚焦**:最需要关注的预警及其影响
|
||
4. ✅ **行动进展**:改善计划执行情况
|
||
5. 💡 **今日建议**:今天最应该做的1-2件事"""
|
||
|
||
try:
|
||
import asyncio
|
||
analysis = asyncio.run(call_deepseek(prompt))
|
||
except Exception as e:
|
||
logger.error(f"AI简报生成异常: {e}")
|
||
analysis = f"简报生成异常: {str(e)}"
|
||
|
||
return {
|
||
"period": period,
|
||
"brief": analysis,
|
||
"kpi_count": len(kpi_lines),
|
||
"red_alerts": red_count,
|
||
"yellow_alerts": yellow_count,
|
||
"plan_count": len(plan_lines),
|
||
}
|
||
|
||
|
||
def push_brief(brief: dict):
|
||
"""将简报推送到企微"""
|
||
from app.utils.notifier import send_wecom_app
|
||
|
||
db = get_session_local()()
|
||
try:
|
||
from app.models import NotificationChannel
|
||
channels = db.query(NotificationChannel).filter(
|
||
NotificationChannel.enabled == True,
|
||
NotificationChannel.channel_type == "wecom_app",
|
||
).all()
|
||
|
||
pushed = 0
|
||
for ch in channels:
|
||
config = ch.config or {}
|
||
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", "@all"),
|
||
title=f"📋 {brief['period']} 经营简报",
|
||
content=brief["brief"],
|
||
alert_level="green",
|
||
)
|
||
if result.get("success"):
|
||
pushed += 1
|
||
logger.info(f"简报推送成功: {ch.name}")
|
||
else:
|
||
logger.warning(f"简报推送失败: {result.get('message')}")
|
||
|
||
return pushed
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
def run_brief():
|
||
"""主入口:生成简报并推送"""
|
||
logger.info("开始生成AI经营简报...")
|
||
db = get_session_local()()
|
||
try:
|
||
brief = generate_brief(db)
|
||
logger.info(f"简报生成完成: {brief['period']}, KPI数={brief['kpi_count']}, 预警={brief['red_alerts']}红/{brief['yellow_alerts']}黄")
|
||
|
||
if brief["brief"] and len(brief["brief"]) > 50:
|
||
pushed = push_brief(brief)
|
||
logger.info(f"推送完成: {pushed} 个渠道")
|
||
else:
|
||
logger.warning("简报内容不足50字,跳过推送")
|
||
|
||
return brief
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||
)
|
||
result = run_brief()
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|