This commit is contained in:
2023-11-28 20:45:15 +08:00
9 changed files with 923 additions and 5 deletions

View File

@@ -0,0 +1,24 @@
package com.lld.im.common.model;
import lombok.Data;
/**
* @author: Chackylee
* @description:
**/
@Data
public class ChatHistoryReq extends RequestBase {
private static final long serialVersionUID = 1L;
private String operater;
private String userId;
//单次拉取数量
private Integer limit;
//第几页
private Integer offset;
}

View File

@@ -34,6 +34,10 @@
<artifactId>zkclient</artifactId> <artifactId>zkclient</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- redis --> <!-- redis -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@@ -1,8 +1,12 @@
package com.lld.im.service; package com.lld.im.service;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@@ -17,6 +21,12 @@ public class Application {
SpringApplication.run(Application.class, args); SpringApplication.run(Application.class, args);
} }
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
return interceptor;
}
} }

View File

@@ -1,9 +1,11 @@
package com.lld.im.service.message.controller; package com.lld.im.service.message.controller;
import com.lld.im.common.ResponseVO; import com.lld.im.common.ResponseVO;
import com.lld.im.common.model.ChatHistoryReq;
import com.lld.im.common.model.SyncReq; import com.lld.im.common.model.SyncReq;
import com.lld.im.common.model.message.CheckSendMessageReq; import com.lld.im.common.model.message.CheckSendMessageReq;
import com.lld.im.service.message.model.req.SendMessageReq; import com.lld.im.service.message.model.req.SendMessageReq;
import com.lld.im.service.message.model.resp.ImMessageHistoryVo;
import com.lld.im.service.message.service.MessageSyncService; import com.lld.im.service.message.service.MessageSyncService;
import com.lld.im.service.message.service.P2PMessageService; import com.lld.im.service.message.service.P2PMessageService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -12,6 +14,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* @description: * @description:
* @author: lld * @author: lld
@@ -46,7 +50,33 @@ public class MessageController {
return messageSyncService.syncOfflineMessage(req); return messageSyncService.syncOfflineMessage(req);
} }
/**
* 获取会话列表
* @param req
* @param appId
* @return
*/
@RequestMapping("/listMessage")
public ResponseVO listMessage(@RequestBody
@Validated SyncReq req, Integer appId) {
req.setAppId(appId);
List<Object> objects = messageSyncService.listSession(req);
return null;
}
/**
* 获取会话列表
* @param req
* @param appId
* @return
*/
@RequestMapping("/chatHistory")
public ResponseVO chatHistory(@RequestBody
@Validated ChatHistoryReq chatHistoryReq, Integer appId) {
chatHistoryReq.setAppId(appId);
List<ImMessageHistoryVo> imMessageHistoryVos = messageSyncService.chatHistory(chatHistoryReq);
return ResponseVO.successResponse(imMessageHistoryVos);
}

View File

@@ -0,0 +1,15 @@
package com.lld.im.service.message.model.resp;
import com.lld.im.service.message.dao.ImMessageHistoryEntity;
import lombok.Data;
/**
* @author: Chackylee
* @description:
**/
@Data
public class ImMessageHistoryVo extends ImMessageHistoryEntity {
private String messageBody;
}

View File

@@ -1,11 +1,12 @@
package com.lld.im.service.message.service; package com.lld.im.service.message.service;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lld.im.codec.pack.message.MessageReadedPack; import com.lld.im.codec.pack.message.MessageReadedPack;
import com.lld.im.codec.pack.message.RecallMessageNotifyPack; import com.lld.im.codec.pack.message.RecallMessageNotifyPack;
import com.lld.im.codec.proto.Message;
import com.lld.im.common.ResponseVO; import com.lld.im.common.ResponseVO;
import com.lld.im.common.constant.Constants; import com.lld.im.common.constant.Constants;
import com.lld.im.common.enums.ConversationTypeEnum; import com.lld.im.common.enums.ConversationTypeEnum;
@@ -14,6 +15,7 @@ import com.lld.im.common.enums.MessageErrorCode;
import com.lld.im.common.enums.command.Command; import com.lld.im.common.enums.command.Command;
import com.lld.im.common.enums.command.GroupEventCommand; import com.lld.im.common.enums.command.GroupEventCommand;
import com.lld.im.common.enums.command.MessageCommand; import com.lld.im.common.enums.command.MessageCommand;
import com.lld.im.common.model.ChatHistoryReq;
import com.lld.im.common.model.ClientInfo; import com.lld.im.common.model.ClientInfo;
import com.lld.im.common.model.SyncReq; import com.lld.im.common.model.SyncReq;
import com.lld.im.common.model.SyncResp; import com.lld.im.common.model.SyncResp;
@@ -24,12 +26,13 @@ import com.lld.im.common.model.message.RecallMessageContent;
import com.lld.im.service.conversation.service.ConversationService; import com.lld.im.service.conversation.service.ConversationService;
import com.lld.im.service.group.service.ImGroupMemberService; import com.lld.im.service.group.service.ImGroupMemberService;
import com.lld.im.service.message.dao.ImMessageBodyEntity; import com.lld.im.service.message.dao.ImMessageBodyEntity;
import com.lld.im.service.message.dao.ImMessageHistoryEntity;
import com.lld.im.service.message.dao.mapper.ImMessageBodyMapper; import com.lld.im.service.message.dao.mapper.ImMessageBodyMapper;
import com.lld.im.service.message.dao.mapper.ImMessageHistoryMapper;
import com.lld.im.service.message.model.resp.ImMessageHistoryVo;
import com.lld.im.service.seq.RedisSeq; import com.lld.im.service.seq.RedisSeq;
import com.lld.im.service.utils.ConversationIdGenerate; import com.lld.im.service.utils.*;
import com.lld.im.service.utils.GroupMessageProducer; import org.apache.commons.lang3.StringUtils;
import com.lld.im.service.utils.MessageProducer;
import com.lld.im.service.utils.SnowflakeIdWorker;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.DefaultTypedTuple; import org.springframework.data.redis.core.DefaultTypedTuple;
@@ -39,7 +42,9 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
/** /**
* @description: * @description:
@@ -73,6 +78,8 @@ public class MessageSyncService {
@Autowired @Autowired
GroupMessageProducer groupMessageProducer; GroupMessageProducer groupMessageProducer;
@Autowired
private ImMessageHistoryMapper imMessageHistoryMapper;
public void receiveMark(MessageReciveAckContent messageReciveAckContent){ public void receiveMark(MessageReciveAckContent messageReciveAckContent){
messageProducer.sendToUser(messageReciveAckContent.getToId(), messageProducer.sendToUser(messageReciveAckContent.getToId(),
@@ -156,6 +163,103 @@ public class MessageSyncService {
return ResponseVO.successResponse(resp); return ResponseVO.successResponse(resp);
} }
public List<Object> listSession(SyncReq req){
List<OfflineMessageContent> listOfflineMessage = listOfflineMessage(req);
List<ImMessageHistoryEntity> listReadMessage = listReadMessage(req);
Map<String, Long> collect = listReadMessage.stream().collect(Collectors.toMap(ImMessageHistoryEntity::getOwnerId, ImMessageHistoryEntity::getMessageKey));
return null;
}
public List<ImMessageHistoryVo> chatHistory(ChatHistoryReq chatHistoryReq){
Page<ImMessageHistoryEntity> objectPage = new Page<>(chatHistoryReq.getOffset(),chatHistoryReq.getLimit());
LambdaQueryWrapper<ImMessageHistoryEntity> and = new LambdaQueryWrapper<ImMessageHistoryEntity>().eq(ImMessageHistoryEntity::getOwnerId, chatHistoryReq.getOperater())
.and(x -> x.eq(ImMessageHistoryEntity::getFromId, chatHistoryReq.getUserId()).or().eq(ImMessageHistoryEntity::getToId, chatHistoryReq.getUserId()))
.orderByDesc(ImMessageHistoryEntity::getMessageKey);
Page<ImMessageHistoryEntity> imMessageHistoryEntities = imMessageHistoryMapper.selectPage(objectPage,and);
List<Long> messageKeys = imMessageHistoryEntities.getRecords().stream().map(x -> x.getMessageKey()).collect(Collectors.toList());
List<ImMessageBodyEntity> imMessageBodyEntities = imMessageBodyMapper.selectList(new LambdaQueryWrapper<ImMessageBodyEntity>().in(ImMessageBodyEntity::getMessageKey, messageKeys));
Map<Long, ImMessageBodyEntity> collect = imMessageBodyEntities.stream().collect(Collectors.toMap(ImMessageBodyEntity::getMessageKey, x -> x));
List<ImMessageHistoryVo> imMessageHistoryVos = BeanCopyUtils.copyList(imMessageHistoryEntities.getRecords(), ImMessageHistoryVo.class);
for (ImMessageHistoryVo imMessageHistoryVo : imMessageHistoryVos) {
ImMessageBodyEntity imMessageBodyEntity = collect.get(imMessageHistoryVo.getMessageKey());
imMessageHistoryVo.setMessageBody(imMessageBodyEntity.getMessageBody());
}
return imMessageHistoryVos;
}
/**
* 获取离线消息
* @param req
* @return
*/
public List<OfflineMessageContent> listOfflineMessage(SyncReq req) {
String key = req.getAppId() + ":" + Constants.RedisConstants.OfflineMessage + ":" + req.getOperater();
//获取最大的seq
Long maxSeq = 0L;
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
Set set = zSetOperations.reverseRangeWithScores(key, 0, 0);
if(!CollectionUtils.isEmpty(set)){
List list = new ArrayList(set);
DefaultTypedTuple o = (DefaultTypedTuple) list.get(0);
maxSeq = o.getScore().longValue();
}
List<OfflineMessageContent> respList = new ArrayList<>();
Set<ZSetOperations.TypedTuple> querySet = zSetOperations.rangeByScoreWithScores(key,
req.getLastSequence(), maxSeq, 0, -1);
for (ZSetOperations.TypedTuple<String> typedTuple : querySet) {
String value = typedTuple.getValue();
OfflineMessageContent offlineMessageContent = JSONObject.parseObject(value, OfflineMessageContent.class);
respList.add(offlineMessageContent);
}
//
// List<OfflineMessageContent> imMessageBodyIds = new ArrayList<>();
// Map<String, OfflineMessageContent> toIdMap = respList.stream().collect(Collectors.toMap(OfflineMessageContent::getToId, x -> x));
// Map<String, Optional<OfflineMessageContent>> toIdMap2 = respList.stream().
// collect(Collectors.groupingBy(x -> x.getFromId() + "-"+ x.getToId(),Collectors.maxBy(Comparator.comparingLong(OfflineMessageContent::getMessageKey))));
// for (OfflineMessageContent offlineMessageContent : respList) {
// OfflineMessageContent messageKey1 = toIdMap.get(offlineMessageContent.getToId());
// if (messageKey1 != null){
// imMessageBodyIds.add(messageKey1.getMessageKey() > offlineMessageContent.getMessageKey() ? messageKey1 : offlineMessageContent);
// }
// }
return respList;
}
/**
* 获取已读消息
* @param req
* @return
*/
public List<ImMessageHistoryEntity> listReadMessage(SyncReq req) {
List<ImMessageHistoryEntity> imMessageHistoryEntities = imMessageHistoryMapper.selectMessageByOwnId(req.getOperater());
List<Long> imMessageBodyIds = new ArrayList<>();
Map<String, Long> fromIdMap = imMessageHistoryEntities.stream()
.filter(x -> !StringUtils.equals(x.getFromId(),req.getOperater()))
.collect(Collectors.toMap(ImMessageHistoryEntity::getFromId, ImMessageHistoryEntity::getMessageKey));
Map<String, Long> toIdMap = imMessageHistoryEntities.stream()
.filter(x -> !StringUtils.equals(x.getToId(),req.getOperater()))
.collect(Collectors.toMap(ImMessageHistoryEntity::getToId, ImMessageHistoryEntity::getMessageKey));
fromIdMap.forEach((key,value) -> {
toIdMap.merge(key,value,(x,y) -> {
if (value > y) return value;
return y;
});
});
List<ImMessageBodyEntity> imMessageBodyEntities = imMessageBodyMapper.selectList(new LambdaQueryWrapper<ImMessageBodyEntity>().in(ImMessageBodyEntity::getMessageKey, imMessageBodyIds));
return null;
}
//修改历史消息的状态 //修改历史消息的状态
//修改离线消息的状态 //修改离线消息的状态
//ack给发送方 //ack给发送方

View File

@@ -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 性能优异)
* <p>
* 重点 cglib 不支持 拷贝到链式对象
* 例如: 源对象 拷贝到 目标(链式对象)
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BeanCopyUtils {
/**
* 单对象基于class创建拷贝
*
* @param source 数据来源实体
* @param desc 描述对象 转换后的对象
* @return desc
*/
public static <T, V> V copy(T source, Class<V> 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 <T, V> 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 <T, V> List<V> copyList(List<T> sourceList, Class<V> 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 <T> Map<String, Object> 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> T mapToBean(Map<String, Object> map, Class<T> 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> T mapToBean(Map<String, Object> 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 <T, V> Map<String, V> mapToMap(Map<String, T> map, Class<V> clazz) {
if (MapUtil.isEmpty(map)) {
return null;
}
if (ObjectUtil.isNull(clazz)) {
return null;
}
Map<String, V> copyMap = new LinkedHashMap<>(map.size());
map.forEach((k, v) -> copyMap.put(k, copy(v, clazz)));
return copyMap;
}
/**
* BeanCopier属性缓存<br>
* 缓存用于防止多次反射造成的性能问题
*
* @author Looly
* @since 5.4.1
*/
public enum BeanCopierCache {
/**
* BeanCopier属性缓存单例
*/
INSTANCE;
private final SimpleCache<String, BeanCopier> 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();
}
}
}

View File

@@ -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 <E> List<E> filter(Collection<E> collection, Predicate<E> 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 <E> String join(Collection<E> collection, Function<E, String> function) {
return join(collection, function, StringUtils.SEPARATOR);
}
/**
* 将collection拼接
*
* @param collection 需要转化的集合
* @param function 拼接方法
* @param delimiter 拼接符
* @return 拼接后的list
*/
public static <E> String join(Collection<E> collection, Function<E, String> 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 <E> List<E> sorted(Collection<E> collection, Comparator<E> comparing) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
}
/**
* 将collection转化为类型不变的map<br>
* <B>{@code Collection<V> ----> Map<K,V>}</B>
*
* @param collection 需要转化的集合
* @param key V类型转化为K类型的lambda方法
* @param <V> collection中的泛型
* @param <K> map中的key类型
* @return 转化后的map
*/
public static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> 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的泛型不同)<br>
* <B>{@code Collection<E> -----> Map<K,V> }</B>
*
* @param collection 需要转化的集合
* @param key E类型转化为K类型的lambda方法
* @param value E类型转化为V类型的lambda方法
* @param <E> collection中的泛型
* @param <K> map中的key类型
* @param <V> map中的value类型
* @return 转化后的map
*/
public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> 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<br>
* <B>{@code Collection<E> -------> Map<K,List<E>> } </B>
*
* @param collection 需要分类的集合
* @param key 分类的规则
* @param <E> collection中的泛型
* @param <K> map中的key类型
* @return 分类后的map
*/
public static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> 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<br>
* <B>{@code Collection<E> ---> Map<T,Map<U,List<E>>> } </B>
*
* @param collection 需要分类的集合
* @param key1 第一个分类的规则
* @param key2 第二个分类的规则
* @param <E> 集合元素类型
* @param <K> 第一个map中的key类型
* @param <U> 第二个map中的key类型
* @return 分类后的map
*/
public static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> 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<br>
* <B>{@code Collection<E> ---> Map<T,Map<U,E>> } </B>
*
* @param collection 需要分类的集合
* @param key1 第一个分类的规则
* @param key2 第二个分类的规则
* @param <T> 第一个map中的key类型
* @param <U> 第二个map中的key类型
* @param <E> collection中的泛型
* @return 分类后的map
*/
public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> 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集合但是两者的泛型不同<br>
* <B>{@code Collection<E> ------> List<T> } </B>
*
* @param collection 需要转化的集合
* @param function collection中的泛型转化为list泛型的lambda表达式
* @param <E> collection中的泛型
* @param <T> List中的泛型
* @return 转化后的list
*/
public static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
return collection
.stream()
.map(function)
.filter(Objects::nonNull)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList());
}
/**
* 将collection转化为Set集合但是两者的泛型不同<br>
* <B>{@code Collection<E> ------> Set<T> } </B>
*
* @param collection 需要转化的集合
* @param function collection中的泛型转化为set泛型的lambda表达式
* @param <E> collection中的泛型
* @param <T> Set中的泛型
* @return 转化后的Set
*/
public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> 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 <K> map中的key类型
* @param <X> 第一个 map的value类型
* @param <Y> 第二个 map的value类型
* @param <V> 最终map的value类型
* @return 合并后的map
*/
public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> 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<K> key = new HashSet<>();
key.addAll(map1.keySet());
key.addAll(map2.keySet());
Map<K, V> 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;
}
}

View File

@@ -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);
}
/**
* 格式化文本, {} 表示占位符<br>
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
* 例:<br>
* 通常使用format("this is {} for {}", "a", "b") -> this is a for b<br>
* 转义{} format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
* 转义\ format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
*
* @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<String> 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<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
List<String> 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<String> 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();
}
}