101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
// 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()
|
|
}
|