GoFrame学习随便记4(待续)

GoFrame虽然有一些模板代码生成工具,但对于基本的CRUD还是显得比较麻烦,所以我们改造yii2的gii,用它来生成样板代码。在yii2中,gii的代码在 vendor/yiisoft/yii2-gii/src下:views/default/index.php 中可以修改主页面上部的提示信息。views/default/view/files.php 决定了预览时,需要生成的文件和路径的显示,我们会修改它以便展示实际需要的golang文件的路径,唯一需要真正生成的文件是yii2 Model文件,因为我们会借用它的属性标签名(从数据库表注释生成或手动修改来改成中文)来得到接口定义中的中文描述。generators目录下的每个子目录对应了一种生成器,每个子目录里都有Generator.php生成类,主页面中以及对应生成器页面上部的文字描述其实是 Generator类中的getDescription() 方法的返回值。我们会改动Generator类的generate() 方法,$files 确定来最终生成的是哪些文件,每个文件是一个CodeFile实例(参数包括目标路径和实际渲染的模板,其中目标路径会和views/default/view/files.php 实际视图显示相关)。用来渲染的模板放在 default 子目录下,我们只需要使用 generators/model/default(名称Model/Logic Generator)generators/crud/default(名称CRUD Generator) 。前者目录下增加模板文件 iomodel.php (代表IO数据模型)logic.php (代表业务逻辑) ,另外增加一个 user-only.php (用户表涉及注册、登录、验证等比较复杂,不使用 iomodel.php和logic.php,而是把所有可能需要的多个文件生成在一个文件中,主要是IO数据模型和logic业务逻辑)。后者目录下的 controller.php 实际生成的是 API接口文件views/_form.php 实际生成 delete 动作文件 ,views/create.php|index.php|update.php|view.php 则是对应动作文件,增加 views/@api.http.php 作为API RESTful 请求测试文件,另外,同样增加一个 user-only.php 用来单独处理用户表(一个文件内包含了众多内容,主要是自定义数据验证规则、user|account两套API接口、user|account两套API接口测试、Auth中间件、user|account两套控制器动作)。

使用 gii 来生成样板代码是有一些约定的,例如 id 表示自增字段,使用 int64,时间戳字段用 created_at 和 updated_at(类型datetime对应 gtime.Time),用户表字段用 username和password,author_id 格式(即 xxx_id)表示这是一个外键(即对应另一表的对象,此外键不一定对应数据库外键),对 username 会生成唯一性自定义验证规则,对 author_id 生成外部对象必须存在自定义验证规则。对于需要根据实际手动调整的部分,标记上 @todo,部分为方便参考官网就直接注释中标注url。

用 tbl_post 描述大致工作流程。首先确保数据表已经存在,gf gen dao 已经生成有关文件。gf run main.go 已经运行。确保PHP本地Web服务器已经运行,gii可以工作。

在 Model/Logic Generator 中选择表 tbl_post ,自动产生模型名 Post,点 Preview 看看生成情况,点 Generate 在 yii2项目下生成 models/Post.php,打开这个文件,在 attributeLabels() 方法 中,键 chnName 对应值改成"博客",其余各键字段值改成所需的中文名称(如果数据库本身支持注释,可能不需要改)。再次在 Model/Logic Generator 中点击 Preview,点开 IO数据模型文件,把内容复制到internal/model/post.go(文件需要新建),去掉头部不需要的部分。

Go 复制代码
package model

import (
	"tempToDel/internal/model/entity"

	"github.com/gogf/gf/v2/os/gtime"
)

type PostIndexInput struct {
	Id        *int64
	Title     *string
	Content   *string
	Tags      *string // null
	Status    *int32
	CreatedAt *gtime.Time // null
	UpdatedAt *gtime.Time // null
	AuthorId  *int32
}

type PostIndexOutput struct {
	List []*entity.Post
}

type PostCreateInput struct {
	Title    *string
	Content  *string
	Tags     *string // null
	Status   *int32
	AuthorId *int32
}

type PostCreateOutput struct {
	Id int64
}

type PostUpdateInput struct {
	Id       *int64
	Title    *string
	Content  *string
	Tags     *string // null
	Status   *int32
	AuthorId *int32
}

type PostUpdateOutput struct{}

type PostDeleteInput struct {
	Id int64
}

type PostDeleteOutput struct{}

type PostViewInput struct {
	Id int64
}

type PostViewOutput struct {
	Post *entity.Post
}

点开 业务逻辑文件,把内容复制到internal/logic/post/post.go(文件需要新建),去掉头部不需要的部分。

Go 复制代码
package post

import (
	"context"
	"tempToDel/internal/dao"
	"tempToDel/internal/model"
	"tempToDel/internal/model/do"
	"tempToDel/internal/model/entity"
	"tempToDel/internal/service"

	"github.com/gogf/gf/v2/database/gdb"
	"github.com/gogf/gf/v2/errors/gcode"
	"github.com/gogf/gf/v2/errors/gerror"
)

type sPost struct{}

func init() {
	service.RegisterPost(New())
}

func New() *sPost {
	return &sPost{}
}

func (s *sPost) Index(ctx context.Context, in *model.PostIndexInput) (out *model.PostIndexOutput, err error) {
	out = &model.PostIndexOutput{}
	err = dao.Post.Ctx(ctx).Where(do.Post{
		Id:        in.Id,
		Title:     in.Title,
		Content:   in.Content,
		Tags:      in.Tags,
		Status:    in.Status,
		CreatedAt: in.CreatedAt,
		UpdatedAt: in.UpdatedAt,
		AuthorId:  in.AuthorId,
	}).
		//WhereLike("tags", "%" + *in.Tags + "%").  // @todo 是否有不同于相等的筛选条件 https://goframe.org/docs/core/gdb-chaining-query-where
		//OrderAsc(dao.Post.Columns().Code).  // @todo 是否有额外排序条件
		OrderAsc(dao.Post.Columns().Id).
		Scan(&out.List)
	return
}

func (s *sPost) Create(ctx context.Context, in *model.PostCreateInput) (out *model.PostCreateOutput, err error) {
	// 检查是否已经存在记录 (允许重复删除下面代码块,不允许则调整筛查条件)
	//    var cols = dao.Post.Columns()
	//    cnt, err := dao.Post.Ctx(ctx).
	//        Where(cols.Code, in.Code).        // @todo 筛查条件
	//        Where(cols.Type, in.Type).Count() // @todo 筛查条件
	//    if err != nil {
	//        return nil, err
	//    }
	//    if cnt > 0 {
	//        return nil, gerror.New("该类型此代码的条目已经存在")  // @todo 调整提示信息
	//    }
	// 执行插入
	insertId, err := dao.Post.Ctx(ctx).Data(do.Post{
		Title:    in.Title,
		Content:  in.Content,
		Tags:     in.Tags,
		Status:   in.Status,
		AuthorId: in.AuthorId,
	}).InsertAndGetId()
	if err != nil {
		return nil, err
	}
	out = &model.PostCreateOutput{
		Id: insertId,
	}
	return
}

func checkModelId(dbModel *gdb.Model, id any) error {
	if exist, err := dbModel.WherePri(id).Exist(); err != nil {
		return err
	} else if exist {
		return nil
	}
	return gerror.NewCodef(gcode.CodeNotFound, "The requested record (id=%d) not found", id)
}

func (s *sPost) Update(ctx context.Context, id int64, in *model.PostUpdateInput) (out *model.PostUpdateOutput, err error) {
	dbModel := dao.Post.Ctx(ctx)
	if err = checkModelId(dbModel, id); err != nil {
		return nil, err
	}
	_, err = dbModel.Data(do.Post{
		Title:    in.Title,
		Content:  in.Content,
		Tags:     in.Tags,
		Status:   in.Status,
		AuthorId: in.AuthorId,
	}).WherePri(id).Update()
	return
}

func (s *sPost) Delete(ctx context.Context, id int64) (out *model.PostDeleteOutput, err error) {
	dbModel := dao.Post.Ctx(ctx)
	if err = checkModelId(dbModel, id); err != nil {
		return nil, err
	}
	_, err = dbModel.WherePri(id).Delete()
	return
}

func (s *sPost) View(ctx context.Context, id int64) (out *model.PostViewOutput, err error) {
	dbModel := dao.Post.Ctx(ctx)
	if err = checkModelId(dbModel, id); err != nil {
		return nil, err
	}
	out = &model.PostViewOutput{
		Post: &entity.Post{},
	}
	err = dbModel.WherePri(id).Scan(out.Post)
	return
}

在 CRUD Generator中,Model Class部分输入 app\models\Post ,自动生成Controller Class,点 Preview 生成可预览的各文件,点开 API接口 ,把内容前后两部分分别复制到 internal/logic/custom_validators/post_author_exist.go (存在外键,故需额外新建自定义验证规则文件 ) 和 api/post/v1/post.go (需新建此API接口定义文件 ,且 PROJECT_NAME_XXX 需要改成你的项目根目录名),并且去掉头部不需要的部分,另外,规则中模型名根据 author_id 字段生成了 Author,但实际应该是 User,需要手动调整此类问题。

Go 复制代码
package custom_validators

import (
	"context"
	"tempToDel/internal/dao"
	"time"

	"github.com/gogf/gf/v2/database/gdb"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/util/gvalid"
)

const ValidatorPostAuthorExist = "post-author-exist"

func init() {
	gvalid.RegisterRule(ValidatorPostAuthorExist, RulePostAuthorExist) // 注册验证规则
}

// 外键校验规则(此外键不一定对应数据库外键)
func RulePostAuthorExist(ctx context.Context, in gvalid.RuleFuncInput) error {
	cnt, err := dao.User.Ctx(ctx). // 模型名可能不是 Author
		Cache(gdb.CacheOption{
			Duration: time.Hour,
			Name:     "",
			Force:    false,
		}).
		Where("id", in.Value.Int64()). // 这里 id 类型 int64
		Count()
	if err != nil {
		return err
	}
	if cnt == 0 {
		return gerror.Newf(`{field}: {value} does not exist`) // 覆盖 in.Message
	}
	return nil
}
Go 复制代码
package v1

import (
	_ "tempToDel/internal/logic/custom_validators"
	"tempToDel/internal/model/entity"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gtime"
)

type PostIndexReq struct {
	g.Meta    `path:"/post/index" method:"get" sm:"列表" tags:"博客"`
	Id        *int64      `json:"id" dc:"ID"`
	Title     *string     `json:"title" dc:"标题"`
	Content   *string     `json:"content" dc:"内容"`
	Tags      *string     `json:"tags" dc:"标签"` // allowNull
	Status    *int32      `json:"status" dc:"状态"`
	CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"` // allowNull
	UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"` // allowNull
	AuthorId  *int32      `json:"author_id" dc:"作者"`
}

type PostIndexRes struct {
	List []*entity.Post `json:"list" dc:"博客列表"`
}

type PostCreateReq struct {
	g.Meta   `path:"/post/create" method:"post" sm:"创建" tags:"博客"`
	Title    string `json:"title" v:"required|length:1,128" dc:"标题"`
	Content  string `json:"content" v:"required|length:1,4096" dc:"内容"`
	Tags     string `json:"tags" v:"length:1,4096" dc:"标签"`
	Status   int32  `json:"status" v:"required|integer" dc:"状态"`
	AuthorId int32  `json:"author_id" v:"required|integer|post-author-exist" dc:"作者"`
}

type PostCreateRes struct {
	Id int64 `json:"id" dc:"ID"`
}

type PostUpdateReq struct {
	g.Meta   `path:"/post/update/{id}" method:"post" sm:"更新" tags:"博客"`
	Id       int64   `json:"id" v:"required|integer" dc:"ID"`
	Title    *string `json:"title" v:"length:1,128" dc:"标题"`
	Content  *string `json:"content" v:"length:1,4096" dc:"内容"`
	Tags     *string `json:"tags" v:"length:1,4096" dc:"标签"`
	Status   *int32  `json:"status" v:"integer" dc:"状态"`
	AuthorId *int32  `json:"author_id" v:"integer|post-author-exist" dc:"作者"`
}

type PostUpdateRes struct{}

type PostDeleteReq struct {
	g.Meta `path:"/post/delete/{id}" method:"post" sm:"删除" tags:"博客"`
	Id     int64 `json:"id" v:"required|integer" dc:"ID"`
}

type PostDeleteRes struct{}

type PostViewReq struct {
	g.Meta `path:"/post/view/{id}" method:"get" sm:"详情" tags:"博客"`
	Id     int64 `json:"id" v:"required|integer" dc:"ID"`
}

type PostViewRes struct {
	Post *entity.Post `dc:"博客详情"`
}

点开 API接口测试文件,把内容复制到 api/post/v1/post-crud.http,去掉头部不需要的部分。

bash 复制代码
### GET post list
GET http://localhost:8000/post/index

### GET post list with filter
GET http://localhost:8000/post/index
Content-Type: application/json

{
  "Tags": "yii2",
  "AuthorId": 1
}

### POST post create
POST http://localhost:8000/post/create
Content-Type: application/json

{
  "Title": "api接口create",
  "Content": "接口 create 测试内容",
  "Tags": "goframe,web",
  "Status": 1,
  "AuthorId": 1
}

### POST post create when out object does not exist
POST http://localhost:8000/post/create
Content-Type: application/json

{
  "Title": "api接口create",
  "Content": "接口 create 测试内容",
  "Tags": "goframe,web",
  "Status": {{$random.integer()}},
  "AuthorId": 99999999
}

### POST post update
POST http://localhost:8000/post/update/3
Content-Type: application/json

{
  "Title": "api接口create",
  "Content": "接口 create 测试内容,修改后",
  "Tags": "goframe,web",
  "Status": 1,
  "AuthorId": 1
}

### POST post update when out object does not exist
POST http://localhost:8000/post/update/7
Content-Type: application/json

{
  "Title": "api接口create",
  "Content": "接口 create 测试内容,修改2",
  "Tags": "goframe,web",
  "Status": 1,
  "AuthorId": 99999999
}

### GET post view
GET http://localhost:8000/post/view/3
Content-Type: application/json


### POST post delete
POST localhost:8000/post/delete/3

### end

在继续之前,请确保 gf gen ctrl 和 gf gen service ,也就是根据API接口生成控制器动作(如果存在此前生成的错误的控制器动作,必须删除才能重新生成,可以整个控制器文件夹删除再重新生成)和根据业务逻辑生成服务接口。接下来就是分别点开 控制器动作-删除、控制器动作-新建、控制器动作-列表、控制器动作-更新、控制器动作-详情 ,把方法函数体部分复制到 internal/controller/post/post_v1_delete|create|index|update|view.go对应方法内,头部 import 部分需要去除不再被引用的部分。

Go 复制代码
func (c *ControllerV1) PostDelete(ctx context.Context, req *v1.PostDeleteReq) (res *v1.PostDeleteRes, err error) {
	_, err = service.Post().Delete(ctx, req.Id)
	return
}

--------------------------------------

func (c *ControllerV1) PostCreate(ctx context.Context, req *v1.PostCreateReq) (res *v1.PostCreateRes, err error) {
	var out *model.PostCreateOutput
	out, err = service.Post().Create(ctx, &model.PostCreateInput{
		Title:    &req.Title,    // 根据需要设定必需字段
		Content:  &req.Content,  // 根据需要设定必需字段
		Tags:     &req.Tags,     // 根据需要设定必需字段
		Status:   &req.Status,   // 根据需要设定必需字段
		AuthorId: &req.AuthorId, // 根据需要设定必需字段
	})
	if err != nil {
		return
	}
	res = &v1.PostCreateRes{
		Id: out.Id,
	}
	return
}

---------------------------------------

func (c *ControllerV1) PostIndex(ctx context.Context, req *v1.PostIndexReq) (res *v1.PostIndexRes, err error) {
	var out *model.PostIndexOutput
	out, err = service.Post().Index(ctx, &model.PostIndexInput{
		Id:        req.Id,        // 根据需要设定筛选字段
		Title:     req.Title,     // 根据需要设定筛选字段
		Content:   req.Content,   // 根据需要设定筛选字段
		Tags:      req.Tags,      // 根据需要设定筛选字段
		Status:    req.Status,    // 根据需要设定筛选字段
		CreatedAt: req.CreatedAt, // 根据需要设定筛选字段
		UpdatedAt: req.UpdatedAt, // 根据需要设定筛选字段
		AuthorId:  req.AuthorId,  // 根据需要设定筛选字段
	})
	if err != nil || out == nil {
		return
	}
	res = &v1.PostIndexRes{}
	res.List = out.List
	return
}

----------------------------------------

func (c *ControllerV1) PostUpdate(ctx context.Context, req *v1.PostUpdateReq) (res *v1.PostUpdateRes, err error) {
	_, err = service.Post().Update(ctx, req.Id, &model.PostUpdateInput{
		Title:    req.Title,    // 根据需要设定必需字段
		Content:  req.Content,  // 根据需要设定必需字段
		Tags:     req.Tags,     // 根据需要设定必需字段
		Status:   req.Status,   // 根据需要设定必需字段
		AuthorId: req.AuthorId, // 根据需要设定必需字段
	})
	return
}

----------------------------------------

func (c *ControllerV1) PostView(ctx context.Context, req *v1.PostViewReq) (res *v1.PostViewRes, err error) {
	var out *model.PostViewOutput
	out, err = service.Post().View(ctx, req.Id)
	if err != nil || out == nil {
		return
	}
	res = &v1.PostViewRes{}
	res.Post = out.Post
	return
}

在 internal/cmd/cmd.go 添加 post.NewV1(), 注册控制器 (初始开发没必要放到需要验证,可以后期调整)。观察gf run main.go控制台输出,确保已经没有错误。调整 API接口测试文件 api/post/v1/post-crud.http 中有关 JSON参数和 URL中 id 部分,大致按以下步骤测试: 列表、带过滤条件的列表、创建-查看-外部对象不存在的创建、更新-查看-外部对象不存在的更新、查看-删除-查看。

相关推荐
lucky_chaichai1 小时前
学习《以openclaw为例介绍AI Agent的运作原理》
人工智能·学习
前端小趴菜~时倾1 小时前
python爬虫学习第二课-流程控制
爬虫·python·学习
浅念-2 小时前
C++ 异常
开发语言·数据结构·数据库·c++·经验分享·笔记·学习
知识分享小能手2 小时前
Redis入门学习教程,从入门到精通,Redis服务配置知识点详解(3)
数据库·redis·学习
handler012 小时前
基础算法:BFS
开发语言·数据结构·c++·学习·算法·宽度优先
ycjunhua2 小时前
Gool NoteBookLM 创建无法进入开发界面
笔记·学习
weixin_443478512 小时前
flutter组件学习之Cupertino 组件(iOS风格)
学习·flutter·ios
finegx2 小时前
反汇编objdump和strace学习
linux·经验分享·学习
Shining05962 小时前
前沿模型系列(三)《检索增强的语言模型》
人工智能·学习·其他·语言模型·自然语言处理·大模型·rag