init: 管理会计OS初始代码
包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
"""KPI目标对齐管理 API — 管理会计OS
|
||||
支持三种对齐模式:纵向分解 / 横向支撑 / BSC瀑布链
|
||||
管理员可初始化选择,后续按模式运作"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIDefinition, OperationLog, RolePermission
|
||||
|
||||
router = APIRouter(prefix="/api/cma/alignment", tags=["KPI目标对齐"],
|
||||
# 不设全局权限,每个接口单独控制
|
||||
)
|
||||
|
||||
# 三种对齐模式定义
|
||||
ALIGNMENT_MODES = [
|
||||
{
|
||||
"key": "vertical_decomposition",
|
||||
"name": "纵向分解",
|
||||
"description": "上级KPI直接拆分为多个下级KPI,目标值汇总等于上级目标。适用于营收、成本等可量化指标。",
|
||||
"example": "公司销售总额2000万 → 区域A 800万 + 区域B 700万 + 区域C 500万",
|
||||
},
|
||||
{
|
||||
"key": "horizontal_support",
|
||||
"name": "横向支撑",
|
||||
"description": "下级KPI是上级KPI的驱动因子,下级目标达成支撑上级结果。适用于复合型指标。",
|
||||
"example": "销售毛利率30% ← 销售总额↑ + 成本控制↓ + 高毛利产品占比↑",
|
||||
},
|
||||
{
|
||||
"key": "bsc_chain",
|
||||
"name": "BSC瀑布链",
|
||||
"description": "按平衡计分卡因果链层层传导:学习成长→内部流程→客户→财务。",
|
||||
"example": "培训完成率↑ → 订单交付及时率↑ → 客户满意度↑ → 销售总额↑",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/modes")
|
||||
def list_modes():
|
||||
"""返回三种对齐模式的定义(公开接口)"""
|
||||
return {"modes": ALIGNMENT_MODES}
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def get_alignment_config(db: Session = Depends(get_db)):
|
||||
"""获取当前系统对齐模式配置(公开接口,无需认证)"""
|
||||
perm = db.query(RolePermission).filter(RolePermission.key == "alignment_config").first()
|
||||
if not perm:
|
||||
return {
|
||||
"mode": None,
|
||||
"configured": False,
|
||||
"modes": ALIGNMENT_MODES,
|
||||
}
|
||||
return {
|
||||
"mode": perm.value,
|
||||
"configured": True,
|
||||
"modes": ALIGNMENT_MODES,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/config")
|
||||
def set_alignment_config(
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
user = Depends(require_role("ceo", "it")),
|
||||
):
|
||||
"""初始化/修改系统对齐模式(CEO/IT权限)"""
|
||||
mode_key = data.get("mode")
|
||||
if mode_key not in [m["key"] for m in ALIGNMENT_MODES]:
|
||||
raise HTTPException(400, f"无效的对齐模式: {mode_key}")
|
||||
|
||||
perm = db.query(RolePermission).filter(RolePermission.key == "alignment_config").first()
|
||||
if perm:
|
||||
perm.value = {"mode": mode_key, "set_at": datetime.now().isoformat()}
|
||||
else:
|
||||
perm = RolePermission(key="alignment_config", value={"mode": mode_key, "set_at": datetime.now().isoformat()})
|
||||
db.add(perm)
|
||||
db.commit()
|
||||
|
||||
return {"message": f"对齐模式已设置为: {mode_key}", "mode": mode_key}
|
||||
|
||||
|
||||
@router.get("/tree")
|
||||
def get_alignment_tree(
|
||||
kpi_id: Optional[int] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user = Depends(require_auth),
|
||||
):
|
||||
"""获取KPI对齐关系树
|
||||
|
||||
根据当前系统配置的对齐模式,返回KPI的父子层级关系。
|
||||
如果指定kpi_id,返回该KPI及其下级树;
|
||||
如果不指定,返回整个对齐树。
|
||||
"""
|
||||
# 获取当前模式
|
||||
config_perm = db.query(RolePermission).filter(RolePermission.key == "alignment_config").first()
|
||||
mode = config_perm.value.get("mode") if config_perm else None
|
||||
if not mode:
|
||||
raise HTTPException(400, "系统未配置对齐模式,请先在系统设置中初始化")
|
||||
|
||||
# 获取所有KPI
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").order_by(KPIDefinition.kpi_code).all()
|
||||
kpi_map = {k.id: k for k in kpis}
|
||||
|
||||
# 构建父子关系
|
||||
if mode == "vertical_decomposition":
|
||||
# 纵向分解:BSC编码前缀相同=同一系列
|
||||
return _build_vertical_tree(kpis, kpi_id)
|
||||
elif mode == "horizontal_support":
|
||||
# 横向支撑:按BSC维度+类别的因果关系
|
||||
return _build_horizontal_tree(kpis, kpi_id)
|
||||
elif mode == "bsc_chain":
|
||||
# BSC瀑布链:按维度层级传导
|
||||
return _build_bsc_chain(kpis, kpi_id)
|
||||
else:
|
||||
raise HTTPException(400, f"未知的对齐模式: {mode}")
|
||||
|
||||
|
||||
def _build_vertical_tree(kpis, kpi_id=None):
|
||||
"""纵向分解树:按编码前缀分组,同一前缀=同一系列"""
|
||||
from collections import defaultdict
|
||||
|
||||
# 提取前缀(如 F_REVENUE_001 → F_REVENUE)
|
||||
groups = defaultdict(list)
|
||||
for k in kpis:
|
||||
parts = k.kpi_code.rsplit("_", 1)
|
||||
prefix = parts[0] if len(parts) > 1 else k.kpi_code
|
||||
groups[prefix].append(k)
|
||||
|
||||
def make_node(kpi):
|
||||
return {
|
||||
"id": kpi.id, "kpi_code": kpi.kpi_code, "kpi_name": kpi.kpi_name,
|
||||
"dimension": kpi.dimension, "category": kpi.category,
|
||||
"target_value": kpi.target_value, "unit": kpi.unit,
|
||||
"children": [],
|
||||
}
|
||||
|
||||
trees = []
|
||||
# 每个前缀组中,按序号升序,第一个为父级
|
||||
for prefix, group in sorted(groups.items()):
|
||||
sorted_group = sorted(group, key=lambda k: k.kpi_code)
|
||||
if len(sorted_group) > 1:
|
||||
parent = make_node(sorted_group[0])
|
||||
parent["children"] = [make_node(c) for c in sorted_group[1:]]
|
||||
for c in parent["children"]:
|
||||
c["alignment_type"] = "vertical_split"
|
||||
c["parent_code"] = parent["kpi_code"]
|
||||
parent["child_count"] = len(parent["children"])
|
||||
trees.append(parent)
|
||||
else:
|
||||
trees.append(make_node(sorted_group[0]))
|
||||
|
||||
if kpi_id:
|
||||
# 只返回指定KPI的子树
|
||||
return _filter_tree(trees, kpi_id)
|
||||
|
||||
return {"mode": "vertical_decomposition", "mode_name": "纵向分解", "tree": trees, "total": len(kpis)}
|
||||
|
||||
|
||||
def _build_horizontal_tree(kpis, kpi_id=None):
|
||||
"""横向支撑树:按BSC维度因果关联"""
|
||||
# 因果顺序:learning → process → customer → finance
|
||||
dim_order = {"learning": 0, "process": 1, "customer": 2, "finance": 3}
|
||||
dim_name = {"finance": "财务", "customer": "客户", "process": "内部流程", "learning": "学习成长"}
|
||||
|
||||
# 按维度分组
|
||||
groups = {"finance": [], "customer": [], "process": [], "learning": []}
|
||||
for k in kpis:
|
||||
if k.dimension in groups:
|
||||
groups[k.dimension].append(k)
|
||||
|
||||
def make_node(kpi):
|
||||
return {
|
||||
"id": kpi.id, "kpi_code": kpi.kpi_code, "kpi_name": kpi.kpi_name,
|
||||
"dimension": kpi.dimension, "category": kpi.category,
|
||||
"target_value": kpi.target_value, "unit": kpi.unit,
|
||||
"children": [],
|
||||
}
|
||||
|
||||
# 构建层级:一个维度节点包含该维度所有KPI
|
||||
trees = []
|
||||
for dim, ks in sorted(groups.items(), key=lambda x: dim_order.get(x[0], 9)):
|
||||
if not ks:
|
||||
continue
|
||||
dim_node = {
|
||||
"id": None,
|
||||
"dimension": dim,
|
||||
"kpi_name": dim_name.get(dim, dim),
|
||||
"is_dimension_group": True,
|
||||
"children": [make_node(k) for k in sorted(ks, key=lambda x: x.kpi_code)],
|
||||
"child_count": len(ks),
|
||||
}
|
||||
# 建立因果关联说明
|
||||
if dim == "learning":
|
||||
dim_node["description"] = "驱动因素:人才培养与创新"
|
||||
for c in dim_node["children"]:
|
||||
c["drives"] = "internal_process"
|
||||
elif dim == "process":
|
||||
dim_node["description"] = "过程保障:效率与质量提升"
|
||||
for c in dim_node["children"]:
|
||||
c["drives"] = "customer"
|
||||
elif dim == "customer":
|
||||
dim_node["description"] = "市场反馈:客户规模与满意度"
|
||||
for c in dim_node["children"]:
|
||||
c["drives"] = "finance"
|
||||
elif dim == "finance":
|
||||
dim_node["description"] = "结果指标:收入与盈利"
|
||||
for c in dim_node["children"]:
|
||||
c["drives"] = None
|
||||
trees.append(dim_node)
|
||||
|
||||
if kpi_id:
|
||||
return _filter_tree(trees, kpi_id)
|
||||
|
||||
return {
|
||||
"mode": "horizontal_support",
|
||||
"mode_name": "横向支撑",
|
||||
"tree": trees,
|
||||
"total": len(kpis),
|
||||
"causal_chain": [
|
||||
{"from": "学习成长", "to": "内部流程", "logic": "培训与创新→流程效率提升"},
|
||||
{"from": "内部流程", "to": "客户", "logic": "流程效率→客户满意度提升"},
|
||||
{"from": "客户", "to": "财务", "logic": "客户规模→财务结果达成"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _build_bsc_chain(kpis, kpi_id=None):
|
||||
"""BSC瀑布链:按category类别间的因果传导"""
|
||||
from collections import defaultdict
|
||||
|
||||
# 每个维度的KPI按category分组
|
||||
cat_kpis = defaultdict(list)
|
||||
for k in kpis:
|
||||
if k.category:
|
||||
cat_kpis[k.category].append(k)
|
||||
|
||||
# BSC瀑布链的传导关系
|
||||
chain = [
|
||||
{"cat": "talent_pipeline", "label": "人才梯队", "dim": "learning", "feeds": ["supply_chain", "delivery_quality"]},
|
||||
{"cat": "employee_engagement", "label": "员工敬业", "dim": "learning", "feeds": ["supply_chain"]},
|
||||
{"cat": "innovation", "label": "创新改善", "dim": "learning", "feeds": ["delivery_quality"]},
|
||||
{"cat": "supply_chain", "label": "供应链效率", "dim": "process", "feeds": ["delivery_quality"]},
|
||||
{"cat": "delivery_quality", "label": "交付质量", "dim": "process", "feeds": ["customer_scale", "customer_satisfaction"]},
|
||||
{"cat": "customer_scale", "label": "客户规模", "dim": "customer", "feeds": ["revenue_growth"]},
|
||||
{"cat": "customer_concentration", "label": "客户集中度", "dim": "customer", "feeds": ["profitability"]},
|
||||
{"cat": "customer_satisfaction", "label": "客户满意", "dim": "customer", "feeds": ["revenue_growth", "profitability"]},
|
||||
{"cat": "revenue_growth", "label": "收入增长", "dim": "finance", "feeds": ["profitability"]},
|
||||
{"cat": "profitability", "label": "盈利水平", "dim": "finance", "feeds": None},
|
||||
{"cat": "cost_control", "label": "成本费用", "dim": "finance", "feeds": ["profitability"]},
|
||||
{"cat": "asset_efficiency", "label": "资产效率", "dim": "finance", "feeds": ["profitability"]},
|
||||
{"cat": "cash_risk", "label": "现金流风控", "dim": "finance", "feeds": ["profitability"]},
|
||||
]
|
||||
|
||||
def make_node(kpi):
|
||||
return {
|
||||
"id": kpi.id, "kpi_code": kpi.kpi_code, "kpi_name": kpi.kpi_name,
|
||||
"dimension": kpi.dimension, "category": kpi.category,
|
||||
"target_value": kpi.target_value, "unit": kpi.unit,
|
||||
}
|
||||
|
||||
# 构建瀑布链
|
||||
trees = []
|
||||
for link in chain:
|
||||
cat = link["cat"]
|
||||
if cat not in cat_kpis:
|
||||
continue
|
||||
cat_node = {
|
||||
"id": None,
|
||||
"category": cat,
|
||||
"category_label": link["label"],
|
||||
"dimension": link["dim"],
|
||||
"is_category_group": True,
|
||||
"feeds": link["feeds"],
|
||||
"children": [make_node(k) for k in sorted(cat_kpis[cat], key=lambda x: x.kpi_code)],
|
||||
"child_count": len(cat_kpis[cat]),
|
||||
}
|
||||
trees.append(cat_node)
|
||||
|
||||
if kpi_id:
|
||||
return _filter_tree(trees, kpi_id)
|
||||
|
||||
return {
|
||||
"mode": "bsc_chain",
|
||||
"mode_name": "BSC瀑布链",
|
||||
"tree": trees,
|
||||
"total": len(kpis),
|
||||
"chain": chain,
|
||||
}
|
||||
|
||||
|
||||
def _filter_tree(nodes, target_id):
|
||||
"""在树中查找包含指定KPI的子树"""
|
||||
for node in nodes:
|
||||
if node.get("id") == target_id:
|
||||
return node
|
||||
if node.get("children"):
|
||||
for child in node["children"]:
|
||||
if child.get("id") == target_id:
|
||||
return child
|
||||
# 递归查找
|
||||
found = _filter_tree(node["children"], target_id)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
Reference in New Issue
Block a user