feat: 预算系统6项技术改进(告警归因/实际值自动归集/真零基/派生规则/告警路径统一/现金流分类)

P1-③ 告警归因: budget_deviation_alerts+alert_type/attribution/scenario_id, 归因引擎alert_attribution.py(子KPI/科目/量价差/趋势), deviation-check统一写归因+场景, GET /deviation-alerts/{id}/attribution详情(旧告警现场组装)
P1-④ 实际值自动归集: kpi_value_sources/kpi_value_collect_logs表+CRUD+试跑+覆盖率, 采集器kpi_value_collector.py(voucher_details/进销存/cash_plans按entity+period汇总, 幂等upsert不覆盖人工), crontab每日06:30
P2-① 真零基: budget_zero_based_items逐项论证表+generate, method-comparison有论证项逐项求和is_demo=false否则fallback
P2-② 派生规则: budget_derivation_rules配置表, apply-method优先读规则rule_source=configured
P2-⑤ 告警双路径合并: deviation_engine.build_deviation_alert统一函数, 方向列表配置化kpi_alert_higher_better+alert-direction接口
P2-⑥ 现金流分类: cash_plan_classify_rules规则表+cash_plan_unclassified待分类队列, sync-cash-plans未命中进队列不静默跳过
新增: GET /kpis/{kpi_id}/values + 前端kpiApi.values(归集标签页数据源), scenario_suggestions幂等seed(init_db)
测试: test_budget_tech_improve.py 15用例, 预算相关96 passed, 全量646 passed
This commit is contained in:
Hermes CI Fix
2026-08-28 18:03:47 +08:00
parent 3bc68fa1c6
commit 94aeb14e95
16 changed files with 3164 additions and 118 deletions
+106
View File
@@ -524,6 +524,9 @@ class BudgetDeviationAlert(Base):
alert_level = Column(String(20), default="warning", comment="warning/critical")
status = Column(String(20), default="open", comment="open/resolved/ignored")
suggestion = Column(String(500), nullable=True, comment="处理建议")
alert_type = Column(String(30), nullable=True, comment="归因场景: cost_high/revenue_drop/cash_low/cash_critical (2026-08-28 告警归因P1-③)")
attribution = Column(JSON, nullable=True, comment="归因JSON: 子KPI拆解+科目拆解+量价差+趋势 (2026-08-28)")
scenario_id = Column(Integer, nullable=True, comment="FK scenario_suggestions.id 场景建议 (2026-08-28)")
created_at = Column(DateTime, server_default=func.now())
@@ -805,3 +808,106 @@ class KR(Base):
monthly_milestones = Column(JSON, nullable=True, comment="月度里程碑: [{\"month\":\"2026-07\",\"label\":\"...\",\"status\":\"completed\"}]")
sort_order = Column(Integer, default=0, comment="排序")
created_at = Column(DateTime, server_default=func.now())
# ============================================================
# 预算系统技术改进 (2026-08-28 yanxue-budget-tech-improve)
# ① kpi_value_sources/kpi_value_collect_logs: 实际值自动归集 P1-④
# ② budget_zero_based_items: 真零基逐项论证 P2-①
# ③ budget_derivation_rules: 派生规则可配置 P2-②
# ④ cash_plan_classify_rules/cash_plan_unclassified: 现金流分类规则 P2-⑥
# ============================================================
class KPIValueSource(Base):
"""KPI实际值取数映射 — 自动归集源头 (P1-④ 2026-08-28)"""
__tablename__ = "kpi_value_sources"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="目标KPI")
source_table = Column(String(50), nullable=False, comment="源头表: voucher_details/product_inventory/product_inventory_detail/cash_plans")
source_field = Column(String(50), nullable=False, comment="金额字段: credit_amount/debit_amount/amount/qty")
aggregate = Column(String(10), default="sum", comment="sum/avg/count/max/min")
filter_rule = Column(JSON, nullable=True, comment="过滤: {\"subject_code\":\"6601\",\"direction\":\"credit\"}")
period_field = Column(String(50), default="period", comment="期间字段: period/voucher_date")
unit_conversion = Column(Float, default=1, comment="单位倍率(元→万元/10000)")
status = Column(String(20), default="active", comment="active/inactive")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
__table_args__ = (UniqueConstraint("entity_id", "kpi_id", "source_table", name="uk_source"),)
class KPIValueCollectLog(Base):
"""实际值采集日志 — 每次自动归集记录 (P1-④ 2026-08-28)"""
__tablename__ = "kpi_value_collect_logs"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID")
kpi_id = Column(Integer, nullable=False, comment="KPI ID")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
source_table = Column(String(50), nullable=False, comment="源头表")
collected_value = Column(Float, nullable=True, comment="采集到的值")
status = Column(String(20), default="success", comment="success/failed")
message = Column(String(500), nullable=True, comment="说明/错误信息")
collected_at = Column(DateTime, server_default=func.now())
class BudgetZeroBasedItem(Base):
"""零基预算逐项论证项 (P2-① 2026-08-28)"""
__tablename__ = "budget_zero_based_items"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
period = Column(String(20), nullable=False, comment="期间 YYYY-MM")
item_name = Column(String(200), nullable=False, comment="费用科目名")
item_category = Column(String(20), default="discretionary", comment="fixed/variable/discretionary")
base_value = Column(Float, nullable=False, comment="基准值(上年/上月实际)")
justification = Column(Text, nullable=True, comment="逐项论证理由(为何保留/削减/取消)")
proposed_value = Column(Float, nullable=False, comment="论证后金额")
status = Column(String(20), default="draft", comment="draft/approved")
created_by = Column(String(100), nullable=True, comment="创建人")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class BudgetDerivationRule(Base):
"""KPI派生规则 — apply-method 可配置派生 (P2-② 2026-08-28)"""
__tablename__ = "budget_derivation_rules"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="目标KPI")
rule_type = Column(String(30), nullable=False, comment="incremental/percentage_of/formula")
base_kpi_id = Column(Integer, nullable=True, comment="来源KPI(percentage_of用)")
params = Column(JSON, nullable=True, comment="{\"rate\":0.02,\"field\":\"net_profit\"}")
formula_text = Column(String(500), nullable=True, comment="可读公式说明")
status = Column(String(20), default="active", comment="active/inactive")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
__table_args__ = (UniqueConstraint("entity_id", "kpi_id", "rule_type", name="uk_rule"),)
class CashPlanClassifyRule(Base):
"""现金流收付分类规则 — KPI→receive/pay 可维护 (P2-⑥ 2026-08-28)"""
__tablename__ = "cash_plan_classify_rules"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, nullable=True, comment="精确匹配KPI,优先")
kpi_code_pattern = Column(String(200), nullable=True, comment="关键词/编码模式匹配,兜底")
plan_type = Column(String(10), nullable=False, comment="receive/pay")
priority = Column(Integer, default=10, comment="匹配顺序,小优先")
status = Column(String(20), default="active", comment="active/inactive")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class CashPlanUnclassified(Base):
"""现金流待分类KPI队列 — 无法判别的KPI不静默跳过 (P2-⑥ 2026-08-28)"""
__tablename__ = "cash_plan_unclassified"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, nullable=False, comment="企业ID(多租户隔离)")
kpi_id = Column(Integer, nullable=False, comment="KPI ID")
kpi_name = Column(String(200), nullable=True, comment="KPI名称")
period = Column(String(20), nullable=True, comment="期间")
budget_value = Column(Float, nullable=True, comment="预算值")
reason = Column(String(200), nullable=True, comment="无法分类原因")
status = Column(String(20), default="pending", comment="pending/classified/ignored")
created_at = Column(DateTime, server_default=func.now())
resolved_at = Column(DateTime, nullable=True)