回滚必须基于完整快照并原子还原整个页面状态,涵盖内容、附件、权限等所有关联数据,且需严格校验归属、时序与租户隔离。

回滚操作必须基于完整快照,不能只改数据库字段
很多人以为把 content 字段更新成旧值就完成了回滚,结果发现图片链接失效、元数据错乱、搜索索引没同步。Golang Wiki 系统里,一次编辑可能同时影响 pages 表、revisions 表、page_attachments 关联表,甚至外部对象存储里的文件引用。回滚不是“还原内容”,而是“还原整个页面状态”。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每次保存新版本时,用
json.Marshal将页面结构(含标题、正文、标签、附件 ID 列表、权限配置)存入revisions表的snapshot字段,而非只存content - 回滚接口(如
POST /api/pages/{id}/revert)应调用一个原子函数RestoreRevision(pageID, revisionID),该函数内部一次性更新所有关联字段 - 避免在 HTTP handler 里手动拼 SQL 更新多张表——容易漏掉
updated_at或触发钩子逻辑
revisionID 必须全局唯一且可排序,别用时间戳当主键
用 time.Now().Unix() 生成 revisionID 看似简单,但高并发下会冲突;用自增 ID 虽然唯一,但无法反映真实编辑时序(比如后台批量修复导致 ID 跳变)。回滚依赖严格的时间线,一旦顺序错,用户点“回退到上一版”可能跳到三天前的草稿。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
int64类型的revision_id,由数据库序列(PostgreSQLserial)或分布式 ID 生成器(如github.com/sony/sonyflake)分配 -
revisions表必须有created_at字段,并在查询历史列表时按此字段ORDER BY created_at DESC,不依赖revision_id排序 - 前端展示“第 N 版”时,显示的是
ROW_NUMBER() OVER (ORDER BY created_at DESC)计算出的位置,不是revision_id值本身
回滚前必须校验目标 revision 是否属于该 page
直接通过 URL 参数传 revision_id 并执行还原,是典型的越权漏洞。攻击者可以构造 /api/pages/123/revert?to=456,把别人页面的旧版本强行刷到当前页,尤其当 revision_id 是自增数字时极易遍历。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 回滚前查一次
SELECT COUNT(1) FROM revisions WHERE id = ? AND page_id = ?,不命中就返回404 - 不要复用
GetPageByID的缓存结果来判断归属——缓存可能过期,必须走带page_id条件的精确查询 - 如果系统支持命名空间(如
tenant_id),校验必须包含该字段,防止跨租户污染
附件引用需软删除 + 回滚感知,否则出现“404 图片”
用户编辑时删掉一张图,系统通常只是从 page_attachments 表移除记录,但对象存储里的文件没删。回滚到带图的旧版时,如果只恢复数据库关联关系,而附件文件已被清理,页面就会显示断裂图片。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 附件表加
is_deleted字段,删附件只是标记,真正清理走后台定时任务(配合 S3 生命周期策略) -
RestoreRevision函数中,除了恢复page_attachments关联,还要检查被恢复的 attachment 记录是否is_deleted = true,若是则置为false - 附件上传路径中嵌入
revision_id(如s3://wiki-bucket/pages/123/rev_456/header.jpg),避免不同版本间文件覆盖或误删










