AI 写的表单校验前端好好的后端却收到脏数据,规则到底该写在哪一层

2026-07-29

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

页面上明明弹了红字提示,数据库里却躺着一条手机号是 11 个字母的记录——这种事多半不是 AI 生成的正则写错了,而是你让它把同一条规则在两个地方各写了一遍,然后其中一遍先老化了。 校验这件事的难点从来不在”怎么判断一个字符串是不是邮箱”,而在于同一条业务约束有几个副本、谁是权威、改的时候谁会被漏掉。AI 写单条校验函数的准确率相当高,写”两层之间保持一致”的准确率相当低,因为后者不是代码问题,是你的项目结构问题。你没给它一个唯一落点,它就会在它看得到的那个文件里补一遍,看不到的那层原封不动。

站内已经有两篇相邻的文章:用 AI 做问卷/表单工具 讲的是从零把一个表单产品做出来、跑通收集和部署;模型给的参数别照单全收 讲的是智能体调工具时在函数入口拦住模型编出来的参数。本篇不重复这两件事,只盯住工程里那条最容易烂掉的接缝——人填的表单数据从浏览器走到数据库,中间该设几道闸、每道闸拦什么、规则源码放在哪儿才不会分叉。

一、先把现象分开:四种”校验失效”根本不是一个病

线上冒出一条不合法数据,你要先判断它是怎么进来的,再决定改哪儿。这四种在日志里长得很像,处置动作完全不同。

规则漂移:前后端各有一份规则,某次需求变更只改了一边。典型表现是”新规则的边界值前端拦了后端没拦”或者反过来”后端报 400 但前端没给任何提示,用户看到按钮点了没反应”。

层级缺失:某条约束压根只写在了前端。表单里能填的字段都做了长度限制,后端直接把请求体塞进 ORM。这种情况下正常用户永远不会触发问题,一旦有人用脚本直接打接口,或者你自己写了个批量导入的后台任务绕过了页面,脏数据就进去了。

类型错位:前后端对同一个字段的类型理解不一致。前端把金额当字符串传、后端当浮点数收;前端传的时间戳是毫秒、后端按秒解析;空值前端传空字符串、后端只判 null。这类问题最阴,因为它经常不报错,只是算错。

绕过提交:前端校验完全正常,但用户根本没走那条路径。自动填充、浏览器插件、移动端的旧版本包、第三方渠道回调,都能绕过你精心写的那个 onBlur。

判别表如下,出问题时对着现象往下走:

现象大概率成因怎么验证处置动作
页面正常提示,库里仍有不合法记录层级缺失,后端没有对应校验用 curl 直接打接口发同样的非法值,看是否返回成功在后端补齐该字段校验,并回查历史脏数据范围
前端放行,后端返回 400 且页面无提示规则漂移(后端更严)+ 错误未回显打开浏览器网络面板看响应体,确认错误结构是否被前端解析统一错误响应结构,前端按字段回显;同步前端规则
边界值一边过一边不过(如恰好等于上限)规则漂移,闭区间开区间不一致构造等于边界的值,分别测前端函数和后端接口把边界写进同一份规则定义,两侧引用
数据入库了但数值不对(少两位、差 8 小时)类型错位,单位或精度约定不一致打印请求原始报文和入库前的值做逐字段比对在接口边界做一次显式转换并断言,不依赖隐式转型
只有部分渠道产生脏数据绕过提交,存在不走该页面的入口按来源字段分组统计脏数据分布把校验下沉到服务层而非控制器,所有入口共用
偶发超时、CPU 飙高且伴随特定输入正则回溯,某条校验规则写成了灾难性回溯用超长重复串本地跑该正则并计时换掉该正则或先做长度截断,详见下文止损那条
前端不报错但请求根本没发出去校验函数抛异常被吞掉在提交处的 catch 分支临时加一行日志后复现,看是否进了 catch、异常是否来自校验函数不吞异常,校验失败与校验出错要分开处理

二、AI 能干到哪一步,哪一步必须你来定

把这条线拆开看,AI 的边际收益分布极不均匀。

它干得好的部分:写单条格式判断函数、把一份声明式的规则定义翻译成前端和后端两套代码、根据字段定义批量生成边界测试用例、给一堆散落的 if 判断做归并重构、把错误码和文案对齐成一张表。这些活的共同点是有明确输入输出、正确性可以当场验证。你给它一个字段清单,它十分钟能铺完你写两小时的模板代码。

它干不了、也不该让它干的部分:定业务口径。手机号允不允许境外号段、身份证要不要做校验位、姓名能不能带中间点、金额上限是多少、同一个手机号能不能注册两个账号——这些没有唯一正确答案,只有你们业务的答案。AI 在这里会做一件很危险的事:它会给你一个看起来很专业的默认值,而这个默认值来自它见过的大多数项目,不是你的项目。等你上线以后发现有真实用户被拦在门外,这笔账算不清。

还有一类是它会漏的:跨字段约束和跨记录约束。单字段规则它写得又快又全,一旦规则形态是”选了 A 类型时 B 字段才必填""结束时间必须晚于开始时间""这个编号在库里不能重复”,生成的代码经常只覆盖了表面那一层。跨记录的唯一性尤其容易被写成”先查一次再插入”,并发下直接失效。这类约束的最终防线只能在数据库,不在任何一层应用代码里。

所以分工是清楚的:口径你定,铺代码交给它,验收你做。

三、规则的落点:一份定义,四道闸

现在回答标题里那个问题。答案不是”前端”也不是”后端”,是规则定义只有一份,执行点有四个

第一道闸在浏览器,职责是体验,不是安全。它的任务是让用户在按下提交之前就知道自己填错了,减少往返。它可以被绕过,你必须假设它一定会被绕过。所以这一层怎么写都不构成安全保证,唯一的硬要求是不能只写在这一层,而且松紧度不能超过后端(原因见第四节)。

第二道闸在服务端接口的入口,职责是把不合法的请求挡在业务逻辑之外。这是权威校验,返回结构化的错误,指明哪个字段错了、错在哪。这层必须完整覆盖所有字段,包括那些前端做成了下拉框、看起来”用户不可能填错”的枚举值——下拉框只是前端渲染,请求体里那个字符串你想填什么都行。这里说的”入口”是指请求刚进入应用、完成反序列化的那一刻就做单字段校验,但校验的实现体不要长在控制器函数里——控制器只负责调用,规则本身放在能被别的入口复用的地方,理由见下一道闸。

第三道闸在业务服务层,职责是跨字段和跨实体的一致性。为什么不放在控制器里?因为你早晚会有第二个入口:后台管理页、批量导入、定时任务、消息队列消费者。校验写在控制器上,这些入口全部裸奔。写在服务方法的第一段,所有调用方自动获得同一套保护。

第四道闸在数据库,职责是最终兜底。非空约束、唯一索引、长度上限、外键、check 约束,该建的都建上。很多团队图省事把表字段全设成可空的宽松类型,理由是”应用层会保证”。应用层保证不了并发和历史数据。唯一索引带来的报错处理起来是有点烦,但它是你唯一一道不会被绕过的闸。

而规则定义放哪儿,取决于你的技术栈是否同构。前后端同语言的项目,把字段约束抽成一份共享的 schema 声明,两端引用同一个模块,改一处两端同时生效——这是最稳的形态。前后端异构的项目(比如前端 TS、后端 Java 或 Python),你需要一份中立的字段规格文件,用它作为唯一事实来源生成两端代码,或者至少作为改动时的检查清单。哪怕退化到一个 Markdown 表格,只要它是评审时必须一起改的那份文件,就比两边各写各的强。

让 AI 参与的正确姿势是:你交给它这份规格,让它同步生成或修改两端。而不是分两次对话,一次改前端一次改后端——中间隔着上下文,第二次它多半已经不记得第一次的边界值了。

四、可执行的做法与验收动作

做法层面,按这个顺序推进最省事。

先把字段规格写出来,一个字段一行:名称、类型、必填与否、长度或数值范围、格式约束、允许的枚举值、跨字段依赖、错误文案。这一步不写代码,纯粹是逼你自己把口径想清楚。含糊的地方现在标个问号,比上线以后改数据库便宜。

再让 AI 按这份规格生成服务端校验,这是权威层,先做它。生成完以后你亲自读一遍枚举和边界,重点看闭区间开区间、看必填字段有没有被写成”非空字符串”从而漏掉全是空格的输入、看数值范围有没有把负数和零考虑进去。

然后生成前端校验,规则严格程度必须与后端一致或更宽松。前端比后端严会造成一个很讨厌的现象:某些合法数据用户在页面上死活提交不了,而你查后端日志一条错都没有,因为请求根本没发出来。

接着统一错误结构。服务端返回的字段级错误要有稳定形状,前端才能按字段回显。这个结构一旦定下来就别随意改,字段级错误的形状变一次,所有调用方都得跟着改。

验收动作是这套流程里最不能省的一步,也是最容易被 AI 糊弄过去的一步。三个动作:

绕过前端直打接口。 这是判断层级缺失最快的办法,一条命令就能验:

curl -i -X POST https://your-api.example.com/api/forms \
  -H "Content-Type: application/json" \
  -d '{"phone":"abcdefghijk","amount":-1,"email":"not-an-email"}'

期望看到 4xx 和字段级错误信息。如果返回 2xx,说明后端这道闸是空的,前端那些红字提示纯属装饰。把每个字段的非法值都这么打一遍,写成脚本存下来,以后每次改校验都重跑。

批量灌边界值。 手写用例覆盖不全,用脚本生成更省事:

cases = []
for name, lo, hi in [("age", 0, 150), ("amount", 1, 1000000)]:
    for v in (lo - 1, lo, lo + 1, hi - 1, hi, hi + 1):
        cases.append({name: v})
print(len(cases), "cases")

关注的是等于上下限的那两个值,规则漂移几乎总是在这两个点上暴露。

改动时做一次成对检查。 每次改校验规则,提交前看一眼这次动了哪些文件:

git diff --name-only HEAD

带上 HEAD 是必要的:不加它只列出未暂存的改动,你 git add 过的那些文件不会出现,正好漏掉最需要成对检查的那一半。

如果只有前端目录下的文件,或者只有服务端目录下的文件,停下来问自己另一边要不要跟着改。这个动作听着土,但它能拦住绝大多数漂移。测试全绿也不等于校验真的生效,测试假通过 里那几种自欺场景在校验代码里格外常见——尤其是测试直接调校验函数、绕开了真实的请求解析路径。

五、什么情况下别再折腾

有几个信号出现时,继续在校验代码里打补丁是纯亏。

同一个字段的规则你已经改到第三版,每版都引出新的边界问题。 这说明业务口径本身没定清楚,不是代码写得不好。停手,把产品和业务方拉到一起,把这个字段到底允许什么写成一句人话,再回来动代码。在口径模糊的情况下让 AI 反复重写,只会得到一堆互相矛盾的分支。

为了兼容历史脏数据,你开始在校验里写例外分支。 一旦出现”这批老记录不走这条规则”,你的校验就不再是校验,是一张补丁地图,半年后没人敢动。正确做法是把历史数据的清洗当成独立任务做掉,或者给这批数据打个显式标记走单独路径,别让例外渗进主规则。

正则表达式已经长到你自己读不懂。 长正则是回溯风险和维护风险的双重来源。手机号、金额、日期这类结构化字段,用分段的显式判断远比一条巨型正则可靠。真到了需要复杂模式匹配的场景,先做长度截断再匹配,具体的回溯陷阱见 正则灾难性回溯

改校验以后线上开始有合法用户被拦。 这是最硬的止损信号,立刻回滚,别在线上调参。回滚点应该在你动手之前就想好:这次改动涉及哪些文件、上一版是哪个提交、能不能单独回退。校验属于那种”拦错了比漏放更痛”的逻辑,因为漏放的脏数据你事后能清洗,被拦掉的真实用户不会再来第二次。

换条路的判断依据:如果你发现要写的校验逻辑本质上是在猜测用户意图(比如从一段自由文本里解析地址、判断一个名字是不是真名),那这不是校验问题,是数据采集设计问题。把字段拆细、加下拉、加二次确认,比在后端写一堆启发式规则划算得多。

六、避坑清单

只在控制器里校验。 会踩是因为写的时候手边只有一个入口,看起来足够了。避法是把校验放进服务方法的第一段,控制器只负责把请求转成参数对象。等第二个入口出现时你什么都不用做。

用前端的下拉框当安全边界。 会踩是因为界面上确实选不出别的值,心理上就默认它安全。避法很简单:枚举字段在后端一律显式白名单判断,别信请求体里的任何字符串。

必填校验只判 null。 会踩是因为大多数示例代码就是这么写的。空字符串、纯空格、纯换行都能穿过去,最后你数据库里躺着一堆看不见的空记录。避法是必填判断统一走”去掉首尾空白后长度大于零”,并且在同一个地方定义什么叫”空”。

金额和数量用浮点数直接比较。 会踩是因为它平时看着都对,只在特定小数上出偏差。避法是金额一律用整数最小单位或高精度类型传输和存储,校验也在这个表示上做,细节见 金额浮点精度

唯一性靠先查后插。 会踩是因为单机测试永远测不出并发问题。避法是数据库建唯一索引,应用层捕获冲突错误转成友好提示,查询只用来提前给用户反馈,不作为保证。

错误信息把内部细节直接吐给前端。 会踩是因为调试期这么写最方便,然后忘了删。数据库字段名、SQL 片段、堆栈路径全暴露出去。避法是错误响应固定成字段名加错误码加文案三件套,详细信息只进日志。

校验规则里写死了时区和地区假设。 会踩是因为开发机和生产机时区一致,本地怎么测都对。避法是所有时间在接口边界统一转成带时区的标准表示再校验,别在校验函数里做隐式解析。

让 AI 一次性重写整个校验模块。 会踩是因为看着它一口气产出上百行很爽,实际上它顺手改掉了几条你上次特意调过的边界值,而 diff 太大你没逐行看。避法是按字段小步改,每次提交只动一两个字段的规则,diff 能一眼扫完。

前端做了防抖异步校验,后端却没做对应约束。 比如注册时前端实时查手机号是否已存在,体验很好,后端却只有一条插入语句。会踩是因为异步校验的存在给了你”已经查过了”的错觉。避法是把异步校验当成提示,真正的保证放在数据库。

收束

表单校验的工程价值不在于你写了多少条规则,而在于半年后有人改需求时,他能不能一眼找到这条规则住在哪儿、改完会不会漏掉另一边。AI 能把铺规则的成本压到很低,低到你有余力把每一层都做扎实;但它压不掉定口径和做验收这两件事的成本,这两件事也压不掉。

改完校验以后,对着这张单子过一遍再合并:

  • 这次改的字段,前端和后端两侧是不是都动了?git diff --name-only HEAD 看一眼。
  • 边界值等于上限、等于下限时,两侧行为一致吗?
  • 用 curl 绕过前端打非法值,接口返回的是 4xx 还是 2xx?
  • 必填字段填一串空格,能提交成功吗?
  • 枚举字段传一个不在列表里的字符串,后端拦住了吗?
  • 唯一性约束在数据库里建了索引,还是只有代码里那次查询?
  • 错误响应里有没有泄漏内部字段名或堆栈?
  • 这次改动的回滚点是哪个提交,回滚以后线上会不会留下半套规则?

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。