Write the Code. Change the World.

9月 06

做项目。一般都会选择框架。很少会直接从 0 开始撸的。之所以选择框架,是因为框架既帮你做好了架构,又准备好了很多工具,还定制了一些规范。适合快速稳定的做项目。一般一个语言对应的开源框架会比较多,选一个或两个应用到项目中就可以了。准备进入 go 领域,也得选一个框架。初步了解了 gin 和 goframe。这里还是选择 goframe。gin 还是太轻了,项目中需要的一些工具不够多和完善。

官网: https://goframe.org/

安装升级

这里想做一个接口服务项目。仅仅提供接口给到前端使用就好。现在大多业务场景都是前后端分离的。比如app、小程序、管理后台这些。app,小程序是必须分离。管理后台也可以不分离,但分离起来更好写更爽。

按照官网文档,先安装好 gf。当前最新版本是 2.5.2。

创建项目。

gf init goSimpleAdmin

cd goSimpleAdmin

# 查看版本信息
gf -v

GoFrame CLI Tool v2.5.2, https://goframe.org
GoFrame Version: v2.3.1 in current go.mod
CLI Installed At: /Users/vini123/go/bin/gf
CLI Built Detail:
  Go Version:  go1.20.6
  GF Version:  v2.5.2
  Git Commit:  2023-08-18 09:57:25 cf299273c499aafb5d19829809dfb6b8ef280c12
  Build Time:  2023-08-18 16:23:18

发现 goframe 版本是 2.3.1。可以对刚创建的项目进行升级。在升级前,先做好版本控制。

git init -b main
git add .
git commit -m 'goframe initialize'

使用 gf up 对项目进行升级。

创建入口和出口

只有先定义和输入和输出,才可以用过 gf gen ctrl 来生成 controller 文件。这里就先从用户开始吧。有获取验证码、用户登录、用户注册这三个。

接口规范文档:https://goframe.org/pages/viewpage.action?pageId=47703679

按照规范。必须得在 api 目录下定义。我先把默认的 hello 删掉,再在 api 先建立一个 user 目录,再在 user 目录下建立 admin 目录,不使用 v1 目录了。默认 demo 是 v1 的。就是因为想做管理后台哈,用 admin 更适合。文件结构如下。

.
├── api
│   └── user
│       └── admin
│           └── user.go

user.go 中定义输入输出。定义的内容如下。

package admin

import "github.com/gogf/gf/v2/frame/g"

// 请求数字验证码
type CaptchaReq struct {
    g.Meta `path:"/captcha" method:"get" tags:"CaptchaService" summary:"build captcha"`
}

type CaptchaRes struct {
    Id string
}

// 登录
type SignInReq struct {
    g.Meta   `path:"/user/sign-in" method:"post" tags:"UserService" summary:"user sign in"`
    Passport string `v:"required|length:2,64#请输入账号|账号长度为 {min} 到 {max} 位"`
    Password string `v:"required|length:6,16#请输入密码|密码长度为 {min} 到 {max} 位"`
}

type SignInRes struct{}

// 注册(只需要数字验证码就可以)
type SignUpReq struct {
    g.Meta     `path:"/user/sign-up" method:"post" tags:"UserService" summary:"user sign up"`
    Nickname   string `v:"required|length:2,12#请输入昵称|昵称长度为 {min} 到 {max} 位" json:"nickname"`
    Passport   string `v:"required|length:2,64#请输入账号|账号长度为 {min} 到 {max} 位" json:"passport"`
    Password   string `v:"required|length:6,16#请输入密码|密码长度为 {min} 到 {max} 位" json:"password"`
    VerifyKey  string `v:"required|length:12#缺少验证KEY|验证KEY长度为 12 位" json:"verify_key"`
    VerifyCode string `v:"required|length:4#请输入验证码|验证码长度为 4 位" json:"verify_code"`
}

type SignUpRes struct{}

先初步这么定义,后边也可以调整。观察下命名,都是成对的出现的。 ReqRes,就是 request 和 response。感觉一切一切都离不开请求和响应。也可以说是输入和输出。念念不忘,必有回响。

结构体中, v 是用来做 validate 规则验证的标签。请看数据校验。

https://goframe.org/pages/viewpage.action?pageId=1114367

然后就可以生成 controller 了.

gf gen ctrl

# 输出以下结果。
generated: /Users/vini123/Code/zhoulin/open/goSimpleAdmin/api/user/user.go
generated: internal/controller/user/user.go
generated: internal/controller/user/user_new.go
generated: internal/controller/user/user_admin_captcha.go
generated: internal/controller/user/user_admin_sign_in.go
generated: internal/controller/user/user_admin_sign_up.go
done!

# 目录结构这样的
.
└── user
    ├── user.go
    ├── user_admin_captcha.go
    ├── user_admin_sign_in.go
    ├── user_admin_sign_up.go
    └── user_new.go

# 顺便在 api/user 下,生成了一个 user.go 文件。

看看这些文件的里的内容是啥。

api/user/user.go

// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. 
// =================================================================================

package user

import (
    "context"

    "goSimpleAdmin/api/user/admin"
)

type IUserAdmin interface {
    Captcha(ctx context.Context, req *admin.CaptchaReq) (res *admin.CaptchaRes, err error)
    SignIn(ctx context.Context, req *admin.SignInReq) (res *admin.SignInRes, err error)
    SignUp(ctx context.Context, req *admin.SignUpReq) (res *admin.SignUpRes, err error)
}

user_admin_captcha.go

package user

import (
    "context"

    "github.com/gogf/gf/v2/errors/gcode"
    "github.com/gogf/gf/v2/errors/gerror"

    "goSimpleAdmin/api/user/admin"
)

func (c *ControllerAdmin) Captcha(ctx context.Context, req *admin.CaptchaReq) (res *admin.CaptchaRes, err error) {
    return nil, gerror.NewCode(gcode.CodeNotImplemented)
}

user_admin_sign_in.go

package user

import (
    "context"

    "github.com/gogf/gf/v2/errors/gcode"
    "github.com/gogf/gf/v2/errors/gerror"

    "goSimpleAdmin/api/user/admin"
)

func (c *ControllerAdmin) SignIn(ctx context.Context, req *admin.SignInReq) (res *admin.SignInRes, err error) {
    return nil, gerror.NewCode(gcode.CodeNotImplemented)
}

user_admin_sign_up

package user

import (
    "context"

    "github.com/gogf/gf/v2/errors/gcode"
    "github.com/gogf/gf/v2/errors/gerror"

    "goSimpleAdmin/api/user/admin"
)

func (c *ControllerAdmin) SignUp(ctx context.Context, req *admin.SignUpReq) (res *admin.SignUpRes, err error) {
    return nil, gerror.NewCode(gcode.CodeNotImplemented)
}

user_new.go

// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. 
// =================================================================================

package user

import (
    "goSimpleAdmin/api/user"
)

type ControllerAdmin struct{}

func NewAdmin() user.IUserAdmin {
    return &ControllerAdmin{}
}

user.go

// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================

package user

以上这些代码,都是框架生成的。是不是像介绍说的一样,很方便,也是不是都统一规范了。这还只是生成一个 controller。下边来生成 dao 和 model 文件。

配置业务使用的数据库以及工具使用的数据库

dao 和 model 文件是可以和上边的操作并行的。在创建方面,和上边的操作没有联系没有耦合。但在意义和使用上是有联系的。创建 dao 和 model 必须得先连数据库。那就得先配置。

goframe 将工具配置和业务配置分开的。

之所以要先配置数据库信息。是因为在使用 gf gen dao 的时候需要用到。既然使用框架,既然有这么好(方便、统一规范)的 cli 工具。那就要用上。

配置管理:https://goframe.org/pages/viewpage.action?pageId=57183754

工具配置:https://goframe.org/pages/viewpage.action?pageId=1114260

工具配置文件路径优先查找 hack 目录(hack/config.yaml),其次,按照开光家默认的配置路径检索配置文件。把数据库配置起来。

# hack/config.yaml

# CLI tool, only in development environment.
# https://goframe.org/pages/viewpage.action?pageId=3673173
gfcli:
  docker:
    build: "-a amd64 -s linux -p temp -ew"
    tagPrefixes:
      - my.image.pub/my-app

  gen:
    dao:
    - link:  "mysql:homestead:secret@tcp(192.168.56.56:3306)/go-simple-admin"
      tables: "user"

就先配置这么多。这个表可以随时添加的和修改的。执行 gf gen dao 的时候,就会连接数据库,根据表的信息生成 dao 和 model 文件。这个后边再说。

再来配置业务数据库以及端口这些。

业务配置文件存放于 manifest/config 目录下, 业务配置文件往往不提交到版本库中。觉得这块应该搞个像 .env.example 这样的东西出来。

server:
  address:     ":9885"
  openapiPath: "/api.json"
  swaggerPath: "/swagger"

logger:
  path:                  "storage/logs/daily"  # 日志文件路径。默认为空,表示关闭,仅输出到终端
  file:                  "{Y-m-d}.log"         # 日志文件格式。默认为"{Y-m-d}.log"
  prefix:                ""                    # 日志内容输出前缀。默认为空
  level:                 "all"                 # 日志输出级别
  timeFormat:            "2006-01-02T15:04:05" # 自定义日志输出的时间格式,使用Golang标准的时间格式配置
  ctxKeys:               []                    # 自定义Context上下文变量名称,自动打印Context的变量到日志中。默认为空
  header:                true                  # 是否打印日志的头信息。默认true
  stdout:                true                  # 日志是否同时输出到终端。默认true
  rotateSize:            0                     # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性
  rotateExpire:          0                     # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性
  rotateBackupLimit:     0                     # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除
  rotateBackupExpire:    0                     # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除
  rotateBackupCompress:  0                     # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩
  rotateCheckInterval:   "1h"                  # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时
  stdoutColorDisabled:   false                 # 关闭终端的颜色打印。默认开启
  writerColorEnable:     false                 # 日志文件是否带上颜色。默认false,表示不带颜色
  logger1:
    path:   "storage/losgs/admin/"
    file:   "{Y-m-d}.log"
    stdout: true

database:
  logger:
    path: "storage/logs/mysql"
    level: "all"
    stdout: true
  default:
    link: "mysql:homestead:secret@tcp(192.168.56.56:3306)/go-simple-admin"
    debug: true

这里不仅配置了服务的端口信息,还配置了日志和数据库信息。

生成 dao 文件和 model 文件

❯ gf gen dao
generated: internal/dao/user.go
generated: internal/dao/internal/user.go
generated: internal/model/do/user.go
generated: internal/model/entity/user.go
done!

dao 和 model 文件,可以一直重复生成,生的样子是和数据库里的表相关的。当然也和工具配置相关。暂时,只配置了一个 user 表,生成了四个文件。

├── dao
│   ├── internal
│   │   └── user.go
│   └── user.go
├── model
│   ├── do
│   │   └── user.go
│   └── entity
│       └── user.go

下边来生成 service 文件

生成 service 文件,是由 logic 文件生成。换句话说,就是先写好 logic 文件,再去生成 service 文件。 logic 文件是啥,logic 才是业务逻辑真正的地方。在生成 service 之前,可以先完成 logic 的雏形。logic 会用到具体的 model,这里我们新建一个具体的 model。

# model/admin/user.go
package admin

type SigninReq struct{}

type SigninRes struct{}

以用户登录为例。

package user

import (
    "context"
    "goSimpleAdmin/internal/model"
)

type sUser struct{}

func init() {
}

func New() {
}

func (s *sUser) SignIn(ctx context.Context, in model.SignInInput) (err error) {
    return nil
}

先就写上边这么多,是个引子(控制器想要调用的方法)。然后执行。

❯ gf gen service
generating service go file: internal/service/user.go
generating init go file: internal/logic/logic.go
gofmt go files in "internal/service"
update main.go
done!

生成好 service 后,再回过头来完善 logic。

package user

import (
    "context"
    "goSimpleAdmin/internal/model"
    "goSimpleAdmin/internal/service"
)

type sUser struct{}

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

func New() service.IUser {
    return &sUser{}
}

func (s *sUser) SignIn(ctx context.Context, in model.SignInInput) (err error) {
    return nil
}

在 logic 文件的 init 函数中,将 service 的接口对象 和 logic 进行绑定。绕来绕去,控制器调用的就是 logic 中的方法。通过 logic 中的方法,来生成 service,就是对应 logic 的接口。

搞来搞起,搞了上边那么多。现在关键时候来了。让控制器把服务调用起来。

修改 user_admin_sign_in.go

package user

import (
    "context"

    "goSimpleAdmin/api/user/admin"
    "goSimpleAdmin/internal/model"
    "goSimpleAdmin/internal/service"
)

func (c *ControllerAdmin) SignIn(ctx context.Context, req *admin.SignInReq) (res *admin.SignInRes, err error) {
    service.User().SignIn(ctx, model.SignInInput{
        Passport: req.Passport,
        Password: req.Passport,
    })
    return
}

业务是都串联起来了。那么入口呢。 入口就在路由的绑定上,要把 controller 中的 user 对象绑定到路由上。修改 cmd/cmd.go

package cmd

import (
    "context"
    "goSimpleAdmin/internal/controller/user"

    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
    "github.com/gogf/gf/v2/os/gcmd"
)

var (
    Main = gcmd.Command{
        Name:  "main",
        Usage: "main",
        Brief: "start http server",
        Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
            s := g.Server()
            s.Use(ghttp.MiddlewareHandlerResponse)
            s.Group("/api/admin", func(group *ghttp.RouterGroup) {

                group.Bind(
                    user.NewAdmin(),
                )
            })
            s.Run()
            return nil
        },
    }
)

绑也绑了,串也串了。运行一下下看。

go run main.go

❯ go run main.go
2023-09-06 19:57:38.553 [INFO] pid[56394]: http server started listening on [:9885]
2023-09-06 19:57:38.553 [INFO] {20fa99f1594d82178bf09e11e95f3c4b} swagger ui is serving at address: http://127.0.0.1:9885/swagger/
2023-09-06 19:57:38.554 [INFO] {20fa99f1594d82178bf09e11e95f3c4b} openapi specification is serving at address: http://127.0.0.1:9885/api.json

  ADDRESS |       METHOD        |       ROUTE        |                              HANDLER                                                              |    MIDDLEWARE      
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------
  :9885   | ALL    | /*                                         | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing    | GLOBAL MIDDLEWARE  
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------
  :9885   | ALL    | /*                                         | github.com/gogf/gf/v2/net/ghttp.MiddlewareHandlerResponse           | GLOBAL MIDDLEWARE  
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------
  :9885   | ALL    | /api.json                              | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec                    |                    
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------
  :9885   | GET    | /api/admin/captcha            | goSimpleAdmin/internal/controller/user.(*ControllerAdmin).Captcha  |                    
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------
  :9885   | POST   | /api/admin/user/sign-in | goSimpleAdmin/internal/controller/user.(*ControllerAdmin).SignIn         |                    
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------
  :9885   | POST   | /api/admin/user/sign-up   | goSimpleAdmin/internal/controller/user.(*ControllerAdmin).SignUp     |                    
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------
  :9885   | ALL    | /swagger/*                           | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI                        | HOOK_BEFORE_SERVE  
----------|--------|--------------------------------------|---------------------------------------------------------------------------------------|--------------------

因为只写了 sign-in 的调用。就先用 postman 试试 sign-in 吧。

下边这个图,仅仅是以 post 方式调用接口。没传任何参数。返回 code 51, 以及 message 请输入账号。请注意看 status code 是 200。

再看这张图,账号按要求输入了,密码达不到要求。返回 code 51, 以及 message 密码长度为 6 和 16 位。请注意看 status code 是 200。

最后看这张图,账号密码要求都达到了。返回 code 0,以及 message 位空, data 为 null 。请注意看 status code 是 200。

到这里,流程是通了。为什么 data 是 null 呢。在 logic 中,我啥逻辑都没写,就返回个 nil。到这里成为 null 了。

发现一个问题,无论 validate 错误还是正常返回,状态码都是 200。并且返回的数据都包裹了一层。要是以以往的习惯,有错误返回的都不会是 200。一般是 403 这样的。包裹一层也有点难受。 包裹的 code 码,对于用户,对于前端来说也是毫无意义。

先这么弄吧。

下边,要把注册以及图片验证码 logic 和 service 完善起来。再在 controller 中调用起来。

提交版本控制。

git add .
git commit -m '用户登录注册相关的 api、ctrl、model、dao、logic 的生成以及串联'

到此,基础的进口出口已经串起来了。剩下就是业务逻辑填充。这里,主要是数据库 ORM 的操作。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注