HEXO 开发笔记(10)自建访问统计:用 doratiger-counter 替代卜算子
创建于 2026-09-04
更新于 2026-09-19
科技
hexo
主题开发
Go
SQLite
访问统计
3724 字 · 约 13 分钟

前言

在 HEXO 开发笔记(6)自建主题:核心功能实现 中,已经记录过主题统计功能从 localStorage 迁移到卜算子,再迁移到自建 counter 的过程。当时的实现能够满足日常使用,但计数数据主要停留在内存中,服务重启后 UV 会重新开始,配置和数据库迁移也还比较粗糙。

这次整理 doratiger-counter,主要补上了 UV 持久化、数据库迁移和退出前同步,也调整了来源校验,避免字符串匹配放过不该接受的域名。下面记录这些改动,以及 DoraTiger 主题如何调用统计接口。

  • 20260914:doratiger-counter 已支持按请求来源映射多个逻辑站点,两个博客可以共用一个服务实例而不混合相同路径的统计数据;升级时会将旧数据迁入原有站点键。主题页脚同时修正为显示接口中的 site_pv 和 page_pv,四个返回字段的含义保持不变。

一、为什么不继续使用第三方统计

最早的统计方案是浏览器端 localStorage。它不需要后端,部署成本最低,但统计数据只存在当前浏览器中,换设备或清理浏览器数据后就无法连续累计。后来接入卜算子,站点和页面的统计可以集中保存,但计数脚本依赖外部服务,服务可用性、网络环境和返回格式都不是主题能够控制的。

我的博客只需要站点和文章的 PV/UV,用一个 Go 程序加一个 SQLite 数据库文件就可以实现。主题通过 HTTP API 读取计数,统计口径和升级方式由自己维护,出了问题也方便检查代码和数据库。

二、整体结构

doratiger-counter 目前由三层组成:

text
1
2
3
4
5
浏览器中的 DoraTiger 主题 → GET /count?page=<path>&uid=<visitor-id> → Go HTTP 服务(根据来源解析逻辑站点) → 内存计数器与访客集合 → SQLite(定时同步,退出时最后同步)

主题负责生成访问者标识和当前页面路径,服务端负责校验来源、确定逻辑站点、递增计数并返回结果,数据库只负责保存可恢复的数据。这样划分以后,主题不需要知道数据库结构,服务端也不需要参与 Hexo 构建过程。

当前版本有两个接口:

http
1
2
GET /count?page=/posts/example/&uid=visitor-id Origin: https://blog.example.com
json
1
2
3
4
5
6
{ "site_pv": 42, "page_pv": 3, "site_uv": 12, "page_uv": 2 }

page 是必填的页面键,uid 可选。没有 uid 时仍然会增加 PV,但不会增加 UV。另一个接口是 GET /health,只返回 {"status":"ok"},不要求请求带有站点来源,方便反向代理或监控系统做健康检查。

三、PV 与 UV 如何统计

3.1 PV 使用内存计数器

请求到达后,站点 PV 和当前页面 PV 都在内存中递增。服务每 30 秒把站点计数、页面计数和访客集合放进同一个事务写入 SQLite。收到 SIGTERM 或 Ctrl-C 时,HTTP 服务先停止接收新请求,再触发一次最后同步。

这种做法避免了每次页面访问都执行数据库写操作,适合个人站点的低到中等访问量。但它也意味着:如果进程被强制杀死,最近一次同步之后的计数可能丢失,最大窗口约为 30 秒。因此这套服务适合展示型统计,不适合财务、计费或审计场景。

3.2 UV 使用持久化摘要去重

主题第一次访问时在浏览器中生成一个 UUID,并通过 Cookie 保存一年。之后每次请求都把这个 UUID 作为 uid 发送给服务端。服务端不会保存原始 UUID,而是计算 SHA-256 摘要,再将摘要分别放进站点访客集合和页面访客集合中。

这样做有两个直接效果:同一个访客再次打开页面时,页面 PV 会增加,但页面 UV 不会重复增加;服务重启后,访客集合可以从数据库恢复,站点 UV 不会从零开始。摘要仍然是可以关联的假名标识,所以它不是“完全匿名数据”,部署时仍然应该在隐私说明中告知访客用途。

四、来源限制与 CORS

统计接口不是登录接口,但至少可以减少普通网页和脚本的误调用。单站点时可以配置 allowed_origins;现在多个站点共用服务时,更适合用 sites 将来源主机名映射为逻辑站点键:

toml
1
2
3
4
5
6
7
[counter] site_key = 'dtc_site' enable_cors = true [counter.sites] 'blog-one.example.com' = 'dtc_site' 'blog-two.example.com' = 'site_two'

启用 sites 后,未列出的来源会被拒绝,allowed_origins 不再参与匹配;未配置 sites 时则保留原来的单站点兼容逻辑。相同的页面路径和访客摘要会按站点键隔离,因此两个博客的首页都叫 / 也不会混在一起。

这里假设 blog-one.example.com 是原来的博客,因此继续映射到原有的 dtc_site。升级时应保留实际使用的 site_key,并让原博客的来源映射使用同一个键;否则请求会进入新的站点,无法显示迁移后的旧计数。

这里有两个容易混淆的边界。第一,example.com 和 example.com.evil.test 不能按字符串包含关系判断,否则恶意后缀也可能通过。第二,Origin/Referer 可以被直接构造 HTTP 客户端伪造,所以它只能作为来源限制,不能当作身份认证,更不能用于授权、计费或其他安全决策。

开启 CORS 后,服务只会为通过白名单校验的来源返回 Access-Control-Allow-Origin,并带上 Vary: Origin。如果统计服务和博客由同一个反向代理提供,通常不需要打开宽泛的跨域策略。

五、数据库迁移与恢复

数据库使用单调递增的 schema version。初始版本包含四类数据:页面 PV、站点 PV、站点访客摘要和页面访客摘要。启动时先检查数据库版本,再执行只增加或转换数据的迁移;如果发现数据库版本高于当前程序,服务会拒绝启动,避免旧程序误操作新数据。

对于早期只有 page_stats 和 site_stats 两张表的数据库,迁移会保留原有 PV,再补齐访客表。之后的 schema v2 又为页面统计和页面访客摘要增加了站点键:旧页面数据会归入配置中的 site_key,已有站点统计保留原键,新增站点从独立的数据空间开始计数。服务启动时将已有数据加载到内存,后续请求继续从原来的数值上递增。升级前先正常停止服务,确认进程退出,再备份 counter.db 及仍存在的 counter.db-wal、counter.db-shm 文件;不要在服务持续写入时逐个复制这些文件作为一致性备份。

六、主题中的接入方式

主题配置只需要指定统计类型和 API 地址:

yaml
1
2
3
4
5
6
statistics: enable: true type: counter counter: api: https://counter.example.com/count uv: true

footer.pug 在文章页和普通页面中渲染统计占位符,浏览器脚本读取或创建 dtc_uid,再把 location.pathname 和访客标识拼到 API 请求中。请求成功时,页脚显示服务端返回的 site_pv 和 page_pv;site_uv、page_uv 保留在稳定 API 中供需要单独展示访客数的页面使用。请求失败或没有配置 API 时,则退回到 localStorage 的本地方案。

这个 fallback 很重要:统计服务短暂不可用时,主题不会因为一个附加功能失败而影响文章阅读。但两种方案的统计口径不同,localStorage 只知道当前浏览器访问过哪些路径,不能与服务端的站点总量直接比较,所以它更适合作为临时占位,而不是长期数据源。

七、部署边界与当前限制

服务可以直接运行,也可以通过 Docker 部署。生产环境建议让它监听内网地址,由 Caddy、Nginx 等反向代理提供 HTTPS,并限制后端端口的可访问范围。数据库目录需要持久化挂载,容器更新时不能把 /app/data 一并丢弃。

当前版本刻意保持简单,也保留了几个明确限制:

  • 只支持 SQLite,面向单实例和个人站点;
  • 可以按来源隔离多个逻辑站点,但不提供多实例协调;
  • 计数先写内存,再定时同步,异常退出存在短暂数据窗口;
  • 访客摘要和页面访客集合会随访客量增长,暂不适合大规模多租户部署;
  • 不提供后台管理页面、历史报表和数据导出接口;
  • 来源白名单不是认证机制,不能防止有意伪造请求。

目前我仍按单实例维护。外部数据库、后台报表和多实例协调暂时没有实际需求,后面根据访问量再考虑。

八、总结

整理之后,统计服务可以从数据库恢复访客集合,升级时处理旧表结构,正常退出前也会同步内存计数。日常使用仍需备份数据库,并注意异常退出可能丢失尚未同步的数据。对我目前的博客来说,这些已经够用了。

参考

手机扫码阅读
本文作者: 有次元袋的 tiger
本文链接: https://www.superheaoz.top/2026/09/46128/
版权声明: 本站点所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 我的个人天地!