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("月度摘要生成失败")
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
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.knowledge_article import KnowledgeArticle
|
||||
|
||||
router = APIRouter(prefix="/api/cma/knowledge-articles", tags=["知识库嵌入"],
|
||||
@@ -17,6 +19,7 @@ router = APIRouter(prefix="/api/cma/knowledge-articles", tags=["知识库嵌入"
|
||||
def article_to_dict(a: KnowledgeArticle) -> dict:
|
||||
return {
|
||||
"id": a.id,
|
||||
"entity_id": a.entity_id,
|
||||
"title": a.title,
|
||||
"summary": a.summary,
|
||||
"content": a.content,
|
||||
@@ -32,9 +35,21 @@ def list_articles(
|
||||
related_page: Optional[str] = Query(None, description="按关联页面路由筛选"),
|
||||
category: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""查询知识文章,可按关联页面或分类筛选"""
|
||||
q = db.query(KnowledgeArticle)
|
||||
"""查询知识文章(本企业专属 ∪ 全局共享),可按关联页面或分类筛选
|
||||
|
||||
隔离语义(2026-09-03 多租户分区):
|
||||
- entity_id IS NULL → 全局共享(CMA 系统教学知识,所有企业可见)
|
||||
- entity_id == 当前账套 → 企业专属(只本企业可见)
|
||||
- 其他企业的专属文章不可见
|
||||
"""
|
||||
q = db.query(KnowledgeArticle).filter(
|
||||
or_(
|
||||
KnowledgeArticle.entity_id == entity_id,
|
||||
KnowledgeArticle.entity_id.is_(None),
|
||||
)
|
||||
)
|
||||
if related_page:
|
||||
q = q.filter(KnowledgeArticle.related_page.contains(related_page))
|
||||
if category:
|
||||
@@ -44,8 +59,19 @@ def list_articles(
|
||||
|
||||
|
||||
@router.get("/{article_id}")
|
||||
def get_article(article_id: int, db: Session = Depends(get_db)):
|
||||
a = db.query(KnowledgeArticle).filter(KnowledgeArticle.id == article_id).first()
|
||||
def get_article(
|
||||
article_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
entity_id: int = Depends(get_entity_id),
|
||||
):
|
||||
"""获取文章详情(企业专属文章仅本企业可见,防跨企业探测)"""
|
||||
a = (
|
||||
db.query(KnowledgeArticle)
|
||||
.filter(KnowledgeArticle.id == article_id)
|
||||
.first()
|
||||
)
|
||||
if not a:
|
||||
raise HTTPException(404, "文章不存在")
|
||||
if a.entity_id is not None and a.entity_id != entity_id:
|
||||
raise HTTPException(404, "文章不存在")
|
||||
return article_to_dict(a)
|
||||
|
||||
@@ -22,6 +22,7 @@ class KnowledgeEvent(Base):
|
||||
__tablename__ = "knowledge_events"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_id = Column(Integer, default=1, index=True, comment="企业ID (多租户隔离 2026-09-03)")
|
||||
event_type = Column(String(30), nullable=False, comment="事件类型: kpi_change/alert/decision/plan/map/import/user_action")
|
||||
event_level = Column(String(20), default="info", comment="info/warning/important/critical")
|
||||
source = Column(String(50), nullable=True, comment="来源: operation_log/api/erp_sync/manual")
|
||||
@@ -47,6 +48,7 @@ class KnowledgeSummary(Base):
|
||||
__tablename__ = "knowledge_summaries"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_id = Column(Integer, default=1, index=True, comment="企业ID (多租户隔离 2026-09-03)")
|
||||
level = Column(String(20), nullable=False, comment="摘要层级: daily/weekly/monthly/cumulative")
|
||||
period_key = Column(String(20), nullable=False, comment="期间标识: 2026-06-12 / 2026-W24 / 2026-06 / cumulative")
|
||||
title = Column(String(300), nullable=False, comment="摘要标题")
|
||||
|
||||
@@ -12,6 +12,7 @@ class KnowledgeArticle(Base):
|
||||
__tablename__ = "knowledge_articles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_id = Column(Integer, nullable=True, index=True, comment="企业ID: NULL=全局共享(系统教学), 有值=企业专属 (多租户隔离 2026-09-03)")
|
||||
title = Column(String(200), nullable=False, comment="文章标题")
|
||||
summary = Column(String(500), nullable=True, comment="一句话摘要")
|
||||
content = Column(Text, nullable=False, comment="文章正文(支持Markdown)")
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_
|
||||
|
||||
from app.models import KnowledgeEvent, KnowledgeSummary, OperationLog, KPIAlert, KPIDefinition, KPIValue
|
||||
from app.models import KnowledgeEvent, KnowledgeSummary, OperationLog, KPIAlert, KPIDefinition, KPIValue, UserEntity
|
||||
|
||||
logger = logging.getLogger("cma.knowledge")
|
||||
|
||||
@@ -66,16 +66,31 @@ async def _call_deepseek(prompt: str, timeout: int = 30) -> str:
|
||||
|
||||
# ── 事件抽取 ──
|
||||
|
||||
def extract_events(db: Session, since: datetime, until: Optional[datetime] = None) -> list[dict]:
|
||||
"""从 OperationLog + KPIAlert 中抽取关键事件
|
||||
def extract_events(db: Session, since: datetime, until: Optional[datetime] = None, entity_id: int = 1) -> list[dict]:
|
||||
"""从 OperationLog + KPIAlert 中抽取关键事件(按企业抽屉隔离)
|
||||
返回 dict 列表,用于喂给摘要 prompt
|
||||
|
||||
隔离说明(2026-09-03 多租户分区):
|
||||
- KPIAlert 带 entity_id 列 → 直接按 entity_id 过滤
|
||||
- OperationLog 无 entity_id 列(数据债,P2 follow-up)→ 经 user_id→user_entities
|
||||
归属当前企业;无授权归属的日志(user_id NULL/未授权)不进入本企业记忆
|
||||
"""
|
||||
now = until or datetime.utcnow()
|
||||
|
||||
# 1. 操作日志 → 事件
|
||||
# 0. 当前企业授权用户集合(OperationLog 归属过滤用)
|
||||
auth_user_ids = [
|
||||
uid for (uid,) in db.query(UserEntity.user_id)
|
||||
.filter(UserEntity.entity_id == entity_id).all()
|
||||
]
|
||||
|
||||
# 1. 操作日志 → 事件(本企业授权用户的操作)
|
||||
logs = (
|
||||
db.query(OperationLog)
|
||||
.filter(OperationLog.created_at >= since, OperationLog.created_at <= now)
|
||||
.filter(
|
||||
OperationLog.created_at >= since,
|
||||
OperationLog.created_at <= now,
|
||||
OperationLog.user_id.in_(auth_user_ids), # 空集合 → IN (NULL) 不匹配任何行
|
||||
)
|
||||
.order_by(OperationLog.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
@@ -96,11 +111,15 @@ def extract_events(db: Session, since: datetime, until: Optional[datetime] = Non
|
||||
"detail": detail_str,
|
||||
})
|
||||
|
||||
# 2. 预警记录 → 事件
|
||||
# 2. 预警记录 → 事件(KPIAlert 直接按企业过滤)
|
||||
alerts = (
|
||||
db.query(KPIAlert, KPIDefinition.kpi_name, KPIDefinition.kpi_code)
|
||||
.join(KPIDefinition, KPIAlert.kpi_id == KPIDefinition.id)
|
||||
.filter(KPIAlert.created_at >= since, KPIAlert.created_at <= now)
|
||||
.filter(
|
||||
KPIAlert.created_at >= since,
|
||||
KPIAlert.created_at <= now,
|
||||
KPIAlert.entity_id == entity_id,
|
||||
)
|
||||
.order_by(KPIAlert.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
@@ -136,9 +155,12 @@ def _build_summary_prompt(events: list[dict], level: str, period_key: str) -> st
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _kpi_snapshot(db: Session) -> list[dict]:
|
||||
"""当前KPI快照 — 每个活跃KPI的最新实际值"""
|
||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
||||
async def _kpi_snapshot(db: Session, entity_id: int = 1) -> list[dict]:
|
||||
"""当前KPI快照 — 本企业每个活跃KPI的最新实际值(按账套隔离)"""
|
||||
kpis = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.status == "active",
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
).all()
|
||||
snapshot = []
|
||||
for k in kpis:
|
||||
latest = (
|
||||
@@ -165,6 +187,7 @@ async def generate_summary(
|
||||
since: datetime,
|
||||
until: Optional[datetime] = None,
|
||||
prev_summary: Optional[KnowledgeSummary] = None,
|
||||
entity_id: int = 1,
|
||||
) -> KnowledgeSummary:
|
||||
"""生成一层摘要(daily/weekly/monthly/cumulative)
|
||||
|
||||
@@ -174,11 +197,12 @@ async def generate_summary(
|
||||
since: 事件开始时间
|
||||
until: 事件结束时间
|
||||
prev_summary: 前一层摘要(用于累积摘要继承)
|
||||
entity_id: 企业账套(企业记忆分区 2026-09-03)
|
||||
"""
|
||||
now = until or datetime.utcnow()
|
||||
|
||||
# 1. 抽取事件
|
||||
events = extract_events(db, since, now)
|
||||
# 1. 抽取事件(本企业)
|
||||
events = extract_events(db, since, now, entity_id=entity_id)
|
||||
|
||||
# 2. 构建 prompt
|
||||
prompt = _build_summary_prompt(events, level, period_key)
|
||||
@@ -194,11 +218,12 @@ async def generate_summary(
|
||||
if not result_text or "无重要变化" in result_text:
|
||||
result_text = f"{level}汇总: 窗口 {period_key} 内共 {len(events)} 条事件记录,无重大变化需记录。"
|
||||
|
||||
# 6. 提取 KPI 快照
|
||||
kpi_snapshot = await _kpi_snapshot(db)
|
||||
# 6. 提取 KPI 快照(本企业)
|
||||
kpi_snapshot = await _kpi_snapshot(db, entity_id=entity_id)
|
||||
|
||||
# 7. 存入数据库
|
||||
summary = KnowledgeSummary(
|
||||
entity_id=entity_id,
|
||||
level=level,
|
||||
period_key=period_key,
|
||||
title=f"{level.upper()}摘要 - {period_key}",
|
||||
@@ -223,32 +248,36 @@ async def generate_summary(
|
||||
|
||||
# ── 分层调度 ──
|
||||
|
||||
def get_last_summary(db: Session, level: str) -> Optional[KnowledgeSummary]:
|
||||
"""获取该层最新(最新创建)的摘要"""
|
||||
def get_last_summary(db: Session, level: str, entity_id: int = 1) -> Optional[KnowledgeSummary]:
|
||||
"""获取该层最新(最新创建)的摘要(按企业隔离)"""
|
||||
return (
|
||||
db.query(KnowledgeSummary)
|
||||
.filter(KnowledgeSummary.level == level, KnowledgeSummary.is_stale == 0)
|
||||
.filter(
|
||||
KnowledgeSummary.level == level,
|
||||
KnowledgeSummary.is_stale == 0,
|
||||
KnowledgeSummary.entity_id == entity_id,
|
||||
)
|
||||
.order_by(KnowledgeSummary.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
async def run_daily_summary(db: Session) -> KnowledgeSummary:
|
||||
"""运行每日摘要"""
|
||||
async def run_daily_summary(db: Session, entity_id: int = 1) -> KnowledgeSummary:
|
||||
"""运行每日摘要(企业记忆分区)"""
|
||||
today = date.today()
|
||||
period_key = today.isoformat()
|
||||
since = datetime(today.year, today.month, today.day)
|
||||
|
||||
# 获取前一天摘要作为 prev
|
||||
yesterday = today - timedelta(days=1)
|
||||
prev = get_last_summary(db, "daily")
|
||||
prev = get_last_summary(db, "daily", entity_id=entity_id)
|
||||
|
||||
return await generate_summary(
|
||||
db, "daily", period_key, since, prev_summary=prev,
|
||||
db, "daily", period_key, since, prev_summary=prev, entity_id=entity_id,
|
||||
)
|
||||
|
||||
|
||||
async def run_weekly_summary(db: Session) -> KnowledgeSummary:
|
||||
async def run_weekly_summary(db: Session, entity_id: int = 1) -> KnowledgeSummary:
|
||||
"""运行每周摘要(周日执行)"""
|
||||
today = date.today()
|
||||
# ISO 周算法: 本周一到今天
|
||||
@@ -257,13 +286,13 @@ async def run_weekly_summary(db: Session) -> KnowledgeSummary:
|
||||
since = today - timedelta(days=today.weekday()) # 本周一
|
||||
since_dt = datetime(since.year, since.month, since.day)
|
||||
|
||||
prev = get_last_summary(db, "weekly")
|
||||
prev = get_last_summary(db, "weekly", entity_id=entity_id)
|
||||
return await generate_summary(
|
||||
db, "weekly", period_key, since_dt, prev_summary=prev,
|
||||
db, "weekly", period_key, since_dt, prev_summary=prev, entity_id=entity_id,
|
||||
)
|
||||
|
||||
|
||||
async def run_monthly_summary(db: Session) -> KnowledgeSummary:
|
||||
async def run_monthly_summary(db: Session, entity_id: int = 1) -> KnowledgeSummary:
|
||||
"""运行月度摘要"""
|
||||
today = date.today()
|
||||
period_key = today.strftime("%Y-%m")
|
||||
@@ -271,10 +300,10 @@ async def run_monthly_summary(db: Session) -> KnowledgeSummary:
|
||||
|
||||
# 附属前一个月的 daily 和 weekly 摘要
|
||||
prev_month = today.replace(day=1) - timedelta(days=1)
|
||||
prev = get_last_summary(db, "monthly")
|
||||
prev = get_last_summary(db, "monthly", entity_id=entity_id)
|
||||
|
||||
return await generate_summary(
|
||||
db, "monthly", period_key, since, prev_summary=prev,
|
||||
db, "monthly", period_key, since, prev_summary=prev, entity_id=entity_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -289,40 +318,41 @@ def generate_summary_sync(
|
||||
period_key: str,
|
||||
since: datetime,
|
||||
until: Optional[datetime] = None,
|
||||
entity_id: int = 1,
|
||||
) -> dict:
|
||||
"""同步包装,用于 API 手动触发"""
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
summary = loop.run_until_complete(
|
||||
generate_summary(db, level, period_key, since, until)
|
||||
generate_summary(db, level, period_key, since, until, entity_id=entity_id)
|
||||
)
|
||||
return {"id": summary.id, "level": summary.level, "period_key": summary.period_key}
|
||||
return {"id": summary.id, "entity_id": summary.entity_id, "level": summary.level, "period_key": summary.period_key}
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
def generate_daily_sync(db: Session) -> dict:
|
||||
def generate_daily_sync(db: Session, entity_id: int = 1) -> dict:
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
summary = loop.run_until_complete(run_daily_summary(db))
|
||||
return {"id": summary.id, "level": summary.level, "period_key": summary.period_key}
|
||||
summary = loop.run_until_complete(run_daily_summary(db, entity_id))
|
||||
return {"id": summary.id, "entity_id": summary.entity_id, "level": summary.level, "period_key": summary.period_key}
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
def generate_weekly_sync(db: Session) -> dict:
|
||||
def generate_weekly_sync(db: Session, entity_id: int = 1) -> dict:
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
summary = loop.run_until_complete(run_weekly_summary(db))
|
||||
return {"id": summary.id, "level": summary.level, "period_key": summary.period_key}
|
||||
summary = loop.run_until_complete(run_weekly_summary(db, entity_id))
|
||||
return {"id": summary.id, "entity_id": summary.entity_id, "level": summary.level, "period_key": summary.period_key}
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
def generate_monthly_sync(db: Session) -> dict:
|
||||
def generate_monthly_sync(db: Session, entity_id: int = 1) -> dict:
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
summary = loop.run_until_complete(run_monthly_summary(db))
|
||||
return {"id": summary.id, "level": summary.level, "period_key": summary.period_key}
|
||||
summary = loop.run_until_complete(run_monthly_summary(db, entity_id))
|
||||
return {"id": summary.id, "entity_id": summary.entity_id, "level": summary.level, "period_key": summary.period_key}
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
Reference in New Issue
Block a user