• 个人作业——软件评测


    这个作业属于哪个课程 2020春|S班 (福州大学)
    这个作业要求在哪里 个人作业——软件评测
    这个作业的目标 分析腾讯即时通信IM
    作业正文 个人作业——软件评测
    其他参考文献 构建之法、即时通信官方文档

    1.SDK评测

    1.1应用功能描述:

    1.1.1概述

      此次实践我选择的方式二,使用SDK构建一个小应用。我实现的是一个无需注册的网页聊天室。

      项目体验地址:http://radishbear.top/im/#

      github仓库:https://github.com/RadishBEAR/RadishBearIM

    1.1.2登录功能

      进入网站后,输入用户名(无需注册)。登录后会进入界面,并加载用户已加入的群组。

    1.1.3创建群组

      进入主界面后,点击左下角加号按钮(鼠标hover时会有文字提示),可以创建群组。输入群名和群ID,若群ID被占用了会出现提示创建失败,若群ID可用则成功创建群组。创建群聊成功后,左侧导航栏会出现群名片,右侧聊天框会自动变成此群的聊天框。

    1.1.4加入群聊

    ​  进入主界面后,点击左下角的小飞机按钮(鼠标hover时会有文字提示),可以加入群聊。输入群ID后,若群名不存在,则会提示,若群名存在,则会加入该群,左侧导航栏出现群名片。

    1.1.5群聊

      选择并点击左侧的群名片,右侧聊天框会变成对应的群聊框。在下方输入框内输入信息并点击发送就能开始聊天了。用户发的信息显示在右侧,接收到的信息显示在左侧。信息体包含头像、用户名、信息三部分。因为还没有实现头像功能,所以所有用户的头像都是大白鹅。

    ​  当客户端接收到群消息时,左侧的群名片也会显示出最新接收到的信息。

      下面两张图是我用两台电脑群聊的效果。

    1.1.6滚动屏幕

      聊天框有滚动功能。当接收/发送新的信息后,全部对话超过聊天框高度时,聊天框会跳一行,显示出最新接收/发送的消息。聊天框内也支持滚轮滑动来查看消息。

    1.2设计与实现

    1.2.1系统设计

      官方的web demo使用vue做基础框架,element-ui作组件库,vuex实现事件管理。我也采用了类似的实现方式,用vue做基础框架,element-ui作组件库,不过因为我还不会vuex,所以我用比较原始的事件总线来实现事件管理。

    1.2.2实现流程

      这次的开发过程我个人感觉是比较有意思的。聊天室涉及很多双向通讯,例如发送/接收消息、创建/加入群组。我开发的时候,验证功能模块是否正常,常常是让官方的demo和我的程序相互通讯。例如:官方demo发送消息,我的简单程序接收消息并显示。这就出现了蛮滑稽的一幕,一开始很简单的工程和很专业化的官方demo聊天。在项目双向功能健全后,我才真正开始让我的聊天室相互通讯。下图是最开始的简易聊天室界面。

      此次开发和以往的前端开发还有一点不同的是:这次我比较依赖浏览器控制台。因为对即时通信SDK不熟悉,很多时候我根本不知道操作成功了还是失败了。所以整个项目下来我的浏览器几乎都是处于F12开发者模式。调用SDK后的一切反馈也都是先在控制台里查看。

      讲完大体的实现过程,再讲讲具体的实现步骤。聊天室的具体实现步骤如下:

      1.导入SDK,安装相关依赖。在这一步里有遇到一个问题,会在1.4部分详细描述。

      2.配置main.js,设置全局变量,设置事件总线。这一步里涉及到三个很重要的对象:全局变量tim,它复制管理即时通信SDK内的一切方法;全局变量messageList,负责存放客户端接收/发送的信息。后期渲染信息列表用的就是messageList。事件总线EventBus,负责收发各模块内的各种事件。

    ​  3.实现登录功能。这一步开始正式接入即时通信SDK的开发。

    ​  4.实现简易的收发消息功能。这一阶段实现的就是前面说的简单收发界面,主要是为了大致了解一下SDK核心功能的使用。当时消息的发送方ID和接收方ID都是写死的,主要作测试用。

      5.搭建主界面,实现获取群组列表功能。在这一步里我实现了主界面的一个雏形。然后利用官方的web demo,把测试账号拉到群里,再在我的聊天室里获取测试账号的群组列表,最后一步步实现了获取群列表的功能。这个阶段遇到一个关于SDK状态的问题,会在1.4中细说。

      6.实现创建/加入群组功能。实现创建群聊的时候,也是和官方web demo配合,我的聊天室创建群组,官方demo试着加入群组,如果官方demo能加入我创建的群组,就说明此功能实现了。实现创建群组功能后,加入群组功能就可以脱离官方demo了。

    ​  7.实现具体消息收发功能。这一步的工作是完善第4步的简易收发功能。把原本写死的收发方改成动态的。不过由于此时还没实现消息框部分,所以接收的消息都是通过浏览器控制台查看的。收发的消息都存入全局变量messageList,为第8步作准备。

      8.实现消息框。这一阶段主要就是渲染messageList,就和即使通信SDK没太多关系了,就是单纯的前端。为了实现消息框的滚动效果,我又安装一个名为“vue-happy-scroll”的滚动条组件。

      9.测试和优化。

    1.3代码核心及其描述

    1.3.1三大关键对象

      聊天室中最关键的三个的对象(这里用对象来表述可能不大准确)分别是:全局变量tim(SDK实例)、管理消息列表的全局变量messageList、管理所有事件的事件总线EventBus。

      全局变量tim在main.js里初始化,官方web demo并没有把SDK实例设置为vue全局变量,我为了方便开发将其设为全局变量,也没有细细考究两种实现方式的区别。相关代码如下:

    // 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
    let tim = TIM.create(options); // SDK 实例通常用 tim 表示
    Vue.prototype.tim = tim
    

      全局变量messageList也在main.js里实现,不过main.js里只是声明它是一个数组,但是其具体的结构相当复杂,大致是这样 [ 群组:{ [消息] , [消息] ...}, 群组:{ [消息] , [消息] ... }... ]。声明语句如下:

    Vue.prototype.messageList=[];
    

      存储消息算项目里的小核心,相关代码如下:

    var msg={
    		from:event.data[0]['from'],
    		conversationID:event.data[0]['conversationID'],
    		time:event.data[0]['time'],
    		text:event.data[0]['payload'].text,
    		left:true,
    		right:false
    };
    if (Vue.prototype.messageList[event.data[0]['conversationID']]==null){
    		Vue.prototype.messageList[event.data[0]['conversationID']]=[]
    }
    Vue.prototype.messageList[event.data[0]['conversationID']].push(msg);
    

      事件总线EventBus放在单独的js文件里,这个文件我用了好几个月了。只要在要使用事件总线的组件里import总线文件,就能使用EventBus了。引入EventBus的语句如下:

    import {EventBus} from "../../tools/EventBus";
    

    1.3.2登录

      登录实现比较简单,核心就是用到官方给的genTestUserSig函数。不过这里吐槽一下,官方文档说得特别美好“把genTestUserSig搬到你的项目里就可以使用了”,但是实际不是这样了,控制台的反复报错一度让人崩溃,最后我把genTestUserSig嵌入vue组件里,把另一个lib-generate-test-usersig.min.js文件单独引用才解决问题。

    login() {
                    // this.$store.dispatch('login', this.userID)
                    var userSig = this.genTestUserSig(this.userID).userSig;
                    console.log(userSig);
                    let promise = Vue.prototype.tim.login({
                        userID: this.userID,
                        userSig: userSig
                    });
                    var userID=this.userID;
                    Vue.prototype.ID=this.userID;
                    // eslint-disable-next-line no-unused-vars
                    promise.then(function (imResponse) {
                        console.log('登录成功!');
                        EventBus.$emit('login',userID);
                    }).catch(function (imError) {
                        console.warn('login error:', imError); // 登录失败的相关信息
                        console.log('登录失败!');
                    });
                },
    

    1.3.3收发消息

      接收消息的代码在前面有放出片段,核心是使用SDK实例监听外界的消息。在接收到消息后事件总线发送一个getMessage事件,让其他组件做出响应。

    var that=this;
                let onMessageReceived = function(event) {
                    // event.data - 存储 Message 对象的数组 - [Message]
                    that.message=event.data[0]['payload'].text;
                    var msg={
                        from:event.data[0]['from'],
                        conversationID:event.data[0]['conversationID'],
                        time:event.data[0]['time'],
                        text:event.data[0]['payload'].text,
                        left:true,
                        right:false
                    };
                    if (Vue.prototype.messageList[event.data[0]['conversationID']]==null){
                        Vue.prototype.messageList[event.data[0]['conversationID']]=[]
                    }
                    Vue.prototype.messageList[event.data[0]['conversationID']].push(msg);
                    console.log(Vue.prototype.messageList[event.data[0]['conversationID']]);
                    EventBus.$emit('getMessage',msg);
                };
                Vue.prototype.tim.on(TIM.EVENT.MESSAGE_RECEIVED, onMessageReceived);
    

      发送消息的核心是SDK实例的createTextMessage方法和sendMessage方法。因为我将SDK实例作为vue全局变量存放,所以代码里用的是Vue.prototype.tim。

    sendMessage:function () {
                    var that=this;
                    console.log(that.id);
                    let message = Vue.prototype.tim.createTextMessage({
                        to: that.id,
                        conversationType: TIM.TYPES.CONV_GROUP,
                        payload: {
                            text:that.textarea
                        }
                    });
                    // 2. 发送消息
                    let promise = Vue.prototype.tim.sendMessage(message);
                    promise.then(function(imResponse) {
                        // 发送成功
                        console.log(imResponse);
                        var msg={
                            from:Vue.prototype.ID,
                            conversationID:that.id,
                            time:'',
                            text:that.textarea,
                            left:false,
                            right:true
                        };
                        if(Vue.prototype.messageList['GROUP'+that.id]==null){
                            Vue.prototype.messageList['GROUP'+that.id]=[]
                        }
                        Vue.prototype.messageList['GROUP'+that.id].push(msg);
                        that.textarea='';
                        EventBus.$emit('send');
                    }).catch(function(imError) {
                        // 发送失败
                        console.warn('sendMessage error:', imError);
                    });
                }
    

    1.3.4获取群组列表

    ​  获取群组列表用的是SDK实例的getGroupList方法。一开始做的时候遇到了一个关于SDK状态的问题。一开始的解决方案是在promise外面套层壳,后来把获取群列表另外封装成一个函数,在另一个壳里调用。所以下面的代码段和后面问题集里的会有不同,但是套壳的思路是一样的。

    getGroups:function () {
    		var that=this;
    		let promise = Vue.prototype.tim.getGroupList(TIM.TYPES.GRP_PROFILE_MEMBER_NUM);
            promise.then(function(imResponse) {
    			console.log('获得组群成功!');
    			that.setGroups(imResponse.data.groupList);
    			}).catch(function(imError) {
    		console.warn('getGroupList error:', imError); // 获取群组列表失败的相关信息
            		});
    		},
    

    1.3.5创建/加入群组

      创建群组用的是SDK实例的createGroup方法,其中还涉及到即时通信的一些状态常量。整体上实现不能,甚至能一次点亮。

    createGroup:function () {
                    var that=this;
                    let promise = Vue.prototype.tim.createGroup({
                        type: TIM.TYPES.GRP_PUBLIC,
                        joinOption:TIM.TYPES.JOIN_OPTIONS_FREE_ACCESS,
                        groupID:that.form.id,
                        name: that.form.name,
                    });
                    promise.then(function(imResponse) { // 创建成功
                        that.$message({
                            type: 'success',
                            message: '创建成功!'
                        });
                        console.log(imResponse.data.group); // 创建的群的资料
                        var msg={
                            id:'GROUP'+that.form.id,
                            name:that.form.name
                        }
                        EventBus.$emit('createGroup',msg);
                        that.dialogFormVisible=false;
                    }).catch(function(imError) {
                        that.$message.error({
                            message: '创建失败 ' + imError
                        });
                        console.warn('createGroup error:', imError); // 创建群组失败的相关信息
                    });
                }
    

    ​  加入群聊用的是SDK实例的joinGroup方法。成功加入群聊后会发一个joinGroup事件提示重新渲染群列表。

    joinGroup:function () {
                    var that=this;
                    this.$prompt('请组群ID', '加入组群', {
                        confirmButtonText: '确定',
                        cancelButtonText: '取消',
                    }).then(({ value }) => {
                        let promise =  Vue.prototype.tim.joinGroup({ groupID: value, type: TIM.TYPES.GRP_PUBLIC });
                        promise.then(function(imResponse) {
                            switch (imResponse.data.status) {
                                case TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL: break; // 等待管理员同意
                                case TIM.TYPES.JOIN_STATUS_SUCCESS: // 加群成功
                                    console.log('加群成功!');
                                    console.log(imResponse.data.group); // 加入的群组资料
                                    that.$message({
                                        type: 'success',
                                        message: '成功加入组群 ' + value
                                    });
                                    EventBus.$emit('joinGroup');
                                    break;
                                default: break;
                            }
                        }).catch(function(imError){
                            that.$message.error({
                                message: '加群失败 ' + imError
                            });
                            console.warn('joinGroup error:', imError); // 申请加群失败的相关信息
                        });
                    }).catch(() => {
                        this.$message({
                            type: 'info',
                            message: '取消输入'
                        });
                    });
                }
    

    1.3.6消息框

      渲染消息框用到了v-for和props传值。v-for把信息列表里对应群组的所有信息用自定义的MessageCard组件渲染出来。

    <div style=" 1060px;height: 486px;">
            <happy-scroll color="rgba(0,0,0,0.5)" size="10" :scroll-top=height>
                <div>
                    <MessageCard
                            v-for="item in this.group"
                            :key="item"
                            :left=item.left
                            :right=item.right
                            :text=item.text
                            :name=item.from
                    />
                    <div style=" 1060px;height: 150px;"></div>
                </div>
            </happy-scroll>
        </div>
    

      其中happy-scroll组件有一个属性值:scroll-top用于实现“总是现实最后一条消息”的效果。在接收到消息和发送了新消息后:scroll-top的值都会+300,让滚动条自己往下滚一点。

    EventBus.$on('send',()=>{
                    this.height+=300;
                });
    EventBus.$on('getMessage',()=>{
                    this.height+=300;
                });
    

    1.4问题集

    1.4.1依赖文件缺失

      官方文档建议在开发前先把demo拿来跑一下,于是我下载了web demo,并配置好appkey等信息。但是web demo成功运行后几秒突然崩溃了,显示依赖丢失。此后再无法通过编译。

      我按照报错信息去查了下demo,是cos-js-sdk-v5下的md5.js文件丢失了。那么问题来了,如果是依赖包丢失了,一开始为什么能通过编译呢?

      于是我去查相关博客,只找到一篇2020-03-09发布的博客提到了此问题。博主认为是md5.js文件被win10安全防护拦截了。于是我自己找了个md5.js的源码放入demo里,项目就能成功跑起来了。

      如果demo会遇到这个问题,那么用sdk进行开发也肯定会遇到一样的问题。果然,当我按照腾讯官方文档教程搭建项目时,跳出了win10防护警告:

      原来就是win10把项目里的md5.js给删了。捕获这个威胁报告后就简单了,把文件还原即可。

      那么“sdk安装后无法正常运行”这么明显bug,为什么开发组人员没有注意到呢?我猜测可能有两个原因:1.根据那篇博客的时间可以发现,仅有的一篇关于此bug的博客是2020年3月份发的。也许在此之前,win10不会拦截下这个文件。故开发组人员没有发现;2.也许是因为开发组常年从事开发,在此之前就给了开发环境足够的权限,让win10不会干涉项目里的文件。所以开发人员的电脑不会遇上此bug。

    1.4.2SDK状态问题

      下面这个问题自于官方文档。我的界面左侧有群列表,这就涉及到“获取已加入的群”功能。我安装官方文档的教程写了一个简易的“获取已加入群”功能函数:

    // 该接口默认只拉取这些资料:群类型、群名称、群头像以及最后一条消息的时间。
    let promise = tim.getGroupList();
    promise.then(function(imResponse) {
    		console.log(imResponse.data.groupList); // 群组列表
    	}).catch(function(imError) {
    		console.warn('getGroupList error:', imError); // 获取群组列表失败的相关信息
    });
    

      但是实际运行的时候,浏览器控制台红了一大片,密密麻麻全是错误信息。我翻了一下,发现了其中一条错误信息是这样:

      这个消息应该是来自于SDK自身的。里面提到要在SDK处于ready状态时才能调用getGroupList()。那SDK在什么时候处于ready状态呢?我接着翻控制台,看到报错信息后几十行又有这一则信息:

      原来就在程序报错后的1000ms内,SDK就进入了ready状态。此时如果调用getGroupList()函数就能成功获取到群列表了。我翻查起自己的项目,找到了这行关于SDK状态的代码:

    Vue.prototype.tim.on(TIM.EVENT.MESSAGE_RECEIVED, onMessageReceived);
    

      不同的是上行代码的状态是“接收到消息”,而我要的状态是“SDK ready”。再去查查官方文档,就拿到了“SDK ready”的表示方法。最后我给之前的代码套了个壳,让外层函数在SDK_READY后执行,就解决了问题(后记:后来我把外层的壳放到了其他位置,但是思路是一样的)。代码如下:

    let promiseFather = function() {
                    let promise = Vue.prototype.tim.getGroupList();
                    promise.then(function(imResponse) {
                        console.log('获得组群成功!');
                        console.log(imResponse.data.groupList); // 群组列表
                    }).catch(function(imError) {
                        console.warn('getGroupList error:', imError); // 获取群组列表失败的相关信息
                    });
                };
                Vue.prototype.tim.on(TIM.EVENT.SDK_READY, promiseFather);
    

      那么官方文档在“获取群列表”此篇为什么对SDK状态只字未提呢?我猜测是因为开发人员搭建项目的逻辑和我搭建项目的逻辑不一样。按照开发人员搭建的流程,他们的项目要发动请求群列表方法时,SDK应该已经是在ready态了。而对于我的项目,我在组件初始化时就调用获得群列表函数,此时SDK还不处于ready态。

      其实就我所想,我认为开发组可以在文档里加上个提示“调用此方法时SDK必须处于ready态”,或者加上关于SDK状态的链接指南,这样可以避免其他人像我一样踩雷。

    2.想用SDK开发的产品

      我设想的产品比较单纯简单:用于机房的网页聊天室

      大学这么多次上机课,我有明显感受。机房电脑常常不带QQ,而且比较卡顿。同学间、师生间要相互传文件的时候比较麻烦。常常是好多个人共用一个同学的U盘。

      如果有一个无需注册的网页聊天室,能够快速在网页上传递消息、传输文件,那无疑能提高上机课的效率。因此可以将产品定义为辅助教学工具向各大学校、单位进行推广。在产品销售的时候,一所学校(单位)对应一个appkey,还可以接入需求方的个性化定制(例如导入学号)。

      这个网页聊天室需求方是:配备大量机房,有很多上机课的学校、单位。网页聊天室的用户是:上机课里的师生。

      网页聊天室的功能不多,只要满足临时传递信息、传输文件即可。具体如下:登录、创建/加入/注销聊天室、传输文件、收发消息。

      可以设想一个详细场景。某17级软工学生萝卜璇去机房上课。打开浏览器就直接跳转到了学校最新定制的fzu聊天室。输入自己的学号+姓名,小璇就完成了登录,再输入ds301就加入了机房的聊天室。聊天室上,老师把这节课的文档和讲义发了出来,并让大家三五个人组队完成。小璇拉上舍友,在fzu聊天室上创建了215聊天室。利用聊天室快速分享各自文件。

    3.采访

    3.1采访对象背景

      采访对象是我高中同学黄小同学,他现在学的是化工专业,他的课程里偶尔有上机课。他也是在家上网课,所以我让他假装在上上机课,体验一下即时通信web demo。

    3.2用户体验及意见

    我:用了一下即时通信,你感觉怎么样?

    黄:挺好的。

    我:你觉得这个工具有什么可以改进的地方呢?

    黄:这个好像不能批量发图的,一次只能发一张。如果把很多图片拖进来发送,会变成只发第一张。我上模拟实验图很多。

    3.3用户对我想开发的产品的意见

    我:如果打开机房电脑浏览器,就有一个类似这样的网页聊天室,你会用吗?

    黄:如果打开浏览器就能用的话那会用。就把这个做方便点。

    3.4我对腾讯即时通信的评价

    推荐

    4.分析SDK

    4.1时间规划

      体验了腾讯即时通信的功能,并搭建了一个小小聊天室应用后。我认为6人团队实现这个SDK至少要6个月时间。因为此SDK功能比较全面,而且面向多个平台。

    4.2同类产品对比优劣

      我选择将腾讯即时通信与网易云信做对比,得到优劣对比如下:

      腾讯即时通信优势:腾讯即时通信demo体验比网易云信好。这点感受太明显了,腾讯即时通讯把demo体验按钮放首页,而网易云信的demo体验被放在导航栏选项里。一开始打开网易云信我还以为他们家没有体验版demo。

      腾讯即时通信劣势:网易云信有独立的论坛,而腾讯即时通信没有。网易云信有独立的论坛,且比较活跃,这对开发者来说帮助很大。而腾讯即时通信没有独立的论坛,开发时似乎只能依靠文档和零零散散的博客。

    4.3团队软工方面的提高

      腾讯即时通信团队可以完善官方文档。官方文档直接关系到购买了SDK的开发者的体验,腾讯即时通信团队可以适当完善文档。

    5.规划我的产品

    5.1同类产品分析

      现在市面上也有类似的网页聊天室产品。但是这些产品功能多大而全。但是太多的功能往往带来复杂的操作。而且系统太大的话容易让机房电脑卡顿。

    5.2 NABCD

    5.2.1 Need(需求)

      随着数字化教学模式的推进,各地学习的上机课越来越多。但是出于机房设备的限制,在机房里进行信息、文件传输并不方便。上机的师生需要一套可以让上机课锦上添花的、专业化的、针对机房的网页聊天室。

    5.2.2 Approach(做法)

      利用腾讯即时通信SDK进行开发,只使用其中创建/加入群组、收发文件、收发消息等功能。让产品功能尽可能小而精,让产品能够最好地适配学校机房设备。

    5.2.3 Benefit(好处)

      产品作为网页聊天室最大的好处是:打开浏览器即可使用,无需注册,无需下载任何软件。这就能极大提高上机课效率,提高教学质量。

    5.2.4 Competitors(竞争)

      现在市面上的网页聊天室功能往往大而全,一方面系统过大会让机房电脑卡顿,另一方很多功能在上机课里用不上。我们的产品尽量做到精简,最大程度简化用户操作,减少系统运行开销。

    5.2.5 Delivery(推广)

      我们的产品的潜在需求方是各大学校、单位。所以推广主要针对学校、单位的管理层。在一所学校(单位)成功试行后,可以由下而上地将这套数字化辅助教学产品向当地教育部门进行宣传,然后将产品推广向其他学校(单位)。

    5.3如何带领团队

    5.3.1我领导团队,会有什么不一样

      我会向团队强调“小而精”的概念,让产品的功能模块够硬够精致。同时强调撰写文档,力求把产品交给用户后,用户翻看翻看文档就能很快上手。

    5.3.2人员分配

      如果团队有五人,我会这样安排:1个美工,2个前端,1个后端,1个测试人员。。撰写文档的工作让小组成员在项目各个阶段里分摊。关于前端安排2人,后端安排1人我是这样想的:此次实践我做了简易聊天室,我发现腾讯即时通讯web SDK功能足够强大,开发过程中几乎不涉及后端代码,主要是前端的JavaScript实现。因此前端多安排一个人。

    5.3.3 16周计划

    第一周-第二周:调研、与客户沟通、需求分析

    第三周:原型设计

    第四周:系统设计

    第五周-第十一周:具体开发阶段

    第十二周-第十四周:测试与优化

    第十五周:撰写系统使用说明书等收尾文档

    第十六周:部署与交付

    5.4部署

    应用服务器配置:4核8G*2

    后端服务器配置:8核16G*2

    关系型数据库:MySql*3(读写分离+备份)

    网站安全性:WAF、DDOS

  • 相关阅读:
    jquery实现选项卡(两句即可实现)
    常用特效积累
    jquery学习笔记
    idong常用js总结
    织梦添加幻灯片的方法
    LeetCode "Copy List with Random Pointer"
    LeetCode "Remove Nth Node From End of List"
    LeetCode "Sqrt(x)"
    LeetCode "Construct Binary Tree from Inorder and Postorder Traversal"
    LeetCode "Construct Binary Tree from Preorder and Inorder Traversal"
  • 原文地址:https://www.cnblogs.com/radishbear/p/12730986.html
Copyright © 2020-2023  润新知