Go Web 开发基础【用户登录、注册、验证】

news2024/11/17 16:00:03

前言

        这篇文章主要是学习怎么用 Go 语言(Gin)开发Web程序,前端太弱了,得好好补补课,完了再来更新。

1、环境准备

新建项目,生成 go.mod 文件:

出现报错:go: modules disabled by GO111MODULE=off; see 'go help modules',说明需要开启:

临时设置环境变量: 

set GO111MODULE=on # windows
export GO111MODULE=on # linux

 永久设置环境变量:

再次生成 go.mod 文件:

执行完毕,发现项目下生成 go.mod 文件:

这里的模块名称是我们自定义的,不是说非得和哪个目录或者项目名对应上!

2、用户注册

2.1、需求

  • 用户向地址 /register 发送POST请求(表单携带着 username、password、phone)
  • 后端处理请求(检查手机号位数、手机号是否已经被注册、用户名是否为空)

2.2、需求实现

2.2.1、判断手机号是否存在

func isPhoneExist(db gorm.DB, phone string) bool {
	user := User{}
	db.Where("phone = ?", phone).First(user)
	if user.ID != 0 {
		return true
	}
	return false
}

2.2.2、生成随机10位的用户名

func RandomString(length int) string {
	var letters = []byte("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM")
	result := make([]byte, length)

	rand.Seed(time.Now().Unix())
	for i := range result {
		result[i] = letters[rand.Intn(len(letters))]
	}
	return string(result)
}

2.2.3、设置用户表结构体

        这个结构体的字段会对应用户表中的每个字段,我们会在初始化数据库的时候,使用 gorm.DB 的  AutoMigrate 方法自动帮我进行创建这个结构体对应的表。

type User struct {
	gorm.Model
	Name     string `gorm:"type:varchar(20);not null"`
	Phone    string `gorm:"type:varchar(110);not null,unique"`
	Password string `gorm:"size:255;not null"`
}

 其中 gorm.Model 的源码如下:

我们通过嵌套 gorm.Model 来给我们的表增加四个字段。

2.2.4、获得数据库连接(gorm)

func InitDB() *gorm.DB {
	driverName := "mysql"
	host := "127.0.0.1"
	port := 3306
	database := "go_web"
	username := "root"
	password := "xxxxx"
	charset := "utf8mb4"

	args := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true", username, password, host, port, database, charset)

	db, err := gorm.Open(driverName, args)
	if err != nil {
		panic("failed to connect database, err : " + err.Error())
	}
	db.AutoMigrate(&User{}) // 如果表不存在则自动创建
	return db
}

2.3、完整代码

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
	"math/rand"
	"net/http"
	"time"
)

type User struct {
	gorm.Model
	Name     string `gorm:"type:varchar(20);not null"`
	Phone    string `gorm:"type:varchar(110);not null,unique"`
	Password string `gorm:"size:255;not null"`
}

func main() {

	db := InitDB()
	defer db.Close()

	engine := gin.Default()
	engine.POST("/register", func(ctx *gin.Context) {
		// 获取参数
		name := ctx.PostForm("username")
		phone := ctx.PostForm("phone")
		password := ctx.PostForm("password")

		// 数据验证
		if len(phone) != 11 {
			ctx.JSON(http.StatusUnprocessableEntity, gin.H{
				"code": 422,
				"msg":  "手机号必须为11位!",
			})
			return
		}
		if len(password) < 6 {
			// gin.H 等同于 map[string]any
			ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
				"code": 422,
				"msg":  "密码不能少于6位!",
			})
			return
		}
		if len(name) == 0 {
			name = RandomString(10)
		}

		fmt.Println(name, phone, password)

		// 判断手机号是否存在
		if isPhoneExist(*db, phone) {
			ctx.JSON(http.StatusUnprocessableEntity, gin.H{
				"msg": "用户已存在,不允许重复注册",
			})
			return
		}

		// 创建用户
		newUser := User{
			Name:     name,
			Phone:    phone,
			Password: password,
		}
		db.Create(&newUser)

		// 返回结果
		ctx.JSON(200, gin.H{
			"msg": "注册成功",
		})
	})

	panic(engine.Run()) // 默认端口 8080
}

func isPhoneExist(db gorm.DB, phone string) bool {
	user := User{}
	db.Where("phone = ?", phone).First(user)
	if user.ID != 0 {
		return true
	}
	return false
}

func RandomString(length int) string {
	var letters = []byte("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM")
	result := make([]byte, length)

	rand.Seed(time.Now().Unix())
	for i := range result {
		result[i] = letters[rand.Intn(len(letters))]
	}
	return string(result)
}

func InitDB() *gorm.DB {
	driverName := "mysql"
	host := "127.0.0.1"
	port := 3306
	database := "go_web"
	username := "root"
	password := "xxxxx"
	charset := "utf8mb4"

	args := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true", username, password, host, port, database, charset)

	db, err := gorm.Open(driverName, args)
	if err != nil {
		panic("failed to connect database, err : " + err.Error())
	}
	db.AutoMigrate(&User{}) // 如果表不存在则自动创建
	return db
}

2.4、测试

使用规范的用户信息再次注册:

3、项目重构

        上面我们把连接数据库和响应的代码都放到了一个文件中,显然后期随着业务代码越来越多开发起来越来越难以管理,所以我们这里需要对项目进行重构:

3.1、util 层

存放工具,比如我们上面的随机生成用户名方法

package util

import (
	"math/rand"
	"time"
)

func RandomString(length int) string {
	var letters = []byte("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM")
	result := make([]byte, length)

	rand.Seed(time.Now().Unix())
	for i := range result {
		result[i] = letters[rand.Intn(len(letters))]
	}
	return string(result)
}

3.2、model 层

存放结构体

package model

import "github.com/jinzhu/gorm"

type User struct {
	gorm.Model
	Name     string `gorm:"type:varchar(20);not null"`
	Phone    string `gorm:"type:varchar(110);not null,unique"`
	Password string `gorm:"size:255;not null"`
}

3.3、common 层

存放一些公共的方法,比如连接数据库工具

package common

import (
	"com.lyh/goessential/model"
	"fmt"
	"github.com/jinzhu/gorm"
)

var DB *gorm.DB

func InitDB() *gorm.DB {
	driverName := "mysql"
	host := "127.0.0.1"
	port := 3306
	database := "go_web"
	username := "root"
	password := "Yan1029."
	charset := "utf8mb4"

	args := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true", username, password, host, port, database, charset)

	db, err := gorm.Open(driverName, args)
	if err != nil {
		panic("failed to connect database, err : " + err.Error())
	}
	db.AutoMigrate(&model.User{}) // 如果表不存在则自动创建

	DB = db
	return db
}

func GetDB() *gorm.DB {
	return DB
}

3.4、controller 层

        存放控制器,因为我们使用的 Gin 框架的请求方法都是函数式编程,它的第二个参数是一个处理请求的函数,所以控制器层我们存放的是业务模块对应的方法:

package controller

import (
	"com.lyh/goessential/common"
	"com.lyh/goessential/model"
	"com.lyh/goessential/util"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/gorm"
	"net/http"
)

func Register(ctx *gin.Context) {
	DB := common.GetDB()
	// 获取参数
	name := ctx.PostForm("username")
	phone := ctx.PostForm("phone")
	password := ctx.PostForm("password")

	// 数据验证
	if len(phone) != 11 {
		ctx.JSON(http.StatusUnprocessableEntity, gin.H{
			"code": 422,
			"msg":  "手机号必须为11位!",
		})
		return
	}
	if len(password) < 6 {
		// gin.H 等同于 map[string]any
		ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
			"code": 422,
			"msg":  "密码不能少于6位!",
		})
		return
	}
	if len(name) == 0 {
		name = util.RandomString(10)
	}

	fmt.Println(name, phone, password)

	// 判断手机号是否存在
	if isPhoneExist(DB, phone) {
		ctx.JSON(http.StatusUnprocessableEntity, gin.H{
			"msg": "用户已存在,不允许重复注册",
		})
		return
	}

	// 创建用户
	newUser := model.User{
		Name:     name,
		Phone:    phone,
		Password: password,
	}
	DB.Create(&newUser)

	// 返回结果
	ctx.JSON(200, gin.H{
		"msg": "注册成功",
	})
}

func isPhoneExist(db *gorm.DB, phone string) bool {
	user := model.User{}
	db.Where("phone = ?", phone).First(&user) // 这里的参数是地址
	if user.ID != 0 {
		return true
	}
	return false
}

注意:这里判断用户是否存在需要传入一个 user 地址,因为 user 是值类型,如果不传入地址,则进入方法后的操作无效。 

3.5、routes.go

        所有的请求都将通过这个文件中的方法再传递给 main 方法,其实就是为了简化 main 方法所在go文件的代码量,方便管理和维护。所以它的包名也是 main,相当于它俩在一个文件内。

package main

import (
	"com.lyh/goessential/controller"
	"github.com/gin-gonic/gin"
)

func CollectRoute(engine *gin.Engine) *gin.Engine {
	engine.POST("/register", controller.Register)
	return engine
}

3.6、main.go

这是程序的入口,现在我们已经把它彻底解脱出来了:

package main

import (
	"com.lyh/goessential/common"
	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"
)

func main() {

	db := common.InitDB()
	defer db.Close()

	engine := gin.Default()
	engine = CollectRoute(engine)

	panic(engine.Run()) // 默认端口 8080
}

测试

因为我们有两个文件都是 main 包,所以我们最好使用命令启动:

4、密码加密以及登录测试

4.1、注册加密

        在 controller 的注册方法( Register )中修改创建用户的代码,对将要插入数据库中的代码进行加密:

	// 创建用户
	hasedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"code": 500,
			"msg":  "加密错误",
		})
		return
	}
	newUser := model.User{
		Name:     name,
		Phone:    phone,
		Password: string(hasedPassword),
	}
	DB.Create(&newUser)

4.2、登录方法

func Login(ctx *gin.Context) {
	DB := common.GetDB()
	// 获取参数
	phone := ctx.PostForm("phone")
	password := ctx.PostForm("password")

	// 数据验证
	if len(phone) != 11 {
		ctx.JSON(http.StatusUnprocessableEntity, gin.H{
			"code": 422,
			"msg":  "手机号必须为11位!",
		})
		return
	}
	if len(password) < 6 {
		// gin.H 等同于 map[string]any
		ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
			"code": 422,
			"msg":  "密码不能少于6位!",
		})
		return
	}

	// 判断手机号是否存在
	user := model.User{}
	DB.Where("phone = ?", phone).First(&user)
	if user.ID == 0 {
		ctx.JSON(http.StatusUnprocessableEntity, gin.H{
			"msg": "用户不存在",
		})
		return
	}
	// 判断密码是否正确
	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"code": 400,
			"msg":  "密码错误",
		})
		return
	}

	// 返回 token 给前端
	token := "11"

	// 返回结果
	ctx.JSON(200, gin.H{
		"code": 200,
		"data": gin.H{
			"token": token,
		},
		"msg": "登录成功",
	})

}

4.3、注册请求

        把我们的 Login 方法注册到 /login 地址(只需要在 routes.go 文件的 CollectRoute 函数中添加一行即可):

测试

查看数据库:

登录测试

5、jwt 实现用户认证

jwt 地址:github.com/dgrijalva/jwt-go

5.1、发放 token

在 common 包下来创建一个 jwt.go 文件,定义发放 token 的函数:

package common

import (
	"com.lyh/goessential/model"
	"github.com/dgrijalva/jwt-go"
	"time"
)

var jwtKey = []byte("a_secret_crect")

type Claims struct {
	UserId uint
	jwt.StandardClaims
}

func ReleaseToken(user model.User) (string, error) {
	expirationTime := time.Now().Add(7 * 24 * time.Hour) // 设置token有效期7天
	claims := &Claims{
		UserId: user.ID,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expirationTime.Unix(), // 过期时间
			IssuedAt:  time.Now().Unix(),     // 发放的时间
			Issuer:    "lyh",                 // 发送者
			Subject:   "user token",          // 发送主题
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

// 从 tokenString 中解析出 claims 然后返回
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
	claims := &Claims{}

	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtKey, nil
	})
	return token, claims, err
}

5.2、设置返回 token

        在之前 controller 层下用户模块中的登录请求(/login)中设置返回 token(之前随便写了个 "11"):

	// 返回 token 给前端
	token, err := common.ReleaseToken(user)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"code": 500,
			"msg":  "系统异常",
		})
		log.Printf("token generate error : %v", err)
		return
	}

	// 返回结果
	ctx.JSON(200, gin.H{
		"code": 200,
		"data": gin.H{
			"token": token,
		},
		"msg": "登录成功",
	})

测试登录用户,拿到 token :

token 由三部分组成:

  • 协议头(token 使用的加密协议)
  • 我们给token中存储的信息(解密后是 JSON 格式的数据)
  • 前两部分加上key通过hash后的值

5.3、定义用户认证中间件

        如果不加中间件的话,前端请求时携带token,返回的 user 是 null ,因为我们没有往上下文存储 user 的信息。

        中间件为的是把前端请求时,authorization 中携带的 token 信息解析出来验证并保存到上下文。在 middleware 包下创建 AuthMiddleware.go:

package middleware

import (
	"com.lyh/goessential/common"
	"com.lyh/goessential/model"
	"github.com/gin-gonic/gin"
	"net/http"
	"strings"
)

// 自定义中间件用于用户验证:相当于SpringBoot中的拦截器
func AuthMiddleware() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// 获取 authorization header
		tokenString := ctx.GetHeader("Authorization")

		//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsImV4cCI6MTcxNTE1MDIyNCwiaWF0IjoxNzE0NTQ1NDI0LCJpc3MiOiJseWgiLCJzdWIiOiJ1c2VyIHRva2VuIn0.C6yH99IZDjj6_FnpHaREVPmoCX82nYWv1OZao171iPg
		// 验证格式
		if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer ") {
			ctx.JSON(http.StatusUnauthorized, gin.H{
				"code": 401,
				"msg":  "权限不足",
			})
			ctx.Abort()
			return
		}
		tokenString = tokenString[7:] // Bearer+' '一共7位

		token, claims, err := common.ParseToken(tokenString)
		if err != nil || !token.Valid {
			ctx.JSON(http.StatusUnauthorized, gin.H{
				"code": 401,
				"msg":  "权限不足",
			})
			ctx.Abort()
			return
		}

		// 验证通过后获取claims 中的 userId
		userId := claims.UserId
		DB := common.GetDB()
		var user model.User
		DB.First(&user, userId)

		// 用户不存在
		if user.ID == 0 {
			ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"})
			ctx.Abort()
			return
		}

		// 用户存在 将user信息写入上下文
		ctx.Set("user", user)

		ctx.Next()
	}
}

添加路由并测试

添加到我们的管理所有路由的文件(routes.go)中:

 

测试:

6、统一请求返回格式

        我们在学习 SpringBoot 项目的时候也进行了请求的统一返回格式:(code,data,other),这里也是一样,为的是简化开发,比如我们前面返回前端需要这样写:

    ctx.JSON(200, gin.H{
		"code": 200,
		"msg":  "注册成功",
	})

统一格式后我们只需要写个 200 和 "注册成功" 就可以了。

        此外,我们给前端返回的数据还有一些问题:比如把用户的全部信息都返回出去了(包括密码登隐私信息)

6.1、数据传输对象(dto)

创建一个包 dto 并创建 user_dto.go 用来将返回给前端的 user 转换为 userDto:

package dto

import (
	"com.lyh/goessential/model"
)

type UserDto struct {
	Name  string `json:"name"`
	Phone string `json:"phone"`
}

// 将 user 转换为 userDto
func ToUserDto(user model.User) UserDto {
	return UserDto{
		Name:  user.Name,
		Phone: user.Phone,
	}
}

修改 controller 中的 Info 方法:

func Info(ctx *gin.Context) {
	// 从上下文中获得用户的信息
	user, _ := ctx.Get("user")
	ctx.JSON(http.StatusOK, gin.H{"code": 200, "data": gin.H{"user": dto.ToUserDto(user.(model.User))}})
}

再次进行用户验证:

可以看到,这次返回的数据没有其它敏感信息。

6.2、封装 HTTP 返回

创建目录 response,并创建 response.go :        

package response

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// 这里的 code 是我们自定义的业务code
func Response(ctx *gin.Context, httpStatus int, code int, data gin.H, msg string) {
	ctx.JSON(httpStatus, gin.H{"code": code, "date": data, "msg": msg})
}

func Success(ctx *gin.Context, data gin.H, msg string) {
	Response(ctx, http.StatusOK, 200, data, msg)
}

func Fail(ctx *gin.Context, data gin.H, msg string) {
	Response(ctx, http.StatusOK, 400, data, msg)
}

        定义了统一的前端返回类型之后,我们就可以开始修改之前的返回代码了,之前我们的 HTTP 返回都是通过 ctx.JSON(httpStatus,gin.H) 来返回的,现在我们需要都替换为我们自定义的返回格式,比如下面的:

    ctx.JSON(http.StatusUnprocessableEntity, gin.H{
			"code": 422,
			"msg":  "手机号必须为11位!",
	})

统一之后就清爽多了,而且不会存在前端拿一些 JSON 的属性却拿不到的情况。 

response.Response(ctx, http.StatusUnprocessableEntity, 422, nil, "手机号必须为11位")

7、从文件中读取配置(viper)

        上面我们的很多配置信息都是直接定义在代码中的(比如连接数据库需要的参数),这样很不好管理和维护,所以这里我们统一下配置源。

7.1、安装 viper

go get github.com/spf13/viper

如果需要使用旧版本就去 go.mod 取修改版本号重新下载。

7.2、编写配置文件(yml)

在 config 目录下创建 application.yml:

server:
  port: 1016
datasource:
  driverName: mysql
  host: 127.0.0.1
  port: 3306
  database: go_web
  username: root
  password: Yan1029.
  charset: utf8mb4

7.3、使用 viper 读取配置文件

在 main 方法中添加读取配置文件的函数:

package main

import (
	"com.lyh/goessential/common"
	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"
	"github.com/spf13/viper"
	"os"
)

func main() {

	InitConfig()
	db := common.InitDB()
	defer db.Close()

	engine := gin.Default()
	engine = CollectRoute(engine)
	port := viper.GetString("server.port")
	if port != "" {
		panic(engine.Run(":" + port))
	}
	panic(engine.Run()) // 默认端口 8080
}

func InitConfig() {
	workDir, _ := os.Getwd()
	viper.SetConfigName("application")
	viper.SetConfigType("yml")
	viper.AddConfigPath(workDir + "/config")
	err := viper.ReadInConfig()
	if err != nil {
		panic(err)
	}
}

修改 databse.go 中的 InitDB 方法:

    driverName := viper.GetString("datasource.driverName")
	host := viper.GetString("datasource.host")
	port := viper.GetInt("datasource.port")
	database := viper.GetString("datasource.database")
	username := viper.GetString("datasource.username")
	password := viper.GetString("datasource.password")
	charset := viper.GetString("datasource.charset")

测试

数据库可以查询成功,配置成功。

注意事项 

1、gorm 版本问题

最新版 gorm:

使用旧版本的 gorm:

require (
    github.com/jinzhu/gorm v1.9.12
)

总结

        至此,我大概明白了 Go 语言怎么开发一个 Web 程序,也消除了我的很多疑虑,比如Java一个类就是一个文件,那Go语言怎么对项目进行分层架构等一些简单但又特别重要的内容。

        接下来,学学前端,至少了解怎么和后端交互,写一个功能完整的Web程序。 

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

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

相关文章

【webrtc】RemoteAudioSource的创建线程

m98 代码&#xff1a;I:\webrtc m98_yjf\src\pc\rtp_transmission_manager.cc RtpTransmissionManager::CreateReceiver 在信令线程创建receiver receiver 是&#xff1a; rtc::scoped_refptr<RtpReceiverProxyWithInternal<RtpReceiverInternal>>receiver;其实际…

uniapp微信小程序开发踩坑日记:由于图表数据渲染不出来,我第一次在项目中用watch函数监听数据变化

一、发现问题 在我们团队自己开发的微信小程序中&#xff0c;引入了Echarts图表库 然后突然有一天&#xff0c;后端队友反应图表渲染有问题。后面我去试了一下&#xff0c;确实20次里面必有一次数据渲染不出来 断定代码没问题&#xff0c;于是我们将其鉴定为玄学 二、问题原因…

笔记本无线网络共享给有线使用

1.鼠标右击wifi图标选择打开网络和Internet设置 2.选择WLAN项&#xff0c;点击进入更改适配器选项 3.进入到以下界面&#xff0c;右击以太网选择启动&#xff08;不确定的话可以在设备管理器查看网卡&#xff09; 4.右击WLAN选项&#xff0c;点击属性 5.点击共享&#xff0…

日期类的实现,const成员

目录 一&#xff1a;日期类实现 二&#xff1a;const成员 三&#xff1a;取地址及const取地址操作符重载 一&#xff1a;日期类实现 //头文件#include <iostream> using namespace std;class Date {friend ostream& operator<<(ostream& out, const Dat…

AI大模型探索之路-训练篇9:大语言模型Transformer库-Pipeline组件实践

系列篇章&#x1f4a5; AI大模型探索之路-训练篇1&#xff1a;大语言模型微调基础认知 AI大模型探索之路-训练篇2&#xff1a;大语言模型预训练基础认知 AI大模型探索之路-训练篇3&#xff1a;大语言模型全景解读 AI大模型探索之路-训练篇4&#xff1a;大语言模型训练数据集概…

大连宇都环境 | 成都5月水科技大会暨技术装备成果展览会

中华环保联合会水环境治理专业委员会 秘书处 王小雅 13718793867 —— 展位号&#xff1a;A09 —— 一、企业介绍 大连宇都环境成立于2002年&#xff0c;公司20年 MBBR填料产品及工艺技术&#xff0c;&#xff0c;构建了研发、制造、设计、工程、运营链式服务能力&#xff…

CGAL 点云数据生成DSM、DTM、等高线和数据分类

原文链接 CGAL 点云数据生成DSM、DTM、等高线和数据分类 - 知乎 在GIS应用软件中使用的许多传感器(如激光雷达)都会产生密集的点云。这类应用软件通常利用更高级的数据结构&#xff1a;如&#xff1a;不规则三角格网 (TIN)是生成数字高程模型 (DEM) 的基础&#xff0c;也可以利…

docker系列8:容器卷挂载(上)

传送门 docker系列1&#xff1a;docker安装 docker系列2&#xff1a;阿里云镜像加速器 docker系列3&#xff1a;docker镜像基本命令 docker系列4&#xff1a;docker容器基本命令 docker系列5&#xff1a;docker安装nginx docker系列6&#xff1a;docker安装redis docker系…

1.初探MPI——MPI简介

系列文章目录 初探MPI——MPI简介初探MPI——点对点通信初探MPI——集体通信 文章目录 系列文章目录前言一、MPI_COMM_WORLD, size and ranks二、Hello WorldInstructions 总结参考 前言 Message Passing Interface (MPI) 是一种标准化的消息传递库接口规范。该标准是消息传递…

结构体的对齐原则

一、C语言结构体对齐步骤: 1.每个成员对齐 2.总体对齐 二、C语言结构体对齐规则: 1.结构体第一个成员存放在相较于结构体变量起始位置的偏移量为0的位置 2.从第二个成员开始&#xff0c;往后的每一个成员都要对齐到某个对齐数的整数倍处。 对齐数&#xff1a;结构体成员自身的…

C 408—《数据结构》图、查找、排序专题考点(含解析)

目录 Δ前言 六、图 6.1 图的基本概念 6.2 图的存储及基本操作 6.3 图的遍历 6.4 图的应用 七、查找 7.2 顺序查找和折半查找 7.3 树型查找 7.4 B树和B树 7.5 散列表 八、排序 8.2 插入排序 8.3 交换排序 8.4 选择排序 8.5 归并排序和基数排序 8.6 各种内部排序算法的比较及…

33.基础乐理-原调、移调、转调、离调

原调、移调、转调、离调分为两类&#xff1a;原调是一个定义、一个名词&#xff0c;移调、转调、离调可以称之为是技术或者操作&#xff0c;是一种动词。也就是分为名词和动词两类。 原调&#xff1a; 一种音乐原本的调&#xff0c;就是它的原调&#xff0c;或者说按照简谱的调…

Codeforces Round 941 (Div. 2) (A~D)

1966A - Card Exchange 题意&#xff1a; 思路&#xff1a;手玩一下发现当存在某个数字个数超过k个&#xff0c;那么就能一直操作下去。那么答案就是k-1. void solve() {cin >> n >> m;map<int,int>mp;int maxx 1;for(int i 0 ; i < n ; i ){int x;c…

【热闻速递】Google 裁撤 Python研发团队

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 【&#x1f525;热闻速递】Google 裁撤 Python研发团队引入研究结论 【&#x1f5…

Android AOSP探索之Ubantu下Toolbox的安装

文章目录 概述安装Toolbox解决运行的问题 概述 由于最近需要进军android的framework,所以需要工具的支持&#xff0c;之前听说江湖上都流传source insight,我去弄了一个破解版&#xff0c;功能确实强大&#xff0c;但是作为多年android开发的我习惯使用android studio。虽然使…

SpringWebFlux RequestBody多出双引号问题——ProxyPin抓包揪出真凶

缘起 公司有个服务做埋点收集的&#xff0c;可以参考我之前的文章埋点日志最终解决方案&#xff0c;今天突然发现有些数据日志可以输出&#xff0c;但是没法入库。 多出的双引号 查看Flink日志发现了JSON解析失败&#xff0c;Flink是从Kafka拿数据&#xff0c;Kafka本身不处…

2024深圳杯数学建模竞赛D题(东三省数学建模竞赛D题):建立非均质音板振动模型与参数识别模型

更新完整代码和成品完整论文 《2024深圳杯&东三省数学建模思路代码成品论文》↓↓↓&#xff08;浏览器打开&#xff09; https://www.yuque.com/u42168770/qv6z0d/zx70edxvbv7rheu7?singleDoc# 2024深圳杯数学建模竞赛D题&#xff08;东三省数学建模竞赛D题&#xff0…

深入理解多层感知机MLP

1. 基础理论 神经网络基础&#xff1a; 目标&#xff1a;了解神经网络的结构&#xff0c;包括神经元、权重、偏置和激活函数。 神经网络是由多个层次的神经元组成的网络&#xff0c;它模拟了人脑处理信息的方式。每个神经元可以接收输入、处理输入并生成输出。这一过程涉及到…

Vue项目打包APK----Vue发布App

时隔多年我又来跟新了&#xff0c;今天给大普家及下前端Vue傻瓜式发布App&#xff0c;话不多说直接上干货。 首先准备开发工具HBuilder X&#xff0c;去官网直接下载即可&#xff0c;算了直接给你们上地址吧HBuilderX-高效极客技巧。 打开软件&#xff0c;文件-->新建--&g…

通用漏洞评估系统CVSS4.0简介

文章目录 什么是CVSS&#xff1f;CVSS 漏洞等级分类历史版本的 CVSS 存在哪些问题&#xff1f;CVSS 4.0改进的“命名法”改进的“基本指标”考虑“OT/IOT”新增的“其他指标”CVSS 4.0存在的问题 Reference: 什么是CVSS&#xff1f; 在信息安全评估领域&#xff0c;CVSS为我们…