This commit is contained in:
tamakyi
2026-09-08 10:10:44 +08:00
commit 2ed27dd55e
288 changed files with 67061 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
// 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()
}