package store import ( "time" "daili/internal/domain" ) // Stats 大屏统计数据。 type Stats struct { Customers int ActiveSubs int PausedSubs int DueToday int Due7 int DueTodaySubs []Subscription Heat map[string]int // 结束日 -> 该日到期数量 MonthDue int Top []Subscription TodayTasks int TodayDone int } // Badges 顶栏常驻徽标。 func (db *DB) Badges(today time.Time) (dueToday, due3, due7 int, err error) { t := domain.Date(today) const notArchived = ` AND IFNULL((SELECT archived FROM customers c WHERE c.id=customer_id),0)=0` if err = db.sql.QueryRow(`SELECT COUNT(*) FROM subscriptions WHERE status<>'closed' AND end_date=?`+notArchived, t).Scan(&dueToday); err != nil { return } if err = db.sql.QueryRow(`SELECT COUNT(*) FROM subscriptions WHERE status<>'closed' AND end_date BETWEEN ? AND ?`+notArchived, t, domain.Date(domain.AddDays(today, 2))).Scan(&due3); err != nil { return } err = db.sql.QueryRow(`SELECT COUNT(*) FROM subscriptions WHERE status<>'closed' AND end_date BETWEEN ? AND ?`+notArchived, t, domain.Date(domain.AddDays(today, 6))).Scan(&due7) return } // ScreenStats 汇总大屏所需数据。 func (db *DB) ScreenStats(today time.Time, warnDays int) (*Stats, error) { st := &Stats{Heat: map[string]int{}} var err error if st.Customers, err = db.CustomerCount(); err != nil { return nil, err } if st.ActiveSubs, err = db.CountActiveSubs(domain.Date(today)); err != nil { return nil, err } if err = db.sql.QueryRow(`SELECT COUNT(*) FROM subscriptions WHERE status='paused' AND IFNULL((SELECT archived FROM customers c WHERE c.id=customer_id),0)=0`).Scan(&st.PausedSubs); err != nil { return nil, err } if err = db.sql.QueryRow(`SELECT COUNT(*) FROM subscriptions WHERE status<>'closed' AND end_date=? AND IFNULL((SELECT archived FROM customers c WHERE c.id=customer_id),0)=0`, domain.Date(today)).Scan(&st.DueToday); err != nil { return nil, err } if err = db.sql.QueryRow(`SELECT COUNT(*) FROM subscriptions WHERE status<>'closed' AND end_date BETWEEN ? AND ? AND IFNULL((SELECT archived FROM customers c WHERE c.id=customer_id),0)=0`, domain.Date(today), domain.Date(domain.AddDays(today, 6))).Scan(&st.Due7); err != nil { return nil, err } if st.DueTodaySubs, err = db.DueOn(domain.Date(today)); err != nil { return nil, err } // 本月到期热力 ms := domain.MonthStart(today.Year(), today.Month()) me := domain.MonthEnd(today.Year(), today.Month()) rows, err := db.sql.Query(`SELECT end_date, COUNT(*) FROM subscriptions s JOIN customers c ON c.id=s.customer_id WHERE s.status<>'closed' AND IFNULL(c.archived,0)=0 AND s.end_date BETWEEN ? AND ? GROUP BY s.end_date`, domain.Date(ms), domain.Date(me)) if err != nil { return nil, err } for rows.Next() { var d string var n int if err := rows.Scan(&d, &n); err != nil { rows.Close() return nil, err } st.Heat[d] = n st.MonthDue += n } rows.Close() // TOP:按日按剩余天数、按次按剩余次数,各自排序后交替取前 10 t := domain.Date(today) daySubs, err := db.querySubs(subSelect+` WHERE s.status<>'closed' AND IFNULL(c.archived,0)=0 AND s.unit='day' AND s.end_date>=? ORDER BY s.end_date ASC LIMIT 12`, t) if err != nil { return nil, err } cntSubs, err := db.querySubs(subSelect+` WHERE s.status<>'closed' AND IFNULL(c.archived,0)=0 AND s.unit='count' AND s.used_count < s.total_count AND (IFNULL(s.end_date,'')='' OR s.end_date>=?) ORDER BY (s.total_count - s.used_count) ASC LIMIT 12`, t) if err != nil { return nil, err } for i := 0; i < 10 && len(st.Top) < 10; i++ { if i < len(daySubs) { st.Top = append(st.Top, daySubs[i]) } if len(st.Top) < 10 && i < len(cntSubs) { st.Top = append(st.Top, cntSubs[i]) } if i >= len(daySubs) && i >= len(cntSubs) { break } } _ = warnDays return st, nil } // RecentDue 返回未来 n 天内到期的订阅(按到期日排序)。 func (db *DB) RecentDue(from string, days int) ([]Subscription, error) { to := domain.Date(domain.AddDays(domain.ParseDateOr(from, domain.Today()), days-1)) return db.querySubs(subSelect+` WHERE s.status<>'closed' AND IFNULL(c.archived,0)=0 AND s.end_date BETWEEN ? AND ? ORDER BY s.end_date, s.id`, from, to) }