feat(stats): 新增公开数据页 /stats,入口收敛到页脚

首页底部那张「站点统计」卡片下线,完整数据移到公开只读页 /stats:
今日实时数、概览六格、近 30 天折线与 12 个月柱状、三个占比环、
7×24 时段热力图、提问字数与回答耗时分布。

- 聚合在 internal/db/analytics.go(SUM(CASE WHEN),方言差异收敛在 analyticsDialectFor)
- 结果按自然日缓存,跨日重算一次后全站共用;重算失败降级返回上一份快照
- 图表零前端依赖:服务端算坐标的 SVG + CSS grid,禁用 JS 页面仍完整
- 环比取「上月 vs 上上月」:本月未过完,否则每月 1 号会得出 -95%
- 入口只在页脚「社区规则」与「找站长」之间;route.Home 不再计算统计
- 补 questions(created_at) 索引;启动后异步预热当天快照
- 测试:analytics 连真库验证软删除不计入、分桶下标、DOW 对齐
This commit is contained in:
tamakyi
2026-10-24 00:57:08 +08:00
parent c680671b7e
commit a5441c99fd
25 changed files with 2280 additions and 80 deletions
+77
View File
@@ -951,4 +951,81 @@ var enDict = map[string]string{
"theme.name.sakura": "Sakura",
"theme.name.shiroko": "Shiroko",
"theme.name.new-sakura": "New Sakura",
// Site statistics page (/stats): aggregate numbers only
"site.footer.stats": "Stats",
"stats.title": "Site statistics",
"stats.lead": "Only aggregate numbers here: how many questions the site receives, how fast they get answered, and when people tend to show up. No individual question, asker identity, or source address is shown.",
"stats.unavailable": "Statistics are temporarily unavailable. Please check back later.",
"stats.today.questions": "Asked today",
"stats.today.answers": "Answered today",
"stats.today.tag": "(live)",
"stats.kpi.users": "Registered users",
"stats.kpi.questions": "Questions asked",
"stats.kpi.answered": "Answered",
"stats.kpi.answerRate": "Answer rate",
"stats.kpi.activeBoxes": "Boxes that received questions",
"stats.kpi.messages": "Direct messages",
"stats.insight.title": "At a glance",
"stats.insight.note": "Each line below is read straight from the numbers above — no guesswork.",
"stats.insight.empty": "Not enough question data yet.",
"stats.insight.daily30": "%s questions in the last 30 days, about %.1f per day.",
"stats.insight.peak": "Busiest day was %s with %s questions.",
"stats.insight.busiest": "Questions cluster around %s %d:00.",
"stats.insight.mom": "Last month versus the month before: %s%% questions.",
"stats.insight.fastReply": "%s%% of answered questions got a reply within 24 hours.",
"stats.insight.private": "%s%% of questions were sent privately.",
"stats.trend.dailyTitle": "Questions over the last 30 days",
"stats.trend.dailyNote": "Daily question count.",
"stats.trend.monthlyTitle": "Questions per month (last 12)",
"stats.trend.monthlyNote": "The last bar is the current month, still in progress.",
"stats.mix.title": "Composition",
"stats.mix.note": "Three ratios: public or private, answered or not, and whether the asker was signed in.",
"stats.mix.public": "Public questions",
"stats.mix.answered": "Answered",
"stats.mix.login": "From signed-in users",
"stats.heat.title": "When questions arrive",
"stats.heat.note": "Questions from the last %d days grouped by weekday and hour — darker means busier.",
"stats.heat.less": "Less",
"stats.heat.more": "More",
"stats.content.title": "Length and pace",
"stats.content.note": "How long questions are, and how fast they get answered.",
"stats.content.lengthTitle": "Question length",
"stats.content.replyTitle": "Time to answer",
"stats.len.1": "under 20 chars",
"stats.len.2": "20–49 chars",
"stats.len.3": "50–99 chars",
"stats.len.4": "100–199 chars",
"stats.len.5": "200–499 chars",
"stats.len.6": "500+ chars",
"stats.reply.1": "within 1 hour",
"stats.reply.2": "1–6 hours",
"stats.reply.3": "6–24 hours",
"stats.reply.4": "1–3 days",
"stats.reply.5": "3–7 days",
"stats.reply.6": "over 7 days",
"stats.wd.1": "Mon",
"stats.wd.2": "Tue",
"stats.wd.3": "Wed",
"stats.wd.4": "Thu",
"stats.wd.5": "Fri",
"stats.wd.6": "Sat",
"stats.wd.7": "Sun",
"stats.foot.dataDate": "Data as of",
"stats.foot.updated": "computed at",
"stats.foot.prevMonth": "Last question:",
"stats.foot.prevAnswer": "Last answer:",
"stats.foot.privacy": "All figures are aggregates; no individual question, asker, or source address is shown.",
"stats.foot.cacheNote": "Totals and charts are a snapshot taken at 00:00 local time, refreshed once a day; the two \"today\" figures are live.",
}