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,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameter;
13 import io.swagger.v3.oas.annotations.tags.Tag; 13 import io.swagger.v3.oas.annotations.tags.Tag;
14 import jakarta.annotation.Resource; 14 import jakarta.annotation.Resource;
15 import jakarta.validation.Valid; 15 import jakarta.validation.Valid;
  16 +import jakarta.validation.constraints.Size;
16 import org.springframework.security.access.prepost.PreAuthorize; 17 import org.springframework.security.access.prepost.PreAuthorize;
17 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
18 import org.springframework.web.bind.annotation.*; 19 import org.springframework.web.bind.annotation.*;
@@ -58,7 +59,8 @@ public class ImFacePackItemManagerController { @@ -58,7 +59,8 @@ public class ImFacePackItemManagerController {
58 @Operation(summary = "批量删除表情") 59 @Operation(summary = "批量删除表情")
59 @Parameter(name = "ids", description = "编号列表", required = true) 60 @Parameter(name = "ids", description = "编号列表", required = true)
60 @PreAuthorize("@ss.hasPermission('im:manager:face-pack-item:delete')") 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 facePackItemService.deleteFacePackItemList(ids); 64 facePackItemService.deleteFacePackItemList(ids);
63 return success(true); 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,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameter;
13 import io.swagger.v3.oas.annotations.tags.Tag; 13 import io.swagger.v3.oas.annotations.tags.Tag;
14 import jakarta.annotation.Resource; 14 import jakarta.annotation.Resource;
15 import jakarta.validation.Valid; 15 import jakarta.validation.Valid;
  16 +import jakarta.validation.constraints.Size;
16 import org.springframework.security.access.prepost.PreAuthorize; 17 import org.springframework.security.access.prepost.PreAuthorize;
17 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
18 import org.springframework.web.bind.annotation.*; 19 import org.springframework.web.bind.annotation.*;
@@ -58,7 +59,8 @@ public class ImFacePackManagerController { @@ -58,7 +59,8 @@ public class ImFacePackManagerController {
58 @Operation(summary = "批量删除表情包") 59 @Operation(summary = "批量删除表情包")
59 @Parameter(name = "ids", description = "编号列表", required = true) 60 @Parameter(name = "ids", description = "编号列表", required = true)
60 @PreAuthorize("@ss.hasPermission('im:manager:face-pack:delete')") 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 facePackService.deleteFacePackList(ids); 64 facePackService.deleteFacePackList(ids);
63 return success(true); 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 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item; 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 import cn.iocoder.yudao.framework.common.pojo.PageParam; 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
4 import io.swagger.v3.oas.annotations.media.Schema; 6 import io.swagger.v3.oas.annotations.media.Schema;
5 import lombok.Data; 7 import lombok.Data;
6 import lombok.EqualsAndHashCode; 8 import lombok.EqualsAndHashCode;
@@ -17,6 +19,7 @@ public class ImFacePackItemPageReqVO extends PageParam { @@ -17,6 +19,7 @@ public class ImFacePackItemPageReqVO extends PageParam {
17 private String name; 19 private String name;
18 20
19 @Schema(description = "状态", example = "0") 21 @Schema(description = "状态", example = "0")
  22 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
20 private Integer status; // 参见 CommonStatusEnum 枚举类 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 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.item; 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 import io.swagger.v3.oas.annotations.media.Schema; 5 import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.Max; 6 import jakarta.validation.constraints.Max;
5 import jakarta.validation.constraints.Min; 7 import jakarta.validation.constraints.Min;
@@ -47,6 +49,7 @@ public class ImFacePackItemSaveReqVO { @@ -47,6 +49,7 @@ public class ImFacePackItemSaveReqVO {
47 49
48 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") 50 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
49 @NotNull(message = "状态不能为空") 51 @NotNull(message = "状态不能为空")
  52 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
50 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用) 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 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack; 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 import cn.iocoder.yudao.framework.common.pojo.PageParam; 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
4 import io.swagger.v3.oas.annotations.media.Schema; 6 import io.swagger.v3.oas.annotations.media.Schema;
5 import lombok.Data; 7 import lombok.Data;
6 import lombok.EqualsAndHashCode; 8 import lombok.EqualsAndHashCode;
@@ -19,6 +21,7 @@ public class ImFacePackPageReqVO extends PageParam { @@ -19,6 +21,7 @@ public class ImFacePackPageReqVO extends PageParam {
19 private String name; 21 private String name;
20 22
21 @Schema(description = "状态", example = "0") 23 @Schema(description = "状态", example = "0")
  24 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
22 private Integer status; // 参见 CommonStatusEnum 枚举类 25 private Integer status; // 参见 CommonStatusEnum 枚举类
23 26
24 @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]") 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 package cn.iocoder.yudao.module.im.controller.admin.manager.face.vo.pack; 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 import io.swagger.v3.oas.annotations.media.Schema; 5 import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotBlank; 6 import jakarta.validation.constraints.NotBlank;
5 import jakarta.validation.constraints.NotNull; 7 import jakarta.validation.constraints.NotNull;
@@ -28,6 +30,7 @@ public class ImFacePackSaveReqVO { @@ -28,6 +30,7 @@ public class ImFacePackSaveReqVO {
28 30
29 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") 31 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
30 @NotNull(message = "状态不能为空") 32 @NotNull(message = "状态不能为空")
  33 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
31 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用) 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,6 +18,7 @@ import io.swagger.v3.oas.annotations.Parameter;
18 import io.swagger.v3.oas.annotations.tags.Tag; 18 import io.swagger.v3.oas.annotations.tags.Tag;
19 import jakarta.annotation.Resource; 19 import jakarta.annotation.Resource;
20 import jakarta.validation.Valid; 20 import jakarta.validation.Valid;
  21 +import jakarta.validation.constraints.Size;
21 import org.springframework.security.access.prepost.PreAuthorize; 22 import org.springframework.security.access.prepost.PreAuthorize;
22 import org.springframework.validation.annotation.Validated; 23 import org.springframework.validation.annotation.Validated;
23 import org.springframework.web.bind.annotation.*; 24 import org.springframework.web.bind.annotation.*;
@@ -67,7 +68,9 @@ public class ImSensitiveWordManagerController { @@ -67,7 +68,9 @@ public class ImSensitiveWordManagerController {
67 @Operation(summary = "批量删除敏感词") 68 @Operation(summary = "批量删除敏感词")
68 @Parameter(name = "ids", description = "编号列表", required = true) 69 @Parameter(name = "ids", description = "编号列表", required = true)
69 @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')") 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 sensitiveWordService.deleteSensitiveWordList(ids); 74 sensitiveWordService.deleteSensitiveWordList(ids);
72 return success(true); 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 package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo; 1 package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
2 2
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 import cn.iocoder.yudao.framework.common.pojo.PageParam; 4 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
4 import io.swagger.v3.oas.annotations.media.Schema; 6 import io.swagger.v3.oas.annotations.media.Schema;
5 import lombok.Data; 7 import lombok.Data;
6 import lombok.EqualsAndHashCode; 8 import lombok.EqualsAndHashCode;
@@ -19,6 +21,7 @@ public class ImSensitiveWordPageReqVO extends PageParam { @@ -19,6 +21,7 @@ public class ImSensitiveWordPageReqVO extends PageParam {
19 private String word; 21 private String word;
20 22
21 @Schema(description = "状态", example = "0") 23 @Schema(description = "状态", example = "0")
  24 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
22 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用) 25 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)
23 26
24 @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]") 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 package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo; 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 import io.swagger.v3.oas.annotations.media.Schema; 5 import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotBlank; 6 import jakarta.validation.constraints.NotBlank;
5 import jakarta.validation.constraints.NotNull; 7 import jakarta.validation.constraints.NotNull;
@@ -20,6 +22,7 @@ public class ImSensitiveWordSaveReqVO { @@ -20,6 +22,7 @@ public class ImSensitiveWordSaveReqVO {
20 22
21 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") 23 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
22 @NotNull(message = "状态不能为空") 24 @NotNull(message = "状态不能为空")
  25 + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}")
23 private Integer status; // 参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用) 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,6 +12,7 @@ import io.swagger.v3.oas.annotations.Parameter;
12 import io.swagger.v3.oas.annotations.tags.Tag; 12 import io.swagger.v3.oas.annotations.tags.Tag;
13 import jakarta.annotation.Resource; 13 import jakarta.annotation.Resource;
14 import jakarta.validation.Valid; 14 import jakarta.validation.Valid;
  15 +import jakarta.validation.constraints.Min;
15 import org.springframework.validation.annotation.Validated; 16 import org.springframework.validation.annotation.Validated;
16 import org.springframework.web.bind.annotation.*; 17 import org.springframework.web.bind.annotation.*;
17 18
@@ -40,8 +41,9 @@ public class ImGroupMessageController { @@ -40,8 +41,9 @@ public class ImGroupMessageController {
40 @Operation(summary = "拉取群聊消息(增量)") 41 @Operation(summary = "拉取群聊消息(增量)")
41 @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0") 42 @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
42 @Parameter(name = "size", description = "拉取数量", required = true, example = "100") 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 List<ImGroupMessageDO> messages = groupMessageService.pullGroupMessageList(getLoginUserId(), minId, size); 47 List<ImGroupMessageDO> messages = groupMessageService.pullGroupMessageList(getLoginUserId(), minId, size);
46 return success(BeanUtils.toBean(messages, ImGroupMessageRespVO.class)); 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,6 +12,7 @@ import io.swagger.v3.oas.annotations.Parameter;
12 import io.swagger.v3.oas.annotations.tags.Tag; 12 import io.swagger.v3.oas.annotations.tags.Tag;
13 import jakarta.annotation.Resource; 13 import jakarta.annotation.Resource;
14 import jakarta.validation.Valid; 14 import jakarta.validation.Valid;
  15 +import jakarta.validation.constraints.Min;
15 import org.springframework.validation.annotation.Validated; 16 import org.springframework.validation.annotation.Validated;
16 import org.springframework.web.bind.annotation.*; 17 import org.springframework.web.bind.annotation.*;
17 18
@@ -41,8 +42,9 @@ public class ImPrivateMessageController { @@ -41,8 +42,9 @@ public class ImPrivateMessageController {
41 @Operation(summary = "拉取私聊消息(增量)") 42 @Operation(summary = "拉取私聊消息(增量)")
42 @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0") 43 @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0")
43 @Parameter(name = "size", description = "拉取数量", required = true, example = "100") 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 List<ImPrivateMessageDO> messages = privateMessageService.pullPrivateMessageList(getLoginUserId(), minId, size); 48 List<ImPrivateMessageDO> messages = privateMessageService.pullPrivateMessageList(getLoginUserId(), minId, size);
47 return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class)); 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,7 +20,7 @@ public interface ImFacePackItemMapper extends BaseMapperX&lt;ImFacePackItemDO&gt; {
20 20
21 default List<ImFacePackItemDO> selectListByPackIdsAndStatus(Collection<Long> packIds, Integer status) { 21 default List<ImFacePackItemDO> selectListByPackIdsAndStatus(Collection<Long> packIds, Integer status) {
22 return selectList(new LambdaQueryWrapperX<ImFacePackItemDO>() 22 return selectList(new LambdaQueryWrapperX<ImFacePackItemDO>()
23 - .inIfPresent(ImFacePackItemDO::getPackId, packIds) 23 + .in(ImFacePackItemDO::getPackId, packIds)
24 .eq(ImFacePackItemDO::getStatus, status) 24 .eq(ImFacePackItemDO::getStatus, status)
25 .orderByAsc(ImFacePackItemDO::getPackId) 25 .orderByAsc(ImFacePackItemDO::getPackId)
26 .orderByAsc(ImFacePackItemDO::getSort) 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,7 +73,7 @@ public interface ImStatisticsManagerMapper {
73 // ==================== 群 ==================== 73 // ==================== 群 ====================
74 74
75 /** 75 /**
76 - * 群总数(im_group) 76 + * 当前有效群总数
77 */ 77 */
78 @Select("SELECT COUNT(*) FROM im_group WHERE deleted = 0 AND status = 0") 78 @Select("SELECT COUNT(*) FROM im_group WHERE deleted = 0 AND status = 0")
79 Long selectTotalGroupCount(); 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,7 +90,7 @@ public class LiveKitClient {
90 .setNotBefore(new Date(nowSec * 1000)) 90 .setNotBefore(new Date(nowSec * 1000))
91 .setExpiresAt(new Date((nowSec + ttlSec) * 1000)) 91 .setExpiresAt(new Date((nowSec + ttlSec) * 1000))
92 .setPayload("video", video) 92 .setPayload("video", video)
93 - .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes())); 93 + .setSigner(JWTSignerUtil.hs256(cfg.getApiSecret().getBytes(StandardCharsets.UTF_8)));
94 if (StrUtil.isNotEmpty(displayName)) { 94 if (StrUtil.isNotEmpty(displayName)) {
95 jwt.setPayload("name", displayName); 95 jwt.setPayload("name", displayName);
96 } 96 }
@@ -219,7 +219,7 @@ public class LiveKitClient { @@ -219,7 +219,7 @@ public class LiveKitClient {
219 .setNotBefore(new Date(nowSec * 1000)) 219 .setNotBefore(new Date(nowSec * 1000))
220 .setExpiresAt(new Date((nowSec + ADMIN_TOKEN_TTL.getSeconds()) * 1000)) 220 .setExpiresAt(new Date((nowSec + ADMIN_TOKEN_TTL.getSeconds()) * 1000))
221 .setPayload("video", MapUtil.of("roomAdmin", true)) 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 .sign(); 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 package cn.iocoder.yudao.module.im.framework.web; 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 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.hutool.core.util.BooleanUtil; 4 import cn.hutool.core.util.BooleanUtil;
4 import cn.hutool.extra.spring.SpringUtil; 5 import cn.hutool.extra.spring.SpringUtil;
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -28,6 +29,7 @@ import org.springframework.validation.annotation.Validated; @@ -28,6 +29,7 @@ import org.springframework.validation.annotation.Validated;
28 29
29 import java.time.LocalDateTime; 30 import java.time.LocalDateTime;
30 import java.util.Collection; 31 import java.util.Collection;
  32 +import java.util.Collections;
31 import java.util.List; 33 import java.util.List;
32 34
33 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 35 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -100,6 +102,9 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -100,6 +102,9 @@ public class ImFriendServiceImpl implements ImFriendService {
100 102
101 @Override 103 @Override
102 public List<ImFriendDO> getActiveFriendList(Long userId, Collection<Long> friendUserIds) { 104 public List<ImFriendDO> getActiveFriendList(Long userId, Collection<Long> friendUserIds) {
  105 + if (CollUtil.isEmpty(friendUserIds)) {
  106 + return Collections.emptyList();
  107 + }
103 return friendMapper.selectListByUserIdAndFriendUserIdsAndStatus(userId, friendUserIds, 108 return friendMapper.selectListByUserIdAndFriendUserIdsAndStatus(userId, friendUserIds,
104 CommonStatusEnum.ENABLE.getStatus()); 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,6 +669,10 @@ public class ImGroupServiceImpl implements ImGroupService {
669 * 三档分层禁言权限校验 669 * 三档分层禁言权限校验
670 */ 670 */
671 private void validateMutePermission(ImGroupMemberDO operator, ImGroupMemberDO target) { 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 if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { 677 if (ImGroupMemberRoleEnum.isOwner(target.getRole())) {
674 throw exception(GROUP_MUTE_OWNER_DENIED); 678 throw exception(GROUP_MUTE_OWNER_DENIED);
@@ -677,10 +681,6 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -677,10 +681,6 @@ public class ImGroupServiceImpl implements ImGroupService {
677 if (ImGroupMemberRoleEnum.isAdmin(target.getRole()) && !ImGroupMemberRoleEnum.isOwner(operator.getRole())) { 681 if (ImGroupMemberRoleEnum.isAdmin(target.getRole()) && !ImGroupMemberRoleEnum.isOwner(operator.getRole())) {
678 throw exception(GROUP_MUTE_ADMIN_DENIED); 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 private ImGroupServiceImpl getSelf() { 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,7 +71,7 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic
71 List<Map<String, Object>> rows = statisticsMapper.selectGroupSizeDistribution(); 71 List<Map<String, Object>> rows = statisticsMapper.selectGroupSizeDistribution();
72 return convertMap(rows, 72 return convertMap(rows,
73 row -> (String) row.get("range"), 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,16 +102,16 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic
102 public Map<Integer, Long> getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime) { 102 public Map<Integer, Long> getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime) {
103 List<Map<String, Object>> rows = statisticsMapper.selectMessageTypeDistribution(beginTime, endTime); 103 List<Map<String, Object>> rows = statisticsMapper.selectMessageTypeDistribution(beginTime, endTime);
104 return convertMap(rows, 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 @Override 109 @Override
110 public Map<Long, Long> getTopSenderCountMap(LocalDateTime beginTime, LocalDateTime endTime, int limit) { 110 public Map<Long, Long> getTopSenderCountMap(LocalDateTime beginTime, LocalDateTime endTime, int limit) {
111 List<Map<String, Object>> rows = statisticsMapper.selectTopSenders(beginTime, endTime, limit); 111 List<Map<String, Object>> rows = statisticsMapper.selectTopSenders(beginTime, endTime, limit);
112 return convertMap(rows, 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,7 +120,7 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic
120 private static Map<LocalDateTime, Long> toDailyCountMap(List<Map<String, Object>> rows) { 120 private static Map<LocalDateTime, Long> toDailyCountMap(List<Map<String, Object>> rows) {
121 return convertMap(rows, 121 return convertMap(rows,
122 row -> Convert.convert(LocalDate.class, row.get("date")).atStartOfDay(), 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,6 +17,7 @@ import org.mockito.Mock;
17 import org.springframework.dao.DuplicateKeyException; 17 import org.springframework.dao.DuplicateKeyException;
18 18
19 import java.time.LocalDateTime; 19 import java.time.LocalDateTime;
  20 +import java.util.Collections;
20 import java.util.List; 21 import java.util.List;
21 22
22 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; 23 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
@@ -213,4 +214,12 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { @@ -213,4 +214,12 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
213 assertEquals(2, result.size()); 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,10 +7,12 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO;
8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; 8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO;
9 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; 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 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; 11 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO;
11 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; 12 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO;
12 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; 13 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
13 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; 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 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 16 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
15 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 17 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 18 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
@@ -932,6 +934,76 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -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 // ========== validateGroupExists ========== 1007 // ========== validateGroupExists ==========
936 1008
937 @Test 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 +6,7 @@ import org.junit.jupiter.api.Test;
6 import org.mockito.InjectMocks; 6 import org.mockito.InjectMocks;
7 import org.mockito.Mock; 7 import org.mockito.Mock;
8 8
  9 +import java.math.BigInteger;
9 import java.time.LocalDate; 10 import java.time.LocalDate;
10 import java.time.LocalDateTime; 11 import java.time.LocalDateTime;
11 import java.util.Arrays; 12 import java.util.Arrays;
@@ -75,10 +76,10 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { @@ -75,10 +76,10 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
75 76
76 @Test 77 @Test
77 public void testGetNewUserDailyCountMap_dateConvert() { 78 public void testGetNewUserDailyCountMap_dateConvert() {
78 - // 准备:date 字段以 SQL Date / String 混入,count 为 Long 79 + // 准备:date 字段以 SQL Date / String 混入,count 混入多种 Number
79 when(statisticsMapper.selectNewUserDailyCount(eq(BEGIN), eq(END))).thenReturn(Arrays.asList( 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 Map<LocalDateTime, Long> result = service.getNewUserDailyCountMap(BEGIN, END); 85 Map<LocalDateTime, Long> result = service.getNewUserDailyCountMap(BEGIN, END);
@@ -119,8 +120,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { @@ -119,8 +120,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
119 @Test 120 @Test
120 public void testGetGroupSizeCountMap() { 121 public void testGetGroupSizeCountMap() {
121 when(statisticsMapper.selectGroupSizeDistribution()).thenReturn(Arrays.asList( 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 Map<String, Long> result = service.getGroupSizeCountMap(); 126 Map<String, Long> result = service.getGroupSizeCountMap();
126 assertEquals(3L, result.get("1-9 人")); 127 assertEquals(3L, result.get("1-9 人"));
@@ -130,8 +131,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { @@ -130,8 +131,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
130 @Test 131 @Test
131 public void testGetMessageTypeCountMap() { 132 public void testGetMessageTypeCountMap() {
132 when(statisticsMapper.selectMessageTypeDistribution(BEGIN, END)).thenReturn(Arrays.asList( 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 Map<Integer, Long> result = service.getMessageTypeCountMap(BEGIN, END); 137 Map<Integer, Long> result = service.getMessageTypeCountMap(BEGIN, END);
137 assertEquals(8L, result.get(101)); 138 assertEquals(8L, result.get(101));
@@ -141,8 +142,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { @@ -141,8 +142,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
141 @Test 142 @Test
142 public void testGetTopSenderCountMap_passesLimit() { 143 public void testGetTopSenderCountMap_passesLimit() {
143 when(statisticsMapper.selectTopSenders(BEGIN, END, 3)).thenReturn(Arrays.asList( 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 Map<Long, Long> result = service.getTopSenderCountMap(BEGIN, END, 3); 148 Map<Long, Long> result = service.getTopSenderCountMap(BEGIN, END, 3);
148 assertEquals(10L, result.get(1L)); 149 assertEquals(10L, result.get(1L));