[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"app-site-settings":3,"plugin-slots":41,"article-detail-47":44},{"site_name":4,"site_description":5,"site_keywords":6,"footer_copyright_text":7,"footer_icp_text":8,"footer_icp_url":9,"footer_show_icp":10,"footer_server_info":11,"footer_show_visit":10,"footer_show_sitemap":10,"footer_show_subscribe":10,"contact_email":12,"sec_enable_host_check":10,"sec_allowed_hosts":13,"sec_allowed_origins":14,"sec_enable_hotlink":15,"sec_hotlink_allow":16,"sec_enable_honeypot":10,"sec_rate_limit_window":17,"sec_rate_limit_max":18,"nav_menus":19,"site_logo":11,"site_favicon":20,"comment_moderation":10,"footer_gongan_text":21,"footer_gongan_url":22,"footer_report_text":23,"footer_report_email":12,"comment_ai_review":10,"fl_site_info":24,"auto_defense_enabled":25,"auto_defense_threshold":26,"auto_defense_ban_hours":27,"auto_defense_bad_ua_enabled":10,"auto_defense_scan_enabled":10,"sec_comment_rate_enabled":10,"sec_comment_rate_limit":28,"sec_comment_rate_mode":29,"bg_image_enabled":15,"about_config":30,"about_content":31,"copyright_notice":32,"ack_intro":33,"island_festival_enabled":10,"island_festival_start":34,"island_festival_end":35,"island_festival_text":36,"site_url":37,"theme_active":38,"theme_config":39,"sec_base_domain":11,"brand_name":40,"comment_require_login":10,"ai_nav_seeded":10},"lcrworld's Blog","技术、项目与生活记录。分享技术教程、文学创作、摄影作品与生活感悟。","lcrworld,博客,技术,Vue,Node.js,摄影,文学,井天乐,blog","© {year} {site}","冀ICP备2026028241号","https:\u002F\u002Fbeian.miit.gov.cn\u002F","1","","lcrblog@163.com","[\"lcrworld.xyz\",\"www.lcrworld.xyz\",\"en.lcrworld.xyz\",\"localhost\",\"127.0.0.1\"]","[\"https:\u002F\u002Flcrworld.xyz\",\"https:\u002F\u002Fwww.lcrworld.xyz\",\"https:\u002F\u002Fen.lcrworld.xyz\",\"http:\u002F\u002Flocalhost\",\"http:\u002F\u002F127.0.0.1\",\"http:\u002F\u002Flocalhost:3001\",\"http:\u002F\u002F127.0.0.1:3001\",\"http:\u002F\u002Flocalhost:5173\",\"http:\u002F\u002Flocalhost:5174\"]","0","[]","60","100","[{\"label\":\"首页\",\"path\":\"\u002F\"},{\"label\":\"时光\",\"path\":\"\u002Farchives\"},{\"label\":\"动态\",\"path\":\"\u002Fmoments\"},{\"label\":\"实验\",\"path\":\"\u002Fexperiments\"},{\"label\":\"关于\",\"path\":\"\u002Fabout\"},{\"label\":\"媒体库\",\"path\":\"\u002Fmedia\"},{\"label\":\"小玩具\",\"path\":\"\",\"href\":\"https:\u002F\u002Flab.lcrworld.xyz\",\"exact\":false,\"newtab\":true},{\"label\":\"音乐盒\",\"path\":\"\u002Fmusic\"},{\"label\":\"友人帐\",\"path\":\"\u002Ffriend-links\"},{\"label\":\"LCR AI\",\"path\":\"\u002Fai\"}]","\u002Ffavicon-32x32.png","冀公网安备13010402003584号","https:\u002F\u002Fbeian.mps.gov.cn\u002F#\u002Fquery\u002FwebSearch?code=13010402003584","违法和不良信息举报","{\"name\":\"lcrworld's blog\",\"url\":\"https:\u002F\u002Flcrworld.xyz\",\"avatar\":\"https:\u002F\u002Flcrworld.xyz\u002Fdefault-avatar.jpeg\",\"desc\":\"lcrworld的个人博客，分享文章，记录日常\",\"rss_articles\":\"https:\u002F\u002Flcrworld.xyz\u002Fapi\u002Frss\u002Ffeed\u002Fnewrss\",\"rss_moments\":\"https:\u002F\u002Flcrworld.xyz\u002Fapi\u002Frss\u002Ffeed\u002Fnewrss\"}","true","10","24","3","review","{\"heroSubtitle\":\"lcrworld 的个人数字花园\",\"rssArticleLabel\":\"文章 RSS\",\"rssMomentLabel\":\"动态 RSS\",\"statsLabels\":[\"文章\",\"动态\",\"媒体\",\"评论\",\"总访客\",\"今日访客\"],\"lifeFacts\":[{\"icon\":\"♒\",\"value\":\"水瓶座\",\"label\":\"星座\",\"accent\":\"\"},{\"icon\":\"📆\",\"value\":\"14\",\"label\":\"年龄\",\"accent\":\"\"},{\"icon\":\"❤️\",\"value\":\"女\",\"label\":\"性别\",\"accent\":\"\"}],\"personality\":{\"tips\":\"性格\",\"title\":\"INFJ 提倡者\",\"badge\":\"INFJ\",\"badgeColor\":\"#52b788\",\"desc\":\"理想主义与共情力的结合。喜欢深度思考，安静而坚定，追求有意义的事，相信文字与创造可以改变世界。\",\"linkText\":\"了解 MBTI 人格 →\",\"linkUrl\":\"https:\u002F\u002Fwww.16personalities.com\u002Fch\u002F\",\"imageUrl\":\"https:\u002F\u002Flcrworld.xyz\u002Fuploads\u002Fmedia\u002Fcompressed\u002F6952-1789746513543.webp\"},\"donation\":{\"tips\":\"感谢\",\"title\":\"打赏支持\",\"intro\":\"如果这里的内容对你有帮助，欢迎请我喝一杯咖啡喵～\",\"buttonText\":\"投喂\",\"wechatQr\":\"https:\u002F\u002Flcrworld.xyz\u002Fuploads\u002Fmedia\u002Fcompressed\u002F7363-1790990938950.webp\",\"alipayQr\":\"\"}}","你好，我是 **lcrworld**，一个喜欢折腾的技术爱好者。这里是我的个人数字花园，用来安放技术笔记、文学创作、摄影作品与生活碎片。\n\n## 关于这个博客\n\n博客自 2026 年 5 月上线运行，同年 8 月完成 **Nuxt 4 + Node.js + SQLite** 全栈重构。从数据库到部署、从设计到文案，全部由我一个人完成。\n\n- 文章：Markdown 写作、AI 智能摘要、AI 目录生成、阅读设置（字号 \u002F 行高 \u002F 字体）\n- 动态：碎语板块，记录日常灵感与即兴想法\n- 评论：人机验证 + AI 审核，先审后发，保护讨论环境\n- 其他：背景实验室（BG Lab）、友链、版权中心、更新日志\n\n> 这个站点本身就是我最大的作品——它每天都在被我打磨。\n\n## 我在做什么\n\n- **开发**：Vue \u002F Nuxt \u002F Node.js，喜欢把想法变成能用的东西\n- **写作**：技术教程、文学创作与摄影记录\n- **开源**：在 GitHub \u002F Gitee 维护个人项目，贡献数据见下方\n\n## 联系我\n\n- 邮箱：[lcrblog@163.com](mailto:lcrblog@163.com)\n- B 站：投稿见上方「B站投稿」\n- 友链 \u002F 合作：欢迎前往「友链」页面申请\n\n## 说明\n\n本站内容除特殊声明外均为原创，转载或引用请注明出处并附原文链接，详见[版权中心](\u002Fcopyright)。","{\"source_code\": \"本站源代码为闭源项目，未对外开源，不提供源码下载、分发或二次开发授权。\", \"content\": \"本站文章均为原创并保留所有权利（All Rights Reserved）。任何转载、摘编、翻译、演绎或商业使用，均须事先获得作者书面授权，并注明出处及原文链接。变更生效前（2026 年 9 月 5 日之前）依据 CC BY-NC-SA 协议已合法取得的授权不溯及既往，仍然有效。\", \"updated_at\": \"2026-09-13\"}","{\"title\":\"鸣谢\",\"subtitle\":\"致每一位曾为 v3blog 伸出援手的朋友\",\"text\":\"一个站点的成长，离不开朋友们的真心帮助。以下是 v3blog 开发过程中给予过支持与启发的伙伴，谨以此页，聊表谢意。\",\"updated_at\":\"2026-09-05\"}","2026-09-23","2026-09-30","祖国母亲77周年🎉","https:\u002F\u002Flcrworld.xyz","default","{}","lcrworld",{"slots":42,"active":43},{},[],{"id":45,"title":46,"slug":47,"content":48,"excerpt":49,"cover_url":50,"category_id":51,"author_id":52,"status":53,"view_count":54,"like_count":55,"word_count":56,"is_pinned":55,"total_reading_time":55,"reading_count":55,"avg_reading_time":55,"created_at":57,"updated_at":58,"background_url":59,"seo_keywords":60,"ai_summary":61,"lang":62,"translation_of":59,"is_hidden":55,"scheduled_at":59,"license":63,"ai_toc":59,"category_name":64,"category_slug":64,"author_name":40,"tags":65,"prev_article":66,"next_article":71},47,"从邮件里捞回丢失的评论：SQLite WAL 备份踩坑全记录","从邮件里捞回丢失的评论sqlite-wal-备份踩坑全记录","\u003Cp style=\"text-align:right;font-size:14px;color:#888;\">2026 年 7 月 9 日 · 数据恢复纪实\u003C\u002Fp>\n\n\u003Ch2>评论丢了\u003C\u002Fh2>\n\n\u003Cp>上一篇文章讲到 \u003Ccode>git reset --hard\u003C\u002Fcode> 覆盖了 WAL 文件导致数据库清空，靠凌晨 3 点的自动备份恢复了 45 篇文章和 167 条动态。当时以为\u003Cstrong>数据全部救回来了\u003C\u002Fstrong>。\u003C\u002Fp>\n\n\u003Cp>直到有人反馈：\u003Cspan style=\"text-decoration:underline;\">「昨天晚上的一些评论没了」\u003C\u002Fspan>\u003C\u002Fp>\n\n\u003Cp>一查数据库，评论表最大 ID 是 311，最后一条停在 7 月 8 日上午 11:21。但 nginx 访问日志清楚地记录着——7 月 8 日下午到晚上，还有 \u003Cstrong>10 条评论 POST 请求\u003C\u002Fstrong>。\u003C\u002Fp>\n\n\u003Cblockquote style=\"border-left:4px solid #e74c3c;padding:12px 20px;margin:20px 0;background:#fdf0ef;\">\n\u003Cp style=\"font-size:16px;\">也就是说，311 号到 320 号之间的 9 条评论，\u003Cstrong>在备份里就不存在\u003C\u002Fstrong>。备份本身就不完整。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2>为什么备份没有这些评论？\u003C\u002Fh2>\n\n\u003Cp>答案藏在 SQLite 的 WAL 模式里。\u003C\u002Fp>\n\n\u003Ch3>WAL 模式的工作原理\u003C\u002Fh3>\n\n\u003Cp>SQLite 的 WAL（Write-Ahead Logging）模式下，写入操作不会直接写主数据库文件，而是先写到 \u003Ccode>blog.db-wal\u003C\u002Fcode> 日志文件中。只有当日志文件达到一定大小（默认 1000 页 ≈ 4MB），或手动执行 \u003Ccode>PRAGMA wal_checkpoint\u003C\u002Fcode> 时，数据才会合并（checkpoint）到主库 \u003Ccode>blog.db\u003C\u002Fcode>。\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>写入流程：\n新数据 ──→ blog.db-wal（WAL日志） ──→ checkpoint ──→ blog.db（主库）\n                                    ↑\n                              只有这一步之后\n                              数据才进入主库\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>而我的备份脚本是这样写的：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\"># \u002Fopt\u002Fbackup-blog.sh（旧版）\ncp \u002Fopt\u002Fblog\u002Fserver\u002Fdata\u002Fblog.db \"$BACKUP_DIR\u002Fblog_${DATE}.db\"\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Cmark style=\"background:#fff3cd;padding:2px 4px;\">\u003Cstrong>只 cp 了 blog.db，没管 blog.db-wal。\u003C\u002Fstrong>\u003C\u002Fmark>\u003C\u002Fp>\n\n\u003Cp>那些晚上写的评论，还躺在 WAL 日志里，没来得及 checkpoint 到主库。备份脚本 cp 了一个「不完整」的主库文件，WAL 里的新数据被完美地忽略了。\u003C\u002Fp>\n\n\u003Cp>然后 \u003Ccode>git reset --hard\u003C\u002Fcode> 又把 WAL 文件覆盖了。双重打击，数据彻底消失。\u003C\u002Fp>\n\n\u003Ch2>第一轮恢复：从收件箱的通知邮件\u003C\u002Fh2>\n\n\u003Cp>博客系统有一个功能：访客发评论时，自动给博主发一封邮件通知，邮件里包含\u003Cstrong>评论者昵称、评论内容、来源 IP、评论的文章\u003C\u002Fstrong>。\u003C\u002Fp>\n\n\u003Cp>如果邮件发成功了呢？\u003C\u002Fp>\n\n\u003Ch3>连接 IMAP 邮箱\u003C\u002Fh3>\n\n\u003Cp>博主邮箱是 \u003Ccode>lcrblog@163.com\u003C\u002Fcode>，SMTP 凭证就在服务器的 \u003Ccode>.env\u003C\u002Fcode> 里。直接用 IMAP 连上去读收件箱：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-python\">import imaplib\n\nimaplib.Commands['ID'] = ('AUTH', 'AUTHENTICATED', 'SELECTED')\n\nmail = imaplib.IMAP4_SSL('imap.163.com', 993)\nmail.login('lcrblog@163.com', '******')\n\n# 163 邮箱需要先发 ID 命令，否则 SELECT 会报\n# \"Unsafe Login. Please contact kefu@188.com\"\ntag = mail._new_tag()\nmail.send(tag + b' ID (\"name\" \"imapclient\" \"version\" \"1.0\")\\r\\n')\nwhile True:\n    line = mail.readline()\n    if line.startswith(tag): break\n\nmail.select('INBOX')\nstatus, messages = mail.search(None, 'ALL')\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cblockquote style=\"border-left:4px solid #f39c12;padding:8px 16px;margin:16px 0;background:#fef9e7;\">\n\u003Cp>\u003Cstrong>坑点：\u003C\u002Fstrong>163 邮箱的 IMAP 服务要求客户端先发送 \u003Ccode>ID\u003C\u002Fcode> 命令标识自己，否则 \u003Ccode>SELECT\u003C\u002Fcode> 命令会被拒绝，返回 \u003Ccode>Unsafe Login\u003C\u002Fcode> 错误。Python 标准库 \u003Ccode>imaplib\u003C\u002Fcode> 不内置 ID 命令支持，需要手动注册并发送。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>邮件内容解析\u003C\u002Fh3>\n\n\u003Cp>收件箱一共 11 封邮件，其中 8 封是评论通知。逐封解析后，找到了 4 封属于丢失时间段的邮件：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>#312  asdf   15:50  文章45  「建议您试试草莓云机场...」（广告 spam）\n#313  Vei    17:27  文章45  「文章顶部的进度条让页面有卡顿\u002F掉帧的感觉」\n#314  Mr.Lee 17:43  文章45  「暗色模式的材质bug还是比较多...」\n#315  Mr.Lee 18:17  文章33  「实验这个模块看起来不错，isr敢设置5分钟？羡慕」\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>每封邮件里都有完整的评论内容、访客昵称、IP 地址和时间戳。配合 nginx 访问日志里的 User-Agent，拼齐了所有字段。\u003C\u002Fp>\n\n\u003Ch3>写回数据库\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">const insert = db.prepare(`\n  INSERT INTO comments \n  (visitor_name, target_type, target_id, content, ip, \n   created_at, user_agent, ip_location, like_count, parent_id) \n  VALUES \n  (@visitor_name, @target_type, @target_id, @content, @ip, \n   @created_at, @user_agent, @ip_location, 0, NULL)\n`);\n\nfor (const c of recoveredComments) {\n  insert.run(c);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>4 条访客评论，ID 312-315，成功写回。\u003C\u002Fp>\n\n\u003Cp>但 nginx 日志显示有 9 条评论 POST。剩下的 5 条呢？\u003C\u002Fp>\n\n\u003Ch2>5 条评论去哪了？\u003C\u002Fh2>\n\n\u003Cp>逐条分析 nginx 日志后，真相浮出水面：\u003C\u002Fp>\n\n\u003Ctable style=\"width:100%;border-collapse:collapse;font-size:14px;\">\n\u003Cthead>\n\u003Ctr style=\"background:#f0f0f0;\">\n\u003Cth style=\"border:1px solid #ddd;padding:8px;text-align:left;\">时间\u003C\u002Fth>\n\u003Cth style=\"border:1px solid #ddd;padding:8px;text-align:left;\">IP\u003C\u002Fth>\n\u003Cth style=\"border:1px solid #ddd;padding:8px;text-align:left;\">设备\u003C\u002Fth>\n\u003Cth style=\"border:1px solid #ddd;padding:8px;text-align:left;\">收件箱通知\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">15:50\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">42.200.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Windows Chrome\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:green;\">✅ 有\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">17:27\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">111.9.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Mac Chrome\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:green;\">✅ 有\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">17:43\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">104.28.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Linux Firefox\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:green;\">✅ 有\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">18:17\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">104.28.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Linux Firefox\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:green;\">✅ 有\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr style=\"background:#fff3cd;\">\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">18:29\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">183.197.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Pixel 7\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:red;\">❌ 无\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr style=\"background:#fff3cd;\">\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">18:33\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">183.197.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Pixel 7\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:red;\">❌ 无\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr style=\"background:#fff3cd;\">\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">18:37\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">183.197.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Pixel 7\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:red;\">❌ 无\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr style=\"background:#fff3cd;\">\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">18:39\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">104.28.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Linux Firefox\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:red;\">❌ 无\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr style=\"background:#fff3cd;\">\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">18:50\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">183.197.*\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;\">Windows Chrome\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:8px;color:red;\">❌ 无\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>没有收件箱通知的 5 条评论分两种情况：\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>1. 博主自己的回复（4 条）\u003C\u002Fstrong>\u003C\u002Fp>\n\n\u003Cp>IP \u003Ccode>183.197.*\u003C\u002Fcode> 来自 Pixel 7 和 Windows Chrome，是博主本人的设备。代码逻辑里，博主自己发评论不会触发「新评论通知」邮件——因为 \u003Ccode>!userId\u003C\u002Fcode> 的判断直接跳过了 \u003Ccode>sendBloggerNewCommentNotify\u003C\u002Fcode>。\u003C\u002Fp>\n\n\u003Cp>这是合理的：博主不需要给自己发通知。但在数据恢复场景下，这意味着\u003Cstrong>收件箱里没有任何渠道记录了这些评论的内容\u003C\u002Fstrong>。\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>2. 访客评论但邮件发送失败（1 条）\u003C\u002Fstrong>\u003C\u002Fp>\n\n\u003Cp>18:39 来自 \u003Ccode>104.28.*\u003C\u002Fcode>（Mr.Lee）的评论没有邮件通知。可能的原因是服务器当时已经在反复崩溃重启（PM2 日志显示重启了 47 次），邮件发送过程在崩溃中被中断了。\u003C\u002Fp>\n\n\u003Cp>第一轮恢复结束，我以为这 5 条评论\u003Cstrong>永远地消失在了数字世界里\u003C\u002Fstrong>。还写了一篇博文记录这个遗憾。\u003C\u002Fp>\n\n\u003Ch2>第二轮恢复：已发送文件夹里的回复通知\u003C\u002Fh2>\n\n\u003Cp>博文发出去没多久，一条提醒点醒了我：\u003C\u002Fp>\n\n\u003Cblockquote style=\"border-left:4px solid #3498db;padding:12px 20px;margin:20px 0;background:#ebf5fb;\">\n\u003Cp style=\"font-size:16px;\">博主回复访客评论时，系统会给访客发一封\u003Cstrong>回复通知邮件\u003C\u002Fstrong>，邮件里同时包含博主的回复内容和被回复的原评论。\u003Cstrong>这些邮件在已发送文件夹里。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>对啊！我只查了收件箱（新评论通知），完全忘了还有已发送文件夹（回复通知）。代码里写得清清楚楚：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F commentsController.js\nif (parentComment && parentComment.email) {\n  \u002F\u002F 博主回复不通知博主自己，但会给访客发通知\n  if (!(userId && parentComment.user_id === userId)) {\n    sendCommentReplyNotify(parentComment.email, recipientName, [replyData])\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>博主回复访客时，\u003Ccode>userId\u003C\u002Fcode> 存在但 \u003Ccode>parentComment.user_id\u003C\u002Fcode> 为 null（访客评论），条件成立，邮件发出去了。邮件里包含 \u003Ccode>replyContent\u003C\u002Fcode>（博主回复）和 \u003Ccode>parentContent\u003C\u002Fcode>（访客原评论）\u003Cstrong>两段内容\u003C\u002Fstrong>。\u003C\u002Fp>\n\n\u003Ch3>再次连接 IMAP，这次查已发送\u003C\u002Fh3>\n\n\u003Cp>163 邮箱的已发送文件夹不叫 \u003Ccode>Sent\u003C\u002Fcode>，而是用一个编码后的名字：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-python\"># 163 的文件夹列表\n() \"\u002F\" \"INBOX\"\n(\\Sent) \"\u002F\" \"&XfJT0ZAB-\"    # ← 这就是已发送\n(\\Trash) \"\u002F\" \"&XfJSIJZk-\"\n(\\Junk) \"\u002F\" \"&V4NXPpCuTvY-\"\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>选中 \u003Ccode>&XfJT0ZAB-\u003C\u002Fcode>，70 封邮件。逐封解析，找到了 4 封回复通知邮件：\u003C\u002Fp>\n\n\u003Ctable style=\"width:100%;border-collapse:collapse;font-size:13px;\">\n\u003Cthead>\n\u003Ctr style=\"background:#f0f0f0;\">\n\u003Cth style=\"border:1px solid #ddd;padding:6px;\">邮件\u003C\u002Fth>\n\u003Cth style=\"border:1px solid #ddd;padding:6px;\">时间\u003C\u002Fth>\n\u003Cth style=\"border:1px solid #ddd;padding:6px;\">收件人\u003C\u002Fth>\n\u003Cth style=\"border:1px solid #ddd;padding:6px;\">回复内容\u003C\u002Fth>\n\u003Cth style=\"border:1px solid #ddd;padding:6px;\">原评论\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">#56\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">18:29\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">chzxu******@gmail.com\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">对对对！多亏你提醒我ᗜⰙᗜ...\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">暗色模式的材质bug... (Mr.Lee)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">#57\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">18:33\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">12658******@qq.com\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎ 谢谢你的喜欢...\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">界面、布局、风格都相当奈斯... (瓦匠)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">#58\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">18:37\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">v**@veitzn.top\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">谢谢，你也一样! 进度条已优化...\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">进度条让页面有卡顿\u002F掉帧... (Vei)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">#59\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">18:50\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">chzxu******@gmail.com\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">谢大佬指教，现在就加...\u003C\u002Ftd>\u003Ctd style=\"border:1px solid #ddd;padding:6px;\">关于RSS还有个问题反馈... (Mr.Lee)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>每封邮件都包含\u003Cstrong>完整的回复内容和原评论内容\u003C\u002Fstrong>。更妙的是，回复通知邮件里的 \u003Ccode>parentContent\u003C\u002Fcode> 字段就是被回复的原评论——也就是说，那封「邮件发送失败」的 Mr.Lee RSS 评论，其完整内容恰好被保存在了邮件 #59 的原评论字段里。\u003C\u002Fp>\n\n\u003Ch3>第 5 条评论的意外回归\u003C\u002Fh3>\n\n\u003Cp>邮件 #59 的原评论字段中，Mr.Lee 关于 RSS 的评论完整保存着，共 195 个字符——而邮件模板里的截断阈值是 200 字符。\u003Cmark style=\"background:#d4edda;padding:2px 4px;\">差 5 个字符就截断了，但它没有。\u003C\u002Fmark>\u003C\u002Fp>\n\n\u003Cp>那条「邮件发送失败、收件箱里没有」的评论，就这样从一封\u003Cstrong>回复通知邮件的原评论字段\u003C\u002Fstrong>里完整捞了回来。\u003C\u002Fp>\n\n\u003Ch3>额外收获：访客邮箱\u003C\u002Fh3>\n\n\u003Cp>回复通知邮件的收件人地址，就是访客在评论时留下的邮箱。顺带把 3 条评论的邮箱也补上了：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>#313 Vei    → v**@veitzn.top\n#314 Mr.Lee → chzxu******@gmail.com\n#315 Mr.Lee → chzxu******@gmail.com\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>全部写回\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F 1. 更新访客邮箱\ndb.prepare('UPDATE comments SET email = ? WHERE id = ?')\n  .run('v**@veitzn.top', 313);\n\n\u002F\u002F 2. 插入 Mr.Lee 的 RSS 评论（从邮件 #59 原评论字段恢复）\ndb.prepare(`INSERT INTO comments \n  (visitor_name, email, target_type, target_id, content, ip, user_agent, created_at)\n  VALUES (?, ?, 'article', ?, ?, ?, ?, ?)`)\n  .run('Mr.Lee', 'chzxu******@gmail.com', 45, \n       rssContent, '104.28.157.27', linuxFirefoxUA, '2026-07-08 18:39:12');\n\n\u002F\u002F 3. 插入 4 条博主回复\nfor (const r of replies) {\n  db.prepare(`INSERT INTO comments \n    (user_id, target_type, target_id, content, parent_id, ip, user_agent, created_at)\n    VALUES (1, 'article', 45, ?, ?, ?, ?, ?)`)\n    .run(r.content, r.parent_id, r.ip, r.ua, r.created_at);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>9 条评论，\u003Cstrong>全部回来了\u003C\u002Fstrong>。\u003C\u002Fp>\n\n\u003Ch2>修复备份脚本\u003C\u002Fh2>\n\n\u003Cp>问题的根源是备份脚本只 \u003Ccode>cp\u003C\u002Fcode> 主库文件，不管 WAL。修复很简单——\u003Ccode>cp\u003C\u002Fcode> 之前先做一次 checkpoint：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\"># \u002Fopt\u002Fbackup-blog.sh（修复后）\n\n# 先 checkpoint WAL，把所有数据刷入主库\nnode --input-type=module -e \"\nimport Database from 'better-sqlite3';\nconst db = new Database('data\u002Fblog.db');\ndb.pragma('wal_checkpoint(TRUNCATE)');\ndb.close();\n\"\n\n# 再用 sqlite3 .backup 做一致性快照\nsqlite3 data\u002Fblog.db \".backup '$BACKUP_DIR\u002Fblog_${DATE}.db'\"\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>两个改进：\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>1. WAL checkpoint\u003C\u002Fstrong> — 把 WAL 日志里的数据全部刷入主库文件，确保 \u003Ccode>blog.db\u003C\u002Fcode> 包含最新数据。\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>2. sqlite3 .backup 命令\u003C\u002Fstrong> — 替代 \u003Ccode>cp\u003C\u002Fcode>。\u003Ccode>.backup\u003C\u002Fcode> 是 SQLite 官方提供的一致性备份方法，它会在内部处理好 WAL\u002F主库的一致性问题，即使不手动 checkpoint 也能得到完整快照。\u003C\u002Fp>\n\n\u003Ch2>经验总结\u003C\u002Fh2>\n\n\u003Ch3>1. SQLite WAL 模式的备份必须先 checkpoint\u003C\u002Fh3>\n\n\u003Cp>这是一个非常容易被忽略的坑。WAL 模式下，\u003Ccode>cp blog.db\u003C\u002Fcode> 得到的是一个\u003Cstrong>不完整的快照\u003C\u002Fstrong>。正确做法：\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode># 方法一：先 checkpoint 再 cp\ndb.pragma('wal_checkpoint(TRUNCATE)')\ncp blog.db backup.db\n\n# 方法二：用 sqlite3 .backup（推荐）\nsqlite3 blog.db \".backup 'backup.db'\"\n\n# 方法三：三个文件一起拷\ncp blog.db blog.db-wal blog.db-shm backup\u002F\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>2. 邮件通知是意外的数据备份渠道\u003C\u002Fh3>\n\n\u003Cp>当初加邮件通知功能只是为了让博主及时看到评论，没想到在数据恢复时成了\u003Cstrong>救命稻草\u003C\u002Fstrong>。更关键的是，\u003Cstrong>收件箱和已发送文件夹是两个互补的数据源\u003C\u002Fstrong>：\u003C\u002Fp>\n\n\u003Cul>\n\u003Cli>\u003Cstrong>收件箱\u003C\u002Fstrong>（新评论通知）→ 记录访客评论\u003C\u002Fli>\n\u003Cli>\u003Cstrong>已发送\u003C\u002Fstrong>（回复通知）→ 同时记录博主回复 + 被回复的原评论\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>两个文件夹加起来，几乎覆盖了所有评论场景。唯一恢复不了的，是博主回复一条\u003Cstrong>没留邮箱的访客评论\u003C\u002Fstrong>——这种情况下既没有收件箱通知（博主自己发的），也没有已发送通知（访客没邮箱没法发）。\u003C\u002Fp>\n\n\u003Ch3>3. 永远不要只依赖单一备份\u003C\u002Fh3>\n\n\u003Cp>这次有自动备份（虽然不完整）、有邮件通知（虽然不覆盖全部）、有 nginx 日志（虽然只有元数据）。\u003Cstrong>每一份数据源都不完整，但组合起来恢复了全部数据\u003C\u002Fstrong>。多渠道冗余是数据安全的核心原则。\u003C\u002Fp>\n\n\u003Ch2>最后\u003C\u002Fh2>\n\n\u003Cp>9 条评论，全部找回。\u003C\u002Fp>\n\n\u003Cp>第一轮从收件箱捞回 4 条访客评论时，我以为故事到此结束了。剩下的 5 条——4 条博主回复、1 条邮件发送失败的访客评论——我以为永远消失了。还写了篇博文记录这个遗憾。\u003C\u002Fp>\n\n\u003Cp>结果一条提醒让我想起来：已发送文件夹里还有回复通知邮件。打开一看，不仅 4 条博主回复完整保存，连那条「邮件发送失败」的访客评论，也因为它被回复了，其内容被保存在了回复邮件的原评论字段里。\u003C\u002Fp>\n\n\u003Cp style=\"text-align:center;font-size:18px;margin-top:30px;\">备份要 checkpoint，通知要全覆盖，\u003Cstrong>数据安全没有银弹，只有层层防御\u003C\u002Fstrong>。\u003C\u002Fp>\n\n\u003Cp style=\"text-align:center;font-size:14px;color:#888;margin-top:10px;\">而有时候，你以为丢了的数据，只是换了个地方存着。\u003C\u002Fp>\n\n\u003Chr style=\"margin:30px 0;border:none;border-top:1px solid #eee;\">\n\n\u003Cp style=\"font-size:13px;color:#999;\">相关文章：\u003Ca href=\"\u002Farticles\u002F46\">《一条 git reset --hard，我的数据库空了：一次生产事故的完整复盘》\u003C\u002Fa>\u003C\u002Fp>","数据库被 git reset 覆盖后，9 条评论随之蒸发。备份脚本只 cp 了主库没管 WAL，凌晨三点的备份形同虚设。先从收件箱的评论通知邮件里捞回 4 条访客评论，本以为剩下 5 条永久丢失——直到发现已发送文件夹里的回复通知邮件，把博主自己的评论也全捞了回来。9 条，一条没少。","https:\u002F\u002Fmy.lcrworld.xyz\u002Fu\u002FsLhAojMb4Fc",12,1,"published",214,0,5859,"2026-07-09 10:11:54","2026-09-05 16:10:20",null,"SQLite WAL,数据备份,评论恢复,IMAP,生产事故","因 SQLite WAL 模式下备份脚本仅复制主库而忽略日志文件，导致 9 条评论丢失。作者通过解析 IMAP 邮箱通知成功找回 4 条访客评论并写回数据库，但其余 5 条因未触发通知机制或发送失败而永久消失。","zh","all-rights-reserved","技术",[],{"id":67,"title":68,"slug":69,"cover_url":70},46,"一条 git reset --hard，我的数据库空了!","一条-git-reset-hard我的数据库空了","https:\u002F\u002Fmy.lcrworld.xyz\u002Fu\u002FFp0cTN2zR4o",{"id":72,"title":73,"slug":74,"cover_url":75},49,"为什么我认为“早恋”这个词有存在的必要","为什么我认为早恋这个词有存在的必要","https:\u002F\u002Flcrworld.xyz\u002Fuploads\u002Fmedia\u002Fcompressed\u002F6384-1783305549258.jpg"]