package service import ( "errors" "fmt" "time" "github.com/sundynix/pets-be/internal/model" ) // ErrAIQuotaExceeded 今日 AI 次数用完 var ErrAIQuotaExceeded = errors.New("今日 AI 次数已用完") // AI 调用是真金白银,不设上限等于把钱包交给用户。 // 额度按「用户 + 自然日」计,从后台配置读,改完立刻生效。 const ( aiKindChat = "chat" // 问问 AI aiKindSymptom = "symptom" // 异常观察评估 aiKindPlan = "plan" // AI 生成计划 ) // aiQuotaConfig 从后台配置取每日额度。取不到就用保守默认值, // 绝不「取不到就不限制」——那正是配置出问题时最不该发生的事。 func (s *Service) aiQuotaConfig() model.AIQuotaConfig { var c model.AIQuotaConfig if err := s.db.First(&c, "id = ?", "1").Error; err != nil { return model.AIQuotaConfig{DailyChat: 20, DailySymptom: 10, DailyPlan: 5, Enabled: true} } return c } // aiDailyLimit 某类 AI 功能的每日上限;<=0 表示不限 func (s *Service) aiDailyLimit(kind string) (int, bool) { c := s.aiQuotaConfig() if !c.Enabled { return 0, false } switch kind { case aiKindChat: return c.DailyChat, true case aiKindSymptom: return c.DailySymptom, true case aiKindPlan: return c.DailyPlan, true } return 0, false } // consumeAIQuota 扣一次额度。超限返回 ErrAIQuotaExceeded,调用方要在真正 // 请求大模型之前调用它——扣完再调,失败也算用掉一次,避免刷接口空转烧钱。 func (s *Service) consumeAIQuota(userID, kind string) error { limit, on := s.aiDailyLimit(kind) if !on || limit <= 0 { return nil } day := time.Now().Format("2006-01-02") var u model.AIUsage err := s.db.Where("user_id = ? AND day = ? AND kind = ?", userID, day, kind).First(&u).Error if err == nil { if u.Count >= limit { return fmt.Errorf("%w(每天 %d 次,明天恢复)", ErrAIQuotaExceeded, limit) } return s.db.Model(&model.AIUsage{}).Where("id = ?", u.ID). UpdateColumn("count", u.Count+1).Error } return s.db.Create(&model.AIUsage{UserID: userID, Day: day, Kind: kind, Count: 1}).Error } // AIQuotaLeft 返回各类今日剩余次数,给小程序显示 func (s *Service) AIQuotaLeft(userID string) map[string]any { c := s.aiQuotaConfig() day := time.Now().Format("2006-01-02") var rows []model.AIUsage s.db.Where("user_id = ? AND day = ?", userID, day).Find(&rows) used := map[string]int{} for _, r := range rows { used[r.Kind] = r.Count } left := func(limit int, kind string) int { if !c.Enabled || limit <= 0 { return -1 // -1 表示不限 } if n := limit - used[kind]; n > 0 { return n } return 0 } return map[string]any{ "enabled": c.Enabled, "chat": left(c.DailyChat, aiKindChat), "symptom": left(c.DailySymptom, aiKindSymptom), "plan": left(c.DailyPlan, aiKindPlan), } } // GetAIQuotaConfig / SaveAIQuotaConfig 后台读写 func (s *Service) GetAIQuotaConfig() model.AIQuotaConfig { return s.aiQuotaConfig() } func (s *Service) SaveAIQuotaConfig(in model.AIQuotaConfig) (model.AIQuotaConfig, error) { c := s.aiQuotaConfig() c.ID = "1" c.Enabled = in.Enabled c.DailyChat = maxInt(in.DailyChat, 0) c.DailySymptom = maxInt(in.DailySymptom, 0) c.DailyPlan = maxInt(in.DailyPlan, 0) if err := s.db.Save(&c).Error; err != nil { return c, err } return c, nil }