后端笔记之gin框架学习

gin框架学习

    • 1. 使用脚手架搭建gin框架
    • 2. 应用框架
    • 3. 路由管理
    • 4.自定义中间件的使用
    • 5. 通过中间件设置路由权限校验
      • 1. 自定义校验
      • 2. 配置跨域
      • 3. 使用jwt进行tokn校验
    • 6. 接口入参获取和绑定
      • 2. 参数校验
      • 3. protobuf
    • 7. 集成mysql数据库
      • 1. gorm使用

1. 使用脚手架搭建gin框架

gin框架推荐使用 go mod 来管理依赖,所以使用 go mod 获取 gin

在 $GOPATH/src 下新建一个 gin(项目包名,自定义)文件夹,进入其中后,执行命令: go get -u github.com/gin-gonic/gin 即可自动下载依赖,等到依赖下载完成之后,在当前目录执行 go mod tidy 即可检查依赖完整性。

至此一个 gin 框架就搭建完成了。

2. 应用框架

在 gin 文件夹下建立一个 main.go 文件,编写 main 函数引入 gin 引擎。

        package mainimport ("github.com/gin-gonic/gin")func main() {// 获取 gin 引擎ginEngine := gin.Default()// 设置一个 get 路由,以及 handle 方式ginEngine.GET("/ping", func(c *gin.Context) {c.JSON(200, gin.H{"message": "pong",})})// 运行 gin 进程, 默认 8080 端口ginEngine.Run()}

至此,这个框架已经可以启动了。

3. 路由管理

在 gin 目录下新建一个包,包名通常为 router

router 下的一个 go 文件通常代表一个路由分组,例如下面是个无权限白名单的接口分组示例:

新建 public.go 文件

        package routerimport ("gin/web""github.com/gin-gonic/gin")func InitPublicApi(ginEngine *gin.Engine) {// 分组, ginEngine的Group是设置根目录 apiapi := ginEngine.Group("/api")// 设置次级目录 v1v1 := api.Group("/v1")// 设置 v1 下的子路径,假使这个路径是获取验证码,第二个参数就是具体的处理方式v1.GET("/verificationCode", web.VerificationCode)// 设置一个登录的 POST 路径v1.POST("/login", web.Login)}

同时 gin 目录下新建 web 文件夹,内部搭配新建 public.go 文件,然后用于处理路由指引过来的业务逻辑

        package webimport ("fmt""github.com/gin-gonic/gin")func VerificationCode(context *gin.Context) {fmt.Println("生成了一个验证码")}func Login(context *gin.Context) {fmt.Println("处理了登录逻辑")}

在 router 下新建 routers.go 文件,在这里把所有的路由都创建出来

        package routerimport "github.com/gin-gonic/gin"func InitRouters(ginEngine *gin.Engine) {InitPublicApi(ginEngine)InitUserApi(ginEngine)}

然后在 main 函数中调用即可

        package mainimport ("github.com/gin-gonic/gin")func main() {// 获取 gin 引擎ginEngine := gin.Default()// 创建所有的路由router.InitRouters(ginEngine)// 运行 gin 进程, 默认 8080 端口,但是可以修改为任意的空闲端口,以下改变为了8081ginEngine.Run(":8081")}

4.自定义中间件的使用

在gin框架中,我们所有要对公共的处理都可以使用中间件来实现,所谓的中间件就是通过函数作为参数在完成本函数之前去额外做一些操作,由于。

首先现在gin目录下新建middleware文件夹,然后新建 myHandler.go 文件。

        package middlewareimport ("fmt""github.com/gin-gonic/gin")// MyHandler 这边自定义一个中间件,打印请求路径和方式func MyHandler() gin.HandlerFunc {// 返回一个匿名函数return func(context *gin.Context) {path := context.FullPath()       // 获取完整的请求连接method := context.Request.Method // 获取请求方式类型fmt.Printf("myHandler, path:%s, method: %s\n", path, method)context.Next()}}

然后再 main 函数中引入使用

       func main() {// 获取 gin 引擎ginEngine := gin.Default()// 中间件使用,用ginEngine.useginEngine.Use(middleware.MyHandler())// 创建整个路由router.InitRouters(ginEngine)// 运行 gin 进程, 默认 8080 端口, 可以改变为任意空闲端口ginEngine.Run(":8081")}

当我们再次请求后台时,路径和方式就被打印出来了
在这里插入图片描述

5. 通过中间件设置路由权限校验

1. 自定义校验

在 middleware 文件夹下新建 token.go 文件,用来作为校验 token 的中间件

        package middlewareimport ("errors""github.com/gin-gonic/gin""net/http")var token = "123456"func TokenCheck(context *gin.Context) {// 从请求头中获取tokenaccessToken := context.GetHeader("access_token")// 进行token校验,在正规流程中,这里应该拿着token做完加解密之后,通过jwt获取存于缓存或者redis中的用户信息,并把用户信息写入请求之中// 这里模拟上述操作,仅仅是比较一下token后,塞入用户信息if accessToken != token {context.JSON(http.StatusInternalServerError, gin.H{"message": "token 校验失败",})// 防止此处理继续下去// context.Abort()// 防止此处理继续下去并报错context.AbortWithError(http.StatusInternalServerError, errors.New("token checker fail"))}// 如果校验成功之后,那么将要通过jwt根绝token获取到用户信息,加入请求中context.Set("userId", "123456789")context.Set("userName", "sue")context.Next()}

然后再需要使用 token 校验的路由上配置

        package routerimport ("fmt""gin/middleware""github.com/gin-gonic/gin")func InitUserApi(ginEngine *gin.Engine) {// 在这里配置上需要校验权限的路由的中间件user := ginEngine.Group("/user", middleware.TokenCheck)v1 := user.Group("/v1")//路径传参v1.GET("/detail/:id", func(context *gin.Context) {// 可以通过 Param 获取到参数值id := context.Param("id")// 在这里获取中间件中塞入的信息userId = context.getString("userId")fmt.Println(userId)context.String(200, "ID is %s", id)})}

2. 配置跨域

使用 go get -u github.com/gin-contrib/cors下载跨域依赖包,然后编写中间件

    func Cors() gin.HandlerFunc {return cors.New{AllowAllOrigins: true,AllowHeaders: []string{"Origin", "Content-Length", "Content-Type",},AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "HEAD", "OPTIONS"}}}

然后再 router 最根部使用

     	package routerimport ("github.com/gin-gonic/gin""gin/middleware"func InitRouters(ginEngine *gin.Engine) {api := ginEngine.Group("/api")api.Use(middleware.Cors())InitPublicApi(api)InitUserApi(api)}

3. 使用jwt进行tokn校验

先要去下载 jwt 依赖,使用go get -u github.com/golang-jwt/jwt/v5下载。

之后中间件包下,新建 jwt.go

        package middlewareimport "github.com/golang-jwt/jwt/v5"// 秘钥var key = "abcdefg123456"type Data struct {Id                   stringName                 stringAge                  intSex                  intjwt.RegisteredClaims // 这里就是使用鸭子模式,自动隐式判断继承}// Sign 签发tokenfunc Sign(data jwt.Claims) (string, error) {// 选择加密方式签发tokentoken := jwt.NewWithClaims(jwt.SigningMethodHS256, data)// 签名转化为字符串sign, err := token.SignedString([]byte(key))if err != nil {return "", err}return sign, err}// Verify 验签func Verify(sign string, data jwt.Claims) error {_, err := jwt.ParseWithClaims(sign, data, func(token *jwt.Token) (interface{}, error) {return []byte(key), nil})return err}

然后修改原来的token验证方法

       package middlewareimport ("errors""github.com/gin-gonic/gin""net/http")func TokenCheck(context *gin.Context) {// 从请求头中获取tokenaccessToken := context.GetHeader("access_token")// 进行token校验,在正规流程中,这里应该拿着token做完加解密之后,通过jwt获取存于缓存或者redis中的用户信息,并把用户信息写入请求之中data := &Data{}// 调用验签方法err := Verify(accessToken, data)if err != nil {context.JSON(http.StatusInternalServerError, gin.H{"message": "token 校验失败",})// 防止此处理继续下去// context.Abort()// 防止此处理继续下去并报错context.AbortWithError(http.StatusInternalServerError, errors.New("token checker fail"))}// 把用户信息塞到这次请求的上下文中context.Set("user", data)context.Next()}

之后,修改登录方法

        func Login(context *gin.Context) {req := &loginReq{}err := context.ShouldBindJSON(req)if err != nil {context.JSON(http.StatusInternalServerError, gin.H{"message": err.Error(),})return}// 在这里应该通过name 和 password 去数据库取到用户信息,然后填充进签名信息中data := middleware.Data{Id:   "123",Name: req.Name,Sex:  1,Age:  18,RegisteredClaims: jwt.RegisteredClaims{// 有效期为一个小时ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),// 签发时间IssuedAt: jwt.NewNumericDate(time.Now()),// 有效期的开始时间NotBefore: jwt.NewNumericDate(time.Now()),},}sign, err := middleware.Sign(data)if err != nil {context.JSON(http.StatusInternalServerError, gin.H{"message": err,})return}context.JSON(http.StatusOK, gin.H{"token": sign,})}

最后,先调用登录信息拿到 token 之后放入请求头的 access_token 中,调用获取当前登录人的信息的接口,代码如下:

  	v1.GET("/detail", func(context *gin.Context) {// 从上下文中获取在token中间件中塞入的用户信息if userData, exist := context.Get("user"); exist {// 返回给前端context.JSON(http.StatusOK, userData)return}context.JSON(http.StatusInternalServerError, gin.H{"message": "用户不存在",})})

6. 接口入参获取和绑定

  1. 入参获取

  2. 在restful风格接口里面传参,比如像/xxx/xxx/500,在代码中路由地址中声明为”/xxx/xxx/:id“,那么可以使用Param获取

       v1.GET("/detail/:id", func(context *gin.Context) {// 可以通过 Param 获取到参数值id := context.Param("id")context.String(200, "ID is %s", id)})
  1. 在传统的url传参的形式下,如/xxx/xxx?id=500,那么可以使用Query获取
      	v1.GET("/detail?id=500", func(context *gin.Context) {// 可以通过 Param 获取到参数值id := context.Query("id")context.String(200, "ID is %s", id)})
  1. 在 form 表单提交的时候,使用结构体去承接入参
    在 web 包中的逻辑处理代码如下:
 			type loginReq struct {name     string `form:"nickName" json:"nickName"` // 反括号内中 form是表名提交方式, nickName是表名提交的数据的keypassword string `form:"password" json:"password"` // 那么对应的json提交的时候,这里就是 `json:"password"`,前面的属性也应该首字母大写,JSON会自动转化为小写}func Login(context *gin.Context) {// 声明承接的结构体实例req := &loginReq{}// shouldBind 为绑定提交,基本上可以应对所有的方式提交,json提交时也可以指明为使用ShouldBindJSON// Bind 和 ShouldBind区别在于使用 Bind 当入参不满足格式时会返回400错误,而不是继续向下执行进入代码中设定的错误。err := context.ShouldBind(req)if err != nil {context.JSON(http.StatusInternalServerError, gin.H{"message": err.Error(),})return}fmt.Println(req)context.JSON(http.StatusOK, req)}

请求和返回如下图所示:
在这里插入图片描述

2. 参数校验

验证框架是:validator

几个校验示例:

    type loginReq struct {Name     string `json:"nickName" binding: "required"` // 加入binding: "required"是验证必填Password string `json:"password"`Phone    string `json:"password binding: "required,el64"` // el64是验证电话格式Email    string `json:"password binding: "omitempty,email"` // omitempty为当前值为空,就不在进行后续校验,email是验证邮箱格式}

3. protobuf

Protobuf(Protocol Buffers)是一种由 Google 开发的数据序列化格式和编程语言无关的接口定义语言(IDL,Interface Definition Language)。它的主要目的是用于定义数据结构和消息格式,以便在不同平台和不同语言之间进行高效的数据交换。

使用 Protobuf,你可以定义结构化数据的消息格式,并生成针对多种编程语言的序列化和反序列化代码。这使得不同系统之间可以相互通信、交换数据,而无需关心底层的数据表示和传输细节。

Protobuf 提供了一种简洁、高效的二进制编码格式,它比 XML 和 JSON 等文本格式更紧凑,解析速度更快,同时也更容易进行版本升级和兼容性处理。因此,Protobuf 在诸如分布式系统通信、数据存储和数据交换等领域具有广泛的应用。

使用 Protobuf 的基本流程如下:

  1. 使用 Protobuf 的语法定义消息类型和结构化数据的字段。
  2. 使用 Protobuf 编译器将定义的 Protobuf 文件(通常以 .proto 为后缀)转换为目标语言的代码文件。
  3. 在程序中使用生成的代码来序列化和反序列化消息。

总结起来,Protobuf 提供了一种跨语言、跨平台的数据序列化方案,使得不同系统之间可以高效、可靠地进行数据交换和通信。

7. 集成mysql数据库

1. gorm使用

gorm 是 golang 对于 sql 和对象的一种关系映射的框架,orm因为牵扯到映射,所以不推荐用于多表连接的操作。

使用go get -u gorm.io/gorm 下载依赖,以及go get -u gorm.io/driver/mysql 下mysql驱动。文档地址

  1. 创建连接池
    然后在 middleware 文件夹下面新建 grom.go 文件
 	   package middlewareimport ("gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger""log""time")var DB *gorm.DBvar dsn = "admin:pass12345sx@tcp(127.0.0.1:3306)/hit-plum?charset=utf8mb4&parseTime=True&loc=Local"func init() {var err errorDB, err = gorm.Open(mysql.New(mysql.Config{DSN:                       dsn,   // data source nameDefaultStringSize:         256,   // 默认字符配置DisableDatetimePrecision:  true,  // 禁用日期时间精度,MySQL 5.6之前不支持此功能DontSupportRenameIndex:    true,  // 重命名索引时删除并创建,MySQL 5.7之前不支持重命名索引,MariaDBDontSupportRenameColumn:   true,  // `change`重命名列时,MySQL 8、MariaDB之前不支持重命名列SkipInitializeWithVersion: false, // 基于当前MySQL版本自动配置}), &gorm.Config{Logger: logger.Default.LogMode(logger.Info), // 设置一下log的日志级别})if err != nil {log.Println(err)return}setPool(DB)}// setPool 设置连接池func setPool(db *gorm.DB) {sqlDB, err := db.DB()if err != nil {log.Println(err)return}sqlDB.SetMaxIdleConns(5)                // 最大空闲连接数,即最大可以长期保持的连接数,超过这个数的链接会按照策略自动关闭sqlDB.SetMaxIdleConns(10)               // 最大同时连接数sqlDB.SetConnMaxLifetime(time.Hour / 2) // 设置最大存活周期为半小时,超过这个周期还存在链接将会被回收}
  1. 新增model
    在 web 包下新建 models 包,新增 user.go 文件
       package modelsimport ("time""gorm.io/gorm")// 共有的字段type Model struct {gorm.Model // gorm 提供的共有字段UpdatedBy stringCreatedBy stringDeletedBy string}type User struct {Model     Model  `gorm:"embedded"` // 嵌入共有的一些字段UserName  string `gorm:"column:user_name"` // 声明对应的列名Password  stringRealName  stringUserType  uintEmail     stringPhone     stringDepartId  intGender    uintAvatar    string `gorm:"type:text"` // 声明对应的字段数据库数据类型Enabled   uintDelFlag   uintLoginIp   stringLoginDate time.TimeRemark    string `gorm:"type:text"`}// TableName 方法指定表名, 默认是根据 结构体名字后加个s为表名,不符合此规范就要单独声明表名func (User) TableName() string {return "user"}
  1. 增删改查
    web 下新增一个 dao 包,然后建立一个 user.go 文件
// 用于测试的数据对象var userTempData = models.User{UserName:  "sue",Password:  "123456",RealName:  "ElvisSue",UserType:  1,Email:     "ElvisSue@163.com",Phone:     "15066514789",DepartId:  456,Gender:    1,Avatar:    "",Enabled:   0,DelFlag:   1,LoginIp:   "localhost",LoginDate: time.Now(),Remark:    "this is a handsome man",Model: models.Model{CreatedBy: "admin",UpdatedBy: "",DeletedBy: "",},}

增:

       // CreateUser 新增一条数据func CreateUser() {t := userTempDatares := middleware.DB.Create(&t)fmt.Println(res.RowsAffected)fmt.Println(res.Error)fmt.Println(t)}// PartialCreateUser 部分插入func PartialCreateUser() {t := userTempDatares := middleware.DB.Select("UserName", "Password", "CreatedBy").Create(&t)fmt.Println(res.RowsAffected)fmt.Println(res.Error)fmt.Println(t)}// OmitCreateUser 忽略某些字段插入func OmitCreateUser() {t := userTempDatares := middleware.DB.Omit("LoginIp").Create(&t)fmt.Println(res.RowsAffected)fmt.Println(res.Error)fmt.Println(t)}// BatchCreateUser 批量插入func BatchCreateUser() {users := make([]models.User, 8)for i := 0; i < 3; i++ {t := userTempDatausers = append(users, t)}res := middleware.DB.Create(&users)fmt.Println(res.RowsAffected)fmt.Println(res.Error)}

删:其实现在大部分数据采用的是逻辑删除,删这个操作并不是特别重要,所以一个接口演示出所有的情况

       // DeleteUser 删除用户func DeleteUser(){temp := models.User{id: 10}middleware.DB.Delete(&temp) // 删除主键为10的middleware.DB.Delete(&models.User{}, 10) // 删除主键为10的middleware.DB.Delete(&models.User{}, []int{1,2,3})// 删除主键 in [1, 2, 3]的middleware.DB.Where("user_name = ?", "jinzhu").Delete(&temp) // 删除主键为10的且 user_name 为 “jinzhu” 的middleware.DB.Delete(&models.User{},"user_name LIKE ?", "%jinzhu%") // 删除主键为10的且 user_name 包含 “jinzhu” 的}

改:
// Save 这个接口有些特殊,在未声明where条件且要传入的model不含有主键的情况下,会默认使用 insert 去更新表数据,设置 where 条件后或者 model 中有已存在的主键才会使用 update 更新表数据

        var temp = models.User{ID: 8UserName: "lee",Password: "153654"}// SaveUser 修改一条数据func SaveUser() {t := tempmiddleware.DB.Save(&t)// 因为ID是主键,更改了 ID 为8的数据}// SaveSingleColumn 修改单一列func SaveSingleColumn (){t := temp// 使用空的结构体来声明表名进行操作middleware.DB.Model(&models.User{}).Where("enabled = ?", 1).Update("UserName", "hello")// 使用已有主键的model进行操作, 即 where id = xmiddleware.DB.Model(&userTempData).Update("UserName", "hello")// 使用已有主键的model进行操作加where, 即 where id = x and enabled = 1middleware.DB.Model(&userTempData).Where("enabled = ?", 1).Update("UserName", "hello")}// SaveMultipleColumn 多列更改func SaveMultipleColumn (){t := temp// 当满足 where id = 5 and enabled = 1 的时候,t 中有的属性都会更改middleware.DB.Model(&models.User{ID: 5}).Where("Enabled = ?", 1).Updates(&t)}// SaveSelectUser 选择列修改,其实和多列修改差不多func SaveSelectUser(){// 满足 where id = 5 只修改 user_name 的值middleware.DB.Model(&models.User{ID: 5}).Select("UserName").Updates(&t)}

查:

       func QueryUser() {var user models.Uservar users []models.Usermiddleware.DB.Model(&models.User{}).First(&user) // 按照id排序去第一条middleware.DB.Model(&models.User{}).Take(&user) // 默认第一条middleware.DB.Model(&models.User{}).Last(&user) // id 排序最后一条middleware.DB.Model(&models.User{}).First(&user, 8) // id = 8的middleware.DB.Model(&models.User{}).First(&user,"id = ?",  8) // id = 8的middleware.DB.Model(&models.User{}).First(&user, []int{1, 2, 3})// id in [1,2,3]的middleware.DB.Where("user_name <> ?", "jinzhu").Find(&users) // 所有 user_name <> 'jinzhu'}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/232070.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MapReduce和Yarn部署+入门

看的黑马视频记的笔记 目录 1.入门知识点 分布式计算&#xff1a; 概念&#xff1a; 两种模式&#xff1a; MapReduce&#xff08;分布式计算&#xff0c;分散汇总模式&#xff09; 概念 执行原理 注&#xff1a; Yarn&#xff08;分布式资源调度&#xff09; 概述 Y…

VS Code配置Go语言开发环境

提示&#xff1a;首先这是一个新型语言&#xff0c;最好把vscode更新到最新版。 1&#xff1a;去官网下载Go语言编译器&#xff0c;之后配置到系统环境中&#xff0c;能看到版本就行。 2&#xff1a;创建一个文件夹&#xff0c;存放go的工具文件&#xff0c;我的在D:\GoFile\G…

【数据分析之Numpy】Numpy中复制函数numpy.repeat()与numpy.tile()的使用方法及区别

一、简介 numpy.repeat()与numpy.tile()都是Numpy库中的复制函数&#xff0c;用于将数组中的元素重复指定的次数。 numpy.repeat()函数接受三个参数&#xff1a;要重复的数组、重复的次数和重复的轴。 numpy.tile()函数接受两个参数&#xff1a;要重复的数组和重复的次数。 二…

前端技术的新趋势:React、Vue与Angular的比较

本文将比较当前最流行的前端框架React、Vue和Angular&#xff0c;探讨它们各自的优缺点&#xff0c;并分析它们在未来的发展趋势。 随着互联网技术的不断发展&#xff0c;前端技术也在不断演进。React、Vue和Angular作为当前最流行的前端框架&#xff0c;它们在开发效率、性能和…

使用AI大模型生成动漫人像

在线体验&#xff1a;点击【图像处理】即可使用 public static final String SELFIE_ANIME "https://aip.baidubce.com/rest/2.0/image-process/v1/selfie_anime"; private static final String TOKEN_URL "https://aip.baidubce.com/oauth/2.0/token";…

HTTP协议:简单易用、可扩展的应用层协议

HTTP 协议是一种基于 TCP/IP 的应用层协议&#xff0c;是最常用的Web协议之一&#xff0c;用于客户端和服务器之间传递和交换数据&#xff0c;也常被称为超文本传输协议。在HTTP协议中&#xff0c;每一个TCP连接会产生两个并行的流&#xff0c;一个是传输客户端发送给服务器的请…

elasticsearch简单相关操作

查看索引 GET _cat/indices //获取所有的index GET account发送post不带id新建数据 POST user/_doc/ {"name":"bobby","compamy":"imooc" }如果post带id就和put一样的操作了&#xff0c; put是不允许不带id的 post _create 没有就…

【前端】vscode 相关插件

一 插件&#xff1a; 01、ESLint 用来识别并检查ECMAScript/JavaScript 代码的工具 02、Prettier 用来格式化代码&#xff0c;如.js、.vue、css等都可以进行格式化 03、Vetur 用来识别并高亮vue语法 04、EditorConfig 用来设置vscode的编程行为 二、安装依赖 01、…

干涉光学测试导论

1.用于光学测试的基本干涉仪 2。相移干涉术 3。专业光学测试 4。长波长干涉术 5。非球面试验 6。表面微观结构的测量 7。绝对测量 8。结束语 第1部分-光学测试用基本干涉仪 (1)双光束干涉 (2)菲佐干涉仪和特维曼-格林干涉仪 (3)测试平面和球面的基本技术 (4)球面的基本…

dart遍历树及查找

省市区三级联动后端返回用户选择字符串&#xff0c;前端遍历查找&#xff0c;本来想用递归。后来看了一篇文章&#xff0c;作者使用js写的。目前&#xff0c;我在写flutter项目所以该用dart。文章地址&#xff0c;作者讲解的很详细&#xff0c;不再赘述。 /** 深度遍历树查找*…

QT视频报错:DirectShowPlayerService::doRender: Unresolved error code 0x80040266

原因&#xff1a;QT使用windows默认解码器&#xff0c;如果没有安装有相关DirectShowService解码器&#xff0c;则运行程序也是没法播放视频的&#xff0c;必须安装相关directshow解码器&#xff0c;安装位置在你的qt安装目录&#xff0c;安装完重启电脑就可以解决了。 解决办…

如何获取旧版 macOS

识别机型支持的最新的兼容操作系统 识别 MacBook Air - 官方 Apple 支持 (中国) 社区网站&#xff1a;AppStore 无法找到macos cata… - Apple 社区 官网链接隐藏比较深&#xff1a;如何下载和安装 macOS - 官方 Apple 支持 (中国) 获取磁盘映像 Lion 10.7 https://update…

maui中实现加载更多 RefreshView跟ListView(2)

一个类似商品例表的下拉效果&#xff1a; 代码 新增个类为商品商体类 public class ProductItem{public string ImageSource { get; set; }public string ProductName { get; set; }public string Price { get; set; }}界面代码&#xff1a; <?xml version"1.0&quo…

我的创作纪念日365

机缘 提示&#xff1a;可以和大家分享最初成为创作者的初心 例如&#xff1a; 实战项目中的经验分享日常学习过程中的记录通过文章进行技术交流… 收获 提示&#xff1a;在创作的过程中都有哪些收获 例如&#xff1a; 获得了多少粉丝的关注获得了多少正向的反馈&#xff0c…

通过费用流中的贪心来保证计数正确性:P4249剪刀石头布

https://vj.imken.moe/contest/598718#problem/K 三元环数量尽量多&#xff0c;也就是非三元环数量尽可能少。非三元环的充要条件是存在一个点度数为2&#xff0c;而每条边可以给一个点一个度数&#xff0c;然后就变成了经典网络流问题。 但是&#xff0c;对于一个点&#xf…

计算机与自动医疗检查仓:技术革新引领医疗未来

计算机与自动医疗检查仓&#xff1a;技术革新引领医疗未来 一、引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;和机器学习&#xff08;ML&#xff09;已经成为现代社会不可或缺的一部分。它们的应用领域日益扩展&#xff0c;从简单的日常任务到复杂…

数据结构--图

树具有灵活性&#xff0c;并且存在许多不同的树的应用&#xff0c;但是就树本身而言有一定的局限性&#xff0c;树只能表示层次关系&#xff0c;比如父子关系。而其他的比如兄弟关系只能够间接表示。 推广--- 图 图形结构中&#xff0c;数据元素之间的关系是任意的。 一、图…

The Great Common Factor

描述 输入N及N个正整数。N不大于1000。 输出这N个数的最大公约和最小公倍数 输入 输入N及N个正整数。N不大于1000。 输出 输出这N个数的最大公约和最小公倍数 样例输入 3 2 4 8 5 3 6 8 9 12样例输出 2 8 1 72 思路 gcd&#xff08;greatest common divisor&#xff09;计…

基于ssm的航班订票管理系统论文

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对航班订票信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差…

【PID学习笔记10】PID公式分析

写在前面 前面已经将控制系统的基础知识点过了一遍&#xff0c;从本节开始&#xff0c;将正式学习PID控制的相关知识&#xff0c;将会从基本的PID公式概念解释&#xff0c;再基于matlab仿真介绍十几种数字式PID的基本概念。本文重点讲解PID的经典公式。 一、连续与离散的概念…