feat: 完善通话信令功能 - 修复主叫方和被叫方通话状态同步问题
This commit is contained in:
parent
488329c2fa
commit
0778e5c3ba
|
|
@ -1,6 +1,7 @@
|
||||||
package com.zbkj.front.config;
|
package com.zbkj.front.config;
|
||||||
|
|
||||||
import com.zbkj.front.interceptor.WebSocketAuthInterceptor;
|
import com.zbkj.front.interceptor.WebSocketAuthInterceptor;
|
||||||
|
import com.zbkj.front.websocket.CallSignalingHandler;
|
||||||
import com.zbkj.front.websocket.LiveChatHandler;
|
import com.zbkj.front.websocket.LiveChatHandler;
|
||||||
import com.zbkj.front.websocket.PrivateChatHandler;
|
import com.zbkj.front.websocket.PrivateChatHandler;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
@ -25,13 +26,16 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
||||||
|
|
||||||
private final LiveChatHandler liveChatHandler;
|
private final LiveChatHandler liveChatHandler;
|
||||||
private final PrivateChatHandler privateChatHandler;
|
private final PrivateChatHandler privateChatHandler;
|
||||||
|
private final CallSignalingHandler callSignalingHandler;
|
||||||
private final WebSocketAuthInterceptor webSocketAuthInterceptor;
|
private final WebSocketAuthInterceptor webSocketAuthInterceptor;
|
||||||
|
|
||||||
public WebSocketConfig(LiveChatHandler liveChatHandler,
|
public WebSocketConfig(LiveChatHandler liveChatHandler,
|
||||||
PrivateChatHandler privateChatHandler,
|
PrivateChatHandler privateChatHandler,
|
||||||
|
CallSignalingHandler callSignalingHandler,
|
||||||
WebSocketAuthInterceptor webSocketAuthInterceptor) {
|
WebSocketAuthInterceptor webSocketAuthInterceptor) {
|
||||||
this.liveChatHandler = liveChatHandler;
|
this.liveChatHandler = liveChatHandler;
|
||||||
this.privateChatHandler = privateChatHandler;
|
this.privateChatHandler = privateChatHandler;
|
||||||
|
this.callSignalingHandler = callSignalingHandler;
|
||||||
this.webSocketAuthInterceptor = webSocketAuthInterceptor;
|
this.webSocketAuthInterceptor = webSocketAuthInterceptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,5 +52,10 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
||||||
registry.addHandler(privateChatHandler, "/ws/chat/{conversationId}")
|
registry.addHandler(privateChatHandler, "/ws/chat/{conversationId}")
|
||||||
.addInterceptors(webSocketAuthInterceptor)
|
.addInterceptors(webSocketAuthInterceptor)
|
||||||
.setAllowedOrigins("*");
|
.setAllowedOrigins("*");
|
||||||
|
|
||||||
|
// 通话信令 WebSocket 端点: ws://host:8081/ws/call
|
||||||
|
// 用于语音/视频通话的信令交换
|
||||||
|
registry.addHandler(callSignalingHandler, "/ws/call")
|
||||||
|
.setAllowedOrigins("*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.zbkj.common.result.CommonResult;
|
||||||
import com.zbkj.front.request.call.InitiateCallRequest;
|
import com.zbkj.front.request.call.InitiateCallRequest;
|
||||||
import com.zbkj.front.response.call.CallRecordResponse;
|
import com.zbkj.front.response.call.CallRecordResponse;
|
||||||
import com.zbkj.front.response.call.InitiateCallResponse;
|
import com.zbkj.front.response.call.InitiateCallResponse;
|
||||||
|
import com.zbkj.front.websocket.CallSignalingHandler;
|
||||||
import com.zbkj.service.service.CallService;
|
import com.zbkj.service.service.CallService;
|
||||||
import com.zbkj.service.service.UserService;
|
import com.zbkj.service.service.UserService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
|
|
@ -32,6 +33,9 @@ public class CallController {
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CallSignalingHandler callSignalingHandler;
|
||||||
|
|
||||||
@ApiOperation(value = "发起通话")
|
@ApiOperation(value = "发起通话")
|
||||||
@PostMapping("/initiate")
|
@PostMapping("/initiate")
|
||||||
public CommonResult<InitiateCallResponse> initiateCall(@RequestBody @Validated InitiateCallRequest request) {
|
public CommonResult<InitiateCallResponse> initiateCall(@RequestBody @Validated InitiateCallRequest request) {
|
||||||
|
|
@ -40,6 +44,17 @@ public class CallController {
|
||||||
User callee = userService.getById(request.getCalleeId());
|
User callee = userService.getById(request.getCalleeId());
|
||||||
if (callee == null) return CommonResult.failed("被叫用户不存在");
|
if (callee == null) return CommonResult.failed("被叫用户不存在");
|
||||||
CallRecord record = callService.createCall(currentUser.getUid(), request.getCalleeId(), request.getCallType());
|
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();
|
InitiateCallResponse response = new InitiateCallResponse();
|
||||||
response.setCallId(record.getCallId());
|
response.setCallId(record.getCallId());
|
||||||
response.setCallType(record.getCallType());
|
response.setCallType(record.getCallType());
|
||||||
|
|
@ -57,6 +72,19 @@ public class CallController {
|
||||||
public CommonResult<Boolean> acceptCall(@PathVariable String callId) {
|
public CommonResult<Boolean> acceptCall(@PathVariable String callId) {
|
||||||
User currentUser = userService.getInfo();
|
User currentUser = userService.getInfo();
|
||||||
if (currentUser == null) return CommonResult.failed("请先登录");
|
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()));
|
return CommonResult.success(callService.acceptCall(callId, currentUser.getUid()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +93,14 @@ public class CallController {
|
||||||
public CommonResult<Boolean> rejectCall(@PathVariable String callId) {
|
public CommonResult<Boolean> rejectCall(@PathVariable String callId) {
|
||||||
User currentUser = userService.getInfo();
|
User currentUser = userService.getInfo();
|
||||||
if (currentUser == null) return CommonResult.failed("请先登录");
|
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()));
|
return CommonResult.success(callService.rejectCall(callId, currentUser.getUid()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,17 @@ public class ConversationController {
|
||||||
return CommonResult.success(conversationService.getConversationList(userId));
|
return CommonResult.success(conversationService.getConversationList(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个会话详情
|
||||||
|
*/
|
||||||
|
@ApiOperation(value = "获取单个会话详情")
|
||||||
|
@ApiImplicitParam(name = "id", value = "会话ID", required = true)
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public CommonResult<ConversationResponse> getConversationDetail(@PathVariable Long id) {
|
||||||
|
Integer userId = userService.getUserIdException();
|
||||||
|
return CommonResult.success(conversationService.getConversationDetail(id, userId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索会话
|
* 搜索会话
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
// 关闭旧连接
|
// 关闭旧连接
|
||||||
WebSocketSession oldSession = userCallSessions.get(userId);
|
WebSocketSession oldSession = userCallSessions.get(userId);
|
||||||
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
|
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
|
||||||
|
logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId());
|
||||||
try {
|
try {
|
||||||
oldSession.close();
|
oldSession.close();
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
|
|
@ -117,7 +118,8 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
response.put("type", "registered");
|
response.put("type", "registered");
|
||||||
response.put("userId", userId);
|
response.put("userId", userId);
|
||||||
session.sendMessage(new TextMessage(response.toString()));
|
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 {
|
private void handleCallRequest(WebSocketSession session, JsonNode json) throws IOException {
|
||||||
|
|
@ -179,30 +181,50 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
Integer userId = sessionUserMap.get(session.getId());
|
Integer userId = sessionUserMap.get(session.getId());
|
||||||
String callId = json.has("callId") ? json.get("callId").asText() : null;
|
String callId = json.has("callId") ? json.get("callId").asText() : null;
|
||||||
|
|
||||||
|
logger.info("[CallSignaling] 处理接听请求: callId={}, userId={}", callId, userId);
|
||||||
|
|
||||||
if (userId == null || callId == null) {
|
if (userId == null || callId == null) {
|
||||||
sendError(session, "参数不完整");
|
sendError(session, "参数不完整");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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);
|
joinCallSession(callId, session);
|
||||||
sessionCallMap.put(session.getId(), callId);
|
sessionCallMap.put(session.getId(), callId);
|
||||||
|
|
||||||
// 通知主叫方
|
// 通知主叫方
|
||||||
CallRecord record = callService.getByCallId(callId);
|
Integer callerId = record.getCallerId();
|
||||||
if (record != null) {
|
WebSocketSession callerSession = userCallSessions.get(callerId);
|
||||||
WebSocketSession callerSession = userCallSessions.get(record.getCallerId());
|
logger.info("[CallSignaling] 查找主叫方会话: callerId={}, session存在={}, session打开={}",
|
||||||
if (callerSession != null && callerSession.isOpen()) {
|
callerId, callerSession != null, callerSession != null && callerSession.isOpen());
|
||||||
ObjectNode notify = objectMapper.createObjectNode();
|
|
||||||
notify.put("type", "call_accepted");
|
if (callerSession != null && callerSession.isOpen()) {
|
||||||
notify.put("callId", callId);
|
ObjectNode notify = objectMapper.createObjectNode();
|
||||||
callerSession.sendMessage(new TextMessage(notify.toString()));
|
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) {
|
} catch (Exception e) {
|
||||||
|
logger.error("[CallSignaling] 接听通话异常: callId={}, error={}", callId, e.getMessage(), e);
|
||||||
sendError(session, e.getMessage());
|
sendError(session, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -445,4 +467,84 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
error.put("message", message);
|
error.put("message", message);
|
||||||
session.sendMessage(new TextMessage(error.toString()));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ public interface ConversationService extends IService<Conversation> {
|
||||||
*/
|
*/
|
||||||
List<ConversationResponse> getConversationList(Integer userId);
|
List<ConversationResponse> getConversationList(Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个会话详情
|
||||||
|
*/
|
||||||
|
ConversationResponse getConversationDetail(Long conversationId, Integer userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索会话
|
* 搜索会话
|
||||||
*/
|
*/
|
||||||
|
|
@ -48,6 +53,16 @@ public interface ConversationService extends IService<Conversation> {
|
||||||
*/
|
*/
|
||||||
Boolean deleteMessage(Long messageId, Integer userId);
|
Boolean deleteMessage(Long messageId, Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回消息
|
||||||
|
*/
|
||||||
|
Boolean recallMessage(Long messageId, Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单条消息详情
|
||||||
|
*/
|
||||||
|
ChatMessageResponse getMessageById(Long messageId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取或创建与指定用户的会话
|
* 获取或创建与指定用户的会话
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,16 @@ public class CallServiceImpl extends ServiceImpl<CallRecordDao, CallRecord> impl
|
||||||
CallRecord record = getByCallId(callId);
|
CallRecord record = getByCallId(callId);
|
||||||
if (record == null) throw new CrmebException("通话记录不存在");
|
if (record == null) throw new CrmebException("通话记录不存在");
|
||||||
if (!record.getCalleeId().equals(userId)) 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())) {
|
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();
|
Date now = new Date();
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,19 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
|
||||||
return convertToResponseList(conversations, userId);
|
return convertToResponseList(conversations, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConversationResponse getConversationDetail(Long conversationId, Integer userId) {
|
||||||
|
Conversation conversation = getById(conversationId);
|
||||||
|
if (conversation == null) {
|
||||||
|
throw new CrmebException("会话不存在");
|
||||||
|
}
|
||||||
|
// 检查用户是否是会话参与者
|
||||||
|
if (!conversation.getUser1Id().equals(userId) && !conversation.getUser2Id().equals(userId)) {
|
||||||
|
throw new CrmebException("无权限查看此会话");
|
||||||
|
}
|
||||||
|
return convertToResponse(conversation, userId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ConversationResponse> searchConversations(Integer userId, String keyword) {
|
public List<ConversationResponse> searchConversations(Integer userId, String keyword) {
|
||||||
// 先获取用户的所有会话
|
// 先获取用户的所有会话
|
||||||
|
|
@ -262,35 +275,42 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
|
||||||
private List<ConversationResponse> convertToResponseList(List<Conversation> conversations, Integer userId) {
|
private List<ConversationResponse> convertToResponseList(List<Conversation> conversations, Integer userId) {
|
||||||
List<ConversationResponse> result = new ArrayList<>();
|
List<ConversationResponse> result = new ArrayList<>();
|
||||||
for (Conversation conv : conversations) {
|
for (Conversation conv : conversations) {
|
||||||
ConversationResponse response = new ConversationResponse();
|
result.add(convertToResponse(conv, userId));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
return result;
|
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<ConversationDao, Conver
|
||||||
return privateMessageDao.selectById(messageId);
|
return privateMessageDao.selectById(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean recallMessage(Long messageId, Integer userId) {
|
||||||
|
PrivateMessage message = privateMessageDao.selectById(messageId);
|
||||||
|
if (message == null) {
|
||||||
|
throw new CrmebException("消息不存在");
|
||||||
|
}
|
||||||
|
// 只有发送者才能撤回消息
|
||||||
|
if (!message.getSenderId().equals(userId)) {
|
||||||
|
throw new CrmebException("只能撤回自己发送的消息");
|
||||||
|
}
|
||||||
|
// 检查是否在2分钟内
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long messageTime = message.getCreateTime().getTime();
|
||||||
|
long diffMinutes = (now - messageTime) / (1000 * 60);
|
||||||
|
if (diffMinutes > 2) {
|
||||||
|
throw new CrmebException("消息发送超过2分钟,无法撤回");
|
||||||
|
}
|
||||||
|
// 标记消息为已撤回
|
||||||
|
LambdaUpdateWrapper<PrivateMessage> 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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public PrivateMessage sendMessage(Long conversationId, Integer senderId, String messageType, String content, String mediaUrl) {
|
public PrivateMessage sendMessage(Long conversationId, Integer senderId, String messageType, String content, String mediaUrl) {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,14 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
context.startActivity(intent);
|
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
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -687,6 +695,159 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
binding.messageInput.setOnFocusChangeListener((v, hasFocus) -> {
|
binding.messageInput.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
if (hasFocus) scrollToBottom();
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ public class ConversationItem {
|
||||||
private final String timeText;
|
private final String timeText;
|
||||||
private final int unreadCount;
|
private final int unreadCount;
|
||||||
private final boolean muted;
|
private final boolean muted;
|
||||||
|
private int otherUserId;
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
public ConversationItem(String id, String title, String lastMessage, String timeText, int unreadCount, boolean muted) {
|
public ConversationItem(String id, String title, String lastMessage, String timeText, int unreadCount, boolean muted) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
@ -44,6 +46,22 @@ public class ConversationItem {
|
||||||
return muted;
|
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
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package com.example.livestreaming;
|
package com.example.livestreaming;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.example.livestreaming.call.CallManager;
|
||||||
|
import com.example.livestreaming.net.AuthStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义Application类,用于初始化各种组件
|
* 自定义Application类,用于初始化各种组件
|
||||||
|
|
@ -8,9 +12,13 @@ import android.app.Application;
|
||||||
*/
|
*/
|
||||||
public class LiveStreamingApplication extends Application {
|
public class LiveStreamingApplication extends Application {
|
||||||
|
|
||||||
|
private static final String TAG = "LiveStreamingApp";
|
||||||
|
private static LiveStreamingApplication instance;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
instance = this;
|
||||||
|
|
||||||
// 初始化LeakCanary内存泄漏检测(仅在debug版本中生效)
|
// 初始化LeakCanary内存泄漏检测(仅在debug版本中生效)
|
||||||
// LeakCanary会自动在debug版本中初始化,无需手动调用
|
// LeakCanary会自动在debug版本中初始化,无需手动调用
|
||||||
|
|
@ -20,5 +28,50 @@ public class LiveStreamingApplication extends Application {
|
||||||
|
|
||||||
// 初始化通知渠道
|
// 初始化通知渠道
|
||||||
LocalNotificationManager.createNotificationChannel(this);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,19 @@ public class LoginActivity extends AppCompatActivity {
|
||||||
prefs.edit().putString("profile_phone", loginData.getPhone()).apply();
|
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();
|
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,11 +137,12 @@ public class MessagesActivity extends AppCompatActivity {
|
||||||
conversationsAdapter = new ConversationsAdapter(item -> {
|
conversationsAdapter = new ConversationsAdapter(item -> {
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
try {
|
try {
|
||||||
// 启动会话页面,传递未读数量
|
// 启动会话页面,传递未读数量和对方用户ID
|
||||||
Intent intent = new Intent(this, ConversationActivity.class);
|
Intent intent = new Intent(this, ConversationActivity.class);
|
||||||
intent.putExtra("extra_conversation_id", item.getId());
|
intent.putExtra("extra_conversation_id", item.getId());
|
||||||
intent.putExtra("extra_conversation_title", item.getTitle());
|
intent.putExtra("extra_conversation_title", item.getTitle());
|
||||||
intent.putExtra("extra_unread_count", item.getUnreadCount());
|
intent.putExtra("extra_unread_count", item.getUnreadCount());
|
||||||
|
intent.putExtra("other_user_id", item.getOtherUserId());
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
|
||||||
// 用户点击会话时,减少该会话的未读数量
|
// 用户点击会话时,减少该会话的未读数量
|
||||||
|
|
@ -231,8 +232,11 @@ public class MessagesActivity extends AppCompatActivity {
|
||||||
int unreadCount = item.optInt("unreadCount", 0);
|
int unreadCount = item.optInt("unreadCount", 0);
|
||||||
boolean isMuted = item.optBoolean("muted", item.optBoolean("isMuted", false));
|
boolean isMuted = item.optBoolean("muted", item.optBoolean("isMuted", false));
|
||||||
String avatarUrl = item.optString("avatarUrl", item.optString("otherUserAvatar", ""));
|
String avatarUrl = item.optString("avatarUrl", item.optString("otherUserAvatar", ""));
|
||||||
|
int otherUserId = item.optInt("otherUserId", 0);
|
||||||
|
|
||||||
ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted);
|
ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted);
|
||||||
|
convItem.setOtherUserId(otherUserId);
|
||||||
|
convItem.setAvatarUrl(avatarUrl);
|
||||||
allConversations.add(convItem);
|
allConversations.add(convItem);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "解析会话项失败", e);
|
Log.e(TAG, "解析会话项失败", e);
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,33 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
||||||
otherUserId = getIntent().getIntExtra("otherUserId", 0);
|
otherUserId = getIntent().getIntExtra("otherUserId", 0);
|
||||||
otherUserName = getIntent().getStringExtra("otherUserName");
|
otherUserName = getIntent().getStringExtra("otherUserName");
|
||||||
otherUserAvatar = getIntent().getStringExtra("otherUserAvatar");
|
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() {
|
private void initCallManager() {
|
||||||
callManager = CallManager.getInstance(this);
|
callManager = CallManager.getInstance(this);
|
||||||
callManager.setStateListener(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() {
|
private void setupListeners() {
|
||||||
|
|
@ -159,11 +181,21 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
||||||
layoutVideoToggle.setVisibility(isVideo ? View.VISIBLE : View.GONE);
|
layoutVideoToggle.setVisibility(isVideo ? View.VISIBLE : View.GONE);
|
||||||
btnSwitchCamera.setVisibility(isVideo ? View.VISIBLE : View.GONE);
|
btnSwitchCamera.setVisibility(isVideo ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
// 设置通话状态
|
// 根据连接状态设置界面
|
||||||
if (isCaller) {
|
if (isConnected) {
|
||||||
tvCallStatus.setText("正在呼叫...");
|
// 已接通,显示通话中界面
|
||||||
|
tvCallStatus.setVisibility(View.GONE);
|
||||||
|
tvCallDuration.setVisibility(View.VISIBLE);
|
||||||
|
layoutCallControls.setVisibility(View.VISIBLE);
|
||||||
|
handler.post(durationRunnable);
|
||||||
|
android.util.Log.d("CallActivity", "updateUI: 已接通状态,显示计时器");
|
||||||
} else {
|
} 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() {
|
private void onCallConnected() {
|
||||||
|
android.util.Log.d("CallActivity", "onCallConnected() 开始执行");
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
callStartTime = System.currentTimeMillis();
|
callStartTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
|
@ -203,6 +236,8 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
||||||
layoutCallControls.setVisibility(View.VISIBLE);
|
layoutCallControls.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
handler.post(durationRunnable);
|
handler.post(durationRunnable);
|
||||||
|
android.util.Log.d("CallActivity", "onCallConnected() 执行完成,通话已连接");
|
||||||
|
Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallStateListener 实现
|
// CallStateListener 实现
|
||||||
|
|
@ -218,7 +253,13 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCallConnected(String callId) {
|
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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,13 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
* 连接信令服务器
|
* 连接信令服务器
|
||||||
*/
|
*/
|
||||||
public void connect(int userId) {
|
public void connect(int userId) {
|
||||||
|
Log.d(TAG, "connect() called, userId: " + userId);
|
||||||
if (signalingClient != null && signalingClient.isConnected()) {
|
if (signalingClient != null && signalingClient.isConnected()) {
|
||||||
|
Log.d(TAG, "已经连接,跳过");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String baseUrl = ApiClient.getCurrentBaseUrl(context);
|
String baseUrl = ApiClient.getCurrentBaseUrl(context);
|
||||||
|
Log.d(TAG, "连接信令服务器,baseUrl: " + baseUrl);
|
||||||
signalingClient = new CallSignalingClient(baseUrl, userId);
|
signalingClient = new CallSignalingClient(baseUrl, userId);
|
||||||
signalingClient.setListener(this);
|
signalingClient.setListener(this);
|
||||||
signalingClient.connect();
|
signalingClient.connect();
|
||||||
|
|
@ -106,6 +109,35 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
* 发起通话
|
* 发起通话
|
||||||
*/
|
*/
|
||||||
public void initiateCall(int calleeId, String callType, CallCallback callback) {
|
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);
|
InitiateCallRequest request = new InitiateCallRequest(calleeId, callType);
|
||||||
apiService.initiateCall(request).enqueue(new Callback<CallApiResponse<InitiateCallResponse>>() {
|
apiService.initiateCall(request).enqueue(new Callback<CallApiResponse<InitiateCallResponse>>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -120,6 +152,9 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
otherUserName = data.getCalleeName();
|
otherUserName = data.getCalleeName();
|
||||||
otherUserAvatar = data.getCalleeAvatar();
|
otherUserAvatar = data.getCalleeAvatar();
|
||||||
|
|
||||||
|
Log.d(TAG, "通话创建成功: callId=" + currentCallId + ", WebSocket连接=" +
|
||||||
|
(signalingClient != null && signalingClient.isConnected()));
|
||||||
|
|
||||||
// 启动通话界面
|
// 启动通话界面
|
||||||
startCallActivity(true);
|
startCallActivity(true);
|
||||||
|
|
||||||
|
|
@ -142,18 +177,44 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
* 接听通话
|
* 接听通话
|
||||||
*/
|
*/
|
||||||
public void acceptCall(String callId) {
|
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);
|
signalingClient.sendCallAccept(callId);
|
||||||
|
Log.d(TAG, "发送接听消息");
|
||||||
}
|
}
|
||||||
|
|
||||||
apiService.acceptCall(callId).enqueue(new Callback<CallApiResponse<Boolean>>() {
|
apiService.acceptCall(callId).enqueue(new Callback<CallApiResponse<Boolean>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<CallApiResponse<Boolean>> call, Response<CallApiResponse<Boolean>> response) {
|
public void onResponse(Call<CallApiResponse<Boolean>> call, Response<CallApiResponse<Boolean>> 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
|
@Override
|
||||||
public void onFailure(Call<CallApiResponse<Boolean>> call, Throwable t) {
|
public void onFailure(Call<CallApiResponse<Boolean>> 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 实现
|
// SignalingListener 实现
|
||||||
@Override
|
@Override
|
||||||
public void onConnected() {
|
public void onConnected() {
|
||||||
Log.d(TAG, "Signaling connected");
|
Log.d(TAG, "Signaling connected - WebSocket连接成功并已注册");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisconnected() {
|
public void onDisconnected() {
|
||||||
Log.d(TAG, "Signaling disconnected");
|
Log.d(TAG, "Signaling disconnected - WebSocket断开连接");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -274,7 +335,12 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onIncomingCall(String callId, int callerId, String callerName, String callerAvatar, String callType) {
|
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;
|
currentCallId = callId;
|
||||||
currentCallType = callType;
|
currentCallType = callType;
|
||||||
isCaller = false;
|
isCaller = false;
|
||||||
|
|
@ -283,6 +349,7 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
otherUserAvatar = callerAvatar;
|
otherUserAvatar = callerAvatar;
|
||||||
|
|
||||||
// 启动来电界面
|
// 启动来电界面
|
||||||
|
Log.d(TAG, "启动来电界面 IncomingCallActivity");
|
||||||
Intent intent = new Intent(context, IncomingCallActivity.class);
|
Intent intent = new Intent(context, IncomingCallActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.putExtra("callId", callId);
|
intent.putExtra("callId", callId);
|
||||||
|
|
@ -299,9 +366,16 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCallAccepted(String callId) {
|
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) {
|
if (stateListener != null) {
|
||||||
|
Log.d(TAG, "调用 stateListener.onCallConnected");
|
||||||
stateListener.onCallConnected(callId);
|
stateListener.onCallConnected(callId);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "stateListener 为空,无法通知界面");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,20 @@ public class IncomingCallActivity extends AppCompatActivity implements CallManag
|
||||||
private void initCallManager() {
|
private void initCallManager() {
|
||||||
callManager = CallManager.getInstance(this);
|
callManager = CallManager.getInstance(this);
|
||||||
callManager.setStateListener(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() {
|
private void setupListeners() {
|
||||||
|
|
@ -94,7 +108,7 @@ public class IncomingCallActivity extends AppCompatActivity implements CallManag
|
||||||
stopRinging();
|
stopRinging();
|
||||||
callManager.acceptCall(callId);
|
callManager.acceptCall(callId);
|
||||||
|
|
||||||
// 跳转到通话界面
|
// 跳转到通话界面,并标记为已接通
|
||||||
Intent intent = new Intent(this, CallActivity.class);
|
Intent intent = new Intent(this, CallActivity.class);
|
||||||
intent.putExtra("callId", callId);
|
intent.putExtra("callId", callId);
|
||||||
intent.putExtra("callType", callType);
|
intent.putExtra("callType", callType);
|
||||||
|
|
@ -102,6 +116,7 @@ public class IncomingCallActivity extends AppCompatActivity implements CallManag
|
||||||
intent.putExtra("otherUserId", callerId);
|
intent.putExtra("otherUserId", callerId);
|
||||||
intent.putExtra("otherUserName", callerName);
|
intent.putExtra("otherUserName", callerName);
|
||||||
intent.putExtra("otherUserAvatar", callerAvatar);
|
intent.putExtra("otherUserAvatar", callerAvatar);
|
||||||
|
intent.putExtra("isConnected", true); // 被叫方接听后直接进入通话状态
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,38 @@
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/avatarView"
|
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_constraintStart_toEndOf="@id/avatarView"
|
||||||
app:layout_constraintTop_toTopOf="@id/avatarView" />
|
app:layout_constraintTop_toTopOf="@id/avatarView" />
|
||||||
|
|
||||||
|
<!-- 语音通话按钮 -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/voiceCallButton"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="语音通话"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_call"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/videoCallButton"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="#FF6B6B" />
|
||||||
|
|
||||||
|
<!-- 视频通话按钮 -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/videoCallButton"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="视频通话"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_videocam"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="#FF6B6B" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user