Unity Metaverse(七)、基于环信IM SDK实现的好友系统、私聊、群聊

news2024/11/24 22:39:44

文章目录

  • 🎈 简介
  • 🎈 用户管理
  • 🎈 好友管理
  • 🎈 聊天管理
    • 🔸 发送与接收消息
    • 🔸 消息处理
      • 消息项的对象池管理


🎈 简介

在之前的文章中已经介绍了如何接入环信IM Unity SDK,及基于该SDK实现用户的登录注册功能,该篇文章介绍的是如何通过它来实现用户管理、好友系统(联系人管理)及聊天消息管理功能。

环信IM SDK
核心功能

🎈 用户管理

SDK为我们提供的用户属性管理包括用户昵称头像邮箱电话性别签名生日等,除此之外,我们可以使用扩展字段Ext来管理业务层所需的其它用户属性。例如在我们的Metaverse项目中,就将用户的Avatar人物信息存储在了Ext扩展字段中,在拿到Ext字段的值后通过反序列化即可得到用户的Avatar人数信息,反之,在用户的Avatar信息发生变更时,只需序列化再存储到Ext扩展字段中并更新用户属性即可。

用户属性管理相关的接口调用封装:

/// <summary>
/// 获取用户信息
/// </summary>
/// <param name="userId">UserID</param>
/// <param name="onSuccess">获取成功回调事件</param>
/// <param name="onError">获取失败回调事件</param>
public void GetUserInfo(string userId, Action<AgoraChat.UserInfo> onSuccess, Action<int, string> onError = null)
{
    SDKClient.Instance.UserInfoManager.FetchUserInfoByUserId(new List<string>(1) { userId },
        new ValueCallBack<Dictionary<string, AgoraChat.UserInfo>>(
        onSuccess: dic =>
        {
            AgoraChat.UserInfo userInfo = dic[userId];
            Main.Log.Info("【环信IM SDK】获取用户{0}信息:{1}", userId, userInfo);
            onSuccess.Invoke(userInfo);
        },
        onError: (code, desc) =>
        {
            Main.Log.Info("【环信IM SDK】获取用户{0}信息失败:Code -{1}  Desc -{2}", userId, code, desc);
            onError?.Invoke(code, desc);
        }));
}
/// <summary>
/// 更新自身用户信息
/// </summary>
/// <param name="userInfo">用户信息</param>
/// <param name="onSuccess">更新成功回调事件</param>
/// <param name="onError">更新失败回调事件</param>
public void UpdateOwnUserInfo(AgoraChat.UserInfo userInfo, Action onSuccess, Action<int, string> onError = null)
{
    SDKClient.Instance.UserInfoManager.UpdateOwnInfo(userInfo, new CallBack(
    onSuccess: () =>
    {
        LocalUserInfo.Update(userInfo);
        Main.Log.Info("【环信IM SDK】更新自身用户信息成功:{0}", LocalUserInfo);
        onSuccess?.Invoke();
    },
    onError: (code, desc) =>
    {
        Main.Log.Info("【环信IM SDK】更新自身用户信息失败:Code -{0}  Desc -{1}", code, desc);
        onError?.Invoke(code, desc);
    }));
}

🎈 好友管理

添加好友

好友功能是通过环信IM SDK提供的Contact Manager联系人管理来实现的,例如发起添加联系人请求:

/// <summary>
/// 发送添加好友请求
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="reason">原因/验证信息</param>
/// <param name="onSuccess">请求成功回调事件</param>
/// <param name="onError">请求失败回调事件</param>
public void AddContact(string userId, string reason, Action onSuccess = null, Action<int, string> onError = null)
{
    SDKClient.Instance.ContactManager.AddContact(userId, reason, new CallBack(
        onSuccess: () =>
        {
            Main.Log.Info("【Contact Manager】添加好友请求成功:UserId -{0}  Reason -{1}", userId, reason);
            onSuccess?.Invoke();
        },
        onError: (code, desc) =>
        {
            Main.Log.Info("【Contact Manager】添加好友请求失败:UserId -{0}  Reason -{1}  Code -{2}  Desc -{3}", userId, reason, code, desc);
            onError?.Invoke(code, desc);
        }));
}

请求人发起请求后,被请求人如果监听了与联系人管理相关的回调,会执行On Contact Invited回调事件,在回到事件中我们可以通过弹窗形式来让用户决定是否同意添加联系人。如何监听与联系人管理相关的回调?继承IContactManagerDelegate接口并实现,通过Add Contact Manager DelegateRemove Contact Manager Delegate来开启监听和停止监听。

namespace Metaverse
{
    public class ContactManagerDelegate : IContactManagerDelegate
    {
        /// <summary>
        /// 好友新增事件
        /// 用户B向用户A发送好友请求,用户A同意该请求,用户A收到该事件。
        /// </summary>
        /// <param name="userId">B用户ID</param>
        public void OnContactAdded(string userId)
        {
            Main.Log.Info("【环信IM SDK】新增好友{0}", userId);
        }
        /// <summary>
        /// 被删除好友事件
        /// 用户B将用户A从联系人列表上删除,用户A收到该事件。
        /// </summary>
        /// <param name="userId">B用户ID</param>
        public void OnContactDeleted(string userId)
        {
            Main.Log.Info("【环信IM SDK】被用户{0}删除好友", userId);
        }
        /// <summary>
        /// 被请求添加好友事件
        /// 用户B向用户A发送好友请求,用户A收到该事件。
        /// </summary>
        /// <param name="userId">B用户ID</param>
        /// <param name="reason">原因/验证信息</param>
        public void OnContactInvited(string userId, string reason)
        {
            Main.Log.Info("【环信IM SDK】收到用户{0}添加好友的请求:{1}", userId, reason);

            /*************************************************************
             * 收到添加好友的请求,弹出弹窗,让用户点击同意或拒绝
             * 同意就调用Main.Custom.ContactManager.AcceptAddContact
             * 拒绝就调用Main.Custom.ContactManager.DeclineAddContact
             *************************************************************/

            Main.UI.ShowOrLoadView<PopupView>(ViewLevel.POP, new PopupData("添加好友请求", string.Format("用户{0}请求添加您为好友,是否同意?", userId))
            {
                confirm = "同意",
                cancle = "拒绝",
                onConfirm = () => Main.Custom.ContactManager.AcceptAddContact(userId),
                onCancle = () => Main.Custom.ContactManager.DeclineAddContact(userId),
            });
        }
        /// <summary>
        /// 请求添加好友被对方同意事件
        /// 用户A向用户B发送好友请求,用户B收到好友请求后,同意加好友,则用户A收到该事件。
        /// </summary>
        /// <param name="userId">B用户ID</param>
        public void OnFriendRequestAccepted(string userId)
        {
            Main.Log.Info("【环信IM SDK】添加用户{0}为好友的请求被对方同意", userId);
        }
        /// <summary>
        /// 请求添加好友被对方拒绝事件
        /// 用户A向用户B发送好友请求,用户B收到好友请求后,拒绝加好友,则用户A收到该事件。
        /// </summary>
        /// <param name="userId">B用户ID</param>
        public void OnFriendRequestDeclined(string userId)
        {
            Main.Log.Info("【环信IM SDK】添加用户{0}为好友的请求被对方拒绝", userId);
        }
    }
}
  • 用户A向用户B发送好友请求,用户B同意则调用AcceptAddContact,拒绝则调用DeclineAddContact
  • 用户A向用户B发送好友请求,用户B收到好友请求后,同意加好友,则用户A收到OnFriendRequestAccepted事件;
  • 用户A向用户B发送好友请求,用户B收到好友请求后,拒绝加好友,则用户A收到OnFriendRequestDeclined事件。

🎈 聊天管理

房间 IM消息

私聊消息

聊天是通过环信IM SDK提供的Chat Manager实现的,会话(Conversation)分为三种,单聊群聊聊天室会话

  • 单聊是指两个用户建立的会话,双方可以在会话中收发消息。私聊基于此实现。
  • 群聊会话是由群成员发送消息所组成的,群成员可以在群会话中收发消息。我们的房间IM就是通过群里会话实现的。
  • 聊天室会话并未应用于项目中。

🔸 发送与接收消息

SDK将消息划分为多种类型,包括文本消息语音消息图片消息视频消息文件消息透传消息等,以基本的文本消息为例,消息发送的接口封装如下:

/// <summary>
/// 发送文本消息
/// </summary>
/// <param name="messageType">消息类型 Chat:单聊  Group:群聊  Room:聊天室消息</param>
/// <param name="userOrGroupId">用户或者群聊的ID</param>
/// <param name="content">文本内容</param>
/// <param name="onSuccess">发送成功回调事件</param>
/// <param name="onError">发送失败回调事件</param>
public void SendTextMessage(MessageType messageType, string userOrGroupId,
    string content, Action<Message> onSuccess = null, Action<int, string> onError = null)
{
    Message msg = Message.CreateTextSendMessage(userOrGroupId, content);
    msg.MessageType = messageType;
    SDKClient.Instance.ChatManager.SendMessage(ref msg, new CallBack(
        onSuccess: () =>
        {
            Main.Log.Info("【Chat Manager】发送文本消息成功:MessageType -{0}  UserOrGroupId -{1}  Content -{2}",
                messageType, userOrGroupId, content);
            onSuccess?.Invoke(msg);
        },
        onError: (code, desc) =>
        {
            Main.Log.Info("【Chat Manager】发送文本消息失败:MessageType -{0}  UserOrGroupId -{1}  Content -{2}  Code -{3}  Desc -{4}",
                messageType, userOrGroupId, content, code, desc);
            onError?.Invoke(code, desc);
        }));
}

当用户监听了与聊天管理相关的回调后,收到消息时会执行On Messages Received回调事件,在事件中处理我们的业务逻辑。如何监听与聊天管理相关的回调?继承IChatManagerDelegate接口并实现,通过Add Chat Manager DelegateRemove Chat Manager Delegate来开启监听和停止监听。

/// <summary>
/// 新消息接收事件
/// </summary>
/// <param name="messages">新消息列表</param>
public void OnMessagesReceived(List<Message> messages)
{
    for (int i = 0; i < messages.Count; i++)
    {
        Message msg = messages[i];
        //抛出事件
        Main.Events.Publish(MessageReceivedEventArgs.Allocate(msg));
    }
}

🔸 消息处理

接收到消息后,通过开发框架中Event事件系统将其抛出,好友视图中会订阅该事件来接收来自好友的消息,房间视图中会订阅该事件来接收来自房间内其他用户发送的消息。例如:

namespace Metaverse
{
    public class RoomPlaceView : UIView
    {
        #region >> NonPublic Variables
        [Tooltip("聊天输入框"), SerializeField] private InputField chatInputField;
        [Tooltip("聊天项预制件"), SerializeField] private Text chatItemPrefab;
        [Tooltip("聊天项列表"), SerializeField] private RectTransform chatContent;
        #endregion

        #region >> View
        protected override void OnInit(IViewData data)
        {
            base.OnInit(data);
            //订阅消息接收事件
            Main.Events.Subscribe(MessageReceivedEventArgs.EventID, OnMessageReceivedEvent);
        }
        protected override void OnUnload()
        {
            base.OnUnload();
            //取消订阅消息接收事件
            Main.Events.Unsubscribe(MessageReceivedEventArgs.EventID, OnMessageReceivedEvent);
        }
        #endregion

        #region >> UI Event
        /// <summary>
        /// 聊天发送按钮点击事件
        /// </summary>
        public void OnSendButtonClick()
        {
            //未输入任何内容 返回
            if (string.IsNullOrEmpty(chatInputField.text)) return;
            //将当前聊天框中输入的文字内容发送
            Main.Custom.ChatManager.SendTextMessage(MessageType.Group, (Main.FSM.GetMachine<GamePlace>().CurrentState as PlaceRoom).PlaceID,
                chatInputField.text, message =>
                {
                    //添加聊天项
                    AddChatItem(message);
                    //消息发送成功,将聊天框输入的内容清空
                    chatInputField.text = string.Empty;
                });
        }
        #endregion

        #region >> Subscribed Event
        //消息接收事件
        private void OnMessageReceivedEvent(EventArgs e)
        {
            if (e is MessageReceivedEventArgs mre)
            {
                //新增聊天项
                AddChatItem(mre.message, false);
            }
        }
        #endregion

        #region >> NonPublic Methods
        /**********************************************************************************
         * 添加聊天项:
         *  isFromSelf - 消息发送方是否是自己
         *  如果不是自己发送的消息 需要根据用户ID获取用户信息
         **********************************************************************************/
        private void AddChatItem(Message message, bool isFromSelf = true)
        {
            if (isFromSelf)
                Add(Main.Custom.UserManager.LocalUserInfo.NickName, (message.Body as TextBody).Text);
            else
                Main.Custom.UserManager.GetOrQuery(message.From, userInfo => Add(userInfo.NickName, (message.Body as TextBody).Text));

            void Add(string userName, string content)
            {
                /********************************************************************************
                 * 此处判断如果PlaceID不一致,不执行代码块中逻辑
                 * 因为此处逻辑的执行是在异步回调中 假如在异步期间已经退出之前的房间 
                 * 则此处会实例化会造成异常
                 ********************************************************************************/
                if ((Main.FSM.GetMachine<GamePlace>().CurrentState as PlaceRoom).PlaceID == message.To)
                {
                    //实例化
                    var instance = Instantiate(chatItemPrefab);
                    //设置父级
                    instance.transform.SetParent(chatContent.transform, false);
                    //消息内容
                    instance.text = string.Format("<color=cyan>{0}:</color>{1}", userName, content);
                    //预制件是隐藏的 实例化后显示
                    instance.gameObject.SetActive(true);
                    //一帧之后更新LayoutGroup自动布局
                    Main.Actions.Sequence(this)
                        .Frame(1)
                        .Event(() => LayoutRebuilder.ForceRebuildLayoutImmediate(chatContent))
                        .Begin();
                }
            }
        }
        #endregion
    }
}

关于会话ID:
Conversation Id,即会话ID,在单聊中它其实就是对方用户的User Id(用户ID),在群聊中它其实就是群组的Group Id(群组ID)。

消息项的对象池管理

每一条消息处理时都需要实例化一个消息项,尤其是在与不同的好友聊天时,消息项会被大量使用,因此在项目中考虑使用对象池来管理:

protected override void OnInit(IViewData data)
{
    base.OnInit(data);

    //订阅消息接收事件
    Main.Events.Subscribe(MessageReceivedEventArgs.EventID, OnMessageReceivedEvent);

    /********************************************
     * 初始化对象池
     * 设置创建方法
     * 设置最大缓存数量
     ********************************************/
    Main.ObjectPool.Mono.CreateBy(() =>
    {
        //实例化
        var instance = Instantiate(chatItemPrefabLocal);
        //设置父级
        instance.transform.SetParent(chatItemPrefabLocal.transform.parent, false);
        //获取组件并返回
        return instance.GetComponent<ChatItemLocal>();
    });
    Main.ObjectPool.Mono.SetMaxCacheCount<ChatItemLocal>(99);

    Main.ObjectPool.Mono.CreateBy(() =>
    {
        //实例化
        var instance = Instantiate(chatItemPrefabRemote);
        //设置父级
        instance.transform.SetParent(chatItemPrefabRemote.transform.parent, false);
        //获取组件并返回
        return instance.GetComponent<ChatItemRemote>();
    });
    Main.ObjectPool.Mono.SetMaxCacheCount<ChatItemRemote>(99);
}
  • 从对象池中获取实例
/**********************************************************************************
 * 添加聊天项:
 *  userId - 发送者用户ID 传null表示发送方是自己
 *      如果发送者是自己 会新增一项ChatItemLocal
 *      如果发送者是对方 会新增一项ChatItemRemote
 **********************************************************************************/
private void AddChatItem(Message message, string userId = null)
{
    bool flag = !string.IsNullOrEmpty(userId);
    //从对象池中获取实例
    ChatItemBase instance;
    if (flag) instance = Main.ObjectPool.Mono.Allocate<ChatItemRemote>();
    else instance = Main.ObjectPool.Mono.Allocate<ChatItemLocal>();
    //预制件是隐藏的 实例化后调用显示接口
    instance.gameObject.SetActive(true);
    //设置数据
    instance.Set((message.Body as AgoraChat.MessageBody.TextBody).Text,
        DateTime2MessageTimeString(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(
            message.ServerTime != 0 ? message.ServerTime : message.LocalTime).ToLocalTime()));
    //开始适配大小
    instance.GetComponentInChildren<ChatItemTextBgAdaptor>().Adapt();
    //缓存到字典
    chatItemDic.Add(message, instance);
}
  • 回收实例到对象池中
string CurrentChatUserID
{
    get
    {
        return currentChatUserID;
    }
    set
    {
        /*****************************************************************************
         * 判断当前聊天对象是否发生变更
         *  发生变更后不仅更新值 还要执行聊天对象发生变更事件
         *  即回收当前实例化出的聊天项 并清空字典缓存
         *****************************************************************************/
        if (currentChatUserID != value)
        {
            currentChatUserID = value;
            foreach (var kv in chatItemDic)
            {
                switch (kv.Value.Type)
                {
                    case ChatItemType.LOCAL: Main.ObjectPool.Mono.Recycle(kv.Value as ChatItemLocal); break;
                    case ChatItemType.REMOTE: Main.ObjectPool.Mono.Recycle(kv.Value as ChatItemRemote); break;
                }
            }
            chatItemDic.Clear();
        }
    }
}

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

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

相关文章

使用Python 构建球体/正方体/多面体/兔子/八面体等点云,Open3D可视化及重建

使用Python 构建球体/正方体/多面体/兔子/八面体等点云&#xff0c;Open3D可视化及重建 点云生成8面体点并拟合绘制表面重建结果。&#xff08;官方示例兔子&#xff0c;8面体&#xff0c;多面体&#xff0c;球体&#xff09; 1. 效果图8面体多面体效果图**俩个整8面体效果图**…

学生宿舍信息管理系统

系列文章 任务6 学生宿舍信息管理系统 文章目录 系列文章一、实践目的与要求1、目的2、要求 二、课题任务三、总体设计1.存储结构及数据类型定义2.程序结构3.所实现的功能函数4、程序流程图 四、小组成员及分工五、 测试宿舍信息录入宿舍信息浏览查询学生所住宿舍楼号、宿舍号…

WPF MaterialDesign 初学项目实战(6):设计首页(2),设置样式触发器

原项目视频 WPF项目实战合集(2022终结版) 26P 源码地址 WPF项目源码 其他内容 WPF MaterialDesign 初学项目实战&#xff08;0&#xff09;:github 项目Demo运行 WPF MaterialDesign 初学项目实战&#xff08;1&#xff09;首页搭建 WPF MaterialDesign 初学项目实战&…

npm、cnpm、yarn、pnpm区别以及pnpm 是凭什么对 npm 和 yarn 降维打击的

安装 1、安装npm需要安装nodejs&#xff0c;node中自带npm包管理器 node下载地址&#xff1a;node.js 2、cnpm安装&#xff08;需要安装npm&#xff09; cnpm是淘宝团队做的npm镜像&#xff0c;淘宝镜像每 10分钟 进行一次同步以保证尽量与官方服务同步。 npm install -g …

secure CRT 自定义主题

文章目录 如何切换 SecureCRT 主题如何新建SecureCRT 主题如何拷贝我的主题,主题名为pic如何设置 SecureCRT 关键字高亮 如何切换 SecureCRT 主题 SecureCRT 自带主题 选择 options -> Edit Default Session -> Terminal -> Emulation -> Terminal xterm optio…

【Linux】-vim的介绍,教你手把手使用vim

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树 ❤️‍&#x1fa79;作者宣言&#xff1a;认真写好每一篇博客 &#x1f4a8;作者gitee:gitee &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点…

一台电脑同时安装多个tomcat服务器教程,window同时安装tomcat7、tomcat8、tomcat9三个服务器教程

一台电脑同时安装多个tomcat服务器 . 介绍 A. 解释为什么有时需要同时安装多个Tomcat服务器 应用程序隔离&#xff1a;当你需要在同一台设备上运行多个独立的应用程序时&#xff0c;每个应用程序可能需要使用不同的Tomcat配置和环境。通过同时安装多个Tomcat服务器&#xff0…

车载以太网 - SomeIP - 协议用例 - Messages_02

目录 13.1、验证SomeIP-SD中订阅报文Subscribe和SubscribeAck中IPv4 Endpoint Option中ServiceID一样

【JAVA进阶】Stream流

&#x1f4c3;个人主页&#xff1a;个人主页 &#x1f525;系列专栏&#xff1a;JAVASE基础 目录 1.Stream流的概述 2.Stream流的获取 3.Stream流的常用方法 1.Stream流的概述 什么是Stream流&#xff1f; 在Java 8中&#xff0c;得益于Lambda所带来的函数式编程&#xff0…

HNU数据结构与算法分析-作业4-图结构

1. (简答题) 【应用题】11.3 &#xff08;a&#xff09;画出所示图的相邻矩阵表示 &#xff08;b&#xff09;画出所示图的邻接表表示 &#xff08;c&#xff09;如果每一个指针需要4字节&#xff0c;每一项顶点的标号占用2字节&#xff0c;每一条边的权需要2字节&#xff0…

计算机体系结构存储系统

存储系统原理 两种典型的存储系统&#xff1a;Cache存储系统和虚拟存储系统。前者主要目的是提高存储器速度&#xff0c;后者有主存储器和硬盘构成&#xff0c;主要用于扩大存储器容量。 存储系统的访问效率 e T 1 T 1 H ( 1 − H ) T 2 T 1 f ( H , T 2 T 1 ) e\frac{T_…

魔改车钥匙实现远程控车:(4)基于compose和经典蓝牙编写一个控制APP

前言 这篇文章不出意外的话应该是魔改车钥匙系列的最后一篇了&#xff0c;自此我们的魔改计划除了最后的布线和安装外已经全部完成了。 不过由于布线以及安装不属于编程技术范围&#xff0c;且我也是第一次做&#xff0c;就不献丑继续写一篇文章了。 在前面的文章中&#xf…

基于torch实现模型剪枝

一、剪枝分类 所谓模型剪枝&#xff0c;其实是一种从神经网络中移除"不必要"权重或偏差&#xff08;weigths/bias&#xff09;的模型压缩技术。关于什么参数才是“不必要的”&#xff0c;这是一个目前依然在研究的领域。 1.1、非结构化剪枝 非结构化剪枝&#xff08;…

什么是可持续能源?

随着全球经济的不断发展和人口的不断增长&#xff0c;能源问题越来越受到关注。传统能源已经不能满足人们对能源的需求&#xff0c;同时也对环境和健康带来了严重的影响。为了解决这些问题&#xff0c;出现了可持续能源的概念。那么&#xff0c;什么是可持续能源呢&#xff1f;…

逐渐从土里长出来的小花

从土里逐渐长出来的小花&#xff08;这是长出来后的样子&#xff0c;图片压缩了出现了重影~&#xff09; 代码在这里&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title&g…

MySQL-索引(2)

本文主要讲解MySQL-索引相关的知识点 联合索引前缀索引覆盖索引索引下推索引的优缺点什么时候适合创建索引,什么时候不适合?如何优化索引 ? 索引失效场景 ? 为什么SQL语句使用了索引,却还是慢查询 ? 使用索引有哪些注意事项 ? InnoDB引擎中的索引策略 目录 联合索引 联合…

LeetCode高频算法刷题记录6

文章目录 1. 编辑距离【困难】1.1 题目描述1.2 解题思路1.3 代码实现 2. 寻找两个正序数组的中位数【困难】2.1 题目描述2.2 解题思路2.3 代码实现 3. 合并区间【中等】3.1 题目描述3.2 解题思路3.3 代码实现 4. 爬楼梯【简单】4.1 题目描述4.2 解题思路4.3 代码实现 5. 排序链…

chatgpt赋能Python-python3_9安装numpy

Python 3.9 安装 NumPy 的完整指南 Python是一种功能强大的编程语言&#xff0c;已成为数据分析、人工智能和科学计算领域的主流语言之一。NumPy是一个Python库&#xff0c;用于执行高效的数值计算和科学计算操作。Python 3.9是Python最新版本&#xff0c;带来了许多新功能和改…

一款非常有趣的中国版本的Excalidraw作图工具drawon(桌案)

桌案工具集成了很多有趣的在线作图工具&#xff0c; 思维导图&#xff0c; 流程图&#xff0c;以及草图&#xff0c;在线ppt等功能。 而草图是基于国外有名的Excalidraw而改造而来&#xff0c;使得它更符合国人的使用习惯。 最近在 使用excalidraw时&#xff0c;发现了很多新功…

Excel | 基因名都被Excel篡改了怎么办呢!?~(附3种解决方案)

1写在前面 今天和大家分享一下在做表达矩阵处理时尝尝会遇到的一个问题&#xff0c;但又经常被忽视&#xff0c;就是Excel会修改你的基因名。&#x1f637; 无数大佬在这里都踩过坑&#xff0c;这些普遍的问题已经被写成了paper&#xff08;左右滑动&#xff09;&#xff1a;&a…