通信
This commit is contained in:
parent
c8aaa31212
commit
bae216261a
|
|
@ -0,0 +1,112 @@
|
||||||
|
package com.zbkj.common.model.call;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通话记录实体类
|
||||||
|
* 同时支持 MyBatis-Plus 和 JPA
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@TableName("eb_call_record")
|
||||||
|
@Entity
|
||||||
|
@Table(name = "eb_call_record", indexes = {
|
||||||
|
@Index(name = "idx_caller_id", columnList = "caller_id"),
|
||||||
|
@Index(name = "idx_callee_id", columnList = "callee_id"),
|
||||||
|
@Index(name = "idx_call_time", columnList = "call_time"),
|
||||||
|
@Index(name = "idx_status", columnList = "status")
|
||||||
|
})
|
||||||
|
@ApiModel(value = "CallRecord对象", description = "通话记录")
|
||||||
|
public class CallRecord implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话记录ID")
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话唯一标识(用于信令)")
|
||||||
|
@TableField("call_id")
|
||||||
|
@Column(name = "call_id", length = 64, nullable = false, unique = true)
|
||||||
|
private String callId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "主叫用户ID")
|
||||||
|
@TableField("caller_id")
|
||||||
|
@Column(name = "caller_id", nullable = false)
|
||||||
|
private Integer callerId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "被叫用户ID")
|
||||||
|
@TableField("callee_id")
|
||||||
|
@Column(name = "callee_id", nullable = false)
|
||||||
|
private Integer calleeId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话类型: voice-语音, video-视频")
|
||||||
|
@TableField("call_type")
|
||||||
|
@Column(name = "call_type", length = 20, nullable = false)
|
||||||
|
private String callType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话状态: calling-呼叫中, ringing-响铃中, connected-通话中, ended-已结束, missed-未接, rejected-已拒绝, cancelled-已取消, busy-忙线")
|
||||||
|
@TableField("status")
|
||||||
|
@Column(name = "status", length = 20, nullable = false)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话发起时间")
|
||||||
|
@TableField("call_time")
|
||||||
|
@Column(name = "call_time", nullable = false)
|
||||||
|
private Date callTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话接通时间")
|
||||||
|
@TableField("connect_time")
|
||||||
|
@Column(name = "connect_time")
|
||||||
|
private Date connectTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话结束时间")
|
||||||
|
@TableField("end_time")
|
||||||
|
@Column(name = "end_time")
|
||||||
|
private Date endTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话时长(秒)")
|
||||||
|
@TableField("duration")
|
||||||
|
@Column(name = "duration", columnDefinition = "INT DEFAULT 0")
|
||||||
|
private Integer duration;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "结束原因: normal-正常结束, timeout-超时, network_error-网络错误, user_hangup-用户挂断")
|
||||||
|
@TableField("end_reason")
|
||||||
|
@Column(name = "end_reason", length = 50)
|
||||||
|
private String endReason;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "主叫是否删除记录")
|
||||||
|
@TableField("caller_deleted")
|
||||||
|
@Column(name = "caller_deleted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
|
private Boolean callerDeleted;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "被叫是否删除记录")
|
||||||
|
@TableField("callee_deleted")
|
||||||
|
@Column(name = "callee_deleted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
|
private Boolean calleeDeleted;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间")
|
||||||
|
@TableField("create_time")
|
||||||
|
@Column(name = "create_time")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间")
|
||||||
|
@TableField("update_time")
|
||||||
|
@Column(name = "update_time")
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package com.zbkj.front.controller;
|
||||||
|
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.zbkj.common.model.call.CallRecord;
|
||||||
|
import com.zbkj.common.model.user.User;
|
||||||
|
import com.zbkj.common.request.PageParamRequest;
|
||||||
|
import com.zbkj.common.response.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.service.service.CallService;
|
||||||
|
import com.zbkj.service.service.UserService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/front/call")
|
||||||
|
@Api(tags = "通话管理")
|
||||||
|
public class CallController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CallService callService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@ApiOperation(value = "发起通话")
|
||||||
|
@PostMapping("/initiate")
|
||||||
|
public CommonResult<InitiateCallResponse> initiateCall(@RequestBody @Validated InitiateCallRequest request) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
User callee = userService.getById(request.getCalleeId());
|
||||||
|
if (callee == null) return CommonResult.failed("被叫用户不存在");
|
||||||
|
CallRecord record = callService.createCall(currentUser.getUid(), request.getCalleeId(), request.getCallType());
|
||||||
|
InitiateCallResponse response = new InitiateCallResponse();
|
||||||
|
response.setCallId(record.getCallId());
|
||||||
|
response.setCallType(record.getCallType());
|
||||||
|
response.setCalleeId(callee.getUid());
|
||||||
|
response.setCalleeName(callee.getNickname());
|
||||||
|
response.setCalleeAvatar(callee.getAvatar());
|
||||||
|
response.setStatus(record.getStatus());
|
||||||
|
response.setSignalingUrl("/ws/call/" + record.getCallId());
|
||||||
|
return CommonResult.success(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ApiOperation(value = "接听通话")
|
||||||
|
@PostMapping("/accept/{callId}")
|
||||||
|
public CommonResult<Boolean> acceptCall(@PathVariable String callId) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
return CommonResult.success(callService.acceptCall(callId, currentUser.getUid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "拒绝通话")
|
||||||
|
@PostMapping("/reject/{callId}")
|
||||||
|
public CommonResult<Boolean> rejectCall(@PathVariable String callId) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
return CommonResult.success(callService.rejectCall(callId, currentUser.getUid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "取消通话")
|
||||||
|
@PostMapping("/cancel/{callId}")
|
||||||
|
public CommonResult<Boolean> cancelCall(@PathVariable String callId) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
return CommonResult.success(callService.cancelCall(callId, currentUser.getUid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "结束通话")
|
||||||
|
@PostMapping("/end/{callId}")
|
||||||
|
public CommonResult<Boolean> endCall(@PathVariable String callId, @RequestParam(required = false) String endReason) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
return CommonResult.success(callService.endCall(callId, currentUser.getUid(), endReason));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "获取通话记录列表")
|
||||||
|
@GetMapping("/history")
|
||||||
|
public CommonResult<PageInfo<CallRecordResponse>> getCallHistory(@Validated PageParamRequest pageParamRequest) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
List<CallRecord> records = callService.getCallHistory(currentUser.getUid(), pageParamRequest);
|
||||||
|
List<CallRecordResponse> responseList = new ArrayList<>();
|
||||||
|
Map<Integer, User> userCache = new HashMap<>();
|
||||||
|
for (CallRecord record : records) {
|
||||||
|
if (!userCache.containsKey(record.getCallerId())) {
|
||||||
|
User user = userService.getById(record.getCallerId());
|
||||||
|
if (user != null) userCache.put(record.getCallerId(), user);
|
||||||
|
}
|
||||||
|
if (!userCache.containsKey(record.getCalleeId())) {
|
||||||
|
User user = userService.getById(record.getCalleeId());
|
||||||
|
if (user != null) userCache.put(record.getCalleeId(), user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (CallRecord record : records) {
|
||||||
|
responseList.add(convertToResponse(record, currentUser.getUid(), userCache));
|
||||||
|
}
|
||||||
|
PageInfo<CallRecordResponse> pageInfo = new PageInfo<>(responseList);
|
||||||
|
return CommonResult.success(pageInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "删除通话记录")
|
||||||
|
@DeleteMapping("/record/{recordId}")
|
||||||
|
public CommonResult<Boolean> deleteRecord(@PathVariable Long recordId) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
return CommonResult.success(callService.deleteRecord(recordId, currentUser.getUid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "获取未接来电数量")
|
||||||
|
@GetMapping("/missed/count")
|
||||||
|
public CommonResult<Integer> getMissedCallCount() {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
return CommonResult.success(callService.getMissedCallCount(currentUser.getUid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "检查是否正在通话中")
|
||||||
|
@GetMapping("/status")
|
||||||
|
public CommonResult<Map<String, Object>> getCallStatus() {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("inCall", callService.isUserInCall(currentUser.getUid()));
|
||||||
|
CallRecord currentCall = callService.getCurrentCall(currentUser.getUid());
|
||||||
|
if (currentCall != null) {
|
||||||
|
result.put("callId", currentCall.getCallId());
|
||||||
|
result.put("callType", currentCall.getCallType());
|
||||||
|
result.put("status", currentCall.getStatus());
|
||||||
|
result.put("isCaller", currentCall.getCallerId().equals(currentUser.getUid()));
|
||||||
|
}
|
||||||
|
return CommonResult.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "获取通话详情")
|
||||||
|
@GetMapping("/detail/{callId}")
|
||||||
|
public CommonResult<CallRecordResponse> getCallDetail(@PathVariable String callId) {
|
||||||
|
User currentUser = userService.getInfo();
|
||||||
|
if (currentUser == null) return CommonResult.failed("请先登录");
|
||||||
|
CallRecord record = callService.getByCallId(callId);
|
||||||
|
if (record == null) return CommonResult.failed("通话记录不存在");
|
||||||
|
if (!record.getCallerId().equals(currentUser.getUid()) && !record.getCalleeId().equals(currentUser.getUid())) {
|
||||||
|
return CommonResult.failed("无权查看此通话记录");
|
||||||
|
}
|
||||||
|
Map<Integer, User> userCache = new HashMap<>();
|
||||||
|
User caller = userService.getById(record.getCallerId());
|
||||||
|
User callee = userService.getById(record.getCalleeId());
|
||||||
|
if (caller != null) userCache.put(caller.getUid(), caller);
|
||||||
|
if (callee != null) userCache.put(callee.getUid(), callee);
|
||||||
|
return CommonResult.success(convertToResponse(record, currentUser.getUid(), userCache));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CallRecordResponse convertToResponse(CallRecord record, Integer currentUserId, Map<Integer, User> userCache) {
|
||||||
|
CallRecordResponse response = new CallRecordResponse();
|
||||||
|
response.setId(record.getId());
|
||||||
|
response.setCallId(record.getCallId());
|
||||||
|
response.setCallerId(record.getCallerId());
|
||||||
|
response.setCalleeId(record.getCalleeId());
|
||||||
|
response.setCallType(record.getCallType());
|
||||||
|
response.setStatus(record.getStatus());
|
||||||
|
response.setCallTime(record.getCallTime() != null ? record.getCallTime().getTime() : null);
|
||||||
|
response.setConnectTime(record.getConnectTime() != null ? record.getConnectTime().getTime() : null);
|
||||||
|
response.setEndTime(record.getEndTime() != null ? record.getEndTime().getTime() : null);
|
||||||
|
response.setDuration(record.getDuration());
|
||||||
|
response.setIsOutgoing(record.getCallerId().equals(currentUserId));
|
||||||
|
User caller = userCache.get(record.getCallerId());
|
||||||
|
if (caller != null) {
|
||||||
|
response.setCallerName(caller.getNickname());
|
||||||
|
response.setCallerAvatar(caller.getAvatar());
|
||||||
|
}
|
||||||
|
User callee = userCache.get(record.getCalleeId());
|
||||||
|
if (callee != null) {
|
||||||
|
response.setCalleeName(callee.getNickname());
|
||||||
|
response.setCalleeAvatar(callee.getAvatar());
|
||||||
|
}
|
||||||
|
Integer otherUserId = record.getCallerId().equals(currentUserId) ? record.getCalleeId() : record.getCallerId();
|
||||||
|
response.setOtherUserId(otherUserId);
|
||||||
|
User otherUser = userCache.get(otherUserId);
|
||||||
|
if (otherUser != null) {
|
||||||
|
response.setOtherUserName(otherUser.getNickname());
|
||||||
|
response.setOtherUserAvatar(otherUser.getAvatar());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.zbkj.front.request.call;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InitiateCallRequest", description = "发起通话请求")
|
||||||
|
public class InitiateCallRequest {
|
||||||
|
@ApiModelProperty(value = "被叫用户ID", required = true)
|
||||||
|
@NotNull(message = "被叫用户ID不能为空")
|
||||||
|
private Integer calleeId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "通话类型: voice-语音, video-视频", required = true)
|
||||||
|
@NotNull(message = "通话类型不能为空")
|
||||||
|
private String callType;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.zbkj.front.response.call;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "CallRecordResponse", description = "通话记录响应")
|
||||||
|
public class CallRecordResponse {
|
||||||
|
@ApiModelProperty(value = "记录ID")
|
||||||
|
private Long id;
|
||||||
|
@ApiModelProperty(value = "通话唯一标识")
|
||||||
|
private String callId;
|
||||||
|
@ApiModelProperty(value = "主叫用户ID")
|
||||||
|
private Integer callerId;
|
||||||
|
@ApiModelProperty(value = "主叫用户昵称")
|
||||||
|
private String callerName;
|
||||||
|
@ApiModelProperty(value = "主叫用户头像")
|
||||||
|
private String callerAvatar;
|
||||||
|
@ApiModelProperty(value = "被叫用户ID")
|
||||||
|
private Integer calleeId;
|
||||||
|
@ApiModelProperty(value = "被叫用户昵称")
|
||||||
|
private String calleeName;
|
||||||
|
@ApiModelProperty(value = "被叫用户头像")
|
||||||
|
private String calleeAvatar;
|
||||||
|
@ApiModelProperty(value = "通话类型: voice-语音, video-视频")
|
||||||
|
private String callType;
|
||||||
|
@ApiModelProperty(value = "通话状态")
|
||||||
|
private String status;
|
||||||
|
@ApiModelProperty(value = "通话发起时间戳")
|
||||||
|
private Long callTime;
|
||||||
|
@ApiModelProperty(value = "通话接通时间戳")
|
||||||
|
private Long connectTime;
|
||||||
|
@ApiModelProperty(value = "通话结束时间戳")
|
||||||
|
private Long endTime;
|
||||||
|
@ApiModelProperty(value = "通话时长(秒)")
|
||||||
|
private Integer duration;
|
||||||
|
@ApiModelProperty(value = "是否为呼出(当前用户是主叫)")
|
||||||
|
private Boolean isOutgoing;
|
||||||
|
@ApiModelProperty(value = "对方用户ID")
|
||||||
|
private Integer otherUserId;
|
||||||
|
@ApiModelProperty(value = "对方用户昵称")
|
||||||
|
private String otherUserName;
|
||||||
|
@ApiModelProperty(value = "对方用户头像")
|
||||||
|
private String otherUserAvatar;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.zbkj.front.response.call;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "InitiateCallResponse", description = "发起通话响应")
|
||||||
|
public class InitiateCallResponse {
|
||||||
|
@ApiModelProperty(value = "通话唯一标识")
|
||||||
|
private String callId;
|
||||||
|
@ApiModelProperty(value = "通话类型")
|
||||||
|
private String callType;
|
||||||
|
@ApiModelProperty(value = "被叫用户ID")
|
||||||
|
private Integer calleeId;
|
||||||
|
@ApiModelProperty(value = "被叫用户昵称")
|
||||||
|
private String calleeName;
|
||||||
|
@ApiModelProperty(value = "被叫用户头像")
|
||||||
|
private String calleeAvatar;
|
||||||
|
@ApiModelProperty(value = "通话状态")
|
||||||
|
private String status;
|
||||||
|
@ApiModelProperty(value = "WebSocket信令地址")
|
||||||
|
private String signalingUrl;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.zbkj.front.websocket;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.zbkj.common.model.call.CallRecord;
|
||||||
|
import com.zbkj.common.model.user.User;
|
||||||
|
import com.zbkj.service.service.CallService;
|
||||||
|
import com.zbkj.service.service.UserService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CallSignalingHandler.class);
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private static final long CALL_TIMEOUT = 60 * 1000;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CallService callService;
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
private final Map<String, Set<WebSocketSession>> callSessions = new ConcurrentHashMap<>();
|
||||||
|
private final Map<Integer, WebSocketSession> userCallSessions = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Integer> sessionUserMap = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, String> sessionCallMap = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Long> callCreateTime = new ConcurrentHashMap<>();
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.zbkj.service.dao;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.zbkj.common.model.call.CallRecord;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通话记录数据访问层
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface CallRecordDao extends BaseMapper<CallRecord> {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.zbkj.service.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.zbkj.common.model.call.CallRecord;
|
||||||
|
import com.zbkj.common.request.PageParamRequest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通话服务接口
|
||||||
|
*/
|
||||||
|
public interface CallService extends IService<CallRecord> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建通话记录
|
||||||
|
*/
|
||||||
|
CallRecord createCall(Integer callerId, Integer calleeId, String callType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据callId获取通话记录
|
||||||
|
*/
|
||||||
|
CallRecord getByCallId(String callId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新通话状态
|
||||||
|
*/
|
||||||
|
Boolean updateStatus(String callId, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接听通话
|
||||||
|
*/
|
||||||
|
Boolean acceptCall(String callId, Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝通话
|
||||||
|
*/
|
||||||
|
Boolean rejectCall(String callId, Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消通话
|
||||||
|
*/
|
||||||
|
Boolean cancelCall(String callId, Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束通话
|
||||||
|
*/
|
||||||
|
Boolean endCall(String callId, Integer userId, String endReason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通话超时(未接听)
|
||||||
|
*/
|
||||||
|
Boolean missedCall(String callId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户通话记录列表
|
||||||
|
*/
|
||||||
|
List<CallRecord> getCallHistory(Integer userId, PageParamRequest pageParamRequest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除通话记录(软删除)
|
||||||
|
*/
|
||||||
|
Boolean deleteRecord(Long recordId, Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户未接来电数量
|
||||||
|
*/
|
||||||
|
Integer getMissedCallCount(Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否正在通话中
|
||||||
|
*/
|
||||||
|
Boolean isUserInCall(Integer userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户当前进行中的通话
|
||||||
|
*/
|
||||||
|
CallRecord getCurrentCall(Integer userId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
package com.zbkj.service.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.github.pagehelper.PageHelper;
|
||||||
|
import com.zbkj.common.exception.CrmebException;
|
||||||
|
import com.zbkj.common.model.call.CallRecord;
|
||||||
|
import com.zbkj.common.request.PageParamRequest;
|
||||||
|
import com.zbkj.service.dao.CallRecordDao;
|
||||||
|
import com.zbkj.service.service.CallService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通话服务实现类
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CallServiceImpl extends ServiceImpl<CallRecordDao, CallRecord> implements CallService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CallServiceImpl.class);
|
||||||
|
|
||||||
|
public static final String STATUS_CALLING = "calling";
|
||||||
|
public static final String STATUS_RINGING = "ringing";
|
||||||
|
public static final String STATUS_CONNECTED = "connected";
|
||||||
|
public static final String STATUS_ENDED = "ended";
|
||||||
|
public static final String STATUS_MISSED = "missed";
|
||||||
|
public static final String STATUS_REJECTED = "rejected";
|
||||||
|
public static final String STATUS_CANCELLED = "cancelled";
|
||||||
|
public static final String STATUS_BUSY = "busy";
|
||||||
|
|
||||||
|
public static final String TYPE_VOICE = "voice";
|
||||||
|
public static final String TYPE_VIDEO = "video";
|
||||||
|
|
||||||
|
private static final List<String> ACTIVE_STATUSES = Arrays.asList(STATUS_CALLING, STATUS_RINGING, STATUS_CONNECTED);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public CallRecord createCall(Integer callerId, Integer calleeId, String callType) {
|
||||||
|
if (callerId == null || calleeId == null) {
|
||||||
|
throw new CrmebException("用户ID不能为空");
|
||||||
|
}
|
||||||
|
if (callerId.equals(calleeId)) {
|
||||||
|
throw new CrmebException("不能呼叫自己");
|
||||||
|
}
|
||||||
|
if (!TYPE_VOICE.equals(callType) && !TYPE_VIDEO.equals(callType)) {
|
||||||
|
throw new CrmebException("无效的通话类型");
|
||||||
|
}
|
||||||
|
if (isUserInCall(callerId)) {
|
||||||
|
throw new CrmebException("您正在通话中,无法发起新通话");
|
||||||
|
}
|
||||||
|
if (isUserInCall(calleeId)) {
|
||||||
|
throw new CrmebException("对方正在通话中");
|
||||||
|
}
|
||||||
|
|
||||||
|
CallRecord record = new CallRecord();
|
||||||
|
record.setCallId("call_" + UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
record.setCallerId(callerId);
|
||||||
|
record.setCalleeId(calleeId);
|
||||||
|
record.setCallType(callType);
|
||||||
|
record.setStatus(STATUS_CALLING);
|
||||||
|
record.setCallTime(new Date());
|
||||||
|
record.setDuration(0);
|
||||||
|
record.setCallerDeleted(false);
|
||||||
|
record.setCalleeDeleted(false);
|
||||||
|
record.setCreateTime(new Date());
|
||||||
|
record.setUpdateTime(new Date());
|
||||||
|
|
||||||
|
save(record);
|
||||||
|
logger.info("[Call] 创建通话: callId={}, caller={}, callee={}", record.getCallId(), callerId, calleeId);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallRecord getByCallId(String callId) {
|
||||||
|
LambdaQueryWrapper<CallRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCallId, callId);
|
||||||
|
return getOne(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean updateStatus(String callId, String status) {
|
||||||
|
LambdaUpdateWrapper<CallRecord> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCallId, callId)
|
||||||
|
.set(CallRecord::getStatus, status)
|
||||||
|
.set(CallRecord::getUpdateTime, new Date());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean acceptCall(String callId, Integer userId) {
|
||||||
|
CallRecord record = getByCallId(callId);
|
||||||
|
if (record == null) throw new CrmebException("通话记录不存在");
|
||||||
|
if (!record.getCalleeId().equals(userId)) throw new CrmebException("无权操作此通话");
|
||||||
|
if (!STATUS_CALLING.equals(record.getStatus()) && !STATUS_RINGING.equals(record.getStatus())) {
|
||||||
|
throw new CrmebException("通话状态不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
LambdaUpdateWrapper<CallRecord> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCallId, callId)
|
||||||
|
.set(CallRecord::getStatus, STATUS_CONNECTED)
|
||||||
|
.set(CallRecord::getConnectTime, now)
|
||||||
|
.set(CallRecord::getUpdateTime, now);
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean rejectCall(String callId, Integer userId) {
|
||||||
|
CallRecord record = getByCallId(callId);
|
||||||
|
if (record == null) throw new CrmebException("通话记录不存在");
|
||||||
|
if (!record.getCalleeId().equals(userId)) throw new CrmebException("无权操作此通话");
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
LambdaUpdateWrapper<CallRecord> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCallId, callId)
|
||||||
|
.set(CallRecord::getStatus, STATUS_REJECTED)
|
||||||
|
.set(CallRecord::getEndTime, now)
|
||||||
|
.set(CallRecord::getEndReason, "user_reject")
|
||||||
|
.set(CallRecord::getUpdateTime, now);
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean cancelCall(String callId, Integer userId) {
|
||||||
|
CallRecord record = getByCallId(callId);
|
||||||
|
if (record == null) throw new CrmebException("通话记录不存在");
|
||||||
|
if (!record.getCallerId().equals(userId)) throw new CrmebException("无权操作此通话");
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
LambdaUpdateWrapper<CallRecord> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCallId, callId)
|
||||||
|
.set(CallRecord::getStatus, STATUS_CANCELLED)
|
||||||
|
.set(CallRecord::getEndTime, now)
|
||||||
|
.set(CallRecord::getEndReason, "user_cancel")
|
||||||
|
.set(CallRecord::getUpdateTime, now);
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean endCall(String callId, Integer userId, String endReason) {
|
||||||
|
CallRecord record = getByCallId(callId);
|
||||||
|
if (record == null) throw new CrmebException("通话记录不存在");
|
||||||
|
if (!record.getCallerId().equals(userId) && !record.getCalleeId().equals(userId)) {
|
||||||
|
throw new CrmebException("无权操作此通话");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
int duration = 0;
|
||||||
|
if (record.getConnectTime() != null) {
|
||||||
|
duration = (int) ((now.getTime() - record.getConnectTime().getTime()) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<CallRecord> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCallId, callId)
|
||||||
|
.set(CallRecord::getStatus, STATUS_ENDED)
|
||||||
|
.set(CallRecord::getEndTime, now)
|
||||||
|
.set(CallRecord::getDuration, duration)
|
||||||
|
.set(CallRecord::getEndReason, endReason != null ? endReason : "normal")
|
||||||
|
.set(CallRecord::getUpdateTime, now);
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean missedCall(String callId) {
|
||||||
|
CallRecord record = getByCallId(callId);
|
||||||
|
if (record == null) return false;
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
LambdaUpdateWrapper<CallRecord> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCallId, callId)
|
||||||
|
.set(CallRecord::getStatus, STATUS_MISSED)
|
||||||
|
.set(CallRecord::getEndTime, now)
|
||||||
|
.set(CallRecord::getEndReason, "timeout")
|
||||||
|
.set(CallRecord::getUpdateTime, now);
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CallRecord> getCallHistory(Integer userId, PageParamRequest pageParamRequest) {
|
||||||
|
PageHelper.startPage(pageParamRequest.getPage(), pageParamRequest.getLimit());
|
||||||
|
LambdaQueryWrapper<CallRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.and(w -> w
|
||||||
|
.and(inner -> inner.eq(CallRecord::getCallerId, userId).eq(CallRecord::getCallerDeleted, false))
|
||||||
|
.or(inner -> inner.eq(CallRecord::getCalleeId, userId).eq(CallRecord::getCalleeDeleted, false))
|
||||||
|
);
|
||||||
|
wrapper.orderByDesc(CallRecord::getCallTime);
|
||||||
|
return list(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean deleteRecord(Long recordId, Integer userId) {
|
||||||
|
CallRecord record = getById(recordId);
|
||||||
|
if (record == null) throw new CrmebException("通话记录不存在");
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<CallRecord> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getId, recordId);
|
||||||
|
|
||||||
|
if (record.getCallerId().equals(userId)) {
|
||||||
|
wrapper.set(CallRecord::getCallerDeleted, true);
|
||||||
|
} else if (record.getCalleeId().equals(userId)) {
|
||||||
|
wrapper.set(CallRecord::getCalleeDeleted, true);
|
||||||
|
} else {
|
||||||
|
throw new CrmebException("无权删除此记录");
|
||||||
|
}
|
||||||
|
wrapper.set(CallRecord::getUpdateTime, new Date());
|
||||||
|
return update(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getMissedCallCount(Integer userId) {
|
||||||
|
LambdaQueryWrapper<CallRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(CallRecord::getCalleeId, userId)
|
||||||
|
.eq(CallRecord::getStatus, STATUS_MISSED)
|
||||||
|
.eq(CallRecord::getCalleeDeleted, false);
|
||||||
|
return count(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean isUserInCall(Integer userId) {
|
||||||
|
LambdaQueryWrapper<CallRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.and(w -> w.eq(CallRecord::getCallerId, userId).or().eq(CallRecord::getCalleeId, userId));
|
||||||
|
wrapper.in(CallRecord::getStatus, ACTIVE_STATUSES);
|
||||||
|
return count(wrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallRecord getCurrentCall(Integer userId) {
|
||||||
|
LambdaQueryWrapper<CallRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.and(w -> w.eq(CallRecord::getCallerId, userId).or().eq(CallRecord::getCalleeId, userId));
|
||||||
|
wrapper.in(CallRecord::getStatus, ACTIVE_STATUSES);
|
||||||
|
wrapper.orderByDesc(CallRecord::getCallTime);
|
||||||
|
wrapper.last("LIMIT 1");
|
||||||
|
return getOne(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user