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 部分,大致按以下步骤测试: 列表、带过滤条件的列表、创建-查看-外部对象不存在的创建、更新-查看-外部对象不存在的更新、查看-删除-查看。