修复AI时间节点缓存过期与刷新策略

This commit is contained in:
hello-dd-code
2026-02-28 16:24:00 +08:00
parent 1b8ff310eb
commit ec9517e248
2 changed files with 230 additions and 4 deletions
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
@@ -95,19 +96,41 @@ func (s *SmokeAINextSmokeService) GetOrGenerate(ctx context.Context, user *userm
if err != nil {
return AINextSmokeSuggestion{}, err
}
var cachedSuggestion *AINextSmokeSuggestion
if cachedAdvice != nil {
return s.buildFromCache(ctx, cachedAdvice)
v, buildErr := s.buildFromCache(ctx, cachedAdvice)
if buildErr != nil {
log.Printf("[smoke_ai_next] cache_build_failed uid=%d plan_date=%s advice_id=%d err=%v", user.ID, planDate.Format("2006-01-02"), cachedAdvice.ID, buildErr)
} else if !s.shouldRefreshCache(asOf, planDate, v) {
log.Printf("[smoke_ai_next] cache_hit uid=%d plan_date=%s advice_id=%d", user.ID, planDate.Format("2006-01-02"), cachedAdvice.ID)
return v, nil
} else {
log.Printf("[smoke_ai_next] cache_stale uid=%d plan_date=%s advice_id=%d suggested_at=%s nodes=%d", user.ID, planDate.Format("2006-01-02"), cachedAdvice.ID, v.SuggestedAt, len(v.TimeNodes))
cachedSuggestion = &v
}
}
if s.cfg.APIKey == "" || s.cfg.Model == "" || s.cfg.BaseURL == "" {
if cachedSuggestion != nil {
log.Printf("[smoke_ai_next] ai_disabled_reuse_cache uid=%d plan_date=%s", user.ID, planDate.Format("2006-01-02"))
return *cachedSuggestion, nil
}
return AINextSmokeSuggestion{}, ErrAINextServiceDisabled
}
allowed, err := s.isAllowed(ctx, user, planDate)
if err != nil {
if cachedSuggestion != nil {
log.Printf("[smoke_ai_next] allow_check_failed_reuse_cache uid=%d plan_date=%s err=%v", user.ID, planDate.Format("2006-01-02"), err)
return *cachedSuggestion, nil
}
return AINextSmokeSuggestion{}, err
}
if !allowed {
if cachedSuggestion != nil {
log.Printf("[smoke_ai_next] no_refresh_permission_reuse_cache uid=%d plan_date=%s", user.ID, planDate.Format("2006-01-02"))
return *cachedSuggestion, nil
}
return AINextSmokeSuggestion{}, ErrAINextLocked
}
@@ -182,12 +205,43 @@ func (s *SmokeAINextSmokeService) GetOrGenerate(ctx context.Context, user *userm
Advice: strings.TrimSpace(output.Advice),
TokensIn: tokensIn,
TokensOut: tokensOut,
CreateTime: &createTime,
UpdateTime: &updateTime,
}
if err := s.db.WithContext(ctx).Create(&adviceRecord).Error; err != nil {
return AINextSmokeSuggestion{}, fmt.Errorf("save ai next smoke advice: %w", err)
if cachedAdvice == nil {
adviceRecord.CreateTime = &createTime
if err := s.db.WithContext(ctx).Create(&adviceRecord).Error; err != nil {
return AINextSmokeSuggestion{}, fmt.Errorf("save ai next smoke advice: %w", err)
}
log.Printf("[smoke_ai_next] cache_write_create uid=%d plan_date=%s advice_id=%d", user.ID, planDate.Format("2006-01-02"), adviceRecord.ID)
} else {
adviceRecord.ID = cachedAdvice.ID
adviceRecord.CreateTime = cachedAdvice.CreateTime
var tokensInValue interface{}
if tokensIn != nil {
tokensInValue = *tokensIn
}
var tokensOutValue interface{}
if tokensOut != nil {
tokensOutValue = *tokensOut
}
if err := s.db.WithContext(ctx).
Model(&smokemodel.SmokeAIAdvice{}).
Where("id = ?", cachedAdvice.ID).
Updates(map[string]interface{}{
"provider": adviceRecord.Provider,
"model": adviceRecord.Model,
"input_snapshot": adviceRecord.InputSnapshot,
"advice": adviceRecord.Advice,
"tokens_in": tokensInValue,
"tokens_out": tokensOutValue,
"updatetime": updateTime,
}).Error; err != nil {
return AINextSmokeSuggestion{}, fmt.Errorf("refresh ai next smoke advice: %w", err)
}
log.Printf("[smoke_ai_next] cache_write_refresh uid=%d plan_date=%s advice_id=%d", user.ID, planDate.Format("2006-01-02"), adviceRecord.ID)
}
// 2) 写入时间节点(每个时间点一条)
@@ -196,6 +250,14 @@ func (s *SmokeAINextSmokeService) GetOrGenerate(ctx context.Context, user *userm
return AINextSmokeSuggestion{}, err
}
if cachedAdvice != nil {
if err := s.db.WithContext(ctx).
Where("ai_advice_id = ?", adviceRecord.ID).
Delete(&smokemodel.SmokeAINextSmoke{}).Error; err != nil {
return AINextSmokeSuggestion{}, fmt.Errorf("clear old ai next smoke nodes: %w", err)
}
}
if err := s.saveNodes(ctx, int(user.ID), planDate, adviceRecord.ID, notBeforeAt, suggestedAt, nodes); err != nil {
return AINextSmokeSuggestion{}, err
}
@@ -231,6 +293,11 @@ func (s *SmokeAINextSmokeService) GetCached(ctx context.Context, user *usermodel
if err != nil {
return AINextSmokeSuggestion{}, false, err
}
if s.shouldRefreshCache(time.Now().In(time.Local), planDate, suggestion) {
log.Printf("[smoke_ai_next] cache_auto_expired uid=%d plan_date=%s advice_id=%d", user.ID, planDate.Format("2006-01-02"), cachedAdvice.ID)
return AINextSmokeSuggestion{}, false, nil
}
log.Printf("[smoke_ai_next] cache_auto_hit uid=%d plan_date=%s advice_id=%d", user.ID, planDate.Format("2006-01-02"), cachedAdvice.ID)
return suggestion, true, nil
}
@@ -239,6 +306,7 @@ func (s *SmokeAINextSmokeService) getCachedAdvice(ctx context.Context, uid int,
err := s.db.WithContext(ctx).
Where("uid = ? AND type = ? AND advice_date = ? AND prompt_version = ? AND (deletetime IS NULL OR deletetime = 0)",
uid, SmokeAIAdviceTypeNextSmoke, dateOnly(planDate).Format("2006-01-02"), promptVersion).
Order("id DESC").
First(&record).Error
if err == nil {
return &record, nil
@@ -342,6 +410,30 @@ func (s *SmokeAINextSmokeService) normalizeNodes(raw []string, asOf time.Time, p
return out, nil
}
func (s *SmokeAINextSmokeService) shouldRefreshCache(asOf time.Time, planDate time.Time, suggestion AINextSmokeSuggestion) bool {
if len(suggestion.TimeNodes) == 0 {
return true
}
suggestedAt, err := parseFlexibleTime(suggestion.SuggestedAt, planDate)
if err != nil {
return true
}
asOf = asOf.In(time.Local)
planDate = dateOnly(planDate)
if dateOnly(suggestedAt) != planDate {
return true
}
// 当天建议如果已经到点(或过期),视为缓存失效,触发刷新。
if planDate.Equal(dateOnly(asOf)) && !suggestedAt.After(asOf.Add(2*time.Minute)) {
return true
}
return false
}
func (s *SmokeAINextSmokeService) computeMinNotBefore(asOf time.Time, planDate time.Time, defaultSuggestion NextSmokeSuggestion, profile *adviceUserProfile) time.Time {
asOf = asOf.In(time.Local)
planDate = dateOnly(planDate)