状态字段长出了三个头(以及我们是如何修复的)
I’m happy to help translate the article, but I don’t have the text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line exactly as you provided and preserve all formatting, markdown, and technical terms in the translation.
原罪
当我第一次构建导入/导出配置时,状态一目了然:
type ImportConfig struct {
Status string `json:"status"` // "draft" | "active" | "paused"
}
简洁。简单。完成。
随后用户开始运行导入。我需要知道上一次运行是成功还是失败:
type ImportConfig struct {
Status string `json:"status"` // "draft" | "active" | "paused"
LastRunStatus string `json:"lastRunStatus"` // "success" | "failed" | "running"
}
仍然可控。两个字段,两种关注点。
接着我构建了向导。用户需要在流程中途保存进度,而不产生损坏的配置。轻松修复:
type ImportConfig struct {
Status string `json:"status"`
LastRunStatus string `json:"lastRunStatus"`
IsDraft bool `json:"isDraft"` // wizard in progress
}
三个头。一个怪物。
组合噩梦

下面是三个字段的问题:它们会形成一个状态矩阵。
Status: draft | active | paused | disabled | completed
LastRunStatus: success | failed | running | (empty)
IsDraft: true | false
这就是 5 × 4 × 2 = 40 种可能的状态。其中大多数是毫无意义的。比如 status=active, lastRunStatus=running, isDraft=true 到底表示什么?
前端有这么段代码:
function getDisplayStatus(config: ImportConfig): string {
if (config.isDraft) return 'draft';
if (config.lastRunStatus === 'running') return 'running';
if (config.status === 'paused') return 'paused';
if (config.lastRunStatus === 'failed') return 'failed';
if (config.status === 'active') return 'ready';
return 'draft'; // ¯\_(ツ)_/¯
}
这个函数是错误的,只是我不知道到底哪些情况是错的。
认知时刻
我在添加新功能时意识到自己无法回答一个基本问题:
“这个导入可以直接运行吗?”
要得到答案,需要检查三个字段,理解它们的优先级,并且希望自己的逻辑是正确的。
就在那一刻,我明白了:我并没有对状态进行建模。我已经积累了症状。
修复:一字段统治一切
重构的概念很简单:
之前: 3 个字段追踪相互重叠的关注点
之后: 1 个字段拥有 5 种互斥状态
const (
StatusDraft = "draft" // 向导未完成,缺少必填字段
StatusReady = "ready" // 配置完整,可运行
StatusRunning = "running" // 正在执行
StatusPaused = "paused" // 用户已暂停执行
StatusFailed = "failed" // 最近一次运行失败,需要处理
)
type ImportConfig struct {
Status string `json:"status"` // 以上任意一个,仅此而已。
}
没有矩阵。没有优先级。没有猜测。
“这个导入准备好运行了吗?” → config.Status == StatusReady
但是等等,向导怎么办?
isDraft 字段之所以存在,是因为向导需要保存部分进度。删除它意味着要解决另一个问题:向导状态存放在哪里?
答案:在 DraftData 字段中,仅当 Status == StatusDraft 时才存在:
type ImportConfig struct {
Status string `json:"status"`
DraftData *DraftData `json:"draftData,omitempty"` // nil unless Status=draft
}
type DraftData struct {
CurrentStep string `json:"currentStep"`
PendingSchema *PendingSchema `json:"pendingSchema,omitempty"`
PendingDataType *PendingDataType `json:"pendingDataType,omitempty"`
}
关键洞察:待处理资源存放在草稿数据中,而不在真实表中。当你在向导中途创建模式时,该模式尚未真正存在。它是存储在 DraftData 中的待处理模式。只有在你完成确认后,它才会变为真实的。这样就不会出现因放弃向导而产生的孤立模式。
损害报告
修复此更改涉及:
- 193 个文件
- 11,786 行新增
- 4,179 行删除
- 后端模型、服务、处理程序
- 前端类型、钩子、向导
- 两端的测试
值得吗?getDisplayStatus 函数现在是:
function getDisplayStatus(config: ImportConfig): string {
return config.status;
}
是的,值得。
教训
- 状态字段会成倍增长。 一个变成两个变成三个。每一次添加看似微小。复杂度是组合性的。
- “这是什么状态?”应该是显而易见的。 如果你需要流程图,那说明你有建模问题。
- 待定资源不是真正的资源。 向导状态是草稿数据,而不是部分创建的实体。
- 大型重构其实是许多小改动的集合。 193 个文件听起来很吓人。实际上只是“更新 Status 常量” × 193。
构建 Flywheel —— 为初创公司提供的数据管道。如果你曾经看到一个“简单”的字段长出三个头,我很想听听你的战斗故事。