搭建完整的IM(即时通信)应用(2)

即时通信应用服务,整套包含服务端管理端客户端,欢迎Star支持和查看源码。前端

现已部署上线,欢迎体验客户端管理端node

我们书接上文,继续完成完整的即时通信服务,这篇着重讲下Server端项目中我认为几个重要的点,大部份内容须要去个人仓库源码和 egg 官网查看。mysql

server 端详细说明

使用脚手架npm init egg --type=simple初始化 server 项目,安装 mysql(个人是 8.0 版本),配置上 sequelize 所需的数据库连接密码等,就能够启动了 着重讲下 Server 端项目中我认为几个重要的点,大部份内容须要去 egg 官网查看。ios

// 目录结构说明
 ├── package.json // 项目信息 ├── app.js // 启动文件,其中有一些钩子函数 ├── app | ├── router.js // 路由 │ ├── controller │ ├── service │ ├── middleware // 中间件 │ ├── model // 实体模型 │ └── io // socket.io 相关 │ ├── controller │ └── middleware // io独有的中间件 ├── config // 配置文件 | ├── plugin.js // 插件配置文件 | └── config.default.js // 默认的配置文件 ├── logs // server运行期间产生的log文件 └── public // 静态文件和上传文件目录 复制代码

路由

Router 主要用来描述请求 URL 和具体承担执行动做的 Controller 的对应关系,即 app/routernginx

  1. 路由使用了版本号 v1,方便之后升级,通常的增删改查直接使用 restful 的方式比较简单
  2. 除了登陆和注册的接口,在其他全部 http 接口添加了对 session 的检查,校验登陆状态,位置在 app/middleware/auth.js
  3. 在全部管理端的接口处添加了对 admin 权限的检查,位置在 app/middleware/admin.js

统一鉴权

由于本系统预设有管理员和通常通讯用户的不一样角色,因此须要针对管理和通讯的接口路由作一下统一的鉴权处理。git

好比管理端的路由/v1/admin/...,想在这个系列路由全都添加管理员的鉴权,这时候能够用中间件的方式进行鉴权,下面是在 admin router 中使用中间件的具体例子github

// middware
module.exports = () => {  return async function admin(ctx, next) {  let { session } = ctx;   // 判断admin权限  if (session.user && session.user.rights.some(right => right.keyName === 'admin')) {  await next();  } else {  ctx.redirect('/login');  }  }; };  // router const admin = 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 和 Microsoft SQL Server,使用起来仍是挺方便的。须要先定义模型和模型直接的关系,有了关系以后即可以使用一些预设的方法了。web

model 实体模型

模型的基础信息比较容易处理,须要注意的就是实体之间的关系设计,即 associate,下面是 user 的关系描述sql

// User.js
module.exports = app => {  const { STRING } = app.Sequelize;   const User = app.model.define('user', {  provider: {  type: STRING  },  username: {  type: STRING,  unique: 'username'  },  password: {  type: STRING  }  });   User.associate = function() {  // One-To-One associations  app.model.User.hasOne(app.model.UserInfo);   // One-To-Many associations  app.model.User.hasMany(app.model.Apply);   // Many-To-Many associations  app.model.User.belongsToMany(app.model.Group, { through: 'user_group' });  app.model.User.belongsToMany(app.model.Role, { through: 'user_role' });  };   return User; }; 复制代码

一对一

例如 user 和 userInfo 的关系就是一对一的关系,定义好了以后,咱们在新建 user 的时候就可使用 user.setUserInfo(userInfo)了,想获取此 user 的基础信息的时候也能够经过user.getUserInfo()数据库

一对多

User 和 Apply(申请)的关系就是一对多,即一个用户能够对应多个本身的申请,目前只有好友申请和入群申请:

添加申请的时候能够user.addApply(apply),获取的时候能够这样获取:

const result = await ctx.model.Apply.findAndCountAll({
 where: {  userId: ctx.session.user.id,  hasHandled: false  } }); 复制代码

多对多

user 和 group 的关系就是多对多,即一个用户能够对应多个群组,一个群组也能够对应多个用户,这样 sequelize 会创建一个中间表 user_group 来实现这种关系。

通常我这么使用:

group.addUser(user); // 创建群组和用户的关系
user.getGroups(); // 获取用户的群组信息 复制代码

须要注意的点

  1. sequelize 的全部操做都是基于 Promise 的,全部大多时候都使用 await 进行等待
  2. 修改了某个模型的实例的某个属性后,须要进行 save
  3. 当咱们须要把模型的数据进行组合后返回给前端的时候,须要经过 get({plain: true})这种方式,转化成数据,而后再拼接,例如获取会话列表的时候

socketio

egg 提供了 egg-socket.io 插件,须要在安装 egg-socket.io 后在 config/plugin.js 开启插件,io 有本身的中间件和 controller

socketio 的路由

io 的路由和通常的 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.getMessages); 复制代码

注意:我把群组和好友关系都看作是一个 room(也就是一个会话),这样就是直接向这个 romm 里面发消息,里面的人均可以收到

socketio 的中间件

有两个默认的中间件,一个是链接和断开时候调用的 connection Middleware,这里用来校验登陆状态和处理业务逻辑了;另一个是每次发消息时候调用的 packet Middleware,这里用来打印 log

因为预设了禁言权限,在 controller 里面进行处理

// 对用户发言的权限进行判断
if (!ctx.session.user.rights.some(right => right.keyName === 'speak')) {  return; } 复制代码

聊天

聊天分为单聊和群聊,聊天信息暂时有通常的文字、图片、视频和定位消息,能够根据业务扩展为订单或者商品等

消息

message 的结构设计参考了几家第三方服务的设计,也结合本项目自身的状况作了调整,能够随意扩展,作以下说明:

const Message = app.model.define('message', {
 /**  * 消息类型:  * 0:单聊  * 1:群聊  */  type: {  type: STRING  },  // 消息体  body: {  type: JSON  },  fromId: { type: INTEGER },  toId: { type: INTEGER } }); 复制代码

body 里面存放的是消息体,使用 json 用来存放不一样的消息格式:

// 文本消息
{  "type": "txt",  "msg":"哈哈哈" //消息内容 } 复制代码
// 图片消息
{  "type": "img",  "url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",  "ext":"jpg",  "w":360, //宽  "h":480, //高  "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 // 纬度 } 复制代码

定时任务

当前只有一个,就是更新 baidu 的 token,这里还算简单,参考官方文档便可

机器人聊天

智能对话定制与服务平台 UNIT

这个仍是挺有意思的,能够在 https://ai.baidu.com/ 新建机器人和添加对应的技能,我这里是闲聊,还有智能问答等能够选择

  1. 新建机器人,管理机器人的技能,至少一个
  2. 前往百度云"应用列表"中建立、查看 API Key / Secret Key
  3. 在 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 (const file of ctx.request.files) {
 // 生成文件路径,注意upload文件路径须要存在  const filePath = `./public/upload/${  Date.now() + Math.floor(Math.random() * 100000).toString() + '.' + file.filename.split('.').pop()  }`;  const reader = fs.createReadStream(file.filepath); // 建立可读流  const upStream = fs.createWriteStream(filePath); // 建立可写流  reader.pipe(upStream); // 可读流经过管道写入可写流  data.push({  url: filePath.slice(1)  }); } 复制代码

我这里是存储到了 server 目录的/public/upload/,这个目录须要作一下静态文件的配置:

config.static = {
 prefix: '/public/',  dir: path.join(appInfo.baseDir, 'public') }; 复制代码

passport

这个章节的 egg 官方文档,要你的命,例子啥也没有,必定要去看源码,太坑人了,我研究了好久才弄明白是怎么回事。

由于我想更自由的控制帐户密码登陆,因此帐号密码登陆并无使用 passport,使用的就是普通的接口认证配合 session。

下面详细说下使用第三方平台(我选用的是 GitHub)登陆的过程:

  1. GitHub OAuth Apps新建你的应用,获取 key 和 secret
  2. 在项目安装 egg-passport 和 egg-passport-github

开启插件:

// config/plugin.js
module.exports.passport = {  enable: true,  package: 'egg-passport', };  module.exports.passportGithub = {  enable: true,  package: 'egg-passport-github', }; 复制代码
  1. 配置:
// config.default.js
config.passportGithub = {  key: 'your_clientID',  secret: 'your_clientSecret',  callbackURL: 'http://localhost:3000/api/v1/passport/github/callback' // 注意这里很是的关键,这里须要和你在github上面设置的Authorization callback URL一致 }; 复制代码
  1. 在 app.js 中开启 passport
this.app.passport.verify(verify);
复制代码
  1. 须要设置两个 passport 的 get 请求路由,第一个是咱们在 login 页面点击的请求,第二个是咱们在上一步设置的 callbackURL,这里主要是第三方平台会给咱们一个可用的 code,而后根据 OAuth2 受权规则去获取用户的详细信息
const github = app.passport.authenticate('github', { successRedirect: '/' }); // successRedirect就是最后校验完毕后前端会跳转的路由,我这里直接跳转到主页了
router.get('/v1/passport/github', github); router.get('/v1/passport/github/callback', github); 复制代码
  1. 这时候在前端点击 /v1/passport/github会发起 github 对这个应用的受权,成功后 github 会 302 到 http://localhost:3000/v1/passport/github/callback?code=12313123123,咱们的 githubPassport 插件会去获取用户在 github 上的信息,获取到详细信息后,咱们须要在 app/passport/verify.js 去验证用户信息,而且和咱们自身平台的用户信息作关联,也要给 session 赋值
// verify.js
module.exports = async (ctx, githubUser) => {  const { service } = ctx;  const { provider, name, photo, displayName } = githubUser;  ctx.logger.info('githubUser', { provider, name, photo, displayName });   let user = await ctx.model.User.findOne({  where: {  username: name  }  });   if (!user) {  user = await ctx.model.User.create({  provider,  username: name  });  const userInfo = await ctx.model.UserInfo.create({  nickname: displayName,  photo  });  const role = await ctx.model.Role.findOne({  where: {  keyName: 'user'  }  });  user.setUserInfo(userInfo);  user.addRole(role);  await user.save();  }  const { rights, roles } = await service.user.getUserAttribute(user.id);   // 权限判断  if (!rights.some(item => item.keyName === 'login')) {  ctx.body = {  statusCode: '1',  errorMessage: '不具有登陆权限'  };  return;  }   ctx.session.user = {  id: user.id,  roles,  rights  };   return githubUser; };  复制代码

注意看上面的代码,若是是首次受权将会建立这个用户,若是是第二次受权,那么用户已经被建立了

初始化

系统部署或者运行的时候,须要预设一些数据和表,代码在app.jsapp/service/startup.js

逻辑就是项目启动完毕后,利用 model 同步表结构到数据库中,而后开始新建一些基础数据:

  1. 新建角色和权限,并给角色分配权限
  2. 新建不一样用户,分配角色
  3. 给一些用户创建好友关系
  4. 添加申请
  5. 建立群组,并添加一些人

作完以上这些就算是完成了初始数据了,能够进行正常的运转

部署

我是在腾讯云买的服务器 centos,在阿里云买的域名,装了 node(12.18.2) 、 nginx 和 mysql8.0,直接在 centos 上面启动,前端使用 nginx 进行反向代理。因为服务器资源有限,没有使用一些自动化工具 Jenkins 和 Docker,这就致使了我在更新的时候得有一些手动操做。

未完待续,下一篇讲解前端实现的技术难点

相关文章
相关标签/搜索