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

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

View File

@@ -1,6 +1,10 @@
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
@@ -26,7 +30,7 @@ from app.modules.reports.constants import (
ReportTitle,
)
from app.modules.reports.services.common import _json_safe, _money, _next_code, _rate
from app.modules.reports.services.common import _json_safe, _money, _rate
class ReportEnterpriseAnalyticsMixin:
@@ -39,8 +43,15 @@ class ReportEnterpriseAnalyticsMixin:
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",
)
code = _next_code("ANALYTICS")
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,
@@ -61,9 +72,17 @@ class ReportEnterpriseAnalyticsMixin:
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],
MetricKey.NET_POSITION: funds[MetricKey.NET_POSITION],
MetricKey.RISK_ACCOUNTS: funds[MetricKey.RISK_ACCOUNTS],
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]
),
@@ -77,7 +96,7 @@ class ReportEnterpriseAnalyticsMixin:
MetricKey.ACTUAL_TOTAL: procurements[MetricKey.ACTUAL_TOTAL],
MetricKey.DELIVERY_RISK: procurements[MetricKey.PENDING_DELIVERY],
}
performance = self._enterprise_performance_stats()
performance = self._enterprise_performance_stats(include_global_metrics)
operations = {
MetricKey.READINESS_SCORE: health[MetricKey.SCORE],
MetricKey.LEVEL: health[MetricKey.LEVEL],
@@ -97,9 +116,8 @@ class ReportEnterpriseAnalyticsMixin:
operations,
recommendations,
)
report = _json_safe(
snapshot = _json_safe(
{
EnterpriseAnalyticsKey.CODE: code,
EnterpriseAnalyticsKey.TITLE: ReportTitle.ENTERPRISE_ANALYTICS,
EnterpriseAnalyticsKey.FILTERS: lifecycle[LifecycleResponseKey.FILTERS],
EnterpriseAnalyticsKey.FINANCE: finance,
@@ -111,6 +129,16 @@ class ReportEnterpriseAnalyticsMixin:
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,
@@ -136,7 +164,18 @@ class ReportEnterpriseAnalyticsMixin:
self.db.commit()
return report
def _enterprise_performance_stats(self) -> dict[str, Any]:
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 {