内容简介:即时通讯应用服务,整套包含现已部署上线,欢迎体验咱们书接上文,继续完成完整的即时通讯服务,这篇着重讲下Server端项目中我认为几个重要的点,大部分内容需要去我的仓库
即时通讯应用服务,整套包含 服务端 、 管理端 和 客户端 ,欢迎Star支持和查看源码。
咱们书接上文,继续完成完整的即时通讯服务,这篇着重讲下Server端项目中我认为几个重要的点,大部分内容需要去我的仓库 源码 和 egg 官网 查看。
server 端详细说明
使用脚手架 npm init egg --type=simple
初始化 server 项目,安装 mysql(我的是 8.0 版本),配置上 sequelize 所需的数据库链接密码等,就可以启动了
着重讲下 Server 端项目中我认为几个重要的点,大部分内容需要去 egg 官网 查看。
// 目录结构说明 ├── 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/router
app/middleware/auth.js app/middleware/admin.js
统一鉴权
因为本系统预设有管理员和一般通信用户的不同角色,所以需要针对管理和通信的接口路由做一下统一的鉴权处理。
比如管理端的路由 /v1/admin/...
,想在这个系列路由全都添加管理员的鉴权,这时候可以用中间件的方式进行鉴权,下面是在 admin router 中使用中间件的具体例子
// 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,使用起来还是挺方便的。需要先定义模型和模型直接的关系,有了关系之后便可以使用一些预设的方法了。
model 实体模型
模型的基础信息比较容易处理,需要注意的就是实体之间的关系设计,即 associate,下面是 user 的关系描述
// 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(); // 获取用户的群组信息
需要注意的点
- sequelize 的所有操作都是基于 Promise 的,所有大多时候都使用 await 进行等待
- 修改了某个模型的实例的某个属性后,需要进行 save
- 当我们需要把模型的数据进行组合后返回给前端的时候,需要通过 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/
新建机器人和添加对应的技能,我这里是闲聊,还有智能问答等可以选择
- 新建机器人,管理机器人的技能,至少一个
- 前往百度云"应用列表"中创建、查看 API Key / Secret Key
- 在 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)登录的过程:
- 在 GitHub OAuth Apps 新建你的应用,获取 key 和 secret
- 在项目安装 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', };
- 配置:
// config.default.js config.passportGithub = { key: 'your_clientID', secret: 'your_clientSecret', callbackURL: 'http://localhost:3000/api/v1/passport/github/callback' // 注意这里非常的关键,这里需要和你在github上面设置的Authorization callback URL一致 };
- 在 app.js 中开启 passport
this.app.passport.verify(verify);
- 需要设置两个 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);
-
这时候在前端点击
/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.js
和 app/service/startup.js
逻辑就是项目启动完毕后,利用 model 同步表结构到数据库中,然后开始新建一些基础数据:
- 新建角色和权限,并给角色分配权限
- 新建不同用户,分配角色
- 给一些用户建立好友关系
- 添加申请
- 创建群组,并添加一些人
做完以上这些就算是完成了初始数据了,可以进行正常的运转
部署
我是在腾讯云买的服务器 centos,在阿里云买的域名,装了 node(12.18.2) 、 nginx 和 mysql 8.0,直接在 centos 上面启动,前端使用 nginx 进行反向代理。由于服务器资源有限,没有使用一些自动化工具 Jenkins 和 Docker,这就导致了我在更新的时候得有一些手动操作。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 实战 Prometheus 搭建监控系统
- go micro实战01:快速搭建服务
- Vue最全实战教程系列(1): 环境搭建
- 实战:用Microsoft 365搭建零信任网络
- 「实战」搭建完整的IM(即时通讯)应用(1)
- 「小程序JAVA实战」springboot的后台搭建(30)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。