土拨鼠日志:灯灭不掉、秒表归零、图片传坏,病根是同一个
今天修了三件看起来毫不相干的毛病:一个关不掉的黄灯、一个永远从小数字开始数的秒表、一张上传成功却「格式不支持」的图片。三件事追到底,病根是同一个——同一件事,系统里存了好几份说法,而且谁也不服谁。这篇就按这三条线讲清楚,顺便说说怎么自查。
一、那盏关不掉的灯:状态没有唯一落盘的地方
现象很日常:一个会话挂在那儿等确认,你看完了、觉得这活已经完了,不想再回一句话让它知道。于是那盏黄灯就一直亮着,手机亮、电脑也亮,后台还可能隔一会儿又把它「复活」一次。
做法上加了一个「结束这一轮」的入口,点两下确认(防误触),真的停掉这一轮、把后台的重试票一并撤掉。但真正的关键不在按钮,在结案这件事被记在哪儿:
- 结案结果落到服务端一份持久记录里,手机、电脑都去读它,而不是各记各的;
- 手机点了结束,电脑那边的灯下一轮同步就该灭;电脑点了,手机同样认;
- 结案之后想继续,直接发新消息自动复工——封条要能被新消息解开,否则用户会以为坏了。
一句话:凡是两个设备都要看的状态,就必须有一份落盘的唯一真相,两边只是它的显示器。让两边各自维护一份「我觉得他结案了」,迟早各说各话。
二、秒表从小数字重数:真相存在了页面变量里
第二个毛病更气人:明明已经等了十几分钟,屏幕上却显示「0 分 26 秒」,看着像刚开工,只好打一句「继续」催它——其实它一直在干活。
原因很土:这个计时器的起点存在页面变量里。页面一刷新、服务一重启,变量就没了,秒表乖乖从零开始。用户看到的是「刚开工」,真相是「已经跑了十五分钟」。
改法也没什么花活:起点改由服务端记录,前端每次只负责「现在减去起点」,刷新、重连、重启都不再归零。同一条线上还补了一件事:工具在后台跑长命令时,以前只在开头报一次「正在跑」,之后就长时间没声音,用户只能干等着猜是不是卡死;现在每隔几分钟播报一次「还在跑,已 X 分钟,没卡死」。
这条经验可以推广得很远:任何「过了多久」「跑了几步」的数字,都别存在会消失的地方。存前端变量=骗人,存服务端=可信。
三、图片传上去就坏:判据信了后缀,没看文件头
第三件是最典型的「看起来不可能」:图片上传显示成功,接下来每一步都报「图片格式不支持」。图生视频、智能抠图、修图、扩图,全都在同一处倒下。
挖下去发现,上传那一步把图片数据解码错了,存到服务器上的是一个内容已经乱掉的文件——后缀还叫 .jpg,里面却不是那张图。下游的模型当然认不出来。
改法只有一句:别再信后缀和文件名,去读文件头(前几个字节的魔数)判断真实格式,然后按真实格式存。验收方式是逐字节比对:存下来的文件和原图必须完全一致,一个字节都不能差。改完以后,从上传到出图、出视频整条链路一次跑通。
- 后缀是用户和浏览器告诉你的,文件头是文件自己说的,后者才可信;
- 一次解码错,会让所有依赖它的功能一起报错,所以修要修在公共入口,不是每个功能各修一遍;
- 验收别只看「有没有报错」,要比「存下来的东西对不对」。
四、顺手修的那点小事,其实也关于「以谁为准」
同一批改动里还有几个手机端的排版问题:卡片网格超宽、右边一列被裁掉半个、失败原因被省略号截成半句、输入框字号太小导致苹果手机点进去自动放大页面。这些听着琐碎,但判断标准同样是「以真实环境为准」——不是在代码里看着对,而是在390 像素宽的真机屏上截图核对:两列是不是都完整、页面有没有横向溢出、报错有没有换行显示全。
顺带一个细节:输入框字号提到 16 像素,苹果手机就不会强制放大页面。这类「不是 bug 的 bug」,只能靠真机看,代码里看不出来。
五、这三件事在土拨鼠里长什么样
- 手机和电脑看同一个会话,灯色、状态、是否结案都一致,任一端结束另一端立刻同步;
- 「处理中」的时间以服务端真实开工时刻为准,刷新、重连、重启都不归零,长任务还会定期报「还在跑」;
- 带图的创作走同一套上传与解码,一处修好,出图、图生视频、抠图、修图一起恢复。
想看这些能力整体是怎么组织的,可以翻 功能页;如果你在算「让它替自己干这些活到底值不值」,定价页 有按量的对照;想直接拿一件真事试试,下载 装一个,比看十篇说明都直观。
六、留三条自查,给你自己的项目用
- 这个状态有几个设备、几个进程要看?如果有两个以上,它落盘在哪儿?谁是唯一真相?
- 这个数字(耗时、进度、剩余)存在哪儿?页面一刷新它还在吗?
- 这段校验靠什么判断?文件名、后缀、还是文件自己说的那几个字节?
三条都答得出来,很多「玄学 bug」其实一开始就不会发生。系统里最贵的不是代码写得多,而是同一件事有两份互相打架的答案。
土拨鼠 Marmot