From 27b5b0da476af61f66fd38a76da5524090d42f43 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Sun, 23 Aug 2026 17:53:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E7=A7=9F=E6=88=B7=E9=9A=94?= =?UTF-8?q?=E7=A6=BBP2=E6=89=B91=20=E2=80=94=20OKR=E5=9F=9F+kpi=5Fvalues?= =?UTF-8?q?=E5=8A=A0entity=5Fid=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 模型: Objective/KR/ObjectiveKPI/KPIValue 加 entity_id - DB: 4表加列; kpi_values 1610条按kpi_id回填(1269酣客/341博海) - OKR域: 测试O删除重建; okr list/get/create + ontology trace/objectives 按token企业隔离(跨企业404) - KPIValue写入: data/bot_bridge/bot_kpis 创建时带entity_id - 验证: 酣客创建OKR博海不可见; 跨企业读404; pytest 451通过(expenses单跑37通过为既有排序flaky) --- backend/app/api/bot_bridge.py | 2 +- backend/app/api/bot_kpis.py | 1 + backend/app/api/data.py | 3 +++ backend/app/api/okr.py | 19 +++++++++++-------- backend/app/api/ontology.py | 11 +++++++---- backend/app/models/__init__.py | 4 ++++ 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/backend/app/api/bot_bridge.py b/backend/app/api/bot_bridge.py index 0f96f383..dc4a39d9 100644 --- a/backend/app/api/bot_bridge.py +++ b/backend/app/api/bot_bridge.py @@ -528,7 +528,7 @@ def bot_import_excel( errors.append(f"第{idx+2}行: KPI编码 '{kpi_code}' 不存在,跳过") continue - kv = KPIValue(kpi_id=kpi.id, period=period, actual_value=val, + kv = KPIValue(kpi_id=kpi.id, entity_id=kpi.entity_id, period=period, actual_value=val, source_batch=hashlib.md5(f"{datetime.now()}".encode()).hexdigest()[:12]) db.add(kv) count += 1 diff --git a/backend/app/api/bot_kpis.py b/backend/app/api/bot_kpis.py index ddefa772..de14d365 100644 --- a/backend/app/api/bot_kpis.py +++ b/backend/app/api/bot_kpis.py @@ -161,6 +161,7 @@ def update_bot_kpi_value( else: val = KPIValue( kpi_id=kpi_id, + entity_id=kpi.entity_id if kpi else None, # 账套隔离 P2 period=period, actual_value=actual_value, source_type="manual", diff --git a/backend/app/api/data.py b/backend/app/api/data.py index 8125eb1d..ba107084 100644 --- a/backend/app/api/data.py +++ b/backend/app/api/data.py @@ -39,6 +39,7 @@ async def import_excel(file: UploadFile = File(...), from app.models import KPIDefinition kpi_map = {k.kpi_code: k.id for k in db.query(KPIDefinition).all()} + kpi_entity_map = {k.kpi_code: k.entity_id for k in db.query(KPIDefinition).all()} # 账套隔离 P2 batch = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:12] count = 0 @@ -59,6 +60,7 @@ async def import_excel(file: UploadFile = File(...), db.add(KPIValue( kpi_id=kid, + entity_id=kpi_entity_map.get(kpi_code), period=period, actual_value=float(value), source_type="excel", @@ -298,6 +300,7 @@ async def import_excel_smart( db.add(KPIValue( kpi_id=kpis[kpi_code].id, + entity_id=kpis[kpi_code].entity_id, # 账套隔离 P2 period=raw_period, actual_value=val, source_type="excel", diff --git a/backend/app/api/okr.py b/backend/app/api/okr.py index 5198c268..bc654f16 100644 --- a/backend/app/api/okr.py +++ b/backend/app/api/okr.py @@ -7,8 +7,9 @@ from sqlalchemy import func from datetime import datetime, timedelta from typing import Optional from app.database import get_db +from app.deps import get_entity_id from app.auth_middleware import require_role -from app.models import Objective, ActionPlan +from app.models import Objective, ActionPlan, KR, ObjectiveKPI, KPIDefinition router = APIRouter(prefix="/api/cma/okr", tags=["OKR目标管理"], dependencies=[Depends(require_role("ceo", "finance", "it"))], @@ -21,9 +22,10 @@ def list_objectives( dimension: Optional[str] = Query(None), status: Optional[str] = Query(None), db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), ): - """列出OKR目标""" - q = db.query(Objective) + """列出OKR目标(账套隔离: 按token企业, 2026-08-23 P2)""" + q = db.query(Objective).filter(Objective.entity_id == entity_id) if quarter: q = q.filter(Objective.quarter == quarter) if dimension: @@ -55,8 +57,9 @@ def list_objectives( def create_objective( data: dict, db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), ): - """创建OKR目标(支持JSON Body和Query参数两种方式)""" + """创建OKR目标(支持JSON Body和Query参数两种方式)— 账套隔离: 强制token企业""" # 兼容旧版Query参数 title = data.get("title") or "" quarter = data.get("quarter") or "" @@ -66,7 +69,7 @@ def create_objective( if not title or not quarter: raise HTTPException(422, "缺少必填字段: title, quarter") obj = Objective(title=title, quarter=quarter, description=description, - dimension=dimension, owner=owner) + dimension=dimension, owner=owner, entity_id=entity_id) db.add(obj) db.commit() db.refresh(obj) @@ -74,9 +77,9 @@ def create_objective( @router.get("/{obj_id}") -def get_objective(obj_id: int, db: Session = Depends(get_db)): - """获取单个OKR详情""" - obj = db.query(Objective).filter(Objective.id == obj_id).first() +def get_objective(obj_id: int, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)): + """获取单个OKR详情(账套隔离: 跨企业404)""" + obj = db.query(Objective).filter(Objective.id == obj_id, Objective.entity_id == entity_id).first() if not obj: raise HTTPException(404, "目标不存在") krs = db.query(ActionPlan).filter(ActionPlan.objective_id == obj_id).all() diff --git a/backend/app/api/ontology.py b/backend/app/api/ontology.py index 9655959d..27ac9b20 100644 --- a/backend/app/api/ontology.py +++ b/backend/app/api/ontology.py @@ -10,6 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import Optional from app.database import get_db +from app.deps import get_entity_id from app.auth_middleware import require_role from app.models import Objective, KPIDefinition, ObjectiveKPI, KPISubjectMap, KR, Subject @@ -27,9 +28,10 @@ def _num(v): def trace_ontology( objective_id: int = Query(..., description="OKR目标ID, 从O→KPI→科目逐层追溯"), db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), ): - """本体追溯链: O(目标) → KPI(指标) → 科目(数据)""" - obj = db.query(Objective).filter(Objective.id == objective_id).first() + """本体追溯链: O(目标) → KPI(指标) → 科目(数据) — 账套隔离 (2026-08-23 P2)""" + obj = db.query(Objective).filter(Objective.id == objective_id, Objective.entity_id == entity_id).first() if not obj: raise HTTPException(404, "目标不存在") @@ -122,9 +124,10 @@ def trace_ontology( def list_ontology_objectives( quarter: Optional[str] = Query(None, description="筛选季度: 2026Q3"), db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), ): - """所有OKR目标的三层链路概览(前端OKR页用)""" - q = db.query(Objective) + """所有OKR目标的三层链路概览(前端OKR页用) — 账套隔离 (2026-08-23 P2)""" + q = db.query(Objective).filter(Objective.entity_id == entity_id) if quarter: q = q.filter(Objective.quarter == quarter) objs = q.order_by(Objective.quarter.desc(), Objective.id).all() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 407fcac3..928bd020 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -99,6 +99,7 @@ class KPIValue(Base): """KPI实际值""" __tablename__ = "kpi_values" id = Column(Integer, primary_key=True, index=True) + entity_id = Column(Integer, default=None, comment="企业ID (P2多租户隔离 2026-08-23, 按kpi_id回填)") kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False) period = Column(String(20), nullable=False, comment="期间 2026-05") actual_value = Column(Float, nullable=True, comment="实际值") @@ -193,6 +194,7 @@ class Objective(Base): """OKR目标""" __tablename__ = "objectives" id = Column(Integer, primary_key=True, index=True) + entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)") title = Column(String(200), nullable=False, comment="目标标题") description = Column(Text, nullable=True, comment="目标描述") dimension = Column(String(50), nullable=True, comment="关联维度: finance/customer/process/learning") @@ -703,6 +705,7 @@ class ObjectiveKPI(Base): """KPI↔O支撑 — 目标由哪些KPI度量""" __tablename__ = "objective_kpi" id = Column(Integer, primary_key=True, index=True) + entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)") objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=False, comment="OKR目标ID") kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="KPI ID") weight = Column(Numeric(5, 2), default=1.00, comment="支撑权重") @@ -713,6 +716,7 @@ class KR(Base): """关键结果KR — OKR完整化 (O→KR→KPI)""" __tablename__ = "krs" id = Column(Integer, primary_key=True, index=True) + entity_id = Column(Integer, default=1, comment="企业ID (P2多租户隔离 2026-08-23)") objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=False, comment="OKR目标ID") title = Column(String(200), nullable=False, comment="KR标题") metric_kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=True, comment="度量KPI ID")