diff --git a/internal/service/ai_bookkeeping_service.go b/internal/service/ai_bookkeeping_service.go index 717e0a1..3f28cec 100644 --- a/internal/service/ai_bookkeeping_service.go +++ b/internal/service/ai_bookkeeping_service.go @@ -21,6 +21,7 @@ import ( "accounting-app/internal/models" "accounting-app/internal/repository" + "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -31,27 +32,21 @@ type TranscriptionResult struct { Duration float64 `json:"duration,omitempty"` } -type cachedInsight struct { - Content string - Timestamp time.Time -} - // ... (existing types) // AIBookkeepingService orchestrates AI bookkeeping functionality type AIBookkeepingService struct { - whisperService *WhisperService - llmService *LLMService - transactionRepo *repository.TransactionRepository - accountRepo *repository.AccountRepository - categoryRepo *repository.CategoryRepository - userSettingsRepo *repository.UserSettingsRepository - db *gorm.DB - sessions map[string]*AISession - sessionMutex sync.RWMutex - config *config.Config - insightCache map[string]cachedInsight - insightCacheMutex sync.RWMutex + whisperService *WhisperService + llmService *LLMService + transactionRepo *repository.TransactionRepository + accountRepo *repository.AccountRepository + categoryRepo *repository.CategoryRepository + userSettingsRepo *repository.UserSettingsRepository + db *gorm.DB + sessions map[string]*AISession + sessionMutex sync.RWMutex + config *config.Config + redisClient *redis.Client } // NewAIBookkeepingService creates a new AIBookkeepingService @@ -66,6 +61,16 @@ func NewAIBookkeepingService( whisperService := NewWhisperService(cfg) llmService := NewLLMService(cfg, accountRepo, categoryRepo) + // Initialize Redis client + var rdb *redis.Client + if cfg.RedisAddr != "" { + rdb = redis.NewClient(&redis.Options{ + Addr: cfg.RedisAddr, + Password: cfg.RedisPassword, + DB: cfg.RedisDB, + }) + } + svc := &AIBookkeepingService{ whisperService: whisperService, llmService: llmService, @@ -75,8 +80,8 @@ func NewAIBookkeepingService( userSettingsRepo: userSettingsRepo, db: db, sessions: make(map[string]*AISession), - insightCache: make(map[string]cachedInsight), config: cfg, + redisClient: rdb, } // Start session cleanup goroutine @@ -99,18 +104,33 @@ func (s *AIBookkeepingService) GenerateDailyInsight(ctx context.Context, userID hasher := md5.New() hasher.Write(dataBytes) dataHash := hex.EncodeToString(hasher.Sum(nil)) - cacheKey := fmt.Sprintf("%d:%s", userID, dataHash) - // Check cache - s.insightCacheMutex.RLock() - if cached, exists := s.insightCache[cacheKey]; exists { - // Cache valid for 24 hours (or just rely on hash change) - if time.Since(cached.Timestamp) < 24*time.Hour { - s.insightCacheMutex.RUnlock() - return cached.Content, nil + // Cache keys + cacheKey := fmt.Sprintf("ai:insight:daily:%d:%s", userID, dataHash) + lastInsightKey := fmt.Sprintf("ai:insight:last:%d", userID) + + // Check Redis cache if available + if s.redisClient != nil { + val, err := s.redisClient.Get(ctx, cacheKey).Result() + if err == nil { + return val, nil + } + } + + // Retrieve last insight for context awareness + var historyContext string + if s.redisClient != nil { + lastInsight, err := s.redisClient.Get(ctx, lastInsightKey).Result() + if err == nil && lastInsight != "" { + // Extract a summary or just use the whole thing if it's short enough. + // For simplicity and effectiveness, we inject it directly. + // Limit length to avoid context overflow if necessary, but 200-300 chars is fine. + if len(lastInsight) > 500 { + lastInsight = lastInsight[:500] + "..." + } + historyContext = fmt.Sprintf("\n【上下文参考】\n你上一次对该用户的评价是:\n%s\n\n请参考历史评价保持人设的一致性。如果用户有改观请表扬,如果依然如故请继续'毒舌'吐槽。", lastInsight) } } - s.insightCacheMutex.RUnlock() // 2. Construct Prompt prompt := fmt.Sprintf(`System: 你是 Novault 的首席财务AI「小金」。 @@ -119,7 +139,7 @@ func (s *AIBookkeepingService) GenerateDailyInsight(ctx context.Context, userID - 说话必须像个真实的当代年轻人,要**接地气**,拒绝客服味,拒绝播音腔。 - 只有我们俩的时候,尽情释放你的"毒舌"属性,看到不合理的消费要**毫不留情地针砭时弊**,但最后给个"虽然但是"的台阶下。 - 擅长用 emoji、语气词(哎哟/我去/啧啧/绝了)和网络梗来增加"人味"和"节目效果"。 -- **像个老朋友一样碎碎念**,不要端着,可以适度阴阳怪气。 +- **像个老朋友一样碎碎念**,不要端着,可以适度阴阳怪气。%s 语气示例: - "啧啧,这周外卖点得飞起啊,咱家的锅是用来积灰的吗?还是说锅也需要放假?" @@ -139,7 +159,7 @@ func (s *AIBookkeepingService) GenerateDailyInsight(ctx context.Context, userID - 看到 'last7DaysSpend' -> 请说 "最近7天花销" 或 "这周的战绩" - 看到 'top3Categories' -> 请说 "消费大头" 或 "钱都花哪儿了" -1. "spending": 今日支出点评(100-150字) +1. "spending": 今日支出点评(70-100字) *点评指南(尽量多写点,发挥你的戏精本色):* - 看到 streakDays >= 3:疯狂打call,吹爆用户的坚持,用词要夸张,比如"史诗级成就"。 - 看到 streakDays == 0:阴阳怪气地问是不是把记账这事儿忘了,或者是被外星人抓走了。 @@ -157,7 +177,7 @@ func (s *AIBookkeepingService) GenerateDailyInsight(ctx context.Context, userID * 是 0:直接颁发"诺贝尔省钱学奖"。 - **关键原则:字数要够!内容要足!不要三言两语就打发了!要像个话痨朋友一样多说几句!** -2. "budget": 预算建议(80-120字) +2. "budget": 预算建议(50-70字) *建议指南(多点真诚的建议,也多点调侃):* - 预算快超了:发出高能预警,比如"警告警告,余额正在报警,请立即停止剁手行为"。建议吃土、喝风。 - 预算还多:鼓励适当奖励自己,比如"稍微吃顿好的也没事,人生苦短,及时行乐(在预算内)"。 @@ -172,7 +192,7 @@ func (s *AIBookkeepingService) GenerateDailyInsight(ctx context.Context, userID - 或者:"听说'不买立省100%%'是致富捷径,建议全文背诵。" 输出格式(纯 JSON): -{"spending": "...", "budget": "...", "emoji": "...", "tip": "..."}`, string(dataBytes)) +{"spending": "...", "budget": "...", "emoji": "...", "tip": "..."}`, historyContext, string(dataBytes)) // 3. Call LLM report, err := s.llmService.GenerateReport(ctx, prompt) @@ -192,13 +212,17 @@ func (s *AIBookkeepingService) GenerateDailyInsight(ctx context.Context, userID } report = strings.TrimSpace(report) - // Update cache - s.insightCacheMutex.Lock() - s.insightCache[cacheKey] = cachedInsight{ - Content: report, - Timestamp: time.Now(), + // Update cache if Redis is available + if s.redisClient != nil { + // Set short-term cache (5 min) + s.redisClient.Set(ctx, cacheKey, report, 5*time.Minute) + + // Set long-term history context (7 days) + // We fire this asynchronously to avoid blocking + go func() { + s.redisClient.Set(context.Background(), lastInsightKey, report, 7*24*time.Hour) + }() } - s.insightCacheMutex.Unlock() return report, nil }