# 开发流程

# dao/do/entity 生成

在项目根目录下执行make dao(gf gen dao)生成对应的dao/do/entity文件:

dao # 通过对象方式访问底层数据源,底层基于ORM组件实现
do  # 数据转换模型,业务模型到数据模型的转换,由工具维护,用户不能修改。每次生成代码文件将会被覆盖
entity # 数据模型,由工具维护,用户不能修改。每次生成代码文件将会被覆盖

# dao中的internal/dao/internal/user.go

# 用于封装对数据表user的访问。该文件自动生成了一些数据结构和方法,简化对数据表的CRUD操作
# 该文件每次生成都会覆盖,由开发工具自动维护,开发者无需关心
  • 代码详解
// UserDao 结构体:封装表名、数据库组、列名和自定义处理器
type UserDao struct {
	table    string            
	group    string            
	columns  UserColumns        
	handlers []gdb.ModelHandler
}

// UserColumns结构体:存储字段名常量(id、name、status、age)避免硬编码
type UserColumns struct {
	Id     string
	Name   string
	Status string
	Age    string
}

// UserColumns结构体:赋值
var userColumns = UserColumns{
	Id:     "id",
	Name:   "name",
	Status: "status",
	Age:    "age",
}

// 个工厂函数,用于创建并初始化 UserDao 实例
// 可以传入 0 个或多个 ModelHandler 函数
// 返回值 *UserDao:返回 UserDao 结构体的指针
func NewUserDao(handlers ...gdb.ModelHandler) *UserDao {
	// 结构体初始化
	return &UserDao{
		group:    "default",   // 数据库配置组名,使用默认配置
		table:    "user",      // 操作的数据库表名
		columns:  userColumns, // 列名集合(之前定义的变量)
		handlers: handlers,    // 自定义处理器切片
	}
}

// 根据配置组名获取对应的数据库连接,返回底层数据库对象
func (dao *UserDao) DB() gdb.DB {
	return g.DB(dao.group)
}

// 返回表名 "user"
func (dao *UserDao) Table() string {
	return dao.table
}

// 返回列名结构体
func (dao *UserDao) Columns() UserColumns {
	return dao.columns
}

// 返回数据库配置组名 "default"
func (dao *UserDao) Group() string {
	return dao.group
}

// DAO 的核心方法,用于创建带上下文的数据库查询模型
// 参数 ctx context.Context:接收上下文,用于传递请求级别的信息(如超时、追踪ID等)
// 返回值 *gdb.Model:返回 GoFrame 的查询构建器对象
func (dao *UserDao) Ctx(ctx context.Context) *gdb.Model {
	// 创建基础模型,得到一个基础的查询构建器
	model := dao.DB().Model(dao.table)
	// 遍历所有注册的ModelHandler函数,每个handler接收当前model,返回修改后的model
	// 典型应用场景:
	// 软删除过滤:m.Where("deleted_at", nil)
	// 数据权限控制:m.Where("tenant_id", tenantId)
	// 自动添加条件:m.Where("status", 1)
	for _, handler := range dao.handlers {
		model = handler(model)
	}
	// 设置安全模式和上下文
	// 启用安全模式,防止误操作(如不带条件的 UPDATE/DELETE)
	// 绑定上下文,支持:请求链路追踪、超时控制、日志记录
	return model.Safe().Ctx(ctx)
}

// 事务封装方法,简化了数据库事务的使用,自动化管理:无需手动调用 Begin/Commit/Rollback
// f func(ctx context.Context, tx gdb.TX) error 回调函数:包含需要在事务中执行的业务逻辑
// ctx:上下文(可传递给子操作)
// tx gdb.TX:事务对象,用于执行 SQL
// 返回值:error,决定事务提交还是回滚
func (dao *UserDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
	return dao.Ctx(ctx).Transaction(ctx, f)
}
  • 使用示例
// 带处理器(创建时传入)
dao := NewUserDao(
    func(m *gdb.Model) *gdb.Model {
        return m.Where("status", 1) // 只查询正常状态
    },
)

// 基本查询
users := dao.Ctx(ctx).Where("age > ?", 18).All()

// 事务使用
err := userDao.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
    // 步骤1:扣除转出账户余额
    _, err := tx.Exec("UPDATE user SET balance = balance - 100 WHERE id = ?", fromId)
    if err != nil {
        return err  // 触发回滚
    }
    
    // 步骤2:增加转入账户余额
    _, err = tx.Exec("UPDATE user SET balance = balance + 100 WHERE id = ?", toId)
    if err != nil {
        return err  // 触发回滚
    }
    
    return nil  // 所有操作成功,自动提交
})
if err != nil {
    log.Fatal("转账失败,已回滚")
}

# dao中的internal/dao/user.go

# 对internal/dao/internal/user.go的进一步封装,用于供其他模块直接调用访问
# 该文件开发者可以随意修改,或者扩展dao的能力
  • 代码详解
// 小写 userDao:私有结构体,外部包无法直接创建

type userDao struct {
	*internal.UserDao // 通过匿名组合(Embedding)继承内部 DAO 的所有方法
}

// 分组声明的写法,
// User:大写字母开头,是公开的全局变量
// 单例模式:整个应用共享同一个 User 实例
var (
    // 初始化包装结构体
	User = userDao{
		internal.NewUserDao()
	}
)

// 单独声明的方式,两种写法在功能上完全等价
// 多个变量:推荐用 var () 分组,更符合 Go 社区惯例
// var User = userDao{internal.NewUserDao()}
  • 可以在 userDao 中添加业务特定的查询方法
// 在 user.go 中可以添加自定义方法
func (d *userDao) FindActiveUsers(ctx context.Context) ([]*entity.User, error) {
    var users []*entity.User
    err := d.Ctx(ctx).Where("status", 1).Scan(&users)
    return users, err
}

// 调用
activeUsers, _ := dao.User.FindActiveUsers(ctx)

# api 请求/输出/接口

type CreateReq struct {
	g.Meta `path:"/user" method:"post" tags:"User" summary:"Create user"`
	Name   string `v:"required|length:3,10" dc:"user name"`
	Age    uint   `v:"required|between:18,200" dc:"user age"`
}
type CreateRes struct {
	Id int64 `json:"id" dc:"user id"`
}

注意

  • 校验规则在调用路由函数之前就已经由GoFrame框架的Server自动执行了
  • 如果请求参数校验失败,会立即返回错误,不会进入到路由函数
  • 删除接口
type DeleteReq struct {
    g.Meta `path:"/user/{id}" method:"delete" tags:"User" summary:"Delete user"`
    Id     int64 `v:"required" dc:"user id"`
}
type DeleteRes struct{}
  • 更新接口
// 定义了一个用户状态类型Status
type Status int

const (
    StatusOK       Status = 0 // User is OK.
    StatusDisabled Status = 1 // User is disabled.
)

type UpdateReq struct {
    g.Meta `path:"/user/{id}" method:"put" tags:"User" summary:"Update user"`
    Id     int64   `v:"required" dc:"user id"`
    Name   *string `v:"length:3,10" dc:"user name"`
    Age    *uint   `v:"between:18,200" dc:"user age"`
    Status *Status `v:"in:0,1" dc:"user status"`
}
type UpdateRes struct{}

注意

  • 接口参数我们使用了指针来接收,目的是避免类型默认值对我们修改接口的影响
  • 举个例子,假如Status不定义为指针,那么它就会有默认值0的影响
  • 那么在处理逻辑中,很难判断到底调用端有没有传递该参数,是否要真正修改数值为0
  • 但我们使用指针后,当用户没有传递该参数时,该参数的默认值为nil,处理逻辑便很好做判断
  • 查询接口(单个)
type GetOneReq struct {
    g.Meta `path:"/user/{id}" method:"get" tags:"User" summary:"Get one user"`
    Id     int64 `v:"required" dc:"user id"`
}
type GetOneRes struct {
    *entity.User `dc:"user"`
}
  • 查询接口(列表)
type GetListReq struct {
    g.Meta `path:"/user" method:"get" tags:"User" summary:"Get users"`
    Age    *uint   `v:"between:18,200" dc:"user age"`
    Status *Status `v:"in:0,1" dc:"user status"`
}
type GetListRes struct {
    List []*entity.User `json:"list" dc:"user list"`
}

# controller 代码生成

通过make ctrl命令(或者gf gen ctrl)生成控制器代码,生成的代码主要包含3类文件:

api接口抽象文件
# 用于保证控制器实现的接口完整性,该文件由开发工具维护,开发者无需关心

controller路由对象管理
# 用于管理控制器的初始化,以及一些控制内部使用的数据结构、常量定义
# user.go 是一个空文件,可用于定义一些控制器内部使用的数据结构、常量等内容
# user_new.go 文件是自动生成的路由对象创建文件,这两个文件只会生成一次,随后开发者可以随意修改

controller路由实现代码
# 用于具体的api接口实现的代码文件
# 默认情况下,一个api接口生成一个源码文件。也可以控制按照api文件定义的接口生成到一个源码文件中

为何存在一个空的 go 文件

  • 该文件只会生成一次,用户可以在里面填充必要的预定义代码内容
  • 例如,该模块 controller 内部使用的变量、常量、数据结构定义,或者包初始化 init 方法等

# logic 业务逻辑实现

// 务逻辑的具体实现类
type sUser struct{}

// 自动注册机制:包被导入时自动执行
// 只有在生成完成接口文件后,您才能在每个业务模块中加上接口的具体实现注入
func init() {
	service.RegisterUser(New())
}

// 是一个构造函数,典型的工厂模式,用于创建服务层实例
func New() service.IUser {
	return &sUser{}
}
  • 创建逻辑实现
func (s *sUser) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
	id, err := dao.User.Ctx(ctx).Data(do.User{
		Name:   req.Name,
		Age:    req.Age,
		Status: v1.StatusOK,
	}).InsertAndGetId()
	if err != nil {
		return nil, err
	}
	res = &v1.CreateRes{
		Id: id,
	}
	return
}
  • 删除接口
func (s *sUser) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) {
	// WherePri方法,该方法会将给定的参数req.Id作为主键进行Where条件限制
	_, err = dao.User.Ctx(ctx).WherePri(req.Id).Delete()
	return
}
  • 更新接口
func (s *sUser) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
	//_, err  = dao.User.Ctx(ctx).Data(g.Map{
	//	"name": req.Name,
	//	"age":  req.Age,
	//}).Where("id", req.Id).Update()
	//return

	_, err = dao.User.Ctx(ctx).Data(do.User{
		Name:   req.Name,
		Age:    req.Age,
		Status: v1.StatusOK,
	}).WherePri(req.Id).Update()
	return
}
  • 查询接口(单个)
func (s *sUser) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) {
	res = &v1.GetOneRes{} // 创建一个空的 GetOneRes 实例,因为 Scan(&res.User) 需要一个有效的指针来填充数据
	err = dao.User.Ctx(ctx).WherePri(req.Id).Scan(&res.User)
	return 
}

注意

  • &res.User中的User属性对象其实是没有初始化的,其值为nil
  • 如果查询到了数据,Scan方法会对其做初始化并赋值
  • 如果查询不到数据,那么Scan方法什么都不会做,其值还是nil
  • 查询接口(多个)
func (s *sUser) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) {
	res = &v1.GetListRes{}
	err = dao.User.Ctx(ctx).Where(do.User{
		Age:    req.Age,
		Status: req.Status,
	}).Scan(&res.List)
	 return
}

# service 服务接口层

gf gen service 通过分析给定的 logic 业务逻辑模块下的代码,自动生成 service 代码,包含三部分:

service   # 生成接口文件代码
logic.go  # 接口实现注册文件,用于在程序启动时,将接口的具体实现在启动时执行注册
main.go   # 最顶部 import _ "demo/internal/logic" 后加空行。若还引入了packed包,放到packed后面

注意

  • 由于该命令是根据业务模块生成 service 接口,因此只会解析二级目录下的 go 代码文件,并不会无限递归分析代码文件。以 logic 目录为例,该命令只会解析 logic/xxx/*.go 文件。因此,需要 logic 层代码结构满足一定规范。
  • 不同业务模块中定义的结构体名称在生成的 service 接口名称时可能会重复覆盖,因此需要在设计业务模块时保证名称不能冲突。
type (
	IUser interface {
		Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
		Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error)
		Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error)
		GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error)
		GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error)
	}
)

var (
	localUser IUser
)

func User() IUser {
	if localUser == nil {
		panic("implement not found for interface IUser, forgot register?")
	}
	return localUser
}

func RegisterUser(i IUser) {
	localUser = i
}