ARTICLE · 1123820
从一张被改乱的 Excel 开始.
涵得是一家做叉车出口的公司。
这类公司的日常大概是这样:客户在WhatsApp上问一台三吨电动叉车 FOB Shanghai 多少钱,销售回一句稍等,然后开始翻 Excel。产品价格一张表,配置加价一张表,条款模板一个文件夹。报价单这种东西,在外贸公司里其实是产品目录、成本台账和对外脸面的三合一。
麻烦在于那张 Excel 是“活”的。好几个人一起改,你加一行配置价,我改一个电池型号,改完各存一版发出去。过两周客户回头问“上次你报的 8500 是哪一版”,谁也说不清。财务那边更直接:三个人发来三种格式的表,对不上账,心烦。
我参与这家公司日常维护运作。2026 年 3 月,我决定自己写个系统把这事了结。
那时候我说不清要做个“什么”。如果非要说,目标朴素到有点好笑:让报价只存在一个地方,让打印出来的单子长一个样。
一
v1.0 现在还躺在版本日志里:产品管理、报价员管理、报价生成和打印。连“客户”这个概念都是一个多月后才出现的。一开始系统里只有产品和价格,客户是谁根本不重要,反正单子打出来发出去就完事。
技术选型说不出什么深思熟虑的故事。PHP,MySQL,没用框架,宝塔面板上一放就能跑。我是 PHP 熟练工,初中的时候在用企业微信 API 写过些东西,后来断断续续写了不少页面级的代码,但从没完整做过一个“系统”。这个项目从第一天起就是和 AI 结对写的,纯 PHP 的小文件跟这种开发方式配合得出奇好:一个页面从取参、查库到输出自己说完整,改起来不容易把不相干的东西碰坏。
现在回头看,当时的我想得太简单了。我以为自己在做“表单加表格加打印”,以为最难的是排版。实际上排版反而是整个项目里最稳定的部分——三个月后它还在那里一个字没动,而它周围的一切全换了。
二
后面几个月,功能一个接一个冒出来。有趣的是,几乎没有哪个功能是我坐在那里“规划”出来的,全是前一个功能咬出来的。
报价单需要有效期,一句话的事。但有效期过了客户又来问,就得改价;改价不能销售自己说了算,得有人批,调价审批就这么来了。这个模块我做了两遍。版本日志里 v1.7 和 v1.8 两段一字不差,不是手滑复制粘贴,是真的推倒重做了一遍:内部小范围试用的时候,同事叫苦连天,错误一堆。后来复盘,问题不在界面也不在流程,而在于我没想清楚一件事:改价之后,“总价”到底以谁为准?是单子头上的总数,还是明细行加出来的和?两处各改各的,越改越乱。
这个教训后来变成了公共函数库里的一条注释,我给它起名叫价格权威原则:行级价格是唯一的事实,总价永远是实时合计,任何审批记录都只是过程。现在看上去是代码中的一行注释,其实背后是两遍返工。
再往后是 PI,形式发票。做出口的都懂:报价单把客户哄开心了只是第一步,真正进入收付款流程要走 PI。于是系统里长出了 PI 模块。PI 要审核,就有了审核员角色;审核通过要锁定,要改就出修订版;客户确认之后转订单,订单七个节点从定金到寄单;有订单就有收款计划,有收款就有财务角色。财务进来的那一刻我才发现,原来“谁能看什么”这个问题,我从来没真正回答过。
八月几乎是失控的。版本日志显示整个 8 月冲了九个版本:PI、订单、收款、对外查询、Cloudflare。报价、PI 这些都有了之后,很自然想让客户自己查进度,不然销售天天被问“我的货到哪了”。
三
没有一个明确的瞬间让我意识到“这不是小程序了”。几件事先后砸过来,我砸着砸着就懂了。
第一件是并发。报价编号早期就是“数一下今天几单然后加一”。哪天有人手一抖提交了两次,同一秒两笔请求拿到同一个序号,撞号。数据库的唯一约束兜了底,但那不够体面。解法是 MySQL 的 GET_LOCK 加事务,代码里现在还留着注释:同一用户同一秒并发提交时,只有一个请求能拿到锁。写下这行注释的时候我意识到,v1.0 的我连“手一抖提交两次”这个场景都没有想象过。
第二件是金额。前端算一个总价,后端又算一个,两边对不上就拒绝提交,容差半美元。为什么算两遍?因为前端那个是给用户看的,后端那个才是要落库的,谁都不能替谁做主。做 Excel 的时候根本没有这个问题,因为 Excel 里只有一个数。
第三件最隐蔽:session 文件锁。PHP 的会话是文件锁实现的,一个页面开着会话慢慢跑,同一用户的 AJAX 轮询全在后面排队。症状特别诡异:页面偶尔卡住,刷新又好了。后来给纯接口加了跳过会话的开关,问题消失。这种 bug 的价值在于,它逼着你理解自己每天在用的东西底下是什么。
权限是最后砸下来的。最早的一次是隐私意识——销售只能看自己的客户;然后是角色门禁,不该进的页面直接拦;最后是某次排查时后背发凉:在加固之前,理论上一个恶意网页能用一张图片标签静默删掉我半个产品库。于是所有删除操作从 GET 改成 POST,加 CSRF,裹事务。当初做角色权限时,脑子里第一个想法仅仅是页面上一个 if 的事,但它是从数据层贯穿到整个请求链路的东西。这句话我补了三遍才真正懂。
四
坑挑三个半讲。
最疼的是 Cloudflare 那次。想让客户拿着链接自己查订单进度,源站在国内的宝塔服务器上,没域名,没备案,而 Cloudflare 的 Worker 不接受回源到纯 IP 的请求。我在那儿卡了很久,查文档,试各种姿势,一度觉得这功能要黄。最后的解法是把方向反过来:既然 Worker 不能来找我,那我去找它。服务器主动出站,把公开的订单数据推到 Cloudflare KV 存储,Worker 只读 KV,全程零入站流量,不需要域名也不需要备案。这个方案在设置页里留了句话作纪念:国内服务器零备案依赖。而教训很值钱,出站永远比入站容易。防火墙、备案、端口白名单,拦的都是往里走的流量。
第二个是图片。上传产品图偶尔弹 500,错误日志写着内存耗尽。诡异在文件并不大,两三兆的 JPG。查了半天才知道 GD 库解码时每像素吃四到八字节,一张单反原图四千万像素,解码完几百兆,直接把 512MB 的内存上限打爆。文件大小和内存大小是两码事,“小字节大像素”这个词是那次学会的。现在的代码在解码前有个像素数硬上限,超过四千万直接拒绝。
第三个,静默失败。PI 编辑页的“提交审核”按钮,某天开始点了没反应,保存草稿却正常。查了很久才发现是前后端校验口径不一致:服务端会丢弃数量无效的行,前端却在校验所有行。于是有一类数据永远过不了前端,也永远到不了后端,按钮就那么安静地死掉了。修法除了对齐口径,还把按钮改成容错绑定。就算页面脚本挂了,表单照样能提交,让服务端的红条来说话。静默失败是最坏的失败,因为它连“失败了”都不告诉你。
最后半个是上个星期。给 PI 加扫码签字,AI 半天就把整条流程写完了,然后连坏三次:手机打不开签字页(其实是生成的链接丢了端口,服务器挂在 59188 上);打开了但画布不显示,那是个 flex 容器高度塌陷,canvas 跟着归零;都好了,打印出来签名旁边有条黑线,章图太大越过容器边界,PDF 合成时在边界渲染出伪影。三天,三个 bug,每一个都是代码看着对,但环境不同意。这件事放到下一节讲。
五
这个项目是 vibecoding 做出来的,我得诚实地讲这件事,因为“AI 帮我写代码”这句话遮住的东西比它说出来的多。
AI 在这个项目里的作用分三个阶段。最开始,它解决的是我这个熟练工的手速问题:我没写过完整系统,但我知道每个页面要什么,AI 负责变成代码,纯 PHP 小页面的结构它写得意外地稳。
中间那几个月,AI 帮不上忙了。不是它变弱,是问题变了。4 月到 7 月我几乎没动这个项目,版本日志里那三个月是空的。那段时间挺迷茫的:功能想不出来了,也不知道做出来到底有没有用,公司里大家也提不起劲。瓶颈根本不在代码层面,在于没有人能告诉我“还缺什么”,AI 更不能,因为它不知道我们公司怎么干活,不知道财务为什么烦,不知道哪个客户又投诉了表格对不上。那三个月教会我一件事:需求不是想出来的,是用出来的。8 月的功能大爆发,素材全部来自真实业务的链条,PI、订单、收款,这才慢慢有了这个系统。
第三阶段就是现在。AI 的产出速度已经快到吓人,签字流程半天写完,质量也不差。但连坏三次的那三天里,真正干活的是什么?是我得知道反向代理会吞端口,得看得懂 flex 高度塌陷,得能推断“打印黑线等于元素越界加边界伪影”这条因果链。AI 没有取消理解的必要性,它只是把不理解的代价放大了。以前不理解,代码根本写不出来;现在不理解,代码写得出来,然后在你最想不到的地方坏掉。
所以如果有人问我 vibecoding 的体验,答案是:它把“写”变成了整件事里最便宜的一环,剩下的全是判断。这个方案对不对,这个妥协值不值,这个 bug 的因果链是不是真的。判断力成了瓶颈。这大概是这个时代写软件的人的普遍处境吧。
六
如果重来一次,有些东西我会原样保留:纯 PHP 加 PDO 加宝塔。听着不性感,但“一个文件扔上去就能跑”在真服务器上是超能力,这八个月里无数次救我于部署灾难。还有增量演化的方式。如果第一天就让我设计带审批、签章、订单、收款的完整系统,我大概率设计不出来,或者设计出一个没人用的东西。
有些会改。数据库迁移应该从第一天就有版本纪律,实际上前期是想到什么列就 ALTER 一下,迁移文件是后补的补丁,直到上周部署翻车(签字页打不开,因为表还没建),我才写出第一个自愈迁移,代码启动时自己检查缺什么,缺就补。早干嘛去了。组件规范也该早点统一,空状态、页头、行操作菜单,各页面各自为政了很久。
有些判断不会改:CS 模块不该提前设计。那是 9 月才长的方向,之前没有任何迹象。这是因为生意方向变了才有的需求,提前设计纯属浪费。
还有些重构要诚实交代。安全那一轮是必要的,做完我才安心。但有几轮 UI 重构,一半是为了用户,另一半说实话是为了自己看着舒服。给自己公司写软件的好处是没人砍你需求,坏处是“想重做一遍”的冲动也没人拦。
系统现在处于可交付状态,还没正式上线,接下来先在公司内部用。它最早叫 eHYDER,代码里还留着痕迹,样式表的第一行注释至今写着 eHYDER Quotation System,像地层里的化石。后来改成掌上涵得。明明是电脑端的系统,为什么叫掌上?大概是因为我想要的那种感觉:自己公司的报价、订单、收款,第一次可以整个握在手里。Excel 时代,它散在十几份文件、三台电脑和一个微信收藏夹里。
八个月,一个人加一个 AI,二十六个版本。要说这个项目教会了我什么,具体的话大概是这些:功能从来不难,难的是功能之间的依赖;“能跑”到“能用”之间,隔着真实用户的抱怨;安全问题永远是出了事或者差点出事之后才会被真正当回事;而软件开发里真正困难的地方,不是把东西实现出来,而是系统越来越复杂之后,你还能不能理解自己正在构建的是什么。
对了,还有一条:给自己公司写软件,用户是天天见面的同事,最早抱怨 Excel 的是财务。这种身份没法糊弄,也算是一种工程约束。