wip(strategy-map): V1+V3 合版 + v2/v3 口径(完备度/C1 hold/57 归档组)

This commit is contained in:
Hermes CI Fix
2026-10-19 10:33:40 +08:00
parent 0fc2c6109e
commit 4d20c27de4
16 changed files with 2116 additions and 89 deletions
+176
View File
@@ -0,0 +1,176 @@
"""战略地图 · 数据完备度 API(口径唯一真源:CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0)
主口径(对外唯一主数):仅地图 65(已发布)15/19 = 78.9%,判据 = 被 23-KPI 集挂接。
附注口径(只作附注):全量 4 图 30/31 = 96.8%,判据 = 有KPI挂接(含归档图)。
KPI 级:active 行 117(e1=73 / e2=44),按行计,与目标级分列、不相除。
所有汇总数一律运行时对库内数据现算;判据数据(23-KPI 挂接关系)见
app/data/caliber_map_completeness.py。禁用组合(19/19、83.9%)在本模块无生成路径。
"""
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.auth_middleware import require_auth
from app.data import caliber_map_completeness as cal
from app.database import get_db
from app.deps import get_entity_id
from app.models import KPIDefinition, MapObjective, StrategicMap
router = APIRouter(
prefix="/api/cma/maps/completeness",
tags=["战略地图数据完备度"],
dependencies=[Depends(require_auth)],
)
def _pct(n: int, d: int) -> Optional[float]:
"""占比只保留 1 位小数;分母为 0 返回 None(展示为「无数据」)。"""
if not d:
return None
return round(n * 100.0 / d, 1)
def _entity_scopes(db: Session, entity_id: int):
"""返回(主口径地图, 同属主的地图列表)。主口径地图仅认 map_id=65 且属于该实体。"""
maps = db.query(StrategicMap).filter(StrategicMap.entity_id == entity_id).all()
primary = next((m for m in maps if m.id == cal.PRIMARY_MAP_ID), None)
return primary, maps
def _objective_rows(db: Session, maps):
"""拉取给定地图的战略目标,并逐行标记 23-KPI 挂接 / 是否有 KPI。"""
map_ids = [m.id for m in maps]
if not map_ids:
return [], {}
title_by_map = {m.id: m.title for m in maps}
status_by_map = {m.id: (m.status or "") for m in maps}
objs = (
db.query(MapObjective)
.filter(MapObjective.map_id.in_(map_ids))
.order_by(MapObjective.map_id, MapObjective.sort_order, MapObjective.id)
.all()
)
rows = []
for o in objs:
kpis = o.kpis if isinstance(o.kpis, list) else []
rows.append({
"objective_id": o.id,
"map_id": o.map_id,
"map_title": title_by_map.get(o.map_id),
"map_status": status_by_map.get(o.map_id),
"layer": o.dimension_key,
"layer_label": cal.LAYER_LABELS.get(o.dimension_key, o.dimension_key),
"name": o.name,
"kpis": kpis,
"has_kpi": len(kpis) > 0,
"covered_23kpi": o.id in cal.LINKED_OBJECTIVES,
})
return rows, title_by_map
@router.get("/summary")
def completeness_summary(
entity_id: int = Depends(get_entity_id),
db: Session = Depends(get_db),
):
"""数据完备度总览:目标级(主数 + 附注)与 KPI 级(117 行)两层分列。"""
primary_map, maps = _entity_scopes(db, entity_id)
rows, _ = _objective_rows(db, maps)
# ---- 目标级 · 主口径:仅地图 65,判据 = 被 23-KPI 集挂接 ----
if primary_map is not None:
main_rows = [r for r in rows if r["map_id"] == cal.PRIMARY_MAP_ID]
main_total = len(main_rows)
main_covered = sum(1 for r in main_rows if r["covered_23kpi"])
primary = {
"available": True,
"caliber": cal.PRIMARY_CALIBER,
"label": cal.PRIMARY_LABEL,
"map_id": primary_map.id,
"map_title": primary_map.title,
"map_status": primary_map.status,
"covered": main_covered,
"total": main_total,
"pct": _pct(main_covered, main_total),
"display": f"{main_covered}/{main_total}",
}
else:
primary = {
"available": False,
"caliber": cal.PRIMARY_CALIBER,
"label": cal.PRIMARY_LABEL,
"map_id": cal.PRIMARY_MAP_ID,
"covered": None,
"total": None,
"pct": None,
"display": "—",
}
# ---- 目标级 · 附注口径:全量 4 图,判据 = 有KPI挂接(23-KPI 挂接 或 map_objectives.kpis 非空)----
# 附注与主数同属地图 65 的账套(全量 4 图);无地图 65 的账套不适用,展示「—」而非 0。
footnote_total = len(rows)
footnote_covered = sum(1 for r in rows if r["covered_23kpi"] or r["has_kpi"])
footnote_available = primary_map is not None and bool(rows)
footnote = {
"available": footnote_available,
"caliber": cal.FOOTNOTE_CALIBER,
"label": cal.FOOTNOTE_LABEL,
"covered": footnote_covered if footnote_available else None,
"total": footnote_total if footnote_available else None,
"pct": _pct(footnote_covered, footnote_total) if footnote_available else None,
"display": f"{footnote_covered}/{footnote_total}" if footnote_available else "—",
}
# ---- KPI 级:active 按行(e1=73 / e2=44,合计 117;去重编码 102 另册)----
kpi_rows = (
db.query(KPIDefinition)
.filter(KPIDefinition.status == "active")
.all()
)
by_entity: dict = {}
codes = set()
for k in kpi_rows:
by_entity[k.entity_id] = by_entity.get(k.entity_id, 0) + 1
codes.add(k.kpi_code)
kpi_level = {
"caliber": cal.KPI_CALIBER,
"label": cal.KPI_LABEL,
"total": len(kpi_rows),
"by_entity": [
{"entity_id": eid, "count": cnt} for eid, cnt in sorted(by_entity.items())
],
"distinct_codes": len(codes),
"note": "按 kpi_code 去重为独立口径,不与行数相除或换算",
}
return {
"caliber_id": cal.CALIBER_ID,
"caliber_version": cal.CALIBER_VERSION,
"entity_id": entity_id,
"applicable": primary_map is not None,
"scope_note": "主口径仅覆盖地图 65(已发布);附注口径含归档图,判据不同,不得互换算",
"objective_level": {"primary": primary, "footnote": footnote},
"kpi_level": kpi_level,
}
@router.get("/objectives")
def completeness_objectives(
map_id: Optional[int] = None,
entity_id: int = Depends(get_entity_id),
db: Session = Depends(get_db),
):
"""数据完备度清单(目标级逐行):供空态清单与补齐排期使用。"""
_, maps = _entity_scopes(db, entity_id)
if map_id is not None:
maps = [m for m in maps if m.id == map_id]
rows, _ = _objective_rows(db, maps)
return {
"caliber_id": cal.CALIBER_ID,
"caliber_version": cal.CALIBER_VERSION,
"entity_id": entity_id,
"caliber": cal.FOOTNOTE_CALIBER,
"rows": rows,
}
+24 -1
View File
@@ -16,6 +16,15 @@ import logging
logger = logging.getLogger(__name__)
# ── C1 数据前置(改造单 v2 §3,硬约束)──────────────────────────────
# C_CUST_CONCENTRATION 的 69 个实测点含 2026-10 未来期与重复值;清理完成前:
# ① 相关目标节点显示「数据待核」、**不着色**(level=pending);
# ② 该 KPI 的实测值/达成率不得进入摘要条、节点详情、趋势图(96.3% 零出口)。
# 清理单据落地后删除本表即可恢复;不要在别处另写白名单。
DATA_HOLD_KPIS = {
"C_CUST_CONCENTRATION": "数据待核:C1 实测点含未来期与重复值,清理未完成",
}
router = APIRouter(prefix="/api/cma/maps", tags=["战略地图"],
dependencies=[Depends(require_role("ceo", "finance"))],
)
@@ -74,6 +83,18 @@ def list_maps(db: Session = Depends(get_db), entity_id: int = Depends(get_entity
maps = db.query(StrategicMap).filter(StrategicMap.entity_id == entity_id).order_by(StrategicMap.updated_at.desc()).all()
return {"data": [m_to_dict(m, db) for m in maps]}
@router.get("/strategy/revenue-caliber")
def strategy_revenue_caliber(entity_id: int = Depends(get_entity_id)):
"""战略地图摘要条 · 收入口径状态(真数据铁律:未闭合不出单一汇总数)。
2026-10-18 改造单 v2 §4:A1 收入口径未闭合前,前端只显示「口径待定」+
分业务位(酒类代销 / IT自营,amount=None ⇒ 显示「—/无数据」);
后端不得擅自选总额/净额,也不得把两块合并成一个「营业收入」。
"""
from app.data import revenue_caliber
return revenue_caliber.snapshot(entity_id)
@router.post("")
def create_map(data: dict, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
# 空白地图默认四层空结构(避免dimensions=NULL导致加载报错)
@@ -1026,7 +1047,9 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
if level and kpi_def.kpi_level != level:
continue
lv = latest_values.get(kpi_def.id, {})
actual = lv.get("actual_value")
# C1 数据前置:held KPI 的实测值一律不外泄(96.3% 零出口)
held = code in DATA_HOLD_KPIS
actual = None if held else lv.get("actual_value")
# 多粒度目标优先: 月目标(多数KPI月度) → 兼容legacy target_value (OpenCode审查#5)
target = kpi_def.target_monthly or kpi_def.target_value
# 判断红黄绿灯(绿≥90% / 黄60-90% / 红<60%)
+1
View File
@@ -0,0 +1 @@
"""口径判据数据包(战略地图数据完备度等,只读常量)。"""
@@ -0,0 +1,72 @@
"""战略地图数据完备度 · 口径定义(唯一真源引用,勿改判据/分母)
口径 ID : CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0
定义件 : /srv/hermes/mailbox/project/pending-tasks/_reports/
caliber-strategy-map-completeness-20261018.md
裁定 : ASK-20261018-15 / 任总 2026-10-18T23:07:11「放行14和15」
本模块只放**判据数据**(23-KPI 挂接关系 + 来源指纹),不存任何汇总数:
覆盖率一律由 API 运行时对库内 `map_objectives` / `kpi_definitions` 现算。
口径变更 = 重新立 ASK 并改本文件 + 定义件,禁止就地改分母。
"""
CALIBER_ID = "CMA-CALIBER-MAP-COMPLETENESS-20261018"
CALIBER_VERSION = "1.0"
# 主口径:仅已发布地图 65(三年战略规划 2027-2029)
PRIMARY_MAP_ID = 65
PRIMARY_CALIBER = "map65_23kpi_cover"
PRIMARY_LABEL = "地图65·已发布·判据=被23KPI集挂接"
FOOTNOTE_CALIBER = "map_all_kpi_attached"
FOOTNOTE_LABEL = "全量4图·含归档图·判据=有KPI挂接"
KPI_CALIBER = "active_kpi_rows"
KPI_LABEL = "KPI 行(active·按行)"
# 来源指纹(定义件 §5,任何人可复现)
SOURCE_FILES = {
"map-objectives-dump.csv":
"3896963ee7aff0492073f9b61491a10b10f3b131484e1f1006655d2c2f9bda78",
"a2-linkage-23kpi.csv":
"4494313269938663a197fabecaee644a4f4c66c84a02b725527c07e917f63971",
"active-kpi-118.csv":
"e37dcd699a06a18d5de9aa5b543b7a48b6d7d12c3e4c440f21186ea870cda1c1",
}
# 被 23-KPI 集挂接的战略目标(objective_id -> (map_id, name))
# 来源 a2-linkage-23kpi.csv「挂接类型非空」逐行,26 条(全量 4 图)。
LINKED_OBJECTIVES = {
1020: (53, "营收增长"),
1022: (53, "客户拓展"),
1024: (53, "运营效率"),
1025: (53, "数字化能力"),
1021: (53, "成本与现金流"),
1023: (53, "客户亲密"),
20888: (57, "营收目标"),
20889: (57, "建立渠道分级体系"),
21013: (61, "营收目标"),
21014: (61, "保障现金流安全"),
21016: (61, "产品领先"),
22556: (65, "营业收入"),
22562: (65, "高贡献线收入占比"),
22566: (65, "组合决策机制(负贡献四线退出转投)"),
22571: (65, "贡献率思维全员培训"),
22557: (65, "营业利润"),
22563: (65, "重点客户回款率"),
22567: (65, "月度经营闭环"),
22572: (65, "渠道经理「选品+算账」能力认证"),
22558: (65, "边际贡献率 CM"),
22568: (65, "库存去化"),
22573: (65, "CMA 月度经营驾驶舱"),
22559: (65, "现金周转期 CCC"),
22565: (65, "客户集中度"),
22574: (65, "贡献率/CCC/净现金入考核与激励"),
22570: (65, "数据地基(清脏+改瓶+智享通直连)"),
}
# 分层标签(UI 用,与前端 LAYER_CONFIG 一致)
LAYER_LABELS = {
"finance": "财务",
"customer": "客户",
"process": "内部流程",
"learning": "学习成长",
}
+63
View File
@@ -0,0 +1,63 @@
"""战略地图 · 收入口径(revenue caliber)状态 —— 单一事实源
口径来源:改造单 `project-20261018-cma-strategy-map-ux-revamp-fullstack` 增量 v2 §4。
截至本文件落地(2026-10-18T23:00),A1 收入口径**尚未闭合**:
- 业务称谓「代理」≠ 会计判定(总额法 total / 净额法 net / 混合 mixed 未裁);
- 财务净额件与项目侧「三问」总额判定并存,正由 A1 补充单裁决。
因此本模块只声明**状态**与**业务线维度**,不存任何金额:
- 接口照常返回 `revenue_caliber` 与分业务位(amount=None + status=pending);
- 前端不得写死任一方法,不得把两块合并成一个「营业收入」;
- 口径闭合后只改本文件(status/caliber/decided_by/closed_at),并回填金额取数逻辑,
不需要改前端。
铁律:未闭合前,页面与接口都**不得**输出单一汇总收入数。
"""
# 口径状态:unclosed / closed
STATUS = "unclosed"
CALIBER = None # closed 时取 "total" / "net" / "mixed"
CALIBER_LABEL = "口径待定"
DECIDED_BY = None # closed 时填裁决单/ASK 编号
CLOSED_AT = None # ISO 时间
REASON = (
"A1 收入口径尚未闭合(截至 2026-10-18T23:00):业务称谓「代理」≠ 会计判定,"
"总额法/净额法/混合法并存,待 A1 补充单裁决"
)
# 分业务(不得合并;amount 由 closed 后的取数逻辑回填)
BUSINESS_LINES = [
{"key": "liquor_agency", "label": "酒类代销"},
{"key": "it_self", "label": "IT自营"},
]
def snapshot(entity_id: int) -> dict:
"""返回对外口径快照(无金额;未闭合时 amount 一律 None)。"""
return {
"entity_id": entity_id,
"status": STATUS,
"caliber": CALIBER,
"caliber_label": CALIBER_LABEL,
"caliber_options": [
{"key": "total", "label": "总额法"},
{"key": "net", "label": "净额法"},
{"key": "mixed", "label": "混合法"},
],
"decided_by": DECIDED_BY,
"closed_at": CLOSED_AT,
"reason": REASON,
"businesses": [
{
"key": b["key"],
"label": b["label"],
"amount": None,
"unit": "万元",
"status": "pending",
"missing_reason": "收入口径未闭合" if STATUS != "closed" else None,
}
for b in BUSINESS_LINES
],
"allow_single_total": STATUS == "closed",
"note": "未闭合前不显示单一汇总数;分业务分列,不合并为「营业收入」",
}
+2 -1
View File
@@ -7,7 +7,7 @@ from fastapi.responses import JSONResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from dotenv import load_dotenv
from app.database import init_db, get_db
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, 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, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json, ext_input_ledgers, treasury, doc_center
from app.api import auth, kpis, kpi_governance, templates, maps, dashboard, data, alerts, ai_analysis, ai_suggestions, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, bot_suggestions, 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, ontology, bot_iron_law, analysis_results, expenses, cash, tax_compliance, verify, growth_quality, products, data_classification, value_sources, zero_based, derivation_rules, cash_classify, cashflow_structure, cash_vouchers_json, ext_input_ledgers, treasury, doc_center, map_completeness
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, require_auth_401
@@ -56,6 +56,7 @@ app.include_router(action_plans.router)
app.include_router(alignment.router)
app.include_router(org.router)
app.include_router(objectives.router)
app.include_router(map_completeness.router) # 战略地图数据完备度(口径 CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0)
app.include_router(versions.router)
app.include_router(budget.router)
app.include_router(value_sources.router)