Browse Source

!154 消息转义优化

Merge pull request !154 from blue/v_3.0.0
master
blue 9 months ago
committed by Gitee
parent
commit
8f2b1af205
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 11
      im-client/src/main/java/com/bx/imclient/IMClient.java
  2. 5
      im-client/src/main/java/com/bx/imclient/sender/IMSender.java
  3. 49
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  4. 5
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  5. 14
      im-uniapp/common/str.js
  6. 14
      im-uniapp/common/url.js
  7. 13
      im-uniapp/components/chat-item/chat-item.vue
  8. 17
      im-uniapp/components/chat-message-item/chat-message-item.vue
  9. 2
      im-uniapp/main.js
  10. 20
      im-uniapp/pages/chat/chat-box.vue
  11. 14
      im-web/src/api/str.js
  12. 5
      im-web/src/components/chat/ChatBox.vue
  13. 12
      im-web/src/components/chat/ChatInput.vue
  14. 2
      im-web/src/components/chat/ChatItem.vue
  15. 3
      im-web/src/components/chat/ChatMessageItem.vue
  16. 2
      im-web/src/main.js

11
im-client/src/main/java/com/bx/imclient/IMClient.java

@ -26,6 +26,17 @@ public class IMClient {
return imSender.isOnline(userId);
}
/**
* 判断用户是否在线
*
* @param userId 用户id
* @param terminal 终端可惜
*/
public Boolean isOnline(Long userId,IMTerminalType terminal){
return imSender.isOnline(userId,terminal);
}
/**
* 判断多个用户是否在线
*

5
im-client/src/main/java/com/bx/imclient/sender/IMSender.java

@ -246,6 +246,11 @@ public class IMSender {
return onlineMap;
}
public Boolean isOnline(Long userId, IMTerminalType terminal) {
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), terminal.code().toString());
return redisMQTemplate.hasKey(key);
}
public Boolean isOnline(Long userId) {
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), "*");
return !Objects.requireNonNull(redisMQTemplate.keys(key)).isEmpty();

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

@ -158,13 +158,17 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
this.sendLoadingMessage(false, session);
return;
}
// 只能拉取最近3个月的,移动端只拉最近一个月
int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3;
Date minDate = DateUtils.addMonths(new Date(), -months);
// 只拉最近一个月
Date minDate = DateUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId).gt(GroupMessage::getSendTime, minDate)
.in(GroupMessage::getGroupId, groupIds).orderByAsc(GroupMessage::getId);
wrapper.gt(GroupMessage::getId, minId);
wrapper.gt(GroupMessage::getSendTime, minDate);
wrapper.in(GroupMessage::getGroupId, groupIds);
wrapper.orderByDesc(GroupMessage::getId);
if (minId <= 0) {
// 首次拉取限制消息数量大小,防止内存溢出
wrapper.last("limit 100000");
}
List<GroupMessage> messages = this.list(wrapper);
// 通过群聊对消息进行分组
Map<Long, List<GroupMessage>> messageGroupMap =
@ -173,9 +177,11 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
List<GroupMember> quitMembers = groupMemberService.findQuitInMonth(session.getUserId());
for (GroupMember quitMember : quitMembers) {
wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId).between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime())
.eq(GroupMessage::getGroupId, quitMember.getGroupId())
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByAsc(GroupMessage::getId);
wrapper.gt(GroupMessage::getId, minId);
wrapper.between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime());
wrapper.eq(GroupMessage::getGroupId, quitMember.getGroupId());
wrapper.ne(GroupMessage::getStatus, MessageStatus.RECALL.code());
wrapper.orderByDesc(GroupMessage::getId);
List<GroupMessage> groupMessages = this.list(wrapper);
messageGroupMap.put(quitMember.getGroupId(), groupMessages);
groupMemberMap.put(quitMember.getGroupId(), quitMember);
@ -184,19 +190,28 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 开启加载中标志
this.sendLoadingMessage(true, session);
// 推送消息
AtomicInteger sendCount = new AtomicInteger();
messageGroupMap.forEach((groupId, groupMessages) -> {
// 第一次拉取时,一个群最多推送1w条消息,防止前端接收能力溢出导致卡顿
int sendCount = 0;
for (Map.Entry<Long, List<GroupMessage>> entry : messageGroupMap.entrySet()) {
Long groupId = entry.getKey();
List<GroupMessage> groupMessages = entry.getValue();
// 第一次拉取时,一个群最多推送3000条消息,防止前端接收能力溢出导致卡顿
List<GroupMessage> sendMessages = groupMessages;
if (minId <= 0 && groupMessages.size() > 10000) {
sendMessages = groupMessages.subList(groupMessages.size() - 10000, groupMessages.size());
if (minId <= 0 && groupMessages.size() > 3000) {
sendMessages = groupMessages.subList(0, 3000);
}
// id从小到大排序
CollectionUtil.reverse(sendMessages);
// 填充消息状态
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString());
long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString());
Map<Object, Object> maxIdMap = null;
for (GroupMessage m : sendMessages) {
// 推送过程如果用户下线了,则不再推送
if (!imClient.isOnline(session.getUserId(), IMTerminalType.fromCode(session.getTerminal()))) {
log.info("用户已下线,停止推送离线群聊消息,用户id:{}", session.getUserId());
return;
}
// 排除加群之前的消息
GroupMember member = groupMemberMap.get(m.getGroupId());
if (DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0) {
@ -231,12 +246,12 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
sendMessage.setSendToSelf(false);
sendMessage.setData(vo);
imClient.sendGroupMessage(sendMessage);
sendCount.getAndIncrement();
sendCount++;
}
}
});
// 关闭加载中标志
this.sendLoadingMessage(false, session);
log.info("拉取离线群聊消息,用户id:{},数量:{}", session.getUserId(), sendCount.get());
log.info("拉取离线群聊消息,用户id:{},数量:{}", session.getUserId(), sendCount++);
});
}

5
im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java

@ -154,6 +154,11 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
// 开启加载中标志
this.sendLoadingMessage(true, session);
for (PrivateMessage m : messages) {
// 推送过程如果用户下线了,则不再推送
if (!imClient.isOnline(session.getUserId(), IMTerminalType.fromCode(session.getTerminal()))) {
log.info("用户已下线,停止推送离线私聊消息,用户id:{}", session.getUserId());
return;
}
PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code()));

14
im-uniapp/common/str.js

@ -0,0 +1,14 @@
let html2Escape = (strText) => {
return strText.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
} [c];
});
}
export default {
html2Escape
}

14
im-uniapp/common/url.js

@ -1,7 +1,14 @@
let replaceURLWithHTMLLinks = (content, color) => {
// 使用正则表达式匹配更广泛的URL格式(此正则由deepseek生成)
const urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]|\bwww\.[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
return content.replace(urlRegex, (url) => {
const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]|\bwww\.[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
let containUrl = (content) => {
return regex.test(content)
}
let replaceURLWithHTMLLinks = (content, color) => {
return content.replace(regex, (url) => {
// 如果URL不以http(s)://开头,则添加http://前缀
if (!url.startsWith("http")) {
url = "http://" + url;
@ -11,5 +18,6 @@ let replaceURLWithHTMLLinks = (content, color) => {
}
export default {
containUrl,
replaceURLWithHTMLLinks
}

13
im-uniapp/components/chat-item/chat-item.vue

@ -15,8 +15,8 @@
<view class="chat-content">
<view class="chat-at-text">{{ atText }}</view>
<view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view>
<rich-text class="chat-content-text"
:nodes="$emo.transform(chat.lastContent,'emoji-small')"></rich-text>
<view v-if="!isTextMessage" class="chat-content-text">{{chat.lastContent}}</view>
<rich-text v-else class="chat-content-text" :nodes="nodesText"></rich-text>
<view v-if="chat.isDnd" class="icon iconfont icon-dnd"></view>
<uni-badge v-else-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
</view>
@ -77,6 +77,15 @@ export default {
return "[@全体成员]"
}
return "";
},
isTextMessage() {
let idx = this.chat.messages.length - 1;
let messageType = this.chat.messages[idx].type;
return messageType == this.$enums.MESSAGE_TYPE.TEXT;
},
nodesText() {
let text = this.$str.html2Escape(this.chat.lastContent);
return this.$emo.transform(text, 'emoji-small')
}
}
}

17
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -16,11 +16,11 @@
<view class="bottom">
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<!-- rich-text支持显示表情但是不支持点击a标签 -->
<rich-text v-if="$emo.containEmoji(msgInfo.content)" class="message-text"
:nodes="nodesText"></rich-text>
<!-- up-parse支持点击a标签,但安卓打包后表情无法显示,原因未知 -->
<up-parse v-else class="message-text" :showImgMenu="false" :content="nodesText"></up-parse>
<!-- up-parse支持点击a标签,但是不支持显示emo表情也不支持换行 -->
<up-parse v-if="$url.containUrl(msgInfo.content)&&!$emo.containEmoji(msgInfo.content)"
class="message-text" :showImgMenu="false" :content="nodesText"></up-parse>
<!-- rich-text支持显示emo表情以及消息换行但是不支持点击a标签 -->
<rich-text v-else class="message-text" :nodes="nodesText"></rich-text>
</long-press-menu>
</view>
<view class="message-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
@ -39,8 +39,8 @@
<long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="file-box">
<view class="file-info">
<uni-link class="file-name" :text="data.name" showUnderLine="true"
color="#007BFF" :href="data.url"></uni-link>
<uni-link class="file-name" :text="data.name" showUnderLine="true" color="#007BFF"
:href="data.url"></uni-link>
<view class="file-size">{{ fileSize }}</view>
</view>
<view class="file-icon iconfont icon-file"></view>
@ -236,7 +236,8 @@ export default {
},
nodesText() {
let color = this.msgInfo.selfSend ? 'white' : '';
let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color)
let text = this.$str.html2Escape(this.msgInfo.content)
text = this.$url.replaceURLWithHTMLLinks(text, color)
return this.$emo.transform(text, 'emoji-normal')
}
}

2
im-uniapp/main.js

@ -2,6 +2,7 @@ import App from './App'
import request from './common/request';
import emotion from './common/emotion.js';
import url from './common/url.js';
import str from './common/str.js';
import * as enums from './common/enums.js';
import * as date from './common/date';
import * as socketApi from './common/wssocket';
@ -45,6 +46,7 @@ export function createApp() {
app.config.globalProperties.$msgType = messageType;
app.config.globalProperties.$emo = emotion;
app.config.globalProperties.$url = url;
app.config.globalProperties.$str = str;
app.config.globalProperties.$enums = enums;
app.config.globalProperties.$date = date;
app.config.globalProperties.$rc = recorder;

20
im-uniapp/pages/chat/chat-box.vue

@ -288,7 +288,9 @@ export default {
sendText += op.insert
)
})
if (!sendText.trim() && this.atUserIds.length == 0) {
//
sendText = sendText.trim();
if (!sendText && this.atUserIds.length == 0) {
return uni.showToast({
title: "不能发送空白信息",
icon: "none"
@ -297,7 +299,7 @@ export default {
let receiptText = this.isReceipt ? "【回执消息】" : "";
let atText = this.createAtText();
let msgInfo = {
content: receiptText + this.html2Escape(sendText) + atText,
content: receiptText + sendText + atText,
atUserIds: this.atUserIds,
receipt: this.isReceipt,
type: 0
@ -307,9 +309,11 @@ export default {
this.isReceipt = false;
// id
this.fillTargetId(msgInfo, this.chat.targetId);
//
const chat = this.chat;
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.chatStore.insertMessage(m, this.chat);
this.chatStore.insertMessage(m, chat);
//
this.moveChatToTop();
}).finally(() => {
@ -731,16 +735,6 @@ export default {
let px = info.windowWidth * rpx / 750;
return Math.floor(rpx);
},
html2Escape(strHtml) {
return strHtml.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
} [c];
});
},
sendMessageRequest(msgInfo) {
return new Promise((resolve, reject) => {
// ""

14
im-web/src/api/str.js

@ -0,0 +1,14 @@
let html2Escape = (strText) => {
return strText.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
} [c];
});
}
export default {
html2Escape
}

5
im-web/src/components/chat/ChatBox.vue

@ -456,13 +456,12 @@ export default {
msgInfo.receipt = this.isReceipt;
}
this.lockMessage = true;
const chat = this.chat;
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.chatStore.insertMessage(m, this.chat);
//
this.chatStore.insertMessage(m, chat);
this.moveChatToTop();
}).finally(() => {
//
this.scrollToBottom();
this.isReceipt = false;
resolve();

12
im-web/src/components/chat/ChatInput.vue

@ -367,16 +367,6 @@ export default {
//
this.updateRange();
},
html2Escape(strHtml) {
return strHtml.replace(/[<>&"]/g, function(c) {
return {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;'
} [c];
});
},
submit() {
let nodes = this.$refs.content.childNodes;
let fullList = [];
@ -389,7 +379,7 @@ export default {
continue;
}
if (node.nodeType === 3) {
tempText += this.html2Escape(node.textContent);
tempText += node.textContent;
continue;
}
let nodeName = node.nodeName.toLowerCase();

2
im-web/src/components/chat/ChatItem.vue

@ -16,7 +16,7 @@
<div class="chat-content">
<div class="chat-at-text">{{ atText }}</div>
<div class="chat-send-name" v-show="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</div>
<div class="chat-content-text" v-html="$emo.transform(chat.lastContent, 'emoji-small')"></div>
<div class="chat-content-text" v-html="$emo.transform($str.html2Escape(chat.lastContent), 'emoji-small')"></div>
<div class="icon iconfont icon-dnd" v-if="chat.isDnd"></div>
</div>
</div>

3
im-web/src/components/chat/ChatMessageItem.vue

@ -193,7 +193,8 @@ export default {
},
htmlText() {
let color = this.msgInfo.selfSend ? 'white' : '';
let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color)
let text = this.$str.html2Escape(this.msgInfo.content)
text = this.$url.replaceURLWithHTMLLinks(text, color)
return this.$emo.transform(text, 'emoji-normal')
}
}

2
im-web/src/main.js

@ -10,6 +10,7 @@ import * as socketApi from './api/wssocket';
import * as messageType from './api/messageType';
import emotion from './api/emotion.js';
import url from './api/url.js';
import str from './api/str.js';
import element from './api/element.js';
import * as enums from './api/enums.js';
import * as date from './api/date.js';
@ -31,6 +32,7 @@ Vue.prototype.$date = date;
Vue.prototype.$http = httpRequest // http请求方法
Vue.prototype.$emo = emotion; // emo表情
Vue.prototype.$url = url; // url转换
Vue.prototype.$str = str; // 字符串相关
Vue.prototype.$elm = element; // 元素操作
Vue.prototype.$enums = enums; // 枚举
Vue.prototype.$eventBus = new Vue(); // 全局事件

Loading…
Cancel
Save