Commit bda892277df5e14541f5c2426b20868b4d882d8e

Authored by YunaiV
1 parent bb9a2b53

fix(im):加固管理端入参和统计边界

- 校验敏感词、表情包、表情项状态枚举和批量删除数量上限
- 限制私聊、群聊增量拉取 size 最小值
- 收紧表情包项 Mapper 的 in 查询并在 Service 层处理空好友集合
- 兼容统计聚合数值类型并明确当前有效群总数口径
- 固定 LiveKit token 签名编码和 IM web 包说明
- 修正群禁言权限错误码顺序并补充封禁、踢人、禁言回归测试
- 流转 IM bug 文档并记录本轮不修项语义
Showing 21 changed files with 147 additions and 31 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackItemManagerController.java
... ... @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameter;
13 13 import io.swagger.v3.oas.annotations.tags.Tag;
14 14 import jakarta.annotation.Resource;
15 15 import jakarta.validation.Valid;
  16 +import jakarta.validation.constraints.Size;
16 17 import org.springframework.security.access.prepost.PreAuthorize;
17 18 import org.springframework.validation.annotation.Validated;
18 19 import org.springframework.web.bind.annotation.*;
... ... @@ -58,7 +59,8 @@ public class ImFacePackItemManagerController {
58 59 @Operation(summary = "批量删除表情")
59 60 @Parameter(name = "ids", description = "编号列表", required = true)
60 61 @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:delete')")
61   - public CommonResult<Boolean> deleteFacePackItemList(@RequestParam("ids") List<Long> ids) {
  62 + public CommonResult<Boolean> deleteFacePackItemList(@RequestParam("ids")
  63 + @Size(max = 100, message = "批量删除最多 100 条") List<Long> ids) {
62 64 facePackItemService.deleteFacePackItemList(ids);
63 65 return success(true);
64 66 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/ImFacePackManagerController.java
... ... @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameter;
13 13 import io.swagger.v3.oas.annotations.tags.Tag;
14 14 import jakarta.annotation.Resource;
15 15 import jakarta.validation.Valid;
  16 +import jakarta.validation.constraints.Size;
16 17 import org.springframework.security.access.prepost.PreAuthorize;
17 18 import org.springframework.validation.annotation.Validated;
18 19 import org.springframework.web.bind.annotation.*;
... ... @@ -58,7 +59,8 @@ public class ImFacePackManagerController {
58 59 @Operation(summary = "批量删除表情包")
59 60 @Parameter(name = "ids", description = "编号列表", required = true)
60 61 @PreAuthorize("@ss.hasPermission('im:manager:face-pack:delete')")
61   - public CommonResult<Boolean> deleteFacePackList(@RequestParam("ids") List<Long> ids) {
  62 + public CommonResult<Boolean> deleteFacePackList(@RequestParam("ids")
  63 + @Size(max = 100, message = "批量删除最多 100 条") List<Long> ids) {
62 64 facePackService.deleteFacePackList(ids);
63 65 return success(true);
64 66 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemPageReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
4 6 import io.swagger.v3.oas.annotations.media.Schema;
5 7 import lombok.Data;
6 8 import lombok.EqualsAndHashCode;
... ... @@ -17,6 +19,7 @@ public class ImFacePackItemPageReqVO extends PageParam {
17 19 private String name;
18 20  
19 21 @Schema(description = "状态", example = "0")
  22 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
20 23 private Integer status; // 参见 CommonStatusEnum 枚举类
21 24  
22 25 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/item/ImFacePackItemSaveReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
3 5 import io.swagger.v3.oas.annotations.media.Schema;
4 6 import jakarta.validation.constraints.Max;
5 7 import jakarta.validation.constraints.Min;
... ... @@ -47,6 +49,7 @@ public class ImFacePackItemSaveReqVO {
47 49  
48 50 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
49 51 @NotNull(message = "状态不能为空")
  52 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
50 53 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
51 54  
52 55 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackPageReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
4 6 import io.swagger.v3.oas.annotations.media.Schema;
5 7 import lombok.Data;
6 8 import lombok.EqualsAndHashCode;
... ... @@ -19,6 +21,7 @@ public class ImFacePackPageReqVO extends PageParam {
19 21 private String name;
20 22  
21 23 @Schema(description = "状态", example = "0")
  24 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
22 25 private Integer status; // 参见 CommonStatusEnum 枚举类
23 26  
24 27 @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/face/vo/pack/ImFacePackSaveReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
3 5 import io.swagger.v3.oas.annotations.media.Schema;
4 6 import jakarta.validation.constraints.NotBlank;
5 7 import jakarta.validation.constraints.NotNull;
... ... @@ -28,6 +30,7 @@ public class ImFacePackSaveReqVO {
28 30  
29 31 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
30 32 @NotNull(message = "状态不能为空")
  33 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
31 34 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
32 35  
33 36 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/ImSensitiveWordManagerController.java
... ... @@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.Parameter;
18 18 import io.swagger.v3.oas.annotations.tags.Tag;
19 19 import jakarta.annotation.Resource;
20 20 import jakarta.validation.Valid;
  21 +import jakarta.validation.constraints.Size;
21 22 import org.springframework.security.access.prepost.PreAuthorize;
22 23 import org.springframework.validation.annotation.Validated;
23 24 import org.springframework.web.bind.annotation.*;
... ... @@ -67,7 +68,9 @@ public class ImSensitiveWordManagerController {
67 68 @Operation(summary = "批量删除敏感词")
68 69 @Parameter(name = "ids", description = "编号列表", required = true)
69 70 @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')")
70   - public CommonResult<Boolean> deleteSensitiveWordList(@RequestParam("ids") List<Long> ids) {
  71 + public CommonResult<Boolean> deleteSensitiveWordList(
  72 + @RequestParam("ids")
  73 + @Size(max = 100, message = "批量删除最多 100 条") List<Long> ids) {
71 74 sensitiveWordService.deleteSensitiveWordList(ids);
72 75 return success(true);
73 76 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordPageReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
4 6 import io.swagger.v3.oas.annotations.media.Schema;
5 7 import lombok.Data;
6 8 import lombok.EqualsAndHashCode;
... ... @@ -19,6 +21,7 @@ public class ImSensitiveWordPageReqVO extends PageParam {
19 21 private String word;
20 22  
21 23 @Schema(description = "状态", example = "0")
  24 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
22 25 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
23 26  
24 27 @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordSaveReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
2 2  
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
3 5 import io.swagger.v3.oas.annotations.media.Schema;
4 6 import jakarta.validation.constraints.NotBlank;
5 7 import jakarta.validation.constraints.NotNull;
... ... @@ -20,6 +22,7 @@ public class ImSensitiveWordSaveReqVO {
20 22  
21 23 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
22 24 @NotNull(message = "状态不能为空")
  25 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
23 26 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
24 27  
25 28 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java
... ... @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.Parameter;
12 12 import io.swagger.v3.oas.annotations.tags.Tag;
13 13 import jakarta.annotation.Resource;
14 14 import jakarta.validation.Valid;
  15 +import jakarta.validation.constraints.Min;
15 16 import org.springframework.validation.annotation.Validated;
16 17 import org.springframework.web.bind.annotation.*;
17 18  
... ... @@ -40,8 +41,9 @@ public class ImGroupMessageController {
40 41 @Operation(summary = "拉取群聊消息(增量)")
41 42 @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
42 43 @Parameter(name = "size", description = "拉取数量", required = true, example = "100")
43   - public CommonResult<List<ImGroupMessageRespVO>> pullGroupMessageList(@RequestParam("minId") Long minId,
44   - @RequestParam("size") Integer size) {
  44 + public CommonResult<List<ImGroupMessageRespVO>> pullGroupMessageList(
  45 + @RequestParam("minId") Long minId,
  46 + @RequestParam("size") @Min(value = 1, message = "拉取数量最小值为 1") Integer size) {
45 47 List<ImGroupMessageDO> messages = groupMessageService.pullGroupMessageList(getLoginUserId(), minId, size);
46 48 return success(BeanUtils.toBean(messages, ImGroupMessageRespVO.class));
47 49 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java
... ... @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.Parameter;
12 12 import io.swagger.v3.oas.annotations.tags.Tag;
13 13 import jakarta.annotation.Resource;
14 14 import jakarta.validation.Valid;
  15 +import jakarta.validation.constraints.Min;
15 16 import org.springframework.validation.annotation.Validated;
16 17 import org.springframework.web.bind.annotation.*;
17 18  
... ... @@ -41,8 +42,9 @@ public class ImPrivateMessageController {
41 42 @Operation(summary = "拉取私聊消息(增量)")
42 43 @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
43 44 @Parameter(name = "size", description = "拉取数量", required = true, example = "100")
44   - public CommonResult<List<ImPrivateMessageRespVO>> pullPrivateMessageList(@RequestParam("minId") Long minId,
45   - @RequestParam("size") Integer size) {
  45 + public CommonResult<List<ImPrivateMessageRespVO>> pullPrivateMessageList(
  46 + @RequestParam("minId") Long minId,
  47 + @RequestParam("size") @Min(value = 1, message = "拉取数量最小值为 1") Integer size) {
46 48 List<ImPrivateMessageDO> messages = privateMessageService.pullPrivateMessageList(getLoginUserId(), minId, size);
47 49 return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class));
48 50 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/face/ImFacePackItemMapper.java
... ... @@ -20,7 +20,7 @@ public interface ImFacePackItemMapper extends BaseMapperX&lt;ImFacePackItemDO&gt; {
20 20  
21 21 default List<ImFacePackItemDO> selectListByPackIdsAndStatus(Collection<Long> packIds, Integer status) {
22 22 return selectList(new LambdaQueryWrapperX<ImFacePackItemDO>()
23   - .inIfPresent(ImFacePackItemDO::getPackId, packIds)
  23 + .in(ImFacePackItemDO::getPackId, packIds)
24 24 .eq(ImFacePackItemDO::getStatus, status)
25 25 .orderByAsc(ImFacePackItemDO::getPackId)
26 26 .orderByAsc(ImFacePackItemDO::getSort)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java
... ... @@ -73,7 +73,7 @@ public interface ImStatisticsManagerMapper {
73 73 // ==================== 群 ====================
74 74  
75 75 /**
76   - * 群总数(im_group)
  76 + * 当前有效群总数
77 77 */
78 78 @Select("SELECT COUNT(*) FROM im_group WHERE deleted = 0 AND status = 0")
79 79 Long selectTotalGroupCount();
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/rtc/core/LiveKitClient.java
... ... @@ -90,7 +90,7 @@ public class LiveKitClient {
90 90 .setNotBefore(new Date(nowSec * 1000))
91 91 .setExpiresAt(new Date((nowSec + ttlSec) * 1000))
92 92 .setPayload("video", video)
93   - .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes()));
  93 + .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes(StandardCharsets.UTF_8)));
94 94 if (StrUtil.isNotEmpty(displayName)) {
95 95 jwt.setPayload("name", displayName);
96 96 }
... ... @@ -219,7 +219,7 @@ public class LiveKitClient {
219 219 .setNotBefore(new Date(nowSec * 1000))
220 220 .setExpiresAt(new Date((nowSec + ADMIN_TOKEN_TTL.getSeconds()) * 1000))
221 221 .setPayload("video", MapUtil.of("roomAdmin", true))
222   - .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes()))
  222 + .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes(StandardCharsets.UTF_8)))
223 223 .sign();
224 224 }
225 225  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/web/package-info.java
1 1 /**
2   - * trade 模块的 web 配置
  2 + * IM 模块的 web 配置
3 3 */
4 4 package cn.iocoder.yudao.module.im.framework.web;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.hutool.core.util.BooleanUtil;
4 5 import cn.hutool.extra.spring.SpringUtil;
5 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
... ... @@ -28,6 +29,7 @@ import org.springframework.validation.annotation.Validated;
28 29  
29 30 import java.time.LocalDateTime;
30 31 import java.util.Collection;
  32 +import java.util.Collections;
31 33 import java.util.List;
32 34  
33 35 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -100,6 +102,9 @@ public class ImFriendServiceImpl implements ImFriendService {
100 102  
101 103 @Override
102 104 public List<ImFriendDO> getActiveFriendList(Long userId, Collection<Long> friendUserIds) {
  105 + if (CollUtil.isEmpty(friendUserIds)) {
  106 + return Collections.emptyList();
  107 + }
103 108 return friendMapper.selectListByUserIdAndFriendUserIdsAndStatus(userId, friendUserIds,
104 109 CommonStatusEnum.ENABLE.getStatus());
105 110 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
... ... @@ -669,6 +669,10 @@ public class ImGroupServiceImpl implements ImGroupService {
669 669 * 三档分层禁言权限校验
670 670 */
671 671 private void validateMutePermission(ImGroupMemberDO operator, ImGroupMemberDO target) {
  672 + // 普通成员不能禁言任何人
  673 + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) {
  674 + throw exception(GROUP_NOT_OWNER_OR_ADMIN);
  675 + }
672 676 // 群主不可被禁言
673 677 if (ImGroupMemberRoleEnum.isOwner(target.getRole())) {
674 678 throw exception(GROUP_MUTE_OWNER_DENIED);
... ... @@ -677,10 +681,6 @@ public class ImGroupServiceImpl implements ImGroupService {
677 681 if (ImGroupMemberRoleEnum.isAdmin(target.getRole()) && !ImGroupMemberRoleEnum.isOwner(operator.getRole())) {
678 682 throw exception(GROUP_MUTE_ADMIN_DENIED);
679 683 }
680   - // 普通成员不能禁言任何人
681   - if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) {
682   - throw exception(GROUP_NOT_OWNER_OR_ADMIN);
683   - }
684 684 }
685 685  
686 686 private ImGroupServiceImpl getSelf() {
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImpl.java
... ... @@ -71,7 +71,7 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic
71 71 List<Map<String, Object>> rows = statisticsMapper.selectGroupSizeDistribution();
72 72 return convertMap(rows,
73 73 row -> (String) row.get("range"),
74   - row -> (Long) row.get("count"));
  74 + row -> Convert.toLong(row.get("count")));
75 75 }
76 76  
77 77 // ==================== 消息 ====================
... ... @@ -102,16 +102,16 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic
102 102 public Map<Integer, Long> getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime) {
103 103 List<Map<String, Object>> rows = statisticsMapper.selectMessageTypeDistribution(beginTime, endTime);
104 104 return convertMap(rows,
105   - row -> (Integer) row.get("type"),
106   - row -> (Long) row.get("count"));
  105 + row -> Convert.toInt(row.get("type")),
  106 + row -> Convert.toLong(row.get("count")));
107 107 }
108 108  
109 109 @Override
110 110 public Map<Long, Long> getTopSenderCountMap(LocalDateTime beginTime, LocalDateTime endTime, int limit) {
111 111 List<Map<String, Object>> rows = statisticsMapper.selectTopSenders(beginTime, endTime, limit);
112 112 return convertMap(rows,
113   - row -> (Long) row.get("userId"),
114   - row -> (Long) row.get("messageCount"));
  113 + row -> Convert.toLong(row.get("userId")),
  114 + row -> Convert.toLong(row.get("messageCount")));
115 115 }
116 116  
117 117 /**
... ... @@ -120,7 +120,7 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic
120 120 private static Map<LocalDateTime, Long> toDailyCountMap(List<Map<String, Object>> rows) {
121 121 return convertMap(rows,
122 122 row -> Convert.convert(LocalDate.class, row.get("date")).atStartOfDay(),
123   - row -> (Long) row.get("count"));
  123 + row -> Convert.toLong(row.get("count")));
124 124 }
125 125  
126 126 }
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
... ... @@ -17,6 +17,7 @@ import org.mockito.Mock;
17 17 import org.springframework.dao.DuplicateKeyException;
18 18  
19 19 import java.time.LocalDateTime;
  20 +import java.util.Collections;
20 21 import java.util.List;
21 22  
22 23 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
... ... @@ -213,4 +214,12 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
213 214 assertEquals(2, result.size());
214 215 }
215 216  
  217 + @Test
  218 + public void testGetActiveFriendList_emptySkip() {
  219 + List<ImFriendDO> result = friendService.getActiveFriendList(1L, Collections.emptyList());
  220 +
  221 + assertTrue(result.isEmpty());
  222 + verify(imFriendMapper, never()).selectListByUserIdAndFriendUserIdsAndStatus(anyLong(), anyCollection(), anyInt());
  223 + }
  224 +
216 225 }
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
... ... @@ -7,10 +7,12 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO;
8 8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO;
9 9 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO;
  10 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteMemberReqVO;
10 11 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO;
11 12 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO;
12 13 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
13 14 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
  15 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
14 16 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
15 17 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
16 18 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
... ... @@ -932,6 +934,76 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
932 934 }
933 935 }
934 936  
  937 + // ========== banGroup ==========
  938 +
  939 + @Test
  940 + public void testBanGroup_dissolved() {
  941 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  942 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  943 + .thenReturn(groupService);
  944 +
  945 + ImGroupDO group = ImGroupDO.builder().id(10L).status(CommonStatusEnum.DISABLE.getStatus()).build();
  946 + when(groupMapper.selectById(10L)).thenReturn(group);
  947 +
  948 + ImGroupManagerBanReqVO reqVO = new ImGroupManagerBanReqVO();
  949 + reqVO.setId(10L).setReason("违规");
  950 +
  951 + ServiceException exception = assertThrows(ServiceException.class,
  952 + () -> groupService.banGroup(1L, reqVO));
  953 + assertEquals(GROUP_DISSOLVED.getCode(), exception.getCode());
  954 + verify(groupMapper, never()).updateById(any(ImGroupDO.class));
  955 + verify(groupMessageService, never()).sendGroupMessage(anyLong(), any(ImGroupMessageSendDTO.class));
  956 + }
  957 + }
  958 +
  959 + @Test
  960 + public void testBanGroup_alreadyBannedSkip() {
  961 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  962 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  963 + .thenReturn(groupService);
  964 +
  965 + ImGroupDO group = ImGroupDO.builder().id(10L).banned(true)
  966 + .status(CommonStatusEnum.ENABLE.getStatus()).build();
  967 + when(groupMapper.selectById(10L)).thenReturn(group);
  968 +
  969 + ImGroupManagerBanReqVO reqVO = new ImGroupManagerBanReqVO();
  970 + reqVO.setId(10L).setReason("违规");
  971 +
  972 + groupService.banGroup(1L, reqVO);
  973 +
  974 + verify(groupMapper, never()).updateById(any(ImGroupDO.class));
  975 + verify(groupMessageService, never()).sendGroupMessage(anyLong(), any(ImGroupMessageSendDTO.class));
  976 + }
  977 + }
  978 +
  979 + // ========== muteMember ==========
  980 +
  981 + @Test
  982 + public void testMuteMember_normalCannotMuteAdmin() {
  983 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  984 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  985 + .thenReturn(groupService);
  986 +
  987 + ImGroupDO group = ImGroupDO.builder().id(10L).status(CommonStatusEnum.ENABLE.getStatus()).build();
  988 + when(groupMapper.selectById(10L)).thenReturn(group);
  989 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
  990 + ImGroupMemberDO.builder().groupId(10L).userId(1L)
  991 + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build());
  992 + when(groupMemberService.validateMemberInGroup(10L, 2L)).thenReturn(
  993 + ImGroupMemberDO.builder().groupId(10L).userId(2L)
  994 + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build());
  995 +
  996 + ImGroupMuteMemberReqVO reqVO = new ImGroupMuteMemberReqVO();
  997 + reqVO.setGroupId(10L).setUserId(2L).setMutedSeconds(60);
  998 +
  999 + ServiceException exception = assertThrows(ServiceException.class,
  1000 + () -> groupService.muteMember(1L, reqVO));
  1001 + assertEquals(GROUP_NOT_OWNER_OR_ADMIN.getCode(), exception.getCode());
  1002 + verify(groupMemberService, never()).updateGroupMemberMuteEndTime(anyLong(), anyLong(), any());
  1003 + verify(groupMessageService, never()).sendGroupMessage(anyLong(), any(ImGroupMessageSendDTO.class));
  1004 + }
  1005 + }
  1006 +
935 1007 // ========== validateGroupExists ==========
936 1008  
937 1009 @Test
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImplTest.java
... ... @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
6 6 import org.mockito.InjectMocks;
7 7 import org.mockito.Mock;
8 8  
  9 +import java.math.BigInteger;
9 10 import java.time.LocalDate;
10 11 import java.time.LocalDateTime;
11 12 import java.util.Arrays;
... ... @@ -75,10 +76,10 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
75 76  
76 77 @Test
77 78 public void testGetNewUserDailyCountMap_dateConvert() {
78   - // 准备:date 字段以 SQL Date / String 混入,count 为 Long
  79 + // 准备:date 字段以 SQL Date / String 混入,count 混入多种 Number
79 80 when(statisticsMapper.selectNewUserDailyCount(eq(BEGIN), eq(END))).thenReturn(Arrays.asList(
80   - row("date", java.sql.Date.valueOf("2026-01-10"), "count", 5L),
81   - row("date", "2026-01-11", "count", 8L)));
  81 + row("date", java.sql.Date.valueOf("2026-01-10"), "count", BigInteger.valueOf(5)),
  82 + row("date", "2026-01-11", "count", 8)));
82 83  
83 84 // 调用
84 85 Map<LocalDateTime, Long> result = service.getNewUserDailyCountMap(BEGIN, END);
... ... @@ -119,8 +120,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
119 120 @Test
120 121 public void testGetGroupSizeCountMap() {
121 122 when(statisticsMapper.selectGroupSizeDistribution()).thenReturn(Arrays.asList(
122   - row("range", "1-9 人", "count", 3L),
123   - row("range", "10-49 人", "count", 2L)));
  123 + row("range", "1-9 人", "count", BigInteger.valueOf(3)),
  124 + row("range", "10-49 人", "count", 2)));
124 125  
125 126 Map<String, Long> result = service.getGroupSizeCountMap();
126 127 assertEquals(3L, result.get("1-9 人"));
... ... @@ -130,8 +131,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
130 131 @Test
131 132 public void testGetMessageTypeCountMap() {
132 133 when(statisticsMapper.selectMessageTypeDistribution(BEGIN, END)).thenReturn(Arrays.asList(
133   - row("type", 101, "count", 8L),
134   - row("type", 102, "count", 1L)));
  134 + row("type", 101L, "count", BigInteger.valueOf(8)),
  135 + row("type", 102, "count", 1)));
135 136  
136 137 Map<Integer, Long> result = service.getMessageTypeCountMap(BEGIN, END);
137 138 assertEquals(8L, result.get(101));
... ... @@ -141,8 +142,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
141 142 @Test
142 143 public void testGetTopSenderCountMap_passesLimit() {
143 144 when(statisticsMapper.selectTopSenders(BEGIN, END, 3)).thenReturn(Arrays.asList(
144   - row("userId", 1L, "messageCount", 10L),
145   - row("userId", 2L, "messageCount", 5L)));
  145 + row("userId", 1, "messageCount", BigInteger.valueOf(10)),
  146 + row("userId", 2L, "messageCount", 5)));
146 147  
147 148 Map<Long, Long> result = service.getTopSenderCountMap(BEGIN, END, 3);
148 149 assertEquals(10L, result.get(1L));
... ...