fix: 智能导入——匹配不上的财务报表科目自动创建KPI定义

- 清理科目前缀(一、/减:/加:)后多级匹配
- ⑤仍未匹配→自动创建KPI(PL_001/CF_001/BS_001)
- 避免205条全部跳过的场景
This commit is contained in:
Hermes CI Fix
2026-07-29 18:02:50 +08:00
parent 8a7cf25025
commit 9b973ab9a8
2 changed files with 118 additions and 13 deletions
+47 -13
View File
@@ -190,10 +190,16 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
for code in known_codes: for code in known_codes:
clean = re.sub(r'[\s\-_()()]', '', code).lower() clean = re.sub(r'[\s\-_()()]', '', code).lower()
alias_map[clean] = code 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] batch = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:12]
imported = 0 imported = 0
created_kpis = 0
skipped_rows = [] skipped_rows = []
for idx, row in df.iterrows(): 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}行: 无法确定期间") skipped_rows.append(f"{idx+2}行: 无法确定期间")
continue 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 kpi_code = None
# ① 精确编码匹配(极少情况)
if raw_kpi in known_codes: if raw_kpi in known_codes:
kpi_code = raw_kpi kpi_code = raw_kpi
else: # ② 别名匹配(去符号小写)
# 别名匹配 if not kpi_code:
clean_key = re.sub(r'[\s\-_()()]', '', raw_kpi).lower() clean_key = re.sub(r'[\s\-_()()]', '', raw_kpi).lower()
kpi_code = alias_map.get(clean_key) 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: if not kpi_code:
skipped_rows.append(f"{idx+2}行: 「{raw_kpi}」未匹配到KPI") kpi_code = name_map.get(clean_name)
continue # ④ 中文名模糊匹配
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: try:
val = float(raw_val) val = float(raw_val)
@@ -248,6 +280,8 @@ async def import_excel_smart(file: UploadFile = File(...), db: Session = Depends
# 9. 返回汇总 # 9. 返回汇总
stype_label = {"PL": "利润表", "CF": "现金流量表", "BS": "资产负债表"}.get(stype or "", "数据表") stype_label = {"PL": "利润表", "CF": "现金流量表", "BS": "资产负债表"}.get(stype or "", "数据表")
msg = f"{stype_label}识别成功,导入{imported}" msg = f"{stype_label}识别成功,导入{imported}"
if created_kpis:
msg += f",自动创建{created_kpis}个新KPI"
if skipped_rows: if skipped_rows:
msg += f"{len(skipped_rows)}条跳过:\n" + "\n".join(skipped_rows[:8]) msg += f"{len(skipped_rows)}条跳过:\n" + "\n".join(skipped_rows[:8])
if len(skipped_rows) > 8: if len(skipped_rows) > 8:
+71
View File
@@ -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三级分解树 # KPI-6: KPI三级分解树
# ============================================================ # ============================================================