mirror of
https://github.com/zongzibinbin/MallChat.git
synced 2026-03-13 21:53:41 +08:00
Merge branch 'main' into chat_context
# Conflicts: # mallchat-common/pom.xml # mallchat-custom-server/pom.xml # mallchat-custom-server/src/main/java/com/abin/mallchat/custom/chatai/handler/GPTChatAIHandler.java
This commit is contained in:
1
MallChat
Submodule
1
MallChat
Submodule
Submodule MallChat added at c47b48760d
33
README.md
33
README.md
@@ -13,13 +13,22 @@
|
||||
<a href="https://github.com/zongzibinbin/MallChat"><img src="https://img.shields.io/badge/github-项目地址-yellow.svg?style=plasticr"></a>
|
||||
<a href="https://gitee.com/zhongzhibinbin/MallChat"><img src="https://img.shields.io/badge/码云-项目地址-orange.svg?style=plasticr"></a>
|
||||
<a href="https://github.com/Evansy/MallChatWeb"><img src="https://img.shields.io/badge/前端-项目地址-blueviolet.svg?style=plasticr"></a>
|
||||
|
||||
[](https://github.com/Evansy/MallChatWeb/actions/workflows/deploy.yml)
|
||||
<a href="https://github.com/zongzibinbin/MallChat/commits" target="_blank"><br>
|
||||
<a href="https://github.com/Evansy/MallChatWeb/actions/workflows/deploy.yml" target="_blank">
|
||||
<img alt="Commit" src="https://github.com/Evansy/MallChatWeb/actions/workflows/deploy.yml/badge.svg?branch=main">
|
||||
</a>
|
||||
<a href="https://github.com/zongzibinbin/MallChat/commits" target="_blank">
|
||||
<img alt="Commit" src="https://img.shields.io/github/commit-activity/m/zongzibinbin/MallChat"></a>
|
||||
<img alt="Commit" src="https://img.shields.io/github/commit-activity/m/zongzibinbin/MallChat"></a>
|
||||
<a href="https://github.com/zongzibinbin/MallChat/issues" target="_blank">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/zongzibinbin/MallChat"></a> [](https://github.com/zongzibinbin/MallChat/blob/master/LICENSE)
|
||||
[](https://github.com/zongzibinbin/MallChat/stargazers)
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/zongzibinbin/MallChat">
|
||||
</a>
|
||||
<a href="https://github.com/zongzibinbin/MallChat/blob/master/LICENSE" target="_blank">
|
||||
<img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache--2.0-blue.svg">
|
||||
</a>
|
||||
<a href="https://github.com/zongzibinbin/MallChat/stargazers" target="_blank">
|
||||
<img alt="License" src="https://img.shields.io/github/stars/zongzibinbin/MallChat.svg?style=social">
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
## 项目导航
|
||||
@@ -130,7 +139,7 @@
|
||||
<td><a href="https://github.com/Evansy/MallChatWeb/pull/17">虚拟列表</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">后端</td>
|
||||
<td rowspan="5">后端</td>
|
||||
<td rowspan="2">
|
||||
<a href="https://github.com/1045078399"><img src="https://avatars.githubusercontent.com/u/82020261?v=4" style=" width: 80px; height: 80px;"></a>
|
||||
</td>
|
||||
@@ -145,6 +154,18 @@
|
||||
</td>
|
||||
<td><a href="https://github.com/zongzibinbin/MallChat/pull/99">Ac自动机敏感词检测</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="1">
|
||||
<a href="https://github.com/linzhihan"><img src="https://avatars.githubusercontent.com/u/58815955?v=4" style=" width: 80px; height: 80px;"></a>
|
||||
</td>
|
||||
<td><a href="https://github.com/zongzibinbin/MallChat/pull/95">限流编程式</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="1">
|
||||
<a href="https://github.com/zbzbzzz"><img src="https://avatars.githubusercontent.com/u/42697182?v=4" style=" width: 80px; height: 80px;"></a>
|
||||
</td>
|
||||
<td><a href="https://github.com/zongzibinbin/MallChat/pull/82">握手认证</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
@@ -190,4 +190,16 @@ CREATE TABLE `sensitive_word` (
|
||||
`word` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '敏感词'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='敏感词库';
|
||||
INSERT INTO `sensitive_word` (`word`) VALUES ('TMD');
|
||||
INSERT INTO `sensitive_word` (`word`) VALUES ('tmd');
|
||||
INSERT INTO `sensitive_word` (`word`) VALUES ('tmd');
|
||||
|
||||
DROP TABLE IF EXISTS `user_emoji`;
|
||||
CREATE TABLE `user_emoji` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||
`uid` bigint(20) NOT NULL COMMENT '用户表ID',
|
||||
`expression_url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '表情地址',
|
||||
`delete_status` int(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-正常,1-删除)',
|
||||
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `IDX_USER_EMOJIS_UID` (`uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户表情包';
|
||||
10
docs/version/2023-07-09.sql
Normal file
10
docs/version/2023-07-09.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE `user_emoji` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||
`uid` bigint(20) NOT NULL COMMENT '用户表ID',
|
||||
`expression_url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '表情地址',
|
||||
`delete_status` int(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-正常,1-删除)',
|
||||
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `IDX_USER_EMOJIS_UID` (`uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户表情包';
|
||||
@@ -123,6 +123,16 @@
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>5.3.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-test</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.abin.mallchat.common.chat.domain.entity.msg;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Description: 表情图片消息入参
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-06-04
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class EmojisMsgDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("下载地址")
|
||||
@NotBlank
|
||||
private String url;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.abin.mallchat.common.chat.domain.entity.msg;
|
||||
|
||||
import com.abin.mallchat.common.common.utils.discover.domain.UrlInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -23,7 +24,7 @@ import java.util.Map;
|
||||
public class MessageExtra implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
//url跳转链接
|
||||
private Map<String, String> urlTitleMap;
|
||||
private Map<String, UrlInfo> urlContentMap;
|
||||
//消息撤回详情
|
||||
private MsgRecall recall;
|
||||
//艾特的uid
|
||||
@@ -36,4 +37,9 @@ public class MessageExtra implements Serializable {
|
||||
private SoundMsgDTO soundMsgDTO;
|
||||
//文件消息
|
||||
private VideoMsgDTO videoMsgDTO;
|
||||
|
||||
/**
|
||||
* 表情图片信息
|
||||
*/
|
||||
private EmojisMsgDTO emojisMsgDTO;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ public enum MessageTypeEnum {
|
||||
FILE(4, "文件"),
|
||||
SOUND(5, "语音"),
|
||||
VIDEO(6, "视频"),
|
||||
EMOJI(7, "表情"),
|
||||
;
|
||||
|
||||
private final Integer type;
|
||||
|
||||
@@ -2,9 +2,8 @@ package com.abin.mallchat.common.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.abin.mallchat.common.common.annotation.FrequencyControl;
|
||||
import com.abin.mallchat.common.common.exception.BusinessException;
|
||||
import com.abin.mallchat.common.common.exception.CommonErrorEnum;
|
||||
import com.abin.mallchat.common.common.utils.RedisUtils;
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
import com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlUtil;
|
||||
import com.abin.mallchat.common.common.utils.RequestHolder;
|
||||
import com.abin.mallchat.common.common.utils.SpElUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -15,7 +14,12 @@ import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlStrategyFactory.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
|
||||
|
||||
/**
|
||||
* Description: 频控实现
|
||||
@@ -48,25 +52,25 @@ public class FrequencyControlAspect {
|
||||
}
|
||||
keyMap.put(prefix + ":" + key, frequencyControl);
|
||||
}
|
||||
//批量获取redis统计的值
|
||||
ArrayList<String> keyList = new ArrayList<>(keyMap.keySet());
|
||||
List<Integer> countList = RedisUtils.mget(keyList, Integer.class);
|
||||
for (int i = 0; i < keyList.size(); i++) {
|
||||
String key = keyList.get(i);
|
||||
Integer count = countList.get(i);
|
||||
FrequencyControl frequencyControl = keyMap.get(key);
|
||||
if (Objects.nonNull(count) && count >= frequencyControl.count()) {//频率超过了
|
||||
log.warn("frequencyControl limit key:{},count:{}", key, count);
|
||||
throw new BusinessException(CommonErrorEnum.FREQUENCY_LIMIT);
|
||||
}
|
||||
}
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
//不管成功还是失败,都增加次数
|
||||
keyMap.forEach((k, v) -> {
|
||||
RedisUtils.inc(k, v.time(), v.unit());
|
||||
});
|
||||
}
|
||||
// 将注解的参数转换为编程式调用需要的参数
|
||||
List<FrequencyControlDTO> frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildFrequencyControlDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList());
|
||||
// 调用编程式注解
|
||||
return FrequencyControlUtil.executeWithFrequencyControlList(TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER, frequencyControlDTOS, joinPoint::proceed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将注解参数转换为编程式调用所需要的参数
|
||||
*
|
||||
* @param key 频率控制Key
|
||||
* @param frequencyControl 注解
|
||||
* @return 编程式调用所需要的参数-FrequencyControlDTO
|
||||
*/
|
||||
private FrequencyControlDTO buildFrequencyControlDTO(String key, FrequencyControl frequencyControl) {
|
||||
FrequencyControlDTO frequencyControlDTO = new FrequencyControlDTO();
|
||||
frequencyControlDTO.setCount(frequencyControl.count());
|
||||
frequencyControlDTO.setTime(frequencyControl.time());
|
||||
frequencyControlDTO.setUnit(frequencyControl.unit());
|
||||
frequencyControlDTO.setKey(key);
|
||||
return frequencyControlDTO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.abin.mallchat.common.common.config;
|
||||
|
||||
import com.abin.mallchat.common.common.utils.sensitiveWord.DFAFilter;
|
||||
import com.abin.mallchat.common.common.utils.sensitiveWord.SensitiveWordBs;
|
||||
import com.abin.mallchat.common.sensitive.MyWordDeny;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class SensitiveWordConfig {
|
||||
|
||||
@Autowired
|
||||
private MyWordDeny myWordDeny;
|
||||
|
||||
/**
|
||||
* 初始化引导类
|
||||
*
|
||||
* @return 初始化引导类
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Bean
|
||||
public SensitiveWordBs sensitiveWordBs() {
|
||||
return SensitiveWordBs.newInstance()
|
||||
.filterStrategy(DFAFilter.getInstance())
|
||||
.sensitiveWord(myWordDeny)
|
||||
.init();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.abin.mallchat.common.common.domain.dto;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Data
|
||||
@ToString
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
/** 限流策略定义
|
||||
* @author linzhihan
|
||||
* @date 2023/07/03
|
||||
*
|
||||
*/
|
||||
public class FrequencyControlDTO {
|
||||
/**
|
||||
* 代表频控的Key 如果target为Key的话 这里要传值用于构建redis的Key target为Ip或者UID的话会从上下文取值 Key字段无需传值
|
||||
*/
|
||||
private String key;
|
||||
/**
|
||||
* 频控时间范围,默认单位秒
|
||||
*
|
||||
* @return 时间范围
|
||||
*/
|
||||
private Integer time;
|
||||
|
||||
/**
|
||||
* 频控时间单位,默认秒
|
||||
*
|
||||
* @return 单位
|
||||
*/
|
||||
private TimeUnit unit;
|
||||
|
||||
/**
|
||||
* 单位时间内最大访问次数
|
||||
*
|
||||
* @return 次数
|
||||
*/
|
||||
private Integer count;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.abin.mallchat.common.common.domain.vo.request;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* @author zhongzb create on 2021/05/31
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IdReqVO {
|
||||
@ApiModelProperty("id")
|
||||
@NotNull
|
||||
private long id;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import lombok.Getter;
|
||||
public enum CommonErrorEnum implements ErrorEnum {
|
||||
|
||||
SYSTEM_ERROR(-1, "系统出小差了,请稍后再试哦~~"),
|
||||
PARAM_VALID(-2, "参数校验失败"),
|
||||
PARAM_VALID(-2, "参数校验失败{0}"),
|
||||
FREQUENCY_LIMIT(-3, "请求太频繁了,请稍后再试哦~~"),
|
||||
LOCK_LIMIT(-4, "请求太频繁了,请稍后再试哦~~"),
|
||||
;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.abin.mallchat.common.common.exception;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 自定义限流异常
|
||||
*
|
||||
* @author linzhihan
|
||||
* @date 2023/07/034
|
||||
*/
|
||||
@Data
|
||||
public class FrequencyControlException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
protected Integer errorCode;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
protected String errorMsg;
|
||||
|
||||
public FrequencyControlException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public FrequencyControlException(String errorMsg) {
|
||||
super(errorMsg);
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public FrequencyControlException(ErrorEnum error) {
|
||||
super(error.getErrorMsg());
|
||||
this.errorCode = error.getErrorCode();
|
||||
this.errorMsg = error.getErrorMsg();
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class GlobalExceptionHandler {
|
||||
*/
|
||||
@ExceptionHandler(value = NullPointerException.class)
|
||||
public ApiResult exceptionHandler(NullPointerException e) {
|
||||
log.error("null point exception!The reason is:{}", e.getMessage(), e);
|
||||
log.error("null point exception!The reason is: ", e);
|
||||
return ApiResult.fail(CommonErrorEnum.SYSTEM_ERROR);
|
||||
}
|
||||
|
||||
@@ -73,4 +73,12 @@ public class GlobalExceptionHandler {
|
||||
return ApiResult.fail(-1, String.format("不支持'%s'请求", e.getMethod()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流异常
|
||||
*/
|
||||
@ExceptionHandler(value = FrequencyControlException.class)
|
||||
public ApiResult frequencyControlExceptionHandler(FrequencyControlException e) {
|
||||
log.info("frequencyControl exception!The reason is:{}", e.getMessage(), e);
|
||||
return ApiResult.fail(e.getErrorCode(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ public class GlobalUncaughtExceptionHandler implements Thread.UncaughtException
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
log.error("Exception in thread {} ", t.getName(), e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.abin.mallchat.common.common.service.frequencycontrol;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
import com.abin.mallchat.common.common.exception.CommonErrorEnum;
|
||||
import com.abin.mallchat.common.common.exception.FrequencyControlException;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 抽象类频控服务 其他类如果要实现限流服务 直接注入使用通用限流类
|
||||
* 后期会通过继承此类实现令牌桶等算法
|
||||
*
|
||||
* @author linzhihan
|
||||
* @date 2023/07/03
|
||||
* @see TotalCountWithInFixTimeFrequencyController 通用限流类
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractFrequencyControlService<K extends FrequencyControlDTO> {
|
||||
|
||||
@PostConstruct
|
||||
protected void registerMyselfToFactory() {
|
||||
FrequencyControlStrategyFactory.registerFrequencyController(getStrategyName(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
|
||||
* @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑
|
||||
* @return 业务方法执行的返回值
|
||||
* @throws Throwable
|
||||
*/
|
||||
private <T> T executeWithFrequencyControlMap(Map<String, K> frequencyControlMap, SupplierThrowWithoutParam<T> supplier) throws Throwable {
|
||||
if (reachRateLimit(frequencyControlMap)) {
|
||||
throw new FrequencyControlException(CommonErrorEnum.FREQUENCY_LIMIT);
|
||||
}
|
||||
try {
|
||||
return supplier.get();
|
||||
} finally {
|
||||
//不管成功还是失败,都增加次数
|
||||
addFrequencyControlStatisticsCount(frequencyControlMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 多限流策略的编程式调用方法 无参的调用方法
|
||||
*
|
||||
* @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序
|
||||
* @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑
|
||||
* @return 业务方法执行的返回值
|
||||
* @throws Throwable 被限流或者限流策略定义错误
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T executeWithFrequencyControlList(List<K> frequencyControlList, SupplierThrowWithoutParam<T> supplier) throws Throwable {
|
||||
boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
|
||||
AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值");
|
||||
Map<String, FrequencyControlDTO> frequencyControlDTOMap = frequencyControlList.stream().collect(Collectors.groupingBy(FrequencyControlDTO::getKey, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
|
||||
return executeWithFrequencyControlMap((Map<String, K>) frequencyControlDTOMap, supplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单限流策略的调用方法-编程式调用
|
||||
*
|
||||
* @param frequencyControl 单个频控对象
|
||||
* @param supplier 服务提供着
|
||||
* @return 业务方法执行结果
|
||||
* @throws Throwable
|
||||
*/
|
||||
public <T> T executeWithFrequencyControl(K frequencyControl, SupplierThrowWithoutParam<T> supplier) throws Throwable {
|
||||
return executeWithFrequencyControlList(Collections.singletonList(frequencyControl), supplier);
|
||||
}
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SupplierThrowWithoutParam<T> {
|
||||
|
||||
/**
|
||||
* Gets a result.
|
||||
*
|
||||
* @return a result
|
||||
*/
|
||||
T get() throws Throwable;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Executor {
|
||||
|
||||
/**
|
||||
* Gets a result.
|
||||
*
|
||||
* @return a result
|
||||
*/
|
||||
void execute() throws Throwable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
|
||||
*
|
||||
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
|
||||
* @return true-方法被限流 false-方法没有被限流
|
||||
*/
|
||||
protected abstract boolean reachRateLimit(Map<String, K> frequencyControlMap);
|
||||
|
||||
/**
|
||||
* 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑
|
||||
*
|
||||
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
|
||||
*/
|
||||
protected abstract void addFrequencyControlStatisticsCount(Map<String, K> frequencyControlMap);
|
||||
|
||||
/**
|
||||
* 获取策略名称
|
||||
*
|
||||
* @return 策略名称
|
||||
*/
|
||||
protected abstract String getStrategyName();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.abin.mallchat.common.common.service.frequencycontrol;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 限流策略工厂
|
||||
*
|
||||
* @author linzhihan
|
||||
* @date 2023/07/03
|
||||
*/
|
||||
public class FrequencyControlStrategyFactory {
|
||||
/**
|
||||
* 指定时间内总次数限流
|
||||
*/
|
||||
public static final String TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER = "TotalCountWithInFixTime";
|
||||
/**
|
||||
* 限流策略集合
|
||||
*/
|
||||
static Map<String, AbstractFrequencyControlService<?>> frequencyControlServiceStrategyMap = new ConcurrentHashMap<>(8);
|
||||
|
||||
/**
|
||||
* 将策略类放入工厂
|
||||
*
|
||||
* @param strategyName 策略名称
|
||||
* @param abstractFrequencyControlService 策略类
|
||||
*/
|
||||
public static <K extends FrequencyControlDTO> void registerFrequencyController(String strategyName, AbstractFrequencyControlService<K> abstractFrequencyControlService) {
|
||||
frequencyControlServiceStrategyMap.put(strategyName, abstractFrequencyControlService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取策略类
|
||||
*
|
||||
* @param strategyName 策略名称
|
||||
* @return 对应的限流策略类
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <K extends FrequencyControlDTO> AbstractFrequencyControlService<K> getFrequencyControllerByName(String strategyName) {
|
||||
return (AbstractFrequencyControlService<K>) frequencyControlServiceStrategyMap.get(strategyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造器私有
|
||||
*/
|
||||
private FrequencyControlStrategyFactory() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.abin.mallchat.common.common.service.frequencycontrol;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 限流工具类 提供编程式的限流调用方法
|
||||
*
|
||||
* @author linzhihan
|
||||
* @date 2023/07/03
|
||||
*/
|
||||
public class FrequencyControlUtil {
|
||||
|
||||
/**
|
||||
* 单限流策略的调用方法-编程式调用
|
||||
*
|
||||
* @param strategyName 策略名称
|
||||
* @param frequencyControl 单个频控对象
|
||||
* @param supplier 服务提供着
|
||||
* @return 业务方法执行结果
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static <T, K extends FrequencyControlDTO> T executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.SupplierThrowWithoutParam<T> supplier) throws Throwable {
|
||||
AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
|
||||
return frequencyController.executeWithFrequencyControl(frequencyControl, supplier);
|
||||
}
|
||||
|
||||
public static <K extends FrequencyControlDTO> void executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.Executor executor) throws Throwable {
|
||||
AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
|
||||
frequencyController.executeWithFrequencyControl(frequencyControl, () -> {
|
||||
executor.execute();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 多限流策略的编程式调用方法调用方法
|
||||
*
|
||||
* @param strategyName 策略名称
|
||||
* @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序
|
||||
* @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑
|
||||
* @return 业务方法执行的返回值
|
||||
* @throws Throwable 被限流或者限流策略定义错误
|
||||
*/
|
||||
public static <T, K extends FrequencyControlDTO> T executeWithFrequencyControlList(String strategyName, List<K> frequencyControlList, AbstractFrequencyControlService.SupplierThrowWithoutParam<T> supplier) throws Throwable {
|
||||
boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
|
||||
AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值");
|
||||
AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
|
||||
return frequencyController.executeWithFrequencyControlList(frequencyControlList, supplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造器私有
|
||||
*/
|
||||
private FrequencyControlUtil() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.abin.mallchat.common.common.service.frequencycontrol;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
import com.abin.mallchat.common.common.utils.RedisUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlStrategyFactory.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
|
||||
|
||||
/**
|
||||
* 抽象类频控服务 -使用redis实现 固定时间内不超过固定次数的限流类
|
||||
*
|
||||
* @author linzhihan
|
||||
* @date 2023/07/03
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TotalCountWithInFixTimeFrequencyController extends AbstractFrequencyControlService<FrequencyControlDTO> {
|
||||
|
||||
|
||||
/**
|
||||
* 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
|
||||
*
|
||||
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
|
||||
* @return true-方法被限流 false-方法没有被限流
|
||||
*/
|
||||
@Override
|
||||
protected boolean reachRateLimit(Map<String, FrequencyControlDTO> frequencyControlMap) {
|
||||
//批量获取redis统计的值
|
||||
List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
|
||||
List<Integer> countList = RedisUtils.mget(frequencyKeys, Integer.class);
|
||||
for (int i = 0; i < frequencyKeys.size(); i++) {
|
||||
String key = frequencyKeys.get(i);
|
||||
Integer count = countList.get(i);
|
||||
int frequencyControlCount = frequencyControlMap.get(key).getCount();
|
||||
if (Objects.nonNull(count) && count >= frequencyControlCount) {
|
||||
//频率超过了
|
||||
log.warn("frequencyControl limit key:{},count:{}", key, count);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑
|
||||
*
|
||||
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
|
||||
*/
|
||||
@Override
|
||||
protected void addFrequencyControlStatisticsCount(Map<String, FrequencyControlDTO> frequencyControlMap) {
|
||||
frequencyControlMap.forEach((k, v) -> RedisUtils.inc(k, v.getTime(), v.getUnit()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStrategyName() {
|
||||
return TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public class JwtUtils {
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
return jwt.getClaims();
|
||||
} catch (Exception e) {
|
||||
log.info("decode error,token:{}", token, e);
|
||||
log.error("decode error,token:{}", token, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ 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.abin.mallchat.common.common.utils.discover.domain.UrlInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -20,46 +20,55 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Description: urlTitle查询抽象类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-05-27
|
||||
* @author zhaoqichao
|
||||
* @date 2023/7/3 16:38
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractUrlTitleDiscover implements UrlTitleDiscover {
|
||||
public abstract class AbstractUrlDiscover implements UrlDiscover {
|
||||
//链接识别的正则
|
||||
private static final Pattern PATTERN = Pattern.compile("((http|https)://)?(www.)?([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?");
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, String> getContentTitleMap(String content) {
|
||||
public Map<String, UrlInfo> getUrlContentMap(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 StringUtils.isNotEmpty(title) ? Pair.of(match, title) : null;
|
||||
List<CompletableFuture<Pair<String, UrlInfo>>> futures = matchList.stream().map(match -> CompletableFuture.supplyAsync(() -> {
|
||||
UrlInfo urlInfo = getContent(match);
|
||||
return Objects.isNull(urlInfo) ? null : Pair.of(match, urlInfo);
|
||||
})).collect(Collectors.toList());
|
||||
CompletableFuture<List<Pair<String, String>>> future = FutureUtils.sequenceNonNull(futures);
|
||||
CompletableFuture<List<Pair<String, UrlInfo>>> 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) {
|
||||
public UrlInfo getContent(String url) {
|
||||
Document document = getUrlDocument(assemble(url));
|
||||
if (Objects.isNull(document)) {
|
||||
return null;
|
||||
}
|
||||
return getDocTitle(document);
|
||||
|
||||
return UrlInfo.builder()
|
||||
.title(getTitle(document))
|
||||
.description(getDescription(document))
|
||||
.image(getImage(assemble(url),document)).build();
|
||||
}
|
||||
|
||||
|
||||
private String assemble(String url) {
|
||||
|
||||
if (!StrUtil.startWith(url, "http")) {
|
||||
return "http://" + url;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -69,8 +78,9 @@ public abstract class AbstractUrlTitleDiscover implements UrlTitleDiscover {
|
||||
connect.timeout(2000);
|
||||
return connect.get();
|
||||
} catch (Exception e) {
|
||||
log.error("find title error:url:{}", matchUrl, e);
|
||||
log.error("find error:url:{}", matchUrl, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.jsonwebtoken.lang.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
/**
|
||||
* @author zhaoqichao
|
||||
* @date 2023/7/3 16:54
|
||||
*/
|
||||
public class CommonUrlDiscover extends AbstractUrlDiscover {
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTitle(Document document) {
|
||||
return document.title();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getDescription(Document document) {
|
||||
String description = document.head().select("meta[name=description]").attr("content");
|
||||
String keywords = document.head().select("meta[name=keywords]").attr("content");
|
||||
String content = StrUtil.isNotBlank(description) ? description : keywords;
|
||||
//只保留一句话的描述
|
||||
return StrUtil.isNotBlank(content) ? content.substring(0, content.indexOf("。")) : content;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getImage(String url, Document document) {
|
||||
String image = document.select("link[type=image/x-icon]").attr("href");
|
||||
//如果没有去匹配含有icon属性的logo
|
||||
String href = StrUtil.isEmpty(image) ? document.select("link[rel$=icon]").attr("href") : image;
|
||||
//如果icon中已经包含了url部分域名
|
||||
if (StrUtil.isNotBlank(StrUtil.removeAny(StrUtil.removeAny(href, "/"), "favicon.ico")) &&
|
||||
StrUtil.containsAny(StrUtil.removePrefix(url, "http://"), StrUtil.removeAny(StrUtil.removeAny(href, "/"), "favicon.ico"))) {
|
||||
return "http://" + StrUtil.removePrefix(href, "/");
|
||||
}
|
||||
//如果url已经包含了logo
|
||||
if (StrUtil.containsAny(url, "favicon")) {
|
||||
return url;
|
||||
}
|
||||
//如果logo中有url
|
||||
if (StrUtil.containsAny(href, "http") || StrUtil.containsAny(href, "https")) {
|
||||
return href;
|
||||
}
|
||||
return StrUtil.format("{}/{}", url, StrUtil.removePrefix(href, "/"));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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 CommonUrlTitleDiscover extends AbstractUrlTitleDiscover {
|
||||
@Override
|
||||
public String getDocTitle(Document document) {
|
||||
return document.title();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
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 PrioritizedUrlDiscover extends AbstractUrlDiscover {
|
||||
|
||||
private final List<UrlDiscover> urlDiscovers = new ArrayList<>(2);
|
||||
|
||||
public PrioritizedUrlDiscover() {
|
||||
urlDiscovers.add(new WxUrlDiscover());
|
||||
urlDiscovers.add(new CommonUrlDiscover());
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTitle(Document document) {
|
||||
for (UrlDiscover urlDiscover : urlDiscovers) {
|
||||
String urlTitle = urlDiscover.getTitle(document);
|
||||
if (StrUtil.isNotBlank(urlTitle)) {
|
||||
return urlTitle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getDescription(Document document) {
|
||||
for (UrlDiscover urlDiscover : urlDiscovers) {
|
||||
String urlDescription = urlDiscover.getDescription(document);
|
||||
if (StrUtil.isNotBlank(urlDescription)) {
|
||||
return urlDescription;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getImage(String url, Document document) {
|
||||
for (UrlDiscover urlDiscover : urlDiscovers) {
|
||||
String urlImage = urlDiscover.getImage(url,document);
|
||||
if (StrUtil.isNotBlank(urlImage)) {
|
||||
return urlImage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
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,46 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.date.StopWatch;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.abin.mallchat.common.common.utils.discover.domain.UrlInfo;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author zhaoqichao
|
||||
* @date 2023/7/3 16:34
|
||||
*/
|
||||
public interface UrlDiscover {
|
||||
|
||||
|
||||
@Nullable
|
||||
Map<String,UrlInfo> getUrlContentMap(String content);
|
||||
|
||||
@Nullable
|
||||
UrlInfo getContent(String url);
|
||||
|
||||
@Nullable
|
||||
String getTitle(Document document);
|
||||
|
||||
@Nullable
|
||||
String getDescription(Document document);
|
||||
|
||||
@Nullable
|
||||
String getImage(String url, Document document);
|
||||
|
||||
public static void main(String[] args) {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
String longStr = "其中包含一个URL www.baidu.com,一个带有端口号的URL http://www.jd.com:80, 一个带有路径的URL http://mallchat.cn, 还有美团技术文章https://mp.weixin.qq.com/s/hwTf4bDck9_tlFpgVDeIKg ";
|
||||
// String longStr = "一个带有端口号的URL http://www.jd.com:80,";
|
||||
// String longStr = "一个带有路径的URL http://mallchat.cn";
|
||||
PrioritizedUrlDiscover discover = new PrioritizedUrlDiscover();
|
||||
final Map<String, UrlInfo> map = discover.getUrlContentMap(longStr);
|
||||
System.out.println(map);
|
||||
stopWatch.stop();
|
||||
long cost = stopWatch.getTotalTimeMillis();
|
||||
System.out.println(cost);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import cn.hutool.core.date.StopWatch;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
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,30 @@
|
||||
package com.abin.mallchat.common.common.utils.discover;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
/**
|
||||
* Description: 针对微信公众号文章的标题获取类
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-05-27
|
||||
*/
|
||||
public class WxUrlDiscover extends AbstractUrlDiscover {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTitle(Document document) {
|
||||
return document.getElementsByAttributeValue("property", "og:title").attr("content");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getDescription(Document document) {
|
||||
return document.getElementsByAttributeValue("property", "og:description").attr("content");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getImage(String url, Document document) {
|
||||
return document.getElementsByAttributeValue("property", "og:image").attr("content");
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.abin.mallchat.common.common.utils.discover.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @author zhaoqichao
|
||||
* @date 2023/7/3 16:12
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UrlInfo {
|
||||
/**
|
||||
* 标题
|
||||
**/
|
||||
String title;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
**/
|
||||
String description;
|
||||
|
||||
/**
|
||||
* 网站LOGO
|
||||
**/
|
||||
String image;
|
||||
|
||||
}
|
||||
@@ -136,8 +136,8 @@ public class MinIOTemplate {
|
||||
String uid = Optional.ofNullable(req.getUid()).map(String::valueOf).orElse("000000");
|
||||
cn.hutool.core.lang.UUID uuid = cn.hutool.core.lang.UUID.fastUUID();
|
||||
String suffix = FileNameUtil.getSuffix(req.getFileName());
|
||||
String year = DateUtil.format(new Date(), DatePattern.NORM_YEAR_PATTERN);
|
||||
return req.getFilePath() + StrUtil.SLASH + year + StrUtil.SLASH + uid + StrUtil.SLASH + uuid + StrUtil.DOT + suffix;
|
||||
String yearAndMonth = DateUtil.format(new Date(), DatePattern.NORM_MONTH_PATTERN);
|
||||
return req.getFilePath() + StrUtil.SLASH + yearAndMonth + StrUtil.SLASH + uid + StrUtil.SLASH + uuid + StrUtil.DOT + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.abin.mallchat.common.common.utils;
|
||||
package com.abin.mallchat.common.common.utils.sensitiveWord;
|
||||
|
||||
import com.abin.mallchat.common.common.algorithm.ac.ACTrie;
|
||||
import com.abin.mallchat.common.common.algorithm.ac.MatchResult;
|
||||
@@ -15,7 +15,7 @@ import java.util.Objects;
|
||||
*
|
||||
* Created by berg on 2023/6/18.
|
||||
*/
|
||||
public class SensitiveWordUtils0 {
|
||||
public class ACFilter implements SensitiveWordFilter {
|
||||
|
||||
private final static char mask_char = '*'; // 替代字符
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SensitiveWordUtils0 {
|
||||
* @param text 文本
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean hasSensitiveWord(String text) {
|
||||
public boolean hasSensitiveWord(String text) {
|
||||
if (StringUtils.isBlank(text)) return false;
|
||||
return !Objects.equals(filter(text), text);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class SensitiveWordUtils0 {
|
||||
* @param text 待替换文本
|
||||
* @return 替换后的文本
|
||||
*/
|
||||
public static String filter(String text) {
|
||||
public String filter(String text) {
|
||||
if (StringUtils.isBlank(text)) return text;
|
||||
List<MatchResult> matchResults = ac_trie.matches(text);
|
||||
StringBuffer result = new StringBuffer(text);
|
||||
@@ -62,7 +62,7 @@ public class SensitiveWordUtils0 {
|
||||
*
|
||||
* @param words 敏感词数组
|
||||
*/
|
||||
public static void loadWord(List<String> words) {
|
||||
public void loadWord(List<String> words) {
|
||||
if (words == null) return;
|
||||
ac_trie = new ACTrie(words);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.abin.mallchat.common.common.utils;
|
||||
package com.abin.mallchat.common.common.utils.sensitiveWord;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -18,7 +18,10 @@ import java.util.*;
|
||||
* @author zhaoyuhang
|
||||
* @date 2023/06/19
|
||||
*/
|
||||
public final class SensitiveWordUtils {
|
||||
public final class DFAFilter implements SensitiveWordFilter {
|
||||
|
||||
private DFAFilter() {
|
||||
}
|
||||
private static Word root = new Word(' '); // 敏感词字典的根节点
|
||||
private final static char replace = '*'; // 替代字符
|
||||
private final static String skipChars = " !*-+_=,,.@;:;:。、??()()【】[]《》<>“”\"‘’"; // 遇到这些字符就会跳过
|
||||
@@ -30,6 +33,10 @@ public final class SensitiveWordUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static DFAFilter getInstance() {
|
||||
return new DFAFilter();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断文本中是否存在敏感词
|
||||
@@ -37,7 +44,7 @@ public final class SensitiveWordUtils {
|
||||
* @param text 文本
|
||||
* @return true: 存在敏感词, false: 不存在敏感词
|
||||
*/
|
||||
public static boolean hasSensitiveWord(String text) {
|
||||
public boolean hasSensitiveWord(String text) {
|
||||
if (StringUtils.isBlank(text)) return false;
|
||||
return !Objects.equals(filter(text), text);
|
||||
}
|
||||
@@ -48,7 +55,7 @@ public final class SensitiveWordUtils {
|
||||
* @param text 待替换文本
|
||||
* @return 替换后的文本
|
||||
*/
|
||||
public static String filter(String text) {
|
||||
public String filter(String text) {
|
||||
StringBuilder result = new StringBuilder(text);
|
||||
int index = 0;
|
||||
while (index < result.length()) {
|
||||
@@ -93,7 +100,7 @@ public final class SensitiveWordUtils {
|
||||
*
|
||||
* @param words 敏感词数组
|
||||
*/
|
||||
public static void loadWord(List<String> words) {
|
||||
public void loadWord(List<String> words) {
|
||||
if (!CollectionUtils.isEmpty(words)) {
|
||||
Word newRoot = new Word(' ');
|
||||
words.forEach(word -> loadWord(word, newRoot));
|
||||
@@ -106,7 +113,7 @@ public final class SensitiveWordUtils {
|
||||
*
|
||||
* @param word 词
|
||||
*/
|
||||
public static void loadWord(String word, Word root) {
|
||||
public void loadWord(String word, Word root) {
|
||||
if (StringUtils.isBlank(word)) {
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +143,7 @@ public final class SensitiveWordUtils {
|
||||
*
|
||||
* @param path 文本文件的绝对路径
|
||||
*/
|
||||
public static void loadWordFromFile(String path) {
|
||||
public void loadWordFromFile(String path) {
|
||||
try (InputStream inputStream = Files.newInputStream(Paths.get(path))) {
|
||||
loadWord(inputStream);
|
||||
} catch (IOException e) {
|
||||
@@ -150,7 +157,7 @@ public final class SensitiveWordUtils {
|
||||
* @param inputStream 文本文件输入流
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
public static void loadWord(InputStream inputStream) throws IOException {
|
||||
public void loadWord(InputStream inputStream) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
@@ -167,7 +174,7 @@ public final class SensitiveWordUtils {
|
||||
* @param c 待检测字符
|
||||
* @return true: 需要跳过, false: 不需要跳过
|
||||
*/
|
||||
private static boolean skip(char c) {
|
||||
private boolean skip(char c) {
|
||||
return skipSet.contains(c);
|
||||
}
|
||||
|
||||
@@ -186,17 +193,7 @@ public final class SensitiveWordUtils {
|
||||
|
||||
public Word(char c) {
|
||||
this.c = c;
|
||||
this.end = false;
|
||||
this.next = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String text = "白日,梦";
|
||||
String filter = filter(text);
|
||||
System.out.println(filter);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.abin.mallchat.common.common.utils.sensitiveWord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 敏感词
|
||||
*
|
||||
* @author zhaoyuhang
|
||||
* @date 2023/07/09
|
||||
*/
|
||||
public interface IWordDeny {
|
||||
/**
|
||||
* 获取结果
|
||||
* @return 结果
|
||||
* @since 0.0.13
|
||||
*/
|
||||
List<String> deny();
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.abin.mallchat.common.common.utils.sensitiveWord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 敏感词引导类
|
||||
*
|
||||
* @author zhaoyuhang
|
||||
* @date 2023/07/08
|
||||
*/
|
||||
public class SensitiveWordBs {
|
||||
|
||||
/**
|
||||
* 私有化构造器
|
||||
*/
|
||||
private SensitiveWordBs() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏策略
|
||||
*/
|
||||
private SensitiveWordFilter sensitiveWordFilter = DFAFilter.getInstance();
|
||||
|
||||
/**
|
||||
* 敏感词列表
|
||||
*/
|
||||
private IWordDeny wordDeny;
|
||||
|
||||
public static SensitiveWordBs newInstance() {
|
||||
return new SensitiveWordBs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*
|
||||
* 1. 根据配置,初始化对应的 map。比较消耗性能。
|
||||
* @since 0.0.13
|
||||
* @return this
|
||||
*/
|
||||
public SensitiveWordBs init() {
|
||||
|
||||
List<String> words = wordDeny.deny();
|
||||
loadWord(words);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤策略
|
||||
*
|
||||
* @param filter 过滤器
|
||||
* @return 结果
|
||||
* @since 0.7.0
|
||||
*/
|
||||
public SensitiveWordBs filterStrategy(SensitiveWordFilter filter) {
|
||||
if (filter == null) {
|
||||
throw new IllegalArgumentException("filter can not be null");
|
||||
}
|
||||
this.sensitiveWordFilter = filter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SensitiveWordBs sensitiveWord(IWordDeny wordDeny) {
|
||||
if (wordDeny == null) {
|
||||
throw new IllegalArgumentException("wordDeny can not be null");
|
||||
}
|
||||
this.wordDeny = wordDeny;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 有敏感词
|
||||
*
|
||||
* @param text 文本
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean hasSensitiveWord(String text) {
|
||||
return sensitiveWordFilter.hasSensitiveWord(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤
|
||||
*
|
||||
* @param text 文本
|
||||
* @return {@link String}
|
||||
*/
|
||||
public String filter(String text) {
|
||||
return sensitiveWordFilter.filter(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载敏感词列表
|
||||
*
|
||||
* @param words 敏感词数组
|
||||
*/
|
||||
private void loadWord(List<String> words) {
|
||||
sensitiveWordFilter.loadWord(words);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.abin.mallchat.common.common.utils.sensitiveWord;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 敏感词过滤
|
||||
*
|
||||
* @author zhaoyuhang
|
||||
* @date 2023/07/08
|
||||
*/
|
||||
public interface SensitiveWordFilter {
|
||||
/**
|
||||
* 有敏感词
|
||||
*
|
||||
* @param text 文本
|
||||
* @return boolean
|
||||
*/
|
||||
boolean hasSensitiveWord(String text);
|
||||
|
||||
/**
|
||||
* 过滤
|
||||
*
|
||||
* @param text 文本
|
||||
* @return {@link String}
|
||||
*/
|
||||
String filter(String text);
|
||||
|
||||
/**
|
||||
* 加载敏感词列表
|
||||
*
|
||||
* @param words 敏感词数组
|
||||
*/
|
||||
void loadWord(List<String> words);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.abin.mallchat.common.sensitive;
|
||||
|
||||
import com.abin.mallchat.common.common.utils.sensitiveWord.IWordDeny;
|
||||
import com.abin.mallchat.common.sensitive.dao.SensitiveWordDao;
|
||||
import com.abin.mallchat.common.sensitive.domain.SensitiveWord;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class MyWordDeny implements IWordDeny {
|
||||
@Autowired
|
||||
private SensitiveWordDao sensitiveWordDao;
|
||||
|
||||
@Override
|
||||
public List<String> deny() {
|
||||
return sensitiveWordDao.list()
|
||||
.stream()
|
||||
.map(SensitiveWord::getWord)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.abin.mallchat.common.sensitive.service;
|
||||
|
||||
public interface ISensitiveWordService {
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.abin.mallchat.common.sensitive.service.impl;
|
||||
|
||||
import com.abin.mallchat.common.common.utils.SensitiveWordUtils;
|
||||
import com.abin.mallchat.common.sensitive.dao.SensitiveWordDao;
|
||||
import com.abin.mallchat.common.sensitive.domain.SensitiveWord;
|
||||
import com.abin.mallchat.common.sensitive.service.ISensitiveWordService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SensitiveWordServiceImpl implements ISensitiveWordService {
|
||||
@Autowired
|
||||
private SensitiveWordDao sensitiveWordDao;
|
||||
@Autowired
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
@PostConstruct
|
||||
public void initSensitiveWord() {
|
||||
threadPoolTaskExecutor.execute(() -> {
|
||||
log.info("[initSensitiveWord] start");
|
||||
List<SensitiveWord> list = sensitiveWordDao.list();
|
||||
if (!CollectionUtils.isEmpty(list)) {
|
||||
List<String> wordList = list.stream()
|
||||
.map(SensitiveWord::getWord)
|
||||
.collect(Collectors.toList());
|
||||
SensitiveWordUtils.loadWord(wordList);
|
||||
}
|
||||
log.info("[initSensitiveWord] end; loading sensitiveWords num:{}", list.size());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.abin.mallchat.common.user.dao;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.UserEmoji;
|
||||
import com.abin.mallchat.common.user.mapper.UserEmojiMapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户表情包 服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* @since 2023-07-09
|
||||
*/
|
||||
@Service
|
||||
public class UserEmojiDao extends ServiceImpl<UserEmojiMapper, UserEmoji> {
|
||||
|
||||
public List<UserEmoji> listByUid(Long uid) {
|
||||
return lambdaQuery().eq(UserEmoji::getUid, uid).list();
|
||||
}
|
||||
|
||||
public int countByUid(Long uid) {
|
||||
return lambdaQuery().eq(UserEmoji::getUid, uid).count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.abin.mallchat.common.user.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户表情包
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* @since 2023-07-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName("user_emoji")
|
||||
public class UserEmoji implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户表ID
|
||||
*/
|
||||
@TableField("uid")
|
||||
private Long uid;
|
||||
|
||||
/**
|
||||
* 表情地址
|
||||
*/
|
||||
@TableField("expression_url")
|
||||
private String expressionUrl;
|
||||
|
||||
/**
|
||||
* 逻辑删除(0-正常,1-删除)
|
||||
*/
|
||||
@TableField("delete_status")
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
private Integer deleteStatus;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.abin.mallchat.common.user.mapper;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.UserEmoji;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户表情包 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* @since 2023-07-09
|
||||
*/
|
||||
public interface UserEmojiMapper extends BaseMapper<UserEmoji> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.abin.mallchat.common.user.mapper;
|
||||
|
||||
import com.abin.mallchat.common.user.domain.entity.UserEmoji;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* 用户表情包 Mapper
|
||||
*
|
||||
* @author: WuShiJie
|
||||
* @createTime: 2023/7/3 14:24
|
||||
*/
|
||||
public interface UserEmojisMapper extends BaseMapper<UserEmoji> {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.abin.mallchat.common.user.mapper.UserEmojiMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -16,6 +16,19 @@
|
||||
<groupId>com.abin.mallchat</groupId>
|
||||
<artifactId>mallchat-common</artifactId>
|
||||
</dependency>
|
||||
<!-- Used for unit testing -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>5.3.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- token计算 -->
|
||||
<dependency>
|
||||
<groupId>com.knuddels</groupId>
|
||||
|
||||
@@ -84,7 +84,7 @@ public class ChatController {
|
||||
@GetMapping("/public/msg/page")
|
||||
@ApiOperation("消息列表")
|
||||
@FrequencyControl(time = 120, count = 20, target = FrequencyControl.Target.IP)
|
||||
public ApiResult<CursorPageBaseResp<ChatMessageResp>> getMsgPage1(@Valid ChatMessagePageReq request) {
|
||||
public ApiResult<CursorPageBaseResp<ChatMessageResp>> getMsgPage(@Valid ChatMessagePageReq request) {
|
||||
// black(request);
|
||||
CursorPageBaseResp<ChatMessageResp> msgPage = chatService.getMsgPage(request, RequestHolder.get().getUid());
|
||||
filterBlackMsg(msgPage);
|
||||
@@ -94,7 +94,6 @@ public class ChatController {
|
||||
private void filterBlackMsg(CursorPageBaseResp<ChatMessageResp> memberPage) {
|
||||
Set<String> blackMembers = getBlackUidSet();
|
||||
memberPage.getList().removeIf(a -> blackMembers.contains(a.getFromUser().getUid().toString()));
|
||||
System.out.println(1);
|
||||
}
|
||||
|
||||
@PostMapping("/msg")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.request;
|
||||
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageTypeEnum;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -30,7 +29,7 @@ public class ChatMessageReq {
|
||||
|
||||
@ApiModelProperty("消息类型")
|
||||
@NotNull
|
||||
private Integer msgType = MessageTypeEnum.TEXT.getType();
|
||||
private Integer msgType;
|
||||
|
||||
@ApiModelProperty("消息内容,类型不同传值不同,见https://www.yuque.com/snab/mallcaht/rkb2uz5k1qqdmcmd")
|
||||
@NotNull
|
||||
|
||||
@@ -7,7 +7,6 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Description: 消息
|
||||
@@ -27,16 +26,8 @@ public class ChatMessageResp {
|
||||
|
||||
@Data
|
||||
public static class UserInfo {
|
||||
@ApiModelProperty("用户名称")
|
||||
private String username;
|
||||
@ApiModelProperty("用户id")
|
||||
private Long uid;
|
||||
@ApiModelProperty("头像")
|
||||
private String avatar;
|
||||
@ApiModelProperty("归属地")
|
||||
private String locPlace;
|
||||
@ApiModelProperty("徽章标识,如果没有展示null")
|
||||
private Badge badge;
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -45,36 +36,12 @@ public class ChatMessageResp {
|
||||
private Long id;
|
||||
@ApiModelProperty("消息发送时间")
|
||||
private Date sendTime;
|
||||
@ApiModelProperty("消息内容-废弃")
|
||||
@Deprecated
|
||||
private String content;
|
||||
@ApiModelProperty("消息链接映射-废弃")
|
||||
@Deprecated
|
||||
private Map<String, String> urlTitleMap;
|
||||
@ApiModelProperty("消息类型 1正常文本 2.撤回消息")
|
||||
private Integer type;
|
||||
@ApiModelProperty("消息内容不同的消息类型,内容体不同,见https://www.yuque.com/snab/mallcaht/rkb2uz5k1qqdmcmd")
|
||||
private Object body;
|
||||
@ApiModelProperty("消息标记")
|
||||
private MessageMark messageMark;
|
||||
@ApiModelProperty("父消息,如果没有父消息,返回的是null")
|
||||
private ReplyMsg reply;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Deprecated
|
||||
public static class ReplyMsg {
|
||||
@ApiModelProperty("消息id")
|
||||
private Long id;
|
||||
@ApiModelProperty("用户名称")
|
||||
private String username;
|
||||
@ApiModelProperty("消息内容")
|
||||
private String content;
|
||||
@ApiModelProperty("是否可消息跳转 0否 1是")
|
||||
private Integer canCallback;
|
||||
@ApiModelProperty("跳转间隔的消息条数")
|
||||
private Integer gapCount;
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -88,12 +55,4 @@ public class ChatMessageResp {
|
||||
@ApiModelProperty("该用户是否已经举报 0否 1是")
|
||||
private Integer userDislike;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Badge {
|
||||
@ApiModelProperty("徽章图像")
|
||||
private String img;
|
||||
@ApiModelProperty("徽章说明")
|
||||
private String describe;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.abin.mallchat.custom.chat.domain.vo.response.msg;
|
||||
|
||||
import com.abin.mallchat.common.common.utils.discover.domain.UrlInfo;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -22,7 +23,7 @@ public class TextMsgResp {
|
||||
@ApiModelProperty("消息内容")
|
||||
private String content;
|
||||
@ApiModelProperty("消息链接映射")
|
||||
private Map<String, String> urlTitleMap;
|
||||
private Map<String, UrlInfo> urlContentMap;
|
||||
@ApiModelProperty("艾特的uid")
|
||||
private List<Long> atUidList;
|
||||
@ApiModelProperty("父消息,如果没有父消息,返回的是null")
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.abin.mallchat.custom.chat.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface WeChatMsgOperationService {
|
||||
/**
|
||||
* 向被at的用户微信推送群聊消息
|
||||
*
|
||||
* @param senderUid senderUid
|
||||
* @param receiverUidList receiverUidList
|
||||
* @param msg msg
|
||||
*/
|
||||
void publishChatMsgToWeChatUser(long senderUid, List<Long> receiverUidList, String msg);
|
||||
}
|
||||
@@ -3,14 +3,9 @@ package com.abin.mallchat.custom.chat.service.adapter;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.entity.MessageMark;
|
||||
import com.abin.mallchat.common.chat.domain.entity.msg.MessageExtra;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageStatusEnum;
|
||||
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
|
||||
import com.abin.mallchat.common.user.domain.entity.IpDetail;
|
||||
import com.abin.mallchat.common.user.domain.entity.IpInfo;
|
||||
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.service.strategy.msg.AbstractMsgHandler;
|
||||
@@ -38,37 +33,25 @@ public class MessageAdapter {
|
||||
|
||||
}
|
||||
|
||||
public static List<ChatMessageResp> buildMsgResp(List<Message> messages, Map<Long, Message> replyMap, Map<Long, User> userMap, List<MessageMark> msgMark, Long receiveUid, Map<Long, ItemConfig> itemMap) {
|
||||
public static List<ChatMessageResp> buildMsgResp(List<Message> messages, Map<Long, Message> replyMap, List<MessageMark> msgMark, Long receiveUid) {
|
||||
Map<Long, List<MessageMark>> markMap = msgMark.stream().collect(Collectors.groupingBy(MessageMark::getMsgId));
|
||||
return messages.stream().map(a -> {
|
||||
ChatMessageResp resp = new ChatMessageResp();
|
||||
resp.setFromUser(buildFromUser(userMap.get(a.getFromUid()), itemMap));
|
||||
resp.setMessage(buildMessage(a, replyMap, userMap, markMap.getOrDefault(a.getId(), new ArrayList<>()), receiveUid));
|
||||
resp.setFromUser(buildFromUser(a.getFromUid()));
|
||||
resp.setMessage(buildMessage(a, replyMap, markMap.getOrDefault(a.getId(), new ArrayList<>()), receiveUid));
|
||||
return resp;
|
||||
})
|
||||
.sorted(Comparator.comparing(a -> a.getMessage().getSendTime()))//帮前端排好序,更方便它展示
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static ChatMessageResp.Message buildMessage(Message message, Map<Long, Message> replyMap, Map<Long, User> userMap, List<MessageMark> marks, Long receiveUid) {
|
||||
private static ChatMessageResp.Message buildMessage(Message message, Map<Long, Message> replyMap, List<MessageMark> marks, Long receiveUid) {
|
||||
ChatMessageResp.Message messageVO = new ChatMessageResp.Message();
|
||||
BeanUtil.copyProperties(message, messageVO);
|
||||
messageVO.setSendTime(message.getCreateTime());
|
||||
AbstractMsgHandler msgHandler = MsgHandlerFactory.getStrategyNoNull(message.getType());
|
||||
messageVO.setBody(msgHandler.showMsg(message));
|
||||
messageVO.setUrlTitleMap(Optional.ofNullable(message.getExtra()).map(MessageExtra::getUrlTitleMap).orElse(null));
|
||||
Message replyMessage = replyMap.get(message.getReplyMsgId());
|
||||
|
||||
//回复消息
|
||||
if (Objects.nonNull(replyMessage)) {
|
||||
ChatMessageResp.ReplyMsg replyMsgVO = new ChatMessageResp.ReplyMsg();
|
||||
replyMsgVO.setId(replyMessage.getId());
|
||||
replyMsgVO.setContent(replyMessage.getContent());
|
||||
User replyUser = userMap.get(replyMessage.getFromUid());
|
||||
replyMsgVO.setUsername(replyUser.getName());
|
||||
replyMsgVO.setCanCallback(YesOrNoEnum.toStatus(Objects.nonNull(message.getGapCount()) && message.getGapCount() <= CAN_CALLBACK_GAP_COUNT));
|
||||
replyMsgVO.setGapCount(message.getGapCount());
|
||||
messageVO.setReply(replyMsgVO);
|
||||
if (Objects.nonNull(msgHandler)) {
|
||||
messageVO.setBody(msgHandler.showMsg(message));
|
||||
}
|
||||
//消息标记
|
||||
messageVO.setMessageMark(buildMsgMark(marks, receiveUid));
|
||||
@@ -87,19 +70,9 @@ public class MessageAdapter {
|
||||
return mark;
|
||||
}
|
||||
|
||||
private static ChatMessageResp.UserInfo buildFromUser(User fromUser, Map<Long, ItemConfig> itemMap) {
|
||||
private static ChatMessageResp.UserInfo buildFromUser(Long fromUid) {
|
||||
ChatMessageResp.UserInfo userInfo = new ChatMessageResp.UserInfo();
|
||||
userInfo.setUsername(fromUser.getName());
|
||||
userInfo.setAvatar(fromUser.getAvatar());
|
||||
userInfo.setLocPlace(Optional.ofNullable(fromUser.getIpInfo()).map(IpInfo::getUpdateIpDetail).map(IpDetail::getCity).orElse(null));
|
||||
userInfo.setUid(fromUser.getId());
|
||||
if (Objects.nonNull(fromUser.getItemId())) {
|
||||
ChatMessageResp.Badge badge = new ChatMessageResp.Badge();
|
||||
ItemConfig itemConfig = itemMap.get(fromUser.getItemId());
|
||||
badge.setImg(itemConfig.getImg());
|
||||
badge.setDescribe(itemConfig.getDescribe());
|
||||
userInfo.setBadge(badge);
|
||||
}
|
||||
userInfo.setUid(fromUid);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ import com.abin.mallchat.common.common.domain.vo.response.CursorPageBaseResp;
|
||||
import com.abin.mallchat.common.common.event.MessageSendEvent;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import com.abin.mallchat.common.user.dao.UserDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.ItemConfig;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.enums.ChatActiveStatusEnum;
|
||||
import com.abin.mallchat.common.user.domain.enums.RoleEnum;
|
||||
import com.abin.mallchat.common.user.service.IRoleService;
|
||||
@@ -49,7 +47,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Description: 消息处理类
|
||||
@@ -226,21 +223,14 @@ public class ChatServiceImpl implements ChatService {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<Long, Message> replyMap = new HashMap<>();
|
||||
Map<Long, User> userMap;
|
||||
Map<Long, ItemConfig> itemMap;
|
||||
//批量查出回复的消息
|
||||
List<Long> replyIds = messages.stream().map(Message::getReplyMsgId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
if (CollectionUtil.isNotEmpty(replyIds)) {
|
||||
replyMap = messageDao.listByIds(replyIds).stream().collect(Collectors.toMap(Message::getId, Function.identity()));
|
||||
}
|
||||
//批量查询消息关联用户
|
||||
Set<Long> uidSet = Stream.concat(replyMap.values().stream().map(Message::getFromUid), messages.stream().map(Message::getFromUid)).collect(Collectors.toSet());
|
||||
userMap = userCache.getUserInfoBatch(uidSet);
|
||||
//批量查询item信息
|
||||
itemMap = userMap.values().stream().map(User::getItemId).distinct().filter(Objects::nonNull).map(itemCache::getById).collect(Collectors.toMap(ItemConfig::getId, Function.identity()));
|
||||
//查询消息标志
|
||||
List<MessageMark> msgMark = messageMarkDao.getValidMarkByMsgIdBatch(messages.stream().map(Message::getId).collect(Collectors.toList()));
|
||||
return MessageAdapter.buildMsgResp(messages, replyMap, userMap, msgMark, receiveUid, itemMap);
|
||||
return MessageAdapter.buildMsgResp(messages, replyMap, msgMark, receiveUid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.abin.mallchat.custom.chat.service.impl;
|
||||
|
||||
import cn.hutool.core.thread.NamedThreadFactory;
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
import com.abin.mallchat.common.common.exception.FrequencyControlException;
|
||||
import com.abin.mallchat.common.common.handler.GlobalUncaughtExceptionHandler;
|
||||
import com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlUtil;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.service.cache.UserCache;
|
||||
import com.abin.mallchat.custom.chat.service.WeChatMsgOperationService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.template.WxMpTemplateData;
|
||||
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlStrategyFactory.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WeChatMsgOperationServiceImpl implements WeChatMsgOperationService {
|
||||
|
||||
private static final ExecutorService executor = new ThreadPoolExecutor(1, 10, 3000L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<Runnable>(20),
|
||||
new NamedThreadFactory("wechat-operation-thread", null, false,
|
||||
new GlobalUncaughtExceptionHandler()));
|
||||
|
||||
// at消息的微信推送模板id
|
||||
private final String atMsgPublishTemplateId = "Xd7sWPZsuWa0UmpvLaZPvaJVjNj1KjEa0zLOm5_Z7IU";
|
||||
|
||||
private final String WE_CHAT_MSG_COLOR = "#A349A4";
|
||||
|
||||
@Autowired
|
||||
private UserCache userCache;
|
||||
|
||||
@Autowired
|
||||
WxMpService wxMpService;
|
||||
|
||||
@Override
|
||||
public void publishChatMsgToWeChatUser(long senderUid, List<Long> receiverUidList, String msg) {
|
||||
User sender = userCache.getUserInfo(senderUid);
|
||||
Set uidSet = new HashSet();
|
||||
uidSet.addAll(receiverUidList);
|
||||
Map<Long, User> userMap = userCache.getUserInfoBatch(uidSet);
|
||||
userMap.values().forEach(user -> {
|
||||
if (Objects.nonNull(user.getOpenId())) {
|
||||
executor.execute(() -> {
|
||||
WxMpTemplateMessage msgTemplate = getAtMsgTemplate(sender, user.getOpenId(), msg);
|
||||
publishTemplateMsgCheckLimit(msgTemplate);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void publishTemplateMsgCheckLimit(WxMpTemplateMessage msgTemplate) {
|
||||
try {
|
||||
FrequencyControlDTO frequencyControlDTO = new FrequencyControlDTO();
|
||||
frequencyControlDTO.setKey("TemplateMsg:" + msgTemplate.getToUser());
|
||||
frequencyControlDTO.setUnit(TimeUnit.HOURS);
|
||||
frequencyControlDTO.setCount(1);
|
||||
frequencyControlDTO.setTime(1);
|
||||
FrequencyControlUtil.executeWithFrequencyControl(TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER, frequencyControlDTO,
|
||||
() -> publishTemplateMsg(msgTemplate));
|
||||
} catch (FrequencyControlException e) {
|
||||
log.info("wx push limit openid:{}", msgTemplate.getToUser());
|
||||
} catch (Throwable e) {
|
||||
log.error("wx push error openid:{}", msgTemplate.getToUser());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 构造微信模板消息
|
||||
*/
|
||||
private WxMpTemplateMessage getAtMsgTemplate(User sender, String openId, String msg) {
|
||||
return WxMpTemplateMessage.builder()
|
||||
.toUser(openId)
|
||||
.templateId(atMsgPublishTemplateId)
|
||||
.data(generateAtMsgData(sender, msg))
|
||||
.build();
|
||||
}
|
||||
|
||||
/*
|
||||
* 构造微信消息模板的数据
|
||||
*/
|
||||
private List<WxMpTemplateData> generateAtMsgData(User sender, String msg) {
|
||||
List dataList = new ArrayList<WxMpTemplateData>();
|
||||
// todo: 没有消息模板,暂不实现
|
||||
dataList.add(new WxMpTemplateData("name", sender.getName(), WE_CHAT_MSG_COLOR));
|
||||
dataList.add(new WxMpTemplateData("content", msg, WE_CHAT_MSG_COLOR));
|
||||
return dataList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送微信模板消息
|
||||
*
|
||||
* @param templateMsg 微信模板消息
|
||||
*/
|
||||
protected void publishTemplateMsg(WxMpTemplateMessage templateMsg) {
|
||||
// WxMpTemplateMsgService wxMpTemplateMsgService = wxMpService.getTemplateMsgService();todo 等审核通过
|
||||
// try {
|
||||
// wxMpTemplateMsgService.sendTemplateMsg(templateMsg);
|
||||
// } catch (WxErrorException e) {
|
||||
// log.error("publish we chat msg failed! open id is {}, msg is {}.",
|
||||
// templateMsg.getToUser(), JsonUtils.toStr(templateMsg.getData()));
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.abin.mallchat.custom.chat.service.strategy.msg;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.abin.mallchat.common.chat.dao.MessageDao;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.entity.msg.EmojisMsgDTO;
|
||||
import com.abin.mallchat.common.chat.domain.entity.msg.MessageExtra;
|
||||
import com.abin.mallchat.common.chat.domain.enums.MessageTypeEnum;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.request.ChatMessageReq;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Description:表情消息
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-06-04
|
||||
*/
|
||||
@Component
|
||||
public class EmojisMsgHandler extends AbstractMsgHandler {
|
||||
@Autowired
|
||||
private MessageDao messageDao;
|
||||
|
||||
@Override
|
||||
MessageTypeEnum getMsgTypeEnum() {
|
||||
return MessageTypeEnum.EMOJI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMsg(ChatMessageReq request, Long uid) {
|
||||
EmojisMsgDTO body = BeanUtil.toBean(request.getBody(), EmojisMsgDTO.class);
|
||||
AssertUtil.allCheckValidateThrow(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMsg(Message msg, ChatMessageReq request) {
|
||||
EmojisMsgDTO body = BeanUtil.toBean(request.getBody(), EmojisMsgDTO.class);
|
||||
MessageExtra extra = Optional.ofNullable(msg.getExtra()).orElse(new MessageExtra());
|
||||
Message update = new Message();
|
||||
update.setId(msg.getId());
|
||||
update.setExtra(extra);
|
||||
extra.setEmojisMsgDTO(body);
|
||||
messageDao.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object showMsg(Message msg) {
|
||||
return msg.getExtra().getEmojisMsgDTO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object showReplyMsg(Message msg) {
|
||||
return "表情";
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,9 @@ import com.abin.mallchat.common.chat.domain.enums.MessageTypeEnum;
|
||||
import com.abin.mallchat.common.chat.service.cache.MsgCache;
|
||||
import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import com.abin.mallchat.common.common.utils.SensitiveWordUtils;
|
||||
import com.abin.mallchat.common.common.utils.discover.PrioritizedUrlTitleDiscover;
|
||||
import com.abin.mallchat.common.common.utils.discover.PrioritizedUrlDiscover;
|
||||
import com.abin.mallchat.common.common.utils.discover.domain.UrlInfo;
|
||||
import com.abin.mallchat.common.common.utils.sensitiveWord.SensitiveWordBs;
|
||||
import com.abin.mallchat.common.user.domain.entity.User;
|
||||
import com.abin.mallchat.common.user.domain.enums.RoleEnum;
|
||||
import com.abin.mallchat.common.user.service.IRoleService;
|
||||
@@ -46,8 +47,10 @@ public class TextMsgHandler extends AbstractMsgHandler {
|
||||
private UserInfoCache userInfoCache;
|
||||
@Autowired
|
||||
private IRoleService iRoleService;
|
||||
|
||||
private static final PrioritizedUrlTitleDiscover URL_TITLE_DISCOVER = new PrioritizedUrlTitleDiscover();
|
||||
@Autowired
|
||||
private SensitiveWordBs sensitiveWordBs;
|
||||
|
||||
private static final PrioritizedUrlDiscover URL_TITLE_DISCOVER = new PrioritizedUrlDiscover();
|
||||
|
||||
@Override
|
||||
MessageTypeEnum getMsgTypeEnum() {
|
||||
@@ -81,7 +84,7 @@ public class TextMsgHandler extends AbstractMsgHandler {
|
||||
MessageExtra extra = Optional.ofNullable(msg.getExtra()).orElse(new MessageExtra());
|
||||
Message update = new Message();
|
||||
update.setId(msg.getId());
|
||||
update.setContent(SensitiveWordUtils.filter(body.getContent()));
|
||||
update.setContent(sensitiveWordBs.filter(body.getContent()));
|
||||
update.setExtra(extra);
|
||||
//如果有回复消息
|
||||
if (Objects.nonNull(body.getReplyMsgId())) {
|
||||
@@ -91,8 +94,8 @@ public class TextMsgHandler extends AbstractMsgHandler {
|
||||
|
||||
}
|
||||
//判断消息url跳转
|
||||
Map<String, String> urlTitleMap = URL_TITLE_DISCOVER.getContentTitleMap(body.getContent());
|
||||
extra.setUrlTitleMap(urlTitleMap);
|
||||
Map<String, UrlInfo> urlContentMap = URL_TITLE_DISCOVER.getUrlContentMap(body.getContent());
|
||||
extra.setUrlContentMap(urlContentMap);
|
||||
//艾特功能
|
||||
if (CollectionUtil.isNotEmpty(body.getAtUidList())) {
|
||||
extra.setAtUidList(body.getAtUidList());
|
||||
@@ -106,7 +109,7 @@ public class TextMsgHandler extends AbstractMsgHandler {
|
||||
public Object showMsg(Message msg) {
|
||||
TextMsgResp resp = new TextMsgResp();
|
||||
resp.setContent(msg.getContent());
|
||||
resp.setUrlTitleMap(Optional.ofNullable(msg.getExtra()).map(MessageExtra::getUrlTitleMap).orElse(null));
|
||||
resp.setUrlContentMap(Optional.ofNullable(msg.getExtra()).map(MessageExtra::getUrlContentMap).orElse(null));
|
||||
resp.setAtUidList(Optional.ofNullable(msg.getExtra()).map(MessageExtra::getAtUidList).orElse(null));
|
||||
//回复消息
|
||||
Optional<Message> reply = Optional.ofNullable(msg.getReplyMsgId())
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.abin.mallchat.custom.chatai.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class GPTRequestDTO {
|
||||
/**
|
||||
* 聊天内容
|
||||
*/
|
||||
private String content;
|
||||
/**
|
||||
* 用户Id
|
||||
*/
|
||||
private Long uid;
|
||||
}
|
||||
@@ -4,12 +4,17 @@ import cn.hutool.http.HttpResponse;
|
||||
import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.chat.domain.entity.msg.MessageExtra;
|
||||
import com.abin.mallchat.common.common.constant.RedisKey;
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
import com.abin.mallchat.common.common.exception.FrequencyControlException;
|
||||
import com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlUtil;
|
||||
import com.abin.mallchat.common.common.utils.RedisUtils;
|
||||
import com.abin.mallchat.custom.chatai.dto.GPTRequestDTO;
|
||||
import com.abin.mallchat.custom.chatai.properties.ChatGLM2Properties;
|
||||
import com.abin.mallchat.custom.chatai.utils.ChatGLM2Utils;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserInfoResp;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -21,10 +26,15 @@ import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.abin.mallchat.common.common.constant.RedisKey.USER_GLM2_TIME_LAST;
|
||||
import static com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlStrategyFactory.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ChatGLM2Handler extends AbstractChatAIHandler {
|
||||
/**
|
||||
* ChatGLM2Handler限流前缀
|
||||
*/
|
||||
private static final String CHAT_GLM2_FREQUENCY_PREFIX = "ChatGLM2Handler";
|
||||
|
||||
private static final List<String> ERROR_MSG = Arrays.asList(
|
||||
"还摸鱼呢?你不下班我还要下班呢。。。。",
|
||||
@@ -74,27 +84,42 @@ public class ChatGLM2Handler extends AbstractChatAIHandler {
|
||||
protected String doChat(Message message) {
|
||||
String content = message.getContent().replace("@" + AI_NAME, "").trim();
|
||||
Long uid = message.getFromUid();
|
||||
Long minute;
|
||||
try {
|
||||
FrequencyControlDTO frequencyControlDTO = new FrequencyControlDTO();
|
||||
frequencyControlDTO.setKey(CHAT_GLM2_FREQUENCY_PREFIX + ":" + uid);
|
||||
frequencyControlDTO.setUnit(TimeUnit.MINUTES);
|
||||
frequencyControlDTO.setCount(1);
|
||||
frequencyControlDTO.setTime(glm2Properties.getMinute().intValue());
|
||||
return FrequencyControlUtil.executeWithFrequencyControl(TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER, frequencyControlDTO, () -> sendRequestToGPT(new GPTRequestDTO(content, uid)));
|
||||
} catch (FrequencyControlException e) {
|
||||
return "你太快了亲爱的~过一会再来找人家~";
|
||||
} catch (Throwable e) {
|
||||
return "系统开小差啦~~";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param gptRequestDTO
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private String sendRequestToGPT(GPTRequestDTO gptRequestDTO) {
|
||||
String content = gptRequestDTO.getContent();
|
||||
String text;
|
||||
if ((minute = userMinutesLater(uid)) > 0) {
|
||||
text = "你太快了 " + minute + "分钟后重试";
|
||||
} else {
|
||||
HttpResponse response = null;
|
||||
try {
|
||||
response = ChatGLM2Utils
|
||||
.create()
|
||||
.url(glm2Properties.getUrl())
|
||||
.prompt(content)
|
||||
.timeout(glm2Properties.getTimeout())
|
||||
.send();
|
||||
text = ChatGLM2Utils.parseText(response);
|
||||
} catch (Exception e) {
|
||||
log.warn("glm2 doChat warn:", e);
|
||||
return getErrorText();
|
||||
}
|
||||
if (StringUtils.isNotBlank(text)) {
|
||||
RedisUtils.set(RedisKey.getKey(USER_GLM2_TIME_LAST, uid), new Date(), glm2Properties.getMinute(), TimeUnit.MINUTES);
|
||||
}
|
||||
HttpResponse response = null;
|
||||
try {
|
||||
response = ChatGLM2Utils
|
||||
.create()
|
||||
.url(glm2Properties.getUrl())
|
||||
.prompt(content)
|
||||
.timeout(glm2Properties.getTimeout())
|
||||
.send();
|
||||
text = ChatGLM2Utils.parseText(response);
|
||||
} catch (Exception e) {
|
||||
log.warn("glm2 doChat warn:", e);
|
||||
return getErrorText();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import com.abin.mallchat.custom.chatai.domain.ChatGPTContext;
|
||||
import com.abin.mallchat.custom.chatai.domain.ChatGPTMsg;
|
||||
import com.abin.mallchat.custom.chatai.domain.builder.ChatGPTContextBuilder;
|
||||
import com.abin.mallchat.custom.chatai.domain.builder.ChatGPTMsgBuilder;
|
||||
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
|
||||
import com.abin.mallchat.common.common.exception.FrequencyControlException;
|
||||
import com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlUtil;
|
||||
import com.abin.mallchat.custom.chatai.dto.GPTRequestDTO;
|
||||
import com.abin.mallchat.custom.chatai.properties.ChatGPTProperties;
|
||||
import com.abin.mallchat.custom.chatai.utils.ChatGPTUtils;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserInfoResp;
|
||||
@@ -24,9 +28,15 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.abin.mallchat.common.common.constant.RedisKey.USER_CHAT_CONTEXT;
|
||||
|
||||
import static com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlStrategyFactory.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GPTChatAIHandler extends AbstractChatAIHandler {
|
||||
/**
|
||||
* GPTChatAIHandler限流前缀
|
||||
*/
|
||||
private static final String CHAT_FREQUENCY_PREFIX = "GPTChatAIHandler";
|
||||
|
||||
@Autowired
|
||||
private ChatGPTProperties chatGPTProperties;
|
||||
@@ -36,16 +46,18 @@ public class GPTChatAIHandler extends AbstractChatAIHandler {
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
UserInfoResp userInfo = userService.getUserInfo(chatGPTProperties.getAIUserId());
|
||||
if (userInfo == null) {
|
||||
log.error("根据AIUserId:{} 找不到用户信息", chatGPTProperties.getAIUserId());
|
||||
throw new RuntimeException("根据AIUserId: " + chatGPTProperties.getAIUserId() + " 找不到用户信息");
|
||||
if (isUse()) {
|
||||
UserInfoResp userInfo = userService.getUserInfo(chatGPTProperties.getAIUserId());
|
||||
if (userInfo == null) {
|
||||
log.error("根据AIUserId:{} 找不到用户信息", chatGPTProperties.getAIUserId());
|
||||
throw new RuntimeException("根据AIUserId: " + chatGPTProperties.getAIUserId() + " 找不到用户信息");
|
||||
}
|
||||
if (StringUtils.isBlank(userInfo.getName())) {
|
||||
log.warn("根据AIUserId:{} 找到的用户信息没有name", chatGPTProperties.getAIUserId());
|
||||
throw new RuntimeException("根据AIUserId: " + chatGPTProperties.getAIUserId() + " 找到的用户没有名字");
|
||||
}
|
||||
AI_NAME = userInfo.getName();
|
||||
}
|
||||
if (StringUtils.isBlank(userInfo.getName())) {
|
||||
log.warn("根据AIUserId:{} 找到的用户信息没有name", chatGPTProperties.getAIUserId());
|
||||
throw new RuntimeException("根据AIUserId: " + chatGPTProperties.getAIUserId() + " 找到的用户没有名字");
|
||||
}
|
||||
AI_NAME = userInfo.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,37 +70,41 @@ public class GPTChatAIHandler extends AbstractChatAIHandler {
|
||||
return chatGPTProperties.getAIUserId();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String doChat(Message message) {
|
||||
String prompt = message.getContent().replace("@" + AI_NAME, "").trim();
|
||||
String content = message.getContent().replace("@" + AI_NAME, "").trim();
|
||||
Long uid = message.getFromUid();
|
||||
try {
|
||||
FrequencyControlDTO frequencyControlDTO = new FrequencyControlDTO();
|
||||
frequencyControlDTO.setKey(CHAT_FREQUENCY_PREFIX + ":" + uid);
|
||||
frequencyControlDTO.setUnit(TimeUnit.HOURS);
|
||||
frequencyControlDTO.setCount(chatGPTProperties.getLimit());
|
||||
frequencyControlDTO.setTime(24);
|
||||
return FrequencyControlUtil.executeWithFrequencyControl(TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER, frequencyControlDTO, () -> sendRequestToGPT(new GPTRequestDTO(content, uid)));
|
||||
} catch (FrequencyControlException e) {
|
||||
return "亲爱的,你今天找我聊了" + chatGPTProperties.getLimit() + "次了~人家累了~明天见";
|
||||
} catch (Throwable e) {
|
||||
return "系统开小差啦~~";
|
||||
}
|
||||
}
|
||||
|
||||
private String sendRequestToGPT(GPTRequestDTO gptRequestDTO) {
|
||||
String content = gptRequestDTO.getContent();
|
||||
Long roomId = message.getRoomId();
|
||||
Long chatNum;
|
||||
String text;
|
||||
if ((chatNum = getUserChatNum(uid)) > chatGPTProperties.getLimit()) {
|
||||
text = "你今天已经和我聊了" + chatNum + "次了,我累了,明天再聊吧";
|
||||
} else {
|
||||
try {
|
||||
ChatGPTContext context = buildContext(message, prompt);// 构建上下文
|
||||
context = tailorContext(context);// 裁剪上下文
|
||||
log.info("prompt = {}" , prompt);
|
||||
Response response = ChatGPTUtils.create(chatGPTProperties.getKey())
|
||||
.proxyUrl(chatGPTProperties.getProxyUrl())
|
||||
.model(chatGPTProperties.getModelName())
|
||||
.timeout(chatGPTProperties.getTimeout())
|
||||
.maxTokens(chatGPTProperties.getMaxTokens())
|
||||
.message(context.getMsg())
|
||||
.send();
|
||||
text = ChatGPTUtils.parseText(response);
|
||||
ChatGPTMsg chatGPTMsg = ChatGPTMsgBuilder.assistantMsg(text);
|
||||
context.addMsg(chatGPTMsg);
|
||||
RedisUtils.set(RedisKey.getKey(USER_CHAT_CONTEXT, uid, roomId), context, 1L, TimeUnit.HOURS);
|
||||
userChatNumInrc(uid);
|
||||
} catch (Exception e) {
|
||||
log.warn("gpt doChat warn:", e);
|
||||
text = "我累了,明天再聊吧";
|
||||
}
|
||||
HttpResponse response = null;
|
||||
try {
|
||||
response = ChatGPTUtils.create(chatGPTProperties.getKey())
|
||||
.proxyUrl(chatGPTProperties.getProxyUrl())
|
||||
.model(chatGPTProperties.getModelName())
|
||||
.timeout(chatGPTProperties.getTimeout())
|
||||
.prompt(content)
|
||||
.send();
|
||||
text = ChatGPTUtils.parseText(response);
|
||||
} catch (Exception e) {
|
||||
log.warn("gpt doChat warn:", e);
|
||||
text = "我累了,明天再聊吧";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.abin.mallchat.common.chat.domain.entity.Message;
|
||||
import com.abin.mallchat.common.common.event.MessageSendEvent;
|
||||
import com.abin.mallchat.custom.chat.domain.vo.response.ChatMessageResp;
|
||||
import com.abin.mallchat.custom.chat.service.ChatService;
|
||||
import com.abin.mallchat.custom.chat.service.WeChatMsgOperationService;
|
||||
import com.abin.mallchat.custom.chat.service.impl.WeChatMsgOperationServiceImpl;
|
||||
import com.abin.mallchat.custom.chatai.service.IChatAIService;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.WSAdapter;
|
||||
@@ -15,6 +17,8 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 消息发送监听器
|
||||
*
|
||||
@@ -32,6 +36,9 @@ public class MessageSendListener {
|
||||
@Autowired
|
||||
private IChatAIService openAIService;
|
||||
|
||||
@Autowired
|
||||
WeChatMsgOperationService weChatMsgOperationService;
|
||||
|
||||
@Async
|
||||
@TransactionalEventListener(classes = MessageSendEvent.class, fallbackExecution = true)
|
||||
public void notifyAllOnline(MessageSendEvent event) {
|
||||
@@ -46,4 +53,12 @@ public class MessageSendListener {
|
||||
openAIService.chat(message);
|
||||
}
|
||||
|
||||
@TransactionalEventListener(classes = MessageSendEvent.class, fallbackExecution = true)
|
||||
public void publishChatToWechat(@NotNull MessageSendEvent event) {
|
||||
Message message = messageDao.getById(event.getMsgId());
|
||||
if (Objects.nonNull(message.getExtra().getAtUidList())) {
|
||||
weChatMsgOperationService.publishChatMsgToWeChatUser(message.getFromUid(), message.getExtra().getAtUidList(),
|
||||
message.getContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ public class TokenInterceptor implements HandlerInterceptor {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
MDC.remove(MDCKey.UID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是不是公共方法,可以未登录访问的
|
||||
*
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.abin.mallchat.custom.user.controller;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.vo.request.IdReqVO;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.IdRespVO;
|
||||
import com.abin.mallchat.common.common.utils.RequestHolder;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.UserEmojiReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserEmojiResp;
|
||||
import com.abin.mallchat.custom.user.service.UserEmojiService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户表情包
|
||||
*
|
||||
* @author: WuShiJie
|
||||
* @createTime: 2023/7/3 14:21
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/capi/user/emoji")
|
||||
@Api(tags = "用户表情包管理相关接口")
|
||||
public class UserEmojiController {
|
||||
|
||||
/**
|
||||
* 用户表情包 Service
|
||||
*/
|
||||
@Resource
|
||||
private UserEmojiService emojiService;
|
||||
|
||||
|
||||
/**
|
||||
* 表情包列表
|
||||
*
|
||||
* @return 表情包列表
|
||||
* @author WuShiJie
|
||||
* @createTime 2023/7/3 14:46
|
||||
**/
|
||||
@GetMapping("/list")
|
||||
@ApiOperation("表情包列表")
|
||||
public ApiResult<List<UserEmojiResp>> getEmojisPage() {
|
||||
return ApiResult.success(emojiService.list(RequestHolder.get().getUid()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 新增表情包
|
||||
*
|
||||
* @param req 用户表情包
|
||||
* @return 表情包
|
||||
* @author WuShiJie
|
||||
* @createTime 2023/7/3 14:46
|
||||
**/
|
||||
@PostMapping()
|
||||
@ApiOperation("新增表情包")
|
||||
public ApiResult<IdRespVO> insertEmojis(@Valid @RequestBody UserEmojiReq req) {
|
||||
return emojiService.insert(req, RequestHolder.get().getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除表情包
|
||||
*
|
||||
* @return 删除结果
|
||||
* @author WuShiJie
|
||||
* @createTime 2023/7/3 14:46
|
||||
**/
|
||||
@DeleteMapping()
|
||||
@ApiOperation("删除表情包")
|
||||
public ApiResult<Void> deleteEmojis(@Valid @RequestBody IdReqVO reqVO) {
|
||||
emojiService.remove(reqVO.getId(), RequestHolder.get().getUid());
|
||||
return ApiResult.success();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.util.stream.Collectors;
|
||||
@Getter
|
||||
public enum OssSceneEnum {
|
||||
CHAT(1, "聊天", "/chat"),
|
||||
EMOJI(2, "表情包", "/emoji"),
|
||||
;
|
||||
|
||||
private final Integer type;
|
||||
|
||||
@@ -23,7 +23,7 @@ public class UploadUrlReq {
|
||||
@ApiModelProperty(value = "文件名(带后缀)")
|
||||
@NotBlank
|
||||
private String fileName;
|
||||
@ApiModelProperty(value = "上传场景1.聊天室")
|
||||
@ApiModelProperty(value = "上传场景1.聊天室,2.表情包")
|
||||
@NotNull
|
||||
private Integer scene;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.request.user;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.vo.request.CursorPageBaseReq;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 描述此类的作用
|
||||
*
|
||||
* @author: WuShiJie
|
||||
* @createTime: 2023/7/3 14:52
|
||||
*/
|
||||
@Data
|
||||
public class EmojisPageReq extends CursorPageBaseReq {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.request.user;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
||||
/**
|
||||
* Description: 表情包反参
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-07-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UserEmojiReq {
|
||||
/**
|
||||
* 表情地址
|
||||
*/
|
||||
@ApiModelProperty(value = "新增的表情url")
|
||||
private String expressionUrl;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.abin.mallchat.custom.user.domain.vo.response.user;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
||||
/**
|
||||
* Description: 表情包反参
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-07-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UserEmojiResp {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@ApiModelProperty(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 表情地址
|
||||
*/
|
||||
@ApiModelProperty(value = "表情url")
|
||||
private String expressionUrl;
|
||||
|
||||
}
|
||||
@@ -38,4 +38,5 @@ public interface LoginService {
|
||||
* @return
|
||||
*/
|
||||
Long getValidUid(String token);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.abin.mallchat.custom.user.service;
|
||||
|
||||
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.IdRespVO;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.UserEmojiReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserEmojiResp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户表情包 Service
|
||||
*
|
||||
* @author: WuShiJie
|
||||
* @createTime: 2023/7/3 14:22
|
||||
*/
|
||||
public interface UserEmojiService {
|
||||
|
||||
/**
|
||||
* 表情包列表
|
||||
*
|
||||
* @return 表情包列表
|
||||
* @author WuShiJie
|
||||
* @createTime 2023/7/3 14:46
|
||||
**/
|
||||
List<UserEmojiResp> list(Long uid);
|
||||
|
||||
/**
|
||||
* 新增表情包
|
||||
*
|
||||
* @param emojis 用户表情包
|
||||
* @param uid 用户ID
|
||||
* @return 表情包
|
||||
* @author WuShiJie
|
||||
* @createTime 2023/7/3 14:46
|
||||
**/
|
||||
ApiResult<IdRespVO> insert(UserEmojiReq emojis, Long uid);
|
||||
|
||||
/**
|
||||
* 删除表情包
|
||||
*
|
||||
* @param id
|
||||
* @param uid
|
||||
*/
|
||||
void remove(Long id, Long uid);
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import org.springframework.stereotype.Service;
|
||||
*/
|
||||
@Service
|
||||
public class OssServiceImpl implements OssService {
|
||||
private static final String BUCKET_NAME = "mallchat";
|
||||
@Autowired
|
||||
private MinIOTemplate minIOTemplate;
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.abin.mallchat.custom.user.service.impl;
|
||||
|
||||
import com.abin.mallchat.common.common.annotation.RedissonLock;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.ApiResult;
|
||||
import com.abin.mallchat.common.common.domain.vo.response.IdRespVO;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import com.abin.mallchat.common.user.dao.UserEmojiDao;
|
||||
import com.abin.mallchat.common.user.domain.entity.UserEmoji;
|
||||
import com.abin.mallchat.custom.user.domain.vo.request.user.UserEmojiReq;
|
||||
import com.abin.mallchat.custom.user.domain.vo.response.user.UserEmojiResp;
|
||||
import com.abin.mallchat.custom.user.service.UserEmojiService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户表情包 ServiceImpl
|
||||
*
|
||||
* @author: WuShiJie
|
||||
* @createTime: 2023/7/3 14:23
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserEmojiServiceImpl implements UserEmojiService {
|
||||
|
||||
@Autowired
|
||||
private UserEmojiDao userEmojiDao;
|
||||
|
||||
@Override
|
||||
public List<UserEmojiResp> list(Long uid) {
|
||||
return userEmojiDao.listByUid(uid).
|
||||
stream()
|
||||
.map(a -> UserEmojiResp.builder()
|
||||
.id(a.getId())
|
||||
.expressionUrl(a.getExpressionUrl())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增表情包
|
||||
*
|
||||
* @param uid 用户ID
|
||||
* @return 表情包
|
||||
* @author WuShiJie
|
||||
* @createTime 2023/7/3 14:46
|
||||
**/
|
||||
@Override
|
||||
@RedissonLock(key = "#uid")
|
||||
public ApiResult<IdRespVO> insert(UserEmojiReq req, Long uid) {
|
||||
//校验表情数量是否超过30
|
||||
int count = userEmojiDao.countByUid(uid);
|
||||
AssertUtil.isFalse(count > 30, "最多只能添加30个表情哦~~");
|
||||
//校验表情是否存在
|
||||
Integer existsCount = userEmojiDao.lambdaQuery()
|
||||
.eq(UserEmoji::getExpressionUrl, req.getExpressionUrl())
|
||||
.eq(UserEmoji::getUid, uid)
|
||||
.count();
|
||||
AssertUtil.isFalse(existsCount > 0, "当前表情已存在哦~~");
|
||||
UserEmoji insert = UserEmoji.builder().uid(uid).expressionUrl(req.getExpressionUrl()).build();
|
||||
userEmojiDao.save(insert);
|
||||
return ApiResult.success(IdRespVO.id(insert.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Long id, Long uid) {
|
||||
UserEmoji userEmoji = userEmojiDao.getById(id);
|
||||
AssertUtil.isNotEmpty(userEmoji, "表情不能为空");
|
||||
AssertUtil.equal(userEmoji.getUid(), uid, "小黑子,别人表情不是你能删的");
|
||||
userEmojiDao.removeById(id);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.abin.mallchat.common.common.event.UserBlackEvent;
|
||||
import com.abin.mallchat.common.common.event.UserRegisterEvent;
|
||||
import com.abin.mallchat.common.common.utils.AssertUtil;
|
||||
import com.abin.mallchat.common.common.utils.SensitiveWordUtils;
|
||||
import com.abin.mallchat.common.common.utils.sensitiveWord.SensitiveWordBs;
|
||||
import com.abin.mallchat.common.user.dao.BlackDao;
|
||||
import com.abin.mallchat.common.user.dao.ItemConfigDao;
|
||||
import com.abin.mallchat.common.user.dao.UserBackpackDao;
|
||||
@@ -63,6 +63,8 @@ public class UserServiceImpl implements UserService {
|
||||
private BlackDao blackDao;
|
||||
@Autowired
|
||||
private UserSummaryCache userSummaryCache;
|
||||
@Autowired
|
||||
private SensitiveWordBs sensitiveWordBs;
|
||||
|
||||
@Override
|
||||
public UserInfoResp getUserInfo(Long uid) {
|
||||
@@ -76,7 +78,7 @@ public class UserServiceImpl implements UserService {
|
||||
public void modifyName(Long uid, ModifyNameReq req) {
|
||||
//判断名字是不是重复
|
||||
String newName = req.getName();
|
||||
AssertUtil.isFalse(SensitiveWordUtils.hasSensitiveWord(newName), "名字中包含敏感词,请重新输入"); // 判断名字中有没有敏感词
|
||||
AssertUtil.isFalse(sensitiveWordBs.hasSensitiveWord(newName), "名字中包含敏感词,请重新输入"); // 判断名字中有没有敏感词
|
||||
User oldUser = userDao.getByName(newName);
|
||||
AssertUtil.isEmpty(oldUser, "名字已经被抢占了,请换一个哦~~");
|
||||
//判断改名卡够不够
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.abin.mallchat.custom.user.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.abin.mallchat.common.common.annotation.FrequencyControl;
|
||||
import com.abin.mallchat.common.common.config.ThreadPoolConfig;
|
||||
@@ -20,6 +19,8 @@ import com.abin.mallchat.custom.user.service.LoginService;
|
||||
import com.abin.mallchat.custom.user.service.WebSocketService;
|
||||
import com.abin.mallchat.custom.user.service.adapter.WSAdapter;
|
||||
import com.abin.mallchat.custom.user.websocket.NettyUtil;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import lombok.SneakyThrows;
|
||||
@@ -32,9 +33,13 @@ import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -47,12 +52,18 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
@Slf4j
|
||||
public class WebSocketServiceImpl implements WebSocketService {
|
||||
|
||||
private static final Duration EXPIRE_TIME = Duration.ofHours(1);
|
||||
private static final Long MAX_MUM_SIZE = 10000L;
|
||||
|
||||
private static final AtomicInteger CODE = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* 所有请求登录的code与channel关系
|
||||
* todo 有可能有人请求了二维码,就是不登录,留个坑,之后处理
|
||||
*/
|
||||
private static final ConcurrentHashMap<Integer, Channel> WAIT_LOGIN_MAP = new ConcurrentHashMap<>();
|
||||
private static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
|
||||
.expireAfterWrite(EXPIRE_TIME)
|
||||
.maximumSize(MAX_MUM_SIZE)
|
||||
.build();
|
||||
/**
|
||||
* 所有已连接的websocket连接列表和一些额外参数
|
||||
*/
|
||||
@@ -66,7 +77,6 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
return ONLINE_WS_MAP;
|
||||
}
|
||||
|
||||
public static final int EXPIRE_SECONDS = 60 * 60;
|
||||
@Autowired
|
||||
private WxMpService wxMpService;
|
||||
@Autowired
|
||||
@@ -95,7 +105,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
//生成随机不重复的登录码
|
||||
Integer code = generateLoginCode(channel);
|
||||
//请求微信接口,获取登录码地址
|
||||
WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, EXPIRE_SECONDS);
|
||||
WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) EXPIRE_TIME.getSeconds());
|
||||
//返回给前端
|
||||
sendMsg(channel, WSAdapter.buildLoginResp(wxMpQrCodeTicket));
|
||||
}
|
||||
@@ -107,12 +117,11 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
* @return
|
||||
*/
|
||||
private Integer generateLoginCode(Channel channel) {
|
||||
int code;
|
||||
do {
|
||||
code = RandomUtil.randomInt(Integer.MAX_VALUE);
|
||||
} while (WAIT_LOGIN_MAP.contains(code)
|
||||
|| Objects.nonNull(WAIT_LOGIN_MAP.putIfAbsent(code, channel)));
|
||||
return code;
|
||||
CODE.getAndIncrement();
|
||||
} while (WAIT_LOGIN_MAP.asMap().containsKey(CODE.get())
|
||||
|| Objects.isNull(WAIT_LOGIN_MAP.get(CODE.get(), c -> channel)));
|
||||
return CODE.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,12 +208,12 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
@Override
|
||||
public Boolean scanLoginSuccess(Integer loginCode, User user, String token) {
|
||||
//发送消息
|
||||
Channel channel = WAIT_LOGIN_MAP.get(loginCode);
|
||||
Channel channel = WAIT_LOGIN_MAP.getIfPresent(loginCode);
|
||||
if (Objects.isNull(channel)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
//移除code
|
||||
WAIT_LOGIN_MAP.remove(loginCode);
|
||||
WAIT_LOGIN_MAP.invalidate(loginCode);
|
||||
//用户登录
|
||||
loginSuccess(channel, user, token);
|
||||
return true;
|
||||
@@ -212,7 +221,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
|
||||
@Override
|
||||
public Boolean scanSuccess(Integer loginCode) {
|
||||
Channel channel = WAIT_LOGIN_MAP.get(loginCode);
|
||||
Channel channel = WAIT_LOGIN_MAP.getIfPresent(loginCode);
|
||||
if (Objects.isNull(channel)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
@@ -287,4 +296,6 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
reentrantLock.unlock();
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.abin.mallchat.custom.user.websocket;
|
||||
|
||||
import cn.hutool.core.net.url.UrlBuilder;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
@@ -7,13 +8,23 @@ import io.netty.handler.codec.http.HttpHeaders;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof FullHttpRequest) {
|
||||
HttpHeaders headers = ((FullHttpRequest) msg).headers();
|
||||
FullHttpRequest request = (FullHttpRequest) msg;
|
||||
UrlBuilder urlBuilder = UrlBuilder.ofHttp(request.uri());
|
||||
|
||||
// 获取token参数
|
||||
String token = Optional.ofNullable(urlBuilder.getQuery()).map(k->k.get("token")).map(CharSequence::toString).orElse("");
|
||||
NettyUtil.setAttr(ctx.channel(), NettyUtil.TOKEN, token);
|
||||
|
||||
// 获取请求路径
|
||||
request.setUri(urlBuilder.getPath().toString());
|
||||
HttpHeaders headers = request.headers();
|
||||
String ip = headers.get("X-Real-IP");
|
||||
if (StringUtils.isEmpty(ip)) {//如果没经过nginx,就直接获取远端地址
|
||||
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
|
||||
@@ -21,7 +32,10 @@ public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
NettyUtil.setAttr(ctx.channel(), NettyUtil.IP, ip);
|
||||
ctx.pipeline().remove(this);
|
||||
ctx.fireChannelRead(request);
|
||||
}else
|
||||
{
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.abin.mallchat.custom.user.websocket;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.AttributeKey;
|
||||
|
||||
@@ -15,6 +16,7 @@ public class NettyUtil {
|
||||
public static AttributeKey<String> TOKEN = AttributeKey.valueOf("token");
|
||||
public static AttributeKey<String> IP = AttributeKey.valueOf("ip");
|
||||
public static AttributeKey<Long> UID = AttributeKey.valueOf("uid");
|
||||
public static AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY = AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");
|
||||
|
||||
public static <T> void setAttr(Channel channel, AttributeKey<T> attributeKey, T data) {
|
||||
Attribute<T> attr = channel.attr(attributeKey);
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
@@ -88,7 +89,7 @@ public class NettyWebSocketServer {
|
||||
* 4. WebSocketServerProtocolHandler 核心功能是把 http协议升级为 ws 协议,保持长连接;
|
||||
* 是通过一个状态码 101 来切换的
|
||||
*/
|
||||
pipeline.addLast(new WebSocketHandshakeHandler());
|
||||
pipeline.addLast(new WebSocketServerProtocolHandler("/"));
|
||||
// 自定义handler ,处理业务逻辑
|
||||
pipeline.addLast(new NettyWebSocketServerHandler());
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
|
||||
|
||||
private WebSocketService webSocketService;
|
||||
|
||||
// 当web客户端连接后,触发该方法
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
// getService().connect(ctx.channel());
|
||||
this.webSocketService = getService();
|
||||
}
|
||||
|
||||
// 客户端离线
|
||||
@@ -45,7 +47,7 @@ public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<Tex
|
||||
}
|
||||
|
||||
private void userOffLine(ChannelHandlerContext ctx) {
|
||||
getService().removed(ctx.channel());
|
||||
this.webSocketService.removed(ctx.channel());
|
||||
ctx.channel().close();
|
||||
}
|
||||
|
||||
@@ -65,11 +67,11 @@ public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<Tex
|
||||
// 关闭用户的连接
|
||||
userOffLine(ctx);
|
||||
}
|
||||
} else if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
|
||||
getService().connect(ctx.channel());
|
||||
} else if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
|
||||
this.webSocketService.connect(ctx.channel());
|
||||
String token = NettyUtil.getAttr(ctx.channel(), NettyUtil.TOKEN);
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
getService().authorize(ctx.channel(), new WSAuthorize(token));
|
||||
this.webSocketService.authorize(ctx.channel(), new WSAuthorize(token));
|
||||
}
|
||||
}
|
||||
super.userEventTriggered(ctx, evt);
|
||||
@@ -93,13 +95,13 @@ public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<Tex
|
||||
WSReqTypeEnum wsReqTypeEnum = WSReqTypeEnum.of(wsBaseReq.getType());
|
||||
switch (wsReqTypeEnum) {
|
||||
case LOGIN:
|
||||
getService().handleLoginReq(ctx.channel());
|
||||
this.webSocketService.handleLoginReq(ctx.channel());
|
||||
log.info("请求二维码 = " + msg.text());
|
||||
break;
|
||||
case HEARTBEAT:
|
||||
break;
|
||||
case AUTHORIZE:
|
||||
getService().authorize(ctx.channel(), JSONUtil.toBean(wsBaseReq.getData(), WSAuthorize.class));
|
||||
this.webSocketService.authorize(ctx.channel(), JSONUtil.toBean(wsBaseReq.getData(), WSAuthorize.class));
|
||||
log.info("主动认证 = " + msg.text());
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.abin.mallchat.custom.user.websocket;
|
||||
|
||||
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
|
||||
public class WebSocketHandshakeHandler extends ChannelInboundHandlerAdapter {
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof FullHttpRequest) {
|
||||
FullHttpRequest request = (FullHttpRequest) msg;
|
||||
String token = request.headers().get("Sec-Websocket-Protocol");
|
||||
NettyUtil.setAttr(ctx.channel(), NettyUtil.TOKEN, token);
|
||||
// 构建WebSocket握手处理器
|
||||
WebSocketServerHandshakerFactory handshakeFactory = new WebSocketServerHandshakerFactory(
|
||||
request.uri(), token, false);
|
||||
WebSocketServerHandshaker handshake = handshakeFactory.newHandshaker(request);
|
||||
final ChannelFuture handshakeFuture = handshake.handshake(ctx.channel(), request);
|
||||
ctx.pipeline().remove(this);
|
||||
handshakeFuture.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
if (!future.isSuccess()) {
|
||||
ctx.fireExceptionCaught(future.cause());
|
||||
} else {
|
||||
// 手动触发WebSocket握手状态事件
|
||||
ctx.fireUserEventTriggered(
|
||||
WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.abin.mallchat.common.common.algorithm.ac;
|
||||
package com.abin.mallchat.custom.ac;
|
||||
|
||||
import com.abin.mallchat.common.common.algorithm.ac.ACTrie;
|
||||
import com.abin.mallchat.common.common.algorithm.ac.MatchResult;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.abin.mallchat.common.common.algorithm.ac;
|
||||
package com.abin.mallchat.custom.ac;
|
||||
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
@@ -6,7 +6,6 @@ import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.auth0.jwt.interfaces.JWTVerifier;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -18,27 +17,18 @@ public class CreateTokenTest {
|
||||
@Test
|
||||
public void create(){
|
||||
String token = JWT.create()
|
||||
.withClaim("uid", 123L) // 只存一个uid信息,其他的自己去redis查
|
||||
.withClaim("uid", 10004L) // 只存一个uid信息,其他的自己去redis查
|
||||
.withClaim("createTime", new Date())
|
||||
.sign(Algorithm.HMAC256("dsfsdfsdfsdfsd")); // signature
|
||||
log.info("生成的token为 {}",token);
|
||||
|
||||
|
||||
try {
|
||||
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("dsfsdfsdfsdfsd")).build();
|
||||
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("dsfsdfsdfsdfsc")).build();
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
log.info(jwt.getClaims().toString());
|
||||
} catch (Exception e) {
|
||||
log.info("decode error,token:{}", token, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyToken(){
|
||||
String token = JWT.create()
|
||||
.withClaim("uid", 1) // 只存一个uid信息,其他的自己去redis查
|
||||
.withClaim("createTime", new Date())
|
||||
.sign(Algorithm.HMAC256("dsfsdfsdfsdfsd")); // signature
|
||||
log.info("生成的token为{}",token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.abin.mallchat.custom.spring;
|
||||
|
||||
import com.abin.mallchat.custom.chat.service.WeChatMsgOperationService;
|
||||
import com.abin.mallchat.custom.chat.service.impl.ChatServiceImpl;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Description: 微信模板测试
|
||||
* Author: <a href="https://github.com/zongzibinbin">abin</a>
|
||||
* Date: 2023-07-06
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
public class WXTemplate {
|
||||
|
||||
@Autowired
|
||||
private WeChatMsgOperationService chatMsgOperationService;
|
||||
@Autowired
|
||||
private ChatServiceImpl chatService;
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
chatMsgOperationService.publishChatMsgToWeChatUser(1L, Collections.singletonList(10008L), "你家规下去");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user