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

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 次 Schema,Prisma 没给我添乱。

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.js Promise.allSettled 的坑 。首页用 Promise.allSettled 拉 4 个 API(节点、帖子、热门、置顶),某个 API 失败时静默返回空数组,导致首页渲染「暂无热门内容」但明明数据库里有两万条。应该用 Promise.all 或者至少对 reject 做 fallback 处理。


3. 数据库 Schema 的五次演进

Prisma Schema 的变化本身就是项目的缩影:

erlang 复制代码
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_tsquery wrapper 函数防止空查询抛错,以及写 SQL 回填历史数据。总共 32 行迁移 SQL。

教训三 :加字段容易,加完忘了跑 prisma db push 是灾难。User 表加了 messagePermission 字段但数据库没同步,Prisma Client 编译时不知道这个字段,查询直接 500。记住:改 Prisma Schema → 跑迁移 → 重新生成 Prisma Client,三步少一步都不行。


4. 评论系统:从全局状态到隔离组件的重构

这是整个项目里改动最深的一次重构。

第一版 :一个 CommentTree 组件管理所有评论的回复状态。

typescript 复制代码
// 问题代码
const [replyText, setReplyText] = useState('');
const [replying, setReplying] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);

// 所有评论共享同一套状态!
// 评论 A 点回复 → 评论 B 的输入框也被填了

Bug 表现 :回复评论 A 时,评论 B 的回复表单也被打开。回复提交后,setSubmitting(false) 影响的是全局状态------如果此时评论 B 正在提交,它的 isSubmitting 也会被改成 false,导致评论 B 的回复请求根本没发出去。

第二版 :引入 submittingRef 解决闭包过期,但底层问题还在------共享状态。

第三版 :把 render() 函数拆成独立的 CommentItem 组件。

typescript 复制代码
// 每个 CommentItem 封装自己的回复状态
function CommentItem({ 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 次)。

排查过程

  1. docker ps → 后端容器 Up 6 seconds (healthy),过 10 秒再看又是 Up 3 seconds → 容器在不断重启
  2. docker logs → 看到 Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
  3. 但这不是根因------哪个正常请求会重复发响应?
  4. 继续翻日志 → 发现 ScrapingDetection 的 429 响应先发出去了,然后 HttpExceptionFilter 又尝试 response.json(),因为响应已经发出去了,response.headersSent === true,直接抛未捕获的 ERR_HTTP_HEADERS_SENT,Node.js 进程崩溃

崩溃链

ini 复制代码
搜索爬虫快速请求 → ScrapingDetection 触发 → 返回 429(响应已发送)
→ NestJS 异常处理 → HttpExceptionFilter 尝试 response.json()
→ headersSent = true → 抛 ERR_HTTP_HEADERS_SENT → 未捕获!
→ Node.js 进程 exit(1) → Docker restart: always → 重启
→ 重启后爬虫又来 → 再次崩溃 → 无限循环

修复:3 行代码。

typescript 复制代码
// http-exception.filter.ts
if (response.headersSent) {
  return; // 响应已经被拦截器发过了,别再发一次
}
response.status(status).json({...});

这个 Bug 教会我一件事:永远在全局异常过滤器里检查 headersSent 你的拦截器可能在过滤器之前就发送了响应。


6. Nginx 301 陷阱:一个消失了 3 天的 POST 请求

这是整个项目最诡异的 Bug------它让我困惑了整整 3 天。

现象:点击「发布帖子」按钮,前端显示「发布成功!」,但数据库里没有新帖子。后端日志里没有任何 POST 请求记录。

排查

  1. 检查 CreatePageContent.tsxapi.post('/posts', payload) 确实调了
  2. 检查 api.tsfetch('/api/posts', { method: 'POST' }) 没问题
  3. 在浏览器 DevTools Console 直接调 fetch('/api/posts', { method: 'POST' })能创建帖子!
  4. 在 React 代码里加 console.logapi.post 确实执行了
  5. 在浏览器 Network 面板看 → 只有 GET 请求,没有 POST!
  6. 拦截 window.fetch有 POST!但 URL 是 /api/posts/(带斜杠),状态码 301 → 302 → 请求体丢失

根因:Nginx 配置里:

bash 复制代码
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 与反爬中间件的内战

现象:某天开始,首页「热门问题」和「精华推荐」区域不显示任何帖子,显示「暂无热门内容」。

排查

  1. 后端 API 正常 → 能返回热门帖子
  2. 前端 DevTools Network → 首页根本没发 GET /api/posts/hot 请求
  3. 但 Next.js 是 SSR 的,数据在服务端拉 → 前端不显示代表 SSR 失败了
  4. 进前端容器 wget http://backend:4000/api/posts/hot403 Forbidden!

根因 :前端 SSR 的 Node.js 原生 fetch() 不带浏览器 User-Agent。后端反爬中间件看到缺失或非浏览器的 UA,直接返回 403。Next.js SSR 在请求失败时用 Promise.allSettled 吞了错误,返回空数组,页面渲染空状态。

修复

  1. 前端 api.ts 里给 SSR 请求加 User-Agent: Zhiqu/1.0 (internal-ssr)
  2. 后端反爬中间件 + 反爬拦截器对 Docker 内网 IP(172.17.0.0/16, 172.18.0.0/16)白名单放行

教训:如果你同时有 SSR 和反爬措施,它们会打架。SSR 的请求从服务端发出,它们不是浏览器。你的反爬规则必须给这些内部请求留白名单。按 IP 白名单比按 UA 白名单更安全------UA 任何人都能伪造,但 Docker 内网 IP 不能。


8. SEO:给搜索引擎写代码

写了一个月代码,发现被搜索引擎「看不见」。这是独立开发者最容易忽视的一环。

JSON-LD 结构化数据

json 复制代码
{
  "@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.ac vs zhiqu.acnot_same_site
  • 百度推送有每日配额(免费新站约 10 条/天)
  • HTTPS 不支持,只能用 HTTP

修了 5 次才通。第一次看到 {"success":7,"remain":2} 的时候,比代码跑通还激动。

Sitemap

Next.js 的 sitemap.xml 配置很简单但容易忘:每发布一篇新博客,确认 slug 后手动更新 lastmod。或者写个脚本自动生成。我没写脚本,每次手动改------早晚会忘。


9. 监控:从零到 Grafana Alloy

前 25 天我没有任何监控。每次服务挂了,靠用户告诉我。

第 26 天,配了 Grafana Cloud + Alloy:

bash 复制代码
# 一条命令装好
ARCH="amd64" GCLOUD_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.tsx,700 行。所有功能------统计、用户管理、内容管理、SEO 推送、系统设置------全在一个页面里。

v2(标签页切换) :拆成 4 个 tab。统计、用户、内容、设置。每个 tab 还是在这个文件里,靠 activeTab state 切换。

v3(多页面 + 独立 hooks):56 个文件变更。每个模块独立页面 + 独立 hook。Dashboard、Users、Posts、AI、SEO、Audit、Reports、Sessions、Settings------各走各的路由。

为什么要迭代三次? 因为每次迭代都是「当前代码太乱了,忍不了才改」。如果一开始就知道要拆成多页面,v1 就不会写成一个文件。

但如果一开始就设计成多页面架构,又会过度设计------因为前两周根本没那么多管理功能。这个矛盾没法完美解决。折中方案:前两周单页面快速迭代,功能稳定后立刻拆。


11. 成本清单

项目 月费
Hetzner CX22 (2C4G/50G) €4.51
Cloudflare CDN + DNS 免费
AI API (DeepSeek + Claude) ~$35
阿里云 OSS (图片存储) ~¥5
Resend (邮件) 免费额度
Grafana Cloud 免费额度
UptimeRobot 免费额度
合计 ~¥75/月

一个月 75 块钱,能跑一个全栈 AI 社区。独立开发者的时代,是真的来了。


12. 如果重来一次

  1. 第一天就配监控和告警。 不是第 26 天。UptimeRobot 5 分钟搞定。
  2. Prisma Schema 先想清楚再动手。 5 次 migration 里有 3 次是因为「后面发现需要这个字段」。如果第一天就把 searchVectormessagePermissionisFeatured 都考虑进去,后面不用反复迁移。
  3. 管理后台从第二天就开始用。 我前两周靠直接查数据库管理内容。效率极低。一个简单的 CRUD 表格比任何 SQL 命令都管用。
  4. 安全中间件留白名单。 反爬 UA 中间件、反爬拦截器、Nginx 限流------这些都应该第一天就留好内部网络的白名单。不然你在排查「为什么 SSR 拉不到数据」时,会盯着 403 Forbidden 怀疑人生。
  5. 百度站长平台先把站点验证和领域设置做完再发帖。 我发了 22 篇帖子才通百度推送,前面 22 篇要等每天 10 条的配额慢慢补推。
  6. Docker 构建缓存要定期清理。 30 天累计 32GB,不清理的话一个月就能占满磁盘。

30 天前,我在 VS Code 里按 mkdir zhiqu。30 天后,有人在这里写博客、提问、收藏、点赞。有人搜索「DeepSeek 429」从 Google 点进来。有人在凌晨三点给我的帖子点了个赞。

独立开发最好的部分不是代码跑通了。是你造的东西有人在用。


本文所有代码和架构已在 志趣社区 zhiqu.ac 生产环境运行。如果你也在做独立开发,欢迎来社区聊聊你踩过的坑。

相关推荐
咖啡八杯1 小时前
GoF设计模式——命令模式
java·设计模式·架构
candyTong2 小时前
阿里开源 AI Code Review 工具:ocr review 的执行链路解析
javascript·后端·架构
doiito16 小时前
【Agent Harness】TPS的“自工程完结”教会了我一件事:别把Bug留给下一道工序
架构·rust
烬羽17 小时前
中英文 token 数量差一倍?两段 JS 代码搞懂 LLM 底层是怎么"读"文字的
javascript·程序员·架构
白鲸开源19 小时前
一文读懂DolphinScheduler插件机制:如何轻松扩展任务类型与数据源
java·架构·github
棒槌开发师19 小时前
动态组件设计(elpis)
架构
得物技术1 天前
从表单到 Agent:得物社区活动搭建的 AI 实践之路
人工智能·架构·agent
Ausra无忧1 天前
记录在公司把单服务器升级成多服务器架构流程
前端·后端·架构
不好听6131 天前
拆解 LLM Tool Use 的完整机制:从缸中大脑到 Agent 觉醒
架构·llm·agent