从 PHP 到 AI + Golang,程序员自救转型手记(四十三):前后端数据验证
这是一个系列 Blog,作者将以一个 PHP 全栈工程师的身份,利用 AI 工具(claude code、codex、deepseek、豆包等):从零开始学习 golang 语言,并最终完成 ai-go-admin(github | gitee)开源项目的制作,全程记录分享。

上一期聊了前端表格组件,这一期轮到数据验证上场。前后端各自该验什么、怎么验,整理清楚了,能省不少返工的麻烦。
前后端数据验证
服务端数据验证
服务端的数据验证,主流方案有三种,各有各的适用场景,直接看。
方案一
模型层写 binding tag,控制器层用 gin 的 ShouldBindJSON,绑定 JSON 数据的同时就把验证给办了。基控制器的 Create、Update 方法默认已经调用了 ShouldBindJSON,所以模型里写对验证规则就能自动生效。示例:
Username string `gorm:"comment:用户名;type:varchar(64);not null" binding:"required"`
required 表示必填,完整可用规则可以参考 go-playground/validator 文档,或者 gin 模型绑定和验证章节。注意,go-playground/validator 已随 gin 框架内置,直接拿来用就行。
方案二
模型定义里写 validate tag,这是 go-playground/validator 原生的写法,规则和 binding 一致。比如:
Username string `gorm:"comment:用户名;type:varchar(64)" validate:"required"`
但和方案一不同的是,验证需要手动触发——在服务层或控制器层重写 Create、Update 等方法,调用 validate.Struct 来完成。示例:
type User struct {
FirstName string `validate:"required"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `validate:"required,email"`
}
validate := validator.New()
user := &User{
Age: 135,
Email: "Badger.Smith@gmail.com",
}
err := validate.Struct(user)
方案三
增加 DTO,在 DTO 里定义 binding / validate tag,然后重写控制器或服务层的方法。比如 AdminUpdateRequest 就是一个自定义 DTO,不理解的全局搜索一下就能找到参考。
何时在控制器效验,何时在服务层效验?
这是一个很实际的问题,分清楚能让代码结构更清晰。
服务层
控制器层
title、name、type 的必填,放控制器层就足够了。
举个例子:AdminRule 表
title、name、type 的必填校验放在控制器层;name 的重复性检查、pid 不能是自己的判断,放在服务层。仓储层呢?不做任何校验,只靠数据库本身的 not null、唯一索引等机制兜底。
前端数据验证
前端的数据验证,核心是表单验证。我们准备了一个工具包 src/utils/validate.ts,里面定义了各种验证规则,最重要的是一直导出的验证规则构建器 buildValidatorRule 函数——它能很方便地生成 el-form 表单的常用验证规则。
element plus 的表单验证基于 async-validator。
具体用法参考下面的代码片段:
<script setup lang="ts">
import { regularPassword, buildValidatorRule } from '/@/utils/validate'
import type { FormItemRule } from 'element-plus'
const rules: Partial> = reactive({
username: [buildValidatorRule({ name: 'required', title: '用户名' }), buildValidatorRule({ name: 'account' })],
nickname: [buildValidatorRule({ name: 'required', title: '昵称' })],
email: [buildValidatorRule({ name: 'email', message: '邮箱错误' })],
mobile: [buildValidatorRule({ name: 'mobile', message: '手机号错误' })],
password: [
{
validator: (rule: any, val: string, callback: Function) => {
if (props.manager.form.operate == 'create') {
if (!val) {
return callback(new Error('密码必填'))
}
} else {
if (!val) {
return callback()
}
}
if (!regularPassword(val)) {
return callback(new Error('密码需要 6-32 位,禁止使用特殊符号'))
}
return callback()
},
trigger: 'blur',
},
],
})
</script>
注意密码字段的验证逻辑:创建时必填,编辑时非必填;但只要有值,就必须符合 6-32 位且无特殊符号的规则。这种条件判断在业务中很常见,直接用 validator 函数处理最灵活。