mirror of
https://gitee.com/lab1024/smart-admin.git
synced 2025-11-12 05:33:48 +08:00
【V3.5.0】1、【新增】轻量级定时任务 SmartJob;2、【新增】站内信;3、【新增】个人中心;4、【新增】岗位管理;5、【优化】部门员工管理
This commit is contained in:
@@ -113,6 +113,11 @@
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-text</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-mock</artifactId>
|
||||
@@ -264,6 +269,14 @@
|
||||
<version>${smartdb.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-data-27</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class DictValueVoSerializer extends JsonSerializer<String> {
|
||||
List<DictValueVO> dictValueVOList = Lists.newArrayList();
|
||||
valueCodeList.forEach(e->{
|
||||
if(StringUtils.isNotBlank(e)){
|
||||
DictValueVO dictValueVO = dictCacheService.selectValueByValueCode(value);
|
||||
DictValueVO dictValueVO = dictCacheService.selectValueByValueCode(e);
|
||||
if(dictValueVO != null){
|
||||
dictValueVOList.add(dictValueVO);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.lab1024.sa.base.common.json.serializer.enumeration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 枚举类 序列化 注解
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024年6月29日
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JacksonAnnotationsInside
|
||||
@JsonSerialize(using = EnumSerializer.class, nullsUsing = EnumSerializer.class)
|
||||
public @interface EnumSerialize {
|
||||
|
||||
Class<? extends BaseEnum> value();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.lab1024.sa.base.common.json.serializer.enumeration;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.BeanProperty;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||
import net.lab1024.sa.base.common.constant.StringConst;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
import net.lab1024.sa.base.common.util.SmartEnumUtil;
|
||||
import net.lab1024.sa.base.common.util.SmartStringUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 枚举 序列化
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024年6月29日
|
||||
*/
|
||||
public class EnumSerializer extends JsonSerializer<Object> implements ContextualSerializer {
|
||||
|
||||
private Class<? extends BaseEnum> enumClazz;
|
||||
|
||||
@Override
|
||||
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeObject(value);
|
||||
String fieldName = gen.getOutputContext().getCurrentName() + "Desc";
|
||||
Object desc;
|
||||
// 多个枚举类 逗号分割
|
||||
if (value instanceof String && String.valueOf(value).contains(StringConst.SEPARATOR)) {
|
||||
desc = SmartStringUtil.splitConvertToIntList(String.valueOf(value), StringConst.SEPARATOR)
|
||||
.stream().map(e -> SmartEnumUtil.getEnumDescByValue(e, enumClazz)).collect(Collectors.toList());
|
||||
|
||||
} else {
|
||||
BaseEnum anEnum = SmartEnumUtil.getEnumByValue(value, enumClazz);
|
||||
desc = null != anEnum ? anEnum.getDesc() : null;
|
||||
}
|
||||
gen.writeObjectField(fieldName, desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
|
||||
EnumSerialize annotation = property.getAnnotation(EnumSerialize.class);
|
||||
if (null == annotation) {
|
||||
return prov.findValueSerializer(property.getType(), property);
|
||||
}
|
||||
enumClazz = annotation.value();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.common.constant.StringConst;
|
||||
import org.lionsoul.ip2region.xdb.Searcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -81,4 +85,39 @@ public class SmartIpUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本机第一个ip
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getLocalFirstIp() {
|
||||
List<String> list = getLocalIp();
|
||||
return list.size() > 0 ? list.get(0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本机ip
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static List<String> getLocalIp() {
|
||||
List<String> ipList = new ArrayList<>();
|
||||
try {
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
while (networkInterfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = networkInterfaces.nextElement();
|
||||
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
|
||||
while (inetAddresses.hasMoreElements()) {
|
||||
InetAddress inetAddress = inetAddresses.nextElement();
|
||||
// 排除回环地址和IPv6地址
|
||||
if (!inetAddress.isLoopbackAddress() && !inetAddress.getHostAddress().contains(StringConst.COLON)) {
|
||||
ipList.add(inetAddress.getHostAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return ipList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.lab1024.sa.base.config;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
@@ -10,14 +11,25 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.common.constant.RequestHeaderConst;
|
||||
import net.lab1024.sa.base.common.swagger.SmartOperationCustomizer;
|
||||
import net.lab1024.sa.base.constant.SwaggerTagConst;
|
||||
import org.springdoc.core.GroupedOpenApi;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.*;
|
||||
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
|
||||
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
|
||||
import org.springdoc.core.providers.JavadocProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* springdoc-openapi 配置
|
||||
*
|
||||
* nginx配置前缀时如果需要访问【/swagger-ui/index.html】需添加额外nginx配置
|
||||
* location /v3/api-docs/ {
|
||||
* proxy_pass http://127.0.0.1:11024/v3/api-docs/;
|
||||
* }
|
||||
* @Author 1024创新实验室-主任: 卓大
|
||||
* @Date 2020-03-25 22:54:46
|
||||
* @Wechat zhuoda1024
|
||||
@@ -28,6 +40,11 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
@Conditional(SystemEnvironmentConfig.class)
|
||||
public class SwaggerConfig {
|
||||
/**
|
||||
* 用于解决/swagger-ui/index.html页面ServersUrl 测试环境部署错误问题
|
||||
*/
|
||||
@Value("${springdoc.swagger-ui.server-base-url:''}")
|
||||
private String serverBaseUrl;
|
||||
|
||||
public static final String[] SWAGGER_WHITELIST = {
|
||||
"/swagger-ui/**",
|
||||
@@ -78,4 +95,37 @@ public class SwaggerConfig {
|
||||
.addOperationCustomizer(new SmartOperationCustomizer())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 以下代码可以用于设置 /swagger-ui/index.html 的serverBaseUrl
|
||||
* 如果使用knife4j则不需要
|
||||
* @param openAPI
|
||||
* @param securityParser
|
||||
* @param springDocConfigProperties
|
||||
* @param propertyResolverUtils
|
||||
* @param openApiBuilderCustomizers
|
||||
* @param serverBaseUrlCustomizers
|
||||
* @param javadocProvider
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
|
||||
SecurityService securityParser,
|
||||
SpringDocConfigProperties springDocConfigProperties,
|
||||
PropertyResolverUtils propertyResolverUtils,
|
||||
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
|
||||
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
|
||||
Optional<JavadocProvider> javadocProvider) {
|
||||
List<ServerBaseUrlCustomizer> list = Lists.newArrayList(new ServerBaseUrlCustomizer() {
|
||||
@Override
|
||||
public String customize(String baseUrl) {
|
||||
if (StringUtils.isNotBlank(serverBaseUrl)) {
|
||||
return serverBaseUrl;
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
});
|
||||
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
|
||||
propertyResolverUtils, openApiBuilderCustomizers, Optional.of(list), javadocProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ public class SwaggerTagConst {
|
||||
public static final String TABLE_COLUMN = "业务支撑-列自定义";
|
||||
|
||||
public static final String PROTECT = "业务支撑-网络安全";
|
||||
|
||||
public static final String JOB = "业务支撑-定时任务";
|
||||
|
||||
public static final String MESSAGE = "业务支撑-消息";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import net.lab1024.sa.base.common.domain.SystemEnvironment;
|
||||
import net.lab1024.sa.base.common.enumeration.SystemEnvironmentEnum;
|
||||
import net.lab1024.sa.base.common.exception.BusinessException;
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
@@ -59,10 +58,6 @@ public class GlobalExceptionHandler {
|
||||
@ResponseBody
|
||||
@ExceptionHandler({TypeMismatchException.class, BindException.class})
|
||||
public ResponseDTO<?> paramExceptionHandler(Exception e) {
|
||||
if (!systemEnvironment.isProd()) {
|
||||
log.error("全局参数异常,URL:{}", getCurrentRequestUrl(), e);
|
||||
}
|
||||
|
||||
if (e instanceof BindException) {
|
||||
if (e instanceof MethodArgumentNotValidException) {
|
||||
List<FieldError> fieldErrors = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors();
|
||||
@@ -75,7 +70,6 @@ public class GlobalExceptionHandler {
|
||||
String errorMsg = UserErrorCode.PARAM_ERROR.getMsg() + ":" + error;
|
||||
return ResponseDTO.error(UserErrorCode.PARAM_ERROR, errorMsg);
|
||||
}
|
||||
|
||||
return ResponseDTO.error(UserErrorCode.PARAM_ERROR);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,14 +86,15 @@ public class WebServerListener implements ApplicationListener<WebServerInitializ
|
||||
* 初始化reload
|
||||
*/
|
||||
private void initReload(WebServerApplicationContext applicationContext) {
|
||||
//将applicationContext转换为ConfigurableApplicationContext
|
||||
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
|
||||
|
||||
//获取BeanFactory
|
||||
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
|
||||
|
||||
//动态注册bean
|
||||
SmartReloadManager reloadManager = new SmartReloadManager(applicationContext.getBean(ReloadCommand.class), intervalSeconds);
|
||||
defaultListableBeanFactory.registerSingleton("smartReloadManager", reloadManager);
|
||||
// 将applicationContext转换为ConfigurableApplicationContext
|
||||
// ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
|
||||
//
|
||||
//
|
||||
// //获取BeanFactory
|
||||
// DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
|
||||
//
|
||||
// //动态注册bean
|
||||
// SmartReloadManager reloadManager = new SmartReloadManager(applicationContext.getBean(ReloadCommand.class), intervalSeconds);
|
||||
// defaultListableBeanFactory.registerSingleton("smartReloadManager", reloadManager);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public class ConfigService {
|
||||
/**
|
||||
* 一个简单的系统配置缓存
|
||||
*/
|
||||
private final ConcurrentHashMap<String, ConfigEntity> configCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, ConfigEntity> CONFIG_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
private ConfigDao configDao;
|
||||
@@ -52,13 +52,13 @@ public class ConfigService {
|
||||
*/
|
||||
@PostConstruct
|
||||
private void loadConfigCache() {
|
||||
configCache.clear();
|
||||
CONFIG_CACHE.clear();
|
||||
List<ConfigEntity> entityList = configDao.selectList(null);
|
||||
if (CollectionUtils.isEmpty(entityList)) {
|
||||
return;
|
||||
}
|
||||
entityList.forEach(entity -> this.configCache.put(entity.getConfigKey().toLowerCase(), entity));
|
||||
log.info("################# 系统配置缓存初始化完毕:{} ###################", configCache.size());
|
||||
entityList.forEach(entity -> this.CONFIG_CACHE.put(entity.getConfigKey().toLowerCase(), entity));
|
||||
log.info("################# 系统配置缓存初始化完毕:{} ###################", CONFIG_CACHE.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ public class ConfigService {
|
||||
if (null == configEntity) {
|
||||
return;
|
||||
}
|
||||
this.configCache.put(configEntity.getConfigKey().toLowerCase(), configEntity);
|
||||
this.CONFIG_CACHE.put(configEntity.getConfigKey().toLowerCase(), configEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +100,7 @@ public class ConfigService {
|
||||
if (StrUtil.isBlank(configKey)) {
|
||||
return null;
|
||||
}
|
||||
ConfigEntity entity = this.configCache.get(configKey.toLowerCase());
|
||||
ConfigEntity entity = this.CONFIG_CACHE.get(configKey.toLowerCase());
|
||||
return SmartBeanUtil.copy(entity, ConfigVO.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package net.lab1024.sa.base.module.support.job.api;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobMsg;
|
||||
import net.lab1024.sa.base.module.support.job.config.SmartJobAutoConfiguration;
|
||||
import net.lab1024.sa.base.module.support.job.core.SmartJob;
|
||||
import net.lab1024.sa.base.module.support.job.core.SmartJobExecutor;
|
||||
import net.lab1024.sa.base.module.support.job.core.SmartJobLauncher;
|
||||
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RTopic;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.api.listener.MessageListener;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* smart job 执行端管理
|
||||
* 分布式系统之间 用发布/订阅消息的形式 来管理多个job
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/22 20:31
|
||||
*/
|
||||
@ConditionalOnBean(SmartJobAutoConfiguration.class)
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SmartJobClientManager {
|
||||
|
||||
private final SmartJobLauncher jobLauncher;
|
||||
|
||||
private final SmartJobRepository jobRepository;
|
||||
|
||||
private final List<SmartJob> jobInterfaceList;
|
||||
|
||||
private static final String EXECUTE_LOCK = "smart-job-lock-msg-execute-";
|
||||
|
||||
private static final String TOPIC = "smart-job-instance";
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
private final RTopic topic;
|
||||
|
||||
private final SmartJobMsgListener jobMsgListener;
|
||||
|
||||
public SmartJobClientManager(SmartJobLauncher jobLauncher,
|
||||
SmartJobRepository jobRepository,
|
||||
List<SmartJob> jobInterfaceList,
|
||||
RedissonClient redissonClient) {
|
||||
this.jobLauncher = jobLauncher;
|
||||
this.jobRepository = jobRepository;
|
||||
this.jobInterfaceList = jobInterfaceList;
|
||||
this.redissonClient = redissonClient;
|
||||
|
||||
// 添加监听器
|
||||
this.topic = redissonClient.getTopic(TOPIC);
|
||||
this.jobMsgListener = new SmartJobMsgListener();
|
||||
topic.addListener(SmartJobMsg.class, jobMsgListener);
|
||||
log.info("==== SmartJob ==== client-manager init");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*/
|
||||
public void publishToClient(SmartJobMsg msgDTO) {
|
||||
msgDTO.setMsgId(IdUtil.fastSimpleUUID());
|
||||
topic.publish(msgDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*/
|
||||
private class SmartJobMsgListener implements MessageListener<SmartJobMsg> {
|
||||
|
||||
@Override
|
||||
public void onMessage(CharSequence channel, SmartJobMsg msg) {
|
||||
log.info("==== SmartJob ==== on-message :{}", msg);
|
||||
// 判断消息类型 业务简单就直接判断 复杂的话可以策略模式
|
||||
SmartJobMsg.MsgTypeEnum msgType = msg.getMsgType();
|
||||
// 更新任务
|
||||
if (SmartJobMsg.MsgTypeEnum.UPDATE_JOB == msgType) {
|
||||
updateJob(msg.getJobId());
|
||||
}
|
||||
// 执行任务
|
||||
if (SmartJobMsg.MsgTypeEnum.EXECUTE_JOB == msgType) {
|
||||
executeJob(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务执行类
|
||||
*
|
||||
* @param jobClass
|
||||
* @return
|
||||
*/
|
||||
private Optional<SmartJob> queryJobImpl(String jobClass) {
|
||||
return jobInterfaceList.stream().filter(e -> Objects.equals(e.getClassName(), jobClass)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务
|
||||
*
|
||||
* @param jobId
|
||||
*/
|
||||
private void updateJob(Integer jobId) {
|
||||
SmartJobEntity jobEntity = jobRepository.getJobDao().selectById(jobId);
|
||||
if (null == jobEntity) {
|
||||
return;
|
||||
}
|
||||
jobLauncher.startOrRefreshJob(Lists.newArrayList(jobEntity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即执行任务
|
||||
*
|
||||
* @param msg
|
||||
*/
|
||||
private void executeJob(SmartJobMsg msg) {
|
||||
Integer jobId = msg.getJobId();
|
||||
SmartJobEntity jobEntity = jobRepository.getJobDao().selectById(jobId);
|
||||
if (null == jobEntity) {
|
||||
return;
|
||||
}
|
||||
// 获取定时任务实现类
|
||||
Optional<SmartJob> optional = this.queryJobImpl(jobEntity.getJobClass());
|
||||
if (!optional.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取执行锁 无需主动释放
|
||||
RLock rLock = redissonClient.getLock(EXECUTE_LOCK + msg.getMsgId());
|
||||
try {
|
||||
boolean getLock = rLock.tryLock(0, 20, TimeUnit.SECONDS);
|
||||
if (!getLock) {
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.error("==== SmartJob ==== msg execute err:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过执行器 执行任务
|
||||
jobEntity.setParam(msg.getParam());
|
||||
SmartJobExecutor jobExecutor = new SmartJobExecutor(jobEntity, jobRepository, optional.get(), redissonClient);
|
||||
jobExecutor.execute(msg.getUpdateName());
|
||||
}
|
||||
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
topic.removeListener(jobMsgListener);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package net.lab1024.sa.base.module.support.job.api;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.google.common.collect.Lists;
|
||||
import net.lab1024.sa.base.common.code.UserErrorCode;
|
||||
import net.lab1024.sa.base.common.domain.PageResult;
|
||||
import net.lab1024.sa.base.common.domain.ResponseDTO;
|
||||
import net.lab1024.sa.base.common.util.SmartBeanUtil;
|
||||
import net.lab1024.sa.base.common.util.SmartPageUtil;
|
||||
import net.lab1024.sa.base.module.support.job.api.domain.*;
|
||||
import net.lab1024.sa.base.module.support.job.config.SmartJobAutoConfiguration;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
|
||||
import net.lab1024.sa.base.module.support.job.repository.SmartJobDao;
|
||||
import net.lab1024.sa.base.module.support.job.repository.SmartJobLogDao;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 定时任务 接口业务管理
|
||||
* 如果不需要通过接口管理定时任务 可以删除此类
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 20:41
|
||||
*/
|
||||
@ConditionalOnBean(SmartJobAutoConfiguration.class)
|
||||
@Service
|
||||
public class SmartJobService {
|
||||
|
||||
@Resource
|
||||
private SmartJobDao jobDao;
|
||||
|
||||
@Resource
|
||||
private SmartJobLogDao jobLogDao;
|
||||
|
||||
@Resource
|
||||
private SmartJobClientManager jobClientManager;
|
||||
|
||||
/**
|
||||
* 查询 定时任务详情
|
||||
*
|
||||
* @param jobId
|
||||
* @return
|
||||
*/
|
||||
public ResponseDTO<SmartJobVO> queryJobInfo(Integer jobId) {
|
||||
SmartJobEntity jobEntity = jobDao.selectById(jobId);
|
||||
if (null == jobEntity) {
|
||||
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
|
||||
}
|
||||
SmartJobVO jobVO = SmartBeanUtil.copy(jobEntity, SmartJobVO.class);
|
||||
// 处理设置job详情
|
||||
this.handleJobInfo(Lists.newArrayList(jobVO));
|
||||
return ResponseDTO.ok(jobVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询 定时任务
|
||||
*
|
||||
* @param queryForm
|
||||
* @return
|
||||
*/
|
||||
public ResponseDTO<PageResult<SmartJobVO>> queryJob(SmartJobQueryForm queryForm) {
|
||||
Page<?> page = SmartPageUtil.convert2PageQuery(queryForm);
|
||||
List<SmartJobVO> jobList = jobDao.query(page, queryForm);
|
||||
PageResult<SmartJobVO> pageResult = SmartPageUtil.convert2PageResult(page, jobList);
|
||||
// 处理设置job详情
|
||||
this.handleJobInfo(jobList);
|
||||
return ResponseDTO.ok(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设置 任务信息
|
||||
*
|
||||
* @param jobList
|
||||
*/
|
||||
private void handleJobInfo(List<SmartJobVO> jobList) {
|
||||
if (CollectionUtils.isEmpty(jobList)) {
|
||||
return;
|
||||
}
|
||||
// 查询最后一次执行记录
|
||||
List<Long> logIdList = jobList.stream().map(SmartJobVO::getLastExecuteLogId).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
Map<Long, SmartJobLogVO> lastLogMap = Collections.emptyMap();
|
||||
if (CollectionUtils.isNotEmpty(logIdList)) {
|
||||
lastLogMap = jobLogDao.selectBatchIds(logIdList)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(SmartJobLogEntity::getLogId, e -> SmartBeanUtil.copy(e, SmartJobLogVO.class)));
|
||||
}
|
||||
|
||||
// 循环处理任务信息
|
||||
for (SmartJobVO jobVO : jobList) {
|
||||
// 设置最后一次执行记录
|
||||
Long lastExecuteLogId = jobVO.getLastExecuteLogId();
|
||||
if (null != lastExecuteLogId) {
|
||||
jobVO.setLastJobLog(lastLogMap.get(lastExecuteLogId));
|
||||
}
|
||||
// 计算未来5次执行时间
|
||||
if (jobVO.getEnabledFlag()) {
|
||||
List<LocalDateTime> nextTimeList = SmartJobUtil.queryNextTimeFromNow(jobVO.getTriggerType(), jobVO.getTriggerValue(), jobVO.getLastExecuteTime(), 5);
|
||||
jobVO.setNextJobExecuteTimeList(nextTimeList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询 定时任务-执行记录
|
||||
*
|
||||
* @param queryForm
|
||||
* @return
|
||||
*/
|
||||
public ResponseDTO<PageResult<SmartJobLogVO>> queryJobLog(SmartJobLogQueryForm queryForm) {
|
||||
Page<?> page = SmartPageUtil.convert2PageQuery(queryForm);
|
||||
List<SmartJobLogVO> jobList = jobLogDao.query(page, queryForm);
|
||||
PageResult<SmartJobLogVO> pageResult = SmartPageUtil.convert2PageResult(page, jobList);
|
||||
return ResponseDTO.ok(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时任务
|
||||
*
|
||||
* @param updateForm
|
||||
* @return
|
||||
*/
|
||||
public ResponseDTO<String> updateJob(SmartJobUpdateForm updateForm) {
|
||||
// 校验参数
|
||||
Integer jobId = updateForm.getJobId();
|
||||
SmartJobEntity jobEntity = jobDao.selectById(jobId);
|
||||
if (null == jobEntity) {
|
||||
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
|
||||
}
|
||||
// 校验触发时间配置
|
||||
String triggerType = updateForm.getTriggerType();
|
||||
String triggerValue = updateForm.getTriggerValue();
|
||||
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType) && !SmartJobUtil.checkCron(triggerValue)) {
|
||||
return ResponseDTO.userErrorParam("cron表达式错误");
|
||||
}
|
||||
if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType) && !SmartJobUtil.checkFixedDelay(triggerValue)) {
|
||||
return ResponseDTO.userErrorParam("固定间隔错误:整数且大于0");
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
jobEntity = SmartBeanUtil.copy(updateForm, SmartJobEntity.class);
|
||||
jobDao.updateById(jobEntity);
|
||||
|
||||
// 更新执行端
|
||||
SmartJobMsg jobMsg = new SmartJobMsg();
|
||||
jobMsg.setJobId(jobId);
|
||||
jobMsg.setMsgType(SmartJobMsg.MsgTypeEnum.UPDATE_JOB);
|
||||
jobMsg.setUpdateName(updateForm.getUpdateName());
|
||||
jobClientManager.publishToClient(jobMsg);
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时任务-是否开启
|
||||
*
|
||||
* @param updateForm
|
||||
* @return
|
||||
*/
|
||||
public ResponseDTO<String> updateJobEnabled(SmartJobEnabledUpdateForm updateForm) {
|
||||
Integer jobId = updateForm.getJobId();
|
||||
SmartJobEntity jobEntity = jobDao.selectById(jobId);
|
||||
if (null == jobEntity) {
|
||||
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
|
||||
}
|
||||
Boolean enabledFlag = updateForm.getEnabledFlag();
|
||||
if (Objects.equals(enabledFlag, jobEntity.getEnabledFlag())) {
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
// 更新数据
|
||||
jobEntity = new SmartJobEntity();
|
||||
jobEntity.setJobId(jobId);
|
||||
jobEntity.setEnabledFlag(enabledFlag);
|
||||
jobEntity.setUpdateName(updateForm.getUpdateName());
|
||||
jobDao.updateById(jobEntity);
|
||||
|
||||
// 更新执行端
|
||||
SmartJobMsg jobMsg = new SmartJobMsg();
|
||||
jobMsg.setJobId(jobId);
|
||||
jobMsg.setMsgType(SmartJobMsg.MsgTypeEnum.UPDATE_JOB);
|
||||
jobMsg.setUpdateName(updateForm.getUpdateName());
|
||||
jobClientManager.publishToClient(jobMsg);
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行定时任务
|
||||
* 忽略任务的开启状态,立即执行一次
|
||||
*
|
||||
* @param executeForm
|
||||
* @return
|
||||
*/
|
||||
public ResponseDTO<String> execute(SmartJobExecuteForm executeForm) {
|
||||
Integer jobId = executeForm.getJobId();
|
||||
SmartJobEntity jobEntity = jobDao.selectById(jobId);
|
||||
if (null == jobEntity) {
|
||||
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 更新执行端
|
||||
SmartJobMsg jobMsg = new SmartJobMsg();
|
||||
jobMsg.setJobId(jobId);
|
||||
jobMsg.setParam(executeForm.getParam());
|
||||
jobMsg.setMsgType(SmartJobMsg.MsgTypeEnum.EXECUTE_JOB);
|
||||
jobMsg.setUpdateName(executeForm.getUpdateName());
|
||||
jobClientManager.publishToClient(jobMsg);
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增定时任务
|
||||
* ps:目前没有业务场景需要通过接口 添加任务
|
||||
* 因为新增定时任务无论如何都需要 手动编码
|
||||
* 需要时手动给数据库增加一条就行
|
||||
*
|
||||
* @return
|
||||
* @author huke
|
||||
*/
|
||||
public ResponseDTO<String> addJob() {
|
||||
return ResponseDTO.userErrorParam("暂未支持");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除定时任务
|
||||
* ps:目前没有业务场景需要通过接口移除,理由同 {@link SmartJobService#addJob},
|
||||
* 彻底移除始终都需要手动删除代码
|
||||
* 如果只是想暂停任务执行,可以调用 {@link SmartJobService#updateJobEnabled}
|
||||
*
|
||||
* @return
|
||||
* @author huke
|
||||
*/
|
||||
public ResponseDTO<String> delJob() {
|
||||
return ResponseDTO.userErrorParam("暂未支持");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 定时任务-更新-开启状态
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobEnabledUpdateForm {
|
||||
|
||||
@Schema(description = "任务id")
|
||||
@NotNull(message = "任务id不能为空")
|
||||
private Integer jobId;
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
@NotNull(message = "是否启用不能为空")
|
||||
private Boolean enabledFlag;
|
||||
|
||||
@Schema(hidden = true)
|
||||
private String updateName;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 定时任务-手动执行
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/18 20:30
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobExecuteForm {
|
||||
|
||||
@Schema(description = "任务id")
|
||||
@NotNull(message = "任务id不能为空")
|
||||
private Integer jobId;
|
||||
|
||||
@Schema(description = "定时任务参数|可选")
|
||||
@Length(max = 2000, message = "定时任务参数最多2000字符")
|
||||
private String param;
|
||||
|
||||
@Schema(hidden = true)
|
||||
private String updateName;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.domain.PageParam;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 定时任务-执行记录 分页查询
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 20:50
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobLogQueryForm extends PageParam {
|
||||
|
||||
@Schema(description = "搜索词|可选")
|
||||
@Length(max = 50, message = "搜索词最多50字符")
|
||||
private String searchWord;
|
||||
|
||||
@Schema(description = "任务id|可选")
|
||||
private Integer jobId;
|
||||
|
||||
@Schema(description = "是否成功|可选")
|
||||
private Boolean successFlag;
|
||||
|
||||
@Schema(description = "开始时间|可选", example = "2024-06-06")
|
||||
private LocalDate startTime;
|
||||
|
||||
@Schema(description = "截止时间|可选", example = "2025-10-15")
|
||||
private LocalDate endTime;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 定时任务-执行记录 vo
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobLogVO {
|
||||
|
||||
@Schema(description = "logId")
|
||||
private Long logId;
|
||||
|
||||
@Schema(description = "任务id")
|
||||
private Integer jobId;
|
||||
|
||||
@Schema(description = "任务名称")
|
||||
private String jobName;
|
||||
|
||||
@Schema(description = "定时任务参数|可选")
|
||||
private String param;
|
||||
|
||||
@Schema(description = "执行结果是否成功")
|
||||
private Boolean successFlag;
|
||||
|
||||
@Schema(description = "开始执行时间")
|
||||
private LocalDateTime executeStartTime;
|
||||
|
||||
@Schema(description = "执行时长-毫秒")
|
||||
private Long executeTimeMillis;
|
||||
|
||||
@Schema(description = "执行结果描述")
|
||||
private String executeResult;
|
||||
|
||||
@Schema(description = "执行结束时间")
|
||||
private LocalDateTime executeEndTime;
|
||||
|
||||
@Schema(description = "ip")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "进程id")
|
||||
private String processId;
|
||||
|
||||
@Schema(description = "程序目录")
|
||||
private String programPath;
|
||||
|
||||
private String createName;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
|
||||
/**
|
||||
* 定时任务 发布/订阅消息对象
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/20 21:10
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobMsg {
|
||||
|
||||
/**
|
||||
* 消息id 无需设置
|
||||
*/
|
||||
private String msgId;
|
||||
|
||||
/**
|
||||
* 任务id
|
||||
*/
|
||||
private Integer jobId;
|
||||
|
||||
/**
|
||||
* 任务参数
|
||||
*/
|
||||
private String param;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
private MsgTypeEnum msgType;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updateName;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum MsgTypeEnum implements BaseEnum {
|
||||
|
||||
/**
|
||||
* 1 更新任务
|
||||
*/
|
||||
UPDATE_JOB(1, "更新任务"),
|
||||
|
||||
EXECUTE_JOB(2, "执行任务"),
|
||||
|
||||
;
|
||||
|
||||
private final Integer value;
|
||||
|
||||
private final String desc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.domain.PageParam;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
/**
|
||||
* 定时任务 分页查询
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 20:50
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobQueryForm extends PageParam {
|
||||
|
||||
@Schema(description = "搜索词|可选")
|
||||
@Length(max = 50, message = "搜索词最多50字符")
|
||||
private String searchWord;
|
||||
|
||||
@SchemaEnum(desc = "触发类型", value = SmartJobTriggerTypeEnum.class)
|
||||
@CheckEnum(value = SmartJobTriggerTypeEnum.class, message = "触发类型错误")
|
||||
private String triggerType;
|
||||
|
||||
@Schema(description = "是否启用|可选")
|
||||
private Boolean enabledFlag;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 定时任务 更新
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobUpdateForm {
|
||||
|
||||
@Schema(description = "任务id")
|
||||
@NotNull(message = "任务id不能为空")
|
||||
private Integer jobId;
|
||||
|
||||
@Schema(description = "任务名称")
|
||||
@NotBlank(message = "任务名称不能为空")
|
||||
@Length(max = 100, message = "任务名称最多100字符")
|
||||
private String jobName;
|
||||
|
||||
@Schema(description = "任务执行类")
|
||||
@NotBlank(message = "任务执行类不能为空")
|
||||
@Length(max = 200, message = "任务执行类最多200字符")
|
||||
private String jobClass;
|
||||
|
||||
@SchemaEnum(desc = "触发类型", value = SmartJobTriggerTypeEnum.class)
|
||||
@CheckEnum(value = SmartJobTriggerTypeEnum.class, required = true, message = "触发类型错误")
|
||||
private String triggerType;
|
||||
|
||||
@Schema(description = "触发配置")
|
||||
@NotBlank(message = "触发配置不能为空")
|
||||
@Length(max = 100, message = "触发配置最多100字符")
|
||||
private String triggerValue;
|
||||
|
||||
@Schema(description = "定时任务参数|可选")
|
||||
@Length(max = 1000, message = "定时任务参数最多1000字符")
|
||||
private String param;
|
||||
|
||||
@Schema(description = "是否开启")
|
||||
@NotNull(message = "是否开启不能为空")
|
||||
private Boolean enabledFlag;
|
||||
|
||||
@Schema(description = "备注")
|
||||
@Length(max = 250, message = "任务备注最多250字符")
|
||||
private String remark;
|
||||
|
||||
@NotNull(message = "排序不能为空")
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(hidden = true)
|
||||
private String updateName;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.lab1024.sa.base.module.support.job.api.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.json.serializer.enumeration.EnumSerialize;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 定时任务 vo
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Data
|
||||
public class SmartJobVO {
|
||||
|
||||
@Schema(description = "任务id")
|
||||
private Integer jobId;
|
||||
|
||||
@Schema(description = "任务名称")
|
||||
private String jobName;
|
||||
|
||||
@Schema(description = "执行类")
|
||||
private String jobClass;
|
||||
|
||||
@SchemaEnum(desc = "触发类型", value = SmartJobTriggerTypeEnum.class)
|
||||
@EnumSerialize(SmartJobTriggerTypeEnum.class)
|
||||
private String triggerType;
|
||||
|
||||
@Schema(description = "触发配置")
|
||||
private String triggerValue;
|
||||
|
||||
@Schema(description = "定时任务参数|可选")
|
||||
private String param;
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
private Boolean enabledFlag;
|
||||
|
||||
@Schema(description = "最后一执行时间")
|
||||
private LocalDateTime lastExecuteTime;
|
||||
|
||||
@Schema(description = "最后一次执行记录id")
|
||||
private Long lastExecuteLogId;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
private String updateName;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "上次执行记录")
|
||||
private SmartJobLogVO lastJobLog;
|
||||
|
||||
@Schema(description = "未来N次任务执行时间")
|
||||
private List<LocalDateTime> nextJobExecuteTimeList;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.lab1024.sa.base.module.support.job.config;
|
||||
|
||||
import net.lab1024.sa.base.module.support.job.core.SmartJob;
|
||||
import net.lab1024.sa.base.module.support.job.core.SmartJobLauncher;
|
||||
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 定时任务 配置
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(SmartJobConfig.class)
|
||||
@ConditionalOnProperty(
|
||||
prefix = SmartJobConfig.CONFIG_PREFIX,
|
||||
name = "enabled",
|
||||
havingValue = "true"
|
||||
)
|
||||
public class SmartJobAutoConfiguration {
|
||||
|
||||
private final SmartJobConfig jobConfig;
|
||||
|
||||
private final SmartJobRepository jobRepository;
|
||||
|
||||
private final List<SmartJob> jobInterfaceList;
|
||||
|
||||
public SmartJobAutoConfiguration(SmartJobConfig jobConfig,
|
||||
SmartJobRepository jobRepository,
|
||||
List<SmartJob> jobInterfaceList) {
|
||||
this.jobConfig = jobConfig;
|
||||
this.jobRepository = jobRepository;
|
||||
this.jobInterfaceList = jobInterfaceList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务启动器
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public SmartJobLauncher initJobLauncher(RedissonClient redissonClient) {
|
||||
return new SmartJobLauncher(jobConfig, jobRepository, jobInterfaceList, redissonClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package net.lab1024.sa.base.module.support.job.config;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* smart job 配置
|
||||
* 与配置文件参数对应
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@ConfigurationProperties(prefix = SmartJobConfig.CONFIG_PREFIX)
|
||||
@Data
|
||||
public class SmartJobConfig {
|
||||
|
||||
public static final String CONFIG_PREFIX = "smart.job";
|
||||
|
||||
/**
|
||||
* 任务执行核心线程数 偶数 默认2
|
||||
*/
|
||||
private Integer corePoolSize = 2;
|
||||
|
||||
/**
|
||||
* 任务延迟初始化 默认30秒
|
||||
*/
|
||||
private Integer initDelay = 30;
|
||||
|
||||
/**
|
||||
* 数据库配置检测-开关 默认开启
|
||||
*/
|
||||
private Boolean dbRefreshEnabled = true;
|
||||
|
||||
/**
|
||||
* 数据库配置检测-执行间隔 默认120秒
|
||||
*/
|
||||
private Integer dbRefreshInterval = 120;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.lab1024.sa.base.module.support.job.constant;
|
||||
|
||||
/**
|
||||
* smart job 常量
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/19 20:25
|
||||
*/
|
||||
public class SmartJobConst {
|
||||
|
||||
public static final String SYSTEM_NAME = "system";
|
||||
|
||||
public static final String LOGO = " _____ __ __ __ \n" +
|
||||
" / ___/____ ___ ____ ______/ /_ / /___ / /_ \n" +
|
||||
" \\__ \\/ __ `__ \\/ __ `/ ___/ __/ __ / / __ \\/ __ \\\n" +
|
||||
" ___/ / / / / / / /_/ / / / /_ / /_/ / /_/ / /_/ /\n" +
|
||||
"/____/_/ /_/ /_/\\__,_/_/ \\__/ \\____/\\____/_.___/ \n" +
|
||||
"-->任务执行线程池:%s\n" +
|
||||
"-->任务初始化延迟:%s秒\n" +
|
||||
"-->数据库配置检测:%s\n\n";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.lab1024.sa.base.module.support.job.constant;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
|
||||
/**
|
||||
* job 任务触发类型 枚举类
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024年6月29日
|
||||
**/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum SmartJobTriggerTypeEnum implements BaseEnum {
|
||||
|
||||
/**
|
||||
* 1 cron表达式
|
||||
*/
|
||||
CRON("cron", "cron表达式"),
|
||||
|
||||
FIXED_DELAY("fixed_delay", "固定间隔"),
|
||||
|
||||
;
|
||||
|
||||
private final String value;
|
||||
|
||||
private final String desc;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package net.lab1024.sa.base.module.support.job.constant;
|
||||
|
||||
import org.springframework.scheduling.support.CronExpression;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* smart job util
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/18 20:00
|
||||
*/
|
||||
public class SmartJobUtil {
|
||||
|
||||
private SmartJobUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验cron表达式 是否合法
|
||||
*
|
||||
* @param cron
|
||||
* @return
|
||||
*/
|
||||
public static boolean checkCron(String cron) {
|
||||
return CronExpression.isValidExpression(cron);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验固定间隔 值是否合法
|
||||
*
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
public static boolean checkFixedDelay(String val) {
|
||||
int intVal;
|
||||
try {
|
||||
intVal = Integer.parseInt(val);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
return intVal > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印一些展示信息到控制台
|
||||
* 环保绿
|
||||
*
|
||||
* @param info
|
||||
*/
|
||||
public static void printInfo(String info) {
|
||||
System.out.printf("\033[32;1m %s \033[0m", info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询未来N次执行时间 从最后一次时间时间 开始计算
|
||||
*
|
||||
* @param triggerType
|
||||
* @param triggerVal
|
||||
* @param lastExecuteTime
|
||||
* @param num
|
||||
* @return
|
||||
*/
|
||||
public static List<LocalDateTime> queryNextTimeFromLast(String triggerType,
|
||||
String triggerVal,
|
||||
LocalDateTime lastExecuteTime,
|
||||
int num) {
|
||||
List<LocalDateTime> nextTimeList = null;
|
||||
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType)) {
|
||||
nextTimeList = SmartJobUtil.queryNextTime(triggerVal, lastExecuteTime, num);
|
||||
} else if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType)) {
|
||||
nextTimeList = SmartJobUtil.queryNextTime(getFixedDelayVal(triggerVal), lastExecuteTime, num);
|
||||
}
|
||||
return nextTimeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询未来N次执行时间 从当前时间 开始计算
|
||||
*
|
||||
* @param triggerType
|
||||
* @param triggerVal
|
||||
* @param lastExecuteTime
|
||||
* @param num
|
||||
* @return
|
||||
*/
|
||||
public static List<LocalDateTime> queryNextTimeFromNow(String triggerType,
|
||||
String triggerVal,
|
||||
LocalDateTime lastExecuteTime,
|
||||
int num) {
|
||||
LocalDateTime nowTime = LocalDateTime.now();
|
||||
List<LocalDateTime> nextTimeList = null;
|
||||
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType)) {
|
||||
nextTimeList = SmartJobUtil.queryNextTime(triggerVal, nowTime, num);
|
||||
} else if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType)) {
|
||||
Integer fixedDelay = getFixedDelayVal(triggerVal);
|
||||
LocalDateTime startTime = null == lastExecuteTime || lastExecuteTime.plusSeconds(fixedDelay).isBefore(nowTime)
|
||||
? nowTime : lastExecuteTime;
|
||||
nextTimeList = SmartJobUtil.queryNextTime(fixedDelay, startTime, num);
|
||||
}
|
||||
return nextTimeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据cron表达式 计算N次执行时间
|
||||
*
|
||||
* @param cron
|
||||
* @param startTime
|
||||
* @param num
|
||||
* @return
|
||||
*/
|
||||
public static List<LocalDateTime> queryNextTime(String cron, LocalDateTime startTime, int num) {
|
||||
if (null == startTime) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
CronExpression parse = CronExpression.parse(cron);
|
||||
List<LocalDateTime> timeList = new ArrayList<>(num);
|
||||
for (int i = 0; i < num; i++) {
|
||||
startTime = parse.next(startTime);
|
||||
timeList.add(startTime);
|
||||
}
|
||||
return timeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 固定间隔 计算N次执行时间
|
||||
*
|
||||
* @param fixDelaySecond
|
||||
* @param startTime
|
||||
* @param num
|
||||
* @return
|
||||
*/
|
||||
public static List<LocalDateTime> queryNextTime(Integer fixDelaySecond, LocalDateTime startTime, int num) {
|
||||
if (null == startTime) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<LocalDateTime> timeList = new ArrayList<>(num);
|
||||
for (int i = 0; i < num; i++) {
|
||||
startTime = startTime.plusSeconds(fixDelaySecond);
|
||||
timeList.add(startTime);
|
||||
}
|
||||
return timeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取固定间隔时间
|
||||
*
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
public static Integer getFixedDelayVal(String val) {
|
||||
return Integer.parseInt(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 Java 应用程序的工作目录
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getProgramPath() {
|
||||
return System.getProperty("user.dir");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 Java 应用程序的进程id
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getProcessId() {
|
||||
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
|
||||
return runtime.getName().split("@")[0];
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
LocalDateTime startTime = LocalDateTime.now();
|
||||
List<LocalDateTime> timeList = SmartJobUtil.queryNextTime("5 * * * * *", startTime, 3);
|
||||
System.out.println(timeList);
|
||||
|
||||
timeList = SmartJobUtil.queryNextTime(10, startTime, 3);
|
||||
System.out.println(timeList);
|
||||
|
||||
System.out.println("project path ->" + getProgramPath());
|
||||
System.out.println("project process id ->" + getProcessId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.lab1024.sa.base.module.support.job.core;
|
||||
|
||||
/**
|
||||
* 定时任务 执行接口
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
public interface SmartJob {
|
||||
|
||||
/**
|
||||
* 默认方法
|
||||
* 获取当前任务类名
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
default String getClassName() {
|
||||
return this.getClass().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行定时任务
|
||||
*
|
||||
* @param param 可选参数 任务不需要时不用管
|
||||
* @return 可null, 自行组织语言描述执行结果,例如:本次处理数据N条 等
|
||||
*/
|
||||
String run(String param);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package net.lab1024.sa.base.module.support.job.core;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.common.util.SmartIpUtil;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobConst;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
|
||||
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 定时任务 执行器
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmartJobExecutor implements Runnable {
|
||||
|
||||
private final SmartJobEntity jobEntity;
|
||||
|
||||
private final SmartJobRepository jobRepository;
|
||||
|
||||
private final SmartJob jobInterface;
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
private static final String EXECUTE_LOCK = "smart-job-lock-execute-";
|
||||
|
||||
public SmartJobExecutor(SmartJobEntity jobEntity,
|
||||
SmartJobRepository jobRepository,
|
||||
SmartJob jobInterface,
|
||||
RedissonClient redissonClient) {
|
||||
this.jobEntity = jobEntity;
|
||||
this.jobRepository = jobRepository;
|
||||
this.jobInterface = jobInterface;
|
||||
this.redissonClient = redissonClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统线程执行
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
// 获取当前任务执行锁 最多持有30s自动释放
|
||||
Integer jobId = jobEntity.getJobId();
|
||||
RLock rLock = redissonClient.getLock(EXECUTE_LOCK + jobId);
|
||||
try {
|
||||
boolean lock = rLock.tryLock(0, 30, TimeUnit.SECONDS);
|
||||
if (!lock) {
|
||||
return;
|
||||
}
|
||||
// 查询上次执行时间 校验执行间隔
|
||||
SmartJobEntity dbJobEntity = jobRepository.getJobDao().selectById(jobId);
|
||||
if (null == dbJobEntity) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime lastExecuteTime = dbJobEntity.getLastExecuteTime();
|
||||
if (null != lastExecuteTime) {
|
||||
LocalDateTime nextTime = SmartJobUtil.queryNextTimeFromLast(jobEntity.getTriggerType(), jobEntity.getTriggerValue(), lastExecuteTime, 1).get(0);
|
||||
if (LocalDateTime.now().isBefore(nextTime)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 执行任务
|
||||
SmartJobLogEntity logEntity = this.execute(SmartJobConst.SYSTEM_NAME);
|
||||
log.info("==== SmartJob ==== execute job->{},time-millis->{}ms", jobEntity.getJobName(), logEntity.getExecuteTimeMillis());
|
||||
} catch (Throwable t) {
|
||||
log.error("==== SmartJob ==== execute err:", t);
|
||||
} finally {
|
||||
if (rLock.isHeldByCurrentThread()) {
|
||||
rLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*
|
||||
* @param executorName
|
||||
*/
|
||||
public SmartJobLogEntity execute(String executorName) {
|
||||
// 执行计时
|
||||
LocalDateTime startTime = LocalDateTime.now();
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
// 执行任务
|
||||
boolean successFlag = true;
|
||||
String executeResult;
|
||||
try {
|
||||
executeResult = jobInterface.run(jobEntity.getParam());
|
||||
stopWatch.stop();
|
||||
} catch (Throwable t) {
|
||||
stopWatch.stop();
|
||||
successFlag = false;
|
||||
// ps:异常信息不大于数据库字段长度限制
|
||||
executeResult = ExceptionUtil.stacktraceToString(t, 1800);
|
||||
log.error("==== SmartJob ==== execute err:", t);
|
||||
}
|
||||
|
||||
// 保存执行记录
|
||||
Integer jobId = jobEntity.getJobId();
|
||||
SmartJobLogEntity logEntity = new SmartJobLogEntity();
|
||||
logEntity.setJobId(jobId);
|
||||
logEntity.setJobName(jobEntity.getJobName());
|
||||
logEntity.setParam(jobEntity.getParam());
|
||||
logEntity.setSuccessFlag(successFlag);
|
||||
// 执行开始 结束时间
|
||||
logEntity.setExecuteStartTime(startTime);
|
||||
long totalTimeMillis = stopWatch.getTotalTimeMillis();
|
||||
logEntity.setExecuteTimeMillis(totalTimeMillis);
|
||||
logEntity.setExecuteEndTime(startTime.plus(totalTimeMillis, ChronoUnit.MILLIS));
|
||||
// 执行结果
|
||||
logEntity.setExecuteResult(executeResult);
|
||||
logEntity.setIp(SmartIpUtil.getLocalFirstIp());
|
||||
logEntity.setProcessId(SmartJobUtil.getProcessId());
|
||||
logEntity.setProgramPath(SmartJobUtil.getProgramPath());
|
||||
logEntity.setCreateName(executorName);
|
||||
|
||||
// 更新上次执行
|
||||
SmartJobEntity updateJobEntity = new SmartJobEntity();
|
||||
updateJobEntity.setJobId(jobId);
|
||||
updateJobEntity.setLastExecuteTime(startTime);
|
||||
|
||||
// 持久化数据
|
||||
jobRepository.saveLog(logEntity, updateJobEntity);
|
||||
return logEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 当前任务信息
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public SmartJobEntity getJob() {
|
||||
return jobEntity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package net.lab1024.sa.base.module.support.job.core;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.module.support.job.config.SmartJobConfig;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobConst;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
|
||||
import net.lab1024.sa.base.module.support.job.repository.SmartJobRepository;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 定时任务 作业启动类
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmartJobLauncher {
|
||||
|
||||
private final SmartJobRepository jobRepository;
|
||||
|
||||
private final List<SmartJob> jobInterfaceList;
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
public SmartJobLauncher(SmartJobConfig jobConfig,
|
||||
SmartJobRepository jobRepository,
|
||||
List<SmartJob> jobInterfaceList,
|
||||
RedissonClient redissonClient) {
|
||||
this.jobRepository = jobRepository;
|
||||
this.jobInterfaceList = jobInterfaceList;
|
||||
this.redissonClient = redissonClient;
|
||||
|
||||
// init job scheduler
|
||||
SmartJobScheduler.init(jobConfig);
|
||||
|
||||
// 任务自动检测配置 固定1个线程
|
||||
Integer initDelay = jobConfig.getInitDelay();
|
||||
Boolean refreshEnabled = jobConfig.getDbRefreshEnabled();
|
||||
Integer refreshInterval = jobConfig.getDbRefreshInterval();
|
||||
|
||||
ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("SmartJobLauncher-%d").build();
|
||||
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, factory);
|
||||
Runnable launcherRunnable = () -> {
|
||||
try {
|
||||
// 查询所有任务
|
||||
List<SmartJobEntity> smartJobList = this.queryJob();
|
||||
this.startOrRefreshJob(smartJobList);
|
||||
} catch (Throwable t) {
|
||||
log.error("SmartJob Error:", t);
|
||||
}
|
||||
// 只在启动时 执行一次
|
||||
if (!refreshEnabled) {
|
||||
executor.shutdown();
|
||||
}
|
||||
};
|
||||
executor.scheduleWithFixedDelay(launcherRunnable, initDelay, refreshInterval, TimeUnit.SECONDS);
|
||||
|
||||
// 打印信息
|
||||
String refreshDesc = refreshEnabled ? "开启|检测间隔" + refreshInterval + "秒" : "关闭";
|
||||
String format = String.format(SmartJobConst.LOGO, jobConfig.getCorePoolSize(), initDelay, refreshDesc);
|
||||
SmartJobUtil.printInfo(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询数据库
|
||||
* 启动/刷新任务
|
||||
*/
|
||||
public void startOrRefreshJob(List<SmartJobEntity> smartJobList) {
|
||||
// 查询任务配置
|
||||
if (CollectionUtils.isEmpty(smartJobList) || CollectionUtils.isEmpty(jobInterfaceList)) {
|
||||
log.info("==== SmartJob ==== job list empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// 任务实现类
|
||||
Map<String, SmartJob> jobImplMap = jobInterfaceList.stream().collect(Collectors.toMap(SmartJob::getClassName, Function.identity()));
|
||||
for (SmartJobEntity jobEntity : smartJobList) {
|
||||
// 任务是否存在 判断是否需要更新
|
||||
Integer jobId = jobEntity.getJobId();
|
||||
SmartJobEntity oldJobEntity = SmartJobScheduler.getJobInfo(jobId);
|
||||
if (null != oldJobEntity) {
|
||||
// 不需要更新
|
||||
if (!isNeedUpdate(oldJobEntity, jobEntity)) {
|
||||
continue;
|
||||
}
|
||||
// 需要更新 移除原任务
|
||||
SmartJobScheduler.removeJob(jobId);
|
||||
}
|
||||
// 任务未开启
|
||||
if (!jobEntity.getEnabledFlag()) {
|
||||
continue;
|
||||
}
|
||||
// 查找任务实现类
|
||||
SmartJob jobImpl = jobImplMap.get(jobEntity.getJobClass());
|
||||
if (null == jobImpl) {
|
||||
continue;
|
||||
}
|
||||
// 添加任务
|
||||
SmartJobExecutor jobExecute = new SmartJobExecutor(jobEntity, jobRepository, jobImpl, redissonClient);
|
||||
SmartJobScheduler.addJob(jobExecute);
|
||||
}
|
||||
List<SmartJobEntity> runjJobList = SmartJobScheduler.getJobInfo();
|
||||
List<String> jobNameList = runjJobList.stream().map(SmartJobEntity::getJobName).collect(Collectors.toList());
|
||||
log.info("==== SmartJob ==== start/refresh job num:{}->{}", runjJobList.size(), jobNameList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部任务
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private List<SmartJobEntity> queryJob() {
|
||||
return jobRepository.getJobDao().selectList(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动判断 任务配置 是否需要更新
|
||||
* 新增字段的话 在这个方法里增加判断
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static boolean isNeedUpdate(SmartJobEntity oldJob, SmartJobEntity newJob) {
|
||||
// cron为空时 fixedDelay 才有意义
|
||||
return !Objects.equals(oldJob.getEnabledFlag(), newJob.getEnabledFlag())
|
||||
|| !Objects.equals(oldJob.getTriggerType(), newJob.getTriggerType())
|
||||
|| !Objects.equals(oldJob.getTriggerValue(), newJob.getTriggerValue())
|
||||
|| !Objects.equals(oldJob.getJobClass(), newJob.getJobClass());
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
SmartJobScheduler.destroy();
|
||||
log.info("==== SmartJob ==== destroy job");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package net.lab1024.sa.base.module.support.job.core;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.module.support.job.config.SmartJobConfig;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobUtil;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.scheduling.Trigger;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.scheduling.support.CronTrigger;
|
||||
import org.springframework.scheduling.support.PeriodicTrigger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 定时任务 调度管理
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/22 21:30
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmartJobScheduler {
|
||||
|
||||
/**
|
||||
* Spring线程池任务调度器
|
||||
*/
|
||||
private static ThreadPoolTaskScheduler TASK_SCHEDULER;
|
||||
|
||||
/**
|
||||
* 定时任务 map
|
||||
*/
|
||||
private static Map<Integer, Pair<SmartJobEntity, ScheduledFuture<?>>> JOB_FUTURE_MAP;
|
||||
|
||||
private SmartJobScheduler() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化任务调度配置
|
||||
*/
|
||||
public static void init(SmartJobConfig config) {
|
||||
TASK_SCHEDULER = new ThreadPoolTaskScheduler();
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("SmartJobExecutor-%d").build();
|
||||
TASK_SCHEDULER.setThreadFactory(threadFactory);
|
||||
TASK_SCHEDULER.setPoolSize(config.getCorePoolSize());
|
||||
// 线程池在关闭时会等待所有任务完成
|
||||
TASK_SCHEDULER.setWaitForTasksToCompleteOnShutdown(true);
|
||||
// 在调用shutdown方法后,等待任务完成的最长时间
|
||||
TASK_SCHEDULER.setAwaitTerminationSeconds(10);
|
||||
// 错误处理
|
||||
TASK_SCHEDULER.setErrorHandler((t) -> log.error("SmartJobExecute Err:", t));
|
||||
// 当一个任务在被调度执行前被取消时,是否应该从线程池的任务队列中移除
|
||||
TASK_SCHEDULER.setRemoveOnCancelPolicy(true);
|
||||
TASK_SCHEDULER.initialize();
|
||||
|
||||
JOB_FUTURE_MAP = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务执行对象
|
||||
*
|
||||
* @param jobId
|
||||
* @return
|
||||
*/
|
||||
public static ScheduledFuture<?> getJobFuture(Integer jobId) {
|
||||
Pair<SmartJobEntity, ScheduledFuture<?>> pair = JOB_FUTURE_MAP.get(jobId);
|
||||
if (null == pair) {
|
||||
return null;
|
||||
}
|
||||
return pair.getRight();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前所有执行任务
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static List<SmartJobEntity> getJobInfo() {
|
||||
return JOB_FUTURE_MAP.values().stream().map(Pair::getLeft).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务执行实体类
|
||||
*
|
||||
* @param jobId
|
||||
* @return
|
||||
*/
|
||||
public static SmartJobEntity getJobInfo(Integer jobId) {
|
||||
Pair<SmartJobEntity, ScheduledFuture<?>> pair = JOB_FUTURE_MAP.get(jobId);
|
||||
if (null == pair) {
|
||||
return null;
|
||||
}
|
||||
return pair.getLeft();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任务
|
||||
*
|
||||
* @param jobExecute
|
||||
* @return
|
||||
*/
|
||||
public static void addJob(SmartJobExecutor jobExecute) {
|
||||
// 任务是否存在
|
||||
SmartJobEntity jobEntity = jobExecute.getJob();
|
||||
Integer jobId = jobEntity.getJobId();
|
||||
if (JOB_FUTURE_MAP.containsKey(jobId)) {
|
||||
// 移除任务
|
||||
removeJob(jobId);
|
||||
}
|
||||
// 任务触发类型
|
||||
Trigger trigger = null;
|
||||
String triggerType = jobEntity.getTriggerType();
|
||||
String triggerValue = jobEntity.getTriggerValue();
|
||||
// 优先 cron 表达式
|
||||
if (SmartJobTriggerTypeEnum.CRON.equalsValue(triggerType)) {
|
||||
trigger = new CronTrigger(triggerValue);
|
||||
} else if (SmartJobTriggerTypeEnum.FIXED_DELAY.equalsValue(triggerType)) {
|
||||
trigger = new PeriodicTrigger(SmartJobUtil.getFixedDelayVal(triggerValue), TimeUnit.SECONDS);
|
||||
}
|
||||
String jobName = jobEntity.getJobName();
|
||||
if (null == trigger) {
|
||||
log.error("==== SmartJob ==== trigger-value not null {}", jobName);
|
||||
return;
|
||||
}
|
||||
// 执行任务
|
||||
ScheduledFuture<?> schedule = TASK_SCHEDULER.schedule(jobExecute, trigger);
|
||||
JOB_FUTURE_MAP.put(jobId, Pair.of(jobEntity, schedule));
|
||||
log.info("==== SmartJob ==== add job:{}", jobName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除任务
|
||||
* 等待任务执行完成后移除
|
||||
*
|
||||
* @param jobId
|
||||
*/
|
||||
public static void removeJob(Integer jobId) {
|
||||
ScheduledFuture<?> jobFuture = getJobFuture(jobId);
|
||||
if (null == jobFuture) {
|
||||
return;
|
||||
}
|
||||
// 结束任务
|
||||
stopJob(jobFuture);
|
||||
JOB_FUTURE_MAP.remove(jobId);
|
||||
log.info("==== SmartJob ==== remove job:{}", jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有定时任务
|
||||
*/
|
||||
public static void destroy() {
|
||||
// 启动一个有序的关闭过程,在这个过程中,不再接受新的任务提交,但已提交的任务(包括正在执行的和队列中等待的)会被允许执行完成。
|
||||
TASK_SCHEDULER.destroy();
|
||||
JOB_FUTURE_MAP.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束任务
|
||||
* 如果任务还没有开始执行,会直接被取消。
|
||||
* 如果任务已经开始执行,此时不会中断执行中的线程,任务会执行完成再被取消
|
||||
*
|
||||
* @param scheduledFuture
|
||||
*/
|
||||
private static void stopJob(ScheduledFuture<?> scheduledFuture) {
|
||||
if (null == scheduledFuture || scheduledFuture.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
scheduledFuture.cancel(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.lab1024.sa.base.module.support.job.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobQueryForm;
|
||||
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobVO;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 定时任务 dao
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Mapper
|
||||
@Component
|
||||
public interface SmartJobDao extends BaseMapper<SmartJobEntity> {
|
||||
|
||||
/**
|
||||
* 定时任务-分页查询
|
||||
*
|
||||
* @param page
|
||||
* @param queryForm
|
||||
* @return
|
||||
*/
|
||||
List<SmartJobVO> query(Page<?> page, @Param("query") SmartJobQueryForm queryForm);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.lab1024.sa.base.module.support.job.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobLogQueryForm;
|
||||
import net.lab1024.sa.base.module.support.job.api.domain.SmartJobLogVO;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 定时任务-执行记录 dao
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Mapper
|
||||
@Component
|
||||
public interface SmartJobLogDao extends BaseMapper<SmartJobLogEntity> {
|
||||
|
||||
/**
|
||||
* 定时任务-执行记录-分页查询
|
||||
*
|
||||
* @param page
|
||||
* @param queryForm
|
||||
* @return
|
||||
*/
|
||||
List<SmartJobLogVO> query(Page<?> page, @Param("query") SmartJobLogQueryForm queryForm);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.lab1024.sa.base.module.support.job.repository;
|
||||
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobEntity;
|
||||
import net.lab1024.sa.base.module.support.job.repository.domain.SmartJobLogEntity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* job 持久化业务
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/22 22:28
|
||||
*/
|
||||
@Service
|
||||
public class SmartJobRepository {
|
||||
|
||||
@Autowired
|
||||
private SmartJobDao jobDao;
|
||||
|
||||
@Autowired
|
||||
private SmartJobLogDao jobLogDao;
|
||||
|
||||
public SmartJobDao getJobDao() {
|
||||
return jobDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存执行记录
|
||||
*
|
||||
* @param logEntity
|
||||
* @param jobEntity
|
||||
*/
|
||||
@Transactional(rollbackFor = Throwable.class)
|
||||
public void saveLog(SmartJobLogEntity logEntity, SmartJobEntity jobEntity) {
|
||||
jobLogDao.insert(logEntity);
|
||||
|
||||
jobEntity.setLastExecuteLogId(logEntity.getLogId());
|
||||
jobDao.updateById(jobEntity);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.lab1024.sa.base.module.support.job.repository.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.module.support.job.constant.SmartJobTriggerTypeEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 定时任务 实体类
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Data
|
||||
@TableName("t_smart_job")
|
||||
public class SmartJobEntity {
|
||||
|
||||
/**
|
||||
* 任务id
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer jobId;
|
||||
|
||||
/**
|
||||
* 任务名称
|
||||
*/
|
||||
private String jobName;
|
||||
|
||||
/**
|
||||
* 执行类
|
||||
*/
|
||||
private String jobClass;
|
||||
|
||||
/**
|
||||
* 触发类型
|
||||
*
|
||||
* @see SmartJobTriggerTypeEnum
|
||||
*/
|
||||
private String triggerType;
|
||||
|
||||
/**
|
||||
* 触发配置
|
||||
*/
|
||||
private String triggerValue;
|
||||
|
||||
/**
|
||||
* 定时任务参数 可选
|
||||
*/
|
||||
private String param;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enabledFlag;
|
||||
|
||||
/**
|
||||
* 最后一执行时间
|
||||
*/
|
||||
private LocalDateTime lastExecuteTime;
|
||||
|
||||
/**
|
||||
* 最后一次执行记录id
|
||||
*/
|
||||
private Long lastExecuteLogId;
|
||||
|
||||
/**
|
||||
* 备注描述 可选
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
private String updateName;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.lab1024.sa.base.module.support.job.repository.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 定时任务 执行记录 实体类
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Data
|
||||
@TableName("t_smart_job_log")
|
||||
public class SmartJobLogEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long logId;
|
||||
|
||||
/**
|
||||
* 任务id
|
||||
*/
|
||||
private Integer jobId;
|
||||
|
||||
/**
|
||||
* 任务名称
|
||||
*/
|
||||
private String jobName;
|
||||
|
||||
/**
|
||||
* 定时任务参数 可选
|
||||
*/
|
||||
private String param;
|
||||
|
||||
/**
|
||||
* 执行结果 是否成功
|
||||
*/
|
||||
private Boolean successFlag;
|
||||
|
||||
/**
|
||||
* 开始执行时间
|
||||
*/
|
||||
private LocalDateTime executeStartTime;
|
||||
|
||||
/**
|
||||
* 执行时长-毫秒
|
||||
*/
|
||||
private Long executeTimeMillis;
|
||||
|
||||
/**
|
||||
* 执行结束时间
|
||||
*/
|
||||
private LocalDateTime executeEndTime;
|
||||
|
||||
/**
|
||||
* 执行结果 描述 可选
|
||||
*/
|
||||
private String executeResult;
|
||||
|
||||
/**
|
||||
* ip
|
||||
*/
|
||||
private String ip;
|
||||
|
||||
/**
|
||||
* 进程id
|
||||
*/
|
||||
private String processId;
|
||||
|
||||
/**
|
||||
* 程序目录
|
||||
*/
|
||||
private String programPath;
|
||||
|
||||
private String createName;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.lab1024.sa.base.module.support.job.sample;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.module.support.job.core.SmartJob;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 定时任务 示例1
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SmartJobSample1 implements SmartJob {
|
||||
|
||||
/**
|
||||
* 定时任务示例
|
||||
*
|
||||
* @param param 可选参数 任务不需要时不用管
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String run(String param) {
|
||||
// 写点什么业务逻辑
|
||||
return "执行完毕,随便说点什么吧";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.lab1024.sa.base.module.support.job.sample;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.module.support.config.ConfigDao;
|
||||
import net.lab1024.sa.base.module.support.config.domain.ConfigEntity;
|
||||
import net.lab1024.sa.base.module.support.job.core.SmartJob;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 定时任务 示例2
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/17 21:30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SmartJobSample2 implements SmartJob {
|
||||
|
||||
@Autowired
|
||||
private ConfigDao configDao;
|
||||
|
||||
/**
|
||||
* 定时任务示例
|
||||
* 需要事务时 添加 @Transactional 注解
|
||||
*
|
||||
* @param param 可选参数 任务不需要时不用管
|
||||
* @return
|
||||
*/
|
||||
@Transactional(rollbackFor = Throwable.class)
|
||||
@Override
|
||||
public String run(String param) {
|
||||
// 随便更新点什么东西
|
||||
ConfigEntity configEntity = new ConfigEntity();
|
||||
configEntity.setConfigId(1L);
|
||||
configEntity.setRemark(param);
|
||||
configDao.updateById(configEntity);
|
||||
|
||||
configEntity = new ConfigEntity();
|
||||
configEntity.setConfigId(2L);
|
||||
configEntity.setRemark("SmartJob Sample2 update");
|
||||
configDao.updateById(configEntity);
|
||||
|
||||
return "执行成功,本次处理数据1条";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 定时任务 示例包
|
||||
* 可以删除
|
||||
*/
|
||||
package net.lab1024.sa.base.module.support.job.sample;
|
||||
@@ -16,6 +16,12 @@ import net.lab1024.sa.base.common.domain.PageParam;
|
||||
@Data
|
||||
public class LoginLogQueryForm extends PageParam {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户类型")
|
||||
private Integer userType;
|
||||
|
||||
@Schema(description = "开始日期")
|
||||
private String startDate;
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.lab1024.sa.base.module.support.message.constant;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
|
||||
/**
|
||||
* 消息模板类型
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum MessageTemplateEnum implements BaseEnum {
|
||||
|
||||
|
||||
|
||||
ORDER_AUDIT(1000, "订单审批", MessageTypeEnum.ORDER, "您有一个订单等待审批,订单号【${orderNumber}】"),
|
||||
|
||||
;
|
||||
|
||||
private final Integer value;
|
||||
|
||||
private final String desc;
|
||||
|
||||
private final MessageTypeEnum messageTypeEnum;
|
||||
|
||||
private final String content;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.lab1024.sa.base.module.support.message.constant;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum MessageTypeEnum implements BaseEnum {
|
||||
|
||||
MAIL(1, "站内信"),
|
||||
|
||||
ORDER(2, "订单"),
|
||||
;
|
||||
|
||||
private final Integer value;
|
||||
|
||||
private final String desc;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package net.lab1024.sa.base.module.support.message.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import net.lab1024.sa.base.common.controller.SupportBaseController;
|
||||
import net.lab1024.sa.base.common.domain.PageResult;
|
||||
import net.lab1024.sa.base.common.domain.RequestUser;
|
||||
import net.lab1024.sa.base.common.domain.ResponseDTO;
|
||||
import net.lab1024.sa.base.common.util.SmartRequestUtil;
|
||||
import net.lab1024.sa.base.constant.SwaggerTagConst;
|
||||
import net.lab1024.sa.base.module.support.message.domain.MessageQueryForm;
|
||||
import net.lab1024.sa.base.module.support.message.domain.MessageVO;
|
||||
import net.lab1024.sa.base.module.support.message.service.MessageService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 消息
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@RestController
|
||||
@Tag(name = SwaggerTagConst.Support.MESSAGE)
|
||||
public class MessageController extends SupportBaseController {
|
||||
|
||||
@Resource
|
||||
private MessageService messageService;
|
||||
|
||||
@Operation(summary = "分页查询我的消息 @luoyi")
|
||||
@PostMapping("/message/queryMyMessage")
|
||||
public ResponseDTO<PageResult<MessageVO>> query(@RequestBody @Valid MessageQueryForm queryForm) {
|
||||
RequestUser user = SmartRequestUtil.getRequestUser();
|
||||
if(user == null){
|
||||
return ResponseDTO.userErrorParam("用户未登录");
|
||||
}
|
||||
|
||||
queryForm.setSearchCount(false);
|
||||
queryForm.setReceiverUserId(user.getUserId());
|
||||
queryForm.setReceiverUserType(user.getUserType().getValue());
|
||||
return ResponseDTO.ok(messageService.query(queryForm));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询未读消息数量 @luoyi")
|
||||
@GetMapping("/message/getUnreadCount")
|
||||
public ResponseDTO<Long> getUnreadCount() {
|
||||
RequestUser user = SmartRequestUtil.getRequestUser();
|
||||
if(user == null){
|
||||
return ResponseDTO.userErrorParam("用户未登录");
|
||||
}
|
||||
return ResponseDTO.ok(messageService.getUnreadCount(user.getUserType(), user.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新已读 @luoyi")
|
||||
@GetMapping("/message/read/{messageId}")
|
||||
public ResponseDTO<String> updateReadFlag(@PathVariable Long messageId) {
|
||||
RequestUser user = SmartRequestUtil.getRequestUser();
|
||||
if(user == null){
|
||||
return ResponseDTO.userErrorParam("用户未登录");
|
||||
}
|
||||
|
||||
messageService.updateReadFlag(messageId, user.getUserType(), user.getUserId());
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.lab1024.sa.base.module.support.message.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import net.lab1024.sa.base.module.support.message.domain.MessageEntity;
|
||||
import net.lab1024.sa.base.module.support.message.domain.MessageQueryForm;
|
||||
import net.lab1024.sa.base.module.support.message.domain.MessageVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 消息 接受者类型枚举
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Component
|
||||
@Mapper
|
||||
public interface MessageDao extends BaseMapper<MessageEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询消息
|
||||
*
|
||||
*/
|
||||
List<MessageVO> query(Page<?> page, @Param("query") MessageQueryForm queryForm);
|
||||
|
||||
/**
|
||||
* 更新已读状态
|
||||
*/
|
||||
Integer updateReadFlag(@Param("messageId") Long messageId,
|
||||
@Param("receiverUserType") Integer receiverUserType,
|
||||
@Param("receiverUserId") Long receiverUserId,
|
||||
@Param("readFlag") Boolean readFlag);
|
||||
|
||||
/**
|
||||
* 查询未读消息数
|
||||
*/
|
||||
Long getUnreadCount( @Param("receiverUserType") Integer receiverUserType,
|
||||
@Param("receiverUserId") Long receiverUserId);
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.lab1024.sa.base.module.support.message.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 消息实体
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Data
|
||||
@TableName("t_message")
|
||||
public class MessageEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long messageId;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*
|
||||
* @see MessageTypeEnum
|
||||
*/
|
||||
private Integer messageType;
|
||||
/**
|
||||
* 接收者类型
|
||||
*
|
||||
* @see net.lab1024.sa.base.common.enumeration.UserTypeEnum
|
||||
*/
|
||||
private Integer receiverUserType;
|
||||
|
||||
/**
|
||||
* 接收者id
|
||||
*/
|
||||
private Long receiverUserId;
|
||||
|
||||
/**
|
||||
* 相关业务id
|
||||
*/
|
||||
private String dataId;
|
||||
|
||||
/**
|
||||
* 消息标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 是否已读
|
||||
*/
|
||||
private Boolean readFlag;
|
||||
|
||||
/**
|
||||
* 已读时间
|
||||
*/
|
||||
private LocalDateTime readTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.lab1024.sa.base.module.support.message.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.domain.PageParam;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
|
||||
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 消息查询form
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Data
|
||||
public class MessageQueryForm extends PageParam {
|
||||
|
||||
@Schema(description = "搜索词")
|
||||
@Length(max = 50, message = "搜索词最多50字符")
|
||||
private String searchWord;
|
||||
|
||||
@SchemaEnum(value = MessageTypeEnum.class)
|
||||
@CheckEnum(value = MessageTypeEnum.class, message = "消息类型")
|
||||
private Integer messageType;
|
||||
|
||||
@Schema(description = "是否已读")
|
||||
private Boolean readFlag;
|
||||
|
||||
@Schema(description = "查询开始时间")
|
||||
private LocalDate startDate;
|
||||
|
||||
@Schema(description = "查询结束时间")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Schema(hidden = true)
|
||||
private Long receiverUserId;
|
||||
|
||||
@Schema(hidden = true)
|
||||
private Integer receiverUserType;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.lab1024.sa.base.module.support.message.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
/**
|
||||
* 消息发送form
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Data
|
||||
public class MessageSendForm {
|
||||
|
||||
@SchemaEnum(value = MessageTypeEnum.class, desc = "消息类型")
|
||||
@NotNull(message = "消息类型不能为空")
|
||||
private Integer messageType;
|
||||
|
||||
@SchemaEnum(value = UserTypeEnum.class, desc = "接收者类型")
|
||||
@NotNull(message = "接收者类型不能为空")
|
||||
private Integer receiverUserType;
|
||||
|
||||
@Schema(description = "接收者id")
|
||||
@NotNull(message = "接收者id不能为空")
|
||||
private Long receiverUserId;
|
||||
|
||||
@Schema(description = "标题")
|
||||
@NotBlank(message = "标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "内容")
|
||||
@NotBlank(message = "内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 相关业务id | 可选
|
||||
*/
|
||||
private Object dataId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package net.lab1024.sa.base.module.support.message.domain;
|
||||
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
|
||||
import net.lab1024.sa.base.module.support.message.constant.MessageTemplateEnum;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 消息发送form
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Data
|
||||
public class MessageTemplateSendForm {
|
||||
|
||||
@NotNull(message = "消息子类型不能为空")
|
||||
private MessageTemplateEnum messageTemplateEnum;
|
||||
|
||||
@NotNull(message = "接收者类型不能为空")
|
||||
private UserTypeEnum receiverUserType;
|
||||
|
||||
@NotNull(message = "接收者id不能为空")
|
||||
private Long receiverUserId;
|
||||
|
||||
/**
|
||||
* 相关业务id | 可选
|
||||
* 用于跳转具体业务
|
||||
*/
|
||||
private Object dataId;
|
||||
|
||||
/**
|
||||
* 消息参数 | 可选
|
||||
* 例:订单号:【{orderId}】{time}所提交的对账单被作废,请核实信息重新提交~
|
||||
* {orderId} {time} 就是消息的参数变量
|
||||
* 发送消息时 需要在map中放入k->orderId k->time
|
||||
*/
|
||||
private Map<String, Object> contentParam;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.lab1024.sa.base.module.support.message.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 消息
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Data
|
||||
public class MessageVO {
|
||||
|
||||
private Long messageId;
|
||||
|
||||
@SchemaEnum(value = MessageTypeEnum.class)
|
||||
private Integer messageType;
|
||||
|
||||
@SchemaEnum(value = UserTypeEnum.class)
|
||||
private Integer receiverUserType;
|
||||
|
||||
@Schema(description = "接收者id")
|
||||
private Long receiverUserId;
|
||||
|
||||
@Schema(description = "相关业务id")
|
||||
private String dataId;
|
||||
|
||||
@Schema(description = "消息标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "消息内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "是否已读")
|
||||
private Boolean readFlag;
|
||||
|
||||
@Schema(description = "已读时间")
|
||||
private LocalDateTime readTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.lab1024.sa.base.module.support.message.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import net.lab1024.sa.base.module.support.message.dao.MessageDao;
|
||||
import net.lab1024.sa.base.module.support.message.domain.MessageEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 消息manager
|
||||
*
|
||||
* @author luoyi
|
||||
* @date 2024/06/22 20:20
|
||||
*/
|
||||
@Service
|
||||
public class MessageManager extends ServiceImpl<MessageDao, MessageEntity> {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package net.lab1024.sa.base.module.support.message.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.google.common.collect.Lists;
|
||||
import net.lab1024.sa.base.common.domain.PageResult;
|
||||
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
|
||||
import net.lab1024.sa.base.common.util.SmartBeanUtil;
|
||||
import net.lab1024.sa.base.common.util.SmartPageUtil;
|
||||
import net.lab1024.sa.base.module.support.message.constant.MessageTemplateEnum;
|
||||
import net.lab1024.sa.base.module.support.message.dao.MessageDao;
|
||||
import net.lab1024.sa.base.module.support.message.domain.*;
|
||||
import org.apache.commons.text.StringSubstitutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author luoyi
|
||||
* @date 2024/6/27 12:14 上午
|
||||
*/
|
||||
@Service
|
||||
public class MessageService {
|
||||
|
||||
@Resource
|
||||
private MessageDao messageDao;
|
||||
|
||||
@Resource
|
||||
private MessageManager messageManager;
|
||||
|
||||
/**
|
||||
* 分页查询 消息
|
||||
*/
|
||||
public PageResult<MessageVO> query(MessageQueryForm queryForm) {
|
||||
Page page = SmartPageUtil.convert2PageQuery(queryForm);
|
||||
List<MessageVO> messageVOList = messageDao.query(page, queryForm);
|
||||
return SmartPageUtil.convert2PageResult(page, messageVOList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询未读消息数量
|
||||
*/
|
||||
public Long getUnreadCount(UserTypeEnum userType, Long userId) {
|
||||
return messageDao.getUnreadCount(userType.getValue(), userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新已读状态
|
||||
*/
|
||||
public void updateReadFlag(Long messageId, UserTypeEnum userType, Long receiverUserId) {
|
||||
messageDao.updateReadFlag(messageId, userType.getValue(), receiverUserId, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送【模板消息】
|
||||
*/
|
||||
public void sendTemplateMessage(MessageTemplateSendForm... sendTemplateForms) {
|
||||
List<MessageSendForm> sendFormList = Lists.newArrayList();
|
||||
for (MessageTemplateSendForm sendTemplateForm : sendTemplateForms) {
|
||||
MessageTemplateEnum msgTemplateTypeEnum = sendTemplateForm.getMessageTemplateEnum();
|
||||
StringSubstitutor stringSubstitutor = new StringSubstitutor(sendTemplateForm.getContentParam());
|
||||
String content = stringSubstitutor.replace(msgTemplateTypeEnum.getContent());
|
||||
|
||||
MessageSendForm messageSendForm = new MessageSendForm();
|
||||
messageSendForm.setMessageType(msgTemplateTypeEnum.getMessageTypeEnum().getValue());
|
||||
messageSendForm.setReceiverUserType(sendTemplateForm.getReceiverUserType().getValue());
|
||||
messageSendForm.setReceiverUserId(sendTemplateForm.getReceiverUserId());
|
||||
messageSendForm.setTitle(msgTemplateTypeEnum.getDesc());
|
||||
messageSendForm.setContent(content);
|
||||
messageSendForm.setDataId(sendTemplateForm.getDataId());
|
||||
sendFormList.add(messageSendForm);
|
||||
|
||||
}
|
||||
this.sendMessage(sendFormList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
public void sendMessage(MessageSendForm... sendForms) {
|
||||
this.sendMessage(Lists.newArrayList(sendForms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送通知消息
|
||||
*/
|
||||
public void sendMessage(List<MessageSendForm> sendList) {
|
||||
for (MessageSendForm sendDTO : sendList) {
|
||||
String verify = SmartBeanUtil.verify(sendDTO);
|
||||
if (null != verify) {
|
||||
throw new RuntimeException("send msg error: " + verify);
|
||||
}
|
||||
}
|
||||
List<MessageEntity> messageEntityList = sendList.stream().map(e -> {
|
||||
MessageEntity messageEntity = new MessageEntity();
|
||||
messageEntity.setMessageType(e.getMessageType());
|
||||
messageEntity.setReceiverUserType(e.getReceiverUserType());
|
||||
messageEntity.setReceiverUserId(e.getReceiverUserId());
|
||||
messageEntity.setDataId(String.valueOf(e.getDataId()));
|
||||
messageEntity.setTitle(e.getTitle());
|
||||
messageEntity.setContent(e.getContent());
|
||||
return messageEntity;
|
||||
}).collect(Collectors.toList());
|
||||
messageManager.saveBatch(messageEntityList);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ import lombok.Data;
|
||||
@Data
|
||||
public class OperateLogQueryForm extends PageParam {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long operateUserId;
|
||||
|
||||
@Schema(description = "用户类型")
|
||||
private Integer operateUserType;
|
||||
|
||||
@Schema(description = "开始日期")
|
||||
private String startDate;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package net.lab1024.sa.base.module.support.redis;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.lab1024.sa.base.common.exception.BusinessException;
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RIdGenerator;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Redisson 业务
|
||||
*
|
||||
* @author huke
|
||||
* @date 2024/6/19 20:39
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RedissonService {
|
||||
|
||||
@Autowired
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
public RedissonService(RedissonClient redissonClient) {
|
||||
this.redissonClient = redissonClient;
|
||||
}
|
||||
|
||||
public RedissonClient getRedissonClient() {
|
||||
return redissonClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁 并 执行程序
|
||||
*
|
||||
* @param lockKey
|
||||
* @param waitTime 毫秒
|
||||
* @param lockTime 毫秒
|
||||
* @param supplier
|
||||
*/
|
||||
public <T> T executeWithLock(String lockKey, long waitTime, long lockTime, Supplier<T> supplier) {
|
||||
// 获取锁
|
||||
RLock lock = this.tryLock(lockKey, waitTime, lockTime);
|
||||
try {
|
||||
return supplier.get();
|
||||
} finally {
|
||||
// 释放锁
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁 并 执行程序
|
||||
*
|
||||
* @param lockKey
|
||||
* @param waitTime 毫秒
|
||||
* @param lockTime 毫秒
|
||||
* @param runnable
|
||||
*/
|
||||
public void executeWithLock(String lockKey, long waitTime, long lockTime, Runnable runnable) {
|
||||
// 获取锁
|
||||
RLock lock = this.tryLock(lockKey, waitTime, lockTime);
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
// 释放锁
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁
|
||||
* 最多等待 waitTime 毫秒
|
||||
* 获取锁成功后占用 lockTime 毫秒
|
||||
* ps:需要手动解锁 lock.unlock()
|
||||
*
|
||||
* @param lockKey
|
||||
* @param waitTime 毫秒
|
||||
* @param lockTime 毫秒
|
||||
* @return
|
||||
*/
|
||||
public RLock tryLock(String lockKey, long waitTime, long lockTime) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
boolean getLock = lock.tryLock(waitTime, lockTime, TimeUnit.MILLISECONDS);
|
||||
if (getLock) {
|
||||
return lock;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Redisson tryLock", e);
|
||||
}
|
||||
throw new BusinessException("业务繁忙,请稍后重试~");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 id 生成器
|
||||
* nextId 可生成连续不重复的id
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public RIdGenerator idGenerator(String key) {
|
||||
return redissonClient.getIdGenerator(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 存放任意数据类型
|
||||
*
|
||||
* @param key
|
||||
* @param v
|
||||
* @param duration
|
||||
* @param <T>
|
||||
*/
|
||||
public <T> void putObj(String key, T v, Duration duration) {
|
||||
redissonClient.getBucket(key).set(v, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任意数据类型
|
||||
*
|
||||
* @param key
|
||||
* @param clazz
|
||||
* @param <T>
|
||||
* @return 如果没有找到则返回null
|
||||
*/
|
||||
public <T> T getObj(String key, Class<T> clazz) {
|
||||
RBucket<T> bucket = redissonClient.getBucket(key);
|
||||
return bucket.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,9 +6,15 @@ import net.lab1024.sa.base.module.support.reload.core.annoation.SmartReload;
|
||||
import net.lab1024.sa.base.module.support.reload.core.domain.SmartReloadObject;
|
||||
import net.lab1024.sa.base.module.support.reload.core.thread.SmartReloadRunnable;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.annotation.Resource;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -24,19 +30,32 @@ import java.util.concurrent.TimeUnit;
|
||||
* @Date 2015-03-02 19:11:52
|
||||
* @Wechat zhuoda1024
|
||||
* @Email lab1024@163.com
|
||||
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
|
||||
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SmartReloadManager implements BeanPostProcessor {
|
||||
|
||||
private static final String THREAD_NAME_PREFIX = "smart-reload";
|
||||
private static final int THREAD_COUNT = 1;
|
||||
|
||||
private Map<String, SmartReloadObject> reloadObjectMap = new ConcurrentHashMap<>();
|
||||
@Value("${reload.interval-seconds}")
|
||||
private Integer intervalSeconds;
|
||||
|
||||
@Resource
|
||||
private AbstractSmartReloadCommand reloadCommand;
|
||||
|
||||
private final Map<String, SmartReloadObject> reloadObjectMap = new ConcurrentHashMap<>();
|
||||
|
||||
private ScheduledThreadPoolExecutor threadPoolExecutor;
|
||||
|
||||
public SmartReloadManager(AbstractSmartReloadCommand reloadCommand, int intervalSeconds) {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (threadPoolExecutor != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.threadPoolExecutor = new ScheduledThreadPoolExecutor(THREAD_COUNT, r -> {
|
||||
Thread t = new Thread(r, THREAD_NAME_PREFIX);
|
||||
if (!t.isDaemon()) {
|
||||
@@ -44,17 +63,23 @@ public class SmartReloadManager implements BeanPostProcessor {
|
||||
}
|
||||
return t;
|
||||
});
|
||||
this.threadPoolExecutor.scheduleWithFixedDelay(new SmartReloadRunnable(reloadCommand), 10, intervalSeconds, TimeUnit.SECONDS);
|
||||
reloadCommand.setReloadManager(this);
|
||||
this.threadPoolExecutor.scheduleWithFixedDelay(new SmartReloadRunnable(this.reloadCommand), 10, this.intervalSeconds, TimeUnit.SECONDS);
|
||||
this.reloadCommand.setReloadManager(this);
|
||||
}
|
||||
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
if (this.threadPoolExecutor != null) {
|
||||
this.threadPoolExecutor.shutdownNow();
|
||||
this.threadPoolExecutor = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
|
||||
if (methods == null) {
|
||||
return bean;
|
||||
}
|
||||
for (Method method : methods) {
|
||||
SmartReload smartReload = method.getAnnotation(SmartReload.class);
|
||||
if (smartReload == null) {
|
||||
|
||||
@@ -139,4 +139,17 @@ sa-token:
|
||||
# 启动时的字符画打印
|
||||
is-print: false
|
||||
# 是否从cookie读取token
|
||||
is-read-cookie: false
|
||||
is-read-cookie: false
|
||||
|
||||
# SmartJob 定时任务配置(不需要可以直接删除以下配置,详细文档请看:https://www.xxxxxx.com)
|
||||
smart:
|
||||
job:
|
||||
enabled: true
|
||||
# 任务初始化延迟 默认30秒 可选
|
||||
init-delay: 10
|
||||
# 定时任务执行线程池数量 默认2 可选
|
||||
core-pool-size: 2
|
||||
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
|
||||
db-refresh-enabled: true
|
||||
# 数据库配置检测-执行间隔 默认120秒 可选
|
||||
db-refresh-interval: 60
|
||||
@@ -7,6 +7,15 @@
|
||||
*
|
||||
from t_login_log
|
||||
<where>
|
||||
<if test="query.userId != null">
|
||||
AND user_id = #{query.userId}
|
||||
</if>
|
||||
<if test="query.userType != null">
|
||||
AND user_type = #{query.userType}
|
||||
</if>
|
||||
<if test="query.ip != null">
|
||||
AND INSTR(login_ip,#{query.ip})
|
||||
</if>
|
||||
<if test="query.startDate != null and query.startDate != ''">
|
||||
AND DATE_FORMAT(create_time, '%Y-%m-%d') >= #{query.startDate}
|
||||
</if>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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="net.lab1024.sa.base.module.support.message.dao.MessageDao">
|
||||
|
||||
<!-- 更新已读状态 -->
|
||||
<update id="updateReadFlag">
|
||||
UPDATE t_message
|
||||
SET read_flag = #{readFlag},
|
||||
read_time = now()
|
||||
WHERE message_id = #{messageId}
|
||||
AND receiver_user_type = #{receiverUserType}
|
||||
AND receiver_user_id = #{receiverUserId}
|
||||
AND read_flag != #{readFlag}
|
||||
</update>
|
||||
|
||||
<!-- 分页查询消息 -->
|
||||
<select id="query" resultType="net.lab1024.sa.base.module.support.message.domain.MessageVO">
|
||||
SELECT * FROM t_message
|
||||
<where>
|
||||
<if test="query.receiverUserType != null">
|
||||
AND receiver_user_type = #{query.receiverUserType}
|
||||
</if>
|
||||
<if test="query.receiverUserId != null">
|
||||
AND receiver_user_id = #{query.receiverUserId}
|
||||
</if>
|
||||
<if test="query.messageType != null">
|
||||
AND message_type = #{query.messageType}
|
||||
</if>
|
||||
<if test="query.searchWord != null and query.searchWord !=''">
|
||||
AND ( INSTR(title,#{query.searchWord})
|
||||
OR INSTR(content,#{query.searchWord})
|
||||
)
|
||||
</if>
|
||||
<if test="query.readFlag != null">
|
||||
AND read_flag = #{query.readFlag}
|
||||
</if>
|
||||
<if test="query.startDate != null">
|
||||
AND DATE_FORMAT(create_time, '%Y-%m-%d') >= DATE_FORMAT(#{query.startDate}, '%Y-%m-%d')
|
||||
</if>
|
||||
<if test="query.endDate != null">
|
||||
AND DATE_FORMAT(create_time, '%Y-%m-%d') <= DATE_FORMAT(#{query.endDate}, '%Y-%m-%d')
|
||||
</if>
|
||||
</where>
|
||||
<if test="query.sortItemList == null or query.sortItemList.size == 0">
|
||||
ORDER BY message_id DESC
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getUnreadCount" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM t_message
|
||||
where receiver_user_type = #{receiverUserType}
|
||||
AND receiver_user_id = #{receiverUserId}
|
||||
AND read_flag = false
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -7,6 +7,12 @@
|
||||
*
|
||||
from t_operate_log
|
||||
<where>
|
||||
<if test="query.operateUserId != null">
|
||||
AND operate_user_id = #{query.operateUserId}
|
||||
</if>
|
||||
<if test="query.operateUserType != null">
|
||||
AND operate_user_type = #{query.operateUserType}
|
||||
</if>
|
||||
<if test="query.startDate != null and query.startDate != ''">
|
||||
AND DATE_FORMAT(create_time, '%Y-%m-%d') >= #{query.startDate}
|
||||
</if>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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="net.lab1024.sa.base.module.support.job.repository.SmartJobLogDao">
|
||||
|
||||
<!-- 定时任务-执行记录-分页查询 -->
|
||||
<select id="query" resultType="net.lab1024.sa.base.module.support.job.api.domain.SmartJobLogVO">
|
||||
SELECT *
|
||||
FROM t_smart_job_log
|
||||
<where>
|
||||
<if test="query.searchWord != null and query.searchWord != ''">
|
||||
AND ( INSTR(job_name,#{query.searchWord})
|
||||
OR INSTR(param,#{query.searchWord})
|
||||
OR INSTR(execute_result,#{query.searchWord})
|
||||
OR INSTR(create_name,#{query.searchWord})
|
||||
)
|
||||
</if>
|
||||
<if test="query.jobId != null">
|
||||
AND job_id = #{query.jobId}
|
||||
</if>
|
||||
<if test="query.successFlag != null">
|
||||
AND success_flag = #{query.successFlag}
|
||||
</if>
|
||||
<if test="query.startTime != null">
|
||||
AND DATE_FORMAT(execute_start_time, '%Y-%m-%d') >= #{query.startTime}
|
||||
</if>
|
||||
<if test="query.endTime != null">
|
||||
AND DATE_FORMAT(execute_start_time, '%Y-%m-%d') <= #{query.endTime}
|
||||
</if>
|
||||
</where>
|
||||
<if test="query.sortItemList == null or query.sortItemList.size == 0">
|
||||
ORDER BY log_id DESC
|
||||
</if>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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="net.lab1024.sa.base.module.support.job.repository.SmartJobDao">
|
||||
|
||||
<!-- 定时任务-分页查询 -->
|
||||
<select id="query" resultType="net.lab1024.sa.base.module.support.job.api.domain.SmartJobVO">
|
||||
SELECT *
|
||||
FROM t_smart_job
|
||||
<where>
|
||||
<if test="query.searchWord != null and query.searchWord != ''">
|
||||
AND ( INSTR(job_name,#{query.searchWord})
|
||||
OR INSTR(job_class,#{query.searchWord})
|
||||
OR INSTR(trigger_value,#{query.searchWord})
|
||||
)
|
||||
</if>
|
||||
<if test="query.triggerType != null">
|
||||
AND trigger_type = #{query.triggerType}
|
||||
</if>
|
||||
<if test="query.enabledFlag != null">
|
||||
AND enabled_flag = #{query.enabledFlag}
|
||||
</if>
|
||||
</where>
|
||||
<if test="query.sortItemList == null or query.sortItemList.size == 0">
|
||||
ORDER BY sort ASC,job_id DESC
|
||||
</if>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -141,4 +141,17 @@ sa-token:
|
||||
# 启动时的字符画打印
|
||||
is-print: false
|
||||
# 是否从cookie读取token
|
||||
is-read-cookie: false
|
||||
is-read-cookie: false
|
||||
|
||||
# SmartJob 定时任务配置(不需要可以直接删除以下配置,详细文档请看:https://www.xxxxxx.com)
|
||||
smart:
|
||||
job:
|
||||
enabled: true
|
||||
# 任务初始化延迟 默认30秒 可选
|
||||
init-delay: 10
|
||||
# 定时任务执行线程池数量 默认2 可选
|
||||
core-pool-size: 2
|
||||
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
|
||||
db-refresh-enabled: true
|
||||
# 数据库配置检测-执行间隔 默认120秒 可选
|
||||
db-refresh-interval: 60
|
||||
@@ -1,9 +1,9 @@
|
||||
spring:
|
||||
# 数据库连接信息
|
||||
datasource:
|
||||
url: jdbc:mysql://127.0.0.1:3306/smart_admin_v3?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
|
||||
url: jdbc:mysql://127.0.0.1:3306/smart_admin_v3_dev?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: Zhuoda1024lab
|
||||
password: Zhuoda#1024lab
|
||||
initial-size: 10
|
||||
min-idle: 10
|
||||
max-active: 100
|
||||
@@ -108,7 +108,7 @@ http:
|
||||
keep-alive: 300000
|
||||
|
||||
# 跨域配置
|
||||
access-control-allow-origin: 'smartadmin.vip'
|
||||
access-control-allow-origin: 'https://preview.smartadmin.vip'
|
||||
|
||||
# 心跳配置
|
||||
heart-beat:
|
||||
@@ -141,4 +141,17 @@ sa-token:
|
||||
# 启动时的字符画打印
|
||||
is-print: false
|
||||
# 是否从cookie读取token
|
||||
is-read-cookie: false
|
||||
is-read-cookie: false
|
||||
|
||||
# SmartJob 定时任务配置(不需要可以直接删除以下配置,详细文档请看:https://www.xxxxxx.com)
|
||||
smart:
|
||||
job:
|
||||
enabled: true
|
||||
# 任务初始化延迟 默认30秒 可选
|
||||
init-delay: 10
|
||||
# 定时任务执行线程池数量 默认2 可选
|
||||
core-pool-size: 2
|
||||
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
|
||||
db-refresh-enabled: true
|
||||
# 数据库配置检测-执行间隔 默认120秒 可选
|
||||
db-refresh-interval: 60
|
||||
@@ -74,9 +74,9 @@ file:
|
||||
upload-path: /home/smart_admin_v3/upload/ #文件上传目录
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-qingdao
|
||||
endpoint: oss-cn-qingdao.aliyuncs.com
|
||||
bucket-name: common
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
@@ -89,6 +89,7 @@ springdoc:
|
||||
enabled: true # 开关
|
||||
doc-expansion: none #关闭展开
|
||||
tags-sorter: alpha
|
||||
server-base-url: http://smartadmin.dev.1024lab.net/api/
|
||||
api-docs:
|
||||
enabled: true # 开关
|
||||
knife4j:
|
||||
@@ -141,4 +142,17 @@ sa-token:
|
||||
# 启动时的字符画打印
|
||||
is-print: false
|
||||
# 是否从cookie读取token
|
||||
is-read-cookie: false
|
||||
is-read-cookie: false
|
||||
|
||||
# SmartJob 定时任务配置(不需要可以直接删除以下配置,详细文档请看:https://www.xxxxxx.com)
|
||||
smart:
|
||||
job:
|
||||
enabled: true
|
||||
# 任务初始化延迟 默认30秒 可选
|
||||
init-delay: 10
|
||||
# 定时任务执行线程池数量 默认2 可选
|
||||
core-pool-size: 2
|
||||
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
|
||||
db-refresh-enabled: true
|
||||
# 数据库配置检测-执行间隔 默认120秒 可选
|
||||
db-refresh-interval: 60
|
||||
Reference in New Issue
Block a user