From 0778e5c3ba0c8907ed63ff68d6a37db4b98366ef Mon Sep 17 00:00:00 2001 From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com> Date: Fri, 26 Dec 2025 18:18:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E9=80=9A=E8=AF=9D?= =?UTF-8?q?=E4=BF=A1=E4=BB=A4=E5=8A=9F=E8=83=BD=20-=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=BB=E5=8F=AB=E6=96=B9=E5=92=8C=E8=A2=AB=E5=8F=AB=E6=96=B9?= =?UTF-8?q?=E9=80=9A=E8=AF=9D=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zbkj/front/config/WebSocketConfig.java | 9 + .../zbkj/front/controller/CallController.java | 36 ++++ .../controller/ConversationController.java | 11 ++ .../front/websocket/CallSignalingHandler.java | 126 ++++++++++++-- .../service/service/ConversationService.java | 15 ++ .../service/service/impl/CallServiceImpl.java | 10 +- .../service/impl/ConversationServiceImpl.java | 105 +++++++++--- .../livestreaming/ConversationActivity.java | 161 ++++++++++++++++++ .../livestreaming/ConversationItem.java | 18 ++ .../LiveStreamingApplication.java | 53 ++++++ .../example/livestreaming/LoginActivity.java | 13 ++ .../livestreaming/MessagesActivity.java | 6 +- .../livestreaming/call/CallActivity.java | 51 +++++- .../livestreaming/call/CallManager.java | 88 +++++++++- .../call/IncomingCallActivity.java | 17 +- .../main/res/layout/activity_conversation.xml | 30 +++- 16 files changed, 696 insertions(+), 53 deletions(-) diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebSocketConfig.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebSocketConfig.java index d964cfda..fbcec394 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebSocketConfig.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/config/WebSocketConfig.java @@ -1,6 +1,7 @@ package com.zbkj.front.config; import com.zbkj.front.interceptor.WebSocketAuthInterceptor; +import com.zbkj.front.websocket.CallSignalingHandler; import com.zbkj.front.websocket.LiveChatHandler; import com.zbkj.front.websocket.PrivateChatHandler; import org.springframework.context.annotation.Configuration; @@ -25,13 +26,16 @@ public class WebSocketConfig implements WebSocketConfigurer { private final LiveChatHandler liveChatHandler; private final PrivateChatHandler privateChatHandler; + private final CallSignalingHandler callSignalingHandler; private final WebSocketAuthInterceptor webSocketAuthInterceptor; public WebSocketConfig(LiveChatHandler liveChatHandler, PrivateChatHandler privateChatHandler, + CallSignalingHandler callSignalingHandler, WebSocketAuthInterceptor webSocketAuthInterceptor) { this.liveChatHandler = liveChatHandler; this.privateChatHandler = privateChatHandler; + this.callSignalingHandler = callSignalingHandler; this.webSocketAuthInterceptor = webSocketAuthInterceptor; } @@ -48,5 +52,10 @@ public class WebSocketConfig implements WebSocketConfigurer { registry.addHandler(privateChatHandler, "/ws/chat/{conversationId}") .addInterceptors(webSocketAuthInterceptor) .setAllowedOrigins("*"); + + // 通话信令 WebSocket 端点: ws://host:8081/ws/call + // 用于语音/视频通话的信令交换 + registry.addHandler(callSignalingHandler, "/ws/call") + .setAllowedOrigins("*"); } } diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java index 0f4d9bd3..21da4512 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java @@ -8,6 +8,7 @@ import com.zbkj.common.result.CommonResult; import com.zbkj.front.request.call.InitiateCallRequest; import com.zbkj.front.response.call.CallRecordResponse; import com.zbkj.front.response.call.InitiateCallResponse; +import com.zbkj.front.websocket.CallSignalingHandler; import com.zbkj.service.service.CallService; import com.zbkj.service.service.UserService; import io.swagger.annotations.Api; @@ -32,6 +33,9 @@ public class CallController { @Autowired private UserService userService; + @Autowired + private CallSignalingHandler callSignalingHandler; + @ApiOperation(value = "发起通话") @PostMapping("/initiate") public CommonResult initiateCall(@RequestBody @Validated InitiateCallRequest request) { @@ -40,6 +44,17 @@ public class CallController { User callee = userService.getById(request.getCalleeId()); if (callee == null) return CommonResult.failed("被叫用户不存在"); CallRecord record = callService.createCall(currentUser.getUid(), request.getCalleeId(), request.getCallType()); + + // 通过WebSocket通知被叫方 + callSignalingHandler.notifyIncomingCall( + record.getCallId(), + currentUser.getUid(), + currentUser.getNickname(), + currentUser.getAvatar(), + request.getCalleeId(), + request.getCallType() + ); + InitiateCallResponse response = new InitiateCallResponse(); response.setCallId(record.getCallId()); response.setCallType(record.getCallType()); @@ -57,6 +72,19 @@ public class CallController { public CommonResult acceptCall(@PathVariable String callId) { User currentUser = userService.getInfo(); if (currentUser == null) return CommonResult.failed("请先登录"); + + System.out.println("[CallController] 接听通话API: callId=" + callId + ", userId=" + currentUser.getUid()); + + // 获取通话记录,找到主叫方 + CallRecord record = callService.getByCallId(callId); + if (record != null) { + System.out.println("[CallController] 通话记录: callerId=" + record.getCallerId() + ", status=" + record.getStatus()); + // 通过WebSocket通知主叫方 + callSignalingHandler.notifyCallAccepted(callId, record.getCallerId()); + } else { + System.out.println("[CallController] 通话记录不存在: callId=" + callId); + } + return CommonResult.success(callService.acceptCall(callId, currentUser.getUid())); } @@ -65,6 +93,14 @@ public class CallController { public CommonResult rejectCall(@PathVariable String callId) { User currentUser = userService.getInfo(); if (currentUser == null) return CommonResult.failed("请先登录"); + + // 获取通话记录,找到主叫方 + CallRecord record = callService.getByCallId(callId); + if (record != null) { + // 通过WebSocket通知主叫方 + callSignalingHandler.notifyCallRejected(callId, record.getCallerId()); + } + return CommonResult.success(callService.rejectCall(callId, currentUser.getUid())); } diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java index 94dea5e8..5db2b132 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java @@ -45,6 +45,17 @@ public class ConversationController { return CommonResult.success(conversationService.getConversationList(userId)); } + /** + * 获取单个会话详情 + */ + @ApiOperation(value = "获取单个会话详情") + @ApiImplicitParam(name = "id", value = "会话ID", required = true) + @GetMapping("/{id}") + public CommonResult getConversationDetail(@PathVariable Long id) { + Integer userId = userService.getUserIdException(); + return CommonResult.success(conversationService.getConversationDetail(id, userId)); + } + /** * 搜索会话 */ diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java index 64500e0e..9086d9b7 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java @@ -105,6 +105,7 @@ public class CallSignalingHandler extends TextWebSocketHandler { // 关闭旧连接 WebSocketSession oldSession = userCallSessions.get(userId); if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) { + logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId()); try { oldSession.close(); } catch (Exception ignored) {} @@ -117,7 +118,8 @@ public class CallSignalingHandler extends TextWebSocketHandler { response.put("type", "registered"); response.put("userId", userId); session.sendMessage(new TextMessage(response.toString())); - logger.info("[CallSignaling] 用户注册: userId={}", userId); + logger.info("[CallSignaling] 用户注册成功: userId={}, sessionId={}, 当前在线用户数={}", + userId, session.getId(), userCallSessions.size()); } private void handleCallRequest(WebSocketSession session, JsonNode json) throws IOException { @@ -179,30 +181,50 @@ public class CallSignalingHandler extends TextWebSocketHandler { Integer userId = sessionUserMap.get(session.getId()); String callId = json.has("callId") ? json.get("callId").asText() : null; + logger.info("[CallSignaling] 处理接听请求: callId={}, userId={}", callId, userId); + if (userId == null || callId == null) { sendError(session, "参数不完整"); return; } try { - callService.acceptCall(callId, userId); + // 先获取通话记录,用于后续通知 + CallRecord record = callService.getByCallId(callId); + if (record == null) { + sendError(session, "通话记录不存在"); + return; + } + + logger.info("[CallSignaling] 通话记录: callId={}, status={}, callerId={}", + callId, record.getStatus(), record.getCallerId()); + + // 更新通话状态 + Boolean accepted = callService.acceptCall(callId, userId); + logger.info("[CallSignaling] acceptCall结果: {}", accepted); + joinCallSession(callId, session); sessionCallMap.put(session.getId(), callId); // 通知主叫方 - CallRecord record = callService.getByCallId(callId); - if (record != null) { - WebSocketSession callerSession = userCallSessions.get(record.getCallerId()); - if (callerSession != null && callerSession.isOpen()) { - ObjectNode notify = objectMapper.createObjectNode(); - notify.put("type", "call_accepted"); - notify.put("callId", callId); - callerSession.sendMessage(new TextMessage(notify.toString())); - } + Integer callerId = record.getCallerId(); + WebSocketSession callerSession = userCallSessions.get(callerId); + logger.info("[CallSignaling] 查找主叫方会话: callerId={}, session存在={}, session打开={}", + callerId, callerSession != null, callerSession != null && callerSession.isOpen()); + + if (callerSession != null && callerSession.isOpen()) { + ObjectNode notify = objectMapper.createObjectNode(); + notify.put("type", "call_accepted"); + notify.put("callId", callId); + callerSession.sendMessage(new TextMessage(notify.toString())); + logger.info("[CallSignaling] 已通知主叫方接听: callId={}, callerId={}", callId, callerId); + } else { + logger.warn("[CallSignaling] 主叫方WebSocket未连接: callerId={}", callerId); } - logger.info("[CallSignaling] 接听通话: callId={}, userId={}", callId, userId); + logger.info("[CallSignaling] 接听通话完成: callId={}, userId={}", callId, userId); } catch (Exception e) { + logger.error("[CallSignaling] 接听通话异常: callId={}, error={}", callId, e.getMessage(), e); sendError(session, e.getMessage()); } } @@ -445,4 +467,84 @@ public class CallSignalingHandler extends TextWebSocketHandler { error.put("message", message); session.sendMessage(new TextMessage(error.toString())); } + + /** + * 通知被叫方有来电(供REST API调用) + */ + public void notifyIncomingCall(String callId, Integer callerId, String callerName, String callerAvatar, + Integer calleeId, String callType) { + try { + // 记录通话创建时间 + callCreateTime.put(callId, System.currentTimeMillis()); + + // 通知被叫方 + WebSocketSession calleeSession = userCallSessions.get(calleeId); + if (calleeSession != null && calleeSession.isOpen()) { + ObjectNode incoming = objectMapper.createObjectNode(); + incoming.put("type", "incoming_call"); + incoming.put("callId", callId); + incoming.put("callerId", callerId); + incoming.put("callerName", callerName != null ? callerName : "用户" + callerId); + incoming.put("callerAvatar", callerAvatar != null ? callerAvatar : ""); + incoming.put("callType", callType); + calleeSession.sendMessage(new TextMessage(incoming.toString())); + logger.info("[CallSignaling] 通知来电: callId={}, callee={}", callId, calleeId); + } else { + logger.warn("[CallSignaling] 被叫方未在线: calleeId={}", calleeId); + } + } catch (Exception e) { + logger.error("[CallSignaling] 通知来电异常: callId={}", callId, e); + } + } + + /** + * 检查用户是否在线 + */ + public boolean isUserOnline(Integer userId) { + WebSocketSession session = userCallSessions.get(userId); + return session != null && session.isOpen(); + } + + /** + * 通知主叫方通话已被接听(供REST API调用) + */ + public void notifyCallAccepted(String callId, Integer callerId) { + logger.info("[CallSignaling] REST API调用notifyCallAccepted: callId={}, callerId={}, 当前在线用户={}", + callId, callerId, userCallSessions.keySet()); + try { + WebSocketSession callerSession = userCallSessions.get(callerId); + if (callerSession != null && callerSession.isOpen()) { + ObjectNode notify = objectMapper.createObjectNode(); + notify.put("type", "call_accepted"); + notify.put("callId", callId); + callerSession.sendMessage(new TextMessage(notify.toString())); + logger.info("[CallSignaling] 通知接听成功: callId={}, caller={}", callId, callerId); + } else { + logger.warn("[CallSignaling] 主叫方未在线: callerId={}, session存在={}", + callerId, callerSession != null); + } + } catch (Exception e) { + logger.error("[CallSignaling] 通知接听异常: callId={}", callId, e); + } + } + + /** + * 通知主叫方通话已被拒绝(供REST API调用) + */ + public void notifyCallRejected(String callId, Integer callerId) { + try { + WebSocketSession callerSession = userCallSessions.get(callerId); + if (callerSession != null && callerSession.isOpen()) { + ObjectNode notify = objectMapper.createObjectNode(); + notify.put("type", "call_rejected"); + notify.put("callId", callId); + callerSession.sendMessage(new TextMessage(notify.toString())); + logger.info("[CallSignaling] 通知拒绝: callId={}, caller={}", callId, callerId); + } else { + logger.warn("[CallSignaling] 主叫方未在线: callerId={}", callerId); + } + } catch (Exception e) { + logger.error("[CallSignaling] 通知拒绝异常: callId={}", callId, e); + } + } } diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java index 6952da57..e6392110 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java @@ -18,6 +18,11 @@ public interface ConversationService extends IService { */ List getConversationList(Integer userId); + /** + * 获取单个会话详情 + */ + ConversationResponse getConversationDetail(Long conversationId, Integer userId); + /** * 搜索会话 */ @@ -48,6 +53,16 @@ public interface ConversationService extends IService { */ Boolean deleteMessage(Long messageId, Integer userId); + /** + * 撤回消息 + */ + Boolean recallMessage(Long messageId, Integer userId); + + /** + * 获取单条消息详情 + */ + ChatMessageResponse getMessageById(Long messageId); + /** * 获取或创建与指定用户的会话 */ diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java index 84d2998b..118e6b85 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java @@ -101,8 +101,16 @@ public class CallServiceImpl extends ServiceImpl impl CallRecord record = getByCallId(callId); if (record == null) throw new CrmebException("通话记录不存在"); if (!record.getCalleeId().equals(userId)) throw new CrmebException("无权操作此通话"); + + // 记录当前状态用于调试 + logger.info("[Call] 接听通话: callId={}, 当前状态={}, userId={}", callId, record.getStatus(), userId); + + // 只有在 calling 或 ringing 状态才能接听 + // 如果已经是其他状态(如 missed, ended),说明通话已经结束 if (!STATUS_CALLING.equals(record.getStatus()) && !STATUS_RINGING.equals(record.getStatus())) { - throw new CrmebException("通话状态不正确"); + logger.warn("[Call] 通话状态不正确,无法接听: callId={}, status={}", callId, record.getStatus()); + // 返回 false 而不是抛异常,让前端可以处理 + return false; } Date now = new Date(); diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java index 4c748a3c..f4be294c 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java @@ -47,6 +47,19 @@ public class ConversationServiceImpl extends ServiceImpl searchConversations(Integer userId, String keyword) { // 先获取用户的所有会话 @@ -262,35 +275,42 @@ public class ConversationServiceImpl extends ServiceImpl convertToResponseList(List conversations, Integer userId) { List result = new ArrayList<>(); for (Conversation conv : conversations) { - ConversationResponse response = new ConversationResponse(); - response.setId(String.valueOf(conv.getId())); - response.setLastMessage(conv.getLastMessage()); - response.setTimeText(formatTimeText(conv.getLastMessageTime())); - // 确定对方用户ID - Integer otherUserId = conv.getUser1Id().equals(userId) ? conv.getUser2Id() : conv.getUser1Id(); - response.setOtherUserId(otherUserId); - // 获取未读数和静音状态 - if (conv.getUser1Id().equals(userId)) { - response.setUnreadCount(conv.getUser1UnreadCount()); - response.setMuted(conv.getUser1Muted()); - } else { - response.setUnreadCount(conv.getUser2UnreadCount()); - response.setMuted(conv.getUser2Muted()); - } - // 获取对方用户信息 - User otherUser = userService.getById(otherUserId); - if (otherUser != null) { - response.setTitle(otherUser.getNickname()); - response.setAvatarUrl(otherUser.getAvatar()); - } else { - response.setTitle("未知用户"); - response.setAvatarUrl(""); - } - result.add(response); + result.add(convertToResponse(conv, userId)); } return result; } + /** + * 转换单个会话为响应对象 + */ + private ConversationResponse convertToResponse(Conversation conv, Integer userId) { + ConversationResponse response = new ConversationResponse(); + response.setId(String.valueOf(conv.getId())); + response.setLastMessage(conv.getLastMessage()); + response.setTimeText(formatTimeText(conv.getLastMessageTime())); + // 确定对方用户ID + Integer otherUserId = conv.getUser1Id().equals(userId) ? conv.getUser2Id() : conv.getUser1Id(); + response.setOtherUserId(otherUserId); + // 获取未读数和静音状态 + if (conv.getUser1Id().equals(userId)) { + response.setUnreadCount(conv.getUser1UnreadCount()); + response.setMuted(conv.getUser1Muted()); + } else { + response.setUnreadCount(conv.getUser2UnreadCount()); + response.setMuted(conv.getUser2Muted()); + } + // 获取对方用户信息 + User otherUser = userService.getById(otherUserId); + if (otherUser != null) { + response.setTitle(otherUser.getNickname()); + response.setAvatarUrl(otherUser.getAvatar()); + } else { + response.setTitle("未知用户"); + response.setAvatarUrl(""); + } + return response; + } + /** * 转换消息列表为响应对象列表 */ @@ -374,6 +394,41 @@ public class ConversationServiceImpl extends ServiceImpl 2) { + throw new CrmebException("消息发送超过2分钟,无法撤回"); + } + // 标记消息为已撤回 + LambdaUpdateWrapper uw = new LambdaUpdateWrapper<>(); + uw.eq(PrivateMessage::getId, messageId) + .set(PrivateMessage::getIsRecalled, true) + .set(PrivateMessage::getContent, "[消息已撤回]"); + return privateMessageDao.update(null, uw) > 0; + } + + @Override + public ChatMessageResponse getMessageById(Long messageId) { + PrivateMessage message = privateMessageDao.selectById(messageId); + if (message == null) { + return null; + } + return convertMessageToResponse(message); + } + @Override @Transactional(rollbackFor = Exception.class) public PrivateMessage sendMessage(Long conversationId, Integer senderId, String messageType, String content, String mediaUrl) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java index b27cc09c..3e432df7 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java @@ -77,6 +77,14 @@ public class ConversationActivity extends AppCompatActivity { intent.putExtra(EXTRA_CONVERSATION_TITLE, title); context.startActivity(intent); } + + public static void start(Context context, String conversationId, String title, int otherUserId) { + Intent intent = new Intent(context, ConversationActivity.class); + intent.putExtra(EXTRA_CONVERSATION_ID, conversationId); + intent.putExtra(EXTRA_CONVERSATION_TITLE, title); + intent.putExtra("other_user_id", otherUserId); + context.startActivity(intent); + } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -687,6 +695,159 @@ public class ConversationActivity extends AppCompatActivity { binding.messageInput.setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus) scrollToBottom(); }); + + // 设置通话按钮点击事件 + setupCallButtons(); + } + + /** + * 设置通话按钮 + */ + private void setupCallButtons() { + // 语音通话按钮 + binding.voiceCallButton.setOnClickListener(new DebounceClickListener() { + @Override + public void onDebouncedClick(View v) { + initiateCall("voice"); + } + }); + + // 视频通话按钮 + binding.videoCallButton.setOnClickListener(new DebounceClickListener() { + @Override + public void onDebouncedClick(View v) { + initiateCall("video"); + } + }); + } + + /** + * 发起通话 + */ + private void initiateCall(String callType) { + if (!AuthHelper.requireLoginWithToast(this, "发起通话需要登录")) { + return; + } + + // 获取对方用户ID(从会话中解析) + int otherUserId = getOtherUserIdFromConversation(); + if (otherUserId <= 0) { + // 如果无法从Intent获取,尝试从服务器获取 + fetchOtherUserIdFromServer(callType); + return; + } + + startCallWithUserId(otherUserId, callType); + } + + /** + * 使用指定的用户ID发起通话 + */ + private void startCallWithUserId(int otherUserId, String callType) { + String callTypeText = "voice".equals(callType) ? "语音" : "视频"; + Log.d(TAG, "发起" + callTypeText + "通话,对方用户ID: " + otherUserId); + + // 使用CallManager发起通话 + com.example.livestreaming.call.CallManager callManager = + com.example.livestreaming.call.CallManager.getInstance(this); + + // 先连接信令服务器 + if (currentUserId != null && !currentUserId.isEmpty()) { + try { + int myUserId = (int) Double.parseDouble(currentUserId); + callManager.connect(myUserId); + } catch (NumberFormatException e) { + Log.e(TAG, "解析用户ID失败", e); + } + } + + // 发起通话 + callManager.initiateCall(otherUserId, callType, + new com.example.livestreaming.call.CallManager.CallCallback() { + @Override + public void onSuccess(com.example.livestreaming.call.InitiateCallResponse response) { + Log.d(TAG, "通话发起成功: " + response.getCallId()); + } + + @Override + public void onError(String error) { + runOnUiThread(() -> { + Snackbar.make(binding.getRoot(), "呼叫失败: " + error, Snackbar.LENGTH_SHORT).show(); + }); + } + }); + } + + /** + * 从会话信息中获取对方用户ID + */ + private int getOtherUserIdFromConversation() { + // 尝试从Intent中获取 + int otherUserId = getIntent().getIntExtra("other_user_id", 0); + if (otherUserId > 0) { + Log.d(TAG, "从Intent获取到对方用户ID: " + otherUserId); + return otherUserId; + } + + // 如果Intent中没有,尝试从服务器获取会话详情 + Log.w(TAG, "Intent中没有other_user_id,需要从服务器获取"); + return 0; + } + + /** + * 从服务器获取会话详情以获取对方用户ID + */ + private void fetchOtherUserIdFromServer(String callType) { + String token = AuthStore.getToken(this); + if (token == null || conversationId == null) { + Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show(); + return; + } + + String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId; + Log.d(TAG, "获取会话详情: " + url); + + Request request = new Request.Builder() + .url(url) + .addHeader("Authori-zation", token) + .get() + .build(); + + httpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + Log.e(TAG, "获取会话详情失败", e); + runOnUiThread(() -> Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show()); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + String body = response.body() != null ? response.body().string() : ""; + Log.d(TAG, "会话详情响应: " + body); + runOnUiThread(() -> { + try { + JSONObject json = new JSONObject(body); + if (json.optInt("code", -1) == 200) { + JSONObject data = json.optJSONObject("data"); + if (data != null) { + int otherUserId = data.optInt("otherUserId", 0); + if (otherUserId > 0) { + Log.d(TAG, "从服务器获取到对方用户ID: " + otherUserId); + startCallWithUserId(otherUserId, callType); + } else { + Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show(); + } + } + } else { + Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, "解析会话详情失败", e); + Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show(); + } + }); + } + }); } /** diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationItem.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationItem.java index 7032a306..ce8bfdd9 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ConversationItem.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationItem.java @@ -10,6 +10,8 @@ public class ConversationItem { private final String timeText; private final int unreadCount; private final boolean muted; + private int otherUserId; + private String avatarUrl; public ConversationItem(String id, String title, String lastMessage, String timeText, int unreadCount, boolean muted) { this.id = id; @@ -44,6 +46,22 @@ public class ConversationItem { return muted; } + public int getOtherUserId() { + return otherUserId; + } + + public void setOtherUserId(int otherUserId) { + this.otherUserId = otherUserId; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java index 40eba177..22d2e722 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java +++ b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java @@ -1,6 +1,10 @@ package com.example.livestreaming; import android.app.Application; +import android.util.Log; + +import com.example.livestreaming.call.CallManager; +import com.example.livestreaming.net.AuthStore; /** * 自定义Application类,用于初始化各种组件 @@ -8,9 +12,13 @@ import android.app.Application; */ public class LiveStreamingApplication extends Application { + private static final String TAG = "LiveStreamingApp"; + private static LiveStreamingApplication instance; + @Override public void onCreate() { super.onCreate(); + instance = this; // 初始化LeakCanary内存泄漏检测(仅在debug版本中生效) // LeakCanary会自动在debug版本中初始化,无需手动调用 @@ -20,5 +28,50 @@ public class LiveStreamingApplication extends Application { // 初始化通知渠道 LocalNotificationManager.createNotificationChannel(this); + + // 如果用户已登录,连接通话信令服务器 + connectCallSignalingIfLoggedIn(); + } + + public static LiveStreamingApplication getInstance() { + return instance; + } + + /** + * 如果用户已登录,连接通话信令服务器 + */ + public void connectCallSignalingIfLoggedIn() { + String userId = AuthStore.getUserId(this); + String token = AuthStore.getToken(this); + + if (token != null && !token.isEmpty() && userId != null && !userId.isEmpty()) { + try { + int uid = (int) Double.parseDouble(userId); + if (uid > 0) { + Log.d(TAG, "用户已登录,连接通话信令服务器,userId: " + uid); + CallManager.getInstance(this).connect(uid); + } + } catch (NumberFormatException e) { + Log.e(TAG, "解析用户ID失败: " + userId, e); + } + } else { + Log.d(TAG, "用户未登录,不连接通话信令服务器"); + } + } + + /** + * 用户登录后调用,连接通话信令服务器 + */ + public void onUserLoggedIn(int userId) { + Log.d(TAG, "用户登录成功,连接通话信令服务器,userId: " + userId); + CallManager.getInstance(this).connect(userId); + } + + /** + * 用户登出后调用,断开通话信令服务器 + */ + public void onUserLoggedOut() { + Log.d(TAG, "用户登出,断开通话信令服务器"); + CallManager.getInstance(this).disconnect(); } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java b/android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java index 3bb082c6..44c00a7c 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java @@ -149,6 +149,19 @@ public class LoginActivity extends AppCompatActivity { prefs.edit().putString("profile_phone", loginData.getPhone()).apply(); } + // 连接通话信令服务器 + if (!TextUtils.isEmpty(uid)) { + try { + int userId = (int) Double.parseDouble(uid); + if (userId > 0) { + LiveStreamingApplication app = (LiveStreamingApplication) getApplication(); + app.onUserLoggedIn(userId); + } + } catch (NumberFormatException e) { + android.util.Log.e("LoginActivity", "解析用户ID失败", e); + } + } + // 登录成功 Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java index 5cdf36c7..03418731 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java @@ -137,11 +137,12 @@ public class MessagesActivity extends AppCompatActivity { conversationsAdapter = new ConversationsAdapter(item -> { if (item == null) return; try { - // 启动会话页面,传递未读数量 + // 启动会话页面,传递未读数量和对方用户ID Intent intent = new Intent(this, ConversationActivity.class); intent.putExtra("extra_conversation_id", item.getId()); intent.putExtra("extra_conversation_title", item.getTitle()); intent.putExtra("extra_unread_count", item.getUnreadCount()); + intent.putExtra("other_user_id", item.getOtherUserId()); startActivity(intent); // 用户点击会话时,减少该会话的未读数量 @@ -231,8 +232,11 @@ public class MessagesActivity extends AppCompatActivity { int unreadCount = item.optInt("unreadCount", 0); boolean isMuted = item.optBoolean("muted", item.optBoolean("isMuted", false)); String avatarUrl = item.optString("avatarUrl", item.optString("otherUserAvatar", "")); + int otherUserId = item.optInt("otherUserId", 0); ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted); + convItem.setOtherUserId(otherUserId); + convItem.setAvatarUrl(avatarUrl); allConversations.add(convItem); } catch (Exception e) { Log.e(TAG, "解析会话项失败", e); diff --git a/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java b/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java index feee56bb..62d88e5f 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java @@ -114,11 +114,33 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS otherUserId = getIntent().getIntExtra("otherUserId", 0); otherUserName = getIntent().getStringExtra("otherUserName"); otherUserAvatar = getIntent().getStringExtra("otherUserAvatar"); + + // 如果是被叫方接听,直接进入通话状态 + boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false); + if (alreadyConnected) { + android.util.Log.d("CallActivity", "被叫方接听,直接进入通话状态"); + isConnected = true; + callStartTime = System.currentTimeMillis(); + } } private void initCallManager() { callManager = CallManager.getInstance(this); callManager.setStateListener(this); + + // 确保WebSocket已连接(主叫方需要接收接听/拒绝通知) + String userId = com.example.livestreaming.net.AuthStore.getUserId(this); + if (userId != null && !userId.isEmpty()) { + try { + int uid = (int) Double.parseDouble(userId); + if (uid > 0) { + android.util.Log.d("CallActivity", "确保WebSocket连接,userId: " + uid); + callManager.connect(uid); + } + } catch (NumberFormatException e) { + android.util.Log.e("CallActivity", "解析用户ID失败", e); + } + } } private void setupListeners() { @@ -159,11 +181,21 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS layoutVideoToggle.setVisibility(isVideo ? View.VISIBLE : View.GONE); btnSwitchCamera.setVisibility(isVideo ? View.VISIBLE : View.GONE); - // 设置通话状态 - if (isCaller) { - tvCallStatus.setText("正在呼叫..."); + // 根据连接状态设置界面 + if (isConnected) { + // 已接通,显示通话中界面 + tvCallStatus.setVisibility(View.GONE); + tvCallDuration.setVisibility(View.VISIBLE); + layoutCallControls.setVisibility(View.VISIBLE); + handler.post(durationRunnable); + android.util.Log.d("CallActivity", "updateUI: 已接通状态,显示计时器"); } else { - tvCallStatus.setText("正在连接..."); + // 未接通,显示等待状态 + if (isCaller) { + tvCallStatus.setText("正在呼叫..."); + } else { + tvCallStatus.setText("正在连接..."); + } } } @@ -195,6 +227,7 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS } private void onCallConnected() { + android.util.Log.d("CallActivity", "onCallConnected() 开始执行"); isConnected = true; callStartTime = System.currentTimeMillis(); @@ -203,6 +236,8 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS layoutCallControls.setVisibility(View.VISIBLE); handler.post(durationRunnable); + android.util.Log.d("CallActivity", "onCallConnected() 执行完成,通话已连接"); + Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show(); } // CallStateListener 实现 @@ -218,7 +253,13 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS @Override public void onCallConnected(String callId) { - runOnUiThread(this::onCallConnected); + android.util.Log.d("CallActivity", "========== onCallConnected 被调用 =========="); + android.util.Log.d("CallActivity", "callId: " + callId); + android.util.Log.d("CallActivity", "this.callId: " + this.callId); + runOnUiThread(() -> { + android.util.Log.d("CallActivity", "执行 onCallConnected UI更新"); + onCallConnected(); + }); } @Override diff --git a/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java b/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java index 7417202b..31b4835b 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java +++ b/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java @@ -83,10 +83,13 @@ public class CallManager implements CallSignalingClient.SignalingListener { * 连接信令服务器 */ public void connect(int userId) { + Log.d(TAG, "connect() called, userId: " + userId); if (signalingClient != null && signalingClient.isConnected()) { + Log.d(TAG, "已经连接,跳过"); return; } String baseUrl = ApiClient.getCurrentBaseUrl(context); + Log.d(TAG, "连接信令服务器,baseUrl: " + baseUrl); signalingClient = new CallSignalingClient(baseUrl, userId); signalingClient.setListener(this); signalingClient.connect(); @@ -106,6 +109,35 @@ public class CallManager implements CallSignalingClient.SignalingListener { * 发起通话 */ public void initiateCall(int calleeId, String callType, CallCallback callback) { + // 确保WebSocket已连接,这样才能收到对方的接听/拒绝通知 + String userId = com.example.livestreaming.net.AuthStore.getUserId(context); + int uid = 0; + if (userId != null && !userId.isEmpty()) { + try { + uid = (int) Double.parseDouble(userId); + } catch (NumberFormatException e) { + Log.e(TAG, "解析用户ID失败", e); + } + } + + final int finalUid = uid; + + // 如果WebSocket未连接,先连接再发起通话 + if (signalingClient == null || !signalingClient.isConnected()) { + Log.d(TAG, "发起通话前连接WebSocket, userId=" + finalUid); + connect(finalUid); + // 延迟发起通话,等待WebSocket连接 + new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { + Log.d(TAG, "WebSocket连接状态: " + (signalingClient != null && signalingClient.isConnected())); + doInitiateCall(calleeId, callType, callback); + }, 1000); + } else { + Log.d(TAG, "WebSocket已连接,直接发起通话"); + doInitiateCall(calleeId, callType, callback); + } + } + + private void doInitiateCall(int calleeId, String callType, CallCallback callback) { InitiateCallRequest request = new InitiateCallRequest(calleeId, callType); apiService.initiateCall(request).enqueue(new Callback>() { @Override @@ -119,6 +151,9 @@ public class CallManager implements CallSignalingClient.SignalingListener { otherUserId = data.getCalleeId(); otherUserName = data.getCalleeName(); otherUserAvatar = data.getCalleeAvatar(); + + Log.d(TAG, "通话创建成功: callId=" + currentCallId + ", WebSocket连接=" + + (signalingClient != null && signalingClient.isConnected())); // 启动通话界面 startCallActivity(true); @@ -142,18 +177,44 @@ public class CallManager implements CallSignalingClient.SignalingListener { * 接听通话 */ public void acceptCall(String callId) { - if (signalingClient != null) { + Log.d(TAG, "acceptCall: callId=" + callId); + + // 确保WebSocket已连接 + if (signalingClient == null || !signalingClient.isConnected()) { + Log.w(TAG, "WebSocket未连接,尝试重新连接"); + // 尝试获取当前用户ID并连接 + String userId = com.example.livestreaming.net.AuthStore.getUserId(context); + if (userId != null && !userId.isEmpty()) { + try { + int uid = (int) Double.parseDouble(userId); + connect(uid); + // 延迟发送接听消息,等待连接建立 + new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { + if (signalingClient != null && signalingClient.isConnected()) { + signalingClient.sendCallAccept(callId); + Log.d(TAG, "延迟发送接听消息成功"); + } + }, 500); + } catch (NumberFormatException e) { + Log.e(TAG, "解析用户ID失败", e); + } + } + } else { signalingClient.sendCallAccept(callId); + Log.d(TAG, "发送接听消息"); } + apiService.acceptCall(callId).enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { - Log.d(TAG, "Accept call response: " + (response.isSuccessful())); + Log.d(TAG, "Accept call API response: success=" + response.isSuccessful() + + ", code=" + response.code() + + ", body=" + (response.body() != null ? response.body().getMessage() : "null")); } @Override public void onFailure(Call> call, Throwable t) { - Log.e(TAG, "Accept call failed", t); + Log.e(TAG, "Accept call API failed: " + t.getMessage(), t); } }); } @@ -256,12 +317,12 @@ public class CallManager implements CallSignalingClient.SignalingListener { // SignalingListener 实现 @Override public void onConnected() { - Log.d(TAG, "Signaling connected"); + Log.d(TAG, "Signaling connected - WebSocket连接成功并已注册"); } @Override public void onDisconnected() { - Log.d(TAG, "Signaling disconnected"); + Log.d(TAG, "Signaling disconnected - WebSocket断开连接"); } @Override @@ -274,7 +335,12 @@ public class CallManager implements CallSignalingClient.SignalingListener { @Override public void onIncomingCall(String callId, int callerId, String callerName, String callerAvatar, String callType) { - Log.d(TAG, "Incoming call: " + callId); + Log.d(TAG, "========== 收到来电通知 =========="); + Log.d(TAG, "callId: " + callId); + Log.d(TAG, "callerId: " + callerId); + Log.d(TAG, "callerName: " + callerName); + Log.d(TAG, "callType: " + callType); + currentCallId = callId; currentCallType = callType; isCaller = false; @@ -283,6 +349,7 @@ public class CallManager implements CallSignalingClient.SignalingListener { otherUserAvatar = callerAvatar; // 启动来电界面 + Log.d(TAG, "启动来电界面 IncomingCallActivity"); Intent intent = new Intent(context, IncomingCallActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("callId", callId); @@ -299,9 +366,16 @@ public class CallManager implements CallSignalingClient.SignalingListener { @Override public void onCallAccepted(String callId) { - Log.d(TAG, "Call accepted: " + callId); + Log.d(TAG, "========== 收到通话接听通知 =========="); + Log.d(TAG, "callId: " + callId); + Log.d(TAG, "currentCallId: " + currentCallId); + Log.d(TAG, "stateListener: " + (stateListener != null ? stateListener.getClass().getSimpleName() : "null")); + if (stateListener != null) { + Log.d(TAG, "调用 stateListener.onCallConnected"); stateListener.onCallConnected(callId); + } else { + Log.w(TAG, "stateListener 为空,无法通知界面"); } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/call/IncomingCallActivity.java b/android-app/app/src/main/java/com/example/livestreaming/call/IncomingCallActivity.java index c937490b..46c4da60 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/call/IncomingCallActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/call/IncomingCallActivity.java @@ -81,6 +81,20 @@ public class IncomingCallActivity extends AppCompatActivity implements CallManag private void initCallManager() { callManager = CallManager.getInstance(this); callManager.setStateListener(this); + + // 确保WebSocket已连接(被叫方需要发送接听/拒绝消息) + String userId = com.example.livestreaming.net.AuthStore.getUserId(this); + if (userId != null && !userId.isEmpty()) { + try { + int uid = (int) Double.parseDouble(userId); + if (uid > 0) { + android.util.Log.d("IncomingCallActivity", "确保WebSocket连接,userId: " + uid); + callManager.connect(uid); + } + } catch (NumberFormatException e) { + android.util.Log.e("IncomingCallActivity", "解析用户ID失败", e); + } + } } private void setupListeners() { @@ -94,7 +108,7 @@ public class IncomingCallActivity extends AppCompatActivity implements CallManag stopRinging(); callManager.acceptCall(callId); - // 跳转到通话界面 + // 跳转到通话界面,并标记为已接通 Intent intent = new Intent(this, CallActivity.class); intent.putExtra("callId", callId); intent.putExtra("callType", callType); @@ -102,6 +116,7 @@ public class IncomingCallActivity extends AppCompatActivity implements CallManag intent.putExtra("otherUserId", callerId); intent.putExtra("otherUserName", callerName); intent.putExtra("otherUserAvatar", callerAvatar); + intent.putExtra("isConnected", true); // 被叫方接听后直接进入通话状态 startActivity(intent); finish(); }); diff --git a/android-app/app/src/main/res/layout/activity_conversation.xml b/android-app/app/src/main/res/layout/activity_conversation.xml index bf9e16d3..763efa19 100644 --- a/android-app/app/src/main/res/layout/activity_conversation.xml +++ b/android-app/app/src/main/res/layout/activity_conversation.xml @@ -52,10 +52,38 @@ android:textSize="16sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="@id/avatarView" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/voiceCallButton" app:layout_constraintStart_toEndOf="@id/avatarView" app:layout_constraintTop_toTopOf="@id/avatarView" /> + + + + + +