基于go+vue的多人在线聊天的im系统

基于go+vue的多人在线聊天的im系统

在这里插入图片描述

文章目录

  • 基于go+vue的多人在线聊天的im系统
    • 一、前端部分
    • 二、后端部分
      • 1、中间件middleware设计jwt和cors
      • 2、配置文件设计
      • 3、Mysql和Redis连接
      • 4、路由设计
      • 5、核心功能设计

一、前端部分

打算优化一下界面,正在开发中。。。

二、后端部分

1、中间件middleware设计jwt和cors

jwt.go

package middlewaresimport ("crypto/rsa""fmt""github.com/dgrijalva/jwt-go""github.com/gin-gonic/gin""im/global""io/ioutil""net/http""os""time"
)func JWT() gin.HandlerFunc {return func(ctx *gin.Context) {// 从请求头获取tokentoken := ctx.Request.Header.Get("w-token")if token == "" {ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "请登录",})return}// 打开存储公钥文件file, _ := os.Open(global.SrvConfig.JWTInfo.PublicKeyPath)// 读取公钥文件bytes, _ := ioutil.ReadAll(file)// 解析公钥publickey, _ := jwt.ParseRSAPublicKeyFromPEM(bytes)jwtVerier := &JWTTokenVerifier{PublicKey: publickey}claim, err := jwtVerier.Verify(token)if err != nil {fmt.Println(err)ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "请登录",})return}ctx.Set("claim", claim)        //获取全部信息ctx.Set("name", claim.Subject) // 获取用户名ctx.Next()}
}func Auth(token string) (*MyClaim, error) {if token == "" {return nil, fmt.Errorf("ws认证失败,token为空")}file, _ := os.Open(global.SrvConfig.JWTInfo.PublicKeyPath)bytes, _ := ioutil.ReadAll(file)publickey, _ := jwt.ParseRSAPublicKeyFromPEM(bytes)jwtVerier := &JWTTokenVerifier{PublicKey: publickey}return jwtVerier.Verify(token)
}type JWTTokenVerifier struct {// 存储用于验证签名的公钥PublicKey *rsa.PublicKey
}type MyClaim struct {Role intjwt.StandardClaims
}func (v *JWTTokenVerifier) Verify(token string) (*MyClaim, error) {t, err := jwt.ParseWithClaims(token, &MyClaim{},func(*jwt.Token) (interface{}, error) {return v.PublicKey, nil})if err != nil {return nil, fmt.Errorf("cannot parse token: %v", err)}if !t.Valid {return nil, fmt.Errorf("token not valid")}clm, ok := t.Claims.(*MyClaim)if !ok {return nil, fmt.Errorf("token claim is not MyClaim")}if err := clm.Valid(); err != nil {return nil, fmt.Errorf("claim not valid: %v", err)}return clm, nil}type JWTTokenGen struct {privateKey *rsa.PrivateKeyissuer     stringnowFunc    func() time.Time
}func NewJWTTokenGen(issuer string, privateKey *rsa.PrivateKey) *JWTTokenGen {return &JWTTokenGen{issuer:     issuer,nowFunc:    time.Now,privateKey: privateKey,}
}func (t *JWTTokenGen) GenerateToken(userName string, expire time.Duration) (string, error) {nowSec := t.nowFunc().Unix()tkn := jwt.NewWithClaims(jwt.SigningMethodRS512, &MyClaim{StandardClaims: jwt.StandardClaims{Issuer:    t.issuer,IssuedAt:  nowSec,ExpiresAt: nowSec + int64(expire.Seconds()),Subject:   userName,},})return tkn.SignedString(t.privateKey)
}

cors.go

package middlewaresimport ("github.com/gin-gonic/gin""net/http"
)func Cors() gin.HandlerFnc {return func(c *gin.Context) {method := c.Request.Methodc.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, w-token")c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")c.Header("Access-Control-Allow-Credentials", "true")if method == "OPTIONS" {c.AbortWithStatus(http.StatusNoContent)}}
}

2、配置文件设计

config.yaml

jwt:privateKeyPath: ./config/private.keypublicKeyPath: ./config/public.key
port: 9288
name: user-web
redis:ip: port: 6379
mysql:ip: username: password: db_name: im

config/config.go

package config// 读取yaml配置文件,形成映射的相关类
type JWTconfig struct {PrivateKeyPath string `mapstructure:"privateKeyPath" json:"privateKeyPath"`PublicKeyPath  string `mapstructure:"publicKeyPath" json:"publicKeyPath"`
}type RedisConfig struct {IP   string `mapstructure:"ip"`Port string `mapstructure:"port"`
}type MysqlConfig struct {IP       string `mapstructure:"ip"`Username string `mapstructure:"username"`Password string `mapstructure:"password"`DbName   string `mapstructure:"db_name"`
}type SrvConfig struct {Name      string      `mapstructure:"name" json:"name"`Port      int         `mapstructure:"port" json:"port"`JWTInfo   JWTconfig   `mapstructure:"jwt" json:"jwt"`RedisInfo RedisConfig `mapstructure:"redis" json:"redis"`MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`
}

initalize/config.go

package Initializeimport ("fmt""github.com/fsnotify/fsnotify""github.com/spf13/viper""im/global"
)func InitConfig() {//从配置文件中读取出对应的配置var configFileName = fmt.Sprintf("./config.yaml" )v := viper.New()//文件的路径v.SetConfigFile(configFileName)if err := v.ReadInConfig(); err != nil {panic(err)}// 开启实时监控v.WatchConfig()//这个对象如何在其他文件中使用 - 全局变量if err := v.Unmarshal(&global.SrvConfig); err != nil {panic(err)}// 文件更新的回调函数v.OnConfigChange(func(in fsnotify.Event) {fmt.Println("配置改变")if err := v.Unmarshal(&global.SrvConfig); err != nil {panic(err)}})
}func GetEnvInfo(env string) bool {viper.AutomaticEnv()return viper.IsSet(env)
}

global.go 声明全局变量

package globalimport ("github.com/go-redis/redis/v8""github.com/jinzhu/gorm""im/config""sync"
)var (// 配置信息SrvConfig = config.SrvConfig{}// 分别管理存储已注册用户和在线用户// 已注册用户map,key为name value为passwordUserMap = sync.Map{}// 在线用户map,key为name value为连接句柄listLoginMap = sync.Map{}// redis客户端Redis *redis.Client// db服务DB *gorm.DB
)

3、Mysql和Redis连接

db.go

package Initializeimport ("fmt""github.com/jinzhu/gorm"_ "github.com/jinzhu/gorm/dialects/mysql""im/global""os"
)var err errorfunc InitDB() {// 构建数据库连接字符串dbConfig := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",global.SrvConfig.MysqlInfo.Username,global.SrvConfig.MysqlInfo.Password,global.SrvConfig.MysqlInfo.IP,global.SrvConfig.MysqlInfo.DbName)// 连接数据库global.DB, err = gorm.Open("mysql", dbConfig)if err != nil {fmt.Println("[Initialize] 数据库连接失败:%v", err)return}// 设置连接池参数global.DB.DB().SetMaxIdleConns(10)     //设置数据库连接池最大空闲连接数global.DB.DB().SetMaxOpenConns(100)    //设置数据库最大连接数global.DB.DB().SetConnMaxLifetime(100) //设置数据库连接超时时间// 测试数据库连接if err = global.DB.DB().Ping(); err != nil {fmt.Printf("[Initialize] 数据库连接测试失败:%v\n", err)os.Exit(0)}fmt.Println("[Initialize] 数据库连接测试成功")
}

redis.og

package Initializeimport ("context""fmt""github.com/go-redis/redis/v8""im/global""log""sync""time"
)var once sync.Oncefunc InitRedis() {addr := fmt.Sprintf("%v:%v", global.SrvConfig.RedisInfo.IP, global.SrvConfig.RedisInfo.Port)// once.Do() 在一个应用程序生命周期内只会执行一次once.Do(func() {global.Redis = redis.NewClient(&redis.Options{Network:      "tcp",Addr:         addr,Password:     "",DB:           0,               // 指定Redis服务器的数据库索引,0为默认PoolSize:     15,              // 连接池最大连接数MinIdleConns: 10,              // 连接池最小连接数DialTimeout:  5 * time.Second, // 连接超时时间ReadTimeout:  3 * time.Second, // 读超时时间WriteTimeout: 3 * time.Second, // 写超时时间PoolTimeout:  4 * time.Second, // 连接池获取连接的超时时间IdleCheckFrequency: 60 * time.Second,IdleTimeout:        5 * time.Minute,MaxConnAge:         0 * time.Second,MaxRetries:      0,MinRetryBackoff: 8 * time.Millisecond,MaxRetryBackoff: 512 * time.Millisecond,})pong, err := global.Redis.Ping(context.Background()).Result()if err != nil {log.Fatal(err)}log.Println(pong)})
}

4、路由设计

	// 注册r.POST("/api/register", handle.Register)// 已注册用户列表r.GET("/api/list", handle.UserList)// 登录r.POST("/api/login", handle.Login)// ws连接r.GET("/api/ws", handle.WS)// 获取登录列表(目前没用到)r.GET("/api/loginlist", handle.LoginList)// JWTr.Use(middlewares.JWT())// 获取用户名r.GET("/api/user", handle.UserInfo)

5、核心功能设计

handle/handle.go

package handleimport ("fmt""github.com/dgrijalva/jwt-go""github.com/gin-gonic/gin""im/global""im/middlewares""im/mysql""io/ioutil""net/http""os""time"
)type Reg struct {Name     string `json:"name"`Password string `json:"password"`
}
type UList struct {Names []string `json:"names"`
}
type LoginStruct struct {Name     string `json:"name" `Password string `json:"password" `
}func Register(c *gin.Context) {var reg Regerr := c.Bind(&reg)if err != nil {fmt.Println(err)c.JSON(http.StatusOK, gin.H{"msg":  "用户名或密码格式错误,请重试","code": "4001",})return}mysql.StorageUserToMap()_, ok := global.UserMap.Load(reg.Name)if ok {fmt.Println("用户已存在")c.JSON(http.StatusOK, gin.H{"msg":  "用户已存在,请登录或更换用户名注册","code": "4000",})return}if err := mysql.AddUserToMysql(reg.Name, reg.Password); err != nil {c.JSON(http.StatusInternalServerError, gin.H{"msg":  "内部错误","code": "5000",})return}mysql.StorageUserToMap()c.JSON(http.StatusOK, gin.H{"msg":  "创建用户成功,请登录","code": "2000",})
}func Login(c *gin.Context) {var loginData LoginStructerr := c.Bind(&loginData)if err != nil {fmt.Println(err)c.JSON(http.StatusOK, gin.H{"msg":  "用户名或密码格式错误,请重试","code": "4001",})return}psw, ok := global.UserMap.Load(loginData.Name)if !ok {fmt.Println("用户不存在")c.JSON(http.StatusOK, gin.H{"msg":  "用户不存在,请注册","code": "4003",})return}if loginData.Password != psw.(string) {c.JSON(http.StatusOK, gin.H{"msg":  "密码错误,请重新输入","code": "4005",})return}file, err := os.Open(global.SrvConfig.JWTInfo.PrivateKeyPath)if err != nil {fmt.Println(err)return}pkBytes, err := ioutil.ReadAll(file)privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(pkBytes))tokenGen := middlewares.NewJWTTokenGen("user", privateKey)token, err := tokenGen.GenerateToken(loginData.Name, time.Hour*24*20)if err != nil {fmt.Println(err)return}c.JSON(http.StatusOK, &gin.H{"msg":   "登录成功","code":  "2000","name":  loginData.Name,"token": token,})
}func LoginList(c *gin.Context) {var users UListglobal.LoginMap.Range(func(key, value interface{}) bool {users.Names = append(users.Names, key.(string))return true})c.JSON(http.StatusOK, &users)
}func getLoginList() *UList {var users UListglobal.LoginMap.Range(func(key, value interface{}) bool {users.Names = append(users.Names, key.(string))return true})return &users
}func UserInfo(c *gin.Context) {name, _ := c.Get("name")userName := name.(string)c.JSON(http.StatusOK, gin.H{"msg":  "成功","code": "2000","name": userName,})
}func UserList(c *gin.Context) {var users UListglobal.UserMap.Range(func(key, value interface{}) bool {users.Names = append(users.Names, key.(string))return true})c.JSON(http.StatusOK, &users)
}

ws.go

// websocket 通信package handleimport ("container/list""context""encoding/json""fmt""github.com/gin-gonic/gin""github.com/gorilla/websocket""im/global""im/middlewares""log""net/http"
)type WsInfo struct {Type    string   `json:"type"`Content string   `json:"content"`To      []string `json:"to"`From    string   `json:"from"`
}func WS(ctx *gin.Context) {var claim *middlewares.MyClaimwsConn, _ := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil)for {_, data, err := wsConn.ReadMessage()if err != nil {wsConn.Close()if claim != nil {RemoveWSConnFromMap(claim.Subject, wsConn)r, _ := json.Marshal(gin.H{"type":    "loginlist","content": getLoginList(),"to":      []string{},})SendMsgToAllLoginUser(r)}fmt.Println(claim.Subject, "出错,断开连接:", err)fmt.Println("当前在线用户列表:", getLoginList().Names)return}var wsInfo WsInfojson.Unmarshal(data, &wsInfo)if wsInfo.Type == "auth" {claim, err = middlewares.Auth(wsInfo.Content)if err != nil {// 认证失败fmt.Println(err)rsp := WsInfo{Type:    "no",Content: "认证失败,请重新登录",To:      []string{},}r, _ := json.Marshal(rsp)wsConn.WriteMessage(websocket.TextMessage, r)wsConn.Close()continue}// 认证成功// 将连接加入map记录AddWSConnToMap(claim.Subject, wsConn)fmt.Println(claim.Subject, " 加入连接")fmt.Println("当前在线用户列表:", getLoginList().Names)rsp := WsInfo{Type:    "ok",Content: "连接成功,请发送消息",To:      []string{},}r, _ := json.Marshal(rsp)// 更新登录列表wsConn.WriteMessage(websocket.TextMessage, r)r, _ = json.Marshal(gin.H{"type":    "loginlist","content": getLoginList(),"to":      []string{},})SendMsgToAllLoginUser(r)// 发送离线消息cmd := global.Redis.LRange(context.Background(), claim.Subject, 0, -1)msgs, err := cmd.Result()if err != nil {log.Println(err)continue}for _, msg := range msgs {wsConn.WriteMessage(websocket.TextMessage, []byte(msg))}global.Redis.Del(context.Background(), claim.Subject)} else {rsp, _ := json.Marshal(gin.H{"type":    "normal","content": wsInfo.Content,"to":      []string{},"from":    claim.Subject,})SendMsgToOtherUser(rsp, claim.Subject, wsInfo.To...)}}wsConn.Close()
}var (Upgrader = websocket.Upgrader{//允许跨域CheckOrigin: func(r *http.Request) bool {return true},}
)func AddWSConnToMap(userName string, wsConn *websocket.Conn) {// 同一用户可以有多个ws连接(登录多次)loginListInter, ok := global.LoginMap.Load(userName)if !ok {// 之前没登录loginList := list.New()loginList.PushBack(wsConn)global.LoginMap.Store(userName, loginList)} else {// 多次登录loginList := loginListInter.(*list.List)loginList.PushBack(wsConn)global.LoginMap.Store(userName, loginList)}
}func RemoveWSConnFromMap(userName string, wsConn *websocket.Conn) {loginListInter, ok := global.LoginMap.Load(userName)if !ok {fmt.Println("没有连接可以关闭")} else {// 有连接loginList := loginListInter.(*list.List)if loginList.Len() <= 1 {global.LoginMap.Delete(userName)} else {for e := loginList.Front(); e != nil; e = e.Next() {if e.Value.(*websocket.Conn) == wsConn {loginList.Remove(e)break}}global.LoginMap.Store(userName, loginList)}}
}func SendMsgToOtherUser(data []byte, myName string, otherUserName ...string) {for _, otherName := range otherUserName {if otherName != myName {v, ok := global.LoginMap.Load(otherName)if ok {// 在线,发送给目标用户的所有客户端l := v.(*list.List)for e := l.Front(); e != nil; e = e.Next() {conn := e.Value.(*websocket.Conn)conn.WriteMessage(websocket.TextMessage, data)}} else {_, ok := global.UserMap.Load(otherName)if ok {//离线消息缓存到redisglobal.Redis.LPush(context.Background(), otherName, data)}}}}
}func SendMsgToAllLoginUser(data []byte) {global.LoginMap.Range(func(key, value interface{}) bool {l := value.(*list.List)for e := l.Front(); e != nil; e = e.Next() {conn := e.Value.(*websocket.Conn)conn.WriteMessage(websocket.TextMessage, data)}return true})
}

mysql数据读取 mysql.go

package mysqlimport ("fmt""im/global"
)type User struct {UserName string `gorm:"column:username"`Password string `gorm:"column:password"`
}func StorageUserToMap() {var users []Usererr := global.DB.Find(&users).Errorif err != nil {fmt.Printf("[mysql] 查询用户失败:%v\n", err)return}// 将查询到的用户名和密码存储到 UserMap 中for _, user := range users {global.UserMap.Store(user.UserName, user.Password)}
}func AddUserToMysql(userName, psw string) error {// 创建用户模型user := User{UserName: userName,Password: psw,}// 插入用户记录err := global.DB.Create(&user).Errorif err != nil {fmt.Printf("[mysql] 注册失败:%v\n", err)return err}fmt.Printf("[mysql] 注册成功\n")return nil
}

项目地址:https://github.com/jiangxyb/goim-websocket

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

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

相关文章

OpenHarmony实战开发-如何使用Navigation实现多设备适配。

介绍 在应用开发时&#xff0c;一个应用需要适配多终端的设备&#xff0c;使用Navigation的mode属性来实现一套代码&#xff0c;多终端适配。 效果图预览 使用说明 将程序运行在折叠屏手机或者平板上观看适配效果。 实现思路 本例涉及的关键特性和实现方案如下&#xff1a…

【C++题解】1345. 玫瑰花圃

问题&#xff1a;1345. 玫瑰花圃 类型&#xff1a;基本运算、小数运算 题目描述&#xff1a; 有一块nn&#xff08;n≥5&#xff0c;且 n 是奇数&#xff09;的红玫瑰花圃&#xff0c;由 nn 个小正方形花圃组成&#xff0c;现要求在花圃中最中间的一行、最中间的一列以及 4 个…

智享ai自动直播系统,直播界的流量增长点。

智享ai自动直播系统&#xff0c;直播界的流量增长点&#xff01; 在当今互联网时代&#xff0c;商家面临着日益激烈的竞争&#xff0c;因为一切内容如价格都变得透明&#xff0c;商家们纷纷寻求新的增长点来获取流量。在线下资源饱和的情况下&#xff0c;线上短视频平台成为商…

Acrobat Pro DC2024安装包(亲测可用)

目录 一、软件简介 二、软件下载 一、软件简介 Acrobat软件是由Adobe公司开发的一款专业的PDF&#xff08;Portable Document Format&#xff0c;便携式文档格式&#xff09;编辑和管理软件。它为用户提供了丰富的功能&#xff0c;涵盖了创建、编辑、转换和共享PDF文件、签名和…

使用嘉立创EDA打开JSON格式的PCB及原理图

一、将PCB和原理图放同一文件夹 并打包成.zip文件 二、打开嘉立创EDA并导入.zip文件 文件 -> 导入 -> 嘉立创EDA标准版/专业版 三、选择.zip文件并选择 “导入文件并提取库” 四、自定义工程路径 完成导入并转换为.eprj文件 五、视频教学 bilibili_使用立创EDA打开JSO…

工业自动化,3D视觉技术3C薄片自动化上料

随着制造业的快速发展&#xff0c;3C行业对薄片类零件的上料需求日益增长。传统的上料方式往往依赖于人工操作&#xff0c;效率低下且存在误差。为了解决这一问题&#xff0c;3D视觉技术应运而生&#xff0c;为3C薄片自动化上料提供了强大的技术支持。本文将探讨3D视觉技术如何…

论文阅读-Federated-Unlearning-With-Momentum-Degradation

论文阅读-Federated Unlearning With Momentum Degradation 联邦忘却与动量退化 Yian Zhao IEEE Internet of Things Journal 2023 年 10 月 2 日 CCF-C momentum degradation-MoDe 动量退化 memory guidance-记忆引导 knowledge erasure-知识擦除 Deep-learning neural n…

C++如何使用string类

文章目录 为什么要学习string?库中的string关于编码ASCII编码Unicode编码 迭代器Iteratorsstring常用构造接口接口声明与功能说明接口演示 string类对象的容量操作接口声明与功能说明接口演示reverse与resize在不同平台下的扩容与缩容机制 string类对象的访问及遍历操作接口声…

CUDA 以及MPI并行矩阵乘连接服务器运算vscode配置

一、CUDA Vscode配置 &#xff08;一&#xff09;扩展安装 本地安装 服务器端安装 &#xff08;二&#xff09; CUDA 配置 .vscode c_cpp_properties.json {"configurations": [{"name": "Linux","includePath": ["${workspa…

【C++类和对象】拷贝构造与赋值运算符重载

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

分布式搭载博客网站

一.运行环境&#xff1a; IP主机名系统服务192.168.118.128Server-WebLinuxWeb192.168.118.131Server-NFS-DNSLinuxNFS/DNS 二.基础配置 1. 配置主机名&#xff0c;hosts映射 [rootserver ~]# hostnamectl set-hostname Server-Web [rootserver ~]# hostname Server-Web [r…

【学习笔记】Python大数据处理与分析——数据预处理

一、数据清洗 1、唯一值与重复值 获取唯一值的方法是采用unique()函数&#xff0c;用于Series对象&#xff1a; s1 pd.Series([2, 3, 4, 1, 2, 5, 3, 6, 4, 9, 5, 3, 4, 2, 1, 2])print(s1.unique()) →[2 3 4 1 5 6 9] 但unique()函数不能用于DataFrame对象&#xff0c;而d…

Word分节后,页码不连续、转PDF每节后多出空白页解决办法

1. 问题图例 废话少说&#xff0c;先上图&#xff1a; 2. 问题分析 问题分析&#xff1a;出现以上问题的原因可能有&#xff0c; 未链接到上一节页面布局中节的起始位置设置为[奇数页] 3. 解决问题 若为【1. 未链接到上一节】导致该问题出现&#xff0c;则我们需要选中页脚…

Chatgpt掘金之旅—有爱AI商业实战篇|品牌故事业务|(十六)

演示站点&#xff1a; https://ai.uaai.cn 对话模块 官方论坛&#xff1a; www.jingyuai.com 京娱AI 一、AI技术创业在品牌故事业务有哪些机会&#xff1f; 人工智能&#xff08;AI&#xff09;技术作为当今科技创新的前沿领域&#xff0c;为创业者提供了广阔的机会和挑战。随…

接口压力测试 jmeter--入门篇(一)

一 压力测试的目的 评估系统的能力识别系统的弱点&#xff1a;瓶颈/弱点检查系统的隐藏的问题检验系统的稳定性和可靠性 二 性能测试指标以及测算 【虚拟用户数】&#xff1a;线程用户【并发数】&#xff1a;指在某一时间&#xff0c;一定数量的虚拟用户同时对系统的某个功…

OpenMesh 网格平均曲率计算

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 根据 Laplace-Beltrami 算子与平均曲率法向的关系: 又根据余切 Laplace-Beltrami 算子的定义: 其中 Ai 为该点邻域面积,取 Voronoi cell 面积如下: 得到

PACNet CellNet(代码开源)|bulk数据作细胞分类,评估细胞命运性能的一大利器

文章目录 1.前言2.CellNet2.1CellNet简介2.2CellNet结果 3.PACNet3.1安装R包与加载R包3.2加载数据3.3开始训练和分类3.4可视化分类过程3.5可视化分类结果 4.细胞命运分类和免疫浸润比较 1.前言 今天冲浪看到一个细胞分类性能评估的R包——PACNet&#xff0c;它与转录组分析方法…

Prometheus + Grafana 搭建监控仪表盘

目标要求 1、需要展现的仪表盘&#xff1a; SpringBoot或JVM仪表盘 Centos物理机服务器&#xff08;实际为物理分割的虚拟服务器&#xff09;仪表盘 2、展现要求: 探索Prometheus Grafana搭建起来的展示效果&#xff0c;尽可能展示能展示的部分。 一、下载软件包 监控系统核心…

Spring Cloud Gateway集成聚合型Spring Boot API发布组件knife4j,增强Swagger

大家都知道&#xff0c;在前后端分离开发的时代&#xff0c;前后端接口对接是一项必不可少的工作。 可是&#xff0c;作为后端开发&#xff0c;怎么和前端更好的配合&#xff0c;才能让自己不心累、脑累&#xff0c;直接扔给前端一个后端开放api接口文档或者页面&#xff0c;让…

Unity之OpenXR+XR Interaction Toolkit快速监听手柄任意按键事件

前言 当我们开发一个VR时,有时希望监听一个手柄按键的点击事件,或者一个按钮的Value值等。但是每次有可能监听的按钮有不一样,有可能监听的值不一样,那么每次这么折腾,有点累了,难道就没有一个万能的方法,让我可以直接监听我想要的某个按钮的事件么? 答案是肯定的,今…