前言
在 HEXO 开发笔记(6)自建主题:核心功能实现 中,已经记录过主题统计功能从 localStorage 迁移到卜算子,再迁移到自建 counter 的过程。当时的实现能够满足日常使用,但计数数据主要停留在内存中,服务重启后 UV 会重新开始,配置和数据库迁移也还比较粗糙。
这次重新整理 doratiger-counter,目标不再是“能返回一个数字”这么简单,而是把它变成一个可以长期运行的小服务:接口契约清楚,统计数据能够恢复,来源限制不会因为简单的字符串匹配而失效,服务收到退出信号时也能把内存中的计数写回数据库。本文记录这次整理后的设计和它与 DoraTiger 主题的接入方式。
一、为什么不继续使用第三方统计
最早的统计方案是浏览器端 localStorage。它不需要后端,部署成本最低,但统计数据只存在当前浏览器中,换设备或清理浏览器数据后就无法连续累计。后来接入卜算子,站点和页面的统计可以集中保存,但计数脚本依赖外部服务,服务可用性、网络环境和返回格式都不是主题能够控制的。
自建服务的价值并不是把统计系统做得多复杂,而是把边界收回到自己手里:主题只需要调用一个简单的 HTTP API,数据存放在自己的 SQLite 文件中,统计口径和升级策略都由项目本身决定。对于个人博客这种访问量级,单个 Go 二进制加一个数据库文件已经足够,不需要为了“可扩展”先引入一整套服务集群。
二、整体结构
doratiger-counter 目前由三层组成:
text12345浏览器中的 DoraTiger 主题 → GET /count?page=<path>&uid=<visitor-id> → Go HTTP 服务 → 内存计数器与访客集合 → SQLite(定时同步,退出时最后同步)
主题负责生成访问者标识和当前页面路径,服务端负责校验来源、递增计数并返回结果,数据库只负责保存可恢复的数据。这样划分以后,主题不需要知道数据库结构,服务端也不需要参与 Hexo 构建过程。
当前版本有两个接口:
http12GET /count?page=/posts/example/&uid=visitor-id Origin: https://blog.example.com
json123456{ "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 后,服务会解析 Origin 或缺失时使用的 Referer,只按规范化后的主机名精确匹配允许列表:
toml1234[counter] site_key = 'dtc_site' allowed_origins = ['blog.example.com'] enable_cors = true
这里有两个容易混淆的边界。第一,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,再补齐访客表。服务启动时将已有数据加载到内存,后续请求继续从原来的数值上递增。升级服务前仍建议同时备份 counter.db、counter.db-wal 和 counter.db-shm 文件。
六、主题中的接入方式
主题配置只需要指定统计类型和 API 地址:
yaml123456statistics: enable: true type: counter counter: api: https://counter.example.com/count uv: true
footer.pug 在文章页和普通页面中渲染统计占位符,浏览器脚本读取或创建 dtc_uid,再把 location.pathname 和访客标识拼到 API 请求中。请求成功时显示服务端返回的 site_uv、page_uv;请求失败或没有配置 API 时,则退回到 localStorage 的本地方案。
这个 fallback 很重要:统计服务短暂不可用时,主题不会因为一个附加功能失败而影响文章阅读。但两种方案的统计口径不同,localStorage 只知道当前浏览器访问过哪些路径,不能与服务端的站点总量直接比较,所以它更适合作为临时占位,而不是长期数据源。
七、部署边界与当前限制
服务可以直接运行,也可以通过 Docker 部署。生产环境建议让它监听内网地址,由 Caddy、Nginx 等反向代理提供 HTTPS,并限制后端端口的可访问范围。数据库目录需要持久化挂载,容器更新时不能把 /app/data 一并丢弃。
当前版本刻意保持简单,也保留了几个明确限制:
- 只支持
SQLite,面向单实例和个人站点; - 计数先写内存,再定时同步,异常退出存在短暂数据窗口;
- 访客摘要和页面访客集合会随访客量增长,暂不适合大规模多租户部署;
- 不提供后台管理页面、历史报表和数据导出接口;
- 来源白名单不是认证机制,不能防止有意伪造请求。
这些限制并不是遗漏的功能清单,而是当前项目的使用边界。先把单实例场景的恢复、迁移和接口行为做稳定,再根据真实访问量决定是否需要外部数据库、后台报表或多实例协调,成本会更可控。
八、总结
从卜算子迁移到自建 doratiger-counter,真正改变的不是页脚里显示的几个数字,而是统计功能的控制权:主题拥有稳定的调用契约,服务端拥有明确的数据模型,数据库拥有可验证的迁移和恢复路径。
对于一个个人 Hexo 博客,这套实现已经覆盖了我真正需要的部分:不依赖第三方统计脚本、能够累计 PV/UV、服务重启后数据可恢复,并且在来源校验、隐私说明和异常退出方面保留清晰边界。后续如果访问规模没有明显增长,就没有必要为了架构上的“完整”提前把它改造成更重的系统。

