在测试环境(box-dev-api)上逐项验算后发现「回答耗时」这一口径是错的。 1. 回答耗时不能用 updated_at - created_at(P0) GORM 的 Update 会自动刷新 updated_at,而已读、置顶、公开/私密切换全走 Update。 实测:一条 10 天前的提问被 SetRead 标记已读后,updated_at 被推到当下(delta=240h)。 于是「回答耗时」实际测的是「提问到最后一次被碰」:线上那 6 条「超过 7 天」 和偏低的 24 小时回复占比都是这么来的。 修法:新增 questions.answered_at(首次回答时间,可空),只在 answer 由空变非空时 写一次,改写回答不重置;聚合改用 COALESCE(answered_at, updated_at), 首次加列时用 updated_at 冻结历史一次(Migrator().HasColumn 判断是否刚加列), 否则历史值会随着后续的已读/置顶继续变大。 2. 环比句式读不通 中文「上月提问量比前一个月 -69.6%」、英文「...: -69.6% questions.」都是病句。 改为「上月提问量环比 %s%%」/「Month-over-month change for last month: %s%%」。 3. 脚注说「当日 0 点的快照」不实 实际是当天第一次访问时算、服务重启也会重算(实测算出时刻 01:11:50, 基准日却写 2026-10-24,说明它含了 0 点之后的数据)。改成准确描述。 4. 数据库故障时请求会排成 N×30s ovRefresh 串行化 + 30 秒超时意味着数据库挂起时第 N 个请求要等 N×30 秒。 加 30 秒失败冷却:冷却期内直接降级(有旧快照用旧快照),不再碰数据库。 todayNumbers 失败也盖时间戳,不再每个请求都打两条 COUNT。 测试:新增 internal/db/answered_at_test.go(5 例:只记一次、私密路径也记、 分桶以 answered_at 为准、历史行回退、回填),并做过变异验证 —— 把口径改回 updated_at 后测试立即失败。internal/stats 补 3 例冷却测试。全量构建与测试通过 (internal/storage 的构建失败是 overlay 桩的既有问题)。
5.5 KiB
5.5 KiB
2026-10-24
/stats 入口收敛到页脚
- 用户要求:首页那张统计入口卡取消,入口只放页脚「社区规则」与「找站长」之间。
- 改动:
templates/home.html:删掉统计入口卡整块 +.tb-stats-entry*样式(原</details>后那一段)。route/home.go:Home不再调stats.Get(模板已不读.SiteStats),去掉logrusimport;/api/v1/site-stats与SiteStatsAPI保留未动。- 四份 i18n 字典删掉已无引用的
site.stats.open(site.stats老词条保留)。 - 页脚
templates/base/footer.html的site.footer.stats位置本就在 rules 与 admin 之间,无需再动。
- 验证:
go build+./internal/i18n/./templates/./route/...测试全绿;gofmt 无差异; 重新生成预览确认首页无tb-stats-entry,页脚渲染为「社区规则 | 数据 | 找站长」。 - 教训:删掉页面唯一消费方后,服务端的取数调用(
stats.Get)要同步删, 否则每次访问首页白算一次快照;对应 i18n 词条也要一起清,避免留死条目。
/stats 文案与脚注排版修正
- 页脚入口文案「数据」→「统计数据」(
site.footer.stats,四语言Statistics/統計データ/統計數據)。 - 「提问最集中在三 19 点前后」的 bug:洞察句子复用了热力图列头的
stats.wd.*("一"…"日" / "Mon"…),那是为窄列头设计的单字标签,进句子就成了"三"、英文是 "Wed"。 新增一组完整名stats.weekday.1..7(周一/週一/Monday/月曜日),洞察改用这组;stats.wd.*只留给热力图列头。已在overview_test.go加断言锁住,防止回退。 → 教训:短标签和完整名必须分两套词条,别为了少写一组 key 复用。 - 页面脚注四条时间(基准日 / 计算于 / 最后一条提问 / 最后一条回答)由
「A:x · B:y」挤成一行改为
grid-template-columns: max-content 1fr各占一行, 标签列与值列对齐;下面两段说明仍为独立段落。四个标签统一带冒号。
部署前自查:两个 P0 都已修
用户部署到测试环境后要求自查。查出的问题都是我自己上一轮引入的:
- 「今日」两项被每日缓存冻住(P0,与页面「实时」说明矛盾)。
→
TodayCounts拆出,60 秒 TTL + 跨日按日期失效;命中缓存时返回副本。 新增TestTodayNumbersNotFrozen/TestTodayCacheExpiresAcrossMidnight。 - 日期分组跟着 PG 会话时区(默认 UTC)(P0,隐蔽:数字都"合理",只是整体偏 8 小时)。
psql 实测:
2026-10-24 01:30+08在 UTC 会话下 to_char 得2026-10-23、hour=17。 →analyticsLocalize()用AT TIME ZONE INTERVAL '+08:00'(偏移取time.At()的 Zone, 不用 IANA 名)。EXPLAIN 确认过滤仍走 created_at 索引。 - PeakDay 用
>=会让「最忙的一天」随更新漂移到最近的同值日期 → 改>。 ovRefresh里的聚合改用context.WithoutCancel+ 30s 超时:第一个访客中途关页面时, 排队的请求不会各自重跑全表 GROUP BY。
性能实测(20 万行):totals 337ms / 字数分布 313ms / 热力图 100ms / 日趋势 47ms, 本地合计约 0.8s,1C1G 估 2s,靠预热兜住。百万行才需要合并扫描。
顺带发现(未改,与本次无关):Dockerfile 基础镜像是 golang:1.20-alpine,
而 go.mod 要求 go 1.27.3 —— 1.20 不支持 GOTOOLCHAIN 自动下载,Docker 构建必然失败;
-X 里的包路径还是 NekoWheel/NekoBox。说明实际部署不走这个 Dockerfile。
测试环境实测(box-dev-api.337845818.best,用户时区 UTC+8)
/stats200,首访 5.2s(含 TLS),稳定后 TTFB 1.6s,同站/rules1.34s → 本页只多约 250ms,1C1G 上可接受,无需合并聚合 SQL。- 环境默认语言是 ja(
/、/rules同样是 ja),与本次改动无关;语言存 session (/i18n/{lang}),故 /stats 绝不能加 CDN 缓存头 —— 内容随 session 变化且无 Vary: Cookie。 - 逐项验算自洽:月柱合计 95 = 累计提问;热力图 7×24=168 格合计 95; 最忙时段周三 19 点 = 22 与小时合计 26 峰值一致;折线 30 点峰值 10-19 共 5 条。
/stats/尾斜杠 302 跳首页 —— 与/rules/、/sponsor/一致,框架行为。
P0:回答耗时口径是错的(本次修)
SetRead 把 updated_at 往前推(实测 10 天前的提问,delta = 240h)。
所以「回答耗时 = updated_at − created_at」实际测的是「提问到最后一次被碰」,
线上「6 条超过 7 天」就是这么来的,24 小时占比 66.7% 同样偏低。
修法:questions.answered_at(首次回答时间),只在 answer 由空变非空时写,
改写回答不重置;聚合 COALESCE(answered_at, updated_at);加列时回填一次冻结历史。
测试:internal/db/answered_at_test.go 5 个用例,含变异验证(改回 updated_at 即红)。
顺带修
- 环比句式:中文「比前一个月 -69.6%」、英文「...: -69.6% questions.」读不通 → 「上月提问量环比 %s%%」/「Month-over-month change for last month: %s%%」。
- 脚注「当日 0 点的快照」不实(实际是当天首访/重启时算)→ 改成准确描述。
- DB 故障时
ovRefresh串行 + 30s 超时会让请求排成 N×30s → 加 30s 失败冷却 (ovFailAt),todayNumbers失败也盖时间戳,不再每请求打两条 COUNT。