全栈开发的核心思维
面向 VOZEB PRO。讲的不是「前后端都会写一点」,而是这个仓库强制你建立的一种看法:一个 Web 产品就是一个盒子,盒子里同时装着页面、HTTP、业务、SQL 和出站调用。
0. 什么时候用这套思维
适合:
- ✅ 读这个仓库时,想知道为什么找不到独立的
backend/服务 - ✅ 自己要加功能:登录、列表、生成任务、后台表单,不知道该改哪一层
- ✅ 用 Next.js / Laravel / Rails / Django 这类「框架即应用服务器」的项目
- ✅ 产品还在验证,需要一个人把一条链路从头走到尾
不适合硬套:
- ⚠️ 已经按团队边界拆成多个可独立发布的服务
- ⚠️ 只改 CSS / 只改 SQL,不需要理解整条请求链
- ⚠️ 把「全栈」理解成「一个文件里既写 JSX 又写 SQL」——本项目恰恰禁止这么干
在 VOZEB PRO 中的定位: Next.js App Router 既是前端框架,也是应用服务器。浏览器、Route Handler、
lib/server、pg、上游模型 HTTP,默认都住在同一个web/进程里。这叫整体式全栈,不叫「没分层」。
1. 先纠正一个误会
「前后端不分离」说的是部署和进程,不是代码糊成一团。
| 你可能以为的 | 这个仓库实际是 |
|---|---|
一个 page.tsx 里直接查库、调 AI |
页面只渲染;请求走 services/api |
| 没有后端 | 后端就是 app/api/**/route.ts + lib/server |
| 全栈 = 不分层 | 分层很严,只是层与层在同一个进程、同一套 TypeScript 类型里 |
| 全栈 = 不需要 API | 有 API,契约是 { code, data, msg },只是调用方和实现方在同一个仓库 |
官方请求链可以收成一句:
页面 → services/api → Route Handler → Session → lib/server → Repository → { code, data, msg }
全栈思维要练的,是顺着这条链看完整件事,而不是在某一层里当「纯前端」或「纯后端」。
2. 核心公式
整体式全栈 = 一个进程里跑完一次用户动作所需的全部服务端工作。
一次「点生成」在盒子里会发生:
- 浏览器里的工作台收集提示词、尺寸、参考图
services/api发 JSON 请求(密钥不在浏览器)- Route 做鉴权、限流、入参映射,不写积分公式、不写 SQL
lib/server做领域校验、规划、建任务、扣积分- Repository 用参数化 SQL 读写 PostgreSQL(或文件 Provider 回退)
- 需要上游模型时,同一进程用 undici 出站;渠道密钥只在服务端解密
- 长任务落成
generation_tasks行,页面关掉后仍可由 Worker 续上
你要建立的直觉是:用户动作的权威状态在服务端,不在 Zustand,不在 localStorage。 客户端只持有主题、当前用户、公开配置这类瞬时状态。
3. 「一个盒子」到底共享了什么
同进程、同仓库带来的不是偷懒,是这些东西可以直接共用,而不用再开第二个服务:
- 类型:
web/src/lib/*.ts的领域契约前后端一起 import - 鉴权:用户 Cookie 和 Worker 维护令牌,都在 Route 进站时解析
- 数据库连接与仓储:只有一套
database/,没有「前端库 / 后端库」两套模型 - 任务恢复函数:页面
after()和 Worker 调用的是同一个runGenerationTaskRecoveryBatch - 部署单元:
web/打成 standalone 镜像;Worker 用同一镜像、另一条启动命令
换独立后端等于拆仓:鉴权 Cookie、SSE、standalone 启动、Worker 回调全要重接。这就是「换 Next 当纯前端」代价巨大的原因。
4. 全栈不等于越层
本项目用分层把「一个盒子」管住。读代码或写补丁时,先问自己在哪一层:
| 层 | 可以做 | 禁止做 |
|---|---|---|
| 页面 / hooks | 展示服务端状态、收集输入 | 直连数据库、上游、对象存储 |
services/api |
类型化 HTTP | 把 API Key 塞进用户 Store |
| Route | 鉴权、限流、调服务、映射响应 | 堆业务规则或 SQL |
lib/server |
校验、编排、计费、任务 runtime | 为了省事绕过 Repository 手写散落 SQL |
database/ |
参数化查询、事务、定向过滤 | 先读整表再在 Node 里筛 |
三条全站不变量,改任何层都不能破:
- 计费键由服务端生成,浏览器的
Idempotency-Key不能直接当流水号 - 媒体只存
storageKey,已有站内媒体必须复用 - 删除是聚合根事务,UI 写删除时服务端不得偷换成归档
能在一个进程里完成,不代表可以在一个文件里完成。
5. 对照本仓库,怎么练这个思维
拿一条真实路径走一遍,比背定义有用:
- 打开一个用户工作台页面(生图 / 视频 / Agent)
- 找到它调用的
web/src/services/api/*.ts - 跳到对应
web/src/app/api/**/route.ts,只看它做了哪四件事:入参、鉴权、调服务、返回 - 再进
web/src/lib/server/,看规则和任务是在哪落地的 - 最后看
web/src/lib/server/database/的 SQL 条件:有没有按用户 / 实体 / 时间窗查询
走完你会得到一种手感:前端看到的失败文案,往往是服务端已经裁决过的 msg;前端看到的「生成中」,往往对应库里的一行任务,而不是某个 React state。
再对比旁路 Worker:generation-worker.mjs 里没有业务状态机,只有 fetch 打本站 /api/maintenance/...。这会逼你分清:
- 同盒子 = 业务实现仍在
web进程 - 另一进程 = 只是多了一个会敲门的 HTTP 客户端
6. 写代码时的三问
- 这件事的权威结果应该写在哪? 库表 / 任务行 / 会话 Cookie,而不是组件 state。
- 我现在改的文件属于哪一层? 越层就停,把逻辑挪回该在的地方。
- 如果明天把页面删了,这个动作还能不能被 Worker 或另一个请求续上? 能,说明你把状态放对了;不能,说明你还在用内存当产品。
7. 读完能指挥自己(或 AI)做什么
- 「给图片工作台加查询参数:只改页面 hook、
services/api、Route 入参映射、config 校验。不要动表结构,除非参数要持久化。」 - 「
page.tsx里出现postgresQuery或上游 Base URL,视为失败。」 - 「先画这条请求穿过哪几层,再写补丁。」
相关文档
- 架构决策的思维框架
- 旁路 Worker 的本质
- Ant Design 6 入门
- Tailwind CSS 4 入门
docs/learning/01-技术栈.md— 每块技术站在哪一层docs/learning/02-分层架构.md— 层职责与跨层禁令docs/learning/CH-01-请求进站.md— 把进站规则走成血肉StudyVault/01-架构/系统架构.md— 分层单体总图