feat: 增长质量诊断模块 — 五维评分+雷达图+跨期对比+趋势

This commit is contained in:
Hermes CI Fix
2026-08-11 00:21:10 +08:00
parent 0b191d278e
commit b1e8a55745
7 changed files with 796 additions and 3 deletions
+549
View File
@@ -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"],
}
+2 -1
View File
@@ -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)
+4
View File
@@ -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