搭建完整的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,這就致使了我在更新的時候得有一些手動操做。

未完待續,下一篇講解前端實現的技術難點

相關文章
相關標籤/搜索