init: 管理会计OS初始代码

包含前后端完整代码:
- 前端:Vue3+Vite+ElementPlus
- 后端:FastAPI+SQLAlchemy
- 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动
- 当前版本:v1.0.0
This commit is contained in:
Hermes CI Fix
2026-05-28 17:32:22 +08:00
commit 3dddd36866
142 changed files with 18533 additions and 0 deletions
+201
View File
@@ -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))