init: 管理会计OS初始代码
包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
定时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))
|
||||
Reference in New Issue
Block a user