From b1e8a55745fd608b5d8b420b1aaddd65886d9ae8 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Tue, 11 Aug 2026 00:21:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E9=95=BF=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E6=A8=A1=E5=9D=97=20=E2=80=94=20=E4=BA=94?= =?UTF-8?q?=E7=BB=B4=E8=AF=84=E5=88=86+=E9=9B=B7=E8=BE=BE=E5=9B=BE+?= =?UTF-8?q?=E8=B7=A8=E6=9C=9F=E5=AF=B9=E6=AF=94+=E8=B6=8B=E5=8A=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/growth_quality.py | 549 +++++++++++++++++++++++++++ backend/app/main.py | 3 +- backend/start_dev_8010.sh | 4 + frontend/components.d.ts | 1 - frontend/src/permission.ts | 3 +- frontend/src/router/index.ts | 1 + frontend/src/views/GrowthQuality.vue | 238 ++++++++++++ 7 files changed, 796 insertions(+), 3 deletions(-) create mode 100644 backend/app/api/growth_quality.py create mode 100644 backend/start_dev_8010.sh create mode 100644 frontend/src/views/GrowthQuality.vue diff --git a/backend/app/api/growth_quality.py b/backend/app/api/growth_quality.py new file mode 100644 index 00000000..5a27c684 --- /dev/null +++ b/backend/app/api/growth_quality.py @@ -0,0 +1,549 @@ +"""增长质量诊断 API — 五维评分 + 诊断结论 + 跨期对比 + +数据来源:KPI字典 (kpi_definitions) + KPI实际值 (kpi_values) +五维度:营收增长 / 利润质量 / 现金质量 / 增长效率 / 组织健康 +评分区间:0-100(>=80 好 / 60-79 中 / <60 差) +""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import Optional, List +from datetime import datetime +import re + +from app.database import get_db +from app.auth_middleware import require_auth, require_role +from app.models import KPIDefinition, KPIValue, Entity + +router = APIRouter(prefix="/api/cma/growth-quality", tags=["增长质量诊断"], + dependencies=[Depends(require_role("ceo", "finance", "business", "it"))], +) + +# 五维度定义 +DIMENSIONS = [ + {"key": "revenueGrowth", "name": "营收增长", "weight": 20, "icon": "📈", + "desc": "营收增速、增长持续性"}, + {"key": "profitQuality", "name": "利润质量", "weight": 20, "icon": "💰", + "desc": "毛利率/净利率趋势、利润与收入匹配"}, + {"key": "cashQuality", "name": "现金质量", "weight": 20, "icon": "🏦", + "desc": "经营现金流与净利润匹配度(含金量)"}, + {"key": "growthEfficiency", "name": "增长效率", "weight": 20, "icon": "🚀", + "desc": "获客成本、单位增长投入产出"}, + {"key": "orgHealth", "name": "组织健康", "weight": 20, "icon": "⚡", + "desc": "人效、费用结构"}, +] + +# 关键KPI编码 → 维度用途 +KPI_CODES = { + "F_REVENUE": "营业收入(万元)", + "F_NET_PROFIT": "净利润(万元)", + "F_OP_CFLOW": "经营性现金流(万元)", + "F_GROSS_MARGIN": "毛利率(%)", + "F_OP_PROFIT_MARGIN": "经营利润率(%)", + "F_COST_RATIO": "费用率(%)", + "F_REVENUE_GROWTH": "收入增长率(%)", + "F_FCF": "自由现金流(万元)", + "C_NEW_CLIENTS": "新客户数", + "C_REBATE_RATE": "渠补率(%)", + "C_SATISFACTION": "客户满意度", + "L_TRAINING": "培训完成率", + "P_DELIVERY": "交付及时率", + "F_AR_DAYS": "应收账款周转天数", +} + +MONTH_RE = re.compile(r"^\d{4}-\d{2}$") + + +def _fetch_kpi_values(db: Session, entity_id: int, period: str) -> dict: + """拉取某实体某期间的全部KPI值 {kpi_code: actual_value}""" + rows = (db.query(KPIDefinition.kpi_code, KPIValue.actual_value) + .join(KPIValue, KPIValue.kpi_id == KPIDefinition.id) + .filter(KPIDefinition.entity_id == entity_id, + KPIValue.period == period, + KPIValue.actual_value.isnot(None)) + .all()) + return {code: value for code, value in rows} + + +def _fetch_history(db: Session, entity_id: int, limit: int = 12) -> List[dict]: + """拉取最近 N 个期间(按月,含数据)的 KPI 值,供趋势/持续性分析""" + periods = (db.query(KPIValue.period) + .join(KPIDefinition, KPIDefinition.id == KPIValue.kpi_id) + .filter(KPIDefinition.entity_id == entity_id, + KPIDefinition.kpi_code.in_(["F_REVENUE", "F_NET_PROFIT", "F_OP_CFLOW"])) + .distinct().all()) + plist = sorted({p[0] for p in periods}, reverse=True) + # 只保留 YYYY-MM 格式,按时间排序(旧→新) + months = sorted([p for p in plist if MONTH_RE.match(p)]) + hist = [] + for p in months[-limit:]: + hist.append({"period": p, **{k: None for k in KPI_CODES}}) + if not hist: + return [] + # 批量取数 + rows = (db.query(KPIDefinition.kpi_code, KPIValue.period, KPIValue.actual_value) + .join(KPIValue, KPIValue.kpi_id == KPIDefinition.id) + .filter(KPIDefinition.entity_id == entity_id, + KPIValue.period.in_([h["period"] for h in hist])) + .all()) + idx = {h["period"]: h for h in hist} + for code, period, val in rows: + if period in idx and code in idx[period]: + idx[period][code] = val + return hist + + +def _prev_period(period: str) -> Optional[str]: + """计算上期(YYYY-MM → 上一月;其他格式 → None)""" + m = MONTH_RE.match(period) + if not m: + return None + y, mo = int(period[:4]), int(period[5:7]) + if mo == 1: + return f"{y-1:04d}-12" + return f"{y:04d}-{mo-1:02d}" + + +def _yoy_period(period: str) -> Optional[str]: + """计算去年同期(YYYY-MM → 去年同月)""" + m = MONTH_RE.match(period) + if not m: + return None + return f"{int(period[:4])-1:04d}-{period[5:7]}" + + +# ═══════════════════════════════════════════════ +# 五维度评分引擎(0-100) +# ═══════════════════════════════════════════════ + +def _clamp(v: float, lo: float = 0.0, hi: float = 100.0) -> float: + return max(lo, min(hi, v)) + + +def _linear(value, points: List[tuple]): + """分段线性插值评分: points = [(x, score), ...] 按 x 升序""" + if value is None: + return 50.0 + if value <= points[0][0]: + return points[0][1] + if value >= points[-1][0]: + return points[-1][1] + for (x1, s1), (x2, s2) in zip(points, points[1:]): + if x1 <= value <= x2: + if x2 == x1: + return s1 + return s1 + (s2 - s1) * (value - x1) / (x2 - x1) + return 50.0 + + +# ── 1. 营收增长:增速 + 持续性 ── +def _score_revenue_growth(cur: dict, prev: dict, history: List[dict]) -> float: + rev_cur = cur.get("F_REVENUE") + rev_prev = prev.get("F_REVENUE") if prev else None + # 优先用 KPI 直接给的收入增长率 + kpi_growth = cur.get("F_REVENUE_GROWTH") + growth = None + if kpi_growth is not None: + growth = float(kpi_growth) + elif rev_cur is not None and rev_prev: + growth = (rev_cur - rev_prev) / rev_prev * 100 if rev_prev else None + + score = _linear(growth, [ + (-30, 5), (-20, 15), (-10, 30), (0, 45), (5, 60), (10, 70), + (20, 82), (30, 90), (50, 96), + ]) + # 增长持续性:近6个月中收入增长月占比 + if len(history) >= 2: + revs = [h.get("F_REVENUE") for h in history if h.get("F_REVENUE") is not None] + ups = 0 + for i in range(1, len(revs)): + if revs[i] > revs[i - 1]: + ups += 1 + persist = ups / (len(revs) - 1) if len(revs) > 1 else 0.5 + score = score * 0.7 + persist * 100 * 0.3 + return round(_clamp(score), 1) + + +# ── 2. 利润质量:毛利率/净利率水平 + 趋势 + 收入匹配 ── +def _score_profit_quality(cur: dict, prev: dict) -> float: + gm = cur.get("F_GROSS_MARGIN") + np_ = cur.get("F_NET_PROFIT") + rev = cur.get("F_REVENUE") + net_margin = (np_ / rev * 100) if (np_ is not None and rev) else None + opm = cur.get("F_OP_PROFIT_MARGIN") + + gm_score = _linear(gm, [(-10, 5), (0, 10), (10, 25), (20, 45), (30, 62), + (40, 75), (55, 88), (70, 95)]) + nm_score = _linear(net_margin, [(-50, 0), (-20, 10), (-10, 20), (0, 35), + (10, 60), (20, 78), (30, 90)]) + opm_score = _linear(opm, [(-20, 10), (0, 30), (10, 55), (20, 75), (35, 90)]) + + # 毛利率 40% + 净利率 40% + 经营利润率 20% + base = gm_score * 0.4 + nm_score * 0.4 + opm_score * 0.2 + + # 利润与收入匹配:收入升但利润降 → 扣分 + if prev and rev is not None and np_ is not None: + prev_rev = prev.get("F_REVENUE") + prev_np = prev.get("F_NET_PROFIT") + if prev_rev and prev_np is not None: + rev_up = rev > prev_rev + np_down = np_ < prev_np + if rev_up and np_down: + base -= 10 + elif np_down: + base -= 5 + return round(_clamp(base), 1) + + +# ── 3. 现金质量:含金量(OCF/净利润) + 现金流强度 ── +def _score_cash_quality(cur: dict) -> float: + ocf = cur.get("F_OP_CFLOW") + np_ = cur.get("F_NET_PROFIT") + rev = cur.get("F_REVENUE") + fcf = cur.get("F_FCF") + + # 含金量 = OCF / 净利润(净利润>0时) + gold = None + if ocf is not None and np_ is not None and np_ > 0: + gold = ocf / np_ + # 净利润<=0:利润为负,含金量指标失效 → 低分(除非现金流强) + gold_score = _linear(gold, [(0, 10), (0.5, 35), (0.8, 55), (1.0, 70), + (1.2, 85), (1.5, 95)]) + if np_ is not None and np_ <= 0: + gold_score = 15 if (ocf is None or ocf <= 0) else 35 + + # 现金流强度 = OCF / 收入 + ocf_ratio = (ocf / rev * 100) if (ocf is not None and rev) else None + ocf_score = _linear(ocf_ratio, [(-20, 5), (0, 20), (10, 50), (20, 75), + (30, 90), (50, 98)]) + fcf_score = _linear(fcf, [(-100, 10), (-20, 30), (0, 50), (20, 70), + (100, 90)]) if fcf is not None else 50.0 + + score = gold_score * 0.5 + ocf_score * 0.35 + fcf_score * 0.15 + return round(_clamp(score), 1) + + +# ── 4. 增长效率:费用率水平 + 单位增长投入产出 + 获客成本 ── +def _score_growth_efficiency(cur: dict, prev: dict) -> float: + cost_ratio = cur.get("F_COST_RATIO") + cost_score = _linear(cost_ratio, [(10, 95), (20, 82), (30, 68), (40, 55), + (55, 40), (70, 25), (90, 10)]) + + # 费用增速 vs 收入增速(用费用率变化近似) + eff_score = 60.0 + if prev is not None and cost_ratio is not None: + prev_cr = prev.get("F_COST_RATIO") + if prev_cr: + cr_change = cost_ratio - prev_cr + eff_score = _linear(cr_change, [(-15, 95), (-5, 80), (0, 65), + (5, 45), (15, 25), (30, 10)]) + + # 获客成本代理:收入/新客户数(越高越高效) + rev = cur.get("F_REVENUE") + new_clients = cur.get("C_NEW_CLIENTS") + cac_score = 60.0 + if rev is not None and new_clients: + per_client = rev / new_clients + cac_score = _linear(per_client, [(0, 40), (50, 50), (200, 65), + (500, 78), (1000, 88)]) + + score = cost_score * 0.45 + eff_score * 0.35 + cac_score * 0.2 + return round(_clamp(score), 1) + + +# ── 5. 组织健康:费用结构 + 人效/运营质量 ── +def _score_org_health(cur: dict) -> float: + cost_ratio = cur.get("F_COST_RATIO") + # 费用结构(费用率越低越健康) + cost_score = _linear(cost_ratio, [(10, 95), (20, 82), (30, 68), (40, 55), + (55, 40), (70, 25), (90, 10)]) + # 运营/人效质量代理:满意度、培训、交付、应收 + sat = cur.get("C_SATISFACTION") + train = cur.get("L_TRAINING") + deliver = cur.get("P_DELIVERY") + ar_days = cur.get("F_AR_DAYS") + op_vals = [v for v in [sat, train, deliver] if v is not None] + op_score = (sum(op_vals) / len(op_vals)) if op_vals else 55.0 + ar_score = _linear(ar_days, [(15, 95), (30, 80), (45, 65), (60, 50), + (90, 30), (120, 15)]) if ar_days is not None else 55.0 + + score = cost_score * 0.4 + op_score * 0.35 + ar_score * 0.25 + return round(_clamp(score), 1) + + +_SCORERS = { + "revenueGrowth": _score_revenue_growth, + "profitQuality": _score_profit_quality, + "cashQuality": _score_cash_quality, + "growthEfficiency": _score_growth_efficiency, + "orgHealth": _score_org_health, +} + + +# ═══════════════════════════════════════════════ +# 明细指标 + 改善建议 +# ═══════════════════════════════════════════════ + +def _fmt(v, unit=""): + if v is None: + return "—" + if isinstance(v, float) and v == int(v): + return f"{int(v)}{unit}" + return f"{round(v, 2)}{unit}" + + +def _dim_indicators(dim_key: str, cur: dict, prev: dict) -> List[dict]: + """维度明细指标(label/value/verdict/status)""" + inds = [] + def add(label, value, verdict, status): + inds.append({"label": label, "value": value, "verdict": verdict, "status": status}) + + if dim_key == "revenueGrowth": + rev, prev_rev = cur.get("F_REVENUE"), (prev or {}).get("F_REVENUE") + growth = None + if rev is not None and prev_rev: + growth = (rev - prev_rev) / prev_rev * 100 + add("营业收入", _fmt(rev, "万"), "环比" + (_fmt(growth, "%") if growth is not None else "无上期数据"), + "success" if (growth or 0) >= 0 else "danger") + add("收入增长率(KPI)", _fmt(cur.get("F_REVENUE_GROWTH"), "%"), + "KPI直接值" if cur.get("F_REVENUE_GROWTH") is not None else "未录入", + "success" if (cur.get("F_REVENUE_GROWTH") or 0) >= 10 else "warning") + elif dim_key == "profitQuality": + rev, np_ = cur.get("F_REVENUE"), cur.get("F_NET_PROFIT") + nm = (np_ / rev * 100) if (np_ is not None and rev) else None + add("毛利率", _fmt(cur.get("F_GROSS_MARGIN"), "%"), + "毛利健康" if (cur.get("F_GROSS_MARGIN") or 0) >= 30 else "毛利偏低", + "success" if (cur.get("F_GROSS_MARGIN") or 0) >= 30 else "danger") + add("净利率", _fmt(nm, "%"), + "盈利" if (nm or 0) > 0 else "亏损", + "success" if (nm or 0) > 10 else "danger") + add("经营利润率", _fmt(cur.get("F_OP_PROFIT_MARGIN"), "%"), + "正常" if (cur.get("F_OP_PROFIT_MARGIN") or 0) >= 15 else "偏低", + "success" if (cur.get("F_OP_PROFIT_MARGIN") or 0) >= 15 else "warning") + elif dim_key == "cashQuality": + ocf, np_ = cur.get("F_OP_CFLOW"), cur.get("F_NET_PROFIT") + gold = (ocf / np_) if (ocf is not None and np_ and np_ > 0) else None + add("经营现金流", _fmt(ocf, "万"), + "现金流入" if (ocf or 0) > 0 else "现金流出", + "success" if (ocf or 0) > 0 else "danger") + add("含金量(OCF/净利润)", _fmt(gold, "倍"), + "含金量高" if (gold or 0) >= 1 else ("利润为负" if (np_ or 0) <= 0 else "含金量低"), + "success" if (gold or 0) >= 1 else "danger") + add("自由现金流", _fmt(cur.get("F_FCF"), "万"), + "正常" if (cur.get("F_FCF") or 0) > 0 else "为负", + "success" if (cur.get("F_FCF") or 0) > 0 else "warning") + elif dim_key == "growthEfficiency": + rev, nc = cur.get("F_REVENUE"), cur.get("C_NEW_CLIENTS") + per = (rev / nc) if (rev is not None and nc) else None + add("费用率", _fmt(cur.get("F_COST_RATIO"), "%"), + "费用可控" if (cur.get("F_COST_RATIO") or 0) <= 30 else "费用偏高", + "success" if (cur.get("F_COST_RATIO") or 0) <= 30 else "warning") + add("单位客户营收(万/户)", _fmt(per), + "获客效率高" if (per or 0) >= 200 else "获客效率一般", + "success" if (per or 0) >= 500 else "warning") + add("渠补率", _fmt(cur.get("C_REBATE_RATE"), "%"), + "渠道依赖" if (cur.get("C_REBATE_RATE") or 0) > 50 else "渠道健康", + "danger" if (cur.get("C_REBATE_RATE") or 0) > 50 else "success") + elif dim_key == "orgHealth": + add("费用率(结构)", _fmt(cur.get("F_COST_RATIO"), "%"), + "结构健康" if (cur.get("F_COST_RATIO") or 0) <= 30 else "结构偏重", + "success" if (cur.get("F_COST_RATIO") or 0) <= 30 else "warning") + add("应收周转天数", _fmt(cur.get("F_AR_DAYS"), "天"), + "回款快" if (cur.get("F_AR_DAYS") or 0) <= 45 else "回款偏慢", + "success" if (cur.get("F_AR_DAYS") or 0) <= 45 else "warning") + add("客户满意度", _fmt(cur.get("C_SATISFACTION")), + "满意" if (cur.get("C_SATISFACTION") or 0) >= 80 else "待提升", + "success" if (cur.get("C_SATISFACTION") or 0) >= 80 else "warning") + add("培训完成率", _fmt(cur.get("L_TRAINING"), "%"), + "学习投入足" if (cur.get("L_TRAINING") or 0) >= 80 else "学习投入不足", + "success" if (cur.get("L_TRAINING") or 0) >= 80 else "warning") + return inds + + +def _dim_suggestions(dim_key: str, score: float, cur: dict) -> List[str]: + """按维度评分生成改善建议""" + if score >= 80: + return ["该维度表现良好,建议保持并固化为标准流程"] + sug = [] + if dim_key == "revenueGrowth": + sug = ["挖掘存量客户复购,稳定收入基本盘", + "拓展新渠道/新产品线,提升营收增速", + "跟踪F_REVENUE_GROWTH KPI按月更新,建立增长预警线"] + elif dim_key == "profitQuality": + sug = ["排查毛利率下滑原因(成本/价格/渠补),优先止血", + "控制费用增速不超过收入增速,改善净利率", + "对亏损产品线做盈亏平衡分析,必要时收缩"] + elif dim_key == "cashQuality": + sug = ["加强应收账款催收,缩短回款周期", + "压缩非必要开支,提升经营现金流净额", + "建立现金流月度滚动预测,防范断流风险"] + elif dim_key == "growthEfficiency": + sug = ["优化费用结构,降低费用率至30%以下", + "评估渠道返利政策,降低渠补率与渠道依赖", + "提高获客转化率,降低单位获客成本"] + elif dim_key == "orgHealth": + sug = ["精简组织与费用结构,提升人效", + "强化培训与人才梯队建设(盯L_TRAINING)", + "优化应收管理,缩短周转天数"] + return sug + + +def _level_of(overall: float) -> dict: + if overall >= 80: + return {"level": "好", "level_type": "success", "desc": "增长质量优秀,增长可持续"} + if overall >= 60: + return {"level": "中", "level_type": "warning", "desc": "增长质量中等,存在优化空间"} + return {"level": "差", "level_type": "danger", "desc": "增长质量堪忧,需立即干预"} + + +# ═══════════════════════════════════════════════ +# 诊断主流程 +# ═══════════════════════════════════════════════ + +def _diagnose(db: Session, entity_id: int, period: str, history: List[dict]): + """对单个期间执行五维诊断,返回完整诊断对象""" + cur = _fetch_kpi_values(db, entity_id, period) + prev_period = _prev_period(period) + prev = _fetch_kpi_values(db, entity_id, prev_period) if prev_period else {} + + scores = {} + dims_payload = {} + for dim in DIMENSIONS: + key = dim["key"] + scorer = _SCORERS[key] + if key == "revenueGrowth": + s = scorer(cur, prev, history) + elif key in ("cashQuality", "orgHealth"): + s = scorer(cur) + else: + s = scorer(cur, prev) + scores[key] = s + dims_payload[key] = { + "key": key, "name": dim["name"], "icon": dim["icon"], + "desc": dim["desc"], "weight": dim["weight"], + "score": s, + "indicators": _dim_indicators(key, cur, prev), + "suggestions": _dim_suggestions(key, s, cur), + } + + overall = round(sum(scores.values()) / len(scores), 1) + level = _level_of(overall) + + # 诊断结论文本 + low_dims = [d for d in DIMENSIONS if scores[d["key"]] < 60] + mid_dims = [d for d in DIMENSIONS if 60 <= scores[d["key"]] < 80] + lines = [f"{period} 综合增长质量评分 {overall} 分({level['level']}):{level['desc']}。"] + if low_dims: + lines.append("需重点关注:" + "、".join(f"{d['name']}({scores[d['key']]}分)" for d in low_dims) + "。") + if mid_dims: + lines.append("可优化:" + "、".join(f"{d['name']}({scores[d['key']]}分)" for d in mid_dims) + "。") + if not low_dims: + lines.append("各维度均处于健康区间,增长质量扎实。") + diagnosis = "".join(lines) + + return { + "entity_id": entity_id, + "period": period, + "overall": overall, + "level": level["level"], + "level_type": level["level_type"], + "diagnosis": diagnosis, + "dimensions": dims_payload, + "kpi_available": {k: cur.get(k) is not None for k in KPI_CODES}, + } + + +@router.get("/periods") +def list_periods(entity_id: int = Query(1), db: Session = Depends(get_db)): + """列出某实体有KPI数据的期间(按月,含数据覆盖度,用于前端默认期间选择)""" + rows = (db.query(KPIValue.period, KPIValue.kpi_id) + .join(KPIDefinition, KPIDefinition.id == KPIValue.kpi_id) + .filter(KPIDefinition.entity_id == entity_id) + .all()) + counts: dict = {} + for period, kpi_id in rows: + if MONTH_RE.match(period or ""): + counts[period] = counts.get(period, 0) + 1 + periods = sorted(counts.keys(), reverse=True) + return {"entity_id": entity_id, "periods": periods, + "coverage": {p: counts[p] for p in periods}} + + +@router.get("/diagnosis") +def growth_quality_diagnosis( + entity_id: int = Query(1, ge=1), + period: Optional[str] = Query(None, description="期间 YYYY-MM,默认最近有数据期间"), + db: Session = Depends(get_db), +): + """增长质量诊断 — 五维评分(0-100) + 诊断结论 + 跨期对比(本期/上期/去年同期)""" + ent = db.query(Entity).filter(Entity.id == entity_id).first() + if not ent: + raise HTTPException(404, f"实体 {entity_id} 不存在") + + history = _fetch_history(db, entity_id, limit=12) + if not history: + raise HTTPException(400, "该实体暂无月度KPI数据,请先录入KPI实际值") + + # 默认取最近且有足够数据覆盖的期间(>=5个KPI值,退化为最近一个) + if not period: + cov_rows = (db.query(KPIValue.period) + .join(KPIDefinition, KPIDefinition.id == KPIValue.kpi_id) + .filter(KPIDefinition.entity_id == entity_id) + .all()) + cov: dict = {} + for (p,) in cov_rows: + if MONTH_RE.match(p or ""): + cov[p] = cov.get(p, 0) + 1 + candidates = sorted([p for p in cov if cov[p] >= 5], reverse=True) + period = candidates[0] if candidates else history[-1]["period"] + + current = _diagnose(db, entity_id, period, history) + + # 跨期对比:上期 + 去年同期 + prev_p = _prev_period(period) + yoy_p = _yoy_period(period) + prev_data = _fetch_kpi_values(db, entity_id, prev_p) if prev_p else {} + yoy_data = _fetch_kpi_values(db, entity_id, yoy_p) if yoy_p else {} + + comparison = { + "current": { + "period": period, "overall": current["overall"], + "level": current["level"], "level_type": current["level_type"], + "dimensions": {k: v["score"] for k, v in current["dimensions"].items()}, + }, + } + if prev_p and prev_data: + pdiag = _diagnose(db, entity_id, prev_p, history) + comparison["previous"] = { + "period": prev_p, "overall": pdiag["overall"], + "level": pdiag["level"], "level_type": pdiag["level_type"], + "dimensions": {k: v["score"] for k, v in pdiag["dimensions"].items()}, + } + if yoy_p and yoy_data: + ydiag = _diagnose(db, entity_id, yoy_p, history) + comparison["yoy"] = { + "period": yoy_p, "overall": ydiag["overall"], + "level": ydiag["level"], "level_type": ydiag["level_type"], + "dimensions": {k: v["score"] for k, v in ydiag["dimensions"].items()}, + } + + # 趋势:近12个月综合评分 + trend = [] + for h in history: + try: + d = _diagnose(db, entity_id, h["period"], history) + trend.append({"period": h["period"], "overall": d["overall"]}) + except Exception: + continue + + return { + "entity": {"id": ent.id, "name": ent.name, "short_name": ent.short_name}, + "period": period, + "overall": current["overall"], + "level": current["level"], + "level_type": current["level_type"], + "diagnosis": current["diagnosis"], + "dimensions": current["dimensions"], + "comparison": comparison, + "trend": trend, + "kpi_available": current["kpi_available"], + } diff --git a/backend/app/main.py b/backend/app/main.py index b2995202..1142df86 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from dotenv import load_dotenv from app.database import init_db -from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify +from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, tenant, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality from app.utils.cache import clear_all as clear_cache, delete as delete_cache from scripts.erp_sync import run_sync as run_erp_sync from app.auth_middleware import require_auth @@ -52,6 +52,7 @@ app.include_router(versions.router) app.include_router(budget.router) app.include_router(cost.router) app.include_router(predict.router) +app.include_router(growth_quality.router) app.include_router(reports.router) app.include_router(security.router) app.include_router(knowledge.router) diff --git a/backend/start_dev_8010.sh b/backend/start_dev_8010.sh new file mode 100644 index 00000000..9270c395 --- /dev/null +++ b/backend/start_dev_8010.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# 启动 CMA 后端 (增长质量诊断开发用) +cd /root/cma-management/backend +exec /root/cma-management/backend/venv/bin/python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8010 --workers 1 --limit-max-requests 10000 --timeout-keep-alive 30 --no-access-log diff --git a/frontend/components.d.ts b/frontend/components.d.ts index dab88de1..e2c19de5 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -29,7 +29,6 @@ declare module 'vue' { ElDivider: typeof import('element-plus/es')['ElDivider'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] - ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] diff --git a/frontend/src/permission.ts b/frontend/src/permission.ts index b18dff7e..df7996a5 100644 --- a/frontend/src/permission.ts +++ b/frontend/src/permission.ts @@ -9,7 +9,7 @@ interface MenuItem { // ── 角色路由映射 ── export const ROLE_ROUTES: Record = { - ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/tax-compliance'], + ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/growth-quality', '/tax-compliance'], finance: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/tax-compliance'], business: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/budget', '/deviations', '/action-plans', '/knowledge', '/guide', '/customer', '/expenses', '/cash-plan', '/tax-compliance'], it: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/tax-compliance'], @@ -35,6 +35,7 @@ export const MENU_ITEMS: MenuItem[] = [ { path: '/expenses', label: '费用审核', icon: 'Money', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' }, { path: '/tax-compliance', label: '税务合规', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' }, { path: '/cash-plan', label: '收付款计划', icon: 'Money', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' }, + { path: '/growth-quality', label: '增长质量诊断', icon: 'TrendCharts', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, { path: '/deviations', label: '差异分析', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' }, // ── GROUP 3: 监控与评价(Check)── diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 8384f40d..39173136 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -43,6 +43,7 @@ const routes = [ { path: 'analysis-confidence', name: 'AnalysisConfidence', component: () => import('@/views/AnalysisConfidence.vue'), meta: { title: '分析置信度', roles: ['ceo', 'finance', 'it'] } }, { path: 'expenses', name: 'ExpenseManage', component: () => import('@/views/ExpenseManage.vue'), meta: { title: '费用审核', roles: ['ceo', 'finance', 'business', 'it'] } }, { path: 'cash-plan', name: 'CashPlan', component: () => import('@/views/CashPlan.vue'), meta: { title: '收付款计划', roles: ['ceo', 'finance', 'business', 'it'] } }, + { path: 'growth-quality', name: 'GrowthQuality', component: () => import('@/views/GrowthQuality.vue'), meta: { title: '增长质量诊断', roles: ['ceo', 'finance', 'business', 'it'] } }, { path: 'tax-compliance', name: 'TaxCompliance', component: () => import('@/views/TaxCompliance.vue'), meta: { title: '税务合规', roles: ['ceo', 'finance', 'business', 'it'] } }, ] }, diff --git a/frontend/src/views/GrowthQuality.vue b/frontend/src/views/GrowthQuality.vue new file mode 100644 index 00000000..fd7ccfa9 --- /dev/null +++ b/frontend/src/views/GrowthQuality.vue @@ -0,0 +1,238 @@ + + + + +