fix(im): 强化好友关系、消息历史和前端交互
- 校验群资料字段长度,并在同意好友申请时复验双方用户 - 仅向双向有效好友推送资料更新通知 - WebSocket 推送收件人去重,并忽略空用户编号 - 群聊和私聊历史保留撤回消息记录 - 校验群通话排除发起人后仍需存在被邀请人 - 统一 IM 前端接口参数传递方式 - 抽取全局 URL 安全打开工具,并复用到消息预览 - 防止好友申请同意和拒绝按钮重复操作 - 补充好友、消息、RTC、WebSocket 相关测试im
parent
2ede2b371f
commit
309a4bf4d0
|
|
@ -14,5 +14,5 @@ export interface ImChannelMaterialRespVO {
|
||||||
|
|
||||||
// 获取频道素材详情;用于客户端点击图文卡片渲染详情页
|
// 获取频道素材详情;用于客户端点击图文卡片渲染详情页
|
||||||
export const getChannelMaterial = (id: number) => {
|
export const getChannelMaterial = (id: number) => {
|
||||||
return request.get<ImChannelMaterialRespVO>({ url: '/im/channel/material/get?id=' + id })
|
return request.get<ImChannelMaterialRespVO>({ url: '/im/channel/material/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,5 @@ export const createFaceUserItem = (data: ImFaceUserItemSaveReqVO) => {
|
||||||
|
|
||||||
// 删除个人表情
|
// 删除个人表情
|
||||||
export const deleteFaceUserItem = (id: number) => {
|
export const deleteFaceUserItem = (id: number) => {
|
||||||
return request.delete({ url: '/im/face-user-item/delete?id=' + id })
|
return request.delete({ url: '/im/face-user-item/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const getManagerChannelPage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得频道详情
|
// 获得频道详情
|
||||||
export const getManagerChannel = (id: number) => {
|
export const getManagerChannel = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/channel/get?id=' + id })
|
return request.get({ url: '/im/manager/channel/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增频道
|
// 新增频道
|
||||||
|
|
@ -32,7 +32,7 @@ export const updateManagerChannel = (data: ImManagerChannelVO) => {
|
||||||
|
|
||||||
// 删除频道
|
// 删除频道
|
||||||
export const deleteManagerChannel = (id: number) => {
|
export const deleteManagerChannel = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/channel/delete?id=' + id })
|
return request.delete({ url: '/im/manager/channel/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得启用的频道精简列表(表单选择用)
|
// 获得启用的频道精简列表(表单选择用)
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,14 @@ export const getManagerChannelMaterialPage = (params: PageParam) => {
|
||||||
// 获得指定频道下的素材精简列表
|
// 获得指定频道下的素材精简列表
|
||||||
export const getSimpleManagerChannelMaterialList = (channelId: number) => {
|
export const getSimpleManagerChannelMaterialList = (channelId: number) => {
|
||||||
return request.get({
|
return request.get({
|
||||||
url: '/im/manager/channel-material/simple-list?channelId=' + channelId
|
url: '/im/manager/channel-material/simple-list',
|
||||||
|
params: { channelId }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得素材详情
|
// 获得素材详情
|
||||||
export const getManagerChannelMaterial = (id: number) => {
|
export const getManagerChannelMaterial = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/channel-material/get?id=' + id })
|
return request.get({ url: '/im/manager/channel-material/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增素材
|
// 新增素材
|
||||||
|
|
@ -42,5 +43,5 @@ export const updateManagerChannelMaterial = (data: ImManagerChannelMaterialVO) =
|
||||||
|
|
||||||
// 删除素材
|
// 删除素材
|
||||||
export const deleteManagerChannelMaterial = (id: number) => {
|
export const deleteManagerChannelMaterial = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/channel-material/delete?id=' + id })
|
return request.delete({ url: '/im/manager/channel-material/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const sendManagerChannelMessage = (data: ImManagerChannelMessageSendReqVO
|
||||||
|
|
||||||
// 删除频道消息
|
// 删除频道消息
|
||||||
export const deleteManagerChannelMessage = (id: number) => {
|
export const deleteManagerChannelMessage = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/channel-message/delete?id=' + id })
|
return request.delete({ url: '/im/manager/channel-message/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得频道消息分页
|
// 获得频道消息分页
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const getManagerFacePackItemPage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得表情详情
|
// 获得表情详情
|
||||||
export const getManagerFacePackItem = (id: number) => {
|
export const getManagerFacePackItem = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/face-pack-item/get?id=' + id })
|
return request.get({ url: '/im/manager/face-pack-item/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增表情
|
// 新增表情
|
||||||
|
|
@ -34,7 +34,7 @@ export const updateManagerFacePackItem = (data: ImManagerFacePackItemVO) => {
|
||||||
|
|
||||||
// 删除表情
|
// 删除表情
|
||||||
export const deleteManagerFacePackItem = (id: number) => {
|
export const deleteManagerFacePackItem = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/face-pack-item/delete?id=' + id })
|
return request.delete({ url: '/im/manager/face-pack-item/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量删除表情
|
// 批量删除表情
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const getManagerFacePackPage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得表情包详情
|
// 获得表情包详情
|
||||||
export const getManagerFacePack = (id: number) => {
|
export const getManagerFacePack = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/face-pack/get?id=' + id })
|
return request.get({ url: '/im/manager/face-pack/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增表情包
|
// 新增表情包
|
||||||
|
|
@ -31,7 +31,7 @@ export const updateManagerFacePack = (data: ImManagerFacePackVO) => {
|
||||||
|
|
||||||
// 删除表情包
|
// 删除表情包
|
||||||
export const deleteManagerFacePack = (id: number) => {
|
export const deleteManagerFacePack = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/face-pack/delete?id=' + id })
|
return request.delete({ url: '/im/manager/face-pack/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量删除表情包
|
// 批量删除表情包
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,5 @@ export const getManagerFaceUserItemPage = (params: PageParam) => {
|
||||||
|
|
||||||
// 删除用户表情
|
// 删除用户表情
|
||||||
export const deleteManagerFaceUserItem = (id: number) => {
|
export const deleteManagerFaceUserItem = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/face-user-item/delete?id=' + id })
|
return request.delete({ url: '/im/manager/face-user-item/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export const getManagerGroupPage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得群详情
|
// 获得群详情
|
||||||
export const getManagerGroup = (id: number) => {
|
export const getManagerGroup = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/group/get?id=' + id })
|
return request.get({ url: '/im/manager/group/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封禁群
|
// 封禁群
|
||||||
|
|
@ -48,15 +48,15 @@ export const banManagerGroup = (data: { id: number; reason: string }) => {
|
||||||
|
|
||||||
// 解封群
|
// 解封群
|
||||||
export const unbanManagerGroup = (id: number) => {
|
export const unbanManagerGroup = (id: number) => {
|
||||||
return request.put({ url: '/im/manager/group/unban?id=' + id })
|
return request.put({ url: '/im/manager/group/unban', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解散群
|
// 解散群
|
||||||
export const dissolveManagerGroup = (id: number) => {
|
export const dissolveManagerGroup = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/group/dissolve?id=' + id })
|
return request.delete({ url: '/im/manager/group/dissolve', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得群成员列表(含已退群成员,由前端按需过滤)
|
// 获得群成员列表(含已退群成员,由前端按需过滤)
|
||||||
export const getManagerGroupMemberList = (groupId: number) => {
|
export const getManagerGroupMemberList = (groupId: number) => {
|
||||||
return request.get({ url: '/im/manager/group/member/list?groupId=' + groupId })
|
return request.get({ url: '/im/manager/group/member/list', params: { groupId } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,5 @@ export const getManagerGroupMessagePage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得群聊消息详情
|
// 获得群聊消息详情
|
||||||
export const getManagerGroupMessage = (id: number) => {
|
export const getManagerGroupMessage = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/message/group/get?id=' + id })
|
return request.get({ url: '/im/manager/message/group/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,5 @@ export const getManagerPrivateMessagePage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得私聊消息详情
|
// 获得私聊消息详情
|
||||||
export const getManagerPrivateMessage = (id: number) => {
|
export const getManagerPrivateMessage = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/message/private/get?id=' + id })
|
return request.get({ url: '/im/manager/message/private/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,5 @@ export const getManagerRtcCallPage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得通话参与者列表
|
// 获得通话参与者列表
|
||||||
export const getManagerRtcCallParticipantList = (id: number) => {
|
export const getManagerRtcCallParticipantList = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/rtc/participant-list?id=' + id })
|
return request.get({ url: '/im/manager/rtc/participant-list', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const getManagerSensitiveWordPage = (params: PageParam) => {
|
||||||
|
|
||||||
// 获得敏感词详情
|
// 获得敏感词详情
|
||||||
export const getManagerSensitiveWord = (id: number) => {
|
export const getManagerSensitiveWord = (id: number) => {
|
||||||
return request.get({ url: '/im/manager/sensitive-word/get?id=' + id })
|
return request.get({ url: '/im/manager/sensitive-word/get', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增敏感词
|
// 新增敏感词
|
||||||
|
|
@ -31,7 +31,7 @@ export const updateManagerSensitiveWord = (data: ImManagerSensitiveWordVO) => {
|
||||||
|
|
||||||
// 删除敏感词
|
// 删除敏感词
|
||||||
export const deleteManagerSensitiveWord = (id: number) => {
|
export const deleteManagerSensitiveWord = (id: number) => {
|
||||||
return request.delete({ url: '/im/manager/sensitive-word/delete?id=' + id })
|
return request.delete({ url: '/im/manager/sensitive-word/delete', params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量删除敏感词
|
// 批量删除敏感词
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
const OPENABLE_PROTOCOLS = new Set(['http:', 'https:', 'blob:'])
|
||||||
|
|
||||||
|
/** 判断 URL 是否允许在新窗口打开 */
|
||||||
|
export function isOpenableUrl(url?: string | null): boolean {
|
||||||
|
if (!url) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url, window.location.origin)
|
||||||
|
return OPENABLE_PROTOCOLS.has(parsed.protocol)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 安全打开 URL */
|
||||||
|
export function openSafeUrl(url?: string | null): void {
|
||||||
|
if (!url || !isOpenableUrl(url)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
|
@ -69,8 +69,10 @@
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- 别人加我 + 等待中:同意 / 拒绝 -->
|
<!-- 别人加我 + 等待中:同意 / 拒绝 -->
|
||||||
<template v-if="!iSentIt && request.handleResult === ImFriendRequestHandleResult.UNHANDLED">
|
<template v-if="!iSentIt && request.handleResult === ImFriendRequestHandleResult.UNHANDLED">
|
||||||
<el-button @click="handleRefuse" :loading="refusing">拒绝</el-button>
|
<el-button @click="handleRefuse" :loading="refusing" :disabled="processing">拒绝</el-button>
|
||||||
<el-button type="primary" @click="handleAgree" :loading="agreeing"> 同意 </el-button>
|
<el-button type="primary" @click="handleAgree" :loading="agreeing" :disabled="processing">
|
||||||
|
同意
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<!-- 已拒绝:占位禁用按钮 -->
|
<!-- 已拒绝:占位禁用按钮 -->
|
||||||
<el-button v-if="refused" disabled>已拒绝</el-button>
|
<el-button v-if="refused" disabled>已拒绝</el-button>
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ import { parseMessage, type MaterialMessage } from '@/views/im/utils/message'
|
||||||
import { useConversationStore } from '@/views/im/home/store/conversationStore'
|
import { useConversationStore } from '@/views/im/home/store/conversationStore'
|
||||||
import { useChannelStore } from '@/views/im/home/store/channelStore'
|
import { useChannelStore } from '@/views/im/home/store/channelStore'
|
||||||
import { ImConversationType } from '@/views/im/utils/constants'
|
import { ImConversationType } from '@/views/im/utils/constants'
|
||||||
|
import { openSafeUrl } from '@/utils/url'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
content: string
|
content: string
|
||||||
|
|
@ -110,8 +111,7 @@ const detailHtml = ref('')
|
||||||
/** 点击行为:url 非空跳外链;为空则按 payload.materialId 拉富文本正文,全屏 dialog 渲染 */
|
/** 点击行为:url 非空跳外链;为空则按 payload.materialId 拉富文本正文,全屏 dialog 渲染 */
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
if (payload.value.url) {
|
if (payload.value.url) {
|
||||||
// 外链强制 noopener,noreferrer,阻断目标页面 window.opener 篡改 / referrer 泄露
|
openSafeUrl(payload.value.url)
|
||||||
window.open(payload.value.url, '_blank', 'noopener,noreferrer')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!payload.value.materialId) {
|
if (!payload.value.materialId) {
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,7 @@ import {
|
||||||
type VideoMessage
|
type VideoMessage
|
||||||
} from '@/views/im/utils/message'
|
} from '@/views/im/utils/message'
|
||||||
import { summarizeMessageContent } from '@/views/im/utils/conversation'
|
import { summarizeMessageContent } from '@/views/im/utils/conversation'
|
||||||
|
import { openSafeUrl } from '@/utils/url'
|
||||||
import CardBubble from '@/views/im/home/components/card/CardBubble.vue'
|
import CardBubble from '@/views/im/home/components/card/CardBubble.vue'
|
||||||
import TipSegments from './TipSegments.vue'
|
import TipSegments from './TipSegments.vue'
|
||||||
import { useVoicePlayer } from '@/views/im/home/composables/useVoicePlayer'
|
import { useVoicePlayer } from '@/views/im/home/composables/useVoicePlayer'
|
||||||
|
|
@ -322,8 +323,7 @@ function handleFileClick() {
|
||||||
if (isUploading.value || !filePayload.value?.url) {
|
if (isUploading.value || !filePayload.value?.url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// noopener,noreferrer 切断新窗口对原页面的 window.opener 引用,防 Tabnabbing
|
openSafeUrl(filePayload.value.url)
|
||||||
window.open(filePayload.value.url, '_blank', 'noopener,noreferrer')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 语音点击:托管给 useVoicePlayer 全局互斥播放,新点的语音会停掉旧的 */
|
/** 语音点击:托管给 useVoicePlayer 全局互斥播放,新点的语音会停掉旧的 */
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ import {
|
||||||
type MergeMessage
|
type MergeMessage
|
||||||
} from '@/views/im/utils/message'
|
} from '@/views/im/utils/message'
|
||||||
import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation'
|
import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation'
|
||||||
|
import { openSafeUrl } from '@/utils/url'
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageContentPreview' })
|
defineOptions({ name: 'ImMessageContentPreview' })
|
||||||
|
|
||||||
|
|
@ -227,8 +228,7 @@ const mergePreviewLines = computed(() => {
|
||||||
function openVideo() {
|
function openVideo() {
|
||||||
const url = videoPayload.value?.url
|
const url = videoPayload.value?.url
|
||||||
if (url) {
|
if (url) {
|
||||||
// noopener,noreferrer 切断新窗口对原页面的 window.opener 引用,防 Tabnabbing
|
openSafeUrl(url)
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue