feat(insights): 箱主数据面板——访问量、提问趋势、处理漏斗
新增 /user/insights,让箱主第一次能看到「有多少人来过我的箱、提问处理得怎么样」。 数据来源: - 新表 box_views:一行 = 一访客×一天×一箱,views 累加,唯一索引 (user_id, viewed_on, visitor)。刻意不内嵌 dbutil.Model —— 软删除 + 唯一索引 会让被删行永久占槽,且删过的槽位再也插不进去。 - 访客标识 = HMAC(日盐, IP) 前 32 hex:同日可去重(UV 准)、跨日不可关联(隐私)。 代价是近 7/30 天 UV = 各日 UV 之和,页面上已写明口径。 写入是异步的:buffered channel 512 + 2s 批量 flush,队列满即丢(统计尽力而为, 不该因为记一笔访问就拖慢页面)。过滤箱主本人、空 UA、爬虫 UA、预取请求。 密钥复用 server.xsrf_key;每日清理 400 天前的明细。 页面(纯 CSS 画图,不引图表库): - 今日/近 7 天/近 30 天 KPI + 上周对比 - 30 天 PV/UV 双柱趋势、24 小时时段分布 - 提问处理漏斗 + 回答率进度条 + 自动结论 聚合层 internal/db/box_analytics.go 复用 analyticsDialectFor 的时区处理, 与全站 stats 分开(那个不含单箱维度、按自然日缓存)。 测试:box_views 4 项(含「同批同键先内存合并」——PG 的 ON CONFLICT 不允许 同条 INSERT 二次命中同一行)、insights 的 pct/窗口切分/visitorID/爬虫过滤。
This commit is contained in:
@@ -1054,6 +1054,50 @@ var enDict = map[string]string{
|
||||
"stats.weekday.6": "Saturday",
|
||||
"stats.weekday.7": "Sunday",
|
||||
|
||||
// Owner insights panel (/user/insights)
|
||||
"insights.title": "Insights",
|
||||
"insights.heading": "Your insights",
|
||||
"insights.lead": "These numbers are yours alone: how many people opened your box, how many questions arrived, and how many are still waiting. Everything is aggregated — no visitor or asker is ever identified.",
|
||||
"insights.back": "Back to settings",
|
||||
"insights.entryTitle": "Insights",
|
||||
"insights.entryDesc": "See how many people visit your box and how your questions are doing",
|
||||
"insights.summary": "At a glance",
|
||||
"insights.noViews": "No visits recorded yet. Numbers will appear once someone opens your box.",
|
||||
"insights.today": "Today",
|
||||
"insights.week": "Last 7 days",
|
||||
"insights.month": "Last 30 days",
|
||||
"insights.newQuestions": "new questions",
|
||||
"insights.pv": "views",
|
||||
"insights.uv": "visitors",
|
||||
"insights.trendTitle": "Views over the last 30 days",
|
||||
"insights.trendNote": "One bar per day: the light part is page views, the dark part is unique visitors that day. Hover a bar for its views / visitors / questions.",
|
||||
"insights.legendPV": "Page views",
|
||||
"insights.legendUV": "Unique visitors (per day)",
|
||||
"insights.legendHover": "Hover for daily details",
|
||||
"insights.hourTitle": "When people show up",
|
||||
"insights.hourNote": "When questions arrived during the last 30 days. The brightest bar is your busiest hour.",
|
||||
"insights.funnelTitle": "Question handling",
|
||||
"insights.funnelNote": "All questions split into answered / read but unanswered / untouched.",
|
||||
"insights.funnelTotal": "All questions",
|
||||
"insights.funnelAnswered": "Answered",
|
||||
"insights.funnelReadOnly": "Read, not answered",
|
||||
"insights.funnelUnread": "Not read",
|
||||
"insights.answerRate": "Answer rate: answered ÷ all questions",
|
||||
"insights.privacyTitle": "About these numbers",
|
||||
"insights.privacyNote": "Visit stats only record \"someone visited N times on a given day\". A visitor is an IP hash with a per-day salt — no IP, no user agent, no page path is stored, and the same person cannot be linked across days. The 7-day / 30-day visitor counts are therefore the sum of daily uniques: someone visiting on several days counts several times. Your own visits to your own box are excluded.",
|
||||
|
||||
"insights.insight.empty": "No data yet. This page comes alive once you get your first question or someone opens your box.",
|
||||
"insights.insight.weekViews": "%s views from %s visitors in the last 7 days.",
|
||||
"insights.insight.conversion": "About %s%% of visitors left a message in the last 30 days.",
|
||||
"insights.insight.bestDay": "Your busiest day was %s with %s views.",
|
||||
"insights.insight.peakHour": "Questions cluster around %d:00.",
|
||||
"insights.insight.answerRate": "You have answered %s%% of all questions.",
|
||||
"insights.insight.fastReply": "%s%% of your answers went out within 24 hours.",
|
||||
"insights.insight.unread": "%s questions are still waiting.",
|
||||
"insights.insight.trendUp": "Views this week are up %s%% on last week.",
|
||||
"insights.insight.trendDown": "Views this week are down %s%% on last week.",
|
||||
"insights.insight.trendFlat": "Views this week are about the same as last week.",
|
||||
|
||||
"stats.foot.updated": "Snapshot:",
|
||||
"stats.foot.prevMonth": "Last question:",
|
||||
"stats.foot.prevAnswer": "Last answer:",
|
||||
|
||||
@@ -1053,6 +1053,50 @@ var jaDict = map[string]string{
|
||||
"stats.weekday.6": "土曜日",
|
||||
"stats.weekday.7": "日曜日",
|
||||
|
||||
// 質問箱オーナーのインサイト(/user/insights)
|
||||
"insights.title": "インサイト",
|
||||
"insights.heading": "あなたのインサイト",
|
||||
"insights.lead": "この数字はあなただけのものです。何人が質問箱を開いたか、いくつ質問が届いたか、まだ手つかずの質問がいくつあるか。すべて集計値で、訪問者や質問者の身元は一切含みません。",
|
||||
"insights.back": "設定に戻る",
|
||||
"insights.entryTitle": "インサイト",
|
||||
"insights.entryDesc": "質問箱に何人来たか、質問の対応状況を見る",
|
||||
"insights.summary": "ひと目で",
|
||||
"insights.noViews": "まだ訪問の記録がありません。誰かが質問箱を開くと数字が出はじめます。",
|
||||
"insights.today": "今日",
|
||||
"insights.week": "過去 7 日間",
|
||||
"insights.month": "過去 30 日間",
|
||||
"insights.newQuestions": "新着の質問",
|
||||
"insights.pv": "回表示",
|
||||
"insights.uv": "人",
|
||||
"insights.trendTitle": "過去 30 日間の閲覧推移",
|
||||
"insights.trendNote": "1 本が 1 日分。淡い部分が閲覧数、濃い部分がその日の重複を除いた訪問者数です。棒にカーソルを合わせると当日の閲覧/訪問者/質問数が出ます。",
|
||||
"insights.legendPV": "閲覧数(PV)",
|
||||
"insights.legendUV": "ユニーク訪問者(日ごとに重複排除)",
|
||||
"insights.legendHover": "カーソルで当日の内訳",
|
||||
"insights.hourTitle": "いつ来ているか",
|
||||
"insights.hourNote": "過去 30 日に届いた質問の時間帯分布。最も明るい棒が一番忙しい時間帯です。",
|
||||
"insights.funnelTitle": "質問の対応状況",
|
||||
"insights.funnelNote": "すべての質問を「回答済み/既読・未回答/未読」に分けたものです。",
|
||||
"insights.funnelTotal": "すべての質問",
|
||||
"insights.funnelAnswered": "回答済み",
|
||||
"insights.funnelReadOnly": "既読・未回答",
|
||||
"insights.funnelUnread": "未読",
|
||||
"insights.answerRate": "回答率:回答済み ÷ 全質問",
|
||||
"insights.privacyTitle": "この数字について",
|
||||
"insights.privacyNote": "訪問統計は「ある日に何回来たか」だけを記録します。訪問者の識別子は日ごとに塩を変えた IP ハッシュで、IP・UA・閲覧ページは保存しません。そのため日をまたいで同一人物を紐付けることはできず、7 日/30 日の訪問者数は各日の重複排除後の合計です(同じ人が数日来るとその人数分カウントされます)。質問箱の持ち主自身のアクセスは含みません。",
|
||||
|
||||
"insights.insight.empty": "まだデータがありません。最初の質問が届くか、誰かが質問箱を開くと表示されはじめます。",
|
||||
"insights.insight.weekViews": "過去 7 日間に %s 回の閲覧、%s 人の訪問者。",
|
||||
"insights.insight.conversion": "過去 30 日間、訪問者の約 %s%% が質問を残しました。",
|
||||
"insights.insight.bestDay": "一番にぎわったのは %s で、%s 回閲覧されました。",
|
||||
"insights.insight.peakHour": "質問は %d 時前後に集中しています。",
|
||||
"insights.insight.answerRate": "すべての質問のうち %s%% に回答済みです。",
|
||||
"insights.insight.fastReply": "回答の %s%% は 24 時間以内に返しています。",
|
||||
"insights.insight.unread": "未対応の質問が %s 件あります。",
|
||||
"insights.insight.trendUp": "今週の閲覧は先週より %s%% 増えました。",
|
||||
"insights.insight.trendDown": "今週の閲覧は先週より %s%% 減りました。",
|
||||
"insights.insight.trendFlat": "今週の閲覧は先週とほぼ同じです。",
|
||||
|
||||
"stats.foot.updated": "集計時刻:",
|
||||
"stats.foot.prevMonth": "最後の質問:",
|
||||
"stats.foot.prevAnswer": "最後の回答:",
|
||||
|
||||
@@ -1054,6 +1054,50 @@ var zhCNDict = map[string]string{
|
||||
"stats.weekday.6": "周六",
|
||||
"stats.weekday.7": "周日",
|
||||
|
||||
// 箱主数据面板(/user/insights)
|
||||
"insights.title": "数据面板",
|
||||
"insights.heading": "你的数据面板",
|
||||
"insights.lead": "这些数字只属于你自己:有多少人打开过你的提问箱、你收到了多少提问、还有多少没处理。全部是聚合结果,不含任何一位访客或提问者的身份。",
|
||||
"insights.back": "返回设置",
|
||||
"insights.entryTitle": "数据面板",
|
||||
"insights.entryDesc": "看看有多少人来过你的提问箱、提问处理得怎么样",
|
||||
"insights.summary": "一眼看懂",
|
||||
"insights.noViews": "还没有访客记录。有人打开过你的提问箱之后,这里就会开始有数字。",
|
||||
"insights.today": "今天",
|
||||
"insights.week": "近 7 天",
|
||||
"insights.month": "近 30 天",
|
||||
"insights.newQuestions": "新提问",
|
||||
"insights.pv": "次浏览",
|
||||
"insights.uv": "位访客",
|
||||
"insights.trendTitle": "近 30 天浏览趋势",
|
||||
"insights.trendNote": "每根柱子是一天,浅色是浏览量,深色是当天去重后的访客数。把鼠标停在柱子上能看到当天的浏览 / 访客 / 提问。",
|
||||
"insights.legendPV": "浏览量(PV)",
|
||||
"insights.legendUV": "独立访客(按日去重)",
|
||||
"insights.legendHover": "悬停查看当天明细",
|
||||
"insights.hourTitle": "大家什么时候来",
|
||||
"insights.hourNote": "近 30 天收到的提问分布在一天里的哪个时段,最亮的一根是你最忙的时段。",
|
||||
"insights.funnelTitle": "提问处理情况",
|
||||
"insights.funnelNote": "全部提问按「已回答 / 已读未答 / 未读未答」三类拆开。",
|
||||
"insights.funnelTotal": "全部提问",
|
||||
"insights.funnelAnswered": "已回答",
|
||||
"insights.funnelReadOnly": "已读未答",
|
||||
"insights.funnelUnread": "未读未答",
|
||||
"insights.answerRate": "回答率:已回答 ÷ 全部提问",
|
||||
"insights.privacyTitle": "关于这些数字",
|
||||
"insights.privacyNote": "访客统计只记录「某天有人来过几次」。访客身份是每天更换盐值的 IP 哈希,不保存 IP、不保存 UA、也不保存访问了哪个页面。因此跨天的同一个人无法被关联,近 7 天 / 30 天的访客数是各天去重后相加 —— 同一个人连续几天来访会被记成几个人。箱主自己打开自己的提问箱不计入统计。",
|
||||
|
||||
"insights.insight.empty": "还没有任何数据。等你收到第一条提问,或者有人打开过你的提问箱,这里就会有变化。",
|
||||
"insights.insight.weekViews": "近 7 天有 %s 次浏览,%s 位访客。",
|
||||
"insights.insight.conversion": "近 30 天的访客中,约有 %s%% 留下了提问。",
|
||||
"insights.insight.bestDay": "最热闹的一天是 %s,有 %s 次浏览。",
|
||||
"insights.insight.peakHour": "提问最集中在 %d 点前后。",
|
||||
"insights.insight.answerRate": "你已经回答了 %s%% 的提问。",
|
||||
"insights.insight.fastReply": "已回答的提问里,%s%% 是在 24 小时内回复的。",
|
||||
"insights.insight.unread": "还有 %s 条提问没处理。",
|
||||
"insights.insight.trendUp": "本周浏览比上周多了 %s%%。",
|
||||
"insights.insight.trendDown": "本周浏览比上周少了 %s%%。",
|
||||
"insights.insight.trendFlat": "本周浏览与上周差不多。",
|
||||
|
||||
"stats.foot.updated": "快照时间:",
|
||||
"stats.foot.prevMonth": "最后一条提问:",
|
||||
"stats.foot.prevAnswer": "最后一条回答:",
|
||||
|
||||
@@ -1053,6 +1053,50 @@ var zhTWDict = map[string]string{
|
||||
"stats.weekday.6": "週六",
|
||||
"stats.weekday.7": "週日",
|
||||
|
||||
// 箱主數據面板(/user/insights)
|
||||
"insights.title": "數據面板",
|
||||
"insights.heading": "你的數據面板",
|
||||
"insights.lead": "這些數字只屬於你自己:有多少人開啟過你的提問箱、你收到了多少提問、還有多少沒處理。全部是彙總結果,不含任何一位訪客或提問者的身分。",
|
||||
"insights.back": "返回設定",
|
||||
"insights.entryTitle": "數據面板",
|
||||
"insights.entryDesc": "看看有多少人來過你的提問箱、提問處理得怎麼樣",
|
||||
"insights.summary": "一眼看懂",
|
||||
"insights.noViews": "還沒有訪客紀錄。有人開啟過你的提問箱之後,這裡就會開始有數字。",
|
||||
"insights.today": "今天",
|
||||
"insights.week": "近 7 天",
|
||||
"insights.month": "近 30 天",
|
||||
"insights.newQuestions": "新提問",
|
||||
"insights.pv": "次瀏覽",
|
||||
"insights.uv": "位訪客",
|
||||
"insights.trendTitle": "近 30 天瀏覽趨勢",
|
||||
"insights.trendNote": "每根柱子是一天,淺色是瀏覽量,深色是當天去重後的訪客數。把游標停在柱子上能看到當天的瀏覽/訪客/提問。",
|
||||
"insights.legendPV": "瀏覽量(PV)",
|
||||
"insights.legendUV": "獨立訪客(按日去重)",
|
||||
"insights.legendHover": "停留查看當天明細",
|
||||
"insights.hourTitle": "大家什麼時候來",
|
||||
"insights.hourNote": "近 30 天收到的提問分布在一天裡的哪個時段,最亮的一根是你最忙的時段。",
|
||||
"insights.funnelTitle": "提問處理情況",
|
||||
"insights.funnelNote": "全部提問按「已回答/已讀未答/未讀未答」三類拆開。",
|
||||
"insights.funnelTotal": "全部提問",
|
||||
"insights.funnelAnswered": "已回答",
|
||||
"insights.funnelReadOnly": "已讀未答",
|
||||
"insights.funnelUnread": "未讀未答",
|
||||
"insights.answerRate": "回答率:已回答 ÷ 全部提問",
|
||||
"insights.privacyTitle": "關於這些數字",
|
||||
"insights.privacyNote": "訪客統計只記錄「某天有人來過幾次」。訪客身分是每天更換鹽值的 IP 雜湊,不保存 IP、不保存 UA、也不保存造訪了哪個頁面。因此跨天的同一個人無法被關聯,近 7 天/30 天的訪客數是各天去重後相加 —— 同一個人連續幾天造訪會被記成幾個人。箱主自己開啟自己的提問箱不計入統計。",
|
||||
|
||||
"insights.insight.empty": "還沒有任何資料。等你收到第一條提問,或有人開啟過你的提問箱,這裡就會有變化。",
|
||||
"insights.insight.weekViews": "近 7 天有 %s 次瀏覽,%s 位訪客。",
|
||||
"insights.insight.conversion": "近 30 天的訪客中,約有 %s%% 留下了提問。",
|
||||
"insights.insight.bestDay": "最熱鬧的一天是 %s,有 %s 次瀏覽。",
|
||||
"insights.insight.peakHour": "提問最集中在 %d 點前後。",
|
||||
"insights.insight.answerRate": "你已經回答了 %s%% 的提問。",
|
||||
"insights.insight.fastReply": "已回答的提問裡,%s%% 是在 24 小時內回覆的。",
|
||||
"insights.insight.unread": "還有 %s 條提問沒處理。",
|
||||
"insights.insight.trendUp": "本週瀏覽比上週多了 %s%%。",
|
||||
"insights.insight.trendDown": "本週瀏覽比上週少了 %s%%。",
|
||||
"insights.insight.trendFlat": "本週瀏覽與上週差不多。",
|
||||
|
||||
"stats.foot.updated": "快照時間:",
|
||||
"stats.foot.prevMonth": "最後一則提問:",
|
||||
"stats.foot.prevAnswer": "最後一則回答:",
|
||||
|
||||
Reference in New Issue
Block a user