🎉 Initial commit

This commit is contained in:
2026-07-27 14:26:14 +08:00
commit fd54f07889
6935 changed files with 617410 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
/**
* @author anhaohao
* @since 2024/3/9 下午8:59
*/
package cn.iocoder.yudao.module.im.api;

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.im.controller.admin.channel;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
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 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 static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "用户 APP - IM 频道素材")
@RestController
@RequestMapping("/im/channel/material")
@Validated
public class ImChannelMaterialController {
@Resource
private ImChannelMaterialService channelMaterialService;
@GetMapping("/get")
@Operation(summary = "获取素材详情;用于客户端点击图文卡片渲染详情页")
@Parameter(name = "id", description = "素材编号", required = true, example = "1024")
public CommonResult<ImChannelMaterialRespVO> getMaterial(@RequestParam("id") Long id) {
ImChannelMaterialDO material = channelMaterialService.validateMaterialExists(id);
return success(BeanUtils.toBean(material, ImChannelMaterialRespVO.class));
}
}

View File

@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.im.controller.admin.conversation;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversationReadRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
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.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - IM 会话读位置")
@RestController
@RequestMapping("/im/conversation-read")
@Validated
public class ImConversationReadController {
@Resource
private ImConversationReadService conversationReadService;
@GetMapping("/pull")
@Operation(summary = "增量拉取当前用户的会话读位置(重连 / 离线补偿)")
@Parameters({
@Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
@Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id首次拉取不传"),
@Parameter(name = "limit", description = "单次拉取条数", required = true)
})
public CommonResult<List<ImConversationReadRespVO>> pullMyConversationRead(
@RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
@RequestParam(value = "lastId", required = false) Long lastId,
@RequestParam("limit") @Min(1) @Max(200) Integer limit) {
List<ImConversationReadDO> list = conversationReadService.pullConversationReadList(
getLoginUserId(), lastUpdateTime, lastId, limit);
return success(BeanUtils.toBean(list, ImConversationReadRespVO.class));
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.im.controller.admin.conversation.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* IM 会话读位置 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - IM 会话读位置 Response VO")
@Data
public class ImConversationReadRespVO {
@Schema(description = "读位置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "会话类型", example = "1")
private Integer conversationType; // 参见 ImConversationTypeEnum 枚举
@Schema(description = "目标编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long targetId;
@Schema(description = "最大已读消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
private Long messageId;
@Schema(description = "最近更新时间(增量拉取游标用)")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.im.controller.admin.face;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.face.vo.pack.ImFacePackUserRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO;
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackItemDO;
import cn.iocoder.yudao.module.im.service.face.ImFacePackItemService;
import cn.iocoder.yudao.module.im.service.face.ImFacePackService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
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.RestController;
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.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
@Tag(name = "管理后台 - IM 表情包")
@RestController
@RequestMapping("/im/face-pack")
@Validated
public class ImFacePackController {
@Resource
private ImFacePackService facePackService;
@Resource
private ImFacePackItemService facePackItemService;
@GetMapping("/list")
@Operation(summary = "获得启用的表情包列表(含表情)")
public CommonResult<List<ImFacePackUserRespVO>> getFacePackList() {
// 1.1 拉所有启用表情包
List<ImFacePackDO> packs = facePackService.getEnabledFacePackList();
if (packs.isEmpty()) {
return success(ListUtil.of());
}
// 1.2 拉这些包下所有启用表情,按 packId 分组
List<ImFacePackItemDO> items = facePackItemService.getEnabledItemListByPackIds(
convertList(packs, ImFacePackDO::getId));
Map<Long, List<ImFacePackItemDO>> itemsByPackId = convertMultiMap(items, ImFacePackItemDO::getPackId);
// 2. 拼装BeanUtils 把 pack 字段映射 + 自己塞 items
List<ImFacePackUserRespVO> result = convertList(packs, pack -> {
ImFacePackUserRespVO vo = BeanUtils.toBean(pack, ImFacePackUserRespVO.class);
vo.setItems(BeanUtils.toBean(itemsByPackId.getOrDefault(pack.getId(), ListUtil.of()), ImFacePackUserRespVO.Item.class));
return vo;
});
return success(result);
}
}

View File

@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.im.controller.admin.face;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemRespVO;
import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemSaveReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFaceUserItemDO;
import cn.iocoder.yudao.module.im.service.face.ImFaceUserItemService;
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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - IM 个人表情")
@RestController
@RequestMapping("/im/face-user-item")
@Validated
public class ImFaceUserItemController {
@Resource
private ImFaceUserItemService faceUserItemService;
@GetMapping("/list")
@Operation(summary = "获得我的个人表情列表")
public CommonResult<List<ImFaceUserItemRespVO>> getFaceUserItemList() {
List<ImFaceUserItemDO> items = faceUserItemService.getFaceUserItemList(getLoginUserId());
return success(BeanUtils.toBean(items, ImFaceUserItemRespVO.class));
}
@PostMapping("/create")
@Operation(summary = "添加个人表情")
public CommonResult<Long> createFaceUserItem(@Valid @RequestBody ImFaceUserItemSaveReqVO reqVO) {
return success(faceUserItemService.createFaceUserItem(getLoginUserId(), reqVO));
}
@DeleteMapping("/delete")
@Operation(summary = "删除个人表情")
@Parameter(name = "id", description = "编号", required = true, example = "4096")
public CommonResult<Boolean> deleteFaceUserItem(@RequestParam("id") Long id) {
faceUserItemService.deleteFaceUserItem(getLoginUserId(), id);
return success(true);
}
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.im.controller.admin.face.vo.pack;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "IM 表情包(用户端) Response VO")
@Data
public class ImFacePackUserRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
private String name;
@Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
private String icon;
@Schema(description = "表情列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Item> items;
@Schema(description = "IM 表情包项(用户端)")
@Data
public static class Item {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long id;
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
example = "https://cdn.example.com/face/pack/cat-001.png")
private String url;
@Schema(description = "表情名", example = "狗头")
private String name;
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
private Integer width;
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
private Integer height;
}
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "IM 个人表情 Response VO")
@Data
public class ImFaceUserItemRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
private Long id;
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
example = "https://cdn.example.com/face/user/abc.gif")
private String url;
@Schema(description = "表情名", example = "狗头")
private String name;
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
private Integer width;
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
private Integer height;
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "IM 个人表情新增 Request VO")
@Data
public class ImFaceUserItemSaveReqVO {
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
example = "https://cdn.example.com/face/user/abc.gif")
@NotBlank(message = "表情图 URL 不能为空")
@Size(max = 512, message = "表情图 URL 长度不能超过 512")
private String url;
@Schema(description = "表情名", example = "狗头")
@Size(max = 64, message = "表情名长度不能超过 64")
private String name;
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
@NotNull(message = "渲染宽度不能为空")
@Min(value = 1, message = "渲染宽度不能小于 1 像素")
@Max(value = 2048, message = "渲染宽度不能大于 2048 像素")
private Integer width;
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
@NotNull(message = "渲染高度不能为空")
@Min(value = 1, message = "渲染高度不能小于 1 像素")
@Max(value = 2048, message = "渲染高度不能大于 2048 像素")
private Integer height;
}

View File

@@ -0,0 +1,143 @@
package cn.iocoder.yudao.module.im.controller.admin.friend;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendRespVO;
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
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.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
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.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - IM 好友")
@RestController
@RequestMapping("/im/friend")
@Validated
public class ImFriendController {
@Resource
private ImFriendService friendService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/list")
@Operation(summary = "获得当前登录用户的好友列表")
public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
return success(buildFriendRespVOList(friends));
}
@GetMapping("/pull")
@Operation(summary = "增量拉取当前用户的好友关系(重连 / 离线补偿)")
@Parameters({
@Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
@Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id首次拉取不传"),
@Parameter(name = "limit", description = "单次拉取条数", required = true)
})
public CommonResult<List<ImFriendRespVO>> pullMyFriendList(
@RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
@RequestParam(value = "lastId", required = false) Long lastId,
@RequestParam("limit") @Min(1) @Max(200) Integer limit) {
List<ImFriendDO> list = friendService.pullFriendList(getLoginUserId(), lastUpdateTime, lastId, limit);
return success(buildFriendRespVOList(list));
}
@GetMapping("/get")
@Operation(summary = "获得好友详情")
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
public CommonResult<ImFriendRespVO> getFriend(@RequestParam("friendUserId") Long friendUserId) {
ImFriendDO friend = friendService.getFriend(getLoginUserId(), friendUserId);
return success(buildFriendRespVO(friend));
}
@DeleteMapping("/delete")
@Operation(summary = "删除好友(单向软删除)")
@Parameters({
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048"),
@Parameter(name = "clear", description = "是否级联清理本端相关数据(如私聊会话)")
})
public CommonResult<Boolean> deleteFriend(
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId,
@RequestParam(value = "clear", required = false) Boolean clear) {
friendService.deleteFriend(getLoginUserId(), friendUserId, clear);
return success(true);
}
@PutMapping("/update")
@Operation(summary = "更新好友单边属性(备注 / 免打扰 / 联系人置顶)")
public CommonResult<Boolean> updateFriend(@Valid @RequestBody ImFriendUpdateReqVO reqVO) {
friendService.updateFriend(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/block")
@Operation(summary = "拉黑好友(必须先是好友;单边屏蔽对方私聊消息)")
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
public CommonResult<Boolean> blockFriend(
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
friendService.blockFriend(getLoginUserId(), friendUserId);
return success(true);
}
@PutMapping("/unblock")
@Operation(summary = "移出黑名单")
@Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
public CommonResult<Boolean> unblockFriend(
@RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId) {
friendService.unblockFriend(getLoginUserId(), friendUserId);
return success(true);
}
// ========== 私有方法VO 组装 ==========
private List<ImFriendRespVO> buildFriendRespVOList(Collection<ImFriendDO> friends) {
if (CollUtil.isEmpty(friends)) {
return Collections.emptyList();
}
// 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertList(friends, ImFriendDO::getFriendUserId));
return convertList(friends, friend -> {
ImFriendRespVO vo = BeanUtils.toBean(friend, ImFriendRespVO.class);
MapUtils.findAndThen(userMap, friend.getFriendUserId(), user ->
vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
// 备注 / 昵称的拼音,给前端做字母分桶 + 拼音搜索
vo.setDisplayNamePinyin(StrUtils.toPinyin(vo.getDisplayName()))
.setNicknamePinyin(StrUtils.toPinyin(vo.getNickname()));
return vo;
});
}
private ImFriendRespVO buildFriendRespVO(ImFriendDO friend) {
if (friend == null) {
return null;
}
return CollUtil.getFirst(buildFriendRespVOList(singleton(friend)));
}
}

View File

@@ -0,0 +1,143 @@
package cn.iocoder.yudao.module.im.controller.admin.friend;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.friend.vo.request.ImFriendRequestApplyReqVO;
import cn.iocoder.yudao.module.im.controller.admin.friend.vo.request.ImFriendRequestRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
import cn.iocoder.yudao.module.im.service.friend.ImFriendRequestService;
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.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
/**
* IM 好友申请记录 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - IM 好友申请")
@RestController
@RequestMapping("/im/friend-request")
@Validated
public class ImFriendRequestController {
@Resource
private ImFriendRequestService friendRequestService;
@Resource
private AdminUserApi adminUserApi;
@PostMapping("/apply")
@Operation(summary = "发起好友申请")
public CommonResult<Long> applyFriend(@Valid @RequestBody ImFriendRequestApplyReqVO reqVO) {
ImFriendRequestDO request = friendRequestService.applyFriend(getLoginUserId(), reqVO);
return success(request != null ? request.getId() : null);
}
@PutMapping("/agree")
@Operation(summary = "同意好友申请")
@Parameter(name = "id", description = "申请编号", required = true, example = "1024")
public CommonResult<Boolean> agreeFriendRequest(
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
friendRequestService.agreeFriendRequest(getLoginUserId(), id);
return success(true);
}
@PutMapping("/refuse")
@Operation(summary = "拒绝好友申请")
public CommonResult<Boolean> refuseFriendRequest(
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
@RequestParam(value = "handleContent", required = false)
@Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
friendRequestService.refuseFriendRequest(getLoginUserId(), id, handleContent);
return success(true);
}
@GetMapping("/list")
@Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)")
@Parameters({
@Parameter(name = "maxId", description = "当前列表最旧记录的 id首页不传"),
@Parameter(name = "limit", description = "单次拉取条数", required = true)
})
public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList(
@RequestParam(value = "maxId", required = false) Long maxId,
@RequestParam("limit") @Min(1) @Max(200) Integer limit) {
List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit);
return success(buildList(list));
}
@GetMapping("/pull")
@Operation(summary = "增量拉取「我相关」的好友申请(重连 / 离线补偿)")
@Parameters({
@Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
@Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id首次拉取不传"),
@Parameter(name = "limit", description = "单次拉取条数", required = true)
})
public CommonResult<List<ImFriendRequestRespVO>> pullMyFriendRequestList(
@RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
@RequestParam(value = "lastId", required = false) Long lastId,
@RequestParam("limit") @Min(1) @Max(200) Integer limit) {
List<ImFriendRequestDO> list = friendRequestService.pullFriendRequestList(getLoginUserId(), lastUpdateTime, lastId, limit);
return success(buildList(list));
}
@GetMapping("/get")
@Operation(summary = "按 id 单查「我相关」的申请记录带越权过滤WebSocket 通知到达后用)")
@Parameter(name = "id", description = "申请记录编号", required = true)
public CommonResult<ImFriendRequestRespVO> getMyFriendRequest(@RequestParam("id") Long id) {
ImFriendRequestDO request = friendRequestService.getFriendRequest(id);
// 越权过滤fromUser / toUser 必有一方是当前用户,否则当不存在返回 null
Long currentUserId = getLoginUserId();
if (request == null || (ObjUtil.notEqual(request.getFromUserId(), currentUserId)
&& ObjUtil.notEqual(request.getToUserId(), currentUserId))) {
return success(null);
}
return success(CollUtil.getFirst(buildList(Collections.singletonList(request))));
}
// ========== 私有方法VO 组装 ==========
private List<ImFriendRequestRespVO> buildList(List<ImFriendRequestDO> list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
// 双向 OR 列表userIds 取 from + to 两组并集
Set<Long> userIds = convertSetByFlatMap(list,
request -> Stream.of(request.getFromUserId(), request.getToUserId()));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
return convertList(list, request -> {
ImFriendRequestRespVO vo = BeanUtils.toBean(request, ImFriendRequestRespVO.class);
MapUtils.findAndThen(userMap, request.getFromUserId(), user ->
vo.setFromNickname(user.getNickname()).setFromAvatar(user.getAvatar()));
MapUtils.findAndThen(userMap, request.getToUserId(), user ->
vo.setToNickname(user.getNickname()).setToAvatar(user.getAvatar()));
return vo;
});
}
}

View File

@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* IM 好友 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - IM 好友 Response VO")
@Data
public class ImFriendRespVO {
@Schema(description = "关系记录编号", example = "1024")
private Long id;
@Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long friendUserId;
@Schema(description = "是否免打扰", example = "false")
private Boolean silent;
@Schema(description = "好友展示备注(仅自己可见)", example = "老张")
private String displayName;
@Schema(description = "好友展示备注的拼音(小写无空格)", example = "laozhang")
private String displayNamePinyin;
@Schema(description = "添加来源", example = "1")
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
@Schema(description = "是否置顶联系人", example = "false")
private Boolean pinned;
@Schema(description = "是否拉黑(仅自己可见)", example = "false")
private Boolean blocked;
@Schema(description = "好友状态", example = "0")
private Integer status;
@Schema(description = "添加好友时间")
private LocalDateTime addTime;
@Schema(description = "删除好友时间")
private LocalDateTime deleteTime;
@Schema(description = "最近更新时间(增量拉取游标用)")
private LocalDateTime updateTime;
// ========== 下面是聚合字段,方便前端显示 ==========
@Schema(description = "好友昵称(实时聚合自 AdminUser", example = "芋道")
private String nickname;
@Schema(description = "好友昵称的拼音(小写无空格)", example = "yudao")
private String nicknamePinyin;
@Schema(description = "好友头像(实时聚合自 AdminUser")
private String avatar;
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.im.controller.admin.friend.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 好友更新 Request VO")
@Data
public class ImFriendUpdateReqVO {
@Schema(description = "好友的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
@NotNull(message = "好友用户编号不能为空")
private Long friendUserId;
@Schema(description = "是否免打扰;不传表示不修改", example = "true")
private Boolean silent;
@Schema(description = "好友展示备注(仅自己可见);不传表示不修改,传空串表示清空", example = "老张")
@Size(max = 16, message = "好友备注最多 16 个字符")
private String displayName;
@Schema(description = "是否置顶联系人;不传表示不修改", example = "true")
private Boolean pinned;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.im.enums.friend.ImFriendAddSourceEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* IM 好友申请 - 发起 Request VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - IM 好友申请发起 Request VO")
@Data
public class ImFriendRequestApplyReqVO {
@Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
@NotNull(message = "接收方用户编号不能为空")
private Long toUserId;
@Schema(description = "申请理由", example = "我是芋艿(一种食材)")
@Size(max = 255, message = "申请理由最多 255 个字符")
private String applyContent;
@Schema(description = "对接收方的备注(仅自己可见)", example = "老张")
@Size(max = 16, message = "好友备注最多 16 个字符")
private String displayName;
@Schema(description = "添加来源", example = "1")
@InEnum(ImFriendAddSourceEnum.class)
private Integer addSource;
}

View File

@@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.im.controller.admin.friend.vo.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* IM 好友申请 Response VO
*
* @author 芋道源码
*/
@Schema(description = "管理后台 - IM 好友申请 Response VO")
@Data
public class ImFriendRequestRespVO {
@Schema(description = "申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "发起方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Long fromUserId;
@Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
private Long toUserId;
@Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举
@Schema(description = "申请理由", example = "我是芋艿(一种食材)")
private String applyContent;
@Schema(description = "处理理由(接收方拒绝时可选填)", example = "暂不通过")
private String handleContent;
@Schema(description = "添加来源", example = "1")
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举
@Schema(description = "处理时间")
private LocalDateTime handleTime;
@Schema(description = "申请创建时间")
private LocalDateTime createTime;
@Schema(description = "最近更新时间(增量拉取游标用)")
private LocalDateTime updateTime;
// ========== 下面是聚合字段,方便前端显示 ==========
@Schema(description = "发起方昵称(实时聚合自 AdminUser", example = "芋道")
private String fromNickname;
@Schema(description = "发起方头像(实时聚合自 AdminUser")
private String fromAvatar;
@Schema(description = "接收方昵称(实时聚合自 AdminUser", example = "老张")
private String toNickname;
@Schema(description = "接收方头像(实时聚合自 AdminUser")
private String toAvatar;
}

View File

@@ -0,0 +1,215 @@
package cn.iocoder.yudao.module.im.controller.admin.group;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.group.vo.*;
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
import cn.iocoder.yudao.module.im.service.group.ImGroupService;
import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 群")
@RestController
@RequestMapping("/im/group")
@Validated
public class ImGroupController {
@Resource
private ImGroupService groupService;
@Resource
private ImGroupMemberService groupMemberService;
@Resource
private ImGroupMessageService groupMessageService;
// ==================== 群的写操作 ====================
@PostMapping("/create")
@Operation(summary = "创建群")
public CommonResult<ImGroupRespVO> createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) {
ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId());
// 新建群必无 pinnedMessages跳过关联回填
return success(BeanUtils.toBean(group, ImGroupRespVO.class));
}
@PutMapping("/update")
@Operation(summary = "更新群")
public CommonResult<ImGroupRespVO> updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) {
ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId());
return success(buildGroupRespVO(group, getLoginUserId()));
}
@DeleteMapping("/dissolve")
@Operation(summary = "解散群")
@Parameter(name = "id", description = "群编号", required = true)
public CommonResult<Boolean> dissolveGroup(@RequestParam("id") Long id) {
groupService.dissolveGroup(id, getLoginUserId());
return success(true);
}
// ==================== 群的读操作 ====================
@GetMapping("/get")
@Operation(summary = "获得群")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
public CommonResult<ImGroupRespVO> getGroup(@RequestParam("id") Long id) {
ImGroupDO group = groupService.getGroup(id);
return success(buildGroupRespVO(group, getLoginUserId()));
}
@GetMapping("/list")
@Operation(summary = "获得当前登录用户的群列表")
public CommonResult<List<ImGroupRespVO>> getMyGroupList() {
Long loginUserId = getLoginUserId();
List<ImGroupDO> groups = groupService.getMyGroupList(loginUserId);
return success(buildGroupRespVOList(groups, loginUserId));
}
// ==================== 群成员的写操作 ====================
@PostMapping("/invite")
@Operation(summary = "邀请用户加入群")
public CommonResult<Boolean> inviteGroupMember(@Valid @RequestBody ImGroupMemberInviteReqVO inviteReqVO) {
groupService.inviteGroupMember(getLoginUserId(), inviteReqVO);
return success(true);
}
@DeleteMapping("/quit")
@Operation(summary = "退出群")
@Parameter(name = "groupId", description = "群编号", required = true)
public CommonResult<Boolean> quitGroup(@RequestParam("groupId") Long groupId) {
groupService.quitGroup(groupId, getLoginUserId());
return success(true);
}
@DeleteMapping("/kicking")
@Operation(summary = "移除群成员")
public CommonResult<Boolean> removeGroupMember(@Valid @RequestBody ImGroupMemberRemoveReqVO removeReqVO) {
groupService.removeGroupMember(getLoginUserId(), removeReqVO);
return success(true);
}
@PutMapping("/add-admin")
@Operation(summary = "添加群管理员")
public CommonResult<Boolean> addGroupAdmin(@Valid @RequestBody ImGroupAdminAddReqVO reqVO) {
groupService.addGroupAdmin(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/remove-admin")
@Operation(summary = "撤销群管理员")
public CommonResult<Boolean> removeGroupAdmin(@Valid @RequestBody ImGroupAdminRemoveReqVO reqVO) {
groupService.removeGroupAdmin(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/transfer-owner")
@Operation(summary = "转让群主")
public CommonResult<Boolean> transferGroupOwner(@Valid @RequestBody ImGroupTransferOwnerReqVO transferReqVO) {
groupService.transferGroupOwner(getLoginUserId(), transferReqVO);
return success(true);
}
// ==================== 群消息置顶 ====================
@PutMapping("/pin-message")
@Operation(summary = "置顶群消息(群主 / 管理员)")
public CommonResult<Boolean> pinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
groupService.pinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId());
return success(true);
}
@PutMapping("/unpin-message")
@Operation(summary = "取消置顶群消息(群主 / 管理员)")
public CommonResult<Boolean> unpinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
groupService.unpinGroupMessage(getLoginUserId(), reqVO.getId(), reqVO.getMessageId());
return success(true);
}
// ==================== 群禁言 ====================
@PutMapping("/mute-all")
@Operation(summary = "全群禁言 / 取消(群主 / 管理员)")
public CommonResult<Boolean> muteAll(@Valid @RequestBody ImGroupMuteAllReqVO reqVO) {
groupService.muteAll(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/mute-member")
@Operation(summary = "禁言成员")
public CommonResult<Boolean> muteMember(@Valid @RequestBody ImGroupMuteMemberReqVO reqVO) {
groupService.muteMember(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/cancel-mute-member")
@Operation(summary = "取消成员禁言")
public CommonResult<Boolean> cancelMuteMember(@Valid @RequestBody ImGroupCancelMuteMemberReqVO reqVO) {
groupService.cancelMuteMember(getLoginUserId(), reqVO);
return success(true);
}
/** 单群转 VO + 关联回填 pinnedMessages仅当登录用户是该群有效成员 */
private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) {
if (group == null) {
return null;
}
return buildGroupRespVOList(Collections.singletonList(group), loginUserId).get(0);
}
/**
* 群列表批量转 VO + 关联回填 pinnedMessages
* <p>
* 仅当登录用户是某群的有效成员时才回填该群的 pinnedMessages避免非成员 / 已退群用户越权拿到置顶消息内容
*/
private List<ImGroupRespVO> buildGroupRespVOList(List<ImGroupDO> groups, Long loginUserId) {
if (CollUtil.isEmpty(groups)) {
return Collections.emptyList();
}
// 仅当前用户是有效成员的群才允许回填置顶消息
Map<Long, ImGroupMemberDO> memberMap = convertMap(
groupMemberService.getGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId);
Set<Long> activeGroupIds = convertSet(memberMap.values(), ImGroupMemberDO::getGroupId,
member -> CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus()));
Set<Long> allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId())
? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty());
Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
// 转换输出
return convertList(groups, group -> {
ImGroupRespVO groupVO = BeanUtils.toBean(group, ImGroupRespVO.class);
// 标记登录用户在该群的成员状态,供前端区分当前群与历史退群
boolean joined = activeGroupIds.contains(group.getId());
groupVO.setJoinStatus(joined ? CommonStatusEnum.ENABLE.getStatus() : CommonStatusEnum.DISABLE.getStatus());
MapUtils.findAndThen(memberMap, group.getId(), member ->
groupVO.setGroupRemark(member.getGroupRemark()).setSilent(Boolean.TRUE.equals(member.getSilent())));
if (!joined || CollUtil.isEmpty(group.getPinnedMessageIds())) {
return groupVO;
}
// 按 pin 顺序输出已被删除的消息messageMap 没命中)跳过
List<ImGroupMessageDO> pinnedMesages = convertList(group.getPinnedMessageIds(), messageMap::get);
return groupVO.setPinnedMessages(BeanUtils.toBean(pinnedMesages, ImGroupMessageRespVO.class));
});
}
}

View File

@@ -0,0 +1,140 @@
package cn.iocoder.yudao.module.im.controller.admin.group;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.group.vo.member.ImGroupMemberRespVO;
import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
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.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP;
@Tag(name = "管理后台 - 群成员")
@RestController
@RequestMapping("/im/group-member")
@Validated
public class ImGroupMemberController {
@Resource
private ImGroupMemberService groupMemberService;
@Resource
private AdminUserApi adminUserApi;
@PutMapping("/update")
@Operation(summary = "更新群成员")
public CommonResult<Boolean> updateGroupMember(@Valid @RequestBody ImGroupMemberUpdateReqVO updateReqVO) {
groupMemberService.updateGroupMember(getLoginUserId(), updateReqVO);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得群成员")
@Parameters({
@Parameter(name = "id", description = "编号(与 groupId + userId 二选一)", example = "1024"),
@Parameter(name = "groupId", description = "群编号(与 userId 配合查)", example = "1"),
@Parameter(name = "userId", description = "用户编号(与 groupId 配合查)", example = "100")
})
public CommonResult<ImGroupMemberRespVO> getGroupMember(@RequestParam(value = "id", required = false) Long id,
@RequestParam(value = "groupId", required = false) Long groupId,
@RequestParam(value = "userId", required = false) Long userId) {
// 1. 查询群成员
ImGroupMemberDO member;
if (id != null) {
member = groupMemberService.getGroupMember(id);
} else if (groupId != null && userId != null) {
member = groupMemberService.getGroupMember(groupId, userId);
} else {
// 避免 selectByGroupIdAndUserId 收到 null 参数走全表扫 / 抛 SQL 异常
throw new IllegalArgumentException("参数缺失:需传 id 或 (groupId, userId)");
}
if (member == null) {
return success(null);
}
// 2. 校验当前登录用户是该成员所在群的有效成员
Long loginUserId = getLoginUserId();
groupMemberService.validateMemberInGroup(member.getGroupId(), loginUserId);
// 3. 转化 VO
ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class);
AdminUserRespDTO user = adminUserApi.getUser(member.getUserId());
if (user != null) {
memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar());
}
hidePrivateFieldsIfNotSelf(memberVO, member.getUserId(), loginUserId);
return success(memberVO);
}
@GetMapping("/list")
@Operation(summary = "获得指定群的成员列表(按群全量拉取,作为前端本地成员 cache 的完整基线)")
@Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
public CommonResult<List<ImGroupMemberRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) {
// 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤)
// 说明:保留已退群成员,是为了前端展示历史消息时,仍能通过该接口拿到已退群成员的昵称 / 头像信息,避免显示为空
List<ImGroupMemberDO> members = groupMemberService.getGroupMemberListByGroupId(groupId);
// 1.2 校验当前登录用户是否为群的有效成员,非成员不可查看
Long loginUserId = getLoginUserId();
if (CollUtil.findOne(members, member -> loginUserId.equals(member.getUserId())
&& CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus())) == null) {
throw exception(GROUP_MEMBER_NOT_IN_GROUP);
}
// 2. 批量聚合昵称 / 头像,并对非本人置空私人字段
return success(buildGroupMemberRespVOList(members));
}
// ========== 私有方法VO 组装 ==========
/**
* 群成员列表批量转 VO 关联回填昵称 头像,并对非本人置空私人字段
*/
private List<ImGroupMemberRespVO> buildGroupMemberRespVOList(List<ImGroupMemberDO> members) {
if (CollUtil.isEmpty(members)) {
return Collections.emptyList();
}
Long loginUserId = getLoginUserId();
// 批量聚合 AdminUser 信息(昵称 头像),避免 N+1
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertList(members, ImGroupMemberDO::getUserId));
return convertList(members, member -> {
ImGroupMemberRespVO vo = BeanUtils.toBean(member, ImGroupMemberRespVO.class);
MapUtils.findAndThen(userMap, member.getUserId(), user ->
vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
hidePrivateFieldsIfNotSelf(vo, member.getUserId(), loginUserId);
return vo;
});
}
/**
* 非本人查看时置空成员的私人设置字段groupRemark / silent
*/
private void hidePrivateFieldsIfNotSelf(ImGroupMemberRespVO vo, Long memberUserId, Long loginUserId) {
if (ObjUtil.notEqual(loginUserId, memberUserId)) {
vo.setGroupRemark(null).setSilent(null);
}
}
}

View File

@@ -0,0 +1,174 @@
package cn.iocoder.yudao.module.im.controller.admin.group;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.group.vo.request.ImGroupRequestApplyReqVO;
import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
import cn.iocoder.yudao.module.im.service.group.ImGroupService;
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.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
/**
* IM 加群申请 Controller
*
* @author 芋道源码
*/
@Tag(name = "管理后台 - IM 加群申请")
@RestController
@RequestMapping("/im/group-request")
@Validated
public class ImGroupRequestController {
@Resource
private ImGroupRequestService groupRequestService;
@Resource
private ImGroupService groupService;
@Resource
private ImGroupMemberService groupMemberService;
@Resource
private AdminUserApi adminUserApi;
@PostMapping("/apply")
@Operation(summary = "申请加群")
public CommonResult<Long> applyJoinGroup(@Valid @RequestBody ImGroupRequestApplyReqVO reqVO) {
ImGroupRequestDO request = groupRequestService.applyJoinGroup(getLoginUserId(), reqVO);
return success(request != null ? request.getId() : null);
}
@PutMapping("/agree")
@Operation(summary = "同意加群申请(群主或管理员)")
@Parameter(name = "id", description = "申请编号", required = true, example = "1024")
public CommonResult<Boolean> agreeGroupRequest(
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id) {
groupRequestService.agreeGroupRequest(getLoginUserId(), id);
return success(true);
}
@PutMapping("/refuse")
@Operation(summary = "拒绝加群申请(群主或管理员)")
public CommonResult<Boolean> refuseGroupRequest(
@RequestParam("id") @NotNull(message = "申请编号不能为空") Long id,
@RequestParam(value = "handleContent", required = false)
@Size(max = 255, message = "处理理由最多 255 个字符") String handleContent) {
groupRequestService.refuseGroupRequest(getLoginUserId(), id, handleContent);
return success(true);
}
@GetMapping("/unhandled-list")
@Operation(summary = "查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表")
public CommonResult<List<ImGroupRequestRespVO>> getUnhandledRequestList() {
List<ImGroupRequestDO> list = groupRequestService.getUnhandledRequestListByOwnerOrAdmin(getLoginUserId());
return success(buildVOList(list));
}
@GetMapping("/pull")
@Operation(summary = "增量拉取「我管理的群」下的加群申请(重连 / 离线补偿)")
@Parameters({
@Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
@Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id首次拉取不传"),
@Parameter(name = "limit", description = "单次拉取条数", required = true)
})
public CommonResult<List<ImGroupRequestRespVO>> pullMyGroupRequestList(
@RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
@RequestParam(value = "lastId", required = false) Long lastId,
@RequestParam("limit") @Min(1) @Max(200) Integer limit) {
List<ImGroupRequestDO> list = groupRequestService.pullGroupRequestList(
getLoginUserId(), lastUpdateTime, lastId, limit);
return success(buildVOList(list));
}
@GetMapping("/list-by-group")
@Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查")
@Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
public CommonResult<List<ImGroupRequestRespVO>> getGroupRequestListByGroupId(
@RequestParam("groupId") @NotNull(message = "群编号不能为空") Long groupId) {
List<ImGroupRequestDO> list = groupRequestService.getGroupRequestListByGroupId(getLoginUserId(), groupId);
return success(buildVOList(list));
}
@GetMapping("/get")
@Operation(summary = "按 id 单查申请记录带越权过滤WebSocket 通知到达后用)")
@Parameter(name = "id", description = "申请记录编号", required = true)
public CommonResult<ImGroupRequestRespVO> getGroupRequest(@RequestParam("id") Long id) {
ImGroupRequestDO request = groupRequestService.getGroupRequest(id);
if (request == null) {
return success(null);
}
// 越权过滤:申请人 / 邀请人 / 群主 / 管理员之外,当不存在返回 null
Long currentUserId = getLoginUserId();
boolean canSee = ObjUtil.equal(request.getUserId(), currentUserId)
|| ObjUtil.equal(request.getInviterUserId(), currentUserId)
|| isGroupOwnerOrAdmin(request.getGroupId(), currentUserId);
if (!canSee) {
return success(null);
}
// 转换并返回
return success(CollUtil.getFirst(buildVOList(Collections.singletonList(request))));
}
/**
* 当前用户是否该群的有效群主 / 管理员
*/
private boolean isGroupOwnerOrAdmin(Long groupId, Long userId) {
ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId);
return member != null
&& !CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())
&& ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole());
}
/** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */
private List<ImGroupRequestRespVO> buildVOList(List<ImGroupRequestDO> list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
// 1. 聚合 user / inviter 用户信息convertSetByFlatMap 内部已过滤 null
Set<Long> userIds = convertSetByFlatMap(list,
request -> Stream.of(request.getUserId(), request.getInviterUserId()));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
// 2. 聚合群信息(封禁 / 解散群也要回填,便于前端展示历史)
Set<Long> groupIds = convertSet(list, ImGroupRequestDO::getGroupId);
Map<Long, ImGroupDO> groupMap = groupService.getGroupMap(groupIds);
return convertList(list, request -> {
ImGroupRequestRespVO vo = BeanUtils.toBean(request, ImGroupRequestRespVO.class);
MapUtils.findAndThen(userMap, request.getUserId(), user ->
vo.setUserNickname(user.getNickname()).setUserAvatar(user.getAvatar()));
MapUtils.findAndThen(userMap, request.getInviterUserId(), user ->
vo.setInviterNickname(user.getNickname()).setInviterAvatar(user.getAvatar()));
MapUtils.findAndThen(groupMap, request.getGroupId(), group ->
vo.setGroupName(group.getName()).setGroupAvatar(group.getAvatar()));
return vo;
});
}
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 添加群管理员 Request VO")
@Data
public class ImGroupAdminAddReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]")
@NotEmpty(message = "目标用户编号列表不能为空")
private List<Long> userIds;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 撤销群管理员 Request VO")
@Data
public class ImGroupAdminRemoveReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]")
@NotEmpty(message = "目标用户编号列表不能为空")
private List<Long> userIds;
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 取消成员禁言 Request VO")
@Data
public class ImGroupCancelMuteMemberReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "被取消禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
@NotNull(message = "用户编号不能为空")
private Long userId;
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 群创建 Request VO")
@Data
public class ImGroupCreateReqVO {
@Schema(description = "群名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道技术交流群")
@NotBlank(message = "群名称不能为空")
@Size(max = 64, message = "群名称长度不能超过 64")
private String name;
@Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]")
private List<Long> memberUserIds;
@Schema(description = "进群是否需群主 / 管理员审批;不传默认 false 自由进群", example = "false")
private Boolean joinApproval;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理后台 - 群消息置顶 / 取消置顶 Request VO
*/
@Schema(description = "管理后台 - 群消息置顶 / 取消置顶 Request VO")
@Data
public class ImGroupMessagePinReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9527")
@NotNull(message = "消息编号不能为空")
private Long messageId;
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 全群禁言 / 取消 Request VO")
@Data
public class ImGroupMuteAllReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "是否全群禁言", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否全群禁言不能为空")
private Boolean mutedAll;
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 成员禁言 Request VO")
@Data
public class ImGroupMuteMemberReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "被禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
@NotNull(message = "用户编号不能为空")
private Long userId;
@Schema(description = "禁言时长0 表示永久禁言", requiredMode = Schema.RequiredMode.REQUIRED, example = "600")
@NotNull(message = "禁言时长不能为空")
@Min(value = 0, message = "禁言时长不能小于 0 秒")
private Integer mutedSeconds;
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
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 = "管理后台 - 群分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ImGroupPageReqVO extends PageParam {
@Schema(description = "群名称", example = "芋艿")
private String name;
@Schema(description = "群主用户编号", example = "31460")
private Long ownerUserId;
@Schema(description = "群公告")
private String notice;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 群 Response VO")
@Data
public class ImGroupRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003")
private Long id;
@Schema(description = "群名称", example = "芋艿")
private String name;
@Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460")
private Long ownerUserId;
@Schema(description = "群头像")
private String avatar;
@Schema(description = "群公告")
private String notice;
@Schema(description = "是否封禁")
private Boolean banned;
@Schema(description = "是否全群禁言")
private Boolean mutedAll;
@Schema(description = "进群是否需群主 / 管理员审批", example = "false")
private Boolean joinApproval;
@Schema(description = "封禁时间")
private LocalDateTime bannedTime;
@Schema(description = "群状态", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer status;
@Schema(description = "解散时间")
private LocalDateTime dissolvedTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "当前登录用户在该群的成员状态:在群 / 已退群(历史退群群仍返回,供前端展示离线消息的群名 / 头像)")
private Integer joinStatus; // 参见 CommonStatusEnum 枚举
@Schema(description = "当前登录用户对该群的备注")
private String groupRemark;
@Schema(description = "当前登录用户是否免打扰")
private Boolean silent;
@Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空")
private List<ImGroupMessageRespVO> pinnedMessages;
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import jakarta.validation.constraints.*;
@Schema(description = "管理后台 - 群新增/修改 Request VO")
@Data
public class ImGroupSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003")
private Long id;
@Schema(description = "群名称", example = "芋艿")
private String name;
@Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460")
@NotNull(message = "群主用户编号不能为空")
private Long ownerUserId;
@Schema(description = "群头像")
private String avatar;
@Schema(description = "群公告")
private String notice;
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 群主转让 Request VO")
@Data
public class ImGroupTransferOwnerReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "新群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "202")
@NotNull(message = "新群主用户编号不能为空")
private Long newOwnerUserId;
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 群更新 Request VO")
@Data
public class ImGroupUpdateReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "群名称", example = "芋道技术交流群")
@Size(max = 64, message = "群名称长度不能超过 64")
private String name;
@Schema(description = "群头像")
@Size(max = 512, message = "群头像长度不能超过 512")
private String avatar;
@Schema(description = "群公告")
@Size(max = 2048, message = "群公告长度不能超过 2048")
private String notice;
@Schema(description = "进群是否需群主 / 管理员审批", example = "true")
private Boolean joinApproval;
@AssertTrue(message = "群名称不能为空")
@JsonIgnore
public boolean isNameValid() {
return name == null || StrUtil.isNotBlank(name);
}
@AssertTrue(message = "群头像不能为空")
@JsonIgnore
public boolean isAvatarValid() {
return avatar == null || StrUtil.isNotBlank(avatar);
}
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 群成员邀请 Request VO")
@Data
public class ImGroupMemberCreateReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
@NotNull(message = "群编号不能为空")
private Long groupId;
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730")
@NotNull(message = "用户编号不能为空")
private Long userId;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 群成员邀请 Request VO")
@Data
public class ImGroupMemberInviteReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
@NotNull(message = "群编号不能为空")
private Long groupId;
@Schema(description = "被邀请的用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]")
@NotEmpty(message = "被邀请的用户编号列表不能为空")
private List<Long> memberUserIds;
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 群成员移除 Request VO")
@Data
public class ImGroupMemberRemoveReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "群编号不能为空")
private Long groupId;
@Schema(description = "被移除的用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]")
@NotEmpty(message = "被移除的用户编号列表不能为空")
private List<Long> memberUserIds;
}

View File

@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 群成员 Response VO")
@Data
public class ImGroupMemberRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17071")
private Long id;
@Schema(description = "群编号", example = "13279")
private Long groupId;
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730")
private Long userId;
@Schema(description = "组内显示名", example = "芋艿")
private String displayUserName;
@Schema(description = "群备注", example = "核心群")
private String groupRemark;
@Schema(description = "是否免打扰")
private Boolean silent;
@Schema(description = "成员状态", example = "0")
private Integer status;
@Schema(description = "成员角色", example = "3")
private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类
@Schema(description = "入群时间")
private LocalDateTime joinTime;
@Schema(description = "退群时间")
private LocalDateTime quitTime;
@Schema(description = "禁言到期时间")
private LocalDateTime muteEndTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
// ========== 关联 AdminUser 的字段 ==========
@Schema(description = "用户昵称", example = "芋道")
private String nickname;
@Schema(description = "用户头像")
private String avatar;
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
@Schema(description = "管理后台 - 群成员更新 Request VO")
@Data
@Accessors(chain = true)
public class ImGroupMemberUpdateReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "群编号不能为空")
private Long groupId;
@Schema(description = "群内昵称", example = "芋头")
private String displayUserName;
@Schema(description = "群备注", example = "公司群")
private String groupRemark;
@Schema(description = "是否免打扰")
private Boolean silent;
}

View File

@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo.request;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 加群申请发起 Request VO")
@Data
public class ImGroupRequestApplyReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "群编号不能为空")
private Long groupId;
@Schema(description = "申请理由", example = "我是芋艿(一种食材)")
@Size(max = 255, message = "申请理由最多 255 个字符")
private String applyContent;
@Schema(description = "加入来源", example = "1")
@InEnum(ImGroupAddSourceEnum.class)
private Integer addSource;
}

View File

@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.im.controller.admin.group.vo.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 加群申请 Response VO")
@Data
public class ImGroupRequestRespVO {
@Schema(description = "申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long groupId;
@Schema(description = "申请人 / 被邀请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Long userId;
@Schema(description = "邀请人用户编号NULL 表示用户主动申请", example = "200")
private Long inviterUserId;
@Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer handleResult; // 参见 ImGroupRequestHandleResultEnum 枚举
@Schema(description = "申请理由", example = "我想加入这个群")
private String applyContent;
@Schema(description = "处理理由(拒绝时可选填)", example = "暂不通过")
private String handleContent;
@Schema(description = "处理人用户编号", example = "31460")
private Long handleUserId;
@Schema(description = "加入来源", example = "1")
private Integer addSource; // 参见 ImGroupAddSourceEnum 枚举
@Schema(description = "处理时间")
private LocalDateTime handleTime;
@Schema(description = "申请创建时间")
private LocalDateTime createTime;
@Schema(description = "申请更新时间;增量拉取游标")
private LocalDateTime updateTime;
// ========== 下面是聚合字段,方便前端显示 ==========
@Schema(description = "申请人 / 被邀请人昵称(实时聚合自 AdminUser", example = "芋道")
private String userNickname;
@Schema(description = "申请人 / 被邀请人头像(实时聚合自 AdminUser")
private String userAvatar;
@Schema(description = "邀请人昵称(实时聚合自 AdminUser", example = "老张")
private String inviterNickname;
@Schema(description = "邀请人头像(实时聚合自 AdminUser")
private String inviterAvatar;
@Schema(description = "群名称(实时聚合自 ImGroup", example = "芋道技术交流群")
private String groupName;
@Schema(description = "群头像(实时聚合自 ImGroup")
private String groupAvatar;
}

View File

@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelRespVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel.ImChannelSaveReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
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.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IM 频道")
@RestController
@RequestMapping("/im/manager/channel")
@Validated
public class ImChannelManagerController {
@Resource
private ImChannelService channelService;
@PostMapping("/create")
@Operation(summary = "新增频道")
@PreAuthorize("@ss.hasPermission('im:manager:channel:create')")
public CommonResult<Long> createChannel(@Valid @RequestBody ImChannelSaveReqVO reqVO) {
return success(channelService.createChannel(reqVO));
}
@PutMapping("/update")
@Operation(summary = "修改频道")
@PreAuthorize("@ss.hasPermission('im:manager:channel:update')")
public CommonResult<Boolean> updateChannel(@Valid @RequestBody ImChannelSaveReqVO reqVO) {
channelService.updateChannel(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除频道")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:channel:delete')")
public CommonResult<Boolean> deleteChannel(@RequestParam("id") Long id) {
channelService.deleteChannel(id);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得频道分页")
@PreAuthorize("@ss.hasPermission('im:manager:channel:query')")
public CommonResult<PageResult<ImChannelRespVO>> getChannelPage(@Valid ImChannelPageReqVO pageReqVO) {
PageResult<ImChannelDO> pageResult = channelService.getChannelPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ImChannelRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得频道详情")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:channel:query')")
public CommonResult<ImChannelRespVO> getChannel(@RequestParam("id") Long id) {
ImChannelDO channel = channelService.getChannel(id);
return success(BeanUtils.toBean(channel, ImChannelRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得启用的频道精简列表;前端表单选择频道时调用")
public CommonResult<List<ImChannelRespVO>> getSimpleChannelList() {
List<ImChannelDO> list = channelService.getChannelListByStatus(CommonStatusEnum.ENABLE.getStatus());
return success(BeanUtils.toBean(list, ImChannelRespVO.class));
}
}

View File

@@ -0,0 +1,99 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel;
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.channel.vo.material.ImChannelMaterialPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialRespVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material.ImChannelMaterialSaveReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
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.*;
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.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - IM 频道素材")
@RestController
@RequestMapping("/im/manager/channel-material")
@Validated
public class ImChannelMaterialManagerController {
@Resource
private ImChannelMaterialService channelMaterialService;
@Resource
private ImChannelService channelService;
@PostMapping("/create")
@Operation(summary = "新增素材")
@PreAuthorize("@ss.hasPermission('im:manager:channel-material:create')")
public CommonResult<Long> createMaterial(@Valid @RequestBody ImChannelMaterialSaveReqVO reqVO) {
return success(channelMaterialService.createMaterial(reqVO));
}
@PutMapping("/update")
@Operation(summary = "修改素材")
@PreAuthorize("@ss.hasPermission('im:manager:channel-material:update')")
public CommonResult<Boolean> updateMaterial(@Valid @RequestBody ImChannelMaterialSaveReqVO reqVO) {
channelMaterialService.updateMaterial(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除素材")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:channel-material:delete')")
public CommonResult<Boolean> deleteMaterial(@RequestParam("id") Long id) {
channelMaterialService.deleteMaterial(id);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得素材分页;含频道名回填")
@PreAuthorize("@ss.hasPermission('im:manager:channel-material:query')")
public CommonResult<PageResult<ImChannelMaterialRespVO>> getMaterialPage(@Valid ImChannelMaterialPageReqVO pageReqVO) {
PageResult<ImChannelMaterialDO> pageResult = channelMaterialService.getMaterialPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 回填频道名
List<ImChannelDO> channels = channelService.getChannelList(
convertSet(pageResult.getList(), ImChannelMaterialDO::getChannelId));
Map<Long, ImChannelDO> channelMap = convertMap(channels, ImChannelDO::getId);
return success(BeanUtils.toBean(pageResult, ImChannelMaterialRespVO.class, vo ->
MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName()))));
}
@GetMapping("/get")
@Operation(summary = "获得素材详情(含富文本正文)")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:channel-material:query')")
public CommonResult<ImChannelMaterialRespVO> getMaterial(@RequestParam("id") Long id) {
ImChannelMaterialDO material = channelMaterialService.getMaterial(id);
return success(BeanUtils.toBean(material, ImChannelMaterialRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得指定频道下的素材精简列表;用于推送弹窗的素材下拉")
@Parameter(name = "channelId", description = "频道编号", required = true, example = "1")
public CommonResult<List<ImChannelMaterialRespVO>> getSimpleMaterialList(@RequestParam("channelId") Long channelId) {
List<ImChannelMaterialDO> list = channelMaterialService.getMaterialListByChannelId(channelId);
return success(BeanUtils.toBean(list, ImChannelMaterialRespVO.class));
}
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - IM 频道分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ImChannelPageReqVO extends PageParam {
@Schema(description = "频道业务码", example = "system_notice")
private String code;
@Schema(description = "频道名称", example = "系统")
private String name;
@Schema(description = "状态", example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 频道 Response VO")
@Data
public class ImChannelRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "频道业务码", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_notice")
private String code;
@Schema(description = "频道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统公告")
private String name;
@Schema(description = "频道头像")
private String avatar;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 频道新增 / 修改 Request VO")
@Data
public class ImChannelSaveReqVO {
@Schema(description = "编号(修改时必填)", example = "1024")
private Long id;
@Schema(description = "频道业务码;唯一", requiredMode = Schema.RequiredMode.REQUIRED, example = "system_notice")
@NotBlank(message = "频道编码不能为空")
@Size(max = 64, message = "频道编码长度不能超过 64")
@Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "频道编码只能由小写字母 / 数字 / 下划线组成,且必须以字母开头")
private String code;
@Schema(description = "频道名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "系统公告")
@NotBlank(message = "频道名称不能为空")
@Size(max = 64, message = "频道名称长度不能超过 64")
private String name;
@Schema(description = "频道头像", example = "https://cdn.example.com/channel/system_notice.png")
@Size(max = 512, message = "头像长度不能超过 512")
private String avatar;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "排序不能为空")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
private Integer status; // 参见 CommonStatusEnum 枚举类0 启用 / 1 禁用)
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
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 ImChannelMaterialPageReqVO extends PageParam {
@Schema(description = "频道编号", example = "1")
private Long channelId;
@Schema(description = "内容类型", example = "1")
private Integer type; // 参见 ImChannelMaterialTypeEnum 枚举类
@Schema(description = "标题", example = "活动")
private String title;
@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[] createTime;
}

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 频道素材 Response VO")
@Data
public class ImChannelMaterialRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long channelId;
@Schema(description = "频道名称(关联查询填充)")
private String channelName;
@Schema(description = "内容类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type; // 参见 ImChannelMaterialTypeEnum 枚举类
@Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED)
private String title;
@Schema(description = "封面图")
private String coverUrl;
@Schema(description = "摘要")
private String summary;
@Schema(description = "正文;富文本 HTML")
private String content;
@Schema(description = "跳转链接")
private String url;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.channel.vo.material;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 频道素材新增 / 修改 Request VO")
@Data
public class ImChannelMaterialSaveReqVO {
@Schema(description = "编号(修改时必填)", example = "1024")
private Long id;
@Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "频道编号不能为空")
private Long channelId;
@Schema(description = "内容类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "内容类型不能为空")
private Integer type; // 参见 ImChannelMaterialTypeEnum 枚举类
@Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "双十一活动来啦")
@NotBlank(message = "标题不能为空")
@Size(max = 128, message = "标题长度不能超过 128")
private String title;
@Schema(description = "封面图", example = "https://cdn.example.com/cover.png")
@Size(max = 512, message = "封面图长度不能超过 512")
private String coverUrl;
@Schema(description = "摘要", example = "全场五折,戳详情看玩法")
@Size(max = 255, message = "摘要长度不能超过 255")
private String summary;
@Schema(description = "正文;富文本 HTML")
private String content;
@Schema(description = "跳转链接;为空表示走客户端内置详情页", example = "https://example.com/activity/123")
@Size(max = 512, message = "跳转链接长度不能超过 512")
private String url;
}

View File

@@ -0,0 +1,85 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item.ImFacePackItemPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item.ImFacePackItemRespVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item.ImFacePackItemSaveReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackItemDO;
import cn.iocoder.yudao.module.im.service.face.ImFacePackItemService;
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 jakarta.validation.constraints.Size;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IM 表情包项")
@RestController
@RequestMapping("/im/manager/face-pack-item")
@Validated
public class ImFacePackItemManagerController {
@Resource
private ImFacePackItemService facePackItemService;
@PostMapping("/create")
@Operation(summary = "新增表情")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:create')")
public CommonResult<Long> createFacePackItem(@Valid @RequestBody ImFacePackItemSaveReqVO reqVO) {
return success(facePackItemService.createFacePackItem(reqVO));
}
@PutMapping("/update")
@Operation(summary = "修改表情")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:update')")
public CommonResult<Boolean> updateFacePackItem(@Valid @RequestBody ImFacePackItemSaveReqVO reqVO) {
facePackItemService.updateFacePackItem(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除表情")
@Parameter(name = "id", description = "编号", required = true, example = "2048")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:delete')")
public CommonResult<Boolean> deleteFacePackItem(@RequestParam("id") Long id) {
facePackItemService.deleteFacePackItem(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除表情")
@Parameter(name = "ids", description = "编号列表", required = true)
@PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:delete')")
public CommonResult<Boolean> deleteFacePackItemList(
@RequestParam("ids") @Size(max = 100, message = "批量删除最多 100 条") List<Long> ids) {
facePackItemService.deleteFacePackItemList(ids);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得表情分页")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:query')")
public CommonResult<PageResult<ImFacePackItemRespVO>> getFacePackItemPage(@Valid ImFacePackItemPageReqVO pageReqVO) {
PageResult<ImFacePackItemDO> pageResult = facePackItemService.getFacePackItemPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ImFacePackItemRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得表情详情")
@Parameter(name = "id", description = "编号", required = true, example = "2048")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:query')")
public CommonResult<ImFacePackItemRespVO> getFacePackItem(@RequestParam("id") Long id) {
ImFacePackItemDO item = facePackItemService.getFacePackItem(id);
return success(BeanUtils.toBean(item, ImFacePackItemRespVO.class));
}
}

View File

@@ -0,0 +1,85 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack.ImFacePackPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack.ImFacePackRespVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack.ImFacePackSaveReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO;
import cn.iocoder.yudao.module.im.service.face.ImFacePackService;
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 jakarta.validation.constraints.Size;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IM 表情包")
@RestController
@RequestMapping("/im/manager/face-pack")
@Validated
public class ImFacePackManagerController {
@Resource
private ImFacePackService facePackService;
@PostMapping("/create")
@Operation(summary = "新增表情包")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack:create')")
public CommonResult<Long> createFacePack(@Valid @RequestBody ImFacePackSaveReqVO reqVO) {
return success(facePackService.createFacePack(reqVO));
}
@PutMapping("/update")
@Operation(summary = "修改表情包")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack:update')")
public CommonResult<Boolean> updateFacePack(@Valid @RequestBody ImFacePackSaveReqVO reqVO) {
facePackService.updateFacePack(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除表情包")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack:delete')")
public CommonResult<Boolean> deleteFacePack(@RequestParam("id") Long id) {
facePackService.deleteFacePack(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除表情包")
@Parameter(name = "ids", description = "编号列表", required = true)
@PreAuthorize("@ss.hasPermission('im:manager:face-pack:delete')")
public CommonResult<Boolean> deleteFacePackList(@RequestParam("ids")
@Size(max = 100, message = "批量删除最多 100 条") List<Long> ids) {
facePackService.deleteFacePackList(ids);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得表情包分页")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack:query')")
public CommonResult<PageResult<ImFacePackRespVO>> getFacePackPage(@Valid ImFacePackPageReqVO pageReqVO) {
PageResult<ImFacePackDO> pageResult = facePackService.getFacePackPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, ImFacePackRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得表情包详情")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:face-pack:query')")
public CommonResult<ImFacePackRespVO> getFacePack(@RequestParam("id") Long id) {
ImFacePackDO pack = facePackService.getFacePack(id);
return success(BeanUtils.toBean(pack, ImFacePackRespVO.class));
}
}

View File

@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem.ImFaceUserItemManagerPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem.ImFaceUserItemManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFaceUserItemDO;
import cn.iocoder.yudao.module.im.service.face.ImFaceUserItemService;
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.*;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IM 用户表情")
@RestController
@RequestMapping("/im/manager/face-user-item")
@Validated
public class ImFaceUserItemManagerController {
@Resource
private ImFaceUserItemService faceUserItemService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得用户表情分页")
@PreAuthorize("@ss.hasPermission('im:manager:face-user-item:query')")
public CommonResult<PageResult<ImFaceUserItemManagerRespVO>> getFaceUserItemPage(
@Valid ImFaceUserItemManagerPageReqVO pageReqVO) {
PageResult<ImFaceUserItemDO> pageResult = faceUserItemService.getFaceUserItemPage(pageReqVO);
// 关联回填用户昵称
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
CollectionUtils.convertSet(pageResult.getList(), ImFaceUserItemDO::getUserId));
List<ImFaceUserItemManagerRespVO> voList = CollectionUtils.convertList(pageResult.getList(), item -> {
ImFaceUserItemManagerRespVO vo = BeanUtils.toBean(item, ImFaceUserItemManagerRespVO.class);
AdminUserRespDTO user = userMap.get(item.getUserId());
if (user != null) {
vo.setUserNickname(user.getNickname());
}
return vo;
});
return success(new PageResult<>(voList, pageResult.getTotal()));
}
@DeleteMapping("/delete")
@Operation(summary = "删除用户表情")
@Parameter(name = "id", description = "编号", required = true, example = "4096")
@PreAuthorize("@ss.hasPermission('im:manager:face-user-item:delete')")
public CommonResult<Boolean> deleteFaceUserItem(@RequestParam("id") Long id) {
faceUserItemService.deleteFaceUserItem(id);
return success(true);
}
}

View File

@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - IM 表情包项分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ImFacePackItemPageReqVO extends PageParam {
@Schema(description = "所属表情包编号", example = "1024")
private Long packId;
@Schema(description = "表情名,模糊匹配", example = "")
private String name;
@Schema(description = "状态", example = "0")
@InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
private Integer status; // 参见 CommonStatusEnum 枚举类
}

View File

@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 表情包项 Response VO")
@Data
public class ImFacePackItemRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long id;
@Schema(description = "所属表情包编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long packId;
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
example = "https://cdn.example.com/face/pack/cat-001.png")
private String url;
@Schema(description = "表情名", example = "狗头")
private String name;
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
private Integer width;
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
private Integer height;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 表情包项新增 / 修改 Request VO")
@Data
public class ImFacePackItemSaveReqVO {
@Schema(description = "编号(修改时必填)", example = "2048")
private Long id;
@Schema(description = "所属表情包编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "表情包编号不能为空")
private Long packId;
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
example = "https://cdn.example.com/face/pack/cat-001.png")
@NotBlank(message = "表情图 URL 不能为空")
@Size(max = 512, message = "表情图 URL 长度不能超过 512")
private String url;
@Schema(description = "表情名", example = "狗头")
@Size(max = 64, message = "表情名长度不能超过 64")
private String name;
@Schema(description = "渲染宽度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
@NotNull(message = "渲染宽度不能为空")
@Min(value = 1, message = "渲染宽度不能小于 1 像素")
@Max(value = 2048, message = "渲染宽度不能大于 2048 像素")
private Integer width;
@Schema(description = "渲染高度(像素)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
@NotNull(message = "渲染高度不能为空")
@Min(value = 1, message = "渲染高度不能小于 1 像素")
@Max(value = 2048, message = "渲染高度不能大于 2048 像素")
private Integer height;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "排序不能为空")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
private Integer status; // 参见 CommonStatusEnum 枚举类0 启用 / 1 禁用)
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
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 ImFacePackPageReqVO extends PageParam {
@Schema(description = "表情包名称,模糊匹配", example = "")
private String name;
@Schema(description = "状态", example = "0")
@InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
private Integer status; // 参见 CommonStatusEnum 枚举类
@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[] createTime;
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 表情包 Response VO")
@Data
public class ImFacePackRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
private String name;
@Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
private String icon;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 表情包新增 / 修改 Request VO")
@Data
public class ImFacePackSaveReqVO {
@Schema(description = "编号(修改时必填)", example = "1024")
private Long id;
@Schema(description = "表情包名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "猫主子")
@NotBlank(message = "表情包名称不能为空")
@Size(max = 64, message = "表情包名称长度不能超过 64")
private String name;
@Schema(description = "表情包图标", example = "https://cdn.example.com/face/pack/cat.png")
@Size(max = 512, message = "图标长度不能超过 512")
private String icon;
@Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "排序不能为空")
private Integer sort;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
private Integer status; // 参见 CommonStatusEnum 枚举类0 启用 / 1 禁用)
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem;
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 ImFaceUserItemManagerPageReqVO extends PageParam {
@Schema(description = "所属用户编号", example = "1024")
private Long userId;
@Schema(description = "表情名,模糊匹配", example = "")
private String name;
@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[] createTime;
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.useritem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 用户表情 Response VO")
@Data
public class ImFaceUserItemManagerRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
private Long id;
@Schema(description = "所属用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "所属用户昵称", example = "张三")
private String userNickname;
@Schema(description = "表情图 URL", requiredMode = Schema.RequiredMode.REQUIRED,
example = "https://cdn.example.com/face/user/abc.gif")
private String url;
@Schema(description = "表情名", example = "狗头")
private String name;
@Schema(description = "渲染宽度(像素)", example = "200")
private Integer width;
@Schema(description = "渲染高度(像素)", example = "200")
private Integer height;
@Schema(description = "添加时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.friend;
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.friend.vo.ImFriendManagerPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
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.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.RestController;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
@Tag(name = "管理后台 - IM 好友管理")
@RestController
@RequestMapping("/im/manager/friend")
@Validated
public class ImFriendManagerController {
@Resource
private ImFriendService friendService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得好友关系分页")
@PreAuthorize("@ss.hasPermission('im:manager:friend:query')")
public CommonResult<PageResult<ImFriendManagerRespVO>> getFriendPage(
@Valid ImFriendManagerPageReqVO pageReqVO) {
// 1. 分页查询
PageResult<ImFriendDO> pageResult = friendService.getFriendPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2.1 一次性批量查询用户 + 好友的昵称
Set<Long> userIds = convertSetByFlatMap(pageResult.getList(),
f -> Stream.of(f.getUserId(), f.getFriendUserId()));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
// 2.2 转换为 VO填充昵称
return success(BeanUtils.toBean(pageResult, ImFriendManagerRespVO.class, vo -> {
MapUtils.findAndThen(userMap, vo.getUserId(),
user -> vo.setUserNickname(user.getNickname()));
MapUtils.findAndThen(userMap, vo.getFriendUserId(),
user -> vo.setFriendNickname(user.getNickname()));
}));
}
}

View File

@@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.friend;
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.friend.vo.ImFriendRequestManagerPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendRequestManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
import cn.iocoder.yudao.module.im.service.friend.ImFriendRequestService;
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.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.RestController;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
@Tag(name = "管理后台 - IM 好友申请管理")
@RestController
@RequestMapping("/im/manager/friend-request")
@Validated
public class ImFriendRequestManagerController {
@Resource
private ImFriendRequestService friendRequestService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得好友申请分页")
@PreAuthorize("@ss.hasPermission('im:manager:friend-request:query')")
public CommonResult<PageResult<ImFriendRequestManagerRespVO>> getFriendRequestPage(
@Valid ImFriendRequestManagerPageReqVO pageReqVO) {
// 1. 分页查询
PageResult<ImFriendRequestDO> pageResult = friendRequestService.getFriendRequestPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2.1 一次性批量查询发起方 + 接收方的昵称
Set<Long> userIds = convertSetByFlatMap(pageResult.getList(),
request -> Stream.of(request.getFromUserId(), request.getToUserId()));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
// 2.2 转换为 VO填充昵称
return success(BeanUtils.toBean(pageResult, ImFriendRequestManagerRespVO.class, vo -> {
MapUtils.findAndThen(userMap, vo.getFromUserId(),
user -> vo.setFromNickname(user.getNickname()));
MapUtils.findAndThen(userMap, vo.getToUserId(),
user -> vo.setToNickname(user.getNickname()));
}));
}
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.friend.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 ImFriendManagerPageReqVO extends PageParam {
@Schema(description = "用户编号", example = "1024")
private Long userId;
@Schema(description = "好友用户编号", example = "2048")
private Long friendUserId;
@Schema(description = "好友状态", example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "是否免打扰", example = "false")
private Boolean silent;
@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[] addTime;
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 好友关系 Response VO")
@Data
public class ImFriendManagerRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "用户昵称", example = "张三")
private String userNickname;
@Schema(description = "好友用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long friendUserId;
@Schema(description = "好友昵称", example = "李四")
private String friendNickname;
@Schema(description = "好友展示备注")
private String displayName;
@Schema(description = "添加来源", example = "1")
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举类
@Schema(description = "是否免打扰", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean silent;
@Schema(description = "是否置顶联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean pinned;
@Schema(description = "是否拉黑", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean blocked;
@Schema(description = "好友状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "添加好友时间")
private LocalDateTime addTime;
@Schema(description = "删除好友时间")
private LocalDateTime deleteTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.friend.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 ImFriendRequestManagerPageReqVO extends PageParam {
@Schema(description = "发起方用户编号", example = "1024")
private Long fromUserId;
@Schema(description = "接收方用户编号", example = "2048")
private Long toUserId;
@Schema(description = "处理结果", example = "0")
private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举类
@Schema(description = "添加来源", example = "1")
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举类
@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[] createTime;
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 好友申请 Response VO")
@Data
public class ImFriendRequestManagerRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "发起方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long fromUserId;
@Schema(description = "发起方昵称", example = "张三")
private String fromNickname;
@Schema(description = "接收方用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long toUserId;
@Schema(description = "接收方昵称", example = "李四")
private String toNickname;
@Schema(description = "申请理由", example = "我是芋艿")
private String applyContent;
@Schema(description = "发起方对接收方的备注")
private String displayName;
@Schema(description = "添加来源", example = "1")
private Integer addSource; // 参见 ImFriendAddSourceEnum 枚举类
@Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer handleResult; // 参见 ImFriendRequestHandleResultEnum 枚举类
@Schema(description = "处理理由", example = "暂不通过")
private String handleContent;
@Schema(description = "处理时间")
private LocalDateTime handleTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group;
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.group.vo.ImGroupManagerBanReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
import cn.iocoder.yudao.module.im.service.group.ImGroupService;
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.*;
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;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - IM 群聊管理")
@RestController
@RequestMapping("/im/manager/group")
@Validated
public class ImGroupManagerController {
@Resource
private ImGroupService groupService;
@Resource
private ImGroupMemberService groupMemberService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得群分页")
@PreAuthorize("@ss.hasPermission('im:manager:group:query')")
public CommonResult<PageResult<ImGroupManagerRespVO>> getGroupPage(@Valid ImGroupManagerPageReqVO pageReqVO) {
// 1. 分页查询群
PageResult<ImGroupDO> pageResult = groupService.getGroupPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2.1 批量查询相关数据
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(pageResult.getList(), ImGroupDO::getOwnerUserId));
Map<Long, Long> memberCountMap = groupMemberService.getActiveMemberCountMap(
convertSet(pageResult.getList(), ImGroupDO::getId));
// 2.2 转换为 VO填充群主昵称、群成员数量
return success(BeanUtils.toBean(pageResult, ImGroupManagerRespVO.class, vo -> {
MapUtils.findAndThen(userMap, vo.getOwnerUserId(),
user -> vo.setOwnerNickname(user.getNickname()));
vo.setMemberCount(memberCountMap.getOrDefault(vo.getId(), 0L).intValue());
}));
}
@GetMapping("/get")
@Operation(summary = "获得群详情")
@Parameter(name = "id", description = "群编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:group:query')")
public CommonResult<ImGroupManagerRespVO> getGroup(@RequestParam("id") Long id) {
ImGroupDO group = groupService.getGroup(id);
return success(BeanUtils.toBean(group, ImGroupManagerRespVO.class));
}
@PutMapping("/ban")
@Operation(summary = "封禁群")
@PreAuthorize("@ss.hasPermission('im:manager:group:ban')")
public CommonResult<Boolean> banGroup(@Valid @RequestBody ImGroupManagerBanReqVO reqVO) {
groupService.banGroup(getLoginUserId(), reqVO);
return success(true);
}
@PutMapping("/unban")
@Operation(summary = "解封群")
@Parameter(name = "id", description = "群编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:group:ban')")
public CommonResult<Boolean> unbanGroup(@RequestParam("id") Long id) {
groupService.unbanGroup(getLoginUserId(), id);
return success(true);
}
@DeleteMapping("/dissolve")
@Operation(summary = "解散群")
@Parameter(name = "id", description = "群编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:group:dissolve')")
public CommonResult<Boolean> dissolveGroup(@RequestParam("id") Long id) {
groupService.dissolveGroupByManager(getLoginUserId(), id);
return success(true);
}
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.group.vo.member.ImGroupMemberManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
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 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/group/member")
@Validated
public class ImGroupMemberManagerController {
@Resource
private ImGroupMemberService groupMemberService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/list")
@Operation(summary = "获得群成员列表(含已退群成员,由前端按需过滤)")
@Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:group:query')")
public CommonResult<List<ImGroupMemberManagerRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) {
// 1. 查询群全部成员(含已退群)
List<ImGroupMemberDO> members = groupMemberService.getGroupMemberListByGroupId(groupId);
if (CollUtil.isEmpty(members)) {
return success(Collections.emptyList());
}
// 2.1 批量查询用户信息
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(members, ImGroupMemberDO::getUserId));
// 2.2 转换为 VO填充昵称、头像
return success(BeanUtils.toBean(members, ImGroupMemberManagerRespVO.class, vo ->
MapUtils.findAndThen(userMap, vo.getUserId(), user ->
vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()))));
}
}

View File

@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group;
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.group.vo.ImGroupRequestManagerPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
import cn.iocoder.yudao.module.im.service.group.ImGroupService;
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.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.RestController;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
@Tag(name = "管理后台 - IM 加群申请管理")
@RestController
@RequestMapping("/im/manager/group-request")
@Validated
public class ImGroupRequestManagerController {
@Resource
private ImGroupRequestService groupRequestService;
@Resource
private ImGroupService groupService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得加群申请分页")
@PreAuthorize("@ss.hasPermission('im:manager:group-request:query')")
public CommonResult<PageResult<ImGroupRequestManagerRespVO>> getGroupRequestPage(
@Valid ImGroupRequestManagerPageReqVO pageReqVO) {
// 1. 分页查询
PageResult<ImGroupRequestDO> pageResult = groupRequestService.getGroupRequestPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2.1 批量聚合 user / inviter / handler 用户昵称
Set<Long> userIds = convertSetByFlatMap(pageResult.getList(),
request -> Stream.of(request.getUserId(), request.getInviterUserId(), request.getHandleUserId())
.filter(Objects::nonNull));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
// 2.2 批量聚合群信息(取群名)
Set<Long> groupIds = convertSet(pageResult.getList(), ImGroupRequestDO::getGroupId);
Map<Long, ImGroupDO> groupMap = groupService.getGroupMap(groupIds);
return success(BeanUtils.toBean(pageResult, ImGroupRequestManagerRespVO.class, vo -> {
MapUtils.findAndThen(userMap, vo.getUserId(), user -> vo.setUserNickname(user.getNickname()));
MapUtils.findAndThen(userMap, vo.getInviterUserId(), user -> vo.setInviterNickname(user.getNickname()));
MapUtils.findAndThen(userMap, vo.getHandleUserId(), user -> vo.setHandleNickname(user.getNickname()));
MapUtils.findAndThen(groupMap, vo.getGroupId(), group -> vo.setGroupName(group.getName()));
}));
}
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 群聊封禁 Request VO")
@Data
public class ImGroupManagerBanReqVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "群编号不能为空")
private Long id;
@Schema(description = "封禁原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "违规内容")
@NotBlank(message = "封禁原因不能为空")
@Size(max = 200, message = "封禁原因长度不能超过 200")
private String reason;
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group.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 ImGroupManagerPageReqVO extends PageParam {
@Schema(description = "群名称,模糊匹配", example = "技术交流群")
private String name;
@Schema(description = "群主用户编号", example = "1024")
private Long ownerUserId;
@Schema(description = "群状态", example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类0 正常 / 1 已解散)
@Schema(description = "是否封禁", example = "false")
private Boolean banned;
@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[] createTime;
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 群聊 Response VO")
@Data
public class ImGroupManagerRespVO {
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "群名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "技术交流群")
private String name;
@Schema(description = "群头像")
private String avatar;
@Schema(description = "群公告")
private String notice;
@Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long ownerUserId;
@Schema(description = "群主昵称", example = "张三")
private String ownerNickname;
@Schema(description = "群成员数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
private Integer memberCount;
@Schema(description = "群状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "解散时间")
private LocalDateTime dissolvedTime;
@Schema(description = "是否封禁", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean banned;
@Schema(description = "是否全群禁言")
private Boolean mutedAll;
@Schema(description = "封禁原因")
private String bannedReason;
@Schema(description = "封禁时间")
private LocalDateTime bannedTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group.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 ImGroupRequestManagerPageReqVO extends PageParam {
@Schema(description = "群编号", example = "1024")
private Long groupId;
@Schema(description = "申请人 / 被邀请人用户编号", example = "2048")
private Long userId;
@Schema(description = "邀请人用户编号", example = "31460")
private Long inviterUserId;
@Schema(description = "处理结果", example = "0")
private Integer handleResult; // 参见 ImGroupRequestHandleResultEnum 枚举类
@Schema(description = "加入来源", example = "1")
private Integer addSource; // 参见 ImGroupAddSourceEnum 枚举类
@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[] createTime;
}

View File

@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 加群申请 Response VO")
@Data
public class ImGroupRequestManagerRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long groupId;
@Schema(description = "群名称", example = "芋道技术交流群")
private String groupName;
@Schema(description = "申请人 / 被邀请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Long userId;
@Schema(description = "申请人 / 被邀请人昵称", example = "张三")
private String userNickname;
@Schema(description = "邀请人用户编号NULL 表示用户主动申请", example = "200")
private Long inviterUserId;
@Schema(description = "邀请人昵称", example = "老张")
private String inviterNickname;
@Schema(description = "申请理由", example = "我想加入这个群")
private String applyContent;
@Schema(description = "加入来源", example = "1")
private Integer addSource; // 参见 ImGroupAddSourceEnum 枚举类
@Schema(description = "处理结果", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer handleResult; // 参见 ImGroupRequestHandleResultEnum 枚举类
@Schema(description = "处理人用户编号", example = "31460")
private Long handleUserId;
@Schema(description = "处理人昵称", example = "管理员")
private String handleNickname;
@Schema(description = "处理理由", example = "暂不通过")
private String handleContent;
@Schema(description = "处理时间")
private LocalDateTime handleTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 群成员 Response VO")
@Data
public class ImGroupMemberManagerRespVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "用户昵称", example = "张三")
private String nickname;
@Schema(description = "用户头像")
private String avatar;
@Schema(description = "组内显示名", example = "三哥")
private String displayUserName;
@Schema(description = "群备注", example = "技术交流群")
private String groupRemark;
@Schema(description = "是否免打扰", example = "false")
private Boolean silent;
@Schema(description = "成员状态", example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "成员角色1=群主 2=管理员 3=普通成员", example = "3")
private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类
@Schema(description = "入群时间")
private LocalDateTime joinTime;
@Schema(description = "退群时间")
private LocalDateTime quitTime;
@Schema(description = "禁言到期时间")
private LocalDateTime muteEndTime;
}

View File

@@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message;
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.message.vo.channel.ImChannelMessagePageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessageRespVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessageSendReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
import cn.iocoder.yudao.module.im.service.message.ImChannelMessageService;
import cn.iocoder.yudao.module.im.service.channel.ImChannelService;
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.*;
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/channel-message")
@Validated
public class ImChannelMessageManagerController {
@Resource
private ImChannelMessageService channelMessageService;
@Resource
private ImChannelService channelService;
@Resource
private ImChannelMaterialService channelMaterialService;
@PostMapping("/send")
@Operation(summary = "立即推送频道消息")
@PreAuthorize("@ss.hasPermission('im:manager:channel-message:send')")
public CommonResult<Long> sendMessage(@Valid @RequestBody ImChannelMessageSendReqVO reqVO) {
return success(channelMessageService.sendMessage(reqVO));
}
@DeleteMapping("/delete")
@Operation(summary = "删除频道消息")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:channel-message:delete')")
public CommonResult<Boolean> deleteMessage(@RequestParam("id") Long id) {
channelMessageService.deleteMessage(id);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得频道消息分页;回填频道名 / 素材标题")
@PreAuthorize("@ss.hasPermission('im:manager:channel-message:query')")
public CommonResult<PageResult<ImChannelMessageRespVO>> getMessagePage(@Valid ImChannelMessagePageReqVO pageReqVO) {
PageResult<ImChannelMessageDO> pageResult = channelMessageService.getMessagePage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 批量查询频道和素材,并回填频道名 / 素材标题
Map<Long, ImChannelDO> channelMap = channelService.getChannelMap(
convertSet(pageResult.getList(), ImChannelMessageDO::getChannelId));
Map<Long, ImChannelMaterialDO> materialMap = channelMaterialService.getMaterialMap(
convertSet(pageResult.getList(), ImChannelMessageDO::getMaterialId));
return success(BeanUtils.toBean(pageResult, ImChannelMessageRespVO.class, vo -> {
MapUtils.findAndThen(channelMap, vo.getChannelId(), c -> vo.setChannelName(c.getName()));
MapUtils.findAndThen(materialMap, vo.getMaterialId(),
material -> vo.setMaterialTitle(material.getTitle()).setMaterialCoverUrl(material.getCoverUrl()));
}));
}
}

View File

@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message;
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.message.vo.group.ImGroupMessageManagerPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
import cn.iocoder.yudao.module.im.service.group.ImGroupService;
import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
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.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.AT_USER_ID_ALL;
@Tag(name = "管理后台 - IM 群聊消息")
@RestController
@RequestMapping("/im/manager/message/group")
@Validated
public class ImGroupMessageManagerController {
@Resource
private ImGroupMessageService groupMessageService;
@Resource
private ImGroupService groupService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得群聊消息分页")
@PreAuthorize("@ss.hasPermission('im:manager:message:query')")
public CommonResult<PageResult<ImGroupMessageManagerRespVO>> getGroupMessagePage(
@Valid ImGroupMessageManagerPageReqVO pageReqVO) {
// 1. 分页查询
PageResult<ImGroupMessageDO> pageResult = groupMessageService.getGroupMessagePage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2.1 批量查询群名称、发送人昵称、@ 用户昵称(-1 表示 @所有人,跳过查询,由前端判断渲染)
Map<Long, ImGroupDO> groupMap = groupService.getGroupMap(
convertSet(pageResult.getList(), ImGroupMessageDO::getGroupId));
Set<Long> userIds = convertSetByFlatMap(pageResult.getList(), m -> Stream.concat(
Stream.of(m.getSenderId()),
CollUtil.emptyIfNull(m.getAtUserIds()).stream()
.filter(id -> !Objects.equals(id, AT_USER_ID_ALL))));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
// 2.2 转换为 VO填充群名 / 发送人昵称 / @ 用户昵称(-1 位置留 null由前端展示「@所有人」)
return success(BeanUtils.toBean(pageResult, ImGroupMessageManagerRespVO.class, vo -> {
MapUtils.findAndThen(groupMap, vo.getGroupId(), group -> vo.setGroupName(group.getName()));
MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
if (CollUtil.isNotEmpty(vo.getAtUserIds())) {
vo.setAtUserNicknames(convertList(vo.getAtUserIds(), id -> {
AdminUserRespDTO user = userMap.get(id);
return user != null ? user.getNickname() : null;
}));
}
}));
}
@GetMapping("/get")
@Operation(summary = "获得群聊消息详情")
@Parameter(name = "id", description = "消息编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:message:query')")
public CommonResult<ImGroupMessageManagerRespVO> getGroupMessage(@RequestParam("id") Long id) {
ImGroupMessageDO message = groupMessageService.getGroupMessage(id);
return success(BeanUtils.toBean(message, ImGroupMessageManagerRespVO.class));
}
}

View File

@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message;
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.message.vo.privates.ImPrivateMessageManagerPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
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.Map;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
@Tag(name = "管理后台 - IM 私聊消息")
@RestController
@RequestMapping("/im/manager/message/private")
@Validated
public class ImPrivateMessageManagerController {
@Resource
private ImPrivateMessageService privateMessageService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得私聊消息分页")
@PreAuthorize("@ss.hasPermission('im:manager:message:query')")
public CommonResult<PageResult<ImPrivateMessageManagerRespVO>> getPrivateMessagePage(
@Valid ImPrivateMessageManagerPageReqVO pageReqVO) {
// 1. 分页查询
PageResult<ImPrivateMessageDO> pageResult = privateMessageService.getPrivateMessagePage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2.1 一次性批量查询发送人 + 接收人昵称
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSetByFlatMap(pageResult.getList(),
m -> Stream.of(m.getSenderId(), m.getReceiverId())));
// 2.2 转换为 VO填充昵称
return success(BeanUtils.toBean(pageResult, ImPrivateMessageManagerRespVO.class, vo -> {
MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
MapUtils.findAndThen(userMap, vo.getReceiverId(), user -> vo.setReceiverNickname(user.getNickname()));
}));
}
@GetMapping("/get")
@Operation(summary = "获得私聊消息详情")
@Parameter(name = "id", description = "消息编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:message:query')")
public CommonResult<ImPrivateMessageManagerRespVO> getPrivateMessage(@RequestParam("id") Long id) {
ImPrivateMessageDO message = privateMessageService.getPrivateMessage(id);
return success(BeanUtils.toBean(message, ImPrivateMessageManagerRespVO.class));
}
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel;
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 ImChannelMessagePageReqVO extends PageParam {
@Schema(description = "频道编号", example = "1")
private Long channelId;
@Schema(description = "素材编号", example = "1024")
private Long materialId;
@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[] sendTime;
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - IM 频道消息 Response VO")
@Data
public class ImChannelMessageRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long channelId;
@Schema(description = "频道名称(关联查询填充)")
private String channelName;
@Schema(description = "素材编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long materialId;
@Schema(description = "素材标题(关联查询填充)")
private String materialTitle;
@Schema(description = "素材封面 URL关联查询填充")
private String materialCoverUrl;
@Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "125")
private Integer type; // 参见 ImContentTypeEnum 枚举类
@Schema(description = "消息内容payload JSON 快照")
private String content;
@Schema(description = "接收人编号列表;为空表示全员")
private List<Long> receiverUserIds;
@Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime sendTime;
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - IM 频道消息推送 Request VO")
@Data
public class ImChannelMessageSendReqVO {
@Schema(description = "素材编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "素材编号不能为空")
private Long materialId;
@Schema(description = "接收人编号列表;为空表示全员", example = "[1024, 2048]")
private List<Long> receiverUserIds;
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group;
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 ImGroupMessageManagerPageReqVO extends PageParam {
@Schema(description = "群编号", example = "1024")
private Long groupId;
@Schema(description = "发送人编号", example = "1024")
private Long senderId;
@Schema(description = "消息类型", example = "1")
private Integer type; // 参见 ImContentTypeEnum 枚举类
@Schema(description = "消息内容", example = "你好")
private String content;
@Schema(description = "消息状态", example = "0")
private Integer status; // 参见 ImMessageStatusEnum 枚举类
@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[] sendTime;
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - IM 群聊消息 Response VO")
@Data
public class ImGroupMessageManagerRespVO {
@Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "客户端消息编号", example = "c-uuid-xxx")
private String clientMessageId;
@Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long groupId;
@Schema(description = "群名称", example = "技术交流群")
private String groupName;
@Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long senderId;
@Schema(description = "发送人昵称", example = "张三")
private String senderNickname;
@Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type; // 参见 ImContentTypeEnum 枚举类
@Schema(description = "消息内容JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 ImMessageStatusEnum 枚举类
@Schema(description = "@ 目标用户编号列表(-1 表示 @所有人)")
private List<Long> atUserIds;
@Schema(description = "@ 目标用户昵称列表(-1 位置为 null前端根据 atUserIds 自行展示「@所有人」)")
private List<String> atUserNicknames;
@Schema(description = "回执状态", example = "0")
private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
@Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime sendTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates;
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 ImPrivateMessageManagerPageReqVO extends PageParam {
@Schema(description = "发送人编号", example = "1024")
private Long senderId;
@Schema(description = "接收人编号", example = "2048")
private Long receiverId;
@Schema(description = "消息类型", example = "1")
private Integer type; // 参见 ImContentTypeEnum 枚举类
@Schema(description = "消息内容", example = "你好")
private String content;
@Schema(description = "消息状态", example = "0")
private Integer status; // 参见 ImMessageStatusEnum 枚举类
@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[] sendTime;
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 私聊消息 Response VO")
@Data
public class ImPrivateMessageManagerRespVO {
@Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "客户端消息编号", example = "c-uuid-xxx")
private String clientMessageId;
@Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long senderId;
@Schema(description = "发送人昵称", example = "张三")
private String senderNickname;
@Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
private Long receiverId;
@Schema(description = "接收人昵称", example = "李四")
private String receiverNickname;
@Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer type; // 参见 ImContentTypeEnum 枚举类
@Schema(description = "消息内容JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 ImMessageStatusEnum 枚举类
@Schema(description = "回执状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
@Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime sendTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -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<PageResult<ImRtcCallManagerRespVO>> getCallPage(@Valid ImRtcCallManagerPageReqVO pageReqVO) {
PageResult<ImRtcCallDO> 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<ImRtcCallManagerRespVO> 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<List<ImRtcParticipantManagerRespVO>> getCallParticipantList(@RequestParam("id") Long id) {
List<ImRtcParticipantDO> participants = rtcCallService.getCallParticipantListByCallId(id);
if (CollUtil.isEmpty(participants)) {
return success(Collections.emptyList());
}
// 查询用户信息
Map<Long, AdminUserRespDTO> 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<ImRtcCallManagerRespVO> buildCallRespVOPage(PageResult<ImRtcCallDO> pageResult) {
if (CollUtil.isEmpty(pageResult.getList())) {
return PageResult.empty(pageResult.getTotal());
}
return new PageResult<>(buildCallRespVOList(pageResult.getList()), pageResult.getTotal());
}
private List<ImRtcCallManagerRespVO> buildCallRespVOList(List<ImRtcCallDO> calls) {
// 查询用户信息
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(calls, ImRtcCallDO::getInviterUserId));
Map<Long, ImGroupDO> 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()));
});
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,106 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword;
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.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordRespVO;
import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
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 jakarta.validation.constraints.Size;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
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/sensitive-word")
@Validated
public class ImSensitiveWordManagerController {
@Resource
private ImSensitiveWordService sensitiveWordService;
@Resource
private AdminUserApi adminUserApi;
@PostMapping("/create")
@Operation(summary = "新增敏感词")
@PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:create')")
public CommonResult<Long> createSensitiveWord(@Valid @RequestBody ImSensitiveWordSaveReqVO reqVO) {
return success(sensitiveWordService.createSensitiveWord(reqVO));
}
@PutMapping("/update")
@Operation(summary = "修改敏感词")
@PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:update')")
public CommonResult<Boolean> updateSensitiveWord(@Valid @RequestBody ImSensitiveWordSaveReqVO reqVO) {
sensitiveWordService.updateSensitiveWord(reqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除敏感词")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')")
public CommonResult<Boolean> deleteSensitiveWord(@RequestParam("id") Long id) {
sensitiveWordService.deleteSensitiveWord(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Operation(summary = "批量删除敏感词")
@Parameter(name = "ids", description = "编号列表", required = true)
@PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')")
public CommonResult<Boolean> deleteSensitiveWordList(
@RequestParam("ids")
@Size(max = 100, message = "批量删除最多 100 条") List<Long> ids) {
sensitiveWordService.deleteSensitiveWordList(ids);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得敏感词分页")
@PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:query')")
public CommonResult<PageResult<ImSensitiveWordRespVO>> getSensitiveWordPage(
@Valid ImSensitiveWordPageReqVO pageReqVO) {
// 1. 分页查询
PageResult<ImSensitiveWordDO> pageResult = sensitiveWordService.getSensitiveWordPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty(pageResult.getTotal()));
}
// 2.1 批量查询创建人昵称
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(pageResult.getList(),
word -> NumberUtils.parseLong(word.getCreator())));
// 2.2 转换为 VO填充创建人昵称
return success(BeanUtils.toBean(pageResult, ImSensitiveWordRespVO.class, vo ->
MapUtils.findAndThen(userMap, NumberUtils.parseLong(vo.getCreator()),
user -> vo.setCreatorName(user.getNickname()))));
}
@GetMapping("/get")
@Operation(summary = "获得敏感词详情")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:query')")
public CommonResult<ImSensitiveWordRespVO> getSensitiveWord(@RequestParam("id") Long id) {
ImSensitiveWordDO word = sensitiveWordService.getSensitiveWord(id);
return success(BeanUtils.toBean(word, ImSensitiveWordRespVO.class));
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
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 ImSensitiveWordPageReqVO extends PageParam {
@Schema(description = "敏感词,模糊匹配", example = "敏感")
private String word;
@Schema(description = "状态", example = "0")
@InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
private Integer status; // 参见 CommonStatusEnum 枚举类0 启用 / 1 禁用)
@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[] createTime;
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IM 敏感词 Response VO")
@Data
public class ImSensitiveWordRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词内容")
private String word;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 CommonStatusEnum 枚举类
@Schema(description = "创建人")
private String creator;
@Schema(description = "创建人昵称", example = "张三")
private String creatorName;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - IM 敏感词新增 / 修改 Request VO")
@Data
public class ImSensitiveWordSaveReqVO {
@Schema(description = "编号(修改时必填)", example = "1024")
private Long id;
@Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词内容")
@NotBlank(message = "敏感词不能为空")
@Size(max = 64, message = "敏感词长度不能超过 64")
private String word;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
private Integer status; // 参见 CommonStatusEnum 枚举类0 启用 / 1 禁用)
}

View File

@@ -0,0 +1,160 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.statistics;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo.*;
import cn.iocoder.yudao.module.im.service.statistics.ImStatisticsManagerService;
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.constraints.Max;
import jakarta.validation.constraints.Min;
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.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Tag(name = "管理后台 - IM 数据看板")
@RestController
@RequestMapping("/im/manager/statistics")
@Validated
public class ImStatisticsManagerController {
/**
* 群规模分桶名称的展示顺序
*/
private static final List<String> GROUP_SIZE_BUCKETS = Arrays.asList("1-9 人", "10-49 人", "50-199 人", "200+ 人");
/**
* 看板分布默认时间窗口(天)
*/
private static final int DISTRIBUTION_WINDOW_DAYS = 30;
/**
* TOP 发送者数量
*/
private static final int TOP_SENDER_LIMIT = 10;
@Resource
private ImStatisticsManagerService statisticsService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/overview")
@Operation(summary = "获得数据概览")
@PreAuthorize("@ss.hasPermission('im:manager:statistics:query')")
public CommonResult<ImStatisticsManagerOverviewRespVO> getOverview() {
LocalDateTime todayBegin = LocalDate.now().atStartOfDay();
LocalDateTime tomorrowBegin = todayBegin.plusDays(1);
LocalDateTime yesterdayBegin = todayBegin.minusDays(1);
// 周活/月活定义为 N 天滚动窗口(含今天)
LocalDateTime weekBegin = todayBegin.minusDays(6);
LocalDateTime monthBegin = todayBegin.minusDays(29);
return success(new ImStatisticsManagerOverviewRespVO()
.setTotalUser(statisticsService.getTotalUserCount())
.setNewUserToday(statisticsService.getNewUserCount(todayBegin, tomorrowBegin))
.setTotalGroup(statisticsService.getTotalGroupCount())
.setNewGroupToday(statisticsService.getNewGroupCount(todayBegin, tomorrowBegin))
.setActiveUserDaily(statisticsService.getActiveUserCount(todayBegin, tomorrowBegin))
.setActiveUserWeekly(statisticsService.getActiveUserCount(weekBegin, tomorrowBegin))
.setActiveUserMonthly(statisticsService.getActiveUserCount(monthBegin, tomorrowBegin))
.setPrivateMessageToday(statisticsService.getPrivateMessageCount(todayBegin, tomorrowBegin))
.setGroupMessageToday(statisticsService.getGroupMessageCount(todayBegin, tomorrowBegin))
.setPrivateMessageYesterday(statisticsService.getPrivateMessageCount(yesterdayBegin, todayBegin))
.setGroupMessageYesterday(statisticsService.getGroupMessageCount(yesterdayBegin, todayBegin)));
}
@GetMapping("/message-trend")
@Operation(summary = "获得消息趋势(私聊 / 群聊双线)")
@Parameter(name = "days", description = "回看天数(含今日)", example = "7")
@PreAuthorize("@ss.hasPermission('im:manager:statistics:query')")
public CommonResult<ImStatisticsManagerTrendRespVO> getMessageTrend(
@RequestParam(value = "days", defaultValue = "7") @Min(1) @Max(90) int days) {
List<LocalDateTime> dates = LocalDateTimeUtils.getLatestDays(days);
LocalDateTime beginTime = dates.get(0);
LocalDateTime endTime = dates.get(days - 1).plusDays(1);
Map<LocalDateTime, Long> privateMap = statisticsService.getPrivateMessageDailyCountMap(beginTime, endTime);
Map<LocalDateTime, Long> groupMap = statisticsService.getGroupMessageDailyCountMap(beginTime, endTime);
// 转换格式
Map<String, List<Long>> series = new LinkedHashMap<>();
series.put("private", alignSeries(dates, privateMap));
series.put("group", alignSeries(dates, groupMap));
return success(new ImStatisticsManagerTrendRespVO().setDates(dates).setSeries(series));
}
@GetMapping("/user-trend")
@Operation(summary = "获得用户趋势(新增注册 / 日活双线)")
@Parameter(name = "days", description = "回看天数(含今日)", example = "7")
@PreAuthorize("@ss.hasPermission('im:manager:statistics:query')")
public CommonResult<ImStatisticsManagerTrendRespVO> getUserTrend(
@RequestParam(value = "days", defaultValue = "7") @Min(1) @Max(90) int days) {
List<LocalDateTime> dates = LocalDateTimeUtils.getLatestDays(days);
LocalDateTime beginTime = dates.get(0);
LocalDateTime endTime = dates.get(days - 1).plusDays(1);
Map<LocalDateTime, Long> registerMap = statisticsService.getNewUserDailyCountMap(beginTime, endTime);
Map<LocalDateTime, Long> activeMap = statisticsService.getActiveUserDailyCountMap(beginTime, endTime);
// 转换格式
Map<String, List<Long>> series = new LinkedHashMap<>();
series.put("register", alignSeries(dates, registerMap));
series.put("active", alignSeries(dates, activeMap));
return success(new ImStatisticsManagerTrendRespVO().setDates(dates).setSeries(series));
}
@GetMapping("/message-type-distribution")
@Operation(summary = "获得内容类型分布(最近 30 天)")
@PreAuthorize("@ss.hasPermission('im:manager:statistics:query')")
public CommonResult<List<ImStatisticsManagerMessageTypeRespVO>> getMessageTypeDistribution() {
LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay();
LocalDateTime beginTime = endTime.minusDays(DISTRIBUTION_WINDOW_DAYS);
Map<Integer, Long> typeCountMap = statisticsService.getMessageTypeCountMap(beginTime, endTime);
// 转换格式
return success(convertList(typeCountMap.entrySet(), entry -> new ImStatisticsManagerMessageTypeRespVO()
.setType(entry.getKey()).setValue(entry.getValue())));
}
@GetMapping("/group-size-distribution")
@Operation(summary = "获得群规模分布")
@PreAuthorize("@ss.hasPermission('im:manager:statistics:query')")
public CommonResult<List<ImStatisticsManagerGroupSizeRespVO>> getGroupSizeDistribution() {
Map<String, Long> groupSizeMap = statisticsService.getGroupSizeCountMap();
// 转换格式
return success(convertList(GROUP_SIZE_BUCKETS, bucket -> new ImStatisticsManagerGroupSizeRespVO()
.setRange(bucket).setCount(groupSizeMap.getOrDefault(bucket, 0L))));
}
@GetMapping("/top-senders")
@Operation(summary = "获得消息 TOP 发送者(最近 30 天)")
@PreAuthorize("@ss.hasPermission('im:manager:statistics:query')")
public CommonResult<List<ImStatisticsManagerTopSenderRespVO>> getTopSenders() {
LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay();
LocalDateTime beginTime = endTime.minusDays(DISTRIBUTION_WINDOW_DAYS);
Map<Long, Long> topSenderMap = statisticsService.getTopSenderCountMap(beginTime, endTime, TOP_SENDER_LIMIT);
// TOP 发送者:批量回填昵称
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(topSenderMap.keySet());
return success(convertList(topSenderMap.entrySet(), entry -> {
ImStatisticsManagerTopSenderRespVO item = new ImStatisticsManagerTopSenderRespVO()
.setUserId(entry.getKey()).setMessageCount(entry.getValue());
MapUtils.findAndThen(userMap, entry.getKey(), user -> item.setNickname(user.getNickname()));
return item;
}));
}
/**
* 把每日聚合 Map 对齐到 dates 序列;缺失天补 0
*/
private static List<Long> alignSeries(List<LocalDateTime> dates, Map<LocalDateTime, Long> dailyMap) {
return convertList(dates, date -> dailyMap.getOrDefault(date, 0L));
}
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - IM 数据看板群规模分布项 Response VO")
@Data
public class ImStatisticsManagerGroupSizeRespVO {
@Schema(description = "区间名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1-9 人")
private String range;
@Schema(description = "群数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "320")
private Long count;
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - IM 数据看板内容类型分布项 Response VO")
@Data
public class ImStatisticsManagerMessageTypeRespVO {
@Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer type; // 参见 ImContentTypeEnum 枚举类
@Schema(description = "消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000")
private Long value;
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - IM 数据看板概览 Response VO")
@Data
public class ImStatisticsManagerOverviewRespVO {
@Schema(description = "用户总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12345")
private Long totalUser;
@Schema(description = "今日新增用户数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23")
private Long newUserToday;
@Schema(description = "群总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "678")
private Long totalGroup;
@Schema(description = "今日新建群数", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
private Long newGroupToday;
@Schema(description = "日活用户(今日发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1023")
private Long activeUserDaily;
@Schema(description = "周活用户(最近 7 天发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "4567")
private Long activeUserWeekly;
@Schema(description = "月活用户(最近 30 天发过消息的去重用户数)", requiredMode = Schema.RequiredMode.REQUIRED, example = "8901")
private Long activeUserMonthly;
@Schema(description = "今日私聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8765")
private Long privateMessageToday;
@Schema(description = "今日群聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3210")
private Long groupMessageToday;
@Schema(description = "昨日私聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "7890")
private Long privateMessageYesterday;
@Schema(description = "昨日群聊消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3000")
private Long groupMessageYesterday;
}

View File

@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - IM 数据看板 TOP 发送者项 Response VO")
@Data
public class ImStatisticsManagerTopSenderRespVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long userId;
@Schema(description = "用户昵称", example = "张三")
private String nickname;
@Schema(description = "消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1500")
private Long messageCount;
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - IM 数据看板趋势 Response VO")
@Data
public class ImStatisticsManagerTrendRespVO {
@Schema(description = "横轴日期序列(每天 00:00:00由前端按需取日期部分", requiredMode = Schema.RequiredMode.REQUIRED)
private List<LocalDateTime> dates;
@Schema(description = "数据系列key 为系列名(如 private/group 或 register/activevalue 为与 dates 等长的计数数组",
requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, List<Long>> series;
}

View File

@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.im.controller.admin.message;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.channel.ImChannelMessagePullRespVO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
import cn.iocoder.yudao.module.im.service.message.ImChannelMessageService;
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.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.PositiveOrZero;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
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;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
@Tag(name = "用户 APP - IM 频道消息")
@RestController
@RequestMapping("/im/channel/message")
@Validated
public class ImChannelMessageController {
@Resource
private ImChannelMessageService channelMessageService;
@GetMapping("/pull")
@Operation(summary = "拉取频道消息(离线增量);按 minId 游标分页")
public CommonResult<List<ImChannelMessagePullRespVO>> pullChannelMessageList(
@RequestParam(value = "minId", defaultValue = "0") @PositiveOrZero(message = "minId 不能小于 0") Long minId,
@RequestParam(value = "size", defaultValue = "100")
@Min(value = 1, message = "size 必须大于 0")
@Max(value = 200, message = "size 一次最多 200 条") Integer size) {
// 1. 拉取消息列表
Long userId = getLoginUserId();
List<ImChannelMessageDO> list = channelMessageService.pullChannelMessageList(userId, minId, size);
if (CollUtil.isEmpty(list)) {
return success(Collections.emptyList());
}
// 2. 按已读游标补 receiptStatus已读 DONE / 未读 PENDING
Map<Long, Long> readMaxByChannel = channelMessageService.getChannelReadMaxMessageIdMap(
userId, convertSet(list, ImChannelMessageDO::getChannelId));
return success(BeanUtils.toBean(list, ImChannelMessagePullRespVO.class, vo -> {
Long readMax = readMaxByChannel.get(vo.getChannelId());
vo.setReceiptStatus(readMax != null && readMax >= vo.getId()
? ImMessageReceiptStatusEnum.DONE.getStatus()
: ImMessageReceiptStatusEnum.PENDING.getStatus());
}));
}
@PutMapping("/read")
@Operation(summary = "标记频道消息已读")
@Parameter(name = "channelId", description = "频道编号", required = true, example = "1")
@Parameter(name = "messageId", description = "已读到的消息编号", required = true, example = "100")
public CommonResult<Boolean> readChannelMessages(@RequestParam("channelId") Long channelId,
@RequestParam("messageId") Long messageId) {
channelMessageService.readChannelMessages(getLoginUserId(), channelId, messageId);
return success(true);
}
}

View File

@@ -0,0 +1,85 @@
package cn.iocoder.yudao.module.im.controller.admin.message;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
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 jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - IM 群聊消息")
@RestController
@RequestMapping("/im/message/group")
@Validated
public class ImGroupMessageController {
@Resource
private ImGroupMessageService groupMessageService;
@PostMapping("/send")
@Operation(summary = "发送群聊消息")
public CommonResult<ImGroupMessageRespVO> sendGroupMessage(@Valid @RequestBody ImGroupMessageSendReqVO reqVO) {
ImGroupMessageDO message = groupMessageService.sendGroupMessage(getLoginUserId(), reqVO);
return success(BeanUtils.toBean(message, ImGroupMessageRespVO.class));
}
@GetMapping("/pull")
@Operation(summary = "拉取群聊消息(增量)")
@Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
@Parameter(name = "size", description = "拉取数量", required = true, example = "100")
public CommonResult<List<ImGroupMessageRespVO>> pullGroupMessageList(
@RequestParam("minId") Long minId,
@RequestParam("size") @Min(value = 1, message = "拉取数量最小值为 1") Integer size) {
List<ImGroupMessageDO> messages = groupMessageService.pullGroupMessageList(getLoginUserId(), minId, size);
return success(BeanUtils.toBean(messages, ImGroupMessageRespVO.class));
}
@PutMapping("/read")
@Operation(summary = "标记群聊消息已读")
@Parameter(name = "groupId", description = "群编号", required = true, example = "1")
@Parameter(name = "messageId", description = "已读到的消息编号", required = true, example = "100")
public CommonResult<Boolean> readGroupMessages(@RequestParam("groupId") Long groupId,
@RequestParam("messageId") Long messageId) {
groupMessageService.readGroupMessages(getLoginUserId(), groupId, messageId);
return success(true);
}
@DeleteMapping("/recall")
@Operation(summary = "撤回群聊消息")
@Parameter(name = "id", description = "消息编号", required = true, example = "1")
public CommonResult<ImGroupMessageRespVO> recallGroupMessage(@RequestParam("id") Long id) {
ImGroupMessageDO message = groupMessageService.recallGroupMessage(getLoginUserId(), id);
return success(BeanUtils.toBean(message, ImGroupMessageRespVO.class));
}
@GetMapping("/get-read-user-ids")
@Operation(summary = "获取群消息已读用户列表")
@Parameter(name = "groupId", description = "群编号", required = true, example = "1")
@Parameter(name = "messageId", description = "消息编号", required = true, example = "1")
public CommonResult<List<Long>> getGroupReadUserIds(@RequestParam("groupId") Long groupId,
@RequestParam("messageId") Long messageId) {
return success(groupMessageService.getGroupReadUserIds(getLoginUserId(), groupId, messageId));
}
@GetMapping("/list")
@Operation(summary = "查询群聊历史消息")
public CommonResult<List<ImGroupMessageRespVO>> getGroupMessageList(@Valid ImGroupMessageListReqVO reqVO) {
List<ImGroupMessageDO> messages = groupMessageService.getGroupMessageList(getLoginUserId(), reqVO);
return success(BeanUtils.toBean(messages, ImGroupMessageRespVO.class));
}
}

View File

@@ -0,0 +1,18 @@
### 请求 /send 接口 => 成功
POST {{baseUrl}}/im/message/send
Authorization: Bearer {{token}}
Content-Type: application/json
tenant-id: {{adminTenentId}}
{
"clientMessageId": "123",
"receiverId": 1,
"conversationType": 1,
"contentType": 101,
"content": "你好1"
}
### 请求 /pull 接口 => 成功
GET {{baseUrl}}/im/message/pull?sequence=0&size=2
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}

View File

@@ -0,0 +1,85 @@
package cn.iocoder.yudao.module.im.controller.admin.message;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageRespVO;
import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
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 jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - IM 私聊消息")
@RestController
@RequestMapping("/im/message/private")
@Validated
public class ImPrivateMessageController {
@Resource
private ImPrivateMessageService privateMessageService;
@PostMapping("/send")
@Operation(summary = "发送私聊消息")
public CommonResult<ImPrivateMessageRespVO> sendPrivateMessage(
@Valid @RequestBody ImPrivateMessageSendReqVO reqVO) {
ImPrivateMessageDO message = privateMessageService.sendPrivateMessage(getLoginUserId(), reqVO);
return success(BeanUtils.toBean(message, ImPrivateMessageRespVO.class));
}
@GetMapping("/pull")
@Operation(summary = "拉取私聊消息(增量)")
@Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
@Parameter(name = "size", description = "拉取数量", required = true, example = "100")
public CommonResult<List<ImPrivateMessageRespVO>> pullPrivateMessageList(
@RequestParam("minId") Long minId,
@RequestParam("size") @Min(value = 1, message = "拉取数量最小值为 1") Integer size) {
List<ImPrivateMessageDO> messages = privateMessageService.pullPrivateMessageList(getLoginUserId(), minId, size);
return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class));
}
@PutMapping("/read")
@Operation(summary = "标记私聊消息已读")
@Parameter(name = "receiverId", description = "接收方用户编号(对方)", required = true, example = "2")
@Parameter(name = "messageId", description = "已读位置(含),通常是会话内最大消息编号", required = true, example = "100")
public CommonResult<Boolean> readPrivateMessages(@RequestParam("receiverId") Long receiverId,
@RequestParam("messageId") Long messageId) {
privateMessageService.readPrivateMessages(getLoginUserId(), receiverId, messageId);
return success(true);
}
@GetMapping("/max-read-message-id")
@Operation(summary = "查询对方已读到我发的最大消息 id",
description = "用于多端 / 离线场景下的已读位置补齐:进入会话或断线重连后调用,据此把本地自发消息更新为已读")
@Parameter(name = "peerId", description = "对方用户编号", required = true, example = "2")
public CommonResult<Long> getMaxReadMessageId(@RequestParam("peerId") Long peerId) {
return success(privateMessageService.getMaxReadMessageId(getLoginUserId(), peerId));
}
@DeleteMapping("/recall")
@Operation(summary = "撤回私聊消息")
@Parameter(name = "id", description = "消息编号", required = true, example = "1")
public CommonResult<ImPrivateMessageRespVO> recallPrivateMessage(@RequestParam("id") Long id) {
ImPrivateMessageDO message = privateMessageService.recallPrivateMessage(getLoginUserId(), id);
return success(BeanUtils.toBean(message, ImPrivateMessageRespVO.class));
}
@GetMapping("/list")
@Operation(summary = "查询私聊历史消息")
public CommonResult<List<ImPrivateMessageRespVO>> getPrivateMessageList(@Valid ImPrivateMessageListReqVO reqVO) {
List<ImPrivateMessageDO> messages = privateMessageService.getPrivateMessageList(getLoginUserId(), reqVO);
return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class));
}
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.im.controller.admin.message.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "用户 APP - IM 频道消息拉取 Response VO")
@Data
public class ImChannelMessagePullRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
private Long id;
@Schema(description = "频道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long channelId;
@Schema(description = "素材编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long materialId;
@Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "125")
private Integer type;
@Schema(description = "消息内容payload JSON 快照", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "当前用户回执 / 已读态;按 Redis 读位置计算(已读 DONE未读 PENDING", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
@Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime sendTime;
}

Some files were not shown because too many files have changed in this diff Show More