仓库

觉得写的好就给个star吧 https://github.com/taosu0216/go_stu/tree/main/IM_project

IM即时消息系统

照着马士兵的做的 https://www.bilibili.com/video/BV1rK4y1w7JB/?p=5&spm_id_from=pageDriver&vd_source=593e95872463bd08c88726bf6aade29c 记一下中间的笔记

Day1

下载gorm go get gorm.io/gorm 前面下错成 jinzhu/gorm 了,但是说那个用的少 测试单独新建test目录 gorm文档https://gorm.io/zh_CN/docs/index.html msb的官方代码库https://git.mashibing.com/msb_47094/GinChat go官方文档(用来搜第三方包)https://pkg.go.dev/

gorm官方文档的入门例子

package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

func main() {
  db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

  // 迁移 schema
  db.AutoMigrate(&Product{})

  // Create
  db.Create(&Product{Code: "D42", Price: 100})

  // Read
  var product Product
  db.First(&product, 1) // 根据整型主键查找
  db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

  // Update - 将 product 的 price 更新为 200
  db.Model(&product).Update("Price", 200)
  // Update - 更新多个字段
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

  // Delete - 删除 product
  db.Delete(&product, 1)
}

视频里把sqlite改成了mysql,用phpstudy应该也可以,但为了统一还是搞一下mysql吧

选择内存大的那个下载,然后一路下一步就行(甲骨云真他妈麻烦)

- Server only

- execute
- 同意
- Use Legacy Authentication Method (Retain MySOL 5.x Compatibility(第二个)
- 一路next

验证是否安装成功
```C:\Program Files\MySQL\MySQL Server 8.0\bin```
打开cmd(powershell不行!!!),运行```mysql -h localhost -u root -p ```,然后输入密码,显示mysql>即安装成功
也可以打开```MySQL 8.0 Command Line Client```快捷进入

才发现原来博客的笔记一整个完整的数据库操作都没有,麻了

```mysql
//创立新的数据库,打分号!!!
create database test_2023_10_12;
//选择数据库
use test_2023_10_12;
//建立表
CREATE TABLE test (
    time float NOT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
//删除表
drop table test;
//显示已有的表,打分号!!!
show tables;

剩下的明天再搞

Day2

//查看一个表的结构
desc test(table name);
//查看一个表的数据
select * from test;
//重来一遍
create database IM;
use IM;
create table test(
id float
);
//添加字段
//是alter!!!
alter table test add email int;
alter table test add username char(255);
alter table test add passwd char(255);
//修改字段数据类型
 alter table test modify email char(255);
//向字段添加数据
insert into test (username,passwd,email,id) values ('taosu','123456','[email protected]',1);
//多创建几个用户用于测试
insert into test (username,passwd,email,id) values ('yblue','qwerty','[email protected]',2);
insert into test (username,passwd,email,id) values ('qlu_coder','my_password','[email protected]',3);

/*
mysql> select * from test;
+----+------------------+-----------+-------------+
| id | email            | username  | passwd      |
+----+------------------+-----------+-------------+
|  1 | [email protected] | taosu     | 123456      |
|  2 | [email protected]       | yblue     | qwerty      |
|  3 | [email protected]     | qlu_coder | my_password |
+----+------------------+-----------+-------------+
3 rows in set (0.00 sec)
*/

暂时跟mysql告一段落了应该,感觉还是直接用phpstudy就行了,没必要安mysql

testGorm.go的操作

//第一行修改成这样,将sqlite改为mysql
//"用户名:密码@tcp(地址:端口)/数据库名"
//其他连接选项:charset=utf8 表示使用UTF-8字符集,parseTime=True 表示GORM将尝试解析时间字段,loc=Local 表示使用本地时区。
db, err := gorm.Open(mysql.Open("root:root@tcp(127.0.0.1:3306)/IM?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{})

完整的

package main

import (
	"IM_project/models"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type Product struct {
	gorm.Model
	Code  string
	Price uint
}

func main() {
	//"用户名:密码@tcp(地址:端口)/数据库名"
	db, err := gorm.Open(mysql.Open("root:root@tcp(127.0.0.1:3306)/IM?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	// 迁移 schema
	db.AutoMigrate(&models.UserBasic{})

	// Create
	//这里连接IM数据库后,会自动使用结构体名作为表名(按理说是全变小写+复数,但是我看的没加复数,只变成小写了)
	user:=&models.UserBasic{}
	user.Name="taosu"
	db.Create(user)

	// Read
	fmt.Println("db.First(user, 1) : ",db.First(user, 1))
    
	// Update - 将 user 的 password 更新为 1234
	db.Model(user).Update("PassWord", "1234")
}

/*
打印结果
db.First(user, 1) :  &{0xc0000ec510 <nil> 1 0xc000068000 0}
*/

迁移

在数据库领域,”迁移”(Migration)是指通过代码和脚本来管理数据库模式(结构)的变化。迁移通常用于以下情况:

目录创建

提前创建用于存放各种东西的目录

router创建app.go,将原来的UserBasic.go改名成user_basic.go(这里不加_会报错,很怪)

逻辑

main函数

package main

import (
	"IM_project/router"
)

func main() {
	r := router.Router()
	r.Run("127.0.0.1:8099")
}

在main函数中调用router函数,返回值r是 *gin.Engine,然后r运行在本地的8099端口

IM_project/router中的router函数(app.go)

package router

import (
	"IM_project/service"

	"github.com/gin-gonic/gin"
)

func Router() *gin.Engine {
	//创建基本路由
	r := gin.Default()
	//处理方式放在service中处理,这里不是不是service.GetIndex()
	r.GET("/index", service.GetIndex)
	return r
}

首先使用gin框架创建基本路由,这里的r也是 *gin.Engine类型,然后在有用户使用get请求并且请求路径为uri/index时,调用service包的GetIndex函数,注意这里是调用service.GetIndex的函数,而不是直接使用,是告诉程序有请求时,将请求发送到service.GetIndex函数,所以这里只写函数名,而不是service.GetIndex()

报错修复

在下一个视频中,吧LoginTime,HeartbeatTime和LoginOutTime的类型从uint64修改成了time.Time,但是运行时会报错

PS IM_project> go run "IM_project\test\testGorm.go"

2023/10/13 13:58:18 IM_project/test/testGorm.go:31 Error 1292 (22007): Incorrect datetime value: '0000-00-00' for column 'login_time' at row 1
[17.203ms] [rows:0] INSERT INTO `user_basic` (`created_at`,`updated_at`,`deleted_at`,`name`,`pass_word`,`identity`,`phone`,`email`,`client_ip`,`client_port`,`login_time`,`heartbeat_time`,`login_out_time`,`is_logout`,`device_info`) VALUES ('2023-10-13 13:58:18.207','2023-10-13 13:58:18.207',NULL,'taosu','','','','','','','0000-00-00 00:00:00','0000-00-00 00:00:00','0000-00-00 00:00:00',false,'')

2023/10/13 13:58:18 IM_project/test/testGorm.go:34 record not found
[1.232ms] [rows:0] SELECT * FROM `user_basic` WHERE `user_basic`.`id` = 1 AND `user_basic`.`deleted_at` IS NULL ORDER BY `user_basic`.`id` LIMIT 1
db.First(user, 1) :  &{0xc00012e480 record not found 0 0xc00028d880 0}

2023/10/13 13:58:18 IM_project/test/testGorm.go:37 WHERE conditions required
[0.475ms] [rows:0] UPDATE `user_basic` SET `pass_word`='1234',`updated_at`='2023-10-13 13:58:18.227' WHERE `user_basic`.`deleted_at` IS NULL  

这里很迷,明明grom.Model里的两个时间都是time.Time类型的,但是能用,我这里把自定义的改成time.Time类型就不可以了

问了问gpt,原因是mysql库现在是严格模式,对数据的格式很严,然后time.Time的零值好像是格式有点问题,是存不进去的,然后可以修改成兼容模式 打开mysql终端

//查看当前模式
SELECT @@sql_mode;
//修改成兼容模式
SET GLOBAL sql_mode = 'NO_ENGINE_SUBSTITUTION';

不报错了

第二天有报错了,这玩意好像每重启电脑就得设置一遍

继续

创建utils目录,建立system_init.go,在config目录下创建app.yml

“utils” 是一个缩写,通常用于描述一组实用工具或函数,这些工具和函数可以帮助程序员更容易地完成一些常见的任务,例如处理日期和时间、处理字符串、读写文件、执行数学运算等。这些工具旨在节省开发时间和减少代码的冗余,因此开发人员可以更轻松地构建应用程序。所以,当你在代码中看到 “utils”,它通常指的是一组实用的程序代码,用于处理各种常见编程任务。

Day3

viper

下载

导入需要
```go
import "github.com/spf13/viper"

Viper能够为你执行下列操作:

Viper会按照下面的优先级。每个项目的优先级都高于它下面的项目:

!!! 说人话就是帮忙管理yml,json等这种配置文件的工具 !!!

现在有种感觉就是写之前那个test,就是为了把请求的过程一点点分成很多份,然后分给不同的函数,比如本来test的请求就是直接连接数据库,然后这里是把数据库的信息存放在yml文件中,然后通过viper管理并获取,然后拼接到初始化mysql连接的函数中,然后需要时再调用初始化函数,可能是为了安全和方便修改?

InitMySQL调整

本来是直接mysql.dns获取,改成分批获取配置,增加可读性,更好维护

app.yml

mysql:
  dns: root:root@tcp(127.0.0.1:3306)/IM?charset=utf8mb4&parseTime=True&loc=Local
  username: root
  passwd: root
  host: 127.0.0.1
  port: 3306
  db: IM
  options:
    charset: utf8mb4
    parseTime: true
    loc: Local

InitMySQL

func InitMySQL() {
	username := viper.GetString("mysql.username")
	passwd := viper.GetString("mysql.passwd")
	host := viper.GetString("mysql.host")
	port := viper.GetString("mysql.port")
	db := viper.GetString("mysql.db")
	options := viper.Sub("mysql.options")
	charset := options.GetString("charset")
	parseTime := options.GetBool("parseTime")
	loc := options.GetString("loc")
	databaseURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%t&loc=%s", username, passwd, host, port, db, charset, parseTime, loc)
	DB, _ = gorm.Open(mysql.Open(databaseURL), &gorm.Config{})
}

小问题

username := viper.GetString(“mysql.username”) 这里是直接获取mysql.username,万一后面不止获取app.yml,又获取了app2.yml,并且app2.yml也有mysql.username这个配置项,这个语句会获取哪个信息

可能是把所有配置都放在app.yml文件里了所以没有这个问题?

swag

go install github.com/swaggo/swag/cmd/swag@latest
//废了一个多小时,勉强找到了算是解决办法的办法,傻逼东西,纯nt
//根本原因就是GOBIN跟GOPATH不一样,东西下载在GOPATH的bin目录下,但是调用的时候是用GOBIN路径,但是网上说的设置GOBIN路径的没有一个是能用的,最后把GOPATH的exe复制到GOBIN里,直接完事
//最后找着了,是在用户变量里新建GOBIN,配置路径
//go env GOBIN出了一次配好的路径,再来一次就没了

Day4

昨晚装了一晚上没弄好,回宿舍又找了n久,说可能得调GOROOT的路径,然后今天来了一试,GOPATH它又出来了,难道重启电脑真是解决问题的最好答案吗

swag

//今天又是跟swagger不死不休的一天
//总结,浪费了快9个,得到了还是得用goland不能用vsc的道理
//像群友说的一样拥抱goland好了

//具体操作没啥好说的了
swag init
swag init -g /service/index.html
go run main.go

配下goland

Day5

现在看看swag的功能确实挺方便的,但不知道为什么vsc里打开的终端就是不能用

捣鼓了昨天一天,对这玩意大概了解了. 就是一个类似apifox自动化测试的东西,在后端写出了业务逻辑需要测试,要么就手动curl发请求,要么就是apifox写好请求发过去,然后这个swag是在程序界面,用特定的方式(就是// @)写好注释,在init之后,就会在url/swagger/index.html生成对应的请求方法,就是比较方便(好像还有自动写文档的功能?没用到还)

go install github.com/swaggo/swag/cmd/swag@latest
//然后使用时会自动导包
//app.go
//前面的是别名
	"IM_project/docs"
	"IM_project/service"

	"github.com/gin-gonic/gin"
	swaggerfiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"

剩下的现用现查吧,主要东西都在注释里了

new

增删改查(CRUD)正式收工 Create Read Update Delete

对U的信息校验

比如说在手机号那里输字母,进行这种校验

go get github.com/asaskevich/govalidator

新加修改手机号和邮箱

防止已注册的手机号/名字/邮箱重复注册

魔改了不少,终于有点自己的想法了

Day6

加盐加密

// 小写
func Md5Encode(data string) string {
	h := md5.New()
	h.Write([]byte(data))
	tempStr := h.Sum(nil)
	return hex.EncodeToString(tempStr)
}

// 大写
func MD5Encode(data string) string {
	return strings.ToUpper(Md5Encode(data))
}

// 加密
func MakePassword(plainpwd, salt string) string {
	return Md5Encode(plainpwd + salt)
}

// 解密
func ValidPassword(plainpwd, salt string, passwd string) bool {
	return Md5Encode(plainpwd+salt) == passwd
}

/*
整体逻辑
1.传入用户输入的密码和随机生成的盐,并拼接字符串
2.将拼接生成的新字符串传入md5加密函数
3.对拼接的字符串,对其进行 MD5 加密,然后返回结果的十六进制表示。
4.将加密后的密码和盐传入数据库
*/

在密码学和计算机安全领域,散列(Hash)是一种将任意长度的数据映射为固定长度数据的过程。它的核心目标是将输入数据(称为消息)通过一种数学算法,转换为固定长度的字符串,通常是一串数字和字母组成的十六进制值。这个输出字符串通常称为“散列值”或“摘要”。

散列函数有以下特点:

散列函数在信息安全中有多种应用,包括密码存储、数据完整性验证、数字签名、数字证书等领域。常见的散列算法包括 MD5、SHA-1、SHA-256 等,其中 SHA 系列较为安全,因此在许多安全应用中被广泛使用。但请注意,随着计算能力的增强,一些散列算法可能不再足够安全,需要采用更强大的算法。

用户登录

post请求,更安全点

修改用户信息

盐重新生成并修改数据库中的盐 加了id校验,视频里面是没有这个的(也可能在后面我还没看到) 但感觉好像没必要,因为真要修改信息的话,肯定是先确定账密(也可能再调用一遍login?),然后修改,不会让用户输入id进行判断是否存在,这个功能后面应该是得删掉的

// UpdateUser
// @Summary 修改用户信息
// @Tags 用户模块
// @Param id formData string false "id"
// @Param name formData string false "用户名"
// @Param password formData string false "密码"
// @Param email formData string false "邮箱"
// @Param phone formData string false "手机号"
// @Success 200 {string} json{"code","message"}
// @Router /user/updateUser [post]
func UpdateUser(c *gin.Context) {
	//根据用户id修改信息,但是id不存在时未进行校验
	user := models.UserBasic{}
	id, err := strconv.Atoi(c.PostForm("id"))
	if err != nil {
		log.Fatalln(err)
	}
	user.ID = uint(id)
	is_Exit, _ := models.FindUserById(user.ID)
	if !is_Exit {
		c.JSON(400, gin.H{
			"message": "用户不存在!",
		})
		return
	}
	user.Name = c.PostForm("name")
	passwd := c.PostForm("password")
	salt := fmt.Sprintf("%06d", rand.Int31())
	user.PassWord = utils.MakePassword(passwd, salt)
	user.Phone = c.PostForm("phone")
	user.Email = c.PostForm("email")
	user.Salt = salt
	//对用户输入信息进行校验
	//调用govalidator包的ValidateStruct方法对传入的结构体进行内容校验,校验方法就是在定义结构体时`valid:""`里写的内容
	_, err = govalidator.ValidateStruct(user)
	if err != nil {
		fmt.Println(err)
		c.JSON(400, gin.H{
			"message": "修改用户信息格式错误!",
		})
		return
	} else {
		err = models.UpdateUser(user)
		if err != nil {
			log.Fatalln(err)
		}
		fmt.Println("update :", user)
		c.JSON(200, gin.H{
			"message": "修改用户信息成功",
		})
	}
}


//finduserbyid
unc FindUserById(id uint) (bool, UserBasic) {
	user := UserBasic{}
	utils.DB.Where("id = ?", id).First(&user)
	if user.ID != 0 {
		return true, user
	}
	return false, user
}

新学的东西

思路

对请求分批处理,分成很多小份,交给不同的程序处理

函数

gin相关

//返回值是*gin.Engine
r:=gin.Default()
//类似http.HandleFunc("/", indexHandler),用当请求index时,调用service.GetIndex函数处理
r.Get("index",service.GetIndex)

viper相关

//设置获取文件的路径,即在./config目录下查找对应文件
viper.AddConfigPath("config")
//设置文件名字,即在./config目录下查找名为"app"的文件,因为能获取各种配置文件,所以不用写后缀名/扩展名
viper.SetConfigName("app")
//检测是否能正常读取配置文件
err := viper.ReadInConfig()
//在读取到app.yml文件后,获取mysql.username这个键对应的值(),并强制转换成string类型变量
username := viper.GetString("mysql.username")
//获取配置中的子配置项
options := viper.Sub("mysql.options")
charset := options.GetString("charset")

gorm相关

//gorm这里是数据库类型(MySQL、PostgreSQL、SQLite等)
//opts是一个可选参数列表,用于传递额外的选项,以配置数据库连接。这些选项是 gorm.Option 类型的可变参数。你可以使用这些选项来配置数据库的行为,例如启用日志记录、设置连接池大小等,这里是&gorm.Config{},这里没再填写具体参数,填写的话可以对数据库连接进行更精细的操作,比如打开日志,但是为什么传递指针还不是很明白,这里是空的,所以直接不写这个也可以。
mysql.Open("root:root@tcp(127.0.0.1:3306)/IM)
func gorm.Open(dialector gorm.Dialector, opts ...gorm.Option) (db *gorm.DB, err error)

//db:这是一个 *gorm.DB 类型的指针,表示成功打开的数据库连接。*gorm.DB 是 GORM 库的主要接口,用于执行数据库操作。
//err:这是一个 error 类型的返回值,用于表示在打开数据库连接时是否发生了错误。如果打开成功,err 为 nil;如果发生错误,将包含错误信息。
db, err := gorm.Open(mysql.Open("root:root@tcp(127.0.0.1:3306)/IM?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})

不知道放在哪的


Q: 那用户能在页面看到message" 字段的值 "welecome!!",是通过c.JSON()还是gin.H{},就是是哪个函数把message" 字段的值 "welecome!!"打印在页面上传给用户的

A: 在用户的浏览器页面上看到 "message" 字段的值 "welecome!!" 实际上是通过 c.JSON() 这个函数实现的。
c.JSON(200, ...) 函数用于生成 JSON 格式的响应,其中的参数是 HTTP 状态码和包含要返回的 JSON 数据的 Go 数据结构。在这里,gin.H{ "message": "welecome!!" } 构建了 JSON 数据,而 c.JSON(200, ...) 把这个 JSON 数据包装成 HTTP 响应并发送给客户端。
这意味着用户在浏览器上看到的 "message" 字段的值 "welecome!!" 是由 c.JSON(200, ...) 函数生成的响应传输到客户端的结果。所以,c.JSON() 函数实际上是将 JSON 数据发送给用户的一部分,而 gin.H{} 只是用于构建 JSON 数据的辅助工具。

数据结构

//1.
gin.Default()返回值
*gin.Engine

//2.
gorm.Model
type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}

//3.
*gorm.DB

//4.
*gin.Context
//r.GET("/user/getUserList", service.GetUserList)
//当有请求进行访问/user/getUserList时, service.GetUserList()会接收到一个  
//c *gin.Context类型的变量,这里的c是一个上下文对象,通过这个上下文对象,可以访问请求的参数、请求头、请求体,以及设置响应等操作。

Category: go | Tags: go,projects | Created: 2024-11-30 16:56:55