refactor(core,ai): 调整模块导入路径并移除废弃文件 - 修复 scheduler.py 中的导入路径错误,将 reports.service 改为 reports.services - 移除废弃的 app/core/background/task_queue.py 文件 - 移除废弃的 app/modules/ai_agent/adapters.py 文件 - 修复 ai_memory/service.py 中的导入路径错误,将 events.service 改为 events.services ```
124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
from datetime import date
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.core.http.pagination import bounded_limit
|
|
from app.modules.business.constants import (
|
|
DONE_STATUSES,
|
|
GENERATED_RISK_EVENT_TYPES,
|
|
PROJECT_CLOSED_STATUSES,
|
|
SUPPLIER_RISK_LEVELS,
|
|
StatusValue,
|
|
)
|
|
from app.modules.business.models import (
|
|
FundAccount,
|
|
Project,
|
|
RiskEvent,
|
|
RiskEventAction,
|
|
Supplier,
|
|
WorkTask,
|
|
)
|
|
from app.modules.business.service import serialize_model
|
|
from app.modules.risk.constants import (
|
|
RISK_SCORE_WEIGHTS,
|
|
RiskEventPayloadKey,
|
|
RiskSummaryKey,
|
|
risk_level_for_score,
|
|
)
|
|
|
|
|
|
class RiskQueryMixin:
|
|
def overdue_tasks(self) -> list[dict[str, Any]]:
|
|
stmt = select(WorkTask).where(
|
|
WorkTask.due_date.is_not(None),
|
|
WorkTask.due_date < date.today(),
|
|
WorkTask.status.notin_(DONE_STATUSES),
|
|
)
|
|
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def delayed_projects(self) -> list[dict[str, Any]]:
|
|
stmt = select(Project).where(
|
|
Project.due_date.is_not(None),
|
|
Project.due_date < date.today(),
|
|
Project.status.notin_(PROJECT_CLOSED_STATUSES),
|
|
)
|
|
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def over_budget_projects(self) -> list[dict[str, Any]]:
|
|
stmt = select(Project).where(
|
|
Project.budget_amount > 0,
|
|
Project.actual_amount > Project.budget_amount,
|
|
)
|
|
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def fund_risks(self) -> list[dict[str, Any]]:
|
|
stmt = select(FundAccount).where(FundAccount.current_balance < FundAccount.safety_line)
|
|
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def supplier_risks(self) -> list[dict[str, Any]]:
|
|
stmt = select(Supplier).where(
|
|
(Supplier.blacklist_status != StatusValue.NORMAL)
|
|
| Supplier.risk_level.in_(SUPPLIER_RISK_LEVELS)
|
|
)
|
|
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def list_events(
|
|
self,
|
|
limit: int = 100,
|
|
status_filter: str | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
limit_value = bounded_limit(limit)
|
|
stmt = select(RiskEvent).order_by(RiskEvent.id.desc()).limit(limit_value)
|
|
if status_filter:
|
|
stmt = (
|
|
select(RiskEvent)
|
|
.where(RiskEvent.status == status_filter)
|
|
.order_by(RiskEvent.id.desc())
|
|
.limit(limit_value)
|
|
)
|
|
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def list_actions(self, risk_event_id: int, limit: int = 100) -> list[dict[str, Any]]:
|
|
self._get_event(risk_event_id)
|
|
stmt = (
|
|
select(RiskEventAction)
|
|
.where(RiskEventAction.risk_event_id == risk_event_id)
|
|
.order_by(RiskEventAction.id.desc())
|
|
.limit(bounded_limit(limit))
|
|
)
|
|
return [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def summary(self) -> dict[str, Any]:
|
|
overdue_tasks = self.overdue_tasks()
|
|
delayed_projects = self.delayed_projects()
|
|
over_budget_projects = self.over_budget_projects()
|
|
fund_risks = self.fund_risks()
|
|
supplier_risks = self.supplier_risks()
|
|
open_events = self.list_events(status_filter=StatusValue.OPEN)
|
|
external_open_events = [
|
|
item
|
|
for item in open_events
|
|
if item.get(RiskEventPayloadKey.RISK_TYPE) not in GENERATED_RISK_EVENT_TYPES
|
|
]
|
|
risk_score = (
|
|
len(overdue_tasks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.OVERDUE_TASKS]
|
|
+ len(delayed_projects) * RISK_SCORE_WEIGHTS[RiskSummaryKey.DELAYED_PROJECTS]
|
|
+ len(over_budget_projects)
|
|
* RISK_SCORE_WEIGHTS[RiskSummaryKey.OVER_BUDGET_PROJECTS]
|
|
+ len(fund_risks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.FUND_RISKS]
|
|
+ len(supplier_risks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.SUPPLIER_RISKS]
|
|
+ len(external_open_events) * RISK_SCORE_WEIGHTS[RiskSummaryKey.OPEN_EVENTS]
|
|
)
|
|
return {
|
|
RiskSummaryKey.RISK_LEVEL: risk_level_for_score(risk_score),
|
|
RiskSummaryKey.RISK_SCORE: Decimal(risk_score),
|
|
RiskSummaryKey.OVERDUE_TASKS: overdue_tasks,
|
|
RiskSummaryKey.DELAYED_PROJECTS: delayed_projects,
|
|
RiskSummaryKey.OVER_BUDGET_PROJECTS: over_budget_projects,
|
|
RiskSummaryKey.FUND_RISKS: fund_risks,
|
|
RiskSummaryKey.SUPPLIER_RISKS: supplier_risks,
|
|
RiskSummaryKey.OPEN_EVENTS: open_events,
|
|
}
|