即时通讯应用服务,整套包括服务端、管理端和客户端,欢迎Star支持,查看源码。它已经部署并启动。欢迎体验客户和管理人员。让我们继续完成完整的即时通讯服务。本文重点介绍服务器端项目中的几个要点。大部分内容需要去我的查看仓库源码和egg官网。服务器端详解使用脚手架npminitegg--type=simple初始化服务器项目,安装mysql(我的是8.0版本),配置sequelize需要的数据库链接密码等,然后就可以开始了。关注服务端项目我觉得有几个重点,大部分内容需要去egg官网查看。//目录结构描述├──package.json//项目信息├──app.js//启动文件,里面有一些钩子函数├──app|├──router.js//路由│├──controller│├──service│├──middleware//中间件│├──model//实体模型│└──io//socket.io相关│├──controller│└──middleware//ioMiddleware特有├──config//配置文件|├──plugin.js//插件配置文件|└──config.default.js//默认配置文件├──logs//服务器运行过程中产生的日志文件└──public//静态文件和上传文件目录routesRouter主要用来描述请求之间的对应关系URL和负责执行动作的Controller,即app/router路由使用版本号v1,方便以后升级,一般的增删改查,直接用restful比较简单。除登录和注册接口外,其他所有http接口都增加了session检查,以验证登录状态。位置是app/middleware/auth.js。admin权限检查的位置在app/middleware/admin.js统一认证因为本系统对管理员和普通通信用户的角色不同,所以需要对管理和通信的接口路由进行统一的认证处理。比如管理端的路由/v1/admin/...想给这个系列的所有路由都加上管理员认证。这时候可以使用中间件进行鉴权。下面是admin路由器中使用中间件的具体例子//middwaremodule.exports=()=>{returnasyncfunctionadmin(ctx,next){let{session}=ctx;//确定管理员权限if(session.user&&session.user.rights.some(right=>right.keyName==='admin')){awaitnext();}else{ctx.redirect('/login');}};};//routerconstadmin=app.middleware.admin();router.get('/api/v1/admin/rights',admin,controller.v1.admin.rightsIndex);数据库相关的sequelize+mysql组合,egg也有sequelize相关的插件,sequelize是一个在Node环境下使用的ORM,支持Postgres、MySQL、MariaDB、SQLite和MicrosoftSQLServer,使用起来相当方便。需要先定义模型与模型之间的直接关系。建立关系后,可以使用一些预设的方法。model实体模型model的基本信息比较好处理。需要注意的是实体之间的关系设计,即associate。下面是user的关系描述//User.jsmodule.exports=app=>{const{STRING}=app.续集;constUser=app.model.define('user',{provider:{type:STRING},username:{type:STRING,unique:'username'},password:{type:STRING}});用户。associate=function(){//一对一关联app.model.User.hasOne(app.model.UserInfo);//一对多关联app.model.User.hasMany(app.model.Apply);//多对多关联app.model.User.belongsToMany(app.model.Group,{through:'user_group'});app.model.User.belongsToMany(app.model.Role,{through:'user_role'});};返回用户;};一对一例如,user和userInfo之间的关系是一对一的关系。定义好之后,我们可以在创建新用户的时候使用user.setUserInfo(userInfo),当想获取这个用户的基本信息时,也可以使用user.getUserInfo()。一对多User和Apply(应用)之间的关系是一对多的,即一个用户可以对应多个自己的应用。目前只有好友应用和群组应用:添加应用时可以使用user.addApply(apply),获取时可以这样获取:constresult=a等待ctx.model.Apply.findAndCountAll({其中:{userId:ctx.session.user.id,hasHandled:false}});多对多的user和group是多对多的关系,即一个用户可以对应多个group,一个group也可以对应多个用户,所以sequelize会创建一个中间表user_group来实现这种关系通常,我这样使用它:group.addUser(user);//建立组和用户的关系user.getGroups();//获取用户组信息时需要注意的点sequelize的所有操作都是基于Promise的,所有的大部分时候都是使用await来等待某个model实例的某个属性修改后等待。当我们需要将模型的数据组合起来返回给前端时,我们需要通过get({plain:true})一种方式,将其转化为数据,然后拼接在一起。比如获取session列表,socketioegg提供了egg-socket.io插件。安装egg-socket.io后需要在config/plugin.js中打开插件。io有自己的中间软件和控制器的路由socketioio的路由不同于一般的http请求。注意这里的路由是不能通过添加中间件来处理的(我失败了),所以我在controller中处理静音处理//加入组io。of('/').route('/v1/im/join',app.io.controller.im.join);//发送消息io.of('/').route('/v1/im/new-message',app.io.controller.im.newMessage);//查询消息io.of('/').route('/v1/im/get-messages',app.io.controller.im.获取消息);注:我把群和好友关系看成一个房间(也就是一个session),这样我就可以直接给这个romm发消息,里面的每个人都可以收到socketio。中间件有两个默认的中间件,一个是连接和断开连接时调用的连接中间件,这里用来验证登录状态和处理业务逻辑;另一个是每次发送消息时调用的数据包中间件,这里用于由于预设的Forbidden权限打印日志,在controller中处理//判断用户的发言权限if(!ctx.session.user.rights.some(right=>right.keyName==='speak')){return;}Chat聊天分为对于单聊和群聊,聊天信息暂时包括一般的文字、图片、视频和定位消息,可根据业务扩展为订单或商品消息。message的结构设计参考了几个第三方服务的设计,也结合项目本身的情况进行了调整,可以随意扩展,如下:constMessage=app.model.define('message',{/***消息类型:*0:单聊*1:群聊*/type:{type:STRING},//消息正文body:{type:JSON},fromId:{type:INTEGER},toId:{类型:INTEGER}});body存放消息正文,使用json存放不同的消息格式://文本消息{"type":"txt","msg":"hahaha"//消息内容}//图片消息{"type":"img","url":"http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e","ext":"jpg","w":360,//宽度"h":480,//height"size":388245}//视频消息{"type":'video',"url":"http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e","ext":"mp4","w":360,//宽度"h":480,//高度"size":388245}//地理位置信息{"type":"loc","title":"浙江省杭州市望上路599号",//地理位置title"lng":120.1908686708565,//经度"lat":30.18704515647036//纬度}目前有只有一个定时任务,就是更新百度的token,这里比较简单。参考官方文档看到机器人聊天智能对话定制和服务平台UNIT这个挺有意思的,可以在https://ai.baidu.com/新建机器人并添加相应的技能,我在这里聊天,还有智能问答等。您可以选择新建机器人,管理机器人的技能机器人,至少有一个去百度云“应用列表”创建并查看APIKey/SecretKey在config.default.js中配置baidu相关参数,相关接口说明在这里如果不想启动可以删除app.js和app/schedule/baidu.js中的ctx.service.baidu.getToken();上传文件首先需要在配置文件中进行配置。我这里限制了文件大小,我跨站了ios的视频文件格式:config.multipart={mode:'file',fileSize:'3mb',fileExtensions:['.mov']};统一的接口用于处理文件上传。核心问题是文件的写入。files是从前端发送的文件列表。for(constfileofctx.request.files){//生成文件路径,注意上传文件路径需要存在constfilePath=`./public/upload/${Date.now()+Math.floor(Math.random()*100000).toString()+'.'+文件。文件名.split('.').pop()}`;constreader=fs.createReadStream(file.filepath);//创建可读流constupStream=fs.createWriteStream(filePath);//创建一个可写流读取器.pipe(upStream);//可读流通过管道写入可写流data.push({url:filePath.slice(1)});}我这里存放的是服务器目录/public/upload/,这个目录需要配置静态文件:config.static={prefix:'/public/',dir:path.join(appInfo.baseDir,'public')};passport这章的官方egg文档,要你死啊。没有例子。您必须阅读源代码。太骗人了。研究了半天才搞清楚是怎么回事,因为想更自由的控制账号密码登录,所以账号密码登录没有使用passport,而是使用session的普通接口认证。下面详细介绍使用第三方平台(我选择GitHub)的登录流程:在GitHubOAuthApps中创建你的应用,获取key和secret,在项目中安装egg-passport和egg-passport-github即可打开插件://config/plugin.jsmodule.exports.passport={enable:true,package:'egg-passport',};module.exports.passportGithub={enable:true,package:'egg-passport-github',};configuration://config.default.jsconfig.passportGithub={key:'your_clientID',secret:'your_clientSecret',callbackURL:'http://localhost:3000/api/v1/passport/github/callback'//注意这个很关键,这个需要和你在github上设置的AuthorizationcallbackURL保持一致};在app.js中启用passportthis.app.passport.verify(verify);需要设置两条passportget请求路由,第一个是我们登录页面点击的请求,第二个是我们上一步设置的callbackURL,这里主要是第三方平台会给我们一个可用的code,以及然后根据OAuth2授权规则获取用户的详细信息constgithub=app.passport.authenticate('github',{successRedirect:'/'});//successRedirect是前端最终验证完成后跳转到的路由。我这里是直接跳转到首页的。router.get('/v1/passport/github',github);router.get('/v1/passport/github/callback',github);此时在前端点击/v1/passport/github会发起githubpair这个应用授权成功后,github会发送302到http://localhost:3000/v1/passport/github/callback?code=12313123123,我们的githubPassport插件会获取用户在github上的信息。获取到详细信息后,我们需要对app/passport/verify.js中的用户信息进行校验,并关联到自己平台的用户信息,同时给session赋值//verify.jsmodule.exports=async(ctx,githubUser)=>{const{service}=ctx;const{提供者、名称、照片、显示名称}=githubUser;ctx.logger.info('githubUser',{provider,name,photo,displayName});让用户=等待ctx.model.User。findOne({其中:{用户名:姓名}});if(!user){user=awaitctx.model.User.create({provider,username:name});constuserInfo=awaitctx.model.UserInfo.create({nickname:displayName,photo});constrole=awaitctx.model.Role.findOne({where:{keyName:'user'}});user.setUserInfo(userInfo);user.addRole(角色);等待user.save();}const{权限,角色}=awaitservice.user.getUserAttribute(user.id);//权限判断if(!rights.some(item=>item.keyName==='login')){ctx.body={statusCode:'1',errorMessage:'没有登录权限'};返回;}ctx.session.user={id:user.id,roles,rights};返回githubUser;};注意上面的代码,如果是第一次授权,会创建用户,如果是第二次授权,用户已经在初始化系统部署或运行??时,需要预置一些数据和表。代码在app.js和app/service/startup.js中。逻辑是,项目启动后,使用模型将表结构同步到数据库,然后开始创建一些基础数据:创建新的角色和权限,并为角色分配权限创建不同的用户,为部分用户分配角色,建立友谊,添加应用,创建群组,加一些人。完成以上,初始数据就完成了,可以正常运行部署了,我在腾讯云买了服务器centos,在阿里云买了域名,安装了node(12.18.2),nginx和mysql8.0,直接在centos上启动,前端使用nginx做反向代理由于服务器资源有限,没有使用一些自动化工具Jenkins和Docker,导致更新的时候不得不做一些手动操作。未完待续,下一篇详解前端实现的技术难点
