├── .gitignore ├── LICENSE ├── README.md ├── chat ├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── index.html ├── package.json ├── public │ └── favicon.ico ├── src-tauri │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── icon.png │ ├── src │ │ ├── commands.rs │ │ └── main.rs │ └── tauri.conf.json ├── src │ ├── App.vue │ ├── assets │ │ ├── icon │ │ │ ├── iconfont.css │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ └── logo.png │ ├── common │ │ ├── Api.ts │ │ ├── Constans.ts │ │ ├── Request.ts │ │ └── WindowEvent.ts │ ├── env.d.ts │ ├── main.ts │ ├── router │ │ └── index.ts │ ├── script │ │ └── ChatDataFlow.ts │ └── view │ │ ├── ChatView.vue │ │ ├── Login.vue │ │ ├── VideoChatView.vue │ │ └── VideoSelectionView.vue ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── java-server └── chat │ ├── db │ └── chat.sql │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── cn │ │ └── zyx │ │ └── chat │ │ ├── ChatApplication.java │ │ ├── common │ │ ├── annotation │ │ │ ├── Account.java │ │ │ └── JwtToken.java │ │ ├── constant │ │ │ └── SysConstants.java │ │ ├── exception │ │ │ ├── GlobalExceptionHandler.java │ │ │ └── RRException.java │ │ ├── interceptor │ │ │ └── JwtInterceptor.java │ │ └── resolver │ │ │ └── AccountHandlerMethodArgumentResolver.java │ │ ├── config │ │ ├── CorsConfig.java │ │ ├── DruidConfig.java │ │ ├── WebMvcConfig.java │ │ └── WebSocketConfig.java │ │ ├── controller │ │ └── UcAccountController.java │ │ ├── entity │ │ ├── UcAccount.java │ │ └── UcAccountRelation.java │ │ ├── generator │ │ └── CodeGenerator.java │ │ ├── mapper │ │ ├── UcAccountMapper.java │ │ └── UcAccountRelationMapper.java │ │ ├── service │ │ ├── IUcAccountRelationService.java │ │ ├── IUcAccountService.java │ │ └── impl │ │ │ ├── UcAccountRelationServiceImpl.java │ │ │ └── UcAccountServiceImpl.java │ │ ├── utils │ │ ├── DesUtil.java │ │ ├── JwtUtil.java │ │ └── R.java │ │ └── websocket │ │ └── WebSocketServer.java │ └── resources │ ├── application-dev.yml │ ├── application-pro.yml │ ├── application.yml │ └── mapper │ └── chat │ ├── UcAccountMapper.xml │ └── UcAccountRelationMapper.xml └── rust-server ├── Cargo.toml └── src ├── entity.rs ├── jwt.rs ├── main.rs ├── redis.rs ├── server.rs └── session.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 曹建宇 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 | # tauri-chat 2 | 3 | # Vue 3 + TypeScript + Vite 4 | 5 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "preview": "vite preview", 9 | "tauri": "tauri" 10 | }, 11 | "dependencies": { 12 | "@tauri-apps/api": "^1.2.0", 13 | "vue": "^3.2.45", 14 | "vue-router": "^4.1.6" 15 | }, 16 | "devDependencies": { 17 | "@tauri-apps/cli": "^1.2.2", 18 | "@vitejs/plugin-vue": "^4.0.0", 19 | "less": "^4.1.3", 20 | "less-loader": "^11.0.0", 21 | "style-resources-loader": "^1.5.0", 22 | "typescript": "^4.9.3", 23 | "vite": "^4.0.0", 24 | "vue-cli-plugin-style-resources-loader": "^0.1.5", 25 | "vue-tsc": "^1.0.11" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chat/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/public/favicon.ico -------------------------------------------------------------------------------- /chat/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /chat/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chat" 3 | version = "0.1.0" 4 | description = "tauri chat" 5 | authors = ["994Ay"] 6 | license = "" 7 | repository = "" 8 | default-run = "chat" 9 | edition = "2021" 10 | rust-version = "1.59" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.2.1", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.2.2", features = ["api-all", "system-tray"] } 21 | 22 | [features] 23 | # by default Tauri runs in production mode 24 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 25 | default = [ "custom-protocol" ] 26 | # this feature is used for production builds where `devPath` points to the filesystem 27 | # DO NOT remove this 28 | custom-protocol = [ "tauri/custom-protocol" ] 29 | -------------------------------------------------------------------------------- /chat/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /chat/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /chat/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /chat/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /chat/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /chat/src-tauri/src/commands.rs: -------------------------------------------------------------------------------- 1 | use tauri::command; 2 | 3 | use std::{ 4 | fs::{File, OpenOptions}, 5 | io::Write, 6 | path::Path, 7 | }; 8 | 9 | /// 将聊天记录写入文件 10 | #[command] 11 | pub fn save_chat_history(file_path: String, mut data: String) { 12 | let path = Path::new(&file_path); 13 | 14 | let mut file; 15 | if path.exists() { 16 | file = OpenOptions::new() 17 | .append(true) 18 | .open(file_path) 19 | .expect("cannot open file"); 20 | } else { 21 | file = File::create(file_path).expect("create failed"); 22 | } 23 | 24 | data.push('\n'); 25 | 26 | file.write_all(data.as_bytes()).expect("write failed"); 27 | } 28 | -------------------------------------------------------------------------------- /chat/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | mod commands; 7 | use commands::save_chat_history; 8 | use tauri::{CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu}; 9 | 10 | fn main() { 11 | let quit = CustomMenuItem::new("quit".to_string(), "退出"); 12 | let tray_menu = SystemTrayMenu::new().add_item(quit); 13 | 14 | let tray = SystemTray::new().with_menu(tray_menu); 15 | 16 | tauri::Builder::default() 17 | .system_tray(tray) 18 | .on_system_tray_event(|_app, event| match event { 19 | SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { 20 | "quit" => { 21 | std::process::exit(0); 22 | } 23 | _ => {} 24 | }, 25 | _ => {} 26 | }) 27 | .invoke_handler(tauri::generate_handler![save_chat_history,]) 28 | .run(tauri::generate_context!()) 29 | .expect("error while running tauri application"); 30 | } 31 | -------------------------------------------------------------------------------- /chat/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "npm run build", 5 | "beforeDevCommand": "npm run dev", 6 | "devPath": "http://localhost:5173", 7 | "distDir": "../dist" 8 | }, 9 | "package": { 10 | "productName": "chat", 11 | "version": "0.1.0" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "all": true, 16 | "http": { 17 | "scope": ["https://www.zhiyunxiang.cn/*","http://127.0.0.1:6503/*"] 18 | }, 19 | "fs": { 20 | "scope": ["$DOCUMENT/**"] 21 | } 22 | }, 23 | "bundle": { 24 | "active": true, 25 | "category": "DeveloperTool", 26 | "copyright": "", 27 | "deb": { 28 | "depends": [] 29 | }, 30 | "externalBin": [], 31 | "icon": [ 32 | "icons/32x32.png", 33 | "icons/128x128.png", 34 | "icons/128x128@2x.png", 35 | "icons/icon.icns", 36 | "icons/icon.ico" 37 | ], 38 | "identifier": "com.tauri.build", 39 | "longDescription": "", 40 | "macOS": { 41 | "entitlements": null, 42 | "exceptionDomain": "", 43 | "frameworks": [], 44 | "providerShortName": null, 45 | "signingIdentity": null 46 | }, 47 | "resources": [], 48 | "shortDescription": "", 49 | "targets": "all", 50 | "windows": { 51 | "certificateThumbprint": null, 52 | "digestAlgorithm": "sha256", 53 | "timestampUrl": "" 54 | } 55 | }, 56 | "security": { 57 | "csp": null 58 | }, 59 | "updater": { 60 | "active": false 61 | }, 62 | "windows": [ 63 | { 64 | "title": "登录", 65 | "label": "login", 66 | "width": 430, 67 | "height": 330, 68 | "center": true, 69 | "resizable": false, 70 | "decorations": false 71 | } 72 | ], 73 | "systemTray": { 74 | "iconPath": "icons/icon.png", 75 | "iconAsTemplate": true 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /chat/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /chat/src/assets/icon/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 3501067 */ 3 | src: url('iconfont.woff2?t=1659158215386') format('woff2'), 4 | url('iconfont.woff?t=1659158215386') format('woff'), 5 | url('iconfont.ttf?t=1659158215386') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-lcd-full:before { 17 | content: "\e92b"; 18 | } 19 | 20 | .icon-liaotian:before { 21 | content: "\e61f"; 22 | } 23 | 24 | .icon-guanbi:before { 25 | content: "\e63b"; 26 | } 27 | 28 | .icon-jianhao:before { 29 | content: "\e63c"; 30 | } 31 | 32 | .icon-shipin:before { 33 | content: "\e601"; 34 | } -------------------------------------------------------------------------------- /chat/src/assets/icon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src/assets/icon/iconfont.ttf -------------------------------------------------------------------------------- /chat/src/assets/icon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src/assets/icon/iconfont.woff -------------------------------------------------------------------------------- /chat/src/assets/icon/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src/assets/icon/iconfont.woff2 -------------------------------------------------------------------------------- /chat/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caojianyu/tauri-chat/72576fee7be16c340e5fb899cd7865eb4ad043ad/chat/src/assets/logo.png -------------------------------------------------------------------------------- /chat/src/common/Api.ts: -------------------------------------------------------------------------------- 1 | import request from './Request' 2 | 3 | /** 4 | * 登录 5 | * 6 | * @param params 参数 7 | * @returns 8 | */ 9 | export const login = (params: Record) => { 10 | return request.request({ 11 | url: '/login', 12 | method: 'POST', 13 | data: params, 14 | }) 15 | } 16 | 17 | /** 获取账户信息 */ 18 | export const getUserInfo = () => { 19 | return request.request({ 20 | url: '/getUserInfo', 21 | method: 'GET', 22 | }) 23 | } 24 | 25 | /** 获取好友列表 */ 26 | export const getUserList = () => { 27 | return request.request[]>({ 28 | url: '/getUserList', 29 | method: 'GET', 30 | }) 31 | } -------------------------------------------------------------------------------- /chat/src/common/Constans.ts: -------------------------------------------------------------------------------- 1 | 2 | /** websocket消息类型 */ 3 | enum MsgType { 4 | /** 心跳 */ 5 | HEARTBEAT = "heartbeat", 6 | /** 普通消息 */ 7 | MESSAGE = "message", 8 | /** 视频发起消息,视频接收者获取消息 */ 9 | VIDEO_OFFDER = "video-offer", 10 | /** 视频应答消息,视频发起者获取消息 */ 11 | VIDEO_ANSWER = "video-answer", 12 | /** ICE协商 */ 13 | NEW_ICE_CANDIDATE = "new-ice-candidate", 14 | /** 界面已准备好 */ 15 | READY = "ready", 16 | /** 视频接受状态 */ 17 | ACCEPT_STATUS = "accept-status", 18 | /** 挂断 */ 19 | HANG_UP = "hang-up" 20 | } 21 | 22 | /** 事件消息类型 */ 23 | enum EventType { 24 | /** 聊天界面消息接收事件a */ 25 | CHAT_A = "chat-a", 26 | /** 聊天界面消息接收事件b */ 27 | CHAT_B = "chat-b", 28 | /** 视频聊天界面消息接收 */ 29 | VIDEO_CHAT = "video-chat", 30 | /** 视频聊天界面已准备 */ 31 | VIDEO_CHAT_READY = "video-chat-ready", 32 | /** 视频聊天选择事件 */ 33 | VIDEO_SELECTION = "video-selection" 34 | } 35 | 36 | /** rust方法 */ 37 | class RustFn { 38 | /** 保存聊天记录 */ 39 | public static SAVE_CHAT_HISTORY = "save_chat_history"; 40 | } 41 | 42 | /** 页面路径 */ 43 | class PagePath { 44 | /** 主界面,即聊天界面 */ 45 | public static CHAT_VIEW = "#/chat"; 46 | /** 视频接受、挂断选择界面 */ 47 | public static VIDEO_SELECTION_VIEW = "#/videoSelection"; 48 | /** 视频聊天界面 */ 49 | public static VIDEO_CHAT_VIEW = "#/videoChat"; 50 | } 51 | 52 | /** 聊天软件相关配置 */ 53 | class TauriChat { 54 | /** 软件目录 */ 55 | public static DIR = "TauriChat Files"; 56 | /** 聊天记录文件后缀 */ 57 | public static SUFFIX = ".txt"; 58 | } 59 | 60 | export { 61 | MsgType, 62 | EventType, 63 | RustFn, 64 | PagePath, 65 | TauriChat 66 | } -------------------------------------------------------------------------------- /chat/src/common/Request.ts: -------------------------------------------------------------------------------- 1 | /** tauri api */ 2 | import { getClient, HttpVerb, Body, ResponseType } from "@tauri-apps/api/http"; 3 | import { message } from '@tauri-apps/api/dialog'; 4 | 5 | /** 响应类型 */ 6 | type Res = { code: number, data: T, msg: string } 7 | 8 | /** 9 | * 请求相关 10 | * 11 | * @author 994Ay 12 | * @date 2023/1/3 01:14 13 | * @version 1.0.0 14 | */ 15 | class Request { 16 | 17 | // 服务器基础路径 18 | baseURL: string; 19 | 20 | constructor(params: { baseURL: string }) { 21 | this.baseURL = params.baseURL; 22 | } 23 | 24 | /** 25 | * 发起请求 26 | * 27 | * @param params 请求相关参数 28 | * @returns 29 | */ 30 | async request(params: { url: string, method: HttpVerb, query?: Record, data?: Record, headers?: Record }) { 31 | // 基础请求参数 32 | const url = params.url || ''; 33 | const method = params.method || 'GET'; 34 | const query = params.query || {}; 35 | const data = params.data || {}; 36 | const headers = params.headers || {}; 37 | 38 | const token = localStorage.getItem("token"); 39 | if (token) { 40 | headers.token = token; 41 | } 42 | 43 | const withBaseURL = url.indexOf("http") == 0; 44 | // 请求路径 45 | const requestURL = withBaseURL ? url : this.baseURL + url; 46 | 47 | const client = await getClient(); 48 | const response = await client.request>({ 49 | method, 50 | url: requestURL, 51 | query, 52 | body: Body.json(data), 53 | headers, 54 | responseType: ResponseType.JSON 55 | }); 56 | 57 | const { code, msg } = response.data; 58 | if (code == 401) { 59 | // token不能为空,也有可能失效 60 | // 清除本地token 61 | localStorage.removeItem('token'); 62 | } else if (code == 500) { 63 | message(msg || '请求异常', { title: '提示', type: 'error' }); 64 | } 65 | // TODO 更多错误判断 66 | 67 | return response.data; 68 | } 69 | } 70 | 71 | /** 创建请求对象 */ 72 | const request = new Request({ 73 | baseURL: import.meta.env.VITE_APP_BASE_URL 74 | }) 75 | 76 | export default request -------------------------------------------------------------------------------- /chat/src/common/WindowEvent.ts: -------------------------------------------------------------------------------- 1 | import { appWindow } from "@tauri-apps/api/window"; 2 | 3 | /** 最小化 */ 4 | const minimizeWindow = () => { 5 | appWindow.minimize(); 6 | }; 7 | /** 关闭 */ 8 | const closeWindow = () => { 9 | appWindow.close(); 10 | }; 11 | /** 隐藏 */ 12 | const hideWindow = () => { 13 | appWindow.hide(); 14 | }; 15 | 16 | export { 17 | minimizeWindow, 18 | closeWindow, 19 | hideWindow 20 | } -------------------------------------------------------------------------------- /chat/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_BASE_URL: string; 5 | readonly VITE_APP_BASE_NAME: string; 6 | } 7 | 8 | declare module '*.vue' { 9 | import type { DefineComponent } from 'vue' 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 11 | const component: DefineComponent<{}, {}, any> 12 | export default component 13 | } 14 | -------------------------------------------------------------------------------- /chat/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import "./assets/icon/iconfont.css"; 5 | 6 | createApp(App).use(router).mount('#app') 7 | -------------------------------------------------------------------------------- /chat/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | import Login from '../view/Login.vue' 4 | import ChatView from '../view/ChatView.vue' 5 | import VideoChatView from '../view/VideoChatView.vue' 6 | import VideoSelectionView from '../view/VideoSelectionView.vue' 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | component: Login, 12 | }, 13 | { 14 | path: '/chat', 15 | component: ChatView, 16 | }, 17 | { 18 | path: '/videoChat', 19 | component: VideoChatView, 20 | }, 21 | { 22 | path: '/videoSelection', 23 | component: VideoSelectionView, 24 | } 25 | ] 26 | 27 | const router = createRouter({ 28 | history: createWebHashHistory(), 29 | routes, 30 | }) 31 | 32 | export default router 33 | -------------------------------------------------------------------------------- /chat/src/script/ChatDataFlow.ts: -------------------------------------------------------------------------------- 1 | import { ref, reactive, getCurrentInstance, nextTick } from "vue"; 2 | import { invoke } from "@tauri-apps/api/tauri"; 3 | import { WebviewWindow } from "@tauri-apps/api/window"; 4 | import { emit, listen, once } from '@tauri-apps/api/event'; 5 | import { createDir, readTextFile, BaseDirectory } from '@tauri-apps/api/fs'; 6 | import { exists } from '@tauri-apps/api/fs'; 7 | import { documentDir } from '@tauri-apps/api/path'; 8 | 9 | // 接口 10 | import { getUserInfo, getUserList } from "../common/Api"; 11 | import { MsgType, EventType, RustFn, PagePath, TauriChat } from "../common/Constans"; 12 | 13 | /*******************======数据定义-start======*******************/ 14 | /** 用户信息 */ 15 | const user = reactive({ 16 | account: "", 17 | nickname: "", 18 | avatar: "", 19 | }); 20 | /** 消息对象 */ 21 | const msgObj = reactive>({ 22 | msg: "", 23 | msgType: "", 24 | receiver: "", 25 | sender: "", 26 | senderAvatar: "" 27 | }); 28 | /** 好友列表 */ 29 | const userList = ref[]>([]); 30 | /** 聊天历史 */ 31 | const chatHistory = ref[]>([]); 32 | /** 当前选择用户索引 */ 33 | const currentUserIndex = ref(-1); 34 | /** 视频接受状态 */ 35 | const acceptStatus = ref(false); 36 | /*******************======数据定义-end======*******************/ 37 | 38 | /** 聊天记录框dom */ 39 | let record: HTMLDivElement; 40 | /** websocket连接对象 */ 41 | let ws: WebSocket; 42 | 43 | // 初始化 44 | const init = async () => { 45 | // 消息面板dom 46 | const instance = getCurrentInstance(); 47 | record = instance?.refs.record as HTMLDivElement 48 | 49 | // 获取用户信息 50 | const { account, nickname, avatar } = (await getUserInfo()).data; 51 | // 赋值用户信息 52 | // 初始化消息发送者 53 | user.account = msgObj.sender = account; 54 | user.nickname = nickname; 55 | user.avatar = msgObj.senderAvatar = avatar; 56 | 57 | // 查询朋友列表 58 | userList.value = (await getUserList()).data; 59 | 60 | initWebSocket(); 61 | }; 62 | 63 | /** 初始化websocket */ 64 | const initWebSocket = () => { 65 | ws = new WebSocket(`ws://127.0.0.1:6503/chat/${user.account}`); 66 | // ws = new WebSocket(`wss://www.xxx.cn/chat/${user.account}`); 67 | ws.onopen = () => { 68 | // 发送心跳 69 | setInterval(() => { 70 | sendToServer({ 71 | msgType: MsgType.HEARTBEAT 72 | }); 73 | }, 1000 * 60); 74 | }; 75 | 76 | ws.onmessage = (event: MessageEvent) => { 77 | const data: Record = JSON.parse(event.data); 78 | switch (data.msgType) { 79 | case MsgType.MESSAGE: 80 | // 聊天消息 81 | handleChatMsg(data); 82 | break; 83 | case MsgType.VIDEO_OFFDER: 84 | // 接收到offer,处理视频聊天的请求 85 | handleVideoOfferMsg(data); 86 | break; 87 | case MsgType.VIDEO_ANSWER: 88 | // 接收到answer 89 | emit(EventType.VIDEO_CHAT, data); 90 | break; 91 | case MsgType.NEW_ICE_CANDIDATE: 92 | // 协商 93 | emit(EventType.VIDEO_CHAT, data); 94 | break; 95 | case MsgType.HANG_UP: 96 | // 挂断,通过接收状态来关闭某个窗口 97 | if (data.msg) { 98 | emit(EventType.VIDEO_CHAT, data); 99 | } else { 100 | emit(EventType.VIDEO_SELECTION, data); 101 | } 102 | break; 103 | } 104 | }; 105 | 106 | ws.onerror = (event: Event) => { 107 | // websocket出错重连 108 | initWebSocket(); 109 | }; 110 | 111 | } 112 | 113 | /** 114 | * 处理聊天消息 115 | * 116 | * @param data 聊天消息 117 | */ 118 | const handleChatMsg = async (data: Record) => { 119 | // 收到聊天消息,创建目录以保存聊天记录 120 | await createUserDir(); 121 | // 1.表示发送的消息 2.表示接收的消息 122 | data.isSend = 2; 123 | 124 | // 聊天记录文件路径 125 | const filePath = `${await documentDir()}\\${TauriChat.DIR}\\${user.account}\\${data.sender}${TauriChat.SUFFIX}`; 126 | // 保存聊天记录到本地(由于不清楚有没有持续写入的api,所以采用rust写入) 127 | await invoke(RustFn.SAVE_CHAT_HISTORY, { 128 | filePath, 129 | data: JSON.stringify(data), 130 | }); 131 | 132 | // 当前选中好友账户 133 | let selectAccount; 134 | if (currentUserIndex.value != -1) { 135 | // 已选中好友账户 136 | selectAccount = userList.value[currentUserIndex.value].account; 137 | } 138 | 139 | // 判断消息发送者是否是当前选中好友账户,是则查询添加的聊天记录,不是则添加未读消息标识 140 | if (data.sender == selectAccount) { 141 | await getChatHistory(filePath); 142 | } else { 143 | // 找到好友,添加未读标识。表示好友发送消息过来了 144 | const sender = userList.value.find(item => item.account == data.sender); 145 | sender!.unread = true 146 | } 147 | } 148 | 149 | /** 创建用户目录,用于保存聊天信息 */ 150 | const createUserDir = async () => { 151 | // 以账户作为目录名称 152 | const dir = `${TauriChat.DIR}\\${user.account}`; 153 | // 不存在则创建目录 154 | const isExists = await exists(dir, { dir: BaseDirectory.Document }); 155 | if (!isExists) { 156 | await createDir(dir, { dir: BaseDirectory.Document, recursive: true }); 157 | } 158 | } 159 | 160 | /** 161 | * 处理对方发送offer的事件 162 | * 163 | * @param data 收到offer消息 164 | */ 165 | const handleVideoOfferMsg = async (data: Record) => { 166 | // 打开接受与否的窗口 167 | new WebviewWindow("video-selection", { 168 | url: PagePath.VIDEO_SELECTION_VIEW, 169 | width: 300, 170 | height: 200, 171 | decorations: false, 172 | resizable: false, 173 | x: 0, 174 | y: 200 175 | }); 176 | 177 | // 接收视频界面已准备 178 | const unlisten = await listen>(EventType.CHAT_B, async (event) => { 179 | const payload = event.payload; 180 | switch (payload.msgType) { 181 | case MsgType.READY: 182 | emit(EventType.VIDEO_SELECTION, { 183 | msg: data.sender 184 | }); 185 | break; 186 | case MsgType.ACCEPT_STATUS: 187 | if (payload.msg) { 188 | acceptStatus.value = payload.msg; 189 | // 接受则打开视频界面 190 | videoCall(false, data); 191 | } else { 192 | // 向请求方发送挂断的消息 193 | sendToServer({ 194 | msgType: MsgType.HANG_UP, 195 | receiver: data.sender, 196 | msg: acceptStatus.value 197 | }); 198 | } 199 | 200 | // 不管接受还是挂断都解除监听并关闭界面,因为此次会话已经结束 201 | await unlisten(); 202 | break; 203 | } 204 | }) 205 | } 206 | 207 | /** 208 | * 发送json数据至服务器 209 | * 210 | * @param data 传输的json数据对象 211 | */ 212 | const sendToServer = (data: Record) => { 213 | const json = JSON.stringify(data); 214 | ws.send(json); 215 | } 216 | 217 | /** 发送消息 */ 218 | const sendMsg = async () => { 219 | // 消息和接收者不能为空 220 | if (!msgObj.msg || !msgObj.receiver) { 221 | return; 222 | } 223 | 224 | msgObj.msgType = MsgType.MESSAGE; 225 | 226 | sendToServer(msgObj); 227 | await createUserDir(); 228 | 229 | msgObj.isSend = 1; 230 | const filePath = `${await documentDir()}\\${TauriChat.DIR}\\${user.account}\\${msgObj.receiver}${TauriChat.SUFFIX}`; 231 | // 保存聊天记录 232 | await invoke(RustFn.SAVE_CHAT_HISTORY, { 233 | filePath, 234 | data: JSON.stringify(msgObj), 235 | }); 236 | 237 | // 消息发送完成,置空消息 238 | msgObj.msg = ""; 239 | 240 | await getChatHistory(filePath); 241 | }; 242 | 243 | /** 244 | * 选择用户 245 | * 246 | * @param index 用户当前索引 247 | */ 248 | const selectUser = async (index: number) => { 249 | currentUserIndex.value = index; 250 | // 未读消息标签不再显示 251 | userList.value[index].unread = false; 252 | // 选中用户则是消息接收者 253 | msgObj.receiver = userList.value[index].account; 254 | 255 | const filePath = `${TauriChat.DIR}\\${user.account}\\${msgObj.receiver}${TauriChat.SUFFIX}`; 256 | // 判断聊天记录文件是否存在,不存在则不查询 257 | const isExists = await exists(filePath, { dir: BaseDirectory.Document }); 258 | if (isExists) { 259 | await getChatHistory(filePath); 260 | // 滚动条置于底部 261 | record.scrollTop = record.scrollHeight; 262 | } else { 263 | chatHistory.value = []; 264 | } 265 | }; 266 | 267 | /** 268 | * 获取与相关好友的聊天记录 269 | * 270 | * @param filePath 聊天记录文件路径 271 | */ 272 | const getChatHistory = async (filePath: string) => { 273 | // TODO 在发布版本下,不应直接读取全部,而且根据每天产生的聊天记录来读取 274 | // 读取到聊天记录 275 | const contents = await readTextFile(filePath, { dir: BaseDirectory.Document }); 276 | const history: Record[] = []; 277 | // 聊天记录是以行来记录的,所以直接分割成为数组,并且由于每行聊天记录保存的是json字符串,所以还需要解析成对象 278 | const data = contents.split("\n"); 279 | data.forEach(item => { 280 | if (item) { 281 | history.push(JSON.parse(item)) 282 | } 283 | }) 284 | // 数据处理完成,进行赋值 285 | chatHistory.value = history; 286 | 287 | // 数组渲染完成后将dom滚动条位置固定在底部 288 | nextTick(() => { 289 | scrollBar(); 290 | }); 291 | }; 292 | 293 | /** 滑动滚动条 */ 294 | const scrollBar = () => { 295 | record?.scrollTo({ 296 | top: record.scrollHeight, 297 | left: 0, 298 | behavior: "smooth", 299 | }); 300 | }; 301 | 302 | /** 303 | * 打开视频界面 304 | * 305 | * @param isSend 是否是发送人 306 | * @param data 接收发起者的数据 | 主动发起则忽略 307 | */ 308 | const videoCall = async (isSend: boolean, data?: Record) => { 309 | // 新窗口-视频界面 310 | new WebviewWindow(EventType.VIDEO_CHAT, { 311 | url: PagePath.VIDEO_CHAT_VIEW, 312 | minWidth: 800, 313 | minHeight: 600, 314 | decorations: false, 315 | }); 316 | 317 | // 接收视频界面已准备好事件 318 | const unlisten = await once(EventType.VIDEO_CHAT_READY, async (event) => { 319 | if (isSend) { 320 | // 发起者发起offer 321 | await emit(EventType.VIDEO_CHAT, { 322 | msgType: MsgType.READY, // 主页面收到,表示可以向视频界面发送消息了 323 | msg: isSend // 被动接收还是主动发起 324 | }); 325 | } else { 326 | // 接收者处理offer 327 | await emit(EventType.VIDEO_CHAT, data); 328 | } 329 | // 解除监听 330 | unlisten(); 331 | }) 332 | 333 | // TODO 监听解除? 334 | listen>(EventType.CHAT_A, (event) => { 335 | const payload = event.payload; 336 | // 为不影响信息对象数据,直接传输 337 | sendToServer({ 338 | msgType: payload.msgType, 339 | msg: payload.msg, 340 | receiver: payload.receiver || msgObj.receiver, 341 | sender: msgObj.sender 342 | }); 343 | }); 344 | }; 345 | 346 | export { 347 | selectUser, 348 | videoCall, 349 | sendMsg, 350 | init, 351 | } 352 | 353 | export { 354 | user, 355 | msgObj, 356 | currentUserIndex, 357 | chatHistory, 358 | userList 359 | } -------------------------------------------------------------------------------- /chat/src/view/ChatView.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 96 | 97 | 359 | -------------------------------------------------------------------------------- /chat/src/view/Login.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 73 | 74 | 159 | -------------------------------------------------------------------------------- /chat/src/view/VideoChatView.vue: -------------------------------------------------------------------------------- 1 | 215 | 216 | 235 | 236 | 298 | -------------------------------------------------------------------------------- /chat/src/view/VideoSelectionView.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 61 | 62 | 122 | -------------------------------------------------------------------------------- /chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /chat/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /chat/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | -------------------------------------------------------------------------------- /java-server/chat/db/chat.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 80029 7 | Source Host : localhost:3306 8 | Source Schema : chat 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 80029 12 | File Encoding : 65001 13 | 14 | Date: 17/01/2023 00:33:45 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for uc_account 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `uc_account`; 24 | CREATE TABLE `uc_account` ( 25 | `id` int NOT NULL AUTO_INCREMENT, 26 | `account` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '账户', 27 | `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码', 28 | `salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '加密盐', 29 | `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '昵称', 30 | `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像', 31 | `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', 32 | PRIMARY KEY (`id`) USING BTREE 33 | ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; 34 | 35 | -- ---------------------------- 36 | -- Records of uc_account 37 | -- ---------------------------- 38 | INSERT INTO `uc_account` VALUES (1, 'cjy11', '49f561efb400aef4f5413dab30f5cb56', 'CVbwThfbq6UFkQOLjVv6', '994ay', 'https://img0.baidu.com/it/u=2161795608,1145691066&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', '2023-01-10 20:37:10'); 39 | INSERT INTO `uc_account` VALUES (2, 'cjy12', '49f561efb400aef4f5413dab30f5cb56', 'CVbwThfbq6UFkQOLjVv6', '1212', 'https://img0.baidu.com/it/u=4163049905,243564169&fm=253&fmt=auto&app=138&f=JPEG?w=511&h=500', '2023-01-10 20:37:10'); 40 | INSERT INTO `uc_account` VALUES (3, 'cjy13', '49f561efb400aef4f5413dab30f5cb56', 'CVbwThfbq6UFkQOLjVv6', '1313', 'https://img0.baidu.com/it/u=5061270,3548933205&fm=253&fmt=auto&app=138&f=JPEG?w=501&h=500', '2023-01-10 20:37:10'); 41 | 42 | -- ---------------------------- 43 | -- Table structure for uc_account_relation 44 | -- ---------------------------- 45 | DROP TABLE IF EXISTS `uc_account_relation`; 46 | CREATE TABLE `uc_account_relation` ( 47 | `id` int NOT NULL AUTO_INCREMENT, 48 | `account_id` int NULL DEFAULT NULL COMMENT '用户id(发起人)', 49 | `friend_id` int NULL DEFAULT NULL COMMENT '好友id(被接收人)', 50 | `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', 51 | PRIMARY KEY (`id`) USING BTREE 52 | ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; 53 | 54 | -- ---------------------------- 55 | -- Records of uc_account_relation 56 | -- ---------------------------- 57 | INSERT INTO `uc_account_relation` VALUES (1, 1, 2, '2023-01-11 22:17:30'); 58 | INSERT INTO `uc_account_relation` VALUES (2, 2, 3, '2023-01-11 22:17:40'); 59 | INSERT INTO `uc_account_relation` VALUES (3, 3, 1, '2023-01-11 22:17:47'); 60 | 61 | SET FOREIGN_KEY_CHECKS = 1; 62 | -------------------------------------------------------------------------------- /java-server/chat/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.7 9 | 10 | 11 | cn.zyx 12 | chat 13 | 0.0.1-SNAPSHOT 14 | chat 15 | Demo project for Spring Boot 16 | 17 | 1.8 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-websocket 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | 30 | com.auth0 31 | java-jwt 32 | 3.11.0 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-redis 38 | 39 | 40 | 41 | org.projectlombok 42 | lombok 43 | 1.18.4 44 | 45 | 46 | mysql 47 | mysql-connector-java 48 | 8.0.28 49 | 50 | 51 | 52 | com.alibaba 53 | fastjson 54 | 1.2.83 55 | 56 | 57 | com.alibaba 58 | druid 59 | 1.2.15 60 | 61 | 62 | 63 | org.apache.velocity 64 | velocity-engine-core 65 | 2.2 66 | 67 | 68 | 69 | com.baomidou 70 | mybatis-plus-boot-starter 71 | 3.4.0 72 | 73 | 74 | 75 | com.baomidou 76 | mybatis-plus-generator 77 | 3.4.0 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-starter-test 82 | test 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-maven-plugin 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/ChatApplication.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @MapperScan("cn.zyx.chat.mapper") 8 | @SpringBootApplication 9 | public class ChatApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ChatApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/common/annotation/Account.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.common.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author caojianyu 10 | * @version 1.0.0 11 | * @date 2020-11-11 0:19 12 | * @email jieni_cao@foxmail.com 13 | */ 14 | @Target(ElementType.PARAMETER) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface Account { 17 | } 18 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/common/annotation/JwtToken.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.common.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author caojianyu 10 | * @version 1.0.0 11 | * @date 2020-11-10 22:08 12 | * @email jieni_cao@foxmail.com 13 | */ 14 | @Target({ElementType.METHOD, ElementType.TYPE}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface JwtToken { 17 | boolean required() default true; 18 | } 19 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/common/constant/SysConstants.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.common.constant; 2 | 3 | /** 4 | * @author caojianyu 5 | * @version 1.0.0 6 | * @date 2020-11-11 0:24 7 | * @email jieni_cao@foxmail.com 8 | */ 9 | public interface SysConstants { 10 | 11 | /** 12 | * Account id 13 | */ 14 | String ACCOUNT_KEY = "accountId"; 15 | 16 | /** 17 | * Account token key 18 | */ 19 | String AUTHORIZATION = "token"; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/common/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.common.exception; 2 | 3 | import cn.zyx.chat.utils.R; 4 | import org.springframework.web.bind.annotation.ExceptionHandler; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | import org.springframework.web.bind.annotation.RestControllerAdvice; 7 | 8 | /** 9 | * @author caojianyu 10 | * @version 1.0.0 11 | * @date 2020-11-10 23:46 12 | * @email jieni_cao@foxmail.com 13 | */ 14 | @RestControllerAdvice 15 | public class GlobalExceptionHandler { 16 | 17 | @ResponseBody 18 | @ExceptionHandler(RRException.class) 19 | public R handleException(RRException e) { 20 | String msg = e.getMessage(); 21 | int code = e.getCode(); 22 | if (msg == null || msg.equals("")) { 23 | msg = "服务器出错"; 24 | } 25 | if (code == 100) { 26 | return R.error(code, msg); 27 | } 28 | return R.error(msg); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/common/exception/RRException.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.common.exception; 2 | 3 | /** 4 | * 自定义异常 5 | */ 6 | public class RRException extends RuntimeException { 7 | private static final long serialVersionUID = 1L; 8 | 9 | private String msg; 10 | private int code = 500; 11 | 12 | public RRException(String msg) { 13 | super(msg); 14 | this.msg = msg; 15 | } 16 | 17 | public RRException(String msg, Throwable e) { 18 | super(msg, e); 19 | this.msg = msg; 20 | } 21 | 22 | public RRException(String msg, int code) { 23 | super(msg); 24 | this.msg = msg; 25 | this.code = code; 26 | } 27 | 28 | public RRException(String msg, int code, Throwable e) { 29 | super(msg, e); 30 | this.msg = msg; 31 | this.code = code; 32 | } 33 | 34 | public String getMsg() { 35 | return msg; 36 | } 37 | 38 | public void setMsg(String msg) { 39 | this.msg = msg; 40 | } 41 | 42 | public int getCode() { 43 | return code; 44 | } 45 | 46 | public void setCode(int code) { 47 | this.code = code; 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/common/interceptor/JwtInterceptor.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.common.interceptor; 2 | 3 | import cn.zyx.chat.common.annotation.JwtToken; 4 | import cn.zyx.chat.common.constant.SysConstants; 5 | import cn.zyx.chat.common.exception.RRException; 6 | import cn.zyx.chat.utils.DesUtil; 7 | import cn.zyx.chat.utils.JwtUtil; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.method.HandlerMethod; 11 | import org.springframework.web.servlet.HandlerInterceptor; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.lang.reflect.Method; 16 | 17 | /** 18 | * @author caojianyu 19 | * @version 1.0.0 20 | * @date 2020-11-10 23:01 21 | * @email jieni_cao@foxmail.com 22 | */ 23 | @Component 24 | public class JwtInterceptor implements HandlerInterceptor { 25 | 26 | @Autowired 27 | private JwtUtil jwtUtil; 28 | 29 | @Override 30 | public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) { 31 | // 从http请求头中取出token 32 | String token = httpServletRequest.getHeader(SysConstants.AUTHORIZATION); 33 | // 如果不是映射到方法直接通过 34 | if (!(object instanceof HandlerMethod)) { 35 | return true; 36 | } 37 | HandlerMethod handlerMethod = (HandlerMethod) object; 38 | Method method = handlerMethod.getMethod(); 39 | 40 | // 检查有没有需要用户权限的注解 41 | if (method.isAnnotationPresent(JwtToken.class)) { 42 | JwtToken jwtToken = method.getAnnotation(JwtToken.class); 43 | if (jwtToken.required()) { 44 | // 执行认证 45 | if (token == null) { 46 | throw new RRException("请登录"); 47 | } 48 | if (jwtUtil.isTokenExpired(jwtUtil.verify(token).getExpiresAt())) { 49 | throw new RRException("身份信息已过期,请重新登录"); 50 | } 51 | } 52 | 53 | if (token != null) { 54 | // 获取token中的accountId加密串 55 | String accountId = jwtUtil.getAccountByToken(token); 56 | // 解密 57 | accountId = DesUtil.getDecryptString(accountId); 58 | httpServletRequest.setAttribute(SysConstants.ACCOUNT_KEY, accountId); 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/common/resolver/AccountHandlerMethodArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.common.resolver; 2 | 3 | import cn.zyx.chat.common.annotation.Account; 4 | import cn.zyx.chat.common.constant.SysConstants; 5 | import cn.zyx.chat.common.exception.RRException; 6 | import cn.zyx.chat.entity.UcAccount; 7 | import cn.zyx.chat.service.IUcAccountService; 8 | import com.alibaba.fastjson.JSONObject; 9 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.core.MethodParameter; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | import org.springframework.data.redis.core.ValueOperations; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.bind.support.WebDataBinderFactory; 17 | import org.springframework.web.context.request.NativeWebRequest; 18 | import org.springframework.web.context.request.RequestAttributes; 19 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 20 | import org.springframework.web.method.support.ModelAndViewContainer; 21 | 22 | /** 23 | * @author caojianyu 24 | * @version 1.0.0 25 | * @date 2020-11-11 0:19 26 | * @email jieni_cao@foxmail.com 27 | */ 28 | @Component 29 | public class AccountHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { 30 | 31 | @Autowired 32 | private IUcAccountService accountService; 33 | 34 | @Override 35 | public boolean supportsParameter(MethodParameter parameter) { 36 | return parameter.getParameterType().isAssignableFrom(UcAccount.class) && parameter.hasParameterAnnotation(Account.class); 37 | } 38 | 39 | @Override 40 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, 41 | NativeWebRequest request, WebDataBinderFactory factory) { 42 | //获取用户 43 | Object object = request.getAttribute(SysConstants.ACCOUNT_KEY, RequestAttributes.SCOPE_REQUEST); 44 | if (object == null) { 45 | return null; 46 | } 47 | 48 | // 获取用户信息 49 | UcAccount account = accountService.getOne(new LambdaQueryWrapper() 50 | .eq(UcAccount::getId, object) 51 | .select(UcAccount::getId, UcAccount::getAccount, UcAccount::getAvatar, UcAccount::getNickname)); 52 | if (account == null) { 53 | throw new RRException("登录信息已失效,请重新登录", HttpStatus.UNAUTHORIZED.value()); 54 | } 55 | return account; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-2019 人人开源 All rights reserved. 3 | *

4 | * https://www.renren.io 5 | *

6 | * 版权所有,侵权必究! 7 | */ 8 | 9 | package cn.zyx.chat.config; 10 | 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 13 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 14 | 15 | @Configuration 16 | public class CorsConfig implements WebMvcConfigurer { 17 | 18 | @Override 19 | public void addCorsMappings(CorsRegistry registry) { 20 | registry.addMapping("/**") 21 | .allowedOriginPatterns("*") 22 | .allowCredentials(true) 23 | .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") 24 | .maxAge(3600); 25 | } 26 | } -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/config/DruidConfig.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.config; 2 | 3 | import com.alibaba.druid.pool.DruidDataSource; 4 | import com.alibaba.druid.support.http.StatViewServlet; 5 | import com.alibaba.druid.support.http.WebStatFilter; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 8 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | /** 13 | * @author caojianyu 14 | * @version 1.0.0 15 | * @date 2020-11-22 1:43 16 | * @email jieni_cao@foxmail.com 17 | */ 18 | @Configuration 19 | public class DruidConfig { 20 | 21 | @Bean 22 | @ConfigurationProperties(prefix = "spring.datasource") 23 | public DruidDataSource druidDataSource() { 24 | //Druid 数据源配置 25 | return new DruidDataSource(); 26 | } 27 | 28 | /** 29 | * 注册一个StatViewServlet 30 | * 31 | * @return servlet registration bean 32 | */ 33 | @Bean 34 | public ServletRegistrationBean druidStatViewServlet() { 35 | ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean( 36 | new StatViewServlet(), "/druid/*"); 37 | servletRegistrationBean.addInitParameter("loginUsername", "admin"); 38 | servletRegistrationBean.addInitParameter("loginPassword", "admin"); 39 | servletRegistrationBean.addInitParameter("resetEnable", "false"); 40 | return servletRegistrationBean; 41 | } 42 | 43 | /** 44 | * 注册一个:filterRegistrationBean 45 | * 46 | * @return filter registration bean 47 | */ 48 | @Bean 49 | public FilterRegistrationBean druidStatFilter() { 50 | FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean( 51 | new WebStatFilter()); 52 | // 添加过滤规则. 53 | filterRegistrationBean.addUrlPatterns("/*"); 54 | // 添加不需要忽略的格式信息. 55 | filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); 56 | return filterRegistrationBean; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.config; 2 | 3 | import cn.zyx.chat.common.interceptor.JwtInterceptor; 4 | import cn.zyx.chat.common.resolver.AccountHandlerMethodArgumentResolver; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 8 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * @author caojianyu 15 | * @version 1.0.0 16 | * @date 2020-11-30 0:55 17 | * @email jieni_cao@foxmail.com 18 | */ 19 | @Configuration 20 | public class WebMvcConfig implements WebMvcConfigurer { 21 | 22 | @Autowired 23 | private JwtInterceptor jwtInterceptor; 24 | 25 | @Autowired 26 | private AccountHandlerMethodArgumentResolver accountHandlerMethodArgumentResolver; 27 | 28 | /** 29 | * 添加jwt拦截器 30 | * 31 | * @param registry 32 | */ 33 | @Override 34 | public void addInterceptors(InterceptorRegistry registry) { 35 | registry.addInterceptor(jwtInterceptor) 36 | // 拦截所有请求,通过判断是否有 @JwtToken 注解 决定是否需要登录 37 | .addPathPatterns("/app/**"); 38 | } 39 | 40 | @Override 41 | public void addArgumentResolvers(List argumentResolvers) { 42 | argumentResolvers.add(accountHandlerMethodArgumentResolver); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.server.standard.ServerEndpointExporter; 6 | 7 | /** 8 | * @author caojianyu 9 | * @version 1.0.0 10 | * @date 2022-07-12 15:31 11 | * @email jieni_cao@foxmail.com 12 | */ 13 | @Configuration 14 | public class WebSocketConfig { 15 | 16 | @Bean 17 | public ServerEndpointExporter serverEndpointExporter() { 18 | return new ServerEndpointExporter(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/controller/UcAccountController.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.controller; 2 | 3 | import cn.zyx.chat.common.annotation.Account; 4 | import cn.zyx.chat.common.annotation.JwtToken; 5 | import cn.zyx.chat.entity.UcAccount; 6 | import cn.zyx.chat.service.IUcAccountService; 7 | import cn.zyx.chat.utils.JwtUtil; 8 | import cn.zyx.chat.utils.R; 9 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 10 | import org.apache.commons.lang3.RandomStringUtils; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.util.DigestUtils; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import java.time.LocalDateTime; 16 | import java.util.List; 17 | 18 | /** 19 | * @author caojianyu 20 | * @version 1.0.0 21 | * @date 2022-07-14 13:17 22 | * @email jieni_cao@foxmail.com 23 | */ 24 | @RestController 25 | @RequestMapping("/app") 26 | public class UcAccountController { 27 | 28 | @Autowired 29 | private JwtUtil jwtUtil; 30 | @Autowired 31 | private IUcAccountService accountService; 32 | 33 | @PostMapping("/register") 34 | public R register(@RequestBody UcAccount account) { 35 | String salt = RandomStringUtils.randomAlphanumeric(20); 36 | account.setSalt(salt); 37 | account.setPassword(DigestUtils.md5DigestAsHex((account.getPassword() + account.getSalt()).getBytes())); 38 | account.setCreateTime(LocalDateTime.now()); 39 | accountService.save(account); 40 | return R.ok(); 41 | } 42 | 43 | @PostMapping("/login") 44 | public R login(@RequestBody UcAccount account) { 45 | UcAccount ucAccount = accountService.getOne(new LambdaQueryWrapper() 46 | .eq(UcAccount::getAccount, account.getAccount())); 47 | if (ucAccount == null) { 48 | return R.error("用户不存在"); 49 | } 50 | String password = DigestUtils.md5DigestAsHex((account.getPassword() + ucAccount.getSalt()).getBytes()); 51 | if (ucAccount.getPassword().equals(password)) { 52 | String token = jwtUtil.generateToken(ucAccount.getId()); 53 | return R.ok().put("data", token); 54 | } 55 | return R.error("账号或密码错误"); 56 | } 57 | 58 | @JwtToken 59 | @GetMapping("/getUserInfo") 60 | public R getUserInfo(@Account UcAccount account) { 61 | account.setSalt(null); 62 | account.setPassword(null); 63 | return R.ok().put("data", account); 64 | } 65 | 66 | @JwtToken 67 | @GetMapping("/getUserList") 68 | public R getUserList(@Account UcAccount account) { 69 | List userList = accountService.queryFriends(account.getId()); 70 | return R.ok().put("data", userList); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/entity/UcAccount.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import java.time.LocalDateTime; 6 | import java.io.Serializable; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | 10 | /** 11 | *

12 | * 13 | *

14 | * 15 | * @author caojianyu 16 | * @since 2023-01-05 17 | */ 18 | @Data 19 | @EqualsAndHashCode(callSuper = false) 20 | public class UcAccount implements Serializable { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | @TableId(value = "id", type = IdType.AUTO) 25 | private Integer id; 26 | 27 | /** 28 | * 账户 29 | */ 30 | private String account; 31 | 32 | /** 33 | * 密码 34 | */ 35 | private String password; 36 | 37 | /** 38 | * 加密盐 39 | */ 40 | private String salt; 41 | 42 | /** 43 | * 昵称 44 | */ 45 | private String nickname; 46 | 47 | /** 48 | * 头像 49 | */ 50 | private String avatar; 51 | 52 | /** 53 | * 创建时间 54 | */ 55 | private LocalDateTime createTime; 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/entity/UcAccountRelation.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import java.time.LocalDateTime; 6 | import java.io.Serializable; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | 10 | /** 11 | *

12 | * 13 | *

14 | * 15 | * @author caojianyu 16 | * @since 2023-01-05 17 | */ 18 | @Data 19 | @EqualsAndHashCode(callSuper = false) 20 | public class UcAccountRelation implements Serializable { 21 | 22 | private static final long serialVersionUID = 1L; 23 | 24 | @TableId(value = "id", type = IdType.AUTO) 25 | private Integer id; 26 | 27 | /** 28 | * 用户id(发起人) 29 | */ 30 | private Integer accountId; 31 | 32 | /** 33 | * 好友id(被接收人) 34 | */ 35 | private Integer friendId; 36 | 37 | /** 38 | * 创建时间 39 | */ 40 | private LocalDateTime createTime; 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/generator/CodeGenerator.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.generator; 2 | 3 | import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; 4 | import com.baomidou.mybatisplus.core.toolkit.StringPool; 5 | import com.baomidou.mybatisplus.core.toolkit.StringUtils; 6 | import com.baomidou.mybatisplus.generator.AutoGenerator; 7 | import com.baomidou.mybatisplus.generator.InjectionConfig; 8 | import com.baomidou.mybatisplus.generator.config.*; 9 | import com.baomidou.mybatisplus.generator.config.po.TableInfo; 10 | import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; 11 | import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Scanner; 16 | 17 | /** 18 | * @author caojianyu 19 | * @create 2020-06-12 17:32 20 | */ 21 | // 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中 22 | public class CodeGenerator { 23 | 24 | /** 25 | *

26 | * 读取控制台内容 27 | *

28 | */ 29 | public static String scanner(String tip) { 30 | Scanner scanner = new Scanner(System.in); 31 | StringBuilder help = new StringBuilder(); 32 | help.append("请输入" + tip + ":"); 33 | System.out.println(help.toString()); 34 | if (scanner.hasNext()) { 35 | String ipt = scanner.next(); 36 | if (StringUtils.isNotBlank(ipt)) { 37 | return ipt; 38 | } 39 | } 40 | throw new MybatisPlusException("请输入正确的" + tip + "!"); 41 | } 42 | 43 | public static void main(String[] args) { 44 | // 代码生成器 45 | AutoGenerator mpg = new AutoGenerator(); 46 | 47 | // 全局配置 48 | GlobalConfig gc = new GlobalConfig(); 49 | String projectPath = System.getProperty("user.dir"); 50 | gc.setOutputDir(projectPath + "/src/main/java"); 51 | gc.setAuthor("caojianyu"); 52 | gc.setOpen(false); 53 | // gc.setSwagger2(true); 实体属性 Swagger2 注解 54 | mpg.setGlobalConfig(gc); 55 | 56 | // 数据源配置 57 | DataSourceConfig dsc = new DataSourceConfig(); 58 | dsc.setUrl("jdbc:mysql://localhost:3306/chat?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8"); 59 | // dsc.setSchemaName("public"); 60 | dsc.setDriverName("com.mysql.cj.jdbc.Driver"); 61 | dsc.setUsername("root"); 62 | dsc.setPassword("root"); 63 | mpg.setDataSource(dsc); 64 | 65 | // 包配置 66 | PackageConfig pc = new PackageConfig(); 67 | pc.setModuleName(scanner("模块名")); 68 | pc.setParent("cn.zyx"); 69 | mpg.setPackageInfo(pc); 70 | 71 | // 自定义配置 72 | InjectionConfig cfg = new InjectionConfig() { 73 | @Override 74 | public void initMap() { 75 | // to do nothing 76 | } 77 | }; 78 | 79 | // 如果模板引擎是 freemarker 80 | // String templatePath = "/templates/mapper.xml.ftl"; 81 | // 如果模板引擎是 velocity 82 | String templatePath = "/templates/mapper.xml.vm"; 83 | 84 | // 自定义输出配置 85 | List focList = new ArrayList<>(); 86 | // 自定义配置会被优先输出 87 | focList.add(new FileOutConfig(templatePath) { 88 | @Override 89 | public String outputFile(TableInfo tableInfo) { 90 | // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! 91 | return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() 92 | + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; 93 | } 94 | }); 95 | /* 96 | cfg.setFileCreate(new IFileCreate() { 97 | @Override 98 | public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { 99 | // 判断自定义文件夹是否需要创建 100 | checkDir("调用默认方法创建的目录,自定义目录用"); 101 | if (fileType == FileType.MAPPER) { 102 | // 已经生成 mapper 文件判断存在,不想重新生成返回 false 103 | return !new File(filePath).exists(); 104 | } 105 | // 允许生成模板文件 106 | return true; 107 | } 108 | }); 109 | */ 110 | cfg.setFileOutConfigList(focList); 111 | mpg.setCfg(cfg); 112 | 113 | // 配置模板 114 | TemplateConfig templateConfig = new TemplateConfig(); 115 | 116 | // 配置自定义输出模板 117 | //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 118 | // templateConfig.setEntity("templates/entity2.java"); 119 | // templateConfig.setService(); 120 | // templateConfig.setController(); 121 | 122 | templateConfig.setXml(null); 123 | mpg.setTemplate(templateConfig); 124 | 125 | // 策略配置 126 | StrategyConfig strategy = new StrategyConfig(); 127 | strategy.setNaming(NamingStrategy.underline_to_camel); 128 | strategy.setColumnNaming(NamingStrategy.underline_to_camel); 129 | // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!"); 130 | strategy.setEntityLombokModel(true); 131 | strategy.setRestControllerStyle(true); 132 | // 公共父类 133 | // strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!"); 134 | // 写于父类中的公共字段 135 | // strategy.setSuperEntityColumns("id"); 136 | // prose,prose_classify,sys_admin_role,sys_config,sys_menu,sys_role,sys_role_menu,sys_suggest,uc_account,uc_account_like 137 | // goods_group,oms_order,oms_transaction,sys_admin_role,sys_audit_log,sys_config,sys_menu,sys_role,sys_role_menu,sys_suggest,uc_account,uc_address,uc_merchant,uc_merchant_wallet,uc_merchant_wallet_detail 138 | strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); 139 | strategy.setControllerMappingHyphenStyle(true); 140 | strategy.setTablePrefix(pc.getModuleName() + "_"); 141 | mpg.setStrategy(strategy); 142 | mpg.setTemplateEngine(new VelocityTemplateEngine()); 143 | mpg.execute(); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/mapper/UcAccountMapper.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.mapper; 2 | 3 | import cn.zyx.chat.entity.UcAccount; 4 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | *

10 | * Mapper 接口 11 | *

12 | * 13 | * @author caojianyu 14 | * @since 2023-01-04 15 | */ 16 | public interface UcAccountMapper extends BaseMapper { 17 | 18 | List queryFriends(int accountId); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/mapper/UcAccountRelationMapper.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.mapper; 2 | 3 | import cn.zyx.chat.entity.UcAccountRelation; 4 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author caojianyu 12 | * @since 2023-01-05 13 | */ 14 | public interface UcAccountRelationMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/service/IUcAccountRelationService.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.service; 2 | 3 | import cn.zyx.chat.entity.UcAccountRelation; 4 | import com.baomidou.mybatisplus.extension.service.IService; 5 | 6 | /** 7 | *

8 | * 服务类 9 | *

10 | * 11 | * @author caojianyu 12 | * @since 2023-01-05 13 | */ 14 | public interface IUcAccountRelationService extends IService { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/service/IUcAccountService.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.service; 2 | 3 | import cn.zyx.chat.entity.UcAccount; 4 | import com.baomidou.mybatisplus.extension.service.IService; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | *

10 | * 服务类 11 | *

12 | * 13 | * @author caojianyu 14 | * @since 2023-01-04 15 | */ 16 | public interface IUcAccountService extends IService { 17 | 18 | List queryFriends(int accountId); 19 | } 20 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/service/impl/UcAccountRelationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.service.impl; 2 | 3 | import cn.zyx.chat.entity.UcAccountRelation; 4 | import cn.zyx.chat.mapper.UcAccountRelationMapper; 5 | import cn.zyx.chat.service.IUcAccountRelationService; 6 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | *

11 | * 服务实现类 12 | *

13 | * 14 | * @author caojianyu 15 | * @since 2023-01-05 16 | */ 17 | @Service 18 | public class UcAccountRelationServiceImpl extends ServiceImpl implements IUcAccountRelationService { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/service/impl/UcAccountServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.service.impl; 2 | 3 | import cn.zyx.chat.entity.UcAccount; 4 | import cn.zyx.chat.mapper.UcAccountMapper; 5 | import cn.zyx.chat.service.IUcAccountService; 6 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | *

13 | * 服务实现类 14 | *

15 | * 16 | * @author caojianyu 17 | * @since 2023-01-04 18 | */ 19 | @Service 20 | public class UcAccountServiceImpl extends ServiceImpl implements IUcAccountService { 21 | 22 | @Override 23 | public List queryFriends(int accountId) { 24 | return baseMapper.queryFriends(accountId); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/utils/DesUtil.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.utils; 2 | 3 | import sun.misc.BASE64Decoder; 4 | import sun.misc.BASE64Encoder; 5 | 6 | import javax.crypto.Cipher; 7 | import javax.crypto.KeyGenerator; 8 | import java.security.Key; 9 | import java.security.SecureRandom; 10 | 11 | /** 12 | * @author caojianyu 13 | * @version 1.0.0 14 | * @date 2022-12-13 15:45 15 | * @email jieni_cao@foxmail.com 16 | */ 17 | public class DesUtil { 18 | 19 | private static Key key; 20 | 21 | private static final String KEY_STR = "7a138772683a7462b9053d8aede7ff36"; 22 | private static final String CHARSET_NAME = "UTF-8"; 23 | private static final String ALGORITHM = "DES"; 24 | 25 | 26 | static { 27 | try { 28 | //生成DES算法对象 29 | KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM); 30 | //运用SHA1安全策略 31 | SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); 32 | //设置上密钥种子 33 | secureRandom.setSeed(KEY_STR.getBytes()); 34 | //初始化基于SHA1的算法对象 35 | generator.init(secureRandom); 36 | //生成密钥对象 37 | key = generator.generateKey(); 38 | } catch (Exception e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | 43 | 44 | /*** 45 | * 获取加密的信息 46 | * @param str 47 | * @return 48 | */ 49 | public static String getEncryptString(String str) { 50 | //基于BASE64编码,接收byte[]并转换成String 51 | BASE64Encoder encoder = new BASE64Encoder(); 52 | try { 53 | //按utf8编码 54 | byte[] bytes = str.getBytes(CHARSET_NAME); 55 | //获取加密对象 56 | Cipher cipher = Cipher.getInstance(ALGORITHM); 57 | //初始化密码信息 58 | cipher.init(Cipher.ENCRYPT_MODE, key); 59 | //加密 60 | byte[] byteArr = cipher.doFinal(bytes); 61 | //byte[]to encode好的String 并返回 62 | return encoder.encode(byteArr); 63 | } catch (Exception e) { 64 | throw new RuntimeException(e); 65 | } 66 | } 67 | 68 | 69 | /*** 70 | * 获取解密之后的信息 71 | * @param str 72 | * @return 73 | */ 74 | public static String getDecryptString(String str) { 75 | BASE64Decoder decoder = new BASE64Decoder(); 76 | try { 77 | //将字符串decode成byte[] 78 | byte[] bytes = decoder.decodeBuffer(str); 79 | //获取解密对象 80 | Cipher cipher = Cipher.getInstance(ALGORITHM); 81 | //初始化解密信息 82 | cipher.init(Cipher.DECRYPT_MODE, key); 83 | //解密 84 | byte[] byteArr = cipher.doFinal(bytes); 85 | 86 | return new String(byteArr, CHARSET_NAME); 87 | 88 | } catch (Exception e) { 89 | throw new RuntimeException(e); 90 | } 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/utils/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.utils; 2 | 3 | import cn.zyx.chat.common.exception.RRException; 4 | import com.auth0.jwt.JWT; 5 | import com.auth0.jwt.JWTVerifier; 6 | import com.auth0.jwt.algorithms.Algorithm; 7 | import com.auth0.jwt.exceptions.JWTCreationException; 8 | import com.auth0.jwt.exceptions.JWTDecodeException; 9 | import com.auth0.jwt.exceptions.JWTVerificationException; 10 | import com.auth0.jwt.interfaces.DecodedJWT; 11 | import org.springframework.boot.context.properties.ConfigurationProperties; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.Date; 15 | 16 | /** 17 | * @author caojianyu 18 | * @version 1.0.0 19 | * @date 2020-11-11 22:24 20 | * @email jieni_cao@foxmail.com 21 | */ 22 | @ConfigurationProperties(prefix = "zyx.jwt") 23 | @Component 24 | public class JwtUtil { 25 | 26 | private String secret; 27 | private long expire; 28 | 29 | /** 30 | * 生成jwt token 31 | */ 32 | public String generateToken(long accountId) { 33 | String token = null; 34 | //HMAC 35 | try { 36 | Date date = new Date(System.currentTimeMillis() + expire * 1000); 37 | // des 加密 38 | String uid = DesUtil.getEncryptString(String.valueOf(accountId)); 39 | Algorithm algorithm = Algorithm.HMAC256(secret); 40 | token = JWT.create() 41 | .withIssuer(uid) 42 | .withExpiresAt(date) 43 | .sign(algorithm); 44 | } catch (JWTCreationException exception) { 45 | //Invalid Signing configuration / Couldn't convert Claims. 46 | } 47 | return token; 48 | } 49 | 50 | /** 51 | * 根据token获取账户 52 | * 53 | * @param token 54 | * @return 55 | */ 56 | public String getAccountByToken(String token) { 57 | try { 58 | DecodedJWT jwt = JWT.decode(token); 59 | return jwt.getIssuer(); 60 | } catch (JWTDecodeException exception) { 61 | //Invalid token 62 | } 63 | return null; 64 | } 65 | 66 | public DecodedJWT verify(String token) { 67 | try { 68 | Algorithm algorithm = Algorithm.HMAC256(secret); 69 | JWTVerifier verifier = JWT.require(algorithm) 70 | // .withIssuer("auth0") 71 | .build(); //Reusable verifier instance 72 | DecodedJWT jwt = verifier.verify(token); 73 | return jwt; 74 | } catch (JWTVerificationException exception) { 75 | //Invalid signature/claims 76 | throw new RRException("token无效,请重新获取", 100); 77 | } 78 | } 79 | 80 | /** 81 | * token是否过期 82 | * 83 | * @return true:过期 84 | */ 85 | public boolean isTokenExpired(Date expiration) { 86 | return expiration.before(new Date()); 87 | } 88 | 89 | public String getSecret() { 90 | return secret; 91 | } 92 | 93 | public void setSecret(String secret) { 94 | this.secret = secret; 95 | } 96 | 97 | public long getExpire() { 98 | return expire; 99 | } 100 | 101 | public void setExpire(long expire) { 102 | this.expire = expire; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/utils/R.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.utils; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * 返回数据 8 | */ 9 | public class R extends HashMap { 10 | private static final long serialVersionUID = 1L; 11 | 12 | public R() { 13 | put("code", 0); 14 | put("msg", "success"); 15 | } 16 | 17 | public static R error() { 18 | return error(500, "未知异常,请联系管理员"); 19 | } 20 | 21 | public static R error(String msg) { 22 | return error(500, msg); 23 | } 24 | 25 | public static R error(int code, String msg) { 26 | R r = new R(); 27 | r.put("code", code); 28 | r.put("msg", msg); 29 | return r; 30 | } 31 | 32 | public static R ok(String msg) { 33 | R r = new R(); 34 | r.put("msg", msg); 35 | return r; 36 | } 37 | 38 | public static R ok(Map map) { 39 | R r = new R(); 40 | r.putAll(map); 41 | return r; 42 | } 43 | 44 | public static R ok() { 45 | return new R(); 46 | } 47 | 48 | public R put(String key, Object value) { 49 | super.put(key, value); 50 | return this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /java-server/chat/src/main/java/cn/zyx/chat/websocket/WebSocketServer.java: -------------------------------------------------------------------------------- 1 | package cn.zyx.chat.websocket; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | import org.springframework.data.redis.core.ValueOperations; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.websocket.OnClose; 12 | import javax.websocket.OnMessage; 13 | import javax.websocket.OnOpen; 14 | import javax.websocket.Session; 15 | import javax.websocket.server.PathParam; 16 | import javax.websocket.server.ServerEndpoint; 17 | import java.io.IOException; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | 23 | /** 24 | * @author caojianyu 25 | * @version 1.0.0 26 | * @date 2022-07-12 15:33 27 | * @email jieni_cao@foxmail.com 28 | */ 29 | 30 | @Component 31 | @ServerEndpoint(value = "/chat/{account}") 32 | public class WebSocketServer { 33 | 34 | private static StringRedisTemplate stringRedisTemplate; 35 | 36 | @Autowired 37 | public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) { 38 | WebSocketServer.stringRedisTemplate = stringRedisTemplate; 39 | } 40 | 41 | private static final Map clients = new ConcurrentHashMap<>(); 42 | 43 | @OnMessage 44 | public void onMessage(String msg, Session session) throws IOException { 45 | // 收到消息 46 | JSONObject jsonObject = JSONObject.parseObject(msg); 47 | String msgType = jsonObject.getString("msgType"); 48 | // 接收消息类型1心跳、2发送消息 49 | switch (msgType) { 50 | case "heartbeat": 51 | System.out.println("heartbeat."); 52 | break; 53 | default: 54 | // 获取接收者 55 | String receiver = jsonObject.getString("receiver"); 56 | if (StringUtils.isNotEmpty(receiver)) { 57 | // 查询到session 58 | Session receiverSession = clients.get(receiver); 59 | // 如果用户在线则发送消息,不在线则缓存消息 60 | if (receiverSession != null) { 61 | // 发送消息 62 | receiverSession.getBasicRemote().sendText(jsonObject.toJSONString()); 63 | } else { 64 | ValueOperations valueOperations = stringRedisTemplate.opsForValue(); 65 | String key = String.format("%s_unread_msg", receiver); 66 | // 堆积消息 67 | String contents = valueOperations.get(key); 68 | List list; 69 | if (StringUtils.isEmpty(contents)) { 70 | list = new ArrayList<>(); 71 | list.add(jsonObject); 72 | } else { 73 | list = JSONArray.parseArray(contents, JSONObject.class); 74 | list.add(jsonObject); 75 | } 76 | valueOperations.set(key, JSONObject.toJSONString(list)); 77 | } 78 | 79 | } 80 | } 81 | } 82 | 83 | @OnOpen 84 | public void onOpen(@PathParam("account") String account, Session session) throws IOException { 85 | clients.put(account, session); 86 | 87 | // 查询当前用户有无未读消息,有则发送消息并删除未读消息缓存 88 | ValueOperations valueOperations = stringRedisTemplate.opsForValue(); 89 | String key = String.format("%s_unread_msg", account); 90 | String contents = valueOperations.get(key); 91 | if (StringUtils.isNotEmpty(contents)) { 92 | List list = JSONArray.parseArray(contents, String.class); 93 | for (String msg : list) { 94 | session.getBasicRemote().sendText(msg); 95 | } 96 | stringRedisTemplate.delete(key); 97 | } 98 | 99 | System.out.println("当前已有" + clients.size()); 100 | } 101 | 102 | @OnClose 103 | public void onClose(Session session) { 104 | // 查询退出用户 105 | String key = ""; 106 | for (Map.Entry k : clients.entrySet()) { 107 | if (k.getValue().getId().equals(session.getId())) { 108 | key = k.getKey(); 109 | break; 110 | } 111 | } 112 | 113 | clients.remove(key); 114 | 115 | System.out.println("退出一个用户"); 116 | System.out.println("当前还有用户" + clients.size()); 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /java-server/chat/src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | type: com.alibaba.druid.pool.DruidDataSource 4 | url: jdbc:mysql://localhost:3306/chat?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8 5 | username: root 6 | password: root 7 | # mysql-connector-java 版本在6.0以上要用 com.mysql.cj.jdbc.Driver 8 | driver-class-name: com.mysql.cj.jdbc.Driver 9 | # 连接池初始化大小 10 | initial-size: 5 11 | max-active: 20 12 | min-idle: 5 13 | # 配置获取连接等待超时的时间 14 | max-wait: 60000 15 | # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 16 | time-between-eviction-runs-millis: 60000 17 | # 配置一个连接在池中最小生存的时间,单位是毫秒 18 | min-evictable-idle-time-millis: 300000 19 | # 测试连接 20 | test-while-idle: true 21 | test-on-borrow: false 22 | test-on-return: false 23 | # 配置监控统计拦截的filters 24 | filters: stat 25 | # asyncInit是1.1.4中新增加的配置,如果有initialSize数量较多时,打开会加快应用启动时间 26 | async-init: true 27 | # 打开PSCache,并且指定每个连接上PSCache的大小 28 | pool-prepared-statements: true 29 | max-pool-prepared-statement-per-connection-size: 20 -------------------------------------------------------------------------------- /java-server/chat/src/main/resources/application-pro.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | type: com.alibaba.druid.pool.DruidDataSource 4 | url: jdbc:mysql://localhost:3306/chat?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8 5 | username: root 6 | password: root 7 | # mysql-connector-java 版本在6.0以上要用 com.mysql.cj.jdbc.Driver 8 | driver-class-name: com.mysql.cj.jdbc.Driver 9 | # 连接池初始化大小 10 | initial-size: 5 11 | max-active: 20 12 | min-idle: 5 13 | # 配置获取连接等待超时的时间 14 | max-wait: 60000 15 | # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 16 | time-between-eviction-runs-millis: 60000 17 | # 配置一个连接在池中最小生存的时间,单位是毫秒 18 | min-evictable-idle-time-millis: 300000 19 | # 测试连接 20 | test-while-idle: true 21 | test-on-borrow: false 22 | test-on-return: false 23 | # 配置监控统计拦截的filters 24 | filters: stat 25 | # asyncInit是1.1.4中新增加的配置,如果有initialSize数量较多时,打开会加快应用启动时间 26 | async-init: true 27 | # 打开PSCache,并且指定每个连接上PSCache的大小 28 | pool-prepared-statements: true 29 | max-pool-prepared-statement-per-connection-size: 20 -------------------------------------------------------------------------------- /java-server/chat/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | tomcat: 3 | uri-encoding: utf-8 4 | connection-timeout: 5000ms 5 | port: 6503 6 | 7 | spring: 8 | profiles: 9 | active: dev 10 | redis: 11 | # redis数据库索引(默认为0) 12 | database: 0 13 | host: 127.0.0.1 14 | port: 6379 15 | lettuce: 16 | pool: 17 | # 连接池最大连接数(使用负值表示没有限制)默认8 18 | max-active: 8 19 | # 连接池最大阻塞等待时间(使用负值表示没有限制)默认-1 20 | max-wait: -1 21 | # 连接池中的最大空闲连接默认8 22 | max-idle: 8 23 | # 连接池中的最小空闲连接默认0 24 | min-idle: 0 25 | 26 | mybatis-plus: 27 | # 指定sql映射文件位置 28 | mapper-locations: classpath:mapper/chat/*.xml 29 | type-aliases-package: cn.zyx.chat.entity 30 | configuration: 31 | # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 32 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 33 | global-config: 34 | db-config: 35 | logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) 36 | logic-delete-value: 1 # 逻辑已删除值(默认为 1) 37 | logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) 38 | 39 | zyx: 40 | jwt: 41 | # 加密秘钥 42 | secret: 87h273d8w3g89ys8r239ywesoiuqwjlk23s892hks 43 | # token有效时长,7天,单位秒 44 | expire: 604800 45 | header: authorization -------------------------------------------------------------------------------- /java-server/chat/src/main/resources/mapper/chat/UcAccountMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 | -------------------------------------------------------------------------------- /java-server/chat/src/main/resources/mapper/chat/UcAccountRelationMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /rust-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chat_server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | actix = "0.13" 10 | actix-web = "4" 11 | actix-web-actors = "4.1" 12 | 13 | jsonwebtoken = "8" 14 | serde = "1" 15 | serde_json = "1" 16 | futures = "0.3" 17 | 18 | redis = "0.21.5" 19 | uuid = { version = "0.7", features = ["serde", "v4"] } -------------------------------------------------------------------------------- /rust-server/src/entity.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize)] 4 | pub struct R { 5 | pub code: u32, 6 | pub msg: String, 7 | pub data: T, 8 | } 9 | 10 | impl R { 11 | pub fn new(code: u32, msg: String, data: T) -> Self { 12 | Self { 13 | code: code, 14 | msg: msg, 15 | data: data, 16 | } 17 | } 18 | } 19 | 20 | pub fn ok() -> R { 21 | R::new(200, String::from("success"), Default::default()) 22 | } 23 | 24 | pub fn ok_msg(msg: String) -> R { 25 | R::new(200, msg, Default::default()) 26 | } 27 | 28 | pub fn ok_data(data: T) -> R { 29 | R::new(200, String::from("success"), data) 30 | } 31 | 32 | pub fn error() -> R { 33 | R::new(500, String::from("failed"), String::new()) 34 | } 35 | 36 | pub fn error_msg(msg: String) -> R { 37 | R::new(500, msg, String::new()) 38 | } 39 | 40 | #[derive(Serialize, Deserialize)] 41 | pub struct UcAccount { 42 | pub id: String, 43 | pub account: String, 44 | pub avatar: String, 45 | pub password: String, 46 | } 47 | 48 | impl UcAccount { 49 | pub fn new() -> Self { 50 | Self { 51 | id: Default::default(), 52 | account: Default::default(), 53 | avatar: Default::default(), 54 | password: Default::default(), 55 | } 56 | } 57 | 58 | pub fn build(id: String, account: String, avatar: String, password: String) -> Self { 59 | UcAccount { 60 | id, 61 | account, 62 | avatar, 63 | password, 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /rust-server/src/jwt.rs: -------------------------------------------------------------------------------- 1 | use jsonwebtoken::errors::ErrorKind; 2 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | struct Claims { 7 | aud: String, 8 | exp: usize, 9 | } 10 | 11 | pub fn generate_token(account: String) -> String { 12 | let key = b"secret"; 13 | let my_claims = Claims { 14 | aud: account.to_owned(), 15 | exp: 10000000000, 16 | }; 17 | 18 | let token = match encode( 19 | &Header::default(), 20 | &my_claims, 21 | &EncodingKey::from_secret(key), 22 | ) { 23 | Ok(t) => t, 24 | Err(_) => panic!(), // in practice you would return the error 25 | }; 26 | 27 | token 28 | } 29 | 30 | pub fn get_account_by_token(token: &str) -> String { 31 | let claims = verify(token); 32 | claims.aud 33 | } 34 | 35 | fn verify(token: &str) -> Claims { 36 | let key = b"secret"; 37 | let token_data = match decode::( 38 | token, 39 | &DecodingKey::from_secret(key), 40 | &Validation::default(), 41 | ) { 42 | Ok(c) => c, 43 | Err(err) => match *err.kind() { 44 | ErrorKind::InvalidToken => panic!("Token is invalid"), // Example on how to handle a specific error 45 | ErrorKind::InvalidIssuer => panic!("Issuer is invalid"), // Example on how to handle a specific error 46 | _ => panic!("Some other errors"), 47 | }, 48 | }; 49 | 50 | token_data.claims 51 | } 52 | -------------------------------------------------------------------------------- /rust-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use actix_web::{ 3 | get, 4 | http::header::ContentType, 5 | post, 6 | web::{self}, 7 | Error, HttpRequest, HttpResponse, Responder, Result, 8 | }; 9 | use actix_web_actors::ws; 10 | 11 | use uuid::Uuid; 12 | 13 | use entity::{error_msg, ok, ok_data, UcAccount}; 14 | 15 | mod entity; 16 | mod jwt; 17 | mod redis; 18 | 19 | mod server; 20 | mod session; 21 | 22 | #[post("/app/register")] 23 | async fn register(user: web::Json) -> Result { 24 | let uuid = Uuid::new_v4(); 25 | 26 | let account: UcAccount = user.0; 27 | 28 | // TODO encryption 29 | let uc_account: UcAccount = UcAccount { 30 | id: uuid.to_string(), 31 | ..account 32 | }; 33 | 34 | // json serialization 35 | let account_str = serde_json::to_string(&uc_account).unwrap(); 36 | 37 | redis::set(uc_account.account, account_str); 38 | 39 | Ok(web::Json(ok())) 40 | } 41 | 42 | #[post("/app/login")] 43 | async fn login(user: web::Json) -> Result { 44 | let result = redis::get(user.0.account.to_string()); 45 | if !result.is_ok() { 46 | return Ok(web::Json(error_msg(String::from("用户不存在")))); 47 | } 48 | 49 | let account_str = result.unwrap(); 50 | 51 | let account: UcAccount = serde_json::from_str(account_str.as_str()).unwrap(); 52 | 53 | // TODO encryption 54 | if !user.password.eq(account.password.as_str()) { 55 | return Ok(web::Json(error_msg(String::from("账号或密码错误")))); 56 | } 57 | 58 | let token = jwt::generate_token(user.0.account); 59 | Ok(web::Json(ok_data(token))) 60 | } 61 | 62 | #[get("/app/get_account_info")] 63 | async fn get_account_info(req: HttpRequest) -> HttpResponse { 64 | let header = req.headers(); 65 | let token = header.get("token").unwrap(); 66 | 67 | let account = jwt::get_account_by_token(token.to_str().unwrap()); 68 | 69 | let result = redis::get(account); 70 | if !result.is_ok() { 71 | let r = error_msg(String::from("用户不存在")); 72 | let result_str = serde_json::to_string(&r).unwrap(); 73 | return HttpResponse::Ok().body(result_str); 74 | } 75 | 76 | let account_str = result.unwrap(); 77 | 78 | let account: UcAccount = serde_json::from_str(account_str.as_str()).unwrap(); 79 | 80 | let r = ok_data(account); 81 | let result_str = serde_json::to_string(&r).unwrap(); 82 | HttpResponse::Ok() 83 | .content_type(ContentType::json()) 84 | .body(result_str) 85 | } 86 | 87 | async fn chat_route( 88 | req: HttpRequest, 89 | stream: web::Payload, 90 | srv: web::Data>, 91 | ) -> Result { 92 | let path = req.path(); 93 | 94 | // account 95 | let account = &path[6..path.len()]; 96 | 97 | let resp = ws::start( 98 | session::MyWs { 99 | id: 0, 100 | account: account.to_string(), 101 | addr: srv.get_ref().clone(), 102 | }, 103 | &req, 104 | stream, 105 | )?; 106 | 107 | Ok(resp) 108 | } 109 | 110 | #[actix_web::main] 111 | async fn main() -> std::io::Result<()> { 112 | use actix_web::{App, HttpServer}; 113 | 114 | // start chat server actor 115 | let server = server::ChatServer::new().start(); 116 | 117 | HttpServer::new(move || { 118 | App::new() 119 | .app_data(web::Data::new(server.clone())) 120 | .route("/chat/{account}", web::get().to(chat_route)) 121 | .service(register) 122 | .service(login) 123 | .service(get_account_info) 124 | }) 125 | .bind(("0.0.0.0", 6503))? 126 | .run() 127 | .await 128 | } 129 | -------------------------------------------------------------------------------- /rust-server/src/redis.rs: -------------------------------------------------------------------------------- 1 | use redis::Commands; 2 | 3 | const URL: &str = "redis://:your_password@127.0.0.1:6379/"; 4 | 5 | pub fn set(key: String, value: String) { 6 | // connect to redis 7 | // The URL format is redis://[][:@][:port][/] 8 | let client = redis::Client::open(URL).unwrap(); 9 | let mut con = client.get_connection().unwrap(); 10 | let _: () = con.set(key, value).unwrap(); 11 | } 12 | 13 | pub fn get(key: String) -> redis::RedisResult { 14 | let client = redis::Client::open(URL).unwrap(); 15 | let mut con = client.get_connection().unwrap(); 16 | con.get(key) 17 | } -------------------------------------------------------------------------------- /rust-server/src/server.rs: -------------------------------------------------------------------------------- 1 | //! `ChatServer` is an actor. It maintains list of connection client session. 2 | //! And manages available rooms. Peers send messages to other peers in same 3 | //! room through `ChatServer`. 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::HashMap; 7 | 8 | use actix::prelude::*; 9 | 10 | use crate::entity; 11 | use crate::redis; 12 | 13 | /// Chat server sends this messages to session 14 | #[derive(Message)] 15 | #[rtype(result = "()")] 16 | pub struct Message(pub String); 17 | 18 | pub struct ChatServer { 19 | sessions: HashMap>, 20 | } 21 | 22 | impl ChatServer { 23 | pub fn new() -> ChatServer { 24 | ChatServer { 25 | sessions: HashMap::new(), 26 | } 27 | } 28 | } 29 | 30 | #[derive(Serialize, Deserialize)] 31 | struct ReusltData { 32 | msg_type: String, 33 | user_list: Vec, 34 | } 35 | 36 | impl ChatServer { 37 | /// Send message to all users in the room 38 | fn send_message(&self, msg_type: String, receiver: String, message: String) { 39 | match msg_type.as_str() { 40 | "heartbeat" => { 41 | println!("heartbeat"); 42 | } 43 | "user-list" => { 44 | let mut user_list: Vec = Vec::new(); 45 | 46 | for (k, _) in &self.sessions { 47 | let json = redis::get(k.to_string()).unwrap(); 48 | let account = serde_json::from_str(&json).unwrap(); 49 | user_list.push(account); 50 | } 51 | 52 | let client_message = ReusltData { 53 | msg_type, 54 | user_list, 55 | }; 56 | 57 | let result_json = serde_json::to_string(&client_message).unwrap(); 58 | 59 | for (k, _) in &self.sessions { 60 | if let Some(addr) = self.sessions.get(k.as_str()) { 61 | addr.do_send(Message(result_json.to_string())); 62 | } 63 | } 64 | } 65 | _ => { 66 | if let Some(addr) = self.sessions.get(&receiver) { 67 | addr.do_send(Message(message)); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | /// Make actor from `ChatServer` 75 | impl Actor for ChatServer { 76 | /// We are going to use simple Context, we just need ability to communicate 77 | /// with other actors. 78 | type Context = Context; 79 | } 80 | 81 | #[derive(Message)] 82 | #[rtype(usize)] 83 | pub struct Connect { 84 | pub account: String, 85 | pub addr: Recipient, 86 | } 87 | 88 | /// Handler for Connect message. 89 | /// 90 | /// Register new session and assign unique id to this session 91 | impl Handler for ChatServer { 92 | type Result = usize; 93 | 94 | fn handle(&mut self, msg: Connect, _: &mut Context) -> Self::Result { 95 | let account = msg.account; 96 | self.sessions.insert(account, msg.addr); 97 | 98 | self.send_message(String::from("user-list"), String::new(), String::new()); 99 | // send account back 100 | 0 101 | } 102 | } 103 | 104 | /// Session is disconnected 105 | #[derive(Message)] 106 | #[rtype(result = "()")] 107 | pub struct Disconnect { 108 | pub account: String, 109 | } 110 | 111 | /// Handler for Disconnect message. 112 | impl Handler for ChatServer { 113 | type Result = (); 114 | 115 | fn handle(&mut self, msg: Disconnect, _: &mut Context) { 116 | // remove address 117 | self.sessions.remove(&msg.account); 118 | 119 | // send message to other users 120 | self.send_message(String::from("user-list"), String::new(), String::new()); 121 | } 122 | } 123 | 124 | #[derive(Message)] 125 | #[rtype(result = "()")] 126 | #[derive(Debug, Serialize, Deserialize)] 127 | pub struct ClientMessage { 128 | pub msg_type: String, 129 | pub receiver: String, 130 | pub sender: String, 131 | pub msg: String, 132 | pub sender_avatar: String, 133 | } 134 | 135 | /// Handler for Message message. 136 | impl Handler for ChatServer { 137 | type Result = (); 138 | 139 | fn handle(&mut self, msg: ClientMessage, _: &mut Context) { 140 | let msg_type = msg.msg_type.clone(); 141 | match msg_type.as_str() { 142 | "heartbeat" => { 143 | println!("heartbeat.") 144 | } 145 | _ => { 146 | let receiver = msg.receiver.clone(); 147 | let msg_type = msg.msg_type.clone(); 148 | let json = serde_json::to_string(&msg).unwrap(); 149 | self.send_message(msg_type, receiver, json); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /rust-server/src/session.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use actix_web_actors::ws; 3 | 4 | use crate::server; 5 | 6 | #[derive(Debug)] 7 | pub struct MyWs { 8 | pub id: usize, 9 | pub account: String, 10 | pub addr: Addr, 11 | } 12 | 13 | impl Handler for MyWs { 14 | type Result = (); 15 | 16 | fn handle(&mut self, msg: server::Message, ctx: &mut Self::Context) { 17 | ctx.text(msg.0); 18 | } 19 | } 20 | 21 | impl Actor for MyWs { 22 | type Context = ws::WebsocketContext; 23 | 24 | /// Method is called on actor start. 25 | /// We register ws session with ChatServer 26 | fn started(&mut self, ctx: &mut Self::Context) { 27 | // we'll start heartbeat process on session start. 28 | 29 | // register self in chat server. `AsyncContext::wait` register 30 | // future within context, but context waits until this future resolves 31 | // before processing any other events. 32 | // HttpContext::state() is instance of WsChatSessionState, state is shared 33 | // across all routes within application 34 | let addr = ctx.address(); 35 | self.addr 36 | .send(server::Connect { 37 | account: self.account.clone(), 38 | addr: addr.recipient(), 39 | }) 40 | .into_actor(self) 41 | .then(|res, act, ctx| { 42 | match res { 43 | Ok(res) => act.id = res, 44 | // something is wrong with chat server 45 | _ => ctx.stop(), 46 | } 47 | fut::ready(()) 48 | }) 49 | .wait(ctx); 50 | } 51 | 52 | fn stopping(&mut self, _: &mut Self::Context) -> Running { 53 | // notify chat server 54 | self.addr.do_send(server::Disconnect { 55 | account: self.account.to_string(), 56 | }); 57 | Running::Stop 58 | } 59 | } 60 | 61 | /// Handler for ws::Message message 62 | impl StreamHandler> for MyWs { 63 | fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { 64 | match msg { 65 | Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), 66 | Ok(ws::Message::Text(text)) => { 67 | let text = text.to_string(); 68 | 69 | let message: server::ClientMessage = serde_json::from_str(&text).unwrap(); 70 | 71 | self.addr.do_send(message); 72 | } 73 | Ok(ws::Message::Binary(bin)) => ctx.binary(bin), 74 | _ => (), 75 | } 76 | } 77 | } 78 | --------------------------------------------------------------------------------