├── .gitignore ├── LICENSE ├── README.md ├── assets ├── logo-embodied.png └── screenshot │ ├── chat.png │ ├── home.png │ ├── me.png │ ├── message.png │ ├── post.png │ ├── profile.png │ ├── register.png │ └── sign.png ├── client └── app │ ├── .gitignore │ ├── Readme.md │ ├── craco.config.js │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── server │ └── data.json │ └── src │ ├── App.js │ ├── apis │ ├── discover.js │ ├── file.js │ ├── message.js │ ├── post.js │ ├── topic.js │ └── user.js │ ├── assets │ ├── bg_1.jpg │ └── logo-embodied.png │ ├── components │ ├── AuthRoute.js │ ├── Layout.jsx │ ├── TabNavigator │ │ ├── TabNavigator.jsx │ │ ├── TabNavigator.scoped.scss │ │ ├── TabNavigatorV1.jsx │ │ └── TabTransition.scss │ ├── TopBar │ │ └── TopBar.jsx │ ├── Topic │ │ ├── Topic.jsx │ │ └── Topic.scoped.scss │ └── fileUpload.js │ ├── hooks │ ├── useChannelList.jsx │ ├── useUserDetail.jsx │ └── useWebSocket.jsx │ ├── index.js │ ├── index.scss │ ├── pages │ ├── Chat │ │ ├── Chat.jsx │ │ └── Chat.scoped.scss │ ├── Discover │ │ ├── Discover.jsx │ │ └── Discover.scoped.scss │ ├── Follow │ │ ├── Follow.jsx │ │ └── Follow.scoped.scss │ ├── Friends │ │ ├── MyFriends │ │ │ └── MyFriends.jsx │ │ └── NewFriend │ │ │ ├── NewFriend.jsx │ │ │ └── NewFriend.scoped.scss │ ├── Home │ │ ├── Home.jsx │ │ └── Home.scoped.scss │ ├── Login │ │ ├── Login.jsx │ │ └── Login.scoped.scss │ ├── Message │ │ ├── Message.jsx │ │ └── Message.scoped.scss │ ├── NotFound │ │ └── NotFound.jsx │ ├── Post │ │ ├── Post.jsx │ │ └── Post.scoped.scss │ ├── Profile │ │ ├── Bookmark │ │ │ └── Bookmark.jsx │ │ ├── History │ │ │ └── Histtory.jsx │ │ ├── Like │ │ │ └── Like.jsx │ │ ├── MyPost │ │ │ └── MyPost.jsx │ │ ├── MyProfile │ │ │ ├── MyProfile.jsx │ │ │ └── MyProfile.scoped.scss │ │ ├── OtherProfile │ │ │ ├── OtherProfile.jsx │ │ │ └── OtherProfile.scoped.scss │ │ ├── Profile.jsx │ │ └── UserDetail │ │ │ └── UserDetail.jsx │ ├── Register │ │ ├── Register.jsx │ │ └── Register.scoped.scss │ ├── Search │ │ └── Search.jsx │ ├── Test │ │ ├── index.js │ │ └── index.scss │ ├── TopicDetail │ │ ├── TopicDetail.jsx │ │ └── TopicDetail.scss │ └── View │ │ └── View.jsx │ ├── router │ └── index.js │ ├── store │ ├── index.js │ └── modules │ │ └── user.js │ └── utils │ ├── index.js │ ├── request.js │ ├── token.js │ └── user.js ├── readme └── README.zh_CN.md └── server ├── .gitignore ├── ai ├── .gitignore ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── mars │ │ │ └── social │ │ │ └── ai │ │ │ ├── AiApplication.kt │ │ │ ├── api │ │ │ ├── ApiCall.kt │ │ │ └── codeLog.txt │ │ │ ├── chats │ │ │ └── UserModel.kt │ │ │ ├── core │ │ │ └── CoreProcess.kt │ │ │ └── vo │ │ │ └── ChatStruct.kt │ └── resources │ │ └── application.properties │ └── test │ └── kotlin │ └── com │ └── mars │ └── social │ └── ai │ └── AiApplicationTests.kt └── social ├── .gitignore ├── compose.yaml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── kotlin │ └── com │ │ └── mars │ │ └── social │ │ ├── SocialApplication.kt │ │ ├── configuration │ │ ├── CustomInterceptor.kt │ │ ├── KtormConfiguration.kt │ │ ├── WebConfig.kt │ │ └── WebSocketConfig.java │ │ ├── controller │ │ ├── AiController.kt │ │ ├── ApiCall.kt │ │ ├── ChannelsController.kt │ │ ├── ChatController.java │ │ ├── DemoController.kt │ │ ├── DiscoverController.kt │ │ ├── MessageController.kt │ │ ├── TopicController.kt │ │ ├── UserController.kt │ │ ├── WebSocketConnect.java │ │ └── common │ │ │ └── CommonFunction.kt │ │ ├── dto │ │ ├── FileInfo.kt │ │ ├── PageDTO.kt │ │ ├── PageRequest.kt │ │ └── UserInfoDto.kt │ │ ├── interceptor │ │ └── WebSocketInterceptor.java │ │ ├── model │ │ ├── Demo.kt │ │ ├── mix │ │ │ ├── BookMark.kt │ │ │ ├── Channels.kt │ │ │ ├── ChatMessage.java │ │ │ ├── CommentLike.kt │ │ │ ├── File.kt │ │ │ ├── MessageBean.java │ │ │ ├── Messages.kt │ │ │ ├── SocketMessage.java │ │ │ ├── Tag.kt │ │ │ └── TopicShare.kt │ │ ├── topic │ │ │ ├── Topic.kt │ │ │ ├── TopicComment.kt │ │ │ ├── TopicFiles.kt │ │ │ ├── TopicLike.kt │ │ │ ├── TopicTags.kt │ │ │ └── TopicViewHis.kt │ │ └── user │ │ │ ├── Friendship.kt │ │ │ ├── User.kt │ │ │ ├── UserDetail.kt │ │ │ ├── UserFollow.kt │ │ │ └── UserRole.kt │ │ ├── oss │ │ └── minio │ │ │ ├── MinioConfig.kt │ │ │ ├── MinioController.kt │ │ │ └── MinioProperties.kt │ │ ├── security │ │ ├── SaTokenConfigure.kt │ │ └── StpInterfaceImpl.kt │ │ ├── tools │ │ └── AutoGenModel.kt │ │ └── utils │ │ ├── GlobalException.java │ │ ├── GlobalUtils.kt │ │ ├── LocalTimeSerializer.kt │ │ ├── LocaleConfig.java │ │ ├── MessageUtils.kt │ │ ├── PageCalculator.kt │ │ └── R.kt └── resources │ ├── application.yml │ ├── i18n │ ├── messages.properties │ ├── messages_en_US.properties │ └── messages_zh_CN.properties │ ├── script │ ├── ddl.sql │ └── dump-embodied-202407041056.sql │ └── static │ └── favicon.ico └── test └── kotlin └── com └── mars └── social └── SocialApplicationTests.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /.idea 3 | /server/.idea 4 | /client/app/yarn.lock 5 | /server/social/.mvn 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mars 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 | 2 | 3 | # Embodied 4 | 5 | # 化身 6 | 7 | ## 项目介绍 8 | 9 | 这是一个探索学习项目,旨在尝试使用kotlin+ktorm+mysql以及React+react-vant来进行开发移动社交Apps。 10 | 11 | 从零开始,一步步的搭建属于你的facebook,instagram,twitter,微博,小红书 whatever etc。 12 | 13 | 主要包含了用户登录注册,个人信息编辑,发布主题信息的发布,社交好友添加,消息发送,点赞收藏关注等功能。逐步完善当中。 14 | 15 | 由于开发过程中并未充分考虑安全防护问题,并不建议将该项目用于生产环境,仅做学习交流用,欢迎各位大佬提出宝贵意见。 16 | 17 | PS:多语言目前仅实现了部分技术方案,并未全局支持。 18 | 19 | ## Introduction 20 | 21 | This is an exploratory learning project aimed at trying to develop mobile social apps using kotlin+ktorm+mysql and React+react-vant from scratch, step by step, to build your own facebook, instagram, twitter, Weibo, Xiaohongshu, and more. 22 | 23 | It mainly includes user login and registration, personal information editing, posting theme information, adding social friends, message sending, liking, collecting, following, and other functions. It is gradually being improved. 24 | 25 | Since security protection issues were not fully considered during the development process, it is not recommended to use this project in a production environment. It is only for learning and communication purposes. Welcome all experts to provide valuable feedback. 26 | 27 | PS: Currently, multi-language support has only been partially implemented using certain technical solutions and is not globally supported yet. 28 | 29 | ## 功能介绍截图(建设中,还会优化) 30 | 31 | ### 登录页及注册页面 32 | #### 认证-登录 33 | 34 | 35 | #### 认证-注册 36 | 37 | 38 | ### 主页 39 | 40 | 41 | ### 我的 42 | 43 | 44 | ### 聊天 45 | 46 | 47 | ## Function Introduction Screenshots (Under Construction, will be optimized) 48 | 49 | ### Signin Page and Registration Page 50 | #### Authentication - Signin 51 | 52 | 53 | #### Authentication - Register 54 | 55 | 56 | ### Home 57 | 58 | 59 | ### Me 60 | 61 | 62 | ### Chat 63 | 64 | 65 | ## 技术栈 66 | 67 | ### 前端技术栈 68 | * React 69 | * react-vant 70 | * axios 71 | * Redux 72 | * WebSocket 73 | * normalize 74 | * react-scoped-css 75 | 76 | ### 后端技术栈 77 | * Kotlin 78 | * Spring Boot 79 | * Maven 80 | * Ktorm 81 | * Sa-Token 82 | * WebSocket 83 | * Druid 84 | * OSS 85 | * minio 86 | 87 | ## Introduction 88 | 89 | ### FrontEnd Technologies 90 | * React 91 | * react-vant 92 | * axios 93 | * Redux 94 | * normalize 95 | * react-scoped-css 96 | 97 | ### BackEnd Technology 98 | * Kotlin 99 | * Spring Boot 100 | * Maven 101 | * Ktorm 102 | * Sa-Token 103 | * WebSocket 104 | * Druid 105 | * OSS 106 | * minio 107 | 108 | 109 | ## 功能规划 110 | 111 | - [ ] I18n多语言支持 112 | - [x] 消息模块。 113 | - [x] 话题模块。 114 | - [x] 用户模块,用户注册,用户登录,用户详细信息等。 115 | - [x] UI原型设计,接口,数据库 116 | - [x] 头脑风暴,项目原型设计模块设计。 117 | 118 | # Feature Planning 119 | - [ ] Multi-language support (I18n) 120 | - [x] Messaging module 121 | - [x] Topic module 122 | - [x] User module, including user registration, user login, user profile, etc. 123 | - [x] UI prototype design, interfaces, and database 124 | - [x] Brainstorming, project prototype design module design 125 | -------------------------------------------------------------------------------- /assets/logo-embodied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/logo-embodied.png -------------------------------------------------------------------------------- /assets/screenshot/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/chat.png -------------------------------------------------------------------------------- /assets/screenshot/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/home.png -------------------------------------------------------------------------------- /assets/screenshot/me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/me.png -------------------------------------------------------------------------------- /assets/screenshot/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/message.png -------------------------------------------------------------------------------- /assets/screenshot/post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/post.png -------------------------------------------------------------------------------- /assets/screenshot/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/profile.png -------------------------------------------------------------------------------- /assets/screenshot/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/register.png -------------------------------------------------------------------------------- /assets/screenshot/sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/assets/screenshot/sign.png -------------------------------------------------------------------------------- /client/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/app/Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/client/app/Readme.md -------------------------------------------------------------------------------- /client/app/craco.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | //webpack配置 5 | webpack: { 6 | //配置别名 7 | alias: { 8 | '@': path.resolve(__dirname, 'src') 9 | } 10 | }, 11 | 12 | plugins: [ 13 | { 14 | plugin: require('craco-plugin-scoped-css'), 15 | }, 16 | ], 17 | } -------------------------------------------------------------------------------- /client/app/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*":[ 6 | "src/*" 7 | ] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /client/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^2.2.1", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.6.7", 11 | "classnames": "^2.5.1", 12 | "craco-plugin-scoped-css": "^1.1.1", 13 | "dayjs": "^1.11.10", 14 | "normalize.css": "^8.0.1", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-redux": "^9.1.0", 18 | "react-router-dom": "^6.22.3", 19 | "react-scripts": "5.0.1", 20 | "react-vant": "^3.3.4", 21 | "web-vitals": "^2.1.4", 22 | "websocket": "^1.0.35" 23 | }, 24 | "scripts": { 25 | "start": "craco start", 26 | "build": "craco build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject", 29 | "server": "json-server ./server/data.json --port 8888" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@craco/craco": "^7.1.0", 50 | "json-server": "^1.0.0-alpha.23", 51 | "sass": "^1.72.0" 52 | } 53 | } -------------------------------------------------------------------------------- /client/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/client/app/public/favicon.ico -------------------------------------------------------------------------------- /client/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/client/app/public/logo192.png -------------------------------------------------------------------------------- /client/app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/client/app/public/logo512.png -------------------------------------------------------------------------------- /client/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/app/server/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ka": [ 3 | { 4 | "type": "pay", 5 | "money": -99, 6 | "date": "2022-10-24 10:36:42", 7 | "useFor": "drinks", 8 | "id": 1 9 | }, 10 | { 11 | "type": "pay", 12 | "money": -88, 13 | "date": "2022-10-24 10:37:51", 14 | "useFor": "longdistance", 15 | "id": 2 16 | }, 17 | { 18 | "type": "income", 19 | "money": 100, 20 | "date": "2022-10-22 00:00:00", 21 | "useFor": "bonus", 22 | "id": 3 23 | }, 24 | { 25 | "type": "pay", 26 | "money": -33, 27 | "date": "2022-09-24 16:15:41", 28 | "useFor": "dessert", 29 | "id": 4 30 | }, 31 | { 32 | "type": "pay", 33 | "money": -56, 34 | "date": "2022-10-22T05:37:06.000Z", 35 | "useFor": "drinks", 36 | "id": 5 37 | }, 38 | { 39 | "type": "pay", 40 | "money": -888, 41 | "date": "2022-10-28T08:21:42.135Z", 42 | "useFor": "travel", 43 | "id": 6 44 | }, 45 | { 46 | "type": "income", 47 | "money": 10000, 48 | "date": "2023-03-20T06:45:54.004Z", 49 | "useFor": "salary", 50 | "id": 7 51 | }, 52 | { 53 | "type": "pay", 54 | "money": -10, 55 | "date": "2023-03-22T07:17:12.531Z", 56 | "useFor": "drinks", 57 | "id": 8 58 | }, 59 | { 60 | "type": "pay", 61 | "money": -20, 62 | "date": "2023-03-22T07:51:20.421Z", 63 | "useFor": "dessert", 64 | "id": 9 65 | }, 66 | { 67 | "type": "pay", 68 | "money": -100, 69 | "date": "2023-03-22T09:18:12.898Z", 70 | "useFor": "drinks", 71 | "id": 17 72 | }, 73 | { 74 | "type": "pay", 75 | "money": -50, 76 | "date": "2023-03-23T09:11:23.312Z", 77 | "useFor": "food", 78 | "id": 18 79 | }, 80 | { 81 | "type": "pay", 82 | "money": -10, 83 | "date": "2023-04-03T11:14:56.036Z", 84 | "useFor": "food", 85 | "id": 19 86 | } 87 | ] 88 | } -------------------------------------------------------------------------------- /client/app/src/App.js: -------------------------------------------------------------------------------- 1 | function App() { 2 | return ( 3 |
4 | this is app 5 |
6 | ); 7 | } 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /client/app/src/apis/discover.js: -------------------------------------------------------------------------------- 1 | //发现页相关接口 2 | 3 | import { request } from "@/utils"; 4 | 5 | //1.随机探索发现topic 6 | export function exploreTopicsApi(numbers = 5) { 7 | return request({ 8 | url: '/api/discover/explore', 9 | method: 'GET', 10 | numbers: numbers 11 | }) 12 | } 13 | 14 | //2.随机探索发现topic 15 | export function followTopicsApi(params = { numbers: 5, offset: 0 }) { 16 | return request({ 17 | url: '/api/discover/loadFollowedTargetActivities', 18 | method: 'GET', 19 | params: params 20 | }) 21 | } -------------------------------------------------------------------------------- /client/app/src/apis/file.js: -------------------------------------------------------------------------------- 1 | //文件上传相关接口 2 | import { request } from "@/utils"; 3 | import logo from '@/assets/logo-embodied.png' 4 | 5 | //1.上传文件 6 | export function uploadFileApi(files) { 7 | //创建一个 FormData 对象 8 | const formData = new FormData() 9 | 10 | //假设 files 是一个文件对象或文件对象数组 11 | if (files instanceof File) { 12 | formData.append('files', files); 13 | } else if (Array.isArray(files)) { 14 | files.forEach((file, index) => { 15 | formData.append('files', file); 16 | }); 17 | } 18 | 19 | return request({ 20 | url: '/oss/upload', 21 | method: 'POST', 22 | data: formData, 23 | headers: { 24 | 'Content-Type': 'multipart/form-data' 25 | } 26 | }) 27 | } 28 | 29 | //2.预览文件URL 30 | export function previewFileApi(fid) { 31 | //fid是否为空 32 | if (!fid || fid === '') { 33 | //返回默认图片 34 | return Promise.resolve({ data: logo }) 35 | } else { 36 | return request({ 37 | url: '/oss/preview', 38 | method: 'GET', 39 | params: { fid } 40 | }) 41 | } 42 | } 43 | 44 | 45 | // export function previewFileApi(fid) { 46 | // return request({ 47 | // url: '/oss/preview', 48 | // method: 'GET', 49 | // params: { fid } 50 | // }) 51 | // } -------------------------------------------------------------------------------- /client/app/src/apis/message.js: -------------------------------------------------------------------------------- 1 | //消息相关的接口 2 | import { request } from "@/utils"; 3 | 4 | //1.发送 5 | export function sendMsgApi(data) { 6 | return request({ 7 | url: '/api/msg/send', 8 | method: 'POST', 9 | data: data 10 | }) 11 | } 12 | 13 | //2.查询消息历史记录 14 | export function getMsgHistoryApi(size = 10) { //默认查询多少个 15 | return request({ 16 | url: '/api/msg/history', 17 | method: 'GET', 18 | params: { size: size } 19 | }) 20 | } 21 | 22 | //3.阅知消息 23 | export function checkSenderMsgApi(suid) { 24 | return request({ 25 | url: '/api/msg/checkSenderMsg', 26 | method: 'GET', 27 | params: { suid } 28 | }) 29 | } 30 | 31 | //4.查询和指定用户的消息历史记录 32 | export function getUtuMsgHistoryApi(params = {}) { 33 | //默认参数 34 | const defaultParams = { 35 | msgId: -1, 36 | querySize: 100, 37 | targetUid: '', //传参聊天对象 38 | } 39 | //合并传入的参数和默认参数 40 | const concatParams = {...defaultParams, ...params} 41 | 42 | return request({ 43 | url: '/api/msg/getUtuMsgHistoryList', 44 | method: 'GET', 45 | params: concatParams 46 | }) 47 | } 48 | 49 | 50 | -------------------------------------------------------------------------------- /client/app/src/apis/post.js: -------------------------------------------------------------------------------- 1 | //封装和发布话题相关的接口函数 2 | import { request } from "@/utils"; 3 | 4 | //1.发布话题 5 | export function createTopicApi(formData) { 6 | return request({ 7 | url: '/api/topics/publishTopic', 8 | method: 'POST', 9 | data: formData, 10 | withCredentials: true 11 | }) 12 | } 13 | 14 | //2.存草稿 15 | export function saveTopicDraftApi(formData) { 16 | return request({ 17 | url: '/api/topics/save', 18 | method: 'POST', 19 | data: formData, 20 | withCredentials: true 21 | }) 22 | } -------------------------------------------------------------------------------- /client/app/src/apis/topic.js: -------------------------------------------------------------------------------- 1 | // 话题相关的接口 2 | import { request } from "@/utils"; 3 | 4 | //1.获取频道列表 5 | export function getChannelApi() { 6 | return request({ 7 | url: '/api/channels/list', 8 | method: 'GET' 9 | }) 10 | } 11 | 12 | //2.获取指定频道的话题 13 | export function getChannelTopicsApi(channelKey) { 14 | return request({ 15 | url: '/api/topics/channelTopics', 16 | method: 'GET', 17 | params: { channelKey: channelKey } 18 | }) 19 | } 20 | 21 | //3.查看单个话题 22 | export function getIndividualTopicApi(topicId) { 23 | return request({ 24 | url: '/api/topics/show', 25 | method: 'GET', 26 | params: { id: topicId } 27 | }) 28 | } 29 | 30 | //4.查询话题评论 31 | export function getCommentsApi(topicId) { 32 | return request({ 33 | url: '/api/topics/loadComments', 34 | method: 'GET', 35 | params: { tid: topicId } 36 | }) 37 | } 38 | 39 | //5.发表评论_登录校验 40 | export function postCommentApi(form) { 41 | return request({ 42 | url: '/api/topics/toComment', 43 | method: 'POST', 44 | data: form 45 | }) 46 | } 47 | 48 | //6.点赞_登录校验 49 | export function likeApi(topicId) { 50 | return request({ 51 | url: '/api/topics/like', 52 | method: 'GET', 53 | params: { tid: topicId } 54 | }) 55 | } 56 | 57 | //7.添加收藏 58 | export function addBookmarkApi(topicId) { 59 | return request({ 60 | url: '/api/topics/addBookMark', 61 | method: 'GET', 62 | params: { tid: topicId } 63 | }) 64 | } 65 | 66 | //8.取消收藏 67 | export function removeBookmarkApi(topicId) { 68 | return request({ 69 | url: '/api/topics/removeBookMark', 70 | method: 'GET', 71 | params: { tid: topicId } 72 | }) 73 | } 74 | 75 | //9.获取话题初始状态 76 | export function getTopicActionApi(topicId) { 77 | return request({ 78 | url: '/api/topics/getTopicActions', 79 | method: 'GET', 80 | params: { tid: topicId } 81 | }) 82 | } -------------------------------------------------------------------------------- /client/app/src/apis/user.js: -------------------------------------------------------------------------------- 1 | //用户相关的所有请求 2 | import { request } from "@/utils"; 3 | import { getUserId as _getUserId } from '@/utils' 4 | 5 | //1.用户登录 6 | export function loginAPI(formData) { 7 | //以下写法是axios的通用写法,任何一个请求都可以这样写 8 | return request({ //return返回的结果是一个promise 调用这个函数可以用async await接收返回值 9 | url: '/api/users/login', 10 | method: 'POST', 11 | data: formData, 12 | withCredentials: true 13 | }) 14 | } 15 | 16 | //2.用户注册 17 | export function registerAPI(formData) { 18 | return request({ 19 | url: '/api/users/register', 20 | method: 'POST', 21 | data: formData, 22 | }) 23 | } 24 | 25 | //3.获取用户详细信息 26 | export function getProfileAPI(uid) { 27 | return request({ 28 | url: '/api/users/userDetail', 29 | method: 'GET', 30 | params: { uid: 1 }, 31 | }) 32 | } 33 | 34 | //4.查看用户统计信息 35 | export function getUserExtendsAPI(uid) { 36 | return request({ 37 | url: '/api/users/userExtendsInfo', 38 | method: 'GET', 39 | params: { uid: uid }, 40 | }) 41 | } 42 | 43 | 44 | //关注 45 | //--1.关注用户 46 | export function followAPI(targetUid) { 47 | return request({ 48 | url: '/api/users/follow', 49 | method: 'GET', 50 | params: { targetUid: targetUid }, 51 | // withCredentials: true 52 | }) 53 | } 54 | 55 | //--2.取消关注 56 | export function unFollowAPI(targetUid) { 57 | return request({ 58 | url: '/api/users/unFollow', 59 | method: 'GET', 60 | params: { targetUid: targetUid }, 61 | }) 62 | } 63 | 64 | //--3.查看是否关注 65 | export function checkFollowAPI(targetUid) { 66 | return request({ 67 | url: '/api/users/checkFollow', 68 | method: 'GET', 69 | params: { targetUid: targetUid }, 70 | }) 71 | } 72 | 73 | 74 | //好友 75 | //--1.添加好友 76 | export function applyToFriendAPI(targetUid) { 77 | return request({ 78 | url: '/api/users/applyToFriend', 79 | method: 'GET', 80 | params: { targetUser: targetUid }, 81 | }) 82 | } 83 | 84 | //--2.同意用户好友申请 85 | export function approveApplyAPI(applyId) { 86 | return request({ 87 | url: '/api/users/approveApply', 88 | method: 'GET', 89 | params: { applyId: applyId }, 90 | }) 91 | } 92 | 93 | //--3.查询好友申请 94 | export function getMyApplyListAPI() { 95 | return request({ 96 | url: '/api/users/getMyApplyList', 97 | method: 'GET', 98 | }) 99 | } 100 | 101 | //--4.查询好友列表 102 | export function getMyFriendsAPI() { 103 | return request({ 104 | url: '/api/users/getMyFriends', 105 | method: 'GET', 106 | }) 107 | } 108 | 109 | //--5.查看是否好友 110 | export function checkFriendAPI(targetUid) { 111 | return request({ 112 | url: '/api/users/checkIsFriendByUid', 113 | method: 'GET', 114 | params: { targetUser: targetUid }, 115 | }) 116 | } 117 | 118 | //--6.用昵称模糊查询姓名 119 | export function searchUserByNickNameApi(nickName) { 120 | return request({ 121 | url: '/api/users/searchUserByNickName', 122 | method: 'GET', 123 | params: { nickName: nickName }, 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /client/app/src/assets/bg_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/client/app/src/assets/bg_1.jpg -------------------------------------------------------------------------------- /client/app/src/assets/logo-embodied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/client/app/src/assets/logo-embodied.png -------------------------------------------------------------------------------- /client/app/src/components/AuthRoute.js: -------------------------------------------------------------------------------- 1 | //封装高阶组件 2 | //有 token 正常跳转,无 token 去登录 3 | 4 | import { getToken as _getToken } from "@/utils" 5 | import { Navigate } from "react-router-dom" 6 | 7 | export function AuthRoute({ children }) { //参数children是组件 8 | const token = _getToken() 9 | console.log(token) 10 | if (token) { 11 | return <>{children} 12 | } else { 13 | return 14 | } 15 | } -------------------------------------------------------------------------------- /client/app/src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | import TabNavigator from './TabNavigator/TabNavigator'; 4 | 5 | const Layout = () => { 6 | return ( 7 |
8 |
9 | {/* 渲染子路由的内容 */} 10 |
11 |
12 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default Layout; 19 | 20 | 21 | /* 22 | 当访问 /home 路径时,React Router 会匹配到根路由,并渲染 Layout 组件。 23 | 在 Layout 组件中, 会渲染匹配的子路由 。 24 | 25 | 例如: 26 | 27 | 当你访问 /home 时, 会渲染 。 28 | 当你访问 /discover/message 时,React Router 会先匹配到 /discover 路径,渲染 Discover 组件,然后在 Discover 组件中渲染嵌套的子路由 Message。 29 | 通过这种方式, 组件在 Layout 中起到了占位符的作用,根据当前的 URL 动态渲染对应的子路由内容。 30 | 31 | 这样,页面的布局保持不变,只更新 Outlet 内部的内容。 32 | */ -------------------------------------------------------------------------------- /client/app/src/components/TabNavigator/TabNavigator.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { CommentO, HomeO, Search, UserO, AddO } from '@react-vant/icons' 4 | import './TabNavigator.scoped.scss' 5 | import { useLocation, useNavigate } from 'react-router-dom' 6 | import { getUserId as _getUserId } from '@/utils' 7 | 8 | const TabNavigator = () => { 9 | 10 | const [loginUserId, setLoginUserId] = useState() 11 | const navigate = useNavigate() 12 | const location = useLocation() 13 | 14 | const [activeTab, setActiveTab] = useState(location.pathname === '/' ? '/home' : location.pathname) 15 | 16 | 17 | //当前登录用户 18 | useEffect(() => { 19 | //const loginUsername = _getUserId 20 | setLoginUserId(_getUserId) 21 | }, []) 22 | 23 | const { userInfo } = useSelector(state => state.user.userInfo) 24 | console.log('redux中的userInfo:', userInfo) 25 | // useEffect(() => { 26 | // dispatch(fetchUserInfo()) 27 | // }, [dispatch]) 28 | 29 | const onClickTabbar = (path) => { 30 | setActiveTab(path) 31 | navigate(path) 32 | } 33 | 34 | return ( 35 |
36 |
    37 |
  • onClickTabbar('/home')} 40 | > 41 | {activeTab === '/home' ? 42 |
    43 | 44 |
    Home
    45 |
    46 | : } 47 |
  • 48 | 49 |
  • onClickTabbar('/discover')} 52 | > 53 | {activeTab === '/discover' ? 54 |
    55 | 56 |
    Discover
    57 |
    58 | : } 59 |
  • 60 | 61 |
  • onClickTabbar('/post')} 64 | > 65 | {activeTab === '/post' ? 66 |
    67 | 68 |
    Post
    69 |
    70 | : } 71 |
  • 72 | 73 |
  • onClickTabbar('/message')} 76 | > 77 | {activeTab === '/message' ? 78 |
    79 | 80 |
    Message
    81 |
    82 | : } 83 |
  • 84 | 85 |
  • onClickTabbar(`/profile/${loginUserId}/myPost`)} 88 | > 89 | {activeTab === `/profile/${loginUserId}/myPost` ? 90 |
    91 | 92 |
    Profile
    93 |
    94 | : } 95 |
  • 96 |
97 |
98 | 99 | ) 100 | } 101 | 102 | export default TabNavigator -------------------------------------------------------------------------------- /client/app/src/components/TabNavigator/TabNavigator.scoped.scss: -------------------------------------------------------------------------------- 1 | .tab-bar { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | // padding: 0 2vw; 6 | 7 | background-color: #FFF; 8 | height: 50px; 9 | width: 100vw; 10 | border-top-left-radius: 1rem; 11 | border-top-right-radius: 1rem; 12 | 13 | display: flex; 14 | align-items: center; 15 | // box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1); 16 | 17 | ul { 18 | display: flex; 19 | justify-content: space-around; 20 | flex: 1; 21 | padding: 0; 22 | margin: 1vw; 23 | } 24 | 25 | .tab-item { 26 | text-align: center; 27 | flex: 1; 28 | transition: flex 0.3s ease, background 0.3s ease; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | } 33 | 34 | .active-icon { 35 | display: flex; 36 | flex-direction: row; 37 | gap: 3px; 38 | } 39 | 40 | .tab-item.active { 41 | font-size: 1.2rem; 42 | border-radius: 2rem; 43 | padding: 0.4rem 0; 44 | flex: 2; //Make the active tab wider 45 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 46 | } 47 | 48 | &__home { 49 | color: rgb(34, 95, 159); 50 | background: #E6F0FF; 51 | } 52 | 53 | &__discover { 54 | color: #58437A; 55 | background: #F2E9F7; 56 | } 57 | 58 | .active__post { 59 | color: #A11A0A; 60 | background: #FAE6DA; 61 | } 62 | 63 | .active__message { 64 | color: #1892a6; 65 | background: #F3FCF8; 66 | } 67 | 68 | .active__profile { 69 | color: #EC8243; 70 | background: #FEF0D9; 71 | } 72 | } 73 | 74 | .tab-bar-icon { 75 | height: 6vw; 76 | width: 6vw; 77 | } -------------------------------------------------------------------------------- /client/app/src/components/TabNavigator/TabNavigatorV1.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Tabbar, TabbarItem } from 'react-vant' 3 | import { CommentO, HomeO, Search, UserO, AddO } from '@react-vant/icons' 4 | import './TabNavigatorV1.scoped.scss' 5 | import { useLocation, useNavigate, useParams } from 'react-router-dom' 6 | import { useDispatch } from 'react-redux' 7 | import { fetchUserInfo } from '@/store/modules/user' 8 | import { getUserId as _getUserId } from '@/utils' 9 | 10 | 11 | const TabNavigatorV1 = () => { 12 | const [loginUserId, setLoginUserId] = useState() 13 | 14 | //当前登录用户 15 | useEffect(() => { 16 | //const loginUsername = _getUserId 17 | setLoginUserId(_getUserId) 18 | }, []) 19 | 20 | const tabs = [ 21 | { 22 | key: '/home', 23 | title: '首页', 24 | icon: , 25 | }, 26 | { 27 | key: '/discover', 28 | title: '发现', 29 | icon: , 30 | }, 31 | { 32 | key: '/post', 33 | title: '发布', 34 | icon: , 35 | }, 36 | { 37 | key: '/message', 38 | title: '消息', 39 | icon: , 40 | }, 41 | { 42 | key: `/profile/${loginUserId}/myPost`, 43 | title: '我的', 44 | icon: , 45 | }, 46 | ] 47 | 48 | const navigate = useNavigate() 49 | const location = useLocation() 50 | 51 | //设置当前选项 52 | const [tabRoute, setTabRoute] = useState(location.pathname === '/' ? '/home' : location.pathname) 53 | 54 | //点击事件 55 | const onTabbarClick = (route) => { 56 | console.log('tabbar被点击了', route) 57 | navigate(route) 58 | setTabRoute(route) 59 | } 60 | 61 | //触发个人用户信息action 62 | const dispatch = useDispatch() 63 | useEffect(() => { 64 | dispatch(fetchUserInfo()) 65 | }, [dispatch]) 66 | 67 | return ( 68 |
69 | onTabbarClick(v)} 72 | activeColor='#f44336' inactiveColor='#000' 73 | placeholder 74 | fixed 75 | > 76 | {tabs.map(item => ( 77 | {item.title} 78 | ))} 79 | 80 |
81 | ) 82 | } 83 | 84 | export default TabNavigatorV1 85 | 86 | -------------------------------------------------------------------------------- /client/app/src/components/TabNavigator/TabTransition.scss: -------------------------------------------------------------------------------- 1 | /* 过渡动画样式 */ 2 | .fade-enter { 3 | opacity: 0; 4 | transform: translateX(100%); 5 | } 6 | 7 | .fade-enter-active { 8 | opacity: 1; 9 | transform: translateX(0); 10 | transition: opacity 300ms, transform 300ms; 11 | } 12 | 13 | .fade-exit { 14 | opacity: 1; 15 | transform: translateX(0); 16 | } 17 | 18 | .fade-exit-active { 19 | opacity: 0; 20 | transform: translateX(-100%); 21 | transition: opacity 300ms, transform 300ms; 22 | } -------------------------------------------------------------------------------- /client/app/src/components/TopBar/TopBar.jsx: -------------------------------------------------------------------------------- 1 | import { NavBar, Toast } from "react-vant"; 2 | 3 | const TopBar = () => { 4 | return ( 5 | Toast('返回')} 10 | onClickRight={() => Toast('按钮')} 11 | /> 12 | ); 13 | } 14 | 15 | export default TopBar 16 | 17 | -------------------------------------------------------------------------------- /client/app/src/components/Topic/Topic.jsx: -------------------------------------------------------------------------------- 1 | import { Image } from 'react-vant' 2 | import { Arrow, CommentO, LikeO, BookmarkO } from '@react-vant/icons' 3 | import useChannelList from '@/hooks/useChannelList' 4 | import './Topic.scoped.scss' 5 | 6 | const Topic = ({ 7 | id, 8 | title, 9 | channelKey, 10 | content, 11 | coverImg, 12 | coverUrl, 13 | authorUid, 14 | updateTime, 15 | likes, 16 | bookmarks, 17 | comments, 18 | toTargetProfile, 19 | }) => { 20 | 21 | //根据channelKey获取name 22 | const { channelList, loading } = useChannelList() 23 | const getChannelNameByKey = (key) => { 24 | const channel = channelList.find(item => item.key === key) 25 | return channel ? channel.name : '' 26 | } 27 | 28 | return ( 29 |
30 | 31 |
32 |
33 | {title} 34 |
35 |
36 | {getChannelNameByKey(channelKey)} 37 |
38 | 39 |
40 |
41 | 42 |
43 | 内容:{content} 44 |
45 | 46 | {coverImg ? ( 47 |
48 | 52 |
53 | ) : ( 54 |
55 | )} 56 | 57 |
58 |
59 | toTargetProfile(authorUid)}> 60 | uid{authorUid} 61 | 62 | · {updateTime} 63 |
64 |
65 |
{likes}
66 |
{bookmarks}
67 |
{comments}
68 |
69 |
70 |
71 | ); 72 | } 73 | 74 | 75 | export default Topic -------------------------------------------------------------------------------- /client/app/src/components/Topic/Topic.scoped.scss: -------------------------------------------------------------------------------- 1 | 2 | .topic-box { 3 | background-color: #fff; 4 | margin: 1vw 3vw 3vw 3vw; 5 | border-radius: 0.5rem; 6 | padding: 1rem; 7 | box-shadow: 0 2px 0.6rem 0 rgba(0, 0, 0, 0.2), 0 4px 0.5rem 0 rgb(0 0 0 / 0.2); 8 | } 9 | 10 | .topic-header { 11 | display: flex; 12 | align-items: center; 13 | gap: 0.3rem; 14 | 15 | &__title { 16 | font-size: 1.3rem; 17 | font-weight: 550; 18 | } 19 | 20 | &__channel { 21 | border-radius: 0.5rem; 22 | padding: 0.2rem 0.6rem; 23 | font-size: 0.7rem; 24 | color: #28558f; 25 | background-color: #E5EDF9; 26 | } 27 | } 28 | 29 | // 链接取消变色和下划线 30 | a { 31 | text-decoration: none; //取消下划线 32 | color: inherit; //继承父元素的文本颜色 33 | } 34 | 35 | a:link, 36 | a:visited, 37 | a:hover, 38 | a:active { 39 | text-decoration: none; 40 | color: inherit; 41 | } 42 | 43 | .topic-content { 44 | margin-top: 0.3rem; 45 | font-size: 1rem; 46 | 47 | //设置超出2行文本省略 48 | display: -webkit-box; //设置为弹性盒子布局 49 | -webkit-box-orient: vertical; //设置弹性盒子的方向为垂直方向 50 | -webkit-line-clamp: 2; //设置显示的行数为两行 51 | overflow: hidden; //隐藏溢出的文本 52 | text-overflow: ellipsis; 53 | } 54 | 55 | .topic-cover { 56 | margin-top: 0.5rem; 57 | border-radius: 0.5rem; 58 | // height: 50%; 59 | // width: 50%; 60 | } 61 | 62 | .topic-bottom { 63 | display: flex; 64 | justify-content: space-between; 65 | align-items: center; 66 | margin-top: 0.5rem; 67 | 68 | &__right { 69 | display: flex; 70 | gap: 0.5rem; 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /client/app/src/components/fileUpload.js: -------------------------------------------------------------------------------- 1 | import { uploadFileApi } from '@/apis/file'; 2 | import React, { useState } from 'react'; 3 | 4 | const FileUpload = () => { 5 | const [selectedFile, setSelectedFile] = useState(null); 6 | 7 | const handleFileChange = (event) => { 8 | setSelectedFile(event.target.files[0]); 9 | }; 10 | 11 | const handleUpload = () => { 12 | if (!selectedFile) { 13 | alert('请选择要上传的文件'); 14 | return; 15 | } 16 | 17 | const files = new FormData(); 18 | files.append('files', selectedFile); 19 | 20 | fetch('http://120.78.142.84:8080/oss/upload', { 21 | method: 'POST', 22 | body: files 23 | }) 24 | // uploadFileApi(formData) 25 | .then(response => { 26 | if (!response.ok) { 27 | throw new Error('上传文件失败'); 28 | } 29 | return response.json(); 30 | }) 31 | .then(data => { 32 | alert('文件上传成功'); 33 | console.log('服务器返回的数据:', data); 34 | }) 35 | .catch(error => { 36 | console.error('上传文件出错:', error); 37 | }); 38 | }; 39 | 40 | return ( 41 |
42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default FileUpload; -------------------------------------------------------------------------------- /client/app/src/hooks/useChannelList.jsx: -------------------------------------------------------------------------------- 1 | //自定义hook:获取频道列表 2 | import { useState, useEffect } from 'react' 3 | import { getChannelApi } from '@/apis/topic' 4 | 5 | const useChannelList = () => { 6 | //获取频道列表 7 | const [channelList, setChannelList] = useState([]) 8 | const [loading, setLoading] = useState(true); 9 | 10 | useEffect(() => { 11 | //1.封装函数,在函数体内调用接口 12 | const getChannelList = async () => { 13 | const res = await getChannelApi() 14 | setChannelList(res.data) 15 | setLoading(false); 16 | } 17 | //2.调用函数 18 | getChannelList() 19 | }, []) 20 | 21 | return { channelList, loading } 22 | } 23 | 24 | export default useChannelList -------------------------------------------------------------------------------- /client/app/src/hooks/useUserDetail.jsx: -------------------------------------------------------------------------------- 1 | //自定义hook:获取用户信息、头像url 2 | import { previewFileApi } from '@/apis/file' 3 | import { getProfileAPI } from '@/apis/user' 4 | import { useState, useEffect } from 'react' 5 | import logo from '@/assets/logo-embodied.png' //作为默认头像 6 | 7 | const useUserDetail = (uid) => { 8 | const [avatarUrl, setAvatarUrl] = useState('') 9 | const [userProfile, setUserProfile] = useState({ 10 | userName: '', 11 | email: '', 12 | phone: '', 13 | userDetail: { 14 | id: null, 15 | uid: null, 16 | firstName: '', 17 | secondName: '', 18 | nickName: '', 19 | gender: '', 20 | birthdate: '', 21 | country: '', 22 | address: '', 23 | avatar: '', 24 | createTime: '' 25 | } 26 | }) 27 | 28 | useEffect(() => { 29 | fetchUserProfile() 30 | fetchUserAvatar() 31 | }, []) 32 | 33 | useEffect(() => { 34 | fetchUserAvatar() 35 | }, [userProfile]) 36 | 37 | const fetchUserProfile = async () => { 38 | //获取用户信息 39 | const userProfileRes = await getProfileAPI(uid) 40 | setUserProfile(userProfileRes.data) 41 | // console.log('用户详情:', userProfileRes.data) 42 | } 43 | 44 | const fetchUserAvatar = async () => { 45 | //获取用户头像url 46 | // let avatarId = userProfile.userDetail.avatar === null ? 5 : parseInt(userProfile.userDetail.avatar) 47 | let avatarId = parseInt(userProfile.userDetail.avatar) 48 | const userAvatarRes = await previewFileApi(avatarId) 49 | setAvatarUrl(userAvatarRes.data) 50 | } 51 | 52 | return { userProfile, avatarUrl } 53 | } 54 | 55 | export default useUserDetail -------------------------------------------------------------------------------- /client/app/src/hooks/useWebSocket.jsx: -------------------------------------------------------------------------------- 1 | //自定义hook:websocket连接逻辑 2 | 3 | import { getToken as _getToken } from '@/utils'; 4 | import { useEffect, useRef } from 'react'; 5 | 6 | const useWebSocket = (onMessage) => { 7 | 8 | const baseUrl = 'ws://localhost:8080' //可修改 9 | //const baseUrl = 'ws://120.78.142.84:8080' //可修改 10 | const satoken = _getToken() 11 | const wsUrl = `${baseUrl}/ws-connect?satoken=${satoken}` 12 | 13 | const ws = useRef(null) 14 | 15 | useEffect(() => { 16 | //创建 WebSocket 连接的函数 17 | const connectWebSocket = () => { 18 | ws.current = new WebSocket(wsUrl) 19 | 20 | //连接打开事件 21 | ws.current.onopen = () => { 22 | console.log('WebSocket连接开启') 23 | } 24 | 25 | //连接关闭事件 26 | ws.current.onclose = () => { 27 | console.log('WebSocket连接关闭') 28 | } 29 | 30 | //收到消息事件 31 | ws.current.onmessage = (event) => { 32 | console.log('后端ws返回的原始数据:', event) 33 | //onMessage是回调函数,也就是在 Chat 组件中定义的 handleWebSocketMessage函数 34 | if (onMessage) { 35 | onMessage(event); 36 | } 37 | } 38 | } 39 | 40 | // 如果 WebSocket 实例存在且未关闭,先关闭它 41 | if (ws.current) { 42 | ws.current.close(); 43 | } 44 | 45 | //建立新的 WebSocket 连接 46 | connectWebSocket(); 47 | 48 | //清理函数,在组件卸载时关闭WebSocket连接 49 | return () => { 50 | if (ws.current && ws.current.readyState === WebSocket.OPEN) { 51 | ws.current.close(); 52 | } 53 | } 54 | }, []) 55 | 56 | //返回 WebSocket 实例 57 | return ws.current 58 | } 59 | 60 | export default useWebSocket -------------------------------------------------------------------------------- /client/app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.scss'; 4 | import { RouterProvider } from 'react-router-dom' 5 | import router from './router'; 6 | import { Provider } from 'react-redux'; 7 | import store from './store'; 8 | import 'normalize.css' 9 | 10 | const root = ReactDOM.createRoot(document.getElementById('root')); 11 | root.render( 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | -------------------------------------------------------------------------------- /client/app/src/index.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: calc(100vw / 30); //设置根元素的字体大小1rem=视口宽度的1/40 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | height: 100%; 9 | 10 | font: 1rem / 1.5 Helvetica Neue, Arial, sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | color: #292d33; 13 | letter-spacing: 0.05rem; 14 | } 15 | 16 | #root { 17 | margin: 0; 18 | padding: 0; 19 | height: 100%; 20 | } 21 | 22 | #root { 23 | --rv-nav-bar-height: 46px; 24 | --rv-nav-bar-background-color: #1d6b99; 25 | //--rv-nav-bar-background-color: linear-gradient(135deg, rgba(34, 95, 159, 1) 20%, rgb(170, 54, 46, 0.95) 65%, rgba(249, 144, 69, 1) 98%); 26 | --rv-nav-bar-arrow-size: 16px; 27 | --rv-nav-bar-icon-color: #fff; 28 | --rv-nav-bar-text-color: #fff; 29 | --rv-nav-bar-title-font-size: var(--rv-font-size-lg); 30 | --rv-nav-bar-title-text-color: #fff; 31 | --rv-nav-bar-z-index: 1; 32 | 33 | //tab 34 | // .rv-tabs--line .rv-tabs__wrap, 35 | // .rv-tabs--capsule .rv-tabs__wrap { 36 | // padding: 0 20vw; 37 | // } 38 | } -------------------------------------------------------------------------------- /client/app/src/pages/Chat/Chat.scoped.scss: -------------------------------------------------------------------------------- 1 | .message-container { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | background-image: url("/assets/bg_1.jpg"); 6 | background-attachment: fixed; 7 | } 8 | 9 | .chat-indv { 10 | padding: 0.5rem 0.7rem; 11 | gap: 0.5rem; 12 | } 13 | 14 | .chat-box { 15 | display: flex; 16 | gap: 0.5rem; 17 | margin-top: 1vw; 18 | 19 | &__my { 20 | flex-direction: row-reverse; 21 | } 22 | 23 | &__target { 24 | flex-direction: row; 25 | } 26 | } 27 | 28 | 29 | .chat-box-left { 30 | width: 10vw; 31 | height: 10vw; 32 | } 33 | 34 | .chat-box-right { 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: center; 38 | } 39 | 40 | .chat-sender { 41 | font-size: 1.2rem; 42 | 43 | &__my { 44 | text-align: right; 45 | /* 将文本对齐到右边 */ 46 | align-self: flex-end; 47 | /* 将元素自身对齐到容器的右边 */ 48 | } 49 | 50 | &__target { 51 | text-align: left; 52 | align-self: flex-start; 53 | } 54 | } 55 | 56 | 57 | .chat-content { 58 | padding: 0.5rem 0.8rem; 59 | border: 1px solid rgba(254, 243, 236, 1); //设置边框 60 | background-color: rgba(254, 249, 246, 0.7); 61 | border-bottom-left-radius: 1rem; 62 | border-bottom-right-radius: 1rem; 63 | 64 | &__target { 65 | border-top-right-radius: 1rem; 66 | } 67 | 68 | &__my { 69 | border-top-left-radius: 1rem; 70 | } 71 | } 72 | 73 | 74 | .chat-send-box { 75 | display: flex; 76 | flex-direction: row; 77 | align-items: center; 78 | position: fixed; 79 | position: sticky; 80 | bottom: 0; 81 | padding: 2vw; 82 | gap: 1vw; 83 | } 84 | 85 | .send-box-left { 86 | width: 75vw; 87 | border: solid 1px #7b94ac7a; 88 | border-radius: 2rem; 89 | height: 5.5vh; 90 | padding: 0 2vw; 91 | display: flex; 92 | align-items: center; 93 | background-color: #fff; 94 | } 95 | 96 | .comment-button { 97 | width: 15vw; 98 | height: 5.5vh; 99 | border-radius: 2rem; 100 | color: #fff; 101 | background-color: rgba(5, 106, 150, 0.9); 102 | border: transparent; 103 | margin: 0; 104 | padding: 0; 105 | } -------------------------------------------------------------------------------- /client/app/src/pages/Discover/Discover.jsx: -------------------------------------------------------------------------------- 1 | import { NavBar, Toast, Tabs } from "react-vant" 2 | import './Discover.scoped.scss' 3 | import { Outlet, useLocation, useNavigate } from "react-router-dom" 4 | import { useState } from "react" 5 | 6 | const Discover = () => { 7 | const navigate = useNavigate() 8 | const location = useLocation() 9 | const [activeTab, setActiveTab] = useState(location.pathname === '/discover' ? '/discover/follow' : location.pathname) 10 | const handleClickTab = (path) => { 11 | navigate(path) 12 | setActiveTab(path) 13 | } 14 | 15 | const tabItems = [ 16 | { 17 | path: '/discover/follow', 18 | text: '关注' 19 | }, 20 | { 21 | path: '/discover/view', 22 | text: '随机' 23 | } 24 | ] 25 | 26 | return ( 27 |
28 | 32 |
33 | handleClickTab(v.name)} 36 | > 37 | {tabItems.map(item => ( 38 | 43 | 44 | 45 | ))} 46 | 47 | 48 | {/*
49 |
onClickTab('/discover/follow')} 52 | > 53 | 关注 54 |
55 |
onClickTab('/discover/view')} 58 | > 59 | 随机 60 |
61 |
62 | */} 63 |
64 |
65 | ) 66 | } 67 | 68 | export default Discover -------------------------------------------------------------------------------- /client/app/src/pages/Discover/Discover.scoped.scss: -------------------------------------------------------------------------------- 1 | .discover-container { 2 | --rv-tabs-line-height: 30px; 3 | --rv-tabs-bottom-bar-color: #3b87b2; 4 | --rv-tab-active-text-color: #1a81bd; 5 | --rv-tab-text-color: #53585b; 6 | 7 | } 8 | 9 | .rv-tabs__wrap { 10 | height: var(--rv-tabs-line-height); 11 | padding: 0 20vw; 12 | } 13 | 14 | 15 | .discover-tab-container { 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: center; 19 | gap: 5vw; 20 | width: 100vw; 21 | padding: 5px 0 0 0; 22 | // background-color: pink; 23 | border-bottom: 1px solid #f2f4f5; 24 | box-shadow: 1px 1px 2px 0 rgba(0, 0, 0, 0.1); 25 | 26 | 27 | .tab-item { 28 | text-align: center; 29 | color: #000; 30 | align-self: center; 31 | font-size: 1.1rem; 32 | padding: 0 0.8rem; 33 | //transition: flex 0.3s ease, background 0.3s ease; 34 | //transition: all 0.5s ease; 35 | 36 | position: relative; //必须的,用于让 ::after 的 absolute 定位生效 * 37 | } 38 | 39 | .tab-item::after { 40 | content: ''; 41 | /* 不显示任何文本内容 */ 42 | position: absolute; 43 | bottom: -2px; 44 | /* 将下划线定位到 tab-item 元素的底部 */ 45 | left: 0; 46 | height: 2px; 47 | /* 下划线的高度 */ 48 | width: 0; 49 | /* 初始宽度为 0 */ 50 | background-color: #3b87b2; 51 | transition: all 0.3s ease; 52 | } 53 | 54 | 55 | .tab-item.active { 56 | //border-bottom: 2px solid #3b87b2; 57 | } 58 | 59 | .tab-item.active::after { 60 | width: 100%; 61 | /* 当 tab 处于活动状态时,下划线的宽度变为 100% */ 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /client/app/src/pages/Follow/Follow.jsx: -------------------------------------------------------------------------------- 1 | import { followTopicsApi } from '@/apis/discover' 2 | import { useEffect, useState } from 'react' 3 | import { useNavigate } from 'react-router-dom' 4 | import './Follow.scoped.scss' 5 | import Topic from '@/components/Topic/Topic' 6 | 7 | const Follow = () => { 8 | //获取频道列表 9 | const [topicList, setTopicList] = useState([]) 10 | 11 | //初始化 12 | useEffect(() => { 13 | fetchTopicList() 14 | }, []) 15 | 16 | const fetchTopicList = async () => { 17 | const res = await followTopicsApi() 18 | setTopicList(res.data.topicPostList) 19 | console.log('follow页返回:', res) 20 | } 21 | 22 | //跳转指定用户主页 23 | const navigate = useNavigate() 24 | const toTargetProfile = (targetUid) => { 25 | navigate(`/profile/${targetUid}`) 26 | } 27 | 28 | return ( 29 |
30 |
31 | {topicList.map((item, index) => ( 32 | 47 | )) 48 | } 49 |
50 |
51 | ) 52 | } 53 | 54 | export default Follow -------------------------------------------------------------------------------- /client/app/src/pages/Follow/Follow.scoped.scss: -------------------------------------------------------------------------------- 1 | .follow-container { 2 | width: 100vw; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /client/app/src/pages/Friends/MyFriends/MyFriends.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { NavBar, Cell, Image } from "react-vant" 3 | import { previewFileApi } from "@/apis/file" 4 | import { useNavigate } from "react-router-dom" 5 | import { getMyFriendsAPI } from "@/apis/user" 6 | import { getUserId } from "@/utils" 7 | 8 | const MyFriends = () => { 9 | const navigate = useNavigate() 10 | const [friendList, setFriendList] = useState([]) 11 | 12 | useEffect(() => { 13 | fetchFriendList() 14 | }, []) 15 | 16 | const fetchFriendList = async () => { 17 | const res = await getMyFriendsAPI() 18 | const list = res.data 19 | console.log('返回friendList:', res) 20 | 21 | //拼接avatarUrl 22 | const listWithAvatar = await Promise.all(list.map(async user => { 23 | let friendAvatar = 24 | user.friendShips.uidSource === getUserId 25 | ? user.sourceUserDetail.avatar 26 | : user.toUserDetail.avatar 27 | const avatarRes = await previewFileApi(friendAvatar) 28 | return { ...user, avatarUrl: avatarRes.data } 29 | })) 30 | console.log('拼接后的friendList:', listWithAvatar) 31 | setFriendList(listWithAvatar) 32 | } 33 | 34 | const onClickUser = (uid) => { 35 | navigate(`/profile/${uid}`) 36 | } 37 | 38 | return ( 39 |
40 | 41 | window.history.back(-1)} 45 | /> 46 | 47 |
48 | {friendList.length === 0 ? ( 49 |
未加好友
50 | ) : ( 51 |
52 | {friendList.map((item, index) => ( 53 |
54 | } 62 | isLink 63 | onClick={() => onClickUser(item.uid)} 64 | /> 65 |
66 | )) 67 | } 68 |
69 | )} 70 | 71 |
72 |
73 | ) 74 | } 75 | 76 | export default MyFriends -------------------------------------------------------------------------------- /client/app/src/pages/Friends/NewFriend/NewFriend.jsx: -------------------------------------------------------------------------------- 1 | import { searchUserByNickNameApi } from "@/apis/user" 2 | import { useState } from "react" 3 | import { Search, Cell, Image } from "react-vant" 4 | import './NewFriend.scoped.scss' 5 | import { previewFileApi } from "@/apis/file" 6 | import { useNavigate } from "react-router-dom" 7 | 8 | const NewFriend = () => { 9 | const navigate = useNavigate() 10 | const [searchValue, setSearchValue] = useState('') 11 | const [userList, setUserList] = useState([]) 12 | const [hint, setHint] = useState('') 13 | 14 | const handleSearch = async () => { 15 | const res = await searchUserByNickNameApi(searchValue) 16 | const list = res.data 17 | console.log('搜索返回userList:', res) 18 | setUserList(list) 19 | 20 | if (userList.length === 0) { 21 | setHint('未搜索到相关用户') 22 | } 23 | 24 | //拼接avatarUrl 25 | const listWithAvatar = await Promise.all(list.map(async user => { 26 | const avatarRes = await previewFileApi(user.avatar) 27 | return { ...user, avatarUrl: avatarRes.data } 28 | })) 29 | console.log('拼接后的userList:', listWithAvatar) 30 | setUserList(listWithAvatar) 31 | } 32 | 33 | const onClickUser = (uid) => { 34 | navigate(`/profile/${uid}`) 35 | } 36 | 37 | return ( 38 |
39 | 46 |
47 | {userList.length === 0 ? ( 48 |
{hint}
49 | ) : ( 50 |
51 | {userList.map((item, index) => ( 52 |
53 | } 59 | isLink 60 | onClick={() => onClickUser(item.uid)} 61 | /> 62 |
63 | )) 64 | } 65 |
66 | )} 67 | 68 |
69 |
70 | ) 71 | } 72 | 73 | export default NewFriend -------------------------------------------------------------------------------- /client/app/src/pages/Friends/NewFriend/NewFriend.scoped.scss: -------------------------------------------------------------------------------- 1 | .hint { 2 | width: 100vw; 3 | display: flex; 4 | justify-content: space-around; 5 | font-size: 1.2em; 6 | color: rgb(159, 159, 159); 7 | } -------------------------------------------------------------------------------- /client/app/src/pages/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { 3 | Image, NavBar, Toast, Tabs, Sticky, Typography 4 | } from 'react-vant' 5 | import { Arrow, CommentO, LikeO, BookmarkO } from '@react-vant/icons' 6 | import { useState } from 'react' 7 | import './Home.scoped.scss' 8 | import useChannelList from '@/hooks/useChannelList' 9 | import { getChannelTopicsApi } from '@/apis/topic' 10 | import { previewFileApi } from '@/apis/file' 11 | import { useNavigate } from 'react-router-dom' 12 | import { getProfileAPI } from '@/apis/user' 13 | import Topic from '@/components/Topic/Topic' 14 | 15 | 16 | const Home = () => { 17 | //获取频道列表 18 | const { channelList, loading } = useChannelList() 19 | const [topicList, setTopicList] = useState([]) 20 | 21 | //在组件挂载时,加载初始话题列表 22 | const fetchChannelList = async () => { 23 | const channelKey = channelList[0].key 24 | const res = await getChannelTopicsApi(channelKey) 25 | const list = res.data 26 | 27 | //拼接coverUrl 28 | const topicListWithCover = await Promise.all( 29 | list.map(async topic => { 30 | const coverRes = await previewFileApi(topic.coverImg) 31 | return { ...topic, coverUrl: coverRes.data } 32 | }) 33 | ) 34 | console.log('拼接后的topicList:', topicListWithCover) 35 | setTopicList(topicListWithCover) 36 | } 37 | 38 | //初始化 39 | useEffect(() => { 40 | if (!loading) { 41 | fetchChannelList() 42 | } 43 | }, [channelList, loading]) 44 | 45 | 46 | //点击频道切换 47 | const onTabClick = async (channel) => { 48 | const channelKey = await channel.name 49 | console.log('选中频道:', channelKey) 50 | //根据切换的channelKey,切换展示的话题 51 | const res = await getChannelTopicsApi(channelKey) 52 | const list = res.data 53 | 54 | //拼接coverUrl 55 | const topicListWithCover = await Promise.all( 56 | list.map(async topic => { 57 | const coverRes = await previewFileApi(topic.coverImg) 58 | return { ...topic, coverUrl: coverRes.data } 59 | }) 60 | ) 61 | console.log('拼接后的topicList:', topicListWithCover) 62 | setTopicList(topicListWithCover) 63 | } 64 | 65 | 66 | //跳转指定用户主页 67 | const navigate = useNavigate() 68 | const toTargetProfile = (targetUid) => { 69 | navigate(`/profile/${targetUid}`) 70 | } 71 | 72 | return ( 73 |
74 | 75 | 79 | 80 | 81 |
82 | onTabClick(v)} 86 | > 87 | 88 | {channelList.map(item => ( 89 | 94 |
95 | {topicList.map((topic, index) => ( 96 | 111 | )) 112 | } 113 |
114 |
115 | ))} 116 | 117 |
118 |
119 |
120 | ) 121 | } 122 | 123 | export default Home 124 | 125 | -------------------------------------------------------------------------------- /client/app/src/pages/Home/Home.scoped.scss: -------------------------------------------------------------------------------- 1 | .bg-box { 2 | //background-color: #F6F7F9; 3 | //background-color: #f2f2f2; 4 | background-attachment: fixed; 5 | background-image: url("/assets/bg_1.jpg"); 6 | 7 | --rv-tabs-nav-background-color: rgba(215, 223, 228, 0.8); 8 | --rv-tab-text-color: rgb(55, 78, 101); 9 | --rv-tab-active-text-color: #1d6b99; 10 | --rv-tabs-bottom-bar-color: #1d6b99; 11 | --rv-tabs-line-height: 30px; 12 | } 13 | 14 | .topic-box { 15 | background-color: #fff; 16 | margin: 1vw 3vw 3vw 3vw; 17 | border-radius: 0.5rem; 18 | padding: 1rem; 19 | box-shadow: 0 2px 0.6rem 0 rgba(0, 0, 0, 0.2), 0 4px 0.5rem 0 rgb(0 0 0 / 0.2); 20 | } 21 | 22 | .topic-header { 23 | display: flex; 24 | align-items: center; 25 | gap: 0.3rem; 26 | 27 | &__title { 28 | font-size: 1.3rem; 29 | font-weight: 550; 30 | } 31 | 32 | &__channel { 33 | border-radius: 0.5rem; 34 | padding: 0.2rem 0.6rem; 35 | font-size: 0.7rem; 36 | color: #28558f; 37 | background-color: #E5EDF9; 38 | } 39 | } 40 | 41 | // 链接取消变色和下划线 42 | a { 43 | text-decoration: none; //取消下划线 44 | color: inherit; //继承父元素的文本颜色 45 | } 46 | 47 | a:link, 48 | a:visited, 49 | a:hover, 50 | a:active { 51 | text-decoration: none; 52 | color: inherit; 53 | } 54 | 55 | .topic-content { 56 | margin-top: 0.3rem; 57 | font-size: 1rem; 58 | 59 | //设置超出2行文本省略 60 | display: -webkit-box; //设置为弹性盒子布局 61 | -webkit-box-orient: vertical; //设置弹性盒子的方向为垂直方向 62 | -webkit-line-clamp: 2; //设置显示的行数为两行 63 | overflow: hidden; //隐藏溢出的文本 64 | text-overflow: ellipsis; 65 | } 66 | 67 | .topic-cover { 68 | margin-top: 0.5rem; 69 | border-radius: 0.5rem; 70 | // height: 50%; 71 | // width: 50%; 72 | } 73 | 74 | .topic-bottom { 75 | display: flex; 76 | justify-content: space-between; 77 | align-items: center; 78 | margin-top: 0.5rem; 79 | 80 | &__right { 81 | display: flex; 82 | gap: 0.5rem; 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /client/app/src/pages/Login/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Input, Form, Toast, Image } from 'react-vant'; 3 | import './Login.scoped.scss' 4 | import { useDispatch } from 'react-redux'; 5 | import { fetchLogin } from '@/store/modules/user'; 6 | import { Link, useNavigate } from 'react-router-dom'; 7 | import logoImage from '@/assets/logo-embodied.png' 8 | 9 | const Login = () => { 10 | const [form] = Form.useForm() 11 | const dispatch = useDispatch() //在组件中调dispatch方法,需要用钩子函数useDispatch 12 | const navigate = useNavigate() 13 | 14 | const handleLogin = async (loginForm) => { 15 | console.log('登录信息:', loginForm) 16 | //触发异步action fetchLogin 17 | const resCode = await dispatch(fetchLogin(loginForm)) //参数就是收集到的表单数据values 18 | console.log('dispatch方法返回:', resCode) 19 | 20 | if (resCode === 20000) { 21 | //登录完成后,1跳转到首页 2提示用户是否登录成功 22 | navigate('/') 23 | Toast.success('登录成功') 24 | } else { 25 | Toast.success('登录失败,请检查用户名和密码') 26 | } 27 | } 28 | 29 | return ( 30 |
31 |
38 | 39 |
40 |

还没有账户? 41 | 注册 42 |

43 |
44 |
45 | } 46 | > 47 | 48 |
49 |
50 | 51 |
52 |
Embodied
53 |
54 | 55 | 65 | 68 | 69 | 76 | 80 | 81 | 82 | 83 | ) 84 | } 85 | 86 | export default Login -------------------------------------------------------------------------------- /client/app/src/pages/Login/Login.scoped.scss: -------------------------------------------------------------------------------- 1 | .login-page { 2 | display: flex; 3 | width: 100vw; 4 | height: 100vh; 5 | justify-content: center; 6 | align-items: center; 7 | //background-image: linear-gradient(135deg, #9055ff, #13e2da); // box-shadow: 0.25em 0.25em 2em rgba(0, 0, 0, 0.25); 8 | background-image: url("/assets/bg_1.jpg"); 9 | overflow: hidden; 10 | --rv-cell-background-color: rgba(255, 255, 255, 0.4); 11 | } 12 | 13 | .logo { 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | gap: 2vw; 18 | 19 | &__image { 20 | width: 13vw; 21 | height: 13vw; 22 | } 23 | 24 | &__name { 25 | /*实现文字颜色渐变效果*/ 26 | background: linear-gradient(135deg, rgba(34, 95, 159, 1) 20%, rgb(170, 54, 46, 0.95) 65%, rgba(249, 144, 69, 1) 98%); //设置渐变 27 | -webkit-background-clip: text; //将设置的背景颜色限制在文字中 28 | -webkit-text-fill-color: transparent; //给文字设置成透明 29 | font-family: "STXingkai", Sans-serif; 30 | font-size: 4rem; 31 | } 32 | } 33 | 34 | 35 | .login-form { 36 | width: 80vw; 37 | max-width: 80vw; 38 | margin: 0 auto; 39 | box-sizing: border-box; 40 | padding: 15vw 8vw 8vw 8vw; 41 | background-color: #ffffff; 42 | text-align: center; 43 | box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); 44 | background: rgba(255, 255, 255, 0.4); 45 | border-top-color: rgba(255, 255, 255, .4); 46 | border-left-color: rgba(255, 255, 255, .4); 47 | border-bottom-color: rgba(60, 60, 60, .4); 48 | border-right-color: rgba(60, 60, 60, .4); 49 | } 50 | 51 | .login-button { 52 | width: 60vw; 53 | border: 1px solid; 54 | border-bottom-color: rgba(255, 255, 255, .5); 55 | border-right-color: rgba(60, 60, 60, .35); 56 | border-top-color: rgba(60, 60, 60, .35); 57 | border-left-color: rgba(80, 80, 80, .45); 58 | background-color: rgba(5, 106, 150, 0.7); 59 | background-repeat: no-repeat; 60 | font: bold 0.9rem/1.25rem "Open Sans Condensed", sans-serif; 61 | letter-spacing: .075rem; 62 | color: #fff; 63 | text-shadow: 0 1px 0 rgba(0, 0, 0, .1); 64 | margin-top: 15px; 65 | } 66 | 67 | .route-navigate { 68 | text-align: center; 69 | color: rgb(70, 70, 70); 70 | font-weight: 200; 71 | font-size: 1.1rem; 72 | 73 | a { 74 | font-weight: 600; 75 | text-decoration: none; 76 | color: rgb(216, 139, 80); 77 | } 78 | } -------------------------------------------------------------------------------- /client/app/src/pages/Message/Message.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { NavBar, Badge, Image } from 'react-vant' 3 | import { getMsgHistoryApi } from '@/apis/message' 4 | import './Message.scoped.scss' 5 | import { previewFileApi } from '@/apis/file' 6 | import { useNavigate } from 'react-router-dom' 7 | import { FriendsO } from '@react-vant/icons'; 8 | 9 | const Message = () => { 10 | const navigate = useNavigate() 11 | const [msgHisList, setMsgHisList] = useState([]) 12 | const [avatarUrl, setAvatarUrl] = useState() 13 | 14 | //初始化数据 15 | useEffect(() => { 16 | fetchMsgHistory() 17 | }, []) 18 | 19 | //获取消息历史 20 | const fetchMsgHistory = async () => { 21 | const res = await getMsgHistoryApi() 22 | const list = res.data 23 | 24 | //拼接avatarUrl 25 | const listWithAvatar = await Promise.all(list.map(async item => { 26 | let avatarRes = await previewFileApi(item.senderAvatar) 27 | return { ...item, senderAvatarUrl: avatarRes.data } 28 | })) 29 | console.log('拼接后的消息List:', listWithAvatar) 30 | setMsgHisList(listWithAvatar) 31 | } 32 | 33 | //点击跳转对应sender的聊天页面 34 | const onClickSender = (targetId, senderNickName, senderAvatarUrl) => { 35 | navigate('/chat', { state: { targetId, senderNickName, senderAvatarUrl } }) 36 | } 37 | 38 | return ( 39 |
40 | } 43 | rightText="添加好友" 44 | onClickLeft={() => navigate('/myFriends')} 45 | onClickRight={() => navigate('/newFriend')} 46 | /> 47 | {msgHisList === null || avatarUrl === null ? ( 48 |
loading...
49 | ) : ( 50 |
51 | {msgHisList.map((msg, index) => ( 52 |
onClickSender(msg.senderId, msg.senderNickName, msg.senderAvatarUrl)} 56 | > 57 |
58 | 62 |
63 | 64 |
65 |
66 |
67 | {msg.senderNickName} 68 |
69 |
70 | {msg.lastMsgDateTime} 71 |
72 |
73 |
74 |
75 | {msg.lastMsg} 76 |
77 |
78 | 79 |
80 |
81 |
82 | 83 |
84 | ))} 85 |
) 86 | } 87 |
88 | ) 89 | } 90 | 91 | export default Message -------------------------------------------------------------------------------- /client/app/src/pages/Message/Message.scoped.scss: -------------------------------------------------------------------------------- 1 | .message-layout { 2 | // background-image: url('@/assets/message_nnnoise.svg'); 3 | } 4 | 5 | .msg-box { 6 | display: flex; 7 | padding: 0.5rem 0.7rem; 8 | gap: 0.5rem; 9 | border-bottom: 1px solid rgb(235, 237, 240) 10 | } 11 | 12 | .msg-left { 13 | width: 15vw; 14 | height: 15vw; 15 | } 16 | 17 | 18 | .msg-right { 19 | display: flex; 20 | flex-direction: column; 21 | width: 80vw; 22 | justify-content: space-around; 23 | padding: 0.3rem 0 0.3rem 0; 24 | } 25 | 26 | 27 | .msg-content { 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | align-items: center; 32 | 33 | .msg-sender { 34 | font-size: 1.3rem; 35 | font-weight: 540; 36 | } 37 | 38 | .msg-last-time{ 39 | font-size: 1rem; 40 | color: grey; 41 | } 42 | } 43 | 44 | 45 | .msg-lastMsg { 46 | display: flex; 47 | flex-direction: row; 48 | justify-content: space-between; 49 | align-items: center; 50 | 51 | 52 | .msg-last-content{ 53 | font-size: 1rem; 54 | color: grey; 55 | } 56 | } -------------------------------------------------------------------------------- /client/app/src/pages/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | const NotFound = () => { 2 | return
我是404页 3 |
4 | } 5 | 6 | export default NotFound -------------------------------------------------------------------------------- /client/app/src/pages/Post/Post.scoped.scss: -------------------------------------------------------------------------------- 1 | .post-layout { 2 | //background-color: #F6F7F9; 3 | //background-color: #c7e1ff; 4 | height: 100vh; 5 | background-image: url("/assets/bg_1.jpg"); 6 | overflow: hidden; 7 | } 8 | 9 | 10 | .form-item { 11 | display: flex; 12 | align-items: center; 13 | flex-direction: row; 14 | background-color: #ffffffea; 15 | padding: 3vw 3vw; 16 | margin: 0 3vw; 17 | 18 | &__name { 19 | font-size: 1.2rem; 20 | width: 13vw; 21 | } 22 | 23 | &__value { 24 | left: 20vw; 25 | width: 100%; 26 | } 27 | } 28 | 29 | 30 | .post-container { 31 | display: flex; 32 | flex-direction: column; 33 | height: 80%; 34 | } 35 | 36 | //item1:title 37 | .item-title { 38 | margin-top: 2vh; 39 | border-top-left-radius: 1rem; 40 | border-top-right-radius: 1rem; 41 | border-bottom: solid #f3f6fd 1px; 42 | height: 3vh; 43 | flex: 1; 44 | } 45 | 46 | //item2:content 47 | .item-content { 48 | margin-bottom: 0.3rem; 49 | flex: 2; 50 | } 51 | 52 | //item3:channel 53 | .item-channel { 54 | border-bottom: solid #f3f6fd 1px; 55 | height: 3vh; 56 | flex: 1; 57 | } 58 | 59 | .channel-button { 60 | padding: 1vw 13vw; 61 | height: 2.5rem; 62 | border-radius: var(--rv-popover-border-radius); 63 | } 64 | 65 | //item4:tag 66 | .item-tag { 67 | margin-bottom: 0.3rem; 68 | height: 3vh; 69 | flex: 1; 70 | } 71 | 72 | .tag-button { 73 | padding: 1vw 2vw; 74 | height: 2.3rem; 75 | border-radius: var(--rv-popover-border-radius); 76 | flex: 1; 77 | 78 | &__add { 79 | border-color: #3f45ff; 80 | color: #3f45ff; 81 | } 82 | 83 | &__cancel { 84 | left: 1vw; 85 | border-color: #f44336; 86 | color: #f44336; 87 | } 88 | } 89 | 90 | .form-tag-item { 91 | display: flex; 92 | flex-direction: column; 93 | } 94 | 95 | .post-tag-box { 96 | display: flex; 97 | flex-direction: row; 98 | align-items: center; 99 | flex-wrap: wrap; 100 | } 101 | 102 | .post-tag-input { 103 | display: flex; 104 | flex-direction: row; 105 | align-items: center; 106 | } 107 | 108 | //item5: 封面 109 | .item-cover { 110 | border-bottom: solid #F6F7F9 1px; 111 | height: 10vh; 112 | } 113 | 114 | //item6: 按钮 115 | .item-button { 116 | border-bottom-left-radius: 1rem; 117 | border-bottom-right-radius: 1rem; 118 | padding: 3vw 0; 119 | } 120 | 121 | .post-botton-box { 122 | padding: 2vw; 123 | display: flex; 124 | justify-content: space-between; 125 | } 126 | 127 | .post-button { 128 | border-radius: var(--rv-popover-border-radius); 129 | 130 | &__publish { 131 | width: 72vw; 132 | background-color: #3f45ff; 133 | font-weight: 500; 134 | color: #fff; 135 | } 136 | 137 | &__draft { 138 | width: 20vw; 139 | background-color: #DEDCFF; 140 | // color: #fff; 141 | } 142 | } -------------------------------------------------------------------------------- /client/app/src/pages/Profile/Bookmark/Bookmark.jsx: -------------------------------------------------------------------------------- 1 | 2 | const Bookmark = () => { 3 | return ( 4 |
我是收藏
5 | ) 6 | } 7 | 8 | 9 | export default Bookmark -------------------------------------------------------------------------------- /client/app/src/pages/Profile/History/Histtory.jsx: -------------------------------------------------------------------------------- 1 | const History = () => { 2 | return ( 3 |
我是历史
4 | ) 5 | } 6 | 7 | 8 | export default History -------------------------------------------------------------------------------- /client/app/src/pages/Profile/Like/Like.jsx: -------------------------------------------------------------------------------- 1 | const Like = () => { 2 | return ( 3 |
我是点赞
4 | ) 5 | } 6 | 7 | 8 | export default Like -------------------------------------------------------------------------------- /client/app/src/pages/Profile/MyPost/MyPost.jsx: -------------------------------------------------------------------------------- 1 | const MyPost = () => { 2 | return ( 3 |
我是笔记
4 | ) 5 | } 6 | 7 | 8 | export default MyPost -------------------------------------------------------------------------------- /client/app/src/pages/Profile/MyProfile/MyProfile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Outlet, useNavigate, useParams } from 'react-router-dom' 3 | import { Image, Button, Tabs, Dialog } from 'react-vant' 4 | import { useDispatch } from 'react-redux' 5 | import { clearUserInfo } from '@/store/modules/user' 6 | import './MyProfile.scoped.scss' 7 | import useUserDetail from '@/hooks/useUserDetail' 8 | import { getUserId as _getUserId } from '@/utils' 9 | import { getUserExtendsAPI } from '@/apis/user' 10 | 11 | const MyProfile = () => { 12 | const tabs = [ 13 | { 14 | key: '/myPost', 15 | title: "我的创作" 16 | }, 17 | { 18 | key: '/like', 19 | title: "点赞" 20 | }, 21 | { 22 | key: '/bookmark', 23 | title: "收藏" 24 | }]; 25 | 26 | const navigate = useNavigate() 27 | const dispatch = useDispatch() 28 | const { userId } = useParams() 29 | const [followsCount, setFollowsCount] = useState('--') 30 | const [followersCount, setFollowersCount] = useState('--') 31 | const [collectsCount, setCollectsCount] = useState('--') 32 | 33 | const { userProfile, avatarUrl } = useUserDetail(_getUserId()) 34 | console.log('用户详情:', userProfile, '头像url:', avatarUrl) 35 | console.log('用户头像:', avatarUrl) 36 | 37 | //数据初始化 38 | useEffect(() => { 39 | getUserExtendsInfo() 40 | }, []) 41 | 42 | const getUserExtendsInfo = async () => { 43 | const res = await getUserExtendsAPI(_getUserId()) 44 | console.log('关注数量等:', res.data) 45 | setFollowersCount(res.data.followersCount); 46 | setFollowsCount(res.data.followsCount); 47 | } 48 | 49 | const onTabChange = (path) => { 50 | console.log('切换路由:', path) 51 | navigate(`/profile/${userId}${path}`) 52 | } 53 | 54 | const [logoutDialogVisible, setLogoutDialogVisible] = useState(false) 55 | 56 | return ( 57 |
58 | 59 | { 64 | console.log('确认退出') 65 | dispatch(clearUserInfo()) 66 | navigate('/login') 67 | }} 68 | onCancel={() => setLogoutDialogVisible(false)} 69 | /> 70 | 71 |
72 | 73 |
74 |
75 | 76 |
77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
{followsCount}{followersCount}11
关注粉丝获赞
91 | 92 |
93 | 98 | 99 | 104 |
105 |
106 | 107 |
108 | 109 |
110 |
{userProfile.userDetail.nickName}
111 |
留下你的介绍吧......
112 |
113 |
114 | 115 | 116 |
117 | onTabChange(name)}> 121 | {tabs.map( 122 | item => ( 123 | 124 |
125 | 126 |
127 |
128 | ) 129 | )} 130 |
131 |
132 | 133 | {/*
134 | 135 |
*/} 136 |
137 | ) 138 | } 139 | 140 | export default MyProfile -------------------------------------------------------------------------------- /client/app/src/pages/Profile/MyProfile/MyProfile.scoped.scss: -------------------------------------------------------------------------------- 1 | .top-layout { 2 | //background: linear-gradient(to left, #525252, #3d72b4); 3 | 4 | //1 蓝 #364b85 橙 #b45c39 红 #97102c 5 | //background: linear-gradient(135deg, rgba(54, 75, 133, 1) 20%, rgba(180, 92, 57, 0.9) 60%, rgba(151, 16, 44, 1) 96%); 6 | 7 | //2 蓝 #225f9f 橙 #f99045 8 | //background: linear-gradient(135deg, rgba(34, 95, 159, 1) 10%, rgba(249, 144, 69, 1) 90%); 9 | 10 | //3 蓝 #225f9f 红 #cb3125 橙 #f99045 11 | //background: linear-gradient(135deg, rgba(34, 95, 159, 1) 15%, rgb(170, 54, 46, 0.95) 60%, rgba(249, 144, 69, 1) 95%); 12 | //修改版: 13 | background: linear-gradient(135deg, rgba(34, 95, 159, 1) 20%, rgb(170, 54, 46, 0.95) 65%, rgba(249, 144, 69, 1) 98%); 14 | 15 | padding: 4vh 7vw 4vh 7vw; 16 | 17 | .profile-social { 18 | height: 14vh; 19 | width: 80vw; 20 | display: flex; 21 | gap: 2vw; 22 | 23 | .profile-img { 24 | width: 24vw; 25 | height: 24vw; 26 | } 27 | 28 | .profile-social-right { 29 | display: flex; 30 | flex-direction: column; 31 | 32 | table { 33 | width: 55vw; 34 | height: 8vh; 35 | padding-top: 0.1rem; 36 | } 37 | 38 | table td { 39 | padding: 0.5rem; 40 | text-align: center; 41 | line-height: 0.5rem; 42 | } 43 | 44 | .top-row { 45 | font-size: 1.4rem; 46 | color: #ededed; 47 | font-weight: 600; 48 | } 49 | 50 | .bottom-row { 51 | font-size: 1.1rem; 52 | color: #c8c8c8; 53 | } 54 | 55 | .profile-button { 56 | padding-top: 0.5rem; 57 | 58 | .profile-edit-button { 59 | background-color: rgba(146, 172, 223, 0.354); 60 | border: solid #efebeb 1px; 61 | color: rgb(186, 202, 235); 62 | font-size: 1rem; 63 | height: 2.2rem; 64 | 65 | width: 10rem; 66 | left: 1.5rem; 67 | } 68 | 69 | .profile-logout-button { 70 | background-color: rgba(146, 172, 223, 0.354); 71 | border: solid #efebeb 1px; 72 | color: rgb(186, 202, 235); 73 | font-size: 1rem; 74 | height: 2.2rem; 75 | 76 | width: 4rem; 77 | left: 2rem; 78 | padding: 0; 79 | } 80 | } 81 | } 82 | } 83 | 84 | .profile-user { 85 | display: flex; 86 | flex-direction: column; 87 | 88 | width: 80vw; 89 | height: 18vh; 90 | } 91 | 92 | .profile-username { 93 | font-size: 1.8rem; 94 | color: #F9F7F3; 95 | } 96 | 97 | .profile-intro { 98 | font-size: 1.1rem; 99 | color: #c8c8c8; 100 | margin-top: 0.1rem; 101 | } 102 | } 103 | 104 | .bottom-layout { 105 | // background-color: #f6f7f9; 106 | } -------------------------------------------------------------------------------- /client/app/src/pages/Profile/OtherProfile/OtherProfile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { useParams } from 'react-router-dom' 3 | import { Image, Toast, Button } from 'react-vant' 4 | import { ChatO } from '@react-vant/icons' 5 | import useUserDetail from '@/hooks/useUserDetail' 6 | import './OtherProfile.scoped.scss' 7 | import { checkFollowAPI, checkFriendAPI, followAPI, unFollowAPI } from '@/apis/user' 8 | 9 | const OtherProfile = () => { 10 | const { userId } = useParams() 11 | const { userProfile, avatarUrl } = useUserDetail(16) 12 | console.log('用户详情:', userProfile, '头像url:', avatarUrl) 13 | 14 | const username = userProfile.userName 15 | const userImgUrl = 'https://img.yzcdn.cn/vant/cat.jpeg' 16 | 17 | const [isFollow, setIsFollow] = useState() 18 | const [isFriend, setIsFriend] = useState() 19 | 20 | //初始化 21 | useEffect(() => { 22 | loadData() 23 | }, []) 24 | 25 | const loadData = async () => { 26 | //查看是否关注 27 | const checkFollowRes = await checkFollowAPI(userId) 28 | console.log('是否关注:', checkFollowRes.data) 29 | checkFollowRes.data === 'true' ? setIsFollow(true) : setIsFollow(false) 30 | 31 | //查看是否好友 32 | const checkFriendRes = await checkFriendAPI(userId) 33 | console.log('是否好友:', checkFriendRes.data) 34 | checkFriendRes.data === 'true' ? setIsFriend(true) : setIsFriend(false) 35 | } 36 | 37 | //加关注(取消关注) 38 | const onClickFollow = async () => { 39 | if (isFollow) { 40 | const unFollowRes = await unFollowAPI(userId) 41 | unFollowRes.code === 20000 ? setIsFollow(false) : setIsFollow(true) 42 | Toast.info('取消关注') 43 | } else { 44 | const followRes = await followAPI(userId) 45 | followRes.code === 20000 ? setIsFollow(true) : setIsFollow(false) 46 | Toast.info('关注成功') 47 | } 48 | } 49 | 50 | //加好友(取消好友) 51 | const onClickFriend = async () => { 52 | if (isFriend) { 53 | 54 | } else { 55 | 56 | } 57 | } 58 | 59 | //发消息 60 | const onClickMessage = () => { 61 | 62 | } 63 | 64 | return ( 65 |
66 | {isFollow === null ? ( 67 |
loading...
68 | ) : ( 69 |
70 |
71 |
72 |
73 | 74 |
75 | 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
2,14651M11
关注粉丝获赞
89 | 90 |
91 | { 92 | isFollow ? ( 93 | 97 | ) : ( 98 | 102 | ) 103 | } 104 | 105 | { 106 | isFriend ? ( 107 | 111 | ) : ( 112 | 116 | ) 117 | } 118 | 119 | 120 |
121 |
122 | 123 |
124 | 125 |
126 |
{username}
127 |
留下你的介绍吧......
128 |
129 |
130 | 131 | 132 |
133 | 134 |
135 |
136 | )} 137 | 138 |
139 | ) 140 | 141 | } 142 | 143 | export default OtherProfile -------------------------------------------------------------------------------- /client/app/src/pages/Profile/OtherProfile/OtherProfile.scoped.scss: -------------------------------------------------------------------------------- 1 | .top-layout { 2 | //background: linear-gradient(to left, #525252, #3d72b4); 3 | 4 | //1 蓝 #364b85 橙 #b45c39 红 #97102c 5 | //background: linear-gradient(135deg, rgba(54, 75, 133, 1) 20%, rgba(180, 92, 57, 0.9) 60%, rgba(151, 16, 44, 1) 96%); 6 | 7 | //2 蓝 #225f9f 橙 #f99045 8 | //background: linear-gradient(135deg, rgba(34, 95, 159, 1) 10%, rgba(249, 144, 69, 1) 90%); 9 | 10 | //3 蓝 #225f9f 红 #cb3125 橙 #f99045 11 | background: linear-gradient(135deg, rgba(34, 95, 159, 1) 20%, rgb(170, 54, 46, 0.95) 65%, rgba(249, 144, 69, 1) 98%); 12 | 13 | padding: 4vh 7vw 4vh 7vw; 14 | 15 | .profile-social { 16 | height: 14vh; 17 | width: 80vw; 18 | display: flex; 19 | gap: 2vw; 20 | 21 | .profile-img { 22 | width: 24vw; 23 | height: 24vw; 24 | } 25 | 26 | .profile-social-right { 27 | display: flex; 28 | flex-direction: column; 29 | 30 | table { 31 | width: 55vw; 32 | height: 8vh; 33 | padding-top: 0.1rem; 34 | } 35 | 36 | table td { 37 | padding: 0.5rem; 38 | text-align: center; 39 | line-height: 0.5rem; 40 | } 41 | 42 | .top-row { 43 | font-size: 1.4rem; 44 | color: #ededed; 45 | font-weight: 600; 46 | } 47 | 48 | .bottom-row { 49 | font-size: 1.1rem; 50 | color: #c8c8c8; 51 | } 52 | 53 | .profile-social-button { 54 | display: flex; 55 | gap: 0.5rem; 56 | padding-top: 0.5rem; 57 | 58 | .follow-button, 59 | .friend-button { 60 | background-color: rgba(146, 172, 223, 0.354); 61 | border: solid #efebeb 1px; 62 | color: rgb(186, 202, 235); 63 | font-size: 1rem; 64 | height: 2.2rem; 65 | 66 | width: 5rem; 67 | padding: 0; 68 | left: 1.5rem; 69 | } 70 | 71 | .message-icon { 72 | color: rgb(186, 202, 235); 73 | font-size: 2.2rem; 74 | padding-left: 2rem; 75 | } 76 | } 77 | } 78 | } 79 | 80 | .profile-user { 81 | display: flex; 82 | flex-direction: column; 83 | 84 | width: 80vw; 85 | height: 18vh; 86 | } 87 | 88 | .profile-username { 89 | font-size: 1.8rem; 90 | color: #F9F7F3; 91 | } 92 | 93 | .profile-intro { 94 | font-size: 1.1rem; 95 | color: #c8c8c8; 96 | margin-top: 0.1rem; 97 | } 98 | } 99 | 100 | .profile-social-button { 101 | // background-color: #f6f7f9; 102 | } -------------------------------------------------------------------------------- /client/app/src/pages/Profile/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useParams } from 'react-router-dom' 3 | import { getUserId as _getUserId } from '@/utils' 4 | import OtherProfile from './OtherProfile/OtherProfile' 5 | import MyProfile from './MyProfile/MyProfile' 6 | 7 | const Profile = () => { 8 | //从url参数中获取用户名 9 | const { userId } = useParams() 10 | const [loginUserId, setLoginUserId] = useState(_getUserId) 11 | 12 | return ( 13 |
14 | {userId === loginUserId ? ( 15 | 16 | ) : ( 17 | 18 | ) 19 | } 20 |
21 | ) 22 | } 23 | 24 | export default Profile -------------------------------------------------------------------------------- /client/app/src/pages/Profile/UserDetail/UserDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { Form, Button, Radio, NavBar, Toast, DatetimePicker } from 'react-vant' 3 | import { useNavigate } from 'react-router-dom' 4 | import { getUserId as _getUserId } from '@/utils' 5 | import { useDispatch } from 'react-redux' 6 | import { fetchUserInfo } from '@/store/modules/user' 7 | 8 | const UserDetail = () => { 9 | 10 | const dispatch = useDispatch() 11 | const navigate = useNavigate() 12 | const [form] = Form.useForm() 13 | 14 | // useEffect(() => { 15 | // const userInfo = dispatch(fetchUserInfo()) 16 | // console.log(userInfo) 17 | // }, [dispatch]) 18 | 19 | const onFinish = values => { 20 | console.log(values) 21 | } 22 | 23 | return ( 24 |
25 | 26 | navigate(`/profile/${_getUserId}`)} 31 | /> 32 | 33 |
38 | 39 |
40 | } 41 | > 42 | 43 | 44 |
Embodied
45 |
46 | 47 | 48 |
embodied@...
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | { 64 | action.current?.open() 65 | }} 66 | > 67 | 68 | {(val) => (val ? val.toDateString() : '请选择日期')} 69 | 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | 77 | 78 | export default UserDetail -------------------------------------------------------------------------------- /client/app/src/pages/Register/Register.jsx: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from "react-router-dom" 2 | import { Button, Input, Form, Image, Toast } from 'react-vant'; 3 | import { UserO, Lock, EnvelopO, PhoneO } from '@react-vant/icons' 4 | import './Register.scoped.scss' 5 | import { registerAPI } from "@/apis/user"; 6 | import logoImage from '@/assets/logo-embodied.png' 7 | 8 | const Register = () => { 9 | const navigate = useNavigate() 10 | const [form] = Form.useForm() 11 | const onFinish = async (registerForm) => { 12 | const res = await registerAPI(registerForm) 13 | console.log('注册成功返回:', res) 14 | if (res.code === 20000) { 15 | Toast.info('注册成功') 16 | navigate('/login') 17 | } else { 18 | Toast.info('注册失败') 19 | } 20 | } 21 | 22 | return ( 23 |
24 |
31 | 32 |
33 |

已经有账户? 34 | 登录 35 |

36 |
37 |
38 | } 39 | > 40 | 41 |
42 |
43 | 44 |
45 |
Embodied
46 |
47 | 48 | 58 | > 59 | 60 | 61 | 73 | > 74 | 75 | 76 | 77 | { 84 | return new Promise((resolve, reject) => { 85 | if (value === form.getFieldValue('password')) { 86 | resolve();//校验通过 87 | } else { 88 | reject(new Error('输入的密码不一致,请确认密码'))//校验失败 89 | } 90 | }) 91 | } 92 | } 93 | ]} 94 | leftIcon= 95 | > 96 | 97 | 98 | 99 | 112 | > 113 | 114 | 115 | 116 | 126 | > 127 | 128 | 129 | 130 | 131 | 132 | 133 | ) 134 | } 135 | 136 | export default Register -------------------------------------------------------------------------------- /client/app/src/pages/Register/Register.scoped.scss: -------------------------------------------------------------------------------- 1 | .register-page { 2 | display: flex; 3 | width: 100vw; 4 | height: 100vh; 5 | justify-content: center; 6 | align-items: center; 7 | background-image: url("/assets/bg_1.jpg"); 8 | overflow: hidden; 9 | --rv-cell-background-color: rgba(255, 255, 255, 0.4); 10 | --rv-field-intro-color: rgb(250, 149, 72); 11 | --rv-field-error-message-font-size: 0.8rem; 12 | } 13 | 14 | .logo { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | gap: 2vw; 19 | 20 | &__image { 21 | width: 13vw; 22 | height: 13vw; 23 | } 24 | 25 | &__name { 26 | /*实现文字颜色渐变效果*/ 27 | background: linear-gradient(135deg, rgba(34, 95, 159, 1) 20%, rgb(170, 54, 46, 0.95) 65%, rgba(249, 144, 69, 1) 98%); //设置渐变 28 | -webkit-background-clip: text; //将设置的背景颜色限制在文字中 29 | -webkit-text-fill-color: transparent; //给文字设置成透明 30 | font-family: "STXingkai", Sans-serif; 31 | font-size: 4rem; 32 | } 33 | } 34 | 35 | 36 | .register-form { 37 | width: 80vw; 38 | max-width: 80vw; 39 | margin: 0 auto; 40 | box-sizing: border-box; 41 | padding: 15vw 8vw 8vw 8vw; 42 | background-color: #ffffff; 43 | text-align: center; 44 | box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); 45 | background: rgba(255, 255, 255, 0.4); 46 | border-top-color: rgba(255, 255, 255, .4); 47 | border-left-color: rgba(255, 255, 255, .4); 48 | border-bottom-color: rgba(60, 60, 60, .4); 49 | border-right-color: rgba(60, 60, 60, .4); 50 | } 51 | 52 | .register-button { 53 | width: 60vw; 54 | border: 1px solid; 55 | border-bottom-color: rgba(255, 255, 255, .5); 56 | border-right-color: rgba(60, 60, 60, .35); 57 | border-top-color: rgba(60, 60, 60, .35); 58 | border-left-color: rgba(80, 80, 80, .45); 59 | background-color: rgba(5, 106, 150, 0.7); 60 | background-repeat: no-repeat; 61 | font: bold 0.9rem/1.25rem "Open Sans Condensed", sans-serif; 62 | letter-spacing: .075rem; 63 | color: #fff; 64 | text-shadow: 0 1px 0 rgba(0, 0, 0, .1); 65 | margin-top: 15px; 66 | } 67 | 68 | .route-navigate { 69 | text-align: center; 70 | color: rgb(70, 70, 70); 71 | font-weight: 200; 72 | font-size: 1.1rem; 73 | 74 | a { 75 | font-weight: 600; 76 | text-decoration: none; 77 | color: rgb(216, 139, 80); 78 | } 79 | } -------------------------------------------------------------------------------- /client/app/src/pages/Search/Search.jsx: -------------------------------------------------------------------------------- 1 | const Search = () => { 2 | return
我是搜索Search
3 | } 4 | 5 | export default Search -------------------------------------------------------------------------------- /client/app/src/pages/Test/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { CommentO, HomeO, Search, UserO, AddO } from '@react-vant/icons' 4 | import './index.scss' 5 | import { useNavigate } from 'react-router-dom' 6 | 7 | const Test = ({ activeTab, setActiveTab }) => { 8 | 9 | const tabs = [ 10 | { 11 | key: '/home', 12 | title: '首页', 13 | icon: , 14 | }, 15 | { 16 | key: '/discover', 17 | title: '发现', 18 | icon: , 19 | }, 20 | { 21 | key: '/post', 22 | title: '发布', 23 | icon: , 24 | }, 25 | { 26 | key: '/message', 27 | title: '消息', 28 | icon: , 29 | }, 30 | { 31 | // key: `/profile/${loginUserId}/myPost`, 32 | title: '我的', 33 | icon: , 34 | }, 35 | ] 36 | 37 | // const [activeTab, setActiveTab] = useState('/home') 38 | const dispatch = useDispatch() 39 | const navigate = useNavigate() 40 | 41 | const { userInfo } = useSelector(state => state.user.userInfo) 42 | console.log('redux中的userInfo:', userInfo) 43 | // useEffect(() => { 44 | // dispatch(fetchUserInfo()) 45 | // }, [dispatch]) 46 | 47 | const onClickTabbar = (path) => { 48 | setActiveTab(path) 49 | navigate(path) 50 | } 51 | 52 | return ( 53 | 54 |
55 |
    56 |
  • onClickTabbar('/home')} 59 | > 60 | {activeTab === '/home' ? 61 |
    62 | 63 |
    Home
    64 |
    65 | : } 66 |
  • 67 | 68 |
  • onClickTabbar('/discover')} 71 | > 72 | {activeTab === '/discover' ? 73 |
    74 | 75 |
    Discover
    76 |
    77 | : } 78 |
  • 79 | 80 |
  • setActiveTab('/post')} 83 | > 84 | {activeTab === '/post' ? 85 |
    86 | 87 |
    Post
    88 |
    89 | : } 90 |
  • 91 | 92 |
  • setActiveTab('/message')} 95 | > 96 | {activeTab === '/message' ? 97 |
    98 | 99 |
    Message
    100 |
    101 | : } 102 |
  • 103 | 104 |
  • setActiveTab('/profile')} 107 | > 108 | {activeTab === '/profile' ? 109 |
    110 | 111 |
    Profile
    112 |
    113 | : } 114 |
  • 115 |
116 |
117 | 118 | ) 119 | } 120 | 121 | export default Test -------------------------------------------------------------------------------- /client/app/src/pages/Test/index.scss: -------------------------------------------------------------------------------- 1 | .tab-bar { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | // padding: 0 2vw; 6 | 7 | background-color: #FFF; 8 | height: 50px; 9 | width: 100vw; 10 | border-top-left-radius: 1rem; 11 | border-top-right-radius: 1rem; 12 | 13 | display: flex; 14 | align-items: center; 15 | // box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1); 16 | 17 | ul { 18 | display: flex; 19 | justify-content: space-around; 20 | flex: 1; 21 | padding: 0; 22 | margin: 1vw; 23 | } 24 | 25 | .tab-item { 26 | text-align: center; 27 | flex: 1; 28 | 29 | transition: flex 0.3s ease, background 0.3s ease; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | 35 | .active-icon { 36 | display: flex; 37 | flex-direction: row; 38 | gap: 3px; 39 | } 40 | 41 | .tab-item.active { 42 | font-size: 1.2rem; 43 | border-radius: 2rem; 44 | padding: 0.4rem 0; 45 | flex: 2; //Make the active tab wider 46 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 47 | } 48 | 49 | .active__home { 50 | color: rgb(34, 95, 159); 51 | background: #E6F0FF; 52 | } 53 | 54 | .active__discover { 55 | color: #58437A; 56 | background: #F2E9F7; 57 | } 58 | 59 | .active__post { 60 | color: #A11A0A; 61 | background: #FAE6DA; 62 | } 63 | 64 | .active__message { 65 | color: #1892a6; 66 | background: #F3FCF8; 67 | } 68 | 69 | .active__profile { 70 | color: #EC8243; 71 | background: #FEF0D9; 72 | } 73 | } 74 | 75 | .tab-bar-icon { 76 | height: 6vw; 77 | width: 6vw; 78 | } -------------------------------------------------------------------------------- /client/app/src/pages/TopicDetail/TopicDetail.scss: -------------------------------------------------------------------------------- 1 | .comment-button { 2 | background-color: rgba(229, 229, 229, 0.7); 3 | color: rgb(113, 113, 113); 4 | } 5 | 6 | .topic-container { 7 | padding: 1vh 1vw 1vh 1vw; 8 | } 9 | 10 | .top-info { 11 | display: flex; 12 | gap: 1vw; 13 | 14 | .top-info-right { 15 | display: flex; 16 | flex-direction: column; 17 | 18 | .author-name { 19 | //font-size: 5vw; 20 | font-size: 1.5rem; 21 | } 22 | 23 | .post-time { 24 | color: #9ca3aa; 25 | // font-size: 3.5vw; 26 | font-size: 1rem; 27 | } 28 | } 29 | 30 | .author-avatar { 31 | width: 12vw; 32 | height: 12vw; 33 | } 34 | } 35 | 36 | 37 | .topic-detail-box { 38 | padding: 0.8rem 0.4rem 0 0.4rem; 39 | 40 | .topic-detail-title { 41 | font-size: 1.8rem; 42 | font-weight: 550; 43 | } 44 | 45 | .topic-detail-content { 46 | font-size: 1.2rem; 47 | padding: 0.2rem 0 2rem 0; 48 | } 49 | 50 | .topic-detail-visits { 51 | color: #9ca3aa; 52 | padding-bottom: 0.5rem; 53 | border-bottom: 1px solid rgb(235, 237, 240) 54 | } 55 | } 56 | 57 | .rv-divider { 58 | margin: 0; 59 | } 60 | 61 | 62 | .comment-box { 63 | padding: 0.8rem 0.4rem 0 0.4rem; 64 | 65 | .comment-count { 66 | padding-bottom: 0.8rem; 67 | } 68 | 69 | .indv-comment { 70 | padding-bottom: 0.8rem; 71 | display: flex; 72 | gap: 1rem; 73 | // flex-direction: column; 74 | 75 | .commenter-info {} 76 | 77 | .comment-text { 78 | font-size: 1.1rem; 79 | } 80 | 81 | .comment-seperator { 82 | border: none; 83 | /* 移除默认边框 */ 84 | width: 26rem; 85 | border-bottom: 1px solid rgb(235, 237, 240) 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /client/app/src/pages/View/View.jsx: -------------------------------------------------------------------------------- 1 | import { exploreTopicsApi, followTopicsApi } from '@/apis/discover' 2 | import { useEffect, useState } from 'react' 3 | import { useNavigate } from 'react-router-dom' 4 | import Topic from '@/components/Topic/Topic' 5 | 6 | const View = () => { 7 | //获取频道列表 8 | const [topicList, setTopicList] = useState([]) 9 | 10 | //初始化 11 | useEffect(() => { 12 | fetchTopicList() 13 | }, []) 14 | 15 | const fetchTopicList = async () => { 16 | const res = await exploreTopicsApi() 17 | setTopicList(res.data) 18 | console.log('view页返回:', res) 19 | } 20 | 21 | //跳转指定用户主页 22 | const navigate = useNavigate() 23 | const toTargetProfile = (targetUid) => { 24 | navigate(`/profile/${targetUid}`) 25 | } 26 | 27 | return ( 28 |
29 |
30 | {topicList.map((item, index) => ( 31 | 46 | )) 47 | } 48 |
49 |
50 | ) 51 | } 52 | 53 | export default View -------------------------------------------------------------------------------- /client/app/src/router/index.js: -------------------------------------------------------------------------------- 1 | //创建路由示例 绑定path element 2 | 3 | import { createBrowserRouter } from 'react-router-dom' 4 | import { AuthRoute } from '@/components/AuthRoute' 5 | import Home from '@/pages/Home/Home' //主页 6 | 7 | import Profile from '@/pages/Profile/Profile' //个人信息页 8 | import UserDetail from '@/pages/Profile/UserDetail/UserDetail' //个性信息修改 9 | import Bookmark from '@/pages/Profile/Bookmark/Bookmark' //个人信息页-我的收藏 10 | import Like from '@/pages/Profile/Like/Like' //个人信息页-我的点赞 11 | import MyPost from '@/pages/Profile/MyPost/MyPost' //个人信息页-我的发布 12 | 13 | import Discover from '@/pages/Discover/Discover' //发现页 14 | import Post from '@/pages/Post/Post' //新贴发布页 15 | import TopicDetail from '@/pages/TopicDetail/TopicDetail' //发布页详情 16 | import Message from '@/pages/Message/Message' //消息页 17 | import Chat from '@/pages/Chat/Chat' //聊天页 18 | import NewFriend from '@/pages/Friends/NewFriend/NewFriend' //添加好友 19 | import MyFriends from '@/pages/Friends/MyFriends/MyFriends' //我的好友 20 | 21 | import Login from '@/pages/Login/Login' //登录页 22 | import Register from '@/pages/Register/Register' //注册页 23 | import Test from '@/pages/Test' //测试页 24 | import FileUpload from '@/components/fileUpload' //文件上传测试页 25 | import Layout from '@/components/Layout' //布局页 26 | import Follow from '@/pages/Follow/Follow' //关注页 27 | import View from '@/pages/View/View' //随机推荐页 28 | 29 | 30 | const router = createBrowserRouter([ 31 | // { 32 | // path: '/', 33 | // element: , 34 | // }, 35 | // { 36 | // path: '/home', 37 | // element: 38 | // }, 39 | // { 40 | // path: '/discover', 41 | // element: , 42 | // children: [ 43 | // { 44 | // index: true, 45 | // element: 46 | // }, 47 | // { 48 | // path: 'message', 49 | // element: 50 | // } 51 | // ] 52 | // }, 53 | // { 54 | // path: '/post', 55 | // element: 56 | // }, 57 | // { 58 | // path: '/message', 59 | // element: 60 | // }, 61 | // { 62 | // path: '/profile/:userId', 63 | // element: , 64 | // children: [ 65 | // { 66 | // path: 'myPost', 67 | // element: 68 | // }, 69 | // { 70 | // path: 'bookmark', 71 | // element: 72 | // }, 73 | // { 74 | // path: 'like', 75 | // element: 76 | // }, 77 | // ] 78 | // }, 79 | 80 | { 81 | path: '/', 82 | element: , // 新增 Layout 组件作为布局 83 | children: [ 84 | { 85 | index: true, 86 | element: , 87 | }, 88 | { 89 | path: 'home', 90 | element: 91 | }, 92 | { 93 | path: 'discover', 94 | element: , 95 | children: [ 96 | { 97 | index: true, 98 | element: 99 | }, 100 | { 101 | path: 'follow', 102 | element: 103 | }, 104 | { 105 | path: 'view', 106 | element: 107 | } 108 | ] 109 | }, 110 | { 111 | path: 'post', 112 | element: 113 | }, 114 | { 115 | path: 'message', 116 | element: 117 | }, 118 | { 119 | path: 'profile/:userId', 120 | element: , 121 | children: [ 122 | { 123 | path: 'myPost', 124 | element: 125 | }, 126 | { 127 | path: 'bookmark', 128 | element: 129 | }, 130 | { 131 | path: 'like', 132 | element: 133 | }, 134 | ] 135 | } 136 | ] 137 | }, 138 | 139 | 140 | { 141 | path: '/login', 142 | element: 143 | }, 144 | { 145 | path: '/register', 146 | element: 147 | }, 148 | { 149 | path: '/userDetail', 150 | element: 151 | }, 152 | { 153 | path: '/test', 154 | element: 155 | }, 156 | { 157 | path: '/fileUpload', 158 | element: 159 | }, 160 | { 161 | path: '/topicDetail/:topicId', 162 | element: 163 | }, 164 | { 165 | path: '/chat', 166 | element: 167 | }, 168 | { 169 | path: '/newFriend', 170 | element: 171 | }, 172 | { 173 | path: '/myFriends', 174 | element: 175 | }, 176 | ]) 177 | 178 | export default router -------------------------------------------------------------------------------- /client/app/src/store/index.js: -------------------------------------------------------------------------------- 1 | //组合redux子模块 + 导出store实例 2 | 3 | import { configureStore } from "@reduxjs/toolkit"; 4 | import userReducer from "./modules/user"; 5 | 6 | export default configureStore({ 7 | reducer: { 8 | user: userReducer 9 | } 10 | }) -------------------------------------------------------------------------------- /client/app/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | //和用户相关的状态管理 2 | 3 | import { createSlice } from '@reduxjs/toolkit' 4 | import { removeToken, removeUserId, request } from '@/utils' 5 | import { 6 | setToken as _setToken, getToken as _getToken, 7 | setUserId as _setUserId, getUserId as _getUserId 8 | } from '@/utils' 9 | import { loginAPI, getProfileAPI } from '@/apis/user' 10 | 11 | 12 | const userStore = createSlice({ 13 | name: "user", //模块名 14 | //数据状态 15 | initialState: { 16 | token: _getToken() || '', //后端返回的类型是什么,这里的类型就是什么(这里是类型String) 17 | userInfo: {} 18 | }, 19 | //同步修改方法 20 | reducers: { 21 | setToken(state, action) { 22 | //state.token --> 拿到上面的state数据 23 | //action.payload --> 把action对象中payload载荷赋值给state,做到同步修改 24 | state.token = action.payload 25 | }, 26 | setUserInfo(state, action) { 27 | state.userInfo = action.payload 28 | }, 29 | clearUserInfo(state) { 30 | state.token = '' 31 | state.userInfo = {} 32 | removeToken() 33 | removeUserId() 34 | } 35 | } 36 | }) 37 | 38 | //解构出actionCreater 39 | const { setToken, setUserInfo, clearUserInfo } = userStore.actions 40 | 41 | //获取reducer函数 42 | const userReducer = userStore.reducer 43 | 44 | //异步方法 完成登录获取token 45 | const fetchLogin = (loginForm) => { 46 | return async (dispatch) => { 47 | //1.发送异步请求 48 | const res = await loginAPI(loginForm) 49 | dispatch({ type: 'LOGIN_SUCCESS', payload: res.code }) 50 | 51 | console.log('登录接口返回:', res) 52 | console.log('tokenName:', res.data.tokenName) 53 | console.log('tokenValue:', res.data.tokenValue) 54 | 55 | if (res.code === 20000) { 56 | //2.提交同步action进行token的存入 57 | const token = res.data.tokenValue 58 | dispatch(setToken(token)) 59 | 60 | //localStorage存一份token 61 | _setToken(token) 62 | 63 | //localStorage存一份uid 64 | _setUserId(res.data.loginId) 65 | 66 | //登录成功 67 | return res.code 68 | 69 | } else { 70 | //登录失败 71 | return res.code 72 | } 73 | } 74 | } 75 | 76 | //异步方法 获取个人用户信息 77 | const fetchUserInfo = () => { 78 | return async (dispatch) => { 79 | const res = await getProfileAPI(_getUserId) 80 | dispatch(setUserInfo(res.data)) 81 | } 82 | } 83 | 84 | 85 | //导出 86 | export { fetchLogin, fetchUserInfo, setToken, clearUserInfo } 87 | export default userReducer -------------------------------------------------------------------------------- /client/app/src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { request } from "./request"; 2 | import { getToken, setToken, removeToken } from "./token"; 3 | import { setUserId, getUserId, removeUserId } from "./user"; 4 | 5 | 6 | //统一中转工具模块函数 --> 我们可能封装很多个request模块,统一在这个index导出 7 | //import {request} from '@/utils' 8 | export { 9 | request, 10 | getToken, 11 | setToken, 12 | removeToken, 13 | setUserId, 14 | getUserId, 15 | removeUserId 16 | } 17 | 18 | -------------------------------------------------------------------------------- /client/app/src/utils/request.js: -------------------------------------------------------------------------------- 1 | //axios的封装处理 2 | import axios from "axios"; 3 | import { getToken as _getToken } from "./token"; 4 | //1.根域名配置 5 | //2.超时时间 6 | //3.请求拦截器 / 响应拦截器 7 | 8 | axios.defaults.crossDomain = true 9 | axios.defaults.headers.common['Access-Control-Allow-Origin'] = "*" 10 | 11 | const request = axios.create({ 12 | //baseURL: 'http://120.78.142.84:8080', //根域名配置 13 | baseURL: 'http://localhost:8080', 14 | withCredentials: true, 15 | timeout: 100000 //超时时间 16 | }) 17 | 18 | 19 | //添加请求拦截器:在请求发送之前做拦截,插入一些自定义的配置 20 | request.interceptors.request.use((config) => { 21 | //操作config对象,在请求头里注入token 22 | //1.获取到token 23 | //2.按照后端的格式要求做token拼接 24 | const token = _getToken() 25 | if (token) { 26 | config.headers.mtoken = `satoken=${token}` 27 | document.cookie = `satoken=${token}`; 28 | } 29 | return config 30 | }, (error) => { 31 | return Promise.reject(error) 32 | }) 33 | 34 | //添加响应拦截器:在响应返回到客户端之前做拦截,重点处理返回的数据 35 | request.interceptors.response.use((response) => { 36 | //2xx 范围内的状态码都会触发该函数 37 | //对响应数据做点什么 38 | return response.data 39 | }, (error) => { 40 | //超出2xx范围的状态码都会触发该函数 41 | //对响应错误做点什么 42 | return Promise.reject(error) 43 | }) 44 | 45 | 46 | export { request } //导出实例对象request -------------------------------------------------------------------------------- /client/app/src/utils/token.js: -------------------------------------------------------------------------------- 1 | // 封装和token相关的方法 存 取 删 2 | 3 | const TOKEN_KEY = 'token_key' 4 | 5 | function setToken(token){ 6 | localStorage.setItem(TOKEN_KEY, token) 7 | } 8 | 9 | function getToken(){ 10 | return localStorage.getItem(TOKEN_KEY) 11 | } 12 | 13 | function removeToken(){ 14 | localStorage.removeItem(TOKEN_KEY) 15 | } 16 | 17 | export { 18 | setToken, 19 | getToken, 20 | removeToken 21 | } -------------------------------------------------------------------------------- /client/app/src/utils/user.js: -------------------------------------------------------------------------------- 1 | //封装user相关的方法 2 | const USER_ID = 'user_id' 3 | 4 | function setUserId(userId){ 5 | localStorage.setItem(USER_ID, userId) 6 | } 7 | 8 | function getUserId(){ 9 | return localStorage.getItem(USER_ID) 10 | } 11 | 12 | function removeUserId(){ 13 | localStorage.removeItem(USER_ID) 14 | } 15 | 16 | export { 17 | setUserId, 18 | getUserId, 19 | removeUserId 20 | } -------------------------------------------------------------------------------- /readme/README.zh_CN.md: -------------------------------------------------------------------------------- 1 | 2 | ![Logo]() 3 | 4 | # Embodied 5 | 6 | ## 项目介绍 7 | 8 | 9 | ## 技术栈 10 | 11 | ### 前端技术栈 12 | * React 13 | * react-vant 14 | * axios 15 | * Redux 16 | * normalize 17 | * react-scoped-css 18 | 19 | ### 后端技术栈 20 | * Kotlin 21 | * Spring Boot 22 | * Maven 23 | * Ktorm 24 | * Sa-Token 25 | * WebSocket 26 | * Druid 27 | * OSS 28 | * minio 29 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /server/ai/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Kotlin ### 40 | .kotlin 41 | -------------------------------------------------------------------------------- /server/ai/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "3.2.7" 3 | id("io.spring.dependency-management") version "1.1.5" 4 | kotlin("jvm") version "1.9.24" 5 | kotlin("plugin.spring") version "1.9.24" 6 | } 7 | 8 | group = "com.mars.social" 9 | version = "0.0.1-SNAPSHOT" 10 | 11 | java { 12 | toolchain { 13 | languageVersion = JavaLanguageVersion.of(17) 14 | } 15 | } 16 | 17 | configurations { 18 | compileOnly { 19 | extendsFrom(configurations.annotationProcessor.get()) 20 | } 21 | } 22 | 23 | repositories { 24 | mavenCentral() 25 | maven { url = uri("https://repo.spring.io/milestone") } 26 | } 27 | 28 | extra["springAiVersion"] = "1.0.0-M1" 29 | 30 | dependencies { 31 | implementation("org.springframework.boot:spring-boot-starter-data-redis") 32 | implementation("org.springframework.boot:spring-boot-starter-thymeleaf") 33 | implementation("org.springframework.boot:spring-boot-starter-web") 34 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin") 35 | implementation("org.jetbrains.kotlin:kotlin-reflect") 36 | implementation("com.squareup.okhttp3:okhttp:4.10.0") 37 | implementation ("com.google.code.gson:gson:2.8.8") 38 | implementation("org.json:json:20231013") 39 | compileOnly("org.projectlombok:lombok") 40 | runtimeOnly("com.mysql:mysql-connector-j") 41 | annotationProcessor("org.projectlombok:lombok") 42 | testImplementation("org.springframework.boot:spring-boot-starter-test") 43 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") 44 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 45 | } 46 | 47 | dependencyManagement { 48 | imports { 49 | mavenBom("org.springframework.ai:spring-ai-bom:${property("springAiVersion")}") 50 | } 51 | } 52 | 53 | kotlin { 54 | compilerOptions { 55 | freeCompilerArgs.addAll("-Xjsr305=strict") 56 | } 57 | } 58 | 59 | tasks.withType { 60 | useJUnitPlatform() 61 | } 62 | -------------------------------------------------------------------------------- /server/ai/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/server/ai/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /server/ai/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /server/ai/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /server/ai/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ai" 2 | -------------------------------------------------------------------------------- /server/ai/src/main/kotlin/com/mars/social/ai/AiApplication.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.ai 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class AiApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /server/ai/src/main/kotlin/com/mars/social/ai/api/ApiCall.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.ai.api 2 | 3 | import okhttp3.MediaType.Companion.toMediaType 4 | import okhttp3.OkHttpClient 5 | import okhttp3.Request 6 | import okhttp3.RequestBody.Companion.toRequestBody 7 | import org.json.JSONArray 8 | import org.json.JSONObject 9 | import org.springframework.stereotype.Component 10 | import java.util.concurrent.TimeUnit 11 | 12 | @Component 13 | class ApiCall { 14 | fun call(context:String):String? { 15 | 16 | val request = toGenRequest(context) 17 | val client = OkHttpClient.Builder() 18 | .readTimeout(360, TimeUnit.SECONDS) // 设置读取超时时间为60秒 19 | .writeTimeout(360, TimeUnit.SECONDS) // 设置写入超时时间为60秒 20 | .build() 21 | 22 | val response = client.newCall(request).execute() 23 | 24 | if (response.isSuccessful) { 25 | return response.body?.string() 26 | } else { 27 | println("Error: ${response.code}") 28 | println(response.body?.string()) // 输出服务器返回的具体错误信息 29 | return ""; 30 | } 31 | } 32 | fun toGenRequest(context:String) : Request{ 33 | val apiUrl = "http://127.0.0.1:8000/v1/chat/completions" 34 | val apiKey = "token1" // 此处放置您的有效 API Key 35 | 36 | val jsonObject = JSONObject() 37 | jsonObject.put("model", "chatglm3-6b") 38 | 39 | //system是背景设定 40 | //assistant是返回信息 41 | //user是用户信息 42 | 43 | val messagesArray = JSONArray() 44 | val systemMessage = JSONObject().apply { 45 | put("role", "system") 46 | put("content", "You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user’s instructions carefully. Respond using markdown.") 47 | } 48 | val firstMessage = JSONObject().apply { 49 | put("role", "user") 50 | put("content", context) 51 | } 52 | 53 | // messagesArray.put(systemMessage) 54 | messagesArray.put(firstMessage) 55 | 56 | jsonObject.put("messages", messagesArray) 57 | jsonObject.put("stream", false) 58 | jsonObject.put("max_tokens", 100) 59 | jsonObject.put("temperature", 0.8) 60 | jsonObject.put("top_p", 0.8) 61 | 62 | 63 | val mediaType = "application/json; charset=utf-8".toMediaType() 64 | //val requestBody = requestBodyString.toRequestBody(mediaType) 65 | val requestBody = jsonObject.toString().toRequestBody(mediaType) 66 | 67 | val request = Request.Builder() 68 | .url(apiUrl) 69 | .post(requestBody) 70 | .addHeader("Authorization", "Bearer $apiKey") 71 | .build() 72 | return request 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /server/ai/src/main/kotlin/com/mars/social/ai/api/codeLog.txt: -------------------------------------------------------------------------------- 1 | 2 | // val requestBodyString = """ 3 | // { 4 | // "model": "chatglm3-6b", 5 | // "messages": [ 6 | // { 7 | // "role": "system", 8 | // "content": "You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user’s instructions carefully. Respond using markdown." 9 | // }, 10 | // { 11 | // "role": "user", 12 | // "content": "你好" 13 | // } 14 | // ], 15 | // "stream": false, 16 | // "max_tokens": 100, 17 | // "temperature": 0.8, 18 | // "top_p": 0.8 19 | // } 20 | // """.trimIndent() 21 | 22 | //外网的例子 23 | /** 24 | import openai 25 | 26 | openai.api_key = "your-api-key" 27 | 28 | conversation_history = [ 29 | {"role": "system", "content": "You are a helpful assistant."}, 30 | {"role": "user", "content": "What's the weather like today?"}, 31 | {"role": "assistant", "content": "I'm sorry, I cannot provide real-time weather information."}, 32 | # Continue adding messages as the conversation progresses 33 | ] 34 | 35 | response = openai.ChatCompletion.create( 36 | model="gpt-3.5-turbo", 37 | messages=conversation_history 38 | ) 39 | 40 | print(response['choices'][0]['message']['content']) 41 | **/ -------------------------------------------------------------------------------- /server/ai/src/main/kotlin/com/mars/social/ai/chats/UserModel.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.ai.chats 2 | 3 | class UserModel { 4 | companion object { 5 | fun toGenAccount():String{ 6 | val chat = "请参考以下格式,返回随机的\tuserName,password,email,phone,信息,userName 使用10位长度随机英文字母生成。不许生成Lorem Ipsum\n" + 7 | "{\n" + 8 | " \"userName\": \"Christopher Young\",\n" + 9 | " \"password\": \"123456\",\n" + 10 | " \"email\": \"Christopher@demo.com.cn\",\n" + 11 | " \"phone\": \"18600035806\"\n" + 12 | "}"; 13 | return chat; 14 | } 15 | } 16 | data class UserAccount( 17 | val userName: String, 18 | var password: String, 19 | val email: String, 20 | val phone: String, 21 | var type:String = "AI", 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /server/ai/src/main/kotlin/com/mars/social/ai/core/CoreProcess.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.ai.core 2 | 3 | import com.google.gson.Gson 4 | import com.mars.social.ai.api.ApiCall 5 | import com.mars.social.ai.chats.UserModel 6 | import com.mars.social.ai.vo.ChatStruct 7 | import okhttp3.MediaType 8 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 9 | import okhttp3.OkHttpClient 10 | import okhttp3.Request 11 | import okhttp3.RequestBody 12 | import okhttp3.RequestBody.Companion.toRequestBody 13 | import org.springframework.beans.factory.annotation.Autowired 14 | import org.springframework.stereotype.Service 15 | import org.springframework.web.bind.annotation.GetMapping 16 | import org.springframework.web.bind.annotation.RequestMapping 17 | import org.springframework.web.bind.annotation.RestController 18 | 19 | @Service 20 | @RestController 21 | class CoreProcess { 22 | @Autowired 23 | lateinit var apiCall: ApiCall 24 | @RequestMapping("/test") //测试 25 | fun command(): String { 26 | println("hello world") 27 | return "hello world" 28 | } 29 | 30 | @GetMapping("/initAi") 31 | fun initAi():String{ 32 | val json= apiCall.call(UserModel.toGenAccount()); 33 | println(json) 34 | if (json != null) { 35 | json.trimIndent() 36 | val chatStruct = Gson().fromJson(json, ChatStruct::class.java) 37 | val userAccount = Gson().fromJson(chatStruct.choices.get(0).message.content, UserModel.UserAccount::class.java) 38 | println(chatStruct) 39 | println(userAccount) 40 | userAccount.password="123456" 41 | userAccount.type="AI" 42 | //send register request 43 | val client = OkHttpClient() 44 | val url = "http://localhost:8080/api/users/register" 45 | val params = Gson().toJson(userAccount) 46 | val requestBody = params.toRequestBody("application/json".toMediaTypeOrNull()) 47 | val request = Request.Builder() 48 | .url(url) 49 | .post(requestBody) 50 | .build() 51 | 52 | val response = client.newCall(request).execute() 53 | println(response.code) 54 | println(response.body?.string()) 55 | } 56 | return "Done" 57 | } 58 | } -------------------------------------------------------------------------------- /server/ai/src/main/kotlin/com/mars/social/ai/vo/ChatStruct.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.ai.vo 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ChatStruct( 6 | @SerializedName("model") val model: String, 7 | @SerializedName("id") val id: String, 8 | @SerializedName("object") val objectType: String, 9 | @SerializedName("choices") val choices: List, 10 | @SerializedName("created") val created: Long, 11 | @SerializedName("usage") val usage: Usage 12 | ) 13 | 14 | data class Choice( 15 | @SerializedName("index") val index: Int, 16 | @SerializedName("message") val message: Message, 17 | @SerializedName("finish_reason") val finishReason: String 18 | ) 19 | 20 | data class Message( 21 | @SerializedName("role") val role: String, 22 | @SerializedName("content") val content: String, 23 | @SerializedName("name") val name: String?, 24 | @SerializedName("function_call") val functionCall: String? 25 | ) 26 | 27 | data class Usage( 28 | @SerializedName("prompt_tokens") val promptTokens: Int, 29 | @SerializedName("total_tokens") val totalTokens: Int, 30 | @SerializedName("completion_tokens") val completionTokens: Int 31 | ) 32 | -------------------------------------------------------------------------------- /server/ai/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=ai 2 | server.port=8050 -------------------------------------------------------------------------------- /server/ai/src/test/kotlin/com/mars/social/ai/AiApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.ai 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | @SpringBootTest 7 | class AiApplicationTests { 8 | 9 | @Test 10 | fun contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /server/social/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /server/social/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mariadb: 3 | image: 'mariadb:latest' 4 | environment: 5 | - 'MARIADB_DATABASE=mydatabase' 6 | - 'MARIADB_PASSWORD=secret' 7 | - 'MARIADB_ROOT_PASSWORD=verysecret' 8 | - 'MARIADB_USER=myuser' 9 | ports: 10 | - '3306' 11 | mongodb: 12 | image: 'mongo:latest' 13 | environment: 14 | - 'MONGO_INITDB_DATABASE=mydatabase' 15 | - 'MONGO_INITDB_ROOT_PASSWORD=secret' 16 | - 'MONGO_INITDB_ROOT_USERNAME=root' 17 | ports: 18 | - '27017' 19 | redis: 20 | image: 'redis:latest' 21 | ports: 22 | - '6379' 23 | -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/SocialApplication.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social 2 | 3 | import cn.dev33.satoken.SaManager 4 | import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties 5 | import org.springframework.boot.autoconfigure.SpringBootApplication 6 | import org.springframework.boot.runApplication 7 | import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory 8 | import org.springframework.context.annotation.Bean 9 | 10 | 11 | @SpringBootApplication() 12 | @EnableEncryptableProperties 13 | class SocialApplication 14 | 15 | fun main(args: Array) { 16 | runApplication(*args) 17 | println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()) 18 | } 19 | 20 | -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/configuration/CustomInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.configuration 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import jakarta.servlet.http.HttpServletResponse 5 | import org.springframework.web.servlet.HandlerInterceptor 6 | import org.springframework.web.servlet.ModelAndView 7 | import java.time.LocalDateTime 8 | import java.time.format.DateTimeFormatter 9 | 10 | class CustomInterceptor : HandlerInterceptor { 11 | override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { 12 | val startTime = System.currentTimeMillis() 13 | request.setAttribute("startTime", startTime) 14 | return true 15 | } 16 | 17 | override fun postHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any, modelAndView: ModelAndView?) { 18 | val startTime = request.getAttribute("startTime") as Long 19 | val endTime = System.currentTimeMillis() 20 | val requestTime = endTime - startTime 21 | 22 | val currentTime = LocalDateTime.now() 23 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 24 | val formattedTime = currentTime.format(formatter) 25 | 26 | var ip = request.getHeader("x-forwarded-for") 27 | if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) { 28 | ip = request.getHeader("Proxy-Client-IP") 29 | } 30 | if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) { 31 | ip = request.getHeader("WL-Proxy-Client-IP") 32 | } 33 | if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) { 34 | ip = request.remoteAddr 35 | } 36 | val userAgent = request.getHeader("User-Agent") 37 | 38 | println("Request URL: ${request.requestURL}, Request Time: $requestTime ms, Time: $formattedTime, IP Address: $ip, User Agent: $userAgent") 39 | } 40 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/configuration/KtormConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.configuration 2 | 3 | import com.fasterxml.jackson.databind.Module 4 | import org.ktorm.database.Database 5 | import org.ktorm.jackson.KtormModule 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | import javax.sql.DataSource 10 | 11 | /** 12 | * Created by vince on May 17, 2019. 13 | */ 14 | @Configuration 15 | class KtormConfiguration { 16 | @Autowired 17 | lateinit var dataSource: DataSource 18 | 19 | /** 20 | * Register the [Database] instance as a Spring bean. 21 | */ 22 | @Bean 23 | fun database(): Database { 24 | return Database.connectWithSpringSupport(dataSource) 25 | } 26 | 27 | /** 28 | * Register Ktorm's Jackson extension to the Spring's container 29 | * so that we can serialize/deserialize Ktorm entities. 30 | */ 31 | @Bean 32 | fun ktormModule(): Module { 33 | return KtormModule() 34 | } 35 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/configuration/WebConfig.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.configuration 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import jakarta.servlet.http.HttpServletResponse 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.web.servlet.ModelAndView 7 | import org.springframework.web.servlet.config.annotation.CorsRegistry 8 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer 10 | 11 | @Configuration 12 | class WebConfig : WebMvcConfigurer { 13 | 14 | override fun addInterceptors(registry: InterceptorRegistry) { 15 | registry.addInterceptor(CustomInterceptor()) 16 | } 17 | 18 | @Override 19 | override fun addCorsMappings(registry: CorsRegistry) { 20 | registry.addMapping("/**") 21 | .allowedOriginPatterns("*") 22 | .allowedHeaders("Authorization","Access-Control-Allow-Headers","Origin", 23 | "Content-Type", "Access-Control-Allow-Origin"," X-Requested-With","Accept","X-PINGOTHER","Authorization","satoken","mtoken") 24 | .allowedMethods("GET", "POST", "PUT", "DELETE") 25 | .allowCredentials(true); 26 | } 27 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/configuration/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.configuration; 2 | 3 | import cn.dev33.satoken.stp.StpUtil; 4 | import com.mars.social.controller.WebSocketConnect; 5 | import com.mars.social.interceptor.WebSocketInterceptor; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.socket.config.annotation.*; 10 | 11 | @Configuration 12 | @EnableWebSocket 13 | public class WebSocketConfig implements WebSocketConfigurer{ 14 | 15 | @Autowired 16 | ApplicationContext context; 17 | // 注册 WebSocket 处理器 18 | @Override 19 | public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { 20 | WebSocketConnect connectHandler = new WebSocketConnect(); 21 | WebSocketInterceptor webSocketInterceptor = new WebSocketInterceptor(); 22 | connectHandler.setContext(context); 23 | webSocketHandlerRegistry 24 | // WebSocket 连接处理器 25 | .addHandler(connectHandler, "/ws-connect") 26 | // WebSocket 拦截器 27 | .addInterceptors(new WebSocketInterceptor()) 28 | // 允许跨域 29 | .setAllowedOrigins("*"); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/AiController.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller 2 | 3 | import com.mars.social.model.mix.Channels 4 | import com.mars.social.utils.R 5 | import org.ktorm.database.Database 6 | import org.ktorm.entity.sequenceOf 7 | import org.ktorm.entity.toList 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.http.ResponseEntity 10 | import org.springframework.stereotype.Controller 11 | import org.springframework.web.bind.annotation.GetMapping 12 | 13 | @Controller 14 | class AiController { 15 | 16 | @Autowired 17 | protected lateinit var database: Database 18 | 19 | @GetMapping("/register") 20 | fun list(): ResponseEntity { 21 | val channels = database.sequenceOf(Channels).toList() 22 | return ResponseEntity.ok().body(R.ok(channels)) 23 | } 24 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/ApiCall.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller 2 | import okhttp3.MediaType.Companion.toMediaType 3 | import okhttp3.OkHttpClient 4 | import okhttp3.Request 5 | import okhttp3.RequestBody.Companion.toRequestBody 6 | import org.json.JSONArray 7 | import org.json.JSONObject 8 | import org.springframework.stereotype.Component 9 | import java.util.concurrent.TimeUnit 10 | 11 | @Component 12 | class ApiCall{ 13 | fun call() { 14 | val apiUrl = "http://127.0.0.1:8000/v1/chat/completions" 15 | val apiKey = "EMPTY" // 此处放置您的有效 API Key 16 | 17 | // val requestBodyString = """ 18 | // { 19 | // "model": "chatglm3-6b", 20 | // "messages": [ 21 | // { 22 | // "role": "system", 23 | // "content": "You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user’s instructions carefully. Respond using markdown." 24 | // }, 25 | // { 26 | // "role": "user", 27 | // "content": "你好" 28 | // } 29 | // ], 30 | // "stream": false, 31 | // "max_tokens": 100, 32 | // "temperature": 0.8, 33 | // "top_p": 0.8 34 | // } 35 | // """.trimIndent() 36 | 37 | val jsonObject = JSONObject() 38 | jsonObject.put("model", "chatglm3-6b") 39 | 40 | val messagesArray = JSONArray() 41 | val systemMessage = JSONObject().apply { 42 | put("role", "system") 43 | put("content", "You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user’s instructions carefully. Respond using markdown.") 44 | } 45 | val userMessage = JSONObject().apply { 46 | put("role", "user") 47 | put("content", "你好") 48 | } 49 | messagesArray.put(systemMessage) 50 | messagesArray.put(userMessage) 51 | 52 | jsonObject.put("messages", messagesArray) 53 | jsonObject.put("stream", false) 54 | jsonObject.put("max_tokens", 100) 55 | jsonObject.put("temperature", 0.8) 56 | jsonObject.put("top_p", 0.8) 57 | 58 | 59 | val mediaType = "application/json; charset=utf-8".toMediaType() 60 | //val requestBody = requestBodyString.toRequestBody(mediaType) 61 | val requestBody = jsonObject.toString().toRequestBody(mediaType) 62 | 63 | val request = Request.Builder() 64 | .url(apiUrl) 65 | .post(requestBody) 66 | .addHeader("Authorization", "Bearer $apiKey") 67 | .build() 68 | 69 | val client = OkHttpClient.Builder() 70 | .readTimeout(360, TimeUnit.SECONDS) // 设置读取超时时间为60秒 71 | .writeTimeout(360, TimeUnit.SECONDS) // 设置写入超时时间为60秒 72 | .build() 73 | 74 | val response = client.newCall(request).execute() 75 | 76 | if (response.isSuccessful) { 77 | println(response.body?.string()) 78 | } else { 79 | println("Error: ${response.code}") 80 | println(response.body?.string()) // 输出服务器返回的具体错误信息 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/ChannelsController.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller 2 | 3 | import com.mars.social.model.mix.Channels 4 | import com.mars.social.utils.R 5 | import org.ktorm.database.Database 6 | import org.ktorm.dsl.* 7 | import org.ktorm.entity.* 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.http.ResponseEntity 10 | import org.springframework.web.bind.annotation.* 11 | 12 | @CrossOrigin 13 | @RestController 14 | @RequestMapping("/api/channels") 15 | class ChannelsController { 16 | @Autowired 17 | protected lateinit var database: Database 18 | 19 | @GetMapping("/list") 20 | fun list(): ResponseEntity { 21 | val channels = database.sequenceOf(Channels).toList() 22 | return ResponseEntity.ok().body(R.ok(channels)) 23 | } 24 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/ChatController.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller; 2 | 3 | import com.mars.social.model.mix.ChatMessage; 4 | import org.springframework.messaging.handler.annotation.MessageMapping; 5 | import org.springframework.messaging.handler.annotation.Payload; 6 | import org.springframework.messaging.handler.annotation.SendTo; 7 | import org.springframework.messaging.simp.SimpMessageHeaderAccessor; 8 | import org.springframework.stereotype.Controller; 9 | 10 | @Controller 11 | public class ChatController { 12 | 13 | @MessageMapping("/chat.sendMessage") 14 | @SendTo("/channel/public") 15 | public ChatMessage sendMessage(@Payload ChatMessage chatMessage) { 16 | return chatMessage; 17 | } 18 | 19 | @MessageMapping("/chat.addUser") 20 | @SendTo("/channel/public") 21 | public ChatMessage addUser(@Payload ChatMessage chatMessage, 22 | SimpMessageHeaderAccessor headerAccessor) { 23 | // Add username in web socket session 24 | headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); 25 | return chatMessage; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/DemoController.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller 2 | 3 | import com.mars.social.model.Demo 4 | import org.jasypt.encryption.StringEncryptor 5 | import org.ktorm.database.Database 6 | import org.ktorm.dsl.from 7 | import org.ktorm.dsl.limit 8 | import org.ktorm.dsl.map 9 | import org.ktorm.dsl.select 10 | import org.springframework.beans.factory.annotation.Autowired 11 | import org.springframework.web.bind.annotation.GetMapping 12 | import org.springframework.web.bind.annotation.RestController 13 | 14 | 15 | @RestController 16 | class DemoController { 17 | @Autowired 18 | lateinit var apiCall:ApiCall 19 | 20 | @GetMapping("/") 21 | fun index() = "Hello, mars!" 22 | 23 | @GetMapping("/callAiApi") 24 | fun callAiApi(){ 25 | apiCall.call(); 26 | } 27 | 28 | @GetMapping("/toDB") 29 | fun getDB(){ 30 | val database = Database.connect( 31 | url = "jdbc:mysql://localhost:3306/world", 32 | driver = "com.mysql.cj.jdbc.Driver", 33 | user = "root", 34 | password = "123456" 35 | ) 36 | //var cities = database.sequenceOf(Demo.Cities); 37 | 38 | var cities = database.from(Demo.Cities).select().limit(5).map { row -> Demo.Cities.createEntity(row) } 39 | for(city in cities){ 40 | println(city.name); 41 | } 42 | // for (row in database.from(Demo.City).select()) { 43 | // println(row) 44 | // println(row[Demo.City.name]); 45 | // } 46 | // val query = database.from(Demo.City).select() 47 | // println(query.) 48 | } 49 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/DiscoverController.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller 2 | 3 | import cn.dev33.satoken.annotation.SaCheckLogin 4 | import cn.dev33.satoken.stp.StpUtil 5 | import com.mars.social.model.topic.* 6 | import com.mars.social.model.user.UserFollowDB 7 | import com.mars.social.utils.R 8 | import org.ktorm.database.Database 9 | import org.ktorm.dsl.* 10 | import org.ktorm.entity.sequenceOf 11 | import org.ktorm.entity.sortedBy 12 | import org.ktorm.entity.toList 13 | import org.springframework.beans.factory.annotation.Autowired 14 | import org.springframework.http.ResponseEntity 15 | import org.springframework.web.bind.annotation.CrossOrigin 16 | import org.springframework.web.bind.annotation.GetMapping 17 | import org.springframework.web.bind.annotation.RequestMapping 18 | import org.springframework.web.bind.annotation.RestController 19 | 20 | @CrossOrigin 21 | @RestController 22 | @RequestMapping("/api/discover") 23 | class DiscoverController { 24 | @Autowired 25 | protected lateinit var database: Database 26 | 27 | @Autowired 28 | lateinit var userController:UserController 29 | 30 | data class TopicPost(var userInfo: UserController.UserInfoDto?, var topic:Topic) 31 | //comment action 32 | data class CommentAction(var userInfo: UserController.UserInfoDto?,var comment:TopicComment) 33 | //like action 34 | data class likeAction(var userInfo: UserController.UserInfoDto?,var like:TopicLike) 35 | 36 | data class FollowedTargetActivities(var topicPostList:List,var commentActionList:List,var likeActionList:List) 37 | // data class DiscoverDto(var randomTopics:List) 38 | 39 | @GetMapping("/explore") 40 | fun explore(numbers:Int=3): ResponseEntity{ 41 | val randomTopics = database.sequenceOf(Topics).sortedBy { it.publishTime.desc() }.toList().shuffled().take(numbers) 42 | var topicPostList = ArrayList() 43 | for(topic in randomTopics){ 44 | var userInfo = topic.authorUid?.let { userController.getUserInfo(it) } 45 | var topicPost = TopicPost(userInfo,topic) 46 | topicPostList.add(topicPost) 47 | } 48 | return ResponseEntity.ok().body(R.ok(topicPostList)) 49 | } 50 | 51 | @SaCheckLogin 52 | @GetMapping("/loadFollowedTargetActivities") 53 | fun loadFollowedTargetActivities( offset:Long = 0, numbers:Int=3): ResponseEntity { 54 | val uid = StpUtil.getLoginId().toString().toLong() 55 | var offsetIndex:Long = 0 56 | if(offset.toInt() !=0){ 57 | offsetIndex = offset 58 | }else{ 59 | var topId = database.from(Topics).select(Topics.id).orderBy(Topics.id.desc()).map { row -> Topics.createEntity(row) }.firstOrNull() 60 | if (topId != null) { 61 | offsetIndex = topId.id 62 | } 63 | } 64 | val topics =database.from(Topics).innerJoin(UserFollowDB,on=Topics.authorUid eq UserFollowDB.followedUid) 65 | .select().where { (UserFollowDB.followerUid eq uid) and (Topics.id lessEq offsetIndex) }.orderBy(Topics.publishTime.desc()).limit(numbers) 66 | .map { row -> Topics.createEntity(row) }.toList() 67 | val topicPostList = ArrayList() 68 | for(topic in topics){ 69 | val userInfo = topic.authorUid?.let { userController.getUserInfo(it) } 70 | val topicPost = TopicPost(userInfo,topic) 71 | topicPostList.add(topicPost) 72 | } 73 | 74 | val commentActionList = ArrayList() 75 | val topicComments = database.from(TopicComments).innerJoin(UserFollowDB,on=TopicComments.uid eq UserFollowDB.followedUid) 76 | .select().where{(UserFollowDB.followerUid eq uid)}.orderBy( TopicComments.createTime.desc() ).limit(numbers) 77 | .map{ row-> TopicComments.createEntity(row)}.toList() 78 | for(topicComment in topicComments){ 79 | val userInfo = userController.getUserInfo(topicComment.uid) 80 | val commentAction = CommentAction(userInfo,topicComment) 81 | commentActionList.add(commentAction) 82 | } 83 | 84 | val likeActionList = ArrayList() 85 | val topicLikes = database.from(TopicLikes).innerJoin(UserFollowDB,on=TopicLikes.uid eq UserFollowDB.followedUid) 86 | .select().where{ (UserFollowDB.followerUid eq uid) }.orderBy( TopicLikes.createTime.desc() ).limit(numbers) 87 | .map { row -> TopicLikes.createEntity(row) }.toList() 88 | for(topicLike in topicLikes){ 89 | val userInfo = userController.getUserInfo(topicLike.uid) 90 | val likeAction = likeAction(userInfo,topicLike) 91 | likeActionList.add(likeAction) 92 | } 93 | 94 | var followedTargetActivities:FollowedTargetActivities = FollowedTargetActivities(topicPostList,commentActionList,likeActionList); 95 | 96 | return ResponseEntity.ok().body(R.ok(followedTargetActivities)) 97 | } 98 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/WebSocketConnect.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 9 | import com.mars.social.model.mix.Message; 10 | import com.mars.social.model.mix.MessageBean; 11 | import com.mars.social.model.mix.SocketMessage; 12 | import com.mars.social.utils.R; 13 | import jakarta.websocket.server.ServerEndpoint; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.context.ApplicationContext; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.stereotype.Component; 18 | 19 | import org.springframework.web.socket.CloseStatus; 20 | import org.springframework.web.socket.TextMessage; 21 | import org.springframework.web.socket.WebSocketSession; 22 | import org.springframework.web.socket.handler.TextWebSocketHandler; 23 | 24 | @Component 25 | @ServerEndpoint("/ws-connect/{satoken}") 26 | public class WebSocketConnect extends TextWebSocketHandler { 27 | /** 28 | * 固定前缀 29 | */ 30 | private static final String USER_ID = "user_id_"; 31 | 32 | /** 33 | * 存放Session集合,方便推送消息 34 | */ 35 | private static ConcurrentHashMap webSocketSessionMaps = new ConcurrentHashMap<>(); 36 | 37 | private ApplicationContext context; 38 | 39 | @Autowired 40 | public void setContext(ApplicationContext context) { 41 | this.context = context; 42 | } 43 | 44 | // 监听:连接开启 45 | @Override 46 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 47 | 48 | // put到集合,方便后续操作 49 | String userId = session.getAttributes().get("userId").toString(); 50 | webSocketSessionMaps.put(USER_ID + userId, session); 51 | 52 | // 给个提示 53 | String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId; 54 | String response = " {\"code\":10000,\"message\":\"成功\",\"data\":\""+tips+"\"}"; 55 | System.out.println(tips); 56 | sendMessage(session, response); 57 | } 58 | 59 | // 监听:连接关闭 60 | @Override 61 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 62 | // 从集合移除 63 | String userId = session.getAttributes().get("userId").toString(); 64 | webSocketSessionMaps.remove(USER_ID + userId); 65 | 66 | // 给个提示 67 | String tips = "Web-Socket 连接关闭,sid=" + session.getId() + ",userId=" + userId; 68 | System.out.println(tips); 69 | } 70 | 71 | // 收到消息 72 | @Override 73 | public void handleTextMessage(WebSocketSession session, TextMessage rawText) throws IOException { 74 | System.out.println("sid为:" + session.getId() + ",发来:" + rawText); 75 | SocketMessage socketMessage = new SocketMessage(); 76 | ObjectMapper objectMapper = new ObjectMapper(); 77 | objectMapper.registerModule(new JavaTimeModule()); 78 | socketMessage = objectMapper.readValue(rawText.getPayload(),SocketMessage.class); 79 | 80 | String command = socketMessage.getCommand(); 81 | String userId = session.getAttributes().get("userId").toString(); 82 | Long TargetUser = Long.valueOf(socketMessage.getTargetUser()); 83 | String msg = socketMessage.getMessage(); 84 | if(command.equals("10100")){ 85 | String response = " {\"code\":10100,\"message\":\"成功\",\"data\":"+msg+"}"; 86 | this.broadcastMessage(response); 87 | } 88 | if(command.equals("10200")){ 89 | MessageController messageController = context.getBean(MessageController.class); 90 | MessageController.MessageSDto messageSDto = new MessageController.MessageSDto(userId,TargetUser, msg); 91 | String message = messageController.serverSend(messageSDto); 92 | MessageBean bean = objectMapper.readValue(message,MessageBean.class); 93 | R r = R.Companion.ok(bean); 94 | r.setCode(10200); 95 | String jsonStr = objectMapper.writeValueAsString(r); 96 | sendMessage(2,jsonStr); 97 | sendMessage(TargetUser,jsonStr); 98 | } 99 | } 100 | 101 | // ----------- 102 | 103 | // 向指定客户端推送消息 104 | public static void sendMessage(WebSocketSession session, String message) { 105 | try { 106 | System.out.println("向sid为:" + session.getId() + ",发送:" + message); 107 | session.sendMessage(new TextMessage(message)); 108 | } catch (IOException e) { 109 | throw new RuntimeException(e); 110 | } 111 | } 112 | 113 | // 向指定用户推送消息 114 | public static void sendMessage(long userId, String message) { 115 | WebSocketSession session = webSocketSessionMaps.get(USER_ID + userId); 116 | if(session != null) { 117 | sendMessage(session, message); 118 | } 119 | } 120 | 121 | public void broadcastMessage(String message) { 122 | for (WebSocketSession session : webSocketSessionMaps.values()) { 123 | try { 124 | session.sendMessage(new TextMessage(message)); 125 | } catch (IOException e) { 126 | e.printStackTrace(); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/controller/common/CommonFunction.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.controller.common 2 | 3 | import com.mars.social.controller.UserController 4 | import com.mars.social.model.topic.Topic 5 | 6 | class CommonFunction { 7 | companion object { 8 | fun setTopicUserInfo(topic: Topic, userInfo:UserController.UserInfoDto?) { 9 | if (userInfo != null) { 10 | topic.authorNickName = userInfo.nickName.toString() 11 | } 12 | if (userInfo != null) { 13 | topic.authorAvatar = userInfo.avatar.toString() 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/dto/FileInfo.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.dto 2 | 3 | import java.time.ZonedDateTime 4 | 5 | data class FileInfo( 6 | val fileName: String, 7 | val fileSize: Long, 8 | val contentType: String, 9 | val lastModified: ZonedDateTime 10 | ) -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/dto/PageDTO.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.dto 2 | 3 | data class PageDTO( 4 | val modelList: List, 5 | val totalRecordsInAllPages: Int, 6 | val startIndex: Int, 7 | val pageSize: Int, 8 | val pageCount: Int 9 | ) { 10 | constructor(modelList: List, totalRecordsInAllPages: Int, pageRequest: PageRequest) 11 | : this( 12 | modelList, 13 | totalRecordsInAllPages, 14 | (pageRequest.pageNumber - 1) * pageRequest.pageSize, 15 | pageRequest.pageSize, 16 | if (totalRecordsInAllPages % pageRequest.pageSize == 0) { 17 | totalRecordsInAllPages / pageRequest.pageSize 18 | } else { 19 | totalRecordsInAllPages / pageRequest.pageSize + 1 20 | } 21 | ) 22 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/dto/PageRequest.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.dto 2 | 3 | data class PageRequest( 4 | val pageNumber: Int, //第几页 5 | val pageSize: Int //一页几个 6 | ) -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/dto/UserInfoDto.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.dto 2 | 3 | import com.mars.social.model.user.UserDetail 4 | 5 | 6 | data class UserInfoDto ( 7 | var userName:String ="", 8 | var email:String ="", 9 | var phone:String ="", 10 | val userDetail:UserDetail?, 11 | ) -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/interceptor/WebSocketInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.interceptor; 2 | import java.util.Map; 3 | 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.http.server.ServerHttpRequest; 7 | import org.springframework.http.server.ServerHttpResponse; 8 | import org.springframework.web.socket.WebSocketHandler; 9 | import org.springframework.web.socket.server.HandshakeInterceptor; 10 | 11 | import cn.dev33.satoken.stp.StpUtil; 12 | 13 | public class WebSocketInterceptor implements HandshakeInterceptor { 14 | 15 | // 握手之前触发 (return true 才会握手成功 ) 16 | private ApplicationContext context; 17 | 18 | @Autowired 19 | public void setContext(ApplicationContext context) { 20 | this.context = context; 21 | } 22 | @Override 23 | public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, 24 | Map attr) { 25 | 26 | System.out.println("---- 握手之前触发 " + StpUtil.getTokenValue()); 27 | 28 | // 未登录情况下拒绝握手 29 | if(StpUtil.isLogin() == false) { 30 | System.out.println("---- 未授权客户端,连接失败"); 31 | return false; 32 | } 33 | 34 | // 标记 userId,握手成功 35 | attr.put("userId", StpUtil.getLoginIdAsLong()); 36 | return true; 37 | } 38 | 39 | // 握手之后触发 40 | @Override 41 | public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, 42 | Exception exception) { 43 | System.out.println("---- 握手之后触发 "); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/Demo.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.Table 5 | import org.ktorm.schema.int 6 | import org.ktorm.schema.varchar 7 | 8 | class Demo { 9 | interface City : Entity { 10 | val id: Int? 11 | var name: String 12 | var countryCode: String 13 | var location: String 14 | var population: Int 15 | } 16 | 17 | object Cities : Table("city") { 18 | val id = int("id").primaryKey().bindTo { it.id } 19 | val name = varchar("name").bindTo { it.name } 20 | val countryCode = varchar("countryCode").bindTo { it.countryCode } 21 | val population = int("population").bindTo { it.population } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/BookMark.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix 2 | 3 | import com.mars.social.model.user.Friendships.bindTo 4 | import com.mars.social.model.user.Friendships.primaryKey 5 | import org.ktorm.entity.Entity 6 | import org.ktorm.schema.Table 7 | import org.ktorm.schema.datetime 8 | import org.ktorm.schema.long 9 | import java.time.LocalDateTime 10 | 11 | interface BookMark : Entity { 12 | var id:Long 13 | var uid:Long 14 | var tid:Long 15 | var createTime:LocalDateTime 16 | } 17 | 18 | 19 | object BookMarks : Table("bookmarks"){ 20 | val id = long("id").primaryKey().bindTo { it.id } 21 | val uid = long("uid").bindTo { it.uid } 22 | val tid = long("tid").bindTo { it.tid } 23 | val createTime = datetime("create_time").bindTo { it.createTime } 24 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/Channels.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.* 5 | 6 | interface Channel : Entity { 7 | var id: Int 8 | var key: String 9 | var name: String 10 | var desc: String 11 | var lang: String 12 | } 13 | 14 | object Channels : Table("channel") { 15 | val id = int("id").primaryKey().bindTo { it.id } 16 | val key = varchar("key").bindTo { it.key } 17 | val name = varchar("name").bindTo { it.name } 18 | val desc = varchar("desc").bindTo { it.desc } 19 | val lang = varchar("lang").bindTo { it.lang } 20 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class ChatMessage { 9 | private String content; 10 | private String sender; 11 | private LocalDateTime timeStamp = LocalDateTime.now(); 12 | 13 | public enum MessageType {LEAVE, CHAT, JOIN} 14 | 15 | private MessageType type; 16 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/CommentLike.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix 2 | 3 | import com.mars.social.model.user.Friendships.bindTo 4 | import com.mars.social.model.user.Friendships.primaryKey 5 | import org.ktorm.entity.Entity 6 | import org.ktorm.schema.Table 7 | import org.ktorm.schema.datetime 8 | import org.ktorm.schema.long 9 | import java.time.LocalDateTime 10 | 11 | interface CommentLike : Entity { 12 | var id:Long 13 | var uid:Long 14 | var tcId:Long 15 | var createTime:LocalDateTime 16 | } 17 | 18 | 19 | object CommentLikeDb : Table("comment_like"){ 20 | val id = long("id").primaryKey().bindTo { it.id } 21 | val uid = long("uid").bindTo { it.uid } 22 | val tcId = long("tc_id").bindTo { it.tcId } 23 | val createTime = datetime("create_time").bindTo { it.createTime } 24 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/File.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.Table 5 | import org.ktorm.schema.datetime 6 | import org.ktorm.schema.long 7 | import org.ktorm.schema.varchar 8 | import java.time.LocalDateTime 9 | 10 | interface File : Entity { 11 | var id: Long 12 | var fileName: String 13 | var originalFileName: String 14 | var createTime: LocalDateTime 15 | } 16 | 17 | object Files : Table("files") { 18 | val id = long("id").primaryKey().bindTo { it.id } 19 | val fileName = varchar("file_name").bindTo { it.fileName } 20 | val originalFileName = varchar("original_file_name").bindTo { it.originalFileName } 21 | val createTime = datetime("create_time").bindTo { it.createTime } 22 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/MessageBean.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Data; 5 | 6 | import java.time.LocalDateTime; 7 | 8 | @Data 9 | public class MessageBean { 10 | private long id; 11 | private String msgType; 12 | private String senderId; 13 | private long receiverUid; 14 | private String content; 15 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 16 | private LocalDateTime sendTime; 17 | private String status; 18 | private String mark; 19 | private String sysMsgType; 20 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 21 | private LocalDateTime receiveTime; 22 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 23 | private LocalDateTime deleteTime; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/Messages.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat 4 | import com.fasterxml.jackson.annotation.JsonInclude 5 | import org.ktorm.entity.Entity 6 | import org.ktorm.schema.* 7 | import java.time.* 8 | 9 | interface Message : Entity { 10 | var id: Long 11 | var msgType: String? //默认u,u 用户消息 s 系统消息 12 | var senderId: String? //发送人ID,系统的是0 13 | var receiverUid: Long //收件人ID 14 | var content: String? // 15 | var sendTime: LocalDateTime? //发送日期 16 | var status: String? //unCheck checked deleted 17 | var mark: String? //important top 18 | var sysMsgType: String? //先默认都是notice 19 | var receiveTime: LocalDateTime? //阅读时间 20 | var deleteTime: LocalDateTime? //删除时间 21 | } 22 | 23 | object Messages : Table("messages") { 24 | val id = long("id").primaryKey().bindTo { it.id } 25 | val msgType = varchar("msg_type").bindTo { it.msgType } 26 | val senderId = varchar("sender_id").bindTo { it.senderId } 27 | val receiverUid = long("receiver_uid").bindTo { it.receiverUid } 28 | val content = varchar("content").bindTo { it.content } 29 | val sendTime = datetime("send_time").bindTo { it.sendTime } 30 | val status = varchar("status").bindTo { it.status } 31 | val mark = varchar("mark").bindTo { it.mark } 32 | val sysMsgType = varchar("sys_msg_type").bindTo { it.sysMsgType } 33 | val receiveTime = datetime("receive_time").bindTo { it.receiveTime } 34 | val deleteTime = datetime("delete_time").bindTo { it.deleteTime } 35 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/SocketMessage.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class SocketMessage { 7 | String command; 8 | String targetUser; 9 | String message; 10 | } 11 | -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/Tag.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.Table 5 | import org.ktorm.schema.long 6 | import org.ktorm.schema.varchar 7 | 8 | interface Tag : Entity { 9 | var id: Long 10 | var tagName: String 11 | } 12 | 13 | object Tags : Table("tags") { 14 | val id = long("id").primaryKey().bindTo { it.id } 15 | val tagName = varchar("tag_name").bindTo { it.tagName } 16 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/mix/TopicShare.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.mix 2 | 3 | import com.mars.social.model.user.Friendships.bindTo 4 | import com.mars.social.model.user.Friendships.primaryKey 5 | import org.ktorm.entity.Entity 6 | import org.ktorm.schema.Table 7 | import org.ktorm.schema.datetime 8 | import org.ktorm.schema.long 9 | import org.ktorm.schema.varchar 10 | import java.time.LocalDateTime 11 | 12 | interface TopicShare : Entity { 13 | var id:Long 14 | var uid:Long 15 | var tid:Long 16 | var shareTime:LocalDateTime 17 | var shareToken:String 18 | var urlLink:String 19 | } 20 | 21 | 22 | object TopicShares : Table("user_share"){ 23 | val id = long("id").primaryKey().bindTo { it.id } 24 | val uid = long("uid").bindTo { it.uid } 25 | val tid = long("tid").bindTo { it.tid } 26 | val shareTime = datetime("share_time").bindTo { it.shareTime } 27 | val shareToken = varchar("share_token").bindTo { it.shareToken } 28 | val urlLink = varchar("url_link").bindTo { it.urlLink } 29 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/topic/Topic.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.topic 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.* 5 | import java.time.* 6 | 7 | interface Topic : Entity { 8 | var id: Long 9 | var title : String? //标题 10 | var authorUid: Long? //作者UID 11 | var contentType: String? //内容类型,先都默认是common 12 | var content: String //内容 13 | var publishTime: LocalDateTime //发布日期 14 | var status: String? //状态,草稿,已发布 15 | var likes: Int //赞 16 | var comments: Int //注释 17 | var shares: Int //分享 18 | var bookmarks: Int //收藏 19 | var createTime: LocalDateTime 20 | var updateTime: LocalDateTime 21 | var isDelete: String //是否删除 22 | var visits: Long //访问 23 | var channelKey: String //频道 24 | var coverImg: Int //封面图片ID 25 | //----- 26 | var tags:ArrayList //标签 27 | var tagsName:ArrayList //标签描述 28 | var authorNickName:String 29 | var authorAvatar:String 30 | } 31 | 32 | object Topics : Table("topic") { 33 | val id = long("id").primaryKey().bindTo { it.id } 34 | val title = varchar("title").bindTo { it.title } 35 | val authorUid = long("author_uid").bindTo { it.authorUid } 36 | val contentType = varchar("content_type").bindTo { it.contentType } 37 | val content = varchar("content").bindTo { it.content } 38 | val publishTime = datetime("publish_time").bindTo { it.publishTime } 39 | val status = varchar("status").bindTo { it.status } 40 | val likes = int("likes").bindTo { it.likes } 41 | val comments = int("comments").bindTo { it.comments } 42 | val shares = int("shares").bindTo { it.shares } 43 | val bookmarks = int("bookmarks").bindTo { it.bookmarks } 44 | val createTime = datetime("create_time").bindTo { it.createTime } 45 | val updateTime = datetime("update_time").bindTo { it.updateTime } 46 | val isDelete = varchar("is_delete").bindTo { it.isDelete } 47 | val visits = long("visits").bindTo { it.visits } 48 | val channelKey = varchar("channel_key").bindTo { it.channelKey } 49 | var coverImg = int("cover_img").bindTo { it.coverImg } 50 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/topic/TopicComment.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.topic 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.* 5 | import java.time.* 6 | 7 | interface TopicComment : Entity { 8 | var id: Long 9 | var tid: Long //topic id 10 | var uid: Long 11 | var content: String? //内容 12 | var replyId: Long //回复的评论id,默认-1,表示回复的是当前文章 13 | var createTime: LocalDateTime 14 | var likes:Long //点赞 15 | } 16 | 17 | object TopicComments : Table("topic_comment") { 18 | val id = long("id").bindTo { it.id } 19 | val tid = long("tid").bindTo { it.tid } 20 | val uid = long("uid").bindTo { it.uid } 21 | val content = varchar("content").bindTo { it.content } 22 | val replyId = long("reply_id").bindTo { it.replyId } 23 | val createTime = datetime("create_time").bindTo { it.createTime } 24 | val likes = long("likes").bindTo { it.likes } 25 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/topic/TopicFiles.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.topic 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.Table 5 | import org.ktorm.schema.datetime 6 | import org.ktorm.schema.long 7 | import org.ktorm.schema.varchar 8 | import java.time.LocalDateTime 9 | 10 | interface TopicFile : Entity { 11 | var id: Long 12 | var uid: Long 13 | var tid: Long 14 | var fid: Long 15 | var fileType:String 16 | var fileDesc:String 17 | var createTime: LocalDateTime 18 | var updateTime: LocalDateTime 19 | var isDelete:String 20 | 21 | } 22 | 23 | object TopicFiles : Table("topic_files") { 24 | val id = long("id").primaryKey().bindTo { it.id } 25 | val uid = long("uid").bindTo { it.uid } 26 | val tid = long("tid").bindTo { it.tid } 27 | val fid = long("fid").bindTo { it.fid } 28 | val fileType = varchar("file_type").bindTo { it.fileType } 29 | val fileDesc = varchar("file_desc").bindTo { it.fileDesc } 30 | val createTime = datetime("create_time").bindTo { it.createTime } 31 | val updateTime = datetime("update_time").bindTo { it.updateTime } 32 | val isDelete = varchar("is_delete").bindTo { it.isDelete } 33 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/topic/TopicLike.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.topic 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.* 5 | import java.time.* 6 | 7 | interface TopicLike : Entity { 8 | var id: Long 9 | var tid: Long //topic外键 10 | var uid: Long //用户外键 11 | var createTime: LocalDateTime 12 | } 13 | 14 | object TopicLikes : Table("topic_like") { 15 | val id = long("id").primaryKey().bindTo { it.id } 16 | val tid = long("tid").bindTo { it.tid } 17 | val uid = long("uid").bindTo { it.uid } 18 | val createTime = datetime("create_time").bindTo { it.createTime } 19 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/topic/TopicTags.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.topic 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.Table 5 | import org.ktorm.schema.long 6 | 7 | interface TopicTags : Entity { 8 | var topicId: Long 9 | var tagId: Long 10 | } 11 | 12 | object TopicTagsRelation : Table("topic_tags") { 13 | val topicId = long("topic_id").bindTo { it.topicId } 14 | val tagId = long("tag_id").bindTo { it.tagId } 15 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/topic/TopicViewHis.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.topic 2 | 3 | import com.mars.social.model.topic.TopicLikes.bindTo 4 | import com.mars.social.model.topic.TopicLikes.primaryKey 5 | import org.ktorm.entity.Entity 6 | import org.ktorm.schema.Table 7 | import org.ktorm.schema.datetime 8 | import org.ktorm.schema.long 9 | import java.time.LocalDateTime 10 | 11 | interface TopicViewHis : Entity { 12 | var id :Long 13 | var uid : Long 14 | var tid: Long 15 | var createTime : LocalDateTime 16 | } 17 | 18 | object TopicViewHisDB : Table("topic_view_his"){ 19 | val id = long("id").primaryKey().bindTo { it.id } 20 | val uid = long("uid").bindTo { it.uid } 21 | val tid = long("tid").bindTo { it.tid } 22 | val createTime = datetime("create_time").bindTo { it.createTime } 23 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/user/Friendship.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.user 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.* 5 | import java.time.* 6 | 7 | interface Friendship : Entity { 8 | var id: Long 9 | var uidSource: Long //源用户 10 | var uidTo: Long //目标用户 11 | var status: String? //applying,friends,rejected,blocked 12 | var rejectReason: String? //拒绝原因 13 | var createTime: LocalDateTime 14 | var updateTime: LocalDateTime 15 | var msgId: Long 16 | } 17 | 18 | object Friendships : Table("friendships") { 19 | val id = long("id").primaryKey().bindTo { it.id } 20 | val uidSource = long("uid_source").bindTo { it.uidSource } 21 | val uidTo = long("uid_to").bindTo { it.uidTo } 22 | val status = varchar("status").bindTo { it.status } 23 | val rejectReason = varchar("reject_reason").bindTo { it.rejectReason } 24 | val createTime = datetime("create_time").bindTo { it.createTime } 25 | val updateTime = datetime("update_time").bindTo { it.updateTime } 26 | val msgId = long("msg_id").bindTo { it.msgId } 27 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/user/User.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.user 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.* 5 | import java.time.LocalDateTime 6 | 7 | interface User : Entity { 8 | val id: Long 9 | var userName: String //账号 10 | var password: String //密码 11 | var email: String 12 | var phone: String 13 | var registerTime: LocalDateTime 14 | var lastLoginTime: LocalDateTime 15 | var isActive: String //是否启用状态,字符串的true false 16 | var type:String //用户类型,是不是AI 17 | } 18 | 19 | object Users : Table("user") { 20 | val id = long("id").primaryKey().bindTo { it.id } 21 | val userName = varchar("user_name").bindTo { it.userName } 22 | val password = varchar("password").bindTo { it.password } 23 | val email = varchar("email").bindTo { it.email } 24 | val phone = varchar("phone").bindTo { it.phone } 25 | val registerTime = datetime("register_time").bindTo { it.registerTime } 26 | val lastLoginTime = datetime("last_login_time").bindTo { it.lastLoginTime } 27 | val isActive = varchar("is_active").bindTo { it.isActive } 28 | val type = varchar("type").bindTo { it.type } 29 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/user/UserDetail.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.user 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.* 5 | import java.time.* 6 | 7 | interface UserDetail : Entity { 8 | var id: Long 9 | var uid: Long 10 | var firstName: String //姓 11 | var secondName: String //名 12 | var nickName: String //昵称 13 | var gender: String //性别 14 | var birthdate: LocalDate //出生日期 15 | var country: String //国籍 16 | var city: String //城市 17 | var address: String //住址 18 | var avatar: String //头像,先存链接吧,后面要想想怎么弄个OSS存储 19 | var createTime: LocalDateTime 20 | var signature: String //个性签名 21 | } 22 | 23 | object UserDetails : Table("user_details") { 24 | val id = long("id").primaryKey().bindTo { it.id } 25 | val uid = long("uid").bindTo { it.uid } 26 | val firstName = varchar("first_name").bindTo { it.firstName } 27 | val secondName = varchar("second_name").bindTo { it.secondName } 28 | val nickName = varchar("nick_name").bindTo { it.nickName } 29 | val gender = varchar("gender").bindTo { it.gender } 30 | val birthdate = date("birthdate").bindTo { it.birthdate } 31 | val country = varchar("country").bindTo { it.country } 32 | val address = varchar("address").bindTo { it.address } 33 | val avatar = varchar("avatar").bindTo { it.avatar } 34 | val createTime = datetime("create_time").bindTo { it.createTime } 35 | val signature = varchar("signature").bindTo { it.signature } 36 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/user/UserFollow.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.user 2 | 3 | import com.mars.social.model.user.Friendships.bindTo 4 | import com.mars.social.model.user.Friendships.primaryKey 5 | import org.ktorm.entity.Entity 6 | import org.ktorm.schema.Table 7 | import org.ktorm.schema.datetime 8 | import org.ktorm.schema.long 9 | import org.ktorm.schema.varchar 10 | import java.time.LocalDateTime 11 | 12 | interface UserFollow : Entity { 13 | var id: Long 14 | var followerUid:Long 15 | var followedUid:Long 16 | var createTime: LocalDateTime 17 | var followUserNickName:String? 18 | var followUserUserName:String? 19 | } 20 | 21 | object UserFollowDB : Table("user_follow") { 22 | val id = long("id").primaryKey().bindTo { it.id } 23 | val followerUid = long("follower_uid").primaryKey().bindTo { it.followerUid } 24 | val followedUid = long("followed_uid").primaryKey().bindTo { it.followedUid } 25 | val createTime = datetime("create_time").bindTo { it.createTime } 26 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/model/user/UserRole.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.model.user 2 | 3 | import org.ktorm.entity.Entity 4 | import org.ktorm.schema.Table 5 | import org.ktorm.schema.long 6 | import org.ktorm.schema.varchar 7 | 8 | interface UserRole : Entity { 9 | val id: Long 10 | val uid: Long 11 | val role: String 12 | } 13 | object UserRoles : Table("user_role") { 14 | val id = long("id").primaryKey().bindTo { it.id } 15 | val uid = long("uid").bindTo { it.uid } 16 | val role = varchar("role").bindTo { it.role } 17 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/oss/minio/MinioConfig.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.oss.minio 2 | 3 | import io.minio.MinioClient 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | 7 | @Configuration 8 | class MinioConfig(private val minioProperties: MinioProperties) { 9 | @Bean 10 | fun minioClient(): MinioClient { 11 | return MinioClient.builder() 12 | .endpoint(minioProperties.endpoint) 13 | .credentials(minioProperties.accessKey, minioProperties.secretKey) 14 | .build() 15 | } 16 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/oss/minio/MinioController.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.oss.minio 2 | 3 | import cn.dev33.satoken.annotation.SaCheckLogin 4 | import cn.dev33.satoken.annotation.SaCheckRole 5 | import com.mars.social.dto.FileInfo 6 | import com.mars.social.model.mix.File 7 | import com.mars.social.model.mix.Files 8 | import com.mars.social.model.topic.TopicLike 9 | import com.mars.social.utils.R 10 | import io.minio.* 11 | import io.minio.http.Method 12 | import org.ktorm.database.Database 13 | import org.ktorm.dsl.* 14 | import org.ktorm.entity.Entity 15 | import org.ktorm.entity.add 16 | import org.ktorm.entity.sequenceOf 17 | import org.springframework.beans.factory.annotation.Autowired 18 | import org.springframework.beans.factory.annotation.Value 19 | import org.springframework.http.HttpHeaders 20 | import org.springframework.http.HttpStatus 21 | import org.springframework.http.ResponseEntity 22 | import org.springframework.web.bind.annotation.* 23 | import org.springframework.web.multipart.MultipartFile 24 | import java.net.URLEncoder 25 | import java.time.LocalDateTime 26 | import java.time.format.DateTimeFormatter 27 | import java.util.* 28 | 29 | @RestController 30 | @RequestMapping("/oss") 31 | class MinioController(val minioClient: MinioClient, @Value("\${minio.bucketname}") val bucketName: String) { 32 | 33 | @Autowired 34 | protected lateinit var database: Database 35 | 36 | // @GetMapping("/list-buckets") 37 | // fun listBuckets(): List { 38 | // return minioClient.listBuckets().map { it.name() } 39 | // } 40 | 41 | @SaCheckLogin 42 | @SaCheckRole("sys") 43 | @GetMapping("/list") 44 | fun listFiles(): ResponseEntity { 45 | val objectList = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).maxKeys(100).build()) 46 | var obj = objectList.map{it.get()} 47 | val fileNames = objectList.map { it.get().objectName() } 48 | return ResponseEntity.ok(R.ok(fileNames)) 49 | } 50 | 51 | @GetMapping("/download") 52 | fun downloadFile(@RequestParam fid: Long): ResponseEntity { 53 | val file = database.from(Files).select().where{ Files.id eq fid }.map { row -> Files.createEntity(row) }.firstOrNull() 54 | 55 | val fileName = file?.fileName; 56 | val objectResponse = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).`object`(fileName).build()) 57 | val byteArray = objectResponse.readAllBytes() 58 | val headers = HttpHeaders() 59 | headers.add("Content-Disposition", "attachment; filename=${URLEncoder.encode(fileName, "UTF-8")}") 60 | return ResponseEntity(byteArray, headers, HttpStatus.OK) 61 | } 62 | 63 | // data class UploadedFile(val fileName: String, val originalFilename: String) 64 | @PostMapping("/upload") 65 | fun uploadFiles(@RequestBody files: List): ResponseEntity { 66 | val uploadedFiles = mutableListOf() 67 | 68 | for (file in files) { 69 | // val fileName = "${UUID.randomUUID()}-${file.originalFilename}" 70 | val uuid = UUID.randomUUID().toString().substring(0, 8) 71 | val dateTime = LocalDateTime.now() 72 | val dateTimeFormatted = dateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")) 73 | var fileName = "$dateTimeFormatted-$uuid-${file.originalFilename}" 74 | val putObjectArgs = PutObjectArgs.builder() 75 | .bucket(bucketName) 76 | .`object`(fileName) 77 | .stream(file.inputStream, file.size, -1) 78 | .contentType(file.contentType) 79 | .build() 80 | val result = minioClient.putObject(putObjectArgs) 81 | val originalFilename = file.originalFilename 82 | 83 | // val uploadedFile = Entity(fileName, originalFilename.toString()) 84 | val uploadedFile = Entity.create() 85 | uploadedFile.fileName = fileName 86 | uploadedFile.originalFileName = file.originalFilename.toString() 87 | uploadedFile.createTime = LocalDateTime.now() 88 | database.sequenceOf(Files).add(uploadedFile) 89 | uploadedFiles.add(uploadedFile) 90 | } 91 | return ResponseEntity.ok(R.ok("Files uploaded successfully",uploadedFiles)) 92 | } 93 | 94 | @GetMapping("/detail") 95 | fun getFileInfoByFileName(@RequestParam fid: Long): ResponseEntity { 96 | val file = database.from(Files).select().where{ Files.id eq fid }.map { row -> Files.createEntity(row) }.firstOrNull() 97 | return ResponseEntity.ok(file?.let { R.ok(it) }) 98 | } 99 | 100 | @GetMapping("/preview") 101 | fun getPreviewUrl(@RequestParam fid: Long): ResponseEntity { 102 | val file = database.from(Files).select().where{ Files.id eq fid }.map { row -> Files.createEntity(row) }.firstOrNull() 103 | val fileName = file?.fileName; 104 | val url = minioClient.getPresignedObjectUrl( 105 | GetPresignedObjectUrlArgs.builder() 106 | .method(Method.GET) 107 | .bucket(bucketName) 108 | .`object`(fileName) // 注意这里的`object`需要加上反引号,因为是Kotlin的关键字 109 | .expiry(36000) // 链接有效期,单位为秒 110 | .build() 111 | ) 112 | return ResponseEntity.ok(R.ok(url)) 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/oss/minio/MinioProperties.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.oss.minio 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | import org.springframework.context.annotation.Configuration 5 | 6 | @Configuration 7 | @ConfigurationProperties(prefix = "minio") 8 | class MinioProperties { 9 | lateinit var endpoint: String 10 | lateinit var accessKey: String 11 | lateinit var secretKey: String 12 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/security/SaTokenConfigure.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.security 2 | 3 | import cn.dev33.satoken.interceptor.SaInterceptor 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer 7 | 8 | @Configuration 9 | class SaTokenConfigure : WebMvcConfigurer { 10 | // 注册 Sa-Token 拦截器,打开注解式鉴权功能 11 | override fun addInterceptors(registry: InterceptorRegistry) { 12 | // 注册 Sa-Token 拦截器,打开注解式鉴权功能 13 | registry.addInterceptor(SaInterceptor()).addPathPatterns("/**") 14 | } 15 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/security/StpInterfaceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.security; 2 | 3 | import cn.dev33.satoken.stp.StpInterface 4 | import com.mars.social.model.user.UserRoles 5 | import org.ktorm.database.Database 6 | import org.ktorm.dsl.eq 7 | import org.ktorm.entity.filter 8 | import org.ktorm.entity.sequenceOf 9 | import org.ktorm.entity.toList 10 | import org.springframework.beans.factory.annotation.Autowired 11 | import org.springframework.stereotype.Component 12 | 13 | /** 14 | * 自定义权限加载接口实现类 15 | */ 16 | // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展 17 | @Component 18 | class StpInterfaceImpl : StpInterface { 19 | @Autowired 20 | protected lateinit var database: Database 21 | 22 | /** 23 | * 返回一个账号所拥有的权限码集合 24 | */ 25 | override fun getPermissionList(loginId: Any, loginType: String): kotlin.collections.List { 26 | // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限 27 | val list: MutableList = ArrayList() 28 | list.add("user.*") 29 | return list 30 | } 31 | 32 | /** 33 | * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) 34 | */ 35 | override fun getRoleList(loginId: Any, loginType: String): kotlin.collections.List { 36 | val list: MutableList = ArrayList() 37 | val uid:Long = convertAnyToLong(loginId) 38 | val userRoles = database.sequenceOf(UserRoles).filter { it.uid eq uid}.toList() 39 | for(role in userRoles){ 40 | list.add(role.role) 41 | } 42 | return list 43 | } 44 | fun convertAnyToLong(value: Any): Long { 45 | return when (value) { 46 | is Long -> value // 如果是 Long 类型直接返回 47 | is Int -> value.toLong() // 如果是 Int 类型转换为 Long 48 | is Double -> value.toLong() // 如果是 Double 类型转换为 Long 49 | is String -> value.toLong() // 如果是 String 类型尝试转换为 Long 50 | else -> 0 // 其他类型返回 null 或者可以根据具体需求进行处理 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/tools/AutoGenModel.kt: -------------------------------------------------------------------------------- 1 | //package com.mars.social.tools 2 | // 3 | //import java.io.File 4 | //import java.sql.DriverManager 5 | // 6 | //class AutoGenModel { 7 | //} 8 | //fun main(args: Array) { 9 | // val url = "jdbc:mysql://localhost:3306/embodied" 10 | // val username = "root" 11 | // val password = "123456" 12 | // 13 | // Class.forName("com.mysql.cj.jdbc.Driver") 14 | // val connection = DriverManager.getConnection(url, username, password) 15 | // val metaData = connection.metaData 16 | // 17 | // val tableName = "messages" 18 | // val typeName = "messages"; 19 | // val typeTable = "messages"; 20 | // 21 | // val resultSet = metaData.getColumns(null, null, tableName, null) 22 | // val primaryKeys = metaData.getPrimaryKeys(null, null, tableName) 23 | // 24 | // val columnNameMap = mutableMapOf>() 25 | // 26 | // while (primaryKeys.next()) { 27 | // val columnName = primaryKeys.getString("COLUMN_NAME") 28 | // val keySeq = primaryKeys.getShort("KEY_SEQ") 29 | // val pkName = primaryKeys.getString("PK_NAME") 30 | // columnNameMap[columnName] = Pair(keySeq, pkName) 31 | // println("Primary Key Column Name: $columnName, Key Sequence: $keySeq, Primary Key Name: $pkName") 32 | // } 33 | // val columnInfoList = mutableListOf() 34 | // 35 | // while (resultSet.next()) { 36 | // val columnName = resultSet.getString("COLUMN_NAME") 37 | // val dataType = resultSet.getString("TYPE_NAME") 38 | // val isNullable = resultSet.getString("IS_NULLABLE") 39 | // val isAutoIncrement = resultSet.getString("IS_AUTOINCREMENT") 40 | // var isPrimaryKey = "NO" 41 | // if(columnNameMap.containsKey(columnName)){ 42 | // isPrimaryKey = "YES" 43 | // } 44 | // val columnInfo = ColumnInfo(columnName, dataType, isNullable, isAutoIncrement, isPrimaryKey) 45 | // columnInfoList.add(columnInfo) 46 | //// println("Column Name: $columnName, Data Type: $dataType, Nullable: $isNullable, Auto Increment: $isAutoIncrement , Is PrimaryKey: $isPrimaryKey") 47 | // } 48 | // for (columnInfo in columnInfoList) { 49 | // println("Column Name: ${columnInfo.columnName}, Data Type: ${columnInfo.dataType}, Nullable: ${columnInfo.isNullable}, Auto Increment: ${columnInfo.isAutoIncrement}, Is PrimaryKey: ${columnInfo.isPrimaryKey}") 50 | // } 51 | // resultSet.close() 52 | // connection.close() 53 | // toCodeFile(typeName,typeTable,tableName,columnInfoList) 54 | //} 55 | //data class ColumnInfo(val columnName: String, val dataType: String, val isNullable: String, val isAutoIncrement: String, val isPrimaryKey: String) 56 | //fun toCamelCase(input: String): String { 57 | // return input.split("_").joinToString("") { it.capitalize() } 58 | //} 59 | //fun toCodeFile(typename:String, typeTable:String, tableName:String, columnInfoList: MutableList){ 60 | // val propertyMap = mapOf( 61 | // "String" to "varchar", 62 | // "Long" to "Long", 63 | // "Int" to "int", 64 | // "INT" to "Int", 65 | // "BIGINT" to "long", 66 | // "MEDIUMTEXT" to "String", 67 | // "date" to "LocalDate", 68 | // "DATETIME" to "LocalDateTime", 69 | // ) 70 | // 71 | // val properties = columnInfoList.joinToString("\n") { columnInfo -> 72 | // val propName = toCamelCase(columnInfo.columnName).decapitalize() 73 | // val propType = propertyMap[columnInfo.dataType] ?: "String?" 74 | // "\tvar $propName: $propType" 75 | // } 76 | // 77 | // val code = """ 78 | //package com.mars.social.model 79 | // 80 | //import org.ktorm.entity.Entity 81 | //import org.ktorm.schema.* 82 | //import java.time.* 83 | // 84 | //interface $typename : Entity<$typename> { 85 | // $properties 86 | //} 87 | // 88 | //object $typeTable : Table<$typename>("$tableName") { 89 | // ${columnInfoList.joinToString("\n") { columnInfo -> 90 | // val propName = toCamelCase(columnInfo.columnName).decapitalize() 91 | // val dataType = if (columnInfo.dataType.toLowerCase() == "bigint") "long" else columnInfo.dataType.toLowerCase() 92 | // "\tval $propName = ${dataType}(\"${columnInfo.columnName}\").bindTo { it.$propName }" 93 | // }} 94 | //} 95 | // """.trimIndent() 96 | // 97 | // val fileName = "$typename.kt" 98 | //// val resourcesDir = File("src/main/resources") 99 | // val srcDir = File("src/main/kotlin/com/mars/social/model") 100 | // val filePath = File(srcDir, fileName) 101 | // filePath.writeText(code) 102 | // 103 | // println("File saved successfully at: $filePath") 104 | //} -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/utils/GlobalException.java: -------------------------------------------------------------------------------- 1 | package com.mars.social.utils; 2 | import org.springframework.web.bind.annotation.ExceptionHandler; 3 | import org.springframework.web.bind.annotation.RestControllerAdvice; 4 | 5 | import cn.dev33.satoken.exception.DisableServiceException; 6 | import cn.dev33.satoken.exception.NotBasicAuthException; 7 | import cn.dev33.satoken.exception.NotLoginException; 8 | import cn.dev33.satoken.exception.NotPermissionException; 9 | import cn.dev33.satoken.exception.NotRoleException; 10 | import cn.dev33.satoken.exception.NotSafeException; 11 | import cn.dev33.satoken.util.SaResult; 12 | 13 | /** 14 | * 全局异常处理 15 | */ 16 | @RestControllerAdvice 17 | public class GlobalException { 18 | 19 | // 拦截:未登录异常 20 | @ExceptionHandler(NotLoginException.class) 21 | public SaResult handlerException(NotLoginException e) { 22 | 23 | // 打印堆栈,以供调试 24 | e.printStackTrace(); 25 | 26 | // 返回给前端 27 | return SaResult.error(e.getMessage()); 28 | } 29 | 30 | // 拦截:缺少权限异常 31 | @ExceptionHandler(NotPermissionException.class) 32 | public SaResult handlerException(NotPermissionException e) { 33 | e.printStackTrace(); 34 | System.out.println(e.getPermission()); 35 | return SaResult.error("No permission to access" ); 36 | } 37 | 38 | // 拦截:缺少角色异常 39 | @ExceptionHandler(NotRoleException.class) 40 | public SaResult handlerException(NotRoleException e) { 41 | e.printStackTrace(); 42 | System.out.println(e.getRole()); 43 | return SaResult.error("No role permission to access"); 44 | // return SaResult.error(); 45 | } 46 | 47 | // 拦截:二级认证校验失败异常 48 | @ExceptionHandler(NotSafeException.class) 49 | public SaResult handlerException(NotSafeException e) { 50 | e.printStackTrace(); 51 | return SaResult.error("not safe:" + e.getService()); 52 | } 53 | 54 | // 拦截:服务封禁异常 55 | @ExceptionHandler(DisableServiceException.class) 56 | public SaResult handlerException(DisableServiceException e) { 57 | e.printStackTrace(); 58 | return SaResult.error("current account " + e.getService() + " was ban (level=" + e.getLevel() + "):" + e.getDisableTime() + "seconds to free"); 59 | } 60 | 61 | // 拦截:Http Basic 校验失败异常 62 | @ExceptionHandler(NotBasicAuthException.class) 63 | public SaResult handlerException(NotBasicAuthException e) { 64 | e.printStackTrace(); 65 | return SaResult.error(e.getMessage()); 66 | } 67 | 68 | // 拦截:其它所有异常 69 | @ExceptionHandler(Exception.class) 70 | public SaResult handlerException(Exception e) { 71 | e.printStackTrace(); 72 | return SaResult.error(e.getMessage()); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/utils/GlobalUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.utils 2 | 3 | import java.math.BigInteger 4 | import java.security.SecureRandom 5 | 6 | object GlobalUtils { 7 | fun generateRandomToken(length: Int): String { 8 | val secureRandom = SecureRandom() 9 | val token = BigInteger(130, secureRandom).toString(32) 10 | return token.take(length) 11 | } 12 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/utils/LocalTimeSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.utils 2 | 3 | import com.fasterxml.jackson.core.JsonParser 4 | import com.fasterxml.jackson.core.JsonGenerator 5 | import com.fasterxml.jackson.databind.JsonSerializer 6 | import com.fasterxml.jackson.databind.SerializerProvider 7 | import com.fasterxml.jackson.databind.DeserializationContext 8 | import com.fasterxml.jackson.databind.JsonDeserializer 9 | import com.fasterxml.jackson.databind.module.SimpleModule 10 | import java.io.IOException 11 | import java.time.LocalTime 12 | 13 | //class LocalTimeSerializer : JsonSerializer() { 14 | // @Throws(IOException::class) 15 | // override fun serialize(value: LocalTime?, gen: JsonGenerator, serializers: SerializerProvider) { 16 | // gen.writeString(value.toString()) 17 | // } 18 | //} 19 | // 20 | //class LocalTimeDeserializer : JsonDeserializer() { 21 | // @Throws(IOException::class) 22 | // override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalTime { 23 | // return LocalTime.parse(p.text) 24 | // } 25 | //} -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/utils/LocaleConfig.java: -------------------------------------------------------------------------------- 1 | //package com.mars.social.utils; 2 | // 3 | //import org.springframework.context.annotation.Bean; 4 | //import org.springframework.context.annotation.Configuration; 5 | //import org.springframework.web.servlet.LocaleResolver; 6 | //import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | //import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | //import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; 9 | //import org.springframework.web.servlet.i18n.SessionLocaleResolver; 10 | // 11 | //import java.util.Locale; 12 | // 13 | ///** 14 | // * 配置国际化语言 15 | // */ 16 | //@Configuration 17 | //public class LocaleConfig { 18 | // 19 | // /** 20 | // * 默认解析器 其中locale表示默认语言 21 | // */ 22 | // @Bean 23 | // public LocaleResolver localeResolver() { 24 | // SessionLocaleResolver localeResolver = new SessionLocaleResolver(); 25 | // localeResolver.setDefaultLocale(Locale.CHINA); 26 | // return localeResolver; 27 | // } 28 | // 29 | // /** 30 | // * 默认拦截器 其中lang表示切换语言的参数名 31 | // */ 32 | // @Bean 33 | // public WebMvcConfigurer localeInterceptor() { 34 | // return new WebMvcConfigurer() { 35 | // @Override 36 | // public void addInterceptors(InterceptorRegistry registry) { 37 | // LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor(); 38 | // localeInterceptor.setParamName("lang"); 39 | // registry.addInterceptor(localeInterceptor); 40 | // } 41 | // }; 42 | // } 43 | //} -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/utils/MessageUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.utils 2 | 3 | import org.springframework.context.MessageSource 4 | import org.springframework.context.i18n.LocaleContextHolder 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | class MessageUtil(private val messageSource: MessageSource) { 9 | 10 | /** 11 | * 获取单个国际化翻译值 12 | */ 13 | fun get(msgKey: String): String { 14 | return try { 15 | // val request = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request 16 | // val localeResolver = AcceptHeaderLocaleResolver() 17 | // val locale = localeResolver.resolveLocale(request) 18 | // LocaleContextHolder.setLocale(locale) 19 | //我可真是惊呆了,zh_CN识别不了,只能传入zh或者en,zh-CN不能下划线,必须- 20 | //这鬼玩意,真服了。。。必须要有个messages.properties 搞到两点,吐血。 21 | val local = LocaleContextHolder.getLocale() 22 | messageSource.getMessage(msgKey, null, local) 23 | } catch (e: Exception) { 24 | msgKey 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/utils/PageCalculator.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.utils 2 | 3 | import com.mars.social.dto.PageRequest 4 | 5 | class PageCalculator { 6 | companion object { 7 | fun calculateOffset(pageRequest: PageRequest, totalRecords: Int): Int { 8 | val maxOffset = (totalRecords / pageRequest.pageSize).toInt() * pageRequest.pageSize 9 | return maxOf(0, minOf((pageRequest.pageNumber - 1) * pageRequest.pageSize, maxOffset)) 10 | } 11 | 12 | fun calculateLimit(pageRequest: PageRequest, totalRecords: Int): Int { 13 | val maxOffset = (totalRecords / pageRequest.pageSize).toInt() * pageRequest.pageSize 14 | if ((pageRequest.pageNumber - 1) * pageRequest.pageSize >= maxOffset) { 15 | return 0 16 | } 17 | return minOf(pageRequest.pageSize, (totalRecords - (pageRequest.pageNumber - 1) * pageRequest.pageSize).toInt()) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/social/src/main/kotlin/com/mars/social/utils/R.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social.utils 2 | 3 | data class R( 4 | var code: Int, 5 | var message: String, 6 | var data: Any? 7 | ) 8 | { 9 | companion object { 10 | fun ok(data: Any): R { 11 | return R(20000, "成功", data) 12 | } 13 | fun ok(message: String, data: Any?): R { 14 | return R(20000, "成功", data) 15 | } 16 | fun fail(message: String): R { 17 | return R(30000, message, {}) 18 | } 19 | fun fail(message: String, data: Any?): R { 20 | return R(30000, message, data ?: "System Error") 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /server/social/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | type: com.alibaba.druid.pool.DruidDataSource 4 | url: jdbc:mysql://120.78.142.84:3307/embodied 5 | driver: com.mysql.cj.jdbc.Driver 6 | username: approot 7 | password: ENC(3DXRxPX2xnG7iKkdtXF0Y+fcT+VU+jHn) 8 | druid: 9 | # 初始化时建立物理连接的个数 10 | initial-size: 5 11 | # 连接池的最小空闲数量 12 | min-idle: 5 13 | # 连接池最大连接数量 14 | max-active: 20 15 | # 获取连接时最大等待时间,单位毫秒 16 | max-wait: 60000 17 | # 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 18 | test-while-idle: true 19 | # 既作为检测的间隔时间又作为testWhileIdel执行的依据 20 | time-between-eviction-runs-millis: 60000 21 | # 销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接(配置连接在池中的最小生存时间) 22 | min-evictable-idle-time-millis: 30000 23 | # 用来检测数据库连接是否有效的sql 必须是一个查询语句(oracle中为 select 1 from dual) 24 | validation-query: select 'x' 25 | # 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true 26 | test-on-borrow: false 27 | # 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true 28 | test-on-return: false 29 | # 是否缓存preparedStatement, 也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。 30 | pool-prepared-statements: false 31 | # 置监控统计拦截的filters,去掉后监控界面sql无法统计,stat: 监控统计、Slf4j:日志记录、waLL: 防御sqL注入 32 | filters: stat,wall,slf4j 33 | # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 34 | max-pool-prepared-statement-per-connection-size: -1 35 | # 合并多个DruidDataSource的监控数据 36 | use-global-data-source-stat: true 37 | # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 38 | connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 39 | 40 | web-stat-filter: 41 | # 是否启用StatFilter默认值true 42 | enabled: true 43 | # 添加过滤规则 44 | url-pattern: /* 45 | # 忽略过滤的格式 46 | exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico 47 | 48 | stat-view-servlet: 49 | # 是否启用StatViewServlet默认值true 50 | enabled: true 51 | # 访问路径为/druid时,跳转到StatViewServlet 52 | url-pattern: /druid/* 53 | # 是否能够重置数据 54 | reset-enable: false 55 | # 需要账号密码才能访问控制台,默认为root 56 | login-username: druid 57 | login-password: druid 58 | # IP白名单 59 | allow: 127.0.0.1 60 | # IP黑名单(共同存在时,deny优先于allow) 61 | deny: 62 | # 资源信息 63 | messages: 64 | # 国际化资源文件路径 65 | basename: i18n/messages 66 | encoding: UTF-8 67 | #设置文件上传大小 68 | servlet: 69 | multipart: 70 | max-file-size: 50MB 71 | max-request-size: 50MB 72 | 73 | jasypt: 74 | encryptor: 75 | password: fa7bd4edd42448aea8c9 76 | algorithm: PBEWithMD5AndDES 77 | 78 | # 配置WebSocket监听的端口为8081 79 | #websocket: 80 | # port: 8081 81 | 82 | ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## 83 | sa-token: 84 | # token 名称(同时也是 cookie 名称) 85 | token-name: mtoken 86 | # token-prefix: satoken 87 | # token 有效期(单位:秒) 默认30天,-1 代表永久有效 88 | timeout: 2592000 89 | # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 90 | active-timeout: -1 91 | # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) 92 | is-concurrent: true 93 | # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) 94 | is-share: true 95 | # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) 96 | token-style: uuid 97 | # 是否输出操作日志 98 | is-log: true 99 | is-read-header: false 100 | 101 | #logging: 102 | # level: 103 | # org.springframework.jdbc.core.JdbcTemplate: DEBUG 104 | 105 | 106 | #minio配置 107 | minio: 108 | endpoint: http://120.78.142.84:9000/ 109 | accessKey: 9mheJLVpHTohzMYlOcXl 110 | secretKey: GYuPsHER732uwYOuRJc2yTfqhXI3nH2z2GAAJTg0 111 | bucketname: embodied 112 | -------------------------------------------------------------------------------- /server/social/src/main/resources/i18n/messages.properties: -------------------------------------------------------------------------------- 1 | account.register.succeed=account register succeed 2 | sign.in.succeed=sign in succeed 3 | sign.in.failed=sign in failed 4 | sign.out=sign out 5 | login.state=is login state: -------------------------------------------------------------------------------- /server/social/src/main/resources/i18n/messages_en_US.properties: -------------------------------------------------------------------------------- 1 | account.register.succeed=account register succeed 2 | sign.in.succeed=sign in succeed 3 | sign.in.failed=sign in failed 4 | sign.out=sign out 5 | login.state=is login state: -------------------------------------------------------------------------------- /server/social/src/main/resources/i18n/messages_zh_CN.properties: -------------------------------------------------------------------------------- 1 | account.register.succeed=注册成功 2 | sign.in.succeed=登录成功 3 | sign.in.failed=登录失败 4 | sign.out=登出 5 | login.state=登录状态: -------------------------------------------------------------------------------- /server/social/src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsZone/Embodied/d376b9c72305b2e9c567350ccb88bb1ea75233bc/server/social/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /server/social/src/test/kotlin/com/mars/social/SocialApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.mars.social 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | @SpringBootTest 7 | class SocialApplicationTests { 8 | 9 | @Test 10 | fun contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------