feat: 🏗️ 实现 Agent 消息可靠异步持久化(Outbox + Kafka) * 新增 Outbox 数据模型与消息载荷定义,位于 `backend/model/outbox.go` * 新增 Outbox DAO,支持创建、扫描、发布标记、失败重试与消费落库事务,位于 `backend/dao/outbox.go` * 新增 Kafka 基础封装,包含配置、生产者、消费者与消息包装,位于 `backend/kafka` 文件夹 * `config.go`:Kafka 配置文件 * `producer.go`:Kafka 生产者 * `consumer.go`:Kafka 消费者 * `envelope.go`:消息封装处理 * 新增异步管道服务,处理扫描投递与消费落库,位于 `backend/service/agent_async_pipeline.go` * 接入 Agent 聊天链路的可靠持久化,替换原有 goroutine 直接写库逻辑,位于 `backend/service/agent.go` * 启动流程接入管道初始化与启动,位于 `backend/cmd/start.go` * 增加 Kafka 配置项,更新 `backend/config.yaml` 与 `backend/config.example.yaml` * 引入 Kafka 依赖:`github.com/segmentio/kafka-go`(见 `backend/go.mod`, `backend/go.sum`) fix: 🐛 修复首启偶发 user 消息重复落库问题 * 解决因 Outbox 状态并发回写竞态,导致 `consumed` 被晚到的 `published` 覆盖的问题 * 在 `MarkPublished` 中增加条件,避免覆盖已标记为 `consumed` 或 `dead` 的消息,修复位置:`backend/dao/outbox.go` perf: ⚡ 更新 Docker Compose 配置与 Kafka 相关服务 * 更新 `docker-compose.yml` 文件,新增 Kafka 配置与服务 fix: 🧹 优化缓存删除逻辑 * 在 `cache deleter` 中忽略了 `model.AgentOutboxMessage`、`model.ChatHistory` 与 `model.AgentChat` 这三个结构体 * 防止这些结构体对应的表单删除缓存时,导致控制台消息爆炸
107 lines
3.4 KiB
Go
107 lines
3.4 KiB
Go
package middleware
|
||
|
||
import (
|
||
"context"
|
||
"log"
|
||
"reflect"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/dao"
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type GormCachePlugin struct {
|
||
cacheDAO *dao.CacheDAO
|
||
}
|
||
|
||
func NewGormCachePlugin(dao *dao.CacheDAO) *GormCachePlugin {
|
||
return &GormCachePlugin{
|
||
cacheDAO: dao,
|
||
}
|
||
}
|
||
|
||
// Name 插件名称
|
||
func (p *GormCachePlugin) Name() string {
|
||
return "GormCachePlugin"
|
||
}
|
||
|
||
// Initialize 注册 GORM 钩子
|
||
func (p *GormCachePlugin) Initialize(db *gorm.DB) error {
|
||
// 在增、删、改成功后,统一触发清理逻辑
|
||
_ = db.Callback().Create().After("gorm:create").Register("clear_related_cache_after_create", p.afterWrite)
|
||
_ = db.Callback().Update().After("gorm:update").Register("clear_related_cache_after_update", p.afterWrite)
|
||
_ = db.Callback().Delete().After("gorm:delete").Register("clear_related_cache_after_delete", p.afterWrite)
|
||
return nil
|
||
}
|
||
|
||
func (p *GormCachePlugin) afterWrite(db *gorm.DB) {
|
||
if db.Error != nil || db.Statement.Schema == nil {
|
||
return
|
||
}
|
||
|
||
// 获取 Model 的真实值(剥掉所有指针)
|
||
val := reflect.Indirect(reflect.ValueOf(db.Statement.Model))
|
||
|
||
// 如果是切片,拿切片里元素的类型
|
||
if val.Kind() == reflect.Slice {
|
||
if val.Len() > 0 {
|
||
p.dispatchCacheLogic(val.Index(0).Interface(), db)
|
||
}
|
||
} else {
|
||
p.dispatchCacheLogic(val.Interface(), db)
|
||
}
|
||
}
|
||
|
||
// 根据不同的 Model 类型,调用不同的缓存失效逻辑
|
||
func (p *GormCachePlugin) dispatchCacheLogic(modelObj interface{}, db *gorm.DB) {
|
||
switch m := modelObj.(type) {
|
||
case model.Schedule:
|
||
// 无论传的是 &s, s, 还是 &[]s,剥开后都是 model.Schedule
|
||
p.invalidScheduleCache(m.UserID, m.Week)
|
||
case model.TaskClass:
|
||
p.invalidTaskClassCache(*m.UserID)
|
||
case model.Task:
|
||
p.invalidTaskCache(m.UserID)
|
||
case model.AgentOutboxMessage, model.ChatHistory, model.AgentChat:
|
||
// 这些模型目前没有定义缓存逻辑,先不处理
|
||
default:
|
||
// 只有真正没定义的模型才会到这里
|
||
log.Printf("[GORM-Cache] No logic defined for model: %T", modelObj)
|
||
}
|
||
}
|
||
|
||
func (p *GormCachePlugin) invalidScheduleCache(userID int, week int) {
|
||
if userID == 0 || week == 0 {
|
||
return
|
||
}
|
||
// 3. 异步执行,不阻塞主业务事务
|
||
go func() {
|
||
// 这里调用你的 CacheDAO 删缓存
|
||
_ = p.cacheDAO.DeleteUserWeeklyScheduleFromCache(context.Background(), userID, week)
|
||
_ = p.cacheDAO.DeleteUserTodayScheduleFromCache(context.Background(), userID) // 同时删当天日程的缓存,确保数据一致
|
||
_ = p.cacheDAO.DeleteUserRecentCompletedSchedulesFromCache(context.Background(), userID) // 同时删最近完成日程的缓存,确保数据一致
|
||
_ = p.cacheDAO.DeleteUserOngoingScheduleFromCache(context.Background(), userID) // 同时删正在进行日程的缓存,确保数据一致
|
||
log.Printf("[GORM-Cache] Invalidated cache for user %d, week %d", userID, week)
|
||
}()
|
||
}
|
||
|
||
func (p *GormCachePlugin) invalidTaskClassCache(userID int) {
|
||
if userID == 0 {
|
||
return
|
||
}
|
||
go func() {
|
||
_ = p.cacheDAO.DeleteTaskClassList(context.Background(), userID)
|
||
log.Printf("[GORM-Cache] Invalidated task class list cache for user %d", userID)
|
||
}()
|
||
}
|
||
|
||
func (p *GormCachePlugin) invalidTaskCache(userID int) {
|
||
if userID == 0 {
|
||
return
|
||
}
|
||
go func() {
|
||
_ = p.cacheDAO.DeleteUserTasksFromCache(context.Background(), userID)
|
||
log.Printf("[GORM-Cache] Invalidated task list cache for user %d", userID)
|
||
}()
|
||
}
|