fix: 智能导入——匹配不上的财务报表科目自动创建KPI定义
- 清理科目前缀(一、/减:/加:)后多级匹配 - ⑤仍未匹配→自动创建KPI(PL_001/CF_001/BS_001) - 避免205条全部跳过的场景
This commit is contained in:
+47
-13
@@ -190,10 +190,16 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
|
||||
for code in known_codes:
|
||||
clean = re.sub(r'[\s\-_()()]', '', code).lower()
|
||||
alias_map[clean] = code
|
||||
# 中文名映射("营业收入"→F_REVENUE)
|
||||
name_map: dict[str, str] = {}
|
||||
for code, kpi_obj in kpis.items():
|
||||
name_map[kpi_obj.kpi_name] = code
|
||||
|
||||
# 8. 遍历导入
|
||||
# 8. 遍历导入(匹配不上的自动创建KPI)
|
||||
stype_prefix = {"PL": "PL_", "CF": "CF_", "BS": "BS_"}.get(stype or "", "EXT_")
|
||||
batch = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:12]
|
||||
imported = 0
|
||||
created_kpis = 0
|
||||
skipped_rows = []
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
@@ -208,24 +214,50 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
|
||||
skipped_rows.append(f"第{idx+2}行: 无法确定期间")
|
||||
continue
|
||||
|
||||
# 智能匹配KPI编码
|
||||
# 清理科目名(去掉"一、""减:""加:"等前缀)
|
||||
clean_name = re.sub(r'^[一二三四五六七八九十、\s\+]+', '', raw_kpi)
|
||||
clean_name = re.sub(r'^[减加]?[::]\s*', '', clean_name).strip()
|
||||
if not clean_name:
|
||||
clean_name = raw_kpi
|
||||
|
||||
# 匹配KPI
|
||||
kpi_code = None
|
||||
|
||||
# ① 精确编码匹配(极少情况)
|
||||
if raw_kpi in known_codes:
|
||||
kpi_code = raw_kpi
|
||||
else:
|
||||
# 别名匹配
|
||||
# ② 别名匹配(去符号小写)
|
||||
if not kpi_code:
|
||||
clean_key = re.sub(r'[\s\-_()()]', '', raw_kpi).lower()
|
||||
kpi_code = alias_map.get(clean_key)
|
||||
# 模糊匹配(中文科目名→KPI编码)
|
||||
if not kpi_code:
|
||||
for code, kpi_obj in kpis.items():
|
||||
if raw_kpi in kpi_obj.kpi_name or kpi_obj.kpi_name in raw_kpi:
|
||||
kpi_code = code
|
||||
break
|
||||
|
||||
# ③ 中文名精确匹配
|
||||
if not kpi_code:
|
||||
skipped_rows.append(f"第{idx+2}行: 「{raw_kpi}」未匹配到KPI")
|
||||
continue
|
||||
kpi_code = name_map.get(clean_name)
|
||||
# ④ 中文名模糊匹配
|
||||
if not kpi_code:
|
||||
for code, kpi_obj in kpis.items():
|
||||
if clean_name in kpi_obj.kpi_name or kpi_obj.kpi_name in clean_name:
|
||||
kpi_code = code
|
||||
break
|
||||
|
||||
# ⑤ 仍未匹配 → 自动创建KPI
|
||||
if not kpi_code:
|
||||
new_code = f"{stype_prefix}{len(kpis) + created_kpis + 1:03d}"
|
||||
new_kpi = KPIDefinition(
|
||||
kpi_code=new_code,
|
||||
kpi_name=clean_name,
|
||||
dimension="finance",
|
||||
category="financial_report",
|
||||
data_source_type="excel",
|
||||
status="active",
|
||||
)
|
||||
db.add(new_kpi)
|
||||
db.flush()
|
||||
kpis[new_code] = new_kpi
|
||||
known_codes.add(new_code)
|
||||
name_map[clean_name] = new_code
|
||||
kpi_code = new_code
|
||||
created_kpis += 1
|
||||
|
||||
try:
|
||||
val = float(raw_val)
|
||||
@@ -248,6 +280,8 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
|
||||
# 9. 返回汇总
|
||||
stype_label = {"PL": "利润表", "CF": "现金流量表", "BS": "资产负债表"}.get(stype or "", "数据表")
|
||||
msg = f"✅ {stype_label}识别成功,导入{imported}条"
|
||||
if created_kpis:
|
||||
msg += f",自动创建{created_kpis}个新KPI"
|
||||
if skipped_rows:
|
||||
msg += f",{len(skipped_rows)}条跳过:\n" + "\n".join(skipped_rows[:8])
|
||||
if len(skipped_rows) > 8:
|
||||
|
||||
@@ -233,6 +233,77 @@ def get_kpi_score(
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# KPI-glossary: 知识资产化 — KPI字典实时加载(供ChatBI财务Bot调用)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/glossary")
|
||||
def get_kpi_glossary(
|
||||
entity_id: int = Query(1, ge=1),
|
||||
db: Session = Depends(get_db),
|
||||
current_user = Depends(require_auth),
|
||||
):
|
||||
"""KPI字典实时加载 — 返回所有KPI的定义、当前值、目标值、公式、维度、阈值
|
||||
|
||||
供ChatBI财务Bot在分析前调用,确保口径与系统一致。
|
||||
返回字段: kpi_code, kpi_name, current_value, target_value, formula, dimension, threshold
|
||||
"""
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).order_by(KPIDefinition.kpi_code).all()
|
||||
|
||||
result = []
|
||||
for k in kpis:
|
||||
# 获取最新实际值
|
||||
latest_val = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == k.id,
|
||||
KPIValue.actual_value.isnot(None),
|
||||
).order_by(KPIValue.period.desc()).first()
|
||||
|
||||
current_value = latest_val.actual_value if latest_val else None
|
||||
latest_period = latest_val.period if latest_val else None
|
||||
|
||||
# 组装阈值描述
|
||||
threshold = None
|
||||
if k.threshold_green or k.threshold_yellow or k.threshold_red:
|
||||
parts = []
|
||||
if k.threshold_green:
|
||||
parts.append(f"绿灯:{k.threshold_green}")
|
||||
if k.threshold_yellow:
|
||||
parts.append(f"黄灯:{k.threshold_yellow}")
|
||||
if k.threshold_red:
|
||||
parts.append(f"红灯:{k.threshold_red}")
|
||||
threshold = " | ".join(parts)
|
||||
|
||||
result.append({
|
||||
"kpi_id": k.id,
|
||||
"kpi_code": k.kpi_code,
|
||||
"kpi_name": k.kpi_name,
|
||||
"dimension": k.dimension,
|
||||
"category": k.category,
|
||||
"formula": k.formula,
|
||||
"formula_desc": k.formula_desc,
|
||||
"unit": k.unit,
|
||||
"target_value": k.target_value,
|
||||
"current_value": current_value,
|
||||
"latest_period": latest_period,
|
||||
"threshold": threshold,
|
||||
"responsible_dept": k.responsible_dept,
|
||||
"responsible_user": k.responsible_user,
|
||||
"data_source": k.data_source,
|
||||
"data_owner": k.data_owner,
|
||||
"frequency": k.frequency,
|
||||
"status": k.status,
|
||||
})
|
||||
|
||||
return {
|
||||
"entity_id": entity_id,
|
||||
"total": len(result),
|
||||
"glossary": result,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# KPI-6: KPI三级分解树
|
||||
# ============================================================
|
||||
|
||||
Reference in New Issue
Block a user