【不断更新】Go-admin学习小结(gorm+gin+swagger+casbin)

开会小结

20221020下午16时

Camel-Case 驼峰命名

体现在:

1、函数以及各种变量命名
2、json字段也需要符合驼峰规则,如图
在这里插入图片描述

Logger日志规范

1、不要写中文
2、开头不要英文大写
3、简洁明了的提示给技术人员报错内容(对象是开发者,不是用户)

错误示范:

e.Logger.Error(err)
e.Error(404, err, "您输入的关系类型有误!只能是 认领 或者 关注 关系")

修改为:

e.Logger.Error(err)
e.Error(404, err, fmt.Sprintf("invalid req.type %s",req.Type))

常量命名

如果有一些数据库字段为常量,不要在业务逻辑里赋值写死

错误:

if req.Type == "认领" {
	err = s.InsertClaimRelationship(&req)
} else if req.Type == "关注" {
	err = s.InsertCollectionRelationship(&req)
} else {
	e.Logger.Error(err)
	e.Error(404, err, fmt.Sprintf("invalid req.type %s",req.Type))
	return
}

正确做法:

在DTO中添加常量,然后替换之前硬性规定的字符串,方便后续增删修改命名

const (
	claim = "认领"
	collection = "关注"
)

关于结构体和uri

1、明确dto中的结构体是接收前端json请求体

2、明确models中定义的结构体面向数据库

3、Update和Insert的大部分字段都相同,可以使用同一个结构体,尽量减少重复代码

4、uri字段不会通过json传递,因此不需要在dto的结构体里使用uri标签,使用c.Param()进行传参

错误示范:如果用户输入PatentId不是int类型,服务器panic之后无法掌控,因此写到api中使用c.Param()进行传参,然后进行一个类型的判断,如果类型转换失败(从路由的string转化为PatentId真正的类型int),可以自定义err报错日志,避免不可预知的错误。

type UserPatentObject struct {
	UserId   int    `json:"UserId" gorm:"size:128;comment:用户ID"`
	PatentId int    `uri:"patent_id"`
	Type     string `uri:"type"` //路由对大小写敏感
	common.ControlBy
}

修改:strconv函数和c.Param函数

req.PatentId, err = strconv.Atoi(c.Param("patent_id"))

报错:reflect: NumField of non-struct type *dto.UserPatentObject

非结构体类型的数字字段:*dto.UserPatentObject

修改:Bind(&req)Bind(req)

req := dto.NewUserPatentClaim(user.GetUserId(c), 0, user.GetUserId(c), user.GetUserId(c))

	err := e.MakeContext(c).
		MakeOrm().
		Bind(req).
		//修改&,本来是
		MakeService(&s.Service).
		Errors

	req.PatentId, err = strconv.Atoi(c.Param("patent_id"))

原因:该req是一个函数的返回值,看看函数NewUserPatentClaim的返回值类型是:&UserPatentObject,因此不需要再加地址符&

func NewUserPatentClaim(userId, patentId, createdBy, updatedBy int) *UserPatentObject {
	return &UserPatentObject{
	......
	}
}

关于GIT协作

写到关系表的CRUD需要修改其他人的go文件时,需要先进行沟通,不要自己改,不然merge会冲突,解决很麻烦

关于合并关系表

不要单独把关系表的CRUD和models分开写,经过讨论,按照以下内容分模块完成

1、Patent及其关系表都合并入Patent:

包括User_Patent、Patent_Tag、Patent-Package相关内容合并入Patent

2、Tag及其关系表都合并入Tag

包括User_Tag

3、Package及其关系表都合并入Package

包括User_Package

4、不要暴露关系表的路由接口,以功能为单位暴露接口,而不是以简单的CRUD,CRUD都写在service里,而Api就是组装Service为一个功能。

关于路由命名规则RESTful风格

参考: https://restfulapi.net/resource-naming/

简单来说就是/资源/id method表示做什么

最后就是api/vi都修改了,加user-agent

关于API和SERVICE

1、业务逻辑尽量封装在Service
2、Api内的判断能少则少

Git报错整理

PULL

Your local changes to the following files would be overwritten by merge

解决:确实保留了本地的修改,慎用reset回退,除非确认不想要了

git stash
git pull 
git stash pop

随后提示Dropped refs/stash就是成功了,可以再检查一下

git stash:备份当前工作区内容,从最近的一次提交中读取相关内容,让工作区保证和上次提交的内容一致。同时,将当前工作区内容保存到Git栈中
git pull:拉取服务器上当前分支代码
git stash pop:从Git栈中读取最近一次保存的内容,恢复工作区相关内容。同时,用户可能进行多次stash操作,需要保证后stash的最先被取到,所以用栈(先进后出)来管理;pop取栈顶的内容并恢复
git stash list:显示Git栈内的所有备份,可以利用这个列表来决定从那个地方恢复。
git stash clear:清空Git栈

参考:https://blog.csdn.net/ydm19891101/article/details/104505624/

Go-admin 命令

编译命令

go run main.go server -c 你的配置文件路径

注意:报错Error1049:unknown dbname

1 不要忘了-c 不要忘了配置文件路径
2 要修改配置文件中的端口,用户名和密码,数据库名,保证与本地一致

debug

在这里插入图片描述

数据库

1. 数据库初始化

go run main.go migrate -c 你的配置文件路径

2. 更新数据库字段

(1)生成新的_migrate.go文件

go run main.go migrate -g -c 你的配置文件路径

(2)进入目录:

patent-admin-plat\cmd\migrate\migration\version-local\某某某某_migrate.go

修改新增表结构片段,把Models的内容改为自己新建的models名

// TODO: 这里开始写入要变更的内容

// TODO: 例如 修改表字段 使用过程中请删除此段代码
//err := tx.Migrator().RenameColumn(&models.SysConfig{}, "config_id", "id")
//if err != nil {
// 	return err
//}

// TODO: 例如 新增表结构 使用过程中请删除此段代码
err := tx.Debug().Migrator().AutoMigrate(
	new(models.你的model名),
)
if err != nil {
	return err
}

(3)数据库初始化

go run main.go migrate -c config/settings_dev.yml

刷新navicat就可以看到成功数据迁移~

3. 数据迁移的原因

migration文件夹里的models等等文件都不是最新的,app中最新更新的文件需要每一次都-g增加一个文件,进行增量的更新数据库字段。

因为数据库字段一般不可以进行修改,因此migrate命令用这种可以追溯的方式,既可以修改数据库字段,又可以留下痕迹。

Swagger

简介:根据你自己写的接口上方的注释,生成接口介绍文档。

安装

$ go install github.com/swaggo/swag/cmd/swag@latest

更新

命令行输入:swag init

刷新swagegr文档后可以看到自己的接口已经更新入文档
注意:接口函数和swagger注释之间千万不要空行

效果图

根据效果图可以知道各个注释的修改的具体内容在这里插入图片描述

参考文档

https://github.com/swaggo/swag/blob/master/README_zh-CN.md

数据流图

router不便暴露

数据流:Http向router发送请求👉Router调用Api函数👉Api调用Service业务处理逻辑进行数据库操作,并且传入http发来的请求体req(通过dto的结构体接收req)
在这里插入图片描述

Gin的Github地址

封装的Gin如何传参?(c.JSON)

  • 入口
  • e.OK中,e是继承的api
e.OK(req, "更新成功")
  • OK函数
// OK 通常成功数据处理
func (e Api) OK(data interface{}, msg string) {
	response.OK(e.Context, data, msg)
}
  • response 结构体
    在这里插入图片描述

后端404

在这里插入图片描述

检查路由

  • 注册成功
    路由输入错误等(字母,s等等)

  • 注册失败
    cmd/api文件中注册


import "go-admin/app/文件夹名/router"

func init() {
	AppRouters = append(AppRouters, router.InitRouter)
}

[POST] GIN如何接收多个传参?

如何解析json并操作和处理json数据?

1、dto写一个SysListInsertReq表,定义你需要接受的数据,这也决定了前端json传来哪些数据

尤其是要传参好几个字段,比如高级检索,还是需要一个dto写一个struct比如SysListInsertReq给他接住。

2、在业务逻辑层var data models.SysList

3、操作这个post来的json包里的东西

比如c是SysListInsertReq,c.GenerateList(&data)即重新赋值这个patentid对应得其他数据字段的所有值,然后err = e.Orm.Create(&data),创建数据。

4、api中我们定义传入是json的data body,且对应dto中SysListInsertReq的数据字段。

[GET] GIN如何接收数据库中的数据?

1、dto层*dto.SysListById

2、业务逻辑层db := e.Orm.First(model, d.GetPatentId())

写在Get函数Get(d *dto.SysListById, model *models.SysList)

3、api层

还不太懂,不一定对

4、路由

(抄了一段,不知道能不能用)

//定义路径GET
router.GET("/user/", func(context *gin.Context) {
	//获取name值
	name := context.DefaultQuery("name","默认值")
	age := context.Query("age")
	message := name + " is " + age
	//返回值
	context.String(http.StatusOK, "hello %s", message)
})

Post请求使用

常见场景
1、application/json (request中发送json数据,用post方式发送Content-type用application/json)
2、application/x-www-form-urlencode (常见的表单提交,把query_string的内容放到body体内,同样需要urlencode)
3、application/xml (http作为传输协议,xml作为编码方式远程调用规范)
4、multipart/form-data(文件传输)

router := gin.Default()
//定义POST方法路径
router.POST("/form_post", func(context *gin.Context) {
	//获取传递的值
	name := context.PostForm("name")
	age := context.DefaultPostForm("age","10")
	//以JSON格式返回
	context.JSON(http.StatusOK, gin.H{"status":gin.H{"status_code":http.StatusOK,"status":"ok"},"name":name,"age":age})
}) 
 

参考链接:https://blog.csdn.net/weixin_39218464/article/details/124318134

[DELETE-gorm] 如何实现

1、dto层
2、业务层

//e是继承Service来的,Orm是Service结构体的Orm,但是data是models的data
//也就是Service里的Orm(被DB的指针赋值)去进行了delete的事务
func (e *SysList) Remove(c *dto.SysListDeleteReq) error {
	var err error
	var data models.SysList
	//实例化结构体,根据 c.GetPatentId()先查询到syslist,再删除syslist
	db := e.Orm.Delete(&data, c.GetPatentId()).Where("patent_id = ?", c.GetPatentId())

3、Api层

//在api先接收request数据(来自context上下文的*http.request)
//再用dto.SysListDeleteReq{}接下request
//再调用service里面的方法,使用业务逻辑操作数据库修改&req
func (e SysList) DeleteLists(c *gin.Context) {
	s := service.SysList{}
	req := dto.SysListDeleteReq{}
	err := e.MakeContext(c).
		MakeOrm().
		Bind(&req, binding.JSON).
		MakeService(&s.Service).
		Errors
	err = s.Remove(&req)
	//把地址传进来,就是把原始dto模板,通过数据库修改之后赋值

	}

Api层代码细读

序列化:就是JavaBean对象转化为JSON格式的字符串。

反序列化:就是序列化的反方向,将字符串转化为JavaBean。

要实现一个接口,必须实现该接口里面的所有方法。

GET单个对象为例

func (e SysList) GetPatentById(c *gin.Context) {
	s := service.SysList{}
	//接受了经过业务处理修改的model
	req := dto.SysListById{}
	//请求体req被dto中自己建的结构体接住
	err := e.MakeContext(c).//把c赋给这个*Api的上下文
		MakeOrm().//配置数据库链接
		Bind(&req, nil).//检查http请求的req数据是否存在
		MakeService(&s.Service).//service.ORM被赋值为api.ORM
		Errors
	var object models.SysList
	//只有当结构体实例化时,才会真正地分配内存。
	err = s.Get(&req, &object)
	//调用service里面Get函数,也就是进行了一次查询,把结果放在???,然后传给前端。
}

其中

Request数据结构


type Request struct {
 
	Method string
	URL *url.URL
	// The protocol version for incoming server requests.
	Proto      string // "HTTP/1.0"
	ProtoMajor int    // 1
	ProtoMinor int    // 0
	// Header包含服务器接收或客户端发送的请求Header字段
    //对传入请求incoming request,Host Header将升级为Request.Host字段,并从Header映射中删除。
    //请求解析器通过使用CanonicalHeaderKey来实现这一点,将第一个字符以及连字符后面的任何字符设置为大写,其余字符设置为小写。
    //对于客户端请求,某些Header如Content-Length和Connection)会在需要时自动写入,并且可能会忽略标头中的值。请参阅Request.Write方法的文档。
	Header Header
    //Body必须允许与Close同时调用Read。特别是,调用Close应该解除等待输入的Read。
	Body io.ReadCloser
	// GetBody defines an optional func to return a new copy of Body. 
	// It is used for client requests when a redirect requires
	// reading the body more than once. Use of GetBody still requires setting Body.
	// For server requests, it is unused.
	GetBody func() (io.ReadCloser, error)
	ContentLength int64
	// TransferEncoding lists the transfer encodings from outermost to
	// innermost. An empty list denotes the "identity" encoding.
	// TransferEncoding can usually be ignored; chunked encoding is
	// automatically added and removed as necessary when sending and
	// receiving requests.
	TransferEncoding []string
	// For server requests, the HTTP server handles this automatically,it is not needed by Handlers.
	// For client requests, setting this field prevents re-use of TCP connections between requests to the same hosts, as if Transport.DisableKeepAlives were set.
	Close bool
	// For server requests, Host specifies the host on which the URL is sought. 
	// For client requests, Host optionally overrides the Host header to send. 
	// If empty, the Request.Write method uses the value of URL.Host. 
	// Host may contain an international domain name.
	Host string
    //表单包含解析的表单数据,包括URL字段的查询参数和PATCH、POST或PUT表单数据。
    //此字段仅在调用ParseForm后可用。
    //HTTP客户端忽略Form并使用Body。
	Form url.Values
    //PostForm包含来自PATCH、POST或PUT主体参数的已解析表单数据。
    //此字段仅在调用ParseForm后可用。
    //HTTP客户端忽略PostForm,而使用Body。
	PostForm url.Values
    //MultipartForm是解析的多部分表单,包括文件上传。
    //此字段仅在调用ParseMultipartForm后可用。
    //HTTP客户端忽略MultipartForm,而使用Body。
	MultipartForm *multipart.Form
	Trailer Header
	RemoteAddr string
    // Usually the URL field should be used instead.,It is an error to set this field in an HTTP client request.
	RequestURI string
	TLS *tls.ConnectionState
	Cancel <-chan struct{}
	// This field is only populated during client redirects.
	Response *Response
	ctx context.Context
}

BIND

什么叫绑定?

Eg:对TextBox的Text属性的更改将"传播"到Customer的Name属性,而对Customer的Name属性的更改同样会"传播"到TextBox的Text属性

Binding 描述了绑定请求中的数据所需实现的接口,例如JSON请求正文、查询参数或表单POST。在源码中,有10个实现了Binding的结构体,以及三个接口。

type Binding interface {
	Name() string
	Bind(*http.Request, interface{}) error
}

比如ShouldBindWith:这是所有其他绑定方法的基础,基本上所有的绑定方法都需要用到这个方法来对数据结构进行一个绑定

以上为主要的Binding过程,其他派生出来的BindJSON 和ShouldBindJSON等,为具体的数据类型的快捷方式而已,只是帮我们把具体的binding的数据类型提前给封装起来,如JSON格式的binding函数就是binding.JSON

context.BindJSON
// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj interface{}) error {
   return c.MustBindWith(obj, binding.JSON)
}

context.BindJSON 从源码分析,仅仅比Bind方法少了一句

b := binding.Default(c.Request.Method, c.ContentType())

这一句是为了判断当前的请求方法和contentType,来给context.MustBindWith传的一个具体的bingding类型。

Json的实现的Binding接口如下

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
   if req == nil || req.Body == nil {
      return fmt.Errorf("invalid request")
   }
   return decodeJSON(req.Body, obj)
}

jsonBinding 结构体实现了Binding接口的Bind方法,将请求过来的Body数据进行解码,绑定到obj里面去

ShouldBindJSON
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj interface{}) error {
   return c.ShouldBindWith(obj, binding.JSON)
}

从源码注解来看,ShouldBindJSON其实就是ShouldBindWith(obj,binding.JSON)的快捷方式。简单来说,就是在ShouldBindWith(obj,binding.JSON)上面固定了参数,当我们明确规定,body提交的参数内容为json时,简化了我们的调用和增强了代码的可读性

1、当参数比较简单,不需要结构体进行封装时,还需要采用context的其他方法来获取对应的值
2、Gin在bind的时候,未对结构体的数据进行有效性检查,如果对数据有强要求时,需要自己对结构体的数据内容进行判断
3、建议使用ShouldBIndxxx 函数

参考:https://blog.csdn.net/weixin_52621900/article/details/126674629

本项目的BIND语法糖

第一个d接口就是http的request,第二个参数是可变参数,改变的是绑定的参数类型。

 // Bind 参数校验
func (e *Api) Bind(d interface{}, bindings ...binding.Binding) *Api {
	var err error
	if len(bindings) == 0 {
		bindings = constructor.GetBindingForGin(d)
	}
	for i := range bindings {
		if bindings[i] == nil {
			err = e.Context.ShouldBindUri(d)
		} else {
			err = e.Context.ShouldBindWith(d, bindings[i])
		}
		if err != nil && err.Error() == "EOF" {
			e.Logger.Warn("request body is not present anymore. ")
			err = nil
			continue
		}
		if err != nil {
			e.AddError(err)
			break
		}
	}
	//vd.SetErrorFactory(func(failPath, msg string) error {
	//	return fmt.Errorf(`"validation failed: %s %s"`, failPath, msg)
	//})
	if err1 := vd.Validate(d); err1 != nil {
		e.AddError(err1)
	}
	return e
}

点击bindings = constructor.GetBindingForGin(d)GetBindingForGin(d)

func (e *bindConstructor) GetBindingForGin(d interface{}) []binding.Binding {
	bs := e.getBinding(reflect.TypeOf(d).String())
	if bs == nil {
		//重新构建
		bs = e.resolve(d)
	}
	gbs := make([]binding.Binding, 0)
	mp := make(map[uint8]binding.Binding, 0)
	for _, b := range bs {
		switch b {
		case json:
			mp[json] = binding.JSON
		case xml:
			mp[xml] = binding.XML
		case yaml:
			mp[yaml] = binding.YAML
		case form:
			mp[form] = binding.Form
		case query:
			mp[query] = binding.Query
		default:
			mp[0] = nil
		}
	}
	for e := range mp {
		gbs = append(gbs, mp[e])
	}
	return gbs
}

可以看到这些类型都是可选择的参,也可以不写第二个参数。

MakeService

func (e *Api) MakeService(c *service.Service) *Api {
	c.Log = e.Logger
	c.Orm = e.Orm
	return e
}

Api父类

type Api struct {
	Context *gin.Context
	Logger  *logger.Helper
	Orm     *gorm.DB
	Errors  error
}
//api里的syslist继承了Api结构体

Service父类

type Service struct {
	Orm   *gorm.DB
	Msg   string
	MsgID string
	Log   *logger.Helper
	Error error
}

MakeOrm

// MakeOrm 设置Orm DB
func (e *Api) MakeOrm() *Api {
	var err error
	if e.Logger == nil {
		err = errors.New("at MakeOrm logger is nil")
		e.AddError(err)
		return e
	}
	db, err := pkg.GetOrm(e.Context)
	if err != nil {
		e.Logger.Error(http.StatusInternalServerError, err, "数据库连接获取失败")
		e.AddError(err)
	}
	e.Orm = db
	return e
}

Context数据结构

// Context is the most important part of gin. It allows us to pass variables between middleware,
//上下文是A中最重要的部分。它允许我们在中间件之间传递变量,
//例如,管理数据流、验证请求的JSON并呈现JSON响应。
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter

	Params   Params
	handlers HandlersChain
	index    int8
	fullPath string

	engine       *Engine
	params       *Params
	skippedNodes *[]skippedNode

	// This mutex protect Keys map:互斥锁保护主键映射
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	// Keys 是一种每一个request上下文都有的排他性的键值对
	Keys map[string]interface{}

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	Accepted []string

	// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
	queryCache url.Values

	// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
    //SameSite允许服务器定义cookie属性,使浏览器无法将此cookie与跨站点请求一起发送。
	sameSite http.SameSite
}

参考文档

开源框架:go-admin

Postman测试

1、新建请求
点击这里新建请求

post 功能

1、登录

根据swagger文档里的登录超级管理权限的api

登录:需要的内容就是用户名/密码/code等等,详见swagger文档

在这里插入图片描述

2、登录后,获得token,点击Auth,选择bearer token 填写token

在这里插入图片描述

3、进入post的路由,选择post请求,需要post的内容放入body,然后send

Go语言基础

Map

什么是map

map存储键值对,key-value作为一组数据:就像py的字典!

格式为:map[keytype]valuetype

map[string]int的意思是存储以string为key,int为值的键值对

比如小张,100(代表“小张”考了100分,对应)

如何使用Map

make作为内置函数,用来分配内存,返回Type本身(只能应用于slice, map, channel)

delete          --map中删除key对应的value
make            -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)  

附表:其他的go内置函数

append          -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close           -- 主要用来关闭channel
panic           -- 停止常规的goroutine  (panicrecover:用来做错误处理)
recover         -- 允许程序定义goroutine的panic动作
real            -- 返回complex的实部   (complexreal imag:用于创建和操作复数)
imag            -- 返回complex的虚部
new             -- 用来分配内存,主要用来分配值类型,比如intstruct。返回指向Type的指针
cap             -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 mapcopy            -- 用于复制和连接slice,返回复制的数目
len             -- 来求长度,比如string、array、slice、map、channel ,返回长度
printprintln  -- 底层打印函数,在部署环境中建议使用 fmt 包
 func main() {
    scoreMap := make(map[string]int, 8)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    fmt.Println(scoreMap)
    fmt.Println(scoreMap["小明"])
    fmt.Printf("type of a:%T\n", scoreMap)
}

输出:

   map[小明:100 张三:90]
   100
   type of a:map[string]int

总结:

1、分配内存make,赋给变量名a
2、a[ ] 的[ ] 内写入key键,获取值
3、类型为map[keytype]valuetype

Map的遍历

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k, v := range scoreMap {
        fmt.Println(k, v)
    }
}

删除Map

delete()内置函数可以删除key:delete(scoreMap, "小明")

参考链接

Go文档

结构体转成map[string]interface{}

场景:interface转其他类型 :有时候返回值是interface类型的,直接赋值是无法转化的

Interface接口

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值