mirror of
https://github.com/zongzibinbin/MallChat.git
synced 2026-03-14 06:03:42 +08:00
feat:
1.链接跳转功能 2.归属地功能 fix: 1.修复回复跳转功能 2.优化翻页工具类 3,修复下线重置ip的bug
This commit is contained in:
@@ -24,6 +24,11 @@
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
</dependency>
|
||||
<!-- MyBatis-->
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
|
||||
@@ -55,6 +55,7 @@ public class MessageDao extends ServiceImpl<MessageMapper, Message> {
|
||||
public void updateGapCount(Long id, Integer gapCount) {
|
||||
lambdaUpdate()
|
||||
.eq(Message::getId, id)
|
||||
.set(Message::getGapCount, gapCount);
|
||||
.set(Message::getGapCount, gapCount)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.abin.mallchat.common.chat.domain.entity;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.IpInfo;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -8,6 +9,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.*;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@@ -21,7 +23,7 @@ import lombok.experimental.Accessors;
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName("message")
|
||||
@TableName(value = "message",autoResultMap = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -79,6 +81,11 @@ public class Message implements Serializable {
|
||||
@TableField("type")
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 最后上下线时间
|
||||
*/
|
||||
@TableField(value = "extra", typeHandler = JacksonTypeHandler.class)
|
||||
private MessageExtra extra;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.abin.mallchat.common.chat.domain.entity;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.IpDetail;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Description: 消息扩展属性
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-05-28
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class MessageExtra implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
//注册时的ip
|
||||
private Map<String, String> urlTitleMap;
|
||||
}
|
||||
@@ -32,22 +32,23 @@ public class CursorPageBaseResp<T> {
|
||||
@ApiModelProperty("数据列表")
|
||||
private List<T> list;
|
||||
|
||||
public static CursorPageBaseResp init(CursorPageBaseResp cursorPage, List list) {
|
||||
CursorPageBaseResp cursorPageBaseResp = new CursorPageBaseResp();
|
||||
public static <T> CursorPageBaseResp<T> init(CursorPageBaseResp cursorPage, List<T> list) {
|
||||
CursorPageBaseResp<T> cursorPageBaseResp = new CursorPageBaseResp<T>();
|
||||
cursorPageBaseResp.setIsLast(cursorPage.getIsLast());
|
||||
cursorPageBaseResp.setList(list);
|
||||
cursorPageBaseResp.setCursor(cursorPage.getCursor());
|
||||
return cursorPageBaseResp;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Boolean isEmpty() {
|
||||
return CollectionUtil.isEmpty(list);
|
||||
}
|
||||
|
||||
public static CursorPageBaseResp empty() {
|
||||
CursorPageBaseResp cursorPageBaseResp = new CursorPageBaseResp();
|
||||
public static <T> CursorPageBaseResp<T> empty() {
|
||||
CursorPageBaseResp<T> cursorPageBaseResp = new CursorPageBaseResp<T>();
|
||||
cursorPageBaseResp.setIsLast(true);
|
||||
cursorPageBaseResp.setList(new ArrayList());
|
||||
cursorPageBaseResp.setList(new ArrayList<T>());
|
||||
return cursorPageBaseResp;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.abin.mallchat.common.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 美团的CompletableFuture封装工具类,参考文章https://mp.weixin.qq.com/s/GQGidprakfticYnbVYVYGQ
|
||||
*/
|
||||
@Slf4j
|
||||
public class FutureUtils {
|
||||
/**
|
||||
* 设置CF状态为失败
|
||||
*/
|
||||
public static <T> CompletableFuture<T> failed(Throwable ex) {
|
||||
CompletableFuture<T> completableFuture = new CompletableFuture<>();
|
||||
completableFuture.completeExceptionally(ex);
|
||||
return completableFuture;
|
||||
}
|
||||
/**
|
||||
* 设置CF状态为成功
|
||||
*/
|
||||
public static <T> CompletableFuture<T> success(T result) {
|
||||
CompletableFuture<T> completableFuture = new CompletableFuture<>();
|
||||
completableFuture.complete(result);
|
||||
return completableFuture;
|
||||
}
|
||||
/**
|
||||
* 将List<CompletableFuture<T>> 转为 CompletableFuture<List<T>>
|
||||
*/
|
||||
public static <T> CompletableFuture<List<T>> sequence(Collection<CompletableFuture<T>> completableFutures) {
|
||||
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 将List<CompletableFuture<List<T>>> 转为 CompletableFuture<List<T>>
|
||||
* 多用于分页查询的场景
|
||||
*/
|
||||
public static <T> CompletableFuture<List<T>> sequenceList(Collection<CompletableFuture<List<T>>> completableFutures) {
|
||||
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream()
|
||||
.flatMap( listFuture -> listFuture.join().stream())
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
/*
|
||||
* 将List<CompletableFuture<Map<K, V>>> 转为 CompletableFuture<Map<K, V>>
|
||||
* @Param mergeFunction 自定义key冲突时的merge策略
|
||||
*/
|
||||
public static <K, V> CompletableFuture<Map<K, V>> sequenceMap(
|
||||
Collection<CompletableFuture<Map<K, V>>> completableFutures, BinaryOperator<V> mergeFunction) {
|
||||
return CompletableFuture
|
||||
.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream().map(CompletableFuture::join)
|
||||
.flatMap(map -> map.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, mergeFunction)));
|
||||
}
|
||||
/**
|
||||
* 将List<CompletableFuture<T>> 转为 CompletableFuture<List<T>>,并过滤调null值
|
||||
*/
|
||||
public static <T> CompletableFuture<List<T>> sequenceNonNull(Collection<CompletableFuture<T>> completableFutures) {
|
||||
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 将List<CompletableFuture<List<T>>> 转为 CompletableFuture<List<T>>,并过滤调null值
|
||||
* 多用于分页查询的场景
|
||||
*/
|
||||
public static <T> CompletableFuture<List<T>> sequenceListNonNull(Collection<CompletableFuture<List<T>>> completableFutures) {
|
||||
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream()
|
||||
.flatMap( listFuture -> listFuture.join().stream().filter(Objects::nonNull))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 将List<CompletableFuture<Map<K, V>>> 转为 CompletableFuture<Map<K, V>>
|
||||
* @Param filterFunction 自定义过滤策略
|
||||
*/
|
||||
public static <T> CompletableFuture<List<T>> sequence(Collection<CompletableFuture<T>> completableFutures,
|
||||
Predicate<? super T> filterFunction) {
|
||||
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.filter(filterFunction)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 将List<CompletableFuture<List<T>>> 转为 CompletableFuture<List<T>>
|
||||
* @Param filterFunction 自定义过滤策略
|
||||
*/
|
||||
public static <T> CompletableFuture<List<T>> sequenceList(Collection<CompletableFuture<List<T>>> completableFutures,
|
||||
Predicate<? super T> filterFunction) {
|
||||
return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream()
|
||||
.flatMap( listFuture -> listFuture.join().stream().filter(filterFunction))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 将CompletableFuture<Map<K,V>>的list转为 CompletableFuture<Map<K,V>>。 多个map合并为一个map。 如果key冲突,采用新的value覆盖。
|
||||
*/
|
||||
public static <K, V> CompletableFuture<Map<K, V>> sequenceMap(
|
||||
Collection<CompletableFuture<Map<K, V>>> completableFutures) {
|
||||
return CompletableFuture
|
||||
.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
|
||||
.thenApply(v -> completableFutures.stream().map(CompletableFuture::join)
|
||||
.flatMap(map -> map.entrySet().stream())
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b)));
|
||||
}}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.util.ReUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.abin.mallchat.common.common.utils.FutureUtils;
|
||||
import com.google.errorprone.annotations.Var;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Connection;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.springframework.data.util.Pair;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: urlTitle查询抽象类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-05-27
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractUrlTitleDiscover implements UrlTitleDiscover {
|
||||
//链接识别的正则
|
||||
private static final Pattern PATTERN = Pattern.compile("((http|https)://)?(www.)?([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?");
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, String> getContentTitleMap(String content) {
|
||||
if (StrUtil.isBlank(content)) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
List<String> matchList = ReUtil.findAll(PATTERN, content, 0);
|
||||
//并行请求
|
||||
List<CompletableFuture<Pair<String, String>>> futures = matchList.stream().map(match -> CompletableFuture.supplyAsync(() -> {
|
||||
String title = getUrlTitle(match);
|
||||
return Objects.nonNull(title) ? Pair.of(match, title) : null;
|
||||
})).collect(Collectors.toList());
|
||||
CompletableFuture<List<Pair<String, String>>> future = FutureUtils.sequenceNonNull(futures);
|
||||
//结果组装
|
||||
return future.join().stream().collect(Collectors.toMap(Pair::getFirst, Pair::getSecond, (a, b) -> a));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getUrlTitle(String url) {
|
||||
Document document = getUrlDocument(assemble(url));
|
||||
if (Objects.isNull(document)) {
|
||||
return null;
|
||||
}
|
||||
return getDocTitle(document);
|
||||
}
|
||||
|
||||
private String assemble(String url) {
|
||||
if (!StrUtil.startWith(url, "http")) {
|
||||
return "http://" + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
protected Document getUrlDocument(String matchUrl) {
|
||||
try {
|
||||
Connection connect = Jsoup.connect(matchUrl);
|
||||
connect.timeout(1000);
|
||||
return connect.get();
|
||||
} catch (Exception e) {
|
||||
log.error("find title error:url:{}", matchUrl, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
/**
|
||||
* Description: 通用的标题解析类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-05-27
|
||||
*/
|
||||
public class CommonUrlTitleDiscover extends AbstractUrlTitleDiscover {
|
||||
@Override
|
||||
public String getDocTitle(Document document) {
|
||||
return document.title();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Description: 具有优先级的title查询器
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-05-27
|
||||
*/
|
||||
public class PrioritizedUrlTitleDiscover extends AbstractUrlTitleDiscover {
|
||||
|
||||
private final List<UrlTitleDiscover> urlTitleDiscovers = new ArrayList<>(2);
|
||||
|
||||
public PrioritizedUrlTitleDiscover() {
|
||||
urlTitleDiscovers.add(new CommonUrlTitleDiscover());
|
||||
urlTitleDiscovers.add(new WxUrlTitleDiscover());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDocTitle(Document document) {
|
||||
for (UrlTitleDiscover urlTitleDiscover : urlTitleDiscovers) {
|
||||
String urlTitle = urlTitleDiscover.getDocTitle(document);
|
||||
if (StrUtil.isNotBlank(urlTitle)) {
|
||||
return urlTitle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.date.StopWatch;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.Signed;
|
||||
import java.util.Map;
|
||||
|
||||
public interface UrlTitleDiscover {
|
||||
|
||||
|
||||
@Nullable
|
||||
Map<String, String> getContentTitleMap(String content);
|
||||
|
||||
|
||||
@Nullable
|
||||
String getUrlTitle(String url);
|
||||
|
||||
@Nullable
|
||||
String getDocTitle(Document document);
|
||||
|
||||
public static void main(String[] args) {//用异步多任务查询并合并 974 //串行访问的速度1349 1291 1283 1559
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
String longStr = "这是一个很长的字符串再来 www.github.com,其中包含一个URL www.baidu.com,, 一个带有端口号的URL http://www.jd.com:80, 一个带有路径的URL http://mallchat.cn, 还有美团技术文章https://mp.weixin.qq.com/s/hwTf4bDck9_tlFpgVDeIKg ";
|
||||
PrioritizedUrlTitleDiscover discover =new PrioritizedUrlTitleDiscover();
|
||||
Map<String, String> contentTitleMap = discover.getContentTitleMap(longStr);
|
||||
System.out.println(contentTitleMap);
|
||||
//
|
||||
// Jsoup.connect("http:// www.github.com");
|
||||
stopWatch.stop();
|
||||
long cost = stopWatch.getTotalTimeMillis();
|
||||
System.out.println(cost);
|
||||
}//{http://mallchat.cn=MallChat, www.baidu.com=百度一下,你就知道, https://mp.weixin.qq.com/s/hwTf4bDck9_tlFpgVDeIKg=超大规模数据库集群保稳系列之二:数据库攻防演练建设实践, http://www.jd.com:80=京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物!}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
/**
|
||||
* Description: 针对微信公众号文章的标题获取类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-05-27
|
||||
*/
|
||||
public class WxUrlTitleDiscover extends AbstractUrlTitleDiscover {
|
||||
@Override
|
||||
public String getDocTitle(Document document) {
|
||||
return document.getElementsByAttributeValue("property", "og:title").attr("content");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.*;
|
||||
@@ -99,11 +100,10 @@ public class User implements Serializable {
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
public IpInfo getIpInfo() {
|
||||
public void refreshIp(String ip) {
|
||||
if (ipInfo == null) {
|
||||
ipInfo = new IpInfo();
|
||||
}
|
||||
return ipInfo;
|
||||
ipInfo.refreshIp(ip);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ public class IpServiceImpl implements IpService, DisposableBean {
|
||||
executor.execute(() -> {
|
||||
User user = userDao.getById(uid);
|
||||
IpInfo ipInfo = user.getIpInfo();
|
||||
if (Objects.isNull(ipInfo)) {
|
||||
return;
|
||||
}
|
||||
String ip = ipInfo.needRefreshIp();
|
||||
if (StrUtil.isBlank(ip)) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user