Version: 0.9.64.dev.260503
后端: 1. 服务级 outbox 基础设施全量落地——新增 service route / service catalog / route registry,重构 outbox engine、repository、event bus 和 model,按 `event_type -> service -> table/topic/group` 统一写入与投递,保留 `agent` 兼容壳但不再依赖共享 outbox 2. Kafka 投递、消费与启动装配同步切换——更新 kafka config、consumer、envelope,接入服务级 topic 与 consumer group,并同步调整 mysql 初始化、start/main/router 装配,保证各服务 relay / consumer 独立装配 3. 业务事件处理器按服务归属重接新 bus——`active-scheduler` 触发链路,以及 `agent` / `memory` / `notification` / `task` 相关 outbox handler 统一切到新路由注册与服务目录,避免新流量回流共享表 4. 同步更新《微服务四步迁移与第二阶段并行开发计划》,把阶段 1 改成当前基线并补齐结构图、阶段快照、风险回退和多代理执行口径
This commit is contained in:
@@ -32,7 +32,7 @@ type ActiveScheduleTriggeredProcessor interface {
|
||||
// 3. 若事务返回 error,则 best-effort 回写 trigger failed,并把错误交给 outbox 做 retry;
|
||||
// 4. 这里不直接 import active_scheduler 的具体实现,避免 service/events 和业务编排层互相反向耦合。
|
||||
func RegisterActiveScheduleTriggeredHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
processor ActiveScheduleTriggeredProcessor,
|
||||
) error {
|
||||
@@ -45,24 +45,28 @@ func RegisterActiveScheduleTriggeredHandler(
|
||||
if processor == nil {
|
||||
return errors.New("active schedule triggered processor is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, sharedevents.ActiveScheduleTriggeredEventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||||
if !isAllowedTriggeredEventVersion(envelope.EventVersion) {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, fmt.Sprintf("active_schedule.triggered 版本不受支持: %s", envelope.EventVersion))
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, fmt.Sprintf("active_schedule.triggered 版本不受支持: %s", envelope.EventVersion))
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload sharedevents.ActiveScheduleTriggeredPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析 active_schedule.triggered 载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析 active_schedule.triggered 载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
if validateErr := payload.Validate(); validateErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "active_schedule.triggered 载荷非法: "+validateErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "active_schedule.triggered 载荷非法: "+validateErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
err := outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
err := eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
return processor.ProcessTriggeredInTx(ctx, tx, payload)
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -35,7 +35,7 @@ type AgentStateSnapshotPayload struct {
|
||||
// 2. 使用 upsert 语义,同一 conversation_id 只保留最新快照;
|
||||
// 3. 通过 outbox 通用消费事务保证"业务写入 + consumed 推进"原子一致。
|
||||
func RegisterAgentStateSnapshotHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
) error {
|
||||
@@ -48,15 +48,19 @@ func RegisterAgentStateSnapshotHandler(
|
||||
if repoManager == nil {
|
||||
return errors.New("repo manager is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, EventTypeAgentStateSnapshotPersist)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||||
var payload AgentStateSnapshotPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析快照载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析快照载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
return eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
record := model.AgentStateSnapshotRecord{
|
||||
ConversationID: payload.ConversationID,
|
||||
UserID: payload.UserID,
|
||||
|
||||
@@ -25,7 +25,7 @@ const EventTypeAgentTimelinePersistRequested = "agent.timeline.persist.requested
|
||||
// 3. 通过 outbox 通用消费事务,把“时间线写库 + consumed 推进”放进同一事务;
|
||||
// 4. 若遇到 seq 唯一键冲突,会先判定是否属于重放幂等,再决定是否补新 seq 并回填 Redis。
|
||||
func RegisterAgentTimelinePersistHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
agentRepo *dao.AgentDAO,
|
||||
cacheDAO *dao.CacheDAO,
|
||||
@@ -40,12 +40,16 @@ func RegisterAgentTimelinePersistHandler(
|
||||
if agentRepo == nil {
|
||||
return errors.New("agent repo is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, EventTypeAgentTimelinePersistRequested)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||||
var payload model.ChatTimelinePersistPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
// 1. payload 无法反序列化属于不可恢复错误,直接标 dead,避免无意义重试。
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析时间线持久化载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析时间线持久化载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,7 +57,7 @@ func RegisterAgentTimelinePersistHandler(
|
||||
if !payload.HasValidIdentity() {
|
||||
// 2. 这里只校验“能否唯一定位一条 timeline 记录”的最小字段集合。
|
||||
// 3. content / payload_json 是否为空由事件类型自行决定,不在这里一刀切限制。
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "时间线持久化载荷非法: user_id/conversation_id/seq/kind 非法")
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "时间线持久化载荷非法: user_id/conversation_id/seq/kind 非法")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -61,7 +65,7 @@ func RegisterAgentTimelinePersistHandler(
|
||||
finalSeq := payload.Seq
|
||||
|
||||
// 4. 统一走 outbox 消费事务入口,保证“业务写入成功 -> consumed”原子一致。
|
||||
err := outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
err := eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
finalPayload, repaired, persistErr := persistConversationTimelineEventInTx(ctx, tx, agentRepo.WithTx(tx), payload)
|
||||
if persistErr != nil {
|
||||
return persistErr
|
||||
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
// 3. 通过 outbox 通用事务入口把"业务写入 + consumed 推进"合并为一个事务;
|
||||
// 4. 当前版本仅注册新路由键(chat.history.persist.requested),不再注册旧兼容键。
|
||||
func RegisterChatHistoryPersistHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
) error {
|
||||
@@ -44,6 +44,10 @@ func RegisterChatHistoryPersistHandler(
|
||||
if repoManager == nil {
|
||||
return errors.New("repo manager is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, EventTypeChatHistoryPersistRequested)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 定义统一处理器:
|
||||
// 2.1 解析 payload;
|
||||
@@ -53,12 +57,12 @@ func RegisterChatHistoryPersistHandler(
|
||||
var payload model.ChatHistoryPersistPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
// 2.1 payload 非法属于不可恢复错误,直接标 dead,避免无意义重试。
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析聊天持久化载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析聊天持久化载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2.2 使用 outbox 通用消费事务,保证"业务写入 + consumed 状态推进"原子一致。
|
||||
return outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
return eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
// 2.2.1 基于同一个 tx 构造 RepoManager,复用你现有跨包事务模型。
|
||||
txM := repoManager.WithTx(tx)
|
||||
// 2.2.2 在同事务内写入聊天历史与会话计数。
|
||||
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
// 2. 通过 outbox 统一消费事务入口,保证“业务成功 + consumed 推进”原子一致;
|
||||
// 3. 非法载荷直接标记 dead,避免无意义重试。
|
||||
func RegisterChatTokenUsageAdjustHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
) error {
|
||||
@@ -43,20 +43,24 @@ func RegisterChatTokenUsageAdjustHandler(
|
||||
if repoManager == nil {
|
||||
return errors.New("repo manager is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, EventTypeChatTokenUsageAdjustRequested)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||||
var payload model.ChatTokenUsageAdjustPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析会话 token 调整载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析会话 token 调整载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if payload.UserID <= 0 || payload.TokensDelta <= 0 || payload.ConversationID == "" {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "会话 token 调整载荷无效: user_id/conversation_id/tokens_delta 非法")
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "会话 token 调整载荷无效: user_id/conversation_id/tokens_delta 非法")
|
||||
return nil
|
||||
}
|
||||
|
||||
return outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
return eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
txM := repoManager.WithTx(tx)
|
||||
return txM.Agent.AdjustTokenUsageInTx(ctx, payload.UserID, payload.ConversationID, payload.TokensDelta)
|
||||
})
|
||||
|
||||
@@ -2,11 +2,12 @@ package events
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/LoveLosita/smartflow/backend/dao"
|
||||
outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox"
|
||||
"github.com/LoveLosita/smartflow/backend/memory"
|
||||
"github.com/LoveLosita/smartflow/backend/notification"
|
||||
sharedevents "github.com/LoveLosita/smartflow/backend/shared/events"
|
||||
)
|
||||
|
||||
// RegisterCoreOutboxHandlers 注册核心业务 outbox handler。
|
||||
@@ -15,9 +16,9 @@ import (
|
||||
// 1. 只负责聚合注册当前核心业务 handler,便于 start / worker/all 等启动入口复用同一套接线顺序。
|
||||
// 2. 不负责创建 eventBus/outboxRepo/DAO/memoryModule,也不负责启动或关闭事件总线。
|
||||
// 3. 不改变单个 Register* 函数的职责;具体 payload 解析、幂等消费和业务落库仍由各自 handler 负责。
|
||||
// 4. 入口先完整校验依赖,避免注册到一半才发现依赖缺失,导致事件总线处于半注册状态。
|
||||
// 4. 这里以显式 route table 的方式列出“事件类型 -> 服务归属 -> handler”,避免后续新增事件时只改启动入口不改接线表。
|
||||
func RegisterCoreOutboxHandlers(
|
||||
eventBus *outboxinfra.EventBus,
|
||||
eventBus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
agentRepo *dao.AgentDAO,
|
||||
@@ -28,28 +29,39 @@ func RegisterCoreOutboxHandlers(
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. 按照现有 start.go 的接线顺序注册,保证迁移到 worker/all 后消费行为不发生隐式变化。
|
||||
// 2. 每一步只包一层业务语义错误,便于启动日志直接定位是哪类 handler 注册失败。
|
||||
if err := RegisterChatHistoryPersistHandler(eventBus, outboxRepo, repoManager); err != nil {
|
||||
return fmt.Errorf("注册聊天历史持久化 handler 失败: %w", err)
|
||||
}
|
||||
if err := RegisterTaskUrgencyPromoteHandler(eventBus, outboxRepo, repoManager); err != nil {
|
||||
return fmt.Errorf("注册任务紧急度平移 handler 失败: %w", err)
|
||||
}
|
||||
if err := RegisterChatTokenUsageAdjustHandler(eventBus, outboxRepo, repoManager); err != nil {
|
||||
return fmt.Errorf("注册会话 token 调整 handler 失败: %w", err)
|
||||
}
|
||||
if err := RegisterAgentStateSnapshotHandler(eventBus, outboxRepo, repoManager); err != nil {
|
||||
return fmt.Errorf("注册 agent 状态快照 handler 失败: %w", err)
|
||||
}
|
||||
if err := RegisterAgentTimelinePersistHandler(eventBus, outboxRepo, agentRepo, cacheRepo); err != nil {
|
||||
return fmt.Errorf("注册 agent 时间线持久化 handler 失败: %w", err)
|
||||
}
|
||||
if err := RegisterMemoryExtractRequestedHandler(eventBus, outboxRepo, memoryModule); err != nil {
|
||||
return fmt.Errorf("注册记忆抽取 handler 失败: %w", err)
|
||||
return registerOutboxHandlerRoutes(coreOutboxHandlerRoutes(eventBus, outboxRepo, repoManager, agentRepo, cacheRepo, memoryModule))
|
||||
}
|
||||
|
||||
// RegisterAllOutboxHandlers 注册当前阶段所有 outbox handler。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责把 core / active_scheduler / notification 三类路由一次性接线;
|
||||
// 2. 不负责创建依赖,也不负责启动事件总线;
|
||||
// 3. 供当前启动流程在“总线启动前”统一完成显式路由注册。
|
||||
func RegisterAllOutboxHandlers(
|
||||
eventBus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
agentRepo *dao.AgentDAO,
|
||||
cacheRepo *dao.CacheDAO,
|
||||
memoryModule *memory.Module,
|
||||
activeTriggerWorkflow ActiveScheduleTriggeredProcessor,
|
||||
notificationService *notification.NotificationService,
|
||||
) error {
|
||||
if err := validateAllOutboxHandlerDeps(eventBus, outboxRepo, repoManager, agentRepo, cacheRepo, memoryModule, activeTriggerWorkflow, notificationService); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return registerOutboxHandlerRoutes(allOutboxHandlerRoutes(
|
||||
eventBus,
|
||||
outboxRepo,
|
||||
repoManager,
|
||||
agentRepo,
|
||||
cacheRepo,
|
||||
memoryModule,
|
||||
activeTriggerWorkflow,
|
||||
notificationService,
|
||||
))
|
||||
}
|
||||
|
||||
// validateCoreOutboxHandlerDeps 校验核心 outbox handler 聚合注册所需依赖。
|
||||
@@ -58,7 +70,7 @@ func RegisterCoreOutboxHandlers(
|
||||
// 1. 只做 nil 校验,不做数据库、Redis、Kafka 连通性探测,避免注册函数承担启动健康检查职责。
|
||||
// 2. 返回 error 表示依赖缺失;返回 nil 表示可以安全进入逐项注册流程。
|
||||
func validateCoreOutboxHandlerDeps(
|
||||
eventBus *outboxinfra.EventBus,
|
||||
eventBus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
agentRepo *dao.AgentDAO,
|
||||
@@ -85,3 +97,112 @@ func validateCoreOutboxHandlerDeps(
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAllOutboxHandlerDeps 在核心依赖基础上,额外校验 active_scheduler 和 notification 相关依赖。
|
||||
func validateAllOutboxHandlerDeps(
|
||||
eventBus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
agentRepo *dao.AgentDAO,
|
||||
cacheRepo *dao.CacheDAO,
|
||||
memoryModule *memory.Module,
|
||||
activeTriggerWorkflow ActiveScheduleTriggeredProcessor,
|
||||
notificationService *notification.NotificationService,
|
||||
) error {
|
||||
if err := validateCoreOutboxHandlerDeps(eventBus, outboxRepo, repoManager, agentRepo, cacheRepo, memoryModule); err != nil {
|
||||
return err
|
||||
}
|
||||
if activeTriggerWorkflow == nil {
|
||||
return errors.New("active schedule triggered processor is nil")
|
||||
}
|
||||
if notificationService == nil {
|
||||
return errors.New("notification service is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// coreOutboxHandlerRoutes 只描述 core 阶段的 outbox 路由。
|
||||
func coreOutboxHandlerRoutes(
|
||||
eventBus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
agentRepo *dao.AgentDAO,
|
||||
cacheRepo *dao.CacheDAO,
|
||||
memoryModule *memory.Module,
|
||||
) []outboxHandlerRoute {
|
||||
return []outboxHandlerRoute{
|
||||
{
|
||||
EventType: EventTypeChatHistoryPersistRequested,
|
||||
Service: outboxHandlerServiceAgent,
|
||||
Register: func() error {
|
||||
return RegisterChatHistoryPersistHandler(eventBus, outboxRepo, repoManager)
|
||||
},
|
||||
},
|
||||
{
|
||||
EventType: EventTypeTaskUrgencyPromoteRequested,
|
||||
Service: outboxHandlerServiceTask,
|
||||
Register: func() error {
|
||||
return RegisterTaskUrgencyPromoteHandler(eventBus, outboxRepo, repoManager)
|
||||
},
|
||||
},
|
||||
{
|
||||
EventType: EventTypeChatTokenUsageAdjustRequested,
|
||||
Service: outboxHandlerServiceAgent,
|
||||
Register: func() error {
|
||||
return RegisterChatTokenUsageAdjustHandler(eventBus, outboxRepo, repoManager)
|
||||
},
|
||||
},
|
||||
{
|
||||
EventType: EventTypeAgentStateSnapshotPersist,
|
||||
Service: outboxHandlerServiceAgent,
|
||||
Register: func() error {
|
||||
return RegisterAgentStateSnapshotHandler(eventBus, outboxRepo, repoManager)
|
||||
},
|
||||
},
|
||||
{
|
||||
EventType: EventTypeAgentTimelinePersistRequested,
|
||||
Service: outboxHandlerServiceAgent,
|
||||
Register: func() error {
|
||||
return RegisterAgentTimelinePersistHandler(eventBus, outboxRepo, agentRepo, cacheRepo)
|
||||
},
|
||||
},
|
||||
{
|
||||
EventType: EventTypeMemoryExtractRequested,
|
||||
Service: outboxHandlerServiceMemory,
|
||||
Register: func() error {
|
||||
return RegisterMemoryExtractRequestedHandler(eventBus, outboxRepo, memoryModule)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// allOutboxHandlerRoutes 把当前阶段所有 outbox 路由一次性展开,供启动入口统一接线。
|
||||
func allOutboxHandlerRoutes(
|
||||
eventBus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
agentRepo *dao.AgentDAO,
|
||||
cacheRepo *dao.CacheDAO,
|
||||
memoryModule *memory.Module,
|
||||
activeTriggerWorkflow ActiveScheduleTriggeredProcessor,
|
||||
notificationService *notification.NotificationService,
|
||||
) []outboxHandlerRoute {
|
||||
routes := coreOutboxHandlerRoutes(eventBus, outboxRepo, repoManager, agentRepo, cacheRepo, memoryModule)
|
||||
routes = append(routes,
|
||||
outboxHandlerRoute{
|
||||
EventType: sharedevents.ActiveScheduleTriggeredEventType,
|
||||
Service: outboxHandlerServiceActiveScheduler,
|
||||
Register: func() error {
|
||||
return RegisterActiveScheduleTriggeredHandler(eventBus, outboxRepo, activeTriggerWorkflow)
|
||||
},
|
||||
},
|
||||
outboxHandlerRoute{
|
||||
EventType: sharedevents.NotificationFeishuRequestedEventType,
|
||||
Service: outboxHandlerServiceNotification,
|
||||
Register: func() error {
|
||||
return RegisterFeishuNotificationHandler(eventBus, outboxRepo, notificationService)
|
||||
},
|
||||
},
|
||||
)
|
||||
return routes
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const (
|
||||
// 2. 不在消费回调里执行 LLM 重计算;
|
||||
// 3. 通过 memory.Module.WithTx(tx) 复用同一套接入门面,保证事务边界仍由 outbox 掌控。
|
||||
func RegisterMemoryExtractRequestedHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
memoryModule *memory.Module,
|
||||
) error {
|
||||
@@ -46,20 +46,24 @@ func RegisterMemoryExtractRequestedHandler(
|
||||
if memoryModule == nil {
|
||||
return errors.New("memory module is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, EventTypeMemoryExtractRequested)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||||
var payload model.MemoryExtractRequestedPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析记忆抽取载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析记忆抽取载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if validateErr := validateMemoryExtractPayload(payload); validateErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "记忆抽取载荷非法: "+validateErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "记忆抽取载荷非法: "+validateErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
return eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
jobPayload := memorymodel.ExtractJobPayload{
|
||||
UserID: payload.UserID,
|
||||
ConversationID: strings.TrimSpace(payload.ConversationID),
|
||||
@@ -87,7 +91,7 @@ func RegisterMemoryExtractRequestedHandler(
|
||||
func EnqueueMemoryExtractRequestedInTx(
|
||||
ctx context.Context,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
kafkaCfg kafkabus.Config,
|
||||
maxRetry int,
|
||||
chatPayload model.ChatHistoryPersistPayload,
|
||||
) error {
|
||||
if !isMemoryWriteEnabled() {
|
||||
@@ -107,6 +111,10 @@ func EnqueueMemoryExtractRequestedInTx(
|
||||
return err
|
||||
}
|
||||
|
||||
if maxRetry <= 0 {
|
||||
maxRetry = 20
|
||||
}
|
||||
|
||||
outboxPayload := outboxinfra.OutboxEventPayload{
|
||||
EventType: EventTypeMemoryExtractRequested,
|
||||
EventVersion: outboxinfra.DefaultEventVersion,
|
||||
@@ -114,13 +122,14 @@ func EnqueueMemoryExtractRequestedInTx(
|
||||
Payload: payloadJSON,
|
||||
}
|
||||
|
||||
// 1. 这里只传 eventType 与消息键,服务归属、outbox 表和 Kafka topic 统一交给仓库路由层解析。
|
||||
// 2. 这样聊天持久化链路不会继续感知 memory 服务的物理 topic,避免拆服务时出现双写口径。
|
||||
_, err = outboxRepo.CreateMessage(
|
||||
ctx,
|
||||
EventTypeMemoryExtractRequested,
|
||||
kafkaCfg.Topic,
|
||||
strings.TrimSpace(chatPayload.ConversationID),
|
||||
outboxPayload,
|
||||
kafkaCfg.MaxRetry,
|
||||
maxRetry,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// 2. 不承担 notification_records 状态机细节,状态流转全部下沉到 notification 模块;
|
||||
// 3. 不在 handler 内部创建 provider/service,避免事件消费与 retry loop 使用两套不同配置。
|
||||
func RegisterFeishuNotificationHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
svc *notification.NotificationService,
|
||||
) error {
|
||||
@@ -33,23 +33,27 @@ func RegisterFeishuNotificationHandler(
|
||||
if svc == nil {
|
||||
return errors.New("notification service is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, sharedevents.NotificationFeishuRequestedEventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||||
// 1. 先校验 event_version,避免未来协议破坏性升级后旧 handler 误吃新消息。
|
||||
// 2. 当前阶段只接受 v1;版本不匹配属于不可恢复协议错误,直接标记 dead。
|
||||
eventVersion := strings.TrimSpace(envelope.EventVersion)
|
||||
if eventVersion != "" && eventVersion != sharedevents.NotificationFeishuRequestedEventVersion {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "notification.feishu.requested event_version 不匹配: "+eventVersion)
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "notification.feishu.requested event_version 不匹配: "+eventVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload sharedevents.FeishuNotificationRequestedPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析 notification.feishu.requested 载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析 notification.feishu.requested 载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
if validateErr := payload.Validate(); validateErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "notification.feishu.requested 载荷非法: "+validateErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "notification.feishu.requested 载荷非法: "+validateErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -58,7 +62,7 @@ func RegisterFeishuNotificationHandler(
|
||||
return handleErr
|
||||
}
|
||||
|
||||
if consumeErr := outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, nil); consumeErr != nil {
|
||||
if consumeErr := eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, nil); consumeErr != nil {
|
||||
return consumeErr
|
||||
}
|
||||
|
||||
|
||||
217
backend/service/events/outbox_bus.go
Normal file
217
backend/service/events/outbox_bus.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka"
|
||||
outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox"
|
||||
)
|
||||
|
||||
// OutboxBus 是启动侧和业务侧共享的 outbox 门面。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 对上只暴露 Publish / RegisterEventHandler / Start / Close 这四个能力;
|
||||
// 2. 对内可以按 service 维度路由到多个底层 engine;
|
||||
// 3. 不承载任何业务处理逻辑,只做事件归属、topic/group 和 engine 选择。
|
||||
type OutboxBus interface {
|
||||
outboxinfra.EventPublisher
|
||||
RegisterEventHandler(eventType string, handler outboxinfra.MessageHandler) error
|
||||
Start(ctx context.Context)
|
||||
Close()
|
||||
}
|
||||
|
||||
type routedOutboxBus struct {
|
||||
buses map[string]OutboxBus
|
||||
}
|
||||
|
||||
// NewRoutedOutboxBus 把多个 service 级 outbox bus 组装成一个门面。
|
||||
//
|
||||
// 1. 这里不创建底层 engine,只接收上层已经建好的 service bus;
|
||||
// 2. 事件发布和 handler 注册都按事件归属路由到对应 service;
|
||||
// 3. 任一 service bus 缺失时直接报错,避免静默回落到共享 topic。
|
||||
func NewRoutedOutboxBus(buses map[string]OutboxBus) OutboxBus {
|
||||
normalized := make(map[string]OutboxBus, len(buses))
|
||||
for serviceName, bus := range buses {
|
||||
serviceName = strings.TrimSpace(serviceName)
|
||||
if serviceName == "" || bus == nil {
|
||||
continue
|
||||
}
|
||||
normalized[serviceName] = bus
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &routedOutboxBus{buses: normalized}
|
||||
}
|
||||
|
||||
// NewServiceOutboxBus 基于 service 级 topic / group 创建底层 outbox engine。
|
||||
//
|
||||
// 1. topic / group 由 service 名称推导,不再要求调用方显式传入共享 topic;
|
||||
// 2. kafka 未启用时返回 nil,调用侧可以继续走同步降级路径;
|
||||
// 3. 这里不注册 handler,注册仍由启动侧统一完成。
|
||||
func NewServiceOutboxBus(repo *outboxinfra.Repository, baseCfg kafkabus.Config, serviceName string) (OutboxBus, error) {
|
||||
if repo == nil {
|
||||
return nil, errors.New("outbox repository is nil")
|
||||
}
|
||||
|
||||
serviceName = strings.TrimSpace(serviceName)
|
||||
if serviceName == "" {
|
||||
return nil, errors.New("serviceName is empty")
|
||||
}
|
||||
|
||||
route, _ := outboxinfra.ResolveServiceRoute(serviceName)
|
||||
cfg := baseCfg
|
||||
cfg.Topic = strings.TrimSpace(route.Topic)
|
||||
cfg.GroupID = strings.TrimSpace(route.GroupID)
|
||||
cfg.ServiceName = strings.TrimSpace(route.ServiceName)
|
||||
if cfg.ServiceName == "" {
|
||||
cfg.ServiceName = serviceName
|
||||
}
|
||||
|
||||
bus, err := outboxinfra.NewEventBus(repo.WithRoute(route), cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bus == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return bus, nil
|
||||
}
|
||||
|
||||
func (b *routedOutboxBus) Publish(ctx context.Context, req outboxinfra.PublishRequest) error {
|
||||
serviceBus, err := b.resolveBusByEventType(req.EventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return serviceBus.Publish(ctx, req)
|
||||
}
|
||||
|
||||
func (b *routedOutboxBus) RegisterEventHandler(eventType string, handler outboxinfra.MessageHandler) error {
|
||||
if handler == nil {
|
||||
return errors.New("handler is nil")
|
||||
}
|
||||
serviceBus, err := b.resolveBusByEventType(eventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return serviceBus.RegisterEventHandler(eventType, handler)
|
||||
}
|
||||
|
||||
func (b *routedOutboxBus) Start(ctx context.Context) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
for _, serviceName := range orderedOutboxServiceNames(b.buses) {
|
||||
b.buses[serviceName].Start(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *routedOutboxBus) Close() {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
for _, serviceName := range orderedOutboxServiceNames(b.buses) {
|
||||
b.buses[serviceName].Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *routedOutboxBus) resolveBusByEventType(eventType string) (OutboxBus, error) {
|
||||
if b == nil {
|
||||
return nil, errors.New("outbox bus is not initialized")
|
||||
}
|
||||
|
||||
eventType = strings.TrimSpace(eventType)
|
||||
if eventType == "" {
|
||||
return nil, errors.New("eventType is empty")
|
||||
}
|
||||
|
||||
serviceName, ok := outboxinfra.ResolveEventService(eventType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("outbox route not registered: eventType=%s", eventType)
|
||||
}
|
||||
|
||||
serviceBus, ok := b.buses[strings.TrimSpace(serviceName)]
|
||||
if !ok || serviceBus == nil {
|
||||
return nil, fmt.Errorf("service outbox bus is missing: service=%s eventType=%s", serviceName, eventType)
|
||||
}
|
||||
return serviceBus, nil
|
||||
}
|
||||
|
||||
func orderedOutboxServiceNames(buses map[string]OutboxBus) []string {
|
||||
ordered := make([]string, 0, len(buses))
|
||||
seen := make(map[string]struct{}, len(buses))
|
||||
|
||||
for _, serviceName := range OutboxServiceNames() {
|
||||
if _, ok := buses[serviceName]; ok {
|
||||
ordered = append(ordered, serviceName)
|
||||
seen[serviceName] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
extras := make([]string, 0)
|
||||
for serviceName := range buses {
|
||||
if _, ok := seen[serviceName]; ok {
|
||||
continue
|
||||
}
|
||||
extras = append(extras, serviceName)
|
||||
}
|
||||
sort.Strings(extras)
|
||||
return append(ordered, extras...)
|
||||
}
|
||||
|
||||
// OutboxServiceNames 返回当前阶段启用的 service 级 outbox 名称。
|
||||
func OutboxServiceNames() []string {
|
||||
return []string{
|
||||
string(outboxHandlerServiceAgent),
|
||||
string(outboxHandlerServiceTask),
|
||||
string(outboxHandlerServiceMemory),
|
||||
string(outboxHandlerServiceActiveScheduler),
|
||||
string(outboxHandlerServiceNotification),
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveOutboxTopicForService 把 service 名称映射成独立 Kafka topic。
|
||||
//
|
||||
// 1. 这里保留现在的命名风格:smartflow.<service>.outbox;
|
||||
// 2. 空 service 只作为兜底,不作为主路径;
|
||||
// 3. 调用侧不再传共享 topic,避免入口继续依赖旧结构。
|
||||
func ResolveOutboxTopicForService(serviceName string) string {
|
||||
route, _ := outboxinfra.ResolveServiceRoute(serviceName)
|
||||
if topic := strings.TrimSpace(route.Topic); topic != "" {
|
||||
return topic
|
||||
}
|
||||
serviceName = strings.TrimSpace(serviceName)
|
||||
if serviceName == "" {
|
||||
return kafkabus.DefaultTopic
|
||||
}
|
||||
return "smartflow." + serviceName + ".outbox"
|
||||
}
|
||||
|
||||
// ResolveOutboxGroupForService 把 service 名称映射成独立 Kafka group。
|
||||
func ResolveOutboxGroupForService(serviceName string) string {
|
||||
route, _ := outboxinfra.ResolveServiceRoute(serviceName)
|
||||
if groupID := strings.TrimSpace(route.GroupID); groupID != "" {
|
||||
return groupID
|
||||
}
|
||||
serviceName = strings.TrimSpace(serviceName)
|
||||
if serviceName == "" {
|
||||
return kafkabus.DefaultGroup
|
||||
}
|
||||
return "smartflow-" + serviceName + "-outbox-consumer"
|
||||
}
|
||||
|
||||
// ResolveOutboxTopicForEvent 根据事件归属 service 计算 topic。
|
||||
func ResolveOutboxTopicForEvent(eventType string) (string, error) {
|
||||
route, ok := outboxinfra.ResolveEventRoute(eventType)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("outbox route not registered: eventType=%s", strings.TrimSpace(eventType))
|
||||
}
|
||||
if topic := strings.TrimSpace(route.Topic); topic != "" {
|
||||
return topic, nil
|
||||
}
|
||||
return ResolveOutboxTopicForService(route.ServiceName), nil
|
||||
}
|
||||
74
backend/service/events/outbox_handler_routes.go
Normal file
74
backend/service/events/outbox_handler_routes.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox"
|
||||
)
|
||||
|
||||
// outboxHandlerService 表示 outbox 路由归属的业务服务。
|
||||
//
|
||||
// 这里只记录服务归属,不承载具体实现包名,方便在启动日志和路由表里直接阅读。
|
||||
type outboxHandlerService string
|
||||
|
||||
const (
|
||||
outboxHandlerServiceAgent outboxHandlerService = "agent"
|
||||
outboxHandlerServiceTask outboxHandlerService = "task"
|
||||
outboxHandlerServiceMemory outboxHandlerService = "memory"
|
||||
outboxHandlerServiceActiveScheduler outboxHandlerService = "active-scheduler"
|
||||
outboxHandlerServiceNotification outboxHandlerService = "notification"
|
||||
)
|
||||
|
||||
// outboxHandlerRoute 显式描述“事件类型 -> 服务归属 -> handler 注册动作”。
|
||||
//
|
||||
// 1. EventType 负责唯一定位 outbox 路由键;
|
||||
// 2. Service 负责标明该路由归属的业务服务;
|
||||
// 3. Register 只负责把对应 handler 挂到总线,不承载业务逻辑。
|
||||
type outboxHandlerRoute struct {
|
||||
EventType string
|
||||
Service outboxHandlerService
|
||||
Register func() error
|
||||
}
|
||||
|
||||
// registerOutboxHandlerRoutes 逐条注册路由表里的 handler。
|
||||
//
|
||||
// 1. 先把事件类型和服务归属写进路由表,避免启动入口散落多处 if err != nil;
|
||||
// 2. 再统一执行注册动作,保证失败时能直接定位到具体 event_type 和 service;
|
||||
// 3. 若某条路由缺少注册函数,直接返回 error,避免静默漏注册。
|
||||
func registerOutboxHandlerRoutes(routes []outboxHandlerRoute) error {
|
||||
for _, route := range routes {
|
||||
if route.Register == nil {
|
||||
return fmt.Errorf("outbox handler route 缺少注册函数: event_type=%s service=%s", route.EventType, route.Service)
|
||||
}
|
||||
if err := outboxinfra.RegisterEventService(route.EventType, string(route.Service)); err != nil {
|
||||
return fmt.Errorf("登记 outbox 事件归属失败(event_type=%s, service=%s): %w", route.EventType, route.Service, err)
|
||||
}
|
||||
if err := route.Register(); err != nil {
|
||||
return fmt.Errorf("注册 outbox handler 失败(event_type=%s, service=%s): %w", route.EventType, route.Service, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// scopedOutboxRepoForEvent 负责把通用 outbox 仓库收敛到某个事件所属的服务表。//
|
||||
// 职责边界:
|
||||
// 1. 只做事件->服务->表的路由,不碰业务写入语义;
|
||||
// 2. 返回的仓库只适合当前事件的 MarkDead / ConsumeAndMarkConsumed / MarkFailedForRetry;
|
||||
// 3. 路由缺失时直接返回错误,避免默默写回默认表。
|
||||
func scopedOutboxRepoForEvent(outboxRepo *outboxinfra.Repository, eventType string) (*outboxinfra.Repository, error) {
|
||||
if outboxRepo == nil {
|
||||
return nil, fmt.Errorf("outbox repository is nil")
|
||||
}
|
||||
|
||||
eventType = strings.TrimSpace(eventType)
|
||||
if eventType == "" {
|
||||
return nil, fmt.Errorf("eventType is empty")
|
||||
}
|
||||
|
||||
route, ok := outboxinfra.ResolveEventRoute(eventType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("outbox route not registered: eventType=%s", eventType)
|
||||
}
|
||||
return outboxRepo.WithRoute(route), nil
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
// 2. 只处理 `task.urgency.promote.requested` 事件,不处理其他业务事件;
|
||||
// 3. 通过 `ConsumeAndMarkConsumed` 把“业务更新 + outbox consumed 推进”放进同一事务。
|
||||
func RegisterTaskUrgencyPromoteHandler(
|
||||
bus *outboxinfra.EventBus,
|
||||
bus OutboxBus,
|
||||
outboxRepo *outboxinfra.Repository,
|
||||
repoManager *dao.RepoManager,
|
||||
) error {
|
||||
@@ -45,25 +45,29 @@ func RegisterTaskUrgencyPromoteHandler(
|
||||
if repoManager == nil {
|
||||
return errors.New("repo manager is nil")
|
||||
}
|
||||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, EventTypeTaskUrgencyPromoteRequested)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 定义统一处理函数。
|
||||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||||
// 2.1 先解析 payload;解析失败属于不可恢复错误,直接标记 dead。
|
||||
var payload model.TaskUrgencyPromoteRequestedPayload
|
||||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "解析任务紧急性平移载荷失败: "+unmarshalErr.Error())
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析任务紧急性平移载荷失败: "+unmarshalErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2.2 做轻量参数净化,避免脏数据进入 DAO。
|
||||
payload.TaskIDs = sanitizePositiveUniqueIntIDs(payload.TaskIDs)
|
||||
if payload.UserID <= 0 || len(payload.TaskIDs) == 0 {
|
||||
_ = outboxRepo.MarkDead(ctx, envelope.OutboxID, "任务紧急性平移载荷无效: user_id 或 task_ids 非法")
|
||||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "任务紧急性平移载荷无效: user_id 或 task_ids 非法")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2.3 统一走 outbox 消费事务入口,保证“业务成功 -> consumed”原子一致。
|
||||
return outboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
return eventOutboxRepo.ConsumeAndMarkConsumed(ctx, envelope.OutboxID, func(tx *gorm.DB) error {
|
||||
// 2.3.1 基于同一 tx 构造 RepoManager,复用现有跨 DAO 事务模式。
|
||||
txM := repoManager.WithTx(tx)
|
||||
// 2.3.2 以消费时刻为准做条件更新,确保“到线”判定与真实落库时刻一致。
|
||||
|
||||
Reference in New Issue
Block a user