package store import ( "database/sql" "encoding/json" "fmt" "log" "time" ) // Briefing 每日简报模型 type Briefing struct { ID string `json:"id"` UserID string `json:"user_id"` Date string `json:"date"` // YYYY-MM-DD Weather *WeatherData `json:"weather"` News []NewsItem `json:"news"` Reminders []BriefReminder `json:"reminders"` Summary string `json:"summary"` SummarySource string `json:"summary_source"` // "ai" | "fallback" Status string `json:"status"` // pending, generated, delivered GeneratedAt *time.Time `json:"generated_at,omitempty"` DeliveredAt *time.Time `json:"delivered_at,omitempty"` CreatedAt time.Time `json:"created_at"` } // WeatherData 天气数据 type WeatherData struct { Location string `json:"location"` Temp float64 `json:"temp"` Condition string `json:"condition"` Icon string `json:"icon"` } // NewsItem 新闻条目 type NewsItem struct { Title string `json:"title"` URL string `json:"url"` Source string `json:"source"` Summary string `json:"summary"` } // BriefReminder 简报中的提醒摘要 type BriefReminder struct { ID string `json:"id"` Title string `json:"title"` RemindAt string `json:"remind_at"` } // BriefingStore 每日简报持久化存储 type BriefingStore struct { db *sql.DB } // NewBriefingStore 使用已有数据库连接初始化简报存储并自动建表 func NewBriefingStore(db *sql.DB) (*BriefingStore, error) { store := &BriefingStore{db: db} if err := store.migrate(); err != nil { return nil, fmt.Errorf("简报表迁移失败: %w", err) } log.Println("[BriefingStore] 简报持久化存储已初始化") return store, nil } // migrate 自动创建简报表结构 func (s *BriefingStore) migrate() error { queries := []string{ `CREATE TABLE IF NOT EXISTS daily_briefings ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(255) NOT NULL, date DATE NOT NULL, weather JSONB DEFAULT '{}', news JSONB DEFAULT '[]', reminders JSONB DEFAULT '[]', summary TEXT DEFAULT '', summary_source VARCHAR(20) DEFAULT 'ai', status VARCHAR(20) DEFAULT 'pending', generated_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, date) )`, `ALTER TABLE daily_briefings ADD COLUMN IF NOT EXISTS summary_source VARCHAR(20) DEFAULT 'ai'`, `CREATE INDEX IF NOT EXISTS idx_briefings_user_id ON daily_briefings(user_id)`, `CREATE INDEX IF NOT EXISTS idx_briefings_date ON daily_briefings(date)`, `CREATE INDEX IF NOT EXISTS idx_briefings_user_date ON daily_briefings(user_id, date)`, } for _, q := range queries { if _, err := s.db.Exec(q); err != nil { return fmt.Errorf("迁移SQL执行失败: %w\nSQL: %s", err, q) } } return nil } // CreateOrUpdateBriefing upsert 简报 func (s *BriefingStore) CreateOrUpdateBriefing(b *Briefing) error { weatherJSON, err := json.Marshal(b.Weather) if err != nil { return fmt.Errorf("序列化天气数据失败: %w", err) } newsJSON, err := json.Marshal(b.News) if err != nil { return fmt.Errorf("序列化新闻数据失败: %w", err) } remindersJSON, err := json.Marshal(b.Reminders) if err != nil { return fmt.Errorf("序列化提醒数据失败: %w", err) } if b.SummarySource == "" { b.SummarySource = "ai" } _, err = s.db.Exec( `INSERT INTO daily_briefings (id, user_id, date, weather, news, reminders, summary, summary_source, status, generated_at, delivered_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (user_id, date) DO UPDATE SET weather = EXCLUDED.weather, news = EXCLUDED.news, reminders = EXCLUDED.reminders, summary = EXCLUDED.summary, summary_source = EXCLUDED.summary_source, status = EXCLUDED.status, generated_at = EXCLUDED.generated_at, delivered_at = EXCLUDED.delivered_at`, b.ID, b.UserID, b.Date, string(weatherJSON), string(newsJSON), string(remindersJSON), b.Summary, b.SummarySource, b.Status, b.GeneratedAt, b.DeliveredAt, ) if err != nil { return fmt.Errorf("upsert 简报失败: %w", err) } return nil } // GetBriefingByDate 获取指定日期简报 func (s *BriefingStore) GetBriefingByDate(userID, date string) (*Briefing, error) { row := s.db.QueryRow( `SELECT id, user_id, date::TEXT, weather, news, reminders, summary, COALESCE(summary_source, 'ai'), status, generated_at, delivered_at, created_at FROM daily_briefings WHERE user_id = $1 AND date = $2::DATE`, userID, date, ) b, err := s.scanBriefing(row) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("查询简报失败: %w", err) } return b, nil } // GetLatestBriefings 获取最近简报列表 func (s *BriefingStore) GetLatestBriefings(userID string, limit int) ([]Briefing, error) { if limit <= 0 { limit = 7 } rows, err := s.db.Query( `SELECT id, user_id, date::TEXT, weather, news, reminders, summary, COALESCE(summary_source, 'ai'), status, generated_at, delivered_at, created_at FROM daily_briefings WHERE user_id = $1 ORDER BY date DESC LIMIT $2`, userID, limit, ) if err != nil { return nil, fmt.Errorf("查询简报列表失败: %w", err) } defer rows.Close() var briefings []Briefing for rows.Next() { var ( id, uid, date, summary, summarySource, status string weatherRaw, newsRaw, remindersRaw []byte generatedAt, deliveredAt, createdAt sql.NullTime ) if err := rows.Scan(&id, &uid, &date, &weatherRaw, &newsRaw, &remindersRaw, &summary, &summarySource, &status, &generatedAt, &deliveredAt, &createdAt); err != nil { return nil, fmt.Errorf("扫描简报行失败: %w", err) } b := Briefing{ ID: id, UserID: uid, Date: date, Summary: summary, SummarySource: summarySource, Status: status, } if weatherRaw != nil { var w WeatherData if err := json.Unmarshal(weatherRaw, &w); err == nil { b.Weather = &w } } if newsRaw != nil { json.Unmarshal(newsRaw, &b.News) } if remindersRaw != nil { json.Unmarshal(remindersRaw, &b.Reminders) } if generatedAt.Valid { b.GeneratedAt = &generatedAt.Time } if deliveredAt.Valid { b.DeliveredAt = &deliveredAt.Time } b.CreatedAt = createdAt.Time // 确保切片不为 nil if b.News == nil { b.News = []NewsItem{} } if b.Reminders == nil { b.Reminders = []BriefReminder{} } if b.Weather == nil { b.Weather = &WeatherData{} } briefings = append(briefings, b) } if briefings == nil { briefings = []Briefing{} } return briefings, rows.Err() } // GetUsersWithBriefings 获取拥有简报的所有用户 ID 列表(用于调度器) func (s *BriefingStore) GetUsersWithBriefings() ([]string, error) { rows, err := s.db.Query(`SELECT DISTINCT user_id FROM daily_briefings`) if err != nil { return nil, fmt.Errorf("查询简报用户列表失败: %w", err) } defer rows.Close() var userIDs []string for rows.Next() { var uid string if err := rows.Scan(&uid); err != nil { return nil, fmt.Errorf("扫描用户ID失败: %w", err) } userIDs = append(userIDs, uid) } if userIDs == nil { userIDs = []string{} } return userIDs, rows.Err() } // GetAllUsers 获取所有用户 ID(从 reminders 表获取,作为降级方案) func (s *BriefingStore) GetAllUsers() ([]string, error) { rows, err := s.db.Query(`SELECT DISTINCT user_id FROM reminders`) if err != nil { return nil, fmt.Errorf("查询用户列表失败: %w", err) } defer rows.Close() var userIDs []string for rows.Next() { var uid string if err := rows.Scan(&uid); err != nil { return nil, fmt.Errorf("扫描用户ID失败: %w", err) } userIDs = append(userIDs, uid) } if userIDs == nil { userIDs = []string{} } return userIDs, rows.Err() } // scanBriefing 扫描单行简报 func (s *BriefingStore) scanBriefing(row *sql.Row) (*Briefing, error) { var ( id, uid, date, summary, summarySource, status string weatherRaw, newsRaw, remindersRaw []byte generatedAt, deliveredAt, createdAt sql.NullTime ) if err := row.Scan(&id, &uid, &date, &weatherRaw, &newsRaw, &remindersRaw, &summary, &summarySource, &status, &generatedAt, &deliveredAt, &createdAt); err != nil { return nil, err } b := &Briefing{ ID: id, UserID: uid, Date: date, Summary: summary, SummarySource: summarySource, Status: status, } if weatherRaw != nil { var w WeatherData if err := json.Unmarshal(weatherRaw, &w); err == nil { b.Weather = &w } } if b.Weather == nil { b.Weather = &WeatherData{} } if newsRaw != nil { json.Unmarshal(newsRaw, &b.News) } if b.News == nil { b.News = []NewsItem{} } if remindersRaw != nil { json.Unmarshal(remindersRaw, &b.Reminders) } if b.Reminders == nil { b.Reminders = []BriefReminder{} } if generatedAt.Valid { b.GeneratedAt = &generatedAt.Time } if deliveredAt.Valid { b.DeliveredAt = &deliveredAt.Time } b.CreatedAt = createdAt.Time return b, nil }