diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/ImRtcCallManagerController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/ImRtcCallManagerController.java new file mode 100644 index 0000000..417e58f --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/ImRtcCallManagerController.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.rtc; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcCallManagerPageReqVO; +import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcCallManagerRespVO; +import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcParticipantManagerRespVO; +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; +import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO; +import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO; +import cn.iocoder.yudao.module.im.service.group.ImGroupService; +import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - IM 通话记录") +@RestController +@RequestMapping("/im/manager/rtc") +@Validated +public class ImRtcCallManagerController { + + @Resource + private ImRtcCallService rtcCallService; + @Resource + private ImGroupService groupService; + + @Resource + private AdminUserApi adminUserApi; + + @GetMapping("/page") + @Operation(summary = "获得通话记录分页") + @PreAuthorize("@ss.hasPermission('im:manager:rtc:query')") + public CommonResult> getCallPage(@Valid ImRtcCallManagerPageReqVO pageReqVO) { + PageResult pageResult = rtcCallService.getCallPage(pageReqVO); + return success(buildCallRespVOPage(pageResult)); + } + + @GetMapping("/get") + @Operation(summary = "获得通话记录详情") + @Parameter(name = "id", description = "通话编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('im:manager:rtc:query')") + public CommonResult getCall(@RequestParam("id") Long id) { + ImRtcCallDO call = rtcCallService.getCall(id); + if (call == null) { + return success(null); + } + return success(CollUtil.getFirst(buildCallRespVOList(Collections.singletonList(call)))); + } + + @GetMapping("/participant-list") + @Operation(summary = "获得通话参与者列表") + @Parameter(name = "id", description = "通话编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('im:manager:rtc:query')") + public CommonResult> getCallParticipantList(@RequestParam("id") Long id) { + List participants = rtcCallService.getCallParticipantListByCallId(id); + if (CollUtil.isEmpty(participants)) { + return success(Collections.emptyList()); + } + // 查询用户信息 + Map userMap = adminUserApi.getUserMap( + convertSet(participants, ImRtcParticipantDO::getUserId)); + // 组装返回 + return success(BeanUtils.toBean(participants, ImRtcParticipantManagerRespVO.class, vo -> + MapUtils.findAndThen(userMap, vo.getUserId(), + user -> vo.setUserNickname(user.getNickname())))); + } + + // ========== 私有方法:VO 组装 ========== + + private PageResult buildCallRespVOPage(PageResult pageResult) { + if (CollUtil.isEmpty(pageResult.getList())) { + return PageResult.empty(pageResult.getTotal()); + } + return new PageResult<>(buildCallRespVOList(pageResult.getList()), pageResult.getTotal()); + } + + private List buildCallRespVOList(List calls) { + // 查询用户信息 + Map userMap = adminUserApi.getUserMap( + convertSet(calls, ImRtcCallDO::getInviterUserId)); + Map groupMap = groupService.getGroupMap( + convertSet(calls, ImRtcCallDO::getGroupId)); + // 组装返回 + return BeanUtils.toBean(calls, ImRtcCallManagerRespVO.class, vo -> { + MapUtils.findAndThen(userMap, vo.getInviterUserId(), + user -> vo.setInviterNickname(user.getNickname())); + MapUtils.findAndThen(groupMap, vo.getGroupId(), + group -> vo.setGroupName(group.getName())); + }); + } + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerPageReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerPageReqVO.java new file mode 100644 index 0000000..f7878fa --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerPageReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IM 通话记录分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ImRtcCallManagerPageReqVO extends PageParam { + + @Schema(description = "发起人用户编号", example = "1024") + private Long inviterUserId; + + @Schema(description = "会话类型", example = "1") + private Integer conversationType; // 参见 ImConversationTypeEnum 枚举类 + + @Schema(description = "媒体类型", example = "1") + private Integer mediaType; // 参见 ImRtcCallMediaTypeEnum 枚举类 + + @Schema(description = "通话状态", example = "10") + private Integer status; // 参见 ImRtcCallStatusEnum 枚举类 + + @Schema(description = "结束原因", example = "1") + private Integer endReason; // 参见 ImRtcCallEndReasonEnum 枚举类 + + @Schema(description = "发起时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] startTime; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerRespVO.java new file mode 100644 index 0000000..0151581 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcCallManagerRespVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IM 通话记录 Response VO") +@Data +public class ImRtcCallManagerRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "业务通话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "uuid-xxx") + private String room; + + @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer conversationType; // 参见 ImConversationTypeEnum 枚举类 + + @Schema(description = "媒体类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer mediaType; // 参见 ImRtcCallMediaTypeEnum 枚举类 + + @Schema(description = "发起人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long inviterUserId; + + @Schema(description = "发起人昵称", example = "张三") + private String inviterNickname; + + @Schema(description = "群编号;私聊为空", example = "999") + private Long groupId; + + @Schema(description = "群名称;私聊为空", example = "测试群") + private String groupName; + + @Schema(description = "通话状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer status; // 参见 ImRtcCallStatusEnum 枚举类 + + @Schema(description = "结束原因", example = "1") + private Integer endReason; // 参见 ImRtcCallEndReasonEnum 枚举类 + + @Schema(description = "发起时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + + @Schema(description = "接通时间;未接通为空") + private LocalDateTime acceptTime; + + @Schema(description = "结束时间;未结束为空") + private LocalDateTime endTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcParticipantManagerRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcParticipantManagerRespVO.java new file mode 100644 index 0000000..a990577 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/rtc/vo/ImRtcParticipantManagerRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IM 通话参与者 Response VO") +@Data +public class ImRtcParticipantManagerRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "通话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long callId; + + @Schema(description = "参与者用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "参与者昵称", example = "张三") + private String userNickname; + + @Schema(description = "参与角色", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer role; // 参见 ImRtcParticipantRoleEnum 枚举类 + + @Schema(description = "参与状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer status; // 参见 ImRtcParticipantStatusEnum 枚举类 + + @Schema(description = "被邀请时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime inviteTime; + + @Schema(description = "接听时间;未接听为空") + private LocalDateTime acceptTime; + + @Schema(description = "离开时间;未加入为空") + private LocalDateTime leaveTime; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/rtc/ImRtcParticipantDO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/rtc/ImRtcParticipantDO.java index 586c247..4282baa 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/rtc/ImRtcParticipantDO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/rtc/ImRtcParticipantDO.java @@ -35,6 +35,12 @@ public class ImRtcParticipantDO extends BaseDO { @TableId private Long id; /** + * 通话编号 + *

+ * 关联 {@link ImRtcCallDO#getId()} + */ + private Long callId; + /** * 业务通话编号 *

* 关联 {@link ImRtcCallDO#getRoom()} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcCallMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcCallMapper.java index dee8ff5..65f21a5 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcCallMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcCallMapper.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.im.dal.mysql.rtc; +import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcCallManagerPageReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.apache.ibatis.annotations.Mapper; @@ -48,4 +50,15 @@ public interface ImRtcCallMapper extends BaseMapperX { .lt(ImRtcCallDO::getStartTime, startTimeBefore)); } + default PageResult selectPage(ImRtcCallManagerPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ImRtcCallDO::getInviterUserId, reqVO.getInviterUserId()) + .eqIfPresent(ImRtcCallDO::getConversationType, reqVO.getConversationType()) + .eqIfPresent(ImRtcCallDO::getMediaType, reqVO.getMediaType()) + .eqIfPresent(ImRtcCallDO::getStatus, reqVO.getStatus()) + .eqIfPresent(ImRtcCallDO::getEndReason, reqVO.getEndReason()) + .betweenIfPresent(ImRtcCallDO::getStartTime, reqVO.getStartTime()) + .orderByDesc(ImRtcCallDO::getId)); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcParticipantMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcParticipantMapper.java index 0e9e087..fd77e53 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcParticipantMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcParticipantMapper.java @@ -28,6 +28,10 @@ public interface ImRtcParticipantMapper extends BaseMapperX return selectList(ImRtcParticipantDO::getRoom, room); } + default List selectListByCallId(Long callId) { + return selectList(ImRtcParticipantDO::getCallId, callId); + } + default List selectListByStatusAndInviteTimeBefore(Integer status, LocalDateTime threshold) { return selectList(new LambdaQueryWrapperX() .eq(ImRtcParticipantDO::getStatus, status) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java index fb98b29..33be85f 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallService.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.im.service.rtc; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcCallManagerPageReqVO; import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallCreateReqVO; import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO; @@ -138,4 +140,30 @@ public interface ImRtcCallService { */ void noAnswerCallCheck(Long userId, String room); + // ==================== 管理后台 ==================== + + /** + * 【管理后台】获得通话记录分页 + * + * @param reqVO 分页查询条件 + * @return 通话记录分页 + */ + PageResult getCallPage(ImRtcCallManagerPageReqVO reqVO); + + /** + * 【管理后台】获得通话记录 + * + * @param id 通话编号 + * @return 通话记录 + */ + ImRtcCallDO getCall(Long id); + + /** + * 【管理后台】按通话编号查询参与者明细 + * + * @param id 通话编号 + * @return 参与者明细列表;通话不存在时返回空集合 + */ + List getCallParticipantListByCallId(Long id); + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java index a3b1980..299e8c8 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java @@ -4,7 +4,9 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.im.controller.admin.manager.rtc.vo.ImRtcCallManagerPageReqVO; import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallCreateReqVO; import cn.iocoder.yudao.module.im.controller.admin.rtc.vo.ImRtcCallInviteReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO; @@ -215,13 +217,13 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { rtcCallMapper.insert(call); // 2.2 批量 INSERT 参与表:发起人即时 JOINED,被邀请人 INVITING 等接通 List participants = new ArrayList<>(invitees.size() + 1); - participants.add(new ImRtcParticipantDO().setRoom(room).setUserId(inviterId) + participants.add(new ImRtcParticipantDO().setCallId(call.getId()).setRoom(room).setUserId(inviterId) .setRole(ImRtcParticipantRoleEnum.INVITER.getRole()) .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()) .setInviteTime(now).setAcceptTime(now)); for (Long inviteeId : invitees) { participants.add(new ImRtcParticipantDO() - .setRoom(room).setUserId(inviteeId) + .setCallId(call.getId()).setRoom(room).setUserId(inviteeId) .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()) .setInviteTime(now)); @@ -260,7 +262,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { } } else { // 旁观者主动加入:INSERT role=ACTIVE_JOIN - rtcParticipantMapper.insert(new ImRtcParticipantDO().setRoom(call.getRoom()) + rtcParticipantMapper.insert(new ImRtcParticipantDO() + .setCallId(call.getId()).setRoom(call.getRoom()) .setUserId(userId).setRole(ImRtcParticipantRoleEnum.ACTIVE_JOIN.getRole()) .setStatus(ImRtcParticipantStatusEnum.JOINED.getStatus()).setInviteTime(now).setAcceptTime(now)); } @@ -292,7 +295,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { // 2. 批量 INSERT 新邀请人 LocalDateTime now = LocalDateTime.now(); List participants = CollectionUtils.convertList(incomingUserIds, inviteeId -> - new ImRtcParticipantDO().setRoom(call.getRoom()).setUserId(inviteeId) + new ImRtcParticipantDO().setCallId(call.getId()).setRoom(call.getRoom()).setUserId(inviteeId) .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) .setStatus(ImRtcParticipantStatusEnum.INVITING.getStatus()).setInviteTime(now)); rtcParticipantMapper.insertBatch(participants); @@ -1071,4 +1074,21 @@ public class ImRtcCallServiceImpl implements ImRtcCallService { user == null ? null : user.getNickname(), String.valueOf(userId)); } + // ==================== 管理后台 ==================== + + @Override + public PageResult getCallPage(ImRtcCallManagerPageReqVO reqVO) { + return rtcCallMapper.selectPage(reqVO); + } + + @Override + public ImRtcCallDO getCall(Long id) { + return rtcCallMapper.selectById(id); + } + + @Override + public List getCallParticipantListByCallId(Long id) { + return rtcParticipantMapper.selectListByCallId(id); + } + } diff --git a/yudao-module-im/src/test/resources/sql/create_tables.sql b/yudao-module-im/src/test/resources/sql/create_tables.sql index 8e688e9..1ea601f 100644 --- a/yudao-module-im/src/test/resources/sql/create_tables.sql +++ b/yudao-module-im/src/test/resources/sql/create_tables.sql @@ -202,6 +202,7 @@ CREATE TABLE IF NOT EXISTS "im_rtc_call" ( CREATE TABLE IF NOT EXISTS "im_rtc_participant" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', + "call_id" bigint NOT NULL COMMENT '通话编号', "room" varchar(64) NOT NULL COMMENT '业务通话编号', "user_id" bigint NOT NULL COMMENT '参与者用户编号', "role" tinyint NOT NULL COMMENT '参与角色',