From 3b211e25850d9f1f0c9aa195375e76622ce2eefc Mon Sep 17 00:00:00 2001 From: "[yxf]" <[1524240689@qq.com]> Date: Fri, 17 Apr 2026 21:00:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=AF=8F=E4=B8=AA=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E4=B8=80=E4=B8=AA=E9=93=BE=E6=8E=A5=20token=E5=90=8E?= =?UTF-8?q?=E9=9D=A2=E6=8B=BCkefuuid=E4=B8=8E=E5=AE=A2=E6=9C=8D=E5=9B=9E?= =?UTF-8?q?=E7=AD=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/bx/implatform/dto/LoginDTO.java | 3 + .../bx/implatform/dto/PrivateMessageDTO.java | 4 + .../impl/PrivateMessageServiceImpl.java | 82 +++++++- .../service/impl/UserServiceImpl.java | 64 ++++++- im-uniapp/pages/chat/chat-box.vue | 180 ++++++++++++++---- im-uniapp/pages/login/login.vue | 38 ++-- 6 files changed, 306 insertions(+), 65 deletions(-) diff --git a/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java index fd8225f..26262f5 100644 --- a/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java +++ b/im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java @@ -34,4 +34,7 @@ public class LoginDTO { @Schema(description = "token标识") private String uniqueToken; + @Schema(description = "客服ID") + private String keFuId; + } diff --git a/im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java index 639325c..1c73f1e 100644 --- a/im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java +++ b/im-platform/src/main/java/com/bx/implatform/dto/PrivateMessageDTO.java @@ -13,6 +13,9 @@ public class PrivateMessageDTO { @Schema(description = "临时id") private String tmpId; + @Schema(description = "发送用户id(可选,不传则使用当前登录用户)") + private Long sendId; // 新增字段 + @NotNull(message = "接收用户id不可为空") @Schema(description = "接收用户id") private Long recvId; @@ -26,4 +29,5 @@ public class PrivateMessageDTO { @Schema(description = "消息类型 0:文字 1:图片 2:文件 3:语音 4:视频") private Integer type; + } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java index 349e682..0dec307 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java @@ -51,44 +51,106 @@ public class PrivateMessageServiceImpl extends ServiceImpl sendMessage = new IMPrivateMessage<>(); +// sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); +// sendMessage.setRecvId(msgInfo.getRecvId()); +// sendMessage.setSendToSelf(true); +// sendMessage.setData(msgInfo); +// sendMessage.setSendResult(true); +// imClient.sendPrivateMessage(sendMessage); +// log.info("发送私聊消息,发送id:{},接收id:{},内容:{}", session.getUserId(), dto.getRecvId(), dto.getContent()); +// return msgInfo; +// } @Override public PrivateMessageVO sendMessage(PrivateMessageDTO dto) { UserSession session = SessionContext.getSession(); - Long sendUserId = session.getUserId(); + + // 如果传了sendId就用传入的,否则用当前登录用户 + Long sendUserId = dto.getSendId() != null ? dto.getSendId() : session.getUserId(); Long recvUserId = dto.getRecvId(); - Boolean isFriends = friendService.isFriend(session.getUserId(), dto.getRecvId()); + + // 如果不是自己发送(模拟发送),需要验证权限 + if (!sendUserId.equals(session.getUserId())) { + User sendUser = userMapper.selectById(sendUserId); + User currentUser = userMapper.selectById(session.getUserId()); + + // 验证是否属于同一商户 + if (sendUser == null || !sendUser.getUniqueToken().equals(currentUser.getUniqueToken())) { + throw new GlobalException("无权限模拟该用户发送消息"); + } + + log.info("模拟发送消息 - 操作人:{}, 发送者:{}, 接收者:{}", + session.getUserId(), sendUserId, recvUserId); + } + + Boolean isFriends = friendService.isFriend(sendUserId, recvUserId); if (Boolean.FALSE.equals(isFriends)) { throw new GlobalException("您已不是对方好友,无法发送消息"); } + User sendUser = userMapper.selectById(sendUserId); User recvUser = userMapper.selectById(recvUserId); - // 打印发送用户和接收用户信息 - log.info("发送私聊消息 - 发送用户ID: {}, 接收用户ID: {}", - recvUser, sendUser); + + log.info("发送私聊消息 - 发送用户ID: {}, 接收用户ID: {}", sendUserId, recvUserId); if(!recvUser.getUniqueToken().equals(sendUser.getUniqueToken())){ throw new GlobalException("非法客服,发送失败"); } - // 保存消息 + + // 保存消息(后面的代码保持不变) PrivateMessage msg = BeanUtils.copyProperties(dto, PrivateMessage.class); - msg.setSendId(session.getUserId()); + msg.setSendId(sendUserId); // 使用处理后的发送者ID msg.setStatus(MessageStatus.PENDING.code()); msg.setSendTime(new Date()); + // 过滤内容中的敏感词 if (MessageType.TEXT.code().equals(dto.getType())) { msg.setContent(sensitiveFilterUtil.filter(dto.getContent())); } this.save(msg); + // 推送消息 PrivateMessageVO msgInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class); IMPrivateMessage sendMessage = new IMPrivateMessage<>(); - sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setSender(new IMUserInfo(sendUserId, session.getTerminal())); sendMessage.setRecvId(msgInfo.getRecvId()); - sendMessage.setSendToSelf(true); + // 如果是模拟发送,不发送给自己 + sendMessage.setSendToSelf(sendUserId.equals(session.getUserId())); sendMessage.setData(msgInfo); sendMessage.setSendResult(true); imClient.sendPrivateMessage(sendMessage); - log.info("发送私聊消息,发送id:{},接收id:{},内容:{}", session.getUserId(), dto.getRecvId(), dto.getContent()); + + log.info("发送私聊消息,发送id:{},接收id:{},内容:{}", sendUserId, recvUserId, dto.getContent()); return msgInfo; } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java index 4988a09..1faf1a3 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java @@ -95,7 +95,7 @@ public class UserServiceImpl extends ServiceImpl implements Us @Override public LoginVO login(LoginDTO dto) { - log.info("【测试】前端传的uniqueToken:{}", dto.getUniqueToken()); + log.info("【测试】前端传的uniqueToken:{},kefuid:{}", dto.getUniqueToken(), dto.getKeFuId()); String uniqueToken = dto.getUniqueToken(); // 检查uniqueToken是否为空或已过期 @@ -122,7 +122,8 @@ public class UserServiceImpl extends ServiceImpl implements Us // 保存到数据库 this.save(guestUser); - Long customerServiceId = this.getCustomerServiceIdByUniqueToken(dto.getUniqueToken()); +// Long customerServiceId = this.getCustomerServiceIdByUniqueToken(dto); + Long customerServiceId = this.getCustomerServiceId(dto); UserSession guestSession = new UserSession(); guestSession.setUserId(guestUser.getId()); @@ -158,28 +159,71 @@ public class UserServiceImpl extends ServiceImpl implements Us return vo; } + /** + * 获取客服ID + * 优先使用前端传入的kefuid,没有则通过uniqueToken查询 + */ + private Long getCustomerServiceId(LoginDTO dto) { + // 1. 如果前端传了 kefuid,直接使用并验证该客服是否有效 + if (StrUtil.isNotBlank(dto.getKeFuId())) { + try { + Long kefuid = Long.parseLong(dto.getKeFuId()); + + // 验证该客服是否存在且有效 + User customer = this.getById(kefuid); + if (customer != null && customer.getIsCustomer() == 2) { + // 验证该客服的uniqueToken是否与传入的匹配 + if (dto.getUniqueToken().equals(customer.getUniqueToken())) { + return kefuid; + } else { + log.warn("前端传入的客服ID {} 的uniqueToken与当前不匹配,将使用token查询", kefuid); + } + } else { + log.warn("前端传入的客服ID {} 无效或不是客服,将使用token查询", kefuid); + } + } catch (NumberFormatException e) { + log.warn("前端传入的kefuid格式错误:{}", dto.getKeFuId()); + } + } + // 2. 通过uniqueToken查询客服 + return getCustomerServiceIdByUniqueToken(dto.getUniqueToken()); + } + + + /** + * 通过uniqueToken查询客服ID,如果有多条匹配则随机返回一条 + */ public Long getCustomerServiceIdByUniqueToken(String uniqueToken) { // 1. 构建查询条件 LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); queryWrapper.eq(User::getIsCustomer, 2); // 只查客服 - // 2. 有 token 才按 token 匹配 if (StrUtil.isNotBlank(uniqueToken)) { queryWrapper.eq(User::getUniqueToken, uniqueToken); } - // 3. 只查 ID queryWrapper.select(User::getId); - // 4. 查一条(false=查不到不抛异常) - User customer = this.getOne(queryWrapper, false); - if (customer == null) { + // 2. 查询所有匹配的客服 + List customerList = this.list(queryWrapper); + + // 3. 如果没有匹配的客服,抛出异常 + if (customerList == null || customerList.isEmpty()) { throw new GlobalException("未找到对应的客服,请检查 uniqueToken 是否正确"); } - // 5. 有客服返回ID,没有返回null - return customer == null ? null : customer.getId(); - } + // 4. 如果只有一条,直接返回 + if (customerList.size() == 1) { + Long customerId = customerList.get(0).getId(); + return customerId; + } + + // 5. 如果有多条,随机选择一条返回 + int randomIndex = new Random().nextInt(customerList.size()); + Long randomCustomerId = customerList.get(randomIndex).getId(); + + return randomCustomerId; + } @Override public LoginVO loginCustom(LoginDTO dto) { diff --git a/im-uniapp/pages/chat/chat-box.vue b/im-uniapp/pages/chat/chat-box.vue index 5fe4523..046954f 100644 --- a/im-uniapp/pages/chat/chat-box.vue +++ b/im-uniapp/pages/chat/chat-box.vue @@ -291,57 +291,167 @@ export default { (item) => item.replyTitle === replyTitle ); if (!autoReply) return; - + // 检查是否被封禁 if (this.isBanned) { this.showBannedTip(); return; } - - let msgInfo = { + + // ========== 第一步:用户发送问题(replyTitle) ========== + let userMsgInfo = { tmpId: this.generateId(), - receipt: this.isReceipt, + type: this.$enums.MESSAGE_TYPE.TEXT, + content: autoReply.replyTitle, + receipt: this.isReceipt }; - - if (autoReply.replyType === 0) { - // 文本 - msgInfo.type = this.$enums.MESSAGE_TYPE.TEXT; - msgInfo.content = autoReply.replyContent; - } else if (autoReply.replyType === 1) { - // 图片 - msgInfo.type = this.$enums.MESSAGE_TYPE.IMAGE; - msgInfo.content = JSON.stringify({ - originUrl: autoReply.replyContent, - thumbUrl: autoReply.replyContent, - }); - } else { - return; - } - - this.fillTargetId(msgInfo, this.chat.targetId); + + this.fillTargetId(userMsgInfo, this.chat.targetId); const chat = this.chat; if (!chat) return; - - let tmpMessage = this.buildTmpMessage(msgInfo); - this.chatStore.insertMessage(tmpMessage, chat); + + let tmpUserMessage = this.buildTmpMessage(userMsgInfo); + this.chatStore.insertMessage(tmpUserMessage, chat); this.moveChatToTop(); - - this.sendMessageRequest(msgInfo) + + // 发送用户消息 + this.sendMessageRequest(userMsgInfo) .then((m) => { - tmpMessage = JSON.parse(JSON.stringify(tmpMessage)); - tmpMessage.id = m.id; - tmpMessage.status = m.status; - this.chatStore.updateMessage(tmpMessage, chat); + tmpUserMessage = JSON.parse(JSON.stringify(tmpUserMessage)); + tmpUserMessage.id = m.id; + tmpUserMessage.status = m.status; + this.chatStore.updateMessage(tmpUserMessage, chat); + + // ========== 第二步:调用后端接口发送客服回复 ========== + this.triggerAutoReply(autoReply); + this.scrollToBottom(); this.isReceipt = false; }) .catch(() => { - tmpMessage = JSON.parse(JSON.stringify(tmpMessage)); - tmpMessage.status = this.$enums.MESSAGE_STATUS.FAILED; - this.chatStore.updateMessage(tmpMessage, chat); + tmpUserMessage = JSON.parse(JSON.stringify(tmpUserMessage)); + tmpUserMessage.status = this.$enums.MESSAGE_STATUS.FAILED; + this.chatStore.updateMessage(tmpUserMessage, chat); }); }, - + + // 触发客服自动回复 + triggerAutoReply(autoReply) { + // 生成临时ID用于追踪 + let tmpId = this.generateId(); + + // 构建客服回复消息 + let replyMsgInfo = { + tmpId: tmpId, // 添加 tmpId + sendId: this.chat.targetId, // 客服ID作为发送者 + recvId: this.mine.id, // 当前用户作为接收者 + type: autoReply.replyType, // 回复类型 + receipt: false + }; + + // 根据类型设置内容 + if (autoReply.replyType === 0) { + // 文本回复 + replyMsgInfo.content = autoReply.replyContent; + } else if (autoReply.replyType === 1) { + // 图片回复 + replyMsgInfo.content = JSON.stringify({ + originUrl: autoReply.replyContent, + thumbUrl: autoReply.replyContent, + }); + } + + // 可选:先在前端显示一条临时的"客服正在输入"消息 + let typingMessage = { + id: this.generateId(), + tmpId: tmpId, + sendId: this.chat.targetId, + recvId: this.mine.id, + selfSend: false, + sendTime: new Date().getTime(), + type: this.$enums.MESSAGE_TYPE.TIP_TEXT, + content: "客服正在输入...", + status: this.$enums.MESSAGE_STATUS.SENDING + }; + + this.chatStore.insertMessage(typingMessage, this.chat); + + // 调用发送消息接口 + this.$http({ + url: "/message/private/send", + method: "post", + data: replyMsgInfo + }).then(res => { + console.log("自动回复发送成功", res); + + // 删除"正在输入"的临时消息 + this.chatStore.deleteMessage(typingMessage, this.chat); + + // 消息会通过 WebSocket 推送过来,前端会自动接收并显示 + // 或者如果接口直接返回消息体,可以手动插入 + if (res && res.id) { + let replyMessage = { + id: res.id, + tmpId: tmpId, + sendId: this.chat.targetId, + recvId: this.mine.id, + selfSend: false, + sendTime: new Date().getTime(), + type: autoReply.replyType, + content: replyMsgInfo.content, + status: this.$enums.MESSAGE_STATUS.SENDED + }; + + this.chatStore.insertMessage(replyMessage, this.chat); + this.scrollToBottom(); + } + }).catch(err => { + console.error("自动回复发送失败", err); + + // 删除"正在输入"的临时消息 + this.chatStore.deleteMessage(typingMessage, this.chat); + + // 可选:显示发送失败提示 + uni.showToast({ + title: "自动回复失败", + icon: "none" + }); + }); + }, + + // 新增方法:触发客服自动回复(调用后端接口) + // triggerAutoReply(autoReply) { + // // 构建客服回复消息,关键是要传入 sendId + // let replyMsgInfo = { + // sendId: this.chat.targetId, // 客服ID作为发送者 + // recvId: this.mine.id, // 当前用户作为接收者 + // type: autoReply.replyType, // 回复类型 + // }; + // console.log(replyMsgInfo) + // // 根据类型设置内容 + // if (autoReply.replyType === 0) { + // // 文本回复 + // replyMsgInfo.content = autoReply.replyContent; + // } else if (autoReply.replyType === 1) { + // // 图片回复 + // replyMsgInfo.content = JSON.stringify({ + // originUrl: autoReply.replyContent, + // thumbUrl: autoReply.replyContent, + // }); + // } + + // // 调用发送消息接口 + // this.$http({ + // url: "/message/private/send", + // method: "post", + // data: replyMsgInfo + // }).then(res => { + // console.log("自动回复发送成功", res); + // // 消息会通过 WebSocket 推送过来,前端会自动接收并显示 + // }).catch(err => { + // console.error("自动回复发送失败", err); + // }); + // }, onRecorderInput() { this.showRecord = true; this.switchChatTabBox("none"); @@ -1070,6 +1180,7 @@ export default { sendMessageRequest(msgInfo) { return new Promise((resolve, reject) => { // 请求入队列,防止请求"后发先至",导致消息错序 + this.reqQueue.push({ msgInfo, resolve, reject }); this.processReqQueue(); }); @@ -1078,6 +1189,7 @@ export default { if (this.reqQueue.length && !this.isSending) { this.isSending = true; const reqData = this.reqQueue.shift(); + console.log(reqData.msgInfo); this.$http({ url: this.messageAction, method: "post", diff --git a/im-uniapp/pages/login/login.vue b/im-uniapp/pages/login/login.vue index dd3727a..5ccf2a5 100644 --- a/im-uniapp/pages/login/login.vue +++ b/im-uniapp/pages/login/login.vue @@ -20,7 +20,8 @@ export default { password: '', ip: '', sourceUrl: '', - uniqueToken: '' // 添加token字段 + uniqueToken: '' ,// 添加token字段 + kefuid: '' // 添加客服ID字段 } } }, @@ -55,10 +56,9 @@ export default { console.log("来源网址:", this.dataForm.sourceUrl); }, - // 获取URL参数中的token + // 获取URL参数中的token和kefuid getTokenFromUrl() { // #ifdef H5 - // 方法1:从当前页面的options获取 const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const options = currentPage.options || {}; @@ -66,16 +66,26 @@ export default { if (options.token) { this.dataForm.uniqueToken = options.token; console.log("从options获取到token:", this.dataForm.uniqueToken); - return; } - // 方法2:直接从URL解析 + if (options.kefuid) { + this.dataForm.kefuid = options.kefuid; + console.log("从options获取到kefuid:", this.dataForm.kefuid); + } + const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); + const kefuid = urlParams.get('kefuid'); + if (token) { this.dataForm.uniqueToken = token; console.log("从URL解析获取到token:", this.dataForm.uniqueToken); } + + if (kefuid) { + this.dataForm.kefuid = kefuid; + console.log("从URL解析获取到kefuid:", this.dataForm.kefuid); + } // #endif // #ifdef APP-PLUS @@ -85,7 +95,9 @@ export default { const options = currentPage.options || {}; if (options.token) { this.dataForm.uniqueToken = options.token; - console.log("App端获取到token:", this.dataForm.uniqueToken); + } + if (options.kefuid) { + this.dataForm.kefuid = options.kefuid; } // #endif @@ -96,7 +108,10 @@ export default { const options = currentPage.options || {}; if (options.token) { this.dataForm.uniqueToken = options.token; - console.log("小程序端获取到token:", this.dataForm.uniqueToken); + } + console.log(options); + if (options.kefuid) { + this.dataForm.kefuid = options.kefuid; } // #endif }, @@ -138,10 +153,9 @@ export default { ip: this.dataForm.ip, sourceUrl: this.dataForm.sourceUrl, uniqueToken: this.dataForm.uniqueToken, + keFuId: this.dataForm.kefuid, // 添加kefuid参数 }; - console.log("登录参数:", loginData); - this.$http({ url: '/login', data: loginData, @@ -152,7 +166,6 @@ export default { getApp().$vm.init() getApp().$vm.unloadStore(); - console.log(loginInfo.customerServiceId); this.$http({ url: "/friend/add?friendId=" + loginInfo.customerServiceId, method: "POST" @@ -227,7 +240,10 @@ export default { this.dataForm.uniqueToken = options.token; console.log("onLoad获取到token:", this.dataForm.uniqueToken); } - + if (options.kefuid) { + this.dataForm.kefuid = options.kefuid; + console.log("onLoad获取到kefuid:", this.dataForm.kefuid); + } // 延迟执行,彻底避开双加载 setTimeout(() => { this.autoLogin();