Files
Hermes CI Fix 94aeb14e95 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
2026-08-28 18:03:47 +08:00

193 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""数据库配置"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import inspect
import os
import logging
logger = logging.getLogger("cma")
DB_USER = os.getenv("CMA_DB_USER", "cma_user")
DB_PASS = os.getenv("CMA_DB_PASS", "cma_pass_2026")
DB_HOST = os.getenv("CMA_DB_HOST", "127.0.0.1")
DB_PORT = os.getenv("CMA_DB_PORT", "3306")
DB_NAME = os.getenv("CMA_DB_NAME", "cma")
DATABASE_URL = "mysql+pymysql://%(user)s:%(password)s@%(host)s:%(port)s/%(name)s?charset=utf8mb4" % {
"user": DB_USER,
"password": DB_PASS,
"host": DB_HOST,
"port": DB_PORT,
"name": DB_NAME,
}
_engine = None
_SessionLocal = None
Base = declarative_base()
def get_engine():
global _engine
if _engine is None:
_engine = create_engine(DATABASE_URL, echo=False, pool_size=5, max_overflow=10, pool_pre_ping=True)
return _engine
def get_session_local():
global _SessionLocal
if _SessionLocal is None:
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=get_engine())
return _SessionLocal
def get_db():
db = get_session_local()()
try:
yield db
finally:
db.close()
def init_db():
import app.models
Base.metadata.create_all(bind=get_engine())
logger.info("CMA数据库已初始化")
# ── 初始化组织层级示例数据 ──
try:
inspector = inspect(get_engine())
if "org_nodes" in inspector.get_table_names():
Session = get_session_local()
session = Session()
try:
cnt = session.query(app.models.OrgNode).count()
if cnt == 0:
_seed_org_data(session)
finally:
session.close()
except Exception as e:
logger.warning(f"组织数据初始化跳过: {e}")
# ── user_entities 授权表初始化(账套模式平滑迁移)──
# 首次建表(表空)时,给存量用户默认授权所有 active 企业,保证现有登录不丢权限
try:
inspector = inspect(get_engine())
if "user_entities" in inspector.get_table_names():
Session = get_session_local()
session = Session()
try:
from app.models import UserEntity, User, Entity
cnt = session.query(UserEntity).count()
if cnt == 0:
users = session.query(User).all()
entities = session.query(Entity).filter(Entity.status == "active").all()
if users and entities:
for u in users:
for e in entities:
exists = session.query(UserEntity).filter(
UserEntity.user_id == u.id,
UserEntity.entity_id == e.id,
).first()
if not exists:
session.add(UserEntity(user_id=u.id, entity_id=e.id, granted_by=None))
session.commit()
logger.info(f"user_entities初始化: {len(users)}用户 × {len(entities)}企业")
finally:
session.close()
except Exception as e:
logger.warning(f"user_entities初始化跳过: {e}")
# ── scenario_suggestions 告警场景建议 seed2026-08-28 告警归因 P1-③)──
# 幂等:仅补缺失的 alert_type,不覆盖已有模板
try:
inspector = inspect(get_engine())
if "scenario_suggestions" in inspector.get_table_names():
Session = get_session_local()
session = Session()
try:
from app.models import ScenarioSuggestion
existing_types = {s.alert_type for s in session.query(ScenarioSuggestion).all()}
seeds = [
dict(alert_type="cash_low", title="现金流紧张 — 加强回款催收",
description="现金余额接近警戒线,建议优先处理应收款项,压缩非紧急支出。",
action_template="1. 列出未来30天应收清单,逐笔催收\n2. 暂停非紧急采购/费用支出\n3. 与银行沟通短期授信额度",
priority="high", sort_order=1),
dict(alert_type="cash_critical", title="现金流危急 — 立即止血",
description="现金余额已低于安全阈值,存在断流风险,需要立即采取止血措施。",
action_template="1. 冻结一切非必要支出\n2. 高管紧急复盘资金计划\n3. 启动应收账款特别催收\n4. 评估短期融资",
priority="high", sort_order=2),
dict(alert_type="cost_high", title="成本超支 — 核查费用构成",
description="实际成本超出预算,建议拆解到科目明细定位超支源头。",
action_template="1. 查看科目明细拆解,定位超支前3科目\n2. 分析价差/量差成因(单价上涨/用量增加)\n3. 对可控费用制定压降方案",
priority="medium", sort_order=3),
dict(alert_type="revenue_drop", title="收入下滑 — 追量提效",
description="实际收入低于预算,建议从子KPI量级分解查找差距来源。",
action_template="1. 查看子KPI拆解,定位量差最大维度\n2. 分析客户/渠道/产品线缺口\n3. 制定增量获客或转化提升方案",
priority="medium", sort_order=4),
]
added = 0
for s in seeds:
if s["alert_type"] not in existing_types:
session.add(ScenarioSuggestion(**s))
added += 1
if added:
session.commit()
logger.info(f"scenario_suggestions seed: 新增{added}条场景建议模板")
finally:
session.close()
except Exception as e:
logger.warning(f"scenario_suggestions seed跳过: {e}")
def _seed_org_data(db_session):
"""插入5层级组织示例数据"""
from app.models import OrgNode
# 1. 集团
g = OrgNode(id=1, parent_id=None, name="博海网络科技", code="BH", level=1, sort_order=1, enabled=1)
db_session.add(g)
db_session.flush()
# 2. 事业部
depts = [
OrgNode(parent_id=1, name="技术事业部", code="TECH", level=2, sort_order=1, enabled=1),
OrgNode(parent_id=1, name="销售事业部", code="SALES", level=2, sort_order=2, enabled=1),
OrgNode(parent_id=1, name="财务事业部", code="FIN", level=2, sort_order=3, enabled=1),
]
db_session.add_all(depts)
db_session.flush()
# 3. 区域/部门级
regions = [
OrgNode(parent_id=2, name="华南区域", code="SC", level=3, sort_order=1, enabled=1),
OrgNode(parent_id=2, name="华东区域", code="EC", level=3, sort_order=2, enabled=1),
OrgNode(parent_id=3, name="销售一部", code="S1", level=3, sort_order=1, enabled=1),
OrgNode(parent_id=3, name="销售二部", code="S2", level=3, sort_order=2, enabled=1),
]
db_session.add_all(regions)
db_session.flush()
# 4. 部门
departs = [
OrgNode(parent_id=5, name="研发部", code="RD", level=4, sort_order=1, enabled=1),
OrgNode(parent_id=5, name="实施部", code="IMP", level=4, sort_order=2, enabled=1),
OrgNode(parent_id=5, name="运维部", code="OPS", level=4, sort_order=3, enabled=1),
OrgNode(parent_id=6, name="前端研发", code="FE", level=4, sort_order=1, enabled=1),
OrgNode(parent_id=6, name="后端研发", code="BE", level=4, sort_order=2, enabled=1),
OrgNode(parent_id=8, name="KA客户部", code="KA", level=4, sort_order=1, enabled=1),
]
db_session.add_all(departs)
db_session.flush()
# 5. 班组
teams = [
OrgNode(parent_id=12, name="前端组", code="FE-TEAM", level=5, sort_order=1, enabled=1),
OrgNode(parent_id=12, name="后端组", code="BE-TEAM", level=5, sort_order=2, enabled=1),
OrgNode(parent_id=12, name="测试组", code="QA-TEAM", level=5, sort_order=3, enabled=1),
OrgNode(parent_id=13, name="实施一组", code="IMP1", level=5, sort_order=1, enabled=1),
OrgNode(parent_id=13, name="实施二组", code="IMP2", level=5, sort_order=2, enabled=1),
]
db_session.add_all(teams)
db_session.commit()
logger.info("组织层级示例数据已初始化")