// Package stats 提供首页"提问箱信息统计"所需的全站公开聚合数据。 // // 原本这些数据由 crontab 定时查询数据库、生成 question.count.txt 静态文件、 // 再由前端 fetch 外部 URL 展示。改为程序内直接计算后,这里用进程内 30 分钟缓存 // 吸收请求(节奏与原 crontab 一致),避免每个首页请求都打聚合查询到数据库。 package stats import ( "context" "fmt" "sync" "time" "github.com/tamakyi/TamaBox/internal/db" ) // View 全站公开统计展示模型。 // 同时提供原始数值字段与已排版的多行文本 Text,便于服务端模板渲染与 // 客户端定时刷新(/api/v1/site-stats)复用同一份内容。 type View struct { RegisteredUsers int64 `json:"registered_users"` TotalQuestions int64 `json:"total_questions"` AnsweredQuestions int64 `json:"answered_questions"` LastQuestionAt string `json:"last_question_at"` LastAnswerAt string `json:"last_answer_at"` UpdatedAt string `json:"updated_at"` // Text 已按原 question.count.txt 格式排版好的多行文本,直接展示即可。 Text string `json:"text"` } // cacheTTL 缓存有效期,与原 crontab 生成文件的节奏保持一致(半小时)。 const cacheTTL = 30 * time.Minute var ( mu sync.Mutex cached *View cachedAt time.Time ) // Get 返回全站统计。进程内缓存 cacheTTL,过期后才重新查询数据库。 func Get(ctx context.Context) (*View, error) { mu.Lock() if cached != nil && time.Since(cachedAt) < cacheTTL { v := cached mu.Unlock() return v, nil } mu.Unlock() userCount, err := db.Users.CountAll(ctx) if err != nil { return nil, err } qs, err := db.Questions.QuestionStats(ctx) if err != nil { return nil, err } v := &View{ RegisteredUsers: userCount, TotalQuestions: qs.TotalQuestions, AnsweredQuestions: qs.AnsweredQuestions, } if qs.LastQuestionAt != nil { v.LastQuestionAt = qs.LastQuestionAt.Format("2006-01-02 15:04:05") } if qs.LastAnswerAt != nil { v.LastAnswerAt = qs.LastAnswerAt.Format("2006-01-02 15:04:05") } v.UpdatedAt = time.Now().Format("2006-01-02 15:04:05") v.Text = formatText(v) mu.Lock() cached = v cachedAt = time.Now() mu.Unlock() return v, nil } // formatText 生成与原 question.count.txt 一致的多行展示文本。 func formatText(v *View) string { s := fmt.Sprintf("注册用户总数: %d\n全站问题总数: %d\n已获得回答的问题总数: %d", v.RegisteredUsers, v.TotalQuestions, v.AnsweredQuestions) if v.LastQuestionAt != "" { s += "\n最后一个提问发生在: " + v.LastQuestionAt } if v.LastAnswerAt != "" { s += "\n最后一个回答发生在: " + v.LastAnswerAt } s += "\n数据更新于:" + v.UpdatedAt return s } // Invalidate 清空缓存,供需要立即刷新统计的场景调用(如管理后台手动触发)。 func Invalidate() { mu.Lock() cached = nil cachedAt = time.Time{} mu.Unlock() }