Files
JiuContinent d7db84571d ```
feat: 添加飞书用户模块和订阅功能支持

- 新增feishu_users模块用于处理飞书用户身份验证和权限管理
- 新增subscriptions模块用于处理订阅相关功能
- 新增personalization模块用于个性化服务
- 在alembic迁移配置中注册新的模型模块
- 在API路由器中添加feishu_users和subscriptions路由
- 实现事件调度服务的改进,包括错误处理和状态更新优化
- 添加飞书命令处理的权限检查机制
- 实现飞书应用票据事件处理
- 改进审计日志记录功能
```
2026-07-27 08:02:17 +08:00

101 lines
4.5 KiB
Python

from typing import Any
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.modules.ai_memory.constants import AIMemoryStatus
from app.modules.ai_memory.service import AIMemoryService
from app.modules.business.constants import DONE_STATUSES, PROJECT_CLOSED_STATUSES, StatusValue
from app.modules.business.models import (
LegacySyncRun,
Project,
ReportPushRun,
RiskEvent,
WorkReport,
WorkTask,
)
from app.modules.business.service import serialize_model
from app.modules.events.constants import EventStatus
from app.modules.events.models import DomainEvent
from app.modules.observability.constants import ObservabilityMetricKey
from app.modules.observability.service import ObservabilityService
from app.modules.reports.constants import ReportPushStatus
from app.modules.risk.services import RiskService
from app.modules.workflows.constants import WorkflowStatus
from app.modules.workflows.models import WorkflowInstance
class DashboardService:
"""Build lightweight operational dashboard data for V2/V3."""
def __init__(self, db: Session):
self.db = db
self.risks = RiskService(db)
def summary(self) -> dict[str, Any]:
active_projects = self._count(Project, Project.status.notin_(PROJECT_CLOSED_STATUSES))
open_tasks = self._count(WorkTask, WorkTask.status.notin_(DONE_STATUSES))
open_risk_events = self._count(RiskEvent, RiskEvent.status == StatusValue.OPEN)
unassigned_open_risks = self._count(
RiskEvent,
RiskEvent.status == StatusValue.OPEN,
RiskEvent.assigned_to.is_(None),
)
failed_push_runs = self._count(ReportPushRun, ReportPushRun.status == ReportPushStatus.FAILED)
pending_events = self._count(DomainEvent, DomainEvent.status == EventStatus.PENDING)
failed_events = self._count(DomainEvent, DomainEvent.status == EventStatus.FAILED)
running_workflows = self._count(
WorkflowInstance,
WorkflowInstance.status == WorkflowStatus.RUNNING,
)
failed_workflows = self._count(
WorkflowInstance,
WorkflowInstance.status == WorkflowStatus.FAILED,
)
memory_counts = AIMemoryService(self.db).count_by_status()
active_ai_memory = memory_counts.get(AIMemoryStatus.ACTIVE, 0)
heartbeat_summary = ObservabilityService(self.db).heartbeat_summary()
latest_reports = self.db.execute(
select(WorkReport).order_by(WorkReport.id.desc()).limit(5)
).scalars()
latest_push_runs = self.db.execute(
select(ReportPushRun).order_by(ReportPushRun.id.desc()).limit(10)
).scalars()
latest_sync_runs = self.db.execute(
select(LegacySyncRun).order_by(LegacySyncRun.id.desc()).limit(10)
).scalars()
risk_summary = self.risks.summary()
return {
"metrics": {
"active_projects": active_projects,
"open_tasks": open_tasks,
"open_risk_events": open_risk_events,
"unassigned_open_risks": unassigned_open_risks,
"failed_push_runs": failed_push_runs,
"pending_events": pending_events,
"failed_events": failed_events,
"running_workflows": running_workflows,
"failed_workflows": failed_workflows,
"active_ai_memory": active_ai_memory,
"stale_heartbeats": heartbeat_summary[ObservabilityMetricKey.STALE],
"risk_level": risk_summary["risk_level"],
"risk_score": float(risk_summary["risk_score"]),
},
"risk_counts": {
"overdue_tasks": len(risk_summary["overdue_tasks"]),
"delayed_projects": len(risk_summary["delayed_projects"]),
"over_budget_projects": len(risk_summary["over_budget_projects"]),
"fund_risks": len(risk_summary["fund_risks"]),
"supplier_risks": len(risk_summary["supplier_risks"]),
},
"latest_reports": [serialize_model(item) for item in latest_reports],
"latest_push_runs": [serialize_model(item) for item in latest_push_runs],
"latest_sync_runs": [serialize_model(item) for item in latest_sync_runs],
}
def _count(self, model: type, *conditions: Any) -> int:
stmt = select(func.count()).select_from(model)
for condition in conditions:
stmt = stmt.where(condition)
return int(self.db.execute(stmt).scalar() or 0)