两套系统、加起来不到二十个页面,前后端都是我一个人。两年下来最大的体会是:能不改的代码就不改,能不引入的依赖就不引入。中间有半年我追着新技术跑,把其中一套重写了一遍,最后又全删了。这篇记了三个具体决定和它们各自的代价,包括一个我到现在也不确定对不对的选择。

一、先交代清楚是两个什么系统

系统 A 是内部审批流,报销、请假、采购申请都走它,2023 年 4 月我接手的,.NET 6 + SQL Server 2019,前端是 Razor 页面加 jQuery,一共 11 个页面,日常用的人四十来个。系统 B 是设备与物料台账,2023 年 8 月接手的,当时已经有个能跑的 ASP.NET MVC 版本,2024 年我一个人重写成 .NET 8 + SQL Server 2022,前端换成自己写的 Vue 3,7 个页面,用的人十来个。到 2025 年 5 月我离职,正好两年。

说「一个人」不是说没人管,而是三件事同时成立:没有第二个人读过全部代码,所以我的改动没人评审;没有交接文档,我请假的时候出问题只能电话里说;需求是直接跟我对接的,中间没有产品经理,业务同事说要什么,我判断怎么做。前两条让「能不改就不改」变成了一个生存策略,而不是审美偏好——我改坏的东西,最后一定是我自己在某个晚上修。

还有一点值得先说:这两套系统是内部工具,不是产品。没有人会因为界面旧而不用它,但凡它挂了半小时,就会有四五个人在群里问。这个定位决定了后面所有的取舍。

二、第一个决定:不引入工作流引擎

2024 年 6 月,系统 A 要加条件分支——不同金额、不同部门的申请走不同的人。我正经评估过两个开源方案:Elsa Workflow 和 Workflow Core,都装起来跑通了 demo,Elsa 那套设计器我还花了两个晚上看它的持久化模型。

最后我自己写了。一张 WorkflowNode 表存节点和流转条件,一个 300 行左右的函数按顺序解释执行,遇到分支节点就按金额和部门算下一步审批人。写完加测试一共四天。

当时的理由是:引入 Elsa 意味着我要理解它的活动模型、版本迁移策略,还要跟着它的版本走;而我需要的功能五年内大概不会变。这个理由现在看只对了一半。

代价在 2025 年 4 月来了。业务那边要加会签——同一节点要多个人都通过才往下走。我在那个 300 行的函数里加了分支,加了整整两天,完了以后函数变成 480 行,我自己在改的时候都要反复读三遍才敢动手。那一刻我承认自己算错了:我以为不会变的是「规则」,实际上规则确实没怎么变,变的是「参与人的组织方式」,而后者才是我没做抽象的地方。

补充

2026 年 1 月我又加了一个「加签」的需求,这回我先花了一天把那 480 行拆成四个函数,再加功能。拆的那天我很后悔 2024 年没有这么做——但也不确定如果当时就设计了扩展点,会不会只是把复杂度提前了而已。

三、第二个决定:那半年我追着新技术跑,最后全删了

2025 年上半年我有点飘。3 月到 8 月,每个周末花一天改系统 B:前端从 Options API 重写成组合式 API 加 TypeScript,引入 Pinia 和 VueUse,还上了按组件自动引入的插件;后端把三层架构改成垂直切片,加了 MediatR、FluentValidation 和 Mapster,其中两个模块试了 Minimal API。那半年 git log 上有 200 多个 commit,周一上班经常要花半小时才能想起来上周末改到哪了。

转折点是一次上线。2025 年 8 月的一个版本,一个表单提交开始报错,查下来是我自己封装的 useForm 在重置时没清掉校验状态——这是我重写时新造的抽象,原来用 Options API 的时候根本不存在这个问题。更麻烦的是 Mapster 的映射配置里,两个名字相近的字段被静默映射反了,跑了一个星期才发现,影响了 60 多条台账记录,最后是手工一条条改回来的。

9 月我做了个现在回头看挺狼狈的决定:把 MediatR 和 Mapster 全删了,垂直切片退回成原来的三层,Minimal API 那两个模块改回 Controller。提交历史里那段 revert 现在还留着,我没删,删了就没法提醒自己了。删的那天挺沮丧的,觉得半年白干;删完两个月之后,我第一次觉得这套系统变得好维护了。

留下来的是 Pinia 和 TypeScript——这两个是真的省事,TypeScript 后来帮我提前发现了三次字段类型的问题。我现在的判断标准不再是「这个技术好不好」,而是「删掉它我要改几个文件」。按这个标准,自动引入插件第一个就被删了,因为它散在每个模板里。

四、第三个决定:一个我到现在也不确定对不对的妥协

系统 A 里有张 OrderMain 表,62 个字段,其中二十来个是历史遗留、现在没人读写;还有一个 sp_GenerateReport 存储过程,1800 多行,里面嵌套了四层临时表,注释是五年前的人写的,早就对不上了。全公司没有人敢动它。

2025 年 11 月,业务要加一个字段,报表里要能筛。正确做法我清楚:把表拆成主表和扩展表,把存储过程翻译成 C#,顺便把那二十来个废弃字段清掉。我算了一下——拆表要动 30 多个地方、存储过程翻译至少三天、还要做数据迁移和一轮完整回归,而我手上同时还有系统 B 的两个需求。

我选了最省事的做法:在表上加一个字段,在存储过程里加一个可选参数。二十分钟改完,当天上线。

代价我也清楚:表现在 63 个字段,存储过程 1850 多行,每次打开它我心里都不舒服。我给自己记了账,把这个技术债写进了清单,标了「2026 年 Q2 处理」——到现在四月份了,还没动。

我不确定的地方在于:这到底是懒,还是对的?我的判断依据只有一句话——这套系统的规划生命周期还剩三年左右,三年内这块逻辑大概率不会再动,为了一个可能的未来去承担一次高风险重构,不划算。如果它要活十年,我肯定是错的。这个赌注现在还不知道输赢,我只能把依据写下来,等三年后回来看。

五、两年下来真正留下来的几条

第一条,能不改的代码就不改。我顺手做的那些「顺便清理一下」,有两次引入了比原问题更麻烦的 bug,其中一次是在改一个字段名的同时把另一处引用漏了,线上错了两天数据。现在我的规则是:不在这个需求范围内的改动一律不做,记在清单里攒着,等哪天因为别的原因必须动这个文件,再一起做。

第二条,能不引入的依赖就不引入。每加一个包之前,我先写一句「如果它三年不更新了,我要改几个文件」。这个习惯是从 Mapster 那次来的。System.Text.Json 够用就不加 Newtonsoft——当然我们老项目里还在用,那是历史,不动。

第三条,一个人维护最重要的是可恢复性,不是优雅。备份、一键回滚的脚本、一份写清楚怎么把服务跑起来的 README,这三样优先于任何重构。2025 年我写过一份 12 行的部署清单,那一年它比任何代码都有用——有一次我发烧请假,同事照着它把服务重启好了。

第四条偏私人:每季度抽半天做一次「如果我不在」的演练,把最近一个改动讲给一个不熟悉这套系统的人听,讲不通的地方就是文档该补的地方。这条我执行得不好,2026 年到现在只做过一次。

写在最后

这两套系统会一直这么土下去:旧的 jQuery 页面还在,那个 1850 行的存储过程还在,我的审批流函数还是自己写的。我接受这个结果,甚至有点满意——它们每天在跑,没出过大事,加需求的速度业务那边是认可的。

写完这篇我自己也有点不安:前面这些话读起来很像在给偷懒找理由,尤其是第三节和第四节。也许确实是。我能说的只有一点——这些结论是在「一个人、内部系统、四十个用户」这个具体条件下成立的,换个条件我大概率会做不同的选择。如果哪天我到了一个十几人的团队里维护核心系统,我会回来看这篇,看看自己是不是把舒适区写成了方法论。