分层修复
用户报告说视频生成返回 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
定价界面优化的三层方法
数据诚实、决策引导、视觉精调——三层递进的优化方法。从移除虚假功能到帮助用户选择,再到像素级修复,每一步都在解决真实问题
用 AI Agents 加速测试环境配置
测试环境的配置是重复的琐事。环境变量、测试数据库、配置文件——这些步骤消耗时间但不产生直接价值。AI agents 改变了这个等式。
离屏渲染:照片捕获为什么需要独立的 canvas
实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。
集中式配置:让 Reddit 组件脱离重复泥潭
当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。
在Claude Code中写单元测试:简单高效的实践
测试不是负担,是对话。Claude Code改变了测试的成本结构,让测试回归本质:验证行为,而非追求覆盖率。
让文档跟着代码走
文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。
双重导出管道的架构选择
在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。
Purikura的页面系统
通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。
重复数据的迁移实践:从 N 个文件到 1 个真相源
当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。
Git Hooks 驱动的文档同步
文档不会自动更新,除非你让它自动更新。Git Hooks 是最接近代码变更的触发点,也是对抗文档腐烂最有效的位置。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
使用Jest或Vitest作为测试框架有什么区别?
测试框架的选择不是功能列表的比较,而是关于工具哲学的选择。Jest代表完整性,Vitest代表原生性。
多厂商 AI 调度:统一混乱的供应商生态
当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。
分布式 Workers 的解耦设计
通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。
查询先于假设
数据库迁移后,所有功能失效。问题不在迁移本身,而在假设。真相只存在于查询结果中。
Studio 前端架构:从画布到组件的设计思考
深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验
Studio 系统架构:从状态机到端到端流程
深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑
Context 驱动的认证状态管理
认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。
缺少Jest依赖时的测试选择
测试不一定需要框架。有时候最简单的工具已经足够。Node.js内置的assert模块和基础测试能力,往往比庞大的测试框架更清晰。
第三方回调的状态映射完整性
KIE.AI 视频生成的三个修复揭示了一个本质问题:回调不只是接收结果,是建立状态映射。没有 vendor_task_id,系统就失去了追溯能力。
统一积分系统的设计实践
从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计