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

@@ -47,6 +47,16 @@ class WorkflowAction(Base):
ForeignKey("workflow_instances.code", ondelete="RESTRICT"),
index=True,
)
source_event_id: Mapped[str | None] = mapped_column(
ForeignKey(
"domain_events.event_id",
name="fk_workflow_actions_source_event_id",
ondelete="RESTRICT",
),
nullable=True,
unique=True,
index=True,
)
action: Mapped[str] = mapped_column(String(128), index=True)
actor: Mapped[str] = mapped_column(String(128), default=ActorValue.SYSTEM, index=True)
from_status: Mapped[str | None] = mapped_column(String(32), nullable=True)

View File

@@ -3,6 +3,7 @@ from uuid import uuid4
from fastapi import HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.core.constants import ActorValue
@@ -34,20 +35,30 @@ class WorkflowService:
actor: str = ActorValue.SYSTEM,
payload: dict[str, Any] | None = None,
commit: bool = True,
source_event_id: str | None = None,
) -> WorkflowInstance:
existing_workflow = self._workflow_for_source_event(source_event_id)
if existing_workflow is not None:
return existing_workflow
aggregate_id_text = str(aggregate_id) if aggregate_id is not None else None
record = self.db.execute(
workflow_query = (
select(WorkflowInstance).where(
WorkflowInstance.workflow_type == workflow_type,
WorkflowInstance.aggregate_type == aggregate_type,
WorkflowInstance.aggregate_id == aggregate_id_text,
)
).scalar_one_or_none()
.with_for_update()
)
record = self.db.execute(workflow_query).scalar_one_or_none()
previous_status = None
now = utc_now()
if record is None:
record = WorkflowInstance(
code=f"{WORKFLOW_CODE_PREFIX}-{now:%Y%m%d%H%M%S%f}",
candidate = WorkflowInstance(
code=(
f"{WORKFLOW_CODE_PREFIX}-{now:%Y%m%d%H%M%S%f}-"
f"{uuid4().hex[:8]}"
),
workflow_type=workflow_type,
aggregate_type=aggregate_type,
aggregate_id=aggregate_id_text,
@@ -56,10 +67,26 @@ class WorkflowService:
current_step=action,
payload=payload or {},
)
self.db.add(record)
self.db.flush()
try:
with self.db.begin_nested():
self.db.add(candidate)
self.db.flush()
record = candidate
except IntegrityError:
record = self.db.execute(workflow_query).scalar_one()
previous_status = record.status
else:
previous_status = record.status
existing_workflow = self._workflow_for_source_event(source_event_id)
if existing_workflow is not None:
if existing_workflow.code != record.code:
raise ValueError(
"Source event is already attached to a different workflow"
)
return existing_workflow
if previous_status is not None:
record.status = status_value
record.actor = actor
record.current_step = action
@@ -86,6 +113,7 @@ class WorkflowService:
f"{uuid4().hex[:8]}"
),
workflow_code=record.code,
source_event_id=source_event_id,
action=action,
actor=actor,
from_status=previous_status,
@@ -100,6 +128,23 @@ class WorkflowService:
self.db.flush()
return record
def _workflow_for_source_event(
self,
source_event_id: str | None,
) -> WorkflowInstance | None:
if not source_event_id:
return None
action = self.db.execute(
select(WorkflowAction).where(
WorkflowAction.source_event_id == source_event_id
)
).scalar_one_or_none()
if action is None:
return None
return self.db.execute(
select(WorkflowInstance).where(WorkflowInstance.code == action.workflow_code)
).scalar_one()
def list_workflows(
self,
status_filter: str | None = None,