Skip to content

3.3 契约层开发(dto) ​

概述

dto 是模块的对外契约:json 标签即 API 字段名,binding/msg 标签给出校验规则与提示语,form 标签供 GET 参数绑定。契约一旦发布应保持稳定——前端与生成器都依赖它。

文件位置与命名 ​

text
internal/model/dto/example.go   (package dto,一模块一文件)

结构体按职责命名:ExampleItem(行结构)、ExamplePageReq(分页请求)、ExampleDetailRes(详情响应)……

行结构与类型别名 ​

go
// internal/model/dto/example.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"`     // 案例状态(取值以字典 example_status 为准)
    Sort       int        `json:"sort"`       // 案例排序
    CreateUser string     `json:"createUser"` // 创建人
    CreateTime *LocalTime `json:"createTime"` // 创建时间(*LocalTime,NULL 输出 null)
    StatusText string     `json:"statusText"` // 状态显示名(按字典 example_status 回填)
}

结果类型用别名收敛:

go
type ExamplePageRes = PageData[ExampleItem]   // 分页壳
type ExampleListRes = []ExampleItem           // 平铺列表
type ExampleExportRes = string                // 导出结果直接是文件 URL

请求结构与标签 ​

go
// internal/model/dto/example.go
type ExamplePageReq struct {
    PageReq        // 分页基础参数(内嵌,pageNo/pageSize 即刻可用)
    Name    string `json:"name" form:"name" dc:"案例名称"`      // 模糊匹配,空串跳过
    Type    int    `json:"type" form:"type" dc:"案例类型"`      // 等值,零值跳过
    Status  int    `json:"status" form:"status" dc:"案例状态"`  // 等值,零值跳过
    Keyword string `json:"keyword" form:"keyword" dc:"关键词"` // 多列 OR 关键词
}

type ExampleDetailReq struct {
    Id int64 `json:"id" binding:"required" msg:"缺少ID" dc:"记录ID"`
}
标签作用
json对外 API 字段名(小驼峰)
formGET/query 参数绑定名(与 json 同名)
binding校验规则(required / min / max / oneof 等)
msg校验失败提示语
dc字段说明(供 Swagger/OpenAPI 文档生成)

命名与语义规约 ​

请求/响应语义
AddReq全量必填:新增时业务字段都要给(required 标注)
UpdateReq全量表单语义:提交全部业务字段,未改字段也回传原值,零值照样落库
DeleteReq单条删除,Id required
BatchDeleteReqIds []int64 + binding:"min=1"
StatusReq单列状态更新入口,只提交 ID 与目标状态

时间字段统一 *time.Time(模块内用 *LocalTime 包装输出格式);文件字段用 *multipart.FileHeader。

文件上传字段与解析逃生舱 ​

go
// internal/model/dto/example.go
type ExampleImportReq struct {
    File *multipart.FileHeader `form:"file" binding:"required" msg:"请选择文件" dc:"xls/xlsx 文件"`
}

// UnmarshalBody 实现 multipart 表单解析逃生舱(跳过默认 JSON 解析)
func (r *ExampleImportReq) UnmarshalBody(c *gin.Context) error {
    f, err := c.FormFile("file")
    if err != nil {
        return errors.New("请选择文件")
    }
    r.File = f
    return nil
}

任何结构体只要实现 UnmarshalBody(c *gin.Context) error 接口,绑定引擎就跳过默认解析,完全交给结构体自己处理(文件上传的唯一正确姿势)。

响应侧可选能力 ​

go
// ResMsg 实现 response.MsgProvider:成功通道直接弹出该提示语
func (r *ExampleImportRes) ResMsg() string {
    if r.Failed > 0 {
        return fmt.Sprintf("成功导入 %d 条,失败 %d 条", r.Success, r.Failed)
    }
    return fmt.Sprintf("成功导入 %d 条数据", r.Success)
}

编写检查清单 ​

  • [ ] 每个字段都有 json + form 标签(form 与 json 同名)
  • [ ] 请求字段有 dc 说明;校验字段有 binding + msg
  • [ ] 时间用 *LocalTime,批量删除用 Ids []int64
  • [ ] AddReq 全量 required,UpdateReq 全量不带 required 之外的过滤语义
  • [ ] 文件字段实现 UnmarshalBody

下一步 ​

→ 3.4 数据访问层(repository)

小蚂蚁云团队 · 提供技术支持