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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user