Skip to main content

综合示例

本示例简单实现下列功能的API:

  1. 书本数据获取(分页)
  2. 书本数据添加、编辑、删除
  3. 用户名与密码登录
  4. 书本数据展示与添加、编辑、删除需要登录才能访问

本示例用API实现,即只用JSON格式数据进行返回。如果需要用模板实现,核心部分都相同,只是需要将返回的数据自行组织到模板页面上。

1. 创建项目

gf init gfbook

删除apicontroller中自动生成的文件,删除cmd.go中的路由绑定。

添加依赖的库,mysql驱动和jwt

方法一(需要联网):

go get github.com/gogf/gf/contrib/drivers/mysql/v2
go get github.com/golang-jwt/jwt/v5

mysql驱动初始化导入

main.go

import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
)

方法二(需要之前已经下载过对应版本的库,此次不需要联网)

需要先进行初始化导入,不然没法添加

main.go

import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
_ "github.com/golang-jwt/jwt/v5" // 这行在go mod tidy运行之后删除
)

go.mod

require (
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.5.3
github.com/gogf/gf/v2 v2.5.3
github.com/golang-jwt/jwt/v5 v5.0.0
)

执行以下指令更新

go mod tidy

2. 配置

internal/config/config.yaml

配置数据库连接

server:
address: ":8000"
openapiPath: "/api.json"
swaggerPath: "/swagger"
serverRoot: "resource/public/resource"

database:
link: "mysql:root:root@tcp(127.0.0.1:3306)/goframe?loc=Local&parseTime=true"

logger:
level : "all"
stdout: true

准备需要用到的资源文件

hack/config.yaml

配置DAO生成相关信息

gfcli:
gen:
dao:
link: "mysql:root:root@tcp(127.0.0.1:3306)/goframe"
tables: "user,book"
jsonCase: "Snake"

配置完成之后运行命令生成DAO相关文件

gf gen dao

3.编写API数据结构

api/user/user.go

package user

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

type LoginReq struct {
g.Meta `path:"/login" method:"post"`
Username string `p:"username" v:"required#请输入用户名" dc:"用户名"`
Password string `p:"password" v:"required#请输入密码" dc:"密码"`
}

type UserInfo struct {
Id uint `json:"id" dc:"用户ID"`
Username string `json:"username" dc:"用户名"`
Nickname string `json:"nickname" dc:"昵称"`
Avatar string `json:"avatar" dc:"用户头像"`
}

type LoginRes struct {
Token string `json:"token" dc:"验证token"`
UserInfo *UserInfo `json:"user_info"`
}

api/book/book.go

package book

import (
"gfbook/internal/model/entity"

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

type IndexReq struct {
g.Meta `path:"/book" method:"get"`
Page uint `p:"page" v:"required|integer|min:1#数据页不能为空|数据页只能是整数|数据页不能小于1" dc:"页数"`
PageSize uint `p:"page_size" v:"integer|min:1#数据大小只能为整数|数据大小不能小于1" d:"10" dc:"每页数据量"`
}

type IndexRes struct {
Page uint `json:"page" dc:"页数"`
PageSize uint `json:"page_size" dc:"每页数据量"`
Rows []entity.Book `json:"rows" dc:"查询数据"`
}

type AddReq struct {
g.Meta `path:"/book" method:"post"`
Name string `p:"name" v:"required#书名不能为空" dc:"书名" `
Author string `p:"author" v:"required#作者不能为空" dc:"作者" `
Price float64 `p:"price" v:"required|float|min:0#价格不能为空|价格格式不正确|价格不能小于0" dc:"价格" `
PublishTime *gtime.Time `p:"publish_time" v:"date#出版时间格式不正确" dc:"出版日期"`
}

type AddRes struct {
}

type EditReq struct {
g.Meta `path:"/book" method:"put"`
Id uint `p:"id" v:"required" dc:"书本ID"`
Name string `p:"name" dc:"书名" `
Author string `p:"author" dc:"作者" `
Price float64 `p:"price" v:"float|min:0#价格格式不正确|价格不能小于0" dc:"价格" `
PublishTime *gtime.Time `p:"publish_time" v:"date#出版时间格式不正确" dc:"出版日期"`
}

type EditRes struct{}

type DelReq struct {
g.Meta `path:"/book" method:"delete"`
Id uint `p:"id" v:"required" dc:"书本ID"`
}

type DelRes struct{}

4. 控制器框架及路由绑定

internal/controller/user/user.go

package user

import (
"context"
"gfbook/api/user"
)

var UserController = &cUser{}

type cUser struct {
}

func (c *cUser) Login(ctx context.Context, req *user.LoginReq) (res *user.LoginRes, err error) {
return
}

internal/controller/book/book.go

package book

import (
"context"
"gfbook/api/book"
)

var BookController = &cBook{}

type cBook struct{}

func (c *cBook) Index(ctx context.Context, req *book.IndexReq) (res *book.IndexRes, err error) {
return
}

func (c *cBook) Add(ctx context.Context, req *book.AddReq) (res *book.AddRes, err error) {
return
}

func (c *cBook) Edit(ctx context.Context, req *book.EditReq) (res *book.EditRes, err error) {
return
}

func (c *cBook) Del(ctx context.Context, req *book.DelReq) (res *book.EditRes, err error) {
return
}

internal/cmd/cmd.go

s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Group("/user", func(group *ghttp.RouterGroup) {
group.Bind(user.UserController)
})
group.Group("/", func(group *ghttp.RouterGroup) {
group.Bind(book.BookController)
})
})

5. 设计Book的Service接口并完善控制器

internal/service/book.go

package service

import (
"context"
"gfbook/internal/model/do"
"gfbook/internal/model/entity"
)

type IBook interface {
// 查询书本列表
GetList(ctx context.Context, page uint, pageSize uint) (books []entity.Book, err error)
// 添加书本信息
Add(ctx context.Context, book do.Book) (err error)
// 修改书本信息
Edit(ctx context.Context, book do.Book) (err error)
// 删除书本信息
Del(ctx context.Context, id uint) (err error)
}

var localBook IBook

func Book() IBook {
if localBook == nil {
panic("IBook接口未实现或未注册")
}
return localBook
}

func RegisterBook(i IBook) {
localBook = i
}

internal/controller/book/book.go

package book

import (
"context"
"gfbook/api/book"
"gfbook/internal/model/do"
"gfbook/internal/service"
)

var BookController = &cBook{
service: service.Book(),
}

type cBook struct {
service service.IBook
}

func (c *cBook) Index(ctx context.Context, req *book.IndexReq) (res *book.IndexRes, err error) {
books, err := service.Book().GetList(ctx, req.Page, req.PageSize)
if err == nil {
res = &book.IndexRes{
Page: req.Page,
PageSize: req.PageSize,
Rows: books,
}
}
return
}

func (c *cBook) Add(ctx context.Context, req *book.AddReq) (res *book.AddRes, err error) {
err = c.service.Add(ctx, do.Book{
Name: req.Name,
Author: req.Author,
Price: req.Price,
PublishTime: req.PublishTime,
})
return
}

func (c *cBook) Edit(ctx context.Context, req *book.EditReq) (res *book.EditRes, err error) {
err = c.service.Edit(ctx, do.Book{
Id: req.Id,
Name: req.Name,
Author: req.Author,
Price: req.Price,
PublishTime: req.PublishTime,
})
return
}

func (c *cBook) Del(ctx context.Context, req *book.DelReq) (res *book.EditRes, err error) {
err = c.service.Del(ctx, req.Id)
return
}

6. Logic层Book操作具体实现

  • 新建internal/logic/book/book.go文件,实现上面的IBook接口

internal/logic/book/book.go

package book

import (
"context"
"gfbook/internal/dao"
"gfbook/internal/model/do"
"gfbook/internal/model/entity"
"gfbook/internal/service"
)

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

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

type iBook struct{}

// Add implements service.IBook.
func (*iBook) Add(ctx context.Context, book do.Book) (err error) {
_, err = dao.Book.Ctx(ctx).Data(book).Insert()
return
}

// Del implements service.IBook.
func (*iBook) Del(ctx context.Context, id uint) (err error) {
_, err = dao.Book.Ctx(ctx).Where(dao.Book.Columns().Id, id).Delete()
return
}

// Edit implements service.IBook.
func (*iBook) Edit(ctx context.Context, book do.Book) (err error) {
_, err = dao.Book.Ctx(ctx).Where(dao.Book.Columns().Id, book.Id).Data(book).Update()
return
}

// GetList implements service.IBook.
func (*iBook) GetList(ctx context.Context, page uint, pageSize uint) (books []entity.Book, err error) {
err = dao.Book.Ctx(ctx).Page(int(page), int(pageSize)).Scan(&books)
return
}
  • 新建internal/logic/logic.go文件,并对上述实现进行初始化导入
import (
_ "gfbook/internal/logic/book"
)
  • main.go中对logic进行初始化导入

main.go

import (
_ "gfbook/internal/logic"
)

7. User的Service层设计与控制器完善

internal/service/user.go

package service

import (
"context"
"gfbook/internal/model/entity"
)

type IUser interface {
Login(ctx context.Context, username string, password string) (user *entity.User, err error)
}

var localUser IUser

func User() IUser {
if localUser == nil {
panic("IUser接口未实现或未注册")
}
return localUser
}

func RegisterUser(i IUser) {
localUser = i
}

internal/controller/user/user.go

package user

import (
"context"
api_user "gfbook/api/user"
"gfbook/internal/model/entity"
"gfbook/internal/service"
"time"

"github.com/golang-jwt/jwt/v5"
)

var UserController = &cUser{}

type cUser struct {
}

func (c *cUser) Login(ctx context.Context, req *api_user.LoginReq) (res *api_user.LoginRes, err error) {
user, err := service.User().Login(ctx, req.Username, req.Password)
if err == nil {
res = &api_user.LoginRes{
Token: jwtToken(user),
UserInfo: &api_user.UserInfo{
Id: user.Id,
Username: user.Username,
Nickname: user.Nickname,
Avatar: user.Avatar,
},
}
}
return
}

func jwtToken(user *entity.User) string {

const JwtTokenKey = "zbvxMPp12fibPNnOaYmc0rviniaJCOVJ"
// 实际使用中可将Key存于文件中或放在常量中 consts.JwtTokenKey

claim := jwt.RegisteredClaims{
Subject: user.Username,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
}

token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claim).SignedString([]byte(JwtTokenKey))

if err != nil {
panic("token生成出错")
}

return token
}

8. User的Logic层实现

internal/logic/user/user.go

package user

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

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

type iUser struct{}

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

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

// Login implements service.IUser.
func (*iUser) Login(ctx context.Context, username string, password string) (user *entity.User, err error) {
err = dao.User.Ctx(ctx).Where(do.User{
Username: username,
Password: password,
}).Scan(&user)

if user == nil {
err = gerror.New("用户名或密码有误")
}

return
}

internal/logic/logic.go

import (
_ "gfbook/internal/logic/book"
_ "gfbook/internal/logic/user"
)

9. 添加登录验证中间件

internal/service/middware.go

package service

import "github.com/gogf/gf/v2/net/ghttp"

type IMiddleware interface {
Auth(r *ghttp.Request)
}

var localMiddleware IMiddleware

func Middleware() IMiddleware {
if localMiddleware == nil {
panic("IMiddleware接口未实现或未注册")
}
return localMiddleware
}

func RegisterMiddleware(i IMiddleware) {
localMiddleware = i
}

internal/logic/middleware/middleware.go

package middleware

import (
"gfbook/internal/consts"
"gfbook/internal/service"

"github.com/gogf/gf/v2/net/ghttp"
"github.com/golang-jwt/jwt/v5"
)

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

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

type iMiddleware struct {
}

// Auth implements service.IMiddleware.
func (*iMiddleware) Auth(r *ghttp.Request) {
var res *ghttp.DefaultHandlerResponse
tokenStr := r.Header.Get("Authorization")
if tokenStr == "" {
res = &ghttp.DefaultHandlerResponse{
Code: 403,
Message: "请登录后携带token访问",
}
} else {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return []byte(consts.JwtTokenKey), nil
})

if err != nil || !token.Valid {
res = &ghttp.DefaultHandlerResponse{
Code: 403,
Message: "token已失效,请重新登录",
}
}
}
if res != nil {
r.Response.WriteJsonExit(res)
}
r.Middleware.Next()
}

internal/logic/logic.go

package logic

import (
_ "gfbook/internal/logic/book"
_ "gfbook/internal/logic/middleware"
_ "gfbook/internal/logic/user"
)

internal/cmd/cmd.go

group.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(service.Middleware().Auth)
group.Bind(book.BookController)
})

10. 接口文档

准备对应的JS与CSS文件放入静态资源对应的目录,或者可以用在线的CDN链接

template/api.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/resource/css/swagger-ui.css">
<script src="/resource/js/swagger-ui-bundle.js"></script>
<title>API Doc</title>
</head>
<body>
<div id="swagger-ui"></div>
</body>
</html>

<script type="text/javascript">
window.ui = SwaggerUIBundle({
url: '/api.json',
dom_id: '#swagger-ui'
})
</script>

internal/cmd/cmd.go

group.GET("/api", func(req *ghttp.Request) {
req.Response.WriteTpl("/api.html")
})