Go Web工程

下面是项目的包图,可以通过包图来理清项目包的结构。

Go Web工程

下面是项目的包图,可以通过包图来理清项目包的结构。

因为我是从Java转过来的,其实这种包的结构与Java的类似。Java是Controller、Service、Respository。

Go就变成了 api、service、dao, 其实也差不多,因为Go设计思想跟Java的区别还是很大,但本质还是通过架构来解耦

路由配置

建立 routes/router.go 文件。 此文件用来配置api的URI,配置路由、路由分组、添加中间件。

func InitRouter() http.Handler {

    r := gin.New()
// 设置可信代理
r.SetTrustedProxies([]string{"*"})
// 设置静态文件目录
r.Use(middleware.Logger()) // 自定义的 zap 日志中间件
r.Use(middleware.ErrorRecovery(false)) // 自定义错误处理中间件
r.Use(middleware.Cors()) // 跨域中间件 // 需要鉴权的接口
auth := base.Group("") // "/admin"
// 需要鉴权的接口
auth.Use(middleware.JWTAuth()) // JWT 鉴权中间件
auth.Use(middleware.RBAC()) // casbin 权限中间件
auth.Use(middleware.ListenOnline()) // 监听在线用户
auth.Use(middleware.OperationLog()) // 记录操作日志 // 路由配置
page := auth.Group("/page")
{
page.GET("/list", pageAPI.GetList) // 页面列表
page.POST("", pageAPI.SaveOrUpdate) // 新增/编辑页面
page.DELETE("", pageAPI.Delete) // 删除页面
} return r
}

数据库配置

func InitMySQLDB() *gorm.DB {
mysqlCfg := config.Cfg.Mysql
dns := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
mysqlCfg.Username,
mysqlCfg.Password,
mysqlCfg.Host,
mysqlCfg.Port,
mysqlCfg.Dbname,
) db, err := gorm.Open(mysql.Open(dns), gormConfig()) if err != nil {
log.Fatal("MySQL 连接失败, 请检查参数")
} log.Println("MySQL 连接成功") // 迁移数据表,在没有数据表结构变更时候,建议注释不执行
// MakeMigrate(db) sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10) // 设置连接池中的最大闲置连接
sqlDB.SetMaxOpenConns(100) // 设置数据库的最大连接数量
sqlDB.SetConnMaxLifetime(10 * time.Second) // 设置连接的最大可复用时间 return db
} // gorm 配置
func gormConfig() *gorm.Config {
return &gorm.Config{
// gorm 日志模式
Logger: logger.Default.LogMode(getLogMode(config.Cfg.Mysql.LogMode)),
// 禁用外键约束
DisableForeignKeyConstraintWhenMigrating: true,
// 禁用默认事务(提高运行速度)
SkipDefaultTransaction: true,
NamingStrategy: schema.NamingStrategy{
// 使用单数表名,启用该选项,此时,`User` 的表名应该是 `user`
SingularTable: true,
},
}
}

编码规范

这个主要是看人家怎么写的接口,怎么调用业务层。分析一下。

Api

routes/router.go

	auth.GET("/home", blogInfoAPI.GetHomeInfo) // 后台首页信息

注:这里的API层非常的简单,只是对于 Service的一个调用

api/v1/home.go

func (*BlogInfo) GetHomeInfo(c *gin.Context) {
r.SuccessData(c, blogInfoService.GetHomeInfo())
}

RESTFul 返回操作

utils\r\result.go*gin.Context 实现了返回的方法,因为使用的是指针,所以不需要返回值,可以直接进行传递函数调用。

// 返回 JSON 数据
func ReturnJson(c *gin.Context, httpCode, code int, msg string, data any) {
// c.Header("", "") // 根据需要在头部添加其他信息
c.JSON(httpCode, Response{
Code: code,
Message: msg,
Data: data,
})
}
// 语法糖函数封装 // 自定义 httpCode, code, data
func Send(c *gin.Context, httpCode, code int, data any) {
ReturnJson(c, httpCode, code, GetMsg(code), data)
} // 自动根据 code 获取 message, 且 data == nil
func SendCode(c *gin.Context, code int) {
Send(c, http.StatusOK, code, nil)
} // 自动根据 code 获取 message, 且 data != nil
func SendData(c *gin.Context, code int, data any) {
Send(c, http.StatusOK, code, data)
} func SuccessData(c *gin.Context, data any) {
Send(c, http.StatusOK, OK, data)
} func Success(c *gin.Context) {
Send(c, http.StatusOK, OK, nil)
}

Service

因为在Api内调用的Service服务,返回的都是需要反序列化为Json的View Object,这个VO放在Model内,用于在持久层查询的Model也放在model里。感觉有点混乱,看可不可能把VO放在 service包里。

VO的目的就是把格式变成符合URI业务需求的一个结构体。把一些数据放到里面。

// 根据 [文章id] 获取 [文章详情]
func (*Article) GetInfo(id int) resp.ArticleDetailVO {
article := dao.GetOne(model.Article{}, "id", id)
category := dao.GetOne(model.Category{}, "id", article.CategoryId)
tagNames := tagDao.GetTagNamesByArtId(id) articleVo := utils.CopyProperties[resp.ArticleDetailVO](article)
// 前端 category 为 '' 不显示 placeholder, 为 null 显示 placeholder
if category.ID != 0 {
articleVo.CategoryName = &category.Name
}
articleVo.TagNames = tagNames
return articleVo
}

Dao

Dao层的所有方法或函数

注:

  1. 不具有普适性的函数功能,应变成某个结构体的方法,变成方法后仅能其结构体初始化的变量调用
  2. 反之,则可以使用泛型

如何给 Dao包全局变量 dao.DB 赋值。

dao包下,所有的使用到DB变量的go代码文件内,直接声明var DB *gorm.DB全局变量 。

route初始函数里,对dao包的DB变量 也就是 dao.DB 进行赋值,赋的值,也就是关于初始化的gorm.DB

routes/router.go 初始化全局变量

// 初始化全局变量
func InitGlobalVariable() {
// 初始化 Viper
utils.InitViper()
// 初始化 Logger
utils.InitLogger()
// 初始化数据库 DB
dao.DB = utils.InitMySQLDB() // 需要先导入 gvb.sql
// dao.DB = utils.InitSQLiteDB("gorm.db") // TODO: 默认无数据,暂时无法使用
// 初始化 Redis
utils.InitRedis()
// 初始化 Casbin
utils.InitCasbin(dao.DB)
}

基于泛型的普适型写法

这样,其使用Model进行创建的时候,就不需要指定类型了。非常方便用于简单的业务。如果很多业务的CURD都有共同特性,那么也可以如此。

func Create[T any](data *T) {
err := DB.Create(&data).Error
if err != nil {
panic(err)
}
}

针对特定结构体的写法

Views Object居然也被用于在Model进行查询,这个我感觉没必要。因为实体结构体与视力结构体应有所区分。

func (*Article) GetInfoById(id int) (res resp.FrontArticleDetailVO) {
DB.Table("article").
Preload("Category").
Preload("Tags").
Where("id = ? AND is_delete = 0 AND status = 1", id).
First(&res)
return
}

中间件

鉴权的原理就是,拦截一条线路,然后判断每次请求访问此条线路的权限。

根据我们开发时,我们的开发习惯是,基于一个原始URI为一组路由,这个原始URI就是我们鉴权的根据点。

例如 需要检查 /sys下所有的接口是否已登录,则 与/sys/** 匹配都会被进行鉴权。

就是钩子,一层一层的拦截


// 需要鉴权的接口
auth := base.Group("") // "/admin"
// !注意使用中间件的顺序
auth.Use(middleware.JWTAuth()) // JWT 鉴权中间件
auth.Use(middleware.RBAC()) // casbin 权限中间件
auth.Use(middleware.ListenOnline()) // 监听在线用户
auth.Use(middleware.OperationLog()) // 记录操作日志

Cors 跨域处理

这个就是使用别人写好的中间件,地址在: cors

cros.go

// 跨域中间件
func Cors() gin.HandlerFunc {
return cors.New(cors.Config{
// 允许跨域请求网站
AllowOrigins: []string{"*"},
// 允许使用的请求方式
AllowMethods: []string{"PUT", "POST", "GET", "DELETE", "OPTIONS", "PATCH"},
// 允许使用的请求头
AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "X-Requested-With"},
// 暴露的请求头
ExposeHeaders: []string{"Content-Type"},
// 凭证共享
AllowCredentials: true,
// 允许跨域的源网站
AllowOriginFunc: func(origin string) bool {
return true
},
// 超时时间设定
MaxAge: 24 * time.Hour,
})
}

Middleware 小结

Go语言里,函数也是一个值,可以直接转送匿名函数作为值。

中间件的写法就是

middleware/XXX.go

func XXX() gin.HandlerFunc {
return func(c *gin.Context) {
// anything else
}
}

将XXX挂在哪个基础的URI上。

routes/router.go

sys := router.Group("/sys")
sys.Use(middleware.XXX())

模板技术

渲染这是一个常用的技术。


// 指定导入模板的目录
router.LoadHTMLGlob("template/*")

测试模板渲染的代码

router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是测试", "ce": "123456"})
})

模板代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{.title}}</title>
</head>
<body>
Number: <h1>{{.ce}}</h1>
</body>
</html>

头尾分离,模块化写法

注 : 需要提前定义好 public/header,public/footer

Gin HTML 渲染模板:https://www.topgoer.cn/docs/ginkuangjia/ginhtmlxuanran

{{ define "user/index.html" }}
{{template "public/header" .}}
Address: {{.address}}
{{template "public/footer" .}}
{{ end }}

因为我是从Java转过来的,其实这种包的结构与Java的类似。Java是Controller、Service、Respository。

Go就变成了 api、service、dao, 其实也差不多,因为Go设计思想跟Java的区别还是很大,但本质还是通过架构来解耦

路由配置

建立 routes/router.go 文件。 此文件用来配置api的URI,配置路由、路由分组、添加中间件。

func InitRouter() http.Handler {

    r := gin.New()
// 设置可信代理
r.SetTrustedProxies([]string{"*"})
// 设置静态文件目录
r.Use(middleware.Logger()) // 自定义的 zap 日志中间件
r.Use(middleware.ErrorRecovery(false)) // 自定义错误处理中间件
r.Use(middleware.Cors()) // 跨域中间件 // 需要鉴权的接口
auth := base.Group("") // "/admin"
// 需要鉴权的接口
auth.Use(middleware.JWTAuth()) // JWT 鉴权中间件
auth.Use(middleware.RBAC()) // casbin 权限中间件
auth.Use(middleware.ListenOnline()) // 监听在线用户
auth.Use(middleware.OperationLog()) // 记录操作日志 // 路由配置
page := auth.Group("/page")
{
page.GET("/list", pageAPI.GetList) // 页面列表
page.POST("", pageAPI.SaveOrUpdate) // 新增/编辑页面
page.DELETE("", pageAPI.Delete) // 删除页面
} return r
}

数据库配置

func InitMySQLDB() *gorm.DB {
mysqlCfg := config.Cfg.Mysql
dns := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
mysqlCfg.Username,
mysqlCfg.Password,
mysqlCfg.Host,
mysqlCfg.Port,
mysqlCfg.Dbname,
) db, err := gorm.Open(mysql.Open(dns), gormConfig()) if err != nil {
log.Fatal("MySQL 连接失败, 请检查参数")
} log.Println("MySQL 连接成功") // 迁移数据表,在没有数据表结构变更时候,建议注释不执行
// MakeMigrate(db) sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10) // 设置连接池中的最大闲置连接
sqlDB.SetMaxOpenConns(100) // 设置数据库的最大连接数量
sqlDB.SetConnMaxLifetime(10 * time.Second) // 设置连接的最大可复用时间 return db
} // gorm 配置
func gormConfig() *gorm.Config {
return &gorm.Config{
// gorm 日志模式
Logger: logger.Default.LogMode(getLogMode(config.Cfg.Mysql.LogMode)),
// 禁用外键约束
DisableForeignKeyConstraintWhenMigrating: true,
// 禁用默认事务(提高运行速度)
SkipDefaultTransaction: true,
NamingStrategy: schema.NamingStrategy{
// 使用单数表名,启用该选项,此时,`User` 的表名应该是 `user`
SingularTable: true,
},
}
}

编码规范

这个主要是看人家怎么写的接口,怎么调用业务层。分析一下。

Api

routes/router.go

	auth.GET("/home", blogInfoAPI.GetHomeInfo) // 后台首页信息

注:这里的API层非常的简单,只是对于 Service的一个调用

api/v1/home.go

func (*BlogInfo) GetHomeInfo(c *gin.Context) {
r.SuccessData(c, blogInfoService.GetHomeInfo())
}

RESTFul 返回操作

utils\r\result.go*gin.Context 实现了返回的方法,因为使用的是指针,所以不需要返回值,可以直接进行传递函数调用。

// 返回 JSON 数据
func ReturnJson(c *gin.Context, httpCode, code int, msg string, data any) {
// c.Header("", "") // 根据需要在头部添加其他信息
c.JSON(httpCode, Response{
Code: code,
Message: msg,
Data: data,
})
}
// 语法糖函数封装 // 自定义 httpCode, code, data
func Send(c *gin.Context, httpCode, code int, data any) {
ReturnJson(c, httpCode, code, GetMsg(code), data)
} // 自动根据 code 获取 message, 且 data == nil
func SendCode(c *gin.Context, code int) {
Send(c, http.StatusOK, code, nil)
} // 自动根据 code 获取 message, 且 data != nil
func SendData(c *gin.Context, code int, data any) {
Send(c, http.StatusOK, code, data)
} func SuccessData(c *gin.Context, data any) {
Send(c, http.StatusOK, OK, data)
} func Success(c *gin.Context) {
Send(c, http.StatusOK, OK, nil)
}

Service

因为在Api内调用的Service服务,返回的都是需要反序列化为Json的View Object,这个VO放在Model内,用于在持久层查询的Model也放在model里。感觉有点混乱,看可不可能把VO放在 service包里。

VO的目的就是把格式变成符合URI业务需求的一个结构体。把一些数据放到里面。

// 根据 [文章id] 获取 [文章详情]
func (*Article) GetInfo(id int) resp.ArticleDetailVO {
article := dao.GetOne(model.Article{}, "id", id)
category := dao.GetOne(model.Category{}, "id", article.CategoryId)
tagNames := tagDao.GetTagNamesByArtId(id) articleVo := utils.CopyProperties[resp.ArticleDetailVO](article)
// 前端 category 为 '' 不显示 placeholder, 为 null 显示 placeholder
if category.ID != 0 {
articleVo.CategoryName = &category.Name
}
articleVo.TagNames = tagNames
return articleVo
}

Dao

Dao层的所有方法或函数

注:

  1. 不具有普适性的函数功能,应变成某个结构体的方法,变成方法后仅能其结构体初始化的变量调用
  2. 反之,则可以使用泛型

如何给 Dao包全局变量 dao.DB 赋值。

dao包下,所有的使用到DB变量的go代码文件内,直接声明var DB *gorm.DB全局变量 。

route初始函数里,对dao包的DB变量 也就是 dao.DB 进行赋值,赋的值,也就是关于初始化的gorm.DB

routes/router.go 初始化全局变量

// 初始化全局变量
func InitGlobalVariable() {
// 初始化 Viper
utils.InitViper()
// 初始化 Logger
utils.InitLogger()
// 初始化数据库 DB
dao.DB = utils.InitMySQLDB() // 需要先导入 gvb.sql
// dao.DB = utils.InitSQLiteDB("gorm.db") // TODO: 默认无数据,暂时无法使用
// 初始化 Redis
utils.InitRedis()
// 初始化 Casbin
utils.InitCasbin(dao.DB)
}

基于泛型的普适型写法

这样,其使用Model进行创建的时候,就不需要指定类型了。非常方便用于简单的业务。如果很多业务的CURD都有共同特性,那么也可以如此。

func Create[T any](data *T) {
err := DB.Create(&data).Error
if err != nil {
panic(err)
}
}

针对特定结构体的写法

Views Object居然也被用于在Model进行查询,这个我感觉没必要。因为实体结构体与视力结构体应有所区分。

func (*Article) GetInfoById(id int) (res resp.FrontArticleDetailVO) {
DB.Table("article").
Preload("Category").
Preload("Tags").
Where("id = ? AND is_delete = 0 AND status = 1", id).
First(&res)
return
}

中间件

鉴权的原理就是,拦截一条线路,然后判断每次请求访问此条线路的权限。

根据我们开发时,我们的开发习惯是,基于一个原始URI为一组路由,这个原始URI就是我们鉴权的根据点。

例如 需要检查 /sys下所有的接口是否已登录,则 与/sys/** 匹配都会被进行鉴权。

就是钩子,一层一层的拦截


// 需要鉴权的接口
auth := base.Group("") // "/admin"
// !注意使用中间件的顺序
auth.Use(middleware.JWTAuth()) // JWT 鉴权中间件
auth.Use(middleware.RBAC()) // casbin 权限中间件
auth.Use(middleware.ListenOnline()) // 监听在线用户
auth.Use(middleware.OperationLog()) // 记录操作日志

Cors 跨域处理

这个就是使用别人写好的中间件,地址在: cors

cros.go

// 跨域中间件
func Cors() gin.HandlerFunc {
return cors.New(cors.Config{
// 允许跨域请求网站
AllowOrigins: []string{"*"},
// 允许使用的请求方式
AllowMethods: []string{"PUT", "POST", "GET", "DELETE", "OPTIONS", "PATCH"},
// 允许使用的请求头
AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "X-Requested-With"},
// 暴露的请求头
ExposeHeaders: []string{"Content-Type"},
// 凭证共享
AllowCredentials: true,
// 允许跨域的源网站
AllowOriginFunc: func(origin string) bool {
return true
},
// 超时时间设定
MaxAge: 24 * time.Hour,
})
}

Middleware 小结

Go语言里,函数也是一个值,可以直接转送匿名函数作为值。

中间件的写法就是

middleware/XXX.go

func XXX() gin.HandlerFunc {
return func(c *gin.Context) {
// anything else
}
}

将XXX挂在哪个基础的URI上。

routes/router.go

sys := router.Group("/sys")
sys.Use(middleware.XXX())

模板技术

渲染这是一个常用的技术。


// 指定导入模板的目录
router.LoadHTMLGlob("template/*")

测试模板渲染的代码

router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是测试", "ce": "123456"})
})

模板代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{.title}}</title>
</head>
<body>
Number: <h1>{{.ce}}</h1>
</body>
</html>

头尾分离,模块化写法

注 : 需要提前定义好 public/header,public/footer

Gin HTML 渲染模板:https://www.topgoer.cn/docs/ginkuangjia/ginhtmlxuanran

{{ define "user/index.html" }}
{{template "public/header" .}}
Address: {{.address}}
{{template "public/footer" .}}
{{ end }}

致谢:

https://github.com/szluyu99/gin-vue-blog

GIN

GORM

Go Web项目结构 + 基础代码的更多相关文章

  1. 使用maven构建基本的web项目结构

    由于当前公司在组织进行项目基本结构的整理,将以前通过eclipse/ ant 方式构建的项目向maven上迁移,于是便进行maven项目方面的调研. 对于maven项目,基本的结构已经在标准文件中: ...

  2. Java Web项目结构

    Java Web项目结构(一般) 1.Java src 2.JRE System Library 3.Java EE 6 Libraries 4.Web App Libraries 5.WebRoot ...

  3. 做web项目时对代码改动后浏览器端不生效的应对方法(持续更新)

    做web项目时,常常会遇到改动了代码,但浏览器端没有生效,原因是多种多样的,我会依据我遇到的情况逐步更新解决的方法 1.执行的时候採用debug模式,普通情况下使用项目部署button右边那个butt ...

  4. 做web项目时对代码修改后浏览器端不生效的应对方法(持续更新)

    做web项目时,经常会遇到修改了代码,但浏览器端没有生效,原因是多种多样的,我会根据我遇到的情况逐步更新解决办法 1.运行的时候采用debug模式,一般情况下使用项目部署按钮右边那个按钮下的tomca ...

  5. VS2015 ASP.NET5 Web项目结构浅析

    前言 本文个人同步博客地址http://aehyok.com/Blog/Detail/76.html 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本 ...

  6. vs2017更新后web项目部分后台代码类没有颜色,也没有自动提示输入功能

    vs2017有的版本更新后默认.net framework框架是.net framework4.6.1,将项目的.net framework框架更改为4.6.1,颜色和自动提示出现

  7. IDEA Tomcat Web项目修改了代码,重新部署页面没改变

    今天被IDEA坑的不浅直接说一下问题: 这是html页面不管我怎么修改重启服务器在浏览器中还是一点都不变化,甚至把一些内容都删了都没有变化,target可执行文件是最新的没问题,找了点资料发现是浏览器 ...

  8. node web项目结构

  9. java web 项目中基础技术

    1. 选择版本控制器(git, svn) 2. 用户登录的时候, 你需要进行认证, 权限受理 可以使用 spring shiro 框架,进行上面的工作 3. 过滤器(filter),监听器(liste ...

  10. 主要介绍JavaEE中Maven Web 项目的结构及其它几个小问题

    先说下本篇随笔的目录. 1.介绍windows中环境变量Path与ClassPath的区别. 2.可能导致命令行运行javac编译成功,但 java命令 + 所要执行的类的类名 无效的原因. 3.介绍 ...

随机推荐

  1. 树莓派上使用docker部署aria2,minidlna

    目前在树莓派上安装aria2跟minidlna能搜到的教程基本上都是直接apt-get install安装的.现在是docker的时代了,其实这2个东西可以直接使用docker run跑起来.有什么问 ...

  2. python 环境下使用PIP 报错的解决方法

    最近做一个小程序项目,使用djangorestframework,安装restframework 出现错误,安装环境Python2.7:出现错误如下:  "UnicodeEncodeErro ...

  3. AntV L7 快速入门示例

    1. 引言 L7 地理空间数据可视分析引擎是一种基于 WebGL 技术的地理空间数据可视化引擎,可以用于实现各种地理空间数据可视化应用.L7 引擎支持多种数据源和数据格式,包括 GeoJSON.CSV ...

  4. 3. docker的实践玩法

    1. docker的进程架构 docker服务进程:就是针对docker服务的命令,启动,重启 接口:通过参数指定容器的IP和端口,实现对容器的远程操作 客户端命令行:对docker的操作命令 最后学 ...

  5. Hello Welcome to my blog!

    Hello Welcome to my blog!

  6. Prometheus-2:blackbox_exporter黑盒监控

    黑盒监控blackbox_exporter 前边介绍有很多exporter可以直接将metrics暴露给Prometheus进行监控,这些称为"白盒监控",那些exporter无法 ...

  7. Spring原理之web.xml加载过程

    web.xml是部署描述文件,它不是Spring所特有的,而是在Servlet规范中定义的,是web应用的配置文件.web.xml主要是用来配置欢迎页.servlet.filter.listener等 ...

  8. Kali Sublist3r 报错解决办法

    直接将Sublist3r.py中文件的内容替换为下面的即可 具体的更改的东西改了很多地方就不细说了,直接复制粘贴 如果遇到Error: Virustotal probably now is block ...

  9. 即构✖叮咚课堂:行业第一套AI课堂解决方案是怎么被实现的?

    AI走进教育,是传统教育的一次迭代进化 在教育问题上,我们看到两类话题最容易引发公众讨论:教育公平和个性化教育,"互联网+教育"有可能解决第一类话题,"AI教育" ...

  10. influxdb 保留策略

    转载请注明出处: InfluxDB 中的保留策略用于定义时间序列数据在数据库中的保留期限.保留策略决定了数据在 InfluxDB 中的存储持续时间和精度.以下是 InfluxDB 的保留策略类型以及如 ...