```
feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
This commit is contained in:
@@ -14,7 +14,7 @@ from app.core.background.task_queue import (
|
||||
enqueue_work_weekly_push,
|
||||
)
|
||||
from app.core.database import get_db
|
||||
from app.core.security import ApiPrincipal, require_api_key, require_operations_enabled
|
||||
from app.core.security import ApiPrincipal, require_api_key
|
||||
from app.modules.reports.constants import ReportPushKey
|
||||
from app.modules.reports.schemas import (
|
||||
LifecycleRunRequest,
|
||||
@@ -134,7 +134,6 @@ def enqueue_lifecycle(
|
||||
payload: LifecycleRunRequest,
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
require_operations_enabled()
|
||||
return enqueue_lifecycle_report(
|
||||
report_type=payload.report_type,
|
||||
receive_id=payload.receive_id,
|
||||
@@ -167,8 +166,6 @@ def generate_work_report(
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
if payload.persist:
|
||||
require_operations_enabled()
|
||||
return ReportService(db).generate_work_report(
|
||||
report_type=payload.report_type,
|
||||
reporter=payload.reporter,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -45,7 +45,9 @@ class ReportLifecycleReportMixin:
|
||||
task_conditions,
|
||||
risk_conditions,
|
||||
)
|
||||
include_global_risk = not (project_code or owner)
|
||||
include_global_risk = not any(
|
||||
value is not None for value in (project_code, owner, period_start, period_end)
|
||||
)
|
||||
health = self._lifecycle_health(
|
||||
project_stats,
|
||||
task_stats,
|
||||
|
||||
@@ -26,6 +26,7 @@ class ReportPushRunMixin:
|
||||
receive_id_type: str,
|
||||
actor: str,
|
||||
status: str = ReportPushStatus.PENDING,
|
||||
task_id: str | None = None,
|
||||
idempotency_key: str | None = None,
|
||||
) -> ReportPushRun:
|
||||
if idempotency_key:
|
||||
@@ -43,6 +44,7 @@ class ReportPushRunMixin:
|
||||
receive_id=receive_id,
|
||||
receive_id_type=receive_id_type,
|
||||
status=status,
|
||||
task_id=task_id,
|
||||
actor=actor,
|
||||
queued_at=utc_now(),
|
||||
idempotency_key=idempotency_key,
|
||||
|
||||
Reference in New Issue
Block a user