Files
company-ai-platform/app/modules/reports/services/enterprise.py
JiuContinent d7db84571d ```
feat: 添加飞书用户模块和订阅功能支持

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

234 lines
9.5 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 hashlib import sha256
import json
from typing import Any
from fastapi import HTTPException, status
from app.core.constants import ActorValue
from app.modules.audit.constants import AuditAction, AuditSource, AuditTargetType
from app.modules.audit.schemas import AuditLogCreate
from app.modules.audit.service import AuditService
from app.modules.business.models import (
PerformanceMetric,
)
from app.modules.events.constants import (
EventAggregateType,
EventPayloadKey,
EventSource,
EventType,
)
from app.modules.events.services import EventService
from app.modules.reports.constants import (
EnterpriseAnalyticsKey,
LifecycleResponseKey,
LifecycleSection,
MetricKey,
ReportStatus,
ReportText,
ReportTitle,
)
from app.modules.reports.services.common import _json_safe, _money, _rate
class ReportEnterpriseAnalyticsMixin:
def enterprise_analytics(
self,
project_code: str | None = None,
owner: str | None = None,
period_start: date | None = None,
period_end: date | None = None,
actor: str = ActorValue.API,
) -> dict[str, Any]:
"""Build V3 read-only finance, procurement, performance, and operations analytics."""
if period_start and period_end and period_start > period_end:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="period_start must be before or equal to period_end",
)
include_global_metrics = not any(
value is not None for value in (project_code, owner, period_start, period_end)
)
lifecycle = self.project_lifecycle_report(
project_code=project_code,
owner=owner,
period_start=period_start,
period_end=period_end,
include_ai=False,
actor=actor,
)
metrics = lifecycle[LifecycleResponseKey.METRICS]
projects = metrics[LifecycleSection.PROJECTS]
procurements = metrics[LifecycleSection.PROCUREMENTS]
expenses = metrics[LifecycleSection.EXPENSES]
funds = metrics[LifecycleSection.FUNDS]
tasks = metrics[LifecycleSection.TASKS]
risks = metrics[LifecycleSection.RISKS]
health = metrics[LifecycleSection.HEALTH]
finance = {
MetricKey.BUDGET_TOTAL: projects[MetricKey.BUDGET_TOTAL],
MetricKey.ACTUAL_TOTAL: projects[MetricKey.ACTUAL_TOTAL],
MetricKey.BUDGET_USAGE_RATE: projects[MetricKey.BUDGET_USAGE_RATE],
MetricKey.CURRENT_BALANCE_TOTAL: (
funds[MetricKey.CURRENT_BALANCE_TOTAL]
if include_global_metrics
else 0
),
MetricKey.NET_POSITION: (
funds[MetricKey.NET_POSITION] if include_global_metrics else 0
),
MetricKey.RISK_ACCOUNTS: (
funds[MetricKey.RISK_ACCOUNTS] if include_global_metrics else 0
),
MetricKey.PAYMENT_EXPOSURE: (
procurements[MetricKey.ACTUAL_TOTAL] + expenses[MetricKey.AMOUNT_TOTAL]
),
}
procurement = {
MetricKey.TOTAL: procurements[MetricKey.TOTAL],
MetricKey.PENDING_APPROVAL: procurements[MetricKey.PENDING_APPROVAL],
MetricKey.PENDING_DELIVERY: procurements[MetricKey.PENDING_DELIVERY],
MetricKey.UNPAID: procurements[MetricKey.UNPAID],
MetricKey.EXPECTED_TOTAL: procurements[MetricKey.EXPECTED_TOTAL],
MetricKey.ACTUAL_TOTAL: procurements[MetricKey.ACTUAL_TOTAL],
MetricKey.DELIVERY_RISK: procurements[MetricKey.PENDING_DELIVERY],
}
performance = self._enterprise_performance_stats(include_global_metrics)
operations = {
MetricKey.READINESS_SCORE: health[MetricKey.SCORE],
MetricKey.LEVEL: health[MetricKey.LEVEL],
MetricKey.COMPLETION_RATE: tasks[MetricKey.COMPLETION_RATE],
MetricKey.OVERDUE_TASKS: risks[MetricKey.OVERDUE_TASKS],
MetricKey.DELAYED_PROJECTS: risks[MetricKey.DELAYED_PROJECTS],
MetricKey.OVER_BUDGET_PROJECTS: risks[MetricKey.OVER_BUDGET_PROJECTS],
MetricKey.OPEN_EVENTS: risks[MetricKey.OPEN_EVENTS],
MetricKey.HIGH_EVENTS: risks[MetricKey.HIGH_EVENTS],
}
recommendations = lifecycle[LifecycleResponseKey.RECOMMENDATIONS]
lines = self._enterprise_analytics_lines(
lifecycle[LifecycleResponseKey.FILTERS],
finance,
procurement,
performance,
operations,
recommendations,
)
snapshot = _json_safe(
{
EnterpriseAnalyticsKey.TITLE: ReportTitle.ENTERPRISE_ANALYTICS,
EnterpriseAnalyticsKey.FILTERS: lifecycle[LifecycleResponseKey.FILTERS],
EnterpriseAnalyticsKey.FINANCE: finance,
EnterpriseAnalyticsKey.PROCUREMENT: procurement,
EnterpriseAnalyticsKey.PERFORMANCE: performance,
EnterpriseAnalyticsKey.OPERATIONS: operations,
EnterpriseAnalyticsKey.RECOMMENDATIONS: recommendations,
EnterpriseAnalyticsKey.LINES: lines,
EnterpriseAnalyticsKey.CONTENT: "\n".join(lines),
}
)
canonical_snapshot = json.dumps(
snapshot,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
)
digest = sha256(canonical_snapshot.encode("utf-8")).hexdigest()[:24].upper()
code = f"ANALYTICS-{digest}"
report = {EnterpriseAnalyticsKey.CODE: code, **snapshot}
AuditService(self.db).record(
AuditLogCreate(
actor=actor,
source=AuditSource.REPORTS,
action=AuditAction.ENTERPRISE_ANALYTICS,
target_type=AuditTargetType.ENTERPRISE_ANALYTICS,
target_id=code,
response_payload=report,
)
)
EventService(self.db).enqueue(
event_type=EventType.ENTERPRISE_ANALYTICS_GENERATED,
source=EventSource.ANALYTICS,
aggregate_type=EventAggregateType.ENTERPRISE_ANALYTICS,
aggregate_id=code,
actor=actor,
payload={
EventPayloadKey.CODE: code,
EventPayloadKey.STATUS: ReportStatus.GENERATED,
},
idempotency_key=f"enterprise-analytics:{code}",
)
self.db.commit()
return report
def _enterprise_performance_stats(self, include_global: bool) -> dict[str, Any]:
if not include_global:
return {
MetricKey.TOTAL: 0,
MetricKey.CONFIRMED: 0,
MetricKey.CONFIRMED_RATE: 0.0,
MetricKey.AVERAGE_AUTO_SCORE: 0.0,
MetricKey.AVERAGE_CONFIRMED_SCORE: 0.0,
MetricKey.WEIGHT_TOTAL: 0,
MetricKey.BY_STATUS: {},
}
total = self._count(PerformanceMetric)
confirmed = self._count(PerformanceMetric, PerformanceMetric.confirmed_score.is_not(None))
return {
MetricKey.TOTAL: total,
MetricKey.CONFIRMED: confirmed,
MetricKey.CONFIRMED_RATE: _rate(confirmed, total),
MetricKey.AVERAGE_AUTO_SCORE: self._avg(PerformanceMetric.auto_score),
MetricKey.AVERAGE_CONFIRMED_SCORE: self._avg(PerformanceMetric.confirmed_score),
MetricKey.WEIGHT_TOTAL: self._sum(PerformanceMetric.weight),
MetricKey.BY_STATUS: self._group_counts(PerformanceMetric, PerformanceMetric.status),
}
def _enterprise_analytics_lines(
self,
filters: dict[str, Any],
finance: dict[str, Any],
procurement: dict[str, Any],
performance: dict[str, Any],
operations: dict[str, Any],
recommendations: list[str],
) -> list[str]:
scope = (
"".join(f"{key}={value}" for key, value in filters.items() if value)
or ReportText.DEFAULT_SCOPE
)
lines = [
f"- 范围:{scope}",
(
f"- 财务:预算 {_money(finance[MetricKey.BUDGET_TOTAL])}"
f"实际 {_money(finance[MetricKey.ACTUAL_TOTAL])}"
f"净头寸 {_money(finance[MetricKey.NET_POSITION])}"
f"支付暴露 {_money(finance[MetricKey.PAYMENT_EXPOSURE])}"
),
(
f"- 采购:总数 {procurement[MetricKey.TOTAL]}"
f"待审批 {procurement[MetricKey.PENDING_APPROVAL]}"
f"待交付 {procurement[MetricKey.PENDING_DELIVERY]}"
f"未付款 {procurement[MetricKey.UNPAID]}"
),
(
f"- 绩效:指标 {performance[MetricKey.TOTAL]}"
f"已确认 {performance[MetricKey.CONFIRMED]}"
f"确认率 {performance[MetricKey.CONFIRMED_RATE]}%"
f"平均自动分 {performance[MetricKey.AVERAGE_AUTO_SCORE]}"
),
(
f"- 运营:准备度 {operations[MetricKey.READINESS_SCORE]}"
f"任务完成率 {operations[MetricKey.COMPLETION_RATE]}%"
f"逾期任务 {operations[MetricKey.OVERDUE_TASKS]}"
f"打开风险 {operations[MetricKey.OPEN_EVENTS]}"
),
ReportText.ACTION_HEADER,
]
lines.extend(f" - {item}" for item in recommendations)
return lines