From 88ec021a5890e92d36eb5800d031cb454b6ffaac Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Fri, 4 Sep 2026 00:09:57 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=86=E8=AF=AF=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=9A=84.bak=E5=A4=87=E4=BB=BD=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=88git=E5=8E=86=E5=8F=B2=E5=8F=AF=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=EF=BC=8C=E6=BA=90=E6=96=87=E4=BB=B6=E5=9D=87=E5=9C=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/dashboard.py.bak | 840 ------------------ backend/scripts/run_alert_check.sh.bak | 31 - .../src/components/KnowledgePanel.vue.bak | 143 --- .../strategy-map/NodeEditDialog.vue.bak | 221 ----- .../strategy-map/StrategyLayer.vue.bak | 201 ----- .../strategy/ConnectionLines.vue.bak | 288 ------ frontend/src/views/PredictDashboard.vue.bak2 | 287 ------ 7 files changed, 2011 deletions(-) delete mode 100644 backend/app/api/dashboard.py.bak delete mode 100755 backend/scripts/run_alert_check.sh.bak delete mode 100644 frontend/src/components/KnowledgePanel.vue.bak delete mode 100644 frontend/src/components/strategy-map/NodeEditDialog.vue.bak delete mode 100644 frontend/src/components/strategy-map/StrategyLayer.vue.bak delete mode 100644 frontend/src/components/strategy/ConnectionLines.vue.bak delete mode 100644 frontend/src/views/PredictDashboard.vue.bak2 diff --git a/backend/app/api/dashboard.py.bak b/backend/app/api/dashboard.py.bak deleted file mode 100644 index 539d21e3..00000000 --- a/backend/app/api/dashboard.py.bak +++ /dev/null @@ -1,840 +0,0 @@ -"""驾驶舱 API v2 — 支持时间区间""" -from fastapi import APIRouter, Depends, Query, Request, HTTPException -from sqlalchemy.orm import Session -from sqlalchemy import func, or_ -from datetime import datetime, timedelta -from typing import Optional -from app.database import get_db -from app.auth_middleware import require_auth, require_role -from app.models import KPIDefinition, KPIValue, KPIAlert, User -from app.utils.cache import get as cache_get, set as cache_set -import json -import logging - -logger = logging.getLogger("cma.dashboard") - -router = APIRouter(prefix="/api/cma/dashboard", tags=["驾驶舱"], - dependencies=[Depends(require_role("ceo", "finance", "business", "it"))], -) - -def parse_period(period_type: str, start_date: str = None, end_date: str = None): - """解析时间区间""" - today = datetime.now() - if period_type == "month": - start = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - end = today - elif period_type == "quarter": - q = (today.month - 1) // 3 - start = today.replace(month=q*3+1, day=1, hour=0, minute=0, second=0, microsecond=0) - end = today - elif period_type == "year": - start = today.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) - end = today - elif period_type == "custom" and start_date and end_date: - start = datetime.strptime(start_date, "%Y-%m-%d") - end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) - else: - start = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - end = today - return start, end - -def period_prefix(period_type: str): - """生成SQL期间前缀匹配""" - if period_type == "month": - return datetime.now().strftime("%Y-%m") - elif period_type == "quarter": - now = datetime.now() - q = (now.month - 1) // 3 - months = [f"{now.year}-{m:02d}" for m in range(q*3+1, q*3+4)] - return months - elif period_type == "year": - return str(datetime.now().year) - return None - -@router.get("/summary") -def get_dashboard_summary(role: str = Query("ceo"), period: str = Query("month"), db: Session = Depends(get_db)): - cache_key = f"summary:{role}:{period}" - cached = cache_get("dashboard", cache_key) - if cached: - return cached - kpi_total = db.query(func.count(KPIDefinition.id)).filter(KPIDefinition.status == "active").scalar() - alert_count = db.query(func.count(KPIAlert.id)).filter(KPIAlert.status == "pending").scalar() - dims = db.query(KPIDefinition.dimension, func.count(KPIDefinition.id)).filter( - KPIDefinition.status == "active").group_by(KPIDefinition.dimension).all() - - # 读取最近一次同步状态(从日志文件最后一行) - sync_status = {"last_sync": None, "status": "unknown", "detail": ""} - try: - with open("/var/log/cma-daily-sync.log", "r") as f: - lines = f.readlines() - # 从最后往前找包含 "完成" 或 "失败" 的行 - for line in reversed(lines[-50:]): - if "全部完成" in line: - sync_status["status"] = "success" - sync_status["last_sync"] = line.strip() - break - elif "失败" in line or "ERROR" in line: - sync_status["status"] = "failed" - sync_status["last_sync"] = line.strip() - break - else: - # 没找到完成/失败标记,取最后一行 - sync_status["last_sync"] = lines[-1].strip() if lines else None - except Exception as e: - sync_status["detail"] = str(e) - - result = { - "kpi_total": kpi_total or 0, "alert_count": alert_count or 0, - "dimension_stats": [{"dimension": d[0], "count": d[1]} for d in dims], - "sync_status": sync_status, - } - cache_set("dashboard", cache_key, result, ttl_seconds=30) - return result - -@router.get("/kpis") -def get_dashboard_kpis(role: str = Query("ceo"), period: str = Query("month"), - start_date: str = Query(None), end_date: str = Query(None), - db: Session = Depends(get_db)): - start, end = parse_period(period, start_date, end_date) - period_str = start.strftime("%Y-%m") - - kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all() - result = [] - - for k in kpis: - base_query = db.query(KPIValue).filter(KPIValue.kpi_id == k.id) - - if period == "month": - latest = base_query.filter(KPIValue.period == period_str).order_by(KPIValue.id.desc()).first() - elif period == "quarter": - months = period_prefix("quarter") - values = base_query.filter(KPIValue.period.in_(months)).all() - latest_val = sum(v.actual_value for v in values if v.actual_value) if values else None - latest = type('obj', (object,), {"actual_value": latest_val, "period": f"{months[0]}~{months[-1]}"})() if latest_val else None - elif period == "year": - values = base_query.filter(KPIValue.period.like(f"{period_str[:4]}%")).all() - latest_val = sum(v.actual_value for v in values if v.actual_value) if values else None - latest = type('obj', (object,), {"actual_value": latest_val, "period": period_str[:4]})() if latest_val else None - elif period == "custom" and start_date and end_date: - periods = [] - d = start - while d <= end: - periods.append(d.strftime("%Y-%m")) - d += timedelta(days=32) - d = d.replace(day=1) - values = base_query.filter(KPIValue.period.in_(set(periods))).all() - latest_val = sum(v.actual_value for v in values if v.actual_value) if values else None - latest = type('obj', (object,), {"actual_value": latest_val, "period": f"{start_date}~{end_date}"})() if latest_val else None - else: - latest = base_query.order_by(KPIValue.period.desc()).first() - - alert = db.query(KPIAlert).filter( - KPIAlert.kpi_id == k.id, - KPIAlert.status == "pending", - ).order_by(KPIAlert.id.desc()).first() - - result.append({ - "id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name, - "dimension": k.dimension, "unit": k.unit, "target_value": k.target_value, - "actual_value": latest.actual_value if latest else None, - "period": latest.period if latest else None, - "alert_level": alert.alert_level if alert else "none", - "alert_message": alert.alert_message if alert else None, - "frequency": k.frequency, - "responsible_dept": k.responsible_dept, - }) - - return {"data": result, "period": period, "range": {"start": start.strftime("%Y-%m-%d"), "end": end.strftime("%Y-%m-%d")}} - - -@router.get("/my-kpis") -def get_my_kpis( - current_user: User = Depends(require_auth), - period: str = Query("month"), - db: Session = Depends(get_db), -): - """获取当前用户负责的KPI - - business角色:只看自己负责的KPI - - 其他角色:看所有有预警的KPI - """ - role = current_user.role - username = current_user.username - name = current_user.name - period_str = datetime.now().strftime("%Y-%m") - - kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all() - result = [] - - for k in kpis: - # business角色筛选 - if role == "business": - responsible = (k.responsible_user or "").strip() - if responsible and responsible != username and responsible != name: - continue - - latest = db.query(KPIValue).filter( - KPIValue.kpi_id == k.id, - KPIValue.period == period_str, - ).order_by(KPIValue.id.desc()).first() - - alert = db.query(KPIAlert).filter( - KPIAlert.kpi_id == k.id, - KPIAlert.status == "pending", - ).order_by(KPIAlert.id.desc()).first() - - trend_values = db.query(KPIValue).filter( - KPIValue.kpi_id == k.id, - ).order_by(KPIValue.period.desc()).limit(6).all() - trend = [{"period": v.period, "value": v.actual_value} for v in reversed(trend_values)] - - result.append({ - "id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name, - "dimension": k.dimension, "unit": k.unit, - "target_value": k.target_value, - "actual_value": latest.actual_value if latest else None, - "period": latest.period if latest else period_str, - "alert_level": alert.alert_level if alert else "none", - "alert_message": alert.alert_message if alert else None, - "alert_id": alert.id if alert else None, - "frequency": k.frequency, - "responsible_dept": k.responsible_dept, - "responsible_user": k.responsible_user, - "trend": trend, - "threshold_green": k.threshold_green, - "threshold_yellow": k.threshold_yellow, - "threshold_red": k.threshold_red, - }) - - return {"data": result, "user_role": role, "user_name": name, "period": period_str} - - -@router.get("/finance-analysis") -def get_finance_analysis( - current_user: User = Depends(require_auth), - period: str = Query("month"), - db: Session = Depends(get_db), -): - """财务工作台分析数据""" - period_str = datetime.now().strftime("%Y-%m") - - finance_kpis = db.query(KPIDefinition).filter( - KPIDefinition.status == "active", - KPIDefinition.dimension == "finance", - ).all() - - kpi_data = [] - for k in finance_kpis: - latest = db.query(KPIValue).filter( - KPIValue.kpi_id == k.id, - KPIValue.period == period_str, - ).order_by(KPIValue.id.desc()).first() - - trend_values = db.query(KPIValue).filter( - KPIValue.kpi_id == k.id, - ).order_by(KPIValue.period.desc()).limit(6).all() - trend = [{"period": v.period, "value": v.actual_value} for v in reversed(trend_values)] - - alert = db.query(KPIAlert).filter( - KPIAlert.kpi_id == k.id, - KPIAlert.status == "pending", - ).order_by(KPIAlert.id.desc()).first() - - kpi_data.append({ - "id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name, - "unit": k.unit, "target_value": k.target_value, - "actual_value": latest.actual_value if latest else None, - "threshold_green": k.threshold_green, - "threshold_yellow": k.threshold_yellow, - "threshold_red": k.threshold_red, - "trend": trend, - "alert_level": alert.alert_level if alert else "none", - "frequency": k.frequency, - }) - - total_sales = next((k for k in kpi_data if k["kpi_code"] == "SALES_TOTAL"), None) - gross_profit = next((k for k in kpi_data if k["kpi_code"] == "SALES_PROFIT_RATE"), None) - cost_control = next((k for k in kpi_data if k["kpi_code"] == "COST_CONTROL_RATE"), None) - receivable = next((k for k in kpi_data if k["kpi_code"] == "RECEIVABLE_TURNOVER"), None) - - return { - "period": period_str, - "kpis": kpi_data, - "summary": { - "total_sales": total_sales["actual_value"] if total_sales else None, - "gross_profit_rate": gross_profit["actual_value"] if gross_profit else None, - "cost_control_rate": cost_control["actual_value"] if cost_control else None, - "receivable_turnover": receivable["actual_value"] if receivable else None, - } - } - - -@router.get("/predict") -def predict_kpis(db: Session = Depends(get_db)): - """基于历史趋势预测下月KPI值(简单线性回归)""" - from datetime import datetime, timedelta - - period_str = datetime.now().strftime("%Y-%m") - next_month = int(period_str[5:7]) + 1 - next_year = int(period_str[:4]) - if next_month > 12: - next_month = 1 - next_year += 1 - next_period = f"{next_year}-{next_month:02d}" - - kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all() - predictions = [] - - for k in kpis: - values = db.query(KPIValue).filter( - KPIValue.kpi_id == k.id, - ).order_by(KPIValue.period.asc()).all() - - # 需要至少3个数据点才能做预测 - if len(values) < 3: - continue - - # 简单线性回归: y = a + bx - points = [(i, v.actual_value) for i, v in enumerate(values) if v.actual_value is not None] - if len(points) < 3: - continue - - n = len(points) - sum_x = sum(p[0] for p in points) - sum_y = sum(p[1] for p in points) - sum_xy = sum(p[0] * p[1] for p in points) - sum_xx = sum(p[0] ** 2 for p in points) - - # 斜率 b = (n*sum_xy - sum_x*sum_y) / (n*sum_xx - sum_x*sum_x) - denom = n * sum_xx - sum_x * sum_x - if denom == 0: - continue - b = (n * sum_xy - sum_x * sum_y) / denom - a = (sum_y - b * sum_x) / n - - # 预测下个月(x = n,因为最后一个索引是 n-1) - predicted_value = a + b * n - - # 检查预测值是否触发阈值 - alert_level = "none" - if k.threshold_red: - try: - op = k.threshold_red[:2] if len(k.threshold_red) > 1 and k.threshold_red[1] in "=<>" else k.threshold_red[0] - val_str = k.threshold_red.replace(op, "").strip() - val = float(val_str) - if (op in (">=", ">") and predicted_value >= val) or (op in ("<=", "<") and predicted_value <= val): - alert_level = "red" - except (ValueError, IndexError): - pass - if alert_level == "none" and k.threshold_yellow: - try: - op = k.threshold_yellow[:2] if len(k.threshold_yellow) > 1 and k.threshold_yellow[1] in "=<>" else k.threshold_yellow[0] - val_str = k.threshold_yellow.replace(op, "").strip() - val = float(val_str) - if (op in (">=", ">") and predicted_value >= val) or (op in ("<=", "<") and predicted_value <= val): - alert_level = "yellow" - except (ValueError, IndexError): - pass - - predictions.append({ - "kpi_id": k.id, - "kpi_code": k.kpi_code, - "kpi_name": k.kpi_name, - "target_value": k.target_value, - "last_value": points[-1][1] if points else None, - "predicted_value": round(predicted_value, 2), - "predicted_period": next_period, - "alert_level": alert_level, - "trend": "up" if b > 0 else ("down" if b < 0 else "stable"), - "confidence": "high" if len(points) >= 6 else ("medium" if len(points) >= 4 else "low"), - "data_points": len(points), - }) - - return { - "current_period": period_str, - "next_period": next_period, - "predictions": predictions, - "kpi_count": len(kpis), - "predictable_count": len(predictions), - } - - -# ── 个人工作台 ────────────────────────────── - - -@router.get("/my-dashboard") -def my_dashboard( - current_user: User = Depends(require_auth), - db: Session = Depends(get_db), -): - """个人工作台:返回我的KPI、改善行动、待办提醒""" - username = current_user.username - name = current_user.name - role = current_user.role - - # 角色 → 维度映射(从已发布战略地图中按角色筛选对应维度的KPI) - ROLE_DIMENSIONS = { - "ceo": ["finance", "customer", "process", "learning"], # CEO看全部维度 - "finance": ["finance"], # 财务看财务维度 - "business": ["customer", "process"], # 业务看客户+流程维度 - "it": ["process", "learning"], # IT看流程+学习成长 - } - role_dims = ROLE_DIMENSIONS.get(role, ["finance", "customer"]) - - # 获取所有已发布战略地图的KPI code集合(dimensions中引用的) - from app.models import StrategicMap - published_maps = db.query(StrategicMap).filter(StrategicMap.status == "published").all() - map_kpi_codes = set() - for sm in published_maps: - dims = sm.dimensions - if isinstance(dims, str): - try: - dims = json.loads(dims) - except Exception: - continue - for dim in dims: - for obj in dim.get("objectives", []): - for code in obj.get("kpis", []): - map_kpi_codes.add(code) - - # 1. 按角色维度筛选(从已发布地图的KPI中取符合角色维度的) - map_kpis = [] - if map_kpi_codes: - map_kpis = db.query(KPIDefinition).filter( - KPIDefinition.kpi_code.in_(map_kpi_codes), - KPIDefinition.dimension.in_(role_dims), - KPIDefinition.status == "active", - ).all() - - # 2. 补充负责的KPI(responsible_user匹配) - assigned_kpis = db.query(KPIDefinition).filter( - or_( - KPIDefinition.responsible_user == username, - KPIDefinition.responsible_user == name, - ), - KPIDefinition.status == "active", - ).all() - assigned_ids = {k.id for k in assigned_kpis} - - # 去重合并 - all_kpis = map_kpis + [k for k in assigned_kpis if k.id not in {mk.id for mk in map_kpis}] - - kpi_list = [] - for k in all_kpis: - latest_v = db.query(KPIValue).filter( - KPIValue.kpi_id == k.id - ).order_by(KPIValue.calculated_at.desc()).first() - - actual = latest_v.actual_value if latest_v else None - target = k.target_value - level = "gray" - if actual is not None and target: - ratio = actual / target - level = "green" if ratio >= 0.9 else ("yellow" if ratio >= 0.7 else "red") - - kpi_list.append({ - "id": k.id, - "kpi_code": k.kpi_code, - "kpi_name": k.kpi_name, - "dimension": k.dimension, - "category": k.category, - "target_value": target, - "actual_value": actual, - "unit": k.unit, - "level": level, - "period": latest_v.period if latest_v else None, - }) - - # 2. 我的改善行动(assignee匹配) - from app.models import ActionPlan - my_plans = db.query(ActionPlan).filter( - or_( - ActionPlan.assignee == username, - ActionPlan.assignee == name, - ) - ).order_by(ActionPlan.updated_at.desc()).all() - - plan_list = [] - for p in my_plans: - overdue = False - if p.due_date and p.status not in ("completed", "cancelled"): - overdue = p.due_date < datetime.now() - kpi_name = "" - kpi = db.query(KPIDefinition).filter(KPIDefinition.id == p.kpi_id).first() - if kpi: - kpi_name = kpi.kpi_name - - plan_list.append({ - "id": p.id, - "kpi_id": p.kpi_id, - "kpi_name": kpi_name, - "title": p.title, - "assignee": p.assignee, - "priority": p.priority, - "status": p.status, - "progress": p.progress or 0, - "due_date": p.due_date.isoformat() if p.due_date else None, - "overdue": overdue, - "created_at": p.created_at.isoformat() if p.created_at else None, - }) - - # 3. 待办提醒 - reminders = [] - - # 逾期行动 - for p in plan_list: - if p["overdue"]: - reminders.append({ - "type": "overdue_plan", - "severity": "danger", - "message": f"你负责的「{p['title']}」已逾期", - "related_id": p["id"], - "related_type": "action_plan", - }) - - # 红色预警KPI - for k in kpi_list: - if k["level"] == "red": - reminders.append({ - "type": "red_kpi", - "severity": "danger", - "message": f"你负责的KPI「{k['kpi_name']}」处于红色预警", - "related_id": k["id"], - "related_type": "kpi", - }) - - # 黄色预警KPI - for k in kpi_list: - if k["level"] == "yellow": - reminders.append({ - "type": "yellow_kpi", - "severity": "warning", - "message": f"你负责的KPI「{k['kpi_name']}」处于黄色预警", - "related_id": k["id"], - "related_type": "kpi", - }) - - return { - "kpis": kpi_list, - "action_plans": plan_list, - "reminders": reminders, - } - - -@router.get("/erp-trends") -def get_erp_trends( - current_user: User = Depends(require_auth), - months: int = Query(12, ge=3, le=36), - db: Session = Depends(get_db), -): - """获取ERP关键指标趋势数据(驾驶舱趋势分析用)""" - codes = [ - "F_REVENUE", - "F_PROFIT_RATE", - "F_NET_PROFIT_RATE", - "F_COST_RATIO", - "F_CASH_FLOW", - "F_AR_TURNOVER", - "F_ROE", - "F_ASSET_TURNOVER", - "F_DEBT_RATIO", - "C_CUSTOMER_COUNT", - "C_CUSTOMER_SATISFACTION", - "C_CUSTOMER_CONCENTRATION", - "P_DELIVERY_ON_TIME", - "P_DEFECT_RATE", - "P_SUPPLY_CYCLE", - "L_TRAINING_HOURS", - "L_EMPLOYEE_TURNOVER", - "L_INNOVATION_COUNT", - "L_TECH_COVERAGE", - ] - result = {} - - for code in codes: - kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == code).first() - if not kpi: - continue - values = db.query(KPIValue).filter( - KPIValue.kpi_id == kpi.id, - ).order_by(KPIValue.period.desc()).limit(months).all() - - trend = [{"period": v.period, "value": v.actual_value} for v in reversed(values)] - if trend: - vals = [v["value"] for v in trend if v["value"] is not None] - latest = vals[-1] if vals else 0 - first = vals[0] if vals else 0 - if latest > first * 1.05: - trend_dir = "up" - elif latest < first * 0.95: - trend_dir = "down" - else: - trend_dir = "stable" - - mom_val = vals[-2] if len(vals) >= 2 else None - yoy_val = vals[-12] if len(vals) >= 12 else (vals[0] if len(vals) >= 1 else None) - - result[code] = { - "name": kpi.kpi_name, - "unit": kpi.unit or "", - "target": kpi.target_value, - "trend": trend, - "trend_dir": trend_dir, - "latest": latest, - "mom": mom_val, - "mom_rate": round((latest - mom_val) / abs(mom_val) * 100, 1) if mom_val and mom_val != 0 else None, - "yoy": yoy_val, - "yoy_rate": round((latest - yoy_val) / abs(yoy_val) * 100, 1) if yoy_val and yoy_val != 0 else None, - } - - return {"data": result} - - -@router.get("/dupont") -async def dupont_analysis( - db: Session = Depends(get_db), - current_user: User = Depends(require_auth), -): - """杜邦分析 — ROE分解 - ROE = 净利率 × 资产周转率 × 权益乘数 - """ - cache_key = f"dupont:{current_user.role}" - cached = cache_get("dashboard", cache_key) - if cached: - return cached - - # 获取底层数据KPI - def get_kpi_value(code: str) -> tuple: - kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == code).first() - if not kpi: - return None, None, None - latest = db.query(KPIValue).filter(KPIValue.kpi_id == kpi.id).order_by(KPIValue.period.desc()).first() - prev = db.query(KPIValue).filter(KPIValue.kpi_id == kpi.id).order_by(KPIValue.period.desc()).offset(1).first() - val = latest.actual_value if latest else None - pval = prev.actual_value if prev else None - return val, pval, kpi.unit - - # 营收、利润、总资产、净资产 - revenue, prev_revenue, _ = get_kpi_value("F_REVENUE") - # 用营收×净利润率估算净利润(数据库没有净利润绝对值) - profit_net = None - prev_profit_net = None - if revenue: - net_profit_rate, prev_npr, _ = get_kpi_value("F_NET_PROFIT_RATE") - if net_profit_rate: - profit_net = revenue * (net_profit_rate / 100) - if prev_revenue and prev_npr: - prev_profit_net = prev_revenue * (prev_npr / 100) - # 如果还是算不出来,用毛利率做替代估算 - if profit_net is None and revenue: - gross_profit, _, _ = get_kpi_value("F_PROFIT_RATE") - profit_net = revenue * (gross_profit / 100) * 0.7 if gross_profit else None # 粗略估算净利润=毛利*0.7 - - asset_total, prev_asset, _ = get_kpi_value("F_ASSET_TOTAL") - equity_total, prev_equity, _ = get_kpi_value("F_EQUITY_TOTAL") - - # 计算杜邦因子 - result = {"roe": None, "factors": {}, "raw_data": {}, "history": {}} - - if revenue and profit_net and asset_total and equity_total and all(v > 0 for v in [revenue, asset_total, equity_total]): - net_profit_margin = round(profit_net / revenue, 4) # 净利率 - asset_turnover = round(revenue / asset_total, 4) # 资产周转率 - equity_multiplier = round(asset_total / equity_total, 4) # 权益乘数 - roe = round(net_profit_margin * asset_turnover * equity_multiplier * 100, 2) - - result["roe"] = roe - result["factors"] = { - "net_profit_margin": {"value": net_profit_margin, "label": "净利率", "desc": f"净利润/{'营收' if revenue else '-'} = {net_profit_margin*100:.2f}%"}, - "asset_turnover": {"value": asset_turnover, "label": "资产周转率", "desc": f"营收/总资产 = {asset_turnover:.4f}次"}, - "equity_multiplier": {"value": equity_multiplier, "label": "权益乘数", "desc": f"总资产/净资产 = {equity_multiplier:.4f}"}, - } - result["raw_data"] = { - "revenue": revenue, - "profit_net": profit_net, - "asset_total": asset_total, - "equity_total": equity_total, - } - - # 环比计算 - if prev_revenue and prev_profit_net and prev_asset and prev_equity and all(v > 0 for v in [prev_revenue, prev_asset, prev_equity]): - prev_npm = round(prev_profit_net / prev_revenue, 4) - prev_at = round(prev_revenue / prev_asset, 4) - prev_em = round(prev_asset / prev_equity, 4) - prev_roe = round(prev_npm * prev_at * prev_em * 100, 2) - result["history"]["prev"] = { - "roe": prev_roe, - "net_profit_margin": prev_npm, - "asset_turnover": prev_at, - "equity_multiplier": prev_em, - } - # 同比变化 - change = round(roe - prev_roe, 2) - npm_change = round((net_profit_margin - prev_npm) * 10000, 2) # 转成BP - at_change = round(asset_turnover - prev_at, 4) - em_change = round(equity_multiplier - prev_em, 4) - result["history"]["change"] = { - "roe": change, - "roe_label": f"{'+' if change > 0 else ''}{change}%", - "net_profit_margin_bp": npm_change, - "asset_turnover": at_change, - "equity_multiplier": em_change, - } - result["history"]["trend"] = "up" if change > 0 else ("down" if change < 0 else "stable") - - # 补上原始数据(即使计算不全也返回给前端展示) - if not result.get("raw_data"): - result["raw_data"] = { - "revenue": revenue, - "profit_net": profit_net, - "asset_total": asset_total, - "equity_total": equity_total, - } - - cache_set("dashboard", cache_key, result, ttl_seconds=300) - return result - - -def _get_kpi_trend(kpi_id: int, db: Session) -> dict: - """计算KPI的环比和同比趋势""" - from datetime import datetime - now = datetime.now() - cur_period = now.strftime("%Y-%m") - - # 上月 - if now.month == 1: - prev_month = f"{now.year-1}-12" - else: - prev_month = f"{now.year}-{now.month-1:02d}" - - # 去年同期 - last_year = f"{now.year-1}-{now.month:02d}" - - cur_val = db.query(KPIValue).filter( - KPIValue.kpi_id == kpi_id, - KPIValue.period == cur_period - ).order_by(KPIValue.id.desc()).first() - - prev_val = db.query(KPIValue).filter( - KPIValue.kpi_id == kpi_id, - KPIValue.period == prev_month - ).order_by(KPIValue.id.desc()).first() - - yoy_val = db.query(KPIValue).filter( - KPIValue.kpi_id == kpi_id, - KPIValue.period == last_year - ).order_by(KPIValue.id.desc()).first() - - def calc_rate(curr, prev): - if curr and prev and prev.actual_value and prev.actual_value != 0: - return round((curr.actual_value - prev.actual_value) / prev.actual_value * 100, 2) - return None - - return { - "current_value": cur_val.actual_value if cur_val else None, - "current_period": cur_period, - "mom_value": prev_val.actual_value if prev_val else None, - "mom_rate": calc_rate(cur_val, prev_val), - "yoy_value": yoy_val.actual_value if yoy_val else None, - "yoy_rate": None if not yoy_val else calc_rate(cur_val, yoy_val), - } - - -@router.get("/kpis/enhanced") -def get_kpis_enhanced(role: str = Query("ceo"), period: str = Query("month"), - start_date: str = None, end_date: str = None, - db: Session = Depends(get_db)): - """增强版KPI列表(带趋势)""" - result = get_dashboard_kpis(role=role, period=period, start_date=start_date, end_date=end_date, db=db) - if "data" in result and result["data"]: - for kpi in result["data"]: - if kpi.get("id"): - trend = _get_kpi_trend(kpi["id"], db) - kpi["trend"] = trend - return result - - -@router.get("/trend-analysis") -def get_trend_analysis(kpi_ids: str = Query(""), period: str = Query("month"), - db: Session = Depends(get_db)): - """多KPI趋势对比(折线图数据)""" - ids = [int(x) for x in kpi_ids.split(",") if x.strip().isdigit()] - if not ids: - return {"data": []} - - result = [] - for kpi_id in ids: - kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first() - if not kpi: - continue - - values = db.query(KPIValue).filter( - KPIValue.kpi_id == kpi_id - ).order_by(KPIValue.period).all() - - series = [] - for v in values: - if v.actual_value is not None: - series.append({ - "period": v.period, - "value": v.actual_value, - }) - - result.append({ - "kpi_id": kpi.id, - "kpi_code": kpi.kpi_code, - "kpi_name": kpi.kpi_name, - "unit": kpi.unit, - "target": kpi.target_value, - "data": series, - }) - - return {"data": result} - - -@router.get("/alert-stats") -def get_alert_stats(period: str = Query("month"), db: Session = Depends(get_db)): - """预警统计(按等级和维度)""" - from sqlalchemy import func - - # 按等级统计 - by_level = db.query( - KPIAlert.alert_level, - func.count(KPIAlert.id) - ).group_by(KPIAlert.alert_level).all() - - level_stats = {row[0]: row[1] for row in by_level} - - # 按维度统计 - by_dim = db.query( - KPIDefinition.dimension, - func.count(KPIAlert.id) - ).join(KPIAlert, KPIDefinition.id == KPIAlert.kpi_id - ).group_by(KPIDefinition.dimension).all() - - dim_stats = {row[0]: row[1] for row in by_dim} - - return { - "by_level": level_stats, - "by_dimension": dim_stats, - "total": sum(level_stats.values()) if level_stats else 0, - } - - -@router.get("/export") -def export_kpi_data(kpi_ids: str = "", db: Session = Depends(get_db)): - """导出KPI数据为CSV格式""" - from fastapi.responses import PlainTextResponse - - ids = [int(x) for x in kpi_ids.split(",") if x.strip().isdigit()] - query = db.query(KPIValue).join(KPIDefinition, KPIValue.kpi_id == KPIDefinition.id) - if ids: - query = query.filter(KPIValue.kpi_id.in_(ids)) - - rows = query.order_by(KPIDefinition.kpi_code, KPIValue.period).all() - - csv_lines = ["KPI编码,KPI名称,期间,实际值,目标值,来源,状态"] - for r in rows: - kpi = db.query(KPIDefinition).filter(KPIDefinition.id == r.kpi_id).first() - csv_lines.append(f"{kpi.kpi_code},{kpi.kpi_name},{r.period},{r.actual_value},{kpi.target_value},{r.source_type},{r.data_status}") - - return PlainTextResponse("\n".join(csv_lines), media_type="text/csv", - headers={"Content-Disposition": "attachment; filename=kpi_export.csv"}) diff --git a/backend/scripts/run_alert_check.sh.bak b/backend/scripts/run_alert_check.sh.bak deleted file mode 100755 index 4c8000df..00000000 --- a/backend/scripts/run_alert_check.sh.bak +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# CMA预警检查 — 通过wrapper脚本避免systemd引号冲突 -set -euo pipefail - -cd /root/cma-management/backend - -VENV="/root/cma-management/backend/venv" - -# 1. KPI阈值检查 -"$VENV/bin/python3" -c " -from app.database import get_db_session -from app.api.alert_rules import check_all_kpi_alerts -db = next(get_db_session()) -try: - count = check_all_kpi_alerts(db) - print(f'KPI预警检查完成: 新增{count}条') -finally: - db.close() -" 2>&1 - -# 2. 差异预警推送检查 -"$VENV/bin/python3" -c " -from app.database import get_db_session -from app.api.deviation_push import check_all_push -db = next(get_db_session()) -try: - count = check_all_push(db) - print(f'差异预警检查完成: 新增{count}条') -finally: - db.close() -" 2>&1 diff --git a/frontend/src/components/KnowledgePanel.vue.bak b/frontend/src/components/KnowledgePanel.vue.bak deleted file mode 100644 index e4139147..00000000 --- a/frontend/src/components/KnowledgePanel.vue.bak +++ /dev/null @@ -1,143 +0,0 @@ - - - - - diff --git a/frontend/src/components/strategy-map/NodeEditDialog.vue.bak b/frontend/src/components/strategy-map/NodeEditDialog.vue.bak deleted file mode 100644 index 8220f69d..00000000 --- a/frontend/src/components/strategy-map/NodeEditDialog.vue.bak +++ /dev/null @@ -1,221 +0,0 @@ - - - - - diff --git a/frontend/src/components/strategy-map/StrategyLayer.vue.bak b/frontend/src/components/strategy-map/StrategyLayer.vue.bak deleted file mode 100644 index 9679c071..00000000 --- a/frontend/src/components/strategy-map/StrategyLayer.vue.bak +++ /dev/null @@ -1,201 +0,0 @@ - - - - - diff --git a/frontend/src/components/strategy/ConnectionLines.vue.bak b/frontend/src/components/strategy/ConnectionLines.vue.bak deleted file mode 100644 index a60d52d4..00000000 --- a/frontend/src/components/strategy/ConnectionLines.vue.bak +++ /dev/null @@ -1,288 +0,0 @@ - - - - - diff --git a/frontend/src/views/PredictDashboard.vue.bak2 b/frontend/src/views/PredictDashboard.vue.bak2 deleted file mode 100644 index c3912231..00000000 --- a/frontend/src/views/PredictDashboard.vue.bak2 +++ /dev/null @@ -1,287 +0,0 @@ - - - - -