Browse Source

!163 最大群人数改完3000

Merge pull request !163 from blue/v_3.0.0
master
blue 7 months ago
committed by Gitee
parent
commit
0657766ae8
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 8
      README.md
  2. 2
      im-platform/src/main/java/com/bx/implatform/contant/Constant.java
  3. 4
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  4. 43
      im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java
  5. 10
      im-platform/src/main/java/com/bx/implatform/util/FileUtil.java
  6. 7
      im-uniapp/App.vue
  7. 35
      im-uniapp/store/chatStore.js
  8. 16
      im-web/src/components/common/RightMenu.vue
  9. 36
      im-web/src/store/chatStore.js
  10. 75
      im-web/src/view/Login.vue
  11. 133
      im-web/src/view/Register.vue

8
README.md

@ -21,7 +21,7 @@
- 后台管理端上线,后台管理代码仓库地址:https://gitee.com/bluexsx/box-im-admin
- 框架和组件版本全面升级: jdk17、springboot3.3、node18等
- 部分界面,功能、性能优化1
- 部分界面,功能、性能优化
#### 在线体验
@ -39,10 +39,12 @@
![输入图片说明](%E6%88%AA%E5%9B%BE/h5%E4%BA%8C%E7%BB%B4%E7%A0%81.png)
说明:
1.由于微信小程序每次发布审核过于严苛和繁琐,暂时不再提供体验环境,但uniapp端依然会继续兼容小程序
2.体验环境部署的是商业版本,与开源版本功能存在一定差异,具体请参考:
1.**请勿利用公开账号辱骂他人、发布低俗内容,否则将直接对您的IP进行封禁**
2.由于微信小程序每次发布审核过于严苛和繁琐,暂时不再提供体验环境,但uniapp端依然会继续兼容小程序
3.体验环境部署的是商业版本,与开源版本功能存在一定差异,具体请参考:
https://www.yuque.com/u1475064/imk5n2/qtezcg32q1d0dr29#SbvXq
#### 付费服务
商业版源码: https://www.yuque.com/u1475064/imk5n2/qtezcg32q1d0dr29
远程协助: https://www.yuque.com/u1475064/imk5n2/fettd57rvzc29s5r

2
im-platform/src/main/java/com/bx/implatform/contant/Constant.java

@ -18,7 +18,7 @@ public final class Constant {
/**
* 大群人数上限
*/
public static final Long MAX_LARGE_GROUP_MEMBER = 10000L;
public static final Long MAX_LARGE_GROUP_MEMBER = 3000L;
/**
* 普通群人数上限

4
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java

@ -67,10 +67,10 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
}
// 群聊成员列表
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
if (dto.getReceipt() && userIds.size() > Constant.MAX_LARGE_GROUP_MEMBER) {
if (dto.getReceipt() && userIds.size() > Constant.MAX_NORMAL_GROUP_MEMBER) {
// 大群的回执消息过于消耗资源,不允许发送
throw new GlobalException(
String.format("当前群聊大于%s人,不支持发送回执消息", Constant.MAX_LARGE_GROUP_MEMBER));
String.format("当前群聊大于%s人,不支持发送回执消息", Constant.MAX_NORMAL_GROUP_MEMBER));
}
// 不用发给自己
userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList());

43
im-platform/src/main/java/com/bx/implatform/thirdparty/MinioService.java

@ -1,6 +1,8 @@
package com.bx.implatform.thirdparty;
import cn.hutool.core.util.RandomUtil;
import com.bx.implatform.util.DateTimeUtils;
import com.bx.implatform.util.FileUtil;
import io.minio.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -38,9 +40,7 @@ public class MinioService {
*/
public void makeBucket(String bucketName) {
try {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
log.error("创建bucket失败,", e);
}
@ -52,18 +52,9 @@ public class MinioService {
public void setBucketPublic(String bucketName) {
try {
// 设置公开
String sb = "{\"Version\":\"2012-10-17\"," +
"\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":" +
"{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"," +
"\"s3:GetBucketLocation\"],\"Resource\":[\"arn:aws:s3:::" + bucketName +
"\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\"],\"Resource\":[\"arn:aws:s3:::" +
bucketName +
"/*\"]}]}";
minioClient.setBucketPolicy(
SetBucketPolicyArgs.builder()
.bucket(bucketName)
.config(sb)
.build());
String sb =
"{\"Version\":\"2012-10-17\"," + "\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":" + "{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"," + "\"s3:GetBucketLocation\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(sb).build());
} catch (Exception e) {
log.error("创建bucket失败,", e);
}
@ -82,10 +73,10 @@ public class MinioService {
if (StringUtils.isBlank(originalFilename)) {
throw new RuntimeException();
}
String fileName = System.currentTimeMillis() + "";
if (originalFilename.lastIndexOf(".") >= 0) {
fileName += originalFilename.substring(originalFilename.lastIndexOf("."));
}
// 加入随机数,防止文件重名
String fileName = FileUtil.excludeExtension(originalFilename);
fileName += "_" + RandomUtil.randomString(4);
fileName += "." + FileUtil.getFileExtension(originalFilename);
String objectName = DateTimeUtils.getFormatDate(new Date(), DateTimeUtils.PARTDATEFORMAT) + "/" + fileName;
try {
InputStream stream = new ByteArrayInputStream(file.getBytes());
@ -111,13 +102,15 @@ public class MinioService {
* @return objectName
*/
public String upload(String bucketName, String path, String name, byte[] fileByte, String contentType) {
String fileName = System.currentTimeMillis() + name.substring(name.lastIndexOf("."));
// 加入随机数,防止文件重名
String fileName = FileUtil.excludeExtension(name);
fileName += "_" + RandomUtil.randomString(4);
fileName += "." + FileUtil.getFileExtension(name);
String objectName = DateTimeUtils.getFormatDate(new Date(), DateTimeUtils.PARTDATEFORMAT) + "/" + fileName;
try {
InputStream stream = new ByteArrayInputStream(fileByte);
PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(path + "/" + objectName)
.stream(stream, fileByte.length, -1).contentType(contentType).build();
.stream(stream, fileByte.length, -1).contentType(contentType).build();
//文件名称相同会覆盖
minioClient.putObject(objectArgs);
} catch (Exception e) {
@ -127,7 +120,6 @@ public class MinioService {
return objectName;
}
/**
* 删除
*
@ -138,7 +130,8 @@ public class MinioService {
*/
public boolean remove(String bucketName, String path, String fileName) {
try {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build());
minioClient.removeObject(
RemoveObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build());
} catch (Exception e) {
log.error("删除文件失败,", e);
return false;
@ -156,7 +149,7 @@ public class MinioService {
*/
public Boolean isExist(String bucketName, String path, String fileName) {
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build());
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(path + "/" + fileName).build());
} catch (Exception e) {
return false;
}

10
im-platform/src/main/java/com/bx/implatform/util/FileUtil.java

@ -13,6 +13,16 @@ public final class FileUtil {
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
/**
* 去除文件扩展名
*
* @param fileName 文件名
* @return
*/
public static String excludeExtension(String fileName) {
return fileName.substring(0,fileName.lastIndexOf("."));
}
/**
* 判断文件是否图片类型
*

7
im-uniapp/App.vue

@ -116,8 +116,11 @@ export default {
this.chatStore.refreshChats();
}).catch((e) => {
console.log(e)
this.$message.error("拉取离线消息失败");
this.onExit();
uni.showToast({
title: "拉取离线消息失败",
icon: "none"
})
this.exit();
})
},
pullPrivateOfflineMessage(minId) {

35
im-uniapp/store/chatStore.js

@ -335,11 +335,11 @@ export default defineStore('chatStore', {
}
},
refreshChats() {
if (!cacheChats) return;
let chats = cacheChats || this.chats;
// 更新会话免打扰状态
const friendStore = useFriendStore();
const groupStore = useGroupStore();
cacheChats.forEach(chat => {
chats.forEach(chat => {
if (chat.type == 'PRIVATE') {
let friend = friendStore.findFriend(chat.targetId);
if (friend) {
@ -353,28 +353,37 @@ export default defineStore('chatStore', {
}
})
// 排序
cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
chats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
// #ifndef APP-PLUS
/**
* 由于h5和小程序的stroge只有5m,大约只能存储2w条消息
* 所以这里每个会话只保留1000条消息防止溢出
* 由于h5和小程序的stroge只有5m,大约只能存储2w条消息所以可能需要清理部分历史消息
*/
cacheChats.forEach(chat => {
if (chat.messages.length > 1000) {
let idx = chat.messages.length - 1000;
chat.messages = chat.messages.slice(idx);
}
})
this.fliterMessage(chats, 5000, 1000);
// #endif
// 记录热数据索引位置
cacheChats.forEach(chat => chat.hotMinIdx = chat.messages.length);
chats.forEach(chat => chat.hotMinIdx = chat.messages.length);
// 将消息一次性装载回来
this.chats = cacheChats;
this.chats = chats;
// 清空缓存,不再使用
cacheChats = null;
// 消息持久化
this.saveToStorage(true);
},
fliterMessage(chats, maxTotalSize, maxPerChatSize) {
// 每个会话只保留maxPerChatSize条消息
let remainTotalSize = 0;
chats.forEach(chat => {
if (chat.messages.length > maxPerChatSize) {
let idx = chat.messages.length - maxPerChatSize;
chat.messages = chat.messages.slice(idx);
}
remainTotalSize += chat.messages.length;
})
// 保证消息总数不超过maxTotalSize条,否则继续清理
if (remainTotalSize > maxTotalSize) {
this.fliterMessage(chats, maxTotalSize, maxPerChatSize / 2);
}
},
saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿
if (this.loading) {

16
im-web/src/components/common/RightMenu.vue

@ -26,9 +26,11 @@ export default {
},
methods: {
open(pos, items) {
this.pos = pos;
this.pos.x = pos.x;
this.pos.y = pos.y;
this.items = items;
this.show = true;
this.rejustPos();
},
close() {
this.show = false;
@ -36,6 +38,16 @@ export default {
onSelectMenu(item) {
this.$emit("select", item);
this.close();
},
rejustPos() {
let menuH = this.items.length * 36;
let menuW = 100;
if (this.pos.y > window.innerHeight - menuH) {
this.pos.y = window.innerHeight - menuH;
}
if (this.pos.x > window.innerWidth - menuW) {
this.pos.x = window.innerWidth - menuW;
}
}
}
}
@ -75,4 +87,4 @@ export default {
}
}
}
</style>
</style>

36
im-web/src/store/chatStore.js

@ -333,11 +333,11 @@ export default defineStore('chatStore', {
}
},
refreshChats() {
if (!cacheChats) return;
let chats = cacheChats || this.chats;
// 刷新免打扰状态
const friendStore = useFriendStore();
const groupStore = useGroupStore();
cacheChats.forEach(chat => {
chats.forEach(chat => {
if (chat.type == 'PRIVATE') {
let friend = friendStore.findFriend(chat.targetId);
if (friend) {
@ -351,26 +351,38 @@ export default defineStore('chatStore', {
}
})
// 排序
cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
chats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
/**
* 由于部分浏览器不支持websql或indexdb只能使用localstorage而localstorage大小只有10m,可能会导致缓存空间溢出
* 解决办法:如果是使用localstorage的浏览器每个会话只保留1000条消息防止溢出
* 解决办法:针对只能使用localstorage的浏览器最多保留1w条消息,每个会话最多保留1000条消息
*/
cacheChats.forEach(chat => {
if (localForage.driver().includes("localStorage") && chat.messages.length > 1000) {
let idx = chat.messages.length - 1000;
chat.messages = chat.messages.slice(idx);
}
})
if (localForage.driver().includes("localStorage")) {
this.fliterMessage(chats, 10000, 1000)
}
// 记录热数据索引位置
cacheChats.forEach(chat => chat.hotMinIdx = chat.messages.length);
chats.forEach(chat => chat.hotMinIdx = chat.messages.length);
// 将消息一次性装载回来
this.chats = cacheChats;
this.chats = chats;
// 清空缓存
cacheChats = null;
// 持久化消息
this.saveToStorage(true);
},
fliterMessage(chats, maxTotalSize, maxPerChatSize) {
// 每个会话只保留maxPerChatSize条消息
let remainTotalSize = 0;
chats.forEach(chat => {
if (chat.messages.length > maxPerChatSize) {
let idx = chat.messages.length - maxPerChatSize;
chat.messages = chat.messages.slice(idx);
}
remainTotalSize += chat.messages.length;
})
// 保证消息总数不超过maxTotalSize条,否则继续清理
if (remainTotalSize > maxTotalSize) {
this.fliterMessage(chats, maxTotalSize, maxPerChatSize / 2);
}
},
saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿
if (this.loading) {

75
im-web/src/view/Login.vue

@ -1,5 +1,8 @@
<template>
<div class="login-view">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="decoration decoration-3"></div>
<div class="content">
<el-form class="form" :model="loginForm" status-icon :rules="rules" ref="loginForm"
@keyup.enter.native="submitForm('loginForm')">
@ -121,10 +124,67 @@ export default {
.login-view {
width: 100%;
height: 100%;
background: #E8F2FF;
background-size: cover;
box-sizing: border-box;
background: linear-gradient(135deg,
#87adeb 0%,
#8287ec 25%,
#87adeb 50%,
#898ee3 75%,
#87adeb 100%);
/* 装饰性元素 */
.decoration {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
}
.decoration-1 {
width: 450px;
height: 450px;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.3) 0%, rgba(200, 220, 240, 0.4) 100%);
top: -250px;
right: -100px;
animation: float 16s infinite ease-in-out;
}
.decoration-2 {
width: 400px;
height: 400px;
background: linear-gradient(135deg, rgba(200, 220, 240, 0.4) 0%, rgba(255, 255, 255, 0.3) 100%);
bottom: -200px;
left: -100px;
animation: float 12s infinite ease-in-out;
}
.decoration-3 {
width: 300px;
height: 300px;
background: linear-gradient(45deg, rgba(161, 196, 253, 0.3) 0%, rgba(194, 233, 251, 0.4) 100%);
top: 50%;
right: 50px;
animation: float 8s infinite ease-in-out;
}
@keyframes float {
0%,
100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-60px) translateX(30px);
}
50% {
transform: translateY(30px) translateX(-45px);
}
75% {
transform: translateY(-30px) translateX(-30px);
}
}
.content {
position: relative;
@ -134,15 +194,14 @@ export default {
padding: 10%;
.form {
height: 340px;
width: 400px;
width: 360px;
height: 380px;
padding: 30px;
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(8px);
border-radius: 3%;
overflow: hidden;
border: 1px solid #ccc;
.title {
display: flex;

133
im-web/src/view/Register.vue

@ -1,37 +1,37 @@
<template>
<el-container class="register-view">
<div>
<el-form :model="registerForm" status-icon :rules="rules" ref="registerForm" label-width="80px"
class="content">
<div class="title">
<img class="logo" src="../../public/logo.png" />
<div>欢迎成为盒子IM的用户</div>
</div>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="registerForm.userName" autocomplete="off" placeholder="用户名(登录使用)"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickName">
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off" placeholder="昵称"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password" autocomplete="off" placeholder="密码"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off"
placeholder="确认密码" maxlength="20"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
<el-button @click="resetForm('registerForm')">清空</el-button>
</el-form-item>
<div class="to-login">
<router-link to="/login">已有账号,前往登录</router-link>
</div>
</el-form>
</div>
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="decoration decoration-3"></div>
<el-form :model="registerForm" status-icon :rules="rules" ref="registerForm" label-width="80px" class="content">
<div class="title">
<img class="logo" src="../../public/logo.png" />
<div>欢迎成为盒子IM的用户</div>
</div>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="registerForm.userName" autocomplete="off" placeholder="用户名(登录使用)"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickName">
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off" placeholder="昵称"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password" autocomplete="off" placeholder="密码"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off" placeholder="确认密码"
maxlength="20"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
<el-button @click="resetForm('registerForm')">清空</el-button>
</el-form-item>
<div class="to-login">
<router-link to="/login">已有账号,前往登录</router-link>
</div>
</el-form>
<icp></icp>
</el-container>
</template>
@ -131,18 +131,77 @@ export default {
align-items: center;
width: 100%;
height: 100%;
background: rgb(232, 242, 255);
background: linear-gradient(135deg,
#87adeb 0%,
#8287ec 25%,
#87adeb 50%,
#898ee3 75%,
#87adeb 100%);
/* 装饰性元素 */
.decoration {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
}
.decoration-1 {
width: 450px;
height: 450px;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.3) 0%, rgba(200, 220, 240, 0.4) 100%);
top: -250px;
right: -100px;
animation: float 16s infinite ease-in-out;
}
.decoration-2 {
width: 400px;
height: 400px;
background: linear-gradient(135deg, rgba(200, 220, 240, 0.4) 0%, rgba(255, 255, 255, 0.3) 100%);
bottom: -200px;
left: -100px;
animation: float 12s infinite ease-in-out;
}
.decoration-3 {
width: 300px;
height: 300px;
background: linear-gradient(45deg, rgba(161, 196, 253, 0.3) 0%, rgba(194, 233, 251, 0.5) 100%);
top: 50%;
right: 50px;
animation: float 8s infinite ease-in-out;
}
@keyframes float {
0%,
100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-60px) translateX(30px);
}
50% {
transform: translateY(30px) translateX(-45px);
}
75% {
transform: translateY(-30px) translateX(-30px);
}
}
.content {
width: 500px;
width: 400px;
height: 450px;
padding: 20px;
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
border-radius: 3px;
overflow: hidden;
border-radius: 3%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(8px);
.title {
display: flex;

Loading…
Cancel
Save