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

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

133 lines
4.3 KiB
Python

from typing import Any
from uuid import uuid4
from fastapi import HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from app.core.config import get_settings
from app.core.constants import ActorValue
from app.core.http.pagination import bounded_limit
from app.core.utils.time import utc_now
from app.modules.events.constants import (
EVENT_CODE_PREFIX,
EventErrorDetail,
)
from app.modules.events.models import DomainEvent
from app.modules.events.services.serialization import _serialize_event
class EventQueryMixin:
def enqueue(
self,
event_type: str,
source: str,
aggregate_type: str,
aggregate_id: str | int | None,
actor: str = ActorValue.SYSTEM,
payload: dict[str, Any] | None = None,
idempotency_key: str | None = None,
) -> DomainEvent:
"""Stage an outbox event in the caller's transaction."""
existing = self._find_idempotent_event(idempotency_key)
if existing is not None:
return existing
settings = get_settings()
now = utc_now()
record = DomainEvent(
event_id=(
f"{EVENT_CODE_PREFIX}-{now:%Y%m%d%H%M%S%f}-"
f"{uuid4().hex[:8]}"
),
event_type=event_type,
source=source,
aggregate_type=aggregate_type,
aggregate_id=str(aggregate_id) if aggregate_id is not None else None,
actor=actor,
payload=payload or {},
idempotency_key=idempotency_key,
next_attempt_at=now,
max_attempts=settings.event_dispatch_max_attempts,
)
if not idempotency_key:
self.db.add(record)
self.db.flush()
return record
try:
with self.db.begin_nested():
self.db.add(record)
self.db.flush()
except IntegrityError:
existing = self._find_idempotent_event(idempotency_key)
if existing is None:
raise
return existing
return record
def emit(
self,
event_type: str,
source: str,
aggregate_type: str,
aggregate_id: str | int | None,
actor: str = ActorValue.SYSTEM,
payload: dict[str, Any] | None = None,
idempotency_key: str | None = None,
) -> DomainEvent:
existing = self._find_idempotent_event(idempotency_key)
if existing is not None:
return existing
record = self.enqueue(
event_type=event_type,
source=source,
aggregate_type=aggregate_type,
aggregate_id=aggregate_id,
actor=actor,
payload=payload,
idempotency_key=idempotency_key,
)
self.db.commit()
self.db.refresh(record)
return record
def _find_idempotent_event(self, idempotency_key: str | None) -> DomainEvent | None:
if not idempotency_key:
return None
return self.db.execute(
select(DomainEvent).where(DomainEvent.idempotency_key == idempotency_key)
).scalar_one_or_none()
def list_events(
self,
status_filter: str | None = None,
event_type: str | None = None,
limit: int = 100,
) -> list[dict[str, Any]]:
stmt = select(DomainEvent).order_by(DomainEvent.id.desc()).limit(bounded_limit(limit))
if status_filter:
stmt = stmt.where(DomainEvent.status == status_filter)
if event_type:
stmt = stmt.where(DomainEvent.event_type == event_type)
return [_serialize_event(item) for item in self.db.execute(stmt).scalars()]
def count_by_status(self) -> dict[str, int]:
rows = self.db.execute(
select(DomainEvent.status, func.count()).group_by(DomainEvent.status)
).all()
return {str(status_value): int(count) for status_value, count in rows}
def get_event(self, event_id: str) -> DomainEvent:
record = self.db.execute(
select(DomainEvent).where(DomainEvent.event_id == event_id)
).scalar_one_or_none()
if record is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=EventErrorDetail.EVENT_NOT_FOUND,
)
return record