# 3.4 请求与校验 :::tip 概述 本项目没有独立的"验证器文件"——请求校验由 `api/v1` 请求结构体上的 gvalid 标签完成,路由/权限/日志元数据由内嵌 gmeta.Meta 声明。一个结构体即接口契约的全部。 ::: ## 请求结构体解剖(真实代码,摘自 api/v1/example.go) ```go // ============================== 案例演示模块(goframe_example) ============================== // ExamplePageReq 案例演示分页请求 type ExamplePageReq struct { gmeta.Meta `method:"get" path:"/api/example/page" summary:"案例演示分页" permission:"sys:example:page" log:"案例演示-查询分页记录" logType:"query"` PageReq Name string `json:"name" dc:"案例名称"` // 案例名称 Type int `json:"type" dc:"案例类型"` // 案例类型 Status int `json:"status" dc:"案例状态"` // 案例状态 Keyword string `json:"keyword" dc:"关键词"` // 关键词 } // ExampleDetailReq 案例演示详情请求 type ExampleDetailReq struct { gmeta.Meta `method:"get" path:"/api/example/detail/:id" summary:"案例演示详情" permission:"sys:example:detail" log:"案例演示-查询详情" logType:"query"` Id int64 `json:"id" v:"required#缺少ID" dc:"记录ID"` // 记录ID } // ExampleAddReq 新增案例演示请求 type ExampleAddReq struct { gmeta.Meta `method:"post" path:"/api/example/add" summary:"新增案例演示" permission:"sys:example:add" log:"案例演示-新增记录" logType:"add"` Name string `json:"name" v:"required#请输入案例名称" dc:"案例名称"` // 案例名称 Avatar string `json:"avatar" dc:"案例图片"` // 案例图片 Type int `json:"type" dc:"案例类型"` // 案例类型 Status int `json:"status" dc:"案例状态"` // 案例状态 Sort int `json:"sort" dc:"案例排序"` // 案例排序 } ``` ## 标签体系一览 | 标签 | 来源 | 作用 | |------|------|------| | `gmeta.Meta` | gf 框架 | 内嵌后由本项目 bind.go 扩展解析 | | `method/path/summary` | gmeta | HTTP 方法、路由模板(`:id` 路由参数)、OpenAPI 摘要 | | `permission` | gmeta | 权限节点 `sys:{module小驼峰}:{action}`;空 = 登录即可 | | `log/logType` | gmeta | 操作日志标题与类型(query/add/update/delete/import/export…) | | `public` | gmeta | `"true"` = 免登录(须与 middleware/public.go 白名单一致) | | `json` | 标准 | 出入参字段名,**一律驼峰**(例外仅登录/令牌接口的下划线字段) | | `v` | gvalid | 校验规则,如 `required#请输入案例名称`(# 后为自定义错误文案) | | `dc` | goai | OpenAPI 文档中的字段说明 | | `p` | ghttp | multipart 上传字段名(如 `p:"file"`) | ## gvalid 常用规则 ```go Id int64 `json:"id" v:"required#缺少ID"` // 必填 Age int `json:"age" v:"min:0#年龄不能为负"` // 数值范围 Ids []int64 `json:"ids" v:"required#缺少ID列表"` // 数组必填 Email string `json:"email" v:"email#邮箱格式不正确"` // 格式 Status int `json:"status" v:"in:1,2#状态值不合法"` // 枚举(如角色 dataScope 用 in:1,2,3,4) ``` 校验失败由框架返回 code=400 信封,错误文案取 `#` 后缀;无需在业务代码里手写 if 判断。 ## 路由参数承接 gmeta path 中的 `:id` 由请求结构体同名(驼峰 json)字段承接: ```text 路由模板 GET /api/example/detail/:id └─▶ ExampleDetailReq.Id int64 `json:"id"` ← r.Parse 自动绑定 ``` ## 请求解析流程(Bind 包装层完成) ```text 1. r.Parse(req):路由参数 + query + body 一次性绑定(含 gvalid 校验) 2. 实现 BodyUnmarshaler 接口的请求对象走自定义解析逃生舱 (请求体顶层形态无法用结构体标签表达时使用,gvalid 需在实现内自完成) 3. 校验失败 → response.Fail(code=400) 短路返回,业务方法不执行 ``` ## 响应结构体(真实代码,摘自 api/v1/example.go) ```go // ExamplePageRes 案例演示分页结果 // 统一壳:records/total/size/current/pages type ExamplePageRes = PageData[ExampleItem] // ExampleListRes 案例演示平铺列表结果 // data 直接为数组,树形结构由前端 buildTree 构建 type ExampleListRes = []ExampleItem // ExampleIdRes 新增返回ID结构 type ExampleIdRes struct { Id int64 `json:"id"` // 主键ID } // ExampleUpdateRes 修改案例演示结果 type ExampleUpdateRes struct{} ``` 要点: + 分页结果统一用泛型壳 `PageData[T]`(`v1.NewPageData` 构造) + 新增返回统一 `{id}` 结构(模块自定义 `XxxIdRes`;公共场景用 `common.IdRes`) + 修改/删除等空结果返回空结构体 `struct{}` + 行结构体含 `{Field}Text string` 字段时由基座自动填字典显示名 ## 行结构体示例(含字典文本字段) ```go // ExampleItem 案例演示行数据 // 用于分页列表等场景的基础信息 type ExampleItem struct { Id int `json:"id"` // 主键ID Name string `json:"name"` // 案例名称 Avatar string `json:"avatar"` // 案例图片 Type int `json:"type"` // 案例类型 Status int `json:"status"` // 案例状态 Sort int `json:"sort"` // 案例排序 CreateUser string `json:"createUser"` // 创建人 CreateTime *gtime.Time `json:"createTime"` // 创建时间 StatusText string `json:"statusText"` // 状态显示名(dict_item 解析) } ``` `StatusText` 与 `Status` 配对,配合 logic 里 `SerializeMaps = {"status": "example_status"}` 声明,读取时自动填充(见 3.5 逻辑层开发)。