├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── assets
├── database.png
├── demo1.png
├── demo2.png
├── demo3.png
├── demo4.png
├── demo5.png
├── electron1.png
└── electron2.png
├── client
├── .browserslistrc
├── .env.dev
├── .env.in
├── .env.out
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── mime
│ │ ├── doc.png
│ │ ├── docx.png
│ │ ├── exe.png
│ │ ├── img.png
│ │ ├── other.png
│ │ ├── pdf.png
│ │ ├── ppt.png
│ │ ├── rar.png
│ │ ├── txt.png
│ │ ├── xls.png
│ │ ├── xlsx.png
│ │ └── zip.png
├── src
│ ├── App.vue
│ ├── api
│ │ ├── apis
│ │ │ ├── index.ts
│ │ │ └── modules
│ │ │ │ ├── friend.ts
│ │ │ │ ├── group.ts
│ │ │ │ └── user.ts
│ │ └── axios
│ │ │ ├── index.ts
│ │ │ └── interceptors.ts
│ ├── assets
│ │ ├── friend.png
│ │ ├── group.png
│ │ └── send.png
│ ├── common
│ │ ├── index.ts
│ │ └── theme.ts
│ ├── components
│ │ ├── Avatar.vue
│ │ ├── Contact.vue
│ │ ├── ContactModal.vue
│ │ ├── Emoji.vue
│ │ ├── Entry.vue
│ │ ├── Login.vue
│ │ ├── Message.vue
│ │ ├── Nav.vue
│ │ ├── Panel.vue
│ │ ├── Room.vue
│ │ └── Search.vue
│ ├── index.d.ts
│ ├── main.ts
│ ├── plugins
│ │ └── ant-desigin.ts
│ ├── router
│ │ └── index.ts
│ ├── store
│ │ ├── index.ts
│ │ └── modules
│ │ │ ├── app
│ │ │ ├── actions.ts
│ │ │ ├── getters.ts
│ │ │ ├── index.ts
│ │ │ ├── mutation-types.ts
│ │ │ ├── mutations.ts
│ │ │ └── state.ts
│ │ │ └── chat
│ │ │ ├── actions.ts
│ │ │ ├── getters.ts
│ │ │ ├── index.ts
│ │ │ ├── mutation-types.ts
│ │ │ ├── mutations.ts
│ │ │ └── state.ts
│ ├── styles
│ │ ├── coverAntd.scss
│ │ ├── index.scss
│ │ └── theme.scss
│ ├── types
│ │ ├── chat.d.ts
│ │ ├── shims-tsx.d.ts
│ │ ├── shims-vue-expand.d.ts
│ │ ├── shims-vue.d.ts
│ │ └── user.d.ts
│ ├── utils
│ │ └── common.ts
│ └── views
│ │ └── Chat.vue
├── tsconfig.json
├── vue.config.js
└── yarn.lock
├── deploy.md
├── node_modules
└── .yarn-integrity
├── server
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── nest-cli.json
├── package-lock.json
├── package.json
├── public
│ ├── avatar
│ │ ├── avatar1.png
│ │ ├── avatar10.png
│ │ ├── avatar11.png
│ │ ├── avatar12.png
│ │ ├── avatar13.png
│ │ ├── avatar14.png
│ │ ├── avatar15.png
│ │ ├── avatar16.png
│ │ ├── avatar17.png
│ │ ├── avatar18.png
│ │ ├── avatar19.png
│ │ ├── avatar2.png
│ │ ├── avatar20.png
│ │ ├── avatar3.png
│ │ ├── avatar4.png
│ │ ├── avatar5.png
│ │ ├── avatar6.png
│ │ ├── avatar7.png
│ │ ├── avatar8.png
│ │ ├── avatar9.png
│ │ └── robot.png
│ └── static
│ │ ├── file
│ │ └── info.md
│ │ └── image
│ │ └── info.md
├── src
│ ├── app.module.ts
│ ├── common
│ │ ├── constant
│ │ │ ├── global.ts
│ │ │ └── rcode.ts
│ │ ├── filters
│ │ │ ├── http-exception.filter.ts
│ │ │ └── ws-exception.filter.ts
│ │ ├── guards
│ │ │ └── WsJwtGuard.ts
│ │ ├── interceptor
│ │ │ └── response.interceptor.ts
│ │ ├── middleware
│ │ │ ├── elasticsearch.ts
│ │ │ └── logger.middleware.ts
│ │ └── tool
│ │ │ └── utils.ts
│ ├── fix.ts
│ ├── main.ts
│ └── modules
│ │ ├── auth
│ │ ├── auth.controller.ts
│ │ ├── auth.module.ts
│ │ ├── auth.service.ts
│ │ ├── constants.ts
│ │ ├── jwt.strategy.ts
│ │ └── local.strategy.ts
│ │ ├── chat
│ │ ├── chat.gateway.ts
│ │ ├── chat.module.ts
│ │ └── index.d.ts
│ │ ├── friend
│ │ ├── entity
│ │ │ ├── friend.entity.ts
│ │ │ └── friendMessage.entity.ts
│ │ ├── friend.controller.ts
│ │ ├── friend.module.ts
│ │ └── friend.service.ts
│ │ ├── group
│ │ ├── entity
│ │ │ ├── group.entity.ts
│ │ │ └── groupMessage.entity.ts
│ │ ├── group.controller.ts
│ │ ├── group.module.ts
│ │ └── group.service.ts
│ │ └── user
│ │ ├── entity
│ │ └── user.entity.ts
│ │ ├── user.controller.ts
│ │ ├── user.module.ts
│ │ └── user.service.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
├── webSocket建立流程.md
└── yarn.lock
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.workingDirectories": [
3 | "./client",
4 | "./server"
5 | ]
6 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 BoBoooooo
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tyloo-Chat(仿wechat)
2 | [](https://github.com/BoBoooooo)
3 | [](https://github.com/BoBoooooo/tyloo-chat)
4 | [](http://nodejs.org/download)
5 | [](https://github.com/BoBoooooo/tyloo-chat/LICENSE)
6 | [](https://github.com/BoBoooooo/tyloo-chat)
7 |
8 | ## 线上访问地址暂时关闭
9 |
10 | ## 说明
11 |
12 | 本项目fork自[genal-chat]('https://github.com/genaller/genal-chat.git')做了优化升级,感谢大佬`Genal`开源提供思路!
13 |
14 | 目前还在抽空持续优化中,敬请期待!!!
15 |
16 | 觉得还不错的话可以点个Star鼓励一下!!!
17 |
18 | ## 🚀 Electron版本客户端已出炉,详见release
19 |
20 | ## 部分功能截图
21 | - 整体界面
22 |
23 | 
24 | - 通讯录
25 |
26 | 
27 |
28 | - 群聊功能(群成员列表,在线状态,支持添加群成员)
29 | 
30 | - 会话列表(置顶/删除)
31 |
32 | 
33 | - 消息撤回功能
34 |
35 | 
36 |
37 | ## Electron版本客户端(位于electron_version分支)
38 | - windows版本(exe)
39 |
40 | 
41 |
42 | - mac版本(dmg)
43 |
44 | 
45 | ## Feature
46 | - 用户登陆注册 (支持嵌入第三方系统单点登陆)
47 | - [单点登陆地址](http://server.boboooooo.top:9998),登录后点击右上角在线交流按钮
48 | - 群聊 (类似qq群)
49 | - 邀请好友加入群聊
50 | - 修改群名/群公告
51 | - 好友功能
52 | - 通讯录功能(支持接入第三方系统组织架构,直接发起聊天)
53 | - 聊天功能
54 | - Emoji表情包
55 | - 图片发送/图片预览
56 | - 发送附件
57 | - 消息分页
58 | - 消息撤回/复制
59 | - 自定义主题
60 | - 会话置顶/删除
61 | - 重连提醒
62 | - **智能助手(默认,位于main分支,采用ES搜索引擎需要手动创建ES词库)**
63 | - **第三方API机器人(当前线上demo版本,位于feature_APIROBOT分支)**
64 | - **Electron版本(位于electron_version分支,支持生成dmg,exe客户端)**
65 | ## 技术栈
66 | - **前端**
67 | - **vue cli 4.x**
68 | - **Antd for vue**
69 | - **后端**
70 | - **NestJS**
71 | - **TypeORM**
72 | - **Mysql**
73 | - **Socket.io**
74 |
75 | - **ElasticSearch** ES搜索引擎(用于机器人快捷自动回复)
76 | - **[Nodejieba](https://github.com/yanyiwu/nodejieba)** node版本中文分词器
77 | ## 数据库表结构设计
78 | 
79 |
80 | ## 环境准备
81 | - mysql
82 | - chat数据库 (需要手动创建,**注意数据库编码格式为 `utf8bm64``utf8bm64``utf8bm64` !!!**)
83 | - node v10.16.3
84 |
85 | ## 拉取代码时注意事项
86 |
87 | ```
88 | // windows系统需要配置一下,提交时转换为LF,检出时不转换
89 | git config --global core.autocrlf input
90 | ```
91 |
92 | ```
93 | // 设置为区分大小写
94 | git config core.ignorecase false
95 | ```
96 |
97 | ## 运行项目
98 | ```js
99 | // client
100 | cd client
101 | cnpm i
102 | npm start
103 | ```
104 |
105 | ```js
106 | // server
107 | cd server
108 | cnpm i
109 | npm run start
110 | ```
111 |
112 | ## 如何部署
113 |
114 | [Deploy](./deploy.md)
115 |
116 | [CentOS下部署聊天室](https://notes.zhangxiaocai.cn/posts/39142aea.html)
117 |
118 | ## 第三方集成/单点登陆
119 |
120 | - 第三方系统里嵌入如下跳转代码,需要携带`userId`以及`username`参数
121 |
122 | ``` javascript
123 | let chatUrl // 当前聊天室客户端地址
124 | let userId // 当前系统用户userId
125 | let username // 当前系统用户昵称
126 |
127 | window.open(`${chatUrl}?userId=${userId}&username=${username}`);
128 |
129 | ```
130 |
131 | - 聊天室获取参数并自动完成登陆(若为首次登陆会自动注册账号)
132 |
133 | - 设置聊天室client `VUE_APP_ORG_URL` 为获取第三方系统组织架构的接口地址
134 |
135 | - 设置VUE_APP_ORG_URL
136 | ``` javascript
137 |
138 | // .env.xxx
139 | // 此接口可以获取到第三方系统的所有部门和人员信息,注意为嵌套tree结构
140 | VUE_APP_ORG_URL=http://127.0.0.1:8080/api/getDeptUsersTree
141 |
142 | ```
143 |
144 | - 切换到联系人界面自动发出请求
145 | ``` javascript
146 | // Contact.vue
147 | axios.post(process.env.VUE_APP_ORG_URL).then((res) => {
148 | this.organizationArr = res.data.data;
149 | });
150 | ```
151 |
152 | - 返回值格式如下
153 | ``` javascript
154 | interface node {
155 | id: string; // id
156 | label: string;// 名称
157 | flag: boolean;// 是否有下级结点
158 | children: node[];// 下级结点
159 | }
160 | ```
161 | - 若不需要集成第三方组织架构清空`VUE_APP_ORG_URL`即可,其他情况自行定制修改。
162 |
163 | ## 思路概述
164 | [webSocket建立流程](./webSocket建立流程.md)
165 | ## TODO
166 | - `@功能实现`
167 | - `消息转发`
168 | - `代码性能优化`
169 | - `群聊功能继续完善`
170 | - `微信快捷登陆`
171 | - `Electron客户端检查更新`
172 |
173 | ## 交流群
174 |
175 |
176 |
177 |
--------------------------------------------------------------------------------
/assets/database.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/database.png
--------------------------------------------------------------------------------
/assets/demo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/demo1.png
--------------------------------------------------------------------------------
/assets/demo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/demo2.png
--------------------------------------------------------------------------------
/assets/demo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/demo3.png
--------------------------------------------------------------------------------
/assets/demo4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/demo4.png
--------------------------------------------------------------------------------
/assets/demo5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/demo5.png
--------------------------------------------------------------------------------
/assets/electron1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/electron1.png
--------------------------------------------------------------------------------
/assets/electron2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/assets/electron2.png
--------------------------------------------------------------------------------
/client/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/client/.env.dev:
--------------------------------------------------------------------------------
1 | #本地开发环境
2 | NODE_ENV=development
3 |
4 | #公共组件使用cdn加速
5 | VUE_APP_CDN=false
6 |
7 | #后台地址
8 | VUE_APP_API_URL=http://localhost:3000
9 |
10 | #第三方组织架构地址
11 | VUE_APP_ORG_URL=
--------------------------------------------------------------------------------
/client/.env.in:
--------------------------------------------------------------------------------
1 | #生产环境
2 | NODE_ENV=production
3 |
4 | #公共组件使用cdn加速
5 | VUE_APP_CDN=false
6 |
7 | #后台地址
8 | VUE_APP_API_URL=http://server.boboooooo.top:3000
9 |
10 | #第三方组织架构地址
11 | VUE_APP_ORG_URL=
--------------------------------------------------------------------------------
/client/.env.out:
--------------------------------------------------------------------------------
1 | #生产环境
2 | NODE_ENV=production
3 |
4 | #公共组件使用cdn加速
5 | VUE_APP_CDN=true
6 |
7 | #后台地址
8 | VUE_APP_API_URL=http://server.boboooooo.top:3000
9 |
10 | #第三方组织架构地址
11 | VUE_APP_ORG_URL=
--------------------------------------------------------------------------------
/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | '@vue/airbnb',
9 | '@vue/typescript',
10 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
11 | ],
12 | parserOptions: {
13 | parser: '@typescript-eslint/parser',
14 | ecmaFeatures: {
15 | legacyDecorators: true,
16 | },
17 | },
18 | rules: {
19 | // "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
21 | 'vue/no-unused-component': 'off',
22 | 'no-console': 'off',
23 | 'no-irregular-whitespace': 'off',
24 | 'prefer-spread': 0,
25 | 'no-plusplus': 0,
26 | 'max-len': 0,
27 | 'eslint-disable-next-line': 'off',
28 | // 允许class中方法不使用this
29 | 'class-methods-use-this': 'off',
30 | // 允许下划线变量命名
31 | 'no-underscore-dangle': 'off',
32 | // 不强制返回值
33 | 'consistent-return': 'off',
34 | camelcase: 'off',
35 | // 允许循环引入
36 | 'import/no-cycle': 'off',
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 | package-lock.json
--------------------------------------------------------------------------------
/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "printWidth": 140,
7 | "endOfLine": "auto",
8 | "arrowParens": "always"
9 | }
10 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # client
2 |
3 | ## Project setup
4 | ```
5 | yarn install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | yarn serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | yarn build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | yarn lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/client/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/app'],
3 | };
4 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tyloo-chat-client",
3 | "version": "2.1.7",
4 | "author": "boboooooo159@gmail.com",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "vue-cli-service serve --mode dev",
8 | "build": "vue-cli-service build --mode out",
9 | "lint": "vue-cli-service lint"
10 | },
11 | "dependencies": {
12 | "@types/js-cookie": "^2.2.6",
13 | "@types/socket.io-client": "^1.4.33",
14 | "ant-design-vue": "^1.6.2",
15 | "axios": "^0.19.2",
16 | "cnchar": "^2.2.7",
17 | "core-js": "^2.6.5",
18 | "js-cookie": "^2.2.1",
19 | "less-loader": "^6.1.2",
20 | "localforage": "^1.9.0",
21 | "moment": "^2.27.0",
22 | "sass": "^1.26.9",
23 | "sass-loader": "^8.0.2",
24 | "socket.io-client": "^2.3.0",
25 | "v-contextmenu": "^2.9.0",
26 | "v-viewer": "^1.5.1",
27 | "vue": "^2.6.11",
28 | "vue-class-component": "^7.2.3",
29 | "vue-property-decorator": "^8.4.2",
30 | "vue-router": "^3.2.0",
31 | "vue-runtime-helpers": "^1.1.2",
32 | "vuex": "^3.4.0",
33 | "vuex-class": "^0.3.2",
34 | "lodash": "^4.17.20"
35 | },
36 | "devDependencies": {
37 | "@types/lodash": "^4.14.165",
38 | "@typescript-eslint/eslint-plugin": "^2.26.0",
39 | "@typescript-eslint/parser": "^2.26.0",
40 | "@vue/cli-plugin-babel": "^3.9.2",
41 | "@vue/cli-plugin-eslint": "^3.9.2",
42 | "@vue/cli-plugin-router": "^4.4.0",
43 | "@vue/cli-plugin-typescript": "~4.3.0",
44 | "@vue/cli-plugin-vuex": "^4.4.0",
45 | "@vue/cli-service": "^3.9.2",
46 | "@vue/eslint-config-airbnb": "^4.0.1",
47 | "@vue/eslint-config-typescript": "^5.0.2",
48 | "babel-eslint": "^10.0.2",
49 | "compression-webpack-plugin": "^4.0.0",
50 | "eslint": "^6.0.1",
51 | "eslint-plugin-import": "^2.18.0",
52 | "eslint-config-prettier": "^8.1.0",
53 | "eslint-plugin-vue": "^5.2.3",
54 | "eslint-plugin-prettier": "^3.3.1",
55 | "typescript": "~3.8.3",
56 | "vue-template-compiler": "^2.6.11",
57 | "webpack": "^4.5.0",
58 | "webpack-bundle-analyzer": "^3.8.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | TylooChat聊天室
10 |
11 |
12 |
15 |
16 | <% if (process.env.VUE_APP_CDN === 'true') { %>
17 | <% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
18 |
19 | <% } %>
20 | <% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
21 |
22 | <% } %>
23 | <% } %>
24 |
25 |
26 |
--------------------------------------------------------------------------------
/client/public/mime/doc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/doc.png
--------------------------------------------------------------------------------
/client/public/mime/docx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/docx.png
--------------------------------------------------------------------------------
/client/public/mime/exe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/exe.png
--------------------------------------------------------------------------------
/client/public/mime/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/img.png
--------------------------------------------------------------------------------
/client/public/mime/other.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/other.png
--------------------------------------------------------------------------------
/client/public/mime/pdf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/pdf.png
--------------------------------------------------------------------------------
/client/public/mime/ppt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/ppt.png
--------------------------------------------------------------------------------
/client/public/mime/rar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/rar.png
--------------------------------------------------------------------------------
/client/public/mime/txt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/txt.png
--------------------------------------------------------------------------------
/client/public/mime/xls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/xls.png
--------------------------------------------------------------------------------
/client/public/mime/xlsx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/xlsx.png
--------------------------------------------------------------------------------
/client/public/mime/zip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/public/mime/zip.png
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
![]()
7 |
8 |
9 |
42 |
65 |
--------------------------------------------------------------------------------
/client/src/api/apis/index.ts:
--------------------------------------------------------------------------------
1 | export * from './modules/group';
2 | export * from './modules/user';
3 | export * from './modules/friend';
4 |
--------------------------------------------------------------------------------
/client/src/api/apis/modules/friend.ts:
--------------------------------------------------------------------------------
1 | import axios from '@/api/axios';
2 |
3 | /**
4 | * 群分页消息
5 | * @param params
6 | */
7 | // eslint-disable-next-line import/prefer-default-export
8 | export async function getFriendMessage(params: PagingParams) {
9 | // eslint-disable-next-line no-return-await
10 | return await axios.get('/friend/friendMessages', {
11 | params,
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/api/apis/modules/group.ts:
--------------------------------------------------------------------------------
1 | import axios from '@/api/axios';
2 |
3 | /**
4 | * 群名模糊搜索用户
5 | * @param string
6 | */
7 | export function getGroupsByName(groupName: string) {
8 | return axios.get(`/group/findByName?groupName=${groupName}`);
9 | }
10 |
11 | /**
12 | * 群分页消息
13 | * @param params
14 | */
15 | export async function getGroupMessages(params: PagingParams) {
16 | // eslint-disable-next-line no-return-await
17 | return await axios.get('/group/groupMessages', {
18 | params,
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/api/apis/modules/user.ts:
--------------------------------------------------------------------------------
1 | import axios from '@/api/axios';
2 |
3 | /**
4 | * 更新用户名
5 | * @param username
6 | */
7 | export const patchUserName = (username: string) => axios.patch(`/user/username?username=${username}`);
8 |
9 | /**
10 | * 更新用户密码
11 | * @param password
12 | *
13 | */
14 | export const patchPassword = (password: string) => axios.patch(`/user/password?password=${password}`);
15 |
16 | /**
17 | * 用户名模糊搜索用户
18 | * @param username
19 | */
20 | export function getUsersByName(username: string) {
21 | return axios.get(`/user/findByName?username=${username}`);
22 | }
23 |
24 | /**
25 | * 用户头像上传
26 | * @param params
27 | */
28 | export function setUserAvatar(params: FormData) {
29 | return axios.post('/user/avatar', params, {
30 | headers: {
31 | 'Content-Type': 'multipart/form-data',
32 | },
33 | });
34 | }
35 |
36 | /**
37 | * 删除用户
38 | * @param params
39 | */
40 | export function deleteUser(params: any) {
41 | return axios.delete('/user', { params });
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/api/axios/index.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance } from 'axios';
2 | import { requestSuccess, requestFail, responseSuccess, responseFail } from './interceptors';
3 |
4 | const fetch: AxiosInstance = axios.create({
5 | timeout: 60000, // 超时时间一分钟
6 | baseURL: process.env.VUE_APP_API_URL,
7 | headers: {
8 | 'Cache-Control': 'no-cache',
9 | Pragma: 'no-cache',
10 | },
11 | // 不携带cookie
12 | withCredentials: false,
13 | });
14 |
15 | fetch.interceptors.request.use(requestSuccess, requestFail);
16 | fetch.interceptors.response.use(responseSuccess, responseFail);
17 |
18 | export default fetch;
19 |
--------------------------------------------------------------------------------
/client/src/api/axios/interceptors.ts:
--------------------------------------------------------------------------------
1 | import { AxiosRequestConfig, AxiosResponse } from 'axios';
2 | import cookie from 'js-cookie';
3 | import Vue from 'vue';
4 | import store from '@/store/index';
5 | import { CLEAR_USER, SET_LOADING } from '../../store/modules/app/mutation-types';
6 |
7 | // 请求拦截器
8 | export const requestSuccess = (request: AxiosRequestConfig) => {
9 | const token = cookie.get('token');
10 | // eslint-disable-next-line no-param-reassign
11 | request.headers.token = token;
12 | return request;
13 | };
14 |
15 | export const requestFail = (error: any) => {
16 | store.commit(`app/${SET_LOADING}`, false);
17 | Vue.prototype.$message.error(error.message);
18 | return Promise.reject(error);
19 | };
20 |
21 | // 接收拦截器
22 | export const responseSuccess = (response: AxiosResponse) => {
23 | // token过期时清空登录态重新登录
24 | const { data } = response;
25 | if (data.code === 401) {
26 | Vue.prototype.$message.error(data.msg);
27 | store.commit(`app/${CLEAR_USER}`);
28 | setTimeout(() => {
29 | window.location.reload();
30 | }, 2000);
31 | }
32 | return response;
33 | };
34 |
35 | export const responseFail = (error: any) => {
36 | store.commit(`app/${SET_LOADING}`, false);
37 | Vue.prototype.$message.error(error.message);
38 | return Promise.reject(error);
39 | };
40 |
--------------------------------------------------------------------------------
/client/src/assets/friend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/src/assets/friend.png
--------------------------------------------------------------------------------
/client/src/assets/group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/src/assets/group.png
--------------------------------------------------------------------------------
/client/src/assets/send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/client/src/assets/send.png
--------------------------------------------------------------------------------
/client/src/common/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 默认群组,需结合后台 (用户新建后默认进入该群组)
3 | * @copyright: BoBo
4 | * @author: BoBo
5 | * @Date: 2020年11月05 16:40:11
6 | */
7 |
8 | // 默认群组Id
9 | let bg = '';
10 | if (process.env.VUE_APP_CDN !== 'true' && process.env.NODE_ENV === 'production') {
11 | // 默认背景图片
12 | bg = 'http://11.176.37.20:8090/img/secret.jpg';
13 | } else {
14 | bg = 'https://pic.downk.cc/item/5fc744ea394ac5237897a81d.jpg';
15 | }
16 |
17 | export const DEFAULT_GROUP = 'group';
18 |
19 | export const DEFAULT_BACKGROUND = bg;
20 | // 默认机器人Id
21 | export const DEFAULT_ROBOT = 'robot';
22 |
23 | // 图片/附件请求路径
24 | export const IMAGE_SAVE_PATH = '/static/image/';
25 | export const FILE_SAVE_PATH = '/static/file/';
26 |
27 | // MIME类型
28 | export const MIME_TYPE = ['xls', 'xlsx', 'doc', 'docx', 'exe', 'pdf', 'ppt', 'txt', 'zip', 'img', 'rar'];
29 | // 图片类型
30 | export const IMAGE_TYPE = ['png', 'jpg', 'jpeg', 'gif'];
31 |
--------------------------------------------------------------------------------
/client/src/common/theme.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 背景图片
3 | * @author: BoBo
4 | * @copyright: BoBo
5 | * @Date: 2020-12-02 15:33:37
6 | */
7 | export default [
8 | {
9 | url: 'https://pic.downk.cc/item/5fc7432a394ac52378970d9e.jpg',
10 | name: '壁纸一',
11 | },
12 | {
13 | url: 'https://pic.downk.cc/item/5fc7438c394ac5237897328d.jpg',
14 | name: '壁纸二',
15 | },
16 | {
17 | url: 'https://pic.downk.cc/item/5fc744ca394ac52378979ca1.jpg',
18 | name: '壁纸三',
19 | },
20 | {
21 | url: 'https://pic.downk.cc/item/5fc744d6394ac5237897a1f4.jpg',
22 | name: '壁纸四',
23 | },
24 | {
25 | url: 'https://pic.downk.cc/item/5fc744e1394ac5237897a560.jpg',
26 | name: '壁纸五',
27 | },
28 | {
29 | url: 'https://pic.downk.cc/item/5fc744ea394ac5237897a81d.jpg',
30 | name: '壁纸六',
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/client/src/components/Avatar.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ (userGather[data.userId] && userGather[data.userId].username) || data.username }}
14 |
19 |
20 |
27 | 删除用户
28 |
29 | 发消息
30 | 添加好友
31 |
32 |
33 |
39 |
40 |
47 |
{{ (userGather[data.userId] && userGather[data.userId].username) || data.username }}
48 |
49 | {{ _formatTime(data.time) }}
50 |
51 |
52 |
53 |
54 |
116 |
167 |
--------------------------------------------------------------------------------
/client/src/components/Contact.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
35 |
36 |
37 |
38 |
39 |
158 |
159 |
205 |
--------------------------------------------------------------------------------
/client/src/components/ContactModal.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
19 |
20 | {
31 | onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
32 | }
33 | "
34 | @select="
35 | (_, props) => {
36 | onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
37 | }
38 | "
39 | >
40 |
41 |
42 |
43 |
44 | 添加
45 | 取消
46 |
47 |
48 |
49 |
50 |
51 |
179 |
185 |
--------------------------------------------------------------------------------
/client/src/components/Emoji.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 | 😃
11 | 😁
12 | 😂
13 | 😄
14 | 😅
15 | 😆
16 | 😇
17 | 😈
18 | 😉
19 |
20 |
21 | 😊
22 | 😋
23 | 😌
24 | 😍
25 | 😎
26 | 😏
27 | 😐
28 | 😒
29 | 😓
30 |
31 |
32 | ❓
33 | 😕
34 | 😖
35 | 😗
36 | 😘
37 | 😙
38 | 😚
39 | 😜
40 | 😝
41 |
42 |
43 | 😞
44 | 😟
45 | 😠
46 | 😡
47 | 😢
48 | 😣
49 | 😤
50 | 😥
51 | 😦
52 |
53 |
54 | 😨
55 | 😩
56 | 😪
57 | 😫
58 | 😬
59 | 😭
60 | 😮
61 | 😯
62 | 😰
63 |
64 |
65 | 😲
66 | 😳
67 | 😴
68 | 😵
69 | 🧐
70 | 😷
71 | 🙁
72 | 🙂
73 | 🙃
74 |
75 |
76 | 🤐
77 | 🤑
78 | 🤒
79 | 🤓
80 | 🤔
81 | 🤕
82 | 🤠
83 | 🤡
84 | 🤢
85 |
86 |
87 | 🤤
88 | 🤥
89 | 🤧
90 | 🤨
91 | 🤩
92 | 🤪
93 | 🤫
94 | 🤬
95 | 🤭
96 |
97 |
98 |
99 |
100 |
110 |
133 |
--------------------------------------------------------------------------------
/client/src/components/Entry.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
54 |
55 |
56 |
313 |
388 |
--------------------------------------------------------------------------------
/client/src/components/Login.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
39 | 记住密码
40 |
41 |
42 | {{ buttonText }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
104 |
115 |
--------------------------------------------------------------------------------
/client/src/components/Nav.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
107 |
108 |
109 |
255 |
463 |
--------------------------------------------------------------------------------
/client/src/components/Panel.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
17 |
25 |
26 |
27 |
群名
28 |
29 |
{{ activeRoom.groupName }}
30 |
31 |
32 |
群公告
33 |
34 |
{{ activeRoom.notice }}
35 |
36 |
37 |
38 |
群人数: ({{ activeNum }}/{{ groupUsers.length }})
39 |
43 |
44 |
45 |
46 |
47 | {{ data.username }}
48 |
49 |
50 |
51 |
52 |
退出群聊
53 |
54 |
55 |
56 |
57 |
62 |
63 |
(showGroupNoticeDialog = false)">
64 |
65 |
66 | {{ activeRoom.notice }}
67 |
68 |
69 | 修改
70 | (showGroupNoticeDialog = false)">关闭
71 |
72 |
73 |
74 |
(showGroupNameDialog = false)">
75 |
76 |
77 | {{ activeRoom.groupName }}
78 |
79 |
80 | 修改
81 | (showGroupNameDialog = false)">关闭
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
217 |
246 |
--------------------------------------------------------------------------------
/client/src/components/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 | {{ chat.username }}
17 | {{ chat.groupName }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | (visibleAddGroup = !visibleAddGroup)">创建群
26 |
27 |
28 | (visibleJoinGroup = !visibleJoinGroup)">搜索群
29 |
30 |
31 | (visibleAddFriend = !visibleAddFriend)">搜索用户
32 |
33 |
34 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
45 |
56 |
57 | {{ group.groupName }}
58 |
59 |
60 |
加入群
61 |
62 |
63 |
64 |
65 |
76 |
77 | {{ user.username }}
78 |
79 |
80 |
添加好友
81 |
82 |
83 |
84 |
85 |
86 |
229 |
262 |
--------------------------------------------------------------------------------
/client/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'ant-design-vue/lib/locale-provider/zh_CN';
2 |
--------------------------------------------------------------------------------
/client/src/main.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: Main js 入口文件
3 | * @copyright: BoBo
4 | * @author: BoBo
5 | * @Date: 2020年11月05 16:40:11
6 | */
7 |
8 | import Vue from 'vue';
9 | import Viewer from 'v-viewer'; // 图片预览插件
10 | import moment from 'moment'; // 引入moment
11 | import contentmenu from 'v-contextmenu';
12 | import lodash from 'lodash';
13 | import localforage from 'localforage';
14 | import App from './App.vue';
15 | import router from './router';
16 | import store from './store';
17 | // eslint-disable-next-line import/no-extraneous-dependencies
18 | import 'viewerjs/dist/viewer.css';
19 | import './plugins/ant-desigin'; // 引入ant-desigin
20 | import 'v-contextmenu/dist/index.css';
21 |
22 | Vue.use(contentmenu);
23 | Vue.config.productionTip = false;
24 | // 使用中文时间
25 | Vue.prototype.$moment = moment;
26 | // lodash
27 | Vue.prototype.$lodash = lodash;
28 |
29 | // localforage https://localforage.docschina.org/
30 | // 基于IndexedDB二次封装
31 | Vue.prototype.$localforage = localforage;
32 | Vue.use(Viewer, {
33 | defaultOptions: {
34 | navbar: false,
35 | title: false,
36 | toolbar: {
37 | zoomIn: 1,
38 | zoomOut: 1,
39 | oneToOne: 4,
40 | reset: 4,
41 | prev: 0,
42 | next: 0,
43 | rotateLeft: 4,
44 | rotateRight: 4,
45 | flipHorizontal: 4,
46 | flipVertical: 4,
47 | },
48 | },
49 | });
50 |
51 | new Vue({
52 | router,
53 | store,
54 | render: (h) => h(App),
55 | }).$mount('#app');
56 |
--------------------------------------------------------------------------------
/client/src/plugins/ant-desigin.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 按需引入antd
3 | * @copyright: BoBo
4 | * @author: BoBo
5 | * @Date: 2020年11月05 16:40:11
6 | */
7 | import Vue from 'vue';
8 | import 'ant-design-vue/dist/antd.less';
9 | import {
10 | message,
11 | Button,
12 | Input,
13 | Modal,
14 | Form,
15 | Checkbox,
16 | Icon,
17 | Tabs,
18 | Popover,
19 | Dropdown,
20 | Menu,
21 | Avatar,
22 | Card,
23 | Select,
24 | Upload,
25 | Tooltip,
26 | Drawer,
27 | Popconfirm,
28 | Badge,
29 | Tree,
30 | Collapse,
31 | Transfer,
32 | ConfigProvider,
33 | Alert,
34 | } from 'ant-design-vue';
35 |
36 | Vue.use(Avatar);
37 | Vue.use(Button);
38 | Vue.use(Input);
39 | Vue.use(Modal);
40 | Vue.use(Form);
41 | Vue.use(Checkbox);
42 | Vue.use(Icon);
43 | Vue.use(Tabs);
44 | Vue.use(Popover);
45 | Vue.use(Dropdown);
46 | Vue.use(Menu);
47 | Vue.use(Card);
48 | Vue.use(Select);
49 | Vue.use(Upload);
50 | Vue.use(Tooltip);
51 | Vue.use(Drawer);
52 | Vue.use(Popconfirm);
53 | Vue.use(Badge);
54 | Vue.use(Tree);
55 | Vue.use(Collapse);
56 | Vue.use(Transfer);
57 | Vue.use(ConfigProvider);
58 | Vue.use(Alert);
59 | Vue.prototype.$message = message;
60 |
--------------------------------------------------------------------------------
/client/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter, { RouteConfig } from 'vue-router';
3 |
4 | Vue.use(VueRouter);
5 |
6 | const routes: Array = [
7 | {
8 | path: '/',
9 | name: 'Chat',
10 | component: () => import('@/views/Chat.vue'),
11 | },
12 | ];
13 |
14 | const router = new VueRouter({
15 | mode: 'history',
16 | base: process.env.BASE_URL,
17 | routes,
18 | });
19 |
20 | export default router;
21 |
--------------------------------------------------------------------------------
/client/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex, { ModuleTree } from 'vuex';
3 | // app
4 | import app from './modules/app';
5 | import { AppState } from './modules/app/state';
6 | // chat
7 | import chat from './modules/chat';
8 | import { ChatState } from './modules/chat/state';
9 |
10 | export type RootState = {
11 | app: AppState;
12 | chat: ChatState;
13 | };
14 | Vue.use(Vuex);
15 |
16 | const modules: ModuleTree = {
17 | app,
18 | chat,
19 | };
20 |
21 | export default new Vuex.Store({
22 | modules,
23 | });
24 |
--------------------------------------------------------------------------------
/client/src/store/modules/app/actions.ts:
--------------------------------------------------------------------------------
1 | import { ActionTree } from 'vuex';
2 | import axios from '@/api/axios';
3 | import { processReturn } from '@/utils/common';
4 | import { SET_USER, SET_TOKEN, SET_LOADING } from './mutation-types';
5 | import { AppState } from './state';
6 | import { RootState } from '../../index';
7 |
8 | const actions: ActionTree = {
9 | async register({ commit }, payload) {
10 | commit(SET_LOADING, true);
11 | const res = await axios.post('/auth/register', {
12 | ...payload,
13 | });
14 | const data = processReturn(res);
15 | commit(SET_LOADING, false);
16 | if (data) {
17 | commit(SET_USER, data.user);
18 | commit(SET_TOKEN, data.token);
19 | return data;
20 | }
21 | },
22 | async login({ commit }, payload) {
23 | commit(SET_LOADING, true);
24 | const res = await axios.post('/auth/login', {
25 | ...payload,
26 | });
27 | const data = processReturn(res);
28 | commit(SET_LOADING, false);
29 | if (data) {
30 | commit(SET_USER, data.user);
31 | commit(SET_TOKEN, data.token);
32 | return data;
33 | }
34 | },
35 | };
36 |
37 | export default actions;
38 |
--------------------------------------------------------------------------------
/client/src/store/modules/app/getters.ts:
--------------------------------------------------------------------------------
1 | import { GetterTree } from 'vuex';
2 | import cookie from 'js-cookie';
3 | import { AppState } from './state';
4 | import { RootState } from '../../index';
5 |
6 | const getters: GetterTree = {
7 | user(state) {
8 | // eslint-disable-next-line no-unused-expressions
9 | state.user;
10 | const user = cookie.get('user');
11 | if (!user) {
12 | return {};
13 | }
14 | state.user = JSON.parse(user);
15 | return state.user;
16 | },
17 | mobile(state) {
18 | return state.mobile;
19 | },
20 | background(state) {
21 | // eslint-disable-next-line no-unused-expressions
22 | state.background;
23 | return localStorage.getItem('background');
24 | },
25 | activeTabName(state) {
26 | return state.activeTabName;
27 | },
28 | token(state) {
29 | return state.token;
30 | },
31 | apiUrl(state) {
32 | return state.apiUrl;
33 | },
34 | loading(state) {
35 | return state.loading;
36 | },
37 | };
38 |
39 | export default getters;
40 |
--------------------------------------------------------------------------------
/client/src/store/modules/app/index.ts:
--------------------------------------------------------------------------------
1 | import { Module } from 'vuex';
2 | import actions from './actions';
3 | import mutations from './mutations';
4 | import getters from './getters';
5 | import state, { AppState } from './state';
6 | import { RootState } from '../../index';
7 |
8 | const app: Module = {
9 | namespaced: true,
10 | state,
11 | mutations,
12 | actions,
13 | getters,
14 | };
15 |
16 | export default app;
17 |
--------------------------------------------------------------------------------
/client/src/store/modules/app/mutation-types.ts:
--------------------------------------------------------------------------------
1 | export const SET_USER = 'set_user';
2 | export const CLEAR_USER = 'clear_user';
3 | export const SET_TOKEN = 'set_token';
4 | export const SET_MOBILE = 'set_mobile';
5 | export const SET_BACKGROUND = 'set_background';
6 | export const SET_ACTIVETABNAME = 'set_activeTabName';
7 | export const SET_LOADING = 'set_loading';
8 |
--------------------------------------------------------------------------------
/client/src/store/modules/app/mutations.ts:
--------------------------------------------------------------------------------
1 | import cookie from 'js-cookie';
2 | import { MutationTree } from 'vuex';
3 | import { SET_USER, CLEAR_USER, SET_TOKEN, SET_MOBILE, SET_BACKGROUND, SET_ACTIVETABNAME, SET_LOADING } from './mutation-types';
4 | import { AppState } from './state';
5 |
6 | const mutations: MutationTree = {
7 | [SET_USER](state, payload: User) {
8 | state.user = payload;
9 | // 数据持久化
10 | cookie.set('user', payload, { expires: 3650 });
11 | },
12 |
13 | [CLEAR_USER](state) {
14 | state.user = {
15 | userId: '',
16 | username: '',
17 | password: '',
18 | avatar: '',
19 | createTime: 0,
20 | };
21 | cookie.set('user', '');
22 | cookie.set('token', '');
23 | },
24 |
25 | [SET_TOKEN](state, payload) {
26 | state.token = payload;
27 | cookie.set('token', payload, { expires: 3 });
28 | },
29 |
30 | [SET_MOBILE](state, payload: boolean) {
31 | state.mobile = payload;
32 | },
33 |
34 | [SET_BACKGROUND](state, payload: string) {
35 | state.background = payload;
36 | localStorage.setItem('background', payload);
37 | },
38 | [SET_ACTIVETABNAME](state, payload: 'message' | 'contacts') {
39 | state.activeTabName = payload;
40 | },
41 | [SET_LOADING](state, payload: boolean) {
42 | state.loading = payload;
43 | },
44 | };
45 |
46 | export default mutations;
47 |
--------------------------------------------------------------------------------
/client/src/store/modules/app/state.ts:
--------------------------------------------------------------------------------
1 | import cookie from 'js-cookie';
2 |
3 | export interface AppState {
4 | user: User;
5 | token: string;
6 | mobile: boolean;
7 | background: string;
8 | activeTabName: 'message' | 'contacts';
9 | apiUrl: string;
10 | loading: boolean;
11 | }
12 |
13 | const appState: AppState = {
14 | user: {
15 | userId: '',
16 | username: '',
17 | password: '',
18 | avatar: '',
19 | createTime: 0,
20 | },
21 | token: cookie.get('token') as string,
22 | mobile: false,
23 | background: '',
24 | activeTabName: 'message',
25 | apiUrl: process.env.VUE_APP_API_URL, // 后台api地址
26 | loading: false, // 全局Loading状态
27 | };
28 |
29 | export default appState;
30 |
--------------------------------------------------------------------------------
/client/src/store/modules/chat/actions.ts:
--------------------------------------------------------------------------------
1 | import { ActionTree } from 'vuex';
2 | import io from 'socket.io-client';
3 | import Vue from 'vue';
4 | import localforage from 'localforage';
5 | import { DEFAULT_GROUP } from '@/common/index';
6 | import { SET_LOADING, CLEAR_USER } from '../app/mutation-types';
7 | import { ChatState } from './state';
8 | import { RootState } from '../../index';
9 | import {
10 | SET_SOCKET,
11 | SET_DROPPED,
12 | ADD_GROUP_MESSAGE,
13 | ADD_FRIEND_MESSAGE,
14 | SET_GROUP_GATHER,
15 | SET_FRIEND_GATHER,
16 | SET_USER_GATHER,
17 | SET_ACTIVE_ROOM,
18 | DEL_GROUP,
19 | DEL_GROUP_MEMBER,
20 | DEL_FRIEND,
21 | ADD_UNREAD_GATHER,
22 | REVOKE_MESSAGE,
23 | USER_ONLINE,
24 | USER_OFFLINE,
25 | ADD_GROUP_MEMBER,
26 | UPDATE_USER_INFO,
27 | } from './mutation-types';
28 |
29 | const actions: ActionTree = {
30 | // 初始化socket连接和监听socket事件
31 | async connectSocket({ commit, state, dispatch, rootState }) {
32 | const { user, token } = rootState.app;
33 | const socket: SocketIOClient.Socket = io.connect(`ws://${process.env.VUE_APP_API_URL.split('http://')[1]}`, {
34 | reconnection: true,
35 | query: {
36 | token,
37 | userId: user.userId,
38 | },
39 | });
40 | // token校验,失败则要求重新登录
41 | socket.on('unauthorized', (msg: string) => {
42 | Vue.prototype.$message.error(msg);
43 | // 清空token,socket
44 | commit(`app/${CLEAR_USER}`, {}, { root: true });
45 | setTimeout(() => {
46 | window.location.reload();
47 | }, 1000);
48 | });
49 |
50 | socket.on('connect', async () => {
51 | console.log('连接成功');
52 | // 获取聊天室所需所有信息
53 | socket.emit('chatData', token);
54 |
55 | // 先保存好socket对象
56 | commit(SET_SOCKET, socket);
57 | });
58 | // 用户上线
59 | socket.on('userOnline', (data: any) => {
60 | console.log('userOnline', data);
61 | commit(USER_ONLINE, data.data);
62 | });
63 |
64 | // 用户下线
65 | socket.on('userOffline', (data: any) => {
66 | console.log('userOffline', data);
67 | commit(USER_OFFLINE, data.data);
68 | });
69 |
70 | // 新建群组
71 | socket.on('addGroup', (res: ServerRes) => {
72 | console.log('on addGroup', res);
73 | if (res.code) {
74 | return Vue.prototype.$message.error(res.msg);
75 | }
76 | Vue.prototype.$message.success(res.msg);
77 | commit(SET_GROUP_GATHER, res.data);
78 | commit(`app/${SET_LOADING}`, false, { root: true });
79 | });
80 |
81 | // 加入群组
82 | socket.on('joinGroup', async (res: ServerRes) => {
83 | if (res.code) {
84 | return Vue.prototype.$message.error(res.msg);
85 | }
86 | console.log('on joinGroup', res);
87 | const { invited, group, userId } = res.data;
88 |
89 | // 此处区分是搜索群加入群聊还是被邀请加入群聊
90 | if (invited) {
91 | // 被邀请的用户Id
92 | const { friendIds } = res.data;
93 | // 当前用户被邀请加入群,则加入群
94 | if (friendIds.includes(user.userId) && !state.groupGather[group.groupId]) {
95 | // commit(SET_GROUP_GATHER, group);
96 | // 获取群里面所有用户的用户信息
97 | socket.emit('chatData', token);
98 | } else if (userId === user.userId) {
99 | // 邀请发起者
100 | commit(ADD_GROUP_MEMBER, {
101 | groupId: group.groupId,
102 | members: Object.values(state.friendGather).filter((friend) => friendIds.includes(friend.userId)),
103 | });
104 | const groupGather2 = state.groupGather;
105 | // ?? 待优化
106 | commit(SET_ACTIVE_ROOM, groupGather2[group.groupId]);
107 | return Vue.prototype.$message.info(res.msg);
108 | }
109 | } else {
110 | const newUser = res.data.user as Friend;
111 | newUser.online = 1;
112 | // 新用户加入群
113 | if (newUser.userId !== rootState.app.user.userId) {
114 | commit(ADD_GROUP_MEMBER, {
115 | groupId: group.groupId,
116 | members: [newUser],
117 | });
118 | return Vue.prototype.$message.info(`${newUser.username}加入群${group.groupName}`);
119 | }
120 | // 是用户自己 则加入到某个群
121 | if (!state.groupGather[group.groupId]) {
122 | commit(SET_GROUP_GATHER, group);
123 | // 获取群里面所有用户的用户信息
124 | socket.emit('chatData', token);
125 | }
126 | Vue.prototype.$message.info(`成功加入群${group.groupName}`);
127 | commit(SET_ACTIVE_ROOM, state.groupGather[group.groupId]);
128 | commit(`app/${SET_LOADING}`, false, { root: true });
129 | }
130 | });
131 | //
132 | socket.on('joinGroupSocket', (res: ServerRes) => {
133 | console.log('on joinGroupSocket', res);
134 | if (res.code) {
135 | return Vue.prototype.$message.error(res.msg);
136 | }
137 | const newUser: Friend = res.data.user;
138 | newUser.online = 1;
139 | const { group } = res.data;
140 | const groupObj = state.groupGather[group.groupId];
141 | // 新用户注册后默认进入到DEFAULT_GROUP,此处需要判断一下是否在群内,不在群内的话需要加入本群中
142 | // 否则在线的用户无法收到新成员进群的变更
143 | if (!groupObj.members!.find((member) => member.userId === newUser.userId)) {
144 | newUser.isManager = 0;
145 | groupObj.members!.push(newUser);
146 | Vue.prototype.$message.info(res.msg);
147 | }
148 | commit(SET_USER_GATHER, newUser);
149 | });
150 |
151 | socket.on('groupMessage', (res: ServerRes) => {
152 | console.log('on groupMessage', res);
153 | if (!res.code) {
154 | commit(ADD_GROUP_MESSAGE, res.data);
155 | const { activeRoom } = state;
156 | if (activeRoom && activeRoom.groupId !== res.data.groupId) {
157 | commit(ADD_UNREAD_GATHER, res.data.groupId);
158 | }
159 | } else {
160 | Vue.prototype.$message.error(res.msg);
161 | }
162 | });
163 |
164 | socket.on('addFriend', (res: ServerRes) => {
165 | console.log('on addFriend', res);
166 | if (!res.code) {
167 | commit(SET_FRIEND_GATHER, res.data);
168 | commit(SET_USER_GATHER, res.data);
169 | // 取消loading
170 | Vue.prototype.$message.info(res.msg);
171 | socket.emit('joinFriendSocket', {
172 | userId: user.userId,
173 | friendId: res.data.userId,
174 | });
175 | } else {
176 | Vue.prototype.$message.error(res.msg);
177 | }
178 | commit(`app/${SET_LOADING}`, false, { root: true });
179 | });
180 |
181 | socket.on('joinFriendSocket', (res: ServerRes) => {
182 | console.log('on joinFriendSocket', res);
183 | // 添加好友之后默认进入好友聊天房间,初始化时不默认选中该好友房间
184 | if (!state.activeRoom) {
185 | commit(SET_ACTIVE_ROOM, state.friendGather[res.data.friendId]);
186 | }
187 | if (!res.code) {
188 | console.log('成功加入私聊房间');
189 | }
190 | });
191 |
192 | socket.on('friendMessage', async (res: ServerRes) => {
193 | console.log('on friendMessage', res);
194 | if (!res.code) {
195 | if (res.data.friendId === user.userId || res.data.userId === user.userId) {
196 | console.log('ADD_FRIEND_MESSAGE', res.data);
197 | commit(ADD_FRIEND_MESSAGE, res.data);
198 | // 新增私聊信息需要检测本地是否已删除聊天,如已删除需要恢复
199 | let deletedChat = (await localforage.getItem(`${user.userId}-deletedChatId`)) as string[];
200 | if (deletedChat) {
201 | if (res.data.friendId === user.userId) {
202 | deletedChat = deletedChat.filter((id) => id !== res.data.userId);
203 | } else {
204 | deletedChat = deletedChat.filter((id) => id !== res.data.friendId);
205 | }
206 | await localforage.setItem(`${user.userId}-deletedChatId`, deletedChat);
207 | }
208 | const { activeRoom } = state;
209 | if (activeRoom && activeRoom.userId !== res.data.userId && activeRoom.userId !== res.data.friendId) {
210 | commit(ADD_UNREAD_GATHER, res.data.userId);
211 | }
212 | }
213 | } else {
214 | Vue.prototype.$message.error(res.msg);
215 | }
216 | });
217 |
218 | socket.on('chatData', (res: ServerRes) => {
219 | if (res.code) {
220 | return Vue.prototype.$message.error(res.msg);
221 | }
222 | console.log(res);
223 | dispatch('handleChatData', res.data);
224 | commit(SET_DROPPED, false);
225 | });
226 |
227 | // 退出群组
228 | socket.on('exitGroup', (res: ServerRes) => {
229 | if (!res.code) {
230 | // 如果是当前用户退群,则删除群聊
231 | if (res.data.userId === user.userId) {
232 | commit(DEL_GROUP, res.data);
233 | commit(SET_ACTIVE_ROOM, state.groupGather[DEFAULT_GROUP]);
234 | Vue.prototype.$message.success(res.msg);
235 | } else {
236 | console.log(`--用户--${res.data.userId}`, '--退出群--', res.data.groupId);
237 | // 广播给其他用户,从群成员中删除该成员
238 | commit(DEL_GROUP_MEMBER, res.data);
239 | }
240 | } else if (res.data.userId === user.userId) {
241 | Vue.prototype.$message.error(res.msg);
242 | }
243 | });
244 |
245 | // 更新群信息
246 | socket.on('updateGroupInfo', (res: ServerRes) => {
247 | if (!res.code) {
248 | const group = state.groupGather[res.data.groupId];
249 | if (group) {
250 | group.groupName = res.data.groupName;
251 | group.notice = res.data.notice;
252 | if (state.activeRoom!.groupId) {
253 | state.activeRoom!.groupName = res.data.groupName;
254 | state.activeRoom!.notice = res.data.notice;
255 | }
256 | if (res.data.userId === user.userId) {
257 | Vue.prototype.$message.success(res.msg);
258 | }
259 | }
260 | }
261 | });
262 |
263 | // 更新好友信息
264 | socket.on('updateUserInfo', (res: ServerRes) => {
265 | if (!res.code) {
266 | commit(UPDATE_USER_INFO, res.data);
267 | }
268 | });
269 | // 删除好友
270 | socket.on('exitFriend', (res: ServerRes) => {
271 | if (!res.code) {
272 | commit(DEL_FRIEND, res.data);
273 | commit(SET_ACTIVE_ROOM, state.groupGather[DEFAULT_GROUP]);
274 | Vue.prototype.$message.success(res.msg);
275 | } else {
276 | Vue.prototype.$message.error(res.msg);
277 | }
278 | });
279 |
280 | // 消息撤回
281 | socket.on('revokeMessage', (res: ServerRes) => {
282 | if (!res.code) {
283 | commit(REVOKE_MESSAGE, res.data);
284 | } else {
285 | Vue.prototype.$message.error(res.msg);
286 | }
287 | });
288 | },
289 |
290 | // 根据chatData返回的好友列表群组列表
291 | // 建立各自socket连接
292 | // 并保存至各自Gather
293 | async handleChatData({ commit, dispatch, state, rootState }, payload) {
294 | const { user } = rootState.app;
295 | const { socket } = state;
296 | const { groupGather } = state;
297 | const groupArr = payload.groupData;
298 | const friendArr = payload.friendData;
299 | const userArr = payload.userData;
300 | if (groupArr.length) {
301 | // eslint-disable-next-line no-restricted-syntax
302 | for (const group of groupArr) {
303 | socket.emit('joinGroupSocket', {
304 | groupId: group.groupId,
305 | userId: user.userId,
306 | });
307 | commit(SET_GROUP_GATHER, group);
308 | }
309 | }
310 | if (friendArr.length) {
311 | // eslint-disable-next-line no-restricted-syntax
312 | for (const friend of friendArr) {
313 | socket.emit('joinFriendSocket', {
314 | userId: user.userId,
315 | friendId: friend.userId,
316 | });
317 | commit(SET_FRIEND_GATHER, friend);
318 | }
319 | }
320 | if (userArr.length) {
321 | // eslint-disable-next-line no-restricted-syntax
322 | for (const user_ of userArr) {
323 | commit(SET_USER_GATHER, user_);
324 | }
325 | }
326 |
327 | /**
328 | * 由于groupgather和userGather都更新了, 但是activeGather依旧是老对象,
329 | * 这里需要根据老的activeGather找到最新的gather对象,这样才能使得vue的watch监听新gather
330 | */
331 |
332 | const { activeRoom } = state;
333 | console.log('init');
334 | console.log(activeRoom);
335 | const groupGather2 = state.groupGather;
336 | const friendGather2 = state.friendGather;
337 | if (!activeRoom) {
338 | console.log(DEFAULT_GROUP);
339 | // 更新完数据没有默认activeRoom设置群为DEFAULT_GROUP
340 | return commit(SET_ACTIVE_ROOM, groupGather[DEFAULT_GROUP]);
341 | }
342 | commit(SET_ACTIVE_ROOM, groupGather2[activeRoom.groupId] || friendGather2[activeRoom.userId]);
343 | },
344 | };
345 |
346 | export default actions;
347 |
--------------------------------------------------------------------------------
/client/src/store/modules/chat/getters.ts:
--------------------------------------------------------------------------------
1 | import { GetterTree } from 'vuex';
2 | import { ChatState } from './state';
3 | import { RootState } from '../../index';
4 |
5 | const getters: GetterTree = {
6 | socket(state) {
7 | return state.socket;
8 | },
9 | dropped(state) {
10 | return state.dropped;
11 | },
12 | activeRoom(state) {
13 | return state.activeRoom;
14 | },
15 | groupGather(state) {
16 | return state.groupGather;
17 | },
18 | friendGather(state) {
19 | return state.friendGather;
20 | },
21 | userGather(state) {
22 | return state.userGather;
23 | },
24 | unReadGather(state) {
25 | return state.unReadGather;
26 | },
27 | };
28 |
29 | export default getters;
30 |
--------------------------------------------------------------------------------
/client/src/store/modules/chat/index.ts:
--------------------------------------------------------------------------------
1 | import { Module } from 'vuex';
2 | import actions from './actions';
3 | import mutations from './mutations';
4 | import getters from './getters';
5 | import state, { ChatState } from './state';
6 | import { RootState } from '../../index';
7 |
8 | const chat: Module = {
9 | namespaced: true,
10 | state,
11 | mutations,
12 | actions,
13 | getters,
14 | };
15 |
16 | export default chat;
17 |
--------------------------------------------------------------------------------
/client/src/store/modules/chat/mutation-types.ts:
--------------------------------------------------------------------------------
1 | export const SET_SOCKET = 'set_socket';
2 | export const SET_DROPPED = 'set_dropped';
3 | export const SET_ACTIVE_GROUP_USER = 'set_active_group_user';
4 | export const SET_ACTIVE_ROOM = 'set_active_room';
5 | export const SET_USER_GATHER = 'set_user_gather';
6 | export const SET_FRIEND_GATHER = 'set_friend_gather';
7 | export const SET_GROUP_GATHER = 'set_group_gather';
8 | export const ADD_GROUP_MESSAGE = 'add_group_message';
9 | export const SET_GROUP_MESSAGES = 'set_group_messages';
10 | export const ADD_FRIEND_MESSAGE = 'add_friend_message';
11 | export const SET_FRIEND_MESSAGES = 'set_friend_messages';
12 | export const DEL_GROUP = 'del_group';
13 | export const DEL_GROUP_MEMBER = 'del_group_member';
14 | export const DEL_FRIEND = 'del_friend';
15 | export const ADD_UNREAD_GATHER = 'set_unread_gather';
16 | export const LOSE_UNREAD_GATHER = 'lose_unread_gather';
17 | export const REVOKE_MESSAGE = 'revoke_message';
18 | export const USER_ONLINE = 'user_online';
19 | export const USER_OFFLINE = 'user_offline';
20 | export const ADD_GROUP_MEMBER = 'add_group_member';
21 | export const UPDATE_USER_INFO = 'update_user_info';
22 |
--------------------------------------------------------------------------------
/client/src/store/modules/chat/mutations.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import { MutationTree } from 'vuex';
3 | import {
4 | SET_SOCKET,
5 | SET_DROPPED,
6 | ADD_GROUP_MEMBER,
7 | ADD_GROUP_MESSAGE,
8 | SET_GROUP_MESSAGES,
9 | ADD_FRIEND_MESSAGE,
10 | SET_FRIEND_MESSAGES,
11 | SET_ACTIVE_ROOM,
12 | SET_GROUP_GATHER,
13 | SET_FRIEND_GATHER,
14 | SET_USER_GATHER,
15 | DEL_GROUP,
16 | DEL_GROUP_MEMBER,
17 | DEL_FRIEND,
18 | ADD_UNREAD_GATHER,
19 | LOSE_UNREAD_GATHER,
20 | REVOKE_MESSAGE,
21 | USER_ONLINE,
22 | USER_OFFLINE,
23 | UPDATE_USER_INFO,
24 | } from './mutation-types';
25 | import { ChatState } from './state';
26 |
27 | const mutations: MutationTree = {
28 | // 保存socket
29 | [SET_SOCKET](state, payload: SocketIOClient.Socket) {
30 | state.socket = payload;
31 | },
32 |
33 | // 设置用户是否处于掉线重连状态
34 | [SET_DROPPED](state, payload: boolean) {
35 | state.dropped = payload;
36 | },
37 |
38 | /**
39 | * 用户上线
40 | * @param state
41 | * @param payload userId
42 | */
43 | [USER_ONLINE](state, userId: string) {
44 | // 更新好友列表用户状态
45 | if (state.friendGather[userId]) {
46 | console.log(`${userId}----上线`);
47 | Vue.set(state.friendGather[userId], 'online', 1);
48 | console.log(state.friendGather);
49 | }
50 | // 更新所有群组中该成员在线状态
51 | (Object.values(state.groupGather) as Group[]).forEach((group) => {
52 | const member = group.members!.find((m) => m.userId === userId);
53 | if (member) {
54 | member.online = 1;
55 | }
56 | });
57 | },
58 |
59 | // 用户下线
60 | [USER_OFFLINE](state, userId: string) {
61 | if (state.friendGather[userId]) {
62 | Vue.set(state.friendGather[userId], 'online', 0);
63 | }
64 | // 更新所有群组中该成员在线状态
65 | (Object.values(state.groupGather) as Group[]).forEach((group) => {
66 | const member = group.members!.find((m) => m.userId === userId);
67 | if (member) {
68 | member.online = 0;
69 | }
70 | });
71 | },
72 | // 新增群成员
73 | [ADD_GROUP_MEMBER](
74 | state,
75 | payload: {
76 | groupId: string;
77 | members: Friend[];
78 | }
79 | ) {
80 | const members: Friend[] = payload.members.map((member) => ({
81 | ...member,
82 | isManager: 0,
83 | }));
84 | if (state.groupGather[payload.groupId].members && members) {
85 | state.groupGather[payload.groupId].members = state.groupGather[payload.groupId].members!.concat(members);
86 | } else {
87 | // vuex对象数组中对象改变不更新问题
88 | Vue.set(state.groupGather[payload.groupId], 'members', members);
89 | }
90 | },
91 | // 新增一条群消息
92 | [ADD_GROUP_MESSAGE](state, payload: GroupMessage) {
93 | if (state.groupGather[payload.groupId].messages) {
94 | state.groupGather[payload.groupId].messages!.push(payload);
95 | } else {
96 | // vuex对象数组中对象改变不更新问题
97 | Vue.set(state.groupGather[payload.groupId], 'messages', [payload]);
98 | }
99 | },
100 |
101 | // 设置群消息
102 | [SET_GROUP_MESSAGES](state, payload: GroupMessage[]) {
103 | if (payload && payload.length) {
104 | Vue.set(state.groupGather[payload[0].groupId], 'messages', payload);
105 | }
106 | },
107 |
108 | // 新增一条私聊消息
109 | [ADD_FRIEND_MESSAGE](state, payload: FriendMessage) {
110 | // @ts-ignore
111 | const { userId } = this.getters['app/user'];
112 | if (payload.friendId === userId) {
113 | if (state.friendGather[payload.userId].messages) {
114 | state.friendGather[payload.userId].messages!.push(payload);
115 | } else {
116 | Vue.set(state.friendGather[payload.userId], 'messages', [payload]);
117 | }
118 | } else if (state.friendGather[payload.friendId].messages) {
119 | state.friendGather[payload.friendId].messages!.push(payload);
120 | } else {
121 | Vue.set(state.friendGather[payload.friendId], 'messages', [payload]);
122 | }
123 | },
124 |
125 | // 设置私聊记录
126 | [SET_FRIEND_MESSAGES](state, payload: FriendMessage[]) {
127 | // @ts-ignore
128 | const { userId } = this.getters['app/user'];
129 | if (payload && payload.length) {
130 | if (payload[0].friendId === userId) {
131 | Vue.set(state.friendGather[payload[0].userId], 'messages', payload);
132 | } else {
133 | Vue.set(state.friendGather[payload[0].friendId], 'messages', payload);
134 | }
135 | }
136 | },
137 |
138 | // 设置当前聊天对象(群或好友)
139 | [SET_ACTIVE_ROOM](state, payload: Friend & Group) {
140 | state.activeRoom = payload;
141 | },
142 |
143 | // 设置所有的群的群详细信息(头像,群名字等)
144 | [SET_GROUP_GATHER](state, payload: Group) {
145 | Vue.set(state.groupGather, payload.groupId, payload);
146 | },
147 |
148 | // 设置所有的用户的用户详细信息(头像,昵称等)
149 | [SET_USER_GATHER](state, payload: User) {
150 | Vue.set(state.userGather, payload.userId, payload);
151 | },
152 |
153 | // 设置所有的好友的用户详细信息(头像,昵称等)
154 | [SET_FRIEND_GATHER](state, payload: User) {
155 | Vue.set(state.friendGather, payload.userId, payload);
156 | },
157 |
158 | // 设置所有的用户的用户详细信息(头像,昵称等)
159 | [UPDATE_USER_INFO](state, user: User) {
160 | const { userId, username, avatar } = user;
161 | const { userGather, friendGather } = state;
162 | if (userGather[userId]) {
163 | userGather[userId].username = username;
164 | userGather[userId].avatar = avatar;
165 | }
166 | if (friendGather[userId]) {
167 | friendGather[userId].username = username;
168 | friendGather[userId].avatar = avatar;
169 | }
170 | },
171 |
172 | // 退群
173 | [DEL_GROUP](state, payload: GroupMap) {
174 | Vue.delete(state.groupGather, payload.groupId);
175 | },
176 |
177 | // 删除群成员
178 | [DEL_GROUP_MEMBER](state, payload: GroupMap) {
179 | const group = state.groupGather[payload.groupId];
180 | if (group) {
181 | group.members = group.members!.filter((member) => member.userId !== payload.userId);
182 | }
183 | },
184 |
185 | // 删好友
186 | [DEL_FRIEND](state, payload: UserMap) {
187 | Vue.delete(state.friendGather, payload.friendId);
188 | },
189 |
190 | // 给某个聊天组添加未读消息
191 | [ADD_UNREAD_GATHER](state, payload: string) {
192 | document.title = '【有未读消息】TylooChat聊天室';
193 | if (!state.unReadGather[payload]) {
194 | Vue.set(state.unReadGather, payload, 1);
195 | } else {
196 | ++state.unReadGather[payload];
197 | }
198 | },
199 |
200 | // 给某个聊天组清空未读消息
201 | [LOSE_UNREAD_GATHER](state, payload: string) {
202 | document.title = 'TylooChat聊天室';
203 | Vue.set(state.unReadGather, payload, 0);
204 | },
205 |
206 | // 消息撤回
207 | [REVOKE_MESSAGE](state, payload: FriendMessage & GroupMessage & { username: string }) {
208 | // @ts-ignore
209 | const { userId } = this.getters['app/user'];
210 | // 撤回的为群消息
211 | if (payload.groupId) {
212 | const { messages } = state.groupGather[payload.groupId];
213 | // 将该消息设置为isRevoke,并设置撤回人姓名
214 | if (messages) {
215 | const msg = messages.find((message) => message._id === payload._id);
216 | if (msg) {
217 | Vue.set(msg, 'isRevoke', true);
218 | Vue.set(msg, 'revokeUserName', payload.username);
219 | }
220 | }
221 | } else {
222 | const { messages } = state.friendGather[payload.friendId === userId ? payload.userId : payload.friendId];
223 | // 将该消息设置为isRevoke,并设置撤回人姓名
224 | if (messages) {
225 | const msg = messages.find((message) => message._id === payload._id);
226 | if (msg) {
227 | Vue.set(msg, 'isRevoke', true);
228 | Vue.set(msg, 'revokeUserName', payload.username);
229 | }
230 | }
231 | }
232 | },
233 | };
234 |
235 | export default mutations;
236 |
--------------------------------------------------------------------------------
/client/src/store/modules/chat/state.ts:
--------------------------------------------------------------------------------
1 | export interface ChatState {
2 | socket: SocketIOClient.Socket;
3 | dropped: boolean;
4 | activeRoom: (Group & Friend) | null;
5 | groupGather: GroupGather;
6 | userGather: FriendGather;
7 | friendGather: FriendGather;
8 | unReadGather: UnReadGather;
9 | }
10 |
11 | const chatState: ChatState = {
12 | // @ts-ignore
13 | socket: null, // ws实例
14 | dropped: false, // 是否断开连接
15 | activeRoom: null, // 当前访问房间
16 | groupGather: {}, // 群组列表
17 | userGather: {}, // 设置群在线用户列表
18 | friendGather: {}, // 好友列表
19 | unReadGather: {}, // 所有会话未读消息集合
20 | };
21 |
22 | export default chatState;
23 |
--------------------------------------------------------------------------------
/client/src/styles/coverAntd.scss:
--------------------------------------------------------------------------------
1 | // 修改antd tab默认样式
2 | .ant-tabs-bar {
3 | margin: 0 0 10px 0 !important;
4 | }
5 |
6 | // 抽屉组件重写样式
7 | .ant-drawer-content-wrapper {
8 | background-color: #f8f8f8 !important;
9 | .ant-drawer-content {
10 | background-color: #f8f8f8 !important;
11 | .ant-drawer-body {
12 | padding: 0 !important;
13 | // 移动端群组部分
14 | .room-card {
15 | background-color: rgba(115, 165, 200, 0.2);
16 | }
17 | .room-card.active {
18 | background-color: rgba(115, 165, 200, 0.35);
19 | }
20 | .room-card-name {
21 | color: #080808;
22 | }
23 | .text {
24 | color: #080808;
25 | }
26 |
27 | // 在线人数部分
28 | .active-content {
29 | height: 100%;
30 | overflow-y: scroll;
31 | border-radius: 0;
32 | text-align: left;
33 | padding: 10px;
34 | position: relative;
35 | .active-content-title {
36 | text-align: left;
37 | height: 100px;
38 | line-height: 20px;
39 | padding: 0 12px;
40 | font-size: 14px;
41 | color: #080808;
42 | border-bottom: 1px solid #d9d9d9 !important;
43 | .active-content-title-label {
44 | font-size: 12px;
45 | color: #888;
46 | }
47 | .active-content-title-detail {
48 | display: inline-block;
49 | width: 90%;
50 | vertical-align: middle;
51 | }
52 | & > div,span {
53 | overflow: hidden; //超出的文本隐藏
54 | text-overflow: ellipsis; //溢出用省略号显示
55 | white-space: nowrap; //溢出不换行
56 | }
57 | }
58 | .active-content-sum {
59 | // font-weight: bold;
60 | height: 40px;
61 | line-height: 40px;
62 | padding: 0 12px;
63 | color: #080808;
64 | }
65 | .active-content-adduser{
66 | padding: 0 14px;
67 | line-height: 40px;
68 | height: 40px;
69 | display: flex;
70 | color: #080808;
71 | align-items: center;
72 | .icon{
73 | font-size: 32px;
74 | margin-right: 15px;
75 | }
76 | .label{
77 | font-size: 16px;
78 | }
79 | }
80 | .active-content-users {
81 | max-height: calc(100% - 220px);
82 | overflow-y: scroll;
83 | .active-content-user {
84 | width: 260px;
85 | overflow: hidden; //超出的文本隐藏
86 | text-overflow: ellipsis; //溢出用省略号显示
87 | white-space: nowrap; //溢出不换行
88 | text-align: left;
89 | height: 40px;
90 | line-height: 40px;
91 | color: #080808;
92 | padding: 0 12px;
93 | position: relative;
94 | font-size: 16px;
95 | .icon{
96 | font-size: 16px;
97 | color: #080808;
98 | position: absolute;
99 | top: 9px;
100 | right: 35px;
101 | }
102 | }
103 | }
104 | .active-content-out {
105 | position: absolute;
106 | bottom: 0;
107 | font-size: 18px;
108 | left: 0;
109 | height: 50px;
110 | color: #e23b38;
111 | background: #f8f8f8;
112 | border-top: 1px solid #e1e1e1;
113 | border-left:none;
114 | border-right:none;
115 | border-bottom:none;
116 | border-radius: 0 !important;
117 | box-shadow: none !important;
118 | width: 100%;
119 | right: 0;
120 | }
121 | }
122 | }
123 | }
124 | }
125 | .ant-drawer-mask {
126 | background: transparent !important;
127 | }
128 |
129 | // antd drawer
130 | .ant-drawer .ant-drawer-content {
131 | overflow: hidden;
132 | }
133 | .ant-drawer-body {
134 | height: 100%;
135 | overflow: hidden;
136 | }
137 | .chat-drawer {
138 | height: 100%;
139 | overflow: hidden;
140 | }
141 |
142 | // 修改antd collpase默认样式
143 | .ant-collapse {
144 | background: #fbfbfb !important;
145 | border: none;
146 | }
147 | .ant-collapse-item {
148 | border: none !important;
149 | }
150 | .ant-collapse-content {
151 | border: none !important;
152 | }
153 | .ant-collapse-header {
154 | text-align: left;
155 | }
156 | .ant-collapse-content-box {
157 | padding: 0 !important;
158 | }
159 | // 修改antd ant-tree-title 默认样式
160 | .ant-tree-title,
161 | .ant-tree-switcher-line-icon {
162 | color: #080808 !important;
163 | }
164 |
165 | // 上传附件字体修改
166 | .ant-upload {
167 | font-size: inherit!important;
168 | }
169 |
170 | // disabled tree节点默认隐藏
171 | .ant-tree-treenode-switcher-open .ant-tree-checkbox-disabled,.ant-tree-switcher-noop{
172 | // display: none!important;
173 | }
--------------------------------------------------------------------------------
/client/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './coverAntd.scss';
2 |
3 | .message {
4 | // 滚动条样式
5 | ::-webkit-scrollbar-track-piece {
6 | background-color: rgba(54, 50, 50, 0.4);
7 | }
8 | ::-webkit-scrollbar {
9 | width: 5px;
10 | height: 10px;
11 | display: block;
12 | }
13 | ::-webkit-scrollbar-thumb {
14 | height: 50px;
15 | background-color: rgba(9, 185, 85, 0.3);
16 | outline-offset: -2px;
17 | filter: alpha(opacity = 50);
18 | -moz-opacity: 0.5;
19 | opacity: 0.5;
20 | border-radius: 4px;
21 | }
22 | ::-webkit-scrollbar-thumb:hover {
23 | background-color: rgba(9, 185, 85, 0.5);
24 | }
25 | }
26 | ::-webkit-scrollbar {
27 | display: none;
28 | }
29 |
30 | // * {
31 | // // 禁止文字被鼠标选中
32 | // moz-user-select: -moz-none;
33 | // -moz-user-select: none;
34 | // -o-user-select:none;
35 | // -webkit-user-select:none;
36 | // -ms-user-select:none;
37 | // user-select:none;
38 | // }
39 |
40 | .viewer-button {
41 | background-color: rgba(231, 226, 226, 0.6)!important;
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/styles/theme.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 主题配色
3 | * @author: BoBo
4 | * @copyright: BoBo
5 | * @Date: 2020-11-26 14:21:25
6 | */
7 |
8 | // 主题色
9 | $primary-color: #09b955;
10 | $room-bg-color: #fbfbfb;
11 | $message-bg-color:#f1f1f1;
--------------------------------------------------------------------------------
/client/src/types/chat.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'socket.io-client';
2 |
3 | // 所有群的群信息
4 | interface GroupGather {
5 | [groupId: string]: Group;
6 | }
7 |
8 | // 群组
9 | interface Group {
10 | groupId: string;
11 | userId: string; // 群主id
12 | groupName: string;
13 | notice: string;
14 | messages?: GroupMessage[];
15 | createTime: number;
16 | isTop?: boolean; // 是否置顶聊天
17 | members?: Friend[]; // 群成员列表
18 | }
19 |
20 | // 群与用户关联表
21 | interface GroupMap {
22 | groupId: string;
23 | userId: string;
24 | }
25 |
26 | // 群消息
27 | interface GroupMessage {
28 | _id?: number;
29 | userId: string;
30 | groupId: string;
31 | content: string;
32 | messageType: MessageType;
33 | time: number;
34 | isRevoke?: boolean; // 是否已撤回
35 | revokeUserName?: string; // 撤回人姓名
36 | }
37 |
38 | // 所有好友的好友信息
39 | interface FriendGather {
40 | [userId: string]: Friend;
41 | }
42 |
43 | // 好友
44 | interface Friend {
45 | userId: string;
46 | username: string;
47 | avatar?: string;
48 | role?: string;
49 | tag?: string;
50 | messages?: FriendMessage[];
51 | createTime?: number;
52 | isTop?: boolean; // 是否置顶聊天
53 | online?: 1 | 0; // 是否在线
54 | isManager?: 1 | 0; // 是否为群主
55 | }
56 |
57 | // 用户与好友关联表
58 | interface UserMap {
59 | friendId: string;
60 | userId: string;
61 | }
62 |
63 | // 好友消息
64 | interface FriendMessage {
65 | _id?: number;
66 | userId: string;
67 | friendId: string;
68 | content: string;
69 | messageType: MessageType;
70 | time: number;
71 | type?: string;
72 | isRevoke?: boolean; // 是否已撤回
73 | revokeUserName?: string; // 撤回人姓名
74 | }
75 |
76 | interface SendMessage {
77 | type: string;
78 | message: string | File;
79 | width?: number;
80 | height?: number;
81 | fileName?: string; // 上传附件名
82 | messageType: MessageType[0] | MessageType[1];
83 | size?: number; // 附件大小
84 | }
85 |
86 | // 消息类型
87 | declare enum MessageType {
88 | text = 'text',
89 | image = 'image',
90 | file = 'file',
91 | video = 'video',
92 | }
93 |
94 | // 图片尺寸
95 | interface ImageSize {
96 | width: number;
97 | height: number;
98 | }
99 |
100 | // 服务端返回值格式
101 | interface ServerRes {
102 | code: number;
103 | msg: string;
104 | data: any;
105 | }
106 |
107 | // 未读消息对象
108 | interface UnReadGather {
109 | [key: string]: number;
110 | }
111 |
112 | // 获取群分页消息参数
113 | interface PagingParams {
114 | groupId?: string;
115 | userId?: string;
116 | friendId?: string;
117 | current: number;
118 | pageSize: number;
119 | }
120 |
121 | // 群分页消息返回值
122 | interface PagingResponse {
123 | messageArr: GroupMessage[];
124 | userArr: User[];
125 | }
126 |
127 | interface FriendMap {
128 | friendId: string;
129 | friendUserName: string;
130 | }
131 |
132 | // 右键菜单操作烈性
133 | declare enum ContextMenuType {
134 | COPY = 'COPY', // 复制
135 | REVOKE = 'REVOKE', // 撤回
136 | TOP_REVERT = 'TOP_REVERT', // 取消置顶
137 | TOP = 'TOP', // 置顶
138 | READ = 'READ', // 一键已读
139 | DELETE = 'DELETE', // 删除
140 | }
141 |
--------------------------------------------------------------------------------
/client/src/types/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue';
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/types/shims-vue-expand.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import VueRouter, { Route } from 'vue-router';
3 | import { Store } from 'vuex';
4 | import * as lodash from 'lodash';
5 |
6 | // 扩充
7 | declare module 'vue/types/vue' {
8 | interface Vue {
9 | $router: VueRouter;
10 | $localforage: any
11 | $route: Route;
12 | $store: Store;
13 | $lodash: typeof lodash;
14 | }
15 | }
--------------------------------------------------------------------------------
/client/src/types/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue';
3 |
4 | export default Vue;
5 | }
6 |
7 | declare module 'v-contextmenu';
8 |
--------------------------------------------------------------------------------
/client/src/types/user.d.ts:
--------------------------------------------------------------------------------
1 | interface User {
2 | userId: string;
3 | username: string;
4 | password: string;
5 | avatar: string;
6 | role?: string;
7 | tag?: string;
8 | createTime: number;
9 | online?: 1 | 0; // 是否在线
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/utils/common.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import { AxiosResponse } from 'axios';
3 |
4 | // 处理所有后端返回的数据
5 | export function processReturn(res: AxiosResponse) {
6 | // code 0:成功 1:错误 2:后端报错
7 | const { code, msg, data } = res.data;
8 | if (code) {
9 | Vue.prototype.$message.error(msg);
10 | return;
11 | }
12 | if (msg) {
13 | Vue.prototype.$message.success(msg);
14 | }
15 | return data;
16 | }
17 |
18 | // 判断一个字符串是否包含另外一个字符串
19 | export function isContainStr(str1: string, str2: string) {
20 | return str2.indexOf(str1) >= 0;
21 | }
22 |
23 | /**
24 | * 屏蔽词
25 | * @param text 文本
26 | */
27 | export function parseText(text: string) {
28 | return text;
29 | }
30 |
31 | /**
32 | * 判断是否URL
33 | * @param text 文本
34 | */
35 | export function isUrl(text: string) {
36 | // 解析网址
37 | // eslint-disable-next-line no-useless-escape
38 | const UrlReg = new RegExp(/http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/);
39 | return UrlReg.test(text);
40 | }
41 |
42 | /**
43 | * 消息时间格式化
44 | * @param time
45 | */
46 | export function formatTime(time: number) {
47 | const moment = Vue.prototype.$moment;
48 | // 大于昨天
49 | if (moment().add(-1, 'days').startOf('day') > time) {
50 | return moment(time).format('M/D HH:mm');
51 | }
52 | // 昨天
53 | if (moment().startOf('day') > time) {
54 | return `昨天 ${moment(time).format('HH:mm')}`;
55 | }
56 | // 大于五分钟不显示秒
57 | // if (new Date().valueOf() > time + 300000) {
58 | // return moment(time).format('HH:mm');
59 | // }
60 | return moment(time).format('HH:mm');
61 | }
62 |
63 | /**
64 | * 群名/用户名校验
65 | * @param name
66 | */
67 | export function nameVerify(name: string): boolean {
68 | const nameReg = /^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/;
69 | if (name.length === 0) {
70 | Vue.prototype.$message.error('请输入名字');
71 | return false;
72 | }
73 | if (!nameReg.test(name)) {
74 | Vue.prototype.$message.error('名字只含有汉字、字母、数字和下划线 不能以下划线开头和结尾');
75 | return false;
76 | }
77 | if (name.length > 16) {
78 | Vue.prototype.$message.error('名字太长');
79 | return false;
80 | }
81 | return true;
82 | }
83 |
84 | /**
85 | * 密码校验
86 | * @param password
87 | */
88 | export function passwordVerify(password: string): boolean {
89 | const passwordReg = /^\w+$/gis;
90 | if (password.length === 0) {
91 | Vue.prototype.$message.error('请输入密码');
92 | return false;
93 | }
94 | if (!passwordReg.test(password)) {
95 | Vue.prototype.$message.error('密码只含有字母、数字和下划线');
96 | return false;
97 | }
98 | if (password.length > 16) {
99 | Vue.prototype.$message.error('密码最多16位,请重新输入');
100 | return false;
101 | }
102 | return true;
103 | }
104 |
--------------------------------------------------------------------------------
/client/src/views/Chat.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
204 |
302 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strictPropertyInitialization": false,
13 | "sourceMap": true,
14 | "baseUrl": ".",
15 | "types": [
16 | "webpack-env"
17 | ],
18 | "paths": {
19 | "@/*": [
20 | "src/*"
21 | ]
22 | },
23 | "lib": [
24 | "esnext",
25 | "dom",
26 | "dom.iterable",
27 | "scripthost"
28 | ]
29 | },
30 | "include": [
31 | "src/**/*.ts",
32 | "src/**/*.tsx",
33 | "src/**/*.vue",
34 | ],
35 | "exclude": [
36 | "node_modules"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/client/vue.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | const isDev = process.env.NODE_ENV === 'development';
4 |
5 | // cdn链接
6 | const cdn = {
7 | css: [
8 | // antd css 由于引入失败只好放弃了antd的按需引入
9 | ],
10 | js: [
11 | // vue
12 | 'https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js',
13 | // vue-router
14 | 'https://cdn.bootcdn.net/ajax/libs/vue-router/3.1.3/vue-router.min.js',
15 | // vuex
16 | 'https://cdn.bootcdn.net/ajax/libs/vuex/3.1.2/vuex.min.js',
17 | // axios
18 | 'https://cdn.bootcdn.net/ajax/libs/axios/0.18.0/axios.min.js',
19 | // moment
20 | 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js',
21 | // lodash
22 | 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/lodash.min.js',
23 | ],
24 | };
25 |
26 | const CompressionWebpackPlugin = require('compression-webpack-plugin');
27 |
28 | module.exports = {
29 | chainWebpack: (config) => {
30 | // 需要打包分析时取消注释
31 | // eslint-disable-next-line global-require
32 | // config.plugin('webpack-bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin);
33 |
34 | // 配置cdn引入
35 | if (process.env.NODE_ENV === 'production' && process.env.VUE_APP_CDN === 'true') {
36 | const externals = {
37 | vue: 'Vue',
38 | axios: 'axios',
39 | 'vue-router': 'VueRouter',
40 | vuex: 'Vuex',
41 | moment: 'moment',
42 | lodash: '_',
43 | };
44 | config.externals(externals);
45 | // 通过 html-webpack-plugin 将 cdn 注入到 index.html 之中
46 | config.plugin('html').tap((args) => {
47 | // eslint-disable-next-line no-param-reassign
48 | args[0].cdn = cdn;
49 | return args;
50 | });
51 | }
52 | },
53 | configureWebpack: (config) => {
54 | // 代码 gzip
55 | const productionGzipExtensions = ['html', 'js', 'css'];
56 | // 开发模式下不走gzip
57 | if (!isDev) {
58 | config.plugins.push(
59 | new CompressionWebpackPlugin({
60 | filename: '[path].gz[query]',
61 | algorithm: 'gzip',
62 | test: new RegExp(`\\.(${productionGzipExtensions.join('|')})$`),
63 | threshold: 10240, // 只有大小大于该值的资源会被处理 10240
64 | minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
65 | deleteOriginalAssets: false, // 删除原文件
66 | })
67 | );
68 | }
69 |
70 | // 不打包moment的语言包
71 | config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));
72 |
73 | // 去除console
74 | if (process.env.NODE_ENV === 'production') {
75 | // eslint-disable-next-line no-param-reassign
76 | config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
77 | }
78 | },
79 | css: {
80 | loaderOptions: {
81 | less: {
82 | lessOptions: {
83 | modifyVars: {
84 | 'primary-color': '#09b955',
85 | // 'link-color': '#1DA57A',
86 | // 'border-radius-base': '2px',
87 | },
88 | javascriptEnabled: true,
89 | },
90 | },
91 | sass: {
92 | prependData: "@import '@/styles/index.scss';",
93 | },
94 | },
95 | },
96 | // webSocket本身不存在跨域问题,所以我们可以利用webSocket来进行非同源之间的通信。
97 | publicPath: './',
98 | devServer: {
99 | port: 1997,
100 | },
101 | productionSourceMap: false,
102 | };
103 |
--------------------------------------------------------------------------------
/deploy.md:
--------------------------------------------------------------------------------
1 | # 部署说明
2 | - 部署策略采用前后端分离部署,后端采用CORS解决跨域问题
3 | - **请先切换`feature_APIROBOT`分支进行部署**
4 |
5 | ## 前端部署
6 | 1. 修改.env.out环境变量中的`VUE_APP_API_URL`地址为你的服务器地址
7 | ```js
8 | VUE_APP_API_URL=http://xxx.xxx.xxx.xxx:3000
9 | ```
10 | 2. 构建前端包
11 | ```js
12 | // client
13 | cnpm i
14 | npm run build
15 | ```
16 | 3. 将 dist 下所有文件放到 nginx 下的 html 文件夹中 (或者是其他http服务器的对应目录下)
17 | 4. nginx conf
18 | ```js
19 | // nginx.conf
20 | http {
21 | #避免mime类型丢失导致css样式无法正常加载
22 | include mime.types;
23 | #nginx开启gzip
24 | #前端文件在build的时候已经配置好压缩,需要再配置一下nginx;
25 | gzip on;
26 | gzip_static on;
27 | gzip_buffers 4 16k;
28 | gzip_comp_level 5;
29 | gzip_types text/plain application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg
30 | image/gif image/png;
31 |
32 | #nginx请求级别配置
33 | server {
34 | listen 80;
35 | server_name www.server.com;
36 | location / {
37 | root html;
38 | index index.html index.htm;
39 | add_header Cache-Control public;
40 | }
41 | location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
42 | {
43 | expires 30d;
44 | }
45 |
46 | location ~ .*\.(js|css)?$
47 | {
48 | expires 12h;
49 | }
50 | error_page 500 502 503 504 /50x.html;
51 | location = /50x.html {
52 | root html;
53 | }
54 | }
55 | }
56 | ```
57 | 5. nginx -s reload
58 |
59 | ## 数据库配置
60 | 1. 创建名为 `chat` 的数据库
61 | 2. 配置后端 `app.module.ts` 中的 mysql 账号密码
62 | ```js
63 | // /server/src/app.module.ts
64 | @Module({
65 | imports: [
66 | TypeOrmModule.forRoot({
67 | type: 'mysql',
68 | port: 3306,
69 | username: 'root', // 默认账号
70 | password: '123456', // 默认密码
71 | database: 'chat',
72 | charset: "utf8mb4",
73 | autoLoadEntities: true,
74 | synchronize: true
75 | }),
76 | ],
77 | })
78 | ```
79 |
80 | ## 部署后端服务
81 |
82 | **后台服务默认端口号为`3000`有需要自行修改 main.ts文件**
83 | - 方式一(整个项目拷贝至服务器)
84 | 1. 安装 pm2
85 | ```js
86 | npm i pm2 -g
87 | ```
88 | 2. 生成 dist 文件
89 | ```js
90 | npm i
91 | npm run build
92 | ```
93 | 3. 使用 pm2 运行
94 | ```js
95 | npm run pm2
96 | ```
97 | - 方式二(结合ncc打包,便于离线环境下部署)
98 | 1. 本地执行`npm run pkg`
99 | 2. 拷贝`deploy`文件夹至服务器
100 | 3. 服务器上运行
101 | ```js
102 | node deploy/index 或者 pm2 deploy/index
103 | ```
104 |
105 | ## 其他注意事项
106 |
107 | - 如果在CentOS上部署出现libc.so.6版本过低,考虑是系统环境问题,升级版本
108 |
109 | - 后端服务器注意安全策略中放行`3000`端口
110 |
111 | - 加qq群..
--------------------------------------------------------------------------------
/node_modules/.yarn-integrity:
--------------------------------------------------------------------------------
1 | {
2 | "systemParams": "darwin-arm64-93",
3 | "modulesFolders": [],
4 | "flags": [],
5 | "linkedModules": [],
6 | "topLevelPatterns": [],
7 | "lockfileEntries": {},
8 | "files": [],
9 | "artifacts": {}
10 | }
--------------------------------------------------------------------------------
/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // .eslintrc.js
2 | module.exports = {
3 | parser: '@typescript-eslint/parser',
4 | extends: [
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:prettier/recommended'
7 | ],
8 | plugins: ['@typescript-eslint'],
9 | rules: {
10 | // 这条规则是为了防止写class interface的member时,分隔符和prettier产生冲突
11 | '@typescript-eslint/ban-ts-comment': 'off',
12 | '@typescript-eslint/no-var-requires': 'off',
13 | '@typescript-eslint/explicit-module-boundary-types': 'off',
14 | '@typescript-eslint/no-explicit-any': 'off',
15 | '@typescript-eslint/member-delimiter-style': [
16 | 'error',
17 | {
18 | multiline: {
19 | delimiter: 'none',
20 | requireLast: false
21 | },
22 | singleline: {
23 | delimiter: 'comma',
24 | requireLast: false
25 | }
26 | }
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 | /deploy
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false
4 | }
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # server
2 |
3 | nestjs
--------------------------------------------------------------------------------
/server/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src/modules"
4 | }
5 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tyloo-chat-server",
3 | "version": "2.1.7",
4 | "description": "",
5 | "author": "boboooooo159@gmail.com",
6 | "private": true,
7 | "license": "MIT",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json",
21 | "pm2": "pm2 start --name tyloochat dist/main.js -i max",
22 | "pkg": "npm run build && ncc build dist/main.js -o deploy"
23 | },
24 | "dependencies": {
25 | "@nestjs/common": "^7.0.0",
26 | "@nestjs/core": "^7.0.0",
27 | "@nestjs/jwt": "^7.1.0",
28 | "@nestjs/passport": "^7.1.0",
29 | "@nestjs/platform-express": "^7.0.0",
30 | "@nestjs/platform-socket.io": "^7.2.0",
31 | "@nestjs/typeorm": "^7.1.0",
32 | "@nestjs/websockets": "^7.2.0",
33 | "axios": "^0.21.0",
34 | "crypto": "^1.0.1",
35 | "mongodb": "^3.5.9",
36 | "mysql": "^2.18.1",
37 | "nodejieba": "^2.4.2",
38 | "passport": "^0.4.1",
39 | "passport-jwt": "^4.0.0",
40 | "passport-local": "^1.0.0",
41 | "reflect-metadata": "^0.1.13",
42 | "rimraf": "^3.0.2",
43 | "rxjs": "^6.5.4",
44 | "typeorm": "^0.2.25",
45 | "uuid": "^8.2.0"
46 | },
47 | "devDependencies": {
48 | "@nestjs/cli": "^7.0.0",
49 | "@nestjs/schematics": "^7.0.0",
50 | "@nestjs/testing": "^7.0.0",
51 | "@types/express": "^4.17.3",
52 | "@types/jest": "25.2.3",
53 | "@types/node": "^13.9.1",
54 | "@types/socket.io": "^2.1.8",
55 | "@types/supertest": "^2.0.8",
56 | "@typescript-eslint/eslint-plugin": "3.0.2",
57 | "@typescript-eslint/parser": "3.0.2",
58 | "@zeit/ncc": "^0.22.3",
59 | "eslint": "7.1.0",
60 | "eslint-config-prettier": "^6.10.0",
61 | "eslint-plugin-import": "^2.20.1",
62 | "eslint-plugin-prettier": "^3.1.4",
63 | "husky": "^4.3.0",
64 | "jest": "26.0.1",
65 | "lint-staged": "^10.5.1",
66 | "prettier": "^1.19.1",
67 | "supertest": "^4.0.2",
68 | "ts-jest": "26.1.0",
69 | "ts-loader": "^6.2.1",
70 | "ts-node": "^8.6.2",
71 | "tsconfig-paths": "^3.9.0",
72 | "typescript": "^3.7.4"
73 | },
74 | "husky": {
75 | "hooks": {
76 | "pre-commit": "lint-staged"
77 | }
78 | },
79 | "lint-staged": {
80 | "src/**/*.{js,ts}": [
81 | "prettier --write",
82 | "eslint --fix",
83 | "git add"
84 | ]
85 | },
86 | "jest": {
87 | "moduleFileExtensions": [
88 | "js",
89 | "json",
90 | "ts"
91 | ],
92 | "rootDir": "src",
93 | "testRegex": ".spec.ts$",
94 | "transform": {
95 | "^.+\\.(t|j)s$": "ts-jest"
96 | },
97 | "coverageDirectory": "../coverage",
98 | "testEnvironment": "node"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/server/public/avatar/avatar1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar1.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar10.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar11.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar12.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar13.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar14.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar15.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar16.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar17.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar18.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar19.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar2.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar20.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar3.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar4.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar5.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar6.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar7.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar8.png
--------------------------------------------------------------------------------
/server/public/avatar/avatar9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/avatar9.png
--------------------------------------------------------------------------------
/server/public/avatar/robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BoBoooooo/Tyloo-Chat/8314298d62a9e80e8426d08052e3ffdd40055378/server/public/avatar/robot.png
--------------------------------------------------------------------------------
/server/public/static/file/info.md:
--------------------------------------------------------------------------------
1 | 聊天附件上传目录
--------------------------------------------------------------------------------
/server/public/static/image/info.md:
--------------------------------------------------------------------------------
1 | 聊天图片上传目录
--------------------------------------------------------------------------------
/server/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { UserModule } from './modules/user/user.module'
4 | import { ChatModule } from './modules/chat/chat.module'
5 | import { FriendModule } from './modules/friend/friend.module'
6 | import { GroupModule } from './modules/group/group.module'
7 | import { AuthModule } from './modules/auth/auth.module'
8 |
9 | @Module({
10 | imports: [
11 | TypeOrmModule.forRoot({
12 | type: 'mysql',
13 | host: 'localhost',
14 | port: 3306,
15 | username: 'root',
16 | password: '',
17 | database: 'chat',
18 | charset: 'utf8mb4', // 设置chatset编码为utf8mb4
19 | autoLoadEntities: true,
20 | synchronize: true
21 | }),
22 | UserModule,
23 | ChatModule,
24 | FriendModule,
25 | GroupModule,
26 | AuthModule
27 | ]
28 | })
29 | export class AppModule {}
30 |
--------------------------------------------------------------------------------
/server/src/common/constant/global.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 全局变量定义
3 | * @author: BoBo
4 | * @copyright: BoBo
5 | * @Date: 2020-11-20 17:40:24
6 | */
7 |
8 | export const defaultPassword = '123456' // 默认用户密码
9 | export const defaultRobot = '小冰机器人' // 机器人名称
10 | export const defaultRobotId = 'robot' // 机器人ID
11 | export const defaultWelcomeMessage = '欢迎使用小冰机器人,有什么能帮您的呢?😃' // 机器人欢迎语
12 | export const defaultGroup = '系统问题反馈群' // 默认用户群组名称
13 | export const defaultGroupId = 'group' // 默认用户群组ID
14 | export const IMAGE_SAVE_PATH = 'public/static/image' // 聊天图片缓存路径
15 | export const FILE_SAVE_PATH = 'public/static/file' // 聊天附件缓存路径
16 |
17 | export const defaultGroupMessageTime = 86400000 // 新用户进群默认可以看历史消息的时限(此处默认为24小时内的消息)
18 |
--------------------------------------------------------------------------------
/server/src/common/constant/rcode.ts:
--------------------------------------------------------------------------------
1 | export enum RCode {
2 | OK,
3 | FAIL,
4 | ERROR
5 | }
6 |
--------------------------------------------------------------------------------
/server/src/common/filters/http-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentsHost,
3 | Catch,
4 | ExceptionFilter,
5 | HttpException
6 | } from '@nestjs/common'
7 |
8 | @Catch(HttpException)
9 | export class HttpExceptionFilter implements ExceptionFilter {
10 | catch(exception: HttpException, host: ArgumentsHost) {
11 | const ctx = host.switchToHttp()
12 | const response = ctx.getResponse()
13 | const request = ctx.getRequest()
14 | const status = exception.getStatus()
15 | const exceptionRes: any = exception.getResponse()
16 | const error = exceptionRes.error
17 | let message = exceptionRes.message
18 |
19 | if (status === 401) {
20 | message = '身份过期,请重新登录'
21 | }
22 | response.status(200).json({
23 | code: status,
24 | timestamp: new Date().toISOString(),
25 | path: request.url,
26 | error,
27 | msg: message
28 | })
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/src/common/filters/ws-exception.filter.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 全局ws连接异常捕获
3 | * @author: BoBo
4 | * @copyright: BoBo
5 | * @Date: 2020-12-26 13:25:26
6 | */
7 |
8 | import { Catch, ArgumentsHost } from '@nestjs/common'
9 | import { BaseWsExceptionFilter } from '@nestjs/websockets'
10 |
11 | @Catch()
12 | export class WsExceptionFilter extends BaseWsExceptionFilter {
13 | catch(exception: unknown, host: ArgumentsHost) {
14 | super.catch(exception, host)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/server/src/common/guards/WsJwtGuard.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: WS JWT鉴权守卫
3 | * @author: BoBo
4 | * @copyright: BoBo
5 | * @Date: 2020-12-26 11:48:56
6 | */
7 | import { AuthService } from './../../modules/auth/auth.service'
8 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
9 | import { WsException } from '@nestjs/websockets'
10 | import { Socket } from 'socket.io'
11 |
12 | @Injectable()
13 | export class WsJwtGuard implements CanActivate {
14 | constructor(private authService: AuthService) {}
15 |
16 | async canActivate(context: ExecutionContext): Promise {
17 | let client: Socket
18 | try {
19 | client = context.switchToWs().getClient()
20 | const authToken: string = client.handshake?.query?.token
21 | const user = this.authService.verifyUser(authToken)
22 | return Boolean(user)
23 | } catch (err) {
24 | client.emit('unauthorized', '用户信息校验失败,请重新登录!')
25 | client.disconnect()
26 | throw new WsException(err.message)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/common/interceptor/response.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NestInterceptor,
3 | ExecutionContext,
4 | CallHandler,
5 | Injectable
6 | } from '@nestjs/common'
7 | import { map } from 'rxjs/operators'
8 | import { RCode } from '../constant/rcode'
9 |
10 | @Injectable()
11 | export class ResponseInterceptor implements NestInterceptor {
12 | intercept(
13 | context: ExecutionContext,
14 | next: CallHandler
15 | ): import('rxjs').Observable | Promise> {
16 | return next.handle().pipe(
17 | map(content => {
18 | return {
19 | data: content.data || {},
20 | code: content.code || RCode.OK,
21 | msg: content.msg || null
22 | }
23 | })
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/server/src/common/middleware/elasticsearch.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: elasticsearch中间件模块,需要先安装es,并启动
3 | * @author: BoBo
4 | * @copyright: BoBo
5 | * @Date: 2020-11-19 14:16:37
6 | */
7 |
8 | const axios = require('axios')
9 |
10 | const base = 'http://localhost:9200/robot/_doc/_search'
11 |
12 | export const getElasticData = (query: string): Promise =>
13 | axios.get(base, {
14 | params: {
15 | source: JSON.stringify({
16 | query: {
17 | match: {
18 | title: query
19 | }
20 | }
21 | }),
22 | source_content_type: 'application/json'
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/server/src/common/middleware/logger.middleware.ts:
--------------------------------------------------------------------------------
1 | export function logger(req, res, next) {
2 | const { method, path } = req
3 | console.log(`${method} ${path}`)
4 | next()
5 | }
6 |
--------------------------------------------------------------------------------
/server/src/common/tool/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 群名/用户名校验
3 | * @param name
4 | */
5 | export function nameVerify(name: string): boolean {
6 | const nameReg = /^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/
7 | if (name.length === 0) {
8 | return false
9 | }
10 | if (!nameReg.test(name)) {
11 | return false
12 | }
13 | if (name.length > 16) {
14 | return false
15 | }
16 | return true
17 | }
18 |
19 | /**
20 | * 获取文件大小
21 | * @param bytes
22 | * @param decimals
23 | */
24 | export function formatBytes(bytes, decimals = 2) {
25 | if (bytes === 0) return '0 Bytes'
26 | const k = 1024
27 | const dm = decimals < 0 ? 0 : decimals
28 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
29 | const i = Math.floor(Math.log(bytes) / Math.log(k))
30 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]
31 | }
32 |
33 | // md5加盐
34 | const crypto = require('crypto')
35 |
36 | // md5加盐处理password
37 | export function md5(str) {
38 | const m = crypto.createHash('md5')
39 | m.update(str, 'utf8')
40 | return m.digest('hex')
41 | }
42 |
--------------------------------------------------------------------------------
/server/src/fix.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 修改socket.io serveClient,避免ncc打包后启动报错
3 | * https://stackoverflow.com/questions/51914186/cannot-find-module-socket-io-client-dist-socket-io-js-when-starting-express-ap
4 | * @author: BoBo
5 | * @copyright: BoBo
6 | * @Date: 2020-11-30 16:04:32
7 | */
8 |
9 | const fs = require('fs')
10 | const path = require('path')
11 |
12 | module.exports = async () => {
13 | // 如果已经拉取过依赖包
14 | const socket_io_path = `${path.resolve(
15 | __dirname,
16 | '..'
17 | )}/node_modules/socket.io/lib/index.js`
18 | // 破解gojs
19 | if (fs.existsSync(socket_io_path)) {
20 | fs.readFile(socket_io_path, 'utf8', (err1, files) => {
21 | const result = files.replace(
22 | 'this.serveClient(false !== opts.serveClient);',
23 | '// this.serveClient(false !== opts.serveClient);'
24 | )
25 | fs.writeFile(socket_io_path, result, 'utf8', err2 => console.log(err2))
26 | })
27 | Promise.resolve()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core'
2 | import { NestExpressApplication } from '@nestjs/platform-express'
3 | import { AppModule } from './app.module'
4 | import { HttpExceptionFilter } from './common/filters/http-exception.filter'
5 | import { logger } from './common/middleware/logger.middleware'
6 | import { ResponseInterceptor } from './common/interceptor/response.interceptor'
7 | import { join } from 'path'
8 | import { IoAdapter } from '@nestjs/platform-socket.io'
9 |
10 | const fix_socket_io_bug = require('./fix')
11 |
12 | async function bootstrap() {
13 | await fix_socket_io_bug()
14 |
15 | const app = await NestFactory.create(AppModule, {
16 | cors: true
17 | })
18 | // https://github.com/vercel/ncc/issues/513
19 | // fix ncc打包后提示找不到该依赖问题
20 | app.useWebSocketAdapter(new IoAdapter(app))
21 |
22 | // 全局中间件
23 | app.use(logger)
24 |
25 | // 全局过滤器
26 | app.useGlobalFilters(new HttpExceptionFilter())
27 |
28 | // 配置全局拦截器
29 | app.useGlobalInterceptors(new ResponseInterceptor())
30 |
31 | // 配置静态资源
32 | app.useStaticAssets(join(__dirname, '../public', '/'), {
33 | prefix: '/',
34 | setHeaders: res => {
35 | res.set('Cache-Control', 'max-age=2592000')
36 | }
37 | })
38 |
39 | await app.listen(3000)
40 | }
41 | bootstrap()
42 |
--------------------------------------------------------------------------------
/server/src/modules/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: 权限登陆模块
3 | * @author: BoBo
4 | * @copyright: BoBo
5 | * @Date: 2020-11-18 09:18:10
6 | */
7 | import { Body, Controller, Post } from '@nestjs/common'
8 | import { AuthService } from './auth.service'
9 |
10 | @Controller('auth')
11 | export class AuthController {
12 | constructor(private readonly authService: AuthService) {}
13 |
14 | // 登录测试
15 | @Post('/login')
16 | async login(@Body() body) {
17 | return this.authService.login(body)
18 | }
19 |
20 | @Post('/register')
21 | async register(@Body() body) {
22 | return this.authService.register(body)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/server/src/modules/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { FriendMessage } from './../friend/entity/friendMessage.entity'
2 | import { UserMap } from './../friend/entity/friend.entity'
3 | import { Module } from '@nestjs/common'
4 | import { AuthService } from './auth.service'
5 | import { PassportModule } from '@nestjs/passport'
6 | import { JwtModule } from '@nestjs/jwt'
7 | import { AuthController } from './auth.controller'
8 | import { LocalStrategy } from './local.strategy'
9 | import { JwtStrategy } from './jwt.strategy'
10 | import { jwtConstants } from './constants'
11 | import { TypeOrmModule } from '@nestjs/typeorm'
12 | import { User } from '../user/entity/user.entity'
13 | import { GroupMap } from '../group/entity/group.entity'
14 |
15 | @Module({
16 | imports: [
17 | TypeOrmModule.forFeature([User, GroupMap, UserMap, FriendMessage]),
18 | JwtModule.register({
19 | secret: jwtConstants.secret,
20 | signOptions: { expiresIn: '3d' }
21 | }),
22 | PassportModule
23 | ],
24 | controllers: [AuthController],
25 | providers: [AuthService, LocalStrategy, JwtStrategy],
26 | exports: [AuthService]
27 | })
28 | export class AuthModule {}
29 |
--------------------------------------------------------------------------------
/server/src/modules/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultPassword,
3 | defaultRobotId,
4 | defaultGroupId,
5 | defaultWelcomeMessage
6 | } from 'src/common/constant/global'
7 | import { FriendMessage } from './../friend/entity/friendMessage.entity'
8 | import { UserMap } from './../friend/entity/friend.entity'
9 | import { Injectable } from '@nestjs/common'
10 | import { JwtService } from '@nestjs/jwt'
11 | import { InjectRepository } from '@nestjs/typeorm'
12 | import { Repository } from 'typeorm'
13 | import { User } from '../user/entity/user.entity'
14 | import { GroupMap } from '../group/entity/group.entity'
15 | import { md5 } from 'src/common/tool/utils'
16 | import { RCode } from 'src/common/constant/rcode'
17 | import * as jwt from 'jsonwebtoken'
18 | import { jwtConstants } from './constants'
19 | @Injectable()
20 | export class AuthService {
21 | constructor(
22 | @InjectRepository(User)
23 | private readonly userRepository: Repository,
24 | @InjectRepository(GroupMap)
25 | private readonly groupUserRepository: Repository,
26 | @InjectRepository(UserMap)
27 | private readonly userMapRepository: Repository,
28 | @InjectRepository(FriendMessage)
29 | private readonly friendMessageRepository: Repository,
30 | private readonly jwtService: JwtService
31 | ) {}
32 |
33 | async login(data: User): Promise {
34 | let user
35 | // 如果之前传userId 表示为单点登录,直接登录
36 | if (data.userId && !data.password) {
37 | user = await this.userRepository.findOne({ userId: data.userId })
38 | // 如果当前不存在该用户,自动注册,初始密码为 123456
39 | if (!user) {
40 | const res = this.register({
41 | ...data,
42 | password: defaultPassword
43 | })
44 | return res
45 | }
46 | } else {
47 | user = await this.userRepository.findOne({
48 | username: data.username,
49 | password: md5(data.password)
50 | })
51 | }
52 | if (!user) {
53 | return { code: 1, msg: '用户名或密码错误', data: '' }
54 | }
55 | const payload = { userId: user.userId }
56 | return {
57 | msg: '登录成功',
58 | data: {
59 | user: user,
60 | token: this.jwtService.sign(payload)
61 | }
62 | }
63 | }
64 |
65 | async register(user: User): Promise {
66 | const isHave = await this.userRepository.find({ username: user.username })
67 | if (isHave.length) {
68 | return { code: RCode.FAIL, msg: '用户名重复', data: '' }
69 | }
70 | user.avatar = `/avatar/avatar${Math.round(Math.random() * 19 + 1)}.png`
71 | user.role = 'user'
72 | user.userId = user.userId
73 | user.password = md5(user.password)
74 | const newUser = await this.userRepository.save(user)
75 | const payload = { userId: newUser.userId }
76 | // 默认加入群组
77 | await this.groupUserRepository.save({
78 | userId: newUser.userId,
79 | groupId: defaultGroupId
80 | })
81 | // 默认添加机器人为好友
82 | await this.userMapRepository.save({
83 | userId: newUser.userId,
84 | friendId: defaultRobotId
85 | })
86 | // 机器人欢迎语(默认留言)
87 | await this.friendMessageRepository.save({
88 | userId: defaultRobotId,
89 | friendId: newUser.userId,
90 | content: defaultWelcomeMessage,
91 | messageType: 'text',
92 | time: new Date().valueOf()
93 | })
94 | return {
95 | msg: '注册成功',
96 | data: {
97 | user: newUser,
98 | token: this.jwtService.sign(payload)
99 | }
100 | }
101 | }
102 |
103 | /**
104 | * 获取当前token携带信息
105 | * jwt token
106 | * @param authorization
107 | */
108 | verifyUser(token): User {
109 | if (!token) return null
110 | const user = jwt.verify(token, jwtConstants.secret) as User
111 | return user
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/server/src/modules/auth/constants.ts:
--------------------------------------------------------------------------------
1 | export const jwtConstants = {
2 | secret: 'tyloo-chat'
3 | }
4 |
--------------------------------------------------------------------------------
/server/src/modules/auth/jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Strategy, ExtractJwt } from 'passport-jwt'
2 | import { Injectable } from '@nestjs/common'
3 | import { PassportStrategy } from '@nestjs/passport'
4 | import { jwtConstants } from './constants'
5 | import { User } from '../user/entity/user.entity'
6 | import { InjectRepository } from '@nestjs/typeorm'
7 | import { Repository } from 'typeorm'
8 |
9 | @Injectable()
10 | export class JwtStrategy extends PassportStrategy(Strategy) {
11 | constructor(
12 | @InjectRepository(User)
13 | private readonly userRepository: Repository
14 | ) {
15 | super({
16 | jwtFromRequest: ExtractJwt.fromHeader('token'),
17 | ignoreExpiration: false,
18 | secretOrKey: jwtConstants.secret
19 | })
20 | }
21 |
22 | async validate(payload: User) {
23 | const user = this.userRepository.findOne({
24 | userId: payload.userId,
25 | password: payload.password
26 | })
27 | if (!user) {
28 | return false
29 | }
30 | return { username: payload.username, password: payload.password }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/src/modules/auth/local.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Strategy } from 'passport-local'
2 | import { PassportStrategy } from '@nestjs/passport'
3 | import { Injectable } from '@nestjs/common'
4 | import { AuthService } from './auth.service'
5 |
6 | @Injectable()
7 | export class LocalStrategy extends PassportStrategy(Strategy) {
8 | constructor(private readonly authService: AuthService) {
9 | super()
10 | }
11 |
12 | async validate(username: string, password: string): Promise {
13 | if (!username || !password) {
14 | return false
15 | }
16 | return { username, password }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/src/modules/chat/chat.module.ts:
--------------------------------------------------------------------------------
1 | import { GroupModule } from './../group/group.module'
2 | import {
3 | defaultGroup as defaultGroupName,
4 | defaultGroupId,
5 | defaultRobotId
6 | } from './../../common/constant/global'
7 | import { AuthModule } from './../auth/auth.module'
8 | import { Module } from '@nestjs/common'
9 | import { TypeOrmModule } from '@nestjs/typeorm'
10 | import { ChatGateway } from './chat.gateway'
11 | import { User } from '../user/entity/user.entity'
12 | import { Group, GroupMap } from '../group/entity/group.entity'
13 | import { GroupMessage } from '../group/entity/groupMessage.entity'
14 | import { UserMap } from '../friend/entity/friend.entity'
15 | import { FriendMessage } from '../friend/entity/friendMessage.entity'
16 | import { InjectRepository } from '@nestjs/typeorm'
17 | import { Repository } from 'typeorm'
18 | import { defaultRobot } from 'src/common/constant/global'
19 | import { md5 } from 'src/common/tool/utils'
20 |
21 | @Module({
22 | imports: [
23 | TypeOrmModule.forFeature([
24 | User,
25 | Group,
26 | GroupMap,
27 | GroupMessage,
28 | UserMap,
29 | FriendMessage
30 | ]),
31 | AuthModule,
32 | GroupModule
33 | ],
34 | providers: [ChatGateway]
35 | })
36 | export class ChatModule {
37 | constructor(
38 | @InjectRepository(Group)
39 | private readonly groupRepository: Repository,
40 | @InjectRepository(User)
41 | private readonly userRepository: Repository,
42 | @InjectRepository(GroupMap)
43 | private readonly groupUserRepository: Repository
44 | ) {}
45 | async onModuleInit() {
46 | // 默认新增群组 用户问题反馈群
47 | const defaultGroup = await this.groupRepository.findOne({
48 | groupId: defaultGroupId
49 | })
50 | if (!defaultGroup) {
51 | await this.groupRepository.save({
52 | groupId: defaultGroupId,
53 | groupName: defaultGroupName,
54 | userId: defaultRobotId, // 群主默认为智能助手
55 | createTime: new Date().valueOf()
56 | })
57 | console.log('create default group ' + defaultGroupName)
58 | // 机器人默认加入群组
59 | await this.groupUserRepository.save({
60 | userId: defaultRobotId,
61 | groupId: defaultGroupId
62 | })
63 | }
64 |
65 | // 默认新建机器人
66 | const defaultRobotArr = await this.userRepository.find({
67 | username: defaultRobot
68 | })
69 | if (!defaultRobotArr.length) {
70 | await this.userRepository.save({
71 | userId: 'robot',
72 | username: defaultRobot,
73 | avatar: '/avatar/robot.png',
74 | role: 'robot',
75 | tag: '',
76 | status: 'on',
77 | createTime: new Date().valueOf(),
78 | password: md5('robot')
79 | })
80 | console.log('create default robot ' + defaultRobot)
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/server/src/modules/chat/index.d.ts:
--------------------------------------------------------------------------------
1 | // 群组
2 | interface GroupDto {
3 | groupId: string
4 | userId: string // 群主id
5 | groupName: string
6 | notice: string
7 | messages?: GroupMessageDto[]
8 | members?: FriendDto[]
9 | createTime: number
10 | }
11 |
12 | // 群消息
13 | interface GroupMessageDto {
14 | _id: number
15 | userId: string
16 | groupId: string
17 | content: string
18 | width?: number
19 | height?: number
20 | messageType: string
21 | time: number
22 | fileName?: string // 附件名称
23 | size?: number // 附件大小
24 | }
25 |
26 | // 好友
27 | interface FriendDto {
28 | userId: string
29 | username: string
30 | avatar: string
31 | role?: string
32 | tag?: string
33 | messages?: FriendMessageDto[]
34 | createTime: number
35 | online?: 1 | 0 // 是否在线
36 | isManager?: 1 | 0 // 是否为群主
37 | }
38 |
39 | // 好友消息
40 | interface FriendMessageDto {
41 | _id: number
42 | userId: string
43 | friendId: string
44 | content: string
45 | width?: number
46 | height?: number
47 | messageType: string
48 | time: number
49 | fileName?: string // 附件名称
50 | size?: number // 附件大小
51 | }
52 |
53 | // 自定义好友DTO
54 | interface UserFriendMap {
55 | userId: string
56 | friendId: string
57 | friendUserName: string
58 | }
59 |
60 | // 邀请好友入群DTO
61 | interface FriendsIntoGroup {
62 | friendIds: string[] // 被邀请人
63 | groupId: string // 群组ID
64 | userId: string // 邀请人
65 | }
66 |
--------------------------------------------------------------------------------
/server/src/modules/friend/entity/friend.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 |
3 | @Entity()
4 | export class UserMap {
5 | @PrimaryGeneratedColumn()
6 | _id: number
7 |
8 | @Column()
9 | friendId: string
10 |
11 | @Column()
12 | userId: string
13 | }
14 |
--------------------------------------------------------------------------------
/server/src/modules/friend/entity/friendMessage.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 |
3 | @Entity()
4 | export class FriendMessage {
5 | @PrimaryGeneratedColumn()
6 | _id: number
7 |
8 | @Column()
9 | userId: string
10 |
11 | @Column()
12 | friendId: string
13 |
14 | @Column()
15 | content: string
16 |
17 | @Column()
18 | messageType: string
19 |
20 | @Column('double')
21 | time: number
22 | }
23 |
--------------------------------------------------------------------------------
/server/src/modules/friend/friend.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Query, UseGuards } from '@nestjs/common'
2 | import { FriendService } from './friend.service'
3 | import { AuthGuard } from '@nestjs/passport'
4 |
5 | @Controller('friend')
6 | @UseGuards(AuthGuard('jwt'))
7 | export class FriendController {
8 | constructor(private readonly friendService: FriendService) {}
9 |
10 | @Get()
11 | getFriends(@Query('userId') userId: string) {
12 | return this.friendService.getFriends(userId)
13 | }
14 |
15 | @Get('/friendMessages')
16 | getFriendMessage(@Query() query: any) {
17 | return this.friendService.getFriendMessages(
18 | query.userId,
19 | query.friendId,
20 | query.current,
21 | query.pageSize
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/server/src/modules/friend/friend.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { FriendController } from './friend.controller'
3 | import { FriendService } from './friend.service'
4 | import { TypeOrmModule } from '@nestjs/typeorm'
5 | import { UserMap } from './entity/friend.entity'
6 | import { FriendMessage } from './entity/friendMessage.entity'
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([UserMap, FriendMessage])],
10 | controllers: [FriendController],
11 | providers: [FriendService]
12 | })
13 | export class FriendModule {}
14 |
--------------------------------------------------------------------------------
/server/src/modules/friend/friend.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 | import { Repository, getRepository } from 'typeorm'
3 | import { InjectRepository } from '@nestjs/typeorm'
4 | import { UserMap } from './entity/friend.entity'
5 | import { FriendMessage } from './entity/friendMessage.entity'
6 | import { RCode } from 'src/common/constant/rcode'
7 |
8 | @Injectable()
9 | export class FriendService {
10 | constructor(
11 | @InjectRepository(UserMap)
12 | private readonly friendRepository: Repository,
13 | @InjectRepository(FriendMessage)
14 | private readonly friendMessageRepository: Repository
15 | ) {}
16 |
17 | async getFriends(userId: string) {
18 | try {
19 | if (userId) {
20 | return {
21 | msg: '获取用户好友成功',
22 | data: await this.friendRepository.find({ userId: userId })
23 | }
24 | } else {
25 | return {
26 | msg: '获取用户好友失败',
27 | data: await this.friendRepository.find()
28 | }
29 | }
30 | } catch (e) {
31 | return { code: RCode.ERROR, msg: '获取用户好友失败', data: e }
32 | }
33 | }
34 |
35 | async getFriendMessages(
36 | userId: string,
37 | friendId: string,
38 | current: number,
39 | pageSize: number
40 | ) {
41 | const messages = await getRepository(FriendMessage)
42 | .createQueryBuilder('friendMessage')
43 | .orderBy('friendMessage.time', 'DESC')
44 | .where(
45 | 'friendMessage.userId = :userId AND friendMessage.friendId = :friendId',
46 | { userId: userId, friendId: friendId }
47 | )
48 | .orWhere(
49 | 'friendMessage.userId = :friendId AND friendMessage.friendId = :userId',
50 | { userId: userId, friendId: friendId }
51 | )
52 | .skip(current)
53 | .take(pageSize)
54 | .getMany()
55 | return { msg: '', data: { messageArr: messages.reverse() } }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/server/src/modules/group/entity/group.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 |
3 | @Entity()
4 | export class Group {
5 | @PrimaryGeneratedColumn('uuid')
6 | groupId: string
7 |
8 | @Column()
9 | userId: string
10 |
11 | @Column()
12 | groupName: string
13 |
14 | @Column({ default: '群主很懒,没写公告' })
15 | notice: string
16 |
17 | @Column({ type: 'double', default: new Date().valueOf() })
18 | createTime: number
19 | }
20 |
21 | @Entity()
22 | export class GroupMap {
23 | @PrimaryGeneratedColumn()
24 | _id: number
25 |
26 | @Column()
27 | groupId: string
28 |
29 | @Column()
30 | userId: string
31 |
32 | @Column({
33 | type: 'double',
34 | default: new Date().valueOf(),
35 | comment: '进群时间'
36 | })
37 | createTime: number
38 | }
39 |
--------------------------------------------------------------------------------
/server/src/modules/group/entity/groupMessage.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 |
3 | @Entity()
4 | export class GroupMessage {
5 | @PrimaryGeneratedColumn()
6 | _id: number
7 |
8 | @Column()
9 | userId: string
10 |
11 | @Column()
12 | groupId: string
13 |
14 | @Column()
15 | content: string
16 |
17 | @Column()
18 | messageType: string
19 |
20 | @Column('double')
21 | time: number
22 | }
23 |
--------------------------------------------------------------------------------
/server/src/modules/group/group.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Get, Body, Query, UseGuards } from '@nestjs/common'
2 | import { GroupService } from './group.service'
3 | import { AuthGuard } from '@nestjs/passport'
4 |
5 | @Controller('group')
6 | @UseGuards(AuthGuard('jwt'))
7 | export class GroupController {
8 | constructor(private readonly groupService: GroupService) {}
9 |
10 | @Post()
11 | postGroups(@Body('groupIds') groupIds: string) {
12 | return this.groupService.postGroups(groupIds)
13 | }
14 |
15 | @Get('/userGroup')
16 | getUserGroups(@Query('userId') userId: string) {
17 | return this.groupService.getUserGroups(userId)
18 | }
19 |
20 | @Get('/findByName')
21 | getGroupsByName(@Query('groupName') groupName: string) {
22 | return this.groupService.getGroupsByName(groupName)
23 | }
24 |
25 | @Get('/groupMessages')
26 | getGroupMessages(
27 | @Query('userId') userId: string,
28 | @Query('groupId') groupId: string,
29 | @Query('current') current: number,
30 | @Query('pageSize') pageSize: number
31 | ) {
32 | return this.groupService.getGroupMessages(
33 | userId,
34 | groupId,
35 | current,
36 | pageSize
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/server/src/modules/group/group.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common'
2 | import { TypeOrmModule } from '@nestjs/typeorm'
3 | import { GroupService } from './group.service'
4 | import { GroupController } from './group.controller'
5 | import { Group, GroupMap } from './entity/group.entity'
6 | import { GroupMessage } from './entity/groupMessage.entity'
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([Group, GroupMap, GroupMessage])],
10 | providers: [GroupService],
11 | controllers: [GroupController],
12 | exports: [GroupService]
13 | })
14 | export class GroupModule {}
15 |
--------------------------------------------------------------------------------
/server/src/modules/group/group.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 | import { Repository, Like, getRepository } from 'typeorm'
3 | import { InjectRepository } from '@nestjs/typeorm'
4 | import { Group, GroupMap } from './entity/group.entity'
5 | import { GroupMessage } from './entity/groupMessage.entity'
6 | import { RCode } from 'src/common/constant/rcode'
7 | import { User } from '../user/entity/user.entity'
8 | import { defaultGroupMessageTime } from 'src/common/constant/global'
9 |
10 | @Injectable()
11 | export class GroupService {
12 | constructor(
13 | @InjectRepository(Group)
14 | private readonly groupRepository: Repository,
15 | @InjectRepository(GroupMap)
16 | private readonly groupUserRepository: Repository
17 | ) {}
18 |
19 | async postGroups(groupIds: string) {
20 | try {
21 | if (groupIds) {
22 | const groupIdArr = groupIds.split(',')
23 | const groupArr = []
24 | for (const groupId of groupIdArr) {
25 | const data = await this.groupRepository.findOne({ groupId: groupId })
26 | groupArr.push(data)
27 | }
28 | return { msg: '获取群信息成功', data: groupArr }
29 | }
30 | return { code: RCode.FAIL, msg: '获取群信息失败', data: null }
31 | } catch (e) {
32 | return { code: RCode.ERROR, msg: '获取群失败', data: e }
33 | }
34 | }
35 |
36 | async getUserGroups(userId: string) {
37 | try {
38 | let data
39 | if (userId) {
40 | data = await this.groupUserRepository.find({ userId: userId })
41 | return { msg: '获取用户所有群成功', data }
42 | }
43 | data = await this.groupUserRepository.find()
44 | return { msg: '获取系统所有群成功', data }
45 | } catch (e) {
46 | return { code: RCode.ERROR, msg: '获取用户的群失败', data: e }
47 | }
48 | }
49 |
50 | async getGroupMessages(
51 | userId: string,
52 | groupId: string,
53 | current: number,
54 | pageSize: number
55 | ) {
56 | const groupUser = await this.groupUserRepository.findOne({
57 | userId,
58 | groupId
59 | })
60 | const { createTime } = groupUser
61 | let groupMessage = await getRepository(GroupMessage)
62 | .createQueryBuilder('groupMessage')
63 | .orderBy('groupMessage.time', 'DESC')
64 | .where('groupMessage.groupId = :id', { id: groupId })
65 | .andWhere('groupMessage.time >= :createTime', {
66 | createTime: createTime - defaultGroupMessageTime // 新用户进群默认可以看群近24小时消息
67 | })
68 | .skip(current)
69 | .take(pageSize)
70 | .getMany()
71 | groupMessage = groupMessage.reverse()
72 |
73 | const userGather: { [key: string]: User } = {}
74 | let userArr: FriendDto[] = []
75 | for (const message of groupMessage) {
76 | if (!userGather[message.userId]) {
77 | userGather[message.userId] = await getRepository(User)
78 | .createQueryBuilder('user')
79 | .where('user.userId = :id', { id: message.userId })
80 | .getOne()
81 | }
82 | }
83 | userArr = Object.values(userGather)
84 | return { msg: '', data: { messageArr: groupMessage, userArr: userArr } }
85 | }
86 |
87 | async getGroupsByName(groupName: string) {
88 | try {
89 | if (groupName) {
90 | const groups = await this.groupRepository.find({
91 | groupName: Like(`%${groupName}%`)
92 | })
93 | return { data: groups }
94 | }
95 | return { code: RCode.FAIL, msg: '请输入群昵称', data: null }
96 | } catch (e) {
97 | return { code: RCode.ERROR, msg: '查找群错误', data: null }
98 | }
99 | }
100 |
101 | async update(group: GroupDto) {
102 | try {
103 | await this.groupRepository.update(group.groupId, group)
104 | return {
105 | code: RCode.OK,
106 | msg: '修改成功',
107 | data: group
108 | }
109 | } catch (e) {
110 | return { code: RCode.ERROR, msg: '更新失败', data: null }
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/server/src/modules/user/entity/user.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
2 |
3 | @Entity()
4 | export class User {
5 | @PrimaryGeneratedColumn('uuid')
6 | userId: string
7 |
8 | @Column({ default: 'test' })
9 | username: string
10 |
11 | @Column({ default: '123456', select: false })
12 | password: string
13 |
14 | @Column({ default: 'avatar1.png' })
15 | avatar: string
16 |
17 | @Column({ default: 'user' })
18 | role: string
19 |
20 | @Column({ default: 'on' })
21 | status: string
22 |
23 | @Column({ default: '' })
24 | tag: string
25 |
26 | @Column({ type: 'double', default: new Date().valueOf() })
27 | createTime: number
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/modules/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Post,
4 | Get,
5 | Body,
6 | Query,
7 | Patch,
8 | Delete,
9 | UseInterceptors,
10 | UploadedFile,
11 | UseGuards,
12 | Req
13 | } from '@nestjs/common'
14 | import { UserService } from './user.service'
15 | import { AuthService } from './../auth/auth.service'
16 | import { FileInterceptor } from '@nestjs/platform-express'
17 | import { AuthGuard } from '@nestjs/passport'
18 |
19 | @Controller('user')
20 | @UseGuards(AuthGuard('jwt'))
21 | export class UserController {
22 | constructor(
23 | private readonly userService: UserService,
24 | private readonly authService: AuthService
25 | ) {}
26 |
27 | @Get()
28 | getUsers(@Query('userId') userId: string) {
29 | return this.userService.getUser(userId)
30 | }
31 |
32 | @Post()
33 | postUsers(@Body('userIds') userIds: string) {
34 | return this.userService.postUsers(userIds)
35 | }
36 |
37 | @Patch('username')
38 | updateUserName(@Req() req, @Query('username') username) {
39 | const oldUser = this.authService.verifyUser(req.headers.token)
40 | return this.userService.updateUserName(oldUser, username)
41 | }
42 |
43 | @Patch('password')
44 | updatePassword(@Req() req, @Query('password') password) {
45 | const user = this.authService.verifyUser(req.headers.token)
46 | return this.userService.updatePassword(user, password)
47 | }
48 |
49 | @Delete()
50 | delUser(@Req() req, @Query() { did }) {
51 | const user = this.authService.verifyUser(req.headers.token)
52 | return this.userService.delUser(user, did)
53 | }
54 |
55 | @Get('/findByName')
56 | getUsersByName(@Query('username') username: string) {
57 | return this.userService.getUsersByName(username)
58 | }
59 |
60 | @Post('/avatar')
61 | @UseInterceptors(FileInterceptor('avatar'))
62 | setUserAvatar(@Req() req, @UploadedFile() file) {
63 | const user = this.authService.verifyUser(req.headers.token)
64 | return this.userService.setUserAvatar(user, file)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/server/src/modules/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { AuthModule } from './../auth/auth.module'
2 | import { Module } from '@nestjs/common'
3 | import { TypeOrmModule } from '@nestjs/typeorm'
4 | import { User } from '../user/entity/user.entity'
5 | import { UserController } from './user.controller'
6 | import { UserService } from './user.service'
7 | import { Group, GroupMap } from '../group/entity/group.entity'
8 | import { GroupMessage } from '../group/entity/groupMessage.entity'
9 | import { UserMap } from '../friend/entity/friend.entity'
10 | import { FriendMessage } from '../friend/entity/friendMessage.entity'
11 |
12 | @Module({
13 | imports: [
14 | TypeOrmModule.forFeature([
15 | User,
16 | Group,
17 | GroupMap,
18 | GroupMessage,
19 | UserMap,
20 | FriendMessage
21 | ]),
22 | AuthModule
23 | ],
24 | providers: [UserService],
25 | controllers: [UserController]
26 | })
27 | export class UserModule {}
28 |
--------------------------------------------------------------------------------
/server/src/modules/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common'
2 | import { Repository, Like } from 'typeorm'
3 | import { InjectRepository } from '@nestjs/typeorm'
4 | import { User } from './entity/user.entity'
5 | import { Group, GroupMap } from '../group/entity/group.entity'
6 | import { createWriteStream } from 'fs'
7 | import { join } from 'path'
8 | import { RCode } from 'src/common/constant/rcode'
9 | import { GroupMessage } from '../group/entity/groupMessage.entity'
10 | import { UserMap } from '../friend/entity/friend.entity'
11 | import { FriendMessage } from '../friend/entity/friendMessage.entity'
12 | import { md5 } from 'src/common/tool/utils'
13 | import { AuthService } from './../auth/auth.service'
14 |
15 | @Injectable()
16 | export class UserService {
17 | constructor(
18 | @InjectRepository(User)
19 | private readonly userRepository: Repository,
20 | @InjectRepository(Group)
21 | private readonly groupRepository: Repository,
22 | @InjectRepository(GroupMap)
23 | private readonly groupUserRepository: Repository,
24 | @InjectRepository(GroupMessage)
25 | private readonly groupMessageRepository: Repository,
26 | @InjectRepository(UserMap)
27 | private readonly friendRepository: Repository,
28 | @InjectRepository(FriendMessage)
29 | private readonly friendMessageRepository: Repository,
30 | private readonly authService: AuthService
31 | ) {}
32 |
33 | async getUser(userId: string) {
34 | try {
35 | let data
36 | if (userId) {
37 | data = await this.userRepository.findOne({
38 | where: { userId: userId }
39 | })
40 | return { msg: '获取用户成功', data }
41 | }
42 | } catch (e) {
43 | return { code: RCode.ERROR, msg: '获取用户失败', data: e }
44 | }
45 | }
46 |
47 | async postUsers(userIds: string) {
48 | try {
49 | if (userIds) {
50 | const userIdArr = userIds.split(',')
51 | const userArr = []
52 | for (const userId of userIdArr) {
53 | if (userId) {
54 | const data = await this.userRepository.findOne({
55 | where: { userId: userId }
56 | })
57 | userArr.push(data)
58 | }
59 | }
60 | return { msg: '获取用户信息成功', data: userArr }
61 | }
62 | return { code: RCode.FAIL, msg: '获取用户信息失败', data: null }
63 | } catch (e) {
64 | return { code: RCode.ERROR, msg: '获取用户信息失败', data: e }
65 | }
66 | }
67 |
68 | async updateUserName(oldUser: User, username: string) {
69 | try {
70 | if (oldUser) {
71 | const isHaveName = await this.userRepository.findOne({
72 | username
73 | })
74 | if (isHaveName) {
75 | return { code: 1, msg: '用户名重复', data: '' }
76 | }
77 | const newUser = await this.userRepository.findOne({
78 | userId: oldUser.userId
79 | })
80 | newUser.username = username
81 | await this.userRepository.update(oldUser.userId, newUser)
82 | return { msg: '更新用户名成功', data: newUser }
83 | }
84 | return { code: RCode.FAIL, msg: '更新失败', data: '' }
85 | } catch (e) {
86 | return { code: RCode.ERROR, msg: '更新用户名失败', data: e }
87 | }
88 | }
89 |
90 | async updatePassword(user: User, password: string) {
91 | try {
92 | if (user) {
93 | const newUser = await this.userRepository.findOne({
94 | userId: user.userId
95 | })
96 | const backUser = JSON.parse(JSON.stringify(newUser))
97 | newUser.password = md5(password)
98 | await this.userRepository.update(user.userId, newUser)
99 | return { msg: '更新用户密码成功', data: backUser }
100 | }
101 | return { code: RCode.FAIL, msg: '更新失败', data: '' }
102 | } catch (e) {
103 | return { code: RCode.ERROR, msg: '更新用户密码失败', data: e }
104 | }
105 | }
106 |
107 | async delUser(user: User, did: string) {
108 | try {
109 | if (user.role === 'admin') {
110 | // 被删用户自己创建的群
111 | const groups = await this.groupRepository.find({ userId: did })
112 | for (const group of groups) {
113 | await this.groupRepository.delete({ groupId: group.groupId })
114 | await this.groupUserRepository.delete({ groupId: group.groupId })
115 | await this.groupMessageRepository.delete({ groupId: group.groupId })
116 | }
117 | // 被删用户加入的群
118 | await this.groupUserRepository.delete({ userId: did })
119 | await this.groupMessageRepository.delete({ userId: did })
120 | // 被删用户好友
121 | await this.friendRepository.delete({ userId: did })
122 | await this.friendRepository.delete({ friendId: did })
123 | await this.friendMessageRepository.delete({ userId: did })
124 | await this.friendMessageRepository.delete({ friendId: did })
125 | await this.userRepository.delete({ userId: did })
126 | return { msg: '用户删除成功' }
127 | }
128 | return { code: RCode.FAIL, msg: '用户删除失败' }
129 | } catch (e) {
130 | return { code: RCode.ERROR, msg: '用户删除失败', data: e }
131 | }
132 | }
133 |
134 | async getUsersByName(username: string) {
135 | try {
136 | if (username) {
137 | const users = await this.userRepository.find({
138 | where: { username: Like(`%${username}%`) }
139 | })
140 | return { data: users }
141 | }
142 | return { code: RCode.FAIL, msg: '请输入用户名', data: null }
143 | } catch (e) {
144 | return { code: RCode.ERROR, msg: '查找用户错误', data: null }
145 | }
146 | }
147 |
148 | async setUserAvatar(user: User, file) {
149 | const newUser = await this.userRepository.findOne({
150 | userId: user.userId
151 | })
152 | if (newUser) {
153 | const random = Date.now() + '&'
154 | const stream = createWriteStream(
155 | join('public/avatar', random + file.originalname)
156 | )
157 | stream.write(file.buffer)
158 | newUser.avatar = `/avatar/${random}${file.originalname}`
159 | await this.userRepository.save(newUser)
160 | return { msg: '修改头像成功', data: newUser }
161 | } else {
162 | return { code: RCode.FAIL, msg: '修改头像失败' }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/server/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing'
2 | import { INestApplication } from '@nestjs/common'
3 | import * as request from 'supertest'
4 | import { AppModule } from './../src/app.module'
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule]
12 | }).compile()
13 |
14 | app = moduleFixture.createNestApplication()
15 | await app.init()
16 | })
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!')
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/server/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/webSocket建立流程.md:
--------------------------------------------------------------------------------
1 |
7 | - websocket流程
8 | - 建立连接
9 | - 用户默认加入userId房间
10 | - 默认加入defaultGroup房间
11 | - 广播给所有其他在线用户,我上线了
12 | - 初始化聊天数据
13 | - socket.emit('chatData', user);
14 | - 获取当前账号所有好友 (friendGather)
15 | - 获取当前账号已加入的群 (groupGather)
16 | - 获取群聊记录 (group.messages)
17 | - 获取私聊记录 (friend.messages)
18 | - 获取群成员信息 (group.members)
19 | - 获取跟当前账号有关的所有用户信息(userGather)
20 | - 包括friendGataher
21 | - 群聊记录中非好友的用户信息
22 | - this.server.to(user.userId).emit('chatData',xxx)
23 | - vuex初始化数据 (handleChatData)
24 | - 建立私聊房间socket连接 (joinFriendSocket)
25 | - 建立群聊房间socket连接 (joinGroupSocket)
26 | - 保存数据
27 | - commit(SET_GROUP_GATHER, group);
28 | - commit(SET_FRIEND_GATHER, friend);
29 | - commit(SET_USER_GATHER, user_);
30 | - 设置/刷新activeRoom
31 | - 默认为DEFAULT_GROUP
32 | - 已有activeRoom则重新赋值activeRoom已便Watch触发监听
33 | - 断开连接
34 | - 广播给所有其他在线用户,我下线了
35 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------