Beego案例-新闻发布系统
1.注册
后台代码和昨天案例代码一致。,所以这里面只写一个注册的业务流程图。
**业务流程图 **
2.登陆
业务流程图
登陆和注册业务和我们昨天登陆和注册基本一样,所以就不再重复写这个代码
但是我们遇到的问题是如何做代码的迁移,把昨天的登陆和注册拿过来直接用?
-
首先,我们需要把静态页面拷贝到我们项目目录下面。
- 进入项目目录,删除掉原来的static文件夹
- 进入项目目录,删除掉原来的static文件夹
-
然后拷贝我们昨天课堂资料中的
static.zip
到这个目录,并解压,解压之后如下图:
- 打开static文件夹,显示如下,则说明拷贝成功:
- 打开static文件夹,显示如下,则说明拷贝成功:
-
把static文件夹中所有的html文件都拷贝到views文件夹下面(昨天的几个页面已经没用了,可以删除),拷贝之后views文件显示如下:
这时候用GoLand打开我们的项目,显示如下:
-
打开
register.html
页面,修改页面中form表单的内容-
给
<form>
标签加上action="/register" method="post"
属性 -
给两个
<input>
标签的name分别改为name="userName"
和name = "passwd"
- 表单相关代码如下:
<form class="login_form" name = "login" action="/register" method="post"><h1 class="login_title">用户注册</h1><input type="text" placeholder="用户名" class="input_txt" name="userName"><input type="password" placeholder="密码" class="input_txt" name = "passwd"><input type="submit" value="注 册" class="input_sub"> </form>
-
-
打开
login.html
页面,修改form表单的内容-
给
<form>
标签加上action="/login" method="post"
属性 -
给两个
<input>
标签的name分别改为name="userName"
和name = "passwd"
- 表单相关代码如下:
<form class="login_form" name = "login" action="/login" method="post"><h1 class="login_title">用户登录</h1><input type="text" class="input_txt" name = "userName"><input type="password" name = "passwd" class="input_txt"><div class="remember"><input type="checkbox" name="remember" ><label>记住用户名</label></div><input type="submit" value="登 录" class="input_sub"> </form>
(登陆界面多了个记住用户名标签,明天我们实现这个功能)
-
改完之后,运行项目,测试注册和登陆页面能够能唱显示,并且功能没有问题说明代码迁移成功。
3.创建数据库
3.1数据库表的设计
接下来我们就要实现文章相关的操作,所以这里我们要在数据库中生成一个文章表。
我们以前在数据库中创建表的时候,会给字段加很多限制属性,比如非空,长度,默认值等等,在ORM中,创建表时也可以给各个字段添加相应的限制。那如何去加限制呢?我们先看例子:
type Article struct {Id int `orm:"pk;auto"`ArtiName string `orm:"size(20)"`Atime time.Time `orm:"auto_now"`Acount int `orm:"default(0);null"`Acontent string `orm:"size(500)"`Aimg string `orm:"size(100)"`
}
由上面的代码可以看出,要给哪个字段添加属性,需要在这个字段后面添加 `` 括起来的内容,格式为orm:"限制条件"
。那这些限制条件都有哪些呢?我在这里给大家列了一个表格。
限制条件 | 作用 |
---|---|
pk | 设置该字段为主键 |
auto | 这只该字段自增,但是要求该字段必须为整型 |
default(0) | 设置该字段的默认值,需要注意字段类型和默认值类型一致 |
size(100) | 设置该字段长度为100个字节,一般用来设置字符串类型 |
null | 设置该字段允许为空,默认不允许为空 |
unique | 设置该字段全局唯一 |
digits(12);decimals(4) | 设置浮点数位数和精度。比如这个是说,浮点数总共12位,小数位为四位。 |
auto_now | 针对时间类型字段,作用是保存数据的更新时间 |
auto_now_add | 针对时间类型字段,作用是保存数据的添加时间 |
注意:当模型定义里没有主键时,符合int, int32, int64, uint, uint32, uint64 类型且名称为 Id 的 Field 将被视为主键,能够自增. "
Mysql中时间类型有date和datetime两种类型,但是我们go里面只有time.time一种类型,如果项目里面要求精确的话,就需要指定类型,指定类型用的是type(date)或者type(datetime)
3.2生成表
这时候注意,我们添加了结构体对象之后,并不能直接生成表,需要注册,注册的代码就是初始化数据库三行代码中的第二行,注册表结构,把要创建的表对应的结构体对象作为函数的参数,代码如下:
orm.RegisterModel(new(User),new(Article))
创建之后,我们可以在goland下方查看创建表过程,也可以进入数据库查看是否建表成功,成功的话,数据库显示如下:
登陆成功之后,访问新闻列表展示页面,但是我们现在还没有新闻,所以我们先实现插入文章界面。
添加文字显示实现
修改路由文件
在router.go文件的init函数中添加下面这一行代码
//文章列表页访问
beego.Router("/showArticleList", &controllers.ArticleController{}, "get:ShowArticleList")
显示文章界面的显示
-
先创建一个article.go文件用来存放文章有关的业务代码
-
然后在article.go文件中创建一个ArticleController控制器,并定义一个ShowArticleList函数代码如下:
package controllersimport beego "github.com/beego/beego/v2/server/web"type ArticleController struct {beego.Controller
}func (this *ArticleController) ShowArticleList() {this.TplName = "index.html"
}
写完代码之后,我们从浏览器登录以后跳转http://localhost:8080/showArticleList
,如果能在浏览器中看到下面这个界面,表示页面展示成功:
4.插入文章
业务流程图
插入页面我们用的视图是add.html
,这里我们规定添加文章界面的请求路径为/addArticle
4.1修改路由文件
在router.go文件的init函数中添加下面这一行代码
beego.Router("/addArticle",&controllers.ArticleControlle{},"get:ShowAddArticle")
写完代码之后,我们从浏览器发出一个请求localhost:8080/addArticle
,如果能在浏览器中看到下面这个界面,表示页面展示成功:
4.2添加文章界面的显示
-
先创建一个article.go文件用来存放文章有关的业务代码
-
然后在article.go文件中创建一个ArticleController控制器,并定义一个ShowAddArticle函数代码如下:
import "github.com/astaxie/beego"type ArticleController struct {beego.Controller }func (this*ArticleController)ShowAddArticle(){}
-
接着我们来实现
ShowAddArticle
函数,这个函数只是用来展示页面的,所以我们只需要给他制定一个视图就可以,代码如下:func (this*ArticleController)ShowAddArticle(){this.TplName = "add.html" }
写完代码之后,我们从浏览器发出一个请求
localhost:8080/addArticle
,如果能在浏览器中看到下面这个界面,表示页面展示成功:
4.3插入文章数据处理
上面我们显示了添加文章界面,观察界面可以发现,我们需要获取文章标题,文章类型, 文章内容,上传图片。其中文章类型牵涉到多表操作,暂时只做简单的单表操作。首先让我们来看一下,插入页面的前端部分修改。
4.3.1前端页面修改
由页面可知,我们这里面是要上传数据,所以我们这里需要一个form表单,打开前端界面add.html
,能看到我们这里面确实有一个标签,只是没有属性,我们需要给标签添加action和method属性,这个请求还是添加文章,所以我们还可以用添加文章的请求路径,设置action属性action="/addArticle"
。因为上传数据,我们这里用post方法,设置method属性method="post"
。其他部分不用修改。form修改代码如下:
<form method="post" action="/addArticle">
4.3.2路由内容修改
我们在前端添加了addArticle请求的post方法,所以需要修改一下router.go,给addArticle的post请求指定一个函数,修改代码如下:
beego.Router("/addArticle",&controllers.ArticleController{},"get:ShowAddArticle;post:HandleAddArticle")
4.3.3后台代码实现
有了函数名之后,我们就需要在后台中实现这个函数。
-
首先是获取数据
这时候我们看一下前端界面,我们需要获取文章标题, 文章内容,上传图片数据,文章标题和文章内容都是字符串,比较简单,直接通过GetString获取,所以我们先获取这两个内容。通过查看add.html代码我们发现,文章标题对应的标签name等于articleName,文章内容对应的标签name等于content(注意这里用的是textarea标签,不是用的input,但是获取数据方式一样)。获取数据的代码如下:
//获取数据 articleName := this.GetString("articleName") content := this.GetString("content")
-
获取数据之后就做数据校验,我们这里还是做判空校验
//对数据进行校验 if articleName == ""|| content == ""{beego.Info("添加文章数据不完整")this.TplName = "add.html"return }
正常的添加流程,在校验完数据之后就要把数据插入数据库了,但是我们添加文章这个界面有点特殊,因为这里面牵涉到一个静态文件的上传,所以我们先处理静态文件上传功能。
-
静态文件上传(难点)
-
前端代码
如果form表单中牵涉到文件上传,在form表单中就需要添加一个属性
enctype="multipart/form-data"
不然上传就是假上传,后台不能获取到上传的文件。修改如下:<form method="post" action="/addArticle" enctype="multipart/form-data"><input type="file" class="input_file" name="uploadname">
-
后台代码修改
后台接收上传文件有两个函数可以用。
GetFile(key string) (multipart.File, *multipart.FileHeader, error)
作用: 是获取前端传递过来的文件。
参数: 是input标签中的name值
返回值: 有三个,一个是文件流(就是我们打开文件返回的内容),第二个是文件相关信息,包括文件头,文件大小,文件名字等,第三个是错误信息。示例代码如下:
file,head,err := this.GetFile("uploadname")if err != nil{beego.Info("上传图片错误,请重新添加!")this.TplName = "add.html"return}
SaveToFile(fromfile, tofile string) error
作用:直接保存前端出过来的文件。
参数: 有两个参数,第一个参数是前端标签的name属性值,第二个参数是文件在服务器端存储的位置。**注意:这个位置字符串在前面需要加一个
.
返回值:是错误信息。示例代码如下:
err := this.SaveToFile("uploadname","./static/img/1.jpg") if err != nil{beego.Info("上传图片错误,请重新添加!")this.TplName = "add.html"return}
-
-
在我们开发过程中,如果后台接收文件并存储需要做以下几种判断
文件格式判断
我们通过GetFile可以获取到文件名,然后通过path包,可以分离出文件的后缀,即文件格式,把你需要的文件格式过滤出来,不需要的返回即可。我们根据文件名获取文件后缀,代码如下:
//文件格式判断fileExt := path.Ext(head.Filename)if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg"{beego.Info("上传图片格式不正确,请重新添加!")this.TplName = "add.html"return}
文件大小的判断
我们获取文件之后,在存储之前,文件流一般是在内存中,所以文件不易过大,我们在这里做一个文件大小的判断。代码如下:
//文件大小判断if head.Size > 5000000{beego.Info("上传图片太大,请重新添加!")this.TplName = "add.html"return}
避免文件重名
获取文件之后我们要把文件存储到服务器上,但是用户可能会上传同名的文件,如果文件同名的话,后来上传的文件就把之前上传的文件给覆盖了,所以我们要给上传的文件重新确定一个名字。这里我们以上传文件时的时间作为上传文件的文件名。默认的时间格式和我们常见的时间格式不一样,所以这里我们需要对事件做一个格式化。格式化字符串为
"2006-01-02-15-04-05(规定的必须是这个,方便记忆可以用6-1-2-3-4-5来记)
代码如下:fileName := time.Now().Format("2006-01-02-15-04-05") //存储 this.SaveToFile("uploadname","./static/img/"+fileName+fileExt)
-
保存数据到数据库
这里是数据的插入操作,我们不做详细解释,直接看代码:
//插入数据库//获取orm对象o := orm.NewOrm()//获取要插入的对象var article models.Article//给对象赋值article.ArtiName = articleNamearticle.Acontent = content//这一步需要注意,我们存储的图片是图片地址,没有 .article.Aimg = "/static/img/"+fileName+fileExt//插入o.Insert(&article)
-
返回视图
如果没有视图,先返回一句话,代码如下:
this.Ctx.WriteString("添加成功")
-
完整代码如下:
//获取数据articleName := this.GetString("articleName")content := this.GetString("content")//数据校验if articleName == "" || content == ""{beego.Info("添加文章数据不完整,请重新输入")this.TplName = "add.html"return}//获取上传图片file,head,err := this.GetFile("uploadname")defer file.Close()if err != nil{beego.Info("上传图片错误,请重新添加!")this.TplName = "add.html"return}//文件格式判断fileExt := path.Ext(head.Filename)if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg"{beego.Info("上传图片格式不正确,请重新添加!")this.TplName = "add.html"return}//文件大小判断if head.Size > 5000000{beego.Info("上传图片太大,请重新添加!")this.TplName = "add.html"return}//避免文件重名fileName := time.Now().Format("2006-01-02-15-04-05")this.SaveToFile("uploadname","./static/img/"+fileName+fileExt)//插入数据库//获取orm对象o := orm.NewOrm()//获取要插入的对象var article models.Article//给对象赋值article.ArtiName = articleNamearticle.Acontent = content//这一步需要注意,我们存储的图片是图片地址,没有.article.Aimg = "/static/img/"+fileName+fileExt//插入o.Insert(&article)//返回视图this.Ctx.WriteString("添加成功")
-
-
后台完整代码—添加文章内容
package controllersimport ("github.com/beego/beego/v2/client/orm""github.com/beego/beego/v2/core/logs"beego "github.com/beego/beego/v2/server/web""path""projectName/models""time"
)type ArticleController struct {beego.Controller
}// 展示文章列表页
func (this *ArticleController) ShowArticleList() {this.TplName = "index.html"
}// 展示添加文章页面
func (this *ArticleController) ShowAddArticle() {this.TplName = "add.html"
}// 获取添加文章数据
func (this *ArticleController) HandleAddArticle() {//获取数据 这里获取数据是前端name值articleName := this.GetString("articleName")content := this.GetString("content")select1 := this.GetString("select")//校验数据if articleName == "" || content == "" {this.Data["errmsg"] = "添加数据不完整"this.TplName = "add.html"}logs.Info("chengpeng:", articleName, content, select1)//处理文件上传 文件流,文件相关信息,错误file, head, err := this.GetFile("uploadname")defer file.Close()if err != nil {this.Data["errmsg"] = "文件上传失败"this.TplName = "add.html"return}//1 文件大小if head.Size > 50000000 {this.Data["errmsg"] = "文件太大,请重新上传"this.TplName = "add.html"return}//文件格式//a.jpg 获取后缀名ext := path.Ext(head.Filename)if ext != ".jpg" && ext != ".png" && ext != ".bmp" && ext != ".jpeg" {this.Data["errmsg"] = "文件格式错误,请重新上传"this.TplName = "add.html"return}//防止重名fileName := time.Now().Format("2006-01-02-15-04-05") + ext//存储/**第一个参数是前端标签的name属性值,第二个参数是文件在服务器端存储的位置。**注意:这个位置字符串在前面需要加一个*/err = this.SaveToFile("uploadname", "./static/img/"+fileName)if err != nil {logs.Info("保存前端出过来的文件")return}//数据处理//插入操作newOrm := orm.NewOrm()var article models.Articlearticle.ArtiName = articleNamearticle.Acontent = contentarticle.Aimg = "/static/img/" + fileNamenewOrm.Insert(&article)//返回页面this.Redirect("/showArticleList", 302)
}
添加文章路由代码
package routersimport (beego "github.com/beego/beego/v2/server/web""projectName/controllers"
)func init() {//登录删除这个在上一个有写//beego.Router("/", &controllers.MainController{}, "get:ShowGet;post:Post")//beego.Router("/register", &controllers.UserController{}, "get:ShowRegister;post:HandlePost")//beego.Router("/login", &controllers.UserController{}, "get:ShowLogin;post:HandleLogin")//文章列表页访问beego.Router("/showArticleList", &controllers.ArticleController{}, "get:ShowArticleList")//添加文字beego.Router("/addArticle", &controllers.ArticleController{}, "get:ShowAddArticle;post:HandleAddArticle")
}
5.新闻信息展示
添加文章之后我们回到文章显示界面,我们这里固定显示文章列表页的请求为/ShowArticleList
,然后给这个请求指定控制器,以及相应的方法修改。
5.1修改路由文件
首先我们修改路由文件,代码如下:
beego.Router("/ShowArticleList",&controllers.ArticleController{},"get:ShowArticleList")
5.2后台代码
修改路由文件之后,我们实现ShowArticleList函数。
5.2.1获取所有文章
-
获取orm对象
o := orm.NewOrm()
-
定义一个对象数组(切片),用来存储获取的所有对象
var articles []models.Article
-
指定要查询的数据库表,用QueryTable函数,参数是表名,返回值是queryseter,ORM 以 QuerySeter 来组织查询,每个返回 QuerySeter 的方法都会获得一个新的 QuerySeter 对象。
qs := o.QueryTable("Article")
-
获取所有数据,用all方法,参数是对象数组地址
qs.All(&articles)
-
获取数据之后把数据传递给视图,并且指定视图文件
this.Data["articles"] = articles this.TplName = "index.html"
在浏览器里面输入地址之后能获取下面页面,表示代码没有问题
-
ORM高级查询**(重点)**
我们在后面项目开发中对数据库的查询,一般都是指定数据库表,用高级查询的方法进行查询。ORM支持如下几种高级查询。
函数名 作用 用法 Limit() 获取部分数据 有两个参数,第一个参数是指定获取几条数据,第二个参数指定从哪里获取qs.Limit(size,start)。返回值还是qs OrderBy() 根据指定的字段排序 只有一个参数,参数作用是指定按照哪个字段排序,返回值是qs Distinct() 去重 没有参数,返回值是qs Count() 查询符合条件的数据条目数 没有参数,返回值是查询到的条目数和错误信息 All() 把查询到的数据全部存储到指定的容器里面 只有一个参数,指定存储查询对象的存储容器 RelatedSel() 多表查询的时候使用,指定关联的数据库表 参数长度不限,关联几个表,放几个参数 Filter() 过滤器,相当于SQL语句中的where 有两个参数,第一个参数是指定查询条件,第二个参数是值 … … … 还有其他很多高级查询,具体参考:https://beego.me/docs/mvc/model/query.md页面查看
5.3前端代码
5.3.1视图循环语法
后台传递给视图的数据是对象数组,要访问到每一个对象需要循环访问这个数组,那我们来看一下这个循环语法。循环语法有两种,一种格式如下:
{{range $index,$val := .articles}}{{$val}}
{{end}}
$index
表示的是下标,$val
表示的数组元素,循环的内容放在range和end之间。
另外一种循环如下:
{{range .articles}}{{.Name}}
{{end}}
在range和end之间通过{{.}}直接获取数组元素的字段值。
5.3.2视图数据展示
了解了视图的循环语法之后,我们就可以循环获取控制器传递过来的对象数组数据。代码如下:
{{range .articles}}<tr><td>{{.ArtiName}}</td><td><a href="#">查看详情</a></td><td> {{.Atime.Format "2006-01-02-15-04-05"}}</td><td>{{.Acount}}</td><td><a href="#" class="dels">删除</a></td><td><a href="#">编辑</a></td><td>财经新闻</td></tr>
{{end}}
实现之后我们就可以把添加文章最后的跳转改成显示文章列表页,超链接和文章类型,我们在接下来的页面实现。
5.4数据的分页显示(难点)
观察我们的列表页可以发现,我们文章里表下面是分页展示,接着我们来实现这个分页。分页功能在我们平常浏览网页的时候也比较常见,这里我们先实现简单的分页功能,等到我们项目实战的时候会给大家封装一个分页函数,实现一个高级点的分页。首页显示如下:
分页的好处:如果没有分页,我们访问完数据就要全部在页面显示,有分页之后我们可以显示部分数据,好处有一下两点。
- 方便浏览,分页浏览可以更方便我们平常访问网页。
- 提高访问网站速度。如果一次性把数据全部从数据库中取出来,效率没有一次取出部分数据块。
了解了上面的内容之后我们开始写代码实现分页的功能,一般开发中遇见这种大的功能模块,我们都是划分为几个小块,一点一点来实现。我们从简单到复杂来实现相应功能,首先我们先获取总页数和总记录数。
5.4.1获取总记录数和总页数
首页显示代码是ShowArticleList函数,所以我们分页的业务代码也在这个函数里面。
-
获取总记录数,orm是用count函数来获取数据的记录数,没有参数,返回值为记录数和错误信息,代码如下:
count,err := qs.Count()
-
获取总页数
总页数 = 总记录数 / 每页显示的数据条数
总记录数我们已经获取了,所以需要我们自己设置每页显示多少条数据,然后相除就可以获得,代码如下:
//确定每页显示数pageSize := 2 //获取总页数pageCount := count / pageSize
-
把数据传递给视图,并在视图中显示。
this.Data["count"] = count this.Data["pageCount"] = pageCount
这时候你会发现,当你的最后一页显示的数据不满的话,总页数会少计算一页,原因是我们求总页数的计算是两个整数相除,除不尽的时候会自动舍去小数位。这和我们真实的业务不相符。所以我们需要修改获取总页数的代码。怎么修改呢?完全改成浮点数显然也不行,因为总页码不会是小数。这里面我们用天花板函数Ceil()。Ceil()的作用是传递过来一个浮点数,获取比这个浮点数大的又离这个浮点数最近的整数,代码如下:
//获取总页数pageCount :=math.Ceil(float64(count) / float64(pageSize))
页码这时候显示正确。
5.4.2获取首页和末页数据
获取完总页数和总记录数之后,最简单的功能模块就是首页和末页内容的显示。首页和末页,我们需要把相应的页码传递过来才能知道获取哪些数据。那视图如何给后台传递数据呢?我们在平常浏览网页的时候经常会遇到类似于这样的URL地址
http://tieba.baidu.com/f?fr=index&fp=0&ie=utf-8&red_tag=m2329796506
我们重点关注?后面的内容,他们是成对出现的,每对之间用&
连接,这种是URL传值的一种。我们在后台通过GetString函数可以获取到相应的值。
-
设置首页的超链接
我们可以通过URL传值的方式把页码传递过来。这里我们设置首页的标签超链接为
/ShowArticleList?pageIndex=1
-
获取首页数据
我们先通过GetString()获取到页码,然后通过页码获取相应的数据。这里给大家介绍数据库获取部分数据的函数**Limit() **
Limit()-----这个有bug如果写在下面就会报错–指定之后要查询的数据库表以后必须先查询
**作用:**获取数据库中部分数据
**参数:**第一个参数是获取多少数据,第二个参数是从哪里开始取数据
返回值是queryseter类型,示例代码如下
qs := qs.Limit(pageSize,start)
我们掌握了limit函数之后,现在要获取数据库中部分数据,pageSize我们已经知道了,这个start怎么去求呢?我们可以根据start的规律来找,比如说,第一页数据的起始位置是0,第二页的其实位置是2,第三页的其实位置是4,你发现起始位置刚好是页码减一乘以pageSize,由此我们得出公式。start = (pageIndex - 1) * pageSize
那么我们获取首页的代码如下:
//获取页码pageIndex,_ := this.GetInt("pageIndex") //确定数据的起始位置start := (pageIndex - 1) * pageSize //查询数据库部分数据qs.Limit(pageSize,start).All(&articles)
这时候有个问题,我们从其他页面跳转到首页的时候没有指定pageIndex,所以我们需要对获取不到pageIndex的情况进行处理,**处理方案:**当没有获取到pageIndex的时候默认pageIndex等于1,即默认访问首页内容。修改后的代码如下:
//获取页码pageIndex,err := this.GetInt("pageIndex")if err != nil{pageIndex = 1} //确定数据的起始位置start := (pageIndex - 1) * pageSize //查询数据库部分数据qs.Limit(pageSize,start).All(&articles)
-
获取末页数据只要参考着首页,把传过来的pageIndex改为总页码数即可。设置末页的链接为
/ShowArticleList?pageIndex={{.pageCount}}
这时候记得把页码也传递给视图
5.4.2获取上一页和下一页数据
前面我们已经获取了首页和末页的数据,仿照着链接,我们可以把上一页下一页的链接也实现,设置上一页的超链接为/ShowArticleList?pageIndex={{.pageIndex}} - 1
,但是你在index.html写了这个之后,编辑器会报错,html标签属性不能直接进行数学运算。这时候我们就要想办法,不在视图里面操作,并且给pageIndex减1,方法有很多,这里呢,老师给你们介绍一种beego处理这种简单业务逻辑的方法,视图函数
-
视图函数(模板函数)
**使用条件:**beego支持用户定义视图函数,但是必须在beego.Run()调用之前。
设置如下:
-
先定义函数
func hello(in string)(out string){out = in + "world"return }
-
添加映射
添加映射是把后台的函数名和视图中调用的函数名关联起来,两个名字可以不一样。用的方法是AddFuncMap(),第一个参数是视图中调用的函数名,第二个参数是后台的函数名
beego.AddFuncMap("hi",hello)这一步必须在beego.Run()之前调用
-
在视图中调用,有两种形式
第一种调用视图函数
{{.Content | hi}}
注意,这里面的.Content是传递给函数的参数,类型要一致,函数的返回值将在这里显示,只能传递一个参数
第二种调用视图函数
{{hi .Content}}
第二种方法刚好和第一种方法顺序反过来,是先写函数名,再写参数,如果参数比较多,可以一直往后写。这种方法在开发中也比较常用。
-
beego默认封装的视图函数
函数名 函数作用 使用方法 dateformat 实现了时间的格式化,返回字符串。 {{dateformat .Time “2006-01-02T15:04:05Z07:00”}} date 实现了类似 PHP 的 date 函数,可以很方便的根据字符串返回时间 。 {{date .T “Y-m-d H:i:s”}} compare 实现了比较两个对象的比较,如果相同返回 true,否者 false。 {{compare .A .B}} substr 实现了字符串的截取,支持中文截取的完美截取 {{substr .Str 0 30}} html2str 实现了把 html 转化为字符串,剔除一些 script、css 之类的元素,返回纯文本信息 。 {{html2str .Htmlinfo}} str2html 实现了把相应的字符串当作 HTML 来输出,不转义 {{str2html .Strhtml}} 还有一些其他不常用的, 可以参考开发文档了解
-
-
用视图函数实现获取上一页下一页页码
-
定义函数
因为函数要在beego.Run()之前执行,我们可以把函数直接定义在main.go中,定义函数如下:
//获取下一页页码 func ShowNextPage(pageIndex int)int{return pageIndex + 1 }//获取上一页页码 func ShowPrePage(pageIndex int)int{return pageIndex - 1 }
-
添加映射
beego.AddFuncMap("next",ShowNextPage) beego.AddFuncMap("pre",ShowPrePage)
-
在视图中调用
我们这里用第二种调用视图函数的方法
<li><a href="/ShowArticleList?pageIndex={{pre .pageIndex}}">上一页 </a> </li><li> <a href="/ShowArticleList?pageIndex={{next .pageIndex}}">下一页</a></li>
问题:显示之后,我们点击上一页下一页发现功能实现了,但是有一个问题,一直点击上一页页码能出现负值,一直点击下一页页码能超过总页码,那我们怎么解决呢?
-
问题解决
页码超出范围的问题,思路:只需要在获取上一页下一页页码的时候对页码做一个判断即可,代码如下:
//获取下一页页码 func ShowNextPage(pageIndex int,pageCount int)int{if pageIndex == pageCount{return pageIndex}return pageIndex + 1 }//获取上一页页码 func ShowPrePage(pageIndex int)int{if pageIndex == 1{return pageIndex}return pageIndex - 1 }
到这里我们的分页功能就完全实现了
-
完整后台代码
package controllersimport ("github.com/beego/beego/v2/client/orm""github.com/beego/beego/v2/core/logs"beego "github.com/beego/beego/v2/server/web""math""path""projectName/models""time"
)type ArticleController struct {beego.Controller
}// 展示文章列表页
func (this *ArticleController) ShowArticleList() {//获取数据//高级查询pageSize := 2//获取页码pageIndex, err := this.GetInt("pageIndex")if err != nil {pageIndex = 1}//起始位置计算start := (pageIndex - 1) * pageSize//指定表newOrm := orm.NewOrm()//指定要查询的数据库表queryset := newOrm.QueryTable("Article") //QuerySeter//查询所有的 返回值查询多少数据var articles []models.Article//有bug,写下面就会报错 expected 1 destination arguments in Scan, not 6_, err1 := queryset.Limit(pageSize, start).All(&articles)if err1 != nil {logs.Info("查询数据错误", err1)return}//查询总记录数————总的记录数count, err := queryset.Count()if err != nil {logs.Info("查询总记录数错误", err)return}//获取总页数pageCount := math.Ceil(float64(count) / float64(pageSize))//获取数据//logs.Info("chengpengaini", pageIndex)//传递数据this.Data["pageIndex"] = pageIndexthis.Data["count"] = countthis.Data["pageCount"] = int(pageCount)this.Data["articles"] = articlesthis.TplName = "index.html"
}// 展示添加文章页面
func (this *ArticleController) ShowAddArticle() {this.TplName = "add.html"
}// 获取添加文章数据
func (this *ArticleController) HandleAddArticle() {//获取数据articleName := this.GetString("articleName")content := this.GetString("content")select1 := this.GetString("select")//校验数据if articleName == "" || content == "" {this.Data["errmsg"] = "添加数据不完整"this.TplName = "add.html"}logs.Info("chengpeng:", articleName, content, select1)//处理文件上传 文件流,文件相关信息,错误file, head, err := this.GetFile("uploadname")defer file.Close()if err != nil {this.Data["errmsg"] = "文件上传失败"this.TplName = "add.html"return}if head.Filename == "" {this.Data["errmsg"] = "请上传文件"this.TplName = "add.html"return}//1 文件大小if head.Size > 50000000 {this.Data["errmsg"] = "文件太大,请重新上传"this.TplName = "add.html"return}//文件格式//a.jpg 获取后缀名ext := path.Ext(head.Filename)if ext != ".jpg" && ext != ".png" && ext != ".bmp" && ext != ".jpeg" {this.Data["errmsg"] = "文件格式错误,请重新上传"this.TplName = "add.html"return}//防止重名fileName := time.Now().Format("2006-01-02-15-04-05") + ext//存储/**第一个参数是前端标签的name属性值,第二个参数是文件在服务器端存储的位置。**注意:这个位置字符串在前面需要加一个*/err = this.SaveToFile("uploadname", "./static/img/"+fileName)if err != nil {logs.Info("保存前端出过来的文件")return}//数据处理//插入操作newOrm := orm.NewOrm()var article models.Articlearticle.ArtiName = articleNamearticle.Acontent = contentarticle.Aimg = "/static/img/" + fileNamenewOrm.Insert(&article)//返回页面this.Redirect("/showArticleList", 302)
}
package mainimport (beego "github.com/beego/beego/v2/server/web"//项目运行期间,操作数据库的代码_ "projectName/models"_ "projectName/routers"
)func main() {beego.AddFuncMap("prepage", ShowPrePage)beego.AddFuncMap("nextpage", ShowNextPage)beego.Run()
}// 后台定义函数----视图函数
func ShowPrePage(pageIndex int) int {if pageIndex == 1 {return pageIndex}return pageIndex - 1
}func ShowNextPage(pageIndex int, pageCount int) int {if pageIndex == pageCount {return pageIndex}return pageIndex + 1
}
//文章列表页访问beego.Router("/showArticleList", &controllers.ArticleController{}, "get:ShowArticleList")//添加文字beego.Router("/addArticle", &controllers.ArticleController{}, "get:ShowAddArticle;post:HandleAddArticle")
}
add.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>添加文章内容</title><link rel="stylesheet" type="text/css" href="/static/css/reset.css"><link rel="stylesheet" type="text/css" href="/static/css/main.css">
</head>
<body><div class="header"><a href="#" class="logo fl"><img src="/static/img/logo.png" alt="logo"></a><a href="#" class="logout fr">退 出</a></div><div class="side_bar"><div class="user_info"><img src="/static/img/person.png" alt="张大山"><p>欢迎你 <em>李雷</em></p></div><div class="menu_con"><div class="first_menu active"><a href="javascript:;" class="icon02">文章管理</a></div><ul class="sub_menu show"><li><a href="#" class="icon031">文章列表</a></li><li><a href="#" class="icon032">添加文章</a></li><li><a href="#" class="icon034">添加分类</a></li></ul></div></div><div class="main_body" id="main_body"><div class="breadcrub">当前位置:文章管理>添加文章</div><div class="pannel"><form method="post" action="/addArticle" enctype="multipart/form-data"><h3 class="review_title">添加文章</h3><div class="form_group"><label>文章标题:</label><input type="text" class="input_txt2" name="articleName" ></div><div class="form_group"><label>文章类型:</label><select class="sel_opt" name="select"><option>体育新闻</option><option>财经新闻</option><option>科技新闻</option></select></div><div class="form_group"><label>文章内容:</label><textarea class="input_multxt" name="content"></textarea></div><div class="form_group"><label>上传图片:</label><input type="file" class="input_file" name="uploadname"></div><div class="form_group indent_group line_top"><input type="submit" value="添 加" class="confirm"><span>{{.errmsg}}</span></div></form></div>
</div></body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>后台管理页面</title><link rel="stylesheet" type="text/css" href="/static/css/reset.css"><link rel="stylesheet" type="text/css" href="/static/css/main.css"><script type="text/javascript" src="/static/js/jquery-1.12.4.min.js"></script></head>
<body><div class="header"><a href="#" class="logo fl"><img src="/static/img/logo.png" alt="logo"></a><a href="#" class="logout fr">退 出</a></div><div class="side_bar"><div class="user_info"><img src="/static/img/person.png" alt="张大山"><p>欢迎你 <em>李雷</em></p></div><div class="menu_con"><div class="first_menu active"><a href="javascript:;" class="icon02">文章管理</a></div><ul class="sub_menu show"><li><a href="#" class="icon031">文章列表</a></li><li><a href="/addArticle" class="icon032">添加文章</a></li><li><a href="#" class="icon034">添加分类</a></li></ul></div></div><div class="main_body" id="main_body"><div class="breadcrub">当前位置:文章管理>文章列表</div><div class="pannel"><span class="sel_label">请选择文章分类:</span><select name="select" id="select" class="sel_opt"><option selected="true">财经新闻</option><option>体育新闻</option><option>科技新闻</option></select><table class="common_table"><tr><th width="43%">文章标题</th><th width="10%">文章内容</th><th width="16%">添加时间</th><th width="7%">阅读量</th><th width="7%">删除</th><th width="7%">编辑</th><th width="10%">文章类型</th></tr>{{range $index,$val := .articles}}<tr><td>{{$val.ArtiName}}</td><td><a href="#">查看详情</a></td><td> {{$val.Atime.Format "2006-01-02-15:04:05"}}</td><td>{{$val.Acount}}</td><td><a href="#" class="dels">删除</a></td><td><a href="#">编辑</a></td><td>财经新闻</td></tr>{{end}}</table><ul class="pagenation"><li><a href="/showArticleList?pageIndex=1">首页</a></li><li><a href="/showArticleList?pageIndex={{.pageIndex | prepage }}">上一页 </a> </li><li><a href="/showArticleList?pageIndex={{nextpage .pageIndex .pageCount}}">下一页</a></li><li><a href="/showArticleList?pageIndex={{.pageCount}}">末页</a></li><li>共{{.count}}条记录/共{{.pageCount}}页/当前{{.pageIndex}}页</li></ul></div></div>
</body>
</html>
6.查看文章详情
业务流程图如下:
首先我们还是需要设计一下查看详情的请求路径。分析可知,我们查看文章详情必须指定要查看哪一篇文章,所以我们在点击查看详情的时候需要把能够标识具体哪一篇文章的数据传递给后台,这里我们通过URL传值的方式,传递文章ID给后台,设计路由为/ShowArticleDetail?id=article.Id
6.1文章详情页面显示
-
前端处理
修改查看详情的超链接代码如下:
<td><a href="ShowArticleDetail?id={{.Id}}">查看详情</a></td>
-
修改路由文件,添加查看详情的路由匹配,然后指定控制和请求对应的方法,修改如下:
beego.Router("/ShowArticleDetail",&controllers.ArticleController{},"get:ShowArticleDetail")
-
实现ShowArticleDetail()函数
首先呢,我们需要获取传递过来的文章id
id,err := this.GetInt("id")
然后做数据校验
//数据校验if err != nil{beego.Info("请求路径错误")this.Redirect("/ShowArticleList",302)return}
数据没问题的话,就根据文章id查询文章信息
//查询数据o := orm.NewOrm()var article models.Articlearticle.Id = ido.Read(&article)
获取数据之后,指定视图,并给视图传递数据
//传递数据给视图,并指定视图this.Data["article"] = articlethis.TplName = "content.html"
完整代码如下
//获取文章idid,err := this.GetInt("id") //数据校验if err != nil{beego.Info("请求路径错误")this.Redirect("/ShowArticleList",302)return} //查询数据o := orm.NewOrm()var article models.Articlearticle.Id = ido.Read(&article)//传递数据给视图,并指定视图this.Data["article"] = articlethis.TplName = "content.html"
访问浏览器,查看页面如下:
这时候页面显示的是假数据,我们修改视图文件,让页面显示的数据为我们添加的文章数据:
-
视图文件修改,还没有添加的数据不做修改。
<div class="pannel"><h3 class="review_title">文章详情</h3><div class="form_group"><label>文章标题:</label><p class="detail"><b>{{.article.ArtiName}}</b></p></div><div class="form_group"><label>文章类型:</label><p class="detail">体育新闻</p></div><div class="form_group"><label>文章内容:</label><p class="detail"><img src="{{.article.Aimg}}">{{.article.Acontent}}</p></div><div class="form_group"><label>阅读次数:</label><p class="detail">{{.article.Acount}}</p></div><div class="form_group"><label>最近浏览:</label><p class="detail">张三 | 李四 |</p></div><div class="form_group"><label>创建时间:</label><p class="detail">{{.article.Atime.Format "2006-01-02-15-04-05"}}</p><span>{{.errmsg}}</span></div> </div>
保存之后再次刷新页面,显示如下:
我们查看详情页面的显示这部分就实现了。
6.2阅读次数增加
每次查看详情其实就是阅读次数的增加,我们需要在查看详情函数里面给阅读次数加一,代码如下:
//给查询出来的文章阅读次数加一article.Acount += 1o.Update(&article)
7.编辑文章内容
7.1编辑页面显示
业务流程图如下:
编辑页面显示和文章详情页面处理流程基本一样,也同样需要传递文章ID,先需要确定请求路径,这里我们设置请求路径为UpdateArticle?id=article.id
,修改路由文件,代码如下:
beego.Router("/UpdateArticle",&controllers.ArticleController{},"get:ShowUpdateArticle")
然后在后台查询数据传递给视图,逻辑重复,就不详细分析了,代码如下:
//展示编辑文章界面
func(this*ArticleController)ShowUpdateArticle(){//获取文章idid,err := this.GetInt("id")//数据校验if err != nil{beego.Info("请求路径错误")this.Redirect("/ShowArticleList",302)return}//查询数据o := orm.NewOrm()var article models.Articlearticle.Id = ido.Read(&article)//传递数据给视图,并指定视图this.Data["article"] = articlethis.TplName = "update.html"
}
前端数据展示代码如下:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>更新文章内容</title><link rel="stylesheet" type="text/css" href="/static/css/reset.css"><link rel="stylesheet" type="text/css" href="/static/css/main.css">
</head>
<body><div class="header"><a href="#" class="logo fl"><img src="/static/img/logo.png" alt="logo"></a><a href="#" class="logout fr">退 出</a></div><div class="side_bar"><div class="user_info"><img src="/static/img/person.png" alt="张大山"><p>欢迎你 <em>李雷</em></p></div><div class="menu_con"><div class="first_menu active"><a href="javascript:;" class="icon02">文章管理</a></div><ul class="sub_menu show"><li><a href="#" class="icon031">文章列表</a></li><li><a href="#" class="icon032">添加文章</a></li><li><a href="#" class="icon034">添加分类</a></li></ul></div></div><div class="main_body" id="main_body"><div class="breadcrub">当前位置:文章管理>编辑文章</div><div class="pannel"><form name = logon ><h3 class="review_title">编辑文章</h3><div class="form_group"><label>文章标题:</label><input type="text" class="input_txt2" name = "articleName" value="{{.article.ArtiName}}"></div><div class="form_group"><label>文章内容:</label><textarea class="input_multxt" name="content">{{.article.Acontent}}</textarea></div><div class="form_group"><label>上传图片:</label><img src="{{.article.Aimg}}"><input type="file" name="uploadname" class="input_file"></div><div class="form_group indent_group line_top"><input type="submit" value="添 加" class="confirm"><span>{{.errmsg}}</span></div></form></div></div>
</body>
</html>
在浏览器输入http://192.168.110.73:8080/UpdateArticle?id=1
,显示如下:
7.2编辑文章数据
这一步其实是对查询到的文章进行更新操作,我们还用获取页面时的请求路径UpdateArticle?id=article.id
,但是请求改为post请求,form标签修改如下:
<form name = logon method="post" action="/UpdateArticle?id={{.article.Id}}" enctype="multipart/form-data">
这里需要上传图片,记得给form添加enctype属性
接着我们去路由文件里面给我们这个请求指定方法。
beego.Router("/UpdateArticle",&controllers.ArticleController{},"get:ShowUpdateArticle;post:HandleUpdate")
然后去实现HandleUpdate函数,过程仍然是获取数据,校验数据,更新数据,返回视图这几步,没有什么新的知识点,我们就不做详细分析,直接看代码:
//抽离上传文件函数
func UploadFile(filePath string,this beego.Controller)string{file,head,err :=this.GetFile(filePath)defer file.Close()if err != nil{beego.Info("上传图片错误,请重新添加!")return ""}//文件格式判断fileExt := path.Ext(head.Filename)if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg"{beego.Info("上传图片格式不正确,请重新添加!")return ""}//文件大小判断if head.Size > 5000000{beego.Info("上传图片太大,请重新添加!")return ""}//避免文件重名fileName := time.Now().Format("2006-01-02-15-04-05")this.SaveToFile("uploadname","./static/img/"+fileName+fileExt)return "/static/img/"+fileName+fileExt
}
//处理更新数据
func(this*ArticleController)HandleUpdate(){//获取数据id,err := this.GetInt("id")articleName :=this.GetString("articleName")content := this.GetString("content")img := UploadFile("uploadname",this.Controller)//校验数据,如果数据出错,返回当前编辑页面if err !=nil || articleName =="" || content == "" || img == ""{beego.Info("编辑数据不完整")this.Redirect("/UpdateArticle?id="+strconv.Itoa(id),302)return}//更新数据o := orm.NewOrm()var article models.Articlearticle.Id = idif err := o.Read(&article);err != nil{beego.Info("传递的文章id错误")this.Redirect("/UpdateArticle?id="+strconv.Itoa(id),302)return}article.ArtiName = articleNamearticle.Acontent = contentarticle.Aimg = imgo.Update(&article)//返回视图this.Redirect("/ShowArticleList",302)
}
8.删除文章
8.1删除功能实现
业务流程图如下:
删除功能相比较前面的功能算是比较简单的,只需要传递过来文章id值,然后删除文章即可。
首先我们还是要指定删除文章的请求路径:DeleteArticle?id=article.Id
然后修改路由文件,为删除请求指定控制器,指定函数。
beego.Router("/DeleteArticle",&controllers.ArticleController{},"get:DeleteArticle")
然后在后台实现DeleteArticle函数,代码如下:
//删除文章
func(this*ArticleController)DeleteArticle(){//获取文章Idid,err := this.GetInt("id")if err != nil{beego.Info("删除文章请求路径错误")this.Redirect("/ShowArticleList",302)return}//删除文章o := orm.NewOrm()var article models.Articlearticle.Id = ido.Delete(&article)//返回视图界面this.Redirect("/ShowArticleList",302)
}
这时候你发现功能实现了,但是存在误删的可能,整个页面显的特别不友好,我们给页面加个js提示,防止误删。
8.2删除js提示
业务分析:当点击删除超链接的时候,弹出对话框,如果确认就发送请求,如果取消,就不发送请求:代码如下:
<script type="text/javascript">$(".dels").click(function () {if(!confirm("是否确认删除?")){return false}})
</script>
1.类型相关内容
在实现类型相关业务之前,我们先创建类型表。这里我们添加上一对多多对多的关系。
一个类型下面有很多篇文章,但是一篇文章只属于一个类型,所以文章与类型属于一对多。
同时我们分析,一个用户可以阅读多篇文章,一篇文章也可以被多个用户阅读,所以文章和用户之间属于多对多关系。
由此,我们开始建表,建表代码如下,我们根据代码分析一对多,多对多如何设置:
type User struct {Id intName string `orm:"unique"`Passwd string `orm:"size(20)"`Articles []*Article `orm:"rel(m2m)"` //设置多对多关系
}
//文章结构体
type Article struct {Id int `orm:"pk;auto"`ArtiName string `orm:"size(20)"`Atime time.Time `orm:"auto_now"`Acount int `orm:"default(0);null"`Acontent string `orm:"size(500)"`Aimg string `orm:"size(100)"`ArticleType*ArticleType `orm:"rel(fk)"` //设置一对多关系Users []*User `orm:"reverse(many)"` //设置多对多的反向关系
}
//类型表
type ArticleType struct {Id intTname string `orm:"size(20)"`Articles []*Article `orm:"reverse(many)"` //设置一对多的反向关系
}func init(){//1.连接数据库orm.RegisterDataBase("default","mysql","root:123456@tcp(127.0.0.1:3306)/test?charset=utf8")//2.注册表orm.RegisterModel(new(User),new(Article),new(ArticleType))//3.生成表//1.数据库别名//2.是否强制更新//3.创建表过程是否可见orm.RunSyncdb("default",false,true)
}
根据我们以前学过数据库知识,表与表之间有几种关系?一般有三种,一对一,一对多,多对多,但是我们开发中常用的是一对多和多对多,这里我们重点掌握这两种,了解一对一即可。
orm中如何设置两个表之间的关系呢?
如果两个表之间有关系,ORM通过在两个表对应的结构体中添加对象指针或者对象指针数组来把两个表之间关联起来,并且在对象指针和对象指针数组字段添加上相应的属性,比如我们上面的文章表和类型表属于一对多,就需要在文章结构体中添加一个类型的对象指针,然后设置一对多关系(orm:“rel(fk)”),同样的,在类型表里面需要有一个文章的对象指针数组,并且设置一对多的反向关系(orm:“reverse(many)”)。
-
**一对一 **
关系设置:两个对应的结构体中都添加对方的结构体指针,然后设置一对一关系(orm:“rel(one)”),反向关系设置为orm:“reverse(one)”
-
**一对多 **
关系设置:一对多中两表之间的关系不可互换,以文章表和类型表为例,当创建表的时候
-
在 文章表对应的文章结构体中添加类型表的对象指针,并且设置一对多关系(orm:“rel(fk)”),
-
在 类型张表对应的结构体中添加文章表的对象指针数组,并且设置一对多的反向关系(orm:“reverse(many)”)
生成表的时候,数据库会自动在 文章表中添加类型表的表的Id作为文章表的外键。如图:
一对多插入操作:只需要在文章表插入类型对象即可。代码如下:o := orm.NewOrm() article := models.Article{} artiType := models.ArticleType{Id:id} o.Read(&artiType) article.ArticleType = &artiType o.Insert(&article)
一对多查询: ORM做多表查询的时候默认是惰性查询,即不明确指出来要做多表查询,即便是两个表之间存在关系,ORM也不会给两个表做关联。指定多表查询的函数是RelatedSel()。参数是要关联的表名,可以有多个。代码如下:
count,err = o.QueryTable("Article").RelatedSel("ArticleType").Count()
如果关联表的那个字段没有值,那么数据查不到
-
-
多对多
关系设置:多对多中两表之间的关系是平等的,所以他们的属性设置可以呼唤,以文章表和用户表为例,当创建表的时候
-
在 文章表对应的文章结构体中添加用户表的对象指针数组,并且设置多对多关系(orm:“rel(m2m)”),
-
在用户表对应的结构体中添加文章表的对象指针数组,并且设置多对多的反向关系(orm:“reverse(many)”)
生成表的时候,数据库会生成一个用户和文章之间的关系表,有三个字段,Id,用户表Id,文章表ID。如下图:
多对多插入操作:o := orm.NewOrm() //1.获取操作对象 arti:= Article{Id: 1} //获取article的多对多操作对象 m2m := o.QueryM2M(&arti, "Users")//第一个参数对象必须有主键,第二个参数是字段名 //获取要插入的对象 user := &User{Id:1} o.Read(&user) //多对多对象插入 num, err := m2m.Add(user)//参数可以为对象,指针,对象数组,指针数组
多对多查询:
有两种方法:
第一种:直接用read查询,然后加上LoadRelated ()函数来关联两张表。代码如下:
post := Post{Id: 1} err := o.Read(&post) num, err := o.LoadRelated(&post, "Tags")
优点是简单,快捷。
缺点是返回值不是queryseter,不能调用其他的高级查询。
第二种方法,是通过过滤器查询,指定表之后,用Filter()过滤相应的条件,第一个参数是
表示另一张表的字段__另外一张表的表名__比较的字段
(注意是双下划线),第二个字段是要比较的值,需要注意的是这个顺序是和表的插入顺序相反的。代码如下: -
1.1添加类型
分析过多表之间的操作之后,我们来实现类型有关的业务,首先我们需要先添加类型。
1.1.1添加类型页面显示
-
确定添加类型显示的请求路径为
/AddArticleType
-
在路由文件中添加相关代码。
beego.Router("/addArticleType",&controllers.ArticleController{},"get:ShowAddType")
-
然后去控制器中实现ShowAddType函数,先简单的指定视图。代码如下:
//展示添加文章类型页面 func(this*ArticleController)ShowAddType(){this.TplName = "addType.html" }
-
然后在浏览器输入请求http://192.168.110.74:8080/addArticleType,页面显示如下:
-
由页面可知,我们添加文章类型界面,分两块,一块是上面以表格的形式显示所有类型,一块是下面增加分类。我们先来处理增加分类。
1.1.2添加类型数据处理
添加类型业务比较简单,首先是修改我们的视图页面内容,给form标签请求方式和请求路径,代码如下:
<form method="post" action="/HandleAddType">
接着我们要修改路由文件,给请求指定控制器,指定方法:
beego.Router("/addArticleType",&controllers.ArticleController{},"get:ShowAddType;post:HandleAddType")
然后我们实现一下后台处理函数,这个函数的实现步骤和以前实现添加文章的步骤一样,代码处理还更简单,不详细分析,我们直接看代码:
//处理添加文章类型数据
func(this*ArticleController)HandleAddType(){//获取数据typeName := this.GetString("typeName")//数据校验if typeName == ""{beego.Info("添加数据失败")return}//插入数据库o := orm.NewOrm()var articleType models.ArticleTypearticleType.Tname = typeNameif _,err :=o.Insert(&articleType);err != nil{beego.Info("添加数据失败")return}//返回视图this.TplName = "addType.html"
}
这里我们用渲染的方式返回视图合适不合适,思考一下!
1.1.3查询类型数据
现在我们类型表有数据了,可以在显示页面的时候把数据填充在页面上
-
后台代码
//展示添加文章类型页面 func(this*ArticleController)ShowAddType(){//查询数据o := orm.NewOrm()var articleTypes []models.ArticleTypeo.QueryTable("ArticleType").All(&articleTypes)//传递数据给视图并指定视图this.Data["articleTypes"] = articleTypesthis.TplName = "addType.html" }
-
视图代码
在视图页面中,我们循环控制器传递过来的数组,拿到我们需要的数据
{{range .articleTypes}}<tr><td>{{.Id}}</td><td>{{.Tname}}</td><td><a href="javascript:;" class="edit">删除</a></td></tr> {{end}}
这时候我们在浏览器输入地址
http://192.168.110.75:8080/addArticleType
,得到如下页面:
添加一个类型测试,然后发现页面还是没有类型显示,这个说明我们代码处理出问题了,哪里出问题了呢?还记得前面给大家留的思考题吗?我们添加完文章类型之后,是直接渲染加载了视图,这时候并没有给视图传递数据,所以也就没有类型显示。这样的结果和我们的业务 不符合,所以我们需要把添加完类型之后跳转页面的方式改为重定向,然后再看结果,发现类型显示正常。1.2首页根据下拉框选项不同,获取不同类型数据
现在有类型数据了,我们添加文章的时候也需要添加上类型了。
1.2.1添加带类型的文章
-
在展示页面的时候需要把类型数据绑定添加类型的下拉框
-
后台获取数据(在展示添加文章界面那个函数里面写相关代码)
//展示添加文章界面 func (this*ArticleController)ShowAddArticle(){//查询数据o := orm.NewOrm()var articleTypes []models.ArticleTypeo.QueryTable("ArticleType").All(&articleTypes)//传递数据给视图并指定视图this.Data["articleTypes"] = articleTypesthis.TplName = "add.html" }
-
视图展示数据
循环获取数据,在下拉框中显示类型名称
<select class="sel_opt" name="select">{{range .articleTypes}}<option>{{.Tname}}</option>{{end}} </select>
-
-
添加文章的时候指定文章类型,代码如下:
//给文章对象指定文章类型var articleType models.ArticleTypearticleType.Tname = typeNameo.Read(&articleType,"Tname")article.ArticleType = &articleType//插入o.Insert(&article)
1.2.2列表页展示文章时,展示类型信息。
-
查询所有问章,关联文章类型表(查询的时候加上RelatedSel(“ArticleType”)),代码如下:
qs.Limit(pageSize,start).RelatedSel("ArticleType").All(&articles)
显示的时候显示出来
{{range .articles}}<tr><td>{{.ArtiName}}</td><td><a href="ShowArticleDetail?id={{.Id}}">查看详情</a></td><td> {{.Atime.Format "2006-01-02-15-04-05"}}</td><td>{{.Acount}}</td><td><a href="/DeleteArticle?id={{.Id}}" class="dels">删除</a></td><td><a href="UpdateArticle?id={{.Id}}">编辑</a></td><td>{{.ArticleType.Tname}}</td></tr> {{end}}
这时候你发现,以前添加的文章都没有显示,还记得我们前面介绍多表操作的时候介绍的吗,加上RelatedSel之后,如果相应的字段没有数据,将查询不出来。
1.2.3根据下拉框选项不同,获取不同类型数据(难点)
-
查询类型数据,并把数据绑定到下拉框
这个业务代码和添加文章的业务代码一样,我们就不做详细分析,直接看代码:
//查询数据var articleTypes []models.ArticleTypeo.QueryTable("ArticleType").All(&articleTypes)this.Data["articleTypes"] = articleTypes
视图代码:
<select name="select" id="select" class="sel_opt">{{range .articleTypes}}<option selected="true">{{.Tname}}</option>{{end}} </select>
-
根据下拉框选中类型,获取相同类型的文章
-
把选中的类型数据传递给后台
我们以前传递数据是用form表单,这里我们还是用form表单把下拉框包起来,然后把选中的数据传递给后台。代码如下:
<form method="get" action="/ShowArticleList"><select name="select" id="select" class="sel_opt">{{range .articleTypes}}<option selected="true">{{.Tname}}</option>{{end}}</select> </form>
思考,我们为什么用get请求不用post请求
这里没有发送请求按钮(尽量不要改美工设计的页面),我们通过js代码发送请求,js代码如下:
$("#select").change(function () {$("#form").submit() })
-
根据获取的类型,查询有多少条数据,以及显示相同类型的文章
-
获取前端传递过来的数据
//获取类型名称typeName := this.GetString("select")
-
根据类型,查询有多少条符合条件的数据,但是,需要注意这里面要考虑没有传递类型名称的请求,所以需要做个判断,代码如下:
//获取类型名称 typeName := this.GetString("select") //查询数据,以及分页显示 o := orm.NewOrm() qs := o.QueryTable("Article") var count int64 //数据校验 if typeName == ""{count,_ =qs.RelatedSel("ArticleType").Filter("ArticleType__Tname",typeName).Count() }else {count,_ =qs.RelatedSel("ArticleType").Filter("ArticleType__Tname",typeName).Count() }
-
其他处理分页的业务代码不变,代码如下:
//确定每页显示数 pageSize := 2 //获取总页数 pageCount :=math.Ceil(float64(count) / float64(pageSize)) //获取页码 pageIndex,err := this.GetInt("pageIndex") if err != nil{pageIndex = 1 } //确定数据的起始位置 start := (pageIndex - 1) * pageSize
-
根据类型查询相同类型的数据,同样需要做一个判断。代码如下:
//查询相应类型的数据 var articles []models.Article if typeName ==""{qs.RelatedSel("ArticleType").Limit(pageSize,start).All(&articles) }else {qs.RelatedSel("ArticleType").Filter("ArticleType__Tname",typeName).Limit(pageSize,start).All(&articles) }
-
其他代码不变,获取列表页完整代码如下:
func(this*ArticleController)ShowArticleList(){//获取类型名称typeName := this.GetString("select")//查询数据,以及分页显示o := orm.NewOrm()qs := o.QueryTable("Article")var count int64//数据校验sif typeName == ""{count,_ =qs.RelatedSel("ArticleType").Filter("ArticleType__Tname",typeName).Count()}else {count,_ =qs.RelatedSel("ArticleType").Filter("ArticleType__Tname",typeName).Count()}//确定每页显示数pageSize := 2//获取总页数pageCount :=math.Ceil(float64(count) / float64(pageSize))//获取页码pageIndex,err := this.GetInt("pageIndex")if err != nil{pageIndex = 1}//确定数据的起始位置start := (pageIndex - 1) * pageSize//查询相应类型的数据var articles []models.Articleif typeName ==""{qs.RelatedSel("ArticleType").Limit(pageSize,start).All(&articles)}else {qs.RelatedSel("ArticleType").Filter("ArticleType__Tname",typeName).Limit(pageSize,start).All(&articles)}//查询数据库部分数据//获取类型数据//查询数据var articleTypes []models.ArticleTypeo.QueryTable("ArticleType").All(&articleTypes)this.Data["articleTypes"] = articleTypesthis.Data["count"] = countthis.Data["pageCount"] = int(pageCount)this.Data["pageIndex"] = pageIndex//传递数据并指定视图this.Data["articles"] = articlesthis.TplName = "index.html" }
这时候你再看页面,会发现一个问题,有一个选项一直都不能够选中。为什么呢?
是因为,我们每一次改变下拉框的选项,都会让js发出get请求给后台,后台就会重新查询所有的类型表绑定下拉框,所以每次显示的都是一个数据,这样的话,我们选中显示的那条数据,就无法触发js发送请求,因为js认为下拉框显示内容并没有变化。这时候下拉框显示也有问题,那怎么解决这个问题呢?
1.2.4解决下拉框选项显示的问题
-
-
通过前面的分析,我们知道每次下拉框都是重新从数据库中获取类型数据进行绑定,这里面我们就需要对选中的类型加一个判断,当从数据库中取出的数据是选中的类型时,就给下拉框选项属性selected设置为true。首先后台要传递当前选中的类型名称给视图,代码如下:
//传递当下拉框选择的类型名给视图
this.Data["typeName"] = typeName
-
前端代码处理
视图中我们接收控制器传递过来的当前选中类型,然后与数据库中的类型名进行比较,如果相同就设置选中不同就不设置,代码如下:
<select name="select" id="select" class="sel_opt">{{range .articleTypes}}{{if compare .Tname $.typeName}}<option selected="true">{{.Tname}}</option>{{else}}<option>{{.Tname}}</option>{{end}}{{end}} </select>
需要注意的是,如果是在循环中获取控制器传递过来的数据,不能直接用
.
,要用$.
然后刷新页面,我们发现问题能够解决了。
2.Session和Cookie
接着我们再来重新看一下我们的项目还有哪些功能没有实现呢?
1.我们打开登陆界面发现,登陆界面有一个记录用户名选项,这个功能我们还没有实现。
2.我们实现功能其实都是类似一个新闻类APP的后台,这种页面肯定需要做登陆判断,所以我们还需要做登陆判断。
3.有登陆判断,就要实现退出登陆功能。
4.打开文章详情页,我们发现最近浏览这一行内容没有实现,这里我们也需要实现一下。
在实现这四个功能之前老师要给你们介绍一个新的知识点,Session和Cookie,我们这四个功能都需要用到这四个功能。那么Session和Cookie又是什么呢?Session和Cookie作用在有些时候是一样的,他们都是用来保存用户数据的。但是他们的某些特性又非常的不同,导致他们的应用场景不同。接下来我们来详细的了解一下这两种技术。
Cookie
用来一定时间的保存用户数据,数据存储在客户端(网站的客户端就是浏览器),启用的时候能设置Cookie的有效时间,当时间截至的时候,Cookie失效.
Beego中对Cookie的存取删
Beego把数据存储到Cookie中代码如下:
this.Ctx.SetCookie(key,value,time)//第一个参数是Cookie的key值,第二个参数是Cookie的value值,第三个参数是设置的Cookie的有效时间。
取Cookie的代码如下:
this.Ctx.GetCookie(key)//参数是Cookie的key值,返回值是对应的value值。当没有对应的Cookie或者Cookie已失效,返回空字符串
删除Cookie的代码如下:
this.Ctx.SetCookie(key,value,0)//第一个参数是Cookie的key值,第二个参数任意值,第三个参数把Cookie的值设置为小于0,就马上失效。
Session
也是用来一定时间的保存用户数据,不过数据存储在服务器,Beego启用Sesssion的时候需要在配置文件中开启Session功能。在Beego使用中,一般不设置Session的时间,当浏览器关闭的时候,Session失效。
**Beego中对Session的存取 **
如果想要在项目中使用Session功能,需要先在配置文件中设置Sessionon=true
Beego存储Session的代码:
this.SetSession(key,value)//两个参数,一个是Session的key,第二个是Session的Value
获取Session的代码如下:
this.GetSession(key)//参数是Session的key值,返回值是Session对应的value值,类型是interface{}
删除Session的代码如下:
this.DelSession(key)//参数是Session的key值
我们通过表格来分析他们的不同
不同点 | Cookie | Session |
---|---|---|
数据存储位置 | 客户端 | 服务器 |
数据安全性(相比较而言) | 低 | 高 |
生命周期 | 随着设置时间的结束,生命周期结束 | 当浏览器关闭的时候,生命周期结束 |
适用场景 | 对安全性要求不高的,需要存储时间较长的数据 | 安全性要求搞,不需要长期存储的数据 |
简单了解了这两个知识点之后,我们来看一下,如何实现我们项目剩余的四个功能。
2.1记住用户名
在登录页如果我们勾选了记住用户名的选项框,在下次登陆的时候,用户名那一栏就默认显示上次存储的用户名。并且记住用户名默认勾选,如果我们取消勾选记住用户名,下次访问登陆页面的时候就不显示用户名,记住用户名也不默认勾选。一般情况下,记住用户名都能记住很久,对安全系数要求也不是很高,这里我们用Cookie来实现这个功能。
我们观察视图代码发现,当登陆的时候,form表单提交了记住用户名单选框的数据,用beego.Info()打印一下获取到的数据,发现当记住用户名选中的时候我们在后台会会获取到字符串"on",没有选中的时候获取不到,根据这个现象,我们可以用来判断是否邓丽,当登陆的时候,我们可以用Cookie存储用户名,在没有选中的时候删除Cookie。代码如下:
//处理注册用户名数据
//获取数据
remember := this.GetString("remember")
beego.Info(remember)
if remember == "on"{beego.Info(remember)this.Ctx.SetCookie("userName",userName,1000)
}else {this.Ctx.SetCookie("userName",userName,-1)
}
在展示登陆页面的时候,我们需要去获取Cookie的值,然后判断,如果获取到了Cookie的值,就在用户名里面显示,并且把记住用户名设置为选中状态,如果没有获取到Cookie的值就把用户名设置为空,记住用户名设置为非选中状态,代码如下:
//获取数据
userName := this.Ctx.GetCookie("userName")
//对数据进行判断,然后设置数据传递给视图
if userName != ""{this.Data["userName"] = userNamethis.Data["checked"] = "checked"
}else{this.Data["userName"] = ""this.Data["checked"] = ""
}
视图中接收数据:
<form class="login_form" name = "login" action="/login" method="post"><h1 class="login_title">用户登录</h1><input type="text" class="input_txt" name = "userName" value="{{.userName}}"><input type="password" name = "passwd" class="input_txt"><div class="remember"><input type="checkbox" name="remember" {{.checked}} ><label>记住用户名</label></div><input type="submit" value="登 录" class="input_sub">
</form>
注意,当checkbox添加一个checked属性时,checkbox就为选中状态
2.2登陆判断
因为我们操作的都是后台管理界面,所以我们需要做登陆判断。我们这里面用Session来实现这个功能。
在使用Session之前记得要在配置文件中设置sessionon=true
当登陆成功之后就设置Session,代码如下:
//设置session
this.SetSession("userName",userName)
后台几个展示页面的函数都需要获取session,然后判断,代码如下:
//获取session,并判断是否为空,如果为空跳转到登录页面
userName := this.GetSession("userName")
if userName == nil{this.Redirect("/ShowLogin",302)return
}
2.3退出登陆
退出登录其实就是删除登陆session,然后跳转回登陆界面。
-
在文章列表页有个退出登陆,我们需要给他加一个href,这里我们规定退出登陆的请求路径为
/logout
:<a href="/logout" class="logout fr">退 出</a>
-
接着我们在路由中指定请求对应的控制器和方法
beego.Router("/logout",&controllers.ArticleController{},"get:Logout")
-
然后我们实现一个Logout函数,业务逻辑很简单,我们直接看代码
//退出登录 func(this*ArticleController)Logout(){//删除sessionthis.DelSession("userName")//跳转this.Redirect("/login",302) }
2.4最近浏览
最近浏览也就是在我们浏览文章的时候给文章添加上用户信息,然后在再查询这些信息,在页面中显示。
-
添加浏览信息
我们这里是给文章表添加浏览的用户信息。代码如下:
//获取ORM对象 o := orm.NewOrm() //获取插入数据的对象 var article models.Article article.Id = id o.Read(&article) //获取多对多操作对象,用的是函数QueryM2M(),第一个参数是要插入数据的对象,第二个参数是要插入数据的字段名,返回值是多对多操作对象 m2m := o.QueryM2M(&article,"Users") //获取要插入的对象 user := models.User{Name:userName.(string)} o.Read(&user,"Name") //多对多插入 m2m.Add(user)
-
显示浏览信息
有两种显示多对多信息的方法
第一种,直接加载多对多关系,用的函数是LoadRelated(),第一个参数是查询对象,第二个参数是多对多关系字段,代码如下:
num,err := o.LoadRelated(&article,"Users")
这时候我们在前端就可以循环显示最近浏览的用户信息,这里我们用第二种视图循环语法:
<label>最近浏览:</label> <p class="detail">{{range .article.Users}}{{.Name}} | {{end}}</p>
这时候我们多点几次查看详情会发现个问题,我们添加关系的时候是浏览一次就添加一次,那么我们显示的时候就会重复显示相同用户的用户名,效果如下:
但是我们一般浏览网页的时候,一个用户浏览过了只显示一次该用户信息即可,所以这里面我们需要去重,还记得我们前面介绍的高级查询去重的方法吗?Distinct()
去重,但是这个函数必须要是queryseter对象才能操作,所以我们第一种多对多查询方法就不行了。这里我们用第二种多对多查询。代码如下:
var users []models.User
o.QueryTable("User").Filter("Articles__Article__Id",article.Id).Distinct().All(&users)
注意:我们这里插入的是想article中插入user,但是查询的是从user中去获取。
3.项目优化
3.1路由过滤器
我们在项目实现的时候,只给文章列表页和详情页添加了登陆判断,我们思考一下,我们这个案例其实是整个的后台管理,所以每个页面都需要添加登陆判断,那我们就需要每个地方都要添加登陆判断,重复代码很多。这里给大家介绍一个新的技术,路由过滤器,在路由层面添加一个过滤,实现登陆判断。那我们来看一下什么是路由过滤器。
作用:可以根据指定的匹配规则在特定的项目运行阶段去执行自定义函数,函数一般放在beego.router()之前 。
那我们看一下路由过滤器函数的格式:
beego.InsertFilter("/article/*", position int, filter FilterFunc)
第一个参数是路由匹配规则,支持正则
第二个参数是指定项目运行阶段,在beego项目运行过程中,框架帮我们分了五个阶段,分别是:
a) BeforeStatic 静态地址之前
b) BeforeRouter 寻找路由之前
c) BeforeExec 找到路由之后,开始执行相应的 Controller 之前
d) AfterExec 执行完 Controller 逻辑之后执行的过滤器
e) FinishRouter 执行完逻辑之后执行的过滤器
具体对应是如下这种图的时间点:
第三个参数,就是指定过滤器函数。
路由过滤器一般放在beego.Router()之前。
那么我们接着来看一下过滤器函数的格式:
type FilterFunc func(*context.Context)
参数必须是context.Context
示例代码:
var BeforeExecFunc = func(ctx * context.Context) {userName:=ctx.Input.Session("userName")if userName == nil{ctx.Redirect(302,"/login")}
}
beego.InsertFilter("/index",beego.BeforeExec,BeforeExecFunc)
3.2视图布局
实现了过滤器函数之后,我们再来看我们整个项目,页面显示如下:
你会发现有些内容在每个页面中都有显示,那我们能不能避免这些重复操作呢?这里给大家介绍一个新的知识点,视图布局:
**作用:**通过设置模板页面,其他页面可以直接调用模板,避免再次处理重复代码。
视图布局本质上就是两个html界面的拼接,比如我们现在有一个包含重复部分的html界面layout.html,还有一个只包含添加文章业务的界面,我们可以根据如下去实现两个页面的拼接 。
操作如下:
控制器代码如下:
this.Layout = "layout.html"
this.TplName = "add.html"
layout.html中的代码:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>后台管理页面</title><link rel="stylesheet" type="text/css" href="/static/css/reset.css"><link rel="stylesheet" type="text/css" href="/static/css/main.css"><script type="text/javascript" src="/static/js/jquery-1.12.4.min.js"></script></head>
<body><div class="header"><a href="#" class="logo fl"><img src="/static/img/logo.png" alt="logo"></a><a href="/logout" class="logout fr">退 出</a>
</div><div class="side_bar"><div class="user_info"><img src="/static/img/person.png" alt="张大山"><p>欢迎你 <em>李雷</em></p></div><div class="menu_con"><div class="first_menu active"><a href="javascript:;" class="icon02">文章管理</a></div><ul class="sub_menu show"><li><a href="#" class="icon031">文章列表</a></li><li><a href="/addArticle" class="icon032">添加文章</a></li><li><a href="#" class="icon034">添加分类</a></li></ul></div>
</div>{{.LayoutContent}}</body>
</html>
注意这里面的 {{.LayoutContent}},这个标签的地方就是用来存放add.html的地方。
add.html中就可以删除掉相同的代码,代码如下:
<div class="main_body" id="main_body"><div class="breadcrub">当前位置:文章管理>添加文章</div><div class="pannel"><form method="post" action="/addArticle" enctype="multipart/form-data"><h3 class="review_title">添加文章</h3><div class="form_group"><label>文章标题:</label><input type="text" class="input_txt2" name="articleName" ></div><div class="form_group"><label>文章类型:</label><select class="sel_opt" name="select">{{range .articleTypes}}<option>{{.Tname}}</option>{{end}}</select></div><div class="form_group"><label>文章内容:</label><textarea class="input_multxt" name="content"></textarea></div><div class="form_group"><label>上传图片:</label><input type="file" class="input_file" name="uploadname"></div><div class="form_group indent_group line_top"><input type="submit" value="添 加" class="confirm"><span>{{.errmsg}}</span></div></form></div>
</div>
在浏览器输入网址,这时候你可能会发现问题,我们的
-
js代码传递
细心的同学还会发现,我们在某些页面需要加js代码,这个 内容怎么传递到页面当中呢,这里再给大家介绍一个功能LayoutSection。
**LayoutSection **作用:this.Layout指定了模板文件,可以实现两个页面的拼接,那有时候某些js或者是css样式,该如何传递呢?我们可以用LayoutSection传递。
**LayoutSection **:用法:
控制器代码:
this.Layout = " layout.html" this.LayoutSections = make(map[string]string) this.LayoutSections["Scripts"] = "scripts.html"
在layout.html中添加下面相应内容:
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>后台管理页面</title><link rel="stylesheet" type="text/css" href="/static/css/reset.css"><link rel="stylesheet" type="text/css" href="/static/css/main.css"><script type="text/javascript" src="/static/js/jquery-1.12.4.min.js"></script></head> <body><div class="header"><a href="#" class="logo fl"><img src="/static/img/logo.png" alt="logo"></a><a href="/logout" class="logout fr">退 出</a> </div><div class="side_bar"><div class="user_info"><img src="/static/img/person.png" alt="张大山"><p>欢迎你 <em>李雷</em></p></div><div class="menu_con"><div class="first_menu active"><a href="javascript:;" class="icon02">文章管理</a></div><ul class="sub_menu show"><li><a href="#" class="icon031">文章列表</a></li><li><a href="/addArticle" class="icon032">添加文章</a></li><li><a href="#" class="icon034">添加分类</a></li></ul></div> </div>{{.LayoutContent}}</body> </html> {{.Scripts}}
3.3补充
我们回顾一下,看看我们的项目还有哪点没有实现呢?类型的删除是不是还没有实现,可能有的学生会说,老师这个删除和文章的删除一样,直接删除不久行了嘛!这里老师要特别提醒:**类型是与多表操作有关的,删除效果和单表的文章不一样 **
那我们来看一下类型的删除:
同样还是四步骤:**请求->路由->控制器->视图 **
-
请求
删除类型是在添加类型页面中实现的,在这个页面中有一个删除的标签,如下图所示:
源码路径:https://gitee.com/cheng-penga/chengpeng-beego-go