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

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

231 lines
8.7 KiB
Python
Raw Permalink 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.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()