Files
company-ai-platform/app/modules/reports/lifecycle_pipeline.py
JiuContinent f8020cab56 ```
feat(market): 添加市场分析数据基础架构和功能模块

- 新增市场分析相关数据库表结构,包括市场工具、每日报价、财务指标、
  宏观指标和公告等数据模型
- 创建市场分析相关的Alembic迁移脚本,包含完整的up和downgrade逻辑
- 集成市场分析路由到主API路由器中
- 添加市场数据定时任务调度,支持盘前、收盘和周度市场分析报告
- 实现市场数据后台任务队列,包含报告生成和收盘分析功能
- 扩展系统配置设置,添加市场分析启用开关和相关参数配置
- 增加AI智能技能支持,包含市场概览分析和股票分析功能
- 添加审计日志记录,支持市场数据同步和自选股更新操作追踪
- 实现飞书命令集成,支持股票分析、自选股管理、公告查询等交互
- 提供市场数据服务层,包含行业分析、股票对比、市场概览等功能
```
2026-07-12 22:05:23 +08:00

236 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.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]:
if get_settings().read_only_mode:
return {
"period_key": self.period_key(report_type),
"deduplicated": False,
"status": "operations_disabled",
}
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:
report_service.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()