diff --git a/backend/app/api/budget.py b/backend/app/api/budget.py index 087aebb8..35f5fa47 100644 --- a/backend/app/api/budget.py +++ b/backend/app/api/budget.py @@ -908,6 +908,9 @@ def check_budget_deviation( ) db.add(alert) alerts_generated += 1 + # 立即 flush:autoflush=False 下,下一次循环的同 KPI 去重查询 + # (existing_alert) 需看到本次已 add 的行,否则同期间同 KPI 多预算计划会重复告警 + db.flush() alerts.append({ "kpi_id": bp.kpi_id, diff --git a/backend/tests/test_budget.py b/backend/tests/test_budget.py index f6916534..55e27569 100644 --- a/backend/tests/test_budget.py +++ b/backend/tests/test_budget.py @@ -461,6 +461,34 @@ class TestBudgetDeviationCheck: json={"period": "2026-06", "threshold": 20}) assert resp.json()["alerts_generated"] == 0 + def test_deviation_check_dedups_same_kpi_multiple_plans(self, client: TestClient, db: Session): + """回归:同期间同 KPI 有多条 active 预算计划时,只应生成 1 条 open 告警 + + 根因:database.py sessionmaker(autoflush=False),端点内循环的 existing_alert + 去重查询查不到本次刚 add 未 flush 的行 → 第二个计划重复 INSERT。 + """ + create_test_user(db) + token = get_token_for_user(client) + kpi = create_test_kpi(db, kpi_code="BUDGET_ALERT_DUP") + for _ in range(2): # 两条同 KPI 计划(模拟数据面重复计划) + db.add(BudgetPlan(kpi_id=kpi.id, period="2026-06", budget_value=100.0, + budget_year=2026, budget_month=6, status="active")) + from app.models import KPIValue + db.add(KPIValue(kpi_id=kpi.id, period="2026-06", actual_value=150.0)) # +50% + db.commit() + + resp = client.post(f"{self.BASE}/deviation-check", headers=auth_header(token), + json={"period": "2026-06", "threshold": 20}) + assert resp.status_code == 200 + assert resp.json()["alerts_generated"] == 1 + from app.models import BudgetDeviationAlert + n = db.query(BudgetDeviationAlert).filter( + BudgetDeviationAlert.kpi_id == kpi.id, + BudgetDeviationAlert.period == "2026-06", + BudgetDeviationAlert.status == "open", + ).count() + assert n == 1, f"同 KPI 重复告警未去重,落库 {n} 条" + class TestBudgetDeviationAlerts: """偏差预警记录查询/更新"""