2097 字
约 6 分钟
3
架构决策的思维框架

架构决策的思维框架

面向 VOZEB PRO。不是 mermaid 图怎么画,而是接到一个需求时,按什么顺序问,才不会把实时接口写成后台任务,也不会把该落库的状态塞进进程内存。


0. 什么时候用这套框架

适合:

  • ✅ 新产品或新模块还在验证,默认倾向「先放进现有盒子」
  • ✅ 纠结一段逻辑该写在 Route、service,还是扔给 Worker
  • ✅ 有人提议「拆成微服务 / 上 Redis 队列 / 换 Prisma」时,要先算代价
  • ✅ 长任务(视频、Agent、退款)和短请求(登录、列表、保存提示词)混在同一需求里

不必硬套:

  • ⚠️ 改文案、改主题 token、改一个 antd 表单校验
  • ⚠️ 决策已经被 AGENTS.md 写死(例如密钥不准进浏览器)
  • ⚠️ 团队已经拆仓,你只是在其中一个服务里修 bug

在 VOZEB PRO 中的定位: 默认答案是整体式全栈。旁路 Worker 是对长任务的补丁,不是第二套后端。拆服务是最后一步,不是第一步。


1. 先记住仓库已经替你选过的默认值

做新决策前,把这些当成约束,而不是选项:

  1. 一个 web/ 进程承担页面、API、业务、SQL、出站 AI。
  2. Worker 只当 HTTP 客户端:同一镜像,打本站 maintenance Route,不直连数据库,不直连上游。
  3. 状态以 PostgreSQL(或文件回退)为准。进程内 Map、Zustand、React state 都不是产品真相。
  4. 换依赖先问代价。 pg 换 ORM、Next 换成纯前端 + 独立 API,都等于重接半个仓。

默认值的含义:你的框架是在这个盒子里做加减法,不是每次从白纸重选架构。


2. 三步思考法

接到需求,按这个顺序问,不要跳。

Step 1:用户关掉标签后,这件事还要不要继续?

关掉标签之后 放哪里 本仓库例子
不必继续,下一次请求重来即可 主进程同步请求:Route → service → 立刻返回 登录、保存提示词、后台改开关、读列表
必须继续,且不能再 create 一次上游 先落 generation_tasks(或同类持久行),再由页面 after() Worker 续跑同一行 图 / 视频 / 音频 / Agent / 短剧合成
必须继续,但是计费或清理,不是生成 同样走 maintenance,但用专用 lane / 专用 Route billing-refunds、订单过期、邀请结算

判断口诀:内存里的 await 扛不住关标签、热更新、换容器副本。 扛不住就落库。

Step 2:这段逻辑是「响应一次请求」还是「推进一行状态」?

  • 响应一次请求:入参、鉴权、校验、写一条业务记录、返回 { code, data, msg }。留在主进程。
  • 推进一行状态:已有 upstream.id 就只 poll;没有且从未提交过才 create 一次;不确定就 needs_review。这是任务底座,不是普通 CRUD。

不要用「这个函数也是 async」当分类标准。分类标准是:有没有一张别人能认领的任务行。

Step 3:还要不要离开这个盒子?

只有同时满足,才值得讨论拆进程 / 拆仓:

  • 某类负载已经把整个 web 进程拖死,而 Worker 续取也救不了(例如 CPU 密集转码占满事件循环)
  • 团队边界已经按发布节奏拆开,两边需要独立回滚
  • 你算过:Cookie、SSE、standalone、Worker origin、维护令牌全部重接的成本,仍然低于继续挤在一起

否则继续整体式。旁路 Worker 已经是这个阶段对「单点拖累」的标准答案。


3. 决策表:常见需求往哪放

需求 主进程同步 落任务行 + Worker 续取 先别做的事
后台表格筛选、表单保存 不要为此起队列
用户点「生成」后马上看到占位 ✓ 建任务、扣积分、返回 recordId ✓ 后续 poll / 落盘 不要在 Route 里 await 完整视频
用户点重试 ✓ 校验并创建新的上游尝试身份 ✓ 沿用同一记录槽 不要按提示词文本去重合并
关页面后视频仍要出片 ✓ 认领同一行,只许 poll 不要再 POST 一次 create
生成失败退积分 可以在 runtime 终态触发 ✓ 退款 lane 扫未完成退款 不要让 Worker 自己改钱包表
「先用 Redis 做任务队列」 先问:现有表 + lease 是否已够 不要为了队列而加基础设施
「Worker 里直接 pg.query 禁止。业务必须回 maintenance Route

4. 本仓库里已经做过的示范决策

对照源码看,比背框架快。

长生成必须可租赁。
generation-task-store + claimDueGenerationTasksFOR UPDATE SKIP LOCKED)+ leaseUntil。多个 lane / 多个 Worker 不会抢同一行。过期租约谁都能续领。这是在「不拆服务」的前提下解决多实例。

页面和 Worker 共用恢复函数。
runGenerationTaskRecoveryBatch 只有一份。页面 after() 是顺手推一把;Worker 是关页面之后的保底。决策含义:恢复逻辑属于领域层,不属于「Worker 脚本」。

Worker 脚本故意很瘦。
generation-worker.mjs:维护令牌、origin、心跳、N 条 generation lane、1 条 refund lane、失败指数退避。状态机、runtime、SQL 全在 web 进程。决策含义:再加一种后台活,优先加一条 maintenance Route + 一条 lane,而不是把脚本写成第二套后端。

进程内并发只是软限制。
withGenerationConcurrencyLimit 用进程内 Map 排队。多实例时每进程一份,硬约束仍是库里的 ACTIVE 行。决策含义:单机够用先别上分布式锁中间件。

人审不占生成槽。
needs_review 不进 ACTIVE_CONCURRENCY_PHASES。决策含义:产品规则(别卡死用户)优先于「所有未完成都算占用」。


5. 换栈 / 加中间件时怎么问代价

开口之前列出受影响的边界,不要只说「更现代」。

提议 先列出的边界
pg → Prisma / Drizzle web/src/lib/server/database/ 全部 Repository、文件 Provider 回退还要不要
独立 Nest / Spring 后端 Cookie 名与策略、SSE、standalone、VOZEB_PRO_WORKER_API_ORIGIN、维护令牌
Redis / Bull 队列 现有 lease + next_poll_at 哪一条不够;多一处状态怎么和任务表对齐
Worker 直连上游 密钥封装、SSRF、一次 create 多次 poll 的不变量谁来守
视频超时只改 AbortSignal 同时核对 Route maxDuration、undici headersTimeout/bodyTimeout、模型策略

docs/learning/01-技术栈.md 的「换它代价」列就是给这一步用的。


6. 写补丁前的五秒清单

  1. 用户关标签,这件事死掉是否可接受?
  2. 权威状态在哪张表 / 哪一行?没有行就先设计行,再写 UI。
  3. 推进状态的代码能不能让页面和 Worker 共用?不能,说明你写进了脚本或组件。
  4. 失败时会不会二次 create 上游?会,就还没做成任务底座。
  5. 这个改动有没有逼你违反「密钥不进浏览器 / Route 不堆 SQL / Worker 不直连库」?有,方案重做。

7. 读完能指挥自己(或 AI)做什么

  • 「视频超时不要只改 AbortSignal。同时核对 Route maxDuration、undici 超时、模型策略是否覆盖同一窗口。」
  • 「续取逻辑加在 generation-task-recovery-service / maintenance Route,不要在 generation-worker.mjs 里连数据库或上游。」
  • 「先回答:这是同步响应,还是推进已有任务行。再选文件。」

相关文档

架构决策的思维框架
http://clxhxhhr.top/posts/520/
作者
clxstart
发布于
2026-09-07
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。