from datetime import date from typing import Any from sqlalchemy import select from sqlalchemy.orm import Session from app.core.config import get_settings from app.core.constants import ActorValue from app.core.utils.time import utc_now from app.application.delivery import ReportDeliveryService from app.modules.feishu.service import FeishuService from app.modules.legacy_mysql.intasect import IntasectSyncService from app.modules.reports.constants import ReportPushStatus, ReportType from app.modules.reports.services import ReportService from app.modules.workflows.constants import WorkflowStatus, WorkflowType from app.modules.workflows.models import WorkflowInstance from app.modules.workflows.service import WorkflowService class LifecyclePipelineService: def __init__(self, db: Session): self.db = db self.workflows = WorkflowService(db) def period_key(self, report_type: str, reference_date: date | None = None) -> str: start, end = ReportService(self.db)._management_period(report_type, reference_date) period_value = end.isoformat() if report_type == ReportType.DAILY else start.isoformat() return f"{report_type}:{period_value}" def find(self, period_key: str) -> WorkflowInstance | None: return self.db.execute( select(WorkflowInstance).where( WorkflowInstance.workflow_type == WorkflowType.LIFECYCLE_REPORT, WorkflowInstance.aggregate_type == "report_period", WorkflowInstance.aggregate_id == period_key, ) ).scalar_one_or_none() def prepare( self, report_type: str, actor: str, force: bool = False, ) -> tuple[WorkflowInstance, str, bool]: period_key = self.period_key(report_type) existing = self.find(period_key) if existing is not None and existing.status == WorkflowStatus.COMPLETED and not force: return existing, period_key, True workflow = self.workflows.start_or_update( workflow_type=WorkflowType.LIFECYCLE_REPORT, aggregate_type="report_period", aggregate_id=period_key, status_value=WorkflowStatus.RUNNING, action="queued", actor=actor, payload={"report_type": report_type, "period_key": period_key, "force": force}, ) return workflow, period_key, False def run( self, report_type: str, receive_id: str | None = None, receive_id_type: str = "chat_id", force: bool = False, actor: str = ActorValue.SCHEDULER, ) -> dict[str, Any]: workflow, period_key, deduplicated = self.prepare(report_type, actor, force) if deduplicated: return { "workflow_code": workflow.code, "period_key": period_key, "deduplicated": True, "status": workflow.status, } try: workflow = self.workflows.start_or_update( workflow_type=WorkflowType.LIFECYCLE_REPORT, aggregate_type="report_period", aggregate_id=period_key, status_value=WorkflowStatus.RUNNING, action="source_sync", actor=actor, payload={"report_type": report_type}, ) sync_result = IntasectSyncService(self.db).sync_all( run_code=workflow.code, force_full=False, ) self.workflows.start_or_update( workflow_type=WorkflowType.LIFECYCLE_REPORT, aggregate_type="report_period", aggregate_id=period_key, status_value=WorkflowStatus.RUNNING, action="analysis", actor=actor, payload={ "datasets": {name: result["processed"] for name, result in sync_result.items()} }, ) report_service = ReportService(self.db) report = report_service.management_lifecycle_report( report_type=report_type, actor=actor, include_ai=True, ) settings = get_settings() target_receive_id = receive_id or settings.feishu_default_chat_id ai_analysis = report.get("ai_analysis") or {} if not ai_analysis.get("ok"): notified = self._notify_ai_unavailable( target_receive_id, receive_id_type, period_key, actor, ) failed = self.workflows.start_or_update( workflow_type=WorkflowType.LIFECYCLE_REPORT, aggregate_type="report_period", aggregate_id=period_key, status_value=WorkflowStatus.FAILED, action="ai_unavailable", actor=actor, payload={ "ai_unavailable": True, "notified": notified, "error_type": ai_analysis.get("type") or "AIUnavailable", }, ) return { "workflow_code": failed.code, "period_key": period_key, "deduplicated": False, "status": failed.status, "ai_unavailable": True, "notified": notified, } idempotency_key = period_key if force: idempotency_key = f"{period_key}:force:{utc_now():%Y%m%d%H%M%S%f}" push_run = report_service.create_push_run( report_type=report_type, title=str(report["title"]), receive_id=target_receive_id, receive_id_type=receive_id_type, actor=actor, status=ReportPushStatus.PENDING, idempotency_key=idempotency_key, ) if push_run.status != ReportPushStatus.SUCCESS: ReportDeliveryService(self.db).push_report( report, target_receive_id, receive_id_type, actor, push_run_code=push_run.code, ) completed = self.workflows.start_or_update( workflow_type=WorkflowType.LIFECYCLE_REPORT, aggregate_type="report_period", aggregate_id=period_key, status_value=WorkflowStatus.COMPLETED, action="pushed", actor=actor, payload={"push_run_code": push_run.code, "idempotency_key": idempotency_key}, ) return { "workflow_code": completed.code, "period_key": period_key, "push_run_code": push_run.code, "deduplicated": False, "status": completed.status, } except Exception as exc: self.db.rollback() self.workflows.start_or_update( workflow_type=WorkflowType.LIFECYCLE_REPORT, aggregate_type="report_period", aggregate_id=period_key, status_value=WorkflowStatus.FAILED, action="failed", actor=actor, payload={"error": str(exc)[:2000]}, ) self._notify_failure(receive_id, receive_id_type, period_key, exc, actor) raise def _notify_ai_unavailable( self, receive_id: str | None, receive_id_type: str, period_key: str, actor: str, ) -> bool: settings = get_settings() if ( not receive_id or not settings.feishu_app_id or not settings.feishu_app_secret ): return False FeishuService(self.db).send_text( f"生命周期报告 {period_key}:AI 当前不可用,本次分析报告未发送。请检查模型服务。", receive_id, receive_id_type, actor, ) return True def _notify_failure( self, receive_id: str | None, receive_id_type: str, period_key: str, error: Exception, actor: str, ) -> None: settings = get_settings() target = receive_id or settings.feishu_default_chat_id if not target or not settings.feishu_app_id or not settings.feishu_app_secret: return try: FeishuService(self.db).send_text( f"生命周期报告 {period_key} 执行失败:{type(error).__name__}", target, receive_id_type, actor, ) except Exception: self.db.rollback()