├── docs ├── .nojekyll ├── logo.png ├── index.html └── README.md ├── .gitignore ├── docker ├── .gitignore ├── nginx │ ├── logs │ │ └── .gitkeep │ └── nginx.conf ├── docker-compose.yml └── mysql │ └── speedy-im_2020-10-14.sql ├── client ├── config │ ├── dev.ts │ ├── prod.ts │ └── index.ts ├── .gitignore ├── .prettierrc ├── static │ └── images │ │ ├── logo.png │ │ ├── icon │ │ ├── add.png │ │ ├── apply.png │ │ ├── emoji.png │ │ ├── group.png │ │ ├── plus.png │ │ ├── scan.png │ │ └── triangle.png │ │ └── tab │ │ ├── chat.png │ │ ├── user.png │ │ ├── chat-active.png │ │ ├── user-active.png │ │ ├── address-book.png │ │ └── address-book-active.png ├── unpackage │ ├── speedy-im.keystore │ └── res │ │ └── icons │ │ ├── 20x20.png │ │ ├── 29x29.png │ │ ├── 40x40.png │ │ ├── 58x58.png │ │ ├── 60x60.png │ │ ├── 72x72.png │ │ ├── 76x76.png │ │ ├── 80x80.png │ │ ├── 87x87.png │ │ ├── 96x96.png │ │ ├── 1024x1024.png │ │ ├── 120x120.png │ │ ├── 144x144.png │ │ ├── 152x152.png │ │ ├── 167x167.png │ │ ├── 180x180.png │ │ └── 192x192.png ├── shim.d.ts ├── helper │ ├── styles │ │ └── color.scss │ ├── request.ts │ ├── util.ts │ └── storage.ts ├── App.vue ├── enum │ └── message.ts ├── interface │ ├── response.ts │ └── entity.ts ├── package.json ├── main.js ├── store │ ├── index.ts │ └── modules │ │ ├── message.ts │ │ └── user.ts ├── tsconfig.json ├── router │ ├── routes.ts │ └── index.ts ├── public │ └── template.h5.html ├── platform │ └── app-plus │ │ └── subNVue │ │ └── menu.nvue ├── components │ └── popupMenu.vue ├── uni.scss ├── pages.json ├── pages │ ├── user │ │ ├── me.vue │ │ ├── signUp.vue │ │ └── signIn.vue │ ├── addressBook │ │ └── index.vue │ └── chat │ │ ├── list.vue │ │ ├── chat.nvue.back │ │ └── chat.vue ├── socket │ └── chat.ts ├── manifest.json └── yarn.lock ├── server ├── src │ ├── config │ │ ├── dev.ts │ │ └── index.ts │ ├── lib │ │ └── db.ts │ ├── routes │ │ ├── index.ts │ │ ├── message.ts │ │ └── user.ts │ ├── interface │ │ ├── config.ts │ │ ├── response.ts │ │ └── entity.ts │ ├── enum │ │ └── message.ts │ ├── service │ │ ├── group.ts │ │ ├── relation.ts │ │ ├── message.ts │ │ └── user.ts │ ├── socket │ │ ├── auth.ts │ │ └── chat.ts │ ├── helper │ │ ├── jwt.blacklist.ts │ │ └── util.ts │ └── app.ts ├── .gitignore ├── .prettierrc ├── ecosystem.config.js ├── Dockerfile ├── .eslintrc.js ├── package.json └── tsconfig.json ├── README.md └── LICENSE /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | mysql/data -------------------------------------------------------------------------------- /docker/nginx/logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/config/dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | } -------------------------------------------------------------------------------- /server/src/config/dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/config/prod.ts 3 | dist -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | unpackage/dist 2 | unpackage/debug 3 | node_modules -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/docs/logo.png -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /client/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/logo.png -------------------------------------------------------------------------------- /client/static/images/icon/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/icon/add.png -------------------------------------------------------------------------------- /client/static/images/icon/apply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/icon/apply.png -------------------------------------------------------------------------------- /client/static/images/icon/emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/icon/emoji.png -------------------------------------------------------------------------------- /client/static/images/icon/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/icon/group.png -------------------------------------------------------------------------------- /client/static/images/icon/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/icon/plus.png -------------------------------------------------------------------------------- /client/static/images/icon/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/icon/scan.png -------------------------------------------------------------------------------- /client/static/images/tab/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/tab/chat.png -------------------------------------------------------------------------------- /client/static/images/tab/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/tab/user.png -------------------------------------------------------------------------------- /client/unpackage/speedy-im.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/speedy-im.keystore -------------------------------------------------------------------------------- /client/shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | declare module "*.jpg"; 3 | declare module "*.svg"; 4 | declare module "*.webp"; -------------------------------------------------------------------------------- /client/unpackage/res/icons/20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/20x20.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/29x29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/29x29.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/40x40.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/58x58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/58x58.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/60x60.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/72x72.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/76x76.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/80x80.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/87x87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/87x87.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/96x96.png -------------------------------------------------------------------------------- /client/static/images/icon/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/icon/triangle.png -------------------------------------------------------------------------------- /client/static/images/tab/chat-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/tab/chat-active.png -------------------------------------------------------------------------------- /client/static/images/tab/user-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/tab/user-active.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/1024x1024.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/120x120.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/144x144.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/152x152.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/167x167.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/180x180.png -------------------------------------------------------------------------------- /client/unpackage/res/icons/192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/unpackage/res/icons/192x192.png -------------------------------------------------------------------------------- /client/static/images/tab/address-book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/tab/address-book.png -------------------------------------------------------------------------------- /server/src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import Mysql from '@hyoga/mysql'; 2 | import config from '../config'; 3 | 4 | export default new Mysql(config.mysql); 5 | -------------------------------------------------------------------------------- /client/static/images/tab/address-book-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyue88/speedy-im/HEAD/client/static/images/tab/address-book-active.png -------------------------------------------------------------------------------- /client/helper/styles/color.scss: -------------------------------------------------------------------------------- 1 | $gray: #999; 2 | $lightGray: #666; 3 | $background: #f5f5f5; 4 | $blue: #1441B8; 5 | $lightBlue: #5a85f9; 6 | $borderColor: #fafafa; -------------------------------------------------------------------------------- /client/config/prod.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | baseUrl: 'http://app.speedy-im.com/api', 3 | ws: { 4 | host: 'http://app.speedy-im.com', 5 | namespace: 'chat', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /server/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | /* GET home page. */ 6 | router.get('/', (req, res) => { 7 | res.send('welcome'); 8 | }); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # speedy-im 2 | 3 | **该项目已停止更新**,由于项目无需支持小程序以及 H5,故使用 React-Native 进行重构,并将项目拆分重构,新项目功能更完整,性能更佳。 4 | 5 | 新项目请查看[KitIM](https://gitee.com/kitim) 6 | 7 | - [kitim-react-native](https://gitee.com/kitim/kitim-react-native) 8 | - [kitim-server](https://gitee.com/kitim-server) 9 | -------------------------------------------------------------------------------- /server/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: 'speedy-im', 4 | script: './dist/app.js', 5 | env: { 6 | NODE_ENV: 'development', 7 | }, 8 | env_production: { 9 | NODE_ENV: 'production', 10 | }, 11 | }], 12 | }; 13 | -------------------------------------------------------------------------------- /server/src/interface/config.ts: -------------------------------------------------------------------------------- 1 | import { Config as MysqlConfig } from '@hyoga/mysql'; 2 | 3 | export type EnvType = 'local' | 'development' | 'production'; 4 | 5 | export interface Config { 6 | mysql: MysqlConfig; 7 | jwt: { 8 | secret: string; // jwt加密秘钥 9 | routeWhiteList: string[]; 10 | }, 11 | passwordSecret: string; // 密码加密存储秘钥 12 | } 13 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 4 | RUN apk add --no-cache tzdata \ 5 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 6 | && echo "Asia/Shanghai" > /etc/timezone \ 7 | &&rm -rf /var/cache/apk/* /tmp/* /var/tmp/* $HOME/.cache 8 | 9 | WORKDIR /app 10 | 11 | COPY . . 12 | 13 | RUN yarn install && yarn build 14 | 15 | EXPOSE 8360 16 | 17 | CMD npm run prod 18 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mysql: 4 | image: mysql:5.6 5 | container_name: speedy-mysql 6 | ports: 7 | - "3307:3306" 8 | volumes: 9 | - ./mysql/data:/var/lib/mysql 10 | environment: 11 | - TZ=Asia/Shanghai 12 | - MYSQL_ROOT_PASSWORD=123456 13 | command: --default-authentication-plugin=mysql_native_password 14 | im: 15 | build: ../server 16 | container_name: speedy-im 17 | ports: 18 | - "8360:8360" 19 | depends_on: 20 | - mysql -------------------------------------------------------------------------------- /client/config/index.ts: -------------------------------------------------------------------------------- 1 | import dev from './dev'; 2 | import prod from './prod'; 3 | 4 | type EnvType = 'local' | 'development' | 'production'; 5 | const env: EnvType = (process.env.NODE_ENV as EnvType) || 'local'; 6 | 7 | const configMap = { 8 | local: {}, 9 | development: dev, 10 | production: prod, 11 | }; 12 | 13 | const defaults = { 14 | baseUrl: 'http://10.12.174.77:8360/api', 15 | ws: { 16 | host: 'http://10.12.174.77:8360', 17 | namespace: 'chat', 18 | }, 19 | }; 20 | 21 | const config: any = { 22 | ...defaults, 23 | ...(configMap[env] || {}), 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /client/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /client/enum/message.ts: -------------------------------------------------------------------------------- 1 | export enum ENUM_MESSAGE_DIST_TYPE { 2 | PRIVATE = 1, 3 | GROUP = 2, 4 | } 5 | 6 | export enum ENUM_MESSAGE_CONTENT_TYPE { 7 | TEXT = 'text', 8 | AUDIO = 'audio', 9 | IMAGE = 'image', 10 | VIDEO = 'video', 11 | } 12 | 13 | // socket通信内容类型 14 | export enum ENUM_SOCKET_MESSAGE_TYPE { 15 | PRIVATE_CHAT = 1000, 16 | GROUP_CHAT, 17 | MESSAGE_STATUS_CONFIRM, // 消息发送状态确认 18 | } 19 | 20 | // 消息发送成功与否的状态 21 | export enum ENUM_MESSAGE_RESPONSE_STATUS { 22 | SUCCESS = 1000, 23 | ERROR, 24 | INVALID_PARAMS, 25 | USER_NOT_EXIST, 26 | NOT_FRIEND_OF_OTHER, // 自己不是对方好友 27 | NOT_FRIEND_OF_MINE, // 对方不是自己好友 28 | NOT_IN_GROUP, 29 | } 30 | -------------------------------------------------------------------------------- /server/src/enum/message.ts: -------------------------------------------------------------------------------- 1 | export enum ENUM_MESSAGE_DIST_TYPE { 2 | PRIVATE = 1, 3 | GROUP = 2, 4 | } 5 | 6 | export enum ENUM_MESSAGE_CONTENT_TYPE { 7 | TEXT = 'text', 8 | AUDIO = 'audio', 9 | IMAGE = 'image', 10 | VIDEO = 'video', 11 | } 12 | 13 | // socket通信内容类型 14 | export enum ENUM_SOCKET_MESSAGE_TYPE { 15 | PRIVATE_CHAT = 1000, 16 | GROUP_CHAT, 17 | MESSAGE_STATUS_CONFIRM, // 消息发送状态确认 18 | } 19 | 20 | // 消息发送成功与否的状态 21 | export enum ENUM_MESSAGE_RESPONSE_STATUS { 22 | SUCCESS = 1000, 23 | ERROR, 24 | INVALID_PARAMS, 25 | USER_NOT_EXIST, 26 | NOT_FRIEND_OF_OTHER, // 自己不是对方好友 27 | NOT_FRIEND_OF_MINE, // 对方不是自己好友 28 | NOT_IN_GROUP, 29 | } 30 | -------------------------------------------------------------------------------- /server/src/service/group.ts: -------------------------------------------------------------------------------- 1 | import db from '../lib/db'; 2 | 3 | class Group { 4 | private table = 'user_group'; 5 | 6 | /** 7 | * 获取用户加入的某个群 8 | * 9 | * @param {number} uid 用户ID 10 | * @param {number} groupId 群ID 11 | * @returns 群信息 12 | */ 13 | async getUserGroup(uid: number, groupId: number) { 14 | try { 15 | const data = await db.table(this.table) 16 | .where({ 17 | uid, 18 | group_id: groupId, 19 | status: 1, 20 | }) 21 | .select(); 22 | return [null, data]; 23 | } catch (err) { 24 | return [err, null]; 25 | } 26 | } 27 | } 28 | 29 | export default new Group(); 30 | -------------------------------------------------------------------------------- /server/src/interface/response.ts: -------------------------------------------------------------------------------- 1 | import { ENUM_MESSAGE_RESPONSE_STATUS, ENUM_MESSAGE_DIST_TYPE, ENUM_SOCKET_MESSAGE_TYPE } from '../enum/message'; 2 | import { MessageRecord } from './entity'; 3 | 4 | // 发送给其他人消息 5 | export interface CHAT_MESSAGE { 6 | type: ENUM_MESSAGE_DIST_TYPE; // 私聊还是群聊 7 | sender_id: number; // 发送者ID 8 | receive_id: number; // 接收者ID 9 | messages: MessageRecord[]; 10 | } 11 | 12 | export interface RESPONSE_MESSAGE { 13 | status: ENUM_MESSAGE_RESPONSE_STATUS; 14 | data: any; 15 | } 16 | 17 | // SOCKET统一返回内容 18 | export interface SOCKET_RESPONSE { 19 | message_type: ENUM_SOCKET_MESSAGE_TYPE; 20 | message: CHAT_MESSAGE | RESPONSE_MESSAGE; 21 | } 22 | -------------------------------------------------------------------------------- /server/src/service/relation.ts: -------------------------------------------------------------------------------- 1 | import db from '../lib/db'; 2 | 3 | class Relation { 4 | private table = 'relation'; 5 | 6 | /** 7 | * 获取用户加入的某个群 8 | * 9 | * @param {number} uid 用户ID 10 | * @param {number} friendId 好友用户ID 11 | * @returns 群信息 12 | */ 13 | async getUserFriend(uid: number, friendId: number) { 14 | try { 15 | const data = await db.table(this.table) 16 | .where({ 17 | uid, 18 | friend_id: friendId, 19 | status: 1, 20 | }) 21 | .select(); 22 | return [null, data]; 23 | } catch (err) { 24 | return [err, null]; 25 | } 26 | } 27 | } 28 | 29 | export default new Relation(); 30 | -------------------------------------------------------------------------------- /server/src/socket/auth.ts: -------------------------------------------------------------------------------- 1 | import socketIO from 'socket.io'; 2 | import jwt from 'jsonwebtoken'; 3 | import config from '../config'; 4 | import User from '../service/user'; 5 | 6 | export default (socket: socketIO.Socket, next: any) => { 7 | // const socketId = socket.id; 8 | const { token } = socket.handshake.query; 9 | if (!token) { 10 | return next(new Error('用户未登录')); 11 | } 12 | const user: any = jwt.verify(token, config.jwt.secret); 13 | const { uid } = user; 14 | if (!uid) { 15 | return next(new Error('用户不存在')); 16 | } 17 | const info = User.getUserInfoById(uid); 18 | if (!info) { 19 | return next(new Error('用户不存在')); 20 | } 21 | next(); 22 | }; 23 | -------------------------------------------------------------------------------- /client/interface/response.ts: -------------------------------------------------------------------------------- 1 | // 与后端一致 2 | import { ENUM_MESSAGE_RESPONSE_STATUS, ENUM_MESSAGE_DIST_TYPE, ENUM_SOCKET_MESSAGE_TYPE } from '../enum/message'; 3 | import { MessageRecord } from './entity'; 4 | 5 | // 发送给其他人消息 6 | export interface CHAT_MESSAGE { 7 | type: ENUM_MESSAGE_DIST_TYPE; // 私聊还是群聊 8 | sender_id: number; // 发送者ID 9 | receive_id: number; // 接收者ID 10 | messages: MessageRecord[]; 11 | } 12 | 13 | export interface RESPONSE_MESSAGE { 14 | status: ENUM_MESSAGE_RESPONSE_STATUS; 15 | data: any; 16 | } 17 | 18 | // SOCKET统一返回内容 19 | export interface SOCKET_RESPONSE { 20 | message_type: ENUM_SOCKET_MESSAGE_TYPE; 21 | message: CHAT_MESSAGE | RESPONSE_MESSAGE; 22 | } 23 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@hyoga/uni-socket.io": "^1.0.1", 14 | "dayjs": "^1.8.28", 15 | "md5": "^2.3.0", 16 | "tslib": "^2.0.3", 17 | "uni-simple-router": "^1.5.4", 18 | "uview-ui": "^1.2.8", 19 | "vue": "^2.6.11", 20 | "vuex": "^3.4.0" 21 | }, 22 | "devDependencies": { 23 | "@types/md5": "^2.2.0", 24 | "@types/node": "^14.14.16", 25 | "@types/uni-app": "^1.4.2", 26 | "@types/webpack-env": "^1.15.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import uView from "uview-ui"; 3 | import { RouterMount } from 'uni-simple-router'; 4 | import request from "./helper/request"; 5 | import App from "./App"; 6 | import store from "./store"; 7 | import router from './router'; 8 | 9 | Vue.use(uView); 10 | 11 | Vue.prototype.$request = request; 12 | Vue.prototype.$store = store; 13 | 14 | Vue.config.productionTip = false; 15 | 16 | App.mpType = "app"; 17 | 18 | const app = new Vue({ 19 | store, 20 | router, 21 | ...App, 22 | }); 23 | 24 | //v1.3.5起 H5端 你应该去除原有的app.$mount();使用路由自带的渲染方式 25 | // #ifdef H5 26 | RouterMount(app, '#app'); 27 | // #endif 28 | 29 | // #ifndef H5 30 | app.$mount(); //为了兼容小程序及app端必须这样写才有效果 31 | // #endif 32 | -------------------------------------------------------------------------------- /client/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | // https://webpack.js.org/guides/dependency-management/#requirecontext 7 | const modulesFiles = require.context('./modules', false, /\.ts$/) 8 | 9 | // you do not need `import app from './modules/app'` 10 | // it will auto require all vuex module from modules file 11 | const modules = modulesFiles.keys().reduce((modules: { [x: string]: any; }, modulePath: string) => { 12 | // set './app.js' => 'app' 13 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 14 | const value = modulesFiles(modulePath) 15 | modules[moduleName] = value.default 16 | return modules 17 | }, {}); 18 | 19 | const store = new Vuex.Store({ 20 | modules, 21 | }); 22 | 23 | export default store; 24 | -------------------------------------------------------------------------------- /server/src/helper/jwt.blacklist.ts: -------------------------------------------------------------------------------- 1 | interface Item { 2 | exp: number, token: string 3 | } 4 | 5 | /** 6 | * 用于吊销jwt凭证 7 | * TODO: 使用redis等持久存储黑名单 8 | */ 9 | export default class BlackList { 10 | private static list: Item[] = []; 11 | 12 | // 过滤掉已经过期的token 13 | private static filterOverdueToken() { 14 | const time = +new Date(); 15 | this.list = this.list.filter((item: Item) => item.exp > time); 16 | } 17 | 18 | public static add(token: string, exp: number) { 19 | this.filterOverdueToken(); 20 | 21 | if (!this.isTokenInList(token)) { 22 | this.list.push({ 23 | exp: exp * 1000, 24 | token, 25 | }); 26 | } 27 | } 28 | 29 | public static isTokenInList(token: string) { 30 | return this.list.find((item: Item) => item.token === token); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references":[], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "importHelpers": true, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "./*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.vue" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "unpackage" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /server/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import dev from './dev'; 2 | import prod from './prod'; 3 | import { EnvType, Config } from '../interface/config'; 4 | 5 | const env: EnvType = (process.env.NODE_ENV as EnvType) || 'local'; 6 | 7 | const configMap = { 8 | local: {}, 9 | development: dev, 10 | production: prod, 11 | }; 12 | 13 | const defaults = { 14 | mysql: { 15 | host: '127.0.0.1', 16 | port: 3307, 17 | user: 'root', 18 | password: '123456', 19 | database: 'speedy-im', 20 | }, 21 | passwordSecret: 'speedy-im', 22 | jwt: { 23 | secret: 'speedy-im', 24 | routeWhiteList: [ 25 | '/', 26 | '/favicon.ico', 27 | '/api/user/signIn', 28 | '/api/user/signUp', 29 | ], 30 | }, 31 | }; 32 | 33 | const config: Config = { 34 | ...defaults, 35 | ...(configMap[env] || {}), 36 | }; 37 | 38 | export default config; 39 | -------------------------------------------------------------------------------- /client/router/routes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/pages/chat/list', 4 | aliasPath:'/', //对于h5端你必须在首页加上aliasPath并设置为/ 5 | name: 'chatList', 6 | meta: { 7 | title: '消息', 8 | }, 9 | auth: true, 10 | }, { 11 | path: '/pages/chat/chat', 12 | name: 'chatMain', 13 | meta: { 14 | title: '聊天', 15 | }, 16 | auth: true, 17 | }, { 18 | path: '/pages/user/signIn', 19 | name: 'userSignIn', 20 | meta: { 21 | title: '用户登录', 22 | }, 23 | }, { 24 | path: '/pages/user/signUp', 25 | name: 'userSignUp', 26 | meta: { 27 | title: '用户注册', 28 | }, 29 | }, { 30 | path: '/pages/user/me', 31 | name: 'userMain', 32 | meta: { 33 | title: '个人中心', 34 | }, 35 | auth: true, 36 | }, { 37 | path: '/pages/addressBook/index', 38 | name: 'addressBookMain', 39 | meta: { 40 | title: '通讯录', 41 | }, 42 | auth: true, 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /server/src/interface/entity.ts: -------------------------------------------------------------------------------- 1 | import { ENUM_MESSAGE_CONTENT_TYPE, ENUM_MESSAGE_DIST_TYPE } from '../enum/message'; 2 | 3 | export interface Message { 4 | id?: number; 5 | hash: string; 6 | user_id: number; 7 | dist_id: number; 8 | dist_type: ENUM_MESSAGE_DIST_TYPE; 9 | content_type: ENUM_MESSAGE_CONTENT_TYPE; 10 | is_received?: number; 11 | is_sent?: number; 12 | content: string; 13 | create_time: number; 14 | status: number; 15 | } 16 | 17 | export interface User { 18 | id: number; 19 | nickname: string; 20 | mobile: number; 21 | password: string; 22 | avatar: string; 23 | sex: number; 24 | token: string; 25 | client_id: string; 26 | client_type: 'android' | 'ios'; 27 | create_time: number; 28 | status: number; 29 | } 30 | 31 | export interface FriendInfo extends User { 32 | uid: number; 33 | friend_id: number; 34 | remark: string; 35 | } 36 | 37 | // 扩展的接口 38 | export interface MessageRecord extends Message { 39 | is_owner: 0 | 1; 40 | } 41 | -------------------------------------------------------------------------------- /client/interface/entity.ts: -------------------------------------------------------------------------------- 1 | // 与后端一致 2 | import { ENUM_MESSAGE_CONTENT_TYPE, ENUM_MESSAGE_DIST_TYPE } from '../enum/message'; 3 | 4 | export interface Message { 5 | id?: number; 6 | hash: string; 7 | user_id: number; 8 | dist_id: number; 9 | dist_type: ENUM_MESSAGE_DIST_TYPE; 10 | content_type: ENUM_MESSAGE_CONTENT_TYPE; 11 | is_received?: number; 12 | is_sent?: number; 13 | content: string; 14 | create_time: number; 15 | status?: number; 16 | } 17 | 18 | export interface User { 19 | id: number; 20 | nickname: string; 21 | mobile: number; 22 | password: string; 23 | avatar: string; 24 | sex: number; 25 | token: string; 26 | client_id: string; 27 | client_type: 'android' | 'ios'; 28 | create_time: number; 29 | status: number; 30 | } 31 | 32 | 33 | // 扩展的接口 34 | export interface MessageRecord extends Message { 35 | is_owner: 0 | 1; 36 | } 37 | 38 | export interface FriendInfo extends User { 39 | uid: number; 40 | friend_id: number; 41 | remark: string; 42 | } 43 | -------------------------------------------------------------------------------- /client/helper/request.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | 3 | declare let uni: any; 4 | 5 | export default async function request(params: any) { 6 | const { baseUrl } = config; 7 | const url = params.url; 8 | delete params.url; 9 | 10 | const token = uni.getStorageSync('token'); 11 | const defaultHeader = { 12 | 'Content-Type': 'application/x-www-form-urlencoded', 13 | 'x-access-token': token, 14 | }; 15 | params.data = params.data || {}; 16 | const options = { 17 | url: /^http/.test(url) ? url : `${baseUrl}${url}`, 18 | header: defaultHeader, 19 | ...params, 20 | }; 21 | 22 | const { success, fail, complete } = options; 23 | delete options.success; 24 | delete options.fail; 25 | delete options.complete; 26 | 27 | const [error, res ] = await uni.request(options); 28 | 29 | if (res && res.statusCode === 200 ){ 30 | success && success(res.data); 31 | complete && complete(res.data); 32 | return [null, res.data]; 33 | } else { 34 | fail && fail(error); 35 | complete && complete(error); 36 | return [error || new Error('网络错误'), null]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/src/helper/util.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import crypto from 'crypto'; 3 | import config from '../config'; 4 | 5 | export default class Util { 6 | public static getToken = (req: Request): string => req.headers['x-access-token'] || req.body.token || req.query.token; 7 | 8 | public static success(data: any, errno = 200, errmsg = '') { 9 | return { 10 | data, 11 | errno, 12 | errmsg, 13 | }; 14 | } 15 | 16 | public static fail(errmsg: string, errno = 500) { 17 | return { 18 | data: null, 19 | errno, 20 | errmsg, 21 | }; 22 | } 23 | 24 | public static encodePassword(password: string): string { 25 | return crypto.createHmac('sha1', config.passwordSecret).update(password).digest('hex'); 26 | } 27 | 28 | public static isPhoneNumber(number: number): boolean { 29 | return /^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57]|19[0-9])[0-9]{8}$/.test(`${number}`); 30 | } 31 | 32 | public static encryptPhoneNumber(number: number): string { 33 | return (`${number}`).replace(/^(\d{3})(\d{4})(\d{4})/, '$1****$2'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'uni-simple-router'; 3 | import routes from './routes'; 4 | import store from '../store'; 5 | 6 | declare let uni: any; 7 | 8 | Vue.use(Router); 9 | 10 | //初始化 11 | const router = new Router({ 12 | encodeURI: false, 13 | routes: [...routes], 14 | }); 15 | 16 | //全局路由前置守卫 17 | router.beforeEach(async (to: any, from: any, next: any) => { 18 | if (!to.auth) { 19 | return next(); 20 | } 21 | const { user_info } = store.state.user; 22 | if (user_info && user_info.id) { 23 | // 已登录则跳转 24 | return next(); 25 | } else { 26 | // 未登录则尝试自动登录 27 | const token = uni.getStorageSync('token'); 28 | if (!token) { 29 | return next({ name: 'userSignIn', NAVTYPE: 'push' }); 30 | } 31 | const res = await store.dispatch('user/autoLogin'); 32 | if (res && res.data && res.data.id) { 33 | // 自动登录后,正常跳转 34 | return next(); 35 | } else { 36 | return next({ name: 'userSignIn', NAVTYPE: 'push' }); 37 | } 38 | } 39 | }); 40 | 41 | // 全局路由后置守卫 42 | // router.afterEach((to, from) => { 43 | // }); 44 | 45 | export default router; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 罗强 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/src/routes/message.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import debug from 'debug'; 3 | import Util from '../helper/util'; 4 | 5 | import Message from '../service/message'; 6 | 7 | const log = debug('speedy-im message'); 8 | 9 | const router = express.Router(); 10 | 11 | /** 12 | * 更新消息状态 13 | * 14 | * @method GET 15 | * @param {token} string 16 | * @param {number[]} ids 17 | * @param {string} is_received 18 | */ 19 | router.put('/status', async (req, res) => { 20 | const { ids = [], columns = {} } = req.body; 21 | const allowColumns = ['is_received', 'is_read']; 22 | const data: Record = {}; 23 | allowColumns.forEach((key) => { 24 | const item = columns[key]; 25 | // eslint-disable-next-line no-restricted-globals 26 | if (item !== undefined && !isNaN(item) && (+item === 0 || +item === 1)) { 27 | data[key] = item; 28 | } 29 | }); 30 | if (!Object.keys(data).length) { 31 | return res.json(Util.fail('参数不合法', 0)); 32 | } 33 | const [err] = await Message.updateMultipleMessage(ids, data); 34 | if (err) { 35 | log(err); 36 | return res.json(Util.fail('数据库查询失败', 500)); 37 | } 38 | return res.json(Util.success(null)); 39 | }); 40 | 41 | export default router; 42 | -------------------------------------------------------------------------------- /client/public/template.h5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: [ 5 | 'airbnb-base', 6 | 'plugin:@typescript-eslint/recommended', 7 | ], 8 | plugins: [ 9 | 'import', 10 | '@typescript-eslint', 11 | ], 12 | env: { 13 | es6: true, 14 | node: true, 15 | }, 16 | globals: { 17 | Atomics: 'readonly', 18 | SharedArrayBuffer: 'readonly', 19 | }, 20 | parserOptions: { 21 | ecmaVersion: 2018, 22 | sourceType: 'module', 23 | }, 24 | settings: { 25 | 'import/resolver': { 26 | alias: { 27 | extensions: ['.ts', '.js'], 28 | }, 29 | }, 30 | }, 31 | rules: { 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/explicit-module-boundary-types': 'off', 34 | 'import/extensions': [ 35 | 'error', 36 | 'ignorePackages', 37 | { 38 | js: 'never', 39 | mjs: 'never', 40 | ts: 'never', 41 | }, 42 | ], 43 | 'class-methods-use-this': 'off', 44 | 'no-console': ['error', { allow: ['warn'] }], 45 | 'max-len': ['error', { code: 150 }], 46 | 'no-mixed-operators': 'off', 47 | camelcase: 'off', 48 | 'consistent-return': 'off', 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /client/helper/util.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | export default class Util { 4 | 5 | static isPhoneNumber(number: number) { 6 | return /^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57]|19[0-9])[0-9]{8}$/.test('' + number); 7 | } 8 | 9 | static encryptPhoneNumber (number: number) { 10 | return (number + '').replace(/^(\d{3})(\d{4})(\d{4})/, '$1****$2'); 11 | } 12 | 13 | static formatTime (dateNumber?: number | Date | string): string { 14 | if (!dateNumber) { 15 | return ''; 16 | } 17 | const now = Date.now(); 18 | const timer = dateNumber ? +dayjs(dateNumber) : now; 19 | const date = new Date(timer); 20 | const interval = Math.floor((now - timer) / 1000);// 秒 21 | const current = new Date(); 22 | if (interval < 60) { 23 | return '刚刚'; 24 | } else if (interval < 3600) { 25 | return `${Math.floor(interval / 60)}分钟前`; 26 | } else if (interval < 86400) { 27 | return `${Math.floor(interval / 3600)}小时前`; 28 | } else if (interval < 3 * 86400) { 29 | return `${Math.floor(interval / 86400)}天前`; 30 | } else if (current.getFullYear() === date.getFullYear()) { 31 | return dayjs(date).format('MM-DD'); 32 | } else { 33 | return dayjs(date).format('YYYY-MM-DD'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/platform/app-plus/subNVue/menu.nvue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 31 | 32 | 73 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "im", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "dist/app.js", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development nodemon src/app.ts", 8 | "prod": "cross-env NODE_ENV=production node dist/app.js", 9 | "build": "rimraf dist && tsc -p .", 10 | "deploy": "npm run build && pm2 start ecosystem.config.js --env production", 11 | "lint": "eslint src --ext .ts", 12 | "lint-fix": "eslint src --ext .ts --fix" 13 | }, 14 | "dependencies": { 15 | "@hyoga/mysql": "^1.0.12", 16 | "cookie-parser": "~1.4.4", 17 | "cors": "^2.8.5", 18 | "debug": "^4.1.1", 19 | "express": "~4.16.1", 20 | "express-jwt": "^6.0.0", 21 | "http-errors": "~1.6.3", 22 | "jsonwebtoken": "^8.5.1", 23 | "morgan": "~1.9.1", 24 | "pinyin": "^2.9.1", 25 | "socket.io": "^2.3.0" 26 | }, 27 | "devDependencies": { 28 | "@types/cookie-parser": "^1.4.2", 29 | "@types/cors": "^2.8.6", 30 | "@types/debug": "^4.1.5", 31 | "@types/express": "^4.17.6", 32 | "@types/express-jwt": "^0.0.42", 33 | "@types/http-errors": "^1.6.3", 34 | "@types/jsonwebtoken": "^8.5.0", 35 | "@types/morgan": "^1.9.0", 36 | "@types/node": "^13.7.6", 37 | "@types/pinyin": "^2.8.1", 38 | "@types/socket.io": "^2.1.8", 39 | "@typescript-eslint/eslint-plugin": "^3.0.2", 40 | "@typescript-eslint/parser": "^3.0.2", 41 | "cross-env": "^7.0.2", 42 | "eslint": "^7.1.0", 43 | "eslint-config-airbnb-base": "^14.1.0", 44 | "eslint-import-resolver-alias": "^1.1.2", 45 | "eslint-plugin-import": "^2.20.2", 46 | "nodemon": "^2.0.4", 47 | "rimraf": "^3.0.2", 48 | "ts-node": "^8.10.2", 49 | "typescript": "^3.9.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Speedy IM即时通讯 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 21 |
22 | 25 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /client/components/popupMenu.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 48 | 49 | -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream speedy_app { 2 | least_conn; 3 | server 127.0.0.1:8360 weight=10 max_fails=3 fail_timeout=30s; 4 | } 5 | 6 | server { 7 | listen 80; 8 | server_name app.speedy-im.com; 9 | root /home/luoqiang/web/speedy-im/server/dist; 10 | 11 | access_log /home/luoqiang/web/speedy-im/docker/nginx/logs/access.log; 12 | error_log /home/luoqiang/web/speedy-im/docker/nginx/logs/error.log; 13 | 14 | # ssl_certificate /home/luoqiang/web/speedy-im/docker/nginx/ssl/domain.cer; 15 | # ssl_certificate_key /home/luoqiang/web/speedy-im/docker/nginx/ssl/domain.key; 16 | # ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 17 | # ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-CHACHA20-POLY1305-D:ECDHE-RSA-CHACHA20-POLY1305-D:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA384; 18 | # ssl_prefer_server_ciphers on; 19 | # ssl_session_cache shared:SSL:10m; 20 | # ssl_session_timeout 10m; 21 | 22 | location ^~ /.well-known/acme-challenge/ { 23 | alias /home/luoqiang/web/speedy-im/server/dist/.well-known/acme-challenge/; 24 | try_files $uri = 404; 25 | } 26 | 27 | location / { 28 | proxy_pass http://speedy_app; 29 | proxy_set_header Upgrade $http_upgrade; 30 | proxy_set_header Connection 'upgrade'; 31 | proxy_set_header Host $host; 32 | proxy_set_header X-Real-IP $remote_addr; 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_cache_bypass $http_upgrade; 35 | } 36 | } 37 | 38 | # server { 39 | # listen 80; 40 | # server_name www.jump.ink jump.ink; 41 | # rewrite ^(.*) https://www.jump.ink$1 permanent; 42 | # } -------------------------------------------------------------------------------- /server/src/service/message.ts: -------------------------------------------------------------------------------- 1 | import db from '../lib/db'; 2 | import { Message as MessageData } from '../interface/entity'; 3 | 4 | class Message { 5 | private table = 'message'; 6 | 7 | /** 8 | * 创建一条信息 9 | * 10 | * @param {MessageData} message 消息 11 | * @returns 12 | */ 13 | async createMessage(message: MessageData) { 14 | try { 15 | const data = await db.table(this.table) 16 | .add(message); 17 | return [null, data]; 18 | } catch (err) { 19 | return [err, null]; 20 | } 21 | } 22 | 23 | /** 24 | * 获取用户未读消息列表 25 | * 26 | * @param {number} uid 用户ID 27 | * @returns 未读消息列表 28 | */ 29 | async getUnreadMessage(uid: number) { 30 | try { 31 | const data = await db.table(this.table) 32 | .where({ 33 | dist_id: uid, 34 | dist_type: 1, 35 | }) 36 | .where({ 'is_received|is_sent': 0 }) 37 | .select(); 38 | return [null, data]; 39 | } catch (err) { 40 | return [err, null]; 41 | } 42 | } 43 | 44 | /** 45 | * 更新消息信息 46 | * 47 | * @param {number} id 消息ID 48 | * @param {Record} columns 数据列 49 | */ 50 | async updateMessage(id: number, columns: Record) { 51 | try { 52 | const data = await db.table(this.table) 53 | .where({ 54 | id, 55 | }) 56 | .update(columns); 57 | return [null, data]; 58 | } catch (err) { 59 | return [err, null]; 60 | } 61 | } 62 | 63 | /** 64 | * 一次性更新多条消息信息的某个字段 65 | * 66 | * @param {number[]} ids 主键列表 67 | * @param {Record[]} columns 数据列 68 | */ 69 | async updateMultipleMessage(ids: number[], columns: Record) { 70 | try { 71 | const data = await db.table(this.table) 72 | .where({ 73 | id: ['in', ids || []], 74 | }) 75 | .update(columns); 76 | return [null, data]; 77 | } catch (err) { 78 | return [err, null]; 79 | } 80 | } 81 | } 82 | 83 | export default new Message(); 84 | -------------------------------------------------------------------------------- /client/uni.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里是uni-app内置的常用样式变量 3 | * 4 | * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 5 | * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App 6 | * 7 | */ 8 | 9 | /** 10 | * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 11 | * 12 | * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 13 | */ 14 | 15 | /* 颜色变量 */ 16 | 17 | /* 行为相关颜色 */ 18 | $uni-color-primary: #007aff; 19 | $uni-color-success: #4cd964; 20 | $uni-color-warning: #f0ad4e; 21 | $uni-color-error: #dd524d; 22 | 23 | /* 文字基本颜色 */ 24 | $uni-text-color:#333;//基本色 25 | $uni-text-color-inverse:#fff;//反色 26 | $uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 27 | $uni-text-color-placeholder: #808080; 28 | $uni-text-color-disable:#c0c0c0; 29 | 30 | /* 背景颜色 */ 31 | $uni-bg-color:#ffffff; 32 | $uni-bg-color-grey:#f8f8f8; 33 | $uni-bg-color-hover:#f1f1f1;//点击状态颜色 34 | $uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 35 | 36 | /* 边框颜色 */ 37 | $uni-border-color:#c8c7cc; 38 | 39 | /* 尺寸变量 */ 40 | 41 | /* 文字尺寸 */ 42 | $uni-font-size-sm:24rpx; 43 | $uni-font-size-base:28rpx; 44 | $uni-font-size-lg:32rpx; 45 | 46 | /* 图片尺寸 */ 47 | $uni-img-size-sm:40rpx; 48 | $uni-img-size-base:52rpx; 49 | $uni-img-size-lg:80rpx; 50 | 51 | /* Border Radius */ 52 | $uni-border-radius-sm: 4rpx; 53 | $uni-border-radius-base: 6rpx; 54 | $uni-border-radius-lg: 12rpx; 55 | $uni-border-radius-circle: 50%; 56 | 57 | /* 水平间距 */ 58 | $uni-spacing-row-sm: 10px; 59 | $uni-spacing-row-base: 20rpx; 60 | $uni-spacing-row-lg: 30rpx; 61 | 62 | /* 垂直间距 */ 63 | $uni-spacing-col-sm: 8rpx; 64 | $uni-spacing-col-base: 16rpx; 65 | $uni-spacing-col-lg: 24rpx; 66 | 67 | /* 透明度 */ 68 | $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 69 | 70 | /* 文章场景相关 */ 71 | $uni-color-title: #2C405A; // 文章标题颜色 72 | $uni-font-size-title:40rpx; 73 | $uni-color-subtitle: #555555; // 二级标题颜色 74 | $uni-font-size-subtitle:36rpx; 75 | $uni-color-paragraph: #3F536E; // 文章段落颜色 76 | $uni-font-size-paragraph:30rpx; 77 | 78 | @import 'uview-ui/theme.scss'; -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # speedy-im 2 | 3 | [![star](https://img.shields.io/github/stars/AspenLuoQiang/speedy-im?style=social)](https://github.com/AspenLuoQiang/speedy-im) [![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-207879913-yellowgreen.svg)](https://jq.qq.com/?_wv=1027&k=9f25XGCW) 4 | 5 | [介绍](#介绍) | [DEMO](#DEMO) | [开发](#开发) | [开发计划](#开发计划) | [系统架构](#系统架构) | [联系作者](#联系作者) 6 | 7 | 8 | ## 介绍 9 | 10 | 基于`uni-app` + `@hyoga/uni-socket.io` + `express` + `mysql`开发高性能的即时通讯系统。已支持点对点通讯,计划支持群组通讯、上下线等事件消息等众多功能。 11 | 12 | ## DEMO 13 | 14 | [IM.apk](https://im.wangcai.me/__UNI__0CE1D62_1030114757.apk) ,已有基础UI以及登陆、点到点聊天等功能。 15 | 16 | ## 开发 17 | 18 | 客户端测试账号密码: 19 | 账号:13600000003 20 | 密码:admin 21 | 22 | ```shell 23 | # 克隆项目 24 | $ git clone git@github.com:AspenLuoQiang/speedy-im.git 25 | $ cd speedy-im 26 | 27 | # 启动数据库 28 | $ cd docker 29 | $ docker-compose up -d mysql 30 | # 导入数据库,见下方导入数据库 31 | 32 | # 启动服务端 33 | $ cd server 34 | $ yarn && yarn dev 35 | 36 | # 以上为已开发模式启动服务端,不想改动服务端代码,只是单纯想开启服务器可以如下操作 37 | $ cd docker 38 | $ docker-compose up 39 | 40 | # 安装客户端依赖,安装完成后使用HBuilder X运行到浏览器即可,请确保此时服务端已正确运行,否则会导致接口无法调用 41 | $ cd client 42 | $ yarn 43 | ``` 44 | 45 | ## 导入数据库 46 | 47 | * 本项目使用docker部署开发,待docker数据库启动后连接数据库,默认数据库配置见下方[MySQL默认配置](#MySQL默认配置)。 48 | * 导入初始数据库,位置为`docker/mysql/speedy-im.sql`。 49 | 50 | ### MySQL默认配置 51 | 52 | 地址:127.0.0.1 53 | 端口:3307 54 | 用户名:root 55 | 密码:123456 56 | 57 | ## 开发计划 58 | 59 | * [x] [UI开发](#UI开发) 60 | * [x] [后端框架](#后端框架) 61 | * [x] [私聊](#私聊) 62 | * [ ] [群聊](#群聊) 63 | 64 | ## 系统架构 65 | 66 | ### 后端框架 67 | 68 | 采用`express` + `socket.io` + `mysql`开发,使用`docker`部署。 69 | 70 | #### 错误码 71 | 72 | 返回结果采用以下结构,错误码参考HTTP状态码设计,更多状态码逐步添加中。 73 | ``` 74 | { 75 | errno: 200, 76 | errmsg: '', 77 | data: {}, 78 | } 79 | ``` 80 | 错误码|含义|备注 81 | ---|:--:|---: 82 | 0|业务操作失败|业务上操作失败导致的错误,但未定义具体code值 83 | 200|正常|HTTP 状态码 84 | 401|未登陆|HTTP 状态码 85 | 500|内部错误|HTTP 状态码 86 | 87 | ### 客户端 88 | 89 | 客户端使用`uni-app`开发,可以同时开发安卓端与IOS端,简单快捷。 90 | 91 | #### UI开发 92 | 93 | ![UI图](https://i.loli.net/2020/05/28/29YadEVhGSqojZU.png) 94 | 95 | * [x] [好友列表](#好友列表) 96 | * [x] [对话页](#对话页) 97 | * [x] [通讯录](#通讯录) 98 | * [x] [登录](#登录) 99 | * [x] [注册](#注册) 100 | * [ ] [我的信息](#我的信息) 101 | * [ ] [好友信息](#好友信息) 102 | 103 | ## 联系作者 104 | 105 | - [qq群](https://jq.qq.com/?_wv=1027&k=9f25XGCW) 106 | - 公众号,欢迎关注,不定时更新 107 | 108 | ![前端方程式](https://i.loli.net/2020/05/28/CNcjhm17d9zfvkQ.jpg) 109 | 110 | -------------------------------------------------------------------------------- /server/src/app.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | Application, Request, Response, NextFunction, 3 | } from 'express'; 4 | import http from 'http'; 5 | import cookieParser from 'cookie-parser'; 6 | import logger from 'morgan'; 7 | import cors from 'cors'; 8 | import socketIO from 'socket.io'; 9 | import debug from 'debug'; 10 | import expressJwt from 'express-jwt'; 11 | import config from './config'; 12 | import indexRouter from './routes'; 13 | import userRouter from './routes/user'; 14 | import Util from './helper/util'; 15 | import SocketAuth from './socket/auth'; 16 | import Chat from './socket/chat'; 17 | import BlackList from './helper/jwt.blacklist'; 18 | 19 | const log = debug('speedy-im'); 20 | const isDev = process.env.NODE_ENV === 'development'; 21 | const { jwt } = config; 22 | 23 | const app: Application = express(); 24 | const server: http.Server = new http.Server(app); 25 | const io: socketIO.Server = socketIO(server, { 26 | pingInterval: 5000, 27 | pingTimeout: 5000, 28 | }); 29 | io.use(SocketAuth); 30 | new Chat(io).setup(); 31 | 32 | app.use(cors()); 33 | app.use(logger(isDev ? 'dev' : 'combined')); 34 | app.use(express.json()); 35 | app.use(express.urlencoded({ extended: false })); 36 | app.use(cookieParser()); 37 | 38 | app.use( 39 | expressJwt({ 40 | secret: jwt.secret, 41 | algorithms: ['HS256'], 42 | getToken: Util.getToken, 43 | isRevoked(req, payload, done) { 44 | const token = Util.getToken(req); 45 | const isRevoked = BlackList.isTokenInList(token); 46 | if (isRevoked) { 47 | return done('invalid token', true); 48 | } 49 | return done(null, false); 50 | }, 51 | }) 52 | .unless({ 53 | path: jwt.routeWhiteList, 54 | }), 55 | ); 56 | 57 | app.use('/', indexRouter); 58 | app.use('/api/user', userRouter); 59 | 60 | // catch 404 and forward to error handler 61 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 62 | app.use((req, res, next) => { 63 | res.json(Util.fail('not found', 404)); 64 | }); 65 | 66 | // 500 error handler 67 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 68 | app.use((err: { message: string; status: number; name: string; }, req: Request, res: Response, _: NextFunction) => { 69 | if (err.name === 'UnauthorizedError') { 70 | return res.json(Util.fail('invalid token', 401)); 71 | } 72 | 73 | return res.json(Util.success({ 74 | message: err.message, 75 | error: isDev ? err : {}, 76 | }, err.status || 500, '内部服务器错误')); 77 | }); 78 | 79 | server.listen(8360, () => { 80 | log('IM 服务在 8360端口启动'); 81 | }); 82 | -------------------------------------------------------------------------------- /client/helper/storage.ts: -------------------------------------------------------------------------------- 1 | import { MessageRecord } from "../interface/entity"; 2 | import store from "../store"; 3 | 4 | declare var uni: any; 5 | 6 | type LocalMessages = Record; 7 | 8 | class Message { 9 | private key = 'local_message_history'; 10 | 11 | // 从本地存储中恢复数据 12 | public async recover() { 13 | const messages = await this.getAll(); 14 | Object.keys(messages).length && store.dispatch('message/recoverMessages', { messages }); 15 | } 16 | 17 | // 保存消息 18 | public async save(messages: MessageRecord[]) { 19 | const data = await this.getAll(); 20 | 21 | messages.forEach(message => { 22 | const { dist_id, user_id, is_owner } = message; 23 | // 自己发的消息,friend_id是dist_id,否则则是user_id 24 | const friend_id = is_owner ? dist_id : user_id; 25 | let localItem = data[friend_id]; 26 | if (!localItem) { 27 | localItem = [message]; 28 | } else { 29 | !localItem.find(l => l.hash === message.hash) && localItem.push(message); 30 | } 31 | // 只存储100条对话 32 | data[friend_id] = localItem.slice(-100); 33 | }); 34 | this.saveAll(data); 35 | } 36 | 37 | // 更新消息id 38 | public async update({ messages }: { messages: { id: number, hash: string, friend_id: number } }) { 39 | const list = await this.getAll(); 40 | const { id, hash, friend_id } = messages; 41 | const msg = list[friend_id]; 42 | if (!msg) { 43 | return; 44 | } 45 | list[friend_id] = msg.map(m => { 46 | if (m.hash === hash) { 47 | m.id = id; 48 | } 49 | return m; 50 | }); 51 | this.saveAll(list); 52 | } 53 | 54 | private async saveAll(data: LocalMessages) { 55 | return uni.setStorage({ key: this.key, data }); 56 | } 57 | 58 | private async getAll() { 59 | const [,data = {}] = await uni.getStorage({ key: this.key }); 60 | return data.data || {}; 61 | } 62 | 63 | } 64 | 65 | class Contacts { 66 | private key = 'local_recent_contacts'; 67 | // 从本地存储中恢复数据 68 | public async recover() { 69 | const list = await this.getAll(); 70 | list.length && store.dispatch('user/setFullRecentContacts', { list }); 71 | } 72 | 73 | // 保存消息 74 | public async save(id: number) { 75 | const data = await this.getAll(); 76 | 77 | this.saveAll(Array.from(new Set([id, ...data]))); 78 | } 79 | 80 | private async saveAll(data: LocalMessages) { 81 | return uni.setStorage({ key: this.key, data }); 82 | } 83 | 84 | private async getAll() { 85 | const [,data = {}] = await uni.getStorage({ key: this.key }); 86 | return data.data || []; 87 | } 88 | } 89 | 90 | export default { 91 | message: new Message(), 92 | contacts: new Contacts(), 93 | }; 94 | -------------------------------------------------------------------------------- /client/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "easycom": { 3 | "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" 4 | }, 5 | "pages": [ 6 | { 7 | "path": "pages/chat/list", 8 | "style": { 9 | "navigationBarTitleText": "消息", 10 | "navigationStyle": "custom", 11 | "app-plus": { 12 | "subNVues":[{ 13 | "id": "menu", 14 | "path": "platform/app-plus/subNVue/menu.nvue", 15 | "type": "popup", 16 | "style": { 17 | "position": "absolute", 18 | "mask": "rgba(0, 0, 0, .1)", 19 | "top": "0px", 20 | "left": "0px", 21 | "right": "0px", 22 | "bottom": "0px", 23 | "zindex": 2000, 24 | "background": "transparent" 25 | } 26 | }] 27 | } 28 | } 29 | }, { 30 | "path": "pages/chat/chat", 31 | "style": { 32 | "navigationBarTitleText": "聊天" 33 | } 34 | }, { 35 | "path": "pages/user/signIn", 36 | "style": { 37 | "navigationBarTitleText": "用户登录", 38 | "navigationStyle": "custom" 39 | } 40 | }, { 41 | "path" : "pages/user/signUp", 42 | "style" : { 43 | "navigationBarTitleText": "用户注册", 44 | "navigationStyle": "custom" 45 | } 46 | }, { 47 | "path": "pages/user/me", 48 | "style": { 49 | "navigationBarTitleText": "用户中心", 50 | "navigationStyle": "custom" 51 | } 52 | }, { 53 | "path": "pages/addressBook/index", 54 | "style": { 55 | "navigationBarTitleText": "通讯录", 56 | "navigationStyle": "custom", 57 | "app-plus": { 58 | "subNVues":[{ 59 | "id": "menu", 60 | "path": "platform/app-plus/subNVue/menu.nvue", 61 | "type": "popup", 62 | "style": { 63 | "position": "absolute", 64 | "mask": "rgba(0, 0, 0, .1)", 65 | "top": "0px", 66 | "left": "0px", 67 | "right": "0px", 68 | "bottom": "0px", 69 | "zindex": 2000, 70 | "background": "transparent" 71 | } 72 | }] 73 | } 74 | } 75 | } 76 | ], 77 | "globalStyle": { 78 | "navigationBarTextStyle": "black", 79 | "navigationBarTitleText": "IM", 80 | "navigationBarBackgroundColor": "#F7F7F7", 81 | "backgroundColor": "#f5f5f5" 82 | }, 83 | "tabBar": { 84 | "color": "#666666", 85 | "selectedColor": "#1441B8", 86 | "borderStyle": "black", 87 | "backgroundColor": "#F7F7F7", 88 | "list": [ 89 | { 90 | "pagePath": "pages/chat/list", 91 | "iconPath": "static/images/tab/chat.png", 92 | "selectedIconPath": "static/images/tab/chat-active.png", 93 | "text": "消息" 94 | }, { 95 | "pagePath": "pages/addressBook/index", 96 | "iconPath": "static/images/tab/address-book.png", 97 | "selectedIconPath": "static/images/tab/address-book-active.png", 98 | "text": "通讯录" 99 | }, { 100 | "pagePath": "pages/user/me", 101 | "iconPath": "static/images/tab/user.png", 102 | "selectedIconPath": "static/images/tab/user-active.png", 103 | "text": "用户中心" 104 | } 105 | ] 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/pages/user/me.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 63 | 64 | -------------------------------------------------------------------------------- /client/store/modules/message.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext } from 'vuex'; 2 | import request from '../../helper/request'; 3 | import { MessageRecord } from '../../interface/entity'; 4 | 5 | export interface State { 6 | list: Record; 7 | lastHashIdMap: Record; // 用于设置滚动到底部的hash id 8 | } 9 | 10 | const state: State = { 11 | list: {}, 12 | lastHashIdMap: {}, 13 | }; 14 | 15 | const mutations = { 16 | SET_USER_MESSAGES(state: State, { messages }: { messages: MessageRecord[] }) { 17 | const list = {...state.list}; 18 | const lastHashIdMap = {...state.lastHashIdMap}; 19 | messages.forEach(item => { 20 | const { dist_id, user_id, is_owner } = item; 21 | // 自己发的消息,friend_id是dist_id,否则则是user_id 22 | const friend_id = is_owner ? dist_id : user_id; 23 | const msg = list[friend_id]; 24 | if (!msg) { 25 | list[friend_id] = [item]; 26 | } else { 27 | !msg.find(l => l.hash === item.hash) && msg.push(item); 28 | } 29 | lastHashIdMap[friend_id] = item.hash; 30 | }); 31 | state.list = list; 32 | state.lastHashIdMap = lastHashIdMap; 33 | }, 34 | RECOVER_MESSAGE(state: State, { messages }: { messages: Record}) { 35 | // 接收消息或者发消息,先本地存储,所以本地消息永远先与当前消息,直接取用即可 36 | const list = {...state.list}; 37 | const lastHashIdMap = {...state.lastHashIdMap}; 38 | for (let friend_id in messages) { 39 | const localItem = messages[friend_id] || []; 40 | const item = list[friend_id] || []; 41 | const { length: localLength } = localItem; 42 | const { length } = item; 43 | lastHashIdMap[friend_id] = localItem[localLength - 1].hash; 44 | if (localLength > length) { 45 | list[friend_id] = localItem; 46 | } 47 | } 48 | state.list = list; 49 | state.lastHashIdMap = lastHashIdMap; 50 | }, 51 | UPDATE_MESSAGE_ID(state: State, { messages }: { messages: { id: number, hash: string, friend_id: number } }) { 52 | const list = {...state.list}; 53 | const { id, hash, friend_id } = messages; 54 | const msg = list[friend_id]; 55 | if (!msg) { 56 | return; 57 | } 58 | list[friend_id] = msg.map(m => { 59 | if (m.hash === hash) { 60 | m.id = id; 61 | } 62 | return m; 63 | }); 64 | state.list = list; 65 | }, 66 | }; 67 | 68 | const actions = { 69 | async getUnreadMessage({ commit }: ActionContext) { 70 | const [err, res] = await request({ 71 | url: '/user/unreadMessage', 72 | method: 'get', 73 | }); 74 | if (res && res.errno === 200) { 75 | commit('SET_USER_MESSAGES', { messages: res.data }); 76 | } 77 | }, 78 | setMessage({ commit }: ActionContext, payload: { messages: MessageRecord[] }) { 79 | const { messages } = payload; 80 | commit('SET_USER_MESSAGES', { messages }); 81 | }, 82 | recoverMessages({ commit }: ActionContext, payload: { messages: Record }) { 83 | const { messages } = payload; 84 | commit('RECOVER_MESSAGE', { messages }); 85 | }, 86 | updateMessage({ commit }: ActionContext, payload: { messages: { id: number, hash: string, friend_id: number } }) { 87 | const { messages } = payload; 88 | commit('UPDATE_MESSAGE_ID', { messages }); 89 | } 90 | }; 91 | 92 | export default { 93 | namespaced: true, 94 | state, 95 | mutations, 96 | actions 97 | } 98 | -------------------------------------------------------------------------------- /client/pages/user/signUp.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 56 | 57 | -------------------------------------------------------------------------------- /client/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext } from 'vuex'; 2 | import request from '../../helper/request'; 3 | import { User, FriendInfo } from '../../interface/entity'; 4 | import Chat from '../../socket/chat'; 5 | 6 | declare let uni: any; 7 | 8 | export interface State { 9 | user_info: User; 10 | friends_map: Record; 11 | friends: { 12 | key: string; // 用户名首字母 13 | list: number[] 14 | }[]; 15 | recent_contacts: number[], 16 | } 17 | 18 | const state: State = { 19 | user_info: {} as User, 20 | friends_map: {}, 21 | friends: [], 22 | recent_contacts: [], 23 | }; 24 | 25 | const mutations = { 26 | SET_USER_INFO(state: State, payload: { user_info: User }) { 27 | state.user_info = payload.user_info; 28 | }, 29 | SET_USER_FRIENDS(state: State, payload: { friends: { key: string; list: FriendInfo[] }[] }) { 30 | const { friends } = payload; 31 | const map = {}; 32 | const tmp = friends.map(item => { 33 | const { key, list } = item; 34 | return { 35 | key, 36 | list: list.map(l => { 37 | l.nickname = l.remark || l.nickname; 38 | map[l.friend_id] = l; 39 | return l.friend_id; 40 | }), 41 | } 42 | }); 43 | state.friends = tmp; 44 | state.friends_map = map; 45 | }, 46 | SET_RECENT_CONTACT(state: State, payload: { friend_id: number }) { 47 | state.recent_contacts = Array.from(new Set([payload.friend_id, ...state.recent_contacts])); 48 | }, 49 | SET_FULL_RECENT_CONTACT(state: State, payload: { list: number[] }) { 50 | state.recent_contacts = payload.list; 51 | }, 52 | }; 53 | 54 | const actions = { 55 | async getFriendsList({ commit }: ActionContext) { 56 | const [err, res] = await request({ 57 | url: '/user/friends', 58 | method: 'get', 59 | }); 60 | if (res && res.errno === 200) { 61 | commit('SET_USER_FRIENDS', { friends: res.data }); 62 | } 63 | }, 64 | async login({ commit, dispatch }: ActionContext, { mobile, password }: { mobile: number, password: string }) { 65 | const [err, res] = await request({ 66 | url: '/user/signIn', 67 | method: 'PUT', 68 | data: { 69 | mobile, 70 | password, 71 | } 72 | }); 73 | if (res && res.errno === 200) { 74 | await dispatch('getFriendsList'); 75 | commit('SET_USER_INFO', { user_info: res.data }); 76 | uni.setStorageSync('token', res.data.token); 77 | // 登陆成功后,建立ws连接 78 | Chat.setup(); 79 | return { 80 | errno: 200, 81 | errmsg: '', 82 | data: res.data, 83 | }; 84 | } 85 | return { 86 | errno: res && res.errno || 1, 87 | errmsg: res && res.errmsg || '网络错误', 88 | data: null, 89 | }; 90 | }, 91 | async autoLogin({ commit, dispatch }: ActionContext) { 92 | const [err, res] = await request({ 93 | url: '/user/info', 94 | }); 95 | if (res && res.errno === 200) { 96 | await dispatch('getFriendsList'); 97 | commit('SET_USER_INFO', { user_info: res.data }); 98 | // 登陆成功后,建立ws连接 99 | Chat.setup(); 100 | return { 101 | errno: 200, 102 | errmsg: '', 103 | data: res.data, 104 | }; 105 | } 106 | return { 107 | errno: res && res.errno || 1, 108 | errmsg: res && res.errmsg || '网络错误', 109 | data: null, 110 | }; 111 | }, 112 | setRecentContacts({ commit }: ActionContext, { friend_id }: { friend_id: number }) { 113 | commit('SET_RECENT_CONTACT', { friend_id }); 114 | }, 115 | setFullRecentContacts({ commit }: ActionContext, { list }: { list: number[] }) { 116 | commit('SET_FULL_RECENT_CONTACT', { list }); 117 | }, 118 | }; 119 | 120 | export default { 121 | namespaced: true, 122 | state, 123 | mutations, 124 | actions 125 | } 126 | -------------------------------------------------------------------------------- /server/src/service/user.ts: -------------------------------------------------------------------------------- 1 | import db from '../lib/db'; 2 | 3 | class User { 4 | private table = 'user'; 5 | 6 | /** 7 | * 创建用户 8 | * 9 | * @param {number} info.mobile 用户手机号 10 | * @param {string} info.password 用户密码 11 | * @returns 用户信息 12 | */ 13 | async createUser(info: { mobile: number; password: string }) { 14 | try { 15 | const data = await db.table(this.table) 16 | .add(info); 17 | return [null, data]; 18 | } catch (err) { 19 | return [err, null]; 20 | } 21 | } 22 | 23 | /** 24 | * 通过手机号查找用户 25 | * 26 | * @param {number} mobile 手机号 27 | * @returns 用户信息 28 | */ 29 | async getUserInfoByMobile(mobile: number) { 30 | try { 31 | const data = await db.table(this.table) 32 | .where({ 33 | mobile, 34 | }) 35 | .find(); 36 | return [null, data]; 37 | } catch (err) { 38 | return [err, null]; 39 | } 40 | } 41 | 42 | /** 43 | * 通过用户ID查找用户 44 | * 45 | * @param {number} uid 用户u 46 | * @returns 用户信息 47 | */ 48 | async getUserInfoById(uid: number) { 49 | try { 50 | const data = await db.table(this.table) 51 | .where({ 52 | id: uid, 53 | }) 54 | .find(); 55 | return [null, data]; 56 | } catch (err) { 57 | return [err, null]; 58 | } 59 | } 60 | 61 | /** 62 | * 通过手机号与密码查找用户,登陆验证 63 | * 64 | * @param {number} mobile 手机号 65 | * @param {string} password 密码 66 | * @returns 用户信息 67 | */ 68 | async getUserInfoByPassword(mobile: number, password: string) { 69 | try { 70 | const data = await db.table(this.table) 71 | .where({ 72 | mobile, 73 | password, 74 | }) 75 | .find(); 76 | return [null, data]; 77 | } catch (err) { 78 | return [err, null]; 79 | } 80 | } 81 | 82 | /** 83 | * 通过用户id获取好友列表 84 | * 85 | * @param {number} uid 用户ID 86 | * @returns 好友列表 87 | */ 88 | async getRelationByUid(uid: number) { 89 | try { 90 | const data = await db.table('view_user_friends') 91 | .where({ 92 | uid, 93 | }) 94 | .select(); 95 | return [null, data]; 96 | } catch (err) { 97 | return [err, null]; 98 | } 99 | } 100 | 101 | /** 102 | * 用户上下线更新token 103 | * 104 | * @param {number} uid 用户ID 105 | * @param {string} payload.token token 106 | * @param {string} payload.platform 平台 107 | * @returns 用户信息 108 | */ 109 | async updateUserToken(uid: number, payload: { token: string; platform: string }) { 110 | try { 111 | const data = await db.table(this.table) 112 | .where({ 113 | id: uid, 114 | }) 115 | .update({ 116 | token: payload.token, 117 | client_type: payload.platform, 118 | }); 119 | return [null, data]; 120 | } catch (err) { 121 | return [err, null]; 122 | } 123 | } 124 | 125 | /** 126 | * 用户WS上下线更新client_id 127 | * 128 | * @param {number} uid 用户ID 129 | * @param {string} client_id 130 | * @returns 用户信息 131 | */ 132 | async updateUserClientId(uid: number, client_id: string) { 133 | try { 134 | const data = await db.table(this.table) 135 | .where({ 136 | id: uid, 137 | }) 138 | .update({ 139 | client_id, 140 | }); 141 | return [null, data]; 142 | } catch (err) { 143 | return [err, null]; 144 | } 145 | } 146 | 147 | /** 148 | * 获取用户群组 149 | * 150 | * @param {number} uid 用户ID 151 | */ 152 | async getUserGroup(uid: number) { 153 | try { 154 | const data = await db.table('view_user_group') 155 | .where({ 156 | uid, 157 | }) 158 | .select(); 159 | return [null, data]; 160 | } catch (err) { 161 | return [err, null]; 162 | } 163 | } 164 | } 165 | 166 | export default new User(); 167 | -------------------------------------------------------------------------------- /client/socket/chat.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import io from '@hyoga/uni-socket.io'; 3 | import md5 from 'md5'; 4 | import config from '../config'; 5 | import { ENUM_MESSAGE_CONTENT_TYPE, ENUM_MESSAGE_DIST_TYPE, ENUM_MESSAGE_RESPONSE_STATUS, ENUM_SOCKET_MESSAGE_TYPE } from '../enum/message'; 6 | import { CHAT_MESSAGE, RESPONSE_MESSAGE, SOCKET_RESPONSE } from '../interface/response'; 7 | import { User, FriendInfo, Message, MessageRecord } from '../interface/entity'; 8 | import store from "../store"; 9 | import Storage from '../helper/storage'; 10 | 11 | declare let uni: any; 12 | const { ws } = config; 13 | 14 | class Chat { 15 | private static instance: Chat; 16 | private token: string = ''; 17 | private socket: any = null; 18 | 19 | public static getInstance() { 20 | if (!this.instance) { 21 | this.instance = new Chat(); 22 | } 23 | return this.instance; 24 | } 25 | 26 | public setup() { 27 | this.token = uni.getStorageSync('token'); 28 | 29 | if (!this.token) return; 30 | 31 | const socket = io(`${ws.host}/${ws.namespace}`, { 32 | query: { 33 | token: this.token, 34 | }, 35 | transports: [ 'websocket', 'polling' ], 36 | timeout: 5000, 37 | }); 38 | this.socket = socket; 39 | socket.on('connect', () => { 40 | console.log('ws 已连接'); 41 | const { id } = socket; 42 | socket.on(id, (data: SOCKET_RESPONSE) => { 43 | console.log('ws 收到服务器消息:', data); 44 | switch(data.message_type) { 45 | case ENUM_SOCKET_MESSAGE_TYPE.PRIVATE_CHAT: 46 | this.onMessage(data.message as CHAT_MESSAGE); 47 | break; 48 | case ENUM_SOCKET_MESSAGE_TYPE.MESSAGE_STATUS_CONFIRM: 49 | this.onConfirmMessage(data.message as RESPONSE_MESSAGE); 50 | break; 51 | } 52 | }); 53 | }); 54 | socket.on('error', (msg: any) => { 55 | console.log('ws error', msg); 56 | }); 57 | } 58 | 59 | /** 60 | * 接收到好友发来消息 61 | * @param data { CHAT_MESSAGE } 接收到的好友消息 62 | */ 63 | public async onMessage(data: CHAT_MESSAGE) { 64 | const { messages, sender_id } = data; 65 | await Storage.message.save(messages); 66 | await Storage.contacts.save(sender_id); 67 | await store.dispatch('message/setMessage', { messages }); 68 | await store.dispatch('user/setRecentContacts', { friend_id: sender_id }); 69 | } 70 | 71 | /** 72 | * 消息确认,确认消息已被接收,并更新消息ID 73 | * @param message { RESPONSE_MESSAGE } 收到的消息 74 | */ 75 | public async onConfirmMessage(message: RESPONSE_MESSAGE) { 76 | const { status, data } = message; 77 | switch(status) { 78 | // 消息发送成功,更新消息ID 79 | case ENUM_MESSAGE_RESPONSE_STATUS.SUCCESS: 80 | await Storage.message.save(data); 81 | await store.dispatch('message/updateMessage', { messages: data }); 82 | break; 83 | // TODO: 消息发送失败等处理 84 | } 85 | } 86 | 87 | /** 88 | * 发送消息 89 | * @param content {string} 发送内容 90 | * @param options.user_info {User} 发送者信息 91 | * @param options.friend_info {User} 接收者信息 92 | * @param options.is_group {boolean} 是否是群消息 93 | */ 94 | public async sendMessage(content: string, options: { user_info: User, friend_info: FriendInfo, is_group: boolean }) { 95 | if (!this.socket) return; 96 | content = content.trim(); 97 | if (!content) return; 98 | 99 | const { user_info, friend_info, is_group } = options; 100 | const message: Message = { 101 | hash: md5(`${user_info.id}_${friend_info.friend_id}_${+new Date()}`), 102 | user_id: user_info.id, 103 | dist_id: friend_info.friend_id, 104 | dist_type: is_group ? ENUM_MESSAGE_DIST_TYPE.GROUP : ENUM_MESSAGE_DIST_TYPE.PRIVATE, 105 | content_type: ENUM_MESSAGE_CONTENT_TYPE.TEXT, 106 | content, 107 | create_time: +new Date(), 108 | }; 109 | const record: MessageRecord = { 110 | ...message, 111 | is_owner: 1, 112 | }; 113 | this.socket.emit('message', { message }); 114 | await Storage.message.save([record]); 115 | await Storage.contacts.save(friend_info.friend_id); 116 | await store.dispatch('message/setMessage', { messages: [record] }); 117 | await store.dispatch('user/setRecentContacts', { friend_id: friend_info.friend_id }); 118 | } 119 | } 120 | 121 | 122 | export default Chat.getInstance(); 123 | -------------------------------------------------------------------------------- /client/pages/addressBook/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 76 | 77 | -------------------------------------------------------------------------------- /client/pages/user/signIn.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 89 | 90 | -------------------------------------------------------------------------------- /client/pages/chat/list.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 97 | 98 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist/", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "include":[ 70 | "./src/**/*" 71 | ], 72 | } 73 | -------------------------------------------------------------------------------- /client/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "快聊", 3 | "appid" : "__UNI__0CE1D62", 4 | "description" : "", 5 | "versionName" : "1.0.0", 6 | "versionCode" : "100", 7 | "transformPx" : false, 8 | /* 5+App特有相关 */ 9 | "app-plus" : { 10 | "nvue" : { 11 | "flex-direction" : "row" 12 | }, 13 | "usingComponents" : true, 14 | "nvueCompiler" : "uni-app", 15 | "compilerVersion" : 3, 16 | "splashscreen" : { 17 | "alwaysShowBeforeRender" : true, 18 | "waiting" : true, 19 | "autoclose" : true, 20 | "delay" : 0 21 | }, 22 | /* 模块配置 */ 23 | "modules" : { 24 | "Push" : {} 25 | }, 26 | /* 应用发布信息 */ 27 | "distribute" : { 28 | /* android打包配置 */ 29 | "android" : { 30 | "permissions" : [ 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "", 41 | "", 42 | "", 43 | "", 44 | "", 45 | "", 46 | "", 47 | "", 48 | "", 49 | "", 50 | "", 51 | "", 52 | "" 53 | ], 54 | "schemes" : "speedy", 55 | "abiFilters" : [ "armeabi-v7a", "arm64-v8a" ] 56 | }, 57 | /* ios打包配置 */ 58 | "ios" : {}, 59 | /* SDK配置 */ 60 | "sdkConfigs" : { 61 | "ad" : {} 62 | }, 63 | "icons" : { 64 | "android" : { 65 | "hdpi" : "unpackage/res/icons/72x72.png", 66 | "xhdpi" : "unpackage/res/icons/96x96.png", 67 | "xxhdpi" : "unpackage/res/icons/144x144.png", 68 | "xxxhdpi" : "unpackage/res/icons/192x192.png" 69 | }, 70 | "ios" : { 71 | "appstore" : "unpackage/res/icons/1024x1024.png", 72 | "ipad" : { 73 | "app" : "unpackage/res/icons/76x76.png", 74 | "app@2x" : "unpackage/res/icons/152x152.png", 75 | "notification" : "unpackage/res/icons/20x20.png", 76 | "notification@2x" : "unpackage/res/icons/40x40.png", 77 | "proapp@2x" : "unpackage/res/icons/167x167.png", 78 | "settings" : "unpackage/res/icons/29x29.png", 79 | "settings@2x" : "unpackage/res/icons/58x58.png", 80 | "spotlight" : "unpackage/res/icons/40x40.png", 81 | "spotlight@2x" : "unpackage/res/icons/80x80.png" 82 | }, 83 | "iphone" : { 84 | "app@2x" : "unpackage/res/icons/120x120.png", 85 | "app@3x" : "unpackage/res/icons/180x180.png", 86 | "notification@2x" : "unpackage/res/icons/40x40.png", 87 | "notification@3x" : "unpackage/res/icons/60x60.png", 88 | "settings@2x" : "unpackage/res/icons/58x58.png", 89 | "settings@3x" : "unpackage/res/icons/87x87.png", 90 | "spotlight@2x" : "unpackage/res/icons/80x80.png", 91 | "spotlight@3x" : "unpackage/res/icons/120x120.png" 92 | } 93 | } 94 | }, 95 | "splashscreen" : { 96 | "androidStyle" : "default" 97 | } 98 | }, 99 | "uniStatistics" : { 100 | "enable" : true 101 | } 102 | }, 103 | /* 快应用特有相关 */ 104 | "quickapp" : {}, 105 | /* 小程序特有相关 */ 106 | "mp-weixin" : { 107 | "appid" : "", 108 | "setting" : { 109 | "urlCheck" : false 110 | }, 111 | "usingComponents" : true, 112 | "uniStatistics" : { 113 | "enable" : true 114 | } 115 | }, 116 | "mp-alipay" : { 117 | "usingComponents" : true 118 | }, 119 | "mp-baidu" : { 120 | "usingComponents" : true 121 | }, 122 | "mp-toutiao" : { 123 | "usingComponents" : true 124 | }, 125 | "uniStatistics" : { 126 | "enable" : false 127 | }, 128 | "h5" : { 129 | "router" : { 130 | "mode" : "history", 131 | "base" : "/" 132 | }, 133 | "template" : "public/template.h5.html", 134 | "title" : "快聊 - 开源即时通讯项目", 135 | "optimization" : { 136 | "treeShaking" : { 137 | "enable" : true 138 | } 139 | }, 140 | "domain" : "https://speedy-im.gitee.io" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /server/src/routes/user.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import debug from 'debug'; 4 | import pinyin from 'pinyin'; 5 | import config from '../config'; 6 | import Util from '../helper/util'; 7 | import User from '../service/user'; 8 | import Message from '../service/message'; 9 | import { Message as MessageData, FriendInfo } from '../interface/entity'; 10 | import BlackList from '../helper/jwt.blacklist'; 11 | 12 | const log = debug('speedy-im user'); 13 | 14 | const router = express.Router(); 15 | 16 | /** 17 | * 从token获取用户信息 18 | * 19 | * @method GET 20 | * @param {token} string 21 | */ 22 | router.get('/info', async (req, res) => { 23 | const { user } = req as any; 24 | const { uid } = user || {}; 25 | const [err, info] = await User.getUserInfoById(+uid); 26 | if (err) { 27 | log(err); 28 | return res.json(Util.fail('内部服务器错误', 500)); 29 | } 30 | if (!info) { 31 | return res.json(Util.success('用户不存在', 401)); 32 | } 33 | delete info.password; 34 | delete info.client_id; 35 | delete info.create_time; 36 | return res.json(Util.success(info)); 37 | }); 38 | 39 | /** 40 | * 登录 41 | * 42 | * @method POST 43 | * @param {number} mobile 手机号 44 | * @param {string} password 密码 45 | * @param {'android' | 'ios' | 'web'} platform 登陆平台 46 | */ 47 | router.put('/signIn', async (req, res) => { 48 | const { mobile, password, platform = 'android' } = req.body; 49 | if (!mobile || mobile.length !== 11 || !password) { 50 | return res.json(Util.fail('用户不存在或密码错误', 0)); 51 | } 52 | const passwordEncode = Util.encodePassword(password); 53 | const [err, userInfo] = await User.getUserInfoByPassword(mobile, passwordEncode); 54 | if (err) { 55 | log(err); 56 | return res.json(Util.fail('数据库查询失败', 500)); 57 | } 58 | if (!userInfo || !userInfo.id) { 59 | return res.json(Util.fail('用户不存在或密码错误', 0)); 60 | } 61 | const payload = { 62 | uid: userInfo.id, 63 | }; 64 | const options = { 65 | expiresIn: '7d', 66 | }; 67 | const token = jwt.sign(payload, config.jwt.secret, options); 68 | const [err2] = await User.updateUserToken(userInfo.id, { token, platform }); 69 | if (err2) { 70 | log(err2); 71 | return res.json(Util.fail('数据库写入失败', 500)); 72 | } 73 | 74 | delete userInfo.password; 75 | return res.json(Util.success({ 76 | ...userInfo, 77 | token, 78 | })); 79 | }); 80 | 81 | /** 82 | * 注册 83 | * 84 | * @method POST 85 | * @param {number} mobile 手机号 86 | * @param {string} password 密码 87 | */ 88 | router.post('/signUp', async (req, res) => { 89 | let { mobile, password = '' } = req.body; 90 | password = password.trim(); 91 | 92 | if (!mobile || mobile.length !== 11) { 93 | return res.json(Util.fail('手机号不正确', 0)); 94 | } 95 | if (!password) { 96 | return res.json(Util.fail('密码不能为空', 0)); 97 | } 98 | mobile = +mobile; 99 | const [err, _user] = await User.getUserInfoByMobile(mobile); 100 | if (err) { 101 | log(err); 102 | return res.json(Util.fail('数据库操作失败', 500)); 103 | } 104 | if (_user) { 105 | return res.json(Util.fail('手机号已存在', 0)); 106 | } 107 | const passwordEncode = Util.encodePassword(password); 108 | const [err2, info] = await User.createUser({ mobile, password: passwordEncode }); 109 | if (err2) { 110 | log(err2); 111 | return res.json(Util.fail('数据库操作失败', 500)); 112 | } 113 | const { insertId } = info; 114 | if (!insertId) { 115 | return res.json(Util.fail('数据库操作失败', 500)); 116 | } 117 | return res.json(Util.success(true)); 118 | }); 119 | 120 | /** 121 | * 注销登录 122 | */ 123 | router.put('/signOut', async (req, res) => { 124 | const { user } = req as any; 125 | const { uid } = user || {}; 126 | const token = Util.getToken(req); 127 | const payload = jwt.decode(token); 128 | if (Object.prototype.toString.call(payload) === '[object Object]') { 129 | const exp: number = payload && (payload as any).exp; 130 | BlackList.add(token, exp); 131 | } 132 | await User.updateUserToken(uid, { token: '', platform: '' }); 133 | return res.json(Util.success('ok')); 134 | }); 135 | 136 | /** 137 | * 获取好友列表 138 | * 139 | * @method GET 140 | * @param {token} string 141 | */ 142 | router.get('/friends', async (req, res) => { 143 | const { user } = req as any; 144 | const { uid } = user || {}; 145 | const [err, data] = await User.getRelationByUid(uid); 146 | if (err) { 147 | log(err); 148 | return res.json(Util.fail('数据库查询失败', 500)); 149 | } 150 | let final: { key: string; list: any[] }[] = []; 151 | const obj: any = {}; 152 | const others: any = []; 153 | data.forEach((item: FriendInfo) => { 154 | const name = item.remark || item.nickname; 155 | const p = pinyin(name, { 156 | style: pinyin.STYLE_FIRST_LETTER, 157 | }); 158 | const firstLetter: string = p && p[0] && p[0][0] || ''; 159 | if (firstLetter) { 160 | const letter = firstLetter.toLocaleUpperCase(); 161 | if (!obj[letter]) { 162 | obj[letter] = []; 163 | } 164 | obj[letter].push(item); 165 | } else { 166 | others.push(item); 167 | } 168 | }); 169 | Object.keys(obj).forEach((key) => { 170 | final.push({ 171 | key, 172 | list: obj[key], 173 | }); 174 | }); 175 | final = final.sort((a, b) => (a.key > b.key ? 1 : -1)); 176 | if (others.length) { 177 | final.push({ 178 | key: '#', 179 | list: others, 180 | }); 181 | } 182 | return res.json(Util.success(final)); 183 | }); 184 | 185 | /** 186 | * 获取群组 187 | * 188 | * @method GET 189 | * @param {token} string 190 | */ 191 | router.get('/groups', async (req, res) => { 192 | const { user } = req as any; 193 | const { uid } = user || {}; 194 | const [err, list] = await User.getUserGroup(uid); 195 | if (err) { 196 | log(err); 197 | return res.json(Util.fail('数据库查询失败', 500)); 198 | } 199 | return res.json(Util.success(list)); 200 | }); 201 | 202 | /** 203 | * 获取未读消息 204 | * 205 | * @method GET 206 | * @param {token} string 207 | */ 208 | router.get('/unreadMessage', async (req, res) => { 209 | const { user } = req as any; 210 | const { uid } = user || {}; 211 | // TODO,分页 212 | const [err, list] = await Message.getUnreadMessage(uid); 213 | if (err) { 214 | log(err); 215 | return res.json(Util.fail('数据库查询失败', 500)); 216 | } 217 | const tmp: number[] = []; 218 | 219 | const result = list.map((item: MessageData) => { 220 | tmp.push(item.id as number); 221 | return { 222 | ...item, 223 | is_owner: uid === item.user_id ? 1 : 0, 224 | }; 225 | }); 226 | if (tmp.length) { 227 | Message.updateMultipleMessage(tmp, { is_sent: 1 }); 228 | } 229 | return res.json(Util.success(result)); 230 | }); 231 | 232 | export default router; 233 | -------------------------------------------------------------------------------- /server/src/socket/chat.ts: -------------------------------------------------------------------------------- 1 | import socketIO from 'socket.io'; 2 | import debug from 'debug'; 3 | import jwt from 'jsonwebtoken'; 4 | import User from '../service/user'; 5 | import Group from '../service/group'; 6 | import Relation from '../service/relation'; 7 | import Message from '../service/message'; 8 | import config from '../config'; 9 | import { MessageRecord, Message as MessageData } from '../interface/entity'; 10 | import { CHAT_MESSAGE, RESPONSE_MESSAGE, SOCKET_RESPONSE } from '../interface/response'; 11 | import { 12 | ENUM_MESSAGE_DIST_TYPE, ENUM_MESSAGE_CONTENT_TYPE, ENUM_SOCKET_MESSAGE_TYPE, ENUM_MESSAGE_RESPONSE_STATUS, 13 | } from '../enum/message'; 14 | 15 | const log = debug('ws'); 16 | 17 | export default class Chat { 18 | private namespace = 'chat'; 19 | 20 | private io: socketIO.Server; 21 | 22 | private nsp: socketIO.Namespace; 23 | 24 | constructor(io: socketIO.Server) { 25 | this.io = io; 26 | this.nsp = io.of(this.namespace); 27 | } 28 | 29 | setup() { 30 | this.nsp.on('connect', async (socket: socketIO.Socket) => { 31 | log('用户已连接'); 32 | 33 | const { handshake } = socket; 34 | const { query } = handshake; 35 | const { token } = query; 36 | const user: any = jwt.verify(token, config.jwt.secret); 37 | const { uid } = user; 38 | await User.updateUserClientId(uid, socket.id); 39 | this.onMessage(socket, uid); 40 | 41 | // 用户下线 42 | socket.on('disconnect', (reason: string) => { 43 | log('用户断开连接', reason); 44 | User.updateUserClientId(uid, ''); 45 | }); 46 | }); 47 | } 48 | 49 | private onMessage(socket: socketIO.Socket, uid: number) { 50 | socket.on('message', async (payload: { message: MessageRecord }) => { 51 | const { id } = socket; 52 | const { message } = payload; 53 | log('收到消息:', message); 54 | const { 55 | dist_id, 56 | dist_type = ENUM_MESSAGE_DIST_TYPE.PRIVATE, 57 | content, 58 | } = message; 59 | 60 | const response_status_message: RESPONSE_MESSAGE = { 61 | status: ENUM_MESSAGE_RESPONSE_STATUS.SUCCESS, 62 | data: null, 63 | }; 64 | const response: SOCKET_RESPONSE = { 65 | message_type: ENUM_SOCKET_MESSAGE_TYPE.MESSAGE_STATUS_CONFIRM, 66 | message: response_status_message, 67 | }; 68 | 69 | // 参数错误 70 | if (!dist_id || !content) { 71 | response_status_message.status = ENUM_MESSAGE_RESPONSE_STATUS.INVALID_PARAMS; 72 | socket.emit(id, response); 73 | return; 74 | } 75 | if (dist_type === ENUM_MESSAGE_DIST_TYPE.PRIVATE) { 76 | this.handlePrivateMessage(socket, uid, payload); 77 | } else { 78 | this.handleGroupMessage(socket, uid, payload); 79 | } 80 | }); 81 | } 82 | 83 | private async handlePrivateMessage(socket: socketIO.Socket, uid: number, payload: { message: MessageRecord }) { 84 | const { id } = socket; 85 | const { message } = payload; 86 | const { 87 | dist_id, 88 | dist_type = ENUM_MESSAGE_DIST_TYPE.PRIVATE, 89 | content_type = ENUM_MESSAGE_CONTENT_TYPE.TEXT, 90 | content, 91 | hash, 92 | } = message; 93 | const create_time = +new Date(); 94 | const response_status_message: RESPONSE_MESSAGE = { 95 | status: ENUM_MESSAGE_RESPONSE_STATUS.SUCCESS, 96 | data: null, 97 | }; 98 | const response: SOCKET_RESPONSE = { 99 | message_type: ENUM_SOCKET_MESSAGE_TYPE.MESSAGE_STATUS_CONFIRM, 100 | message: response_status_message, 101 | }; 102 | 103 | // 判断对方是否存在 104 | const [, friend_info] = await User.getUserInfoById(dist_id); 105 | if (!friend_info) { 106 | response_status_message.status = ENUM_MESSAGE_RESPONSE_STATUS.USER_NOT_EXIST; 107 | socket.emit(id, response); 108 | return; 109 | } 110 | // 判断对方是否是自己的好友,可能未添加对方 111 | const [, info1] = await Relation.getUserFriend(uid, dist_id); 112 | if (!info1) { 113 | response_status_message.status = ENUM_MESSAGE_RESPONSE_STATUS.NOT_FRIEND_OF_OTHER; 114 | socket.emit(id, response); 115 | return; 116 | } 117 | // 判断自己是对方好友,可能自己已被对方拉黑 118 | const [, info2] = await Relation.getUserFriend(dist_id, uid); 119 | if (!info2) { 120 | response_status_message.status = ENUM_MESSAGE_RESPONSE_STATUS.NOT_FRIEND_OF_MINE; 121 | socket.emit(id, response); 122 | return; 123 | } 124 | const sql_message: MessageData = { 125 | hash, 126 | user_id: uid, 127 | dist_id, 128 | dist_type, 129 | content_type, 130 | content, 131 | create_time, 132 | status: 1, 133 | is_sent: friend_info.client_id ? 1 : 0, 134 | }; 135 | const [error, result] = await Message.createMessage(sql_message); 136 | if (error || !result.insertId) { 137 | response_status_message.status = ENUM_MESSAGE_RESPONSE_STATUS.ERROR; 138 | // 数据库插入失败 139 | socket.emit(id, response); 140 | return; 141 | } 142 | 143 | // 告诉用户,消息发送成功 144 | response_status_message.data = { id: result.insertId, hash, friend_id: dist_id }; 145 | socket.emit(id, response); 146 | 147 | if (!friend_info.client_id) return; 148 | // 对方在线,发送消息给对方 149 | const final_message: MessageRecord = { 150 | ...sql_message, 151 | is_owner: 0, 152 | id: result.insertId, 153 | }; 154 | const user_message: CHAT_MESSAGE = { 155 | type: ENUM_MESSAGE_DIST_TYPE.PRIVATE, 156 | sender_id: uid, 157 | receive_id: dist_id, 158 | messages: [final_message], 159 | }; 160 | const user_response: SOCKET_RESPONSE = { 161 | message_type: ENUM_SOCKET_MESSAGE_TYPE.PRIVATE_CHAT, 162 | message: user_message, 163 | }; 164 | this.nsp.emit(friend_info.client_id, user_response); 165 | await Message.updateMessage(result.insertId, { is_sent: 1 }); 166 | } 167 | 168 | private async handleGroupMessage(socket: socketIO.Socket, uid: number, payload: { message: MessageRecord }) { 169 | const { id } = socket; 170 | const { message } = payload; 171 | const { 172 | dist_id, 173 | dist_type = ENUM_MESSAGE_DIST_TYPE.PRIVATE, 174 | content_type = ENUM_MESSAGE_CONTENT_TYPE.TEXT, 175 | content, 176 | hash, 177 | } = message; 178 | const create_time = +new Date(); 179 | const response_status_message: RESPONSE_MESSAGE = { 180 | status: ENUM_MESSAGE_RESPONSE_STATUS.SUCCESS, 181 | data: null, 182 | }; 183 | const response: SOCKET_RESPONSE = { 184 | message_type: ENUM_SOCKET_MESSAGE_TYPE.MESSAGE_STATUS_CONFIRM, 185 | message: response_status_message, 186 | }; 187 | 188 | // 判断群是否存在并且自己在群里面 189 | const [, dist_group] = await Group.getUserGroup(uid, dist_id); 190 | if (!dist_group) { 191 | response_status_message.status = ENUM_MESSAGE_RESPONSE_STATUS.NOT_IN_GROUP; 192 | socket.emit(id, response); 193 | return; 194 | } 195 | const final_message: MessageRecord = { 196 | hash, 197 | user_id: uid, 198 | dist_id, 199 | dist_type, 200 | content_type, 201 | content, 202 | create_time, 203 | status: 1, 204 | is_sent: 1, 205 | is_owner: 0, 206 | }; 207 | const result: any = await Message.createMessage(final_message); 208 | final_message.id = result.insert_id; 209 | 210 | // TODO 群聊处理 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /client/pages/chat/chat.nvue.back: -------------------------------------------------------------------------------- 1 | 61 | 62 | 104 | 105 | -------------------------------------------------------------------------------- /client/pages/chat/chat.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 141 | 142 | -------------------------------------------------------------------------------- /docker/mysql/speedy-im_2020-10-14.sql: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Sequel Pro SQL dump 3 | # Version 4541 4 | # 5 | # http://www.sequelpro.com/ 6 | # https://github.com/sequelpro/sequelpro 7 | # 8 | # Host: 127.0.0.1 (MySQL 5.6.49) 9 | # Database: speedy-im 10 | # Generation Time: 2020-10-14 10:09:49 +0000 11 | # ************************************************************ 12 | 13 | 14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 17 | /*!40101 SET NAMES utf8 */; 18 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 19 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 20 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 21 | 22 | 23 | # Dump of table group 24 | # ------------------------------------------------------------ 25 | 26 | DROP TABLE IF EXISTS `group`; 27 | 28 | CREATE TABLE `group` ( 29 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 30 | `name` varchar(255) NOT NULL DEFAULT '', 31 | `avatar` varchar(255) DEFAULT NULL, 32 | `intrduce` text COMMENT '群介绍', 33 | `limit` int(5) NOT NULL DEFAULT '100' COMMENT '群上限', 34 | `create_uid` bigint(20) NOT NULL COMMENT '创建人', 35 | `create_time` bigint(20) NOT NULL, 36 | `status` int(2) NOT NULL DEFAULT '1' COMMENT '状态', 37 | PRIMARY KEY (`id`) 38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 39 | 40 | LOCK TABLES `group` WRITE; 41 | /*!40000 ALTER TABLE `group` DISABLE KEYS */; 42 | 43 | INSERT INTO `group` (`id`, `name`, `avatar`, `intrduce`, `limit`, `create_uid`, `create_time`, `status`) 44 | VALUES 45 | (1000,'测试一群','https://im.wangcai.me/speedy_avatar_7.jpg','测试一群',100,1000,1591349594288,1); 46 | 47 | /*!40000 ALTER TABLE `group` ENABLE KEYS */; 48 | UNLOCK TABLES; 49 | 50 | 51 | # Dump of table message 52 | # ------------------------------------------------------------ 53 | 54 | DROP TABLE IF EXISTS `message`; 55 | 56 | CREATE TABLE `message` ( 57 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 58 | `user_id` bigint(20) NOT NULL, 59 | `hash` varchar(40) NOT NULL DEFAULT '', 60 | `dist_id` bigint(20) NOT NULL COMMENT '群聊是id是群的id', 61 | `dist_type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '1 - 私聊 2 - 群聊', 62 | `is_received` tinyint(2) NOT NULL DEFAULT '0' COMMENT '对方是否收到', 63 | `is_sent` tinyint(2) NOT NULL DEFAULT '0' COMMENT '是否已经发送给对方', 64 | `content_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT 'text,audio,image,video', 65 | `content` text NOT NULL COMMENT '内容或者地址', 66 | `create_time` bigint(20) NOT NULL, 67 | `status` tinyint(2) NOT NULL DEFAULT '1', 68 | PRIMARY KEY (`id`) 69 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 70 | 71 | LOCK TABLES `message` WRITE; 72 | /*!40000 ALTER TABLE `message` DISABLE KEYS */; 73 | 74 | INSERT INTO `message` (`id`, `user_id`, `hash`, `dist_id`, `dist_type`, `is_received`, `is_sent`, `content_type`, `content`, `create_time`, `status`) 75 | VALUES 76 | (1,1000,'',1001,1,0,0,'text','你好',1591349594288,1), 77 | (2,1000,'',1002,1,0,0,'text','你也好',1591349594288,1), 78 | (3,1000,'',1001,1,0,0,'text','在吗?',1591355800809,1), 79 | (4,1001,'',1000,1,0,1,'text','你好',1591349594288,1), 80 | (5,1002,'',1000,1,0,1,'text','你好',1591349594288,1), 81 | (6,1003,'',1000,1,0,1,'text','你好',1591349594288,1), 82 | (7,1000,'a7dbd60473e1fad7e4a2cb12d15876d9',1002,1,0,0,'text','dada',1602592829490,1), 83 | (8,1000,'b8ff71585ec8b878872718ebee9e401d',1002,1,0,0,'text','12',1602593358750,1); 84 | 85 | /*!40000 ALTER TABLE `message` ENABLE KEYS */; 86 | UNLOCK TABLES; 87 | 88 | 89 | # Dump of table relation 90 | # ------------------------------------------------------------ 91 | 92 | DROP TABLE IF EXISTS `relation`; 93 | 94 | CREATE TABLE `relation` ( 95 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 96 | `uid` bigint(20) NOT NULL, 97 | `friend_id` bigint(20) NOT NULL, 98 | `remark` varchar(255) NOT NULL DEFAULT '', 99 | `status` int(2) NOT NULL DEFAULT '1' COMMENT '0 - 删除 1 - 正常 2 - 拉黑', 100 | PRIMARY KEY (`id`) 101 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 102 | 103 | LOCK TABLES `relation` WRITE; 104 | /*!40000 ALTER TABLE `relation` DISABLE KEYS */; 105 | 106 | INSERT INTO `relation` (`id`, `uid`, `friend_id`, `remark`, `status`) 107 | VALUES 108 | (1,1000,1001,'',1), 109 | (2,1000,1002,'',1), 110 | (3,1000,1003,'',1), 111 | (4,1001,1000,'',1), 112 | (5,1002,1000,'',1), 113 | (6,1003,1000,'',1); 114 | 115 | /*!40000 ALTER TABLE `relation` ENABLE KEYS */; 116 | UNLOCK TABLES; 117 | 118 | 119 | # Dump of table role 120 | # ------------------------------------------------------------ 121 | 122 | DROP TABLE IF EXISTS `role`; 123 | 124 | CREATE TABLE `role` ( 125 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 126 | `name` varchar(255) NOT NULL DEFAULT '', 127 | `status` tinyint(4) NOT NULL DEFAULT '1', 128 | PRIMARY KEY (`id`) 129 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 130 | 131 | LOCK TABLES `role` WRITE; 132 | /*!40000 ALTER TABLE `role` DISABLE KEYS */; 133 | 134 | INSERT INTO `role` (`id`, `name`, `status`) 135 | VALUES 136 | (1000,'管理员',1); 137 | 138 | /*!40000 ALTER TABLE `role` ENABLE KEYS */; 139 | UNLOCK TABLES; 140 | 141 | 142 | # Dump of table user 143 | # ------------------------------------------------------------ 144 | 145 | DROP TABLE IF EXISTS `user`; 146 | 147 | CREATE TABLE `user` ( 148 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 149 | `nickname` varchar(255) NOT NULL DEFAULT '', 150 | `mobile` bigint(11) NOT NULL, 151 | `password` varchar(255) NOT NULL DEFAULT '', 152 | `avatar` varchar(255) DEFAULT '', 153 | `sex` tinyint(3) NOT NULL DEFAULT '2' COMMENT '0 - 男 1 - 女 2 - 未知', 154 | `token` varchar(255) DEFAULT NULL, 155 | `client_id` varchar(255) DEFAULT NULL COMMENT 'socket_id', 156 | `client_type` varchar(50) DEFAULT NULL COMMENT 'android/ios', 157 | `create_time` bigint(20) NOT NULL, 158 | `status` tinyint(2) NOT NULL DEFAULT '1', 159 | PRIMARY KEY (`id`) 160 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 161 | 162 | LOCK TABLES `user` WRITE; 163 | /*!40000 ALTER TABLE `user` DISABLE KEYS */; 164 | 165 | INSERT INTO `user` (`id`, `nickname`, `mobile`, `password`, `avatar`, `sex`, `token`, `client_id`, `client_type`, `create_time`, `status`) 166 | VALUES 167 | (1000,'罗老魔',13600000000,'81c4369bea82d8daafd75818497dc962033a1dcc','https://im.wangcai.me/speedy_avatar_6.jpg',0,'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEwMDAsImlhdCI6MTYwMjU5MjI1NywiZXhwIjoxNjAzMTk3MDU3fQ.5n49dPHRqCf8i82rYlL1YknrIeCqSWhrNeUvFjEE9jA','/chat#sy6u7rmq0Ag-uxH-AAAC','android',1591349594288,1), 168 | (1001,'小七',13600000001,'81c4369bea82d8daafd75818497dc962033a1dcc','https://im.wangcai.me/speedy_avatar_1.jpg',1,NULL,NULL,NULL,1591349594288,1), 169 | (1002,'小白',13600000002,'81c4369bea82d8daafd75818497dc962033a1dcc','https://im.wangcai.me/speedy_avatar_2.jpg',1,NULL,NULL,NULL,1591349594288,1), 170 | (1003,'小青',13600000003,'81c4369bea82d8daafd75818497dc962033a1dcc','https://im.wangcai.me/speedy_avatar_3.jpg',1,NULL,NULL,NULL,1591349594288,1); 171 | 172 | /*!40000 ALTER TABLE `user` ENABLE KEYS */; 173 | UNLOCK TABLES; 174 | 175 | 176 | # Dump of table user_group 177 | # ------------------------------------------------------------ 178 | 179 | DROP TABLE IF EXISTS `user_group`; 180 | 181 | CREATE TABLE `user_group` ( 182 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 183 | `uid` bigint(20) NOT NULL, 184 | `group_id` bigint(20) NOT NULL, 185 | `remark` varchar(255) DEFAULT NULL COMMENT '群备注,别名', 186 | `role_id` int(11) DEFAULT NULL COMMENT '群角色', 187 | `status` tinyint(4) NOT NULL DEFAULT '1', 188 | PRIMARY KEY (`id`) 189 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 190 | 191 | LOCK TABLES `user_group` WRITE; 192 | /*!40000 ALTER TABLE `user_group` DISABLE KEYS */; 193 | 194 | INSERT INTO `user_group` (`id`, `uid`, `group_id`, `remark`, `role_id`, `status`) 195 | VALUES 196 | (1000,1000,1000,NULL,1000,1); 197 | 198 | /*!40000 ALTER TABLE `user_group` ENABLE KEYS */; 199 | UNLOCK TABLES; 200 | 201 | 202 | # Dump of table view_user_friends 203 | # ------------------------------------------------------------ 204 | 205 | DROP VIEW IF EXISTS `view_user_friends`; 206 | 207 | CREATE TABLE `view_user_friends` ( 208 | `id` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0', 209 | `uid` BIGINT(20) NOT NULL, 210 | `friend_id` BIGINT(20) NOT NULL, 211 | `remark` VARCHAR(255) NOT NULL DEFAULT '', 212 | `nickname` VARCHAR(255) NOT NULL DEFAULT '', 213 | `mobile` BIGINT(11) NOT NULL, 214 | `avatar` VARCHAR(255) NULL DEFAULT '', 215 | `sex` TINYINT(3) NOT NULL DEFAULT '2', 216 | `client_id` VARCHAR(255) NULL DEFAULT NULL, 217 | `client_type` VARCHAR(50) NULL DEFAULT NULL, 218 | `create_time` BIGINT(20) NOT NULL, 219 | `status` INT(2) NOT NULL DEFAULT '1' 220 | ) ENGINE=MyISAM; 221 | 222 | 223 | 224 | # Dump of table view_user_group 225 | # ------------------------------------------------------------ 226 | 227 | DROP VIEW IF EXISTS `view_user_group`; 228 | 229 | CREATE TABLE `view_user_group` ( 230 | `uid` BIGINT(20) NOT NULL, 231 | `user_group_remark` VARCHAR(255) NULL DEFAULT NULL, 232 | `user_group_status` TINYINT(4) NOT NULL DEFAULT '1', 233 | `group_id` BIGINT(20) NOT NULL, 234 | `name` VARCHAR(255) NULL DEFAULT '', 235 | `avatar` VARCHAR(255) NULL DEFAULT NULL, 236 | `intrduce` TEXT NULL DEFAULT NULL, 237 | `limit` INT(5) NULL DEFAULT '100', 238 | `group_status` INT(2) NULL DEFAULT '1', 239 | `create_time` BIGINT(20) NULL DEFAULT NULL, 240 | `role_name` VARCHAR(255) NULL DEFAULT '', 241 | `role_id` INT(11) NULL DEFAULT NULL, 242 | `role_status` TINYINT(4) NULL DEFAULT '1' 243 | ) ENGINE=MyISAM; 244 | 245 | 246 | 247 | 248 | 249 | # Replace placeholder table for view_user_friends with correct view syntax 250 | # ------------------------------------------------------------ 251 | 252 | DROP TABLE `view_user_friends`; 253 | 254 | CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `view_user_friends` 255 | AS SELECT 256 | `R`.`id` AS `id`, 257 | `R`.`uid` AS `uid`, 258 | `R`.`friend_id` AS `friend_id`, 259 | `R`.`remark` AS `remark`, 260 | `U`.`nickname` AS `nickname`, 261 | `U`.`mobile` AS `mobile`, 262 | `U`.`avatar` AS `avatar`, 263 | `U`.`sex` AS `sex`, 264 | `U`.`client_id` AS `client_id`, 265 | `U`.`client_type` AS `client_type`, 266 | `U`.`create_time` AS `create_time`, 267 | `R`.`status` AS `status` 268 | FROM (`user` `U` join `relation` `R` on((`U`.`id` = `R`.`friend_id`))); 269 | 270 | 271 | # Replace placeholder table for view_user_group with correct view syntax 272 | # ------------------------------------------------------------ 273 | 274 | DROP TABLE `view_user_group`; 275 | 276 | CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `view_user_group` 277 | AS SELECT 278 | `u`.`uid` AS `uid`, 279 | `u`.`remark` AS `user_group_remark`, 280 | `u`.`status` AS `user_group_status`, 281 | `u`.`group_id` AS `group_id`, 282 | `a`.`name` AS `name`, 283 | `a`.`avatar` AS `avatar`, 284 | `a`.`intrduce` AS `intrduce`, 285 | `a`.`limit` AS `limit`, 286 | `a`.`status` AS `group_status`, 287 | `a`.`create_time` AS `create_time`, 288 | `b`.`name` AS `role_name`, 289 | `u`.`role_id` AS `role_id`, 290 | `b`.`status` AS `role_status` 291 | FROM ((`user_group` `u` left join `group` `a` on((`u`.`group_id` = `a`.`id`))) left join `role` `b` on((`u`.`role_id` = `b`.`id`))); 292 | 293 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 294 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 295 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 296 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 297 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 298 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 299 | -------------------------------------------------------------------------------- /client/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@hyoga/uni-socket.io@^1.0.1": 6 | version "1.0.1" 7 | resolved "https://registry.yarnpkg.com/@hyoga/uni-socket.io/-/uni-socket.io-1.0.1.tgz#e66b76cdf635a5789c39d12fe7c7bd7734163be7" 8 | integrity sha512-+a3PvweyFHx39IJfJWoO9wYsvv50XCIslJ23OiOs8/jMdCZud7c4uWoZF5gH2iskcX72JcDm1Gk5g5o8X7dxEg== 9 | dependencies: 10 | socket.io-client "^2.1.1" 11 | 12 | "@types/md5@^2.2.0": 13 | version "2.2.0" 14 | resolved "https://npm.garenanow.com/@types%2fmd5/-/md5-2.2.0.tgz#cd82e16b95973f94bb03dee40c5b6be4a7fb7fb4" 15 | integrity sha512-JN8OVL/wiDlCWTPzplsgMPu0uE9Q6blwp68rYsfk2G8aokRUQ8XD9MEhZwihfAiQvoyE+m31m6i3GFXwYWomKQ== 16 | dependencies: 17 | "@types/node" "*" 18 | 19 | "@types/node@*": 20 | version "14.14.0" 21 | resolved "https://npm.garenanow.com/@types%2fnode/-/node-14.14.0.tgz#f1091b6ad5de18e8e91bdbd43ec63f13de372538" 22 | integrity sha512-BfbIHP9IapdupGhq/hc+jT5dyiBVZ2DdeC5WwJWQWDb0GijQlzUFAeIQn/2GtvZcd2HVUU7An8felIICFTC2qg== 23 | 24 | "@types/node@^14.14.16": 25 | version "14.14.16" 26 | resolved "https://npm.garenanow.com/@types%2fnode/-/node-14.14.16.tgz#3cc351f8d48101deadfed4c9e4f116048d437b4b" 27 | integrity sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw== 28 | 29 | "@types/uni-app@^1.4.2": 30 | version "1.4.2" 31 | resolved "https://registry.yarnpkg.com/@types/uni-app/-/uni-app-1.4.2.tgz#e72af63b41ab77a869d058e3259d5c90fa01ba1b" 32 | integrity sha512-PRMseHhBDaqHldOVkQrkTYkIG4RBJJKLVlSpLtc4GgqOlnL9nLpuqMz5e/UPDOnClsCeT4cfug6fqCGtRD3M9Q== 33 | dependencies: 34 | vue "^2.6.8" 35 | 36 | "@types/webpack-env@^1.15.3": 37 | version "1.15.3" 38 | resolved "https://npm.garenanow.com/@types%2fwebpack-env/-/webpack-env-1.15.3.tgz#fb602cd4c2f0b7c0fb857e922075fdf677d25d84" 39 | integrity sha512-5oiXqR7kwDGZ6+gmzIO2lTC+QsriNuQXZDWNYRV3l2XRN/zmPgnC21DLSx2D05zvD8vnXW6qUg7JnXZ4I6qLVQ== 40 | 41 | after@0.8.2: 42 | version "0.8.2" 43 | resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" 44 | integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= 45 | 46 | arraybuffer.slice@~0.0.7: 47 | version "0.0.7" 48 | resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" 49 | integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== 50 | 51 | async-limiter@~1.0.0: 52 | version "1.0.1" 53 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" 54 | integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== 55 | 56 | backo2@1.0.2: 57 | version "1.0.2" 58 | resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" 59 | integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= 60 | 61 | base64-arraybuffer@0.1.5: 62 | version "0.1.5" 63 | resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" 64 | integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= 65 | 66 | better-assert@~1.0.0: 67 | version "1.0.2" 68 | resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" 69 | integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= 70 | dependencies: 71 | callsite "1.0.0" 72 | 73 | blob@0.0.5: 74 | version "0.0.5" 75 | resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" 76 | integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== 77 | 78 | callsite@1.0.0: 79 | version "1.0.0" 80 | resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" 81 | integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= 82 | 83 | charenc@0.0.2: 84 | version "0.0.2" 85 | resolved "https://npm.garenanow.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 86 | integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= 87 | 88 | component-bind@1.0.0: 89 | version "1.0.0" 90 | resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" 91 | integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= 92 | 93 | component-emitter@1.2.1: 94 | version "1.2.1" 95 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" 96 | integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= 97 | 98 | component-emitter@~1.3.0: 99 | version "1.3.0" 100 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" 101 | integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== 102 | 103 | component-inherit@0.0.3: 104 | version "0.0.3" 105 | resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" 106 | integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= 107 | 108 | crypt@0.0.2: 109 | version "0.0.2" 110 | resolved "https://npm.garenanow.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" 111 | integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= 112 | 113 | dayjs@^1.8.28: 114 | version "1.8.28" 115 | resolved "https://registry.npm.taobao.org/dayjs/download/dayjs-1.8.28.tgz#37aa6201df483d089645cb6c8f6cef6f0c4dbc07" 116 | integrity sha1-N6piAd9IPQiWRctsj2zvbwxNvAc= 117 | 118 | debug@~3.1.0: 119 | version "3.1.0" 120 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 121 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 122 | dependencies: 123 | ms "2.0.0" 124 | 125 | debug@~4.1.0: 126 | version "4.1.1" 127 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 128 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== 129 | dependencies: 130 | ms "^2.1.1" 131 | 132 | engine.io-client@~3.4.0: 133 | version "3.4.2" 134 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.2.tgz#4fb2ef2b1fe1d3aa1c621c6a8d87f1fc55426b50" 135 | integrity sha512-AWjc1Xg06a6UPFOBAzJf48W1UR/qKYmv/ubgSCumo9GXgvL/xGIvo05dXoBL+2NTLMipDI7in8xK61C17L25xg== 136 | dependencies: 137 | component-emitter "~1.3.0" 138 | component-inherit "0.0.3" 139 | debug "~4.1.0" 140 | engine.io-parser "~2.2.0" 141 | has-cors "1.1.0" 142 | indexof "0.0.1" 143 | parseqs "0.0.5" 144 | parseuri "0.0.5" 145 | ws "~6.1.0" 146 | xmlhttprequest-ssl "~1.5.4" 147 | yeast "0.1.2" 148 | 149 | engine.io-parser@~2.2.0: 150 | version "2.2.0" 151 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" 152 | integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== 153 | dependencies: 154 | after "0.8.2" 155 | arraybuffer.slice "~0.0.7" 156 | base64-arraybuffer "0.1.5" 157 | blob "0.0.5" 158 | has-binary2 "~1.0.2" 159 | 160 | has-binary2@~1.0.2: 161 | version "1.0.3" 162 | resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" 163 | integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== 164 | dependencies: 165 | isarray "2.0.1" 166 | 167 | has-cors@1.1.0: 168 | version "1.1.0" 169 | resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" 170 | integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= 171 | 172 | indexof@0.0.1: 173 | version "0.0.1" 174 | resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 175 | integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= 176 | 177 | is-buffer@~1.1.6: 178 | version "1.1.6" 179 | resolved "https://npm.garenanow.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 180 | integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 181 | 182 | isarray@2.0.1: 183 | version "2.0.1" 184 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" 185 | integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= 186 | 187 | md5@^2.3.0: 188 | version "2.3.0" 189 | resolved "https://npm.garenanow.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" 190 | integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== 191 | dependencies: 192 | charenc "0.0.2" 193 | crypt "0.0.2" 194 | is-buffer "~1.1.6" 195 | 196 | ms@2.0.0: 197 | version "2.0.0" 198 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 199 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 200 | 201 | ms@^2.1.1: 202 | version "2.1.2" 203 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 204 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 205 | 206 | object-component@0.0.3: 207 | version "0.0.3" 208 | resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" 209 | integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= 210 | 211 | parseqs@0.0.5: 212 | version "0.0.5" 213 | resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" 214 | integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= 215 | dependencies: 216 | better-assert "~1.0.0" 217 | 218 | parseuri@0.0.5: 219 | version "0.0.5" 220 | resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" 221 | integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= 222 | dependencies: 223 | better-assert "~1.0.0" 224 | 225 | socket.io-client@^2.1.1: 226 | version "2.3.0" 227 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" 228 | integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== 229 | dependencies: 230 | backo2 "1.0.2" 231 | base64-arraybuffer "0.1.5" 232 | component-bind "1.0.0" 233 | component-emitter "1.2.1" 234 | debug "~4.1.0" 235 | engine.io-client "~3.4.0" 236 | has-binary2 "~1.0.2" 237 | has-cors "1.1.0" 238 | indexof "0.0.1" 239 | object-component "0.0.3" 240 | parseqs "0.0.5" 241 | parseuri "0.0.5" 242 | socket.io-parser "~3.3.0" 243 | to-array "0.1.4" 244 | 245 | socket.io-parser@~3.3.0: 246 | version "3.3.0" 247 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" 248 | integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== 249 | dependencies: 250 | component-emitter "1.2.1" 251 | debug "~3.1.0" 252 | isarray "2.0.1" 253 | 254 | to-array@0.1.4: 255 | version "0.1.4" 256 | resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" 257 | integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= 258 | 259 | tslib@^2.0.3: 260 | version "2.0.3" 261 | resolved "https://npm.garenanow.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" 262 | integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== 263 | 264 | uni-hold-tabbar@^1.0.0: 265 | version "1.0.0" 266 | resolved "https://registry.yarnpkg.com/uni-hold-tabbar/-/uni-hold-tabbar-1.0.0.tgz#92be7924414a79a8e49d2f191d87959e60ade932" 267 | integrity sha512-y2aeK5x5+znct18UiD8/LHX3F6ilOPHWRuJgA72+FbEmrzKE4otXRqiRZDqhifo0Zq4mby/wIRzKyiEyOCq5iQ== 268 | 269 | uni-simple-router@^1.5.4: 270 | version "1.5.4" 271 | resolved "https://registry.yarnpkg.com/uni-simple-router/-/uni-simple-router-1.5.4.tgz#1f20b299e0a0b742f78d0931926406623d33348a" 272 | integrity sha512-P0qO2UN9FdSRQC8nBkmNHSY4a71B8doZp9lUmvvUPYQP6EUlFJUuMcbfiHgbChSb0ovLx8v+WFHwvdCuH0KTxg== 273 | dependencies: 274 | uni-hold-tabbar "^1.0.0" 275 | 276 | uview-ui@^1.2.8: 277 | version "1.2.8" 278 | resolved "https://registry.yarnpkg.com/uview-ui/-/uview-ui-1.2.8.tgz#cf9e43dbcf58445f5c2adb16defa6bb5e2f16f06" 279 | integrity sha512-GfEMUS8XfYR07iLpUUnDEMEb1EJOnLEuJ5ZH0DFGBjJnClz0R+v/SYfvW3XRZN9+a7PLc8rIwOj+DYkdj20VuQ== 280 | 281 | vue@^2.6.11, vue@^2.6.8: 282 | version "2.6.11" 283 | resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" 284 | integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== 285 | 286 | vuex@^3.4.0: 287 | version "3.4.0" 288 | resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.4.0.tgz#20cc086062d750769fce1febb34e7fceeaebde45" 289 | integrity sha512-ajtqwEW/QhnrBZQsZxCLHThZZaa+Db45c92Asf46ZDXu6uHXgbfVuBaJ4gzD2r4UX0oMJHstFwd2r2HM4l8umg== 290 | 291 | ws@~6.1.0: 292 | version "6.1.4" 293 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" 294 | integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== 295 | dependencies: 296 | async-limiter "~1.0.0" 297 | 298 | xmlhttprequest-ssl@~1.5.4: 299 | version "1.5.5" 300 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" 301 | integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= 302 | 303 | yeast@0.1.2: 304 | version "0.1.2" 305 | resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" 306 | integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= 307 | --------------------------------------------------------------------------------