网站建设需要那种技术,信息服务平台怎么赚钱,网站开发建设赚钱吗,Wordpress报表主题文章目录 前情提要本节目标 优化配置结构讲解落实修改配置文件优化配置读取及设置初始化顺序第一步 验证 抽离file 实现上传图片接口图片名加密封装image的处理逻辑编写上传图片的业务逻辑增加图片上传的路由 验证实现前端访问 http.FileServerr.StaticFS修改文章接口新增、更新… 文章目录 前情提要本节目标 优化配置结构讲解落实修改配置文件优化配置读取及设置初始化顺序第一步 验证 抽离file 实现上传图片接口图片名加密封装image的处理逻辑编写上传图片的业务逻辑增加图片上传的路由 验证实现前端访问 http.FileServerr.StaticFS修改文章接口新增、更新文章接口 前情提要
学习项目github地址
上一部分学习笔记
本节目标
优化配置结构因为配置项越来越多抽离 原 logging 的 File 便于公用logging、upload 各保有一份并不合适实现上传图片接口需限制文件格式、大小修改文章接口需支持封面地址参数增加 blog_article 文章的数据库字段实现 http.FileServer
优化配置结构
讲解
在先前章节中,我们通过读取KEY的方式读取配置项(建立setting模块) 本次需求中需要增加图片的配置项总体就有些冗余了
我们采用以下解决方法
映射结构体使用 MapTo 来设置配置参数配置统管所有的配置项统管到 setting 中
落实
修改配置文件
修改 conf/app.ini
增加了 5 个配置项用于上传图片的功能4 个文件日志方面的配置项
[app]
PageSize 10
JwtSecret 233RuntimeRootPath runtime/ImagePrefixUrl http://127.0.0.1:8000
ImageSavePath upload/images/
# MB
ImageMaxSize 5
ImageAllowExts .jpg,.jpeg,.pngLogSavePath logs/
LogSaveName log
LogFileExt log
TimeFormat 20060102[server]
#debug or release
RunMode debug
HttpPort 8000
ReadTimeout 60
WriteTimeout 60[database]
Type mysql
User root
Password rootroot
Host 127.0.0.1:3306
Name blog
TablePrefix blog_优化配置读取及设置初始化顺序
第一步
将散落在其他文件里的配置都删掉统一在 setting 中处理以及修改 init 函数为 Setup 方法
打开 pkg/setting/setting.go 文件修改如下
package modelsimport (fmtlogtimegithub.com/jinzhu/gorm_ github.com/jinzhu/gorm/dialects/mysqlgithub.com/kingsill/gin-example/pkg/setting
)// 定义一个全局的数据库连接变量
var db *gorm.DB// Model 设定常用结构体可以作为匿名结构体嵌入到别的表格对应的结构体
type Model struct {ID int gorm:primary_key json:idCreatedOn int json:created_onModifiedOn int json:modified_onDeletedOn int json:deleted_on
}func Setup() {//配置文件加载Cfg, err : ini.Load(conf/app.ini)if err ! nil {log.Fatalf(Fail to parse conf/app.ini: %v, err)}//将app section 部分映射到AppSetting结构体上err Cfg.Section(app).MapTo(AppSetting)if err ! nil {log.Fatalf(Cfg.MapTo AppSetting err: %v, err)}//将图片最大大小设置从5字节Byte转换为5兆字节MBAppSetting.ImageMaxSize AppSetting.ImageMaxSize * 1024 * 1024err Cfg.Section(server).MapTo(ServerSetting)if err ! nil {log.Fatalf(Cfg.MapTo ServerSetting err: %v, err)}//将读取时自动转换的类型转换为时间间隔了只不过是最小单位纳秒ServerSetting.ReadTimeout ServerSetting.ReadTimeout * time.SecondServerSetting.WriteTimeout ServerSetting.WriteTimeout * time.Seconderr Cfg.Section(database).MapTo(DatabaseSetting)if err ! nil {log.Fatalf(Cfg.MapTo DatabaseSetting err: %v, err)}
}
在这里我们做了如下几件事
编写与配置项保持一致的结构体App、Server、Database使用 MapTo 将配置项映射到结构体上对一些需特殊设置的配置项进行再赋值
修改models.go 将init函数改为Setup方法,将独立读取的DB配置项删除,改为统一读取setting
package modelsimport (
...
)// 定义一个全局的数据库连接变量
var db *gorm.DB// Model 设定常用结构体可以作为匿名结构体嵌入到别的表格对应的结构体
type Model struct {ID int gorm:primary_key json:idCreatedOn int json:created_onModifiedOn int json:modified_onDeletedOn int json:deleted_on
}func Setup() {var err error//使用gorm框架初始化数据库连接db, err gorm.Open(setting.DatabaseSetting.Type, fmt.Sprintf(%s:%stcp(%s)/%s?charsetutf8parseTimeTruelocLocal,setting.DatabaseSetting.User,setting.DatabaseSetting.Password,setting.DatabaseSetting.Host,setting.DatabaseSetting.Name))if err ! nil {log.Println(err)}//自定义默认表的表名使用匿名函数在原默认表名的前面加上配置文件中定义的前缀gorm.DefaultTableNameHandler func(db *gorm.DB, defaultTableName string) string {return setting.DatabaseSetting.TablePrefix defaultTableName}//gorm默认使用复数映射当前设置后即进行严格匹配db.SingularTable(true)//log记录打开db.LogMode(true)//进行连接池设置db.DB().SetMaxIdleConns(10)db.DB().SetMaxOpenConns(100)//替换Create和Update回调函数db.Callback().Create().Replace(gorm:update_time_stamp, updateTimeStampForCreateCallback)db.Callback().Update().Replace(gorm:update_time_stamp, updateTimeStampForUpdateCallback)//添加删除的回调CallBacksdb.Callback().Delete().Replace(gorm:delete, deleteCallback)
}// CloseDB 与数据库断开连接函数
func CloseDB() {defer db.Close()
}// updateTimeStampForCreateCallback 在创建记录时设置 CreatedOn, ModifiedOn
func updateTimeStampForCreateCallback(scope *gorm.Scope) {...
}// updateTimeStampForUpdateCallback 在更新记录时设置 ModifyOn
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
...
}// 设定delete操作的callback逻辑
func deleteCallback(scope *gorm.Scope) {...
}// 判断是否为空来进行空格插入防止sql注入保证安全性
func addExtraSpaceIfExist(str string) string {...
}
修改log.go init函数改为Setup方法
func Setup() {//获取log文件目录filePath : getLogFileFullPath()//得到log文件句柄F openLogFile(filePath)//创建一个新的日志记录器logger log.New(F, DefaultPrefix, log.LstdFlags)
}修改pkg/logging/file.go 独立的 LOG 配置项删除改为统一读取 setting,修改这两个函数即可
// 返回log文件的前缀路径算是一个具有仪式感的函数
func getLogFilePath() string {return fmt.Sprintf(%s, setting.AppSetting.LogSavePath)
}// 获得log文件的整体路径以当前日期作为.log文件的名字
func getLogFileFullPath() string {prefixPath : getLogFilePath()suffixPath : fmt.Sprintf(%s%s.%s, setting.AppSetting.LogSaveName, time.Now().Format(setting.AppSetting.TimeFormat), setting.AppSetting.LogFileExt)return fmt.Sprintf(%s%s, prefixPath, suffixPath)
}
其他漏下的未改为统一读取setting的根据报错进行修改即可
验证
在这里为止针对本需求的配置优化就完毕了你需要执行 go run main.go 验证一下你的功能是否正常哦
抽离file
pkg目录下新建file/file.go
package fileimport (iomime/multipartospath
)// GetSize multipart.file用于处理HTTP请求中文件上传到类型 os.file则主要是本地文件的操作
func GetSize(f multipart.File) (int, error) {content, err : io.ReadAll(f)return len(content), err
}// GetExt 获取文件扩展名
func GetExt(filename string) string {return path.Ext(filename)
}// CheckExist 检查文件是否存在
func CheckExist(src string) bool {//os.stat用于获取文件的相关信息_, err : os.Stat(src)return os.IsNotExist(err)
}// CheckPermission 检查访问文件的权限
func CheckPermission(src string) bool {_, err : os.Stat(src)//检查是否有访问文件的权限return os.IsPermission(err)
}// IsNotExistMkDir 检查是否存在目录不存在则创建目录
func IsNotExistMkDir(src string) error {if notExist : CheckExist(src); notExist true {if err : MkDir(src); err ! nil {return err}}return nil
}// MkDir 创建目录
func MkDir(src string) error {err : os.MkdirAll(src, os.ModePerm) //权限0777权限拉满if err ! nil {return err}return nil
}// Open 算是简单包装os.openfile
func Open(name string, flag int, perm os.FileMode) (*os.File, error) {f, err : os.OpenFile(name, flag, perm)if err ! nil {return nil, err}return f, nil
}
在这里我们用到了 mime/multipart 包它主要实现了 MIME 的multipart解析主要适用于 HTTP 和常见浏览器生成的 multipart 主体
修改原logging包的方法
修改pkg/logging/file.go
package loggingimport (fmtgithub.com/kingsill/gin-example/pkg/filegithub.com/kingsill/gin-example/pkg/settingostime
)// 返回log文件的前缀路径算是一个具有仪式感的函数
func getLogFilePath() string {return fmt.Sprintf(%s, setting.AppSetting.LogSavePath)
}// 获得log文件的整体路径以当前日期作为.log文件的名字 runtime/log20010212.log
func getLogFileFullPath() string {prefixPath : getLogFilePath()suffixPath : fmt.Sprintf(%s%s.%s,setting.AppSetting.LogSaveName,time.Now().Format(setting.AppSetting.TimeFormat),setting.AppSetting.LogFileExt,)return fmt.Sprintf(%s%s, prefixPath, suffixPath)
}// 打开日志文件返回写入的句柄handle
func openLogFile() (*os.File, error) {//获取文件整体路径fileName : getLogFileFullPath()//创建目录mkDir()//如果.log文件不存在这里会创建一个handle, err : file.Open(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)if err ! nil {return nil, fmt.Errorf(fail to open:%s\n, fileName)}return handle, nil
}// 创建log目录
func mkDir() {//获得当前目录 dir: /home/wang2/gin-exampledir, _ : os.Getwd()//检查目录访问权限perm : file.CheckPermission(getLogFilePath())if perm true {panic(Permission denied)}//如果目录不存在创建目录err : file.IsNotExistMkDir(dir / getLogFilePath())if err ! nil {panic(err)}
}
修改pkg/logging/log.go 由于原方法传参有变这里也进行相关调整
...// Setup 自定义logger的初始化
func Setup() {var err error//得到log文件句柄F, err openLogFile()if err ! nil {log.Fatalln(err)}//创建一个新的日志记录器logger log.New(F, DefaultPrefix, log.LstdFlags)
}
...
实现上传图片接口
首先需要在 blog_article 中增加字段 cover_image_url格式为 varchar(255) DEFAULT COMMENT 封面图片地址
alter table blog_article add cover_image_url varchar(255) DEFAULT COMMENT 封面图片地址;图片名加密
我们通过 MD5 对图片进行加密防止图片名暴露 util目录下新建md5.go写入文件内容
package utilimport (crypto/md5encoding/hex
)// EncodeMD5 计算给定字符的MD5哈希值返回其十六进制表示
func EncodeMD5(value string) string {//创建一个新的MD5计算器实例m : md5.New()//将value写入到MD5计算器中m.Write([]byte(value))//nil表示计算完哈希值后不添加后缀return hex.EncodeToString(m.Sum(nil))
}
封装image的处理逻辑
在 pkg 目录下新建upload/image.go文件写入文件内容
这里基本是对底层代码的二次封装为了更灵活的处理一些图片特有的逻辑并且方便修改不直接对外暴露下层
package uploadimport (
...
)func GetImageFullUrl(name string) string {return setting.AppSetting.ImagePrefixUrl / GetImagePath() name
}// GetImageName 计算MD5加密之后的图片名
func GetImageName(name string) string {//将图片的名字剥离扩展名ext : path.Ext(name)fileName : strings.TrimSuffix(name, ext)//对单纯的图片名进行MD5加密fileName util.EncodeMD5(fileName)//将MD5加密后的图片名和后缀返回return fileName ext
}// GetImagePath 包装文件路径 upload/images/
func GetImagePath() string {return setting.AppSetting.ImageSavePath
}// GetImageFullPath 拼凑完整路径 runtime/upload/images/
func GetImageFullPath() string {return setting.AppSetting.RuntimeRootPath GetImagePath()
}// CheckImageExt 检查图片格式是否正确
func CheckImageExt(fileName string) bool {ext : file.GetExt(fileName)for _, allowExt : range setting.AppSetting.ImageAllowExts {//都大写进行对比if strings.ToUpper(allowExt) strings.ToUpper(ext) {return true}}return false
}// CheckImageSize 检查图片的大小是否小于规定的最大值 5M
func CheckImageSize(f multipart.File) bool {size, err : file.GetSize(f)if err ! nil {log.Println(err)logging.Warn(err)return false}return size setting.AppSetting.ImageMaxSize
}func CheckImage(src string) error {dir, err : os.Getwd()if err ! nil {return fmt.Errorf(os.Getwd err: %v, err)}//检查图片目录err file.IsNotExistMkDir(dir / src)if err ! nil {return fmt.Errorf(file.IsNotExistMkDir err: %v, err)}//检查访问权限perm : file.CheckPermission(src)if perm true {return fmt.Errorf(file.CheckPermission Permission denied src: %s, src)}return nil
}编写上传图片的业务逻辑
在 routers/api 目录下新建 upload.go 文件写入内容
package apiimport (
...
)func UploadImage(c *gin.Context) {code : e.SUCCESSdata : make(map[string]string)file, image, err : c.Request.FormFile(image)if err ! nil {logging.Warn(err)code e.ERRORc.JSON(http.StatusOK, gin.H{code: code,msg: e.GetMsg(code),data: data,})}if image nil {code e.INVALID_PARAMS} else {imageName : upload.GetImageName(image.Filename) //获取图片名fullPath : upload.GetImageFullPath() //图片完整路径savePath : upload.GetImagePath() //仓库内保存路径//图片路径名字src : fullPath imageName//检查图片格式和大小if !upload.CheckImageExt(imageName) || !upload.CheckImageSize(file) {code e.ERROR_UPLOAD_CHECK_IMAGE_FORMAT} else {//检查图片目录、访问权限err : upload.CheckImage(fullPath)if err ! nil {logging.Warn(err)code e.ERROR_UPLOAD_CHECK_IMAGE_FAIL} else if err : c.SaveUploadedFile(image, src); err ! nil { //图片保存到指定位置logging.Warn(err)code e.ERROR_UPLOAD_SAVE_IMAGE_FAIL} else {//data[image_url] upload.GetImageFullUrl(imageName)data[image_save_url] savePath imageName}}}c.JSON(http.StatusOK, gin.H{code: code,msg: e.GetMsg(code),data: data,})
}
在这一大段的业务逻辑中我们做了如下事情
c.Request.FormFile获取上传的图片返回提供的表单键的第一个文件CheckImageExt、CheckImageSize 检查图片大小检查图片后缀CheckImage检查上传图片所需权限、文件夹SaveUploadedFile保存图片 总的来说就是 入参 - 检查 -》 保存 的应用流程
增加图片上传的路由
打开 routers/router.go 文件增加路由 r.POST(/upload, api.UploadImage)
func InitRouter() *gin.Engine {r : gin.New()...r.GET(/auth, api.GetAuth)r.GET(/swagger/*any, ginSwagger.WrapHandler(swaggerFiles.Handler))r.POST(/upload, api.UploadImage)apiv1 : r.Group(/api/v1)apiv1.Use(jwt.JWT()){...}return r
}验证
使用 postman测试图片上传功能 看到runtime/upload/images下存在我们上传的文件
实现前端访问 http.FileServer
在完成了上一小节后我们还需要让前端能够访问到图片一般是如下
CDNhttp.FileSystem
在公司的话CDN 或自建分布式文件系统居多也不需要过多关注。而在实践里的话肯定是本地搭建了Go 本身对此就有很好的支持而 Gin 更是再封装了一层只需要在路由增加一行代码即可
r.StaticFS
打开 routers/router.go 文件增加路由 r.StaticFS(/upload/images, http.Dir(upload.GetImageFullPath()))
func InitRouter() *gin.Engine {...//网页 请求我们指定目录内的内容r.StaticFS(/upload/images, http.Dir(upload.GetImageFullPath()))r.GET(/auth, api.GetAuth)r.GET(/swagger/*any, ginSwagger.WrapHandler(swaggerFiles.Handler))r.POST(/upload, api.UploadImage)...
}http.dir 创建了文件系统将 /upload/image 路径映射到我们指定的文件目录中这里为 runtime/upload/images/ 即我们放置图片的文件夹下
更多内容可以查看源码进行学习到这里可以自行进行验证访问 127.0.0.18000/upload/images/图片名
修改文章接口
新增、更新文章接口
支持入参 cover_image_url、增加对cover_image_url的非空、最长长度的检验
修改 models/article.go
...// Article 建立对应article表的struct结构体方便进行信息读写
type Article struct {
...CoverImageUrl string json:cover_image_url
}// AddArticle 添加文章
func AddArticle(data map[string]interface{}) bool {db.Create(Article{
...CoverImageUrl: data[cover_image_url].(string),})return true
}
...
修改 routers/api/v1/article.go 对 AddArticle 和 EditArticle 方法在原来的基础上进行修改首先将之前为了方便验证写的使用 查询参数 改为 表单参数 更安全
// Summary 新增文章
// Produce json
// Param tagId body int true tagId
// Param title body string true title
// Param desc body string true desc
// Param content body string true content
// Param createdBy body string true createdBy
// Param state body int true state
// Success 200 {string} json {code:200,data:{},msg:ok}
// Router /api/v1/tags [post]
func AddArticle(c *gin.Context) {tagId : com.StrTo(c.PostForm(tag_id)).MustInt()title : c.PostForm(title)desc : c.PostForm(desc)content : c.PostForm(content)createdBy : c.PostForm(created_by)coverImageUrl : c.PostForm(cover_image_url)//**********state : com.StrTo(c.DefaultQuery(state, 0)).MustInt()valid : validation.Validation{}valid.Min(tagId, 1, tag_id).Message(标签ID必须大于0)valid.Required(title, title).Message(标题不能为空)valid.Required(desc, desc).Message(简述不能为空)valid.Required(content, content).Message(内容不能为空)valid.Required(createdBy, created_by).Message(创建人不能为空)valid.Range(state, 0, 1, state).Message(状态只允许0或1)valid.Required(coverImageUrl, cover_image_url).Message(封面地址不能为空)//***********code : e.INVALID_PARAMSif !valid.HasErrors() {if models.ExistTagByID(tagId) {data : make(map[string]interface{})data[tag_id] tagIddata[title] titledata[desc] descdata[content] contentdata[created_by] createdBydata[state] statedata[cover_image_url] coverImageUrl//****************models.AddArticle(data)code e.SUCCESS} else {code e.ERROR_NOT_EXIST_TAG}} else {for _, err : range valid.Errors {logging.Info(err.key: %s, err.message: %s, err.Key, err.Message)}}c.JSON(http.StatusOK, gin.H{code: code,msg: e.GetMsg(code),data: make(map[string]interface{}),})
}// Summary 修改文章
// Produce json
// Param id path int true id
// Param tagId body int true tagId
// Param title body string true title
// Param desc body string true desc
// Param content body string true content
// Param modifiedBy body string true modifiedBy
// Param state body int false state
// Success 200 {string} json {code:200,data:{},msg:ok}
// Router /api/v1/tags [post]
func EditArticle(c *gin.Context) {valid : validation.Validation{}id : com.StrTo(c.Param(id)).MustInt()tagId : com.StrTo(c.PostForm(tag_id)).MustInt()title : c.PostForm(title)desc : c.PostForm(desc)content : c.PostForm(content)coverImageUrl : c.PostForm(cover_image_url)//******modifiedBy : c.PostForm(modified_by)var state int -1if arg : c.Query(state); arg ! {state com.StrTo(arg).MustInt()valid.Range(state, 0, 1, state).Message(状态只允许0或1)}valid.Min(id, 1, id).Message(ID必须大于0)valid.Min(tagId, 1, tag_id).Message(标签ID必须大于0)valid.MaxSize(title, 100, title).Message(标题最长为100字符)valid.Required(title, title).Message(标题不能为空)valid.MaxSize(desc, 255, desc).Message(简述最长为255字符)valid.Required(desc, desc).Message(简述不能为空)valid.MaxSize(content, 65535, content).Message(内容最长为65535字符)valid.Required(modifiedBy, modified_by).Message(修改人不能为空)valid.Required(coverImageUrl, cover_image_url).Message(封面地址不能为空)//****************valid.MaxSize(coverImageUrl, 255, cover_image_url).Message(封面地址最长为255字符)//*************valid.MaxSize(modifiedBy, 100, modified_by).Message(修改人最长为100字符)code : e.INVALID_PARAMSif !valid.HasErrors() {if models.ExistArticleByID(id) {if models.ExistTagByID(tagId) {data : make(map[string]interface{})data[tag_id] tagIddata[title] titledata[desc] descdata[content] contentdata[modified_by] modifiedBymodels.EditArticle(id, data)code e.SUCCESS} else {code e.ERROR_NOT_EXIST_TAG}} else {code e.ERROR_NOT_EXIST_ARTICLE}} else {for _, err : range valid.Errors {logging.Info(err.key: %s, err.message: %s, err.Key, err.Message)}}c.JSON(http.StatusOK, gin.H{code: code,msg: e.GetMsg(code),data: make(map[string]string),})
}
接下来进行验证即可