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 ```
184 lines
8.5 KiB
Python
184 lines
8.5 KiB
Python
from datetime import date
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.core.utils.time import utc_now
|
|
from app.modules.business.constants import (
|
|
DONE_STATUSES,
|
|
PROJECT_CLOSED_STATUSES,
|
|
SUPPLIER_RISK_LEVELS,
|
|
BusinessDomain,
|
|
RiskEventType,
|
|
RiskLevel,
|
|
StatusValue,
|
|
)
|
|
from app.modules.business.models import (
|
|
FundAccount,
|
|
Project,
|
|
Supplier,
|
|
WorkTask,
|
|
)
|
|
from app.modules.business.service import serialize_model
|
|
from app.modules.risk.constants import (
|
|
RiskEventPayloadKey,
|
|
)
|
|
|
|
|
|
class RiskDetectorMixin:
|
|
def _build_event_payloads(self) -> list[dict[str, Any]]:
|
|
payloads: list[dict[str, Any]] = []
|
|
payloads.extend(self._overdue_task_payloads())
|
|
payloads.extend(self._delayed_project_payloads())
|
|
payloads.extend(self._over_budget_project_payloads())
|
|
payloads.extend(self._fund_risk_payloads())
|
|
payloads.extend(self._supplier_risk_payloads())
|
|
return payloads
|
|
|
|
def _overdue_task_payloads(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),
|
|
)
|
|
payloads = []
|
|
for task in self.db.execute(stmt).scalars():
|
|
payloads.append(
|
|
{
|
|
RiskEventPayloadKey.CODE: f"RISK-TASK-OVERDUE-{task.id}",
|
|
RiskEventPayloadKey.TITLE: f"任务逾期:{task.title}",
|
|
RiskEventPayloadKey.RISK_TYPE: RiskEventType.OVERDUE_TASK,
|
|
RiskEventPayloadKey.RISK_LEVEL: RiskLevel.MEDIUM,
|
|
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
|
|
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.TASKS,
|
|
RiskEventPayloadKey.SOURCE_RECORD_ID: str(task.id),
|
|
RiskEventPayloadKey.PROJECT_CODE: task.project_code,
|
|
RiskEventPayloadKey.OWNER: task.owner,
|
|
RiskEventPayloadKey.DUE_DATE: task.due_date,
|
|
RiskEventPayloadKey.DETECTED_AT: utc_now(),
|
|
RiskEventPayloadKey.DESCRIPTION: "任务已超过截止日期且未完成。",
|
|
RiskEventPayloadKey.MITIGATION: (
|
|
"请负责人更新进度、明确阻塞项并给出新的完成时间。"
|
|
),
|
|
RiskEventPayloadKey.EVIDENCE: serialize_model(task),
|
|
}
|
|
)
|
|
return payloads
|
|
|
|
def _delayed_project_payloads(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),
|
|
)
|
|
payloads = []
|
|
for project in self.db.execute(stmt).scalars():
|
|
level = RiskLevel.HIGH if project.progress_percent < 80 else RiskLevel.MEDIUM
|
|
payloads.append(
|
|
{
|
|
RiskEventPayloadKey.CODE: f"RISK-PROJECT-DELAY-{project.id}",
|
|
RiskEventPayloadKey.TITLE: f"项目延期:{project.name}",
|
|
RiskEventPayloadKey.RISK_TYPE: RiskEventType.DELAYED_PROJECT,
|
|
RiskEventPayloadKey.RISK_LEVEL: level,
|
|
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
|
|
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.PROJECTS,
|
|
RiskEventPayloadKey.SOURCE_RECORD_ID: str(project.id),
|
|
RiskEventPayloadKey.PROJECT_CODE: project.code,
|
|
RiskEventPayloadKey.OWNER: project.owner,
|
|
RiskEventPayloadKey.DUE_DATE: project.due_date,
|
|
RiskEventPayloadKey.DETECTED_AT: utc_now(),
|
|
RiskEventPayloadKey.DESCRIPTION: "项目已超过计划截止日期且未进入完成状态。",
|
|
RiskEventPayloadKey.MITIGATION: (
|
|
"请项目负责人提交延期原因、资源需求和纠偏计划。"
|
|
),
|
|
RiskEventPayloadKey.EVIDENCE: serialize_model(project),
|
|
}
|
|
)
|
|
return payloads
|
|
|
|
def _over_budget_project_payloads(self) -> list[dict[str, Any]]:
|
|
stmt = select(Project).where(
|
|
Project.budget_amount > 0,
|
|
Project.actual_amount > Project.budget_amount,
|
|
)
|
|
payloads = []
|
|
for project in self.db.execute(stmt).scalars():
|
|
payloads.append(
|
|
{
|
|
RiskEventPayloadKey.CODE: f"RISK-PROJECT-BUDGET-{project.id}",
|
|
RiskEventPayloadKey.TITLE: f"项目超预算:{project.name}",
|
|
RiskEventPayloadKey.RISK_TYPE: RiskEventType.OVER_BUDGET_PROJECT,
|
|
RiskEventPayloadKey.RISK_LEVEL: RiskLevel.HIGH,
|
|
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
|
|
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.PROJECTS,
|
|
RiskEventPayloadKey.SOURCE_RECORD_ID: str(project.id),
|
|
RiskEventPayloadKey.PROJECT_CODE: project.code,
|
|
RiskEventPayloadKey.OWNER: project.owner,
|
|
RiskEventPayloadKey.DUE_DATE: project.due_date,
|
|
RiskEventPayloadKey.DETECTED_AT: utc_now(),
|
|
RiskEventPayloadKey.DESCRIPTION: "项目实际成本已超过预算。",
|
|
RiskEventPayloadKey.MITIGATION: (
|
|
"请复核预算科目、冻结非必要采购并补充审批依据。"
|
|
),
|
|
RiskEventPayloadKey.EVIDENCE: serialize_model(project),
|
|
}
|
|
)
|
|
return payloads
|
|
|
|
def _fund_risk_payloads(self) -> list[dict[str, Any]]:
|
|
stmt = select(FundAccount).where(FundAccount.current_balance < FundAccount.safety_line)
|
|
payloads = []
|
|
for account in self.db.execute(stmt).scalars():
|
|
payloads.append(
|
|
{
|
|
RiskEventPayloadKey.CODE: f"RISK-FUND-{account.id}",
|
|
RiskEventPayloadKey.TITLE: f"资金低于安全线:{account.name}",
|
|
RiskEventPayloadKey.RISK_TYPE: RiskEventType.FUND_SAFETY_LINE,
|
|
RiskEventPayloadKey.RISK_LEVEL: RiskLevel.HIGH,
|
|
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
|
|
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.FUND_ACCOUNTS,
|
|
RiskEventPayloadKey.SOURCE_RECORD_ID: str(account.id),
|
|
RiskEventPayloadKey.OWNER: None,
|
|
RiskEventPayloadKey.DETECTED_AT: utc_now(),
|
|
RiskEventPayloadKey.DESCRIPTION: "账户当前余额低于设置的安全线。",
|
|
RiskEventPayloadKey.MITIGATION: (
|
|
"请财务确认收付款计划,"
|
|
"并优先处理关键项目资金安排。"
|
|
),
|
|
RiskEventPayloadKey.EVIDENCE: serialize_model(account),
|
|
}
|
|
)
|
|
return payloads
|
|
|
|
def _supplier_risk_payloads(self) -> list[dict[str, Any]]:
|
|
stmt = select(Supplier).where(
|
|
(Supplier.blacklist_status != StatusValue.NORMAL)
|
|
| Supplier.risk_level.in_(SUPPLIER_RISK_LEVELS)
|
|
)
|
|
payloads = []
|
|
for supplier in self.db.execute(stmt).scalars():
|
|
level = (
|
|
RiskLevel.HIGH
|
|
if supplier.blacklist_status != StatusValue.NORMAL
|
|
else supplier.risk_level
|
|
)
|
|
payloads.append(
|
|
{
|
|
RiskEventPayloadKey.CODE: f"RISK-SUPPLIER-{supplier.id}",
|
|
RiskEventPayloadKey.TITLE: f"供应商风险:{supplier.name}",
|
|
RiskEventPayloadKey.RISK_TYPE: RiskEventType.SUPPLIER_RISK,
|
|
RiskEventPayloadKey.RISK_LEVEL: level,
|
|
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
|
|
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.SUPPLIERS,
|
|
RiskEventPayloadKey.SOURCE_RECORD_ID: str(supplier.id),
|
|
RiskEventPayloadKey.OWNER: supplier.contact,
|
|
RiskEventPayloadKey.DETECTED_AT: utc_now(),
|
|
RiskEventPayloadKey.DESCRIPTION: "供应商风险等级或黑名单状态需要关注。",
|
|
RiskEventPayloadKey.MITIGATION: (
|
|
"请采购负责人复核供应商准入、履约和替代方案。"
|
|
),
|
|
RiskEventPayloadKey.EVIDENCE: serialize_model(supplier),
|
|
}
|
|
)
|
|
return payloads
|