init: 管理会计OS初始代码
包含前后端完整代码: - 前端:Vue3+Vite+ElementPlus - 后端:FastAPI+SQLAlchemy - 模块:驾驶舱/KPI/战略地图/预警/预算/成本/预测/改善行动 - 当前版本:v1.0.0
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""KPI字典 API"""
|
||||
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, filter_kpis_by_role, kpi_visible_dims
|
||||
from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog
|
||||
|
||||
router = APIRouter(prefix="/api/cma/kpis", tags=["KPI字典"],
|
||||
dependencies=[Depends(require_role("ceo", "finance", "business", "it"))],
|
||||
)
|
||||
|
||||
# 写操作只允许 ceo/finance/it
|
||||
WRITE_ROLES = Depends(require_role("ceo", "finance", "it"))
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_kpis(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
dimension: Optional[str] = None,
|
||||
keyword: Optional[str] = None,
|
||||
epic: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user = Depends(require_auth),
|
||||
):
|
||||
query = db.query(KPIDefinition).filter(KPIDefinition.status == "active")
|
||||
# 角色权限过滤
|
||||
dims = kpi_visible_dims(current_user.role, db)
|
||||
if dims:
|
||||
query = query.filter(KPIDefinition.dimension.in_(dims))
|
||||
if dimension:
|
||||
query = query.filter(KPIDefinition.dimension == dimension)
|
||||
if keyword:
|
||||
query = query.filter(KPIDefinition.kpi_name.contains(keyword))
|
||||
if epic:
|
||||
query = query.filter(KPIDefinition.epic == epic)
|
||||
if category:
|
||||
query = query.filter(KPIDefinition.category == category)
|
||||
total = query.count()
|
||||
kpis = query.order_by(KPIDefinition.kpi_code).offset((page-1)*page_size).limit(page_size).all()
|
||||
return {"total": total, "page": page, "page_size": page_size, "data": [kpi_to_dict(k) for k in kpis]}
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
def get_kpi_categories(current_user = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
"""获取BSC分类结构(带可见性过滤)"""
|
||||
from sqlalchemy import func as sa_func
|
||||
dims = kpi_visible_dims(current_user.role, db)
|
||||
query = db.query(
|
||||
KPIDefinition.dimension,
|
||||
KPIDefinition.category,
|
||||
sa_func.count(KPIDefinition.id)
|
||||
).filter(KPIDefinition.status == "active")
|
||||
if dims:
|
||||
query = query.filter(KPIDefinition.dimension.in_(dims))
|
||||
rows = query.group_by(KPIDefinition.dimension, KPIDefinition.category).all()
|
||||
|
||||
# 构建树形结构
|
||||
dim_map = {"finance": "财务", "customer": "客户", "process": "内部流程", "learning": "学习成长"}
|
||||
cat_map = {
|
||||
"revenue_growth": "收入增长", "profitability": "盈利水平", "cost_control": "成本费用",
|
||||
"asset_efficiency": "资产效率", "cash_risk": "现金流风控",
|
||||
"customer_scale": "客户规模", "customer_concentration": "客户集中度", "customer_satisfaction": "客户满意",
|
||||
"supply_chain": "供应链效率", "delivery_quality": "交付质量",
|
||||
"talent_pipeline": "人才梯队", "employee_engagement": "员工敬业", "innovation": "创新改善",
|
||||
}
|
||||
tree = []
|
||||
for dim, cat, cnt in rows:
|
||||
# 找或创建维度节点
|
||||
dim_node = next((n for n in tree if n["key"] == dim), None)
|
||||
if not dim_node:
|
||||
dim_node = {"key": dim, "label": dim_map.get(dim, dim), "children": []}
|
||||
tree.append(dim_node)
|
||||
dim_node["children"].append({
|
||||
"key": cat,
|
||||
"label": cat_map.get(cat, cat),
|
||||
"count": cnt,
|
||||
})
|
||||
dim_counts = {}
|
||||
for d in tree:
|
||||
dim_counts[d["key"]] = sum(c["count"] for c in d["children"])
|
||||
d["count"] = dim_counts[d["key"]]
|
||||
return {"tree": tree, "total": sum(dim_counts.values())}
|
||||
|
||||
|
||||
@router.get("/{kpi_id}")
|
||||
def get_kpi(kpi_id: int, db: Session = Depends(get_db)):
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
if not kpi:
|
||||
raise HTTPException(404, "KPI不存在")
|
||||
return kpi_to_dict(kpi)
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_kpi(data: dict, db: Session = Depends(get_db), user=WRITE_ROLES):
|
||||
kpi = KPIDefinition(**data)
|
||||
db.add(kpi)
|
||||
db.commit()
|
||||
db.refresh(kpi)
|
||||
_log(db, 1, "create", "kpi", kpi.id, data)
|
||||
return kpi_to_dict(kpi)
|
||||
|
||||
|
||||
@router.put("/{kpi_id}")
|
||||
def update_kpi(kpi_id: int, data: dict, db: Session = Depends(get_db), user=WRITE_ROLES):
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
if not kpi:
|
||||
raise HTTPException(404, "KPI不存在")
|
||||
for k, v in data.items():
|
||||
if hasattr(kpi, k) and v is not None:
|
||||
setattr(kpi, k, v)
|
||||
db.commit()
|
||||
_log(db, 1, "update", "kpi", kpi_id, data)
|
||||
return kpi_to_dict(kpi)
|
||||
|
||||
|
||||
@router.delete("/{kpi_id}")
|
||||
def delete_kpi(kpi_id: int, db: Session = Depends(get_db), user=WRITE_ROLES):
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
if kpi:
|
||||
kpi.status = "disabled"
|
||||
db.commit()
|
||||
return {"message": "已删除"}
|
||||
|
||||
|
||||
def kpi_to_dict(k):
|
||||
return {c.name: getattr(k, c.name) for c in k.__table__.columns}
|
||||
|
||||
|
||||
def _log(db, user_id, action, target_type, target_id, detail):
|
||||
log = OperationLog(user_id=user_id, action=action, target_type=target_type, target_id=target_id, detail=json.dumps(detail, ensure_ascii=False) if detail else None)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("/{kpi_id}/objectives")
|
||||
def get_kpi_objectives(kpi_id: int, db: Session = Depends(get_db)):
|
||||
"""查看KPI所属的目标和战略地图"""
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
if not kpi:
|
||||
raise HTTPException(404, "KPI不存在")
|
||||
|
||||
# 通过 kpi_definitions.objective 字段关联目标
|
||||
# 也通过 map_id 关联地图
|
||||
result = {
|
||||
"kpi": {"id": kpi.id, "kpi_code": kpi.kpi_code, "kpi_name": kpi.kpi_name},
|
||||
"objectives": [],
|
||||
"map": None,
|
||||
}
|
||||
|
||||
if kpi.map_id:
|
||||
m = db.query(StrategicMap).filter(StrategicMap.id == kpi.map_id).first()
|
||||
if m:
|
||||
result["map"] = {"id": m.id, "title": m.title, "status": m.status}
|
||||
|
||||
if kpi.objective:
|
||||
objs = db.query(MapObjective).filter(
|
||||
MapObjective.map_id == kpi.map_id,
|
||||
MapObjective.name == kpi.objective,
|
||||
).all()
|
||||
result["objectives"] = [{"id": o.id, "name": o.name, "dimension_key": o.dimension_key} for o in objs]
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user