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.",
}
+77
View File
@@ -950,4 +950,81 @@ var jaDict = map[string]string{
"theme.name.sakura": "さくら",
"theme.name.shiroko": "シロコ",
"theme.name.new-sakura": "新さくら",
// サイト統計ページ(/stats):集計値のみ
"site.footer.stats": "統計",
"stats.title": "サイト統計",
"stats.lead": "ここにあるのは集計値だけです。サイト全体にどれだけ質問が届き、どれくらいで回答され、いつ人が来るのか。個々の質問内容・質問者の身元・送信元アドレスは一切表示しません。",
"stats.unavailable": "統計データを一時的に取得できません。しばらくしてから再度お試しください。",
"stats.today.questions": "今日の質問",
"stats.today.answers": "今日の回答",
"stats.today.tag": "(リアルタイム)",
"stats.kpi.users": "登録ユーザー",
"stats.kpi.questions": "質問の総数",
"stats.kpi.answered": "回答済み",
"stats.kpi.answerRate": "回答率",
"stats.kpi.activeBoxes": "質問を受け取った箱",
"stats.kpi.messages": "DM の総数",
"stats.insight.title": "ひと目でわかる数字",
"stats.insight.note": "以下の文はすべて上の数値から直接読み取ったもので、推測は含みません。",
"stats.insight.empty": "まだ分析できるほどの質問データがありません。",
"stats.insight.daily30": "直近 30 日で %s 件の質問、1 日あたり約 %.1f 件。",
"stats.insight.peak": "最も多かった日は %s で %s 件でした。",
"stats.insight.busiest": "質問は %s %d 時ごろに集中しています。",
"stats.insight.mom": "先月は前月比 %s%%。",
"stats.insight.fastReply": "回答済みのうち %s%% が 24 時間以内に回答されています。",
"stats.insight.private": "質問の %s%% は非公開が選ばれています。",
"stats.trend.dailyTitle": "直近 30 日の質問数",
"stats.trend.dailyNote": "1 日ごとの質問数です。",
"stats.trend.monthlyTitle": "直近 12 か月の質問数",
"stats.trend.monthlyNote": "最後の棒は当月で、まだ集計中です。",
"stats.mix.title": "内訳",
"stats.mix.note": "3 つの割合:公開か非公開か、回答済みか、ログイン済みの利用者か。",
"stats.mix.public": "公開の質問",
"stats.mix.answered": "回答済み",
"stats.mix.login": "ログイン利用者の質問",
"stats.heat.title": "質問が届く時間帯",
"stats.heat.note": "直近 %d 日の質問を「曜日 × 時間」で集計。色が濃いほど集中しています。",
"stats.heat.less": "少",
"stats.heat.more": "多",
"stats.content.title": "長さと速さ",
"stats.content.note": "質問の長さと、回答までの速さ。",
"stats.content.lengthTitle": "質問の文字数",
"stats.content.replyTitle": "回答までの時間",
"stats.len.1": "20 文字未満",
"stats.len.2": "20–49 文字",
"stats.len.3": "50–99 文字",
"stats.len.4": "100–199 文字",
"stats.len.5": "200–499 文字",
"stats.len.6": "500 文字以上",
"stats.reply.1": "1 時間以内",
"stats.reply.2": "1–6 時間",
"stats.reply.3": "6–24 時間",
"stats.reply.4": "1–3 日",
"stats.reply.5": "3–7 日",
"stats.reply.6": "7 日以上",
"stats.wd.1": "月",
"stats.wd.2": "火",
"stats.wd.3": "水",
"stats.wd.4": "木",
"stats.wd.5": "金",
"stats.wd.6": "土",
"stats.wd.7": "日",
"stats.foot.dataDate": "集計基準日:",
"stats.foot.updated": "算出時刻",
"stats.foot.prevMonth": "最後の質問:",
"stats.foot.prevAnswer": "最後の回答:",
"stats.foot.privacy": "数値はすべて集計値です。個々の質問内容・質問者の身元・送信元アドレスは表示しません。",
"stats.foot.cacheNote": "累計値とグラフは当日 0 時のスナップショットで、1 日 1 回だけ更新されます。「今日」の 2 項目はリアルタイムです。",
}
+77
View File
@@ -951,4 +951,81 @@ var zhCNDict = map[string]string{
"form.register.recaptcha": "无法加载人机验证,请刷新页面或切换网络后重试",
"form.register.agreeRules": "请先阅读并同意社区规则",
"form.register.repeatPassword": "两次输入的密码不一致",
// 全站数据页(/stats):只展示聚合数字,不含任何单条内容
"site.footer.stats": "数据",
"stats.title": "全站数据",
"stats.lead": "这里只有汇总后的数字:全站收到了多少提问、回答得有多快、大家习惯什么时候来。不含任何一条具体提问的内容、提问者身份或来源地址。",
"stats.unavailable": "数据暂时不可用,稍后再刷新看看。",
"stats.today.questions": "今日提问",
"stats.today.answers": "今日回答",
"stats.today.tag": "(实时)",
"stats.kpi.users": "注册用户",
"stats.kpi.questions": "累计提问",
"stats.kpi.answered": "已回答",
"stats.kpi.answerRate": "回答率",
"stats.kpi.activeBoxes": "收到过提问的箱子",
"stats.kpi.messages": "累计私聊消息",
"stats.insight.title": "数据速览",
"stats.insight.note": "下面几句是从上面的数字直接读出来的,没有推测。",
"stats.insight.empty": "提问数据还太少,暂时看不出什么。",
"stats.insight.daily30": "近 30 天共收到 %s 条提问,日均 %.1f 条。",
"stats.insight.peak": "最忙的一天是 %s,收到 %s 条提问。",
"stats.insight.busiest": "提问最集中在 %s %d 点前后。",
"stats.insight.mom": "上月提问量比前一个月 %s%%。",
"stats.insight.fastReply": "已回答的提问中,%s%% 在 24 小时内拿到了回复。",
"stats.insight.private": "%s%% 的提问选择了私密(不公开)。",
"stats.trend.dailyTitle": "近 30 天提问趋势",
"stats.trend.dailyNote": "按天统计的提问量。",
"stats.trend.monthlyTitle": "近 12 个月提问量",
"stats.trend.monthlyNote": "最后一根柱子是当月,还没过完。",
"stats.mix.title": "构成",
"stats.mix.note": "三组比例:公开还是私密、有没有被回答、提问者有没有登录。",
"stats.mix.public": "公开提问",
"stats.mix.answered": "已回答",
"stats.mix.login": "登录用户提问",
"stats.heat.title": "一周内的提问时段",
"stats.heat.note": "最近 %d 天的提问按「星期 × 小时」聚合,颜色越深表示越集中。",
"stats.heat.less": "少",
"stats.heat.more": "多",
"stats.content.title": "内容与节奏",
"stats.content.note": "提问有多长,回答有多快。",
"stats.content.lengthTitle": "提问字数分布",
"stats.content.replyTitle": "回答耗时分布",
"stats.len.1": "20 字以内",
"stats.len.2": "20–49 字",
"stats.len.3": "50–99 字",
"stats.len.4": "100–199 字",
"stats.len.5": "200–499 字",
"stats.len.6": "500 字以上",
"stats.reply.1": "1 小时内",
"stats.reply.2": "1–6 小时",
"stats.reply.3": "6–24 小时",
"stats.reply.4": "1–3 天",
"stats.reply.5": "3–7 天",
"stats.reply.6": "7 天以上",
"stats.wd.1": "一",
"stats.wd.2": "二",
"stats.wd.3": "三",
"stats.wd.4": "四",
"stats.wd.5": "五",
"stats.wd.6": "六",
"stats.wd.7": "日",
"stats.foot.dataDate": "数据基准日:",
"stats.foot.updated": "计算于",
"stats.foot.prevMonth": "最后一条提问:",
"stats.foot.prevAnswer": "最后一条回答:",
"stats.foot.privacy": "所有数字均为聚合结果,不展示单条提问内容、提问者身份或来源地址。",
"stats.foot.cacheNote": "累计数字与图表是当日 0 点的快照,每天只更新一次;「今日」两项为实时数字。",
}
+77
View File
@@ -950,4 +950,81 @@ var zhTWDict = map[string]string{
"theme.name.sakura": "櫻花",
"theme.name.shiroko": "砂狼白子",
"theme.name.new-sakura": "新·櫻花",
// 全站數據頁(/stats):只顯示彙總數字,不含任何單則內容
"site.footer.stats": "數據",
"stats.title": "全站數據",
"stats.lead": "這裡只有彙總後的數字:全站收到多少提問、回答得有多快、大家習慣什麼時候來。不含任何一則具體提問的內容、提問者身分或來源位址。",
"stats.unavailable": "數據暫時無法使用,稍後再重新整理看看。",
"stats.today.questions": "今日提問",
"stats.today.answers": "今日回答",
"stats.today.tag": "(即時)",
"stats.kpi.users": "註冊使用者",
"stats.kpi.questions": "累計提問",
"stats.kpi.answered": "已回答",
"stats.kpi.answerRate": "回答率",
"stats.kpi.activeBoxes": "收到過提問的箱子",
"stats.kpi.messages": "累計私聊訊息",
"stats.insight.title": "數據速覽",
"stats.insight.note": "下面幾句是直接從上面的數字讀出來的,沒有推測。",
"stats.insight.empty": "提問數據還太少,暫時看不出什麼。",
"stats.insight.daily30": "近 30 天共收到 %s 則提問,日均 %.1f 則。",
"stats.insight.peak": "最忙的一天是 %s,收到 %s 則提問。",
"stats.insight.busiest": "提問最集中在 %s %d 點前後。",
"stats.insight.mom": "上月提問量比前一個月 %s%%。",
"stats.insight.fastReply": "已回答的提問中,%s%% 在 24 小時內得到回覆。",
"stats.insight.private": "%s%% 的提問選擇了私密(不公開)。",
"stats.trend.dailyTitle": "近 30 天提問趨勢",
"stats.trend.dailyNote": "依天統計的提問量。",
"stats.trend.monthlyTitle": "近 12 個月提問量",
"stats.trend.monthlyNote": "最後一根柱子是當月,還沒過完。",
"stats.mix.title": "組成",
"stats.mix.note": "三組比例:公開還是私密、有沒有被回答、提問者有沒有登入。",
"stats.mix.public": "公開提問",
"stats.mix.answered": "已回答",
"stats.mix.login": "登入使用者提問",
"stats.heat.title": "一週內的提問時段",
"stats.heat.note": "最近 %d 天的提問依「星期 × 小時」彙總,顏色越深表示越集中。",
"stats.heat.less": "少",
"stats.heat.more": "多",
"stats.content.title": "內容與節奏",
"stats.content.note": "提問有多長,回答有多快。",
"stats.content.lengthTitle": "提問字數分布",
"stats.content.replyTitle": "回答耗時分布",
"stats.len.1": "20 字以內",
"stats.len.2": "20–49 字",
"stats.len.3": "50–99 字",
"stats.len.4": "100–199 字",
"stats.len.5": "200–499 字",
"stats.len.6": "500 字以上",
"stats.reply.1": "1 小時內",
"stats.reply.2": "1–6 小時",
"stats.reply.3": "6–24 小時",
"stats.reply.4": "1–3 天",
"stats.reply.5": "3–7 天",
"stats.reply.6": "7 天以上",
"stats.wd.1": "一",
"stats.wd.2": "二",
"stats.wd.3": "三",
"stats.wd.4": "四",
"stats.wd.5": "五",
"stats.wd.6": "六",
"stats.wd.7": "日",
"stats.foot.dataDate": "數據基準日:",
"stats.foot.updated": "計算於",
"stats.foot.prevMonth": "最後一則提問:",
"stats.foot.prevAnswer": "最後一則回答:",
"stats.foot.privacy": "所有數字均為彙總結果,不展示單則提問內容、提問者身分或來源位址。",
"stats.foot.cacheNote": "累計數字與圖表是當日 0 點的快照,每天只更新一次;「今日」兩項為即時數字。",
}