diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/BeanCopyUtils.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/BeanCopyUtils.java
new file mode 100644
index 0000000..4bfcfda
--- /dev/null
+++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/BeanCopyUtils.java
@@ -0,0 +1,203 @@
+package com.lld.im.service.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.SimpleCache;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.cglib.beans.BeanCopier;
+import org.springframework.cglib.beans.BeanMap;
+import org.springframework.cglib.core.Converter;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * bean拷贝工具(基于 cglib 性能优异)
+ *
+ * 重点 cglib 不支持 拷贝到链式对象
+ * 例如: 源对象 拷贝到 目标(链式对象)
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class BeanCopyUtils {
+
+ /**
+ * 单对象基于class创建拷贝
+ *
+ * @param source 数据来源实体
+ * @param desc 描述对象 转换后的对象
+ * @return desc
+ */
+ public static V copy(T source, Class desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ final V target = ReflectUtil.newInstanceIfPossible(desc);
+ return copy(source, target);
+ }
+
+ /**
+ * 单对象基于对象创建拷贝
+ *
+ * @param source 数据来源实体
+ * @param desc 转换后的对象
+ * @return desc
+ */
+ public static V copy(T source, V desc) {
+ if (ObjectUtil.isNull(source)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(desc)) {
+ return null;
+ }
+ BeanCopier beanCopier = BeanCopierCache.INSTANCE.get(source.getClass(), desc.getClass(), null);
+ beanCopier.copy(source, desc, null);
+ return desc;
+ }
+
+ /**
+ * 列表对象基于class创建拷贝
+ *
+ * @param sourceList 数据来源实体列表
+ * @param desc 描述对象 转换后的对象
+ * @return desc
+ */
+ public static List copyList(List sourceList, Class desc) {
+ if (ObjectUtil.isNull(sourceList)) {
+ return null;
+ }
+ if (CollUtil.isEmpty(sourceList)) {
+ return CollUtil.newArrayList();
+ }
+ return StreamUtils.toList(sourceList, source -> {
+ V target = ReflectUtil.newInstanceIfPossible(desc);
+ copy(source, target);
+ return target;
+ });
+ }
+
+ /**
+ * bean拷贝到map
+ *
+ * @param bean 数据来源实体
+ * @return map对象
+ */
+ @SuppressWarnings("unchecked")
+ public static Map copyToMap(T bean) {
+ if (ObjectUtil.isNull(bean)) {
+ return null;
+ }
+ return BeanMap.create(bean);
+ }
+
+ /**
+ * map拷贝到bean
+ *
+ * @param map 数据来源
+ * @param beanClass bean类
+ * @return bean对象
+ */
+ public static T mapToBean(Map map, Class beanClass) {
+ if (MapUtil.isEmpty(map)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(beanClass)) {
+ return null;
+ }
+ T bean = ReflectUtil.newInstanceIfPossible(beanClass);
+ return mapToBean(map, bean);
+ }
+
+ /**
+ * map拷贝到bean
+ *
+ * @param map 数据来源
+ * @param bean bean对象
+ * @return bean对象
+ */
+ public static T mapToBean(Map map, T bean) {
+ if (MapUtil.isEmpty(map)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(bean)) {
+ return null;
+ }
+ BeanMap.create(bean).putAll(map);
+ return bean;
+ }
+
+ /**
+ * map拷贝到map
+ *
+ * @param map 数据来源
+ * @param clazz 返回的对象类型
+ * @return map对象
+ */
+ public static Map mapToMap(Map map, Class clazz) {
+ if (MapUtil.isEmpty(map)) {
+ return null;
+ }
+ if (ObjectUtil.isNull(clazz)) {
+ return null;
+ }
+ Map copyMap = new LinkedHashMap<>(map.size());
+ map.forEach((k, v) -> copyMap.put(k, copy(v, clazz)));
+ return copyMap;
+ }
+
+ /**
+ * BeanCopier属性缓存
+ * 缓存用于防止多次反射造成的性能问题
+ *
+ * @author Looly
+ * @since 5.4.1
+ */
+ public enum BeanCopierCache {
+ /**
+ * BeanCopier属性缓存单例
+ */
+ INSTANCE;
+
+ private final SimpleCache cache = new SimpleCache<>();
+
+ /**
+ * 获得类与转换器生成的key在{@link BeanCopier}的Map中对应的元素
+ *
+ * @param srcClass 源Bean的类
+ * @param targetClass 目标Bean的类
+ * @param converter 转换器
+ * @return Map中对应的BeanCopier
+ */
+ public BeanCopier get(Class> srcClass, Class> targetClass, Converter converter) {
+ final String key = genKey(srcClass, targetClass, converter);
+ return cache.get(key, () -> BeanCopier.create(srcClass, targetClass, converter != null));
+ }
+
+ /**
+ * 获得类与转换器生成的key
+ *
+ * @param srcClass 源Bean的类
+ * @param targetClass 目标Bean的类
+ * @param converter 转换器
+ * @return 属性名和Map映射的key
+ */
+ private String genKey(Class> srcClass, Class> targetClass, Converter converter) {
+ final StringBuilder key = StrUtil.builder()
+ .append(srcClass.getName()).append('#').append(targetClass.getName());
+ if (null != converter) {
+ key.append('#').append(converter.getClass().getName());
+ }
+ return key.toString();
+ }
+ }
+
+}
diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/StreamUtils.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/StreamUtils.java
new file mode 100644
index 0000000..c2eb3ef
--- /dev/null
+++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/StreamUtils.java
@@ -0,0 +1,254 @@
+package com.lld.im.service.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * stream 流工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StreamUtils {
+
+ /**
+ * 将collection过滤
+ *
+ * @param collection 需要转化的集合
+ * @param function 过滤方法
+ * @return 过滤后的list
+ */
+ public static List filter(Collection collection, Predicate function) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().filter(function).collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection拼接
+ *
+ * @param collection 需要转化的集合
+ * @param function 拼接方法
+ * @return 拼接后的list
+ */
+ public static String join(Collection collection, Function function) {
+ return join(collection, function, StringUtils.SEPARATOR);
+ }
+
+ /**
+ * 将collection拼接
+ *
+ * @param collection 需要转化的集合
+ * @param function 拼接方法
+ * @param delimiter 拼接符
+ * @return 拼接后的list
+ */
+ public static String join(Collection collection, Function function, CharSequence delimiter) {
+ if (CollUtil.isEmpty(collection)) {
+ return StringUtils.EMPTY;
+ }
+ return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
+ }
+
+ /**
+ * 将collection排序
+ *
+ * @param collection 需要转化的集合
+ * @param comparing 排序方法
+ * @return 排序后的list
+ */
+ public static List sorted(Collection collection, Comparator comparing) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection转化为类型不变的map
+ * {@code Collection ----> Map}
+ *
+ * @param collection 需要转化的集合
+ * @param key V类型转化为K类型的lambda方法
+ * @param collection中的泛型
+ * @param map中的key类型
+ * @return 转化后的map
+ */
+ public static Map toIdentityMap(Collection collection, Function key) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+ }
+
+ /**
+ * 将Collection转化为map(value类型与collection的泛型不同)
+ * {@code Collection -----> Map }
+ *
+ * @param collection 需要转化的集合
+ * @param key E类型转化为K类型的lambda方法
+ * @param value E类型转化为V类型的lambda方法
+ * @param collection中的泛型
+ * @param map中的key类型
+ * @param map中的value类型
+ * @return 转化后的map
+ */
+ public static Map toMap(Collection collection, Function key, Function value) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
+ }
+
+ /**
+ * 将collection按照规则(比如有相同的班级id)分类成map
+ * {@code Collection -------> Map> }
+ *
+ * @param collection 需要分类的集合
+ * @param key 分类的规则
+ * @param collection中的泛型
+ * @param map中的key类型
+ * @return 分类后的map
+ */
+ public static Map> groupByKey(Collection collection, Function key) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
+ }
+
+ /**
+ * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map
+ * {@code Collection ---> Map>> }
+ *
+ * @param collection 需要分类的集合
+ * @param key1 第一个分类的规则
+ * @param key2 第二个分类的规则
+ * @param 集合元素类型
+ * @param 第一个map中的key类型
+ * @param 第二个map中的key类型
+ * @return 分类后的map
+ */
+ public static Map>> groupBy2Key(Collection collection, Function key1, Function key2) {
+ if (CollUtil.isEmpty(collection)) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
+ }
+
+ /**
+ * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map
+ * {@code Collection ---> Map> }
+ *
+ * @param collection 需要分类的集合
+ * @param key1 第一个分类的规则
+ * @param key2 第二个分类的规则
+ * @param 第一个map中的key类型
+ * @param 第二个map中的key类型
+ * @param collection中的泛型
+ * @return 分类后的map
+ */
+ public static Map> group2Map(Collection collection, Function key1, Function key2) {
+ if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
+ return MapUtil.newHashMap();
+ }
+ return collection
+ .stream().filter(Objects::nonNull)
+ .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
+ }
+
+ /**
+ * 将collection转化为List集合,但是两者的泛型不同
+ * {@code Collection ------> List }
+ *
+ * @param collection 需要转化的集合
+ * @param function collection中的泛型转化为list泛型的lambda表达式
+ * @param collection中的泛型
+ * @param List中的泛型
+ * @return 转化后的list
+ */
+ public static List toList(Collection collection, Function function) {
+ if (CollUtil.isEmpty(collection)) {
+ return CollUtil.newArrayList();
+ }
+ return collection
+ .stream()
+ .map(function)
+ .filter(Objects::nonNull)
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 将collection转化为Set集合,但是两者的泛型不同
+ * {@code Collection ------> Set }
+ *
+ * @param collection 需要转化的集合
+ * @param function collection中的泛型转化为set泛型的lambda表达式
+ * @param collection中的泛型
+ * @param Set中的泛型
+ * @return 转化后的Set
+ */
+ public static Set toSet(Collection collection, Function function) {
+ if (CollUtil.isEmpty(collection) || function == null) {
+ return CollUtil.newHashSet();
+ }
+ return collection
+ .stream()
+ .map(function)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ }
+
+
+ /**
+ * 合并两个相同key类型的map
+ *
+ * @param map1 第一个需要合并的 map
+ * @param map2 第二个需要合并的 map
+ * @param merge 合并的lambda,将key value1 value2合并成最终的类型,注意value可能为空的情况
+ * @param map中的key类型
+ * @param 第一个 map的value类型
+ * @param 第二个 map的value类型
+ * @param 最终map的value类型
+ * @return 合并后的map
+ */
+ public static Map merge(Map map1, Map map2, BiFunction merge) {
+ if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
+ return MapUtil.newHashMap();
+ } else if (MapUtil.isEmpty(map1)) {
+ map1 = MapUtil.newHashMap();
+ } else if (MapUtil.isEmpty(map2)) {
+ map2 = MapUtil.newHashMap();
+ }
+ Set key = new HashSet<>();
+ key.addAll(map1.keySet());
+ key.addAll(map2.keySet());
+ Map map = new HashMap<>();
+ for (K t : key) {
+ X x = map1.get(t);
+ Y y = map2.get(t);
+ V z = merge.apply(x, y);
+ if (z != null) {
+ map.put(t, z);
+ }
+ }
+ return map;
+ }
+
+}
diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/StringUtils.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/StringUtils.java
new file mode 100644
index 0000000..38ed95a
--- /dev/null
+++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/StringUtils.java
@@ -0,0 +1,274 @@
+package com.lld.im.service.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 字符串工具类
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+
+ public static final String SEPARATOR = ",";
+
+ /**
+ * 获取参数不为空值
+ *
+ * @param str defaultValue 要判断的value
+ * @return value 返回值
+ */
+ public static String blankToDefault(String str, String defaultValue) {
+ return StrUtil.blankToDefault(str, defaultValue);
+ }
+
+ /**
+ * * 判断一个字符串是否为空串
+ *
+ * @param str String
+ * @return true:为空 false:非空
+ */
+ public static boolean isEmpty(String str) {
+ return StrUtil.isEmpty(str);
+ }
+
+ /**
+ * * 判断一个字符串是否为非空串
+ *
+ * @param str String
+ * @return true:非空串 false:空串
+ */
+ public static boolean isNotEmpty(String str) {
+ return !isEmpty(str);
+ }
+
+ /**
+ * 去空格
+ */
+ public static String trim(String str) {
+ return StrUtil.trim(str);
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ * @return 结果
+ */
+ public static String substring(final String str, int start) {
+ return substring(str, start, str.length());
+ }
+
+ /**
+ * 截取字符串
+ *
+ * @param str 字符串
+ * @param start 开始
+ * @param end 结束
+ * @return 结果
+ */
+ public static String substring(final String str, int start, int end) {
+ return StrUtil.sub(str, start, end);
+ }
+
+ /**
+ * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is {} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ *
+ * @param template 文本模板,被替换的部分用 {} 表示
+ * @param params 参数值
+ * @return 格式化后的文本
+ */
+ public static String format(String template, Object... params) {
+ return StrUtil.format(template, params);
+ }
+
+ /**
+ * 是否为http(s)://开头
+ *
+ * @param link 链接
+ * @return 结果
+ */
+ public static boolean ishttp(String link) {
+ return Validator.isUrl(link);
+ }
+
+ /**
+ * 字符串转set
+ *
+ * @param str 字符串
+ * @param sep 分隔符
+ * @return set集合
+ */
+ public static Set str2Set(String str, String sep) {
+ return new HashSet<>(str2List(str, sep, true, false));
+ }
+
+ /**
+ * 字符串转list
+ *
+ * @param str 字符串
+ * @param sep 分隔符
+ * @param filterBlank 过滤纯空白
+ * @param trim 去掉首尾空白
+ * @return list集合
+ */
+ public static List str2List(String str, String sep, boolean filterBlank, boolean trim) {
+ List list = new ArrayList<>();
+ if (isEmpty(str)) {
+ return list;
+ }
+
+ // 过滤空白字符串
+ if (filterBlank && isBlank(str)) {
+ return list;
+ }
+ String[] split = str.split(sep);
+ for (String string : split) {
+ if (filterBlank && isBlank(string)) {
+ continue;
+ }
+ if (trim) {
+ string = trim(string);
+ }
+ list.add(string);
+ }
+
+ return list;
+ }
+
+ /**
+ * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+ *
+ * @param cs 指定字符串
+ * @param searchCharSequences 需要检查的字符串数组
+ * @return 是否包含任意一个字符串
+ */
+ public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
+ return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
+ }
+
+ /**
+ * 驼峰转下划线命名
+ */
+ public static String toUnderScoreCase(String str) {
+ return StrUtil.toUnderlineCase(str);
+ }
+
+ /**
+ * 是否包含字符串
+ *
+ * @param str 验证字符串
+ * @param strs 字符串组
+ * @return 包含返回true
+ */
+ public static boolean inStringIgnoreCase(String str, String... strs) {
+ return StrUtil.equalsAnyIgnoreCase(str, strs);
+ }
+
+ /**
+ * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+ *
+ * @param name 转换前的下划线大写方式命名的字符串
+ * @return 转换后的驼峰式命名的字符串
+ */
+ public static String convertToCamelCase(String name) {
+ return StrUtil.upperFirst(StrUtil.toCamelCase(name));
+ }
+
+ /**
+ * 驼峰式命名法 例如:user_name->userName
+ */
+ public static String toCamelCase(String s) {
+ return StrUtil.toCamelCase(s);
+ }
+
+ /**
+ * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+ *
+ * @param str 指定字符串
+ * @param strs 需要检查的字符串数组
+ * @return 是否匹配
+ */
+ public static boolean matches(String str, List strs) {
+ if (isEmpty(str) || CollUtil.isEmpty(strs)) {
+ return false;
+ }
+ for (String pattern : strs) {
+ if (isMatch(pattern, str)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断url是否与规则配置:
+ * ? 表示单个字符;
+ * * 表示一层路径内的任意字符串,不可跨层级;
+ * ** 表示任意层路径;
+ *
+ * @param pattern 匹配规则
+ * @param url 需要匹配的url
+ */
+ public static boolean isMatch(String pattern, String url) {
+ AntPathMatcher matcher = new AntPathMatcher();
+ return matcher.match(pattern, url);
+ }
+
+ /**
+ * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+ *
+ * @param num 数字对象
+ * @param size 字符串指定长度
+ * @return 返回数字的字符串格式,该字符串为指定长度。
+ */
+ public static String padl(final Number num, final int size) {
+ return padl(num.toString(), size, '0');
+ }
+
+ /**
+ * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+ *
+ * @param s 原始字符串
+ * @param size 字符串指定长度
+ * @param c 用于补齐的字符
+ * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+ */
+ public static String padl(final String s, final int size, final char c) {
+ final StringBuilder sb = new StringBuilder(size);
+ if (s != null) {
+ final int len = s.length();
+ if (s.length() <= size) {
+ for (int i = size - len; i > 0; i--) {
+ sb.append(c);
+ }
+ sb.append(s);
+ } else {
+ return s.substring(len - size, len);
+ }
+ } else {
+ for (int i = size; i > 0; i--) {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+}