├── .eslintrc ├── .gitignore ├── README.md ├── app ├── config │ ├── _README.md │ ├── config.common.js │ ├── config.development.js │ ├── config.production.js │ ├── plugin.development.js │ └── plugin.production.js ├── controller │ ├── _README.md │ └── v1 │ │ ├── message.js │ │ └── user.js ├── deploy │ └── deploy.sh ├── helper │ └── _README.md ├── index.js ├── model │ ├── _README.md │ ├── message.js │ └── user.js ├── schema │ ├── mysql │ │ ├── _README.md │ │ └── im │ │ │ └── user.js │ └── swagger │ │ ├── definitions.js │ │ ├── info.yaml │ │ └── tags.yaml └── socket │ └── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 2017 8 | }, 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 4, 14 | { 15 | "SwitchCase": 1 16 | } 17 | ], 18 | "linebreak-style": [ 19 | "error", 20 | "unix" 21 | ], 22 | "quotes": [ 23 | "error", 24 | "single" 25 | ], 26 | "semi": [ 27 | "error", 28 | "always" 29 | ], 30 | "no-console": [ 31 | "error", 32 | { 33 | "allow": ["log", "info", "error"] 34 | } 35 | ], 36 | "guard-for-in": [ 37 | "error" 38 | ], 39 | "strict": [ 40 | "error", 41 | "global" 42 | ], 43 | "no-var": [ 44 | "error" 45 | ], 46 | "no-multiple-empty-lines": [ 47 | "error", 48 | { 49 | "max": 1 50 | } 51 | ], 52 | "space-before-blocks": [ 53 | "error", 54 | "always" 55 | ], 56 | "no-trailing-spaces": [ 57 | "error", 58 | { 59 | "skipBlankLines": true 60 | } 61 | ] 62 | }, 63 | "globals":{ 64 | "describe": true, 65 | "it": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,vim,node,webstorm,visualstudiocode 3 | 4 | ### OSX ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | # Thumbnails 12 | ._* 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | 29 | ### Vim ### 30 | # swap 31 | [._]*.s[a-v][a-z] 32 | [._]*.sw[a-p] 33 | [._]s[a-v][a-z] 34 | [._]sw[a-p] 35 | # session 36 | Session.vim 37 | # temporary 38 | .netrwhist 39 | *~ 40 | # auto-generated tag files 41 | tags 42 | 43 | 44 | ### Node ### 45 | # Logs 46 | logs 47 | *.log 48 | npm-debug.log* 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Directory for instrumented libs generated by jscoverage/JSCover 57 | lib-cov 58 | 59 | # Coverage directory used by tools like istanbul 60 | coverage 61 | 62 | # nyc test coverage 63 | .nyc_output 64 | 65 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 66 | .grunt 67 | 68 | # node-waf configuration 69 | .lock-wscript 70 | 71 | # Compiled binary addons (http://nodejs.org/api/addons.html) 72 | build/Release 73 | 74 | # Dependency directories 75 | node_modules 76 | jspm_packages 77 | 78 | # Optional npm cache directory 79 | .npm 80 | 81 | # Optional eslint cache 82 | .eslintcache 83 | 84 | # Optional REPL history 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | *.tgz 89 | 90 | # Yarn Integrity file 91 | .yarn-integrity 92 | 93 | 94 | 95 | ### WebStorm ### 96 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 97 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 98 | 99 | # User-specific stuff: 100 | .idea/workspace.xml 101 | .idea/tasks.xml 102 | 103 | # Sensitive or high-churn files: 104 | .idea/dataSources/ 105 | .idea/dataSources.ids 106 | .idea/dataSources.xml 107 | .idea/dataSources.local.xml 108 | .idea/sqlDataSources.xml 109 | .idea/dynamic.xml 110 | .idea/uiDesigner.xml 111 | 112 | # Gradle: 113 | .idea/gradle.xml 114 | .idea/libraries 115 | 116 | # Mongo Explorer plugin: 117 | .idea/mongoSettings.xml 118 | 119 | ## File-based project format: 120 | *.iws 121 | 122 | ## Plugin-specific files: 123 | 124 | # IntelliJ 125 | /out/ 126 | 127 | # mpeltonen/sbt-idea plugin 128 | .idea_modules/ 129 | 130 | # JIRA plugin 131 | atlassian-ide-plugin.xml 132 | 133 | # Crashlytics plugin (for Android Studio and IntelliJ) 134 | com_crashlytics_export_strings.xml 135 | crashlytics.properties 136 | crashlytics-build.properties 137 | fabric.properties 138 | 139 | ### WebStorm Patch ### 140 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 141 | 142 | # *.iml 143 | # modules.xml 144 | # .idea/misc.xml 145 | # *.ipr 146 | 147 | 148 | ### VisualStudioCode ### 149 | .vscode/* 150 | !.vscode/settings.json 151 | !.vscode/tasks.json 152 | !.vscode/launch.json 153 | !.vscode/extensions.json 154 | 155 | # End of https://www.gitignore.io/api/osx,vim,node,webstorm,visualstudiocode 156 | 157 | ### Deploy ### 158 | /app/deploy/.node/ 159 | /app/deploy/.pm2/ 160 | 161 | 162 | ### cloverx-cli ### 163 | # ignore local config 164 | /app/config/config.local.js 165 | /app/config/plugin.local.js 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # im.js.server 2 | >[im.js](https://github.com/im-js/im.js) 服务端代码,基于 [cloverx](https://github.com/clover-x/cloverx) 开发。 3 | 4 | 5 | ## Requirements 6 | * Node >= 7.6.0 7 | * Redis >= 3.0.2 8 | * Mysql >= 5.6.5 9 | 10 | ## Usage 11 | 先前往 `config` 目录,配置基本信息,然后 12 | ```shell 13 | npm install 14 | ``` 15 | 启动 16 | ```shell 17 | npm run dev 18 | ``` 19 | 20 | ## Nginx 配置参考 21 | [x-config/nginx](https://github.com/plusmancn/x-config/tree/master/nginx) 22 | -------------------------------------------------------------------------------- /app/config/_README.md: -------------------------------------------------------------------------------- 1 | `config.local` 和 `plugin.local` 不纳入版本管理 2 | -------------------------------------------------------------------------------- /app/config/config.common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.01.03 10:46:59 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 通用配置文件 9 | */ 10 | 11 | module.exports = { 12 | }; 13 | -------------------------------------------------------------------------------- /app/config/config.development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2016.12.30 11:16:22 4 | * 5 | * Copyright (c) 2016 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 测试环境配置文件 9 | */ 10 | 11 | module.exports = { 12 | 'host': '127.0.0.1', 13 | 'port': 7078 14 | }; 15 | -------------------------------------------------------------------------------- /app/config/config.production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2016.12.30 11:15:36 4 | * 5 | * Copyright (c) 2016 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 生产环境配置文件 9 | */ 10 | 11 | module.exports = { 12 | }; 13 | -------------------------------------------------------------------------------- /app/config/plugin.development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.01.12 16:44:17 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 插件配置-测试环境 9 | */ 10 | 11 | module.exports = { 12 | // 文档配置 13 | doc: { 14 | swaggerDocHost: 'http://swagger.plusman.cn/?url=', 15 | pathHash: '6def414e82cdd4bbeeb8e56b7543fe35', 16 | host: 'im-server.plusman.cn' 17 | }, 18 | mysql: { 19 | 'im': { 20 | database: 'im', 21 | user: 'im_rw', 22 | password: null, 23 | host: '127.0.0.1', 24 | pool: { 25 | max: 10, 26 | min: 0, 27 | idle: 10000 28 | } 29 | } 30 | }, 31 | redis: { 32 | 'main': { 33 | port: 6379, 34 | host: '127.0.0.1', 35 | family: 4, // 4 (IPv4) or 6 (IPv6),, 36 | password: null, 37 | db: '0' 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /app/config/plugin.production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.01.12 16:44:17 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 插件配置-生产环境 9 | */ 10 | 11 | module.exports = { 12 | }; 13 | -------------------------------------------------------------------------------- /app/controller/_README.md: -------------------------------------------------------------------------------- 1 | * 控制器 2 | * 路由 3 | -------------------------------------------------------------------------------- /app/controller/v1/message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * created at 2017 3 | * 4 | * Copyright (c) 2017 plusmancn, all rights 5 | * reserved. 6 | * 7 | * 消息管理 8 | */ 9 | const cloverx = require('cloverx'); 10 | 11 | let router = new cloverx.Router(); 12 | 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /app/controller/v1/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.02.10 11:31:39 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 用户信息维护-控制器 9 | */ 10 | 11 | const cloverx = require('cloverx'); 12 | const modelUser = cloverx.model.get('user'); 13 | 14 | let router = new cloverx.Router(); 15 | let V = cloverx.validator; 16 | 17 | /**jsdoc 18 | * 用户注册 19 | * @tags user 20 | * @httpMethod post 21 | * @path / 22 | * @param {string#formData} name - 用户名,长度小于 20 23 | * @param {string#formData} phone - 用户手机号 24 | * @param {string#formData} socketId - 系统分配的通道 id 25 | * @response @UserInfo 26 | */ 27 | router.push({ 28 | method: 'post', 29 | path: '/', 30 | body: { 31 | name: V.string().max(20).required(), 32 | phone: V.string().regex(/^1\d{10}$/).required(), 33 | socketId: V.string().empty('').optional() 34 | }, 35 | processors: [ 36 | async (ctx, next) => { 37 | let body = ctx.filter.body; 38 | let result = await modelUser.login( 39 | body.name, 40 | body.phone, 41 | body.socketId 42 | ); 43 | 44 | ctx.body = cloverx 45 | .checker 46 | .module('@UserInfo') 47 | .checkAndFormat(result); 48 | return next(); 49 | } 50 | ] 51 | }); 52 | 53 | /**jsdoc 54 | * 更新用户属性 55 | * @tags user 56 | * @httpMethod put 57 | * @path /:userId/property/:field 58 | * @param {string#path} userId - 用户ID 59 | * @param {string#path} field - 需要更新的属性 60 | * @param {string#formData} value - 更新值 61 | * @response @UserInfo 62 | */ 63 | router.push({ 64 | method: 'put', 65 | path: '/:userId/property/:field', 66 | params: { 67 | userId: V.string().required(), 68 | field: V.string().required() 69 | }, 70 | body: { 71 | value: V.any().required() 72 | }, 73 | processors: [ 74 | async (ctx, next) => { 75 | let result = await modelUser.modifyUserInfo( 76 | ctx.filter.params.userId, 77 | ctx.filter.params.field, 78 | ctx.filter.body.value 79 | ); 80 | ctx.body = cloverx 81 | .checker 82 | .module('@UserInfo') 83 | .checkAndFormat(result); 84 | 85 | return next(); 86 | } 87 | ] 88 | }); 89 | 90 | /**jsdoc 91 | * 在线用户列表 92 | * @tags user 93 | * @httpMethod get 94 | * @path /online/list 95 | * @response {:[@UserInfo]} 96 | */ 97 | router.push({ 98 | method: 'get', 99 | path: '/online/list', 100 | processors: [ 101 | async (ctx, next) => { 102 | let result = await modelUser.list(); 103 | 104 | ctx.body = cloverx 105 | .checker 106 | .module('{:[@UserInfo]}') 107 | .checkAndFormat(result); 108 | 109 | return next(); 110 | } 111 | ] 112 | }); 113 | 114 | /**jsdoc 115 | * 用户登出 116 | * @tags user 117 | * @httpMethod delete 118 | * @path /:userId/status 119 | * @param {integer#path} userId - 需要登出的用户ID 120 | * @response @UserInfo 121 | */ 122 | router.push({ 123 | method: 'delete', 124 | path: '/:userId/status', 125 | params: { 126 | userId: V.number().required() 127 | }, 128 | processors: [ 129 | async (ctx, next) => { 130 | let result = await modelUser.logout(ctx.filter.params.userId); 131 | 132 | ctx.body = cloverx 133 | .checker 134 | .module('@UserInfo') 135 | .checkAndFormat(result); 136 | 137 | return next(); 138 | } 139 | ] 140 | }); 141 | 142 | module.exports = router; 143 | -------------------------------------------------------------------------------- /app/deploy/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 应用名 4 | APP_NAME="im-server" 5 | 6 | # linux or darwin 7 | SYSTEM=$(echo $(uname) | awk '{print tolower($0)}'); 8 | 9 | NODE_VERSION="v7.6.0" 10 | NODE_DIR="./.node/" 11 | NODE_TAR="node-${NODE_VERSION}-${SYSTEM}-x64.tar.gz" 12 | 13 | function installNode() { 14 | # https://npm.taobao.org/mirrors/node/v7.6.0/node-v7.6.0-darwin-x64.tar.gz 15 | # https://npm.taobao.org/mirrors/node/v7.6.0/node-v7.6.0-linux-x64.tar.gz 16 | # download and unpack tar file 17 | mkdir -p ./.node/${NODE_VERSION}-${SYSTEM} 18 | wget -P ${NODE_DIR} https://npm.taobao.org/mirrors/node/${NODE_VERSION}/${NODE_TAR} 19 | tar --strip-components=1 -xvzf ${NODE_DIR}${NODE_TAR} -C ${NODE_DIR}${NODE_VERSION}-${SYSTEM} 20 | 21 | # Clean temp files 22 | rm -rf ${NODE_DIR}/${NODE_TAR} 23 | } 24 | 25 | function exportPath() { 26 | NODE_PATH="$(cd "${NODE_DIR}${NODE_VERSION}-${SYSTEM}/bin" && pwd)" 27 | export PATH=$NODE_PATH:$PATH; 28 | 29 | export PM2_HOME='./.pm2/'; 30 | } 31 | 32 | function npmInstall() { 33 | exportPath; 34 | cd ../../; 35 | pwd; 36 | npm -v; 37 | npm install -d --registry=https://registry.npm.taobao.org; 38 | } 39 | 40 | function start() { 41 | if [ -z $1 ]; then 42 | echo '环境字段不能为空,{development|prepub|production}' 43 | exit 1 44 | fi 45 | 46 | exportPath; 47 | node -v 48 | export DEBUG=cloverx:*; 49 | ../../node_modules/.bin/pm2 start ../index.js --name=${APP_NAME} -- --env=$1 50 | } 51 | 52 | function list() { 53 | exportPath; 54 | node -v 55 | ../../node_modules/.bin/pm2 list 56 | } 57 | 58 | function log() { 59 | exportPath; 60 | node -v 61 | ../../node_modules/.bin/pm2 log 62 | } 63 | 64 | function reload() { 65 | exportPath; 66 | node -v 67 | ../../node_modules/.bin/pm2 restart all 68 | } 69 | 70 | function clean() { 71 | exportPath; 72 | node -v 73 | ../../node_modules/.bin/pm2 stop all 74 | ../../node_modules/.bin/pm2 delete all 75 | } 76 | 77 | case "$1" in 78 | start) 79 | start $2 80 | ;; 81 | npm-install) 82 | npmInstall 83 | ;; 84 | install-node) 85 | installNode 86 | ;; 87 | list) 88 | list 89 | ;; 90 | log) 91 | log 92 | ;; 93 | reload) 94 | reload 95 | ;; 96 | 97 | clean) 98 | clean 99 | ;; 100 | *) 101 | echo 'Usage {start|npm-install|install-node}' 102 | exit 1 103 | ;; 104 | esac 105 | -------------------------------------------------------------------------------- /app/helper/_README.md: -------------------------------------------------------------------------------- 1 | * 辅助函数,单一功能函数 2 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.01.03 10:28:11 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 测试项目入口文件 9 | */ 10 | 11 | const yargv = require('yargs').argv; 12 | // 变量可选值:development, production 13 | process.env.NODE_ENV = (yargv.env || 'development').toLowerCase(); 14 | 15 | // moment 配置 16 | require('moment').locale('zh-cn'); 17 | 18 | // 框架启动 19 | require('cloverx').start({ 20 | baseDir: __dirname, 21 | cloverEnv: process.env.NODE_ENV 22 | }); 23 | 24 | // 启动 socket 服务 25 | require('./socket/index.js'); 26 | 27 | process.on('uncaughtException', (err) => { 28 | console.error(err); 29 | }); 30 | 31 | process.on('unhandledRejection', (err) => { 32 | console.error(err); 33 | }); 34 | -------------------------------------------------------------------------------- /app/model/_README.md: -------------------------------------------------------------------------------- 1 | * 业务处理逻辑 2 | -------------------------------------------------------------------------------- /app/model/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017 4 | * 5 | * Copyright (c) 2017 plusmancn, all rights 6 | * reserved. 7 | * 8 | * 消息管理,基于 redis 9 | */ 10 | const uuid = require('uuid'); 11 | 12 | const cloverx = require('cloverx'); 13 | const modelUser = cloverx.model.get('user'); 14 | const redis = cloverx.connection.get('redis').get('main'); 15 | 16 | /** 17 | * 私聊消息 18 | * 判断将用户消息存入队列还是直接发送 19 | */ 20 | async function sendPeerMessage(socket, payloads) { 21 | let user = await modelUser.getByUserId(payloads[0].to); 22 | 23 | // 补充消息内容 24 | payloads = payloads.map((payload) => { 25 | payload.uuid = payload.uuid || uuid.v4(); 26 | payload.ext.timestamp = +(new Date()); 27 | return payload; 28 | }); 29 | 30 | if (user.status === 'online' && user.socketId) { 31 | socket 32 | .to(user.socketId) 33 | .emit('message', payloads); 34 | } else { 35 | let serialPayloads = payloads.map((payload) => { 36 | return JSON.stringify(payload); 37 | }); 38 | redis.rpush(`offline:queue:userId:${user.userId}`, ...serialPayloads); 39 | } 40 | } 41 | 42 | /** 43 | * 私聊消息 44 | * 将用户离线消息推送给用户,离线消息封顶 1000 条 45 | */ 46 | async function sendOfflineMessage(socket, userId) { 47 | let redisKey = `offline:queue:userId:${userId}`; 48 | let queue = await redis.lrange(redisKey, 0, -1); 49 | if(!queue.length) { 50 | return; 51 | } 52 | 53 | let payloads = queue.map((item) => { 54 | return JSON.parse(item); 55 | }); 56 | 57 | // 数据拆分 58 | let payloadsDict = new Map(); 59 | for(let i = 0; i < payloads.length; i++) { 60 | let payload = payloads[i]; 61 | let { from } = payload; 62 | 63 | if (payloadsDict.get(from)) { 64 | payloadsDict.get(from).push(payload); 65 | } else { 66 | payloadsDict.set(from, [payload]); 67 | } 68 | } 69 | 70 | for (let value of payloadsDict.values()) { 71 | socket 72 | .emit('message', value); 73 | } 74 | 75 | // 收到 Ack 后,清空离线队列 76 | await redis.del(redisKey); 77 | } 78 | 79 | module.exports = { 80 | sendPeerMessage, 81 | sendOfflineMessage 82 | }; 83 | -------------------------------------------------------------------------------- /app/model/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.02.10 13:56:21 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 用户信息维护-模型 9 | */ 10 | 11 | const cloverx = require('cloverx'); 12 | const schemaUser = cloverx.mysql.get('im/user'); 13 | const pinyin = require('pinyin'); 14 | 15 | const RANDOM_AVATAR = [ 16 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_01.jpg', 17 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_02.png', 18 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_03.jpg', 19 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_04.png', 20 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_05.jpeg', 21 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_06.jpg', 22 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_07.jpg', 23 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_08.png', 24 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_09.png', 25 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_10.png', 26 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_11.jpg', 27 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_12.jpg', 28 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_13.png', 29 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_14.png', 30 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_15.png', 31 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_16.png', 32 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_17.png', 33 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_18.png', 34 | 'http://image-2.plusman.cn/app/im-client/avatar/tuzki_19.jpg' 35 | ]; 36 | 37 | /** 38 | * 用户注册 39 | */ 40 | async function login (name, phone, socketId = '') { 41 | let user = await schemaUser.findOne({ 42 | where: { 43 | phone: phone 44 | } 45 | }); 46 | 47 | // if(user && user.status === 'online') { 48 | // throw cloverx.Error.badParameter(`手机用户 ${phone} 已经在线`); 49 | // } 50 | 51 | // 随机头像 52 | let avatar = RANDOM_AVATAR[Math.floor((Math.random() * RANDOM_AVATAR.length))]; 53 | // 姓名首字母 54 | let firstLetter = getNameFirstLetter(name); 55 | 56 | let result; 57 | if(user) { 58 | result = await user.update({ 59 | name, 60 | socketId, 61 | firstLetter, 62 | status: 'online' 63 | }); 64 | } else { 65 | result = await schemaUser 66 | .build({ 67 | name, 68 | phone, 69 | avatar, 70 | socketId, 71 | firstLetter, 72 | status: 'online' 73 | }) 74 | .save(); 75 | } 76 | 77 | return result; 78 | } 79 | 80 | /** 81 | * 用户在线状态切换 82 | */ 83 | async function changeUserOnlineStatus(socketId, status) { 84 | let user = await schemaUser 85 | .findOne({ 86 | where: { 87 | socketId: socketId 88 | } 89 | }); 90 | 91 | if (user) { 92 | return await user.update({ 93 | status: status 94 | }); 95 | } 96 | } 97 | 98 | /** 99 | * 修改用户属性 100 | */ 101 | async function modifyUserInfo (userId, field, value) { 102 | if (!~['name', 'socketId', 'vibration'].indexOf(field)) { 103 | throw cloverx.Error.badParameter(`字段 ${field} 不可更改`); 104 | } 105 | 106 | let user = await schemaUser 107 | .findOne({ 108 | where: { 109 | userId: userId 110 | } 111 | }); 112 | 113 | if(!user) { 114 | throw cloverx.Error.badParameter(`用户ID ${userId} 不存在`); 115 | } 116 | 117 | if (field === 'name') { 118 | return await user.update({ 119 | [field]: value, 120 | firstLetter: getNameFirstLetter(value) 121 | }); 122 | } else if(field === 'socketId') { 123 | return await user.update({ 124 | [field]: value, 125 | status: 'online' 126 | }); 127 | } else { 128 | return await user.update({ 129 | [field]: value, 130 | }); 131 | } 132 | } 133 | 134 | /** 135 | * 用户登出 136 | */ 137 | async function logout(userId) { 138 | let user = await schemaUser.findOne({ 139 | where: { 140 | userId: userId 141 | } 142 | }); 143 | 144 | if(!user) { 145 | throw cloverx.Error.badParameter(`用户ID ${userId} 不存在`); 146 | } 147 | 148 | return await user.update({ 149 | status: 'offline' 150 | }); 151 | } 152 | 153 | /** 154 | * 获取单个用户 155 | */ 156 | async function getByUserId(userId) { 157 | let user = await schemaUser.findOne({ 158 | where: { 159 | userId: userId 160 | }, 161 | raw: true 162 | }); 163 | 164 | return user; 165 | } 166 | 167 | /** 168 | * 拉取在线用户列表 169 | */ 170 | async function list(status) { 171 | let where = {}; 172 | if(status) { 173 | where.status = status; 174 | } 175 | 176 | let result = await schemaUser.findAll({ 177 | attributes: ['userId', 'avatar', 'name', 'phone', 'socketId', 'status', 'firstLetter', 'vibration'], 178 | where, 179 | order: [ 180 | ['firstLetter', 'asc'], 181 | ['updatedAt', 'desc'] 182 | ], 183 | raw: true 184 | }); 185 | 186 | let sectionSort = {}; 187 | for(let i = 0; i< result.length; i++) { 188 | let { firstLetter, vibration } = result[i]; 189 | // 震动模式 190 | result[i].vibration = !!vibration; 191 | // 首字母 192 | if( !sectionSort[firstLetter] ) { 193 | sectionSort[firstLetter] = [result[i]]; 194 | } else { 195 | sectionSort[firstLetter].push(result[i]); 196 | } 197 | } 198 | 199 | return sectionSort; 200 | } 201 | 202 | /** 203 | * 获取用户首字母 204 | */ 205 | function getNameFirstLetter(name) { 206 | let result = pinyin(name[0]); 207 | return result[0][0][0].toLowerCase(); 208 | } 209 | 210 | module.exports = { 211 | login, 212 | logout, 213 | getByUserId, 214 | list, 215 | modifyUserInfo, 216 | changeUserOnlineStatus 217 | }; 218 | -------------------------------------------------------------------------------- /app/schema/mysql/_README.md: -------------------------------------------------------------------------------- 1 | * mysql 数据库,以文件夹定义数据库名 2 | -------------------------------------------------------------------------------- /app/schema/mysql/im/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.02.10 10:22:43 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 用户表 9 | */ 10 | 11 | const cloverx = require('cloverx'); 12 | const S = cloverx.S; 13 | 14 | module.exports = { 15 | fields: { 16 | userId: { 17 | primaryKey: true, 18 | type: S.INTEGER(11).UNSIGNED, 19 | // 如果为空,则默认值是将键名从 camelCase 转换为 underscore 20 | field: 'id', 21 | allowNull: false, 22 | autoIncrement: true, 23 | comment: '用户 ID' 24 | }, 25 | avatar: { 26 | type: S.STRING(250), 27 | allowNull: false, 28 | comment: '头像地址' 29 | }, 30 | name: { 31 | type: S.STRING(32), 32 | allowNull: false, 33 | comment: '用户名' 34 | }, 35 | firstLetter: { 36 | type: S.STRING(1), 37 | allowNull: false, 38 | comment: '用户首字母' 39 | }, 40 | phone: { 41 | type: S.STRING(11), 42 | allowNull: false, 43 | comment: '用户手机号' 44 | }, 45 | socketId: { 46 | type: S.STRING(64), 47 | allowNull: false, 48 | comment: '当前 socketId' 49 | }, 50 | status: { 51 | type: S.ENUM('online', 'offline', 'delete'), 52 | allowNull: false, 53 | defaultValue: 'offline', 54 | comment: '用户在线状态' 55 | }, 56 | vibration: { 57 | type: S.BOOLEAN, 58 | allowNull: false, 59 | defaultValue: true, 60 | comment: '是否开启震动' 61 | } 62 | }, 63 | comment: '用户表' 64 | }; 65 | -------------------------------------------------------------------------------- /app/schema/swagger/definitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.01.10 19:10:43 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * 数据类型定义 9 | */ 10 | module.exports = { 11 | StdResponse: { 12 | type: 'object', 13 | description: '标准返回模块', 14 | properties: { 15 | success: { 16 | type: 'boolean', 17 | description: '请求是否成功' 18 | }, 19 | code: { 20 | type: 'number', 21 | description: '请求状态码,10000 为无错误', 22 | default: 10000 23 | }, 24 | msg: { 25 | type: 'string', 26 | description: '错误描述' 27 | } 28 | } 29 | }, 30 | UserInfo: { 31 | type: 'object', 32 | description: '用户信息', 33 | properties: { 34 | userId: { 35 | type: 'number', 36 | description: '系统分配的 userId' 37 | }, 38 | avatar: { 39 | type: 'string', 40 | description: '用户头像' 41 | }, 42 | name: { 43 | type: 'string', 44 | description: '用户名' 45 | }, 46 | phone: { 47 | type: 'string', 48 | description: '用户手机号' 49 | }, 50 | socketId: { 51 | type: 'string', 52 | description: '通道 socketId' 53 | }, 54 | status: { 55 | type: 'string', 56 | description: '在线状态' 57 | }, 58 | vibration: { 59 | type: 'boolean', 60 | description: '是否开启震动' 61 | } 62 | } 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /app/schema/swagger/info.yaml: -------------------------------------------------------------------------------- 1 | version: 1.0.0 2 | title: im.js-server 3 | description: | 4 | 基于 socket.io + cloverx 实现 5 | -------------------------------------------------------------------------------- /app/schema/swagger/tags.yaml: -------------------------------------------------------------------------------- 1 | - name: user 2 | description: 用户信息 3 | -------------------------------------------------------------------------------- /app/socket/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * created at 2017.02.10 00:39:25 4 | * 5 | * Copyright (c) 2017 Souche.com, all rights 6 | * reserved. 7 | * 8 | * socket 服务器入口文件 9 | */ 10 | 11 | // 启动 socket.io 12 | const cloverx = require('cloverx'); 13 | const io = require('socket.io')(cloverx.server); 14 | 15 | const modelUser = cloverx.model.get('user'); 16 | const modelMessage = cloverx.model.get('message'); 17 | 18 | io.on('connection', function (socket) { 19 | socket.on('message', function (payloads) { 20 | modelMessage.sendPeerMessage(socket, payloads); 21 | }); 22 | 23 | socket.on('disconnect', function () { 24 | modelUser.changeUserOnlineStatus(socket.id, 'offline'); 25 | }); 26 | 27 | socket.on('user:online', function (data) { 28 | modelMessage.sendOfflineMessage(socket, data.userId); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "im-server", 3 | "version": "1.0.0", 4 | "description": "Cover Start Project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "DEBUG=cloverx:* node app/index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "cloverx": "^1.1.4", 18 | "lodash": "^4.17.3", 19 | "moment": "^2.17.1", 20 | "pinyin": "^2.8.0", 21 | "socket.io": "^1.7.2", 22 | "uuid": "^3.0.1", 23 | "yargs": "^6.5.0" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^3.15.0", 27 | "pm2": "^2.4.2" 28 | } 29 | } 30 | --------------------------------------------------------------------------------