一切的起点,其实就是买的opencode go 快要过期了,才用了 50%,出于不浪费的传统美德,一开始想做一个属于自己的云文档部署到服务器上,这样一来,买来备案的服务器也用上了。

但写到一半发现这不是一个好点子,中途的目标就换成了做一个完全属于自己自定义的简洁的评论系统。

我的需求是:高性能优先,轻量化优先、维护简单优先。尽可能的少依赖,尽量做到不靠外部依赖。没有Redis、消息队列、Vue/React、MySQL、Node.js 运行时。

于是,经过和豆包以及DeepSeek一番探讨,我决定用go+原生HTML去开发一个 极致轻量的自托管评论系统:单二进制 + 单文件数据库 + 纯静态前端。

这里必须声明,这是一个纯vibe coding项目,系统所有代码均由:DeepSeekv4Flash正式版+kimi2.7视觉辅助编写。

比较重要的取舍如下:

既然是要做一个自己用的,那肯定要有自己的取舍,而不是用不同的语言去重写一个用起来实质是一模一样的东西。

设计上,只做自己用的,不做现在不用/未来可能会用的。

  1. 和waline一样,评论者和管理员走完全不同的通道——评论者零认证,管理员才需要账号体系。
  2. 做基本的人工审核、标记垃圾、置顶、回复、删除、条件筛选;不做举报、踩。
  3. 支持多个站点分开浏览审核。
  4. 全量审核,管理员人工过审才对外可见,不做选择逻辑。
  5. 发后不改,评论提交即定稿,不提供编辑和删除。
  6. 不提供管理员修改评论/名字接口,但提供去除评论者网站链接功能。
  7. 不做过多第三方登录绑定,只预留QQ、GitHub,但支持邮箱验证码 2FA。
  8. 不做md适配,因为大概率没有人会在评论区写md。
  9. 不支持直接上传图片,但能直接贴图片链接 ,自动渲染成图。没有上传按钮,也不存图。
  10. 不做自动审核、AI审核,人工审核的劳动成本,朴素但可靠。只做简单的敏感词命中标记为垃圾评论,管理员扫一眼就能处理。
  11. 个性化上支持自定义站长文字徽章,支持站长昵称自定义。
  12. 只能部署在服务器上,暂时不支持其他挂载方式。

简而言之,根据我自己的实际需求而定夺,小站用简单的办法就行。

值得关注的一些信息

架构总览

前端评论组件(原生 JS,嵌入博客页面)
        │ HTTPS
Nginx(静态资源 / SSL / 第一层限流)
        │ 127.0.0.1:8080
Go 业务服务(评论增删改查 / 审核 / 限流 / 反垃圾 / 异步通知 / 迁移)
        │ 本地文件
SQLite 单文件数据库

信息安全

flowchart LR
    DB[("comments 表<br/>email / ip / user_agent")]
    subgraph 对外出口
        Pub["公开接口<br/>GET /api/v1/comments"]
        Adm["管理接口<br/>GET /api/v1/admin/comments"]
        Avt["头像代理<br/>GET /api/v1/avatars/{id}"]
    end
    subgraph 前端
        FW["评论组件 comment.js"]
        FA["头像 img 标签"]
    end
    DB -->|"ToPublic() 丢弃隐私字段"| Pub
    Pub -->|"仅返回 id/nick/content/link/avatarUrls<br/>不含 email / ip / UA"| FW
    DB -->|"ToDTO(true) 附带隐私字段"| Adm
    Adm --> AdmQ{"查看者身份?"}
    AdmQ -- "站长" --> AdmFull["明文 email / ip / UA"]
    AdmQ -- "非站长管理员" --> AdmMask["邮箱脱敏 + IP 脱敏"]
    DB -->|"评论ID反查邮箱<br/>邮箱 md5 作缓存键"| Avt
    Avt -->|"服务端代拉 QQ qlogo / Cravatar<br/>前端永不接触邮箱字段"| FA

设计上,公开响应的头像地址不携带任何可反推邮箱的信息。前台拿不到任何联系方式,站长是唯一能看全量隐私信息的角色(管理接口才附带邮箱 / IP / UA,公开接口对这些字段一律零输出)。

邮箱头像全代理化 :无论 QQ 邮箱还是其他邮箱,前端拿到的 avatarUrls 都只有一个本服务地址 /api/v1/avatars/{id} ——不暴露邮箱、不暴露邮箱的 md5 哈希,杜绝 md5 字典爆破和跨站关联的可能。

字母头像兜底 :没有邮箱、或真实头像加载失败/超时的用户,前端按昵称哈希生成稳定的字母头像——字母(A–Z)和颜色都由哈希决定,同一昵称结果恒定,昵称变化头像也随之变化。

开发方法:文档驱动

项目采用先文档、后代码的流程,任何功能都先评审。

一开始只有一个想法,经过自己和AI对话的不断梳理,最后产出一份:需求规格说明书。接着再不断的细化去拆分,最后形成 7 份规格文档,然后再进入开发。

整个开发周期中,文档几乎占了50%的时间。 可能得益于文档需求写的足够清楚,deepseek 在后端编码的时候几乎没有出过错错误,一遍过。当然,本身评论系统这种业务也很简单。

文档回答的问题
01-需求规格说明书「做什么」:背景、定位、功能清单、产品决策、验收要点
02-系统架构设计「怎么设计」:选型、分层、流程、模块、安全、ADR
03-数据库设计文档表结构、索引、SQLite 优化、迁移策略、数据访问模式
04-API 接口文档统一响应、错误码、全部公开 / 管理接口契约
05-前端实现方案组件架构、渲染规则、XSS 防护、嵌入方式、性能约束
06-部署运维手册编译、systemd、Nginx、备份恢复、升级、故障排查
07-性能测试报告压测方法、实测数据、内存表现、线上验证

开发过程:怎么降低AI写bug的概率

AI写代码,它可能自己根本意识不到自己的编码有问题,即使你让它回头检查。我习惯通过引入不一样的模型来交叉审核寻找问题。

一个笨拙而直白的操作方法:一个 主agent 写,写完后多开窗口用其他几个模型同步进行代码审查。例如用DeepSeek写,分别用:DeepSeek、kimi、glm一起审写出来的代码。

找出问题,就把其他模型找到的bug给负责编写代码的AI,让它自己再次审一遍,看问题是否属实,是就修复,修复完后就再次进入审核,重复操作,直到没有问题为止。一般而言,循环三轮足以。

这个方法在当前这个时间点以及目前使用效果看来,除了费点钱,暂时没发现什么弊端。

本地测试

虽然是本地测试,但为了尽可能减少失真,所有数据都是 hey(压测)+ Go 官方 pprof(内存采样)实测的结果。

容量测试/可靠性

image-20260814225324670

读 ≈ 44,481 req/s (5 实例合计) 每秒能处理约 4.4 万个"看评论列表"的请求 。

写 ≈ 1,008 条/s (不限速) 每秒能存约 1000 条新评论。

延迟

image-20260814230105037

读 p99 = 2.2ms (独立采样 60 万次请求),发 100 次请求,99 次在 2.2 毫秒内返回 ,平均 2.2~2.3ms(5 实例高并发下)。

限流测试

image-20260814230133282

image-20260814230202389

内存占用

采样对象WorkingSet 峰值HeapInuse 峰值goroutine 峰值
严格实例(Phase A 写压测)26.4 MB4.6 MB43
bench 实例(Phase B/C/D 全程)29.2 MB7.7 MB147(读压测期)

image-20260814230225260

总结

组件预估占用实测表现
Go 后端(单实例)25~45 MB压测峰值 WorkingSet 29.2MB、HeapInuse 7.7MB(pprof 实测)
SQLite(WAL + 内存映射)5~10 MB压测库 bench.db 含 WAL 合计 ≈ 23MB(磁盘,非驻留内存)
goroutine峰值 147,压测后回落 31,无泄漏
整机合计≤ 100MB单实例实测 ≤ 29.2MB,1 核 512MB服务器占用约 5%

完整报告:deepwhitex.com/dwxcmt

实际使用

经过本地测试和服务器测试都表现良好,本站留言系统已经从waline切换成该系统,目前个人使用下来感觉良好,假若有问题,只能待线上长期观测进一步确认长期稳定性。

如果你感兴趣折腾,也不想从零开始写业务代码,可以随时到GitHub上下载使用或使用AI进行二次开发使用:

https://github.com/FXnadu/dwxcmt

或你发现bug或有任何意见,欢迎留言。

dwxComment,一个单二进制 + 单文件数据库 + 纯静态前端的自托管评论系统。