This commit is contained in:
N1KO 2024-12-13 15:45:09 +08:00
commit cf399b316e
32 changed files with 1531 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@ -0,0 +1,73 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="INNER_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="METHOD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
</value>
</option>
<option name="FIELD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="IGNORE_DEPRECATED" value="false" />
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="description:,author:,date:,date" />
</inspection_tool>
<inspection_tool class="JavadocDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ADDITIONAL_TAGS" value="description:,author:,date:,date" />
</inspection_tool>
<inspection_tool class="MissingJavadoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="PACKAGE_SETTINGS">
<Options>
<option name="ENABLED" value="false" />
</Options>
</option>
<option name="MODULE_SETTINGS">
<Options>
<option name="ENABLED" value="false" />
</Options>
</option>
<option name="TOP_LEVEL_CLASS_SETTINGS">
<Options>
<option name="ENABLED" value="false" />
</Options>
</option>
<option name="INNER_CLASS_SETTINGS">
<Options>
<option name="ENABLED" value="false" />
</Options>
</option>
<option name="METHOD_SETTINGS">
<Options>
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
<option name="ENABLED" value="false" />
</Options>
</option>
<option name="FIELD_SETTINGS">
<Options>
<option name="ENABLED" value="false" />
</Options>
</option>
</inspection_tool>
<inspection_tool class="SerializableHasSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

20
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ComposerSettings">
<execution />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="zulu-11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml generated Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

130
pom.xml Normal file
View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.baogutang.demo</groupId>
<artifactId>baogutang-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.1-jre</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>templates/fonts/**</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>templates/fonts/**</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,24 @@
package top.baogutang.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 14:48
*/
@Slf4j
@SpringBootApplication(scanBasePackages = {"top.baogutang.demo.*"})
@EnableAsync
public class BaoGuTangDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BaoGuTangDemoApplication.class, args);
log.info("==============DEMO服务启动成功!!=================");
}
}

View File

@ -0,0 +1,70 @@
package top.baogutang.demo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.net.UnknownHostException;
import java.util.TimeZone;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 10:56
*/
@Configuration
@Slf4j
public class RedisConfig {
/**
* 编写自定义的 redisTemplate
* 这是一个比较固定的模板
*/
@SuppressWarnings("all")
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 为了开发方便直接使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// Json 配置序列化
// 使用 jackson 解析任意的对象
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 使用 objectMapper 进行转义
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.setTimeZone(TimeZone.getDefault());
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 采用 String 的序列化方式
template.setKeySerializer(stringRedisSerializer);
// Hash key 采用 String 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 采用 jackson 的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
// Hash value 采用 String 的序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
// 把所有的配置 set template
template.afterPropertiesSet();
return template;
}
}

View File

@ -0,0 +1,21 @@
package top.baogutang.demo.constants;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 13:38
*/
public class MsgConstant {
/**
* 消费端策略异常时再次消费
*/
public static final String CONSUMER_STRATEGY_RECONSUME = "RECONSUME";
/**
* 调整支持多topic订阅时兼容旧的项目未设置handler的topic时使用默认值
*/
public static final String DEFAULT_TOPIC_NAME = "DEFAULT_TOPIC_NAME";
}

View File

@ -0,0 +1,36 @@
package top.baogutang.demo.constants;
import com.google.common.base.Joiner;
import top.baogutang.demo.constants.enums.QueueType;
import java.util.Arrays;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 13:44
*/
public class RedisKey {
private RedisKey() {
// empty private constructor
}
private static final String MODULE = "baogutang-demo";
private static final String SEPARATOR = ":";
/**
* 影像推送key
*
* @param queueType queueType
* @param userId userId
* @param messageKey messageKey
* @return key
*/
public static String getImagePushMsgLockKey(QueueType queueType, Long userId, String messageKey) {
return Joiner.on(SEPARATOR).join(Arrays.asList(MODULE, queueType.getTopicName(), userId, messageKey));
}
}

View File

@ -0,0 +1,17 @@
package top.baogutang.demo.constants.enums;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 11:42
*/
public enum QueuePlatform {
REDIS,
KAFKA,
;
}

View File

@ -0,0 +1,47 @@
package top.baogutang.demo.constants.enums;
import lombok.Getter;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 10:56
*/
@Getter
public enum QueueType {
/**
* 影像推送
*/
IMAGE_PUSH("IMAGE_PUSH", "影像推送"),
;
private final String topicName;
private final String topicDesc;
QueueType(String topicName, String topicDesc) {
this.topicName = topicName;
this.topicDesc = topicDesc;
}
public static QueueType[] allTypes() {
return values();
}
public static QueueType get(String topicName) {
for (QueueType value : values()) {
if (value.getTopicName().equals(topicName)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,66 @@
package top.baogutang.demo.message;
import top.baogutang.demo.constants.enums.QueueType;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 10:56
*/
public interface IQueueMsg<T> extends Serializable {
/**
* getMsgType
*
* @return QueueType
*/
QueueType getMsgType();
/**
* body
*
* @return body
*/
T getMsgBody();
/**
* getMsgBizKey
*
* @return getMsgBizKey
*/
String getMsgBizKey();
/**
* getDelayTime
*
* @return getDelayTime
*/
long getDelayTime();
/**
* setDelayTime
*
* @param delayTime delayTime
*/
void setDelayTime(long delayTime);
/**
* getTimeUnit
*
* @return getTimeUnit
*/
TimeUnit getTimeUnit();
/**
* setTimeUnit
*
* @param timeUnit timeUnit
*/
void setTimeUnit(TimeUnit timeUnit);
}

View File

@ -0,0 +1,22 @@
package top.baogutang.demo.message;
import lombok.Data;
import java.io.Serializable;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 13:48
*/
@Data
public class ImagePushMessage implements Serializable {
private static final long serialVersionUID = -870177103586251908L;
private Long userId;
// 这个类就是具体的消息类需要什么字段就加什么字段
}

View File

@ -0,0 +1,101 @@
package top.baogutang.demo.message;
import lombok.Data;
import top.baogutang.demo.constants.enums.QueueType;
import java.util.concurrent.TimeUnit;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 10:56
*/
@Data
public class QueueMsg<T> implements IQueueMsg<T> {
private static final long serialVersionUID = 8969365246492490122L;
/**
* 消息类型
*/
private QueueType msgType;
/**
* 消息内容
*/
private T msgBody;
/**
* 消息业务key
*/
private String msgBizKey;
/**
* 延迟时间
*/
private long delayTime = 0;
/**
* 延迟时间单位
*/
private TimeUnit timeUnit = TimeUnit.SECONDS;
public QueueMsg() {
}
public QueueMsg(QueueType msgType, T msgBody, String msgBizKey) {
this.msgType = msgType;
this.msgBody = msgBody;
this.msgBizKey = msgBizKey;
}
public QueueMsg(QueueType msgType, T msgBody) {
this.msgType = msgType;
this.msgBody = msgBody;
}
@Override
public QueueType getMsgType() {
return msgType;
}
@Override
public T getMsgBody() {
return msgBody;
}
@Override
public String getMsgBizKey() {
return msgBizKey;
}
@Override
public long getDelayTime() {
return delayTime;
}
@Override
public void setDelayTime(long delayTime) {
this.delayTime = delayTime;
}
@Override
public TimeUnit getTimeUnit() {
return timeUnit;
}
@Override
public void setTimeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
}
}

View File

@ -0,0 +1,36 @@
package top.baogutang.demo.message;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 10:56
*/
@Data
@Builder
public class SendResult implements Serializable {
private static final long serialVersionUID = -3562968976733007349L;
/**
* 发送结果
*/
private boolean success;
/**
* 已发送消息的ID(如果有)
*/
private String messageId;
/**
* 已发送消息的主题
*/
private String topic;
}

View File

@ -0,0 +1,39 @@
package top.baogutang.demo.message.consumer;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 13:41
*/
public abstract class AbstractQueueMessageConsumer implements IQueueMessageConsumer {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 重复消息处理
*
* @param key key
* @return res
*/
protected boolean duplicateMsg(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, 1, 300, TimeUnit.SECONDS);
return Boolean.FALSE.equals(flag);
}
/**
* 移除lock
*
* @param key key
*/
protected void removeLock(String key) {
redisTemplate.delete(key);
}
}

View File

@ -0,0 +1,31 @@
package top.baogutang.demo.message.consumer;
import top.baogutang.demo.constants.MsgConstant;
import top.baogutang.demo.message.IQueueMsg;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 11:04
*/
public interface IQueueMessageConsumer {
/**
* 消费消息
* @param queueMsg 消息体
*/
<T> void consume(IQueueMsg<T> queueMsg);
/**
* getTopicName
*
* @return topic
*/
default String getTopicName() {
return MsgConstant.DEFAULT_TOPIC_NAME;
}
}

View File

@ -0,0 +1,37 @@
package top.baogutang.demo.message.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 14:37
*/
@Slf4j
@Component
public class MessageConsumerFactory implements ApplicationListener<ApplicationReadyEvent> {
public final Map<String, IQueueMessageConsumer> consumerMap = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ConfigurableApplicationContext applicationContext = event.getApplicationContext();
Map<String, IQueueMessageConsumer> beansOfType = applicationContext.getBeansOfType(IQueueMessageConsumer.class);
beansOfType.forEach((key, value) -> consumerMap.put(value.getTopicName(), value));
log.info(">>>>>>>>>>queue message consumer inject complete<<<<<<<<<<");
}
public IQueueMessageConsumer getConsumer(String topicName) {
return consumerMap.get(topicName);
}
}

View File

@ -0,0 +1,49 @@
package top.baogutang.demo.message.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import top.baogutang.demo.constants.RedisKey;
import top.baogutang.demo.constants.enums.QueueType;
import top.baogutang.demo.message.IQueueMsg;
import top.baogutang.demo.message.ImagePushMessage;
import top.baogutang.demo.utils.JacksonUtil;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 13:47
*/
@Slf4j
@Component
public class RedisImagePushQueueMessageConsumer extends AbstractQueueMessageConsumer {
@Override
public <T> void consume(IQueueMsg<T> queueMsg) {
log.info("RedisImagePushQueueMessageConsumer received message:{}", JacksonUtil.toJson(queueMsg));
ImagePushMessage imagePushMessage = (ImagePushMessage) queueMsg.getMsgBody();
String lockKey = RedisKey.getImagePushMsgLockKey(queueMsg.getMsgType(), imagePushMessage.getUserId(), queueMsg.getMsgBizKey());
if (duplicateMsg(lockKey)) {
log.info("RedisImagePushQueueMessageConsumer received duplicate message:{}", lockKey);
return;
}
try {
// TODO: process message
// ...
} catch (Exception e) {
log.error("RedisImagePushQueueMessageConsumer process message:{} error:{}", JacksonUtil.toJson(queueMsg), e.getMessage(), e);
} finally {
removeLock(lockKey);
}
}
@Override
public String getTopicName() {
return QueueType.IMAGE_PUSH.getTopicName();
}
}

View File

@ -0,0 +1,28 @@
package top.baogutang.demo.message.producer;
import top.baogutang.demo.constants.enums.QueuePlatform;
import top.baogutang.demo.message.IQueueMsg;
import top.baogutang.demo.message.SendResult;
import java.io.Serializable;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 10:56
*/
public interface IQueueMessageProducer {
QueuePlatform getPlatform();
/**
* 发送延迟消息
* @param topic 队列
* @param queueMsg 消息
* @return 发送结果
*/
<T> SendResult sendDelayMessage(String topic, IQueueMsg<T> queueMsg);
}

View File

@ -0,0 +1,38 @@
package top.baogutang.demo.message.producer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import top.baogutang.demo.constants.enums.QueuePlatform;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 14:27
*/
@Slf4j
@Component
public class MessageProducerFactory implements ApplicationListener<ApplicationReadyEvent> {
public final Map<QueuePlatform, IQueueMessageProducer> producerMap = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ConfigurableApplicationContext applicationContext = event.getApplicationContext();
Map<String, IQueueMessageProducer> beansOfType = applicationContext.getBeansOfType(IQueueMessageProducer.class);
beansOfType.forEach((key, value) -> producerMap.put(value.getPlatform(), value));
log.info(">>>>>>>>>>queue message producer inject complete<<<<<<<<<<");
}
public IQueueMessageProducer getProducer(QueuePlatform platform) {
return producerMap.get(platform);
}
}

View File

@ -0,0 +1,50 @@
package top.baogutang.demo.message.producer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.baogutang.demo.constants.enums.QueuePlatform;
import top.baogutang.demo.message.IQueueMsg;
import top.baogutang.demo.message.SendResult;
import top.baogutang.demo.utils.JacksonUtil;
import javax.annotation.Resource;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 11:43
*/
@Slf4j
@Component
public class RedisQueueMessageProducer implements IQueueMessageProducer {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public QueuePlatform getPlatform() {
return QueuePlatform.REDIS;
}
@Override
public <T> SendResult sendDelayMessage(String topic, IQueueMsg<T> queueMsg) {
long delayTime = System.currentTimeMillis() + queueMsg.getTimeUnit().toMillis(queueMsg.getDelayTime());
Boolean addResult = null;
try {
addResult = redisTemplate.opsForZSet().add(topic, queueMsg, delayTime);
} catch (Exception e) {
log.error(">>>>>>>>>>send delay message error:{} topic:{},message:{}<<<<<<<<<<", e.getMessage(), topic, JacksonUtil.toJson(queueMsg), e);
return SendResult.builder()
.success(Boolean.FALSE)
.topic(topic)
.build();
}
return SendResult.builder()
.success(Boolean.TRUE.equals(addResult))
.topic(topic)
.build();
}
}

View File

@ -0,0 +1,13 @@
package top.baogutang.demo.service;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 15:00
*/
public interface IConsumeMessageService {
<T> void consumeRedisMessage(String messageTopic);
}

View File

@ -0,0 +1,15 @@
package top.baogutang.demo.service;
import java.util.concurrent.TimeUnit;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 14:40
*/
public interface ISendMessageService {
<T> void sendMessage(T msgBody, Long delayTime, TimeUnit timeUnit, String messageTopic);
}

View File

@ -0,0 +1,52 @@
package top.baogutang.demo.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import top.baogutang.demo.message.IQueueMsg;
import top.baogutang.demo.message.consumer.MessageConsumerFactory;
import top.baogutang.demo.service.IConsumeMessageService;
import javax.annotation.Resource;
import java.util.Set;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 15:00
*/
@Slf4j
@Service
public class ConsumeMessageServiceImpl implements IConsumeMessageService {
@Resource
private MessageConsumerFactory messageConsumerFactory;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@SuppressWarnings("unchecked")
public <T> void consumeRedisMessage(String messageTopic) {
while (true) {
long currentTime = System.currentTimeMillis();
// 拉取到期消息
Set<Object> queueMsgSet = redisTemplate.opsForZSet().rangeByScore(messageTopic, 0, currentTime);
if (CollectionUtils.isEmpty(queueMsgSet)) {
return;
}
queueMsgSet.forEach(queueMsgObj -> {
IQueueMsg<T> queueMsg = (IQueueMsg<T>) queueMsgObj;
messageConsumerFactory.getConsumer(queueMsg.getMsgType().getTopicName())
.consume(queueMsg);
// 删除已处理消息
redisTemplate.opsForZSet().remove(messageTopic, queueMsgObj);
});
}
}
}

View File

@ -0,0 +1,46 @@
package top.baogutang.demo.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import top.baogutang.demo.constants.enums.QueuePlatform;
import top.baogutang.demo.constants.enums.QueueType;
import top.baogutang.demo.message.IQueueMsg;
import top.baogutang.demo.message.QueueMsg;
import top.baogutang.demo.message.SendResult;
import top.baogutang.demo.message.producer.IQueueMessageProducer;
import top.baogutang.demo.message.producer.MessageProducerFactory;
import top.baogutang.demo.service.ISendMessageService;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 14:40
*/
@Slf4j
@Service
public class SendMessageServiceImpl implements ISendMessageService {
@Resource
private MessageProducerFactory messageProducerFactory;
public <T> void sendMessage(T msgBody, Long delayTime, TimeUnit timeUnit,String messageTopic) {
// TODO 业务消息key可自定义
String msgKey = UUID.randomUUID().toString();
IQueueMsg<T> queueMsg = new QueueMsg<>(QueueType.IMAGE_PUSH, msgBody, msgKey);
queueMsg.setDelayTime(delayTime);
queueMsg.setTimeUnit(timeUnit);
IQueueMessageProducer producer = messageProducerFactory.getProducer(QueuePlatform.REDIS);
SendResult sendResult = producer.sendDelayMessage(messageTopic, queueMsg);
if (Objects.isNull(sendResult) || !Boolean.TRUE.equals(sendResult.isSuccess())) {
// TODO 消息发送不成功逻辑处理
}
}
}

View File

@ -0,0 +1,191 @@
package top.baogutang.demo.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.text.SimpleDateFormat;
import java.util.*;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 11:50
*/
@Slf4j
public class JacksonUtil {
private JacksonUtil() {
// empty private constructor
}
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
// yyyy-MM-dd HH:mm:ss
OBJECT_MAPPER.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_FORMAT));
// JavaTimeModule Java 8
OBJECT_MAPPER.registerModule(new JavaTimeModule());
//
OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// ObjectMapper null
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// ObjectMapper Bean ()
OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// ObjectMapper JSON Java
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* json
*
* @param content
* @return json
*/
public static String toJson(Object content) {
String result = StringUtils.EMPTY;
if (Objects.isNull(content)) {
return result;
}
try {
result = OBJECT_MAPPER.writeValueAsString(content);
} catch (Exception e) {
log.error(">>>>>>>>>>parse object to json fail:{}<<<<<<<<<<", e.getMessage());
}
return result;
}
/**
*
*
* @param content
* @param clazz
* @param <T>
* @return
*/
public static <T> T fromJson(String content, Class<T> clazz) {
T result = null;
if (StringUtils.isBlank(content)) {
return result;
}
try {
result = OBJECT_MAPPER.readValue(content, clazz);
} catch (Exception e) {
log.error(">>>>>>>>>>parse json to object fail:{}<<<<<<<<<<", e.getMessage());
}
return result;
}
/**
*
*
* @param content
* @param type
* @param <T>
* @return
*/
public static <T> T fromJson(String content, TypeReference<T> type) {
T result = null;
if (StringUtils.isBlank(content)) {
return result;
}
try {
result = OBJECT_MAPPER.readValue(content, type);
} catch (Exception e) {
log.error(">>>>>>>>>>parse json to object fail:{}<<<<<<<<<<", e.getMessage());
}
return result;
}
/**
* jsonList
*
* @param content
* @param clazz
* @param <T>
* @return
*/
public static <T> List<T> jsonToList(String content, Class<T> clazz) {
List<T> list = new ArrayList<>();
if (StringUtils.isBlank(content) || clazz == null) {
return list;
}
try {
list = OBJECT_MAPPER.readValue(content, new TypeReference<List<T>>() {
});
} catch (Exception e) {
log.error(">>>>>>>>>>parse json to list fail:{}<<<<<<<<<<", e.getMessage());
}
return list;
}
/**
* jsonMap
*
* @param content
* @param keyType key
* @param valueType value
* @param <K> Class<K>
* @param <V> Class<V>
* @return Map
*/
public static <K, V> Map<K, V> jsonToMap(String content, Class<K> keyType, Class<V> valueType) {
Map<K, V> result = new HashMap<>();
if (StringUtils.isBlank(content) || Objects.isNull(keyType) || Objects.isNull(valueType)) {
return result;
}
try {
result = OBJECT_MAPPER.readValue(content, new TypeReference<>() {
});
} catch (Exception e) {
log.error(">>>>>>>>>>parse json to map fail:{}<<<<<<<<<<", e.getMessage());
}
return result;
}
public static <K, V> Map<K, V> beanToMap(Object content, Class<K> keyType, Class<V> valueType) {
Map<K, V> result = new HashMap<>();
if (Objects.isNull(content) || Objects.isNull(keyType) || Objects.isNull(valueType)) {
return result;
}
try {
result = OBJECT_MAPPER.convertValue(content, new TypeReference<>() {
});
} catch (Exception e) {
log.error(">>>>>>>>>>parse json to map fail:{}<<<<<<<<<<", e.getMessage());
}
return result;
}
/**
* jsonJsonNode
*
* @param content
* @return JsonNode
*/
public static JsonNode jsonToNode(String content) {
JsonNode jsonNode = null;
if (StringUtils.isBlank(content)) {
return null;
}
try {
jsonNode = OBJECT_MAPPER.readTree(content);
} catch (Exception e) {
log.error(">>>>>>>>>>parse json to node fail:{}<<<<<<<<<<", e.getMessage());
}
return jsonNode;
}
}

View File

@ -0,0 +1,36 @@
server:
port: 8106
spring:
profiles:
active: ${SPRING_ACTIVE_PROFILE:local}
application:
name: baogutang-demo
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://117.72.78.133:3306/baogutang-music?characterEncoding=UTF-8
username: baogutang-music
password: xCcFfY3zXh8sC28e
hikari:
max-lifetime: 60000
redis:
host: 117.72.78.133
port: 6379
password: admin@pwdpwd
servlet:
multipart:
max-file-size: 2048MB
max-request-size: 32MB
resources:
static-locations: classpath:/templates/
thread:
pool:
core: 10
max: 10
alive: 10
capacity: 200

View File

@ -0,0 +1,32 @@
package top.baogutang.demo.service;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 15:26
*/
@Slf4j
@SpringBootTest
class ConsumerServiceTest {
@Resource
private IConsumeMessageService consumeMessageService;
private static final String MESSAGE_TOPIC = "baogutang.demo.message";
@Test
void test_consume_message() throws InterruptedException {
consumeMessageService.consumeRedisMessage(MESSAGE_TOPIC);
Thread.sleep(10000);
}
}

View File

@ -0,0 +1,34 @@
package top.baogutang.demo.service;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.baogutang.demo.message.ImagePushMessage;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
*
* @description:
*
* @author: N1KO
* @date: 2024/12/13 : 15:28
*/
@Slf4j
@SpringBootTest
class SendMessageServiceTest {
@Resource
private ISendMessageService sendMessageService;
private static final String MESSAGE_TOPIC = "baogutang.demo.message";
@Test
void test_send_message() {
ImagePushMessage imagePushMessage = new ImagePushMessage();
imagePushMessage.setUserId(1L);
sendMessageService.sendMessage(imagePushMessage, 2L, TimeUnit.SECONDS, MESSAGE_TOPIC);
}
}