├── .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 |
46 | : }
47 |
48 |
49 | - onClickTabbar('/discover')}
52 | >
53 | {activeTab === '/discover' ?
54 |
58 | : }
59 |
60 |
61 | - onClickTabbar('/post')}
64 | >
65 | {activeTab === '/post' ?
66 |
70 | : }
71 |
72 |
73 | - onClickTabbar('/message')}
76 | >
77 | {activeTab === '/message' ?
78 |
82 | : }
83 |
84 |
85 | - onClickTabbar(`/profile/${loginUserId}/myPost`)}
88 | >
89 | {activeTab === `/profile/${loginUserId}/myPost` ?
90 |
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 |
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 |
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 |
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 | 2,146 |
80 | 51M |
81 | 11 |
82 |
83 |
84 | 关注 |
85 | 粉丝 |
86 | 获赞 |
87 |
88 |
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 |
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 |
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 |
65 | : }
66 |
67 |
68 | - onClickTabbar('/discover')}
71 | >
72 | {activeTab === '/discover' ?
73 |
77 | : }
78 |
79 |
80 | - setActiveTab('/post')}
83 | >
84 | {activeTab === '/post' ?
85 |
89 | : }
90 |
91 |
92 | - setActiveTab('/message')}
95 | >
96 | {activeTab === '/message' ?
97 |
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 |
--------------------------------------------------------------------------------