独立开发 30 天:2.5 万行代码,23 个 Bug,5 次重构——一个 AI 社区的诞生

📅 2026/6/26 1:27:36
独立开发 30 天:2.5 万行代码,23 个 Bug,5 次重构——一个 AI 社区的诞生
30 天前我打开 VS Code新建了一个空文件夹。30 天后这个文件夹变成了一个运行在生产环境的全栈 AI 开发者社区——22 篇帖子、75 个独立用户、平均停留 5 分钟。这篇文章不是什么「从零到上线」的教程而是纯粹的后见之明那些做对了的决策、做错了的返工、以及每一个让我半夜爬起来修 Bug 的瞬间。目录项目定位为什么不做单纯的博客或论坛技术选型全栈 TypeScript 是正确选择吗数据库 Schema 的五次演进评论系统从全局状态到隔离组件的重构生产环境最危险的 Bug崩溃循环Nginx 301 陷阱一个消失了 3 天的 POST 请求SSR 与反爬中间件的内战SEO给搜索引擎写代码监控从零到 Grafana Alloy管理后台的三次重构成本清单如果重来一次1. 项目定位为什么不做单纯的博客或论坛市面上不缺论坛系统。Discuz、Flarum、Discourse 都是好东西。市面上也不缺博客系统。WordPress、Ghost、Hugo 都是好东西。但我要的是一个混合体用户既能写个人博客有自己的主题色、封面图、副标题也能在公共版块里提问讨论。一个账号两种身份。这个决策在 30 天后回头看是对的。我的 22 篇帖子里7 篇是深度博客RAG 指南、API 双轨实战、AI 架构设计15 篇是论坛讨论MCP 踩坑、AI 职业讨论。这两类内容在同一个社区里形成了互补——博客吸引搜索流量论坛留住活跃用户。但代价是需要自己写所有东西。认证、权限、编辑器、通知、搜索、金币、管理后台——一个 Spring Boot Thymeleaf 的项目可能不需要这些但一个现代社区平台一个都不能少。2. 技术选型全栈 TypeScript 是正确选择吗Stack前端Next.js 15 React 19 Tailwind CSS shadcn/ui后端NestJS Prisma PostgreSQL/pgvector Redis部署Docker Compose Nginx Cloudflare Hetzner VPS做对了的Prisma ORM。这是我做的最正确的技术选型。Schema 定义即类型来源迁移管理清晰30 天跑了 4 次 migration查询写法比 TypeORM 简洁一个数量级。中途改了 5 次 SchemaPrisma 没给我添乱。shadcn/ui。组件复制到项目中Tailwind 原生支持想改就改。不像 MUI/Ant Design 那样改一个按钮颜色需要读一页文档。Docker Compose。5 个容器前端、后端、PostgreSQL、Redis、Nginx一个docker-compose.prod.yml描述整个系统。部署是git pull ./deploy.sh。做错了的Edge Runtime 用了又换回来了。OG 图片生成端点最初设了export const runtime edge结果ImageResponse在 Edge Runtime 下报TypeError: immutable。改成runtime nodejs立刻好了。Edge Runtime 的「真 Serverless」承诺很诱人但实际兼容性问题太多。小型项目不值得踩这个坑。Next.jsPromise.allSettled的坑。首页用Promise.allSettled拉 4 个 API节点、帖子、热门、置顶某个 API 失败时静默返回空数组导致首页渲染「暂无热门内容」但明明数据库里有两万条。应该用Promise.all或者至少对 reject 做 fallback 处理。3. 数据库 Schema 的五次演进Prisma Schema 的变化本身就是项目的缩影v1 (Week 1): User, Post, Comment, Node — 4 个模型 v2 (Week 2): Media, Collection, Favorite, Notification — 8 个 v3 (Week 3): CoinLedger, Purchase, UserSession — 11 个 v4 (Week 4): searchVector (tsvector), messagePermission, AI 相关字段 v5 (Week 5): isFeatured, featuredAt, featuredScore — 精选算法教训一UserSession不要为了省事用内存。我的第一版在线状态用Map存服务重启全丢。改 Redis 数据库持久化后在线状态才真正可用。教训二tsvector比你想的复杂。我以为ALTER TABLE ADD COLUMN searchVector tsvector就完事了。结果发现需要创建BEFORE INSERT OR UPDATE触发器、GIN 索引、safe_plainto_tsquerywrapper 函数防止空查询抛错以及写 SQL 回填历史数据。总共 32 行迁移 SQL。教训三加字段容易加完忘了跑prisma db push是灾难。User 表加了messagePermission字段但数据库没同步Prisma Client 编译时不知道这个字段查询直接 500。记住改 Prisma Schema → 跑迁移 → 重新生成 Prisma Client三步少一步都不行。4. 评论系统从全局状态到隔离组件的重构这是整个项目里改动最深的一次重构。第一版一个CommentTree组件管理所有评论的回复状态。// 问题代码const[replyText,setReplyText]useState();const[replying,setReplying]useStatestring|null(null);const[submitting,setSubmitting]useState(false);// 所有评论共享同一套状态// 评论 A 点回复 → 评论 B 的输入框也被填了Bug 表现回复评论 A 时评论 B 的回复表单也被打开。回复提交后setSubmitting(false)影响的是全局状态——如果此时评论 B 正在提交它的isSubmitting也会被改成false导致评论 B 的回复请求根本没发出去。第二版引入submittingRef解决闭包过期但底层问题还在——共享状态。第三版把render()函数拆成独立的CommentItem组件。// 每个 CommentItem 封装自己的回复状态functionCommentItem({comment,onReply}){const[replyText,setReplyText]useState();const[replying,setReplying]useState(false);const[submitting,setSubmitting]useState(false);// 状态隔离。评论 A 和评论 B 的回复表单互不影响。}重构代价172 行 / -52 行总代码量增加但可维护性是质变。教训React 里能用组件隔离就不要用「全局状态 函数」。一个状态变量被多个 UI 节点共享早晚会出 Bug。5. 生产环境最危险的 Bug崩溃循环现象网站突然 502后端容器不停重启30 天累计 100 次。排查过程docker ps→ 后端容器Up 6 seconds (healthy)过 10 秒再看又是Up 3 seconds→ 容器在不断重启docker logs→ 看到Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client但这不是根因——哪个正常请求会重复发响应继续翻日志 → 发现ScrapingDetection的 429 响应先发出去了然后HttpExceptionFilter又尝试response.json()因为响应已经发出去了response.headersSent true直接抛未捕获的ERR_HTTP_HEADERS_SENTNode.js 进程崩溃崩溃链搜索爬虫快速请求 → ScrapingDetection 触发 → 返回 429响应已发送 → NestJS 异常处理 → HttpExceptionFilter 尝试 response.json() → headersSent true → 抛 ERR_HTTP_HEADERS_SENT → 未捕获 → Node.js 进程 exit(1) → Docker restart: always → 重启 → 重启后爬虫又来 → 再次崩溃 → 无限循环修复3 行代码。// http-exception.filter.tsif(response.headersSent){return;// 响应已经被拦截器发过了别再发一次}response.status(status).json({...});这个 Bug 教会我一件事永远在全局异常过滤器里检查headersSent。你的拦截器可能在过滤器之前就发送了响应。6. Nginx 301 陷阱一个消失了 3 天的 POST 请求这是整个项目最诡异的 Bug——它让我困惑了整整 3 天。现象点击「发布帖子」按钮前端显示「发布成功」但数据库里没有新帖子。后端日志里没有任何 POST 请求记录。排查检查CreatePageContent.tsx→api.post(/posts, payload)确实调了检查api.ts→fetch(/api/posts, { method: POST })没问题在浏览器 DevTools Console 直接调fetch(/api/posts, { method: POST })→能创建帖子在 React 代码里加console.log→api.post确实执行了在浏览器 Network 面板看 →只有 GET 请求没有 POST拦截window.fetch→有 POST但 URL 是/api/posts/带斜杠状态码 301 → 302 → 请求体丢失根因Nginx 配置里location /api/posts/ { ... } # 匹配带斜杠 location /api/ { ... } # 匹配不带斜杠POST /api/posts不带斜杠先匹配/api/然后被重定向到/api/posts/301。浏览器跟随 301 时把 POST 变成了 GET请求体丢失。后端收到一个 GET 请求返回 200 和帖子列表前端以为发布成功。修复加location /api/posts精确匹配。修复后 3 天 Bug 又复现因为nginx -s reload没从 Docker volume 挂载里读到新文件。必须docker restart。最终修复docker restart zhiqu-nginx-prod。教训Docker volume 挂载的配置文件nginx -s reload不一定读得到。保险方案是docker restart。7. SSR 与反爬中间件的内战现象某天开始首页「热门问题」和「精华推荐」区域不显示任何帖子显示「暂无热门内容」。排查后端 API 正常 → 能返回热门帖子前端 DevTools Network → 首页根本没发GET /api/posts/hot请求但 Next.js 是 SSR 的数据在服务端拉 → 前端不显示代表 SSR 失败了进前端容器wget http://backend:4000/api/posts/hot→403 Forbidden根因前端 SSR 的 Node.js 原生fetch()不带浏览器 User-Agent。后端反爬中间件看到缺失或非浏览器的 UA直接返回 403。Next.js SSR 在请求失败时用Promise.allSettled吞了错误返回空数组页面渲染空状态。修复前端api.ts里给 SSR 请求加User-Agent: Zhiqu/1.0 (internal-ssr)后端反爬中间件 反爬拦截器对 Docker 内网 IP172.17.0.0/16,172.18.0.0/16白名单放行教训如果你同时有 SSR 和反爬措施它们会打架。SSR 的请求从服务端发出它们不是浏览器。你的反爬规则必须给这些内部请求留白名单。按 IP 白名单比按 UA 白名单更安全——UA 任何人都能伪造但 Docker 内网 IP 不能。8. SEO给搜索引擎写代码写了一个月代码发现被搜索引擎「看不见」。这是独立开发者最容易忽视的一环。JSON-LD 结构化数据{type:DiscussionForumPosting,headline:DeepSeek API 返回 429 怎么办,comment:[{type:Comment,text:建议加指数退避...,author:{type:Person,name:小鼻子的猫,url:https://zhiqu.ac/user/1304674612// ← Google 要求有 url}}]}Google Search Console 报comment.author缺少url字段。加了三行代码修好。百度收录百度推送 API 踩的坑site参数不能用encodeURIComponent编码后百度不认site的值必须和百度站长平台验证的域名完全一致www.zhiqu.acvszhiqu.ac→not_same_site百度推送有每日配额免费新站约 10 条/天HTTPS 不支持只能用 HTTP修了 5 次才通。第一次看到{success:7,remain:2}的时候比代码跑通还激动。SitemapNext.js 的sitemap.xml配置很简单但容易忘每发布一篇新博客确认 slug 后手动更新 lastmod。或者写个脚本自动生成。我没写脚本每次手动改——早晚会忘。9. 监控从零到 Grafana Alloy前 25 天我没有任何监控。每次服务挂了靠用户告诉我。第 26 天配了 Grafana Cloud Alloy# 一条命令装好ARCHamd64GCLOUD_HOSTED_METRICS_URL.../bin/sh-c$(curl-fsSL...)30 分钟后Grafana 大盘上开始出数据CPU 2.2%、内存 52%、磁盘 14%、Nginx QPS 0.5。教训监控应该在项目上线的第一天配好而不是第 26 天。那中间 25 天的「感觉一切正常」只是运气好。UptimeRobot 也是必需品——免费套餐每 5 分钟从全球节点检测一次你的网站挂了立刻邮件通知。我从第 28 天才开始用应该第 1 天就开。10. 管理后台的三次重构管理后台是我个人看得第二多的页面第一是首页。它经历了三次迭代v1单文件巨石一个admin/page.tsx700 行。所有功能——统计、用户管理、内容管理、SEO 推送、系统设置——全在一个页面里。v2标签页切换拆成 4 个 tab。统计、用户、内容、设置。每个 tab 还是在这个文件里靠activeTabstate 切换。v3多页面 独立 hooks56 个文件变更。每个模块独立页面 独立 hook。Dashboard、Users、Posts、AI、SEO、Audit、Reports、Sessions、Settings——各走各的路由。为什么要迭代三次因为每次迭代都是「当前代码太乱了忍不了才改」。如果一开始就知道要拆成多页面v1 就不会写成一个文件。但如果一开始就设计成多页面架构又会过度设计——因为前两周根本没那么多管理功能。这个矛盾没法完美解决。折中方案前两周单页面快速迭代功能稳定后立刻拆。11. 成本清单项目月费Hetzner CX22 (2C4G/50G)€4.51Cloudflare CDN DNS免费AI API (DeepSeek Claude)~$35阿里云 OSS (图片存储)~¥5Resend (邮件)免费额度Grafana Cloud免费额度UptimeRobot免费额度合计~¥75/月一个月 75 块钱能跑一个全栈 AI 社区。独立开发者的时代是真的来了。12. 如果重来一次第一天就配监控和告警。不是第 26 天。UptimeRobot 5 分钟搞定。Prisma Schema 先想清楚再动手。5 次 migration 里有 3 次是因为「后面发现需要这个字段」。如果第一天就把searchVector、messagePermission、isFeatured都考虑进去后面不用反复迁移。管理后台从第二天就开始用。我前两周靠直接查数据库管理内容。效率极低。一个简单的 CRUD 表格比任何 SQL 命令都管用。安全中间件留白名单。反爬 UA 中间件、反爬拦截器、Nginx 限流——这些都应该第一天就留好内部网络的白名单。不然你在排查「为什么 SSR 拉不到数据」时会盯着403 Forbidden怀疑人生。百度站长平台先把站点验证和领域设置做完再发帖。我发了 22 篇帖子才通百度推送前面 22 篇要等每天 10 条的配额慢慢补推。Docker 构建缓存要定期清理。30 天累计 32GB不清理的话一个月就能占满磁盘。30 天前我在 VS Code 里按mkdir zhiqu。30 天后有人在这里写博客、提问、收藏、点赞。有人搜索「DeepSeek 429」从 Google 点进来。有人在凌晨三点给我的帖子点了个赞。独立开发最好的部分不是代码跑通了。是你造的东西有人在用。本文所有代码和架构已在 [志趣社区] 生产环境运行。如果你也在做独立开发欢迎来社区聊聊你踩过的坑。