├── .gitattributes ├── .gitignore ├── Dockerfile ├── README.md ├── api └── v1 │ ├── article.go │ ├── category.go │ ├── login.go │ ├── profile.go │ ├── upload.go │ └── user.go ├── blog.sql ├── config └── config.ini ├── go.mod ├── go.sum ├── main.go ├── middleware ├── cors.go ├── jwt.go └── logger.go ├── model ├── Article.go ├── Category.go ├── Comment.go ├── Email.go ├── Profile.go ├── Update.go ├── User.go ├── UserArticle.go ├── db.go └── session.go ├── proto ├── req.go └── rsp.go ├── routes └── router.go ├── serve.sh ├── static ├── admin │ ├── favicon.ico │ ├── index.html │ └── static │ │ ├── css │ │ ├── app.1c53c242.css │ │ └── chunk-vendors.34cdb930.css │ │ └── js │ │ ├── app.46f36fce.js │ │ ├── app.46f36fce.js.map │ │ ├── chunk-vendors.1ec65ce1.js │ │ └── chunk-vendors.1ec65ce1.js.map └── front │ ├── favicon.ico │ ├── index.html │ └── static │ ├── css │ ├── app.e55e68b9.css │ └── chunk-vendors.65985091.css │ └── js │ ├── app.7b3b961d.js │ ├── app.7b3b961d.js.map │ ├── chunk-vendors.dfe34836.js │ └── chunk-vendors.dfe34836.js.map ├── utils ├── errmsg │ └── errmsg.go ├── setting.go ├── time.go ├── uuid.go └── validator │ └── validator.go └── web ├── admin ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── css │ │ │ └── style.css │ │ └── logo.png │ ├── components │ │ ├── admin │ │ │ ├── Footer.vue │ │ │ ├── Header.vue │ │ │ ├── Index.vue │ │ │ └── Nav.vue │ │ ├── article │ │ │ ├── AddArt.vue │ │ │ └── ArtList.vue │ │ ├── category │ │ │ └── CateList.vue │ │ ├── editor │ │ │ ├── icons │ │ │ │ └── default │ │ │ │ │ └── icons.min.js │ │ │ ├── index.vue │ │ │ ├── langs │ │ │ │ ├── README.md │ │ │ │ └── zh_CN.js │ │ │ ├── plugins │ │ │ │ ├── code │ │ │ │ │ ├── plugin.js │ │ │ │ │ └── plugin.min.js │ │ │ │ ├── codesample │ │ │ │ │ ├── plugin.js │ │ │ │ │ └── plugin.min.js │ │ │ │ ├── image │ │ │ │ │ ├── plugin.js │ │ │ │ │ └── plugin.min.js │ │ │ │ ├── imagetools │ │ │ │ │ ├── plugin.js │ │ │ │ │ └── plugin.min.js │ │ │ │ ├── paste │ │ │ │ │ ├── plugin.js │ │ │ │ │ └── plugin.min.js │ │ │ │ ├── preview │ │ │ │ │ ├── plugin.js │ │ │ │ │ ├── plugin.min.js │ │ │ │ │ └── save │ │ │ │ │ │ ├── plugin.js │ │ │ │ │ │ └── plugin.min.js │ │ │ │ └── wordcount │ │ │ │ │ ├── plugin.js │ │ │ │ │ └── plugin.min.js │ │ │ ├── skins │ │ │ │ ├── content │ │ │ │ │ ├── dark │ │ │ │ │ │ └── content.min.css │ │ │ │ │ ├── default │ │ │ │ │ │ └── content.min.css │ │ │ │ │ ├── document │ │ │ │ │ │ └── content.min.css │ │ │ │ │ └── writer │ │ │ │ │ │ └── content.min.css │ │ │ │ └── ui │ │ │ │ │ ├── oxide-dark │ │ │ │ │ ├── content.inline.min.css │ │ │ │ │ ├── content.min.css │ │ │ │ │ ├── content.mobile.min.css │ │ │ │ │ ├── fonts │ │ │ │ │ │ └── tinymce-mobile.woff │ │ │ │ │ ├── skin.min.css │ │ │ │ │ └── skin.mobile.min.css │ │ │ │ │ └── oxide │ │ │ │ │ ├── content.inline.min.css │ │ │ │ │ ├── content.min.css │ │ │ │ │ ├── content.mobile.min.css │ │ │ │ │ ├── fonts │ │ │ │ │ └── tinymce-mobile.woff │ │ │ │ │ ├── skin.min.css │ │ │ │ │ └── skin.mobile.min.css │ │ │ ├── themes │ │ │ │ ├── mobile │ │ │ │ │ └── theme.min.js │ │ │ │ └── silver │ │ │ │ │ └── theme.min.js │ │ │ └── tinymce.min.js │ │ └── user │ │ │ ├── Profile.vue │ │ │ └── UserList.vue │ ├── config │ │ └── index.js │ ├── main.js │ ├── plugin │ │ ├── antui.js │ │ └── http.js │ ├── router │ │ └── index.js │ ├── store │ │ └── index.js │ └── views │ │ ├── Admin.vue │ │ └── Login.vue ├── vue.config.js └── yarn.lock └── front ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── bg1.png │ ├── bg5.webp.png │ ├── logo.png │ ├── logo.svg │ ├── my4.jpg │ └── 一色3.jpg ├── components │ ├── ArticleList.vue │ ├── Detail.vue │ ├── Footer.vue │ ├── Nav.vue │ └── TopBar.vue ├── main.js ├── plugins │ ├── http.js │ └── vuetify.js ├── router │ └── index.js ├── store │ └── index.js └── views │ ├── Home.vue │ └── Login.vue ├── vue.config.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # THIS IS ONLY FOR THE gitattributes REPOSITORY. 2 | # Handle line endings automatically for files detected as text 3 | # and leave all files detected as binary untouched. 4 | * text=auto 5 | 6 | # 7 | # The above will handle all files NOT found below 8 | # 9 | # These files are text and should be normalized (Convert crlf => lf) 10 | *.gitattributes text 11 | .gitignore text 12 | *.md text diff=markdown 13 | 14 | 15 | 16 | # 17 | # Enable syntax highlighting for files with `.gitattributes` extensions. 18 | # 19 | *.gitattributes linguist-language=gitattributes 20 | *.js linguist-language=golang 21 | *.css linguist-language=golang 22 | *.html linguist-language=golang 23 | *.vue linguist-language=golang 24 | 25 | *.sh linguist-language=shell 26 | *.go -text diff=golang 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log/* 2 | *.log 3 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 as builder 2 | 3 | WORKDIR /build 4 | COPY . /build 5 | 6 | ENV GOPROXY=https://goproxy.cn,direct 7 | 8 | RUN go build -a -o basic 9 | 10 | FROM centos:latest 11 | 12 | WORKDIR /data 13 | 14 | COPY --from=builder /build/basic . 15 | 16 | VOLUME ["/data/log"] 17 | 18 | EXPOSE 80 19 | ENTRYPOINT ["./basic"] 20 | 21 | #FROM golang:latest 22 | # 23 | #MAINTAINER tmnhs 24 | # 25 | #RUN go env -w GO111MODULE=on 26 | #RUN go env -w GOPROXY=https://goproxy.cn,direct 27 | # 28 | #WORKDIR $GOPATH/src/myblog 29 | #COPY . $GOPATH/src/myblog 30 | # 31 | #RUN go build -o myblog . 32 | # 33 | #EXPOSE 80 34 | # 35 | #CMD ["./myblog"] 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 这是一个用gin框架和vue框架搭建一个简易博客前台展示和后台管理系统 2 | 3 | ### 博客前台展示页面 4 | 5 |
6 | 7 |
8 | 9 | ### 博客后台管理页面 10 |
11 | 12 |
13 | 14 | ### 一.技术选型 15 | -前端:用基于vue的``ant design vue``构建后台管理页面和基于vue的``vuetify``构建前台展示页面。 16 | 17 | -后端:用``Gin``快速搭建基础restful风格API,Gin是一个go语言编写的Web框架。 18 | 19 | -数据库:采用``MySql``,使用gorm实现对数据库的基本操作。 20 | 21 | -缓存:使用``Redis``实现记录缓存。 22 | 23 | -日志:使用``logrus``实现日志记录。 24 | 25 | ### 二.项目结构 26 | 27 | ```shell 28 | ├── api 29 | │ └── v1 30 | ├── config 31 | ├── middleware 32 | ├── model 33 | ├── proto 34 | ├── routes 35 | ├── static 36 | │ └── admin 37 | │ └── front 38 | └── utils 39 | | └── errmsg 40 | | └── validator 41 | ├── web 42 | │ └── admin 43 | │ └── front 44 | ``` 45 | | 文件夹 | 说明 | 描述 | 46 | | ------------- | --------- | ---------------- | 47 | | `api` | api层 | api层 | 48 | | `--v1` | v1版本接口 | v1版本接口 | 49 | | `config` | 配置包 | 配置文件 | 50 | | `middleware` | 中间件层 | 用于存放 `gin` 中间件代码 | 51 | | `model` | 模型层 | 模型对应数据表和数据库查询 | 52 | | `routes` | 路由层 | 路由层 | 53 | | `resource` | 静态资源文件夹 | 负责存放静态文件 | 54 | | `--admin` | admin | 后台管理dist文件打包 | 55 | | `--front` | front | 前台展示dist文件打包 | 56 | | `utils` | 工具包 | 工具函数封装 | 57 | | `--errmsg` | errmsg | 错误信息的封装 | 58 | | `--validator` | validator | 后端数据校验 | 59 | | `web` | web | 前端代码 | 60 | | `--admin` | admin | 后台管理的前端代码 | 61 | | `--front` | front | 前台展示的前端代码 | 62 | 63 | ### 三.使用说明 64 | 65 | 1-需要把config/config.ini里的文件配置(特别是数据库mysql的配置)修改成自己需要的配置,本项目使用七牛云对象存储上传的文件,你可以自己在七牛云注册一个账号,可以免费获赠10G的存储空间 66 | 67 | 2-本项目可以直接在windows上运行,建议使用goland,可以将blog.sql导入数据库中,初始登录用户为admin,密码为123456 68 | 69 | 3-[前台展示](http://localhost:8080/front)和 [后台管理] (http://localhost:8080/admin) 的切换需要修改routes/router.go,打开相关注释即可 70 | 71 | 4-如果需要修改前端部分,可以修改web下的文件,修改完后运行`npm run build`,把dist文件覆盖掉static里的文件 72 | 73 | 5-本项目是前后端分离项目,可以注释掉routes/router.go中加载静态资源的代码,把后端代码运行起来,然后在web/admin(或者web/front)目录下,运行`npm run serve`即可 74 | 75 | ### 四.部署项目(Linux系统下) 76 | 77 | 方式一:使用dockerfile部署(确保服务器上有下载docker) 78 | 79 | ```shell 80 | #克隆项目 81 | git clone git@github.com:tmnhs/gin-blog.git 82 | #在gin-blog目录下 83 | cd gin-blog 84 | #docker编译镜像 85 | docker build -t myblog . 86 | #运行docker 87 | docker run -p 8080:8080 -t --name myblog myblog 88 | #注意此时的数据库配置应该改为服务器上的配置 89 | ``` 90 | 91 | 方式二:使用脚本部署 92 | 93 | ```shell 94 | #克隆项目 95 | git clone git@github.com:tmnhs/gin-blog.git 96 | #在gin-blog目录下 97 | cd gin-blog 98 | #必须在linux系统下 99 | #启动项目 100 | ./serve.sh start 101 | #暂停项目 102 | ./serve.sh stop 103 | #重启服务 104 | ./serve.sh restart 105 | #注意,脚本运行的端口号必须大于1024,不然可能会没有权限 106 | ``` 107 | 108 | -------------------------------------------------------------------------------- /api/v1/article.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "demo1/MyBlog/model" 5 | "demo1/MyBlog/proto" 6 | "demo1/MyBlog/utils/errmsg" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | /** 13 | 文章处理模块的接口 14 | */ 15 | 16 | //todo 添加文章 17 | func AddArticle(c *gin.Context) { 18 | var data model.Article 19 | var userArticle model.UserArticle 20 | //绑定 21 | _ = c.ShouldBind(&data) 22 | code := model.CreateArticle(&data) 23 | 24 | userArticle.ArticleId = data.ID 25 | 26 | userName, _ := c.Get("username") 27 | userArticle.UserId = model.FindUserIdByName(userName) 28 | _ = model.CreateUserArticle(&userArticle) 29 | 30 | c.JSON(http.StatusOK, gin.H{ 31 | "status": code, 32 | "data": data, 33 | "message": errmsg.GetErrMsg(code), 34 | }) 35 | } 36 | 37 | // todo 添加评论 38 | func AddComment(c *gin.Context) { 39 | var data model.Comment 40 | _ = c.ShouldBindJSON(&data) 41 | id := data.ArticleID 42 | 43 | code := model.CreateComment(id, data) 44 | 45 | c.JSON(http.StatusOK, gin.H{ 46 | "status": code, 47 | "data": data, 48 | "message": errmsg.GetErrMsg(code), 49 | }) 50 | } 51 | 52 | //todo 删除评论 53 | func DeleteComment(c *gin.Context) { 54 | id, _ := strconv.Atoi(c.Param("id")) 55 | 56 | code := model.DeleteComment(id) 57 | 58 | c.JSON(http.StatusOK, gin.H{ 59 | "status": code, 60 | "message": errmsg.GetErrMsg(code), 61 | }) 62 | } 63 | 64 | //todo 查询单个文章 65 | func GetArticleInfo(c *gin.Context) { 66 | id, _ := strconv.Atoi(c.Param("id")) 67 | 68 | data, code := model.GetArticleInfo(id) 69 | 70 | c.JSON(http.StatusOK, gin.H{ 71 | "status": code, 72 | "data": data, 73 | "message": errmsg.GetErrMsg(code), 74 | }) 75 | } 76 | 77 | //todo 查询文章下的所有评论 78 | func GetComments(c *gin.Context) { 79 | id, _ := strconv.Atoi(c.Param("id")) 80 | 81 | data, code := model.GetCommentsByArticleId(id) 82 | 83 | c.JSON(http.StatusOK, gin.H{ 84 | "status": code, 85 | "data": data, 86 | "message": errmsg.GetErrMsg(code), 87 | }) 88 | } 89 | 90 | //todo 查询某分类下的所有文章 91 | func GetCateArticle(c *gin.Context) { 92 | //获取参数 93 | //var req proto.ReqCommon 94 | id, _ := strconv.Atoi(c.Param("id")) 95 | pageNum, _ := strconv.Atoi(c.Query("pagenum")) 96 | pageSize, _ := strconv.Atoi(c.Query("pagesize")) 97 | 98 | //_=c.ShouldBindJSON(&req) 99 | if pageNum == 0 { 100 | pageNum = -1 101 | } 102 | if pageSize == 0 { 103 | pageSize = -1 104 | } 105 | //fmt.Println(req) 106 | data, code, total := model.GetCateArticle(id, pageSize, pageNum) 107 | articles := make([]*proto.RspFindArticle, 0) 108 | for i := 0; i < len(data); i++ { 109 | article := &proto.RspFindArticle{ 110 | ID: data[i].ID, 111 | Title: data[i].Title, 112 | CreatedAt: data[i].CreatedAt, 113 | Desc: data[i].Desc, 114 | Content: data[i].Content, 115 | Img: data[i].Img, 116 | } 117 | Cate, err := model.FindCategoryById(data[i].Cid) 118 | if err != nil { 119 | article.Name = "" 120 | } else { 121 | article.Name = Cate.Name 122 | } 123 | articles = append(articles, article) 124 | } 125 | c.JSON(http.StatusOK, gin.H{ 126 | "status": code, 127 | "data": articles, 128 | "total": total, 129 | "message": errmsg.GetErrMsg(code), 130 | }) 131 | } 132 | 133 | //todo 查询文章列表 134 | func GetArticle(c *gin.Context) { 135 | //获取参数 136 | var req proto.ReqFindArticle 137 | _ = c.ShouldBindJSON(&req) 138 | //如果pageSize等于0,则取消分页 139 | if req.PageSize == 0 { 140 | req.PageSize = -1 141 | } 142 | if req.PageNum == 0 { 143 | req.PageNum = -1 144 | } 145 | //调用函数查看所有用户 146 | data, code, total := model.GetArticles(req.Title, req.PageSize, req.PageNum) 147 | 148 | articles := make([]*proto.RspFindArticle, 0) 149 | for i := 0; i < len(data); i++ { 150 | article := &proto.RspFindArticle{ 151 | ID: data[i].ID, 152 | Title: data[i].Title, 153 | CreatedAt: data[i].CreatedAt, 154 | Desc: data[i].Desc, 155 | Content: data[i].Content, 156 | Img: data[i].Img, 157 | } 158 | Cate, err := model.FindCategoryById(data[i].Cid) 159 | if err != nil { 160 | article.Name = "" 161 | } else { 162 | article.Name = Cate.Name 163 | } 164 | articles = append(articles, article) 165 | } 166 | c.JSON(http.StatusOK, gin.H{ 167 | "status": code, 168 | "data": articles, 169 | "total": total, 170 | "message": errmsg.GetErrMsg(code), 171 | }) 172 | } 173 | 174 | //todo 编辑文章 175 | func EditArticle(c *gin.Context) { 176 | var data model.Article 177 | //获取参数 178 | id, _ := strconv.Atoi(c.Param("id")) 179 | _ = c.ShouldBindJSON(&data) 180 | 181 | code := model.EditArticle(id, &data) 182 | 183 | c.JSON(http.StatusOK, gin.H{ 184 | "status": code, 185 | "message": errmsg.GetErrMsg(code), 186 | }) 187 | } 188 | 189 | //todo 删除文章 190 | func DeleteArticle(c *gin.Context) { 191 | //获取参数 192 | id, _ := strconv.Atoi(c.Param("id")) 193 | 194 | //调用删除的函数 195 | code := model.DeleteArticle(id) 196 | 197 | c.JSON(http.StatusOK, gin.H{ 198 | "status": code, 199 | "message": errmsg.GetErrMsg(code), 200 | }) 201 | } 202 | -------------------------------------------------------------------------------- /api/v1/category.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | /** 4 | 分类处理模块的接口 5 | */ 6 | import ( 7 | "demo1/MyBlog/model" 8 | "demo1/MyBlog/proto" 9 | "demo1/MyBlog/utils/errmsg" 10 | "github.com/gin-gonic/gin" 11 | "net/http" 12 | "strconv" 13 | ) 14 | 15 | //添加分类 16 | func AddCategory(c *gin.Context) { 17 | var data model.Category 18 | //绑定 19 | _ = c.ShouldBind(&data) 20 | //判断分类是否存在 21 | code := model.CheckCategory(data.Name) 22 | if code == errmsg.SUCCESS { 23 | code = model.CreateCategory(&data) 24 | } 25 | c.JSON(http.StatusOK, gin.H{ 26 | "status": code, 27 | "data": data, 28 | "message": errmsg.GetErrMsg(code), 29 | }) 30 | } 31 | 32 | //查询单个分类 33 | 34 | //查询分类列表 35 | func GetCategory(c *gin.Context) { 36 | //获取参数 37 | var req proto.ReqCommon 38 | _ = c.ShouldBind(&req) 39 | //如果pageSize等于0,则取消分页 40 | if req.PageSize == 0 { 41 | req.PageSize = -1 42 | } 43 | if req.PageNum == 0 { 44 | req.PageNum = -1 45 | } 46 | //调用函数查看所有用户 47 | data, total, err := model.GetCategory(req.PageSize, req.PageNum) 48 | var code int 49 | if err != nil { 50 | code = errmsg.ERROR 51 | } else { 52 | code = errmsg.SUCCESS 53 | } 54 | c.JSON(http.StatusOK, gin.H{ 55 | "status": code, 56 | "data": data, 57 | "total": total, 58 | "message": errmsg.GetErrMsg(code), 59 | }) 60 | } 61 | 62 | //todo 编辑分类名 63 | func EditCategory(c *gin.Context) { 64 | var data model.Category 65 | //获取参数 66 | id, _ := strconv.Atoi(c.Param("id")) 67 | _ = c.ShouldBindJSON(&data) 68 | code := model.CheckCategory(data.Name) 69 | if code == errmsg.SUCCESS { 70 | code = model.EditCategory(id, &data) 71 | } 72 | c.JSON(http.StatusOK, gin.H{ 73 | "status": code, 74 | "message": errmsg.GetErrMsg(code), 75 | }) 76 | } 77 | 78 | //todo 删除分类 79 | func DeleteCategory(c *gin.Context) { 80 | //获取参数 81 | id, _ := strconv.Atoi(c.Param("id")) 82 | //调用删除的函数 83 | code := model.DeleteCategory(id) 84 | 85 | c.JSON(http.StatusOK, gin.H{ 86 | "status": code, 87 | "message": errmsg.GetErrMsg(code), 88 | }) 89 | } 90 | 91 | //通过id查找分类 92 | func FindCategoryById(c *gin.Context) { 93 | var code int 94 | //获取参数 95 | id, _ := strconv.Atoi(c.Param("id")) 96 | //调用删除的函数 97 | cate, err := model.FindCategoryById(id) 98 | if err != nil { 99 | code = errmsg.ERROR 100 | } else { 101 | code = errmsg.SUCCESS 102 | } 103 | c.JSON(http.StatusOK, gin.H{ 104 | "data": cate, 105 | "status": code, 106 | "message": errmsg.GetErrMsg(code), 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /api/v1/login.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "demo1/MyBlog/middleware" 5 | "demo1/MyBlog/model" 6 | "demo1/MyBlog/utils" 7 | "demo1/MyBlog/utils/errmsg" 8 | "demo1/MyBlog/utils/validator" 9 | "encoding/json" 10 | "fmt" 11 | "github.com/gin-gonic/gin" 12 | "net/http" 13 | ) 14 | 15 | //todo 注册 16 | func Register(c *gin.Context) { 17 | var user model.User 18 | _ = c.ShouldBindJSON(&user) 19 | //对传过来的数据进行校验 20 | msg, code := validator.Validate(&user) 21 | //校验失败,返回错误信息(注册失败) 22 | if code != errmsg.SUCCESS { 23 | c.JSON(http.StatusOK, gin.H{ 24 | "code": code, 25 | "message": msg, 26 | }) 27 | return 28 | } 29 | //判断用户名是否存在 30 | code = model.CheckUser(user.Username) 31 | if code == errmsg.SUCCESS { 32 | //用户名不存在,可以注册 33 | //设置激活码,唯一字符串 34 | user.Code = utils.CreateUUID() 35 | //设置激活状态 36 | user.Status = "N" 37 | //激活邮件发送,邮件正文 38 | 39 | content := "点我激活" 40 | 41 | model.SendEmail(user.Email, "激活邮件", content) 42 | 43 | //将注册的用户保存到数据库 44 | _, code = model.CreateUser(&user) 45 | } 46 | c.JSON(http.StatusOK, gin.H{ 47 | "code": code, 48 | "message": errmsg.GetErrMsg(code), 49 | }) 50 | 51 | } 52 | 53 | //todo 邮件激活 54 | func ActiveEmail(c *gin.Context) { 55 | var code int 56 | //获取参数(激活码) 57 | vCode := c.Query("code") 58 | if len(vCode) == 0 { 59 | code = errmsg.ERROR_EMAIL_CODE_NOT_EXIST 60 | c.JSON(http.StatusOK, gin.H{ 61 | "code": code, 62 | "message": errmsg.GetErrMsg(code), 63 | }) 64 | return 65 | } 66 | code = model.UpdateUserStatus(vCode) 67 | 68 | c.JSON(http.StatusOK, gin.H{ 69 | "code": code, 70 | "message": errmsg.GetErrMsg(code), 71 | }) 72 | } 73 | 74 | //todo 登录 75 | func Login(c *gin.Context) { 76 | var data model.User 77 | _ = c.ShouldBindJSON(&data) 78 | //fmt.Println(data) 79 | var token string 80 | var code int 81 | user, code := model.CheckLogin(data.Username, data.Password) 82 | 83 | if code == errmsg.SUCCESS { 84 | token, code = middleware.SetToken(data.Username) 85 | model.ClearSession(c) 86 | model.SetSession(c, token, int(utils.Expiration)) 87 | profile, codeGetProfile := model.GetProfileById(int(user.ID)) 88 | profileJson, _ := json.Marshal(profile) 89 | if codeGetProfile == errmsg.SUCCESS { 90 | _, err := model.Redis.Set(token, string(profileJson), utils.Expiration).Result() 91 | //result, err:= model.Redis.Do("SET",token, profileJson).Result() 92 | if err != nil { 93 | fmt.Println("redis set fail", err) 94 | } 95 | //fmt.Println("set Profile :",result) 96 | } 97 | } 98 | 99 | c.JSON(http.StatusOK, gin.H{ 100 | "code": code, 101 | "message": errmsg.GetErrMsg(code), 102 | "token": token, 103 | }) 104 | } 105 | 106 | //todo 发送邮件 107 | func SendEmailForCode(c *gin.Context) { 108 | to := c.Query("email") 109 | code := utils.CreateVcode() 110 | content := "

欢迎使用博客,您的验证码是

" + code + "

" 111 | //将验证码存储到redis中 112 | model.Redis.Do("set", to, code) 113 | status := model.SendEmail(to, "验证码", content) 114 | c.JSON(http.StatusOK, gin.H{ 115 | "status": status, 116 | "message": errmsg.GetErrMsg(status), 117 | }) 118 | } 119 | 120 | //todo 使用邮件登录 121 | func LoginByEmail(c *gin.Context) { 122 | var token string 123 | var code int 124 | var user model.User 125 | to := c.Query("email") 126 | userCode := c.Query("code") 127 | //从redis中拿到正确的验证码做比较 128 | serverCode := model.Redis.Get(to).Val() 129 | 130 | //比较验证码 131 | if userCode != serverCode { 132 | code = errmsg.ERROR_CODE_WRONG 133 | c.JSON(http.StatusOK, gin.H{ 134 | "code": code, 135 | "message": errmsg.GetErrMsg(code), 136 | }) 137 | return 138 | } 139 | //通过email查找用户 140 | user, code = model.GetUserByEmail(to) 141 | if code == errmsg.SUCCESS { 142 | //jwt验证 143 | token, code = middleware.SetToken(user.Username) 144 | } 145 | //登录成功删除redis中的email,防止重复使用 146 | model.Redis.Del(to) 147 | c.JSON(http.StatusOK, gin.H{ 148 | "code": code, 149 | "message": errmsg.GetErrMsg(code), 150 | "token": token, 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /api/v1/profile.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "demo1/MyBlog/model" 5 | "demo1/MyBlog/utils/errmsg" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func GetProfile(c *gin.Context) { 11 | profile, code := model.GetProfile(c) 12 | //fmt.Println("profile",profile) 13 | c.JSON(http.StatusOK, gin.H{ 14 | "status": code, 15 | "data": profile, 16 | "message": errmsg.GetErrMsg(code), 17 | }) 18 | } 19 | 20 | func UpdateProfile(c *gin.Context) { 21 | var req model.Profile 22 | _ = c.ShouldBindJSON(&req) 23 | 24 | profile, code := model.GetProfile(c) 25 | req.ID = profile.ID 26 | //fmt.Println("update profile",req) 27 | code = model.UpdateProfile(c, req.ID, &req) 28 | c.JSON(http.StatusOK, gin.H{ 29 | "status": code, 30 | "message": errmsg.GetErrMsg(code), 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /api/v1/upload.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "demo1/MyBlog/model" 5 | "demo1/MyBlog/utils/errmsg" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | //todo 上传文件 11 | func Upload(c *gin.Context) { 12 | file ,fileHeadr,_:=c.Request.FormFile("file") 13 | 14 | fileSize:=fileHeadr.Size 15 | 16 | url,code:= model.UploadFile(file,fileSize) 17 | c.JSON(http.StatusOK,gin.H{ 18 | "status":code, 19 | "message":errmsg.GetErrMsg(code), 20 | "url":url, 21 | }) 22 | } -------------------------------------------------------------------------------- /api/v1/user.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | /** 4 | 用户处理模块的接口 5 | */ 6 | 7 | import ( 8 | "demo1/MyBlog/model" 9 | "demo1/MyBlog/proto" 10 | "demo1/MyBlog/utils/errmsg" 11 | "github.com/gin-gonic/gin" 12 | "net/http" 13 | "strconv" 14 | ) 15 | 16 | //todo 查询用户详细信息(包括文章) 17 | func GetUserInfo(c *gin.Context) { 18 | 19 | id, _ := strconv.Atoi(c.Param("id")) 20 | 21 | data, code := model.GetUserInfo(id) 22 | 23 | c.JSON(http.StatusOK, gin.H{ 24 | "status": code, 25 | "data": data, 26 | "message": errmsg.GetErrMsg(code), 27 | }) 28 | } 29 | 30 | //todo 添加用户 31 | func AddUser(c *gin.Context) { 32 | var data proto.ReqAddUser 33 | var code int 34 | //绑定 35 | _ = c.ShouldBindJSON(&data) 36 | //msg,code= validator.Validate(&data) 37 | //if code!=errmsg.SUCCESS{ 38 | // c.JSON(http.StatusOK,gin.H{ 39 | // "status":code, 40 | // "message":msg, 41 | // }) 42 | // return 43 | //} 44 | //判断用户名是否存在 45 | code = model.CheckUser(data.UserName) 46 | if code == errmsg.SUCCESS { 47 | user := &model.User{ 48 | Username: data.UserName, 49 | Password: data.Password, 50 | Email: data.Email, 51 | Role: data.Role, 52 | Status: "Y", 53 | } 54 | result, _ := model.CreateUser(user) 55 | profile := &model.Profile{ 56 | ID: int(result.ID), 57 | Name: result.Username, 58 | Email: result.Email, 59 | } 60 | //fmt.Println("create user:",result) 61 | code = model.CreateProfile(profile) 62 | } 63 | c.JSON(http.StatusOK, gin.H{ 64 | "status": code, 65 | "message": errmsg.GetErrMsg(code), 66 | }) 67 | } 68 | 69 | //查询单个用户 70 | 71 | // todo 查询用户列表 72 | func GetUsers(c *gin.Context) { 73 | //获取参数 74 | var req proto.ReqFindUser 75 | _ = c.ShouldBindJSON(&req) 76 | //fmt.Println(req) 77 | //如果pageSize等于0,则取消分页 78 | if req.PageSize == 0 { 79 | req.PageSize = -1 80 | } 81 | if req.PageNum == 0 { 82 | req.PageNum = -1 83 | } 84 | //调用函数查看所有用户 85 | data, total, err1 := model.GetUsers(req.IdOrName, req.PageSize, req.PageNum) 86 | code := errmsg.SUCCESS 87 | if err1 != nil { 88 | code = errmsg.ERROR 89 | } 90 | c.JSON(http.StatusOK, gin.H{ 91 | "status": code, 92 | "data": data, 93 | "total": total, 94 | "message": errmsg.GetErrMsg(code), 95 | }) 96 | } 97 | 98 | //todo 编辑用户 99 | func EditUser(c *gin.Context) { 100 | var req proto.ReqEditUser 101 | var code int 102 | //获取参数 103 | _ = c.ShouldBindJSON(&req) 104 | user, _ := model.GetUserInfo(req.Id) 105 | if user.Username != req.UserName { 106 | code = model.CheckUser(req.UserName) 107 | if code != errmsg.SUCCESS { 108 | c.JSON(code, gin.H{ 109 | "status": code, 110 | "message": errmsg.GetErrMsg(code), 111 | }) 112 | return 113 | } 114 | } 115 | code = model.EditUser(req.Id, &req) 116 | 117 | profileOld, _ := model.GetProfileById(req.Id) 118 | profile := &model.Profile{ 119 | ID: req.Id, 120 | Name: req.UserName, 121 | Email: req.Email, 122 | Desc: profileOld.Desc, 123 | QqChat: profileOld.QqChat, 124 | WeChat: profileOld.WeChat, 125 | Weibo: profileOld.Weibo, 126 | Img: profileOld.Img, 127 | Avatar: profileOld.Avatar, 128 | } 129 | code = model.UpdateProfile(c, profile.ID, profile) 130 | c.JSON(http.StatusOK, gin.H{ 131 | "status": code, 132 | "message": errmsg.GetErrMsg(code), 133 | }) 134 | } 135 | 136 | //todo 删除用户 137 | func DeleteUser(c *gin.Context) { 138 | //获取参数 139 | id, _ := strconv.Atoi(c.Param("id")) 140 | //调用删除的函数 141 | code := model.DeleteUser(id) 142 | 143 | c.JSON(http.StatusOK, gin.H{ 144 | "status": code, 145 | "message": errmsg.GetErrMsg(code), 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [server] 2 | ;debug 开发模式 release 生产模式 3 | AppMode = debug 4 | ;服务器端口 5 | HttpPort = :8080 6 | 7 | JwtKey=45df45rds4 8 | 9 | 10 | ;数据库相关信息 11 | [database] 12 | ;数据库名称 13 | Db = mysql 14 | ;数据库地址 15 | DbHost = 114.55.178.217 16 | ;数据库端口号 17 | DbPort = :3307 18 | ;数据库用户 19 | DbUser = root 20 | ;数据库密码 21 | DbPassWord = root 22 | ;使用的数据库名称 23 | DbName = blog 24 | 25 | ;使用七牛对象存储 26 | [qiniuyun] 27 | AccessKey = 3v1jdAbn2NJ6tOS36ZKvYLDdobjFQjkecYkdKNBr 28 | SecretKey = ZjM5P7RFoLFO6-A-Hf0F6GYlmw-MORAEljeNPw70 29 | Bucket = tmnhginblog 30 | QiniuServer = http://tmnhs.top/ 31 | 32 | ; redis地址 33 | [redis] 34 | ;redis地址及端口号 35 | RedisAddr = 127.0.0.1:6379 36 | RedisPassword = 37 | RedisDB = 0 38 | 39 | ;邮箱信息 40 | [email] 41 | ;邮箱服务地址 42 | ServerHost=smtp.qq.com 43 | ;邮箱服务端口 44 | ServerPort=465 45 | ;发件人邮箱地址 46 | FromEmail=yourEmail 47 | ;发件人邮箱密码(秘钥) 48 | FromPassword=yourPassword 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module demo1/MyBlog 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/garyburd/redigo v1.6.2 8 | github.com/gin-contrib/cors v1.3.1 9 | github.com/gin-gonic/gin v1.7.4 10 | github.com/go-playground/locales v0.13.0 11 | github.com/go-playground/universal-translator v0.17.0 12 | github.com/go-playground/validator/v10 v10.4.1 13 | github.com/go-redis/redis v6.15.9+incompatible 14 | github.com/go-sql-driver/mysql v1.5.0 15 | github.com/jinzhu/gorm v1.9.16 16 | github.com/jonboulle/clockwork v0.2.2 // indirect 17 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible 18 | github.com/lestrrat-go/strftime v1.0.5 // indirect 19 | github.com/onsi/ginkgo v1.16.5 // indirect 20 | github.com/onsi/gomega v1.16.0 // indirect 21 | github.com/pkg/errors v0.9.1 // indirect 22 | github.com/qiniu/api.v7/v7 v7.4.0 23 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 24 | github.com/satori/go.uuid v1.2.0 25 | github.com/sirupsen/logrus v1.8.1 26 | github.com/stretchr/testify v1.7.0 // indirect 27 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 28 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 29 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 30 | gopkg.in/ini.v1 v1.63.2 31 | ) 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "demo1/MyBlog/model" 5 | "demo1/MyBlog/routes" 6 | ) 7 | 8 | func main() { 9 | //连接数据库 10 | model.InitDb() 11 | model.InitRedis() 12 | routes.InitRouter() 13 | //model.SendEmail() 14 | //fe202a84-64f6-4908-bf7f-cb06e056ac8a 15 | //CreateUUID() 16 | //utils.CreateVcode() 17 | } 18 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | //func Cors() gin.HandlerFunc { 9 | // //return cors.New( 10 | // // cors.Config{ 11 | // // AllowAllOrigins: true, 12 | // // AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONs"}, 13 | // // AllowHeaders: []string{"*"}, 14 | // // ExposeHeaders: []string{"Content-Length", "Authorization", "Content-Type"}, 15 | // // AllowCredentials: true, 16 | // // MaxAge: 12 * time.Hour, 17 | // // }) 18 | // return cors.Default() 19 | // 20 | //} 21 | //func Cors() gin.HandlerFunc { 22 | // return func(c *gin.Context) { 23 | // c.Header("Access-Control-Allow-Origin", "true") 24 | // c.Header("Access-Control-Allow-Headers", "*") 25 | // c.Header("Access-Control-Allow-Methods", "POST, GET, PUT, PATCH, OPTIONS") 26 | // c.Header("Access-Control-Allow-Credentials", "true") 27 | // c.Header("Access-Control-Expose-Headers", "*") 28 | // if c.Request.Method == "OPTIONS" { 29 | // c.JSON(http.StatusOK, "") 30 | // c.Abort() 31 | // return 32 | // } 33 | // c.Next() 34 | // } 35 | // 36 | //} 37 | 38 | // Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法 39 | func Cors() gin.HandlerFunc { 40 | return func(c *gin.Context) { 41 | method := c.Request.Method 42 | origin := c.Request.Header.Get("Origin") 43 | c.Header("Access-Control-Allow-Origin", origin) 44 | c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Content-Length, Authorization, Token,X-Token,X-User-Id,x-requested-with") 45 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT") 46 | c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type") 47 | c.Header("Access-Control-Allow-Credentials", "true") 48 | //Access to XMLHttpRequest at 'http://localhost/api/v1/upload' from origin 'http://localhost:8080' has been blocked by CORS policy: Request header field x-requested-with is not allowed by Access-Control-Allow-Headers in preflight response. 49 | //AddArt.vue?e24c:105 50 | // 放行所有OPTIONS方法 51 | if method == "OPTIONS" { 52 | c.AbortWithStatus(http.StatusNoContent) 53 | } 54 | 55 | // 处理请求 56 | c.Next() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "demo1/MyBlog/utils" 5 | "demo1/MyBlog/utils/errmsg" 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var JwyKey = []byte(utils.JwtKey) 14 | 15 | type MyClaims struct { 16 | Username string `json:"username"` 17 | jwt.StandardClaims 18 | } 19 | 20 | //生成token 21 | func SetToken(username string) (string, int) { 22 | //设置到期时间 23 | expireTime := time.Now().Add(24 * time.Hour) //24小时 24 | //创建Jwt声明 25 | SetClaims := MyClaims{ 26 | username, 27 | jwt.StandardClaims{ 28 | ExpiresAt: expireTime.Unix(), 29 | Issuer: "ginblog", 30 | }, 31 | } 32 | //使用用于签名的算法和令牌 33 | reqClain := jwt.NewWithClaims(jwt.SigningMethodHS256, SetClaims) 34 | //创建jwt字符串 35 | token, err := reqClain.SignedString(JwyKey) 36 | //如果出错,则返回服务器内部错误 37 | if err != nil { 38 | return "", errmsg.ERROR 39 | } 40 | return token, errmsg.SUCCESS 41 | } 42 | 43 | //验证token 44 | func CheckToken(token string) (*MyClaims, int) { 45 | 46 | //解析jwt字符串并将结果存储 47 | setToken, _ := jwt.ParseWithClaims(token, &MyClaims{}, func(token *jwt.Token) (interface{}, error) { 48 | return JwyKey, nil 49 | }) 50 | if key, _ := setToken.Claims.(*MyClaims); setToken.Valid { 51 | return key, errmsg.SUCCESS 52 | } 53 | return nil, errmsg.ERROR 54 | } 55 | 56 | //jwt中间件 57 | var code int 58 | 59 | func JwtToken() gin.HandlerFunc { 60 | return func(c *gin.Context) { 61 | 62 | tokenHeader := c.Request.Header.Get("Authorization") 63 | //如果token为空,返回错误 64 | if tokenHeader == "" { 65 | code = errmsg.ERROR_TOKEN_NOT_EXIST //1004 66 | c.JSON(http.StatusOK, gin.H{ 67 | "status": code, 68 | "message": errmsg.GetErrMsg(code), 69 | }) 70 | c.Abort() 71 | return 72 | } 73 | checkToken := strings.SplitN(tokenHeader, " ", 2) 74 | //token类型有错误 75 | if len(checkToken) != 2 && checkToken[0] != "Bearer" { 76 | code = errmsg.ERROR_TOKEN_TYPE_WRONG 77 | c.JSON(http.StatusOK, gin.H{ 78 | "status": code, 79 | "message": errmsg.GetErrMsg(code), 80 | }) 81 | c.Abort() 82 | return 83 | } 84 | //验证token 85 | key, tCode := CheckToken(checkToken[1]) 86 | if tCode == errmsg.ERROR { 87 | code = errmsg.ERROR_TOKEN_WRONG 88 | c.JSON(http.StatusOK, gin.H{ 89 | "status": code, 90 | "message": errmsg.GetErrMsg(code), 91 | }) 92 | c.Abort() 93 | return 94 | } 95 | //判断时间是否过期 96 | if time.Now().Unix() > key.ExpiresAt { 97 | code = errmsg.ERROR_TOKEN_RUNTIEM 98 | c.JSON(http.StatusOK, gin.H{ 99 | "status": code, 100 | "message": errmsg.GetErrMsg(code), 101 | }) 102 | c.Abort() 103 | return 104 | } 105 | //成功情况下 106 | c.Set("username", key.Username) 107 | //c.Set("userid",key.Id) 108 | c.Next() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | retalog "github.com/lestrrat-go/file-rotatelogs" 7 | "github.com/rifflock/lfshook" 8 | "github.com/sirupsen/logrus" 9 | "math" 10 | "os" 11 | "time" 12 | ) 13 | //日志,此操作可以复用 14 | func Logger() gin.HandlerFunc { 15 | //设置日志文件的路径 16 | filePath:="log/log" 17 | //建立软连接,需要管理员权限 18 | linkName:="latest_log.log" 19 | src,err:=os.OpenFile(filePath,os.O_RDWR|os.O_CREATE,0755) 20 | if err != nil { 21 | fmt.Println("err: ",err) 22 | } 23 | //创建日志 24 | logger := logrus.New() 25 | //输出 26 | logger.Out=src 27 | //设置日志级别 28 | logger.SetLevel(logrus.DebugLevel) 29 | //添加时间分割 30 | logWriter,_:=retalog.New( 31 | filePath+"%Y%m%d.log", 32 | retalog.WithMaxAge(7*24*time.Hour), //日志保留时间:一周 33 | retalog.WithRotationTime(24*time.Hour), //24小时分割一次 34 | retalog.WithLinkName(linkName), //建立软连接 35 | ) 36 | writeMap:=lfshook.WriterMap{ 37 | logrus.InfoLevel: logWriter, 38 | logrus.FatalLevel: logWriter, 39 | logrus.DebugLevel: logWriter, 40 | logrus.WarnLevel: logWriter, 41 | logrus.ErrorLevel: logWriter, 42 | logrus.PanicLevel: logWriter, 43 | } 44 | //实例化 45 | Hook:=lfshook.NewHook(writeMap,&logrus.TextFormatter{ 46 | TimestampFormat: "2006-01-02 15:04:05", 47 | }) 48 | logger.AddHook(Hook) 49 | return func(c *gin.Context) { 50 | startTime := time.Now() 51 | c.Next() 52 | stopTime := time.Since(startTime) 53 | spendTime := fmt.Sprintf("%d ms", int(math.Ceil(float64(stopTime.Nanoseconds()/1000000.0)))) 54 | hostName, err := os.Hostname() 55 | if err != nil { 56 | hostName = "unkown" 57 | } 58 | statusCode := c.Writer.Status() 59 | clientIp := c.ClientIP() 60 | userAgent := c.Request.UserAgent() 61 | dataSize := c.Writer.Size() 62 | if dataSize < 0 { 63 | dataSize = 0 64 | } 65 | method := c.Request.Method 66 | path := c.Request.RequestURI 67 | 68 | entry := logger.WithFields(logrus.Fields{ 69 | "HostName": hostName, 70 | "status": statusCode, 71 | "SpendTime": spendTime, 72 | "Ip": clientIp, 73 | "Method": method, 74 | "Path": path, 75 | "DataSize": dataSize, 76 | "Agent": userAgent, 77 | }) 78 | if len(c.Errors) > 0 { 79 | entry.Error(c.Errors.ByType(gin.ErrorTypePrivate).String()) 80 | } 81 | if statusCode>=500{ 82 | entry.Error() 83 | }else if statusCode>=400{ 84 | entry.Warn() 85 | }else{ 86 | entry.Info() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /model/Article.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | //文章模型 4 | 5 | import ( 6 | "demo1/MyBlog/utils/errmsg" 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | type Article struct { 11 | gorm.Model 12 | Title string `gorm:"type:varchar(100);not null" json:"title"` 13 | Cid int `gorm:"type:int;not null" json:"cid"` 14 | Desc string `gorm:"type:varchar(200)" json:"desc"` 15 | Content string `gorm:"type:longtext;not null" json:"content"` 16 | Img string `gorm:"type:varchar(200)" json:"img"` 17 | Category Category `gorm:"foreignkey:Cid"` 18 | Comments []Comment 19 | } 20 | 21 | //todo 添加文章,此时需要建立中间表 22 | func CreateArticle(data *Article) int { 23 | 24 | //密码加密 25 | //data.Password=ScryptPw(data.Password) 26 | //添加中间表 27 | 28 | err = db.Create(&data).Error 29 | //_=db.Create(&userArticle).Error 30 | if err != nil { 31 | return errmsg.ERROR //500 32 | } 33 | return errmsg.SUCCESS 34 | } 35 | 36 | //todo 添加评论 37 | func CreateComment(id int, data Comment) int { 38 | var article Article 39 | err = db.Model(&Article{}).Where("id = ?", id).First(&article).Error 40 | if err != nil { 41 | return errmsg.ERROR_ARTICLE_NOT_EXIST 42 | } 43 | article.Comments = append(article.Comments, data) 44 | err = db.Save(&article).Error 45 | if err != nil { 46 | return errmsg.ERROR 47 | } 48 | return errmsg.SUCCESS 49 | } 50 | 51 | //todo 查询文章下的所有评论 52 | func GetCommentsByArticleId(id int) ([]Comment, int) { 53 | var comments []Comment 54 | var article Article 55 | var _ = db.Model(&Article{}).Where("id = ?", id).First(&article).Error 56 | err = db.Where("article_id = ?", id).Find(&comments).Error 57 | if article.ID == 0 { 58 | return nil, errmsg.ERROR_ARTICLE_NOT_EXIST 59 | } 60 | if len(comments) == 0 { 61 | return nil, errmsg.ERROR_ARTICLE_NO_COMMENTS 62 | } 63 | return comments, errmsg.SUCCESS 64 | } 65 | 66 | //todo 查询文章列表,带分页效果 67 | func GetArticles(title string, pageSize int, pageNum int) ([]Article, int, int) { 68 | var articles []Article 69 | var total int 70 | DB := db.Model(&Article{}) 71 | if title != "" { 72 | DB = DB.Where("title like ?", "%"+title+"%") 73 | } 74 | err = DB.Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&articles).Error 75 | if err != nil && err != gorm.ErrRecordNotFound { 76 | return nil, errmsg.ERROR, 0 77 | } 78 | //查找总数 79 | err = DB.Count(&total).Error 80 | if err != nil { 81 | return nil, errmsg.ERROR, 0 82 | } 83 | return articles, errmsg.SUCCESS, total 84 | } 85 | 86 | // todo 查询分类下的所有文章 87 | func GetCateArticle(id int, pageSize int, pageNum int) ([]Article, int, int) { 88 | var cateArticleList []Article 89 | var total int 90 | DB := db.Model(&Article{}) 91 | if id > 0 { 92 | DB = DB.Where("cid = ?", id) 93 | } 94 | err := DB.Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&cateArticleList).Error 95 | //查找总数 96 | _ = DB.Count(&total).Error 97 | if err != nil { 98 | return cateArticleList, errmsg.ERROR_CATE_NOT_EXIST, 0 99 | } 100 | return cateArticleList, errmsg.SUCCESS, total 101 | } 102 | 103 | //todo 查询单个文章详细信息 104 | func GetArticleInfo(id int) (Article, int) { 105 | var article Article 106 | //根据文章id查找出其评论 107 | comments, _ := GetCommentsByArticleId(id) 108 | 109 | err := db.Where("id = ?", id).First(&article).Error 110 | for _, comment := range comments { 111 | article.Comments = append(article.Comments, comment) 112 | } 113 | if err != nil { 114 | return article, errmsg.ERROR_ARTICLE_NOT_EXIST 115 | } 116 | return article, errmsg.SUCCESS 117 | } 118 | 119 | //todo 编辑文章信息 120 | func EditArticle(id int, data *Article) int { 121 | //利用map集合更新 122 | var maps = make(map[string]interface{}) 123 | maps["title"] = data.Title 124 | maps["cid"] = data.Cid 125 | maps["desc"] = data.Desc 126 | maps["content"] = data.Content 127 | maps["img"] = data.Img 128 | err = db.Model(&Article{}).Where("id = ?", id).Update(maps).Error 129 | if err != nil { 130 | return errmsg.ERROR 131 | } 132 | return errmsg.SUCCESS 133 | } 134 | 135 | //todo 删除文章 136 | func DeleteArticle(id int) int { 137 | //删除文章时,需要把与之相关联的中间表也删除 138 | DeleteMidByArticleId(id) 139 | err = db.Where("id = ?", id).Delete(&Article{}).Error 140 | if err != nil { 141 | return errmsg.ERROR 142 | } 143 | return errmsg.SUCCESS 144 | } 145 | -------------------------------------------------------------------------------- /model/Category.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | //分类模型 4 | 5 | import ( 6 | "demo1/MyBlog/utils/errmsg" 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | type Category struct { 11 | ID uint `gorm:"primary_key;auto_increment" json:"id"` 12 | Name string `gorm:"type:varchar(20);not null" json:"name"` 13 | } 14 | 15 | //todo 查询分类是否存在 16 | func CheckCategory(name string) (code int) { 17 | var cate Category 18 | db.Model(&Category{}).Select("id").Where("name = ?", name).First(&cate) 19 | if cate.ID > 0 { 20 | return errmsg.ERROR_CATENAME_USED //1001 21 | } 22 | return errmsg.SUCCESS 23 | } 24 | 25 | //todo 添加分类 26 | func CreateCategory(data *Category) int { 27 | //密码加密 28 | //data.Password=ScryptPw(data.Password) 29 | err = db.Model(&Category{}).Create(&data).Error 30 | if err != nil { 31 | return errmsg.ERROR //500 32 | } 33 | return errmsg.SUCCESS 34 | } 35 | 36 | //todo 查询分类列表,带分页效果 37 | func GetCategory(pageSize int,pageNum int )([]Category,int,error) { 38 | var cates []Category 39 | var total int 40 | err=db.Model(&Category{}).Count(&total).Error 41 | if err != nil { 42 | return nil,0,err 43 | } 44 | err=db.Model(&Category{}).Limit(pageSize).Offset((pageNum-1)*pageSize).Find(&cates).Error 45 | if err != nil &&err!=gorm.ErrRecordNotFound{ 46 | return nil,0,err 47 | } 48 | return cates,total,nil 49 | } 50 | 51 | //todo 编辑分类信息 52 | func EditCategory(id int,data *Category) int { 53 | //利用map集合更新 54 | var maps=make(map[string]interface{}) 55 | maps["name"]=data.Name 56 | err = db.Model(&Category{}).Where("id = ?", id).Update(maps).Error 57 | if err != nil { 58 | return errmsg.ERROR 59 | } 60 | return errmsg.SUCCESS 61 | } 62 | //todo 删除分类 63 | func DeleteCategory( id int) int{ 64 | err= db.Where("id = ?",id).Delete(&Category{}).Error 65 | if err != nil { 66 | return errmsg.ERROR 67 | } 68 | return errmsg.SUCCESS 69 | } 70 | //通过id查找分类 71 | func FindCategoryById(id int)(*Category,error) { 72 | var cate Category 73 | err=db.Model(&Category{}).Where("id = ? ",id).Find(&cate).Error 74 | if err != nil { 75 | return nil,err 76 | } 77 | return &cate,nil 78 | } -------------------------------------------------------------------------------- /model/Comment.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | //评论模型 4 | 5 | import ( 6 | "demo1/MyBlog/utils/errmsg" 7 | "github.com/jinzhu/gorm" 8 | ) 9 | 10 | type Comment struct { 11 | gorm.Model 12 | Commentator string `gorm:"type:varchar(20);not null" json:"commentator"` 13 | Content string `gorm:"type:longtext;not null" json:"content"` 14 | ArticleID int `gorm:"type:int;not null" json:"article_id"` 15 | ParentID int `gorm:"type:int " json:"parent_id"` 16 | } 17 | 18 | //todo 根据id删除评论 19 | func DeleteComment(id int) int { 20 | err=db.Where("id = ?",id).Delete(&Comment{}).Error 21 | if err != nil { 22 | return errmsg.ERROR 23 | } 24 | return errmsg.SUCCESS 25 | } -------------------------------------------------------------------------------- /model/Email.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "demo1/MyBlog/utils" 5 | "demo1/MyBlog/utils/errmsg" 6 | "fmt" 7 | "gopkg.in/gomail.v2" 8 | "strconv" 9 | ) 10 | 11 | func SendEmail(to string,title string,text string) int{ 12 | m := gomail.NewMessage() 13 | 14 | m.SetHeader("From", utils.FromEmail) 15 | m.SetHeader("To", to) 16 | //m.SetAddressHeader("Cc", "dan@example.com", "Dan") 17 | m.SetHeader("Subject", title) 18 | m.SetBody("text/html", text) 19 | //m.Attach("/home/Alex/lolcat.jpg") 20 | serverPort,_:= strconv.Atoi(utils.ServerPort) 21 | d := gomail.NewDialer(utils.ServerHost, serverPort , "1685290935@qq.com", utils.FromPassword) 22 | 23 | if err := d.DialAndSend(m); err != nil { 24 | return errmsg.ERROR_EMAIL_SEND 25 | } 26 | fmt.Println("发送结束") 27 | return errmsg.SUCCESS 28 | } 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /model/Profile.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "demo1/MyBlog/utils" 5 | "demo1/MyBlog/utils/errmsg" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type Profile struct { 12 | ID int `gorm:"primaryKey" json:"id"` 13 | Name string `gorm:"type:varchar(20)" json:"name"` 14 | Desc string `gorm:"type:varchar(200)" json:"desc"` 15 | QqChat string `gorm:"type:varchar(32)" json:"qq_chat"` 16 | WeChat string `gorm:"ype:varchar(32)" json:"wechat"` 17 | Weibo string `gorm:"type:varchar(32)" json:"weibo"` 18 | Email string `gorm:"type:varchar(32)" json:"email"` 19 | Img string `gorm:"type:varchar(80)" json:"img"` 20 | Avatar string `gorm:"type:varchar(80)" json:"avatar"` 21 | } 22 | 23 | //获取个人信息 24 | func GetProfileById(id int) (*Profile, int) { 25 | var profile Profile 26 | err = db.Model(&profile).Where("ID = ?", id).First(&profile).Error 27 | //profile:=Redis.Get("profile") 28 | if err != nil { 29 | return nil, errmsg.ERROR 30 | } 31 | return &profile, errmsg.SUCCESS 32 | } 33 | 34 | //获取个人信息 35 | func GetProfile(c *gin.Context) (*Profile, int) { 36 | var profile *Profile 37 | sessionId, err := c.Cookie(SessionName) 38 | //fmt.Println("seesionId:",sessionId) 39 | if err != nil { 40 | return nil, errmsg.ERROR 41 | } 42 | result, err := Redis.Get(sessionId).Result() 43 | if err != nil { 44 | return nil, errmsg.ERROR 45 | } 46 | err = json.Unmarshal([]byte(result), &profile) 47 | if err != nil { 48 | fmt.Println("unmarshall json false") 49 | } 50 | return profile, errmsg.SUCCESS 51 | } 52 | 53 | //更新个人信息 54 | func UpdateProfile(c *gin.Context, id int, profile *Profile) int { 55 | var _profile *Profile 56 | err = db.Model(&Profile{}).Where("ID = ?", id).Update(profile).Error 57 | if err != nil { 58 | return errmsg.ERROR 59 | } 60 | sessionId, err := c.Cookie(SessionName) 61 | if err != nil { 62 | return errmsg.ERROR 63 | } 64 | result, err := Redis.Get(sessionId).Result() 65 | if err != nil { 66 | return errmsg.ERROR 67 | } 68 | err = json.Unmarshal([]byte(result), &_profile) 69 | if err != nil { 70 | fmt.Println("unmarshall json false") 71 | } 72 | if _profile.ID == profile.ID { 73 | //需要更新redis 74 | profileJson, _ := json.Marshal(profile) 75 | _, err = Redis.Set(sessionId, string(profileJson), utils.Expiration).Result() 76 | if err != nil { 77 | return errmsg.ERROR 78 | } 79 | } 80 | return errmsg.SUCCESS 81 | } 82 | 83 | func CreateProfile(profile *Profile) int { 84 | err = db.Model(&Profile{}).Create(&profile).Error 85 | if err != nil { 86 | return errmsg.ERROR 87 | } 88 | return errmsg.SUCCESS 89 | } 90 | -------------------------------------------------------------------------------- /model/Update.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "context" 5 | "demo1/MyBlog/utils" 6 | "demo1/MyBlog/utils/errmsg" 7 | "github.com/qiniu/api.v7/v7/auth/qbox" 8 | "github.com/qiniu/api.v7/v7/storage" 9 | "mime/multipart" 10 | ) 11 | //使用七牛对象存储 12 | var AccessKey = utils.AccessKey 13 | var SecretKey =utils.SecretKey 14 | var Bucket =utils.Bucket 15 | var ImgUrl = utils.QiniuServer 16 | 17 | //todo 上传文件 18 | func UploadFile(file multipart.File,fileSize int64)(string,int) { 19 | putPolicy:= storage.PutPolicy{ 20 | Scope: Bucket, 21 | } 22 | mac:=qbox.NewMac(AccessKey,SecretKey) 23 | upToken:=putPolicy.UploadToken(mac) 24 | 25 | cfg:=storage.Config{ 26 | Zone: &storage.ZoneHuadong, 27 | UseCdnDomains: false, 28 | UseHTTPS: false, 29 | } 30 | 31 | putExtra:=storage.PutExtra{} 32 | 33 | formUploader:=storage.NewFormUploader(&cfg) 34 | ret:=storage.PutRet{} 35 | 36 | err:= formUploader.PutWithoutKey(context.Background(),&ret,upToken,file,fileSize,&putExtra) 37 | if err != nil { 38 | return "",errmsg.ERROR 39 | } 40 | url:=ImgUrl+ret.Key 41 | return url,errmsg.SUCCESS 42 | } 43 | -------------------------------------------------------------------------------- /model/User.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "demo1/MyBlog/proto" 5 | "demo1/MyBlog/utils/errmsg" 6 | "encoding/base64" 7 | "fmt" 8 | "github.com/jinzhu/gorm" 9 | "golang.org/x/crypto/scrypt" 10 | "log" 11 | "strconv" 12 | ) 13 | 14 | type User struct { 15 | gorm.Model 16 | Username string `gorm:"type:varchar(20);not null" json:"username" validate:"required,min=4,max=12" label:"用户名"` 17 | Password string `gorm:"type:varchar(20);not null" json:"password" validate:"required,min=6,max=20" label:"密码"` 18 | Email string `gorm:"type:varchar(32);not null" json:"email" validate:"required,email" label:"邮箱"` 19 | Role int `gorm:"type:int ;DEFAULT:2" json:"role" validate:"required,gte=2" label:"角色"` 20 | Article []Article `gorm:"many2many:user_article"` 21 | Status string `gorm:"type:varchar(12)" default:"N"` //激活状态 22 | Code string `gorm:"type:varchar(80)"` //激活码 23 | } 24 | 25 | //todo 查询用户是否存在 26 | func CheckUser(name string) (code int) { 27 | var user User 28 | db.Model(&user).Select("id").Where("username = ?", name).First(&user) 29 | if user.ID > 0 { 30 | return errmsg.ERROR_USERNAME_USERD //1001 31 | } 32 | return errmsg.SUCCESS 33 | } 34 | 35 | //todo 添加用户 36 | func CreateUser(data *User) (*User, int) { 37 | //密码加密 38 | //data.Password=ScryptPw(data.Password) 39 | err = db.Model(&User{}).Create(&data).Error 40 | if err != nil { 41 | return nil, errmsg.ERROR //500 42 | } 43 | return data, errmsg.SUCCESS 44 | } 45 | 46 | //todo 查询用户详细信息,包括文章 47 | func GetUserInfo(id int) (User, int) { 48 | var user User 49 | 50 | err = db.Model(&user).Preload("Article").Where("id = ?", id).First(&user).Error 51 | if err != nil { 52 | return user, errmsg.ERROR 53 | } 54 | return user, errmsg.SUCCESS 55 | } 56 | 57 | //todo 查询用户列表,带分页效果 58 | func GetUsers(IdOrName string, pageSize int, pageNum int) ([]User, int, error) { 59 | var users []User 60 | var total int 61 | DB := db.Model(&users) 62 | Id, err := strconv.Atoi(IdOrName) 63 | if err != nil { 64 | if IdOrName != "" { 65 | DB = DB.Where("username like ?", "%"+IdOrName+"%") 66 | } 67 | } else { 68 | if Id > 0 { 69 | DB = DB.Where("id = ?", Id) 70 | } 71 | } 72 | err = DB.Count(&total).Error 73 | if err != nil { 74 | return nil, 0, err 75 | } 76 | err = DB.Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&users).Error 77 | if err != nil { 78 | return nil, 0, err 79 | } 80 | 81 | return users, total, nil 82 | } 83 | 84 | //todo 编辑用户 85 | func EditUser(id int, data *proto.ReqEditUser) int { 86 | var maps = make(map[string]interface{}) 87 | 88 | maps["username"] = data.UserName 89 | maps["role"] = data.Role 90 | maps["email"] = data.Email 91 | err = db.Model(&User{}).Where("id = ?", id).Update(maps).Error 92 | if err != nil { 93 | return errmsg.ERROR 94 | } 95 | return errmsg.SUCCESS 96 | } 97 | 98 | //todo 删除用户 99 | func DeleteUser(id int) int { 100 | //删除与该用户相关联的中间表 101 | DeleteMidByUserId(id) 102 | fmt.Println(id) 103 | err = db.Model(&User{}).Where("id = ?", id).Delete(&User{}).Error 104 | 105 | if err != nil { 106 | return errmsg.ERROR 107 | } 108 | return errmsg.SUCCESS 109 | } 110 | 111 | //在调用函数之前执行 112 | func (u *User) BeforeSave() { 113 | u.Password = ScryptPw(u.Password) 114 | } 115 | 116 | // todo 使用scrypt密码加密 117 | func ScryptPw(password string) string { 118 | const KeyLen = 10 119 | salt := make([]byte, 8) 120 | salt = []byte{12, 32, 14, 6, 66, 22, 43, 11} 121 | //加密 122 | HashPw, err := scrypt.Key([]byte(password), salt, 16384, 8, 1, KeyLen) 123 | if err != nil { 124 | log.Fatal(err) 125 | } 126 | //将加密后的密码转化为字符串 127 | fpw := base64.StdEncoding.EncodeToString(HashPw) 128 | return fpw 129 | } 130 | 131 | //todo 登录验证 132 | func CheckLogin(username string, password string) (*User, int) { 133 | var user User 134 | db.Where("username = ?", username).First(&user) 135 | //fmt.Println(user) 136 | if user.Status == "N" { 137 | return nil, errmsg.ERROR_EMAIL_HAVE_NOT_ACTIVE 138 | } 139 | //判断该用户是否存在 140 | if user.ID == 0 { 141 | return nil, errmsg.ERROR_USER_NOT_EXIST 142 | } 143 | //判断用户密码是否正确 144 | if ScryptPw(password) != user.Password { 145 | return nil, errmsg.ERROR_PASSWORD_WRONG 146 | } 147 | //用户无权限 148 | //if user.Role!=2{ 149 | // return errmsg.ERROR_USER_NO_RIGHT 150 | //} 151 | 152 | return &user, errmsg.SUCCESS 153 | } 154 | 155 | //todo 通过用户名查找用户id 156 | func FindUserIdByName(username interface{}) uint { 157 | 158 | var user User 159 | 160 | db.Model(&user).Where("username = ?", username).First(&user) 161 | 162 | return user.ID 163 | 164 | } 165 | 166 | //todo 通过激活码查找用户并且激活 167 | func UpdateUserStatus(code string) int { 168 | 169 | err = db.Model(&User{}).Where("code = ?", code).UpdateColumn(map[string]interface{}{ 170 | "status": "Y", 171 | }).Error 172 | if err != nil { 173 | return errmsg.ERROR_EMAIL_ACTIVE 174 | } 175 | //激活 176 | return errmsg.SUCCESS 177 | } 178 | 179 | //todo 通过邮箱查找用户 180 | func GetUserByEmail(email string) (User, int) { 181 | var user User 182 | 183 | err := db.Where("email = ?", email).First(&user).Error 184 | if err != nil { 185 | return user, errmsg.ERROR_USER_NOT_EXIST 186 | } 187 | return user, errmsg.SUCCESS 188 | } 189 | -------------------------------------------------------------------------------- /model/UserArticle.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "demo1/MyBlog/utils/errmsg" 5 | ) 6 | 7 | type UserArticle struct { 8 | ArticleId uint `gorm:"type:not null" json:"article_id"` 9 | UserId uint `gorm:"type:not null" json:"user_id"` 10 | } 11 | 12 | //todo 创建中间表 13 | func CreateUserArticle(userArticle *UserArticle) int { 14 | err=db.Model(&UserArticle{}).Create(&userArticle).Error 15 | if err != nil { 16 | return errmsg.ERROR //500 17 | } 18 | return errmsg.SUCCESS 19 | } 20 | //todo 根据用户id删除中间表 21 | func DeleteMidByUserId(id int) int { 22 | err=db.Model(&UserArticle{}).Where("user_id = ?",id).Delete(&UserArticle{}).Error 23 | if err != nil { 24 | return errmsg.ERROR //500 25 | } 26 | return errmsg.SUCCESS 27 | } 28 | //todo 根据文章id删除中间表 29 | func DeleteMidByArticleId(id int) int { 30 | err=db.Model(&UserArticle{}).Where("article_id = ?",id).Delete(&UserArticle{}).Error 31 | if err != nil { 32 | return errmsg.ERROR //500 33 | } 34 | return errmsg.SUCCESS 35 | } 36 | -------------------------------------------------------------------------------- /model/db.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | //数据库入口 4 | 5 | import ( 6 | "demo1/MyBlog/utils" 7 | "fmt" 8 | _ "github.com/garyburd/redigo/redis" 9 | "github.com/go-redis/redis" 10 | _ "github.com/go-sql-driver/mysql" 11 | "github.com/jinzhu/gorm" 12 | "time" 13 | ) 14 | 15 | //定义全局变量 16 | var db *gorm.DB 17 | var err error 18 | var Redis *redis.Client 19 | 20 | //初始化数据库 21 | func InitDb() { 22 | //连接数据库 23 | db, err = gorm.Open(utils.Db, fmt.Sprintf("%s:%s@(%s%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", 24 | utils.DbUser, 25 | utils.DbPassWord, 26 | utils.DbHost, 27 | utils.DbPort, 28 | utils.DbName, 29 | )) 30 | if err != nil { 31 | fmt.Println("连接数据库失败,请检查参数: ", err) 32 | return 33 | } 34 | //defer db.Close() 35 | 36 | //禁用默认表的复数形式 37 | db.SingularTable(true) 38 | //迁移 39 | db.AutoMigrate(&User{}, &Article{}, &Category{}, &Comment{}, &UserArticle{}, &Profile{}) 40 | 41 | //设置连接池的最大闲置连接数 42 | db.DB().SetMaxIdleConns(10) 43 | //设置连接池中的最大连接数量 44 | db.DB().SetMaxOpenConns(100) 45 | //设置连接的最大复用时间 46 | db.DB().SetConnMaxLifetime(10 * time.Second) 47 | } 48 | 49 | func InitRedis() { 50 | Redis = redis.NewClient(&redis.Options{ 51 | Addr: utils.RedisAddr, 52 | Password: utils.RedisPassword, // no password set 53 | DB: utils.RedisDB, // use default DB 54 | }) 55 | _, err = Redis.Ping().Result() 56 | if err != nil { 57 | fmt.Println("redis连接失败") 58 | return 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /model/session.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | const SessionName = "session" 8 | 9 | //设置客户端session_id 10 | func SetSession(c *gin.Context, sessionID string, expireTime int) { 11 | c.SetCookie(SessionName, sessionID, expireTime, "/", "localhost", false, true) 12 | //http.SetCookie(c.Writer, &http.Cookie{ 13 | // Name: SessionName, 14 | // Value: sessionID, 15 | // MaxAge: expireTime, 16 | // Path: "/", 17 | //}) 18 | } 19 | 20 | func ClearSession(c *gin.Context) { 21 | c.SetCookie(SessionName, "", -1, "/", "localhost", false, true) 22 | } 23 | -------------------------------------------------------------------------------- /proto/req.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type ( 4 | ReqCommon struct { 5 | PageNum int `json:"pagenum"` 6 | PageSize int `json:"pagesize"` 7 | } 8 | ReqFindUser struct { 9 | PageNum int `json:"pagenum"` 10 | PageSize int `json:"pagesize"` 11 | IdOrName string `json:"idorname"` 12 | } 13 | ReqAddUser struct { 14 | UserName string `json:"username"` 15 | Password string `json:"password"` 16 | Email string `json:"email"` 17 | Role int `json:"role"` 18 | } 19 | ReqEditUser struct { 20 | Id int `json:"id"` 21 | UserName string `json:"username"` 22 | Email string `json:"email"` 23 | Role int `json:"role"` 24 | } 25 | ReqFindArticle struct { 26 | PageNum int `json:"pagenum"` 27 | PageSize int `json:"pagesize"` 28 | Title string `json:"title"` 29 | } 30 | ReqCateArticle struct { 31 | PageNum int `json:"pagenum"` 32 | PageSize int `json:"pagesize"` 33 | Id int `json:"id"` 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /proto/rsp.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import "time" 4 | 5 | type RspFindArticle struct { 6 | ID uint `json:"id"` 7 | Title string `json:"title"` 8 | Name string `json:"name"` 9 | CreatedAt time.Time `json:"CreatedAt"` 10 | Desc string ` json:"desc"` 11 | Content string `json:"content"` 12 | Img string ` json:"img"` 13 | } -------------------------------------------------------------------------------- /routes/router.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | v1 "demo1/MyBlog/api/v1" 5 | "demo1/MyBlog/middleware" 6 | "demo1/MyBlog/utils" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | func InitRouter() { 12 | 13 | gin.SetMode(utils.AppMode) 14 | r := gin.New() 15 | r.Use(gin.Recovery()) 16 | r.Use(middleware.Logger()) 17 | r.Use(middleware.Cors()) 18 | //这里是后台管理的页面加载 19 | r.LoadHTMLGlob("static/admin/index.html") 20 | r.Static("admin/static", "static/admin/static") 21 | r.StaticFile("admin/favicon.ico", "static/admin/favicon.ico") 22 | r.GET("admin", func(c *gin.Context) { 23 | c.HTML(http.StatusOK, "index.html", nil) 24 | }) 25 | //这里是博客前台展示的页面加载,如需要可以打开,切记不可同时打开 26 | //r.LoadHTMLGlob("static/front/index.html") 27 | //r.Static("front/static", "static/front/static") 28 | //r.StaticFile("front/favicon.ico", "static/front/favicon.ico") 29 | //r.GET("front", func(c *gin.Context) { 30 | // c.HTML(http.StatusOK, "index.html", nil) 31 | //}) 32 | //设置中间件,以下操作需要用户权限(凭证) 33 | auth := r.Group("api/v1") 34 | auth.Use(middleware.JwtToken()) 35 | { 36 | 37 | // 用户模块的路由接口 38 | 39 | //编辑用户信息 40 | auth.POST("user/update", v1.EditUser) 41 | //删除用户 42 | auth.DELETE("user/:id", v1.DeleteUser) 43 | 44 | // 分类模块的路由接口 45 | //添加分类 46 | auth.POST("category/add", v1.AddCategory) 47 | 48 | //编辑分类信息 49 | auth.PUT("category/:id", v1.EditCategory) 50 | //删除分类 51 | auth.DELETE("category/:id", v1.DeleteCategory) 52 | 53 | // 文章模块的路由接口 54 | //添加文章 55 | auth.POST("article/add", v1.AddArticle) 56 | //添加评论 57 | auth.POST("comment", v1.AddComment) 58 | //编辑文章 59 | auth.PUT("article/:id", v1.EditArticle) 60 | //删除文章 61 | auth.DELETE("article/:id", v1.DeleteArticle) 62 | //删除评论 63 | auth.DELETE("comment/:id", v1.DeleteComment) 64 | //上传文件 65 | auth.POST("upload", v1.Upload) 66 | 67 | //更新个人设置 68 | auth.PUT("profile", v1.UpdateProfile) 69 | } 70 | 71 | routeV1 := r.Group("api/v1") 72 | { 73 | // 添加用户 74 | routeV1.POST("user/add", v1.AddUser) 75 | //查询所有用户 76 | routeV1.POST("users", v1.GetUsers) 77 | //查询用户详细信息,包括文章 78 | routeV1.GET("user/:id", v1.GetUserInfo) 79 | //通过id查询分类信息 80 | routeV1.GET("category/:id", v1.FindCategoryById) 81 | //查询所有分类 82 | routeV1.GET("category", v1.GetCategory) 83 | //查询所有文章信息 84 | routeV1.POST("articles", v1.GetArticle) 85 | //查询某分类下的所有文章 86 | routeV1.GET("article/cate/:id", v1.GetCateArticle) 87 | //查询谋篇文章的基本信息 88 | routeV1.GET("article/info/:id", v1.GetArticleInfo) 89 | //查询某文章下的所有评论 90 | routeV1.GET("comment/:id", v1.GetComments) 91 | //登录 92 | routeV1.POST("login", v1.Login) 93 | //注册用户 94 | routeV1.POST("/register", v1.Register) 95 | //邮件激活 96 | routeV1.GET("/active", v1.ActiveEmail) 97 | //登录发送邮件,需要参数email 98 | routeV1.GET("sendemail", v1.SendEmailForCode) 99 | //使用邮箱登录,需要参数email和验证码 100 | routeV1.GET("loginbyemail", v1.LoginByEmail) 101 | 102 | //获取个人信息 103 | routeV1.GET("profile", v1.GetProfile) 104 | } 105 | 106 | r.Run(utils.HttpPort) 107 | } 108 | -------------------------------------------------------------------------------- /serve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash #变量 PROJECTNAME="myblog" PROJECTBASE="." PROJECTBIN="$PROJECTBASE/bin" PROJECTLOGS="$PROJECTBASE/log" prog=$PROJECTNAME #获取当前目录 CURDIR=$(dirname $0) cd $CURDIR #运行服务 start() { echo -e "Begin to compile the project ---$PROJECTNAME..." #编译go项目 go build -o $PROJECTNAME main.go #赋予权限 chmod 777 "$CURDIR/$PROJECTNAME" echo "Compilation completed" echo "starting $PROJECTNAME,please waiting..." #后台运行项目 nohup ./$PROJECTNAME > $PROJECTLOGS/run.log 2>&1 & echo -e "ok" } #暂停服务 stop(){ echo -e $"Stopping the project ---$prog: " #获取进程 pid=$(ps -ef | grep $prog | grep -v grep | awk '{print $2}') if [ "$pid" ]; then echo -n $"kill process pid: $pid " #杀掉进程 kill -9 $pid ret=0 #多次循环杀掉进程 for ((i=1;i<=15;i++)); do sleep 1 pid=$(ps -ef | grep $prog | grep -v grep | awk '{print $2}') if [ "$pid" ]; then kill -9 $pid ret=0 else ret=1 break fi done if [ "$ret" ]; then echo -e $"ok" else echo -e $"no" fi else echo -e $"no program process to stop" fi } #重启服务 restart(){ stop sleep 2 start } #判断第一个参数 case "$1" in start) $1 ;; stop) $1 ;; restart) $1 ;; *) echo $"Usage: $0 {start|stop|restart}" exit 2 ;; esac -------------------------------------------------------------------------------- /static/admin/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/static/admin/favicon.ico -------------------------------------------------------------------------------- /static/admin/index.html: -------------------------------------------------------------------------------- 1 | admin
-------------------------------------------------------------------------------- /static/front/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/static/front/favicon.ico -------------------------------------------------------------------------------- /static/front/index.html: -------------------------------------------------------------------------------- 1 | front
-------------------------------------------------------------------------------- /static/front/static/css/app.e55e68b9.css: -------------------------------------------------------------------------------- 1 | .container[data-v-dec4de66]{height:100%;background-color:#282c34;width:100%}.loginBox[data-v-dec4de66]{width:400px;height:300px;background-color:#fff;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border:1px solid #000;border-radius:9px}.loginForm[data-v-dec4de66]{width:100%;position:absolute;bottom:10%;padding:0 20px;box-sizing:border-box}.loginBtn[data-v-dec4de66]{display:flex;justify-content:flex-end} -------------------------------------------------------------------------------- /static/front/static/js/app.7b3b961d.js: -------------------------------------------------------------------------------- 1 | (function(t){function e(e){for(var r,o,i=e[0],c=e[1],l=e[2],d=0,f=[];d=4&&t.length<=12||"用户名长度必须在4-12之间"}],passwordRules:[function(t){return!!t||"密码不能为空"},function(t){return t&&t.length>=6&&t.length<=20||"密码长度必须在6-20之间"}]}},methods:{resetForm:function(){this.$refs.loginFormRef.reset()},login:function(){var t=this;return Object(b["a"])(regeneratorRuntime.mark((function e(){var a,r,n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(a=t.$refs.loginFormRef.validate(),a){e.next=5;break}return e.abrupt("return");case 5:return e.next=7,t.$http.post("login",t.formdata);case 7:if(r=e.sent,n=r.data,200==n.code){e.next=11;break}return e.abrupt("return");case 11:window.sessionStorage.setItem("token",n.token),t.$router.push("/home");case 13:case"end":return e.stop()}}),e)})))()}}},_t=wt,kt=(a("a2c7"),a("4bd4")),xt=a("8654"),yt=Object(c["a"])(_t,ht,gt,!1,null,"dec4de66",null),Ct=yt.exports;w()(yt,{VApp:ot["a"],VBtn:x["a"],VForm:kt["a"],VTextField:xt["a"]}),r["a"].use(d["a"]);var Vt=[{path:"/login",name:"login",component:Ct,meta:{title:"请先登录"}},{path:"/home",name:"home",component:ut,children:[{path:"",component:rt,meta:{title:"欢迎来到GinBlog"}},{path:"detail/:id",component:jt,props:!0,meta:{title:"文章详情"}},{path:":id",component:rt,meta:{title:"欢迎来到GinBlog"},props:!0}]},{path:"*",redirect:"login"}],It=new d["a"]({mode:"history",base:"/front/",routes:Vt});It.beforeEach((function(t,e,a){t.meta.title&&(document.title=t.meta.title);var r=window.sessionStorage.getItem("token");if(console.log("token",r),"/"==t.path&&a("/login"),"/login"===t.path)return a();r||"/home"!=t.path?a():a("/login")}));var zt=It,Ot=a("f309");r["a"].use(Ot["a"]);var $t=new Ot["a"]({}),Rt=a("2f62");r["a"].use(Rt["a"]);var At=new Rt["a"].Store({state:{cateId:-1},getters:{},mutations:{saveCateId:function(t,e){t.cateId=e}},actions:{}}),qt=a("c1df"),Mt=a.n(qt),Pt=a("bc3a"),Ft=a.n(Pt);Ft.a.defaults.baseURL="http://localhost:8080/api/v1",Ft.a.defaults.withCredentials=!0,Ft.a.interceptors.request.use((function(t){return t.headers.Authorization="Bearer ".concat(window.sessionStorage.getItem("token")),t})),r["a"].prototype.$http=Ft.a,r["a"].config.productionTip=!1,r["a"].filter("dateformat",(function(t,e){return Mt()(t).format(e)})),new r["a"]({store:At,router:zt,vuetify:$t,render:function(t){return t(u)}}).$mount("#app")},"80e2":function(t,e,a){},a2c7:function(t,e,a){"use strict";a("80e2")}}); 2 | //# sourceMappingURL=app.7b3b961d.js.map -------------------------------------------------------------------------------- /utils/errmsg/errmsg.go: -------------------------------------------------------------------------------- 1 | package errmsg 2 | 3 | //错误处理常量及信息 4 | 5 | const ( 6 | SUCCESS = 200 7 | ERROR = 500 8 | ERROR_Params = 5001 9 | //code=1000...用户模块的错误 10 | //用户名已被使用 11 | ERROR_USERNAME_USERD = 1001 12 | //用户名密码错误 13 | ERROR_PASSWORD_WRONG = 1002 14 | //用户不存在 15 | ERROR_USER_NOT_EXIST = 1003 16 | //TOKEN不存在 17 | ERROR_TOKEN_NOT_EXIST = 1004 18 | //TOKEN过期了 19 | ERROR_TOKEN_RUNTIEM = 1005 20 | //TOKEN错误 21 | ERROR_TOKEN_WRONG = 1006 22 | //TOKEN格式错误 23 | ERROR_TOKEN_TYPE_WRONG=1007 24 | //用户无权限 25 | ERROR_USER_NO_RIGHT=1008 26 | 27 | //code=2000...文章模块的错误 28 | ERROR_ARTICLE_NOT_EXIST=2001 29 | ERROR_ARTICLE_NO_COMMENTS=2002 30 | 31 | //code =3000...分类模块的错误 32 | ERROR_CATENAME_USED=3001 33 | ERROR_CATE_NOT_EXIST=3002 34 | 35 | //邮件激活码不存在 36 | ERROR_EMAIL_CODE_NOT_EXIST=4001 37 | 38 | ERROR_EMAIL_ACTIVE=4002 39 | 40 | ERROR_EMAIL_HAVE_NOT_ACTIVE=4003 41 | ERROR_EMAIL_SEND=4004 42 | ERROR_CODE_WRONG=4005 43 | ) 44 | 45 | var codeMsg = map[int]string{ 46 | SUCCESS :"OK", 47 | ERROR :"Fail", 48 | ERROR_Params:"参数有问题", 49 | ERROR_USERNAME_USERD :"用户名已存在!", 50 | ERROR_PASSWORD_WRONG :"密码错误!", 51 | ERROR_USER_NOT_EXIST :"用户名不存在!", 52 | ERROR_TOKEN_NOT_EXIST :"TOKEN不存在", 53 | ERROR_TOKEN_RUNTIEM :"TOKEN已过期", 54 | ERROR_TOKEN_WRONG :"TOKEN不正确", 55 | ERROR_TOKEN_TYPE_WRONG:"TOKEN格式错误", 56 | ERROR_USER_NO_RIGHT:"该用户无权限", 57 | 58 | ERROR_CATENAME_USED:"该分类已存在!", 59 | ERROR_CATE_NOT_EXIST:"给分类不存在!", 60 | 61 | ERROR_ARTICLE_NOT_EXIST:"文章不存在!", 62 | ERROR_ARTICLE_NO_COMMENTS:"该文章没有评论", 63 | 64 | ERROR_EMAIL_CODE_NOT_EXIST:"邮件激活码不存在", 65 | ERROR_EMAIL_ACTIVE:"邮件激活失败", 66 | ERROR_EMAIL_HAVE_NOT_ACTIVE:"邮箱尚未激活", 67 | ERROR_EMAIL_SEND:"邮件发送失败", 68 | ERROR_CODE_WRONG:"验证码错误", 69 | } 70 | 71 | func GetErrMsg(code int) string { 72 | return codeMsg[code] 73 | } 74 | -------------------------------------------------------------------------------- /utils/setting.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | //解析配置文件并设置参数 4 | 5 | import ( 6 | "fmt" 7 | "gopkg.in/ini.v1" 8 | ) 9 | 10 | var ( 11 | AppMode string 12 | HttpPort string 13 | JwtKey string 14 | 15 | Db string 16 | DbHost string 17 | DbPort string 18 | DbUser string 19 | DbPassWord string 20 | DbName string 21 | 22 | AccessKey string 23 | SecretKey string 24 | Bucket string 25 | QiniuServer string 26 | 27 | RedisAddr string 28 | RedisPassword string 29 | RedisDB int 30 | 31 | ServerHost string 32 | ServerPort string 33 | FromEmail string 34 | FromPassword string 35 | ) 36 | 37 | func init() { 38 | //解析配置文件config.ini 39 | file, err := ini.Load("config/config.ini") 40 | if err != nil { 41 | fmt.Println("配置文件读取错误,请检查文件路径:", err) 42 | } 43 | 44 | //读取服务端相关配置 45 | LoadServer(file) 46 | //读取数据库相关配置 47 | LoadDate(file) 48 | //读取七牛云相关配置 49 | LoadQiniu(file) 50 | //获取redis相关配置 51 | LoadRedis(file) 52 | //获取email相关配置 53 | LoadEmailServer(file) 54 | } 55 | 56 | func LoadServer(file *ini.File) { 57 | AppMode = file.Section("server").Key("AppMode").MustString("debug") 58 | HttpPort = file.Section("server").Key("HttpPort").MustString(":80") 59 | JwtKey = file.Section("server").Key("JwtKey").MustString("45df45rds4") 60 | } 61 | 62 | func LoadDate(file *ini.File) { 63 | Db = file.Section("database").Key("Db").MustString("mysql") 64 | DbHost = file.Section("database").Key("DbHost").MustString("localhost") 65 | DbPort = file.Section("database").Key("DbPort").MustString(":3306") 66 | DbUser = file.Section("database").Key("DbUser").MustString("root") 67 | DbPassWord = file.Section("database").Key("DbPassWord").MustString("020821mnh") 68 | DbName = file.Section("database").Key("DbName").MustString("ginblog") 69 | } 70 | 71 | func LoadQiniu(file *ini.File) { 72 | AccessKey = file.Section("qiniuyun").Key("AccessKey").String() 73 | SecretKey = file.Section("qiniuyun").Key("SecretKey").String() 74 | Bucket = file.Section("qiniuyun").Key("Bucket").String() 75 | QiniuServer = file.Section("qiniuyun").Key("QiniuServer").String() 76 | } 77 | func LoadRedis(file *ini.File) { 78 | var redisSection = file.Section("redis") 79 | RedisAddr = redisSection.Key("RedisAddr").String() 80 | RedisPassword = redisSection.Key("RedisPassword").String() 81 | RedisDB = redisSection.Key("RedisDB").MustInt(0) 82 | } 83 | func LoadEmailServer(file *ini.File) { 84 | var emailSection=file.Section("email") 85 | ServerHost =emailSection.Key("ServerHost").String() 86 | ServerPort =emailSection.Key("ServerPort").String() 87 | FromEmail =emailSection.Key("FromEmail").String() 88 | FromPassword =emailSection.Key("FromPassword").String() 89 | } 90 | -------------------------------------------------------------------------------- /utils/time.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | const ( 6 | TimeFormatSecond = "2006-01-02 15:04:05" 7 | TimeFormatMinute = "2006-01-02 15:04" 8 | TimeFormatDateV1 = "2006-01-02" 9 | TimeFormatDateV2 = "2006_01_02" 10 | Expiration time.Duration = 24 * 60 * 60 * 1000 * 1000 * 1000 //过期时间 11 | ) 12 | -------------------------------------------------------------------------------- /utils/uuid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | uuid "github.com/satori/go.uuid" 6 | "math/rand" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | //工具,生成唯一字符串(激活码) 12 | func CreateUUID() string { 13 | id := uuid.NewV4() 14 | return id.String() 15 | } 16 | 17 | //生成四位数验证码 18 | func CreateVcode() string { 19 | codeHub := "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM" 20 | rand.Seed(time.Now().Unix()) 21 | var code strings.Builder 22 | for i := 0; i < 4; i++ { 23 | index := rand.Intn(len(codeHub)) 24 | fmt.Fprintf(&code, "%c", codeHub[index]) 25 | } 26 | //fmt.Println(code.String()) 27 | return code.String() 28 | } 29 | -------------------------------------------------------------------------------- /utils/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "demo1/MyBlog/utils/errmsg" 5 | "fmt" 6 | "github.com/go-playground/locales/zh_Hans_CN" 7 | unTrans "github.com/go-playground/universal-translator" 8 | "github.com/go-playground/validator/v10" 9 | zhTrans "github.com/go-playground/validator/v10/translations/zh" 10 | "reflect" 11 | ) 12 | 13 | //后端数据验证 14 | func Validate(data interface{}) (string, int) { 15 | validate:=validator.New() 16 | uni:=unTrans.New(zh_Hans_CN.New()) 17 | trans,_:=uni.GetTranslator("zh_Hans_CN") 18 | 19 | err:= zhTrans.RegisterDefaultTranslations(validate,trans) 20 | if err != nil { 21 | fmt.Println("err: ",err) 22 | } 23 | 24 | validate.RegisterTagNameFunc(func(field reflect.StructField) string { 25 | label:=field.Tag.Get("label") 26 | return label 27 | }) 28 | 29 | err = validate.Struct(data) 30 | if err != nil { 31 | for _,v:=range err.(validator.ValidationErrors){ 32 | return v.Translate(trans),errmsg.ERROR 33 | } 34 | } 35 | return "",errmsg.SUCCESS 36 | } -------------------------------------------------------------------------------- /web/admin/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /web/admin/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /web/admin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | // 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /web/admin/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /web/admin/README.md: -------------------------------------------------------------------------------- 1 | # admin 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /web/admin/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: [ 6 | [ 7 | "import", 8 | { libraryName: "ant-design-vue", libraryDirectory: "es", style: 'css' } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /web/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@tinymce/tinymce-vue": "^3.2.2", 12 | "ant-design-vue": "^1.7.8", 13 | "axios": "^0.24.0", 14 | "core-js": "^3.6.5", 15 | "tinymce": "^5.4.1", 16 | "vue": "^2.6.11", 17 | "vue-router": "^3.2.0", 18 | "vuex": "^3.6.2" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~4.5.0", 22 | "@vue/cli-plugin-eslint": "~4.5.0", 23 | "@vue/cli-plugin-router": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "@vue/eslint-config-standard": "^5.1.2", 26 | "babel-eslint": "^10.1.0", 27 | "babel-plugin-import": "^1.13.3", 28 | "eslint": "^6.7.2", 29 | "eslint-plugin-import": "^2.20.2", 30 | "eslint-plugin-node": "^11.1.0", 31 | "eslint-plugin-promise": "^4.2.1", 32 | "eslint-plugin-standard": "^4.0.0", 33 | "eslint-plugin-vue": "^6.2.2", 34 | "vue-template-compiler": "^2.6.11" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/admin/public/favicon.ico -------------------------------------------------------------------------------- /web/admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /web/admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /web/admin/src/assets/css/style.css: -------------------------------------------------------------------------------- 1 | html,body,#app{ 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | .ant-card { 7 | margin:10px; 8 | background-color: #fff; 9 | } 10 | 11 | .ant-table{ 12 | margin-top: 15px; 13 | } -------------------------------------------------------------------------------- /web/admin/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/admin/src/assets/logo.png -------------------------------------------------------------------------------- /web/admin/src/components/admin/Footer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /web/admin/src/components/admin/Header.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | -------------------------------------------------------------------------------- /web/admin/src/components/admin/Index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/admin/src/components/admin/Nav.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 41 | 42 | -------------------------------------------------------------------------------- /web/admin/src/components/article/AddArt.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 142 | -------------------------------------------------------------------------------- /web/admin/src/components/article/ArtList.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 195 | 196 | 202 | -------------------------------------------------------------------------------- /web/admin/src/components/category/CateList.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 213 | 214 | -------------------------------------------------------------------------------- /web/admin/src/components/editor/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /web/admin/src/components/editor/langs/README.md: -------------------------------------------------------------------------------- 1 | This is where language files should be placed. 2 | 3 | Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/ 4 | -------------------------------------------------------------------------------- /web/admin/src/components/editor/plugins/code/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.4.2 (2020-08-17) 8 | */ 9 | (function () { 10 | 'use strict'; 11 | 12 | var global = tinymce.util.Tools.resolve('tinymce.PluginManager'); 13 | 14 | var setContent = function (editor, html) { 15 | editor.focus(); 16 | editor.undoManager.transact(function () { 17 | editor.setContent(html); 18 | }); 19 | editor.selection.setCursorLocation(); 20 | editor.nodeChanged(); 21 | }; 22 | var getContent = function (editor) { 23 | return editor.getContent({ source_view: true }); 24 | }; 25 | 26 | var open = function (editor) { 27 | var editorContent = getContent(editor); 28 | editor.windowManager.open({ 29 | title: 'Source Code', 30 | size: 'large', 31 | body: { 32 | type: 'panel', 33 | items: [{ 34 | type: 'textarea', 35 | name: 'code' 36 | }] 37 | }, 38 | buttons: [ 39 | { 40 | type: 'cancel', 41 | name: 'cancel', 42 | text: 'Cancel' 43 | }, 44 | { 45 | type: 'submit', 46 | name: 'save', 47 | text: 'Save', 48 | primary: true 49 | } 50 | ], 51 | initialData: { code: editorContent }, 52 | onSubmit: function (api) { 53 | setContent(editor, api.getData().code); 54 | api.close(); 55 | } 56 | }); 57 | }; 58 | 59 | var register = function (editor) { 60 | editor.addCommand('mceCodeEditor', function () { 61 | open(editor); 62 | }); 63 | }; 64 | 65 | var register$1 = function (editor) { 66 | editor.ui.registry.addButton('code', { 67 | icon: 'sourcecode', 68 | tooltip: 'Source code', 69 | onAction: function () { 70 | return open(editor); 71 | } 72 | }); 73 | editor.ui.registry.addMenuItem('code', { 74 | icon: 'sourcecode', 75 | text: 'Source code', 76 | onAction: function () { 77 | return open(editor); 78 | } 79 | }); 80 | }; 81 | 82 | function Plugin () { 83 | global.add('code', function (editor) { 84 | register(editor); 85 | register$1(editor); 86 | return {}; 87 | }); 88 | } 89 | 90 | Plugin(); 91 | 92 | }()); 93 | -------------------------------------------------------------------------------- /web/admin/src/components/editor/plugins/code/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.4.2 (2020-08-17) 8 | */ 9 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),o=function(o){var e=o.getContent({source_view:!0});o.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:e},onSubmit:function(e){var t,n;t=o,n=e.getData().code,t.focus(),t.undoManager.transact(function(){t.setContent(n)}),t.selection.setCursorLocation(),t.nodeChanged(),e.close()}})};!function t(){e.add("code",function(e){var t,n;return(t=e).addCommand("mceCodeEditor",function(){o(t)}),(n=e).ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:function(){return o(n)}}),n.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:function(){return o(n)}}),{}})}()}(); -------------------------------------------------------------------------------- /web/admin/src/components/editor/plugins/imagetools/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.4.2 (2020-08-17) 8 | */ 9 | !function(p){"use strict";var t,e,n,l=function(t){var e=t;return{get:function(){return e},set:function(t){e=t}}},r=tinymce.util.Tools.resolve("tinymce.PluginManager"),d=tinymce.util.Tools.resolve("tinymce.util.Tools"),o=function(){},f=function(t){return function(){return t}},i=f(!1),u=f(!0),a=function(){return c},c=(t=function(t){return t.isNone()},{fold:function(t,e){return t()},is:i,isSome:i,isNone:u,getOr:n=function(t){return t},getOrThunk:e=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:f(null),getOrUndefined:f(undefined),or:n,orThunk:e,map:a,each:o,bind:a,exists:i,forall:u,filter:a,equals:t,equals_:t,toArray:function(){return[]},toString:f("none()")}),s=function(n){var t=f(n),e=function(){return o},r=function(t){return t(n)},o={fold:function(t,e){return e(n)},is:function(t){return n===t},isSome:u,isNone:i,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:e,orThunk:e,map:function(t){return s(t(n))},each:function(t){t(n)},bind:r,exists:r,forall:r,filter:function(t){return t(n)?o:c},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(t){return t.is(n)},equals_:function(t,e){return t.fold(i,function(t){return e(n,t)})}};return o},v={some:s,none:a,from:function(t){return null===t||t===undefined?c:s(t)}};function m(t,e){return y(p.document.createElement("canvas"),t,e)}function h(t){var e=m(t.width,t.height);return g(e).drawImage(t,0,0),e}function g(t){return t.getContext("2d")}function y(t,e,n){return t.width=e,t.height=n,t}var w,b,I,T=window.Promise?window.Promise:(b=(w=function(t){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof t)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],L(t,_(U,this),_(A,this))}).immediateFn||"function"==typeof window.setImmediate&&window.setImmediate||function(t){p.setTimeout(t,1)},I=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)},w.prototype["catch"]=function(t){return this.then(null,t)},w.prototype.then=function(n,r){var o=this;return new w(function(t,e){R.call(o,new x(n,r,t,e))})},w.all=function(){for(var t=[],e=0;e'; 54 | if (contentStyle) { 55 | headHtml += ''; 56 | } 57 | var cors = shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : ''; 58 | global$2.each(editor.contentCSS, function (url) { 59 | headHtml += ''; 60 | }); 61 | var bodyId = getBodyId(editor); 62 | var bodyClass = getBodyClass(editor); 63 | var isMetaKeyPressed = global$1.mac ? 'e.metaKey' : 'e.ctrlKey && !e.altKey'; 64 | var preventClicksOnLinksScript = ' '; 65 | var directionality = editor.getBody().dir; 66 | var dirAttr = directionality ? ' dir="' + encode(directionality) + '"' : ''; 67 | var previewHtml = '' + '' + '' + headHtml + '' + '' + editor.getContent() + preventClicksOnLinksScript + '' + ''; 68 | return previewHtml; 69 | }; 70 | 71 | var open = function (editor) { 72 | var content = getPreviewHtml(editor); 73 | var dataApi = editor.windowManager.open({ 74 | title: 'Preview', 75 | size: 'large', 76 | body: { 77 | type: 'panel', 78 | items: [{ 79 | name: 'preview', 80 | type: 'iframe', 81 | sandboxed: true 82 | }] 83 | }, 84 | buttons: [{ 85 | type: 'cancel', 86 | name: 'close', 87 | text: 'Close', 88 | primary: true 89 | }], 90 | initialData: { preview: content } 91 | }); 92 | dataApi.focus('close'); 93 | }; 94 | 95 | var register = function (editor) { 96 | editor.addCommand('mcePreview', function () { 97 | open(editor); 98 | }); 99 | }; 100 | 101 | var register$1 = function (editor) { 102 | editor.ui.registry.addButton('preview', { 103 | icon: 'preview', 104 | tooltip: 'Preview', 105 | onAction: function () { 106 | return editor.execCommand('mcePreview'); 107 | } 108 | }); 109 | editor.ui.registry.addMenuItem('preview', { 110 | icon: 'preview', 111 | text: 'Preview', 112 | onAction: function () { 113 | return editor.execCommand('mcePreview'); 114 | } 115 | }); 116 | }; 117 | 118 | function Plugin () { 119 | global.add('preview', function (editor) { 120 | register(editor); 121 | register$1(editor); 122 | }); 123 | } 124 | 125 | Plugin(); 126 | 127 | }()); 128 | -------------------------------------------------------------------------------- /web/admin/src/components/editor/plugins/preview/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.4.2 (2020-08-17) 8 | */ 9 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),g=tinymce.util.Tools.resolve("tinymce.Env"),w=tinymce.util.Tools.resolve("tinymce.util.Tools"),i=function(e){var t=function(t){var n="",i=t.dom.encode,e=t.getParam("content_style","");n+='',e&&(n+='");var o=t.getParam("content_css_cors",!1,"boolean")?' crossorigin="anonymous"':"";w.each(t.contentCSS,function(e){n+='"});var r,a,c,s,d,m,l,u=-1===(s=(r=t).getParam("body_id","tinymce","string")).indexOf("=")?s:(c=(a=r).getParam("body_id","","hash"))[a.id]||c,y=-1===(l=(d=t).getParam("body_class","","string")).indexOf("=")?l:(m=d).getParam("body_class","","hash")[m.id]||"",v=' 130 | 131 | -------------------------------------------------------------------------------- /web/admin/src/components/user/UserList.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 310 | 311 | -------------------------------------------------------------------------------- /web/admin/src/config/index.js: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /web/admin/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import './plugin/antui' 5 | import './assets/css/style.css' 6 | import store from './store' //引入vuex 7 | Vue.prototype.$store = store //引入vuex 8 | 9 | 10 | 11 | Vue.config.productionTip = false 12 | 13 | new Vue({ 14 | // store, 15 | router, 16 | render: h => h(App) 17 | }).$mount('#app') 18 | -------------------------------------------------------------------------------- /web/admin/src/plugin/antui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { 3 | Button, 4 | FormModel , 5 | Input, 6 | Icon, 7 | message, 8 | Layout, 9 | Menu, 10 | Card, 11 | Table, 12 | Row, 13 | Col, 14 | ConfigProvider, 15 | Modal, 16 | Select, 17 | Upload 18 | } from 'ant-design-vue' 19 | 20 | message.config({ 21 | top: `100px`, 22 | duration: 2, 23 | maxCount: 3, 24 | }); 25 | 26 | Vue.prototype.$message=message 27 | Vue.prototype.$confirm=Modal.confirm 28 | 29 | Vue.use(Button) 30 | Vue.use(FormModel) 31 | Vue.use(Input) 32 | Vue.use(Icon) 33 | Vue.use(Layout) 34 | Vue.use(Menu) 35 | Vue.use(Card) 36 | Vue.use(Table) 37 | Vue.use(Row) 38 | Vue.use(Col) 39 | Vue.use(ConfigProvider) 40 | Vue.use(Modal) 41 | Vue.use(Select) 42 | Vue.use(Upload) 43 | -------------------------------------------------------------------------------- /web/admin/src/plugin/http.js: -------------------------------------------------------------------------------- 1 | 2 | import Vue from 'vue' 3 | import axios from 'axios' 4 | 5 | let Url='http://localhost:8080/api/v1' 6 | 7 | axios.defaults.baseURL=Url 8 | axios.defaults.withCredentials=true 9 | axios.interceptors.request.use(config=>{ 10 | config.headers.Authorization=`Bearer ${window.sessionStorage.getItem("token")}` 11 | return config 12 | }) 13 | Vue.prototype.$http=axios 14 | 15 | export {Url} 16 | -------------------------------------------------------------------------------- /web/admin/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Login from '../views/Login.vue' 4 | import Admin from '../views/Admin.vue' 5 | Vue.use(VueRouter) 6 | //页面路由组建 7 | import Index from '../components/admin/Index.vue' 8 | import AddArt from '../components/article/AddArt.vue' 9 | import ArtList from '../components/article/ArtList.vue' 10 | import CateList from '../components/category/CateList.vue' 11 | import UserList from '../components/user/UserList.vue' 12 | import Profile from '../components/user/Profile.vue' 13 | 14 | 15 | 16 | const routes = [ 17 | { 18 | path: '/login', 19 | name: 'login', 20 | component: Login 21 | }, 22 | { 23 | path: '/', 24 | name: 'admin ', 25 | component: Admin, 26 | children:[ 27 | {path:'index',component:Index}, 28 | {path:'addart',component:AddArt}, 29 | { path:'addart/:id',component:AddArt, props:true}, 30 | {path:'artlist',component:ArtList}, 31 | {path:'catelist',component:CateList}, 32 | {path:'userlist',component:UserList}, 33 | {path:'profile',component:Profile,props:true}, 34 | ] 35 | } 36 | ] 37 | 38 | const router = new VueRouter({ 39 | mode: 'history', 40 | base: process.env.BASE_URL, 41 | routes 42 | }) 43 | 44 | router.beforeEach((to,from,next)=>{ 45 | const token = window.sessionStorage.getItem('token') 46 | console.log("token",token) 47 | if(to.path==='/login')return next() 48 | if(!token &&to.path==='/'){ 49 | Vue.prototype.$message.error("请先登录") 50 | next('/login') 51 | }else{ 52 | next() 53 | } 54 | }) 55 | 56 | export default router 57 | -------------------------------------------------------------------------------- /web/admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import Vuex from "vuex" 3 | Vue.use(Vuex) 4 | export default new Vuex.Store({ 5 | //当做data 6 | state:{ 7 | // collapsed:false 8 | }, 9 | //相当于计算属性 10 | getters:{ 11 | }, 12 | //同步一些方法 13 | mutations:{ 14 | }, 15 | //存放异步的方法 16 | actions:{ 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /web/admin/src/views/Admin.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 78 | 79 | -------------------------------------------------------------------------------- /web/admin/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 99 | 100 | 128 | -------------------------------------------------------------------------------- /web/admin/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports={ 2 | lintOnSave:false, 3 | publicPath:'/admin', 4 | outputDir:'dist', 5 | assetsDir:'static', 6 | } -------------------------------------------------------------------------------- /web/front/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /web/front/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /web/front/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /web/front/README.md: -------------------------------------------------------------------------------- 1 | # front 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /web/front/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /web/front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "ant-design-vue": "^1.7.8", 12 | "axios": "^0.25.0", 13 | "core-js": "^3.6.5", 14 | "moment": "^2.29.1", 15 | "vue": "^2.6.11", 16 | "vue-bus": "^1.2.1", 17 | "vue-router": "^3.2.0", 18 | "vuetify": "^2.4.0", 19 | "vuex": "^3.6.2" 20 | }, 21 | "devDependencies": { 22 | "@mdi/font": "^6.5.95", 23 | "@vue/cli-plugin-babel": "~4.5.0", 24 | "@vue/cli-plugin-eslint": "~4.5.0", 25 | "@vue/cli-plugin-router": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "babel-eslint": "^10.1.0", 28 | "eslint": "^6.7.2", 29 | "eslint-plugin-vue": "^6.2.2", 30 | "sass": "~1.32.0", 31 | "sass-loader": "^10.0.0", 32 | "vue-cli-plugin-vuetify": "~2.4.5", 33 | "vue-template-compiler": "^2.6.11", 34 | "vuetify-loader": "^1.7.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/front/public/favicon.ico -------------------------------------------------------------------------------- /web/front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /web/front/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /web/front/src/assets/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/front/src/assets/bg1.png -------------------------------------------------------------------------------- /web/front/src/assets/bg5.webp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/front/src/assets/bg5.webp.png -------------------------------------------------------------------------------- /web/front/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/front/src/assets/logo.png -------------------------------------------------------------------------------- /web/front/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /web/front/src/assets/my4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/front/src/assets/my4.jpg -------------------------------------------------------------------------------- /web/front/src/assets/一色3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmnhs/gin-blog/dc3fb392c932c9b6a7621a67000d2f93fe8fb230/web/front/src/assets/一色3.jpg -------------------------------------------------------------------------------- /web/front/src/components/ArticleList.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 70 | 71 | -------------------------------------------------------------------------------- /web/front/src/components/Detail.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | 39 | -------------------------------------------------------------------------------- /web/front/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /web/front/src/components/Nav.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 80 | 81 | -------------------------------------------------------------------------------- /web/front/src/components/TopBar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 54 | 55 | -------------------------------------------------------------------------------- /web/front/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import vuetify from './plugins/vuetify' 5 | import store from './store' 6 | import moment from 'moment' 7 | import './plugins/http' 8 | Vue.config.productionTip = false 9 | Vue.filter('dateformat',function(indate,outdate){ 10 | return moment(indate).format(outdate) 11 | }) 12 | new Vue({ 13 | store, 14 | router, 15 | vuetify, 16 | render: h => h(App) 17 | }).$mount('#app') 18 | -------------------------------------------------------------------------------- /web/front/src/plugins/http.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | 4 | axios.defaults.baseURL = 'http://localhost:8080/api/v1' 5 | 6 | axios.defaults.withCredentials=true 7 | axios.interceptors.request.use(config=>{ 8 | config.headers.Authorization=`Bearer ${window.sessionStorage.getItem("token")}` 9 | return config 10 | }) 11 | Vue.prototype.$http=axios 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/front/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /web/front/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | import ArticleList from '../components/ArticleList.vue' 5 | import Detail from '../components/Detail.vue' 6 | import Login from '../views/Login.vue' 7 | 8 | Vue.use(VueRouter) 9 | 10 | const routes = [ 11 | { 12 | path: '/login', 13 | name: 'login', 14 | component: Login, 15 | meta:{title:"请先登录",}, 16 | 17 | }, 18 | { 19 | path: '/home', 20 | name: 'home', 21 | component: Home, 22 | children:[ 23 | { 24 | path:'', 25 | component:ArticleList, 26 | meta:{title:"欢迎来到GinBlog",}, 27 | }, 28 | 29 | { 30 | path:`detail/:id`, 31 | component:Detail, 32 | props:true, 33 | meta:{title:"文章详情",}, 34 | }, 35 | { 36 | path:`:id`, 37 | component:ArticleList, 38 | meta:{title:"欢迎来到GinBlog",}, 39 | props:true 40 | }, 41 | ] 42 | }, 43 | { 44 | path: "*", 45 | redirect: "login" 46 | } 47 | 48 | ] 49 | 50 | const router = new VueRouter({ 51 | mode: 'history', 52 | base: process.env.BASE_URL, 53 | routes 54 | }) 55 | router.beforeEach((to,from,next)=>{ 56 | if (to.meta.title){ 57 | document.title=to.meta.title 58 | } 59 | const token = window.sessionStorage.getItem('token') 60 | console.log("token",token) 61 | if (to.path=="/")next("/login") 62 | if(to.path==='/login')return next() 63 | if(!token &&to.path=='/home'){ 64 | // Vue.prototype.$message.error("请先登录") 65 | next("/login") 66 | }else{ 67 | next() 68 | } 69 | }) 70 | 71 | export default router 72 | -------------------------------------------------------------------------------- /web/front/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import Vuex from "vuex" 3 | Vue.use(Vuex) 4 | export default new Vuex.Store({ 5 | //当做data 6 | state:{ 7 | // collapsed:false 8 | cateId:-1 9 | }, 10 | //相当于计算属性 11 | getters:{ 12 | }, 13 | //同步一些方法 14 | mutations:{ 15 | saveCateId(state, cateId) { 16 | state.cateId= cateId; 17 | }, 18 | }, 19 | //存放异步的方法 20 | actions:{ 21 | 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /web/front/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 42 | -------------------------------------------------------------------------------- /web/front/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 86 | 87 | 116 | -------------------------------------------------------------------------------- /web/front/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [ 3 | 'vuetify' 4 | ], 5 | lintOnSave:false, 6 | publicPath:'/front', 7 | outputDir:'dist', 8 | assetsDir:'static', 9 | } 10 | --------------------------------------------------------------------------------