fix(im): 强化好友关系、消息历史和前端交互
- 校验群资料字段长度,并在同意好友申请时复验双方用户 - 仅向双向有效好友推送资料更新通知 - WebSocket 推送收件人去重,并忽略空用户编号 - 群聊和私聊历史保留撤回消息记录 - 校验群通话排除发起人后仍需存在被邀请人 - 统一 IM 前端接口参数传递方式 - 抽取全局 URL 安全打开工具,并复用到消息预览 - 防止好友申请同意和拒绝按钮重复操作 - 补充好友、消息、RTC、WebSocket 相关测试im
parent
2ede2b371f
commit
309a4bf4d0
|
|
@ -14,5 +14,5 @@ export interface ImChannelMaterialRespVO {
|
|||
|
||||
// 获取频道素材详情;用于客户端点击图文卡片渲染详情页
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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>
|
||||
<!-- 别人加我 + 等待中:同意 / 拒绝 -->
|
||||
<template v-if="!iSentIt && request.handleResult === ImFriendRequestHandleResult.UNHANDLED">
|
||||
<el-button @click="handleRefuse" :loading="refusing">拒绝</el-button>
|
||||
<el-button type="primary" @click="handleAgree" :loading="agreeing"> 同意 </el-button>
|
||||
<el-button @click="handleRefuse" :loading="refusing" :disabled="processing">拒绝</el-button>
|
||||
<el-button type="primary" @click="handleAgree" :loading="agreeing" :disabled="processing">
|
||||
同意
|
||||
</el-button>
|
||||
</template>
|
||||
<!-- 已拒绝:占位禁用按钮 -->
|
||||
<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 { useChannelStore } from '@/views/im/home/store/channelStore'
|
||||
import { ImConversationType } from '@/views/im/utils/constants'
|
||||
import { openSafeUrl } from '@/utils/url'
|
||||
|
||||
const props = defineProps<{
|
||||
content: string
|
||||
|
|
@ -110,8 +111,7 @@ const detailHtml = ref('')
|
|||
/** 点击行为:url 非空跳外链;为空则按 payload.materialId 拉富文本正文,全屏 dialog 渲染 */
|
||||
const onClick = async () => {
|
||||
if (payload.value.url) {
|
||||
// 外链强制 noopener,noreferrer,阻断目标页面 window.opener 篡改 / referrer 泄露
|
||||
window.open(payload.value.url, '_blank', 'noopener,noreferrer')
|
||||
openSafeUrl(payload.value.url)
|
||||
return
|
||||
}
|
||||
if (!payload.value.materialId) {
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ import {
|
|||
type VideoMessage
|
||||
} from '@/views/im/utils/message'
|
||||
import { summarizeMessageContent } from '@/views/im/utils/conversation'
|
||||
import { openSafeUrl } from '@/utils/url'
|
||||
import CardBubble from '@/views/im/home/components/card/CardBubble.vue'
|
||||
import TipSegments from './TipSegments.vue'
|
||||
import { useVoicePlayer } from '@/views/im/home/composables/useVoicePlayer'
|
||||
|
|
@ -322,8 +323,7 @@ function handleFileClick() {
|
|||
if (isUploading.value || !filePayload.value?.url) {
|
||||
return
|
||||
}
|
||||
// noopener,noreferrer 切断新窗口对原页面的 window.opener 引用,防 Tabnabbing
|
||||
window.open(filePayload.value.url, '_blank', 'noopener,noreferrer')
|
||||
openSafeUrl(filePayload.value.url)
|
||||
}
|
||||
|
||||
/** 语音点击:托管给 useVoicePlayer 全局互斥播放,新点的语音会停掉旧的 */
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ import {
|
|||
type MergeMessage
|
||||
} from '@/views/im/utils/message'
|
||||
import { buildFacePreviewText, summarizeMessageContent } from '@/views/im/utils/conversation'
|
||||
import { openSafeUrl } from '@/utils/url'
|
||||
|
||||
defineOptions({ name: 'ImMessageContentPreview' })
|
||||
|
||||
|
|
@ -227,8 +228,7 @@ const mergePreviewLines = computed(() => {
|
|||
function openVideo() {
|
||||
const url = videoPayload.value?.url
|
||||
if (url) {
|
||||
// noopener,noreferrer 切断新窗口对原页面的 window.opener 引用,防 Tabnabbing
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
openSafeUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue