分层修复

3 min read
Zekari
工程实践问题修复优先级Purikura 项目

用户报告说视频生成返回 402 错误。402 的语义是"需要付费"。按照字面意思,应该是积分不足。

但这只是表象。

检查日志,发现部分请求确实是积分不足,但另一部分请求的用户明明有足够积分,却仍然收到 402。

真正的问题不在积分系统,而在模型配置。某些 AI 模型(Flux, Midjourney)没有配置定价信息。代码在计算成本时找不到配置,返回了一个 missing 标记。但错误处理逻辑看到成本计算结果存在,就假设一切正常,继续往下执行。最终在扣费环节发现成本为 0,触发了 402 错误。

错误的信号传递链断裂了。配置缺失(系统问题)被误报为积分不足(用户问题)。

这个问题需要修复。但如何修复?

分层的必要性

面对这类生产问题,直觉反应是"全面修复"。列出所有可能的隐患,逐一解决。

但这不现实。

资源有限。时间有限。更重要的是,理解有限。

当你刚发现问题时,你对系统的理解是不完整的。你看到了 402 错误,看到了配置缺失,但你不知道还有多少类似的坑。你不知道代码里有多少地方假设配置总是存在。你不知道用户输入有多少种边界情况会触发问题。

在这种不确定性下,"全面修复"是危险的。你可能改了 10 个地方,引入了 3 个新 bug。你可能花了 3 天时间,最后发现根本问题在别的地方。

分层修复不是保守,而是务实。它承认不确定性,承认资源约束,承认理解的局限。

P0 解决的是"系统现在不能用"。P1 解决的是"系统能用但不安全"。P2 解决的是"系统安全但不够好"。

每一层都有其价值。但在资源有限时,必须有先后。

P0:止血

第一步是让系统从混乱回到可控。

配置缺失的模型必须立即禁用。不是"修复配置",而是"禁用模型"。因为修复配置需要业务团队确认定价,需要财务审核,需要时间。而用户现在正在遇到 402 错误。

禁用模型是最快的止血方法。执行两条 SQL 语句,30 秒完成。用户不能调用这些模型了,但至少不会收到错误的 402 提示。

第二步是修正错误信号。

代码在发现 cost.source === 'missing' 时,不应该继续执行,而应该立即返回 500 错误,并发送告警。500 的语义是"服务器内部错误",这才是准确的描述。配置缺失是系统问题,不是用户问题。

这个修改很小,只有几行代码。但影响深远。它让错误信号变得精准。当运维团队看到 500 错误时,他们知道要检查系统配置。当用户看到提示时,他们知道这不是自己的问题,而是服务暂时不可用。

参考 let-errors-surface 了解如何让错误浮出水面,而非掩盖或误报。

💡 Click the maximize icon to view in fullscreen

P0 的目标不是"彻底解决问题",而是"让系统可控"。禁用模型让用户不再遇到错误。修正信号让团队能够正确诊断问题。

这两个改动花了不到 1 小时。部署上线后,402 误报停止了。系统从混乱回到了可控。

P1:加固

系统可控之后,下一步是安全加固。

P0 解决了配置缺失的问题,但没有解决恶意输入的问题。

视频生成 API 接受用户的 prompt(提示词)和图片。如果用户在 prompt 中注入 <script> 标签,或者上传一个 50MB 的文件伪装成图片,会发生什么?

代码没有验证。它假设输入总是合法的。这在内部工具中可能没问题,但在面向用户的 API 中很危险。

P1 加入了输入验证:

  • Prompt 长度限制 2000 字符
  • XSS 防护:移除 <script>, <iframe> 等危险模式
  • 图片大小限制 10MB
  • 图片格式验证:检查文件魔数(JPEG, PNG, WebP)
  • 参数范围验证:duration 1-60 秒,num_frames 1-500 帧

这些验证不复杂,但很重要。它们把不安全的输入拦截在系统边界之外。攻击者无法注入脚本。用户无法上传超大文件压垮服务器。

第二个 P1 修复是前端防重复提交。

用户在等待视频生成时,如果不耐烦,可能会快速点击多次"生成"按钮。每次点击都会发送一个新请求,扣除一次积分。这不是恶意行为,只是焦虑和不耐烦。

前端加入了状态锁:isSubmitting 标记防止并发提交。加入了冷却期:2 秒强制间隔。加入了友好提示:"请等待 X 秒后再试。"

这不是技术难题,而是用户体验细节。但这些细节决定了用户是否信任你的系统。

输入验证代码片段 (workers/src/api-gateway.js):

// Prompt 验证
if (prompt.length > 2000) {
  return new Response('Prompt too long', { status: 400 });
}

// XSS 防护
const dangerousPatterns = [
  /<script[^>]*>.*?<\/script>/gi,
  /<iframe[^>]*>.*?<\/iframe>/gi,
  /javascript:/gi,
  /on\w+\s*=/gi
];
for (const pattern of dangerousPatterns) {
  if (pattern.test(prompt)) {
    return new Response('Invalid prompt', { status: 400 });
  }
}

// 图片验证
if (imageSize > 10 * 1024 * 1024) {
  return new Response('Image too large', { status: 400 });
}

// 文件魔数检查
const magicNumbers = {
  'ffd8ff': 'JPEG',
  '89504e47': 'PNG',
  '52494646': 'WebP'
};
const header = imageBuffer.slice(0, 4).toString('hex');
if (!Object.keys(magicNumbers).some(magic => header.startsWith(magic))) {
  return new Response('Invalid image format', { status: 400 });
}

防重复提交代码片段 (components/VideoCreator.tsx):

const [isSubmitting, setIsSubmitting] = useState(false);
const [lastSubmitTime, setLastSubmitTime] = useState(0);

const handleSubmit = async () => {
  const now = Date.now();
  if (now - lastSubmitTime < 2000) {
    const waitTime = Math.ceil((2000 - (now - lastSubmitTime)) / 1000);
    toast.error(`Please wait ${waitTime} seconds`);
    return;
  }

  if (isSubmitting) {
    toast.warning('Already submitting...');
    return;
  }

  setIsSubmitting(true);
  setLastSubmitTime(now);

  try {
    await createVideo();
  } finally {
    setIsSubmitting(false);
  }
};

这些代码不复杂,但有效。它们把系统从"能用"推进到"安全"。

P1 的修复花了约 3 小时。这些改动让系统变得更健壮。即使遇到恶意输入或用户误操作,系统也能正确处理。

P2:优化

P2 是长期优化。它解决的不是"能不能用"或"安不安全",而是"好不好用"。

计划中的 P2 包括:

  • 模型配置缓存(5 分钟 TTL),减少数据库查询
  • Cost calculation debounce(500ms),避免频繁计算
  • Webhook 签名验证,提升第三方集成安全性
  • 流式文件下载,避免大文件阻塞 Worker

这些优化都有价值。缓存能提升性能。Debounce 能减少无效计算。Webhook 验证能防止伪造请求。流式下载能改善响应速度。

但它们不紧急。

系统现在可用,安全,只是不够快、不够优雅。P2 可以等。等到 P0 和 P1 验证通过,等到团队有更多资源,等到有更清晰的性能瓶颈数据。

这不是拖延,而是优先级。P2 的价值在于"更好",但 P0 和 P1 的价值在于"可用"和"安全"。在资源有限时,后者永远优先。

参考 query-before-assumption 了解如何在不确定性下做决策。P2 的延后不是因为不重要,而是因为需要更多数据来验证假设。

验证的困境

P0 和P1 修复完成。代码部署上线。Worker 启动正常。数据库配置验证通过。Telegram 告警配置就绪。

技术上,一切就绪。

但我们不知道修复是否真的有效。

查询数据库任务表,最近 7 天只有 22 个视频生成任务。最后一个任务是 3 天前。最近 24 小时,0 个新任务。

生产环境处于低流量状态。没有用户流量,就无法验证修复效果。402 错误率下降了吗?不知道,因为没有新请求。输入验证生效了吗?不知道,因为没有用户尝试恶意输入。

这是工程实践中常见的困境。你修复了问题,部署了代码,但环境不配合。你无法立即看到结果。你只能等待。

这种等待不舒服。它让人怀疑:"我真的修好了吗?还是只是改了代码但没解决问题?"

但这种不确定性是正常的。生产环境不是测试环境。你无法控制用户流量。你无法强制用户发送请求来验证你的修复。

唯一能做的是准备好监控。当流量恢复时,立即观察关键指标:

  • 402 错误率
  • 500 错误率(应该能捕获配置错误)
  • Telegram 告警数量(应该能看到 missing cost 告警)
  • 输入验证拦截次数

监控计划已经就绪。现在只能等待用户流量恢复。

P1 的防重复提交功能修改了前端代码(VideoCreator.tsx),但这部分代码尚未部署到生产环境。

Worker 的修复(P0 错误处理、P1 输入验证)已经上线,但前端的防重复提交仍在开发分支。

这意味着用户仍然可能快速点击多次,导致重复扣费。后端虽然不会崩溃(因为加入了输入验证),但用户体验仍然不理想。

为什么前端没有部署?因为前端部署需要重新构建和发布,涉及更多流程。而 Worker 可以快速部署,只需要 wrangler deploy

这是一个权衡。优先部署后端修复(P0),让系统快速恢复可控。前端优化(P1)可以稍后部署。

但这也是一个教训:完整的修复需要前后端协同。只修复一端,问题只解决了一半。

承认局限是诚实的表现。部署成功不等于问题解决。代码修复不等于系统健康。你需要数据来验证假设。

但数据需要等待。

优先级的本质

P0, P1, P2 不只是标签。它们是决策框架。

P0 问的是:"如果不做这个,系统会崩溃吗?" P1 问的是:"如果不做这个,系统会有安全隐患吗?" P2 问的是:"如果不做这个,系统会不够优雅吗?"

前两个问题的答案是"是",那就必须做。第三个问题的答案是"是",那就可以做。

区别在于"必须"和"可以"。

在资源无限的理想世界里,所有问题都应该立即解决。P0, P1, P2 应该同时完成。

但现实世界资源有限。时间有限。理解有限。

分层修复不是妥协,而是适应现实。它承认你无法一次性解决所有问题,但你可以先解决最关键的问题。

它承认你的理解不完整,但你可以通过增量修复来逐步验证假设。

它承认部署成功不等于问题解决,但你可以通过监控来追踪真实效果。

优先级不是排序,而是在不确定性下的路径选择。每个层次都是一次赌注。P0 赌的是"禁用模型能止血"。P1 赌的是"输入验证能防御攻击"。P2 赌的是"缓存能提升性能"。

P0 和 P1 的赌注更确定,因为问题已经暴露。P2 的赌注更模糊,因为收益尚未量化。

所以 P0 先做,P1 次之,P2 最后。

最后

生产问题没有银弹。

你无法预见所有边界情况。你无法一次性写出完美的代码。你无法在部署前发现所有问题。

但你可以分层修复。

先止血,让系统从混乱回到可控。再加固,让系统从可控到安全。最后优化,让系统从安全到高效。

每一层都有其价值。但在资源有限时,必须有先后。

分层修复不是技巧,而是对现实的尊重。它尊重资源的有限性,尊重理解的局限性,尊重不确定性的存在。

P0 已经上线。P1 大部分完成。P2 在路上。

系统现在可控。等待用户流量恢复,验证修复效果。

这就是工程实践的真实面貌。不完美,但务实。

Related Posts

Articles you might also find interesting

Featured

定价界面优化的三层方法

4 min read

数据诚实、决策引导、视觉精调——三层递进的优化方法。从移除虚假功能到帮助用户选择,再到像素级修复,每一步都在解决真实问题

UI/UX定价策略
Read More

用 AI Agents 加速测试环境配置

3 min read

测试环境的配置是重复的琐事。环境变量、测试数据库、配置文件——这些步骤消耗时间但不产生直接价值。AI agents 改变了这个等式。

Claude Code测试
Read More

离屏渲染:照片捕获为什么需要独立的 canvas

2 min read

实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。

架构设计前端开发
Read More

集中式配置:让 Reddit 组件脱离重复泥潭

2 min read

当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。

架构设计React组件
Read More

在Claude Code中写单元测试:简单高效的实践

2 min read

测试不是负担,是对话。Claude Code改变了测试的成本结构,让测试回归本质:验证行为,而非追求覆盖率。

Claude Code测试
Read More

让文档跟着代码走

2 min read

文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。

文档软件工程
Read More

双重导出管道的架构选择

2 min read

在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。

架构设计图像导出
Read More

Purikura的页面系统

3 min read

通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。

架构设计React
Read More

重复数据的迁移实践:从 N 个文件到 1 个真相源

3 min read

当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。

架构设计配置管理
Read More

Git Hooks 驱动的文档同步

2 min read

文档不会自动更新,除非你让它自动更新。Git Hooks 是最接近代码变更的触发点,也是对抗文档腐烂最有效的位置。

Git自动化
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

使用Jest或Vitest作为测试框架有什么区别?

1 min read

测试框架的选择不是功能列表的比较,而是关于工具哲学的选择。Jest代表完整性,Vitest代表原生性。

测试工程实践
Read More

多厂商 AI 调度:统一混乱的供应商生态

3 min read

当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。

Purikura 项目系统架构
Read More

分布式 Workers 的解耦设计

3 min read

通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。

Purikura 项目系统架构
Read More

查询先于假设

3 min read

数据库迁移后,所有功能失效。问题不在迁移本身,而在假设。真相只存在于查询结果中。

数据库迁移
Read More

Studio 前端架构:从画布到组件的设计思考

3 min read

深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验

Purikura 项目前端架构
Read More

Studio 系统架构:从状态机到端到端流程

3 min read

深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑

Purikura 项目系统架构
Read More

Context 驱动的认证状态管理

3 min read

认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。

软件设计认证系统
Read More

缺少Jest依赖时的测试选择

4 min read

测试不一定需要框架。有时候最简单的工具已经足够。Node.js内置的assert模块和基础测试能力,往往比庞大的测试框架更清晰。

测试工程实践
Read More

第三方回调的状态映射完整性

5 min read

KIE.AI 视频生成的三个修复揭示了一个本质问题:回调不只是接收结果,是建立状态映射。没有 vendor_task_id,系统就失去了追溯能力。

Purikura 项目系统设计
Read More

统一积分系统的设计实践

2 min read

从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计

系统架构数据库设计
Read More