★ 实战问答:规范路由实现用户 CRUD 的完整套路
面试问法:现场写一个“用户模块 CRUD”,你会怎么组织 GoFrame 代码? 一句话结论:八步套路:建表 →
gf gen dao生成数据层 → api 定义 Req/Res(含 g.Meta 路由与 v 校验)→ controller 薄接线 → service 实现业务 → 分组绑定 + 统一响应中间件 → 补分页/错误码 → 测试与文档。关键在“controller 薄、service 厚、api 即文档”。
八步套路
| 步骤 | 产出 | 要点 |
|---|---|---|
| 1 建表 | 表结构 | 字段注释写清楚,会进生成代码 |
| 2 gen dao | dao/do/entity | hack/config.yaml 配 gfcli.gen.dao |
| 3 定义 api | Req/Res | g.Meta 声明 path/method,v 标签校验 |
| 4 生成/手写 controller | 接口骨架 | 签名 (ctx, req) (res, err) |
| 5 service 实现 | 业务逻辑 | CRUD、密码哈希、校验业务规则 |
| 6 注册绑定 | 路由生效 | group.Bind + 统一响应中间件 |
| 7 公共能力 | 分页/错误码 | Page+ScanAndCount、gerror.NewCode |
| 8 验证 | 文档/测试 | openapiPath 出文档,接口测试回归 |
骨架代码
// api/user/v1/user.go —— 接口即定义
type UserCreateReq struct {
g.Meta `path:"/user" method:"post" tags:"User" summary:"create user"`
Name string `v:"required|length:2,30" json:"name"`
Password string `v:"required|length:6,64" json:"password"`
}
type UserCreateRes struct{ Id int64 `json:"id"` }
type UserGetOneReq struct {
g.Meta `path:"/user/{id}" method:"get" tags:"User" summary:"get user"`
Id int64 `v:"required" in:"path" json:"id"`
}
type UserGetOneRes struct {
Id int64 `json:"id"`
Name string `json:"name"`
}
// internal/service(或 logic)—— 业务实现
func Create(ctx context.Context, in *v1.UserCreateReq) (out *v1.UserCreateRes, err error) {
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) // 框架未内置哈希,选型以官方为准
if err != nil { return nil, err }
id, err := dao.User.Ctx(ctx).Data(do.User{Name: in.Name, Password: string(hash)}).InsertAndGetId()
if err != nil { return nil, err }
return &v1.UserCreateRes{Id: id}, nil
}
func GetOne(ctx context.Context, in *v1.UserGetOneReq) (out *v1.UserGetOneRes, err error) {
var user entity.User
if err = dao.User.Ctx(ctx).Where("id", in.Id).Scan(&user); err != nil { return nil, err }
if user.Id == 0 { return nil, gerror.NewCode(code.UserNotExist) } // 业务错误码 >1000
return &v1.UserGetOneRes{Id: user.Id, Name: user.Name}, nil
}
// main:统一响应 + 分组绑定;大量接口时用 controller 包 + gen ctrl 生成骨架
s := g.Server()
s.Use(ghttp.MiddlewareHandlerResponse)
s.Group("/api", func(group *ghttp.RouterGroup) {
group.Bind(new(controller.User))
})
s.Run()
常见追问
- 追问:为什么 controller 要薄?——参数绑定/校验/路由由规范路由承接,controller 只做编排;业务放 service 才能被多个入口复用(HTTP/gRPC/定时任务)。
- 追问:校验失败返回什么?——规范路由自动校验失败即停并返回错误,经统一响应中间件输出非零 code;前端按统一结构解析即可。
- 追问:列表带条件怎么加?——Where 链按需拼接,分页用 Page+ScanAndCount(见分页章节),排序字段白名单化。
记忆点
- 口诀:表→dao→api→controller→service→Bind→公共能力→文档测试;api 即文档、controller 薄、service 厚。