From bae216261aefa09d2b87302db9c86733033c035b Mon Sep 17 00:00:00 2001 From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com> Date: Fri, 26 Dec 2025 15:43:37 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zbkj/common/model/call/CallRecord.java | 112 ++++++++ .../zbkj/front/controller/CallController.java | 195 ++++++++++++++ .../request/call/InitiateCallRequest.java | 18 ++ .../response/call/CallRecordResponse.java | 46 ++++ .../response/call/InitiateCallResponse.java | 24 ++ .../front/websocket/CallSignalingHandler.java | 42 +++ .../com/zbkj/service/dao/CallRecordDao.java | 12 + .../com/zbkj/service/service/CallService.java | 78 ++++++ .../service/service/impl/CallServiceImpl.java | 250 ++++++++++++++++++ 9 files changed, 777 insertions(+) create mode 100644 Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/call/CallRecord.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/call/InitiateCallRequest.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/CallRecordResponse.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/InitiateCallResponse.java create mode 100644 Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java create mode 100644 Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/CallRecordDao.java create mode 100644 Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/CallService.java create mode 100644 Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/call/CallRecord.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/call/CallRecord.java new file mode 100644 index 00000000..98b91741 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/call/CallRecord.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java new file mode 100644 index 00000000..fc8c3588 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CallController.java @@ -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 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 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 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 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 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> getCallHistory(@Validated PageParamRequest pageParamRequest) { + User currentUser = userService.getInfo(); + if (currentUser == null) return CommonResult.failed("请先登录"); + List records = callService.getCallHistory(currentUser.getUid(), pageParamRequest); + List responseList = new ArrayList<>(); + Map 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 pageInfo = new PageInfo<>(responseList); + return CommonResult.success(pageInfo); + } + + @ApiOperation(value = "删除通话记录") + @DeleteMapping("/record/{recordId}") + public CommonResult 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 getMissedCallCount() { + User currentUser = userService.getInfo(); + if (currentUser == null) return CommonResult.failed("请先登录"); + return CommonResult.success(callService.getMissedCallCount(currentUser.getUid())); + } + + @ApiOperation(value = "检查是否正在通话中") + @GetMapping("/status") + public CommonResult> getCallStatus() { + User currentUser = userService.getInfo(); + if (currentUser == null) return CommonResult.failed("请先登录"); + Map 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 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 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 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; + } +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/call/InitiateCallRequest.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/call/InitiateCallRequest.java new file mode 100644 index 00000000..24a8db95 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/request/call/InitiateCallRequest.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/CallRecordResponse.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/CallRecordResponse.java new file mode 100644 index 00000000..222a299c --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/CallRecordResponse.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/InitiateCallResponse.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/InitiateCallResponse.java new file mode 100644 index 00000000..17b5bafe --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/call/InitiateCallResponse.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java new file mode 100644 index 00000000..bcf9e4bd --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/websocket/CallSignalingHandler.java @@ -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> callSessions = new ConcurrentHashMap<>(); + private final Map userCallSessions = new ConcurrentHashMap<>(); + private final Map sessionUserMap = new ConcurrentHashMap<>(); + private final Map sessionCallMap = new ConcurrentHashMap<>(); + private final Map callCreateTime = new ConcurrentHashMap<>(); diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/CallRecordDao.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/CallRecordDao.java new file mode 100644 index 00000000..17b15daa --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/CallRecordDao.java @@ -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 { +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/CallService.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/CallService.java new file mode 100644 index 00000000..36c40c3c --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/CallService.java @@ -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 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 getCallHistory(Integer userId, PageParamRequest pageParamRequest); + + /** + * 删除通话记录(软删除) + */ + Boolean deleteRecord(Long recordId, Integer userId); + + /** + * 获取用户未接来电数量 + */ + Integer getMissedCallCount(Integer userId); + + /** + * 检查用户是否正在通话中 + */ + Boolean isUserInCall(Integer userId); + + /** + * 获取用户当前进行中的通话 + */ + CallRecord getCurrentCall(Integer userId); +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java new file mode 100644 index 00000000..84d2998b --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/CallServiceImpl.java @@ -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 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 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CallRecord::getCallId, callId); + return getOne(wrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateStatus(String callId, String status) { + LambdaUpdateWrapper 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 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 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 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 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 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 getCallHistory(Integer userId, PageParamRequest pageParamRequest) { + PageHelper.startPage(pageParamRequest.getPage(), pageParamRequest.getLimit()); + LambdaQueryWrapper 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 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 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 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 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); + } +}