feat(knowledge): 企业记忆分区——知识三表entity_id多租户隔离 (entity-memory-partition P1)
- knowledge_events/knowledge_summaries: entity_id NOT NULL DEFAULT 1 + 索引(企业记忆抽屉) - knowledge_articles: entity_id 可空——NULL=全局共享(CMA教学知识), 有值=企业专属 - knowledge_service 全链路 entity_id: extract_events(KPIAlert直过滤+OperationLog经user_entities归属)/_kpi_snapshot(修跨企业KPI泄漏)/generate_summary/get_last_summary/run_*/sync - api/knowledge.py 8端点 Depends(get_entity_id) 强制过滤;详情越权404 - api/knowledge_articles.py 全局∪本企业查询;企业专属越权404 - 生产DB三表ALTER+索引已完成; 存量38篇教学文章保持NULL全局共享 - 测试 test_security_knowledge_partition.py 8条(双实体互查越权/生成自动带entity/全局文章双可见) - 方案文档 docs/knowledge-entity-partition-plan-20260903.md(附评审决策点)
This commit is contained in:
@@ -15,6 +15,7 @@ from typing import Optional
|
||||
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_role
|
||||
from app.deps import get_entity_id
|
||||
from app.models import KnowledgeEvent, KnowledgeSummary
|
||||
from app.services.knowledge_service import (
|
||||
generate_summary_sync,
|
||||
@@ -59,18 +60,22 @@ def list_summaries(
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取摘要列表,按层级筛选,按时间倒序"""
|
||||
"""获取摘要列表,按层级筛选,按时间倒序(按企业抽屉隔离)"""
|
||||
query = db.query(KnowledgeSummary)
|
||||
query = query.filter(KnowledgeSummary.entity_id == entity_id)
|
||||
if level:
|
||||
query = query.filter(KnowledgeSummary.level == level)
|
||||
query = query.order_by(desc(KnowledgeSummary.id)).offset(offset).limit(limit)
|
||||
total = db.query(func.count(KnowledgeSummary.id)).select_from(KnowledgeSummary)
|
||||
total = total.filter(KnowledgeSummary.entity_id == entity_id)
|
||||
if level:
|
||||
total = total.filter(KnowledgeSummary.level == level)
|
||||
total = total.scalar()
|
||||
return {
|
||||
"total": total,
|
||||
"entity_id": entity_id,
|
||||
"items": [summary_to_dict(s) for s in query.all()],
|
||||
}
|
||||
|
||||
@@ -79,18 +84,30 @@ def list_summaries(
|
||||
def latest_summary(
|
||||
level: str = Query("daily", description="层级: daily/weekly/monthly/cumulative"),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取指定层级的最新摘要"""
|
||||
s = get_last_summary(db, level)
|
||||
"""获取指定层级的最新摘要(按企业隔离)"""
|
||||
s = get_last_summary(db, level, entity_id)
|
||||
if not s:
|
||||
return {"detail": f"没有{level}层级的摘要"}, 404
|
||||
return summary_to_dict(s)
|
||||
|
||||
|
||||
@router.get("/summaries/{summary_id}")
|
||||
def get_summary(summary_id: int, db: Session = Depends(get_db)):
|
||||
"""获取单条摘要详情"""
|
||||
s = db.query(KnowledgeSummary).filter(KnowledgeSummary.id == summary_id).first()
|
||||
def get_summary(
|
||||
summary_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取单条摘要详情(仅本企业,防跨企业探测)"""
|
||||
s = (
|
||||
db.query(KnowledgeSummary)
|
||||
.filter(
|
||||
KnowledgeSummary.id == summary_id,
|
||||
KnowledgeSummary.entity_id == entity_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not s:
|
||||
raise HTTPException(status_code=404, detail="摘要不存在")
|
||||
return summary_to_dict(s)
|
||||
@@ -105,8 +122,9 @@ def list_events(
|
||||
until: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""查询未摘要的原始事件
|
||||
"""查询未摘要的原始事件(按企业隔离)
|
||||
|
||||
如果不传时间,默认返回最近7天的操作记录和预警。
|
||||
"""
|
||||
@@ -116,18 +134,21 @@ def list_events(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="时间格式错误,请使用 ISO 格式如 2026-06-01T00:00:00")
|
||||
|
||||
events = extract_events(db, dt_since, dt_until)
|
||||
return {"since": dt_since.isoformat(), "until": dt_until.isoformat(), "total": len(events), "events": events[:limit]}
|
||||
events = extract_events(db, dt_since, dt_until, entity_id=entity_id)
|
||||
return {"since": dt_since.isoformat(), "until": dt_until.isoformat(), "entity_id": entity_id, "total": len(events), "events": events[:limit]}
|
||||
|
||||
|
||||
# ── 手动触发 ──
|
||||
|
||||
|
||||
@router.post("/generate/daily")
|
||||
def trigger_daily_summary(db: Session = Depends(get_db)):
|
||||
"""手动触发每日摘要生成"""
|
||||
def trigger_daily_summary(
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""手动触发每日摘要生成(写入当前企业抽屉)"""
|
||||
try:
|
||||
result = generate_daily_sync(db)
|
||||
result = generate_daily_sync(db, entity_id=entity_id)
|
||||
return {"message": "每日摘要已生成", "summary": result}
|
||||
except Exception as e:
|
||||
logger.exception("每日摘要生成失败")
|
||||
@@ -135,10 +156,13 @@ def trigger_daily_summary(db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/generate/weekly")
|
||||
def trigger_weekly_summary(db: Session = Depends(get_db)):
|
||||
"""手动触发周度摘要生成"""
|
||||
def trigger_weekly_summary(
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""手动触发周度摘要生成(写入当前企业抽屉)"""
|
||||
try:
|
||||
result = generate_weekly_sync(db)
|
||||
result = generate_weekly_sync(db, entity_id=entity_id)
|
||||
return {"message": "周度摘要已生成", "summary": result}
|
||||
except Exception as e:
|
||||
logger.exception("周度摘要生成失败")
|
||||
@@ -146,10 +170,13 @@ def trigger_weekly_summary(db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/generate/monthly")
|
||||
def trigger_monthly_summary(db: Session = Depends(get_db)):
|
||||
"""手动触发月度摘要生成"""
|
||||
def trigger_monthly_summary(
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""手动触发月度摘要生成(写入当前企业抽屉)"""
|
||||
try:
|
||||
result = generate_monthly_sync(db)
|
||||
result = generate_monthly_sync(db, entity_id=entity_id)
|
||||
return {"message": "月度摘要已生成", "summary": result}
|
||||
except Exception as e:
|
||||
logger.exception("月度摘要生成失败")
|
||||
|
||||
Reference in New Issue
Block a user