fix(cma): entity isolation batch2 (roll-forward/check-timeout/by-id/cost-abc)
ci/woodpecker/push/main Pipeline failed
ci/woodpecker/push/main Pipeline failed
Scope: backend/app/api/{alerts,action_plans,budget,cost}.py + tests
Task: 0-P1-project-20260918-entity-isolation-batch2
This commit is contained in:
@@ -241,9 +241,11 @@ def update_plan(
|
||||
plan_id: int,
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""更新行动计划"""
|
||||
plan = db.query(ActionPlan).filter(ActionPlan.id == plan_id).first()
|
||||
"""更新行动计划(账套隔离: join KPI按企业过滤, 2026-09-18)"""
|
||||
plan = db.query(ActionPlan).join(KPIDefinition, KPIDefinition.id == ActionPlan.kpi_id).filter(
|
||||
ActionPlan.id == plan_id, KPIDefinition.entity_id == entity_id).first()
|
||||
if not plan:
|
||||
raise HTTPException(404, "计划不存在")
|
||||
|
||||
@@ -285,9 +287,10 @@ def update_plan(
|
||||
|
||||
|
||||
@router.delete("/{plan_id}")
|
||||
def delete_plan(plan_id: int, db: Session = Depends(get_db)):
|
||||
"""删除行动计划"""
|
||||
plan = db.query(ActionPlan).filter(ActionPlan.id == plan_id).first()
|
||||
def delete_plan(plan_id: int, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""删除行动计划(账套隔离: join KPI按企业过滤, 2026-09-18)"""
|
||||
plan = db.query(ActionPlan).join(KPIDefinition, KPIDefinition.id == ActionPlan.kpi_id).filter(
|
||||
ActionPlan.id == plan_id, KPIDefinition.entity_id == entity_id).first()
|
||||
if not plan:
|
||||
raise HTTPException(404, "计划不存在")
|
||||
db.delete(plan)
|
||||
|
||||
+17
-12
@@ -45,9 +45,10 @@ def resolve_alert(alert_id: int, data: dict, db: Session = Depends(get_db), enti
|
||||
|
||||
|
||||
@router.post("/{alert_id}/process")
|
||||
def process_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""标记预警为处理中"""
|
||||
alert = db.query(KPIAlert).filter(KPIAlert.id == alert_id).first()
|
||||
def process_alert(alert_id: int, data: dict, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""标记预警为处理中(账套隔离: join kpi_definitions 按企业过滤, 2026-09-18)"""
|
||||
alert = db.query(KPIAlert).join(KPIDefinition, KPIDefinition.id == KPIAlert.kpi_id).filter(
|
||||
KPIAlert.id == alert_id, KPIDefinition.entity_id == entity_id).first()
|
||||
if not alert:
|
||||
raise HTTPException(404, "预警不存在")
|
||||
if alert.status == "resolved":
|
||||
@@ -63,9 +64,10 @@ def process_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/{alert_id}/escalate")
|
||||
def escalate_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""升级预警级别"""
|
||||
alert = db.query(KPIAlert).filter(KPIAlert.id == alert_id).first()
|
||||
def escalate_alert(alert_id: int, data: dict, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""升级预警级别(账套隔离: join kpi_definitions 按企业过滤, 2026-09-18)"""
|
||||
alert = db.query(KPIAlert).join(KPIDefinition, KPIDefinition.id == KPIAlert.kpi_id).filter(
|
||||
KPIAlert.id == alert_id, KPIDefinition.entity_id == entity_id).first()
|
||||
if not alert:
|
||||
raise HTTPException(404, "预警不存在")
|
||||
assignee = data.get("assignee")
|
||||
@@ -82,11 +84,13 @@ def escalate_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/check-timeout")
|
||||
def check_alert_timeout(db: Session = Depends(get_db)):
|
||||
"""超时预警检测 — 超过24小时未处理的pending预警自动升级为红色"""
|
||||
def check_alert_timeout(db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""超时预警检测 — 超过24小时未处理的pending预警自动升级为红色
|
||||
(账套隔离: join kpi_definitions 按企业过滤, 2026-09-18 — 原为全账套批处理)"""
|
||||
from datetime import datetime, timedelta
|
||||
cutoff = datetime.now() - timedelta(hours=24)
|
||||
timeout_alerts = db.query(KPIAlert).filter(
|
||||
timeout_alerts = db.query(KPIAlert).join(KPIDefinition, KPIDefinition.id == KPIAlert.kpi_id).filter(
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
KPIAlert.status == "pending",
|
||||
KPIAlert.created_at < cutoff,
|
||||
KPIAlert.alert_level != "red",
|
||||
@@ -102,9 +106,10 @@ def check_alert_timeout(db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/{alert_id}/create-action-plan")
|
||||
def create_action_plan_from_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""从预警创建改善行动计划"""
|
||||
alert = db.query(KPIAlert).filter(KPIAlert.id == alert_id).first()
|
||||
def create_action_plan_from_alert(alert_id: int, data: dict, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""从预警创建改善行动计划(账套隔离: join kpi_definitions 按企业过滤, 2026-09-18)"""
|
||||
alert = db.query(KPIAlert).join(KPIDefinition, KPIDefinition.id == KPIAlert.kpi_id).filter(
|
||||
KPIAlert.id == alert_id, KPIDefinition.entity_id == entity_id).first()
|
||||
if not alert:
|
||||
raise HTTPException(404, "预警不存在")
|
||||
if alert.action_plan_linked_id:
|
||||
|
||||
@@ -534,12 +534,14 @@ def set_budget_config(
|
||||
def budget_roll_forward(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(require_auth),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""
|
||||
滚动预算自动延展:
|
||||
- 删除最早一个月的预测数据
|
||||
- 新增未来一个月的预测数据(取最近三个月均值)
|
||||
- 返回延展结果
|
||||
账套隔离:只处理会话账套(join kpi_definitions 按企业过滤),2026-09-18
|
||||
"""
|
||||
from app.models import SystemConfig
|
||||
cfg = db.query(SystemConfig).filter(SystemConfig.config_key == "budget_mode").first()
|
||||
@@ -557,8 +559,11 @@ def budget_roll_forward(
|
||||
now = datetime.now()
|
||||
current_year, current_month = now.year, now.month
|
||||
|
||||
# 获取所有active的预算记录
|
||||
plans = db.query(BudgetPlan).filter(BudgetPlan.status == "active").all()
|
||||
# 获取本账套 active 的预算记录(账套隔离: join KPI 按企业过滤, 2026-09-18 — 原为全账套)
|
||||
plans = (db.query(BudgetPlan)
|
||||
.join(KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id)
|
||||
.filter(BudgetPlan.status == "active", KPIDefinition.entity_id == entity_id)
|
||||
.all())
|
||||
|
||||
# 按KPI分组
|
||||
from collections import defaultdict
|
||||
@@ -598,6 +603,7 @@ def budget_roll_forward(
|
||||
|
||||
if not existing:
|
||||
bp = BudgetPlan(
|
||||
entity_id=entity_id,
|
||||
kpi_id=kpi_id,
|
||||
period=new_period,
|
||||
budget_value=avg_value,
|
||||
|
||||
+10
-5
@@ -176,14 +176,19 @@ def create_actual_cost(data: dict, db: Session = Depends(get_db),
|
||||
|
||||
# ============================================================
|
||||
# ABC 作业成本
|
||||
# 注:ABC 作业中心/分配为单实体演示数据(ab_activities/ab_allocations 全为 entity 1,
|
||||
# 历史演示用途),OpenCode 多租户审查 2026-09-07 判定暂不强制按账套隔离;若启用多企业 ABC 需补 entity 过滤。
|
||||
# 账套隔离(2026-09-18,0-P1-project-20260918-entity-isolation-batch2):
|
||||
# GET /abc/activities 原为全表取(生产 10 行全属 entity2)→ 酣客会话可见博海 ABC 作业中心;
|
||||
# 现按会话账套过滤(AbcActivity.entity_id)。
|
||||
# OpenCode 多租户审查 2026-09-07 的「单实体演示数据暂不强制隔离」豁免就此撤销。
|
||||
# ============================================================
|
||||
|
||||
@router.get("/abc/activities")
|
||||
def list_abc_activities(db: Session = Depends(get_db)):
|
||||
"""查询ABC作业中心列表"""
|
||||
items = db.query(AbcActivity).order_by(AbcActivity.activity_code).all()
|
||||
def list_abc_activities(db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""查询ABC作业中心列表(账套隔离:只返回本账套)"""
|
||||
items = (db.query(AbcActivity)
|
||||
.filter(AbcActivity.entity_id == entity_id)
|
||||
.order_by(AbcActivity.activity_code)
|
||||
.all())
|
||||
return {"data": items}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user