diff --git a/server/src/main/java/com/aisino/iles/Application.java b/server/src/main/java/com/aisino/iles/Application.java new file mode 100644 index 0000000..d97027f --- /dev/null +++ b/server/src/main/java/com/aisino/iles/Application.java @@ -0,0 +1,33 @@ +package com.aisino.iles; + +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.retry.annotation.EnableRetry; + +import java.security.Security; +import java.util.Arrays; + +@SpringBootApplication(scanBasePackages = { + "com.aisino", + "com.smartlx" +}) +@EnableCaching +@EnableRetry // 启用重试功能 +@Slf4j +public class Application { + public static void main(String[] args) { + // 1. 检查并添加 BC 提供商 + Security.removeProvider("BC"); + Security.addProvider(new BouncyCastleProvider()); + log.info("BC 提供商版本:{} ", Security.getProvider("BC").getVersionStr()); + log.info("已注册的加密提供商:"); + Arrays.stream(Security.getProviders()).forEach(p -> + log.info(" - " + p.getName() + " (" + p.getInfo() + ")") + ); + + SpringApplication.run(Application.class, args); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/annotation/CurrentUser.java b/server/src/main/java/com/aisino/iles/core/annotation/CurrentUser.java new file mode 100644 index 0000000..1d245aa --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/annotation/CurrentUser.java @@ -0,0 +1,13 @@ +package com.aisino.iles.core.annotation; + +import java.lang.annotation.*; + +/** + * 当前用户标记,用于帮助标记参数,方便用户属性注入 + */ +@Documented +@Target({ElementType.PARAMETER,ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CurrentUser { + +} diff --git a/server/src/main/java/com/aisino/iles/core/annotation/Log.java b/server/src/main/java/com/aisino/iles/core/annotation/Log.java new file mode 100644 index 0000000..ad0cb23 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/annotation/Log.java @@ -0,0 +1,37 @@ +package com.aisino.iles.core.annotation; + +import com.aisino.iles.core.model.enums.OperateType; + +import java.lang.annotation.*; + +/** + * 操作日志注解标记 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log { + /** + * 日志内容 + * @return 日志内容描述 + */ + String value(); + + /** + * 操作类型 + * @return 操作日志类型分类 增 删 改 查 等 + */ + OperateType type(); + + /** + * 格外的描述信息 + * @return 格外的描述信息 + */ + String description() default ""; + + /** + * 忽略参数 + * @return 忽略参数 + */ + boolean ignoreArgs() default false; +} diff --git a/server/src/main/java/com/aisino/iles/core/annotation/handlers/CurrentUserMethodArgResolver.java b/server/src/main/java/com/aisino/iles/core/annotation/handlers/CurrentUserMethodArgResolver.java new file mode 100644 index 0000000..b89ca09 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/annotation/handlers/CurrentUserMethodArgResolver.java @@ -0,0 +1,90 @@ +package com.aisino.iles.core.annotation.handlers; + +import cn.hutool.core.util.StrUtil; +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.annotation.CurrentUser; +import com.aisino.iles.core.exception.TokenError; +import com.aisino.iles.core.model.User; +import com.aisino.iles.core.util.RedisUtil; +import com.smartlx.sso.client.model.AccessToken; +import com.smartlx.sso.client.model.RemoteUserInfo; +import com.smartlx.sso.client.service.SsoClientService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * 当前登录用户方法参数注入处理器工具 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class CurrentUserMethodArgResolver implements HandlerMethodArgumentResolver { + private final SsoClientService ssoClientService; + + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + if (methodParameter.getParameterType().isAssignableFrom(User.class) && methodParameter.hasParameterAnnotation(CurrentUser.class)) { + return true; + } + return methodParameter.getParameterType().isAssignableFrom(RemoteUserInfo.class) && methodParameter.hasParameterAnnotation(CurrentUser.class); + } + + @Override + public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { + if (methodParameter.getParameterType().isAssignableFrom(RemoteUserInfo.class)) { + // 如果请求属性中有用户ID,则从redisutil中返回 + if (nativeWebRequest.getAttribute(Constants.CURRENT_USER_ID, RequestAttributes.SCOPE_REQUEST) != null) { + String userId = (String) nativeWebRequest.getAttribute(Constants.CURRENT_USER_ID, RequestAttributes.SCOPE_REQUEST); + + // 构建Redis中存储的用户信息的key + String userInfoKey = StrUtil.format(Constants.RedisKeyPrefix.userInfo.getValue(), userId); + + // 从Redis中获取用户信息 + RemoteUserInfo userInfo = RedisUtil.get(userInfoKey, RemoteUserInfo.class); + if (userInfo != null) { + return userInfo; + } + + } + // 如果请求属性中没有用户ID,则从header中的Authorization信息获取 + String authorization = nativeWebRequest.getHeader("Authorization"); + + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) { + try { + // 提取access_token + String token = authorization.substring(7); + + // 创建AccessToken对象 + AccessToken accessToken = new AccessToken(); + accessToken.setAccess_token(token); + + // 使用SsoClientService获取用户信息 + RemoteUserInfo userInfo = ssoClientService.getRemoteUserInfo(accessToken); + + if (userInfo != null && StringUtils.hasText(userInfo.getYhwybs())) { + return userInfo; + } else { + log.warn("无效的access_token或用户信息不完整"); + throw new TokenError("无效的access_token或用户信息不完整"); + } + } catch (Exception e) { + log.error("获取用户信息时发生错误", e); + throw new TokenError("获取用户信息时发生错误"); + } + } + + // 如果没有Authorization头或格式不正确,抛出异常 + log.warn("请求中缺少有效的Authorization头"); + throw new TokenError("请求中缺少有效的Authorization头"); + } + throw new TokenError("请求中缺少有效的Authorization头"); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/aop/LogAspect.java b/server/src/main/java/com/aisino/iles/core/aop/LogAspect.java new file mode 100644 index 0000000..c196725 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/aop/LogAspect.java @@ -0,0 +1,152 @@ +package com.aisino.iles.core.aop; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.util.Constants.GlobalParams.OperateLogSwitch; +import com.aisino.iles.common.util.InetAddressUtil; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.annotation.CurrentUser; +import com.aisino.iles.core.annotation.Log; +import com.aisino.iles.core.model.OperateLog; +import com.aisino.iles.core.model.OperateLogText; +import com.aisino.iles.core.model.enums.IpType; +import com.aisino.iles.core.model.enums.OperateLogTextType; +import com.aisino.iles.core.model.enums.OperateStatus; +import com.aisino.iles.core.repository.OperateLogRepo; +import com.aisino.iles.core.service.GlobalParamService; +import com.aisino.iles.core.util.TokenUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Aspect +@Component +@ConditionalOnProperty(prefix = "operate-log", name = "enable", havingValue = "true", matchIfMissing = true) +@Slf4j +public class LogAspect { + private final HttpServletRequest request; + private final ObjectMapper http2JacksonObjectMapper; + private final OperateLogRepo operateLogRepo; + private final TransactionTemplate transactionTemplate; + private final GlobalParamService globalParamService; + + @Autowired + public LogAspect(HttpServletRequest request, + ObjectMapper http2JacksonObjectMapper, + OperateLogRepo operateLogRepo, + TransactionTemplate transactionTemplate, + GlobalParamService globalParamService) { + this.request = request; + this.http2JacksonObjectMapper = http2JacksonObjectMapper; + this.operateLogRepo = operateLogRepo; + this.transactionTemplate = transactionTemplate; + this.globalParamService = globalParamService; + } + + @Pointcut("@annotation(com.aisino.iles.core.annotations.Log)") + private void pointcut() { + } + + /** + * 记录日志 + * + * @param point 接入点 + */ + @Around("pointcut()") + public Object saveLog(ProceedingJoinPoint point) throws Throwable { + // 获取内外网标记参数 + OperateLogSwitch logSwitch = globalParamService.findByCode(Constants.GlobalParams.OperateLogSwitch.code) + .map(g->Constants.GlobalParams.OperateLogSwitch.fromValue(g.getGlobalParamValue())) + .orElse(Constants.GlobalParams.OperateLogSwitch.file); // 默认为文件记录 + // 标记方法调用 + Object proceed; + Log logAnnotation = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(Log.class); + String userIpAddr = InetAddressUtil.getIpAddress(request); + // 序列化排除用户信息,用户信息的链式反应太多了,去掉性能应该会好一点 + Set args = IntStream.range(0, point.getArgs().length) + .filter(i -> { + Annotation[] parameterAnnotation = ((MethodSignature) point.getSignature()).getMethod().getParameterAnnotations()[i]; + Class[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes(); + boolean annotationResult = Arrays.stream(parameterAnnotation).noneMatch(a -> (a.annotationType().isAssignableFrom(CurrentUser.class))); + boolean parTypeResult = Arrays.stream(parameterTypes).noneMatch(p -> p.equals(HttpServletRequest.class) || p.equals(HttpServletResponse.class)); + return annotationResult && parTypeResult; + }) + .mapToObj(i -> point.getArgs()[i]) + .collect(Collectors.toSet()); + OperateLog.OperateLogBuilder builder = OperateLog.builder() + .executionBeginTime(LocalDateTime.now()) + .method(Optional.ofNullable(point.getSignature().getDeclaringTypeName()).map(n -> n + ".").orElse("") + point.getSignature().getName()) + .args(!logAnnotation.ignoreArgs() ? + OperateLogText.builder() + .content(http2JacksonObjectMapper.writeValueAsString(args)) + .type(OperateLogTextType.args).build() : null) + .ip(userIpAddr) + .ipType(InetAddressUtil.isIPv4Address(userIpAddr) ? IpType.v4 : IpType.v6) + .name(logAnnotation.value()) + .description(logAnnotation.description()) + .operateType(logAnnotation.type()); + try { + String token = ""; // todo 这里需要重新实现。 + Optional.ofNullable(TokenUtil.parseToken(token)) + .ifPresent(tk -> { + try { + Optional.ofNullable(http2JacksonObjectMapper.readTree(tk.getJsonProfile()) + .path("user") + .path("idNum").asText()) + .filter(StringUtils::isNotEmpty) + .ifPresent(builder::idNum); + } catch (JsonProcessingException e) { + log.error("解析令牌错误:" + e.getMessage(), e); + } + }); + } catch (RuntimeException ignored) { + + } + + try { + proceed = point.proceed(); + } catch (Throwable throwable) { + StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw)); + String fullStackError = sw.toString(); + sw.close(); + builder.status(OperateStatus.FAILURE) + .executionEndTime(LocalDateTime.now()) + .error(OperateLogText.builder().type(OperateLogTextType.errors).content(throwable.getLocalizedMessage() + "\n" + fullStackError).build()); + throw throwable; + } finally { + OperateLog olog = builder.status(OperateStatus.SUCCESS) + .executionEndTime(LocalDateTime.now()).build(); + if (logSwitch == OperateLogSwitch.db) { + transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + transactionTemplate.executeWithoutResult(status-> operateLogRepo.save(olog)); + } + else if (logSwitch == OperateLogSwitch.file) { + log.info("operate_name|operate_type|status|execution_begin_time|execution_end_time|ip_type|ip|user_id|method_name|args|error\n\n" + + String.format("%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s", olog.getName(), olog.getOperateType(), olog.getStatus(), olog.getExecutionBeginTime(), olog.getExecutionEndTime(), olog.getIpType(), olog.getIp(), Optional.ofNullable(request.getHeader(Constants.CURRENT_USER_ID)).orElse(""), olog.getMethod(), Optional.ofNullable(olog.getArgs()).map(OperateLogText::getContent).orElse(""), Optional.ofNullable(olog.getError()).map(OperateLogText::getContent).orElse(""))); + } + } + return proceed; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/config/AuditConfig.java b/server/src/main/java/com/aisino/iles/core/config/AuditConfig.java new file mode 100644 index 0000000..bf9270a --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/AuditConfig.java @@ -0,0 +1,73 @@ +package com.aisino.iles.core.config; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.Token; +import com.aisino.iles.core.util.TokenUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Optional; + +@Configuration +public class AuditConfig { + @EnableJpaAuditing(dateTimeProviderRef = "auditDateTimeProvider") + public static class JpaAuditConfig { + + } + + /** + * 获取当前用户信息接口 + * 字符串主键实现 + * + * @author huxin + * @since 2020-10-12 + */ + @Component + @ConditionalOnProperty(prefix = "data-audit", name = "enable", havingValue = "true", matchIfMissing = true) + static class AuditorAwareStringImpl implements AuditorAware { + private final HttpServletRequest request; + + AuditorAwareStringImpl(HttpServletRequest request) { + this.request = request; + } + + @Override + public Optional getCurrentAuditor() { + return Optional.ofNullable(request).map(req -> req.getHeader(Constants.Headers.AUTH_TOKEN)) + .map(TokenUtil::parseToken) + .map(Token::getUid); + } + } + + /** + * 审计功能用的日期时间提供者 + * + * @author hx + * @since 20210818 + */ + static class AuditDateTimeProvider implements DateTimeProvider { + @Override + public Optional getNow() { + return Optional.of(LocalDateTime.parse(LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss")))); + } + } + /** + * 审计功能用的日期时间提供者(实例) + * + * @author hx + * @since 20210818 + */ + @Bean + public AuditDateTimeProvider auditDateTimeProvider() { + return new AuditDateTimeProvider(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/config/CoreCacheConfig.java b/server/src/main/java/com/aisino/iles/core/config/CoreCacheConfig.java new file mode 100644 index 0000000..50073ef --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/CoreCacheConfig.java @@ -0,0 +1,37 @@ +package com.aisino.iles.core.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableCaching +public class CoreCacheConfig { + @Deprecated + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, ObjectMapper objectMapper) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + ObjectMapper mapper = objectMapper.copy(); + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL); + mapper.addMixIn(Object.class, JacksonConfig.JsonMixinForDataPackage.class); + + GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(mapper); + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + template.setHashKeySerializer(stringRedisSerializer); + template.setKeySerializer(stringRedisSerializer); + template.setValueSerializer(jsonRedisSerializer); + template.setHashValueSerializer(jsonRedisSerializer); + template.afterPropertiesSet(); + + return template; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/config/DataPackageConfig.java b/server/src/main/java/com/aisino/iles/core/config/DataPackageConfig.java new file mode 100644 index 0000000..c78c3d8 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/DataPackageConfig.java @@ -0,0 +1,52 @@ +package com.aisino.iles.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@ConfigurationProperties(prefix = "data-package") +@Data +public class DataPackageConfig { + /** + * 需要打包的实体名称(列表) + */ + private ClassPattern processEntityNames; + /** + * 写入目标redis失败时,重试次数 + */ + private int retryTimes; + /** + * 写入目标redis失败时,重试间隔时间(毫秒) + */ + private int retryInterval; + /** + * 打包标志 + */ + private boolean packagingFlag = false; + /** + * 打包接口地址 + */ + private String packagingApi; + + private List dataPackProcessors = new ArrayList<>(); + + /** + * 类名匹配 + */ + @Data + public static class ClassPattern { + /** + * 排除列表 + */ + private List exclude = new ArrayList<>(); + /** + * 包含列表 + */ + private List include = new ArrayList<>(); + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/config/DataSourceConfig.java b/server/src/main/java/com/aisino/iles/core/config/DataSourceConfig.java new file mode 100644 index 0000000..3df64e2 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/DataSourceConfig.java @@ -0,0 +1,57 @@ +package com.aisino.iles.core.config; + +import com.aisino.iles.common.repository.impl.CommonRepositoryImpl; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings; +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; + +import javax.sql.DataSource; +import java.util.Map; + +@Configuration +@EnableJpaRepositories( + basePackages = { + "com.aisino.iles.*.repository" + }, + repositoryBaseClass = CommonRepositoryImpl.class, + entityManagerFactoryRef = "entityManagerFactoryIles") +public class DataSourceConfig { + private final JpaProperties jpaProperties; + private final HibernateProperties hibernateProperties; + private final HibernateSettings hibernateSettings; + + + public DataSourceConfig(JpaProperties jpaProperties, HibernateProperties hibernateProperties, HibernateSettings hibernateSettings) { + this.jpaProperties = jpaProperties; + this.hibernateProperties = hibernateProperties; + this.hibernateSettings = hibernateSettings; + } + + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.hikari") + public DataSource dataSource() { + return new HikariDataSource(); + } + + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryIles(EntityManagerFactoryBuilder builder, DataSource dataSource) { + return builder.dataSource(dataSource) + .packages("com.aisino.iles.*.model") + .persistenceUnit("persistenceIles") + .properties(getVendorProperties()) + .build(); + } + + private Map getVendorProperties() { + return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), hibernateSettings); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/config/HibernateConfig.java b/server/src/main/java/com/aisino/iles/core/config/HibernateConfig.java new file mode 100644 index 0000000..dbde206 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/HibernateConfig.java @@ -0,0 +1,25 @@ +package com.aisino.iles.core.config; + +import com.aisino.iles.core.hibernate.DataPackageInterceptor; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Set; + +@Configuration +public class HibernateConfig { + @Bean + public HibernateSettings hibernateSettings(Set hibernatePropertiesCustomizers) { + HibernateSettings hibernateSettings = new HibernateSettings(); + hibernateSettings.hibernatePropertiesCustomizers(hibernatePropertiesCustomizers); + return hibernateSettings; + } + + @Bean + public HibernatePropertiesCustomizer dataPackageInterceptorCustomizer(DataPackageInterceptor dataPackageInterceptor) { + return hp -> hp.put("hibernate.session_factory.interceptor", dataPackageInterceptor); + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/config/JacksonConfig.java b/server/src/main/java/com/aisino/iles/core/config/JacksonConfig.java new file mode 100644 index 0000000..a4a0c61 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/JacksonConfig.java @@ -0,0 +1,129 @@ +package com.aisino.iles.core.config; + +import com.aisino.iles.core.converter.json.*; +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import jakarta.persistence.Transient; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Configuration +public class JacksonConfig { + + @Bean + @Primary + ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.registerModule(new Jdk8Module()); + objectMapper.registerModule(new ParameterNamesModule()); + // 注册自定义模块 + SimpleModule customModule = new SimpleModule(); + // 自己带时区转换的LOCAL_DATE_TIME + customModule.addDeserializer(LocalDateTime.class, new HttpJackson2LocalDateTimeConverter()); + customModule.addDeserializer(LocalDate.class, new HttpJackson2LocalDateConverter()); + // 处理字符串参数内容两端包含空格的数据,自动转换去掉 + customModule.addDeserializer(String.class, new StringTrimDeserializer()); + objectMapper.registerModule(customModule); + // 处理JPA延迟加载类型未加载时序列化问题 + Hibernate6Module hibernateModule = new Hibernate6Module(); + // 序列化时候,只序列化延迟加载未载入的manytoone对象的主键信息 + hibernateModule.enable(Hibernate6Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS); + // 序列化使用了临时标记的属性 + hibernateModule.disable(Hibernate6Module.Feature.USE_TRANSIENT_ANNOTATION); + // 序列化时使用基本集合类型hashset,arraylist,hashmap等替换hibernate的持久化集合类型 +// hibernateModule.enable(Hibernate5Module.Feature.REPLACE_PERSISTENT_COLLECTIONS); + // 序列化不存在的实体为空值 + hibernateModule.enable(Hibernate6Module.Feature.WRITE_MISSING_ENTITIES_AS_NULL); + objectMapper.registerModule(hibernateModule); + // 关闭标记了忽略的属性出现报错的功能 + objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES,false); + // 关闭遇到未知属性报错的功能 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + // 把数组,集合之类的为null的,转换为空数组 (注意不包含延迟加载的属性) + objectMapper.setSerializerFactory(objectMapper.getSerializerFactory() + .withSerializerModifier(new SerializerModifier())); + return objectMapper; + } + + @Bean + public ObjectMapper dataPackageObjectMapper(Jackson2ObjectMapperBuilder builder) { + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.registerModule(new Jdk8Module()); + objectMapper.registerModule(new ParameterNamesModule()); + // 自己带时区转换的LOCAL_DATE_TIME + SimpleModule javaTimeModule = new SimpleModule(); + javaTimeModule.addDeserializer(LocalDateTime.class, new HttpJackson2LocalDateTimeConverter()); + javaTimeModule.addDeserializer(LocalDate.class, new HttpJackson2LocalDateConverter()); + objectMapper.registerModule(javaTimeModule); + // 预加载延迟集合属性 + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // 激活默认类型方式 +// objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), +// ObjectMapper.DefaultTyping.NON_FINAL); + // 忽略标记了不序列化的属性,保证数据完整。(这个会照成延迟属性失效) + objectMapper.setAnnotationIntrospector(new DataPackageJacksonAnnotationIntrospector()); + // 忽略数据为空的属性,即可以提高序列化速度也可以,缩短序列化长度 + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.setSerializerFactory(objectMapper.getSerializerFactory() + // 不序列化需要延迟加载的集合,序列化空集合或者数组为空数组 + .withSerializerModifier(new DataPackageHibernateDontLazySerializerModifier()) + // 把数组,集合之类的为null的,转换为空数组 (注意不包含延迟加载的属性) + .withSerializerModifier(new SerializerModifier())); + // 打包数据id编号,以及忽略不存在的属性. + objectMapper.addMixIn(Object.class, JsonMixinForDataPackage.class); + return objectMapper; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class JsonMixinForDataPackage { + } + + /** + * 打包用json注解处理器 + */ + public static class DataPackageJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector { + /** + * 这个方法的作用是处理设置了 {@link JsonIgnoreProperties} 的注解内容 + *

+ * (这里忽略所有标记了json忽略属性的注解{@link JsonIgnoreProperties}) + * + * @param a 被注解的属性 + * @return 返回不序列化注解属性的值,(这里默认返回一个空的忽略属性,也就是让{@link JsonIgnoreProperties}注解失效) + */ + @Override + public JsonIgnoreProperties.Value findPropertyIgnorals(Annotated a) { + return JsonIgnoreProperties.Value.empty(); + } + + /** + * 这个方法的作用是处理所有设置或者标记为不参与序列化的业务。 + *

+ * (这里所有标记了属性JSON的{@link JsonIgnore}注解,以及其他有着类似性质的注解属性都无效,都强制参与序列化 「{@link Transient}注解除外」) + * 设置了{@link Transient}注解的属性不参与序列化(在同步业务上,这些属性不会写入数据库,所以不需要序列化出来) + * + * @param m 被注解的成员 + * @return false 忽略所有标记 + */ + @Override + public boolean hasIgnoreMarker(AnnotatedMember m) { + // 标记了@transient注解的在打包是需要忽略掉 + return m.hasAnnotation(Transient.class); + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/config/LocaleTimezoneConfig.java b/server/src/main/java/com/aisino/iles/core/config/LocaleTimezoneConfig.java new file mode 100644 index 0000000..479e06e --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/LocaleTimezoneConfig.java @@ -0,0 +1,26 @@ +package com.aisino.iles.core.config; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +import java.util.Locale; +import java.util.TimeZone; + +public class LocaleTimezoneConfig implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext context) { + // 获取默认locale + String locale = context.getEnvironment().getProperty("spring.jackson.locale", Locale.getDefault().toString()); + String timeZone = context.getEnvironment().getProperty("spring.jackson.time-zone", TimeZone.getDefault().toZoneId().toString()); + // 设置默认时区 + TimeZone.setDefault(TimeZone.getTimeZone(timeZone)); + // 设置默认区域 + Locale.Builder builder = new Locale.Builder(); + String[] localeParts = locale.split("_|-"); + builder.setLanguage(localeParts[0]); + if (localeParts.length > 1) + builder.setRegion(localeParts[1]); + Locale.setDefault(builder.build()); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/config/RedissonConfig.java b/server/src/main/java/com/aisino/iles/core/config/RedissonConfig.java new file mode 100644 index 0000000..51b9029 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/RedissonConfig.java @@ -0,0 +1,50 @@ +package com.aisino.iles.core.config; + +import com.aisino.iles.core.hibernate.RedissonRegionFactoryPlus; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.cfg.AvailableSettings; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.codec.CompositeCodec; +import org.redisson.codec.Kryo5Codec; +import org.redisson.codec.LZ4Codec; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Configuration +public class RedissonConfig { + + @Bean + public RedissonClient redissonClient(@Value("${redisson.config}") String redissonClientConfig) throws IOException { + Config config = Config.fromYAML(redissonClientConfig); + // 设置锁超时时间 + config.setLockWatchdogTimeout(10000); + // 使用Kryo编码器+Lz4编码器,减少体积提高性能。 + config.setCodec(new CompositeCodec(new Kryo5Codec(), new LZ4Codec())); + return Redisson.create(config); + } + + + @AutoConfigureBefore(JpaRepositoriesAutoConfiguration.class) + @ConditionalOnProperty(name = "spring.jpa.properties.hibernate.cache.region.factory_class",havingValue = "redisson", matchIfMissing = true) + @Slf4j + public static class RedissonRegionFactoryAutoConfiguration { + @Bean + public RedissonRegionFactoryPlus redissonRegionFactoryPlus(RedissonClient redisson) { + return new RedissonRegionFactoryPlus(redisson); + } + @Bean + public HibernatePropertiesCustomizer redissonRegionFactoryCustomizer(RedissonRegionFactoryPlus redissonRegionFactoryPlus) { + // 注入自定义缓存实现 + return (properties) -> properties.put(AvailableSettings.CACHE_REGION_FACTORY, redissonRegionFactoryPlus); + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/config/WebConfig.java b/server/src/main/java/com/aisino/iles/core/config/WebConfig.java new file mode 100644 index 0000000..df831d0 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/config/WebConfig.java @@ -0,0 +1,252 @@ +package com.aisino.iles.core.config; + +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.annotation.handlers.CurrentUserMethodArgResolver; +import com.aisino.iles.core.controller.GlobalExceptionController; +import com.aisino.iles.core.converter.DateConverter; +import com.aisino.iles.core.converter.ISO_DATE_TIME2LocalDateConverter; +import com.aisino.iles.core.converter.ISO_DATE_TIME2LocalDateTimeConverter; +import com.aisino.iles.core.converter.ValueEnumConverterFactory; +import com.aisino.iles.core.interceptor.AccessTokenInterceptor; +import com.aisino.iles.core.interceptor.CorsInterceptor; +import com.aisino.iles.core.interceptor.EncryptRequestResponseFilter; +import com.aisino.iles.core.interceptor.JsonTokenValidatorInterceptor; +import com.aisino.iles.core.repository.ResourceRepo; +import com.aisino.iles.core.repository.UserRepo; +import com.aisino.iles.core.service.DynamicEncryptService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.smartlx.sso.client.service.SsoClientService; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.Http11NioProtocol; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.format.FormatterRegistry; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * WEB配置 + */ +@Configuration +@Slf4j +@SuppressWarnings("unused") +public class WebConfig implements WebMvcConfigurer { + private final CurrentUserMethodArgResolver currentUserMethodArgResolver; + private final ResourceRepo resourceRepo; + private final UserRepo userRepo; + private final DynamicEncryptService dynamicEncryptService; + private final ObjectMapper objectMapper; + private final GlobalExceptionController globalExceptionController; + private final RedisTemplate redisTemplate; + private final SsoClientService ssoClientService; + + @Autowired + public WebConfig(CurrentUserMethodArgResolver currentUserMethodArgResolver, + ResourceRepo resourceRepo, + UserRepo userRepo, + DynamicEncryptService dynamicEncryptService, + ObjectMapper objectMapper, + GlobalExceptionController globalExceptionController, RedisTemplate redisTemplate, SsoClientService ssoClientService) { + this.currentUserMethodArgResolver = currentUserMethodArgResolver; + this.resourceRepo = resourceRepo; + this.userRepo = userRepo; + this.dynamicEncryptService = dynamicEncryptService; + this.objectMapper = objectMapper; + this.globalExceptionController = globalExceptionController; + this.redisTemplate = redisTemplate; + this.ssoClientService = ssoClientService; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(currentUserMethodArgResolver); + } + + /** + * 拦截器 + * + * @param registry InterceptorRegistry + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + /* + 跨域拦截器 + */ + registry.addInterceptor(corsInterceptor()) + .addPathPatterns("/**"); + + registry.addInterceptor(accessTokenInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns( + "/lawenforcement/v1/auth/login", + "/lawenforcement/v1/auth/getLoginUrl", + "/lawenforcement/v1/auth/logout", + "/lawenforcement/v1/auth/refreshToken", + "/lawenforcement/v1/auth/backend-token", + "/api/lawenforcement/enterprises/warn", + "/lawenforcement/v1/auth/refreshToken", + "/api/lawenforcement/deliveryRecords/smsStatusCallback", + "/api/lawenforcement/big-screen-system/**"); + /* + 用户令牌验证 + */ +// registry.addInterceptor(jsonTokenInterceptor()) +// .addPathPatterns("/logout") +// .addPathPatterns("/api/**"); + + } + + + /** + * 消息转换器配置 + * + * @param converters 转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + // 调整一下消息转换器顺序,将String转换器放在最后 + converters.removeIf(converter -> converter instanceof org.springframework.http.converter.StringHttpMessageConverter); + converters.add(new org.springframework.http.converter.StringHttpMessageConverter()); + } + + @Override + public void addFormatters(FormatterRegistry registry) { +// JSON的通用单个参数枚举类型转换器用于转换控制器类参数中的枚举类型 + registry.addConverterFactory(new ValueEnumConverterFactory()); + registry.addConverter(new DateConverter()); +// 基于ISO_DATE_TIME格式的JAVA TIME类型转换器 + registry.addConverter(new ISO_DATE_TIME2LocalDateTimeConverter()); + registry.addConverter(new ISO_DATE_TIME2LocalDateConverter()); + } + + @Bean + @ConfigurationProperties("server.cors") + public HandlerInterceptor corsInterceptor() { + return new CorsInterceptor(); + } + + @Bean + public HandlerInterceptor jsonTokenInterceptor() { + return new JsonTokenValidatorInterceptor(resourceRepo, userRepo); + } + + @Bean + public HandlerInterceptor accessTokenInterceptor() { + return new AccessTokenInterceptor(ssoClientService); + } + + + @Configuration + @ConditionalOnProperty(name = "server.http-port") + static class SslConfig { + /** + * 嵌入式容器配置 默认https ,添加一个http的连接器 + * + * @return 服务工厂定制配置器 + */ + @Bean + public WebServerFactoryCustomizer webServerFactoryCustomizer( + Connector httpTomcatConnector, + @Value("${server.ssl.force-ssl-patterns}") String forceSslPatterns) { + return factory -> { + if (factory instanceof TomcatServletWebServerFactory) { + TomcatServletWebServerFactory fac = (TomcatServletWebServerFactory) factory; + // 支持http协议 + fac.addAdditionalTomcatConnectors(httpTomcatConnector); + // 设置新的这个连接器的属性为主连接器的配置。 + fac.getTomcatConnectorCustomizers().forEach(tomcatConnectorCustomizer -> { + tomcatConnectorCustomizer.customize(httpTomcatConnector); + }); + // ssl上下文设置 + TomcatContextCustomizer sslContextCustomizer = context -> { + SecurityConstraint constraint = new SecurityConstraint(); + constraint.setUserConstraint("CONFIDENTIAL"); + SecurityCollection collection = new SecurityCollection(); + collection.setName("useSslURICollection"); + // 需要强制使用ssl的路径匹配 + Optional.ofNullable(forceSslPatterns).filter(StringUtils::isNotEmpty) + .map(fsp -> Arrays.asList(fsp.split(","))) + .ifPresent(patterns -> patterns.forEach(collection::addPattern)); + constraint.addCollection(collection); + context.addConstraint(constraint); + }; + fac.addContextCustomizers(sslContextCustomizer); + } + }; + } + + + /** + * 创建http的连接器 + * + * @return tomcat http连接器 + */ + @Bean + public Connector createHttpConnector(ServerProperties serverProperties, + @Value("${server.http-port}") int httpPort) { + Http11NioProtocol http11NioProtocol = new Http11NioProtocol(); + Connector connector = new Connector(http11NioProtocol); + connector.setPort(httpPort); + connector.setRedirectPort(serverProperties.getPort()); + connector.setSecure(false); + return connector; + } + } + + /** + * 自定义字符串属性处理工具注册器 + * 处理去掉字符串两端空格 + * 处理空字符串""转换为null + * + * @author hx + * @since 2021-03-16 + */ + @ControllerAdvice + public static class StringTrimmerEditorRegister { + @InitBinder + public void initBinder(WebDataBinder binder) { + // 处理提交字符串参数两端包含空格,还有空字符串"" + binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); + } + } + + /** + * 通用加密解密请求响应过滤器 + * + * @return 过滤器注册器 + */ + @Bean + public FilterRegistrationBean encryptRequestResponseFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new EncryptRequestResponseFilter(dynamicEncryptService, globalExceptionController, objectMapper)); + filterRegistrationBean.addUrlPatterns("/*"); + filterRegistrationBean.setOrder(0); + return filterRegistrationBean; + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/AddrInfoController.java b/server/src/main/java/com/aisino/iles/core/controller/AddrInfoController.java new file mode 100644 index 0000000..a5132ca --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/AddrInfoController.java @@ -0,0 +1,73 @@ +package com.aisino.iles.core.controller; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.model.Ok; +import com.aisino.iles.common.model.PageResult; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.core.model.AddrInfo; +import com.aisino.iles.core.model.query.AddrQuery; +import com.aisino.iles.core.repository.DictItemRepo; +import com.aisino.iles.core.service.AddrInfoService; +import com.aisino.iles.core.service.StreetInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping(Constants.API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM) +public class AddrInfoController { + + private static final String ADDR_PAGE_URI = "/addrs"; + private static final String STREET_ADDR_URI = ADDR_PAGE_URI + "/streetaddr"; + private final AddrInfoService service; + private final StreetInfoService streetInfoService; + private final DictItemRepo dictItemRepo; + + @Autowired + public AddrInfoController(AddrInfoService service, + StreetInfoService streetInfoService, + DictItemRepo dictItemRepo) { + this.service = service; + this.streetInfoService = streetInfoService; + this.dictItemRepo = dictItemRepo; + } + + /** + * 获取地址信息分页 + * + * @param query 查询条件 + * @return json结果 + */ + @GetMapping(ADDR_PAGE_URI) + //@Log(type = OperateType.QUERY, value = "地址信息") + public PageResult pageAddrs(AddrQuery query) { + Page addrs = service.pageAddrs(query); + return PageResult.of(addrs); + } + + @GetMapping(STREET_ADDR_URI) + public Result streetaddr(AddrQuery query) { + Map map = new HashMap<>(); + Map result = new HashMap<>(); + service.pageAddrs(query).get().findFirst().ifPresent(o -> { + map.put("xzqh", o.getXzqh()); + map.put("mlph", o.getXxdz()); + map.put("fwbm", o.getFwbm()); + query.setDm(o.getJlh()); + result.put("jingdu",o.getY()); + result.put("weidu",o.getX()); + result.put("zfsq",o.getZfsq()); + }); + streetInfoService.pageStreets(query).get().findFirst().ifPresent(o -> map.put("jd", o.getMc())); + dictItemRepo.findByDictDictCodeAndValue("dm_xzqh", map.get("xzqh")).ifPresent(o -> map.put("value", o.getDisplay())); + + result.put("id", map.get("fwbm")); + result.put("value", map.get("value") + map.get("jd") + map.get("mlph")); + return Ok.of(result); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/CacheAdminController.java b/server/src/main/java/com/aisino/iles/core/controller/CacheAdminController.java new file mode 100644 index 0000000..91aaa11 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/CacheAdminController.java @@ -0,0 +1,148 @@ +package com.aisino.iles.core.controller; + +import com.aisino.iles.common.model.Fail; +import com.aisino.iles.common.model.Ok; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.core.annotation.Log; +import com.aisino.iles.core.hibernate.RedissonLocalStorage; +import com.aisino.iles.core.model.enums.OperateType; +import com.aisino.iles.core.service.CacheAdminService; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/cache") +public class CacheAdminController { + + private final CacheAdminService cacheAdminService; + + public CacheAdminController(CacheAdminService cacheAdminService) { + this.cacheAdminService = cacheAdminService; + } + + private static final String CACHE_REGIONS_URI = "/regions"; + private static final String CACHE_REGION_URI = CACHE_REGIONS_URI + "/{region}"; + private static final String CACHE_ENTRY_URI = "/{region}/{key}"; + private static final String CACHE_ENTRY_CLASS_ID_URI = "/class-id/{className}/{id}"; + private static final String CACHE_STATS_URI = "/stats/{region}"; + private static final String CACHE_TTL_URI = "/ttl/{region}/{key}"; + + /** + * 获取所有缓存区域信息 + * @return 缓存区域统计信息 + */ + @GetMapping(CACHE_REGIONS_URI) + @Log(type = OperateType.QUERY, value = "缓存区域查询") + public Result> getAllRegions() { + Map regions = cacheAdminService.getAllRegions(); + Map result = new LinkedHashMap<>(); + regions.forEach((name, storage) -> { + Map dataMap = new HashMap<>(); + dataMap.put("size", storage.getSize()); + dataMap.put("ttl", storage.getTtl()); + dataMap.put("maxIdle", storage.getMaxIdle()); + dataMap.put("liveTime", storage.getCacheTtl()); + dataMap.put("liveSize", storage.getCacheSize()); + // 最后一次更新时间 +// dataMap.put("lastUpdate", storage.); + result.put(name, dataMap); + }); + return Ok.of(result); + } +/** + * 删除缓存区域 + * @param region 缓存区域名称 + * @return 操作结果 + */ + +@DeleteMapping(CACHE_REGION_URI) +@Log(type = OperateType.REMOVE, value = "缓存区域删除") +public Result removeRegion(@PathVariable String region) { + boolean success = cacheAdminService.removeRegion(region); + return success ? Ok.of() : Fail.of("操作失败"); +} + + /** + * 获取指定缓存条目 + * @param region 缓存区域名称 + * @param key 缓存键 + * @return 缓存值或404 + */ + @GetMapping(CACHE_ENTRY_URI) + @Log(type = OperateType.QUERY, value = "缓存条目查询") + public Result getValue( + @PathVariable String region, + @PathVariable String key) { + return Ok.of(cacheAdminService.getCacheValue(region, key)); + } + + /** + * 根据实体类名和ID删除缓存条目 + * @param className 实体类名 + * @param id 实体ID + * @return 操作结果 + */ + @DeleteMapping(CACHE_ENTRY_CLASS_ID_URI) + @Log(type = OperateType.REMOVE, value = "缓存条目删除") + public Result removeValueByClassName( + @PathVariable String className, + @PathVariable Object id) { + boolean success = cacheAdminService.removeCacheValueByEntityManager(className,id); + return success ? + Ok.of() : + Fail.of("操作失败"); + } + + /** + * 刷新本地缓存 + * @param region 缓存区域名称 + * @param key 缓存键 + * @return 操作结果 + */ + @PostMapping(CACHE_ENTRY_URI + "/refresh") + @Log(type = OperateType.MODIFY, value = "本地缓存刷新") + public Result refreshLocalCache( + @PathVariable String region, + @PathVariable String key) { + cacheAdminService.refreshLocalCache(region, key); + return Ok.of(); + } + +// /** +// * 获取缓存统计信息 +// * @param region 缓存区域名称 +// * @return 统计信息或404 +// */ +// @GetMapping(CACHE_STATS_URI) +// @Log(type = OperateType.QUERY, value = "缓存统计查询") +// public Result getStats( +// @PathVariable String region) { +// return cacheAdminService.getCacheStats(region) +// .map(Ok::of) +// .orElse(Ok.of()); +// } + +// /** +// * 设置缓存过期时间 +// * @param region 缓存区域名称 +// * @param key 缓存键 +// * @param ttl 过期时间数值 +// * @param unit 时间单位 +// * @return 操作结果 +// */ +// @PostMapping(CACHE_TTL_URI) +// @Log(type = OperateType.MODIFY, value = "缓存TTL设置") +// public Result setTTL( +// @PathVariable String region, +// @PathVariable String key, +// @RequestParam long ttl, +// @RequestParam TimeUnit unit) { +// boolean success = cacheAdminService.putCacheValue(region, key, ttl, unit); +// return success ? +// Ok.of() : +// Result.error("操作失败"); +// } +} \ No newline at end of file diff --git a/server/src/main/java/com/aisino/iles/core/controller/DictController.java b/server/src/main/java/com/aisino/iles/core/controller/DictController.java new file mode 100644 index 0000000..36bbb1d --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/DictController.java @@ -0,0 +1,193 @@ +package com.aisino.iles.core.controller; + +import com.aisino.iles.common.model.Ok; +import com.aisino.iles.common.model.PageResult; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.annotation.Log; +import com.aisino.iles.core.model.Dict; +import com.aisino.iles.core.model.DictItem; +import com.aisino.iles.core.model.dto.DictDTO; +import com.aisino.iles.core.model.dto.DictItemDTO; +import com.aisino.iles.core.model.enums.OperateType; +import com.aisino.iles.core.model.query.DictItemQuery; +import com.aisino.iles.core.model.query.DictQuery; +import com.aisino.iles.core.service.DictService; +import jakarta.validation.constraints.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Set; + +/** + * 字典控制器 + */ +@RestController +@RequestMapping(Constants.API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM) +public class DictController { + private static final String DICT_URI = "/dicts"; + private static final String DICT_SINGLE_URI = DICT_URI + "/{dictId}"; + private static final String DICT_ITEM_URI = "/dictItems"; + private static final String DICT_ITEM_SINGLE_URI = DICT_ITEM_URI + "/{dictItemId}"; + private static final String DICT_ITEM_LIST_URI = DICT_ITEM_URI + "/list"; + private final DictService dictService; + + @Autowired + public DictController(DictService dictService) { + this.dictService = dictService; + } + + /** + * 获取字典列表 分页 + * + * @param page 页码 + * @param pagesize 每页数 + * @param sort 排序列 + * @param dir 升降序 + * @return 分页的字典信息 + */ + @GetMapping(DICT_URI) + //@Log(type = OperateType.QUERY, value = "列表查询分页") + public PageResult pageDicts( + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer pagesize, + @RequestParam(required = false) String sort, + @RequestParam(required = false) String dir, + DictQuery dictQuery + ) { + return PageResult.of(dictService.pageDicts(page,pagesize,sort,dir,dictQuery)); + } + + /** + * 新增字典 + * @param dict 字典 + * @return JSON结果 + */ + @PostMapping(value = DICT_URI) + @Log(type = OperateType.ADD, value = "字典新增") + public Result addDict(@NotNull @RequestBody Dict dict) { + return Ok.of(dictService.addDict(dict)); + } + + /** + * 修改字典 + * @param dict 字典 + * @param dictId 字典ID + * @return JSON结果 + */ + @PutMapping(DICT_SINGLE_URI) + @Log(type = OperateType.MODIFY, value = "字典修改") + public Result modifyDict(@NotNull @RequestBody Dict dict, @PathVariable String dictId) { + dict.setDictId(dictId); + dictService.modifyDict(dict); + return Ok.of(); + } + + /** + * 删除字典 + * @param dictId 字典ID + * @return json结果 + */ + @DeleteMapping(DICT_SINGLE_URI) + @Log(type = OperateType.REMOVE, value = "字典删除") + public Result removeDict(@PathVariable String dictId) { + dictService.removeDict(dictId); + return Ok.of(); + } + + /** + * 批量删除字典信息 + * @param dictIds 要删除的字典ID集合 + * @return json结果 + */ + @DeleteMapping(DICT_URI) + @Log(type = OperateType.REMOVE, value = "字典批量删除") + public Result removeDicts(@RequestBody Set dictIds) { + dictService.removeDicts(dictIds); + return Ok.of(); + } + /** + * 获取字典项信息分页 + * @param query 查询条件 + * @return json结果 + */ + @GetMapping(DICT_ITEM_URI) + //@Log(type = OperateType.QUERY, value = "字典项查询") + public PageResult pageDictItems(DictItemQuery query) { + Page dictItems = dictService.pageDictItems(query); + return PageResult.of(dictItems); + } + + /** + * 获取字典项信息不分页 + * + * @param query 查询条件 + * @return json结果 + */ + @GetMapping(DICT_ITEM_LIST_URI) + //@Log(type = OperateType.QUERY, value = "字典项列表查询(不分页)") + public Result> listDictItems(DictItemQuery query) { + return Ok.of(dictService.listDictItems(query)); + } + /** + * 添加字典项 + * @param dictItem 字典项 + * @return json结果 + */ + @PostMapping(DICT_ITEM_URI) + @Log(type = OperateType.ADD, value = "字典项新增") + public Result addDictItem(@NotNull @RequestBody DictItem dictItem) { + return Ok.of(dictService.addDictItem(dictItem)); + } + + /** + * 修改字典项 + * @param dictItem 字典项 + * @param dictItemId 字典项id + * @return json结果 + */ + @PutMapping(DICT_ITEM_SINGLE_URI) + @Log(type = OperateType.MODIFY, value = "字典项修改") + public Result modifyDictItem(@NotNull @RequestBody DictItem dictItem,@NotNull @PathVariable String dictItemId) { + dictItem.setDictItemId(dictItemId); + dictService.modifyDictItems(dictItem); + return Ok.of(); + } + + /** + * 删除字典项 + * @param dictItemId 字典项 + * @return json结果 + */ + @DeleteMapping(DICT_ITEM_SINGLE_URI) + @Log(type = OperateType.REMOVE, value = "字典项删除") + public Result removeDictItem(@PathVariable String dictItemId) { + dictService.removeDictItem(dictItemId); + return Ok.of(); + } + + /** + * 查询单个字典数据 + * @param dictId 字典ID + * @return json结果 + */ + @GetMapping(DICT_SINGLE_URI) + //@Log(type = OperateType.QUERY, value = "字典详情") + public Result findOneDict(@PathVariable String dictId) { + return dictService.findDictById(dictId).map(Ok::of).orElseGet(Ok::of); + } + + /** + * 查询单个字典项 + * @param dictItemId 字典项ID + * @return json结果 + */ + @GetMapping(DICT_ITEM_SINGLE_URI) + //@Log(type = OperateType.QUERY, value = "字典项详情") + public Result findOneDictItem(@PathVariable String dictItemId) { + return dictService.findDictItemById(dictItemId).map(Ok::of) + .orElseGet(Ok::of); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/GlobalExceptionController.java b/server/src/main/java/com/aisino/iles/core/controller/GlobalExceptionController.java new file mode 100644 index 0000000..67c99bf --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/GlobalExceptionController.java @@ -0,0 +1,118 @@ +package com.aisino.iles.core.controller; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.core.exception.ArgError; +import com.aisino.iles.core.exception.BusinessError; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import java.io.IOException; +import java.util.stream.Collectors; + +/** + * 全局异常处理控制器 + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionController { + + /** + * 验证消息(针对 path) + * + * @param e 验证异常 + * @return 响应信息 + */ + @ExceptionHandler(BindException.class) + public Result validatePath(BindException e) { + String msg = e.getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(",")); + ArgError error = new ArgError(msg); + return businessErrorHandler(error); + } + + /** + * 验证消息(针对 param) + * + * @param error 参数验证异常 + * @return 响应信息 + */ + @ExceptionHandler(value = {ConstraintViolationException.class}) + public Result ConstraintViolationExceptionHandler(ConstraintViolationException error) { + String msg = error.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(",")); + return businessErrorHandler(new ArgError(msg)); + } + + /** + * 验证消息 (针对 body) + * + * @param e 验证异常 + * @return 响应信息 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result validateBody(MethodArgumentNotValidException e) { + String msg = e.getBindingResult() + .getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(",")); + ArgError error = new ArgError(msg); + return businessErrorHandler(error); + } + + /** + * 反序列化错误 + * + * @param e 错误对象 + * @return 响应信息 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public Result httpMessageHandler(HttpMessageNotReadableException e) throws IOException { + String message = e.getMessage(); + message = StrUtil.subAfter(message, ":", false); + message = StrUtil.subPre(message, 200); + // 对于 JSON 格式错误在控制台应该输出完整异常信息 + log.error("反序列化错误:", e); + return businessErrorHandler(new BusinessError(message)); + } + + /** + * 业务错误处理器 + * + * @return 信息 + */ + @ExceptionHandler(value = {BusinessError.class}) + public Result businessErrorHandler(BusinessError error) { + log.info("业务错误:", error); + return Result.of(error.getCode(), false, ExceptionUtil.getMessage(error), null); + } + + /** + * 服务器异常处理 + * + * @param error 服务器异常 + * @return 信息 + */ + @ExceptionHandler(value = {Exception.class}) + public Result serverErrorHandler(Exception error) { + log.info("服务器异常:", error); + return Result.of(Constants.Exceptions.server_error, + false, + "服务器发生了错误,请联系管理员" + ExceptionUtil.getRootCauseMessage(error), + null); + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/GlobalParamController.java b/server/src/main/java/com/aisino/iles/core/controller/GlobalParamController.java new file mode 100644 index 0000000..cb04970 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/GlobalParamController.java @@ -0,0 +1,119 @@ +package com.aisino.iles.core.controller; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.iface.Logger; +import com.aisino.iles.common.model.Ok; +import com.aisino.iles.common.model.PageResult; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.core.annotation.Log; +import com.aisino.iles.core.model.GlobalParam; +import com.aisino.iles.core.model.enums.OperateType; +import com.aisino.iles.core.service.GlobalParamService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.constraints.NotNull; +import java.util.Set; + +/** + * 公共/系统管理/全局参数管理 + * @author hx + * @since 2023-08-31 + */ +@RestController +@RequestMapping(Constants.API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM) +public class GlobalParamController implements Logger { + private static final String GLOBAL_PARAM_URI = "/globalParams"; + private static final String GLOBAL_PARAM_SINGLE_URI = GLOBAL_PARAM_URI + "/{globalParamCode}"; + private final GlobalParamService globalParamService; + + @Autowired + public GlobalParamController(GlobalParamService globalParamService) { + this.globalParamService = globalParamService; + } + + /** + * 获取全局参数列表分页 + * @param page 页码 + * @param pagesize 每页 + * @param sort 排序列名称 + * @param dir 升降序 + * @param globalParamCode 全局参数代码 + * @param globalParamName 全局参数名称 + * @param globalParamValue 全局参数值 + * @return json数据 + */ + @GetMapping(GLOBAL_PARAM_URI) + @Log(type = OperateType.QUERY, value = "全局参数查询") + public PageResult list( + @RequestParam(required = false,defaultValue = "1") Integer page, + @RequestParam(required = false,defaultValue = "20") Integer pagesize, + @RequestParam(required = false,defaultValue = "globalParamCode") String sort, + @RequestParam(required = false,defaultValue = "desc") String dir, + @RequestParam(required = false) String globalParamCode, + @RequestParam(required = false) String globalParamName, + @RequestParam(required = false) String globalParamValue + ) { + return PageResult.of(globalParamService.listGlobalParams(page, pagesize, sort, dir, globalParamCode, globalParamName, globalParamValue)); + } + + /** + * 新增全局参数 + * @param globalParam 全局参数 + * @return 结果 + */ + @PostMapping(GLOBAL_PARAM_URI) + @Log(type = OperateType.ADD, value = "全局参数新增") + public Result add(@NotNull @RequestBody GlobalParam globalParam) { + return Ok.of(globalParamService.saveGlobalParam(globalParam)); + } + + /** + * 修改全局参数 + * @param globalParam 全局参数 + * @param globalParamCode 全局参数代码 + * @return 结果 + */ + @PutMapping(GLOBAL_PARAM_SINGLE_URI) + @Log(type = OperateType.MODIFY, value = "全局参数修改") + public Result modify(@NotNull @RequestBody GlobalParam globalParam, @PathVariable String globalParamCode) { + globalParam.setGlobalParamCode(globalParamCode); + globalParamService.modifyGlobalParam(globalParam); + return Ok.of(); + } + + /** + * 删除全局参数 + * @param globalParamCode 全局参数代码 + * @return 结果 + */ + @DeleteMapping(GLOBAL_PARAM_SINGLE_URI) + @Log(type = OperateType.REMOVE, value = "全局参数删除") + public Result remove(@NotNull @PathVariable String globalParamCode) { + globalParamService.removeGlobalParam(globalParamCode); + return Ok.of(); + } + + /** + * 批量删除全局参数 + * @param globalParamCodes 全局参数代码 + * @return 结果 + */ + @DeleteMapping(GLOBAL_PARAM_URI) + @Log(type = OperateType.REMOVE, value = "全局参数批量删除") + public Result removeAll(@NotNull @RequestBody Set globalParamCodes) { + globalParamService.removeGlobalParams(globalParamCodes); + return Ok.of(); + } + + /** + * 通过代码查询单个全局参数 + * @param globalParamCode 全局参数代码 + * @return 全局参数 + */ + @GetMapping(GLOBAL_PARAM_SINGLE_URI) + @Log(type = OperateType.QUERY, value = "查询单个全局参数") + public Result get(@NotNull @PathVariable String globalParamCode) { + return globalParamService.findByCode(globalParamCode).map(Ok::of).orElseGet(Ok::of); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/GlobalResponseController.java b/server/src/main/java/com/aisino/iles/core/controller/GlobalResponseController.java new file mode 100644 index 0000000..ac65e5e --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/GlobalResponseController.java @@ -0,0 +1,79 @@ +package com.aisino.iles.core.controller; + +import cn.hutool.json.JSONObject; +import com.aisino.iles.common.model.Fail; +import com.aisino.iles.common.model.Ok; +import com.aisino.iles.common.model.PageResult; +import com.aisino.iles.common.model.Result; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Page; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * 通用rest接口返回统一处理 + * + * @author hx + * @since 2022-03-15 + */ +@RestControllerAdvice +public class GlobalResponseController implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType) && + !returnType.getMethod().getReturnType().getName().equals("com.aisino.socialcollect.uploadinterface.models.UploadInterfaceResult"); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, + ServerHttpResponse response) { + if (body == null) + return Ok.of(); + if (Result.class.isAssignableFrom(returnType.getParameterType())) + return body; + if (Collection.class.isAssignableFrom(returnType.getParameterType())) { + return Ok.of((Collection)body); + } + if (Map.class.isAssignableFrom(returnType.getParameterType())) { + Map map = (Map) body; + if (map.containsKey("success") && map.containsKey("code")) + return body; + } + if (String.class.isAssignableFrom(returnType.getParameterType()) + && MediaType.APPLICATION_JSON == selectedContentType) + return Ok.of(body); + if (Page.class.isAssignableFrom(returnType.getParameterType())) { + return PageResult.of((Page) body); + } + if (Exception.class.isAssignableFrom(body.getClass())) { + Exception ex = (Exception) body; + return Fail.of(ex.getMessage(), 1); + } + if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType()) + && body instanceof Map) { + Map map = (Map) body; + if (map.keySet().containsAll(Arrays.asList("path", "error", "status", "timestamp", "message"))) { + JSONObject json = new JSONObject(map); + Fail of = Fail.of(json.getStr("error") + json.getStr("message"), json.getInt("status")); + of.setData(Collections.singletonList(json)); + return of; + } + } + // 默认为所有json类型的结果包裹成Result结构 + return Ok.of(body); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/NoAuthGlobalParamController.java b/server/src/main/java/com/aisino/iles/core/controller/NoAuthGlobalParamController.java new file mode 100644 index 0000000..5e98717 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/NoAuthGlobalParamController.java @@ -0,0 +1,38 @@ +package com.aisino.iles.core.controller; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.GlobalParam; +import com.aisino.iles.core.service.GlobalParamService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 公共/系统管理/全局参数管理 + * 不需要登录的全局参数接口 + * @author hx + * @since 2023-08-31 + */ +@RestController +@RequestMapping(Constants.NO_AUTH_API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM) +@Slf4j +public class NoAuthGlobalParamController { + private static final String GLOBAL_PARAM_URI = "/globalParams"; + private final GlobalParamService globalParamService; + + public NoAuthGlobalParamController(GlobalParamService globalParamService) { + this.globalParamService = globalParamService; + } + + /** + * 获取不需要登录的全局参数信息 + * @return 全局参数信息 + */ + @GetMapping(GLOBAL_PARAM_URI) + public List list() { + return globalParamService.listNoAuth(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/OperateLogController.java b/server/src/main/java/com/aisino/iles/core/controller/OperateLogController.java new file mode 100644 index 0000000..2bc7d0c --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/OperateLogController.java @@ -0,0 +1,55 @@ +package com.aisino.iles.core.controller; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.model.Ok; +import com.aisino.iles.common.model.PageResult; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.core.model.OperateLog; +import com.aisino.iles.core.model.query.OperateLogQuery; +import com.aisino.iles.core.service.OperateLogService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 操作日志接口 + * + * @author huxin + * @since 2020-10-13 + */ +@RestController +@RequestMapping(Constants.API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM + "/operate-logs") +public class OperateLogController { + /** + * 单个操作日志数据URI + */ + private static final String OPERATE_LOG_SINGLE_URI = "/{operateLogId}"; + private final OperateLogService operateLogService; + + public OperateLogController(OperateLogService operateLogService) { + this.operateLogService = operateLogService; + } + + /** + * 分页查询操作日志接口 + * + * @param query 操作日志查询条件 + * @return 分页的操作日志json响应数据 + */ + @GetMapping + public PageResult findOperateLogForPage(OperateLogQuery query) { + return PageResult.of(operateLogService.findOperateLogForPage(query)); + } + + /** + * 操作日志详细信息查询接口 + * + * @param operateLogId 操作日志主键 + * @return 操作日志详细信息JSON响应数据 + */ + @GetMapping(OPERATE_LOG_SINGLE_URI) + public Result findOperateLogDetail(@PathVariable String operateLogId) { + return operateLogService.findOperateLogDetailById(operateLogId).map(Ok::of).orElseGet(Ok::of); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/controller/StreetInfoController.java b/server/src/main/java/com/aisino/iles/core/controller/StreetInfoController.java new file mode 100644 index 0000000..adc8735 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/controller/StreetInfoController.java @@ -0,0 +1,40 @@ +package com.aisino.iles.core.controller; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.model.PageResult; +import com.aisino.iles.core.model.StreetInfo; +import com.aisino.iles.core.model.query.AddrQuery; +import com.aisino.iles.core.service.StreetInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(Constants.API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM) +public class StreetInfoController { + + private final StreetInfoService service; + + private static final String STREET_PAGE_URI = "/streets"; + + @Autowired + public StreetInfoController(StreetInfoService service) { + this.service = service; + } + + /** + * 获取街道信息分页 + * @param query 查询条件 + * @return json结果 + */ + @GetMapping(STREET_PAGE_URI) + //@Log(type = OperateType.QUERY, value = "街道信息") + public PageResult pageAddrs(AddrQuery query) { + Page streets = service.pageStreets(query); + return PageResult.of(streets); + } + + +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/DateConverter.java b/server/src/main/java/com/aisino/iles/core/converter/DateConverter.java new file mode 100644 index 0000000..3a8559c --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/DateConverter.java @@ -0,0 +1,31 @@ +package com.aisino.iles.core.converter; + +import com.aisino.iles.common.util.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; + +import java.text.SimpleDateFormat; +import java.util.Date; + +@Slf4j +public class DateConverter implements Converter { + private SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd"); + private SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + @Override + public Date convert(String s) { + if(StringUtils.isEmpty(s)){ + return null; + } + try { + if(s.matches("^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}$")){//yyyy-MM-dd HH:mm:ss + return sdf2.parse(s); + }else if(s.matches("^\\d{4}-\\d{2}-\\d{2}$")){//yyyy-MM-dd + return sdf1.parse(s); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE2LocalDateConverter.java b/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE2LocalDateConverter.java new file mode 100644 index 0000000..d9f933f --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE2LocalDateConverter.java @@ -0,0 +1,17 @@ +package com.aisino.iles.core.converter; + +import com.aisino.iles.common.util.StringUtils; +import org.springframework.core.convert.converter.Converter; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +public class ISO_DATE2LocalDateConverter implements Converter { + @Override + public LocalDate convert(String s) { + return Optional.ofNullable(s).filter(StringUtils::isNotEmpty) + .map(ds->LocalDate.parse(ds, DateTimeFormatter.ISO_DATE)) + .orElse(null); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE_TIME2LocalDateConverter.java b/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE_TIME2LocalDateConverter.java new file mode 100644 index 0000000..668dce5 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE_TIME2LocalDateConverter.java @@ -0,0 +1,30 @@ +package com.aisino.iles.core.converter; + +import com.aisino.iles.common.util.StringUtils; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.TimeZone; + +@Component +public class ISO_DATE_TIME2LocalDateConverter implements Converter { + @Override + public LocalDate convert(String dateStr) { + return Optional.ofNullable(dateStr).filter(StringUtils::isNotEmpty) + .map(ds-> { + if (ds.contains("T") && ds.contains("Z")) { + return Instant.parse(ds).atZone(TimeZone.getDefault().toZoneId()).toLocalDate(); + } else if (ds.contains("T")) { + return LocalDateTime.parse(ds, DateTimeFormatter.ISO_DATE_TIME).toLocalDate(); + } else { + return LocalDate.parse(ds,DateTimeFormatter.ISO_DATE); + } + }) + .orElse(null); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE_TIME2LocalDateTimeConverter.java b/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE_TIME2LocalDateTimeConverter.java new file mode 100644 index 0000000..6a0f4a8 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/ISO_DATE_TIME2LocalDateTimeConverter.java @@ -0,0 +1,48 @@ +package com.aisino.iles.core.converter; + +import com.aisino.iles.common.util.StringUtils; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.TimeZone; + +/** + * 时间转换器: 时间字符串类型转为本地时间 + * 支持格式yyyy-MM-dd,yyyy-MM-dd HH:mm:ss,yyyy-MM-ddTHH:mm:ss,yyyy-MM-ddTHH:mm:ss.zzzZ,yyyy-MM-ddTHH:mm:ss.zzzzzzZ + * + * @author huxin + * @since 2020-06 + */ +@Component +public class ISO_DATE_TIME2LocalDateTimeConverter implements Converter { + private final DateTimeFormatter yyyyMMddHHmmss = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss"); + private final DateTimeFormatter yyyyMMddHHmmss_no_sign = DateTimeFormatter.ofPattern("uuuuMMddHHmmss"); + @Override + public LocalDateTime convert(String dtStr) { + return Optional.of(dtStr).filter(StringUtils::isNotEmpty) + .map(dts -> { + if (dts.contains("T") && dts.contains("Z")) { + return Instant.parse(dts).atZone(TimeZone.getDefault().toZoneId()).toLocalDateTime(); + } else if (dts.contains("T")) { + return LocalDateTime.parse(dts, DateTimeFormatter.ISO_DATE_TIME); + } else { + // 根据长度判断时间类型, 长度=10 + if (dts.length() <= 10) + return LocalDate.parse(dts, DateTimeFormatter.ISO_DATE).atTime(0, 0); + else + // 不带T的时间类型转换 + try { + return LocalDateTime.parse(dts, yyyyMMddHHmmss); + } catch (Exception e) { + return LocalDateTime.parse(dts,yyyyMMddHHmmss_no_sign); + } + } + }) + .orElse(null); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/ValueEnumConverterFactory.java b/server/src/main/java/com/aisino/iles/core/converter/ValueEnumConverterFactory.java new file mode 100644 index 0000000..66d7d83 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/ValueEnumConverterFactory.java @@ -0,0 +1,48 @@ +package com.aisino.iles.core.converter; + +import com.aisino.iles.common.iface.Logger; +import com.aisino.iles.core.model.ValueEnum; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; + +/** + * 字符串转换枚举类型的转换器工厂 + */ +public class ValueEnumConverterFactory implements ConverterFactory> { + @Override + public > Converter getConverter(Class aClass) { + // 主要用于获得被转换对象的类信息. + return new ValueEnumConverter<>(aClass); + } + + /** + * 通用枚举转换器 + * @param + * @param + */ + public static class ValueEnumConverter,C> implements Converter, Logger { + private final Class tClass; + + public ValueEnumConverter(Class tClass) { + this.tClass = tClass; + } + + + @Override + @SuppressWarnings("unchecked") + public T convert(C c) { + try { + T[] values = (T[]) tClass.getMethod("values").invoke(tClass); + return Arrays.stream(values).filter(v -> v.getValue().equals(c)).findFirst() + .orElse(null); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + logger().error(e.getMessage(),e); + } + + return null; + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/json/DataPackageHibernateDontLazySerializerModifier.java b/server/src/main/java/com/aisino/iles/core/converter/json/DataPackageHibernateDontLazySerializerModifier.java new file mode 100644 index 0000000..9e6eeef --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/json/DataPackageHibernateDontLazySerializerModifier.java @@ -0,0 +1,132 @@ +package com.aisino.iles.core.converter.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.datatype.jdk8.Jdk8BeanSerializerModifier; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.beanutils.PropertyUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.hibernate.collection.spi.PersistentCollection; + +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 不序列化需要延迟加载的集合和对象 + */ +@Slf4j +public class DataPackageHibernateDontLazySerializerModifier extends Jdk8BeanSerializerModifier { + + /** + * 一对多序列化方法 + */ + private final JsonSerializer o2mSerializer = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNull(); + } + }; + + /** + * 多对多序列化方法 + */ + private final JsonSerializer m2mSerializer = new JsonSerializer() { + public void writeCollection(Collection collection, JsonGenerator gen) throws IOException { + gen.writeStartArray(); + collection.forEach(c -> { + try { + if (c == null) + gen.writeNull(); + else { + Class cClass = c.getClass(); + Map primaryObject = new HashMap<>(); + Map cps = PropertyUtils.describe(c); + FieldUtils.getFieldsListWithAnnotation(cClass, Id.class).forEach(idf -> { + primaryObject.put(idf.getName(), cps.get(idf.getName())); + }); + if (!primaryObject.isEmpty()) { + gen.writeObject(primaryObject); + } + } + } catch (IOException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + log.error(e.getMessage(), e); + } + }); + gen.writeEndArray(); + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (PersistentCollection.class.isAssignableFrom(value.getClass())) { + PersistentCollection persistentCollection = (PersistentCollection) value; + // 没有懒加载的情况下,用自定义 + if (!persistentCollection.wasInitialized()) { + gen.writeNull(); + } else { + if (Collection.class.isAssignableFrom(persistentCollection.getClass())) { + // 处理集合类型 + writeCollection(((Collection) persistentCollection), gen); + } else { + // 处理集合类型以外的情况 + serializers.defaultSerializeValue(value, gen); + } + } + } else { + // 处理集合类型以外的情况 + writeCollection(((Collection) value), gen); + } + } + }; + + /** + * 多对一序列化方法 + */ + private final JsonSerializer m2oSerializer = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + Class vClass = value.getClass(); + Map primaryObject = new HashMap<>(); + FieldUtils.getFieldsListWithAnnotation(vClass, Id.class) + .stream().map(Field::getName) + .forEach(idf -> { + try { + primaryObject.put(idf, PropertyUtils.getProperty(value, idf)); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + log.error(e.getMessage(), e); + } + }); + if (!primaryObject.isEmpty()) { + gen.writeObject(primaryObject); + } else + gen.writeNull(); + } + }; + + @Override + public List changeProperties(SerializationConfig config, BeanDescription beanDesc, List beanProperties) { + List beanPropertyWriters = super.changeProperties(config, beanDesc, beanProperties); + beanPropertyWriters.forEach(bpw -> { + if (bpw.getAnnotation(OneToMany.class) != null) { + bpw.assignSerializer(o2mSerializer); + } else if (bpw.getAnnotation(ManyToMany.class) != null) { + bpw.assignSerializer(m2mSerializer); + } else if (bpw.getAnnotation(ManyToOne.class) != null) { + bpw.assignSerializer(m2oSerializer); + } + }); + return beanPropertyWriters; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/json/HttpJackson2LocalDateConverter.java b/server/src/main/java/com/aisino/iles/core/converter/json/HttpJackson2LocalDateConverter.java new file mode 100644 index 0000000..0336948 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/json/HttpJackson2LocalDateConverter.java @@ -0,0 +1,29 @@ +package com.aisino.iles.core.converter.json; + +import com.aisino.iles.core.converter.ISO_DATE_TIME2LocalDateConverter; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDate; + +public class HttpJackson2LocalDateConverter extends JsonDeserializer { + private final ISO_DATE_TIME2LocalDateConverter iso_date_time2LocalDateConverter; + + + public HttpJackson2LocalDateConverter() { + this.iso_date_time2LocalDateConverter = new ISO_DATE_TIME2LocalDateConverter(); + } + + @Override + public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + return iso_date_time2LocalDateConverter.convert(jsonParser.getText()); + } + + @Override + public Class handledType() { + return LocalDate.class; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/json/HttpJackson2LocalDateTimeConverter.java b/server/src/main/java/com/aisino/iles/core/converter/json/HttpJackson2LocalDateTimeConverter.java new file mode 100644 index 0000000..25c685b --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/json/HttpJackson2LocalDateTimeConverter.java @@ -0,0 +1,38 @@ +package com.aisino.iles.core.converter.json; + +import com.aisino.iles.core.converter.ISO_DATE_TIME2LocalDateTimeConverter; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.LocalDateTime; + +/** + * 按理说客户端不应该左右时间的值格式,通过Date.toJson方法,可以把本地时间转换为UTC时间。 + * 然后使用ISO_DATE 或者 ISO_DATE_TIME的格式风格,通过默认的这个格式,把时间转换为服务器本地时间。 + * 这样来操作时间比较准确和谨慎,即便出现了跨越时区的情况,也可以应对。 + * 但是这里为了兼容之前已经存在的设置了本地时间的value-format的时间格式。使用value-formate指定yyyy-MM-dd HH:mm:ss的时间属性 + * 按照本地时间的方式来进行转换。 + */ +@Component +public class HttpJackson2LocalDateTimeConverter extends JsonDeserializer { + private final ISO_DATE_TIME2LocalDateTimeConverter iso_date_time2LocaleDateTimeConverter; + @Autowired + public HttpJackson2LocalDateTimeConverter() { + this.iso_date_time2LocaleDateTimeConverter = new ISO_DATE_TIME2LocalDateTimeConverter(); + } + + @Override + public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + return iso_date_time2LocaleDateTimeConverter.convert(jsonParser.getText()); + } + + @Override + public Class handledType() { + return LocalDateTime.class; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/json/ManyToOneSerializerModifier.java b/server/src/main/java/com/aisino/iles/core/converter/json/ManyToOneSerializerModifier.java new file mode 100644 index 0000000..6b1c87c --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/json/ManyToOneSerializerModifier.java @@ -0,0 +1,81 @@ +package com.aisino.iles.core.converter.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.impl.TypeWrappedSerializer; +import com.fasterxml.jackson.datatype.jdk8.Jdk8BeanSerializerModifier; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.beanutils.PropertyUtils; + +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Slf4j +public class ManyToOneSerializerModifier extends Jdk8BeanSerializerModifier { + private final JsonSerializer manyToOneSerializer = new JsonSerializer() { + @SneakyThrows + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + Object idObj = value.getClass().newInstance(); + Arrays.stream(value.getClass().getDeclaredFields()) + .filter(f -> + f.getType().isArray() || Map.class.isAssignableFrom(f.getType()) + || Collection.class.isAssignableFrom(f.getType()) + ).map(Field::getName) + .forEach(n -> { + try { + PropertyUtils.setProperty(idObj, n, null); + } catch (Exception e) { + log.error("json 错误:{}", e); + } + }); + Arrays.stream(value.getClass().getDeclaredFields()) + .filter(f -> f.isAnnotationPresent(Id.class)) + .map(Field::getName) + .forEach(n -> { + try { + Object v = PropertyUtils.getProperty(value, n); + log.debug("field[{}]:{}", n, v); + PropertyUtils.setProperty(idObj, n, v); + } catch (Exception e) { + log.error("json 错误:{}", e); + } + }); + serializers.defaultSerializeValue(idObj, gen); + } + + @Override + public void serializeWithType(Object value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException { + serialize(value, gen, serializers); + } + + @Override + public Class handledType() { + return Object.class; + } + }; + + @Override + public List changeProperties(SerializationConfig config, BeanDescription beanDesc, List beanProperties) { + List bpws = super.changeProperties(config, beanDesc, beanProperties); + bpws.forEach(bpw -> { + if (bpw.getAnnotation(ManyToOne.class) != null) { + //bpw.assignTypeSerializer(new Ser(bpw.getTypeSerializer().getTypeIdResolver(),bpw)); + bpw.assignSerializer(new TypeWrappedSerializer(bpw.getTypeSerializer(), manyToOneSerializer)); + } + }); + return bpws; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/json/SerializerModifier.java b/server/src/main/java/com/aisino/iles/core/converter/json/SerializerModifier.java new file mode 100644 index 0000000..7019ca2 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/json/SerializerModifier.java @@ -0,0 +1,41 @@ +package com.aisino.iles.core.converter.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.datatype.jdk8.Jdk8BeanSerializerModifier; + +import java.io.IOException; +import java.util.List; + +/** + * 用于转换默认为null的数组或者集合类型的数据,到默认空数组,而不是 null + */ +public class SerializerModifier extends Jdk8BeanSerializerModifier { + /** + * 空数组序列化方法 + */ + private final JsonSerializer nullArraySerializer = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + // 数组开括号 + jsonGenerator.writeStartArray(); + // 数组闭括号 + jsonGenerator.writeEndArray(); + } + }; + + @Override + public List changeProperties(SerializationConfig config, BeanDescription beanDesc, List beanProperties) { + List beanPropertyWriters = super.changeProperties(config, beanDesc, beanProperties); + beanPropertyWriters.forEach(bpw -> { + if (bpw.getType().isArrayType() || bpw.getType().isCollectionLikeType()) { + bpw.assignNullSerializer(nullArraySerializer); + } + }); + return beanPropertyWriters; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/json/StringTrimDeserializer.java b/server/src/main/java/com/aisino/iles/core/converter/json/StringTrimDeserializer.java new file mode 100644 index 0000000..7e5c3ed --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/json/StringTrimDeserializer.java @@ -0,0 +1,22 @@ +package com.aisino.iles.core.converter.json; + + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; + +/** + * 两端包含空格的字符串处理反序列化器 + * + * @author hx + * @since 2021-03-16 + */ +public class StringTrimDeserializer extends JsonDeserializer { + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return p.getText().trim(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/persistence/IconClsStringArray2VarcharConverter.java b/server/src/main/java/com/aisino/iles/core/converter/persistence/IconClsStringArray2VarcharConverter.java new file mode 100644 index 0000000..a76b106 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/persistence/IconClsStringArray2VarcharConverter.java @@ -0,0 +1,27 @@ +package com.aisino.iles.core.converter.persistence; + +import jakarta.persistence.AttributeConverter; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * 菜单图标类型 字符串数组转varchar的转换器 + */ +public class IconClsStringArray2VarcharConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(String[] attribute) { + return Optional.ofNullable(attribute).map(attr -> String.join(",", attr)).orElse(null); + } + + @Override + public String[] convertToEntityAttribute(String dbData) { + return Optional.ofNullable(dbData).map(db -> { + String[] dbs = db.split(","); + if (dbs.length == 1) { + return Stream.of("fas", dbs[0]).toArray(String[]::new); + } else { + return dbs; + } + }).orElse(null); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/persistence/StringArray2VarcharConverter.java b/server/src/main/java/com/aisino/iles/core/converter/persistence/StringArray2VarcharConverter.java new file mode 100644 index 0000000..ca39e30 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/persistence/StringArray2VarcharConverter.java @@ -0,0 +1,27 @@ +package com.aisino.iles.core.converter.persistence; + +import jakarta.persistence.AttributeConverter; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * 字符串数组转换器 + */ +public class StringArray2VarcharConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(String[] attribute) { + return Optional.ofNullable(attribute).map(attr -> String.join(",", attr)).orElse(null); + } + + @Override + public String[] convertToEntityAttribute(String dbData) { + return Optional.ofNullable(dbData).map(db -> { + String[] dbs = db.split(","); + if (dbs.length == 1) { + return Stream.of(dbs[0]).toArray(String[]::new); + } else { + return dbs; + } + }).orElse(null); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/converter/persistence/ValueEnumConverter.java b/server/src/main/java/com/aisino/iles/core/converter/persistence/ValueEnumConverter.java new file mode 100644 index 0000000..b123fea --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/converter/persistence/ValueEnumConverter.java @@ -0,0 +1,46 @@ +package com.aisino.iles.core.converter.persistence; + +import com.aisino.iles.common.iface.Logger; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.ValueEnum; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; + + +@Converter(autoApply = true) +@SuppressWarnings("unchecked") +public abstract class ValueEnumConverter> implements AttributeConverter, Logger { + private final Class tClass; + + + public ValueEnumConverter() { + this.tClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[1]; + } + + @Override + public X convertToDatabaseColumn(T t) { + if (t == null) + return null; + return t.getValue(); + } + + @Override + public T convertToEntityAttribute(X x) { + if (x == null) + return null; + if (x instanceof String && StringUtils.isEmpty(x.toString())) + return null; + try { + return Arrays.stream(((T[]) tClass.getMethod("values").invoke(tClass))) + .filter(v -> v.getValue().equals(x)).findFirst().orElseThrow(() -> new BusinessError("这个值[" + x + "], 无法通过值在[" + tClass.getName() + "]里找到对应的枚举类型!")); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + logger().error(e.getMessage(), e); + } + return null; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/ArgError.java b/server/src/main/java/com/aisino/iles/core/exception/ArgError.java new file mode 100644 index 0000000..e0e0a2f --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/ArgError.java @@ -0,0 +1,22 @@ +package com.aisino.iles.core.exception; + +import com.aisino.iles.common.util.Constants; + +/** + * 参数错误,用于方法的参数验证抛出 + */ +public class ArgError extends BusinessError { + private static String msg = "参数错误"; + + public ArgError() { + super(msg, Constants.Exceptions.arg_error); + } + + public ArgError(String msg, Throwable throwable) { + super(ArgError.msg+":"+msg, Constants.Exceptions.arg_error, throwable); + } + + public ArgError(String msg) { + super(ArgError.msg+":"+msg, Constants.Exceptions.arg_error); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/BusinessError.java b/server/src/main/java/com/aisino/iles/core/exception/BusinessError.java new file mode 100644 index 0000000..b7ae5aa --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/BusinessError.java @@ -0,0 +1,38 @@ +package com.aisino.iles.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务异常 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class BusinessError extends RuntimeException { + Integer code = 1; + + public BusinessError(Throwable throwable) { + super(throwable); + } + + public BusinessError(String msg, Throwable throwable) { + super(msg, throwable); + } + + public BusinessError() { + super(); + } + + public BusinessError(String msg, Integer code, Throwable throwable) { + super(msg, throwable); + this.code = code; + } + + public BusinessError(String msg, Integer code) { + super(msg); + this.code = code; + } + public BusinessError(String msg) { + super(msg); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/JitdLoginError.java b/server/src/main/java/com/aisino/iles/core/exception/JitdLoginError.java new file mode 100644 index 0000000..e94394a --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/JitdLoginError.java @@ -0,0 +1,29 @@ +package com.aisino.iles.core.exception; + +import com.aisino.iles.common.util.Constants; + +public class JitdLoginError extends BusinessError { + private final static String msg = "吉大正元证书登录错误"; + private String jitdReturnErrorCode = null; + + public JitdLoginError() { + super(msg, Constants.Exceptions.jitd_sign_login); + } + + public JitdLoginError(String msg) { + super(JitdLoginError.msg+": "+msg, Constants.Exceptions.jitd_sign_login); + } + + public JitdLoginError(String msg, Throwable throwable) { + super(JitdLoginError.msg+": "+msg, Constants.Exceptions.jitd_sign_login, throwable); + } + + public JitdLoginError(String msg, String errCode) { + super(JitdLoginError.msg+": "+msg, Constants.Exceptions.jitd_sign_login); + jitdReturnErrorCode = errCode; + } + + public String getJitdReturnErrorCode() { + return jitdReturnErrorCode; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/JurisdictionNotFoundError.java b/server/src/main/java/com/aisino/iles/core/exception/JurisdictionNotFoundError.java new file mode 100644 index 0000000..605af40 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/JurisdictionNotFoundError.java @@ -0,0 +1,32 @@ +package com.aisino.iles.core.exception; + +import com.aisino.iles.common.util.Constants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 机构信息未找到错误 + * + * @author huxin + * @since 2021-01-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class JurisdictionNotFoundError extends BusinessError { + private static final String msg = "机构信息不存在"; + private static final String detailMsg = "机构ID为[{jurisdictionId}]的机构信息在系统里不存在"; + private static final Integer errorCode = Constants.Exceptions.jurisdiction_not_found_error; + + public JurisdictionNotFoundError(String jurisdictionId) { + super(detailMsg.replaceAll("\\{jurisdictionId\\}", jurisdictionId), errorCode); + } + + public JurisdictionNotFoundError(String msg, Throwable throwable) { + super(msg, errorCode, throwable); + } + + public JurisdictionNotFoundError() { + super(msg, errorCode); + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/LoginError.java b/server/src/main/java/com/aisino/iles/core/exception/LoginError.java new file mode 100644 index 0000000..3880f33 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/LoginError.java @@ -0,0 +1,29 @@ +package com.aisino.iles.core.exception; + +import com.aisino.iles.common.util.Constants; + +/** + * 登录错误 + */ +public class LoginError extends BusinessError { + private static final String msg = "登录错误"; + public LoginError() { + super(LoginError.msg, Constants.Exceptions.login_error); + } + + public LoginError(String msg) { + super(LoginError.msg + ":" + msg, Constants.Exceptions.login_error); + } + + public LoginError(String msg,Throwable throwable){ + super(LoginError.msg + ":" + msg, Constants.Exceptions.login_error, throwable); + } + + public LoginError(String msg, Integer code, Throwable throwable) { + super(LoginError.msg + ":" +msg, code, throwable); + } + + public LoginError(String msg, Integer code) { + super(LoginError.msg + ":" +msg, code); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/LoginFiveTimesWrongError.java b/server/src/main/java/com/aisino/iles/core/exception/LoginFiveTimesWrongError.java new file mode 100644 index 0000000..25b70b3 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/LoginFiveTimesWrongError.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.exception; + +public class LoginFiveTimesWrongError extends LoginError { + private static final String msg = "登录错误"; + public LoginFiveTimesWrongError(String msg, Integer code) { + super(LoginFiveTimesWrongError.msg + ":" +msg, code); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/TokenError.java b/server/src/main/java/com/aisino/iles/core/exception/TokenError.java new file mode 100644 index 0000000..d7997ca --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/TokenError.java @@ -0,0 +1,27 @@ +package com.aisino.iles.core.exception; + +import com.aisino.iles.common.util.Constants; + +/** + * 令牌错误 + */ +public class TokenError extends BusinessError { + private static final String msg = "令牌错误"; + public TokenError() { + super(TokenError.msg, Constants.Exceptions.token_error); + } + + public TokenError(Throwable throwable) { + super(TokenError.msg, Constants.Exceptions.token_error, throwable); + } + + public TokenError(String msg,Throwable throwable){ + super(TokenError.msg + ": " + msg, Constants.Exceptions.token_error, throwable); + } + public TokenError(String msg) { + super(TokenError.msg + ":" + msg, Constants.Exceptions.token_error); + } + public TokenError(int code, String msg) { + super(TokenError.msg + ":" + msg, code); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/UploadFileError.java b/server/src/main/java/com/aisino/iles/core/exception/UploadFileError.java new file mode 100644 index 0000000..2d98936 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/UploadFileError.java @@ -0,0 +1,33 @@ +package com.aisino.iles.core.exception; + +import com.aisino.iles.common.util.Constants; + +/** + * 文件上传错误 + * + * @author huxin + * @since 2020-10-08 + */ +public class UploadFileError extends BusinessError { + private static final String msg = "文件上传错误"; + + public UploadFileError() { + super(UploadFileError.msg, Constants.Exceptions.upload_file_error); + } + + public UploadFileError(String msg) { + super(UploadFileError.msg + ":" + msg, Constants.Exceptions.upload_file_error); + } + + public UploadFileError(String msg, Throwable throwable) { + super(UploadFileError.msg + ":" + msg, Constants.Exceptions.upload_file_error, throwable); + } + + public UploadFileError(String msg, Integer code) { + super(UploadFileError.msg + ":" + msg, code); + } + + public UploadFileError(String msg, Integer code, Throwable throwable) { + super(UploadFileError.msg + ":" + msg, code, throwable); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/exception/UserNoRelateJurisdictionError.java b/server/src/main/java/com/aisino/iles/core/exception/UserNoRelateJurisdictionError.java new file mode 100644 index 0000000..ef5a0ff --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/exception/UserNoRelateJurisdictionError.java @@ -0,0 +1,16 @@ +package com.aisino.iles.core.exception; + +import com.aisino.iles.common.util.Constants; + +/** + * 用户没有关联机构异常 + */ +public class UserNoRelateJurisdictionError extends BusinessError { + private static final String msg = "该用户没有关联机构"; + + public UserNoRelateJurisdictionError() { + super(msg, Constants.Exceptions.user_no_relate_jurisdiciton_error); + } + + +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/AbstractDataPackSendToKafkaProcessor.java b/server/src/main/java/com/aisino/iles/core/hibernate/AbstractDataPackSendToKafkaProcessor.java new file mode 100644 index 0000000..e312b93 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/AbstractDataPackSendToKafkaProcessor.java @@ -0,0 +1,142 @@ +package com.aisino.iles.core.hibernate; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.model.Message; +import com.aisino.iles.common.model.MessageProperties; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.core.model.Token; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import jakarta.annotation.Resource; +import java.net.URI; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + + +/** + * 抽象数据包发送kafka处理业务 + * + * @author hx + * @since 20241029 + */ +@Slf4j +public abstract class AbstractDataPackSendToKafkaProcessor implements DataPackSingleRecordProcessor { + @Resource + private RestTemplate restTemplate; + @Resource + private RedissonClient redisson; + @Resource + private MessageProperties messageProperties; + + @Override + public void invoke(PackData data) { + buildMessage(data).forEach(this::sendMessageToKafka); + } + + @Override + public boolean support(PackData data) { + return false; + } + + /** + * 构建消息 + * + * @param data 数据包 + * @return 发送信息 + */ + public abstract List buildMessage(PackData data); + + /** + * 发送消息到kafka + * + * @param message 消息内容 + */ + void sendMessageToKafka(Message message) { + String token = Optional.ofNullable(redisson.getBucket(Constants.RedisKeyPrefix.messageToken.getValue()).get()) + .orElseGet(() -> { + RLock lock = redisson.getLock(Constants.RedisKeyPrefix.messageGenerateToken.getValue()); + AtomicReference mtk = new AtomicReference<>(); + try { + if (lock.tryLock(5, TimeUnit.SECONDS)) { + try { + // 登录消息服务用户,拿到token + Map params = new HashMap<>(); + params.put("username", messageProperties.getUsername()); + params.put("password", messageProperties.getPassword()); + RequestEntity> requestEntity = RequestEntity.post(URI.create(messageProperties.getLoginUrl())) + .body(params); + log.debug("登录获取消息服务token请求参数:{}", params); + ResponseEntity> response = restTemplate.exchange(requestEntity, new ParameterizedTypeReference>() {}); + log.debug("登录获取消息服务token结果:{}", response); + if (response.getStatusCode() == HttpStatus.OK) { + Result result = response.getBody(); + assert result != null; + if (result.getSuccess()) { + Token tk = result.getData(); + String messageTokenKey = tk.getToken(); + mtk.set(messageTokenKey); + redisson.getBucket(Constants.RedisKeyPrefix.messageToken.getValue()).set(messageTokenKey); + + } else { + log.error("登录获取消息服务token发生错误: 错误代码: {}, {}, 错误详情: {}", result.getCode(), result.getMsg(), result.getException()); + } + } else { + log.error("登录获取消息服务token发生错误:错误代码 {}, 错误内容: {}", response.getStatusCode(), response.getBody()); + } + } finally { + lock.unlock(); + } + } + } catch (InterruptedException e) { + log.error("在获取消息服务用户token时,发生了中断错误", e); + } + + RBucket tokenBucket = redisson.getBucket(Constants.RedisKeyPrefix.messageToken.getValue()); + long loopTimeout = 3000; // 循环的超时时间 + long loopTime = 0; + while (Objects.isNull(mtk.get()) && loopTime <= loopTimeout) { + try { + Thread.sleep(100); + loopTime += 100; + mtk.set(tokenBucket.get()); + } catch (InterruptedException e) { + log.error("在循环获取消息服务用户token时,发生了中断错误: " + e.getMessage(), e); + } + } + return Objects.requireNonNull(mtk.get()).toString(); + }); + + for (String topic : message.getTopics()) { + try { + ResponseEntity> response = restTemplate.exchange(RequestEntity.post(URI.create(messageProperties.getSendUrl() + "/" + topic)) + .header(Constants.Headers.AUTH_TOKEN, token) + .body(message), new ParameterizedTypeReference>() {}); + + if (response.getStatusCode() != HttpStatus.OK) { + log.error("发送消息到kafka发生错误:错误代码 {}, 错误内容: {}", response.getStatusCode(), response.getBody()); + } + if (response.getBody() != null && !response.getBody().getSuccess()) { + log.error("发送消息到kafka发生错误: 错误代码: {}, 错误消息:{}, 错误详情: {}", response.getBody().getCode(), response.getBody().getMsg(), response.getBody().getException()); + } + if (response.getBody() != null && (response.getBody().getCode() == Constants.Exceptions.token_expired_error + || response.getBody().getCode() == Constants.Exceptions.token_login_in_other_device_error)){ + redisson.getBucket(Constants.RedisKeyPrefix.messageToken.getValue()).delete(); + sendMessageToKafka(message); + } + } catch (RestClientException e) { + log.error("发送消息到kafka发生错误", e); + } + } + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/ColumnComment.java b/server/src/main/java/com/aisino/iles/core/hibernate/ColumnComment.java new file mode 100644 index 0000000..842a24b --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/ColumnComment.java @@ -0,0 +1,30 @@ +package com.aisino.iles.core.hibernate; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * 注解信息描述,可以用于建表的时候生成注释 + * + * @author huxin + * @since 2020-09-04 + */ +@Retention(RUNTIME) +@Target({ElementType.FIELD}) +public @interface ColumnComment { + /** + * 字段名称,默认就是标记类属性的名称 + * @return 字段 + */ + String name() default ""; + + /** + * 注释信息 + * @return 注释信息 + */ + String value(); +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/DMDialectPlus.java b/server/src/main/java/com/aisino/iles/core/hibernate/DMDialectPlus.java new file mode 100644 index 0000000..48f41fc --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/DMDialectPlus.java @@ -0,0 +1,19 @@ +//package com.aisino.iles.core.hibernate; +// +// +//import org.hibernate.dialect.DmDialect; +// +///** +// * 达梦数据库JPA方言,支持注释 +// * +// * @author huxin +// * @since 2020-09-08 +// */ +// +//public class DMDialectPlus extends DmDialect { +// @Override +// public boolean supportsCommentOn() { +// // 适配达梦数据库支持注解oracle模式 +// return true; +// } +//} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/DataPackAlarmInfoSendToKafkaProcessor.java b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackAlarmInfoSendToKafkaProcessor.java new file mode 100644 index 0000000..d11fdbd --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackAlarmInfoSendToKafkaProcessor.java @@ -0,0 +1,78 @@ +package com.aisino.iles.core.hibernate; + + +import com.aisino.iles.common.model.Message; +import com.aisino.iles.common.model.MessageProperties; +import com.aisino.iles.core.model.BaseModel; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * 公安网报警信息发送kafka处理业务 + * @author hx + * @since 20241029 + */ +@Slf4j +@Component +public class DataPackAlarmInfoSendToKafkaProcessor extends AbstractDataPackSendToKafkaProcessor{ + private final ObjectMapper mapper; + private final MessageProperties messageProperties; + + public DataPackAlarmInfoSendToKafkaProcessor(ObjectMapper mapper, MessageProperties messageProperties) { + this.mapper = mapper; + this.messageProperties = messageProperties; + } + + @Override + public List buildMessage(PackData data) { + List messages = new ArrayList<>(); + Message message = new Message(); + try { + message.setValue(mapper.writeValueAsString(data)); + message.setTopics(new String[]{messageProperties.getTopic().getAlarmInfo()}); + messages.add(message); + } catch (JsonProcessingException e) { + log.error("发送数据到kafka比对报警数据序列化数据包发生错误", e); + } + + return messages; + } + + + public boolean support(PackData data) { + boolean acceptOperation = data.getOperation().equals(DataPackageInterceptor.PackDataOperations.SAVE) || data.getOperation().equals(DataPackageInterceptor.PackDataOperations.MODIFY); + boolean acceptEntityType = false; + if (acceptOperation) { + try { + log.debug("发送kafka解析前数据包信息:{}", data); + Object object = mapper.readValue(data.getDataJson(), Class.forName(data.getClzName())); + log.debug("发送kafka数据实体:{}", object); + acceptEntityType = Objects.nonNull(object) && object instanceof BaseModel && ((BaseModel) object).isNeedAlarm(); + } catch (JsonProcessingException e) { + log.error("json反序列化错误:{}", e.getMessage(), e); + } catch (ClassNotFoundException e) { + log.error("定位类型的时候没有找到这个类型:" + e.getMessage(), e); + } + } + return acceptOperation && acceptEntityType; + } + + @Override + public void invoke(List data) { + log.debug("发送数据到kafka比对报警"); + super.invoke(data); + log.debug("发送数据完成"); + } + + @Override + public String getName() { + return "sendAlarmInfoToKafka"; + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/DataPackProcessor.java b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackProcessor.java new file mode 100644 index 0000000..6586db1 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackProcessor.java @@ -0,0 +1,23 @@ +package com.aisino.iles.core.hibernate; + +/** + * 数据包处理器,用于将数据包根据业务需求进行其他行为,比如发送邮件、发送短信、发送信息等 + * @author hx + * @since 20241028 + */ +public interface DataPackProcessor { + + /** + * 处理数据包 + */ + void invoke(T data); + + /** + * 是否支持当前数据包进入业务处理流程 + * @param data 数据包 + * @return true 可以进入业务 false 不可以 + */ + boolean support(T data); + + String getName(); +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/DataPackSendToDttPackProcessor.java b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackSendToDttPackProcessor.java new file mode 100644 index 0000000..14dd5eb --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackSendToDttPackProcessor.java @@ -0,0 +1,69 @@ +package com.aisino.iles.core.hibernate; + +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.config.DataPackageConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +/** + * 数据包发送到打包服务进行打包 + * @author hx + * @since 20241028 + */ +@Slf4j +@Service +public class DataPackSendToDttPackProcessor implements DataPackProcessor> { + private final DataPackageConfig dataPackageConfig; + private final RestTemplate restTemplate; + + public DataPackSendToDttPackProcessor(DataPackageConfig dataPackageConfig, RestTemplate restTemplate) { + this.dataPackageConfig = dataPackageConfig; + this.restTemplate = restTemplate; + } + + @Override + public void invoke(List data) { + for (int i = 0; i < dataPackageConfig.getRetryTimes(); i++) { + try { + // rest 数据给接口 + HttpEntity> requestEntity = new HttpEntity<>(data); + ResponseEntity resp = restTemplate.exchange(dataPackageConfig.getPackagingApi(), HttpMethod.POST, requestEntity, Void.class); + if (resp.getStatusCode() == HttpStatus.OK) { + break; + } + } catch (Exception e) { + log.error("写入保存打包数据发生错误:" + e.getMessage()); + log.debug(e.getMessage(), e); + if (i == dataPackageConfig.getRetryTimes() - 1) { + // todo rest接口调用错误超过重试次数之后处理 + } else { + try { + Thread.sleep(dataPackageConfig.getRetryInterval()); + } catch (InterruptedException interruptedException) { + log.error(interruptedException.getLocalizedMessage(), interruptedException); + } + } + + } + } + } + + @Override + public boolean support(List data) { + // 是否需要打包,存在打包标记打包api + return dataPackageConfig.isPackagingFlag() && StringUtils.isNotBlank(dataPackageConfig.getPackagingApi()); + } + + @Override + public String getName() { + return "sendToDttPack"; + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/DataPackSingleRecordProcessor.java b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackSingleRecordProcessor.java new file mode 100644 index 0000000..2006820 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackSingleRecordProcessor.java @@ -0,0 +1,22 @@ +package com.aisino.iles.core.hibernate; + +import java.util.List; +import java.util.Objects; + +/** + * 数据包单记录处理 + * @author hx + * @since 20241028 + */ +public interface DataPackSingleRecordProcessor extends DataPackProcessor> { + void invoke(PackData data); + boolean support(PackData data); + + default boolean support(List data) { + return data.stream().anyMatch(this::support); + } + + default void invoke(List data) { + data.stream().filter(this::support).filter(Objects::nonNull).forEach(this::invoke); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/DataPackageInterceptor.java b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackageInterceptor.java new file mode 100644 index 0000000..399921f --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/DataPackageInterceptor.java @@ -0,0 +1,314 @@ +package com.aisino.iles.core.hibernate; + +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.config.DataPackageConfig; +import com.aisino.iles.core.exception.BusinessError; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Interceptor; +import org.hibernate.Transaction; +import org.hibernate.collection.spi.AbstractPersistentCollection; +import org.hibernate.collection.spi.PersistentList; +import org.hibernate.collection.spi.PersistentSet; +import org.hibernate.type.Type; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; + +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 数据打包拦截器 + * + * @author huxin + * @since 2020-08-31 + */ +@Component +@Slf4j +public class DataPackageInterceptor implements Interceptor { + /** + * 用于新增修改删除的数据实体 + */ + private final ThreadLocal> saveEntities = new ThreadLocal<>(); + /** + * 是否已经提交过了 + */ + private final ThreadLocal wasCommitted = new ThreadLocal<>(); + + private final ObjectMapper mapper; + /** + * 路径匹配工具 + */ + private final AntPathMatcher pathMatcher; + private final DataPackageConfig dataPackageConfig; + private final ObjectProvider>> dataPackProcessorProviders; + + public DataPackageInterceptor(DataPackageConfig dataPackageConfig, + @Qualifier("dataPackageObjectMapper") ObjectMapper mapper, + ObjectProvider>> dataPackProcessors) { + this.mapper = mapper; + this.dataPackageConfig = dataPackageConfig; + this.dataPackProcessorProviders = dataPackProcessors; + pathMatcher = new AntPathMatcher(); + } + + /** + * 生成打包数据并加入到集合里面 + * + * @param entity 实体 + * @param operation 操作标志 + * @param id 主键 + */ + private void processPackData(Object entity, String operation, Serializable id) { + if (saveEntities.get() == null) { + saveEntities.set(new LinkedHashMap<>()); + } + String className = entity.getClass().getName(); + if (dataPackageConfig.getProcessEntityNames().getInclude().stream().anyMatch(enp -> pathMatcher.matchStart(enp, className)) + && dataPackageConfig.getProcessEntityNames().getExclude().stream().noneMatch(enp -> pathMatcher.matchStart(enp, className))) { + log.debug("配置包含当前实体加入打包列表。"); + String packDataId = className + "#" + id; + Optional oPackData = Optional.ofNullable(saveEntities.get().get(packDataId)) + .map(pd -> { + String oper = PackDataOperations.PRIORITY_LIST.indexOf(operation) < PackDataOperations.PRIORITY_LIST.indexOf(saveEntities.get().get(packDataId).getOperation()) ? + operation : saveEntities.get().get(packDataId).getOperation(); + if (operation.equals(PackDataOperations.REMOVE)) { + saveEntities.get().remove(packDataId); + return Optional.empty(); + } + return Optional.of(new PackData() + .setClzName(className) + .setEntity(entity) + .setOperation(oper) + .setSystemName("SYSTEM") + .setWaring(false)); + }) + .orElse(Optional.of(new PackData() + .setClzName(className) + .setEntity(entity) + .setOperation(operation) + .setSystemName("SYSTEM") + .setWaring(false))); + + oPackData.ifPresent(packData -> { + try { + log.debug("entityJson:{}", mapper.writeValueAsString(packData)); + saveEntities.get().put(packDataId, packData); + } catch (JsonProcessingException e) { + log.error("打包写入新增实体JSON数据发生错误:" + e.getMessage(), e); + } + }); + + } + } + + /** + * 触发删除 + * + * @param entity 实体 + * @param id 主键 + * @param state 状态(也就是属性的值) + * @param propertyNames 属性名称 + * @param types 类型数组 + */ + @Override + public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { + log.debug("进入删除"); + log.debug("当前线程ID:{}", Thread.currentThread().getId()); + log.debug("当前实体:{}", entity.getClass().getName()); + processPackData(entity, PackDataOperations.REMOVE, id); + } + + /** + * 触发修改 + * + * @param entity 实体 + * @param id 主键 + * @param currentState 当前状态 + * @param previousState 之前的状态 + * @param propertyNames 属性名称 + * @param types 类型 + * @return 如果在这个事件回调里的修改需要生效持久化 返回true 否则false ,默认false + */ + @Override + public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) { + log.debug("进入修改"); + log.debug("当前线程ID:" + Thread.currentThread().getId()); + log.debug("当前实体:" + entity.getClass().getName()); + // 先生成打包数据 + processPackData(entity, PackDataOperations.MODIFY, id); + return false; + } + + /** + * 触发新增 + * + * @param entity 实体 + * @param id 主键 + * @param state 当前状态 + * @param propertyNames 属性名称 + * @param types 类型 + * @return 如果在这个事件回调里的修改需要生效持久化 返回true 否则false ,默认false + */ + @Override + public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { + log.debug("进入新增"); + log.debug("当前线程ID:" + Thread.currentThread().getId()); + log.debug("当前实体:" + entity.getClass().getName()); + // 先生成打包数据 + processPackData(entity, PackDataOperations.SAVE, id); + return false; + } + + @Override + public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { +// log.debug("进入载入实体(查询)"); +// log.debug("当前线程ID:" + Thread.currentThread().getId()); +// log.debug("当前实体:" + entity.getClass().getName()); + return false; + } + + @Override + public String getEntityName(Object object) { + return object.getClass().getName(); + } + + /** + * 当集合类型调用remove时候 + * + * @param collection 集合 + * @param key 主键 + */ + @Override + public void onCollectionRemove(Object collection, Serializable key) { + collectionPackage(collection, key); + } + + /** + * 当集合类型变量被重庆实例化时候 + * + * @param collection 集合 + * @param key 主键 + */ + @Override + public void onCollectionRecreate(Object collection, Serializable key) { + collectionPackage(collection, key); + } + + /** + * 当集合类型变量发生修改的时候 + * + * @param collection 集合 + * @param key 主键 + */ + @Override + public void onCollectionUpdate(Object collection, Serializable key) { + collectionPackage(collection, key); + } + + private void collectionPackage(Object collection, Serializable key) { + if (saveEntities.get() == null) { + saveEntities.set(new LinkedHashMap<>()); + } + if (PersistentSet.class.isAssignableFrom(collection.getClass()) || PersistentList.class.isAssignableFrom(collection.getClass())) { + processPackData(((AbstractPersistentCollection) collection).getOwner(), PackDataOperations.MODIFY, key); + } + } + + /** + * 事务完成前提交了事务,在这里标记一提交状态 + * + * @param tx jpa事务 + */ + @Override + public void beforeTransactionCompletion(Transaction tx) { + // 设置提交标记 + wasCommitted.set(Boolean.TRUE); + } + + /** + * 事务完成的时候触发写入打包的数据信息 + * + * @param tx JPA/HIBERNATE事务 + */ + @Override + public void afterTransactionCompletion(Transaction tx) { + try { + if (wasCommitted.get() == null || !wasCommitted.get()) { + return; + } + // 事务完成的时候触发写入打包的数据信息 + Optional.ofNullable(saveEntities.get()).filter(l -> !l.isEmpty()) + .ifPresent(sel -> { + log.debug("事务完成写入打包数据 ================ "); + log.debug("当前线程ID:{}", Thread.currentThread().getId()); + //log.debug(sel.toString()); + + List packDataList = saveEntities.get().values().stream() + .peek(pd -> { + try { + pd.setDataJson(mapper.writeValueAsString(pd.getEntity())); + } catch (JsonProcessingException e) { + log.error("打包写入新增实体JSON数据发生错误:" + e.getMessage(), e); + } + }) + .collect(Collectors.toList()); + + // 链式调用数据包处理器 + List>> dataPackProcessors = dataPackProcessorProviders.stream() + .filter(dp -> dataPackageConfig.getDataPackProcessors().contains(dp.getName())) + .collect(Collectors.toList()); + + dataPackProcessors.stream() + .filter(dpp -> dpp.support(packDataList)) + .forEach(dpp -> { + try { + dpp.invoke(packDataList); + } catch (Exception e) { + log.error("数据包业务处理发生异常", e); + } + }); + }); + } finally { + wasCommitted.remove(); + saveEntities.remove(); + } + + } + + public static final class PackDataOperations { + public static final String SAVE = "insert"; + public static final String MODIFY = "update"; + public static final String REMOVE = "delete"; + public static final List PRIORITY_LIST = Arrays.asList(SAVE, MODIFY); + + } + + /** + * 打包实体操作类型(这个区别于{@link PackDataOperations}只用来标记saveEntities里面类型. + * 现在不再根据每一个操作insert等,打包操作轨迹. 而只打包每个事物的每个实体的最后状态,这样比较高效. + */ + public enum SaveEntityType { + save, remove; + + public SaveEntityType fromPackDataOperation(String packDataOperation) { + if (StringUtils.isEmpty(packDataOperation)) { + throw new BusinessError("数据包操作类型为空"); + } + if (!PackDataOperations.PRIORITY_LIST.contains(packDataOperation)) { + throw new BusinessError("数据包操作类型错误"); + } + if (packDataOperation.equals(PackDataOperations.SAVE) || packDataOperation.equals(PackDataOperations.MODIFY)) { + return save; + } else { + return remove; + } + } + } + + +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/MysqlDialectPlus.java b/server/src/main/java/com/aisino/iles/core/hibernate/MysqlDialectPlus.java new file mode 100644 index 0000000..ff88969 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/MysqlDialectPlus.java @@ -0,0 +1,74 @@ +package com.aisino.iles.core.hibernate; + + +import cn.hutool.core.util.StrUtil; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.MySQL8Dialect; +import org.hibernate.dialect.MySQLDialect; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * mysql8方言扩展 + * @author hx + * @since 2023-07-26 + */ +public class MysqlDialectPlus extends MySQLDialect { + public MysqlDialectPlus() { + super(DatabaseVersion.make(8,0,34)); + } + + /** + * 支持数据库提示注解(目前主要还是部分支持不同数据库实现hint的方式, + * 是不同的有的使用select /*+ something *\/ 有的使用 from table using something + * 这里为mysql提供select 注释+something的实现) + * @param query 生成的sql + * @param hints 数据库提示注解 + * @return 加入数据库提示注解后的sql语句 + */ + @Override + public String getQueryHintString(String query, String hints) { + // 在query语句是select语句,才使用hints + if(!query.startsWith("select")) + return query; + // hints 里面有字符串模板替换 + // 表名模板使用上有一个局限,他只能用于表示出现一次的表关联语句,如果出现了自连接之类的例如 + // from t_jurisdiction j join t_jurisdiction pj on j.parent_juris_id = pj.jurisdiction_id + // 这样的语句他就不能很好的使用{t_jurisdiction}获取到准确的别名。 + String newHints = StrUtil.format(hints,findHintsTemplateMap(query, hints)); + // 把hints插入到query的select 关键字后面,使用固定格式/*+ */ 包裹住hints + StringBuilder newSql = new StringBuilder(); + newSql.append(query, 0, "select".length()); + newSql.append(" /*+ "); + newSql.append(newHints); + newSql.append(" */ "); + newSql.append(query, "select".length(), query.length()); + return newSql.toString(); + } + + private Map findHintsTemplateMap(String sql, String hints) { + // 获取hints中的{}中间的内容,返回列表 + List tempVars = new ArrayList<>(); + Pattern pattern = Pattern.compile("\\{(.*?)}"); + Matcher matcher = pattern.matcher(hints); + + while(matcher.find()){ + tempVars.add(matcher.group(1)); + } + + return tempVars.stream().reduce(new HashMap<>(), (a, b) -> { + Pattern aliasPattern = Pattern.compile("\\b"+b +"\\b" + "\\s+(.*?)(,|$|\\s+)+"); + Matcher aliasMatcher = aliasPattern.matcher(sql); + while (aliasMatcher.find()) { + a.put(b, aliasMatcher.group(1)); + } + return a; + }, (a, b) -> a); + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/PackData.java b/server/src/main/java/com/aisino/iles/core/hibernate/PackData.java new file mode 100644 index 0000000..c677d00 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/PackData.java @@ -0,0 +1,37 @@ +package com.aisino.iles.core.hibernate; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class PackData { + /** + * 全类名 + */ + private String clzName; + + /** + * 同步的业务数据 + */ + private String dataJson; + + /** + * 系统名称 例如 HOTEL 酒店 + */ + private String systemName; + + /** + * 数据操作 insert, update, delete + */ + private String operation; + + /** + * 是否为预警数据 + */ + private boolean isWaring; + /** 实体对象 不加入序列化和反序列化 */ + @JsonIgnore + private Object entity; +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/PostgreSQLDialectPlus.java b/server/src/main/java/com/aisino/iles/core/hibernate/PostgreSQLDialectPlus.java new file mode 100644 index 0000000..a9d5ba7 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/PostgreSQLDialectPlus.java @@ -0,0 +1,21 @@ +package com.aisino.iles.core.hibernate; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.dialect.PostgreSQLDialect; + +/** + * PostgreSQL方言扩展(beiyong) + * 添加对json_value函数的支持,统一JSON函数调用 + * + * @author hx + * @since 2025-08-08 + */ +public class PostgreSQLDialectPlus extends PostgreSQLDialect { + + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry(functionContributions); + + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/RedissonLocalStorage.java b/server/src/main/java/com/aisino/iles/core/hibernate/RedissonLocalStorage.java new file mode 100644 index 0000000..b38a718 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/RedissonLocalStorage.java @@ -0,0 +1,237 @@ +package com.aisino.iles.core.hibernate; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.cache.CacheException; +import org.hibernate.cache.spi.support.DomainDataStorageAccess; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.redisson.api.RFuture; +import org.redisson.api.RLocalCachedMap; +import org.redisson.connection.ConnectionManager; +import org.redisson.hibernate.RedissonRegionFactory; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class RedissonLocalStorage implements DomainDataStorageAccess { + private final RLocalCachedMap mapCache; + + private final ConnectionManager connectionManager; + + @Getter + int ttl; + @Getter + int maxIdle; + @Getter + int size; + boolean fallback; + volatile boolean fallbackMode; + + public RedissonLocalStorage(RLocalCachedMap mapCache, + ConnectionManager connectionManager, + Map properties, + String defaultKey) { + if (mapCache == null || connectionManager == null) { + throw new IllegalArgumentException("mapCache和connectionManager不能为空"); + } + this.mapCache = mapCache; + this.connectionManager = connectionManager; + + + String maxEntries = getProperty(properties, mapCache.getName(), defaultKey, RedissonRegionFactory.MAX_ENTRIES_SUFFIX); + if (maxEntries != null) { + size = Integer.parseInt(maxEntries); + } + String timeToLive = getProperty(properties, mapCache.getName(), defaultKey, RedissonRegionFactory.TTL_SUFFIX); + if (timeToLive != null) { + ttl = Integer.parseInt(timeToLive); + } + String maxIdleTime = getProperty(properties, mapCache.getName(), defaultKey, RedissonRegionFactory.MAX_IDLE_SUFFIX); + if (maxIdleTime != null) { + maxIdle = Integer.parseInt(maxIdleTime); + } + + String fallbackValue = (String) properties.getOrDefault(RedissonRegionFactory.FALLBACK, "false"); + fallback = Boolean.parseBoolean(fallbackValue); + } + + private String getProperty(Map properties, String name, String defaultKey, String suffix) { + String maxEntries = (String) properties.get(RedissonRegionFactory.CONFIG_PREFIX + name + suffix); + if (maxEntries != null) { + return maxEntries; + } + String defValue = (String) properties.get(RedissonRegionFactory.CONFIG_PREFIX + defaultKey + suffix); + if (defValue != null) { + return defValue; + } + return null; + } + + private void ping() { + fallbackMode = true; + + connectionManager.getServiceManager().newTimeout(t -> { + if (!fallbackMode) { + return; + } + RFuture future = mapCache.isExistsAsync(); + future.onComplete((r, ex) -> { + if (ex == null) { + fallbackMode = false; + log.info("Redis连接恢复正常,退出降级模式"); + } else { + log.warn("Redis连接检查失败: {}", ex.getMessage()); + ping(); + } + }); + }, 5, TimeUnit.SECONDS); // 调整为5秒重试间隔 + } + + @Override + public Object getFromCache(Object key, SharedSessionContractImplementor session) { + if (fallbackMode) { + return null; + } + try { + Object value = mapCache.get(key); + if (log.isDebugEnabled()) { + log.debug("读取缓存[{}]键[{}][{}]", mapCache.getName(), key, Objects.isNull(value) ? "未命中": "命中"); + } + return value; + } catch (Exception e) { + if (fallback) { + ping(); // 在降级模式下调用 ping + return null; // 返回默认值 + } + throw new CacheException("缓存读取失败", e); + } + } + + @Override + public void putIntoCache(Object key, Object value, SharedSessionContractImplementor session) { + if (fallbackMode) { + return; + } + try { + if (log.isDebugEnabled()) { + log.debug("写入缓存[{}]键[{}], TTL:{}ms, maxIdle:{}ms", + mapCache.getName(), key, ttl, maxIdle); + } + RFuture future = mapCache.fastPutAsync(key, value); + future.whenComplete((res, ex) -> { + if (ex != null) { + log.error("异步写入缓存[{}]键[{}]失败: {}", mapCache.getName(), key, ex.getMessage()); + } else if (log.isDebugEnabled()) { + log.debug("异步写入缓存[{}]键[{}]成功", mapCache.getName(), key); + } + }); + } catch (Exception e) { + if (fallback) { + ping(); // 在降级模式下调用 ping + return; // 返回默认值 + } + throw new CacheException("缓存写入失败", e); + } + } + + + @Override + public boolean contains(Object key) { + if (fallbackMode) { + return false; + } + try { + boolean exists = mapCache.containsKey(key); + if (log.isDebugEnabled()) { + log.debug("检查缓存[{}]键[{}]存在: {}", mapCache.getName(), key, exists); + } + return exists; + } catch (Exception e) { + if (fallback) { + ping(); // 在降级模式下调用 ping + return false; // 返回默认值 + } + throw new CacheException("检查缓存失败", e); + } + } + + @Override + public void evictData() { + if (fallbackMode) { + return; + } + try { + mapCache.clear(); + if (log.isDebugEnabled()) { + log.debug("清空缓存[{}]", mapCache.getName()); + } + } catch (Exception e) { + if (fallback) { + ping(); // 在降级模式下调用 ping + return; // 返回默认值 + } + throw new CacheException("清空缓存失败", e); + } + } + + + @Override + public void evictData(Object key) { + if (fallbackMode || key == null) { + if (log.isDebugEnabled()) { + log.debug("拦截无效缓存移除请求[状态:{}, 键类型:{}]", + fallbackMode ? "降级模式" : "运行中", + key != null ? key.getClass().getSimpleName() : "null"); + } + return; + } + try { + // 异步移除缓存并处理结果 + RFuture future = mapCache.fastRemoveAsync(key); + future.whenComplete((result, ex) -> { + if (ex != null) { + log.error("缓存移除失败[{}] - 键[{}]: {}", + mapCache.getName(), key, ex.getMessage()); + } else if (log.isDebugEnabled()) { + log.debug("缓存移除[{}] - 键[{}]: {}", + mapCache.getName(), key, result > 0 ? "成功" : "未同步"); + } + }); + + } catch (Exception e) { + if (fallback) { + ping(); // 在降级模式下调用 ping + return; // 返回默认值 + } + throw new CacheException("缓存清除失败", e); + } + } + + @Override + public void release() { + try { + if (log.isDebugEnabled()) { + log.debug("释放缓存[{}]资源", mapCache.getName()); + } + // 销毁缓存实例,同步清除本地、异步清除远程 + mapCache.destroy(); + } catch (Exception e) { + if (fallback) { + ping(); // 在降级模式下调用 ping + return; + } + log.error("释放缓存[{}]资源失败: {}", mapCache.getName(), e.getMessage()); + throw new CacheException("缓存资源释放失败", e); + } + } + + public int getCacheSize() { + return mapCache.size(); + } + + public long getCacheTtl() { + return mapCache.remainTimeToLive(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/hibernate/RedissonRegionFactoryPlus.java b/server/src/main/java/com/aisino/iles/core/hibernate/RedissonRegionFactoryPlus.java new file mode 100644 index 0000000..a2f7f50 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/hibernate/RedissonRegionFactoryPlus.java @@ -0,0 +1,190 @@ +package com.aisino.iles.core.hibernate; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.boot.registry.selector.spi.StrategySelector; +import org.hibernate.boot.spi.SessionFactoryOptions; +import org.hibernate.cache.CacheException; +import org.hibernate.cache.cfg.spi.DomainDataRegionBuildingContext; +import org.hibernate.cache.cfg.spi.DomainDataRegionConfig; +import org.hibernate.cache.spi.CacheKeysFactory; +import org.hibernate.cache.spi.support.DomainDataStorageAccess; +import org.hibernate.cache.spi.support.RegionNameQualifier; +import org.hibernate.cache.spi.support.StorageAccess; +import org.hibernate.cfg.Environment; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.redisson.Redisson; +import org.redisson.api.LocalCachedMapOptions; +import org.redisson.api.RLocalCachedMap; +import org.redisson.api.RScript; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.LongCodec; +import org.redisson.hibernate.RedissonCacheKeysFactory; +import org.redisson.hibernate.RedissonRegionFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 支持从spring管理的redisson实例注入 + * + * @author hx + */ +@Slf4j +public class RedissonRegionFactoryPlus extends RedissonRegionFactory { + private final RedissonClient redisson; + private CacheKeysFactory cacheKeysFactory; + private final Map regionMap = new ConcurrentHashMap<>(); + + public RedissonRegionFactoryPlus(RedissonClient redisson) { + this.redisson = redisson; + } + + @Override + @SuppressWarnings({"unchecked"}) + protected void prepareForUse(SessionFactoryOptions settings, Map properties) throws CacheException { + String fallbackValue = (String) properties.getOrDefault(FALLBACK, "false"); + fallback = Boolean.parseBoolean(fallbackValue); + + StrategySelector selector = settings.getServiceRegistry().getService(StrategySelector.class); + cacheKeysFactory = selector.resolveDefaultableStrategy(CacheKeysFactory.class, + properties.get(Environment.CACHE_KEYS_FACTORY), new RedissonCacheKeysFactory(redisson.getConfig().getCodec())); + } + + @Override + protected CacheKeysFactory getImplicitCacheKeysFactory() { + return cacheKeysFactory; + } + + @Override + protected void releaseFromUse() { + regionMap.clear(); + this.redisson.shutdown(); + } + + public Map getAllCacheRegions() { + return Collections.unmodifiableMap(regionMap); + } + + private String qualifyName(String name) { + return RegionNameQualifier.INSTANCE.qualify(name, getOptions()); + } + + @Override + protected DomainDataStorageAccess createDomainDataStorageAccess(DomainDataRegionConfig regionConfig, DomainDataRegionBuildingContext buildingContext) { + String defaultKey; + if (!regionConfig.getCollectionCaching().isEmpty()) { + defaultKey = COLLECTION_DEF; + } else if (!regionConfig.getEntityCaching().isEmpty()) { + defaultKey = ENTITY_DEF; + } else if (!regionConfig.getNaturalIdCaching().isEmpty()) { + defaultKey = NATURAL_ID_DEF; + } else { + throw new IllegalArgumentException("Unable to determine entity cache type!"); + } + + RLocalCachedMap mapCache = getLocalCachedMap(qualifyName(regionConfig.getRegionName()), buildingContext.getSessionFactory().getProperties(), defaultKey); + RedissonLocalStorage storage = new RedissonLocalStorage(mapCache, ((Redisson) redisson).getConnectionManager(), buildingContext.getSessionFactory().getProperties(), defaultKey); + regionMap.put(regionConfig.getRegionName(), storage); + return storage; + } + + @Override + protected StorageAccess createQueryResultsRegionStorageAccess(String regionName, SessionFactoryImplementor sessionFactory) { + // 使用本地缓存映射优化查询结果存储 + RLocalCachedMap mapCache = getLocalCachedMap( + qualifyName(regionName), + sessionFactory.getProperties(), + QUERY_DEF + ); + RedissonLocalStorage storage = new RedissonLocalStorage( + mapCache, + ((Redisson) redisson).getConnectionManager(), + sessionFactory.getProperties(), + QUERY_DEF + ); + regionMap.put(regionName, storage); + return storage; + } + + @Override + protected StorageAccess createTimestampsRegionStorageAccess(String regionName, SessionFactoryImplementor sessionFactory) { + // 时间戳区域改用本地缓存存储 + RLocalCachedMap mapCache = getLocalCachedMap( + qualifyName(regionName), + sessionFactory.getProperties(), + TIMESTAMPS_DEF + ); + RedissonLocalStorage storage = new RedissonLocalStorage( + mapCache, + ((Redisson) redisson).getConnectionManager(), + sessionFactory.getProperties(), + TIMESTAMPS_DEF + ); + regionMap.put(regionName, storage); + return storage; + } + + + /** + * 获取本地缓存映射配置 + * @param regionName 缓存区域名称 + * @param properties 会话工厂配置属性 + * @param defaultKey 默认配置键名 + * @return 配置好的本地缓存映射实例 + */ + + private String getPropertyValue(String regionName, String defaultKey, String propertySuffix, Map properties) { + String value = (String) properties.get(RedissonRegionFactory.CONFIG_PREFIX + regionName + propertySuffix); + if (value == null) { + value = (String) properties.get(RedissonRegionFactory.CONFIG_PREFIX + defaultKey + propertySuffix); + } + return value; + } + + public RLocalCachedMap getLocalCachedMap(String name, Map properties, String defaultKey) { + String maxEntries = getPropertyValue(name, defaultKey, RedissonRegionFactory.MAX_ENTRIES_SUFFIX, properties); + int size = (maxEntries != null) ? Integer.parseInt(maxEntries) : 0; + + String timeToLive = getPropertyValue(name, defaultKey, RedissonRegionFactory.TTL_SUFFIX, properties); + long ttl = (timeToLive != null) ? Long.parseLong(timeToLive) : 0; + + String maxIdleTime = getPropertyValue(name, defaultKey, RedissonRegionFactory.MAX_IDLE_SUFFIX, properties); + long maxIdle = (maxIdleTime != null) ? Long.parseLong(maxIdleTime) : 0; + + LocalCachedMapOptions localCachedMapOptions = LocalCachedMapOptions.defaults() + .evictionPolicy(LocalCachedMapOptions.EvictionPolicy.LRU) + .timeToLive(ttl) + .cacheSize(size) + .reconnectionStrategy(LocalCachedMapOptions.ReconnectionStrategy.CLEAR) + .maxIdle(maxIdle) + .syncStrategy(LocalCachedMapOptions.SyncStrategy.UPDATE); + + return redisson.getLocalCachedMap(name, localCachedMapOptions); + } + + @Override + public long nextTimestamp() { + long time = System.currentTimeMillis() << 12; + try { + return redisson.getScript(LongCodec.INSTANCE).eval(RScript.Mode.READ_WRITE, + "local currentTime = redis.call('get', KEYS[1]);" + + "if currentTime == false then " + + "redis.call('set', KEYS[1], ARGV[1]); " + + "return ARGV[1]; " + + "end;" + + "local nextValue = math.max(tonumber(ARGV[1]), tonumber(currentTime) + 1); " + + "redis.call('set', KEYS[1], nextValue); " + + "return nextValue;", + RScript.ReturnType.INTEGER, Collections.singletonList("redisson-hibernate-timestamp"), time); + } catch (Exception e) { + if (fallback) { + return super.nextTimestamp(); + } + throw e; + } + } + + +} diff --git a/server/src/main/java/com/aisino/iles/core/identifiergenerator/GeneratorPlusHelper.java b/server/src/main/java/com/aisino/iles/core/identifiergenerator/GeneratorPlusHelper.java new file mode 100644 index 0000000..d8135ee --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/identifiergenerator/GeneratorPlusHelper.java @@ -0,0 +1,28 @@ +package com.aisino.iles.core.identifiergenerator; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import java.util.Arrays; +import java.util.Optional; + +/** + * ID生成器工具帮助类 + */ +public interface GeneratorPlusHelper { + Log log = LogFactory.getLog(GeneratorPlusHelper.class); + String idColumnName(); + default Optional getCurrentIdValue(Object object) { + return Arrays.stream(ReflectUtil.getFields(object.getClass())) + .filter(v-> v.getAnnotation(Id.class)!=null) + .filter(v-> Optional.ofNullable(v.getAnnotation(Column.class)) + .filter(c -> StrUtil.isNotBlank(c.name())) + .map(c -> StrUtil.equalsAnyIgnoreCase(idColumnName(),c.name())) + .orElseGet(() -> StrUtil.toCamelCase(idColumnName()).equals(v.getName()))).findFirst() + .map(v->ReflectUtil.getFieldValue(object,v)); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/identifiergenerator/ULIDGenerator.java b/server/src/main/java/com/aisino/iles/core/identifiergenerator/ULIDGenerator.java new file mode 100644 index 0000000..f7f385f --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/identifiergenerator/ULIDGenerator.java @@ -0,0 +1,44 @@ +package com.aisino.iles.core.identifiergenerator; + +import com.github.f4b6a3.ulid.Ulid; +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.Configurable; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.PersistentIdentifierGenerator; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.Type; + +import java.io.Serializable; +import java.util.Properties; + +/** + * ulid生成器 + * + * @author suen.sun + * @author hx + */ +public class ULIDGenerator implements GeneratorPlusHelper, IdentifierGenerator, Configurable { + /** + * 主键字段 + */ + private String idColumnName; + + @Override + public Serializable generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException { + return getCurrentIdValue(o) + .map(v -> (Serializable) v) + .orElseGet(() -> Ulid.fast().toLowerCase()); + } + + @Override + public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException { + this.idColumnName = params.getProperty(PersistentIdentifierGenerator.PK); + } + + @Override + public String idColumnName() { + return this.idColumnName; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/identifiergenerator/UUIDGeneratorPlus.java b/server/src/main/java/com/aisino/iles/core/identifiergenerator/UUIDGeneratorPlus.java new file mode 100644 index 0000000..57ae325 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/identifiergenerator/UUIDGeneratorPlus.java @@ -0,0 +1,47 @@ +package com.aisino.iles.core.identifiergenerator; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.PersistentIdentifierGenerator; +import org.hibernate.id.UUIDGenerator; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.Type; + +import java.io.Serializable; +import java.util.Optional; +import java.util.Properties; + +/** + * uuid风格ID生成器、改 + *

+ * 提供当主键属性为空的时候,才生成ID数据,主键属性不为空的时候,使用原来的值保存数据。 + * + * @author huxin + * @since 2020-07-02 + */ +@Slf4j +public class UUIDGeneratorPlus extends UUIDGenerator implements GeneratorPlusHelper { + public static final String ALWAYS_GENERATE = "always_generate"; + private String idColumnName; + private boolean always = false; + @Override + public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException { + super.configure(type, params, serviceRegistry); + this.idColumnName = params.getProperty(PersistentIdentifierGenerator.PK); + this.always = Optional.ofNullable(params.getProperty(ALWAYS_GENERATE)).map(Boolean::parseBoolean).orElse(false); + } + + @Override + public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException { + if(always) + return (Serializable) super.generate(session, object); + return getCurrentIdValue(object).map(r -> ((Serializable) r)).orElse((Serializable) super.generate(session, object)); + } + + @Override + public String idColumnName() { + return idColumnName; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/interceptor/AccessTokenInterceptor.java b/server/src/main/java/com/aisino/iles/core/interceptor/AccessTokenInterceptor.java new file mode 100644 index 0000000..a6104fd --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/interceptor/AccessTokenInterceptor.java @@ -0,0 +1,62 @@ +package com.aisino.iles.core.interceptor; + +import com.aisino.iles.common.util.Constants; +import com.smartlx.sso.client.model.AccessToken; +import com.smartlx.sso.client.model.RemoteUserInfo; +import com.smartlx.sso.client.service.SsoClientService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * Access Token 拦截器 + * 拦截请求中的access_token,通过SsoClientService.check验证, + * 并从RemoteUserInfo中提取yhwybs设置到request属性current_user_id中 + */ +@Slf4j +@RequiredArgsConstructor +public class AccessTokenInterceptor implements HandlerInterceptor { + private final SsoClientService ssoClientService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 从请求头中获取Authorization + String authorization = request.getHeader("Authorization"); + + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) { + try { + // 提取access_token + String token = authorization.substring(7); + + // 创建AccessToken对象 + AccessToken accessToken = new AccessToken(); + accessToken.setAccess_token(token); + + // 获取用户信息 + RemoteUserInfo userInfo = ssoClientService.getRemoteUserInfo(accessToken); + + if (userInfo != null && StringUtils.hasText(userInfo.getYhwybs())) { + // 将yhwybs设置到request属性current_user_id中 + request.setAttribute(Constants.CURRENT_USER_ID, userInfo.getYhwybs()); + log.debug("设置current_user_id: {}", userInfo.getYhwybs()); + return true; + } + log.warn("无效的access_token或用户信息不完整"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } catch (Exception e) { + log.error("验证access_token时发生错误", e); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + // 如果没有Authorization头或格式不正确,返回未授权状态 + log.warn("请求中缺少有效的Authorization头"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/interceptor/CorsInterceptor.java b/server/src/main/java/com/aisino/iles/core/interceptor/CorsInterceptor.java new file mode 100644 index 0000000..7dfa032 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/interceptor/CorsInterceptor.java @@ -0,0 +1,47 @@ +package com.aisino.iles.core.interceptor; + +import com.aisino.iles.common.iface.Logger; +import lombok.Setter; +import org.springframework.http.HttpHeaders; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Optional; + +/** + * 跨域请求拦截器 + */ + +@Setter +public class CorsInterceptor implements HandlerInterceptor , Logger { + private String accessControlAllowOrigin; + private String accessControlAllowHeaders; + private String accessControlAllowCredentials; + private String accessControlMaxAge; + private String accessControlAllowMethods; + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + logger().debug("----------------------------------"); + logger().debug(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN + ": " + accessControlAllowOrigin); + logger().debug(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS + ": " + accessControlAllowCredentials); + logger().debug(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS + ": " + accessControlAllowHeaders); + logger().debug(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS + ": " + accessControlAllowMethods); + logger().debug(HttpHeaders.ACCESS_CONTROL_MAX_AGE + ": " + accessControlMaxAge); + + if("*".equals(accessControlAllowOrigin)){ + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HttpHeaders.ORIGIN)); + + } else { + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, Optional.ofNullable(accessControlAllowOrigin).flatMap(allowOrigins -> Arrays.stream(allowOrigins.split(",")) + .filter(origin -> origin.equals(request.getHeader(HttpHeaders.ORIGIN))).findFirst()) + .orElse("")); + } + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, accessControlAllowCredentials); + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, accessControlAllowHeaders); + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, accessControlAllowMethods); + response.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, accessControlMaxAge); + return true; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/interceptor/EncryptRequestResponseFilter.java b/server/src/main/java/com/aisino/iles/core/interceptor/EncryptRequestResponseFilter.java new file mode 100644 index 0000000..0736081 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/interceptor/EncryptRequestResponseFilter.java @@ -0,0 +1,396 @@ +package com.aisino.iles.core.interceptor; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SmUtil; +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.model.PageResult; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.controller.GlobalExceptionController; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.service.DynamicEncryptService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.util.StreamUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 通用加密解密请求响应过滤器 + * (使用OncePerRequestFilter 是确保每个请求只触发一次) + * @author hx + * @since 2023-05-29 + */ +@Slf4j +public class EncryptRequestResponseFilter extends OncePerRequestFilter { + private final DynamicEncryptService dynamicEncryptService; + private final GlobalExceptionController globalExceptionController; + private final ObjectMapper objectMapper; + + public EncryptRequestResponseFilter(DynamicEncryptService dynamicEncryptService, + GlobalExceptionController globalExceptionController, + ObjectMapper objectMapper) { + this.dynamicEncryptService = dynamicEncryptService; + this.globalExceptionController = globalExceptionController; + this.objectMapper = objectMapper; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws IOException { + String encryptSignHeader = request.getHeader(Constants.Headers.ENCRYPT_SIGN); + HttpServletRequest req; + HttpServletResponse res; + try { + req = Optional.ofNullable(encryptSignHeader) + .filter(esh -> !request.getRequestURI().contains(Constants.NO_AUTH_API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM + "/dynamic-encrypt-key")) + .map(dynamicEncryptService::getDynamicEncryptKey) + .map(encryptKey -> (HttpServletRequest) new EncryptRequestWrapper(request, encryptKey)) + .orElse(request); + res = Optional.ofNullable(encryptSignHeader) + .filter(esh -> !request.getRequestURI().contains(Constants.NO_AUTH_API_PREFIX + Constants.ApiIndustryCategoryPrefixes.SYSTEM + "/dynamic-encrypt-key")) + .map(dynamicEncryptService::getDynamicEncryptKey) + .map(encryptKey -> (HttpServletResponse) new EncryptResponseWrapper(response, encryptKey, objectMapper)) + .orElse(response); + + filterChain.doFilter(req, res); + } catch (BusinessError e) { + Result result = globalExceptionController.businessErrorHandler(e); + responseErrorMsg(response, result); + + } catch (Exception e) { + Result result = globalExceptionController.serverErrorHandler(e); + responseErrorMsg(response, result); + } + } + + private void responseErrorMsg(HttpServletResponse response, Object result) throws IOException { + response.setStatus(200); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + + try (PrintWriter out = response.getWriter()) { + out.write(objectMapper.writeValueAsString(result)); + } + } + + /** + * 加密响应包装 + * + * @author hx + * @since 2023-05-29 + */ + public static class EncryptResponseWrapper extends HttpServletResponseWrapper { + private final HttpServletResponse response; + private final String encryptKey; + private final ObjectMapper objectMapper; + + /** + * Constructs a response adaptor wrapping the given response. + * + * @param response the {@link HttpServletResponse} to be wrapped. + * @param encryptKey 加密钥密 + * @param objectMapper jackson对象(序列化反序列化支持) + * @throws IllegalArgumentException if the response is null + */ + public EncryptResponseWrapper(HttpServletResponse response, String encryptKey, ObjectMapper objectMapper) { + super(response); + this.response = response; + this.encryptKey = encryptKey; + this.objectMapper = objectMapper; + } + + @Override + public PrintWriter getWriter() throws IOException { + return new PrintWriter(getOutputStream()); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (Optional.ofNullable(response.getContentType()).map(ct -> ct.contains(MediaType.APPLICATION_JSON_VALUE)).orElse(false) ) { + response.setHeader(Constants.Headers.ENCRYPTED_DATA, "true"); + return new BodyServletOutputStream(new ByteArrayOutputStream(), response, encryptKey, objectMapper); + } + return super.getOutputStream(); + } + + + + /** + * 加密响应输出流包装(只有用自己的输出流包装才可以支持传入字节输出流,方面对响应数据做修改) + * + * @author hx + * @since 2023-05-29 + */ + private static class BodyServletOutputStream extends ServletOutputStream { + private final ByteArrayOutputStream outputStream; + private final HttpServletResponse response; + private final String encryptKey; + private final ObjectMapper objectMapper; + + private BodyServletOutputStream(ByteArrayOutputStream outputStream, + HttpServletResponse response, + String encryptKey, + ObjectMapper objectMapper) { + this.outputStream = outputStream; + this.response = response; + this.encryptKey = encryptKey; + this.objectMapper = objectMapper; + } + + /** + * 是否准备好(为false不会开始write) + * @return 默认返回true + */ + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + + } + + @Override + public void write(int b) { + outputStream.write(b); + } + + @Override + public void write(@NonNull byte[] b, int off, int len) { + outputStream.write(b, off, len); + } + + /** + * 刷新,在bytearray准备好之后,刷新到浏览器 + * 这个版本的加密响应输出,只加密Result响应体的data数据部分 + * @throws IOException io异常 + */ + @Override + public void flush() throws IOException { + if (!response.isCommitted()) { + Result restResult = objectMapper.readValue(getBody(), PageResult.class); + if(Collection.class.isAssignableFrom(restResult.getData().getClass())) { + Collection data = (Collection) restResult.getData(); + List encryptedData = data.stream().map(o -> { + try { + return (Object)SmUtil.sm4(HexUtil.decodeHex(encryptKey)).encryptHex(objectMapper.writeValueAsBytes(o)); + } catch (JsonProcessingException e) { + throw new BusinessError("加密响应中数据列表序列化错误", e); + } + }).collect(Collectors.toList()); + restResult.setData(encryptedData); + } else { + try { + restResult.setData(SmUtil.sm4(HexUtil.decodeHex(encryptKey)).encryptHex(objectMapper.writeValueAsBytes(restResult.getData()))); + } catch (JsonProcessingException e) { + throw new BusinessError("加密响应中数据序列化错误", e); + } + } + + response.getOutputStream().write(objectMapper.writeValueAsBytes(restResult)); + response.getOutputStream().flush(); + } + } + + @Override + public void write(@NonNull byte[] b) throws IOException { + super.write(b); + } + + /** + * 获取响应体数据 + * @return 响应体数据 + */ + public byte[] getBody() { + return outputStream.toByteArray(); + } + } + + } + + /** + * 加密请求包装 + * + * @author hx + * @since 2023-05-29 + */ + public static class EncryptRequestWrapper extends HttpServletRequestWrapper { + private final String encryptKey; + private final HttpServletRequest request; + + /** + * Constructs a request object wrapping the given request. + * + * @param request the {@link HttpServletRequest} to be wrapped. + * @param encryptKey 加密密钥 + * @throws IllegalArgumentException if the request is null + */ + public EncryptRequestWrapper(HttpServletRequest request, String encryptKey) { + super(request); + this.request = request; + this.encryptKey = encryptKey; + } + + /** + * 加密请求输入流包装 + * @return 服务输入量 + * @throws IOException io异常 + */ + @Override + public ServletInputStream getInputStream() throws IOException { + byte[] body; + if (this.request.getHeader(HttpHeaders.CONTENT_TYPE).contains(MediaType.APPLICATION_JSON_VALUE)) { + String bodyStr = StreamUtils.copyToString(this.request.getInputStream(), StandardCharsets.UTF_8); + try { + body = SmUtil.sm4(HexUtil.decodeHex(encryptKey)).decrypt(bodyStr); + } catch (CryptoException e) { + log.debug("error encryptKey is {}", encryptKey); + throw new BusinessError("通用加密解密解密失败", Constants.Exceptions.data_encrypt_decrypt_error, e); + } + return new BodyServletInputStream(new ByteArrayInputStream(body)); + } + return super.getInputStream(); + } + + /** + * 获取参数,获取参数的加密串后使用sm4解密 + * @param name a String 参数名称 + * @return 解密后的参数值 + */ + @Override + public String getParameter(String name) { + return Optional.ofNullable(super.getParameter(name)) + .filter(StringUtils::isNotEmpty) + .map(value -> { + try { + return SmUtil.sm4(HexUtil.decodeHex(encryptKey)).decryptStr(value); + } catch (CryptoException e) { + throw new BusinessError("通用加密解密解密失败", Constants.Exceptions.data_encrypt_decrypt_error, e); + } + }) + .orElse(null); + } + + /** + * 获取参数(数组,多个值),获取参数的加密串后使用sm4解密取 + * @param name a String 参数名称 + * @return 解密后的参数值数组 + */ + @Override + public String[] getParameterValues(String name) { + return Optional.ofNullable(super.getParameterValues(name)) + .map(values -> Arrays.stream(values) + .filter(StringUtils::isNotEmpty) + .map(value -> { + try { + return SmUtil.sm4(HexUtil.decodeHex(encryptKey)).decryptStr(value); + } catch (CryptoException e) { + throw new BusinessError("通用加密解密解密失败", Constants.Exceptions.data_encrypt_decrypt_error, e); + } + }).toArray(String[]::new)).orElse(null); + } + + /** + * 加密请求输入流包装 (只有用自己的输入流包装才可以支持从外部传入字节输入流,方便我们修改数据) + * + * @author hx + * @since 2023-05-29 + */ + private static class BodyServletInputStream extends ServletInputStream { + private final ByteArrayInputStream bodyInputStream; + + private BodyServletInputStream(ByteArrayInputStream bodyInputStream) { + this.bodyInputStream = bodyInputStream; + } + + /** + * 是否完成,如果这里返回true,那么他就不会在被读取了 + * @return 默认返回false + */ + @Override + public boolean isFinished() { + return false; + } + + /** + * 是否准备好 + * @return 默认返回true + */ + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() { + return bodyInputStream.read(); + } + + @Override + public int read(@NonNull byte[] b) throws IOException { + return bodyInputStream.read(b); + } + + @Override + public int read(@NonNull byte[] b, int off, int len) { + return bodyInputStream.read(b, off, len); + } + + @Override + public long skip(long n) { + return bodyInputStream.skip(n); + } + + @Override + public int available() { + return bodyInputStream.available(); + } + + @Override + public void close() throws IOException { + bodyInputStream.close(); + } + + @Override + public synchronized void mark(int readlimit) { + bodyInputStream.mark(readlimit); + } + + @Override + public synchronized void reset() { + bodyInputStream.reset(); + } + + @Override + public boolean markSupported() { + return bodyInputStream.markSupported(); + } + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/interceptor/JsonTokenValidatorInterceptor.java b/server/src/main/java/com/aisino/iles/core/interceptor/JsonTokenValidatorInterceptor.java new file mode 100644 index 0000000..0aa59ee --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/interceptor/JsonTokenValidatorInterceptor.java @@ -0,0 +1,108 @@ +package com.aisino.iles.core.interceptor; + +import cn.hutool.core.util.StrUtil; +import com.aisino.iles.common.model.Result; +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.exception.TokenError; +import com.aisino.iles.core.model.Token; +import com.aisino.iles.core.model.enums.UserStatus; +import com.aisino.iles.core.repository.ResourceRepo; +import com.aisino.iles.core.repository.UserRepo; +import com.aisino.iles.core.util.PermissionUtils; +import com.aisino.iles.core.util.TokenUtil; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.Objects; + +import static com.aisino.iles.common.util.Constants.Headers.AUTH_TOKEN_MOBILE; + +/** + * 令牌验证拦截器 + * + * @author huxin + * @since 2020-08-21 + */ +@Slf4j +public class JsonTokenValidatorInterceptor implements HandlerInterceptor { + private final ResourceRepo resourceRepo; + private final UserRepo userRepo; + + public JsonTokenValidatorInterceptor(ResourceRepo resourceRepo, + UserRepo userRepo) { + this.resourceRepo = resourceRepo; + this.userRepo = userRepo; + } + + @Override + public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object o) throws Exception { + boolean tokenValidateResult; + String token = request.getHeader(Constants.Headers.AUTH_TOKEN); + String mToken = request.getHeader(AUTH_TOKEN_MOBILE); + + if (request.getMethod().equalsIgnoreCase("options")) { + return true; + } + try { + if (StrUtil.isBlank(token)) { + throw new TokenError("令牌为空,请登录获取,或者把已有的令牌加到请求头参数里"); + } + + tokenValidateResult = TokenUtil.validateToken(token); + if (!tokenValidateResult) { + throw new TokenError(Constants.Exceptions.token_expired_error, "令牌不正确,可能已经过期了,尝试重新登录"); + } + + // 需要判断一下用户信息 + Token tokenEntity = TokenUtil.parseToken(token); + assert tokenEntity != null; + userRepo.findById(tokenEntity.getUid()) + .map(u -> { + if (u.getStatus() == UserStatus.deleted) + throw new TokenError("该用户已经被删除,令牌验证失败"); + else if (u.getStatus() == UserStatus.lock) + throw new TokenError("该用户是锁定状态, 令牌验证失败"); + else if (u.getStatus() == UserStatus.unnormal) + throw new TokenError("该用户处于某种异常状态, 令牌验证失败"); + return u; + }) + .orElseThrow(() -> new TokenError("令牌验证失败:该uid的用户不存在")); + //用户所使用的资源操作权限判断 + tokenValidateResult = PermissionUtils.checkHasResourcePermission(token, request.getRequestURI(), request.getMethod(), resourceRepo); + if (!tokenValidateResult) { + throw new TokenError(Constants.Exceptions.token_no_resource_error, "该令牌不具有该资源的操作权限"); + } + } catch (BusinessError e) { + tokenValidateResult = false; + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + Result result = new Result<>(); + result.setMsg(e.getMessage()); + if (log.isDebugEnabled()) { + result.setException(e); + } + result.setSuccess(false); + result.setCode(e.getCode()); + + ServletOutputStream out = response.getOutputStream(); + ObjectMapper mapper = new ObjectMapper(); + JsonGenerator jsonGenerator = mapper.getFactory().createGenerator(out); + jsonGenerator.writeObject(result); + out.close(); + } + +// 向通过认证的请求的属性添加当前用户的id信息 + if (tokenValidateResult) + request.setAttribute(Constants.CURRENT_USER_ID, Objects.requireNonNull(TokenUtil.parseToken(token)).getUid()); + return tokenValidateResult; + } + + +} diff --git a/server/src/main/java/com/aisino/iles/core/interceptor/JurisdictionListener.java b/server/src/main/java/com/aisino/iles/core/interceptor/JurisdictionListener.java new file mode 100644 index 0000000..f33ca75 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/interceptor/JurisdictionListener.java @@ -0,0 +1,38 @@ +package com.aisino.iles.core.interceptor; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.Jurisdiction; +import com.aisino.iles.core.util.RedisUtil; +import lombok.extern.slf4j.Slf4j; + +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostRemove; +import jakarta.persistence.PostUpdate; +import java.time.Duration; + +/** + * 机构缓存监听,当业务程序里对机构信息数据修改,删除,新增调整,那么就会触发机构缓存的监听对缓存的机构信息进行删除 + * @author hx + * @since 2023-06-12 + */ +@Slf4j +public class JurisdictionListener { + + @PostUpdate + @PostPersist + @PostRemove + public void save(Jurisdiction jurisdiction) { + log.info("存在改动机构信息删除机构缓存"); + removeAllCache(); + } + + /** + * 删除全部缓存的机构信息 + */ + public void removeAllCache() { + if (RedisUtil.hasKey(Constants.CacheKeys.Jurisdiction.all)) { + RedisUtil.expire(Constants.CacheKeys.Jurisdiction.all, Duration.ZERO); + } + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/json/mixins/FunctionMixin.java b/server/src/main/java/com/aisino/iles/core/json/mixins/FunctionMixin.java new file mode 100644 index 0000000..4841739 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/json/mixins/FunctionMixin.java @@ -0,0 +1,32 @@ +package com.aisino.iles.core.json.mixins; + +import com.aisino.iles.core.model.Function; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIdentityReference; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.io.IOException; + +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "funcCode", scope = Function.class) +@JsonIdentityReference(alwaysAsId = true) +@JsonDeserialize(using = FunctionMixin.FunctionTokenDeserializer.class) +public class FunctionMixin { + public static class FunctionTokenDeserializer extends JsonDeserializer { + @Override + public Function deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.hasTextCharacters()) { + return Function.builder().funcCode(p.getText()).build(); + } else { + p.nextToken(); + p.nextValue(); + Function func = Function.builder().funcCode(p.getText()).build(); + p.nextToken(); + return func; + } + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/json/mixins/MenuMixin.java b/server/src/main/java/com/aisino/iles/core/json/mixins/MenuMixin.java new file mode 100644 index 0000000..3268ff9 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/json/mixins/MenuMixin.java @@ -0,0 +1,34 @@ +package com.aisino.iles.core.json.mixins; + +import com.aisino.iles.core.model.Menu; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIdentityReference; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.io.IOException; + +@JsonIgnoreProperties(value = {"iconCls", "leaf"}, ignoreUnknown = true) +@JsonIdentityInfo(property = "menuId", generator = ObjectIdGenerators.PropertyGenerator.class, scope = Menu.class) +@JsonIdentityReference(alwaysAsId = true) +@JsonDeserialize(using = MenuMixin.MenuTokenDeserializer.class) +public class MenuMixin { + public static class MenuTokenDeserializer extends JsonDeserializer { + @Override + public Menu deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.hasTextCharacters()) { + return Menu.builder().menuId(p.getText()).build(); + } else { + p.nextToken(); + p.nextValue(); + Menu menu = Menu.builder().menuId(p.getText()).build(); + p.nextToken(); + return menu; + } + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/json/mixins/ResourceMixin.java b/server/src/main/java/com/aisino/iles/core/json/mixins/ResourceMixin.java new file mode 100644 index 0000000..3c2f88b --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/json/mixins/ResourceMixin.java @@ -0,0 +1,32 @@ +package com.aisino.iles.core.json.mixins; + +import com.aisino.iles.core.model.Resource; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIdentityReference; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.io.IOException; + +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "resourceId", scope = Resource.class) +@JsonIdentityReference(alwaysAsId = true) +@JsonDeserialize(using = ResourceMixin.ResourceTokenDeserializer.class) +public class ResourceMixin { + public static class ResourceTokenDeserializer extends JsonDeserializer { + + @Override + public Resource deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.hasTextCharacters()) { + return Resource.builder().resourceId(p.getText()).build(); + } else { + JsonNode node = p.getCodec().readTree(p); + String resourceId = node.get("resourceId").textValue(); + return Resource.builder().resourceId(resourceId).build(); + } + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/AddrInfo.java b/server/src/main/java/com/aisino/iles/core/model/AddrInfo.java new file mode 100644 index 0000000..7c5462c --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/AddrInfo.java @@ -0,0 +1,82 @@ +package com.aisino.iles.core.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "sys_fwjbxx", + indexes = @Index(name = "idx_fw_upd", columnList = "updateTime")) +@EqualsAndHashCode() +@ToString() +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AddrInfo { + + @Id + @Column(length = 100) + @Comment("房屋编码") + private String fwbm; + + @Column(length = 16) + @Comment("行政区划编码") + private String xzqh; + + @Column(length = 100) + @Comment("街路号") + private String jlh; + + @Column(length = 10) + @Comment("门楼牌号") + private String mlph; + + @Column(length = 10) + @Comment("一级门楼牌号") + private String yjmlph; + + @Column(length = 10) + @Comment("二级门楼牌号") + private String ejmlph; + + @Column(length = 10) + @Comment("三级门楼牌号") + private String sjmlph; + + @Column(length = 10) + @Comment("室号") + private String sh; + + @Column(length = 10) + @Comment("楼层号") + private String szc; + + @Column(length = 100) + @Comment("详情") + private String xxdz; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @Comment("更新时间") + private LocalDateTime updateTime; + + @Column(length = 100) + @Comment("(政府规范)房屋编码") + private String FWDZBM; + + @Column(length = 200) + @Comment("纬度") + private String x; + + @Column(length = 200) + @Comment("经度") + private String y; + + @Column(length = 30) + @Comment("政府社区") + private String zfsq; + +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Audiable.java b/server/src/main/java/com/aisino/iles/core/model/Audiable.java new file mode 100644 index 0000000..354602c --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Audiable.java @@ -0,0 +1,39 @@ +package com.aisino.iles.core.model; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Comment; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +/** + * 审计基类类 + * + * @param + */ +@EqualsAndHashCode(callSuper = true) +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Data +public abstract class Audiable extends BaseModel { + @CreatedBy + @Comment("创建人") + private U createBy; + @CreatedDate + @Comment("创建时间") + private LocalDateTime createTime; + @LastModifiedBy + @Comment("修改人") + private U lastModifiedBy; + @LastModifiedDate + @Comment("修改时间") + private LocalDateTime lastModifiedTime; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/BaseModel.java b/server/src/main/java/com/aisino/iles/core/model/BaseModel.java new file mode 100644 index 0000000..f0eb13e --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/BaseModel.java @@ -0,0 +1,37 @@ +package com.aisino.iles.core.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.Transient; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Data +@EqualsAndHashCode(of = "clientId") +public class BaseModel implements Serializable { + /** + * 客户端ID 或者说 临时ID + */ + @Transient + private String clientId; + + /** + * 是否需要报警 + */ +// @Transient +// private boolean needAlarm = false; + public boolean isNeedAlarm() { + return false; + } + + /** + * 是否需要预警 + */ +// @Transient +// private boolean needEarlyWarning = false; + public boolean isNeedEarlyWarning() { + return false; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/BaseQuery.java b/server/src/main/java/com/aisino/iles/core/model/BaseQuery.java new file mode 100644 index 0000000..5508c2e --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/BaseQuery.java @@ -0,0 +1,57 @@ +package com.aisino.iles.core.model; + +import lombok.Setter; + +/** + * 公共查询实体 + * @author hx + * @since 2023-11-29 + */ +@Setter +public class BaseQuery implements PageQueryable { + private Integer page; + private Integer pagesize; + private long total; + private String sort; + private String dir; + private boolean limit = false; + + @Override + public Integer page() { + return page; + } + + @Override + public Integer pageSize() { + return pagesize; + } + + @Override + public long total() { + return total; + } + + @Override + public String sort() { + return sort; + } + + @Override + public String dir() { + return dir; + } + + @Override + public boolean limit() { + return limit; + } + + // 兼容旧接口 + public void setPageSize(Integer pageSize) { + this.pagesize = pageSize; + } + + public void setPagesize(Integer pageSize) { + this.pagesize = pageSize; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Dict.java b/server/src/main/java/com/aisino/iles/core/model/Dict.java new file mode 100644 index 0000000..6ea19cc --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Dict.java @@ -0,0 +1,74 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.enums.DictType; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; +import org.hibernate.validator.constraints.Length; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * 字典 + */ +@Entity +@Table(name = "sys_dict") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "Dict") +@Data +@ToString(exclude = {"dictItems"}) +@EqualsAndHashCode(of = "dictId", callSuper = false) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Dict extends Audiable { + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + + @Column(length = 36) + @Comment("字典主键") + private String dictId; + @Column(nullable = false, length = 100) + @Comment("字典名称") + @NotEmpty(message = "字典名称不能为空") + @Length(max = 100, message = "字典名称最大长度不超过100") + private String dictName; + @Column(length = 50, nullable = false, unique = true) + @Comment("字典代码") + @NotEmpty(message = "字典代码不能为空") + @Length(max = 50, message = "字典代码最大长度不超过50") + private String dictCode; + @Column(length = 200) + @Comment("字典描述") + @Length(max = 200, message = "字典描述最大长度不超过200") + private String description; + @Column(length = 20) + @Comment("字典名称简拼") + @Length(max = 20, message = "字典名称简拼最大长度不超过20") + private String simplePinyin; + @Comment("字典名称全拼") + @Length(max = 255, message = "字典名称全码最大长度不超过255") + private String allPinyin; + @Comment("字典类型 01 简单 02 树形") + @NotNull(message = "字典类型不能为空") + private DictType dictType; + @Transient + private String dictTypeName; + @OneToMany(mappedBy = "dict", cascade = {CascadeType.MERGE, CascadeType.REMOVE, CascadeType.PERSIST, CascadeType.REFRESH}, orphanRemoval = true) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "DictItem") + @OrderBy(" orderNum asc") + @Builder.Default + private Set dictItems = new LinkedHashSet<>(); + + @Version + @LastModifiedDate + private LocalDateTime lastModifiedTime; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/DictItem.java b/server/src/main/java/com/aisino/iles/core/model/DictItem.java new file mode 100644 index 0000000..4d42fe4 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/DictItem.java @@ -0,0 +1,115 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.enums.DataMode; +import jakarta.persistence.*; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.Index; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.hibernate.annotations.*; +import org.hibernate.annotations.Cache; +import org.hibernate.validator.constraints.Length; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * 字典项 + */ +// 数据映射 +@Entity +@Table(name = "sys_dict_item",indexes = { + @Index(name = Constants.Indexes.idx_dictItem_value,columnList = "value") +}) +@DynamicUpdate +@DynamicInsert +@Cacheable +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "Dict") +@NamedEntityGraphs({ + @NamedEntityGraph( + name = "dict-item-list", + attributeNodes = { + @NamedAttributeNode("dict"), + @NamedAttributeNode("parent") + } + ) +}) +// lombok +@Data +@EqualsAndHashCode(of = "dictItemId", callSuper = false) +@NoArgsConstructor +@AllArgsConstructor +public class DictItem extends Audiable { + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + + @Column(length = 36) + @Comment("字典项主键") + private String dictItemId; + @NotEmpty(message = "字典项值不能为空") + @Length(max = 255, message = "字典项值最大长度不超过255") + @Column(nullable = false) + @Comment("字典项值") + private String value; + @NotEmpty(message = "字典项名称不能为空") + @Length(max = 255, message = "字典项名称最大长度不超过255") + @Column(nullable = false) + @Comment("字典项名称") + private String display; + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(nullable = false) + @Comment("排序号") + private Integer orderNum = 0; + @Length(max = 100, message = "字典项名称简拼最大长度不超过100") + @Column(length = 100) + @Comment("字典项名称简拼") + private String simplePinyin; + @Length(max = 255, message = "字典项名称全拼最大长度不超过255") + @Comment("字典项名称全拼") + private String allPinyin; + @NotNull(message = "所属字典不能为空") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "dict_id", nullable = false) + @Comment("所属字典主键") + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "Dict") + @ToString.Exclude + private Dict dict; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id", foreignKey = @ForeignKey(name="none", value = ConstraintMode.NO_CONSTRAINT)) + @Comment("上级字典项主键") + @ToString.Exclude + private DictItem parent; + @OneToMany(mappedBy = "parent") + @OrderBy(" orderNum asc") + @ToString.Exclude + private Set children = new LinkedHashSet<>(); + @Transient + @Enumerated + private DataMode dataFlag = DataMode.nothing; + @LastModifiedDate + @Version + private LocalDateTime lastModifiedTime; + + public DictItem(String value, String display, String simplePinyin, String allPinyin, Dict dict, DictItem parent) { + this.value = value; + this.display = display; + this.simplePinyin = simplePinyin; + this.allPinyin = allPinyin; + this.dict = dict; + this.parent = parent; + } + + public DictItem(String value, String display, LocalDateTime lastModifiedTime) { + this.value = value; + this.display = display; + this.lastModifiedTime = lastModifiedTime; + } + + +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Function.java b/server/src/main/java/com/aisino/iles/core/model/Function.java new file mode 100644 index 0000000..07af0c3 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Function.java @@ -0,0 +1,33 @@ +package com.aisino.iles.core.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; + +@Entity +@Table(name = "sys_function") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "Function") +@Data +@EqualsAndHashCode(of = {"funcCode"}, callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Function extends BaseModel { + @Id + @Comment("功能代码主键") + @NotEmpty(message = "功能代码不能为空") + private String funcCode; + @NotEmpty(message = "功能名称不能为空") + @Size(max = 100, message = "功能名称最大长度不超过100") + @Column(length = 100, nullable = false) + @Comment("功能名称") + private String funcName; + @Size(max = 400, message = "功能描述最大长度不超过400") + @Column(length = 400) + @Comment("功能描述") + private String description; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/GenerateCode.java b/server/src/main/java/com/aisino/iles/core/model/GenerateCode.java new file mode 100644 index 0000000..5c7a1a4 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/GenerateCode.java @@ -0,0 +1,81 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; + +import jakarta.persistence.*; +import java.io.Serializable; + +/** + * 生成编码信息 + * + * @author huxin + * @since 2020-06-11 + */ +@Entity +@Table(name = "t_scbm") +@Comment("生成编码信息表") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@DynamicUpdate +public class GenerateCode implements Serializable { + /** + * 编码分类 + */ + @Column(name = "bmfl", length = 10, nullable = false) + @Comment("编码分类") + private String codeCategory; + /** + * 行政区划代码 / 编码前缀 + */ + @Column(name = "xzqh", length = 30) + @Comment("行政区划代码 / 编码前缀") + private String codePrefix; + /** + * 日期年份 + */ + @Column(name = "rqnf", length = 20) + @Comment("日期年份") + private String timeParameter; + /** + * 流水号 + */ + @Column(name = "lsh", length = 10) + @Comment("流水号") + private String serialNumber; + /** + * 生成规则 0年;1年月;2日期;3没有年份;4两位年份 + */ + @Column(name = "scgz", length = 1) + @Comment("生成规则 0年;1年月;2日期;3没有年份;4两位年份") + private Integer generateRule; + /** + * 省份简拼 + */ + @Column(name = "sssf", length = 10) + @Comment("省份简拼") + private String provinceSimplePinyin; + /** + * 生成的编码 + */ + @Column(name = "scbm", length = 100) + @Comment("生成的编码") + private String code; + /** + * 主键 , 原主键类型是数字,现在改为uuid + */ + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + @Column(name = "scbmid", length = 36) + @Comment("主键") + private String id; + @Version + private long version; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/GlobalParam.java b/server/src/main/java/com/aisino/iles/core/model/GlobalParam.java new file mode 100644 index 0000000..f8d6d22 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/GlobalParam.java @@ -0,0 +1,49 @@ +package com.aisino.iles.core.model; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; +import org.hibernate.validator.constraints.Length; + +@Entity +@Table(name = "sys_globalpar") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = {"globalParamCode"}, callSuper = true) +@Cacheable +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "GlobalParam") +public class GlobalParam extends BaseModel { + /** + * 全局参数代码 主键 + */ + @Id + @Comment("全局参数主键") + @NotEmpty(message = "全局参数代码不能为空") + @Pattern(regexp = "^[A-z0-9_*!@#$%^&()\\[\\];'\"<>]*$") + @Size(max = 255, message = "全局参数代码最大长度不超过255") + private String globalParamCode; + /** + * 全局参数名称 + */ + @Comment("全局参数名称") + @Length(max = 255, message = "全局参数名称最大长度不超过255") + private String globalParamName; + /** + * 全局参数值 + */ + @Comment("全局参数值") + @Length(max = 255, message = "全局参数值最大长度不超过255") + private String globalParamValue; + + +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Jurisdiction.java b/server/src/main/java/com/aisino/iles/core/model/Jurisdiction.java new file mode 100644 index 0000000..6cddc06 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Jurisdiction.java @@ -0,0 +1,118 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.*; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.validator.constraints.Length; + +import java.util.HashSet; +import java.util.Set; + +/** + * 管辖机构 + */ +@Entity +@Table(name = "sys_jurisdiction") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "Jurisdiction") +@NamedEntityGraphs({ + @NamedEntityGraph( + name="jurisdiction-list", + attributeNodes = { + @NamedAttributeNode("parent") + } + ) +}) +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@EqualsAndHashCode(of = {"jurisdictionId"}, callSuper = false) +@Builder +@AllArgsConstructor +@DynamicUpdate +//@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "jurisdictionId", scope = Jurisdiction.class) +//@EntityListeners(JurisdictionListener.class) +public class Jurisdiction extends Audiable { + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + @Column(length = 36) + @Comment("机构信息主键") + private String jurisdictionId; + @NotEmpty(message = "机构代码不能为空") + @Pattern(regexp = "[A-z0-9]*", message = "机构代码只能是字母和数字") + @Length(max = 20, message = "机构代码最大长度不超过20") + @Column(length = 20, nullable = false, unique = true) + @Comment("机构编码 例如 500103000000") + private String jurisdictionCode; + @NotEmpty(message = "机构名称不能为空") + @Length(max = 255, message = "机构名称最大长度不超过255") + @Column(nullable = false) + @Comment("机构名称") + private String jurisdictionName; +// @Length(max = 50, message = "机构名称简称最大长度不超过50") +// @Column(length = 50, nullable = false) +// @Comment("机构名称简称") +// private String jurisSimpleName; + @NotNull(message = "机构级别不能为空") + @Max(value = 10, message = "机构级别最大值不超过10") + @Column(nullable = false, length = 10) + @Comment("机构级别") + @Builder.Default + private Integer jurisdictionLevel = 1; + @GeneratedValue(strategy = GenerationType.AUTO) + @Builder.Default + @Comment("排序号") + private Integer orderNum = 0; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_juris_id") + @Comment("上级机构主键") + @ToString.Exclude + private Jurisdiction parent; + @Length(max = 255, message = "机构代码全码(包含父级代码)最大长度不超过255") + @Comment("机构信息全码 例如500000000000.500103000000.") + private String jurisFullCode; + @Length(max = 20, message = "机构代码简码(去偶数0)最大长度不超过20") + @Column(length = 20, nullable = false) + @Comment("机构信息简码 例如 500103") + private String jurisSimpleCode; + @Builder.Default + @Comment("是否为最下级") + private Boolean leaf = true; +// @Length(max = 100, message = "机构名称简码最大长度不超过100") +// @Column(length = 100) +// @Comment("机构名称简拼") +// private String simplePinyin; +// @Length(max = 255, message = "机构名称全码最大长度不超过255") +// @Comment("机构名称全拼") +// private String allPinyin; +// @Length(max = 12, message = "公安部代码最大不超过12") +// @Pattern(regexp = "[A-z0-9]*", message = "公安部代码只能是字母和数字") +// @Column(length = 12) +// @Comment("公安部代码") +// private String gabdm; +// @Length(max = 50, message = "公安部名称最大不超过50") +// @Column(length = 50) +// @Comment("公安部名称") +// private String gabmc; + + @ManyToMany(mappedBy = "jurisdictions") + @JsonIgnore + @Builder.Default + @ToString.Exclude + private Set users = new HashSet<>(); + + + @OneToMany(mappedBy = "parent") + @Builder.Default + @ToString.Exclude + private Set children = new HashSet<>(); +} diff --git a/server/src/main/java/com/aisino/iles/core/model/LoginLog.java b/server/src/main/java/com/aisino/iles/core/model/LoginLog.java new file mode 100644 index 0000000..05596ec --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/LoginLog.java @@ -0,0 +1,53 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.enums.LoginLogType; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; + +import java.time.LocalDateTime; + +/** + * 登录日志 + */ +@Entity +@Table(name = "sys_login_log", indexes = {@Index(name = "idx_lglog_sid", columnList = "sessionId")}) +@Data +@EqualsAndHashCode(of = {"loginLogId"}) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoginLog { + + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + @Column(length = 36) + @Comment("登录日志主键") + private String loginLogId; + @Comment("登录用户名") + private String username; + @Column(nullable = false) + @Comment("登录时间") + private LocalDateTime operateTime; + @Column(length = 2, nullable = false) + @Comment("登录日志类型 01 登录 02 登出") + private LoginLogType loginLogType; + @Column(length = 128) + @Comment("登录IP地址") + private String ipAddress; + /** + * 操作结果代码 0 为正常, 非0为错误代码 参考 Constants.Exceptions + */ + @Comment("操作结果代码 0 为正常, 非0为错误代码 参考 Constants.Exceptions") + private Integer resultCode; + /** + * 回话id ,使用token的签名 + */ + @Column(length = 50) + @Comment("会话id") + private String sessionId; + + @Comment("退出时间") + private LocalDateTime exited; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Menu.java b/server/src/main/java/com/aisino/iles/core/model/Menu.java new file mode 100644 index 0000000..5da6a84 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Menu.java @@ -0,0 +1,132 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.converter.persistence.IconClsStringArray2VarcharConverter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotEmpty; +import lombok.*; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Comment; +import org.hibernate.validator.constraints.Length; + +import java.util.LinkedHashSet; +import java.util.Set; + +@Entity +@Table(name = "sys_menu") +@Cacheable +@org.hibernate.annotations.Cache(region = "Menu", usage = CacheConcurrencyStrategy.READ_WRITE) +@NamedEntityGraphs({ + @NamedEntityGraph( + name = "menus-tree", + attributeNodes = { + @NamedAttributeNode("parent") + } + ) +}) +@Data +@EqualsAndHashCode(of = "menuId", callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString(exclude = {"parent", "children"}) +@JsonIgnoreProperties(ignoreUnknown = true, value = {"hibernateLazyInitializer", "handler", "fieldHandler"}) +public class Menu extends BaseModel { + /** + * 菜单ID 主键 + */ + @NotEmpty(message = "菜单ID不能为空", groups = {Add.class}) + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + + @Column(length = 36) + @Comment("菜单主键") + private String menuId; + /** + * 菜单代码 业务唯一标志 + */ + @Column(length = 100, nullable = false, unique = true) + @Comment("菜单代码") + @NotEmpty(message = "菜单代码不能为空", groups = {Add.class, Modify.class}) + @Length(max = 100, message = "菜单名称最大长度不超过100", groups = {Add.class, Modify.class}) + private String menuCode; + /** + * 菜单名称 + */ + @Column(nullable = false) + @Comment("菜单名称") + @NotEmpty(message = "菜单名称不能为空", groups = {Add.class, Modify.class}) + @Length(max = 255, message = "菜单名称最大长度不超过255", groups = {Add.class, Modify.class}) + private String menuName; + /** + * 菜单图标,字符图标类型 + */ + @Length(max = 20, message = "菜单图标类最大长度不超过20", groups = {Add.class, Modify.class}) + @Column(length = 20) + @Comment("菜单图标,字符图标") + @Convert(converter = IconClsStringArray2VarcharConverter.class) + private String[] iconCls; + /** + * 菜单图标,图片路径 + */ + @Comment("菜单图标,图片类型(地址)") + @Length(max = 255, message = "菜单图标路径最大长度不超过255", groups = {Add.class, Modify.class}) + private String iconPath; + /** + * 菜单路径,用来描述菜单层级关系 + */ + @Length(max = 400, message = "菜单路径最大长度不超过400", groups = {Add.class, Modify.class}) + @Column(length = 400, unique = true) + private String path; + /** + * 排序号 + */ + @Max(value = 1000, message = "排序号最大值不超过1000") + @Comment("排序号") + @ColumnDefault("0") + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Builder.Default + private Integer orderNum = 0; + /** + * 是否为最下级菜单 + */ + @Builder.Default + private Boolean leaf = true; + /** + * 菜单名称简拼 + */ + @Column(length = 40) + @Comment("菜单名称简拼") + @Length(max = 40, message = "菜单名称简拼最大长度不超过40", groups = {Add.class, Modify.class}) + private String simplePinyin; + /** + * 菜单名称全拼 + */ + @Length(max = 255, message = "菜单名称全拼最大长度不超过255", groups = {Add.class, Modify.class}) + @Comment("菜单名称全拼") + private String allPinyin; + /** + * 上级菜单(通过上级菜单ID关联) + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_menu_id") + @Comment("上级菜单主键") + private Menu parent; + /** + * 下级菜单 + */ + @OneToMany(mappedBy = "parent") + @BatchSize(size = 10) + @Builder.Default + private Set children = new LinkedHashSet<>(); + + public interface Add { + } + + public interface Modify { + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/OperateLog.java b/server/src/main/java/com/aisino/iles/core/model/OperateLog.java new file mode 100644 index 0000000..16669de --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/OperateLog.java @@ -0,0 +1,123 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.enums.IpType; +import com.aisino.iles.core.model.enums.OperateStatus; +import com.aisino.iles.core.model.enums.OperateType; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import java.time.LocalDateTime; + +/** + * 操作日志 + */ + +@Entity +@Table(name = "sys_operate_log") +@DynamicInsert +@DynamicUpdate +@NamedEntityGraphs({ + @NamedEntityGraph( + name = "operate-log-detail", + attributeNodes = { + @NamedAttributeNode("args"), + @NamedAttributeNode("error") + } + ) +}) +@Data +@EqualsAndHashCode(callSuper = false, of = "operateLogId") +@Builder +@ToString(exclude = {"error", "args"}) +@NoArgsConstructor +@AllArgsConstructor +public class OperateLog extends Audiable { + /** + * 操作日志ID 主键 + */ + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + @Column(length = 36) + @Comment("操作日志ID 主键") + private String operateLogId; + /** + * 操作类型 对应增删改查的枚举类型 + */ + @Enumerated(EnumType.STRING) + @Column(length = 6, nullable = false) + @Comment("操作类型 对应增删改查的枚举类型") + private OperateType operateType; + /** + * 操作函数/方法全名格式为 包.类.方法 + */ + @Column(nullable = false) + @Comment("操作函数/方法全名格式为 包.类.方法") + private String method; + /** + * 操作方法使用的参数,格式为JSON + */ + @ManyToOne(fetch = FetchType.LAZY,cascade = CascadeType.ALL) + @JoinColumn(name = "args_id") + @Comment("操作方法使用的参数,格式为JSON") + private OperateLogText args; + /** + * 操作描述 + */ + @Column(length = 1000) + @Comment("操作描述") + private String description; + /** + * 操作执行者的ip + */ + @Column(length = 30) + @Comment("操作执行者的ip") + private String ip; + /** + * ip类型 + */ + @Column(length = 2, nullable = false) + @Comment("ip类型") + @Enumerated(EnumType.STRING) + private IpType ipType; + /** + * 操作名称 + */ + @Column(length = 100, nullable = false) + @Comment("操作名称") + private String name; + /** + * 执行开始时间 + */ + @Column(nullable = false) + @Comment("执行开始时间") + private LocalDateTime executionBeginTime; + /** + * 执行结束时间 + */ + @Column(nullable = false) + @Comment("执行结束时间") + private LocalDateTime executionEndTime; + /** + * 操作状态 0 成功 1 失败 + */ + @Column(nullable = false, length = 1) + @Comment("操作状态 0 成功 1 失败") + private OperateStatus status; + + /** + * 错误信息 + */ + @ManyToOne(fetch = FetchType.LAZY,cascade = CascadeType.ALL) + @JoinColumn(name = "error_id") + @Comment("错误信息") + private OperateLogText error; + + @Column(length = 64) + @Comment("用户身份证号") + private String idNum; +} + diff --git a/server/src/main/java/com/aisino/iles/core/model/OperateLogText.java b/server/src/main/java/com/aisino/iles/core/model/OperateLogText.java new file mode 100644 index 0000000..521086d --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/OperateLogText.java @@ -0,0 +1,31 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.enums.OperateLogTextType; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table(name = "sys_operate_log_text") +@Comment("操作日志详细信息") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "operateLogTextId") +@Builder +public class OperateLogText { + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + @Column(length = 36) + @Comment("操作日志详细信息ID") + private String operateLogTextId; + @Comment("操作日志类型") + @Enumerated(EnumType.STRING) + private OperateLogTextType type; + @JdbcTypeCode(SqlTypes.LONGVARCHAR) + @Comment("内容") + private String content; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/PageQueryable.java b/server/src/main/java/com/aisino/iles/core/model/PageQueryable.java new file mode 100644 index 0000000..90b0be0 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/PageQueryable.java @@ -0,0 +1,42 @@ +package com.aisino.iles.core.model; + +/** + * 可分页查询的实体 + */ +public interface PageQueryable { + /** + * 页数 + * @return 页数 + */ + Integer page(); + + /** + * 每页数 + * @return 每页数 + */ + Integer pageSize(); + + /** + * 总记录数 + * @return 总记录数 + */ + long total(); + + /** + * 排序字段 + * @return 排序字段 + */ + String sort(); + + /** + * 排序方式 + * @return 排序方式 + */ + String dir(); + + /** + * 是否限制查询 + * @return 是否限制查询 + */ + boolean limit(); +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Permission.java b/server/src/main/java/com/aisino/iles/core/model/Permission.java new file mode 100644 index 0000000..0d1cde4 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Permission.java @@ -0,0 +1,109 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.*; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; +import org.hibernate.validator.constraints.Length; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "sys_permission") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "Permission") +@Data +@EqualsAndHashCode(of = "permissionId", callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString(of = {"permissionId", "name", "description"}) +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "permissionId", scope = Permission.class) +public class Permission extends BaseModel { + + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + + @Column(length = 36) + @Comment("权限许可信息主键") + @NotEmpty(message = "权限ID不能为空", groups = {Modify.class}) + private String permissionId; + @Column(length = 100, nullable = false) + @Comment("权限许可名称") + @NotEmpty(message = "权限名称不能为空", groups = {Add.class, Modify.class}) + @Length(max = 100, message = "权限名称最大长度不超过100") + private String name; + @Column(length = 500) + @Comment("权限许可信息描述") + @Length(max = 500, message = "权限描述最大长度不超过500") + private String description; + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_user_permission", + joinColumns = @JoinColumn(name = "permission_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + @BatchSize(size = 10) + @Builder.Default + private Set users = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "sys_role_permission", + joinColumns = @JoinColumn(name = "permission_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + @BatchSize(size = 10) + @Builder.Default + private Set roles = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "sys_function_permission", + joinColumns = @JoinColumn(name = "permission_id"), + inverseJoinColumns = @JoinColumn(name = "func_code") + ) + @org.hibernate.annotations.Cache(region = "Function", usage = CacheConcurrencyStrategy.READ_WRITE) + @BatchSize(size = 10) + @Builder.Default + private Set functions = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "sys_menu_permission", + joinColumns = @JoinColumn(name = "permission_id"), + inverseJoinColumns = @JoinColumn(name = "menu_id") + ) + @org.hibernate.annotations.Cache(region = "Menu", usage = CacheConcurrencyStrategy.READ_WRITE) + @BatchSize(size = 10) + @Builder.Default + @JsonIgnoreProperties(ignoreUnknown = true, value = {"children"}) + private Set menus = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "sys_resource_permission", + joinColumns = @JoinColumn(name = "permission_id"), + inverseJoinColumns = @JoinColumn(name = "resource_id") + ) + @org.hibernate.annotations.Cache(region = "Resource", usage = CacheConcurrencyStrategy.READ_WRITE) + @BatchSize(size = 15) + @Builder.Default + private Set resources = new HashSet<>(); + + /** + * 新增时候的验证信息分组 + */ + public interface Add { + } + + /** + * 修改的验证信息分组 + */ + public interface Modify { + } +} + diff --git a/server/src/main/java/com/aisino/iles/core/model/QueryMixin.java b/server/src/main/java/com/aisino/iles/core/model/QueryMixin.java new file mode 100644 index 0000000..14fb413 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/QueryMixin.java @@ -0,0 +1,24 @@ +package com.aisino.iles.core.model; + + +public class QueryMixin extends BaseQuery { + @Override + public Integer page() { + return super.page(); + } + + @Override + public Integer pageSize() { + return super.pageSize(); + } + + @Override + public String sort() { + return super.sort(); + } + + @Override + public String dir() { + return super.dir(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Resource.java b/server/src/main/java/com/aisino/iles/core/model/Resource.java new file mode 100644 index 0000000..7a5c296 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Resource.java @@ -0,0 +1,60 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.enums.Action; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.*; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.validator.constraints.Length; + +@Entity +@Table(name = "sys_resource") +@Cacheable +@org.hibernate.annotations.Cache(region = "Resource", usage = CacheConcurrencyStrategy.READ_WRITE) +@DynamicUpdate +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "resourceId", callSuper = true) +public class Resource extends BaseModel { + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + @Column(length = 36) + @Comment("资源信息主键") + @NotEmpty(message = "资源ID不能为空", groups = {Modify.class}) + private String resourceId; + @Column(length = 10, nullable = false) + @Comment("资源方法动作类型") + @Enumerated(EnumType.STRING) + @NotEmpty(message = "资源方法动作不能为空", groups = {Add.class, Modify.class}) + private Action action; + @Column(length = 400, nullable = false) + @Comment("资源路径") + @NotEmpty(message = "资源路径不能为空", groups = {Add.class, Modify.class}) + @Length(max = 400, message = "资源路径不能为空", groups = {Add.class, Modify.class}) + private String resourcePath; + @Column(length = 100) + @Comment("资源说明") + @NotEmpty(message = "资源说明", groups = {Add.class, Modify.class}) + private String description; + /** + * 临时分组编号 + */ + @Transient + private int group; + /** + * 添加验证分组 + */ + public interface Add { + } + + /** + * 修改验证分组 + */ + public interface Modify { + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Role.java b/server/src/main/java/com/aisino/iles/core/model/Role.java new file mode 100644 index 0000000..08559d0 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Role.java @@ -0,0 +1,84 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.*; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; +import org.hibernate.validator.constraints.Length; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "sys_role") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "Role") +@Data +@EqualsAndHashCode(of = "roleId", callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString(exclude = {"users", "userTypes", "permissions"}) +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "roleId", scope = Role.class) +public class Role extends BaseModel { + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + + @Column(length = 36) + @Comment("角色信息主键") + @NotEmpty(message = "角色ID不能为空", groups = {Modify.class}) + private String roleId; + @Column(length = 100, nullable = false) + @Comment("角色信息名称") + @NotEmpty(message = "角色名称不能为空", groups = {Add.class, Modify.class}) + @Length(max = 100, message = "角色名称最大长度不超过100", groups = {Add.class, Modify.class}) + private String roleName; + @Comment("角色信息描述") + @Length(max = 255, message = "角色描述最大长度不超过255", groups = {Add.class, Modify.class}) + private String description; + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_role_permission", + joinColumns = @JoinColumn(name = "role_id"), + inverseJoinColumns = @JoinColumn(name = "permission_id") + ) + @org.hibernate.annotations.Cache(region = "Permission", usage = CacheConcurrencyStrategy.READ_WRITE) + @BatchSize(size = 10) + @Builder.Default + private Set permissions = new HashSet<>(); + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_user_role", + joinColumns = @JoinColumn(name = "roleId"), + inverseJoinColumns = @JoinColumn(name = "userId") + ) + @BatchSize(size = 10) + @Builder.Default + private Set users = new HashSet<>(); + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_user_type_role", + joinColumns = @JoinColumn(name = "roleId"), + inverseJoinColumns = @JoinColumn(name = "user_type_id") + ) + @BatchSize(size = 5) + @Builder.Default + private Set userTypes = new HashSet<>(); + + /** + * 角色新增验证分组 + */ + public interface Add { + } + + /** + * 角色修改验证分组 + */ + public interface Modify { + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/StreetInfo.java b/server/src/main/java/com/aisino/iles/core/model/StreetInfo.java new file mode 100644 index 0000000..b939025 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/StreetInfo.java @@ -0,0 +1,40 @@ +package com.aisino.iles.core.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Entity +@Table(name = "sys_jdjbxx") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StreetInfo { + + @Id + @Column(length = 100) + @Comment("街道编码") + private String dm; + + @Column(length = 100) + @Comment("街道名称") + private String mc; + + @Column(length = 16) + private String wb; + + @Column(length = 16) + @Comment("拼音") + private String py; + + @Column(length = 100) + @Comment("是否有效;1有效;0无效") + private String valid; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/Token.java b/server/src/main/java/com/aisino/iles/core/model/Token.java new file mode 100644 index 0000000..eec23cc --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/Token.java @@ -0,0 +1,19 @@ +package com.aisino.iles.core.model; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.Instant; + +/** + * 令牌实体 + */ +@EqualsAndHashCode(of = "token") +@Data +public class Token { + private String uid; + private Instant expireTime; + private String jsonProfile; + private String token; + private String other; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/User.java b/server/src/main/java/com/aisino/iles/core/model/User.java new file mode 100644 index 0000000..21b84a5 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/User.java @@ -0,0 +1,110 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.enums.UserStatus; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import lombok.*; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "sys_user") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "User") +@Data +@EqualsAndHashCode(of = {"userId"}, callSuper = false) +@ToString(exclude = {"jurisdictions", "permissions", "roles"}) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class User extends Audiable { + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + + @Column(length = 36) + @Comment("用户信息主键") + private String userId; + @Column(length = 36, unique = true, nullable = false) + @Comment("用户名/帐号") + private String username; + @Column(length = 64, nullable = false) + @Comment("密码") + private String password; + @Email + @Column(unique = true, length = 80) + @Comment("邮箱") + private String email; + @Column(length = 2, nullable = false) + @Comment("用户状态 01 正常 02 异常 03 锁定 04 删除") + private UserStatus status; + @Transient + private String statusName; + @Column(length = 32) + @Comment("移动电话") + private String mobilePhone; + @Column(length = 64) + @Comment("身份证号码") + private String idNum; + @Column(length = 45, nullable = false) + @Comment("用户昵称/用户显示名称") + private String nickName; + @Comment("用户头像图标(地址)") + private String userIcon; + @Column(length = 10) + @Comment("是否开通移动警务应用") + private String sfktydjwt; + @ManyToMany + @JoinTable( + name = "sys_user_usertype", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "user_type_id") + ) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "UserType") + @Builder.Default + private Set userTypes = new HashSet<>(); + @Builder.Default + @ManyToMany + @JoinTable( + name = "sys_user_permission", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "permission_id") + ) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "Permission") + private Set permissions = new HashSet<>(); + @ManyToMany(cascade = {CascadeType.REFRESH}) + @JoinTable( + name = "sys_user_jurisdiction", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "jurisdiction_id")) + @Builder.Default + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "Jurisdiction") + @JsonIgnoreProperties(ignoreUnknown = true, value = {"children"}) + private Set jurisdictions = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "sys_user_role", + joinColumns = @JoinColumn(name = "userId"), + inverseJoinColumns = @JoinColumn(name = "roleId") + ) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "Role") + @Builder.Default + private Set roles = new HashSet<>(); + @Transient + private String ipAddress; + @Transient + private LocalDateTime loginTime; + @Column(length = 32) + @Comment("门户注册电话") + private String registerMobilePhone; + + @Version + @LastModifiedDate + private LocalDateTime lastModifiedTime; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/UserStatistics.java b/server/src/main/java/com/aisino/iles/core/model/UserStatistics.java new file mode 100644 index 0000000..5a5c533 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/UserStatistics.java @@ -0,0 +1,16 @@ +package com.aisino.iles.core.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserStatistics { + private String userType;//用户类型 + private String userTypeName;//用户类型 + private Long userTotal;//用户总数 +} diff --git a/server/src/main/java/com/aisino/iles/core/model/UserType.java b/server/src/main/java/com/aisino/iles/core/model/UserType.java new file mode 100644 index 0000000..155cd92 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/UserType.java @@ -0,0 +1,117 @@ +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import lombok.*; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.Comment; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +@Entity +@Table(name = "sys_user_type2") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "UserType") +@Data +@EqualsAndHashCode(of = {"userTypeId"}, callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString(of = {"userTypeId", "typeCode", "typeName"}) +public class UserType extends BaseModel { + /** + * 用户类别id 主键 + */ + @Id + @GeneratedValue(generator = Constants.Genernators.gen_ulid) + + @Column(length = 36) + @Comment("用户类别主键") + @NotEmpty(message = "用户类别ID不能为空", groups = {Modify.class}) + private String userTypeId; + /** + * 用户类别代码 + */ + @NotEmpty(message = "用户类别代码不能为空",groups = {Add.class}) + @Max(groups = {Modify.class}, value = 3,message = "用户类别代码长度最大不超过3") + @Min(groups = {Modify.class}, value = 3,message = "用户类别代码长度最小为3") + @Column(length = 4, nullable = false) + @Comment("用户类别代码") + private String typeCode; + /** + * 用户类别名称 + */ + @NotEmpty(message = "用户类别名称不能为空",groups = Add.class) + @Max(groups = {Modify.class}, value = 20, message = "用户类别名称长度最大不超过20") + @Column(length = 20, nullable = false) + @Comment("用户类别名称") + private String typeName; + /** + * 行业类别代码 + */ + @Column(length = 10) + @Comment("用户类别所属行业类别代码") + private String hylbdm; // 行业类别代码 + /** + * 行业类别名称 + */ + @Column(length = 30) + @Comment("用户类别所属行业类别名称") + private String hylb; // 行业类别 + + @Column(length = 10, name = "level_") + @Comment("用户级别") + private String level; + + /** + * 业务类别代码 + */ + @Column(length = 5) + @Comment("用户类别业务类别代码,对应一些额外的分类情况,比如企业主分类") + private String businessCategory; + /** + * 业务类别名称 + */ + @Column(length = 40) + @Comment("业务类别名称") + private String businessCategoryName; + /** + * 关联菜单信息 + */ + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "sys_user_type_menu", + joinColumns = @JoinColumn(name = "user_type_id"), + inverseJoinColumns = @JoinColumn(name = "menu_id") + ) + @org.hibernate.annotations.Cache(region = "Menu", usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(ignoreUnknown = true, value = {"children"}) + @Builder.Default + private Set menus = new LinkedHashSet<>(); + /** + * 关联角色信息 + */ + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_user_type_role", + joinColumns = @JoinColumn(name = "user_type_id"), + inverseJoinColumns = @JoinColumn(name = "roleId") + ) + @Builder.Default + private Set roles = new HashSet<>(); + + /** + * 新增验证 + */ + public interface Add {} + + /** + * 修改验证 + */ + public interface Modify extends Add {} +} diff --git a/server/src/main/java/com/aisino/iles/core/model/ValueEnum.java b/server/src/main/java/com/aisino/iles/core/model/ValueEnum.java new file mode 100644 index 0000000..7430fdb --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/ValueEnum.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.model; + +/** + * 单值枚举 + */ +public interface ValueEnum { + T getValue(); +} diff --git a/server/src/main/java/com/aisino/iles/core/model/dto/DictDTO.java b/server/src/main/java/com/aisino/iles/core/model/dto/DictDTO.java new file mode 100644 index 0000000..92c3a5b --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/dto/DictDTO.java @@ -0,0 +1,39 @@ +package com.aisino.iles.core.model.dto; + +import com.aisino.iles.core.model.enums.DictType; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * 字典 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DictDTO implements Serializable { + private String dictId; + @NotNull + private String dictName; + @NotNull + private String dictCode; + private String description; + private String simplePinyin; + private String allPinyin; + private DictType dictType; + private String dictTypeName; + private String createBy; + private LocalDateTime createTime; + private String lastModifiedBy; + private LocalDateTime lastModifiedTime; + @Builder.Default + private Set dictItems = new LinkedHashSet<>(); +} diff --git a/server/src/main/java/com/aisino/iles/core/model/dto/DictItemDTO.java b/server/src/main/java/com/aisino/iles/core/model/dto/DictItemDTO.java new file mode 100644 index 0000000..0eda059 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/dto/DictItemDTO.java @@ -0,0 +1,46 @@ +package com.aisino.iles.core.model.dto; + +import com.aisino.iles.core.model.enums.DataMode; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.Transient; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DictItemDTO implements Serializable { + private String dictItemId; + private String value; + private String display; + private String appendValue; + private String appendValueMark; + @Builder.Default + private Integer orderNum = 0; + private String simplePinyin; + private String allPinyin; + @JsonBackReference + private DictDTO dict; + private DictItemDTO parent; + @JsonManagedReference("parent") + @Builder.Default + private Set children = new LinkedHashSet<>(); + @Transient + @Builder.Default + private DataMode dataFlag = DataMode.nothing; + private String createBy; + private LocalDateTime createTime; + private String lastModifiedBy; + private LocalDateTime lastModifiedTime; + @Transient + private String parentId; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/dto/JitPkiAttr.java b/server/src/main/java/com/aisino/iles/core/model/dto/JitPkiAttr.java new file mode 100644 index 0000000..a896a41 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/dto/JitPkiAttr.java @@ -0,0 +1,44 @@ +package com.aisino.iles.core.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 吉大正元证书信息 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JitPkiAttr { + /** + * 证书过期时间,时间格式yyyy年MM月dd日 HH时:mm分:ss秒 或者 yyyyMMddHHmmss格式 + */ + private LocalDateTime expiredTime; + /** + * 证书申请时间,格式同过期时间 + */ + private LocalDateTime notBeforeTime; + /** + * 证件号码 + */ + private String idNum; + /** + * 用户的名称 + */ + private String userName; + /** + * 机构代码 + */ + private String jurisdictionCode; + /** + * 证书附带角色属性 + */ + private String roles; + /** + * ukey 的唯一标志 + */ + private String ukeyId; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/dto/LoginUserDto.java b/server/src/main/java/com/aisino/iles/core/model/dto/LoginUserDto.java new file mode 100644 index 0000000..d8e763b --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/dto/LoginUserDto.java @@ -0,0 +1,48 @@ +package com.aisino.iles.core.model.dto; + +import com.aisino.iles.core.model.Jurisdiction; +import com.aisino.iles.core.model.UserType; +import com.aisino.iles.core.model.enums.UserStatus; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.Email; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 登录用户数据实体 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoginUserDto { + private String userId; + private String username; + @JsonIgnore + private String password; + @Email + private String email; + private UserStatus status; + private String statusName; + private String mobilePhone; + private String idNum; + private String nickName; + private String userIcon; + private String ipAddress; + private LocalDateTime loginTime; + @Builder.Default + private Set userTypes = new HashSet<>(); + @Builder.Default + private Set jurisdictions = new HashSet<>(); + @Builder.Default + private Map> typeMenus = new HashMap<>(); + +} diff --git a/server/src/main/java/com/aisino/iles/core/model/dto/MenuTreeNodeDTO.java b/server/src/main/java/com/aisino/iles/core/model/dto/MenuTreeNodeDTO.java new file mode 100644 index 0000000..f583a5d --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/dto/MenuTreeNodeDTO.java @@ -0,0 +1,31 @@ +package com.aisino.iles.core.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.LinkedHashSet; +import java.util.Set; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MenuTreeNodeDTO { + private String menuId; + private String menuCode; + private String menuName; + private String[] iconCls; + private String iconPath; + private String path; + @Builder.Default + private Integer orderNum = 0; + @Builder.Default + private Boolean leaf = true; + private String simplePinyin; + private String allPinyin; + private MenuTreeNodeDTO parent; + @Builder.Default + private Set children = new LinkedHashSet<>(); +} diff --git a/server/src/main/java/com/aisino/iles/core/model/dto/UserDTO.java b/server/src/main/java/com/aisino/iles/core/model/dto/UserDTO.java new file mode 100644 index 0000000..5ad19f8 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/dto/UserDTO.java @@ -0,0 +1,18 @@ +package com.aisino.iles.core.model.dto; + +import lombok.*; + +@Data +@EqualsAndHashCode +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserDTO { + + private String userId; + private String oldpassword; + private String newpassword; + private String confirmpassword; + private String registerMobilePhone; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/Action.java b/server/src/main/java/com/aisino/iles/core/model/enums/Action.java new file mode 100644 index 0000000..42c4bc3 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/Action.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.model.enums; + +/** + * 动作 + */ +public enum Action { + options,get,post,put,delete +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/CertificateType.java b/server/src/main/java/com/aisino/iles/core/model/enums/CertificateType.java new file mode 100644 index 0000000..a91d407 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/CertificateType.java @@ -0,0 +1,58 @@ +package com.aisino.iles.core.model.enums; + +import com.aisino.iles.core.converter.persistence.ValueEnumConverter; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.ValueEnum; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import jakarta.persistence.Converter; + +import java.util.Arrays; + +/** + * 证件类型 + */ +public enum CertificateType implements ValueEnum { + ID_CARD("11"), + REGISTERED_RESIDENCE("13"), + MILITARY_OFFICER_CARD("90"), + POLICE_OFFICER_CARD("91"), + SOLDIER_CARD("92"), + PASSPORT("93"), + GATJMJZZ("94"), + OTHER("99"), + TEMP_ID_CARD("112"), + PLA_CIVILIAN_CADRE_CERTIFICATE("116"), + CIVILIAN_CADRE_CERTIFICATE_POLICE_FORCE("117"), + ARMED_POLICE_FORCE_SOLDIER_CERTIFICATE("118"), + TRAVEL_CERTIFICATE("415"), + SEAFARER_CERTIFICATE("'s Certificate 419"), + TRAVEL_HONGKON_MACAO("513"), + TRAVEL_HONGKON_MACAO_BUS("518"), + IDENTITY_CERTIFICATE_PUB_SEC_ORG("999"); + + private final String value; + + CertificateType(String s) { + value = s; + } + + @JsonValue + @Override + public String getValue() { + return value; + } + + @JsonCreator + public static CertificateType fromString(String v) { + return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElseThrow(() -> new BusinessError("证件类型[" + v + "], 无法找到对应枚举类型")); + } + + @Converter(autoApply = true) + public static class StringEnumCertificateTypeConvertor extends ValueEnumConverter { + @Override + public CertificateType convertToEntityAttribute(String s) { + return Arrays.stream(values()).filter(c -> c.getValue().equals(s)).findFirst().orElse(CertificateType.OTHER); + } + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/DataMode.java b/server/src/main/java/com/aisino/iles/core/model/enums/DataMode.java new file mode 100644 index 0000000..3a19cb8 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/DataMode.java @@ -0,0 +1,22 @@ +package com.aisino.iles.core.model.enums; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 数据模式 + */ +public enum DataMode { + add, + modify, + remove, + redeem, + @JsonEnumDefaultValue + nothing; + + @JsonValue + @Override + public String toString() { + return super.toString(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/DeleteFlag.java b/server/src/main/java/com/aisino/iles/core/model/enums/DeleteFlag.java new file mode 100644 index 0000000..1bcaf13 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/DeleteFlag.java @@ -0,0 +1,47 @@ +package com.aisino.iles.core.model.enums; + +import com.aisino.iles.core.converter.persistence.ValueEnumConverter; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.ValueEnum; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import jakarta.persistence.Converter; + +import java.util.Arrays; + +/** + * 通用删除标志 + * + * @author huxin + * @since 2020-07-03 + */ +public enum DeleteFlag implements ValueEnum { + not_yet("0"), // 未删除 + deleted("1"); // 已删除 + private final String value; + + DeleteFlag(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + + @JsonValue + @Override + public String getValue() { + return value; + } + + @JsonCreator + public static DeleteFlag fromString(String v) { + return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElseThrow(() -> new BusinessError("删除标志[" + v + "],没有匹配到对应的值")); + } + + @Converter(autoApply = true) + public static class DeleteFlagConverter extends ValueEnumConverter { + + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/DictType.java b/server/src/main/java/com/aisino/iles/core/model/enums/DictType.java new file mode 100644 index 0000000..58de682 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/DictType.java @@ -0,0 +1,45 @@ +package com.aisino.iles.core.model.enums; + +import com.aisino.iles.core.converter.persistence.ValueEnumConverter; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.ValueEnum; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +/** + * 字典类型 + */ +public enum DictType implements ValueEnum { + /** + * 简单 + */ + simple("01"), + /** + * 树形 + */ + tree("02"); + + private final String value; + + DictType(String value) { + this.value = value; + } + + @JsonCreator + public static DictType fromString(String str) { + return Arrays.stream(values()).filter(v -> v.getValue().equals(str)).findFirst().orElseThrow(() -> new BusinessError("字典类型[" + str + "],没有匹配到对应的值")); + } + + @JsonValue + @Override + public String getValue() { + return value; + } + + @jakarta.persistence.Converter(autoApply = true) + public static class Converter extends ValueEnumConverter { + + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/IpType.java b/server/src/main/java/com/aisino/iles/core/model/enums/IpType.java new file mode 100644 index 0000000..9849e1d --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/IpType.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.model.enums; + +/** + * IP 类型 V4 和 V6 + */ +public enum IpType { + v4, v6 +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/LoginLogType.java b/server/src/main/java/com/aisino/iles/core/model/enums/LoginLogType.java new file mode 100644 index 0000000..b365234 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/LoginLogType.java @@ -0,0 +1,38 @@ +package com.aisino.iles.core.model.enums; + +import com.aisino.iles.core.converter.persistence.ValueEnumConverter; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.ValueEnum; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import jakarta.persistence.Converter; + +import java.util.Arrays; + +/** + * 登录日志类型 + */ +public enum LoginLogType implements ValueEnum { + login("01"), // 登录 + logout("02"); // 登出/退出登录 + private final String value; + + @JsonValue + @Override + public String getValue() { + return value; + } + + LoginLogType(String value) { + this.value = value; + } + + @JsonCreator + public static LoginLogType fromString(String value) { + return Arrays.stream(values()).filter(v -> v.getValue().equals(value)).findFirst().orElseThrow(() -> new BusinessError("登录日志类型[" + value + "], 找不到对应的枚举值")); + } + @Converter(autoApply = true) + public static class StringToLoginLogTypeConverter extends ValueEnumConverter { + + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/OperateLogTextType.java b/server/src/main/java/com/aisino/iles/core/model/enums/OperateLogTextType.java new file mode 100644 index 0000000..7d9b918 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/OperateLogTextType.java @@ -0,0 +1,12 @@ +package com.aisino.iles.core.model.enums; + +public enum OperateLogTextType { + /** + * 参数 + */ + args, + /** + * 异常信息 + */ + errors +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/OperateStatus.java b/server/src/main/java/com/aisino/iles/core/model/enums/OperateStatus.java new file mode 100644 index 0000000..7258a46 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/OperateStatus.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.model.enums; + +/** + * 操作状态 + */ +public enum OperateStatus { + SUCCESS,FAILURE +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/OperateType.java b/server/src/main/java/com/aisino/iles/core/model/enums/OperateType.java new file mode 100644 index 0000000..f6f4249 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/OperateType.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.model.enums; + +/** + * 操作类型 + */ +public enum OperateType { + ADD, MODIFY, REMOVE, QUERY, EXPORT, IMPORT +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/StandardAddressTargetType.java b/server/src/main/java/com/aisino/iles/core/model/enums/StandardAddressTargetType.java new file mode 100644 index 0000000..88f7f05 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/StandardAddressTargetType.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.model.enums; + +/** + * 标准地址字段类型 + */ +public enum StandardAddressTargetType { + GXDWID,BZDZID,BZDZ +} diff --git a/server/src/main/java/com/aisino/iles/core/model/enums/UserStatus.java b/server/src/main/java/com/aisino/iles/core/model/enums/UserStatus.java new file mode 100644 index 0000000..a50f03c --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/enums/UserStatus.java @@ -0,0 +1,58 @@ +package com.aisino.iles.core.model.enums; + +import com.aisino.iles.core.converter.persistence.ValueEnumConverter; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.ValueEnum; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +/** + * 用户状态 + */ +public enum UserStatus implements ValueEnum { + /** + * 正常 + */ + normal("01"), + /** + * 异常 + */ + unnormal("02"), + /** + * 锁定 + */ + lock("03"), + /** + * 删除 + */ + deleted("04"); + + private final String value; + + UserStatus(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + @JsonCreator + public static UserStatus fromString(String v) { + return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElseThrow(() -> new BusinessError("用户状态[" + v + "],没有匹配到对应的值")); + } + + + @JsonValue + @Override + public String getValue() { + return value; + } + + @jakarta.persistence.Converter(autoApply = true) + public static class Converter extends ValueEnumConverter { + + } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/package-info.java b/server/src/main/java/com/aisino/iles/core/model/package-info.java new file mode 100644 index 0000000..a8e84f9 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/package-info.java @@ -0,0 +1,11 @@ +@GenericGenerators({ + @GenericGenerator(name = Constants.Genernators.gen_uuid, type = UUIDGeneratorPlus.class, parameters = {@org.hibernate.annotations.Parameter(name = "always_generate", value = "false")}), + @GenericGenerator(name = Constants.Genernators.gen_ulid, type = ULIDGenerator.class) +}) +package com.aisino.iles.core.model; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.identifiergenerator.ULIDGenerator; +import com.aisino.iles.core.identifiergenerator.UUIDGeneratorPlus; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.GenericGenerators; diff --git a/server/src/main/java/com/aisino/iles/core/model/query/AddrQuery.java b/server/src/main/java/com/aisino/iles/core/model/query/AddrQuery.java new file mode 100644 index 0000000..706b9f2 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/query/AddrQuery.java @@ -0,0 +1,20 @@ +package com.aisino.iles.core.model.query; + +import com.aisino.iles.core.model.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AddrQuery extends BaseQuery { + + private String dm; // 街道编码 + + private String mc; // 街道名称 + + private String jlh; // 街路号 + + private String xxdz; //住址详情 + + private String fwbm; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/query/DictItemQuery.java b/server/src/main/java/com/aisino/iles/core/model/query/DictItemQuery.java new file mode 100644 index 0000000..28ce31a --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/query/DictItemQuery.java @@ -0,0 +1,30 @@ +package com.aisino.iles.core.model.query; + +import com.aisino.iles.core.model.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 字典信息查询实体 + * + * @author huxin + * @since 2020-07-22 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class DictItemQuery extends BaseQuery { + private String dictItemId; + private String dictCode; + private String value; + private String valueMarray; + private String display; + private String parentId; + private String dictId; + private Boolean needReturnTree; // 是否需要返回树型 + /** + * 提供模糊查询属性 + */ + private String queryParam; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/query/DictQuery.java b/server/src/main/java/com/aisino/iles/core/model/query/DictQuery.java new file mode 100644 index 0000000..133aa54 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/query/DictQuery.java @@ -0,0 +1,49 @@ +package com.aisino.iles.core.model.query; + +import com.aisino.iles.core.model.Jurisdiction; +import com.aisino.iles.core.model.enums.DictType; +import lombok.Data; + +/** + * 字典查询实体 + */ +@Data +public class DictQuery { + /** + * 字典ID + */ + private String dictId; + /** + * 字典名称 + */ + private String dictName; + /** + * 字典代码 + */ + private String dictCode; + /** + * 字典类型 参考DictType 枚举 + */ + private DictType dictType; + /** + * 名称简拼 + */ + private String simplePinyin; + /** + * 名称全拼 + */ + private String allPinyin; + /** + * 字典描述 + */ + private String description; + private Jurisdiction jurisdiction; + /** + * 字典值 (辅助用于筛选字典信息) + */ + private String value; + /** + * 字典名称模糊查询条件(匹配名称,简拼,全拼) + */ + private String dictNameQuery; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/query/OperateLogQuery.java b/server/src/main/java/com/aisino/iles/core/model/query/OperateLogQuery.java new file mode 100644 index 0000000..d5804ec --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/query/OperateLogQuery.java @@ -0,0 +1,39 @@ +package com.aisino.iles.core.model.query; + +import com.aisino.iles.core.model.BaseQuery; +import com.aisino.iles.core.model.enums.IpType; +import com.aisino.iles.core.model.enums.OperateStatus; +import com.aisino.iles.core.model.enums.OperateType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 操作日志查询条件 + * + * @author huxin + * @since 2020-10-13 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class OperateLogQuery extends BaseQuery { + /** + * 操作类型 + */ + private OperateType operateType; + /** + * ip地址 + */ + private String ip; + /** + * ip类型 + */ + private IpType ipType; + /** + * 操作名称 + */ + private String name; + /** + * 操作状态 + */ + private OperateStatus status; +} diff --git a/server/src/main/java/com/aisino/iles/core/model/query/UserQuery.java b/server/src/main/java/com/aisino/iles/core/model/query/UserQuery.java new file mode 100644 index 0000000..83cdc92 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/query/UserQuery.java @@ -0,0 +1,69 @@ +package com.aisino.iles.core.model.query; + +import com.aisino.iles.core.model.BaseQuery; +import com.aisino.iles.core.model.Jurisdiction; +import com.aisino.iles.core.model.UserType; +import com.aisino.iles.core.model.enums.UserStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户查询 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserQuery extends BaseQuery { + /** + * 用户id + */ + private Long userId; + /** + * 昵称或者用户姓名 + */ + private String nickName; + /** + * 用户名或者用户帐号 + */ + private String username; + /** + * 用户状态 + */ + private UserStatus status; + /** + * 用户类别 + */ + private UserType userType; + /** + * 用户所属机构 + */ + private Jurisdiction jurisdiction; + /** + * 用户邮箱 + */ + private String email; + /** + * 用户手机号码 + */ + private String mobilePhone; + /** + * 身份证号码 + */ + private String idNum; + + private LocalDateTime createTime; + // test + private String userIcon; + +// public void setIdNum(String idNum) { +// if (StringUtils.isNotEmpty(idNum)) { +// this.idNum = SM4Util.sm4Encrypt(idNum); +// } else +// this.idNum = null; +// } +} diff --git a/server/src/main/java/com/aisino/iles/core/model/type/CustomOracleDateType.java b/server/src/main/java/com/aisino/iles/core/model/type/CustomOracleDateType.java new file mode 100644 index 0000000..e08281a --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/model/type/CustomOracleDateType.java @@ -0,0 +1,117 @@ +//package com.aisino.iles.core.model.type; +// +//import lombok.SneakyThrows; +//import oracle.sql.DATE; +//import org.hibernate.HibernateException; +//import org.hibernate.engine.spi.SharedSessionContractImplementor; +//import org.hibernate.type.StandardBasicTypes; +//import org.hibernate.usertype.ParameterizedType; +//import org.hibernate.usertype.UserType; +// +//import java.io.Serializable; +//import java.sql.*; +//import java.time.Instant; +//import java.time.LocalDateTime; +//import java.time.ZoneId; +//import java.util.Date; +//import java.util.Properties; +// +///** +// * oracle date 类型映射 +// */ +//public class CustomOracleDateType implements UserType, Serializable, ParameterizedType { +// private static final int[] SQL_TYPES = new int[] { +// Types.TIMESTAMP +// }; +// private Class returnClassType; +// +// @Override +// public int[] sqlTypes() { +// return SQL_TYPES; +// } +// +// @Override +// public Class returnedClass() { +// return returnClassType; +// } +// +// @Override +// public boolean equals(Object x, Object y) throws HibernateException { +// if (x == y) +// return true; +// if (x == null || y == null) +// return false; +// +// return x.equals(y); +// } +// +// @Override +// public int hashCode(Object x) throws HibernateException { +// return x.hashCode(); +// } +// +// @Override +// public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { +// Object timestamp = StandardBasicTypes.TIMESTAMP.nullSafeGet(rs, names, session, owner); +// if (timestamp == null) { +// return null; +// +// } +// Timestamp ts = (Timestamp) timestamp; +// Instant instant = Instant.ofEpochMilli(ts.getTime()); +// if(LocalDateTime.class.equals(returnClassType)) +// return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); +// else if(Date.class.equals(returnClassType)) +// return Date.from(instant); +// return Date.from(instant); +// } +// +// @Override +// public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { +// if (value == null) { +// StandardBasicTypes.TIMESTAMP.nullSafeSet(st, null, index, session); +// } else { +// DATE d = null; +// if(LocalDateTime.class.equals(returnClassType)) { +// d = new DATE(Timestamp.valueOf((LocalDateTime) value)); +// } else if(Date.class.equals(returnClassType)) { +// d = new DATE(Timestamp.from(((Date) value).toInstant())); +// } else { +// d = new DATE(Timestamp.from(((Date) value).toInstant())); +// } +// st.setObject(index, d); +// } +// } +// +// @Override +// public Object deepCopy(Object value) throws HibernateException { +// return value; +// } +// +// @Override +// public boolean isMutable() { +// return false; +// } +// +// @Override +// public Serializable disassemble(Object value) throws HibernateException { +// return ((Serializable) value); +// } +// +// @Override +// public Object assemble(Serializable cached, Object owner) throws HibernateException { +// return cached; +// } +// +// @Override +// public Object replace(Object original, Object target, Object owner) throws HibernateException { +// return original; +// } +// +// @SneakyThrows +// @Override +// public void setParameterValues(Properties parameters) { +// String returnClassName = parameters.getProperty("returnClassName"); +// this.returnClassType = Class.forName(returnClassName); +// } +//} diff --git a/server/src/main/java/com/aisino/iles/core/repository/AddrInfoRepo.java b/server/src/main/java/com/aisino/iles/core/repository/AddrInfoRepo.java new file mode 100644 index 0000000..9e196d3 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/AddrInfoRepo.java @@ -0,0 +1,15 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.AddrInfo; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface AddrInfoRepo extends BaseRepo{ + + Optional> findAllByFWDZBM(String FWDZBM); + + Optional findByFWDZBM(String FWDZBM); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/BaseRepo.java b/server/src/main/java/com/aisino/iles/core/repository/BaseRepo.java new file mode 100644 index 0000000..2336c4d --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/BaseRepo.java @@ -0,0 +1,11 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.common.repository.PagingAndSortingSpecificationRepository; +import com.aisino.iles.common.repository.SingleResultRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.NoRepositoryBean; + +@NoRepositoryBean +public interface BaseRepo extends JpaRepository, JpaSpecificationExecutor, PagingAndSortingSpecificationRepository, SingleResultRepository { +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/DictItemRepo.java b/server/src/main/java/com/aisino/iles/core/repository/DictItemRepo.java new file mode 100644 index 0000000..abf133b --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/DictItemRepo.java @@ -0,0 +1,47 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.DictItem; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.QueryHint; +import java.util.List; +import java.util.Optional; + +import static org.hibernate.jpa.HibernateHints.*; + +/** + * 字典项 + */ +@Repository +public interface DictItemRepo extends BaseRepo { + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "all-dictItems") + }) + Optional findByDictDictCodeAndValue(String dictDictCode, String value); + Optional findByDictDictCodeAndValueAndDisplay(String dictDictCode, String value, String Display); + + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "all-dictItems") + }) + @EntityGraph(value = "dict-item-list", type = EntityGraph.EntityGraphType.FETCH) + List findByDictDictCodeOrderByOrderNumAsc(@Param("dictCode") String dictCode); + + @EntityGraph(value = "dict-item-list", type = EntityGraph.EntityGraphType.FETCH) + List findByDictDictCodeAndDisplay(@Param("dictCode") String dictCode, String display); + + List findByDictDictCodeAndParentDictItemId(String dictCode, String dictItemId); + + @Query(value="SELECT * FROM t_dict_item WHERE dict_id=(SELECT dict_id FROM t_dict WHERE dict_code = 'dm_xzqh') and display = :address", nativeQuery = true) + Optional findDisplayAndValue(@Param("address") String address); + + @Query(value="select new DictItem (t.value,t.display,t.lastModifiedTime) from DictItem t where t.dict.dictId = :dictId") + List findValueAndDisplayByDictId(@Param("dictId") String dictId); + + Optional findByValue(String value); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/DictRepo.java b/server/src/main/java/com/aisino/iles/core/repository/DictRepo.java new file mode 100644 index 0000000..ffe3037 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/DictRepo.java @@ -0,0 +1,42 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.Dict; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.QueryHint; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.hibernate.jpa.HibernateHints.*; + +/** + * 字典 + */ +@Repository +public interface DictRepo extends BaseRepo{ + + default Optional findByDictCode(String dictCode) { + return findAllByCached().stream().filter(d -> d.getDictCode().equals(dictCode)).findFirst(); + } + + @Query("from Dict ") + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "dict-cache") + }) + Set findAllByCached(); + + List findDictsByDictCodeIsIn(Set dictCode); + + + @Query("from Dict t where t.dictCode = :dictCode") + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "dict-cache") + }) + Optional findDictByDictCode(@Param("dictCode") String dictCode); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/FunctionRepo.java b/server/src/main/java/com/aisino/iles/core/repository/FunctionRepo.java new file mode 100644 index 0000000..1cf33e9 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/FunctionRepo.java @@ -0,0 +1,21 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.Function; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.QueryHint; +import java.util.Set; + +import static org.hibernate.jpa.HibernateHints.*; + +@Repository +public interface FunctionRepo extends BaseRepo { + @Query("select f from Function f") + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "all-functions") + }) + Set findAllByCached(); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/GenerateCodeRepository.java b/server/src/main/java/com/aisino/iles/core/repository/GenerateCodeRepository.java new file mode 100644 index 0000000..e035a3d --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/GenerateCodeRepository.java @@ -0,0 +1,43 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.GenerateCode; +import jakarta.validation.constraints.NotNull; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +/** + * 生成代码 + * + * @author huxin + * @since 2020-06-12 + */ +public interface GenerateCodeRepository extends BaseRepo { + /** + * 查询生成代码数据信息 + * + * @param provinceSimplePinyin 省份简拼 + * @param codeCategory 代码种类 + * @param codePrefix 编码前缀 / 行政区划代码 + * @param generateRule 生成规则 + * @return 生成代码数据信息 + */ + @Query(""" + select g from GenerateCode g + where g.provinceSimplePinyin = ?1 and g.codeCategory = ?2 and (g.codePrefix = ?3 or g.codePrefix is null) and g.generateRule = ?4""") + Optional findByProvinceSimplePinyinAndCodeCategoryAndCodePrefixAndGenerateRule(String provinceSimplePinyin, String codeCategory, String codePrefix, int generateRule); + + /** + * @param code 编码 + * @param codeCategory 代码种类 + * @param codePrefix 编码前缀 / 行政区划代码 / 省份前缀 + * @return + */ + boolean existsByCodeAndCodeCategoryAndCodePrefix(@NotNull String code, @NotNull String codeCategory, @NotNull String codePrefix); + + Optional findByProvinceSimplePinyinAndCodeCategoryAndGenerateRule(String provinceSimplePinyin, String codeCategory, int generateRule); + + Optional findFirstByCodeCategoryAndCodePrefixAndTimeParameterAndProvinceSimplePinyin(String codeCategory, String codePrefix, String timeParameter, String provinceSimplePinyin); + + +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/GlobalParamRepo.java b/server/src/main/java/com/aisino/iles/core/repository/GlobalParamRepo.java new file mode 100644 index 0000000..d1d195a --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/GlobalParamRepo.java @@ -0,0 +1,33 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.GlobalParam; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.QueryHint; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; +import static org.hibernate.jpa.HibernateHints.HINT_CACHE_REGION; + +@Repository +public interface GlobalParamRepo extends BaseRepo{ + List findByGlobalParamCodeIn(Collection globalParamCodes); + /** + * 缓存查询所有全局参数 + * + * @return + */ + @Query("from GlobalParam ") + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "all-globalParams") + }) + Set findAllCached(); + + Optional findByGlobalParamCode(String globalParamCode); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/JurisdictionRepo.java b/server/src/main/java/com/aisino/iles/core/repository/JurisdictionRepo.java new file mode 100644 index 0000000..2c6cf82 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/JurisdictionRepo.java @@ -0,0 +1,89 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.converter.persistence.StringArray2VarcharConverter; +import com.aisino.iles.core.model.Jurisdiction; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.Convert; +import jakarta.persistence.QueryHint; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.hibernate.jpa.HibernateHints.*; + +/** + * 管辖机构 + */ +@Repository +public interface JurisdictionRepo extends BaseRepo{ + +// default Optional findByJurisdictionCode(String jurisdictionCode) { +// return findAllCached().stream().filter(j -> j.getJurisdictionCode().equals(jurisdictionCode)).findFirst(); +// } + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "jurisdiction.query") + }) + Optional findByJurisdictionCode(String jurisdictionCode); + Optional findByJurisSimpleCode(String jurisSimpleCode); + @Query("from Jurisdiction j order by j.orderNum") + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "false"), + @QueryHint(name = HINT_CACHE_REGION, value = "jurisdiction.query"), + @QueryHint(name = HINT_CACHE_MODE, value = "IGNORE") + }) + @EntityGraph("jurisdiction-list") + List findAllCached(); + + List findByParentJurisdictionId(String jurisdictionId); + + @Query("select j from Jurisdiction j where j.jurisdictionCode like :jurisdictionCode and j.jurisdictionLevel = :jurisdictionLevel") + List findLikeJurisdictionCode(String jurisdictionCode, Integer jurisdictionLevel); + + @Query("select j from Jurisdiction j where j.jurisdictionCode like :jurisSimpleCode") + List findLikeJurisSimpleCode(String jurisSimpleCode); + /** + * 通过机构全码查询 + * @param jurisdictionFullCode 机构信息全码 + * @param notJurisdictionFullCode 不等于机构信息全码 + * @return 机构信息 + */ + List findByJurisFullCodeStartingWithAndJurisFullCodeNot(String jurisdictionFullCode, String notJurisdictionFullCode); + + /** + * 内网数据上报 + * + * @param dateTime + * @return + */ + @Convert(converter= StringArray2VarcharConverter.class) + @Query(value="SELECT " + + " jurisdiction_code AS Badwbm, " + + " jurisdiction_name AS DWMC, " + + " '' AS Fzr_XM, " + + " '' AS Dwdz_DZMC, " + + " '' AS LXDH, " + + " '' AS YZBM, " + + " '1' AS Zzzt, " + + " '' AS Fzr_ZJLX, " + + " '' AS Fzr_GMSFHM, " + + " '' AS Fzr_LXDH, " + + " '' AS Xzq_sbm, " + + " '' AS Xzq_dsbm, " + + " '' AS Xzq_qbm, " + + " '' AS Xzq_mc, " + + " '' AS Cjr_XM, " + + " '' AS Cjrq, " + + " '' AS Gxr_XM, " + + " '' AS Gxrq, " + + " '' AS Scr_XM, " + + " '' AS Scrq " + + " FROM " + + " t_jurisdiction where last_modified_time >= ? ", nativeQuery = true) + List> jurReport(LocalDateTime dateTime); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/LoginLogRepo.java b/server/src/main/java/com/aisino/iles/core/repository/LoginLogRepo.java new file mode 100644 index 0000000..9a92435 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/LoginLogRepo.java @@ -0,0 +1,24 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.LoginLog; +import com.aisino.iles.core.model.enums.LoginLogType; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 登录日志 + */ +@Repository +public interface LoginLogRepo extends BaseRepo { + /** + * 获取最后一次登录或者登出的记录 + * + * @param username 用户名 + * @param loginLogType 登录类型 登录/ 登出(退出) + * @return 可能的唯一结果 + */ + Optional findFirstByUsernameAndLoginLogTypeOrderByOperateTimeDesc(String username, LoginLogType loginLogType); + + Optional findFirstBySessionId(String sessionId); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/MenuRepo.java b/server/src/main/java/com/aisino/iles/core/repository/MenuRepo.java new file mode 100644 index 0000000..28108be --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/MenuRepo.java @@ -0,0 +1,18 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.Menu; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.stereotype.Repository; + +import java.util.Set; + +@Repository +public interface MenuRepo extends BaseRepo { + + Long countByMenuCode(String menuCode); + + @EntityGraph("menus-tree") + Set findByMenuIdIn(Set menuIds); + + Set findSimpleByMenuIdIn(Set menuIds); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/OperateLogRepo.java b/server/src/main/java/com/aisino/iles/core/repository/OperateLogRepo.java new file mode 100644 index 0000000..38679c2 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/OperateLogRepo.java @@ -0,0 +1,14 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.OperateLog; +import org.springframework.stereotype.Repository; + +/** + * 操作日志 + * + * @author huxin + * @since 2020-10-10 + */ +@Repository +public interface OperateLogRepo extends BaseRepo { +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/PageCustomRepo.java b/server/src/main/java/com/aisino/iles/core/repository/PageCustomRepo.java new file mode 100644 index 0000000..5b38c21 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/PageCustomRepo.java @@ -0,0 +1,91 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.common.util.PageBean; +import org.hibernate.query.Query; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import jakarta.persistence.TypedQuery; +import java.util.Map; +import java.util.Optional; + +public interface PageCustomRepo { + + /**分页条件*/ + default void pageCondition(Query query, Integer page, Integer pagesize){ + pagesize = Optional.ofNullable(pagesize).filter(f -> f > 0).orElse(20);//每页显示条数 + page = Optional.ofNullable(page).filter(f -> f > 0).map(f -> f - 1).orElse(0);//当前页 + query.setFirstResult(pagesize*page); + query.setMaxResults(pagesize); + } + default void pageCondition(TypedQuery query, Integer page, Integer pagesize){ + pagesize = Optional.ofNullable(pagesize).filter(f -> f > 0).orElse(20);//每页显示条数 + page = Optional.ofNullable(page).filter(f -> f > 0).map(f -> f - 1).orElse(0);//当前页 + query.setFirstResult(pagesize*page); + query.setMaxResults(pagesize); + } + + default void pageCondition(Query query, Integer page, Integer pagesize, Long total) { + Integer count = total.intValue(); + + PageBean pageBean = PageBean.buildPageBean(page, pagesize, count); + pageBean.setPage(page); + pageBean.setStartWith(); + pageBean.setOffset(); + query.setFirstResult(pageBean.getStartWith()); + query.setMaxResults(pageBean.getOffset()); + } + + /** + * 分页条件 + */ + default void pageCondition(jakarta.persistence.Query query, Integer page, Integer pagesize, Long total) { + pagesize = Optional.ofNullable(pagesize).filter(f -> f > 0).orElse(20);//每页显示条数 + page = Optional.ofNullable(page).filter(f -> f > 0).map(f -> f - 1).orElse(0);//当前页 + int count = total.intValue(); + + query.setFirstResult(page * pagesize); + + int totalPage = count % pagesize == 0 ? count / pagesize : count / pagesize + 1; + + page = page > totalPage ? 0 : page; + int max = 0; + if (count > 0) { + max = count > (page + 2) * pagesize ? pagesize : Math.abs(count - (page + 1) * pagesize > 0 ? count - (page + 1) * pagesize : count); + } + + query.setMaxResults(max); + } + + /**排序条件*/ + default Pageable commPageable(Integer page, Integer pagesize, String sort, String dir, String sortDefault){ + sort = Optional.ofNullable(sort).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).orElse(sortDefault); + dir = Optional.ofNullable(dir).filter(d -> Sort.Direction.fromOptionalString(d).isPresent()).orElse("desc"); + return PageRequest.of(page, pagesize, Sort.by(Sort.Direction.fromString(dir), sort)); + } + + /** + * 分页条件(不带排序) + * @param page 页数 + * @param pageSize 每页显示数 + * @return 分页条件 + */ + default Pageable commPageable(Integer page, Integer pageSize) { + return PageRequest.of(page, pageSize); + } + + default void setParameters(Query query,Map params){ + params.forEach(query::setParameter); + } + + default void setParameters(jakarta.persistence.Query query,Mapparams){ + params.forEach(query::setParameter); + } + + default Integer[] setPageAndPageSize(Integer page, Integer pagesize) { + Integer _page = Optional.ofNullable(page).filter(p -> p > 0).map(p -> p - 1).orElse(0); + Integer _psize = Optional.ofNullable(pagesize).filter(p -> p > 0).orElse(20); + return new Integer[]{_page, _psize}; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/PermissionRepo.java b/server/src/main/java/com/aisino/iles/core/repository/PermissionRepo.java new file mode 100644 index 0000000..3f8a8b6 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/PermissionRepo.java @@ -0,0 +1,11 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.Permission; +import org.springframework.stereotype.Repository; + +import java.util.Set; + +@Repository +public interface PermissionRepo extends BaseRepo { + Set findByRolesUsersUserId(String userId); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/ResourceRepo.java b/server/src/main/java/com/aisino/iles/core/repository/ResourceRepo.java new file mode 100644 index 0000000..4f6b34e --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/ResourceRepo.java @@ -0,0 +1,33 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.Resource; +import com.aisino.iles.core.model.enums.Action; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.QueryHint; +import java.util.List; + +import static org.hibernate.jpa.HibernateHints.*; + +@Repository +public interface ResourceRepo extends BaseRepo { + /** + * 通过一组资源主键,查询资源信息 + * @param resourceIds 资源信息主键 + * @return 资源信息 + */ + @QueryHints({ + @QueryHint(name = HINT_CACHEABLE, value = "true"), + @QueryHint(name = HINT_CACHE_REGION, value = "iles-resource-cache") + }) + List findByResourceIdIn(List resourceIds); + + /** + * 通过请求方式和资源路径查询资源信息 接口对接调用 + * @param action + * @param resourcePath + * @return + */ + List findByActionAndResourcePath(Action action, String resourcePath); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/RoleRepo.java b/server/src/main/java/com/aisino/iles/core/repository/RoleRepo.java new file mode 100644 index 0000000..e6ee919 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/RoleRepo.java @@ -0,0 +1,9 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.Role; +import org.springframework.stereotype.Repository; + +@Repository +public interface RoleRepo extends BaseRepo { + +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/StreetInfoRepo.java b/server/src/main/java/com/aisino/iles/core/repository/StreetInfoRepo.java new file mode 100644 index 0000000..8a8dce2 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/StreetInfoRepo.java @@ -0,0 +1,8 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.StreetInfo; +import org.springframework.stereotype.Repository; + +@Repository +public interface StreetInfoRepo extends BaseRepo { +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/UserRepo.java b/server/src/main/java/com/aisino/iles/core/repository/UserRepo.java new file mode 100644 index 0000000..a85c132 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/UserRepo.java @@ -0,0 +1,41 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.User; +import com.aisino.iles.core.model.UserType; +import com.aisino.iles.core.model.enums.UserStatus; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Repository; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; + +@Repository +@Validated +public interface UserRepo extends BaseRepo { + Optional findByUsername(String username); + + Optional findByIdNum(@NotNull String idNum); + Optional findByIdNumAndStatus(@NotNull String idNum, UserStatus status); + + List findByStatusInAndIdNum(UserStatus[] statuses, @NotNull String idNum); + + Optional findByIdNumAndStatusIn(@NonNull String idNum, UserStatus[] statuses); + + boolean existsByUsername(@NonNull String username); + + boolean existsByIdNum(@NotNull String idNum); + + boolean existsByUsernameAndStatusIn(@NotNull String username, UserStatus[] statuses); + + Optional findByMobilePhoneAndUserTypesIn(@NonNull String mobilePhone, @NonNull UserType[] userTypes); + + Optional findByUsernameAndUserTypesIn(@NonNull String username, @NonNull UserType[] userTypes); + + Optional findByUsernameAndIdNumAndMobilePhoneAndStatusIn(@NonNull String username, @NonNull String idNum, @NonNull String mobilePhone, UserStatus[] statuses); + + boolean existsByRegisterMobilePhone(@NonNull String registerMobilePhone); + + Optional findByRegisterMobilePhone(@NonNull String registerMobilePhone); +} diff --git a/server/src/main/java/com/aisino/iles/core/repository/UserTypeRepo.java b/server/src/main/java/com/aisino/iles/core/repository/UserTypeRepo.java new file mode 100644 index 0000000..fa2b6cf --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/repository/UserTypeRepo.java @@ -0,0 +1,16 @@ +package com.aisino.iles.core.repository; + +import com.aisino.iles.core.model.UserType; +import org.springframework.stereotype.Repository; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserTypeRepo extends BaseRepo { + + Optional findByTypeCode(@NotNull String typeCode); + + List findUserTypesByHylbdm(@NotNull String hylbdm); +} diff --git a/server/src/main/java/com/aisino/iles/core/service/AddrInfoService.java b/server/src/main/java/com/aisino/iles/core/service/AddrInfoService.java new file mode 100644 index 0000000..741c3b2 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/AddrInfoService.java @@ -0,0 +1,46 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.core.model.AddrInfo; +import com.aisino.iles.core.model.AddrInfo_; +import com.aisino.iles.core.model.query.AddrQuery; +import com.aisino.iles.core.repository.AddrInfoRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class AddrInfoService { + + private final AddrInfoRepo addrInfoRepo; + + @Autowired + public AddrInfoService(AddrInfoRepo addrInfoRepo) { + this.addrInfoRepo = addrInfoRepo; + } + + public Page pageAddrs(AddrQuery query) { + query.setSort(null); + return addrInfoRepo.findAll(buildAddrQueryCondition(query), + PageableHelper.buildPageRequest(query.page(), query.pageSize(), query.sort(), query.dir())); + } + + private Specification buildAddrQueryCondition(AddrQuery query) { + return Specification.where((root, criteriaQuery, criteriaBuilder)->{ + List predicates = new ArrayList<>(); + Optional.ofNullable(query.getFwbm()) + .map(f->criteriaBuilder.equal(root.get(AddrInfo_.FWBM),query.getFwbm())).ifPresent(predicates::add); + Optional.ofNullable(query.getJlh()) + .map(f->criteriaBuilder.equal(root.get(AddrInfo_.JLH),query.getJlh())).ifPresent(predicates::add); + Optional.ofNullable(query.getXxdz()) + .map(f->criteriaBuilder.like(root.get(AddrInfo_.XXDZ),query.getXxdz() + "%")).ifPresent(predicates::add); + return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()])); + }); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/CacheAdminService.java b/server/src/main/java/com/aisino/iles/core/service/CacheAdminService.java new file mode 100644 index 0000000..80704a5 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/CacheAdminService.java @@ -0,0 +1,103 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.core.hibernate.RedissonLocalStorage; +import com.aisino.iles.core.hibernate.RedissonRegionFactoryPlus; +import jakarta.persistence.Cache; +import jakarta.persistence.EntityManager; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.SessionFactory; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Objects; + +/** + * 缓存管理服务 + */ +@Service +@Slf4j +public class CacheAdminService { + + private final RedissonRegionFactoryPlus regionFactory; + private final EntityManager entityManager; + + public CacheAdminService(RedissonRegionFactoryPlus regionFactory, + EntityManager entityManager) { + this.regionFactory = regionFactory; + this.entityManager = entityManager; + } + + // 获取所有缓存区域 + public Map getAllRegions() { + return regionFactory.getAllCacheRegions(); + } + + // 查询缓存内容 + public Object getCacheValue(String regionName, Object key) { + RedissonLocalStorage region = getRegion(regionName); + return region != null ? region.getFromCache(key, null) : null; + } + + // 写入缓存 + public boolean putCacheValue(String regionName, Object key, Object value) { + RedissonLocalStorage region = getRegion(regionName); + if (region != null) { + region.putIntoCache(key, value, null); + return true; + } + return false; + } + + // 删除缓存 + public boolean removeCacheValue(String regionName, Object key) { + RedissonLocalStorage region = getRegion(regionName); + if (region != null) { + region.removeFromCache(key, null); + return true; + } + return false; + } + @SneakyThrows + public boolean removeCacheValueByEntityManager(String entityName, Object id) { + Cache cache = entityManager.getEntityManagerFactory().getCache(); + Class entityClass = Class.forName(entityName); + if(Objects.nonNull(id)){ + cache.evict(entityClass,id); + } else { + cache.evict(entityClass); + } + entityManager.getEntityManagerFactory().getCache() + .evict(Class.forName(entityName),id); + return true; + } + + // 刷新本地缓存 + public void refreshLocalCache(String regionName, Object key) { + RedissonLocalStorage region = getRegion(regionName); + if (region != null) { + region.getFromCache(key, null); + } + } + + private RedissonLocalStorage getRegion(String regionName) { + return regionFactory.getAllCacheRegions().get(regionName); + } + + + /** + * 删除缓存区域 + * @param regionName 缓存区域名称 + * @return 操作结果 + */ + public boolean removeRegion(String regionName) { + try { + getRegion(regionName).clearCache(null); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + +} \ No newline at end of file diff --git a/server/src/main/java/com/aisino/iles/core/service/DictService.java b/server/src/main/java/com/aisino/iles/core/service/DictService.java new file mode 100644 index 0000000..ac7ad0a --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/DictService.java @@ -0,0 +1,451 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.PinYinUtils; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.Dict; +import com.aisino.iles.core.model.DictItem; +import com.aisino.iles.core.model.DictItem_; +import com.aisino.iles.core.model.Dict_; +import com.aisino.iles.core.model.dto.DictDTO; +import com.aisino.iles.core.model.dto.DictItemDTO; +import com.aisino.iles.core.model.enums.DataMode; +import com.aisino.iles.core.model.query.DictItemQuery; +import com.aisino.iles.core.model.query.DictQuery; +import com.aisino.iles.core.repository.DictItemRepo; +import com.aisino.iles.core.repository.DictRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 字典和字典项 + */ +@Service +@Transactional(readOnly = true) +@Validated +public class DictService { + private final DictRepo dictRepo; + private final DictItemRepo dictItemRepo; + + @Autowired + public DictService(DictRepo dictRepo, DictItemRepo dictItemRepo) { + this.dictRepo = dictRepo; + this.dictItemRepo = dictItemRepo; + } + + /** + * 新增字典,可以新增字典项 + * + * @param dict 字典 + * @return 新增的字典 + */ + @Transactional + public DictDTO addDict(@NotNull Dict dict) { + Dict needSave = new Dict(); + BeanUtils.copyNoNullProperties(dict, needSave, Dict_.DICT_ITEMS); + Dict save = dictRepo.save(needSave); + dict.getDictItems().forEach(di -> { + di.setSimplePinyin(PinYinUtils.toSimplePinyin(di.getDisplay())); + di.setAllPinyin(PinYinUtils.toPinyin(di.getDisplay())); + }); + doDictWithDictItemsAdd(save, dict.getDictItems()); + return this.mapDictFindOneResult(save); + } + + private void doDictWithDictItemsAdd(Dict save, Set dictItems) { + Map addDictItemIdMap = new HashMap<>(); + DictItem virtualDictItemRoot = new DictItem(); + virtualDictItemRoot.setDictItemId("0"); + virtualDictItemRoot.setValue("0"); + virtualDictItemRoot.setDisplay("0"); + dictItems.stream().filter(di -> di.getParent() == null).filter(di -> di.getDataFlag() == DataMode.add).forEach(di -> di.setParent(virtualDictItemRoot)); + dictItems.stream().filter(di -> di.getDataFlag() == DataMode.add) + .sorted(Comparator.comparing(di -> di.getParent().hashCode())) + .forEach(di -> { + String noPersistantDictItemId = di.getDictItemId(); + di.setDictItemId(null); + di.setDict(save); + Optional.ofNullable(di.getParent()).filter(dp -> addDictItemIdMap.containsKey(dp.getDictItemId())) + .map(dp -> { + DictItem dictItem = new DictItem(); + dictItem.setDictItemId(addDictItemIdMap.get(dp.getDictItemId())); + return dictItem; + }) + .ifPresent(di::setParent); + di.setDataFlag(DataMode.nothing); + DictItem sdi = dictItemRepo.save(di); + addDictItemIdMap.put(noPersistantDictItemId, sdi.getDictItemId()); + save.getDictItems().add(sdi); + }); + } + + /** + * 删除字典 + * + * @param dictId 字典的ID + */ + @Transactional + public void removeDict(String dictId) { + dictRepo.findById(dictId).map(d -> { + dictRepo.deleteById(d.getDictId()); + return d; + }).orElseThrow(() -> new BusinessError("该字典ID的字典不存在")); + } + + /** + * 批量删除字典 + * + * @param dictIds 字典id集合 + */ + @Transactional + public void removeDicts(Set dictIds) { + dictRepo.deleteAll(dictIds.stream().map(dictRepo::findById).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet())); + } + + /** + * 修改字典,还可以修改字典项的信息,包含对字典项的增加修改删除 + * + * @param dict 字典 + */ + @Transactional + public void modifyDict(@NotNull @Validated Dict dict) { + dictRepo.findById(dict.getDictId()) + .map(d -> { + BeanUtils.copyNoNullProperties(dict, d, Dict_.DICT_ITEMS, Dict_.DICT_ID); + + // 拼音处理 + dict.getDictItems().forEach(di -> { + di.setSimplePinyin(PinYinUtils.toSimplePinyin(di.getDisplay())); + di.setAllPinyin(PinYinUtils.toPinyin(di.getDisplay())); + }); + // add dict item + doDictWithDictItemsAdd(d, dict.getDictItems()); + // update dict item + dict.getDictItems().stream().filter(di -> di.getDataFlag() == DataMode.modify) + .forEach(di -> dictItemRepo.findById(di.getDictItemId()) + .ifPresent(dii -> { + BeanUtils.copyNoNullProperties(di, dii, "dataFlag"); + dii.setLastModifiedTime(LocalDateTime.now()); + })); + //remove dict item + Set removeDictItems = dict.getDictItems().stream().filter(di -> di.getDataFlag() == DataMode.remove).collect(Collectors.toSet()); + d.getDictItems().removeIf(removeDictItems::contains); + + // 如果字典项发生了改变,那么需要把最后修改的字典项的更新时间,更新字典的修改时间 + if (!dict.getDictItems().isEmpty()) { + d.setLastModifiedTime(LocalDateTime.now()); + } + return d; + }) + .orElseThrow(() -> new BusinessError("该字典ID的字典不存在")); + } + + public Page pageDicts(Integer page, Integer pageSize, String sort, String dir, DictQuery dictQuery) { + return dictRepo.findAll(buildDictCondition(dictQuery), + PageableHelper.buildPageRequest(page, pageSize, sort, dir) + ).map(this::mapDictListResult); + } + + /** + * 查询条件 + */ + private Specification buildDictCondition(DictQuery query) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + Optional.ofNullable(query.getDictId()).ifPresent(f -> predicates.add(criteriaBuilder.equal(root.get(Dict_.dictId), f))); + Optional.ofNullable(query.getDictCode()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .map(p -> criteriaBuilder.equal(root.get(Dict_.dictCode), p)) + .ifPresent(predicates::add); + Optional.ofNullable(query.getDescription()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .map(f -> criteriaBuilder.like(root.get(Dict_.description), f + "%")) + .ifPresent(predicates::add); + Optional.ofNullable(query.getDictName()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .map(dn -> criteriaBuilder.like(root.get(Dict_.dictName), dn + "%")) + .ifPresent(predicates::add); + Optional.ofNullable(query.getSimplePinyin()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .map(sp -> criteriaBuilder.like(root.get(Dict_.simplePinyin), sp + "%")) + .ifPresent(predicates::add); + Optional.ofNullable(query.getAllPinyin()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .map(ap -> criteriaBuilder.like(root.get(Dict_.allPinyin), ap + "%")) + .ifPresent(predicates::add); + Optional.ofNullable(query.getDictType()) + .ifPresent(f -> predicates.add(criteriaBuilder.equal(root.get(Dict_.dictType), f))); + Optional.ofNullable(query.getValue()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .map(p -> criteriaBuilder.equal(root.join(Dict_.dictItems).get(DictItem_.value), p)) + .ifPresent(predicates::add); +// 字典名称的模糊匹配条件 包含字典名称,字典简拼,字典全拼 + Optional.ofNullable(query.getDictNameQuery()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .flatMap(p -> Stream.of( + criteriaBuilder.like(root.get(Dict_.dictName), p + "%"), + criteriaBuilder.like(root.get(Dict_.simplePinyin), p + "%"), + criteriaBuilder.like(root.get(Dict_.allPinyin), p + "%") + ).reduce(criteriaBuilder::or)) + .ifPresent(predicates::add); + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }); + } + + /** + * 添加字典项 + * + * @param dictItem 字典项 + * @return 添加的字典项 + */ + @Transactional + public DictItem addDictItem(@NotNull @Validated DictItem dictItem) { + if (dictItem.getDict() == null || dictItem.getDict().getDictId() == null) { + throw new BusinessError("字典项必须要有一个所属字典"); + } + dictItem.setSimplePinyin(PinYinUtils.toSimplePinyin(dictItem.getDisplay())); + dictItem.setAllPinyin(PinYinUtils.toPinyin(dictItem.getDisplay())); + dictItem.setDictItemId(null); + return dictItemRepo.save(dictItem); + } + + /** + * 删除字典项通过字典项ID + * + * @param dictItemId 字典项ID + */ + @Transactional + public void removeDictItem(String dictItemId) { + dictItemRepo.deleteById(dictItemId); + } + + /** + * 删除字典项通过字典代码 + * + * @param dictCode 字典代码 + */ + @Transactional + public void removeDictItems(@NotNull String dictCode) { + DictItem dictItem = new DictItem(); + Dict dict = new Dict(); + dict.setDictCode(dictCode); + dictItem.setDict(dict); + dictItemRepo.delete(dictItem); + } + + /** + * 删除字典项通过字典ID + * + * @param dictId 字典ID + */ + @Transactional + public void removeDictItemsByDictId(String dictId) { + DictItem dictItem = new DictItem(); + Dict dict = new Dict(); + dict.setDictId(dictId); + dictItem.setDict(dict); + dictItemRepo.delete(dictItem); + } + + /** + * 修改字典项 + * + * @param dictItem 字典项 + */ + @Transactional + public void modifyDictItems(@NotNull @Validated DictItem dictItem) { + dictItemRepo.findById(dictItem.getDictItemId()) + .map(di -> { + di.setSimplePinyin(PinYinUtils.toSimplePinyin(di.getDisplay())); + di.setAllPinyin(PinYinUtils.toPinyin(di.getDisplay())); + BeanUtils.copyNoNullProperties(dictItem, di, DictItem_.LAST_MODIFIED_TIME); + return dictItemRepo.save(di); + }) + .orElseThrow(() -> new BusinessError("该ID的字典项不存在")); + } + + public List listDictItems(DictItemQuery query) { + Map dictItemDTOMap = new HashMap<>(); + List dictItems = Optional.ofNullable(query.getDictCode()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .filter(d -> StringUtils.isEmpty(query.getDictItemId()) + && StringUtils.isEmpty(query.getValue()) + && StringUtils.isEmpty(query.getDisplay()) + && StringUtils.isEmpty(query.getParentId())) + .map(dc -> dictItemRepo.findByDictDictCodeOrderByOrderNumAsc(dc).stream().map(dictItem -> { + DictItemDTO dictItemDto = mapDictItemListResultCached(dictItem); + dictItemDTOMap.put(dictItemDto.getDictItemId(), dictItemDto); + return dictItemDto; + }).collect(Collectors.toList())) + .orElseGet(() -> dictItemRepo.findAll(buildDictItemQueryCondition(query)).stream().map(this::mapDictItemListResult).collect(Collectors.toList())); + if (query.getNeedReturnTree() != null && query.getNeedReturnTree()) { // 需要挽回树型结构 + return buildDictTree(dictItemDTOMap); + } + return dictItems; + } + + /** + * 返回字典树型结构 + * + * @param dictItemDTOMap 树形字典结构 + * @return 字典数据 + */ + private List buildDictTree(Map dictItemDTOMap) { + List dictItemDTOS = new ArrayList<>(); + dictItemDTOMap.forEach((key, value) -> { + if ("0".equals(value.getParentId())) { // 顶层 + dictItemDTOS.add(value); + } else { + DictItemDTO parent = dictItemDTOMap.get(value.getParentId()); + if (parent != null) { + parent.getChildren().add(value); + } + } + }); + return dictItemDTOS; + } + + public Page pageDictItems(DictItemQuery query) { + return dictItemRepo.findAll(buildDictItemQueryCondition(query), + PageableHelper.buildPageRequest(query.page(), query.pageSize(), query.sort(), query.dir())); + } + + + private Specification buildDictItemQueryCondition( + DictItemQuery query) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.and(Stream.of( + Optional.ofNullable(query.getDictItemId()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(DictItem_.dictItemId), f)), + Optional.ofNullable(query.getValue()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(DictItem_.value), f)), + Optional.ofNullable(query.getValueMarray()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> criteriaBuilder.like(root.get(DictItem_.value), f + "%")), + Optional.ofNullable(query.getDisplay()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> criteriaBuilder.like(root.get(DictItem_.display), f + "%")), + Optional.ofNullable(query.getQueryParam()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> + criteriaBuilder.or( + criteriaBuilder.like(root.get(DictItem_.display), "%" + f + "%"), + criteriaBuilder.like(root.get(DictItem_.simplePinyin), "%" + f + "%"), + criteriaBuilder.like(root.get(DictItem_.allPinyin), "%" + f + "%"), + criteriaBuilder.like(root.get(DictItem_.value), f) + ) + ), + Optional.ofNullable(query.getDictCode()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(DictItem_.dict).get(Dict_.dictCode), f)), + Optional.ofNullable(query.getDictId()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(DictItem_.dict).get(Dict_.DICT_ID), f)), + Optional.ofNullable(query.getParentId()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(DictItem_.parent).get(DictItem_.dictItemId), f)) + ) + .filter(Optional::isPresent).map(Optional::get).toArray(Predicate[]::new))); + } + + /** + * 通过ID获取单个字典 + * + * @param dictId 字典ID + * @return 字典 + */ + public Optional findDictById(String dictId) { + return dictRepo.findById(dictId).map(this::mapDictFindOneResult); + } + + /** + * 获取单个字典 + * + * @param dictCode 字典代码 + * @return 字典 + */ + public Optional findOneDict(@NotNull String dictCode) { + return dictRepo.findByDictCode(dictCode).map(this::mapDictFindOneResult); + } + + /** + * 获取单个字典项 + * + * @param dictItemId 字典项id + * @return 字典项 + */ + public Optional findDictItemById(String dictItemId) { + return dictItemRepo.findById(dictItemId); + } + + private Optional findOneDictItem(@NotNull String dictCode, @NotNull String value) { + return dictItemRepo.findByDictDictCodeAndValue(dictCode, value); + } + + private DictDTO mapDictListResult(@NotNull Dict d) { + if (d == null) + return null; + DictDTO.DictDTOBuilder builder = DictDTO.builder() + .dictId(d.getDictId()) + .dictCode(d.getDictCode()) + .dictType(d.getDictType()) + .description(d.getDescription()) + .simplePinyin(d.getSimplePinyin()) + .allPinyin(d.getAllPinyin()) + .createBy(d.getCreateBy()) + .createTime(d.getCreateTime()) + .lastModifiedBy(d.getLastModifiedBy()) + .lastModifiedTime(d.getLastModifiedTime()) + .dictName(d.getDictName()); + this.findOneDictItem(Constants.Dicts.dictTypes, d.getDictType().getValue()).ifPresent(di -> builder.dictTypeName(di.getDisplay())); + return builder.build(); + } + + private DictDTO mapDictFindOneResult(@NotNull Dict d) { + DictDTO dict = mapDictListResult(d); + dict.getDictItems().addAll(d.getDictItems().stream().map(this::mapDictItemListResult).collect(LinkedHashSet::new, Set::add, Set::addAll)); + return dict; + } + + private DictItemDTO mapDictItemListResult(@NotNull DictItem di) { + DictItemDTO.DictItemDTOBuilder builder = DictItemDTO.builder() + .dictItemId(di.getDictItemId()) + .value(di.getValue()) + .display(di.getDisplay()) + .simplePinyin(di.getSimplePinyin()) + .allPinyin(di.getAllPinyin()) + .orderNum(di.getOrderNum()) + .createBy(di.getCreateBy()) + .createTime(di.getCreateTime()) + .lastModifiedBy(di.getLastModifiedBy()) + .lastModifiedTime(di.getLastModifiedTime()) + .dict(mapDictListResult(di.getDict())); + Optional.ofNullable(di.getParent()).ifPresent(diParent -> { + DictItemDTO parent = DictItemDTO.builder() + .dictItemId(diParent.getDictItemId()) + .value(diParent.getValue()) + .display(diParent.getDisplay()) + .simplePinyin(diParent.getSimplePinyin()) + .allPinyin(diParent.getAllPinyin()) + .orderNum(diParent.getOrderNum()) + .createBy(di.getCreateBy()) + .createTime(di.getCreateTime()) + .lastModifiedBy(di.getLastModifiedBy()) + .lastModifiedTime(di.getLastModifiedTime()) + .dict(mapDictListResult(diParent.getDict())) + .build(); + builder.parent(parent); + }); + return builder.build(); + } + + private DictItemDTO mapDictItemListResultCached(@NotNull DictItem di) { + DictItemDTO.DictItemDTOBuilder builder = DictItemDTO.builder() + .dictItemId(di.getDictItemId()) + .value(di.getValue()) + .display(di.getDisplay()) + .simplePinyin(di.getSimplePinyin()) + .allPinyin(di.getAllPinyin()) + .orderNum(di.getOrderNum()) + .createBy(di.getCreateBy()) + .createTime(di.getCreateTime()) + .lastModifiedBy(di.getLastModifiedBy()) + .lastModifiedTime(di.getLastModifiedTime()) + .children(new HashSet<>()) + .parentId(di.getParent() == null ? null : di.getParent().getDictItemId()) + .dict(this.mapDictListResult(di.getDict())); + return builder.build(); + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/service/DynamicEncryptService.java b/server/src/main/java/com/aisino/iles/core/service/DynamicEncryptService.java new file mode 100644 index 0000000..f7bfa43 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/DynamicEncryptService.java @@ -0,0 +1,92 @@ +package com.aisino.iles.core.service; + +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.BCUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.SM2; +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.util.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +import java.security.KeyPair; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * 动态加密服务 + * @author hx + * @since 2023-05-24 + */ +@Service +@Slf4j +public class DynamicEncryptService { + + /** + * 生成动态加密key + * + * @param sign 用户凭证 + * @return 动态加密key + */ + public Map generateDynamicEncryptKey(@NonNull String sign){ + String key = StrUtil.format(Constants.RedisKeyPrefix.requestBodyEncrypt.getValue(), sign); + return Optional.ofNullable(RedisUtil.get(key, String.class)) + .map(ek-> generateDynamicKeyPair(ek,sign)) + .orElseGet(() -> { + try { + String encryptKey = HexUtil.encodeHexStr(SmUtil.sm4().getSecretKey().getEncoded()); + boolean setResult = RedisUtil.setnx(key, encryptKey, Duration.ofMinutes(10)); + if (!setResult) { + encryptKey = Objects.requireNonNull(RedisUtil.get(key, String.class)); + } + // 使用sm2生成钥匙对,使用hutool工具包的sm2工具,生成钥匙对,然后用公钥对encrptkey加密, + // 返回给调用者的内容是密钥和加密后的sm4对称密钥 + return generateDynamicKeyPair(encryptKey, sign); + } catch (Exception e) { + throw new BusinessError("生成动态加密key失败",Constants.Exceptions.data_encrypt_decrypt_error,e); + } + }); + } + + private Map generateDynamicKeyPair(String encryptKey, String sign) { + String lockKey = StrUtil.format(Constants.RedisKeyPrefix.requestBodyEncryptLock.getValue(),sign); + log.debug("生成动态加密lock key:{}",lockKey); + String privateKey; + String publicKey; + if (!RedisUtil.hhasKey(lockKey, "publicKey")) { + KeyPair pair = SecureUtil.generateKeyPair("SM2"); + privateKey = HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(pair.getPrivate())); + publicKey = HexUtil.encodeHexStr(BCUtil.encodeECPublicKey(pair.getPublic(),false)); + RedisUtil.hsetnx(lockKey, "publicKey", publicKey); + RedisUtil.hsetnx(lockKey, "privateKey", privateKey); + RedisUtil.expire(lockKey, Duration.ofSeconds(2)); + } + privateKey = Objects.requireNonNull(RedisUtil.hget(lockKey, "privateKey", String.class)); + publicKey = Objects.requireNonNull(RedisUtil.hget(lockKey, "publicKey", String.class)); + + HashMap resultMap = new HashMap<>(); + SM2 sm2 = SmUtil.sm2(privateKey, publicKey); + resultMap.put("encryptedKey", sm2.encryptBcd(encryptKey, KeyType.PublicKey)); + resultMap.put("publicSign", privateKey); + return resultMap; + } + /** + * 获取动态加密key + * @param sign 凭证 + * @return 动态加密key + */ + public String getDynamicEncryptKey(@NonNull String sign) { + String key = StrUtil.format(Constants.RedisKeyPrefix.requestBodyEncrypt.getValue(), sign); + log.debug("获取动态加密key:{}",key); + return Optional.ofNullable(RedisUtil.get(key, String.class)) + .orElseThrow(()-> new BusinessError("获取动态加密key失败目标key不存在",Constants.Exceptions.data_encrypt_decrypt_not_found_key)); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/FunctionService.java b/server/src/main/java/com/aisino/iles/core/service/FunctionService.java new file mode 100644 index 0000000..26e50c0 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/FunctionService.java @@ -0,0 +1,117 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.model.Function; +import com.aisino.iles.core.model.Function_; +import com.aisino.iles.core.repository.FunctionRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 功能 + */ +@Service +@Validated +@Transactional(readOnly = true) +public class FunctionService { + private final FunctionRepo functionRepo; + + @Autowired + public FunctionService(FunctionRepo functionRepo) { + this.functionRepo = functionRepo; + } + + /** + * 保存功能 + * + * @param function 功能 + * @return 保存的功能 + */ + @Transactional + public Function saveFunction(@NotNull @Valid Function function) { + return functionRepo.save(function); + } + + /** + * 删除功能 + * + * @param funcCode 功能代码 + */ + @Transactional + public void removeFunction(@NotNull String funcCode) { + functionRepo.deleteById(funcCode); + } + + /** + * 获取功能信息分页 + * + * @param page 页码 + * @param pagesize 每页数 + * @param sort 排序列 + * @param dir 升降序 + * @param funcCode 功能代码 + * @param funcName 功能名称 + * @param description 功能描述 + * @return 分页的功能 + */ + public Page listFunctions(Integer page, Integer pagesize, String sort, String dir, String funcCode, String funcName, String description) { + return functionRepo.findAll( + buildQueryCondition(funcCode, funcName, description), + PageableHelper.buildPageRequest(page, pagesize, sort, dir) + ); + } + + /** + * 查询条件 + * + * @param funcCode 功能代码 + * @param funcName 功能名称 + * @param description 功能描述 + * @return 条件 + */ + private Specification buildQueryCondition(String funcCode, String funcName, String description) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + if (!StringUtils.isEmpty(funcCode)) { + predicates.add(criteriaBuilder.equal(root.get(Function_.funcCode), funcCode)); + } + if (!StringUtils.isEmpty(funcName)) { + predicates.add(criteriaBuilder.like(root.get(Function_.funcName), "%" + funcName + "%")); + } + if (!StringUtils.isEmpty(description)) { + predicates.add(criteriaBuilder.like(root.get(Function_.description), "%" + description + "%")); + } + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }); + } + + /** + * 获取单个功能 + * + * @param funcCode 功能代码 + * @return 功能 + */ + public Optional findOneFunction(@NotNull String funcCode) { + return functionRepo.findById(funcCode); + } + + public List listFunctions(String funcCode, String funcName, String description) { + return functionRepo.findAll(buildQueryCondition(funcCode, funcName, description)); + } + + public List listFunctions() { + return listFunctions(null, null, null); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/GenerateCodeService.java b/server/src/main/java/com/aisino/iles/core/service/GenerateCodeService.java new file mode 100644 index 0000000..b0a1a48 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/GenerateCodeService.java @@ -0,0 +1,234 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.core.model.GenerateCode; +import com.aisino.iles.core.repository.GenerateCodeRepository; +import com.aisino.iles.core.repository.GlobalParamRepo; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * 生成代码工具 + */ +@Service +@Slf4j +public class GenerateCodeService { + private final GenerateCodeRepository generateCodeRepository; + private final GlobalParamRepo globalParamRepo; + private final TransactionTemplate transactionTemplate; + private final RedisTemplate redisTemplate; + + public GenerateCodeService(GenerateCodeRepository generateCodeRepository, GlobalParamRepo globalParamRepo, TransactionTemplate transactionTemplate, RedisTemplate redisTemplate) { + this.generateCodeRepository = generateCodeRepository; + this.globalParamRepo = globalParamRepo; + this.transactionTemplate = transactionTemplate; + this.redisTemplate = redisTemplate; + } + + /** + * @param provinceSimplePinyin 省份简拼 + * @param codeCategory 编码类别 + * @param codePrefix 编码前缀,行政区划等。 + * @param generateRule 生成时间规则 0年;1年月;2日期;3没有年份;4两位年份 + * @return 指定类型编码 + */ + @SneakyThrows + public String buildCode(String provinceSimplePinyin, String codeCategory, String codePrefix, int generateRule) { + return buildCodeByRedis(provinceSimplePinyin, codeCategory, codePrefix, generateRule); + } + + /** + * 生成指定类型编码 + * + * @param provinceSimplePinyin 省份简拼 + * @param codeCategory 编码类别 + * @param codePrefix 编码前缀,行政区划等。 + * @param generateRule 生成时间规则 0年;1年月;2日期;3没有年份;4两位年份 + * @return 指定类型编码 + */ + private String buildCode2(String provinceSimplePinyin, String codeCategory, String codePrefix, int generateRule) { + Constants.GlobalParams.InnerNetFlag innerNetFlag = globalParamRepo.findById(Constants.GlobalParams.InnerNetFlag.code) + .map(g -> Constants.GlobalParams.InnerNetFlag.fromValue(g.getGlobalParamValue())) + .orElse(Constants.GlobalParams.InnerNetFlag.outter); + DateTimeFormatter formatter; + if (generateRule == 0) + formatter = DateTimeFormatter.ofPattern("yyyy"); + else if (generateRule == 1) + formatter = DateTimeFormatter.ofPattern("yyyyMM"); + else if (generateRule == 2) + formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + else if (generateRule == 3) + formatter = DateTimeFormatter.ofPattern(""); + else if (generateRule == 4) + formatter = DateTimeFormatter.ofPattern("yy"); + else if (generateRule == 5) + formatter = DateTimeFormatter.ofPattern("yyMMdd"); + else + formatter = DateTimeFormatter.ofPattern("yyyy"); + + transactionTemplate.setPropagationBehavior(Propagation.REQUIRES_NEW.value()); + transactionTemplate.setTimeout(11); + transactionTemplate.setReadOnly(false); + String code = transactionTemplate.execute(status -> { + GenerateCode generateCode = generateCodeRepository.findByProvinceSimplePinyinAndCodeCategoryAndCodePrefixAndGenerateRule(provinceSimplePinyin, codeCategory, codePrefix, generateRule) + .map(gc -> { + if (!gc.getTimeParameter().equals(LocalDate.now().format(formatter))) { + gc.setSerialNumber(null); + } + gc.setTimeParameter(LocalDate.now().format(formatter)); + return gc; + }) + .orElseGet(() -> GenerateCode.builder() + .provinceSimplePinyin(provinceSimplePinyin) + .codeCategory(codeCategory) + .generateRule(generateRule) + .codePrefix(codePrefix) + .timeParameter(LocalDate.now().format(formatter)) + .build()); + + // 业务部分 + // 目前暂时实现几个例子,原过程里面业务内容是在是太多了,具体的业务信息需要使用到生成编码工具的,如果业务没有的,就在这里 + // 新增自己的业务就好了。 + + if (provinceSimplePinyin.equals("hn") && codeCategory.equals("BKRY")) { // 布控人员编码 + processGenerateCode(this::generatorByCodePrefixWithTimeWithSerialNumber, "0001", generateCode); + } else if (provinceSimplePinyin.equals("hn") && codeCategory.equals("ICKH")) { // IC卡受理号 + processGenerateCode(this::generatorByCodePrefixWithTimeWithSerialNumber, "000001", generateCode); + }else if (codeCategory.equals("case")) { + //生成案件号 + processGenerateCode(gc -> "市应急卷〔" + gc.getTimeParameter() + "〕第(ZC-" +gc.getSerialNumber() + ")号", "001", generateCode); + } else { + // 默认生成编码 + if (innerNetFlag == Constants.GlobalParams.InnerNetFlag.inner) { + processGenerateCode(this::generatorByCodeCategoryWithCodePrefixWithTimeWithSerialNumber, "5001", generateCode); + } else + processGenerateCode(this::generatorByCodeCategoryWithCodePrefixWithTimeWithSerialNumber, "0001", generateCode); + } + // 保存生成代码信息 + generateCode = generateCodeRepository.save(generateCode); + // 返回代码 + return generateCode.getCode(); + }); + return code; + } + + /** + * 通过redis实现加锁,生成编码 + * + * @param provinceSimplePinyin 省份简拼 + * @param codeCategory 编码类别 + * @param codePrefix 编码前缀,行政区划等。 + * @param generateRule 生成时间规则 0年;1年月;2日期;3没有年份;4两位年份 + * @return 指定类型编码 + */ + public String buildCodeByRedis(String provinceSimplePinyin, String codeCategory, String codePrefix, int generateRule) throws InterruptedException { + // 加锁 + String lockKey = String.format("lock-key-generate-code-%s-%s-%s-%s", provinceSimplePinyin, codeCategory, codePrefix, generateRule); + String lockValue = UUID.randomUUID().toString(); + Boolean actionR = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 12, TimeUnit.SECONDS); + while (Boolean.FALSE.equals(actionR)) { + Thread.sleep(50); + actionR = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 12, TimeUnit.SECONDS); + } + String code; + try { + code = buildCode2(provinceSimplePinyin, codeCategory, codePrefix, generateRule); + } finally { + // 解锁 + if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { + redisTemplate.delete(lockKey); + } + } + return code; + } + + /** + * 生成编码处理工具 + * + * @param codeGenerator 编码生成方式函数 + * @param defaultSerialNumber 默认流水号 + * @param generateCode 生成编码数据实体 + */ + private void processGenerateCode( + Function codeGenerator, + String defaultSerialNumber, + GenerateCode generateCode) { + generateCode.setSerialNumber(!StringUtils.hasText(generateCode.getSerialNumber()) ? + defaultSerialNumber : String.format("%" + generateCode.getSerialNumber().length() + "d", Long.parseLong(generateCode.getSerialNumber()) + 1).replaceAll(" ", "0")); + generateCode.setCode(codeGenerator.apply(generateCode)); + } + + /** + * 生成编码处理工具 减一 + * + * @param codeGenerator 编码生成方式函数 + * @param defaultSerialNumber 默认流水号 + * @param generateCode 生成编码数据实体 + */ + private void processGenerateCodeCut( + Function codeGenerator, + String defaultSerialNumber, + GenerateCode generateCode) { + generateCode.setSerialNumber(!StringUtils.hasText(generateCode.getSerialNumber()) ? + defaultSerialNumber : String.format("%" + generateCode.getSerialNumber().length() + "d", Long.parseLong(generateCode.getSerialNumber()) - 1).replaceAll(" ", "0")); + generateCode.setCode(codeGenerator.apply(generateCode)); + } + + /* + 提供三种主要的编码生成方式函数,以适应大多数需要。 + 对于一些特殊的模式应该在使用的地方通过匿名函数的方式加入调用。 + 如果有多个业务可以同时使用的编码生常规则,可以参照下面三种方式来提供共有方法。 + */ + + /** + * 编码生成方式函数,组合行政区划/编码前缀+5+流水号 + * + * @param generateCode 生成编码数据实体 + * @return 编码信息 + */ + private String generatorByCodePrefixWith5WithSerialNumber(GenerateCode generateCode) { + return generateCode.getCodePrefix() + "5" + generateCode.getSerialNumber(); + } + + /** + * 编码生成方式函数,组合行政区划/编码前缀+时间+流水号 + * + * @param generateCode 生成编码数据实体 + * @return 编码信息 + */ + private String generatorByCodePrefixWithTimeWithSerialNumber(GenerateCode generateCode) { + return generateCode.getCodePrefix() + generateCode.getTimeParameter() + generateCode.getSerialNumber(); + } + + /** + * 编码生成方式函数,组合编码类别+行政区划/编码前缀+时间+流水号 + * + * @param generateCode 生成编码数据实体 + * @return 编码信息 + */ + private String generatorByCodeCategoryWithCodePrefixWithTimeWithSerialNumber(GenerateCode generateCode) { + return generateCode.getCodeCategory() + generateCode.getCodePrefix() + generateCode.getTimeParameter() + generateCode.getSerialNumber(); + } + + /** + * 编码生常方式函数,组合编码前缀+流水号 + * + * @param generateCode 生成编码数据实体 + * @return 编码信息 + */ + private String generatorByCodePrefixWithSerialNumber(GenerateCode generateCode) { + return generateCode.getCodePrefix() + generateCode.getSerialNumber(); + } + +} diff --git a/server/src/main/java/com/aisino/iles/core/service/GlobalParamService.java b/server/src/main/java/com/aisino/iles/core/service/GlobalParamService.java new file mode 100644 index 0000000..8ae6ed4 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/GlobalParamService.java @@ -0,0 +1,143 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.GlobalParam; +import com.aisino.iles.core.model.GlobalParam_; +import com.aisino.iles.core.repository.GlobalParamRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.constraints.NotNull; +import java.util.*; + +@Service +@Validated +public class GlobalParamService { + private final GlobalParamRepo globalParamRepo; + /** + * 不需要认证的全局参数代码列表 + */ + private final String[] noNeedAuthGlobalParamCodes = new String[]{ + Constants.GlobalParams.ApplicationVersion.code, + Constants.GlobalParams.InnerNetFlag.code, + Constants.GlobalParams.HOTEL_SYSTEM_URL, + Constants.GlobalParams.IMG_BASE64_PREFFIX, + Constants.GlobalParams.PORTAL_INDUSTRY_SHOWCASE + }; + + @Autowired + public GlobalParamService(GlobalParamRepo globalParamRepo) { + this.globalParamRepo = globalParamRepo; + } + + /** + * 新增全局参数 + * + * @param globalParam 全局参数 + * @return 新增的全局参数 + */ + @Transactional + public GlobalParam saveGlobalParam(@NotNull @Validated GlobalParam globalParam) { + if (globalParamRepo.findById(globalParam.getGlobalParamCode()).isPresent()) { + throw new BusinessError("新增全局参数 该全局参数代码已存在"); + } + return globalParamRepo.save(globalParam); + } + + /** + * 修改全局参数 + * + * @param globalParam 全局参数 + */ + @Transactional + public void modifyGlobalParam(@NotNull @Validated GlobalParam globalParam) { + globalParamRepo.findById(globalParam.getGlobalParamCode()) + .map(g -> { + BeanUtils.copyNoNullProperties(globalParam, g); + return globalParamRepo.save(g); + }) + .orElseThrow(() -> new BusinessError("修改全局参数 该全局参数不存在")); + } + + /** + * 删除全局参数 + * + * @param globalParamCode 全局参数主键 + */ + @Transactional + public void removeGlobalParam(@NotNull String globalParamCode) { + globalParamRepo.deleteById(globalParamCode); + } + + /** + * 批量删除全局参数 + * + * @param globalParamCodes 多个全局参数主键 + */ + @Transactional + public void removeGlobalParams(@NotNull Set globalParamCodes) { + globalParamCodes.forEach(globalParamRepo::deleteById); + } + + /** + * 获取全局参数列表分页 + * + * @param page 页码 + * @param pagesize 每页显示 + * @param sort 排序列名称 + * @param dir 升降序 + * @param globalParamCode 全局参数代码 + * @param globalParamName 全局参数名称 + * @param globalParamValue 全局参数值 + * @return 分页的全局参数列表 + */ + public Page listGlobalParams(Integer page, Integer pagesize, String sort, String dir, String globalParamCode, String globalParamName, String globalParamValue) { + return globalParamRepo.findAll( + buildQueryCondition(globalParamCode, globalParamName, globalParamValue), + PageableHelper.buildPageRequest(page, pagesize, sort, dir) + ); + } + + /** + * 生成查询条件 + * + * @param globalParamCode 全局参数代码 + * @param globalParamName 全局参数名称 模糊匹配 + * @param globalParamValue 全局参数值 + * @return 查询条件 + */ + private Specification buildQueryCondition(String globalParamCode, String globalParamName, String globalParamValue) { + return Specification.where(((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + if (!StringUtils.isEmpty(globalParamCode)) + predicates.add(criteriaBuilder.equal(root.get(GlobalParam_.globalParamCode), globalParamCode)); + if (!StringUtils.isEmpty(globalParamName)) + predicates.add(criteriaBuilder.like(root.get(GlobalParam_.globalParamName), "%" + globalParamName + "%")); + if (!StringUtils.isEmpty(globalParamValue)) + predicates.add(criteriaBuilder.like(root.get(GlobalParam_.globalParamValue), globalParamValue)); + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + })); + } + + public Optional findByCode(String code) { + return globalParamRepo.findByGlobalParamCode(code); + } + + /** + * 获取无需登录认证的全局参数列表 + * + * @return 全局参数信息 + */ + public List listNoAuth() { + return globalParamRepo.findByGlobalParamCodeIn(Arrays.asList(noNeedAuthGlobalParamCodes)); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/JurisdictionService.java b/server/src/main/java/com/aisino/iles/core/service/JurisdictionService.java new file mode 100644 index 0000000..07adef2 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/JurisdictionService.java @@ -0,0 +1,245 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.Jurisdiction; +import com.aisino.iles.core.model.Jurisdiction_; +import com.aisino.iles.core.repository.JurisdictionRepo; +import com.aisino.iles.core.util.RedisUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 管辖机构 + */ +@Service +@Transactional(readOnly = true) +@Validated +public class JurisdictionService { + private final JurisdictionRepo jurisdictionRepo; + + @Autowired + public JurisdictionService(JurisdictionRepo jurisdictionRepo) { + this.jurisdictionRepo = jurisdictionRepo; + } + + /** + * 新增机构 + * @param jurisdiction 机构信息 + * @return 机构信息 + */ + @Transactional + public Jurisdiction addJurisdiction(@NotNull @Valid Jurisdiction jurisdiction) { + jurisdiction.setJurisdictionId(null); + Optional.ofNullable(jurisdiction.getParent()).filter(p -> p.getJurisdictionId() != null) + .flatMap(p -> jurisdictionRepo.findById(p.getJurisdictionId())) + .ifPresent(jurisdiction::setParent); + + if (StringUtils.isEmpty(jurisdiction.getJurisFullCode()) && !StringUtils.isEmpty(jurisdiction.getJurisdictionCode())) { + String parentFullCode = jurisdiction.getParent() != null ? jurisdiction.getParent().getJurisFullCode() : ""; + jurisdiction.setJurisFullCode(parentFullCode + jurisdiction.getJurisdictionCode() + "."); + } + if (StringUtils.isEmpty(jurisdiction.getJurisSimpleCode()) && !StringUtils.isEmpty(jurisdiction.getJurisdictionCode())) { + jurisdiction.setJurisSimpleCode(StringUtils.trimTrailingString(jurisdiction.getJurisdictionCode(), "00")); + } + + Jurisdiction save = jurisdictionRepo.save(jurisdiction); + if (save.getParent() != null) { + save.getParent().setLeaf(false); + } + return save; + } + + /** + * 修改机构 + * @param jurisdiction 机构信息 + */ + @Transactional + @Validated + public void modifyJurisdiction(@NotNull @Valid Jurisdiction jurisdiction) { + jurisdictionRepo.findById(jurisdiction.getJurisdictionId()) + .map(j -> { + String newFullCode = j.getParent().getJurisFullCode() + jurisdiction.getJurisdictionCode() + "."; + if (!Objects.equals(jurisdiction.getJurisdictionCode(), j.getJurisdictionCode())) { + // 如果机构代码发生了变化, 需要调整所有子机构的机构全码 和 当前机构的简码 + jurisdictionRepo.findByJurisFullCodeStartingWithAndJurisFullCodeNot(jurisdiction.getJurisFullCode(), jurisdiction.getJurisFullCode()) + .forEach(subJuri-> subJuri.setJurisFullCode(newFullCode+subJuri.getJurisdictionCode())); + } + jurisdiction.setJurisFullCode(newFullCode); + jurisdiction.setJurisSimpleCode(StringUtils.trimTrailingString(jurisdiction.getJurisdictionCode(), "00")); + BeanUtils.copyNoNullProperties(jurisdiction, j); + return j; + }) + .orElseThrow(() -> new BusinessError("无法获取到该id的机构信息")); + } + + /** + * 删除机构 + * @param jurisdictionId 机构信息ID + */ + @Transactional + public void removeJurisdiction(String jurisdictionId) { + jurisdictionRepo.findById(jurisdictionId) + .map(j -> { + jurisdictionRepo.deleteById(j.getJurisdictionId()); + jurisdictionRepo.flush(); + j.getParent().setLeaf(j.getParent().getChildren().isEmpty()); + return j; + }) + .orElseThrow(() -> new BusinessError("无法获取到该id的机构信息")); + + } + + /** + * 批量删除机构 + * @param jurisdictionIdSet 机构信息ID集合 + */ + @Transactional + public void removeJurisdictions(Set jurisdictionIdSet) { + jurisdictionRepo.findAllById(jurisdictionIdSet) + .stream().map(j -> { + jurisdictionRepo.delete(j); + jurisdictionRepo.flush(); + return j.getParent(); + }) + .reduce((a,b)-> a.getParent()) + .ifPresent(p-> p.setLeaf(p.getChildren().isEmpty())); + } + + /** + * 分页查询机构信息 + * @return 机构信息 + */ + public Page listJurisdictions(Integer page, Integer pagesize, String sort, String dir, + String jurisdictionId, + String jurisdictionCode, + String jurisdictionName, + Integer jurisdictionLevel, + Jurisdiction parent, + String jurisFullCode, + String jurisSimpleCode + ) { + return jurisdictionRepo.findAll( + buildJurisdictionQueryCondition(jurisdictionId, jurisdictionCode, jurisdictionName, jurisdictionLevel, parent, jurisFullCode, jurisSimpleCode), + PageableHelper.buildPageRequest(page, pagesize, sort, dir) + ).map(JurisdictionService::mapResultJurisdiction); + } + + /** + * 根据条件查询机构信息(列表) + * @return 机构信息列表 + */ + public List listJurisdictions( + String jurisdictionId, + String jurisdictionCode, + String jurisdictionName, + Integer jurisdictionLevel, + Jurisdiction parent, + String jurisFullCode, + String jurisSimpleCode + ) { + return jurisdictionRepo.findAll(buildJurisdictionQueryCondition(jurisdictionId, jurisdictionCode, jurisdictionName, jurisdictionLevel, parent, jurisFullCode, jurisSimpleCode)).stream().map(JurisdictionService::mapResultJurisdiction).collect(Collectors.toList()); + } + + /** + * 机构信息动态查询条件 + */ + private Specification buildJurisdictionQueryCondition( + String jurisdictionId, + String jurisdictionCode, + String jurisdictionName, + Integer jurisdictionLevel, + Jurisdiction parent, + String jurisFullCode, + String jurisSimpleCode + ) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + if (jurisdictionId != null) { + predicates.add(criteriaBuilder.equal(root.get(Jurisdiction_.jurisdictionId), jurisdictionId)); + } + if (!StringUtils.isEmpty(jurisdictionCode)) { + predicates.add(criteriaBuilder.equal(root.get(Jurisdiction_.jurisdictionCode), jurisdictionCode)); + } + if (!StringUtils.isEmpty(jurisdictionName)) { + predicates.add(criteriaBuilder.like(root.get(Jurisdiction_.jurisdictionName), jurisdictionName + "%")); + } + if (jurisdictionLevel != null) { + predicates.add(criteriaBuilder.equal(root.get(Jurisdiction_.jurisdictionLevel), jurisdictionLevel)); + } + if (parent != null) { + if (parent.getJurisdictionId() != null) { + predicates.add(criteriaBuilder.equal(root.get(Jurisdiction_.parent).get(Jurisdiction_.jurisdictionId), parent.getJurisdictionId())); + } + } + if (!StringUtils.isEmpty(jurisFullCode)) { + predicates.add(criteriaBuilder.like(root.get(Jurisdiction_.jurisFullCode), jurisFullCode + "%")); + } + if (!StringUtils.isEmpty(jurisSimpleCode)) { + predicates.add(criteriaBuilder.like(root.get(Jurisdiction_.jurisSimpleCode), jurisSimpleCode + "%")); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }); + } + + /** + * 获取单个机构信息 + * + * @param jurisdictionId 机构信息ID + * @return 机构信息 + */ + public Optional findJurisdictionById(String jurisdictionId) { + return jurisdictionRepo.findById(jurisdictionId).map(JurisdictionService::mapResultJurisdiction); + } + + /** + * 获取单个机构信息(根据机构代码) + * @param jurisdictionCode 机构代码 + * @return 机构信息 + */ + public Optional findOneJurisdiction(@NotNull(message = "机构代码不能为空") String jurisdictionCode) { + return jurisdictionRepo.findByJurisdictionCode(jurisdictionCode).map(JurisdictionService::mapResultJurisdiction); + } + + public static Jurisdiction mapResultJurisdiction(Jurisdiction j) { + Jurisdiction jurisdiction = new Jurisdiction(); + Optional.ofNullable(j.getParent()) + .ifPresent(jp -> { + Jurisdiction prt = new Jurisdiction(); + BeanUtils.copyProperties(jp, prt, Jurisdiction_.PARENT, Jurisdiction_.CHILDREN, Jurisdiction_.USERS); + jurisdiction.setParent(prt); + }); + BeanUtils.copyProperties(j, jurisdiction, Jurisdiction_.PARENT, Jurisdiction_.CHILDREN, Jurisdiction_.USERS); + return jurisdiction; + } + + /** + * 查询所有机构信息 + * + * @return 机构信息 + */ + public List findJurisdictionAll() { + if (RedisUtil.hasKey(Constants.CacheKeys.Jurisdiction.all)) { + // 使用带缓存的查询全部数据 + return RedisUtil.lrange(Constants.CacheKeys.Jurisdiction.all, 0, -1, Jurisdiction.class); + } else { + return jurisdictionRepo.findAllCached().stream().map(JurisdictionService::mapResultJurisdiction).peek(j->RedisUtil.rpush(Constants.CacheKeys.Jurisdiction.all, j)).collect(Collectors.toList()); + } + + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/LoginService.java b/server/src/main/java/com/aisino/iles/core/service/LoginService.java new file mode 100644 index 0000000..dc00630 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/LoginService.java @@ -0,0 +1,267 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.iface.Logger; +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.InetAddressUtil; +import com.aisino.iles.core.exception.LoginError; +import com.aisino.iles.core.model.*; +import com.aisino.iles.core.model.dto.LoginUserDto; +import com.aisino.iles.core.model.dto.MenuTreeNodeDTO; +import com.aisino.iles.core.model.enums.LoginLogType; +import com.aisino.iles.core.model.enums.UserStatus; +import com.aisino.iles.core.repository.LoginLogRepo; +import com.aisino.iles.core.repository.PermissionRepo; +import com.aisino.iles.core.repository.UserRepo; +import com.aisino.iles.core.util.TokenUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Deprecated +@Service("loginServiceCore") +@Validated +@Transactional(readOnly = true) +public class LoginService implements Logger { + private final UserRepo userRepo; + private final PermissionRepo permissionRepo; + private final LoginLogRepo loginLogRepo; + private final ObjectMapper objectMapper; + + @Autowired + public LoginService(UserRepo userRepo, PermissionRepo permissionRepo, LoginLogRepo loginLogRepo, ObjectMapper objectMapper) { + this.userRepo = userRepo; + this.permissionRepo = permissionRepo; + this.loginLogRepo = loginLogRepo; + this.objectMapper = objectMapper; + } + + public Token login(@NotNull String username, @NotNull String password) { + User user; + try { + user = userRepo.findByUsername(username).map(u -> { + if (u.getStatus() == UserStatus.deleted) + throw new LoginError("用户已被删除", Constants.Exceptions.login_deleted_error); + else if (u.getStatus() == UserStatus.lock) + throw new LoginError("用户被锁定", Constants.Exceptions.login_user_locked_error); + else if (u.getStatus() == UserStatus.unnormal) + throw new LoginError("用户状态异常", Constants.Exceptions.login_user_type_abnormal_error); + else if (!u.getPassword().equals(password)) + throw new LoginError("密码不正确", Constants.Exceptions.login_password_error); + return u; + }) + .orElseThrow(() -> new LoginError("用户不存在", Constants.Exceptions.login_user_not_exist_error)); + + } catch (LoginError e) { + generateLoginLog(LoginLogType.login, username, e.getCode()); + throw e; + } + + Token token = this.buildTokenInformation(user); +// 登录日志 + generateLoginLog(LoginLogType.login, username, 0); + return token; + } + + /** + * 构建令牌信息 + * + * @param user 登录的用户 + * @return 令牌信息 + */ + public Token buildTokenInformation(@NotNull User user) { + Token token = new Token(); + token.setUid(user.getUserId()); + token.setExpireTime(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).plusDays(7L).toInstant()); + ObjectNode profileJson = objectMapper.createObjectNode(); + + Set permissions = new HashSet<>(); + permissions.addAll(user.getPermissions()); + permissions.addAll(permissionRepo.findByRolesUsersUserId(user.getUserId())); + Permission totalPermission = permissions.stream().reduce(new Permission(), (a, b) -> { + a.getFunctions().addAll(b.getFunctions()); + a.getMenus().addAll(b.getMenus().stream().map(m -> + Menu.builder() + .menuId(m.getMenuId()) + .menuCode(m.getMenuCode()) + .menuName(m.getMenuName()) + .iconCls(m.getIconCls()) + .allPinyin(m.getAllPinyin()) + .simplePinyin(m.getSimplePinyin()) + .iconPath(m.getIconPath()) + .leaf(m.getLeaf()) + .orderNum(m.getOrderNum()) + .parent(m.getParent()) + .path(m.getPath()) + .build()).collect(Collectors.toList())); + a.getResources().addAll(b.getResources()); + return a; + }); + Set userTypeMenuLimits = user.getUserTypes().stream().flatMap(ut -> ut.getMenus().stream()).collect(Collectors.toSet()); // 获取用户类别限制的菜单数据 + profileJson.putPOJO("menus", convertToMenuTreeNodeDTO(buildMenus(totalPermission.getMenus().stream().filter(userTypeMenuLimits::contains).collect(Collectors.toSet())))); // 构建符合用户类别的树形菜单 + profileJson.putPOJO("permission", totalPermission); // 总权限许可里的菜单可能和profile里面的菜单不一致,profile的菜单还接受用户类别里关联菜单的约束 + LoginUserDto u = mappingLoginUserData(user, totalPermission); + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + u.setIpAddress(request.getRemoteAddr()); + u.setLoginTime(LocalDateTime.now()); + profileJson.putPOJO("user", u); + try { + token.setJsonProfile(objectMapper.writeValueAsString(profileJson)); + } catch (JsonProcessingException e) { + logger().error(e.getMessage(), e); + } + token.setToken(TokenUtil.generateToken(token.getUid(), token.getJsonProfile(), token.getExpireTime())); + return token; + } + + /** + * 封装用户登录的用户信息 + * + * @param user 用户信息 + * @return 登录需要的用户信息 + */ + private LoginUserDto mappingLoginUserData(User user, Permission permission) { + LoginUserDto u = new LoginUserDto(); + BeanUtils.copyProperties(user, u, User_.USER_TYPES, User_.JURISDICTIONS); + u.getJurisdictions().addAll(user.getJurisdictions()); + u.getUserTypes().addAll(user.getUserTypes()); + return u; + } + + public Set convertToMenuTreeNodeDTO(Set menus) { + return menus.stream().map(m -> { + MenuTreeNodeDTO.MenuTreeNodeDTOBuilder builder = MenuTreeNodeDTO.builder() + .menuId(m.getMenuId()) + .menuCode(m.getMenuCode()) + .menuName(m.getMenuName()) + .iconCls(m.getIconCls()) + .iconPath(m.getIconPath()) + .path(m.getPath()) + .orderNum(m.getOrderNum()) + .leaf(m.getLeaf()) + .simplePinyin(m.getSimplePinyin()) + .allPinyin(m.getAllPinyin()) + .children(convertToMenuTreeNodeDTO(m.getChildren())); + Optional.ofNullable(m.getParent()).map(mm -> convertToMenuTreeNodeDTO(Stream.of(Menu.builder() + .menuId(mm.getMenuId()) + .menuCode(mm.getMenuCode()) + .menuName(mm.getMenuName()) + .iconCls(mm.getIconCls()) + .iconPath(mm.getIconPath()) + .path(mm.getPath()) + .orderNum(mm.getOrderNum()) + .leaf(mm.getLeaf()) + .simplePinyin(mm.getSimplePinyin()) + .allPinyin(mm.getAllPinyin()).build()).collect(Collectors.toSet())).stream().findFirst().get()).ifPresent(builder::parent); + return builder.build(); + }).collect(LinkedHashSet::new, + Set::add, + Set::addAll); + } + + /** + * 构建令牌中包含的菜单树 + * + * @param menus 权限里所包含的菜单信息 + * @return 菜单树 + */ + public Set buildMenus(Set menus) { + Comparator comparator = Comparator.comparing(m -> m.getParent().getMenuId(), Comparator.naturalOrder()); + comparator = comparator.thenComparing(Menu::getOrderNum); + Menu root = new Menu(); + root.setMenuId("0"); + root.setMenuCode("root"); + root.setMenuName("主菜单"); + root.setOrderNum(0); + + menus.stream() + .filter(m -> m.getParent() == null) + .forEach(m -> m.setParent(root)); + + root.setChildren(buildMenus(menus, root, comparator)); + return root.getChildren(); + } + + /** + * 构建菜单树(递归子方法) + * + * @param menus 子菜单 + * @param parent 父级菜单 + * @param comparator 排序器 + * @return 排序和树形处理后的子菜单 + */ + public Set buildMenus(Set menus, Menu parent, Comparator comparator) { + Map> collect = menus.stream().sorted(comparator) + .collect(Collectors.partitioningBy(m -> m.getParent().getMenuId().equals(parent.getMenuId()), + Collector.of(LinkedHashSet::new, HashSet::add, (a, b) -> { + a.addAll(b); + return a; + }))); + + return collect.get(true).stream() + .map(m -> { + Menu newMenu = new Menu(); + BeanUtils.copyProperties(m, newMenu, Menu_.PARENT, Menu_.CHILDREN); + Menu prt = new Menu(); + prt.setMenuId(m.getParent().getMenuId()); + prt.setMenuCode(m.getParent().getMenuCode()); + prt.setMenuName(m.getParent().getMenuName()); + prt.setPath(m.getParent().getPath()); + prt.setIconCls(m.getParent().getIconCls()); + prt.setOrderNum(m.getParent().getOrderNum()); + newMenu.setParent(prt); + + Menu nMenu = new Menu(); + BeanUtils.copyProperties(m, nMenu, Menu_.PARENT, Menu_.CHILDREN); + nMenu.setParent(nMenu); + newMenu.getChildren().addAll(buildMenus(collect.get(false), nMenu, comparator)); + return newMenu; + }) + .collect(LinkedHashSet::new, + HashSet::add, + AbstractCollection::addAll); + } + + /** + * 记录登录日志 + * + * @param loginLogType 登录日志类型 + * @param username 用户id + */ + @Transactional + public void generateLoginLog(LoginLogType loginLogType, String username, Integer resultCode) { + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + LoginLog loginLog = new LoginLog(); + loginLog.setLoginLogType(loginLogType); + loginLog.setOperateTime(LocalDateTime.now()); + // 尝试获取真实客户端ip + loginLog.setIpAddress(InetAddressUtil.getIpAddress(request)); + loginLog.setUsername(username); + loginLog.setResultCode(resultCode); + loginLogRepo.save(loginLog); + } + + /** + * 注销 + */ + @Transactional + public void logout(User loginUser) { +// 注销日志 + generateLoginLog(LoginLogType.logout, loginUser.getUsername(), 0); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/MenuService.java b/server/src/main/java/com/aisino/iles/core/service/MenuService.java new file mode 100644 index 0000000..b6fb77e --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/MenuService.java @@ -0,0 +1,292 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.Menu; +import com.aisino.iles.core.model.Menu_; +import com.aisino.iles.core.repository.MenuRepo; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.constraints.NotNull; +import java.util.*; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * 菜单管理 + */ +@Service +@Validated +@Transactional(readOnly = true) +public class +MenuService { + private final MenuRepo menuRepo; + + @Autowired + public MenuService(MenuRepo menuRepo) { + this.menuRepo = menuRepo; + } + + /** + * 新增菜单 + * + * @param menu 菜单 + * @return 新增的菜单 + */ + @Transactional + public Menu addMenu(@NonNull @Validated(value = Menu.Add.class) Menu menu) { + menu.setMenuId(null); + if (StringUtils.isEmpty(menu.getMenuCode())) { + throw new BusinessError("菜单代码不能为空"); + } + if (menuRepo.countByMenuCode(menu.getMenuCode()) > 0) { + throw new BusinessError("菜单代码已经存在"); + } + Optional.ofNullable(menu.getParent()).filter(m -> m.getMenuId() != null) + .flatMap(m -> menuRepo.findById(m.getMenuId())) + .ifPresent(menu::setParent); + Menu save = menuRepo.save(menu); + save.getParent().setLeaf(false); + return save; + } + + /** + * 修改菜单 + * + * @param menu 菜单 + */ + @Transactional + public void modifyMenu(@NotNull @Validated(Menu.Modify.class) Menu menu) { + if (menu.getMenuId() == null) { + throw new BusinessError("菜单id为空"); + } + menuRepo.findById(menu.getMenuId()) + .map(m -> { + BeanUtils.copyNoNullProperties(menu, m); + return m; + }) + .orElseThrow(() -> new BusinessError("该ID的菜单不存在")); + } + + /** + * 删除菜单 + * + * @param menuId 菜单id + */ + @Transactional + public void removeMenu(String menuId) { + menuRepo.findById(menuId).ifPresent(menu -> { + menuRepo.deleteById(menu.getMenuId()); + menuRepo.flush(); + menu.getParent().setLeaf(menu.getParent().getChildren().isEmpty()); + }); + } + + /** + * 批量删除菜单 + * + * @param menuIds 菜单ID集合 + */ + @Transactional + public void removeMenus(Set menuIds) { + menuRepo.findAllById(menuIds) + .stream() + .peek(menuRepo::delete) + .reduce((a, b) -> a.getParent()) + .ifPresent(p -> { + menuRepo.flush(); + p.setLeaf(p.getChildren().isEmpty()); + }); + } + + /** + * 查询菜单分页 + * + * @param page 页码 + * @param pagesize 每页数 + * @param sort 排序列 + * @param dir 升降序 + * @param menuId 菜单id + * @param menuCode 菜单代码 + * @param menuName 菜单名称 + * @param path 菜单路径 + * @param parent 上级菜单 + * @return 菜单信息分页 + */ + public Page pageMenus(Integer page, Integer pagesize, String sort, String dir, + String menuId, + String menuCode, + String menuName, + String path, + Menu parent + ) { + return menuRepo.findAll(buildQueryCondition(menuId, menuCode, menuName, path, parent), + PageableHelper.buildPageRequest(page, pagesize, sort, dir)) + .map(MenuService::mapListResult); + } + + + private Specification buildQueryCondition( + String menuId, + String menuCode, + String menuName, + String path, + Menu parent) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + if (menuId != null) { + predicates.add(criteriaBuilder.equal(root.get(Menu_.menuId), menuId)); + } + if (!StringUtils.isEmpty(menuCode)) { + predicates.add(criteriaBuilder.equal(root.get(Menu_.menuCode), menuCode)); + } + if (!StringUtils.isEmpty(menuName)) { + predicates.add(criteriaBuilder.like(root.get(Menu_.menuName), menuName + "%")); + } + if (!StringUtils.isEmpty(path)) { + predicates.add(criteriaBuilder.like(root.get(Menu_.path), path + "%")); + } + if (parent != null) { + if (parent.getMenuId() != null) { + predicates.add(criteriaBuilder.equal(root.get(Menu_.parent).get(Menu_.menuId), parent.getMenuId())); + } + } + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }); + } + + /** + * 获取单个菜单 + * + * @param menuId 菜单ID + * @return 菜单 + */ + public Optional findOne(String menuId) { + return menuRepo.findById(menuId); + } + + /** + * 查询菜单不分页 + * + * @param menuId 菜单id + * @param menuCode 菜单代码 + * @param menuName 菜单名称 + * @param path 菜单路径 + * @param parent 上级菜单 + * @return 菜单列表 + */ + public List listMenus(String menuId, + String menuCode, + String menuName, + String path, + Menu parent) { + return menuRepo.findAll(buildQueryCondition(menuId, menuCode, menuName, path, parent)).stream().map(MenuService::mapListResult).collect(Collectors.toList()); + } + + public Set listMenusWithIds(Set ids) { + return menuRepo.findSimpleByMenuIdIn(ids).stream().map(m -> { + Menu newMenu = new Menu(); + BeanUtils.copyProperties(m, newMenu, Menu_.PARENT, Menu_.CHILDREN); + return newMenu; + }).collect(Collectors.toSet()); + } + + public Set treeMenus(String menuId, + String menuCode, + String menuName, + String path, + Menu parent) { + return menuRepo.findAll(buildQueryCondition(menuId, menuCode, menuName, path, parent), "menus-tree").stream().map(this::buildTreeMenus).collect(Collectors.toSet()); + } + + public Set treeMenus(Set menuIds) { + Set menus = menuRepo.findByMenuIdIn(menuIds); + Comparator comparator = Comparator.comparing(m -> m.getParent().getMenuId(), Comparator.naturalOrder()); + comparator = comparator.thenComparing(Menu::getOrderNum); + Menu root = new Menu(); + root.setMenuId("0"); + root.setMenuCode("root"); + root.setMenuName("主菜单"); + root.setOrderNum(0); + + menus.stream() + .filter(m -> m.getParent() == null) + .forEach(m -> m.setParent(root)); + + root.setChildren(buildMenus(menus, root, comparator)); + return root.getChildren(); + } + + /** + * 构建菜单树(递归子方法) + * + * @param menus 子菜单 + * @param parent 父级菜单 + * @param comparator 排序器 + * @return 排序和树形处理后的子菜单 + */ + private Set buildMenus(Set menus, Menu parent, Comparator comparator) { + Map> collect = menus.stream().sorted(comparator) + .collect(Collectors.partitioningBy(m -> m.getParent().equals(parent), + Collector.of(LinkedHashSet::new, HashSet::add, (a, b) -> { + a.addAll(b); + return a; + }))); + + return collect.get(true).stream() + .map(m -> { + Menu newMenu = new Menu(); + BeanUtils.copyProperties(m, newMenu, Menu_.PARENT, Menu_.CHILDREN); + Menu prt = new Menu(); + prt.setMenuId(m.getParent().getMenuId()); + prt.setMenuCode(m.getParent().getMenuCode()); + prt.setMenuName(m.getParent().getMenuName()); + prt.setPath(m.getParent().getPath()); + prt.setIconCls(m.getParent().getIconCls()); + prt.setOrderNum(m.getParent().getOrderNum()); + newMenu.setParent(prt); + + Menu nMenu = new Menu(); + BeanUtils.copyProperties(m, nMenu, Menu_.PARENT, Menu_.CHILDREN); + nMenu.setParent(nMenu); + newMenu.getChildren().addAll(buildMenus(collect.get(false), nMenu, comparator)); + return newMenu; + }) + .collect(LinkedHashSet::new, + HashSet::add, + AbstractCollection::addAll); + } + + private Menu buildTreeMenus(Menu menu) { + Menu mm = new Menu(); + BeanUtils.copyProperties(menu, mm, Menu_.PARENT, Menu_.CHILDREN); + Menu parent = new Menu(); + BeanUtils.copyProperties(menu.getParent(), parent, Menu_.PARENT, Menu_.CHILDREN); + mm.setParent(parent); + if (!menu.getLeaf()) { + mm.getChildren().addAll(menu.getChildren().stream().map(this::buildTreeMenus).collect(Collectors.toSet())); + } + return mm; + } + + public static Menu mapListResult(Menu menu) { + Menu m = new Menu(); + BeanUtils.copyProperties(menu, m); + BeanUtils.copyProperties(menu, m, Menu_.PARENT, Menu_.CHILDREN); + Menu parent = new Menu(); + Optional.ofNullable(menu.getParent()).ifPresent(p -> { + BeanUtils.copyProperties(p, parent, Menu_.PARENT, Menu_.CHILDREN); + m.setParent(parent); + }); + return m; + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/OperateLogService.java b/server/src/main/java/com/aisino/iles/core/service/OperateLogService.java new file mode 100644 index 0000000..1710cfa --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/OperateLogService.java @@ -0,0 +1,62 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.model.OperateLog; +import com.aisino.iles.core.model.OperateLog_; +import com.aisino.iles.core.model.query.OperateLogQuery; +import com.aisino.iles.core.repository.OperateLogRepo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.criteria.Predicate; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * 操作日志服务 + * + * @author huxin + * @since 2020-10-13 + */ +@Service +@Slf4j +public class OperateLogService { + private final OperateLogRepo operateLogRepo; + + public OperateLogService(OperateLogRepo operateLogRepo) { + this.operateLogRepo = operateLogRepo; + } + + /** + * 获取操作日志信息(分页) + * + * @param query 操作日志查询条件 + * @return 分页的操作日志信息 + */ + @Transactional(readOnly = true) + public Page findOperateLogForPage(OperateLogQuery query) { + return operateLogRepo.findAll(Specification.where((root, q, cb) -> cb.and(Stream.of( + Optional.ofNullable(query.getStatus()).map(f -> cb.equal(root.get(OperateLog_.status), f)), + Optional.ofNullable(query.getOperateType()).map(f -> cb.equal(root.get(OperateLog_.operateType), f)), + Optional.ofNullable(query.getIp()).filter(StringUtils::isNotEmpty).map(f -> cb.like(root.get(OperateLog_.ip), f + "%")), + Optional.ofNullable(query.getIpType()).map(f -> cb.equal(root.get(OperateLog_.ipType), f)) + ).filter(Optional::isPresent) + .map(Optional::get) + .toArray(Predicate[]::new))), PageableHelper.buildPageRequest(query.page(), query.pageSize(), query.sort(), query.dir())); + } + + /** + * 获取操作日志详细信息,通过操作日志主键 + * + * @param operateLogId 操作日志主键 + * @return 可能为空的操作日志信息信息 + */ + @Transactional(readOnly = true) + public Optional findOperateLogDetailById(String operateLogId) { + return operateLogRepo.findById(operateLogId, "operate-log-detail"); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/PermissionService.java b/server/src/main/java/com/aisino/iles/core/service/PermissionService.java new file mode 100644 index 0000000..625345d --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/PermissionService.java @@ -0,0 +1,148 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.*; +import com.aisino.iles.core.repository.PermissionRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.constraints.NotNull; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 权限 + */ +@Service +@Validated +@Transactional(readOnly = true, propagation = Propagation.REQUIRED) +public class PermissionService { + private final PermissionRepo permissionRepo; + + @Autowired + public PermissionService(PermissionRepo permissionRepo) { + this.permissionRepo = permissionRepo; + } + + @Transactional + public Permission addPermission(@NotNull @Validated(Permission.Add.class) Permission permission) { + return permissionRepo.save(permission); + } + + @Transactional + public void modifyPermission(@NotNull @Validated(Permission.Modify.class) Permission permission) { + permissionRepo.findById(permission.getPermissionId()) + .map(p -> { + BeanUtils.copyNoNullProperties(permission, p, Permission_.USERS, Permission_.MENUS, Permission_.FUNCTIONS, Permission_.ROLES, Permission_.RESOURCES); +// p.getRoles().addAll(permission.getRoles()); +// p.getRoles().removeIf(r -> !permission.getRoles().contains(r)); + p.getFunctions().addAll(permission.getFunctions()); + p.getFunctions().removeIf(f -> !permission.getFunctions().contains(f)); + p.getResources().addAll(permission.getResources()); + p.getResources().removeIf(r -> !permission.getResources().contains(r)); +// p.getUsers().addAll(permission.getUsers()); +// p.getUsers().removeIf(u -> !permission.getUsers().contains(u)); + p.getMenus().addAll(permission.getMenus()); + p.getMenus().removeIf(m -> !permission.getMenus().contains(m)); + return p; + }) + .orElseThrow(() -> new BusinessError("不存在该ID的权限")); + } + + @Transactional + public void removePermission(String permissionId) { + permissionRepo.deleteById(permissionId); + } + + /** + * 批量删除权限信息 + * + * @param permissionIds 权限信息ID集合 + */ + @Transactional + public void removePermissions(Set permissionIds) { + permissionRepo.findAllById(permissionIds).forEach(permissionRepo::delete); + } + + public Page listPermissions(Integer page, Integer pageSize, String sort, String dir, + String permissionId, + String name, + String description, + Resource resource, Menu menu, Function function) { + return permissionRepo.findAll(buildQueryCondition(permissionId, name, description, resource, menu, function), + PageableHelper.buildPageRequest(page, pageSize, sort, dir)) + .map(PermissionService::mapListPermission); + } + + public Optional findOnePermission(String permissionId) { + return permissionRepo.findById(permissionId).map(permission -> { + Permission p = new Permission(); + BeanUtils.copyProperties(permission, p, Permission_.USERS, Permission_.MENUS, Permission_.FUNCTIONS, Permission_.ROLES, Permission_.RESOURCES); +// p.getUsers().addAll(permission.getUsers().stream().map(UserService::mapListResultUser).collect(Collectors.toSet())); + p.getMenus().addAll(permission.getMenus().stream().map(MenuService::mapListResult).collect(Collectors.toSet())); +// p.getRoles().addAll(permission.getRoles().stream().map(r -> Role.builder().roleId(r.getRoleId()) +// .roleName(r.getRoleName()) +// .description(r.getDescription()) +// .build()).collect(Collectors.toSet())); + p.getFunctions().addAll(permission.getFunctions()); + p.getResources().addAll(permission.getResources()); + return p; + }); + } + + private Specification buildQueryCondition(String permissionId, + String name, + String description, Resource resource, Menu menu, Function function) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + Optional.ofNullable(permissionId).ifPresent(f -> predicates.add(criteriaBuilder.equal(root.get(Permission_.permissionId), f))); + Optional.ofNullable(name).ifPresent(f -> predicates.add(criteriaBuilder.like(root.get(Permission_.name), "%" + f + "%"))); + Optional.ofNullable(description).ifPresent(f -> predicates.add(criteriaBuilder.like(root.get(Permission_.description), "%" + f + "%"))); + Optional.ofNullable(resource).ifPresent(r -> { + Join rs = root.join(Permission_.resources); + Optional.ofNullable(r.getResourcePath()).filter(StringUtils::isNotEmpty).ifPresent(f -> predicates.add(criteriaBuilder.equal(rs.get(Resource_.resourcePath), f))); + }); + Optional.ofNullable(menu).ifPresent(m -> { + Join ms = root.join(Permission_.menus); + Optional.ofNullable(m.getMenuCode()).filter(StringUtils::isNotEmpty).ifPresent(f -> predicates.add(criteriaBuilder.equal(ms.get(Menu_.menuCode), f))); + Optional.ofNullable(m.getMenuName()).filter(StringUtils::isNotEmpty).ifPresent(f -> predicates.add(criteriaBuilder.like(ms.get(Menu_.menuName), f + "%"))); + }); + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }); + } + + public List listPermissions(String permissionId, + String name, + String description, + Resource resource, Menu menu, Function function) { + return permissionRepo.findAll(buildQueryCondition(permissionId, name, description, resource, menu, function)).stream().map(PermissionService::mapListPermission).collect(Collectors.toList()); + } + + static Permission mapListPermission(Permission permission) { + return Permission.builder().permissionId(permission.getPermissionId()) + .name(permission.getName()) + .description(permission.getDescription()) + .build(); + } + + static Permission mapPermissionDetail(Permission permission) { + return Permission.builder().permissionId(permission.getPermissionId()) + .name(permission.getName()) + .description(permission.getDescription()) + .functions(new HashSet<>(permission.getFunctions())) + .menus(new HashSet<>(permission.getMenus())) + .roles(new HashSet<>(permission.getRoles())) + .resources(new HashSet<>(permission.getResources())) + .build(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/ResourceService.java b/server/src/main/java/com/aisino/iles/core/service/ResourceService.java new file mode 100644 index 0000000..077b26f --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/ResourceService.java @@ -0,0 +1,139 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.Resource; +import com.aisino.iles.core.model.Resource_; +import com.aisino.iles.core.model.enums.Action; +import com.aisino.iles.core.repository.ResourceRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * 资源 + */ +@Service +@Validated +@Transactional(readOnly = true) +public class ResourceService { + private final ResourceRepo resourceRepo; + + @Autowired + public ResourceService(ResourceRepo resourceRepo) { + this.resourceRepo = resourceRepo; + } + + /** + * 添加资源 + * + * @param resource 资源 + * @return 添加的资源 + */ + @Transactional + public Resource addResource(@NotNull @Validated(Resource.Add.class) Resource resource) { + return resourceRepo.save(resource); + } + + /** + * 修改资源 + * + * @param resource 资源 + */ + @Transactional + public void modifyResource(@NotNull @Validated(Resource.Modify.class) Resource resource) { + resourceRepo.findById(resource.getResourceId()) + .map(r -> { + BeanUtils.copyNoNullProperties(resource, r); + return resourceRepo.save(r); + }) + .orElseThrow(() -> new BusinessError("不存在该ID的资源")); + } + + /** + * 删除资源 + * + * @param resourceId 资源ID + */ + @Transactional + public void removeResource(String resourceId) { + resourceRepo.deleteById(resourceId); + } + + /** + * 查询资源信息分页 + * + * @param page 页码 + * @param pagesize 每页数 + * @param sort 排序列 + * @param dir 升降序 + * @param resourceId 资源ID + * @param action 资源方法类型 + * @param resourcePath 资源匹配路径 + * @param description 资源说明 + * @return 分页的资源信息 + */ + public Page listResources(Integer page, Integer pagesize, String sort, String dir, + String resourceId, + Action action, + String resourcePath, + String description) { + return resourceRepo.findAll(buildQueryCondition(resourceId, action, resourcePath, description), + PageableHelper.buildPageRequest(page, pagesize, sort, dir)); + } + + private Specification buildQueryCondition(String resourceId, + Action action, + String resourcePath, String description) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.and(Stream.of( + Optional.ofNullable(resourceId).filter(StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(Resource_.resourceId), f)), + Optional.ofNullable(action).map(f -> criteriaBuilder.equal(root.get(Resource_.action), f)), + Optional.ofNullable(resourcePath).filter(StringUtils::isNotEmpty).map(f -> criteriaBuilder.like(root.get(Resource_.resourcePath), "%" + f + "%")), + Optional.ofNullable(description).filter(StringUtils::isNotEmpty).map(f -> criteriaBuilder.like(root.get(Resource_.description), "%" + f + "%")) + ).filter(Optional::isPresent) + .map(Optional::get) + .toArray(Predicate[]::new))); + } + + /** + * 获取单个资源信息 + * + * @param resourceId 资源ID + * @return 单个资源信息 + */ + public Optional findOneResource(String resourceId) { + return resourceRepo.findById(resourceId); + } + + /** + * 查询资源列表不分页 + * + * @param resourceId 资源ID + * @param action 资源方法类型 + * @param resourcePath 资源匹配路径 + * @param description 资源说明 + * @return 资源列表 + */ + public List listResources(String resourceId, + Action action, + String resourcePath, String description) { + return resourceRepo.findAll(buildQueryCondition(resourceId, action, resourcePath, description)); + } + + @Transactional + public void removeResources(Set resourceIds) { + resourceIds.forEach(resourceRepo::deleteById); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/RoleService.java b/server/src/main/java/com/aisino/iles/core/service/RoleService.java new file mode 100644 index 0000000..27be037 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/RoleService.java @@ -0,0 +1,128 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.Role; +import com.aisino.iles.core.model.Role_; +import com.aisino.iles.core.repository.RoleRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 角色 + */ +@Service +@Validated +@Transactional(readOnly = true) +public class RoleService { + private final RoleRepo roleRepo; + + @Autowired + public RoleService(RoleRepo roleRepo) { + this.roleRepo = roleRepo; + } + + @Transactional + public Role addRole(@NotNull(message = "角色不能为空") @Validated(Role.Add.class) Role role) { + return RoleService.mapRoleForList(roleRepo.save(role)); + } + + @Transactional + public void modifyRole(@NotNull(message = "角色不能为空") @Validated(Role.Modify.class) Role role) { + roleRepo.findById(role.getRoleId()) + .map(r -> { + BeanUtils.copyNoNullProperties(role, r, Role_.USERS, Role_.PERMISSIONS); +// r.getUsers().addAll(role.getUsers()); +// r.getUsers().removeIf(u -> !role.getUsers().contains(u)); + r.getPermissions().addAll(role.getPermissions()); + r.getPermissions().removeIf(p -> !role.getPermissions().contains(p)); + return roleRepo.save(r); + }) + .orElseThrow(() -> new BusinessError("不存在该ID的角色")); + } + + @Transactional + public void removeRole(String roleId) { + roleRepo.deleteById(roleId); + } + + @Transactional + public void removeRoles(Set roleIds) { + roleRepo.deleteAll(roleRepo.findAllById(roleIds)); + } + + public List listRoles(String roleId, String roleName, String description) { + return roleRepo.findAll(buildQueryCondition(roleId, roleName, description)).stream().map(RoleService::mapRoleForList).collect(Collectors.toList()); + } + + public Page pageRoles(Integer page, Integer pageSize, String sort, String dir, String roleId, String roleName, String description) { + return roleRepo.findAll(buildQueryCondition(roleId, roleName, description), + PageableHelper.buildPageRequest(page, pageSize, sort, dir) + ).map(RoleService::mapRoleForList); + } + + private Specification buildQueryCondition(String roleId, String roleName, String description) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.and(Stream.of( + Optional.ofNullable(roleId).filter(StringUtils::isNotEmpty) + .map(f -> criteriaBuilder.equal(root.get(Role_.roleId), f)), + Optional.ofNullable(roleName).filter(StringUtils::isNotEmpty) + .map(f -> criteriaBuilder.like(root.get(Role_.roleName), "%" + f + "%")), + Optional.ofNullable(description).filter(StringUtils::isNotEmpty) + .map(f -> criteriaBuilder.like(root.get(Role_.description), "%" + f + "%")) + ).filter(Optional::isPresent) + .map(Optional::get) + .toArray(Predicate[]::new))); + } + + /** + * 通过角色ID,获取单个角色信息 + * + * @param roleId 角色ID + * @return 可选角色信息 + */ + public Optional findOneById(String roleId) { + return roleRepo.findById(roleId).map(RoleService::mapRoleResultForFindOne); + } + + /** + * 列表结果 + * + * @param r 角色 + * @return 转换后角色 + */ + public static Role mapRoleForList(Role r) { + return Role.builder().roleId(r.getRoleId()) + .roleName(r.getRoleName()) + .description(r.getDescription()) + .build(); + } + + /** + * 角色详情结果 + * + * @param r 角色 + * @return 转换后角色 + */ + public static Role mapRoleResultForFindOne(Role r) { + return Role.builder().roleId(r.getRoleId()) + .roleName(r.getRoleName()) + .description(r.getDescription()) +// .users(r.getUsers().stream().map(UserService::mapListResultUser).collect(Collectors.toSet())) + .permissions(r.getPermissions().stream().map(PermissionService::mapPermissionDetail).collect(Collectors.toSet())) + .build(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/StreetInfoService.java b/server/src/main/java/com/aisino/iles/core/service/StreetInfoService.java new file mode 100644 index 0000000..8c2aa4a --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/StreetInfoService.java @@ -0,0 +1,43 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.core.model.StreetInfo; +import com.aisino.iles.core.model.StreetInfo_; +import com.aisino.iles.core.model.query.AddrQuery; +import com.aisino.iles.core.repository.StreetInfoRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class StreetInfoService { + + private final StreetInfoRepo streetInfoRepo; + + @Autowired + public StreetInfoService(StreetInfoRepo streetInfoRepo){ + this.streetInfoRepo = streetInfoRepo; + } + + public Page pageStreets(AddrQuery query) { + return streetInfoRepo.findAll(buildStreetQueryCondition(query), + PageableHelper.buildPageRequest(query.page(), query.pageSize(), query.sort(), query.dir())); + } + + private Specification buildStreetQueryCondition(AddrQuery query) { + return Specification.where((root, criteriaQuery, criteriaBuilder)->{ + List predicates = new ArrayList<>(); + Optional.ofNullable(query.getDm()) + .map(f->criteriaBuilder.like(root.get(StreetInfo_.DM),query.getDm() + "%")).ifPresent(predicates::add); + Optional.ofNullable(query.getMc()) + .map(f->criteriaBuilder.like(root.get(StreetInfo_.MC),query.getMc() + "%")).ifPresent(predicates::add); + return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()])); + }); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/UserService.java b/server/src/main/java/com/aisino/iles/core/service/UserService.java new file mode 100644 index 0000000..46676b5 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/UserService.java @@ -0,0 +1,350 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.Constants; +import com.aisino.iles.common.iface.Logger; +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.Md5Utils; +import com.aisino.iles.common.util.PageableHelper; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.exception.BusinessError; +import com.aisino.iles.core.model.*; +import com.aisino.iles.core.model.dto.UserDTO; +import com.aisino.iles.core.model.enums.UserStatus; +import com.aisino.iles.core.model.query.UserQuery; +import com.aisino.iles.core.repository.DictItemRepo; +import com.aisino.iles.core.repository.UserRepo; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.SetJoin; +import jakarta.validation.constraints.NotNull; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Validated +@Transactional(readOnly = true) +public class UserService implements Logger { + private final UserRepo userRepo; + private static DictItemRepo dictItemRepo; + + public UserService(UserRepo userRepo, DictItemRepo dictItemRepo) { + this.userRepo = userRepo; + UserService.dictItemRepo = dictItemRepo; + } + + /** + * 获取用户分页 + * + * @param page 页码 + * @param pagesize 每页数 + * @param sort 排序列名称 + * @param dir 升降序 + * @param total 总记录数 + * @param query 查询条件 + * @return 已经分页的用户信息 + */ + public Page pageUsers(Integer page, + Integer pagesize, + String sort, + String dir, + Long total, + UserQuery query) { + + return userRepo.findAll(buildQueryCondition(query), + PageableHelper.buildPageRequest(page, pagesize, sort, dir), + total, + "" + ).map(UserService::mapListResultUser); + } + + /** + * 保存用户 + * + * @param user 用户 + * @return 新增的用户信息 + */ + @Transactional + public User saveUser(@NotNull User user) { + if (user.getUserId() != null) { + throw new BusinessError("新增用户id不能有值"); + } +// user.getUserTypes().forEach(userType -> userType.setUser(user)); + Optional.ofNullable(user.getIdNum()) + .flatMap(idn -> userRepo.findByIdNumAndStatusIn(idn, new UserStatus[]{UserStatus.normal, UserStatus.lock, UserStatus.unnormal})) + .ifPresent(ex -> { + throw new BusinessError("该身份证号码的用户存在"); + }); + User saveUser = userRepo.save(user); + + return UserService.mapListResultUser(saveUser); + } + + /** + * 用户修改 + * + * @param user 修改的用户信息 + */ + @Transactional + public void modifyUser(@NotNull User user) { + if (StringUtils.isEmpty(user.getUserId())) { + throw new BusinessError("用户ID为空"); + } + userRepo.findById(user.getUserId()) + .map(u -> { + BeanUtils.copyNoNullProperties(user, u, User_.USER_TYPES, User_.JURISDICTIONS, User_.ROLES, User_.PERMISSIONS); + u.getUserTypes().addAll(user.getUserTypes()); + u.getUserTypes().removeIf(userType -> !user.getUserTypes().contains(userType)); + u.getJurisdictions().addAll(user.getJurisdictions()); + u.getJurisdictions().removeIf(jurisdiction -> !user.getJurisdictions().contains(jurisdiction)); + u.getPermissions().addAll(user.getPermissions()); + u.getPermissions().removeIf(p -> !user.getPermissions().contains(p)); + u.getRoles().addAll(user.getRoles()); + u.getRoles().removeIf(r -> !user.getRoles().contains(r)); + return u; + }) + .orElseThrow(() -> new BusinessError("该ID的用户不存在")); + } + + /** + * 修改密码 + * + * @param userDTO 用户dto + */ + @Transactional + public void modifyPassword(@NotNull UserDTO userDTO) { + if (StringUtils.isEmpty(userDTO.getUserId())) { + throw new BusinessError("用户ID为空"); + } + if (!(Objects.equals(userDTO.getNewpassword(), userDTO.getConfirmpassword()))) { + throw new BusinessError("两次密码输入不一致"); + } + userRepo.findById(userDTO.getUserId()).map(u -> { + if (!(Objects.equals(u.getPassword(), userDTO.getOldpassword()))) { + throw new BusinessError("原密码输入错误"); + } + if (Objects.equals(u.getPassword(), userDTO.getNewpassword())) { + throw new BusinessError("新密码不能和原密码相同"); + } + u.setPassword(userDTO.getNewpassword()); + return userRepo.save(u); + }).orElseThrow(() -> new BusinessError("该用户不存在")); + } + + /** + * 用户删除(非物理删除) + * + * @param userId 用户ID + */ + @Transactional + public void removeUser(String userId) { + userRepo.findById(userId) + .map(u -> { + u.setStatus(UserStatus.deleted); + return userRepo.save(u); + }) + .orElseThrow(() -> new BusinessError("该ID的用户不存在")); + } + + /** + * 用户批量删除( 非物理删除) + * + * @param userIds 用户id集合 + */ + @Transactional + public void removeUsers(Set userIds) { + userRepo.findAllById(userIds) + .forEach(u -> u.setStatus(UserStatus.deleted)); + } + + private Specification buildQueryCondition(@NotNull UserQuery query) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + Optional.ofNullable(query.getUsername()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty) + .map(p -> criteriaBuilder.equal(root.get(User_.username), p)) + .ifPresent(predicates::add); + Optional.ofNullable(query.getStatus()).map(p -> criteriaBuilder.equal(root.get(User_.status), p)).ifPresent(predicates::add); + Optional.ofNullable(query.getEmail()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(p -> criteriaBuilder.equal(root.get(User_.email), p)).ifPresent(predicates::add); + Optional.ofNullable(query.getMobilePhone()).filter(com.aisino.iles.common.util.StringUtils::isNotEmpty).map(p -> criteriaBuilder.like(root.get(User_.mobilePhone), p + "%")).ifPresent(predicates::add); + Optional.ofNullable(query.getUserType()).map(p -> criteriaBuilder.equal(root.join(User_.userTypes).get(UserType_.userTypeId), p.getUserTypeId())).ifPresent(predicates::add); + Optional.ofNullable(query.getNickName()).filter(StringUtils::isNotEmpty).map(p -> criteriaBuilder.like(root.get(User_.nickName), "%" + p + "%")).ifPresent(predicates::add); + Optional.ofNullable(query.getUserId()).map(p -> criteriaBuilder.equal(root.get(User_.userId), p)).ifPresent(predicates::add); + Optional.ofNullable(query.getJurisdiction()).map(p -> criteriaBuilder.equal(root.join(User_.jurisdictions).get(Jurisdiction_.jurisdictionId), p.getJurisdictionId())).ifPresent(predicates::add); + Optional.ofNullable(query.getIdNum()).filter(StringUtils::isNotEmpty).map(p -> criteriaBuilder.equal(root.get(User_.idNum), p)).ifPresent(predicates::add); + Optional.ofNullable(query.getUserIcon()).filter(StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(User_.userIcon), f)).ifPresent(predicates::add); + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }); + } + + /** + * 获取单个用户 + * + * @param userId 用户id + * @return 可能存在的用户 + */ + public Optional findOneUser(String userId) { + return userRepo.findById(userId).map(UserService::mapResultUser); + } + + public List listUsers(UserQuery query) { + return userRepo.findAll(buildQueryCondition(query)).stream().map(UserService::mapListResultUser).collect(Collectors.toList()); + } + + public static User mapListResultUser(User u) { + User user = new User(); + dictItemRepo.findByDictDictCodeAndValue(Constants.Dicts.userStatus, u.getStatus().getValue()).ifPresent(f -> u.setStatusName(f.getDisplay())); + BeanUtils.copyProperties(u, user, User_.PERMISSIONS, User_.JURISDICTIONS, User_.USER_TYPES, User_.ROLES); + return user; + } + + public static User mapResultUser(User u) { + User user = mapListResultUser(u); + user.setJurisdictions(u.getJurisdictions().stream().map(JurisdictionService::mapResultJurisdiction).collect(Collectors.toSet())); + user.setRoles(u.getRoles().stream().map(r -> Role.builder() + .roleId(r.getRoleId()) + .roleName(r.getRoleName()) + .description(r.getDescription()) + .permissions(new HashSet<>(r.getPermissions())) + .build()).collect(Collectors.toSet())); + user.setPermissions(u.getPermissions().stream().map(p -> Permission.builder() + .menus(new HashSet<>(p.getMenus())) + .functions(new HashSet<>(p.getFunctions())) + .resources(new HashSet<>(p.getResources())) + .description(p.getDescription()) + .permissionId(p.getPermissionId()) + .name(p.getName()) + .build()).collect(Collectors.toSet())); + user.getUserTypes().addAll(u.getUserTypes().stream().map(ut -> UserType.builder() + .userTypeId(ut.getUserTypeId()) + .typeName(ut.getTypeName()) + .typeCode(ut.getTypeCode()) + .businessCategory(ut.getBusinessCategory()) + .businessCategoryName(ut.getBusinessCategoryName()) + .hylb(ut.getHylb()) + .hylbdm(ut.getHylbdm()) + .build()).collect(Collectors.toSet())); + return user; + } + + /** + * 针对获取当前用户信息 + * + * @param userId 用户信息主键 + * @return 用户信息 + */ + public Optional findOneUserForCurrentUser(String userId) { + return userRepo.findById(userId) + .map(u -> { + User user = mapListResultUser(u); + user.getJurisdictions().addAll(u.getJurisdictions().stream().map(JurisdictionService::mapResultJurisdiction).collect(Collectors.toSet())); + user.getUserTypes().addAll(u.getUserTypes().stream().map(ut -> UserType.builder() + .userTypeId(ut.getUserTypeId()) + .typeName(ut.getTypeName()) + .typeCode(ut.getTypeCode()) + .businessCategory(ut.getBusinessCategory()) + .businessCategoryName(ut.getBusinessCategoryName()) + .hylb(ut.getHylb()) + .hylbdm(ut.getHylbdm()) + .build()).collect(Collectors.toSet())); + user.getPermissions().addAll(u.getPermissions().stream().map(p -> Permission.builder() + .functions(new HashSet<>(p.getFunctions())) + .resources(new HashSet<>(p.getResources())) + .description(p.getDescription()) + .permissionId(p.getPermissionId()) + .name(p.getName()) + .build()).collect(Collectors.toSet())); + user.getRoles().addAll(u.getRoles().stream().map(r -> Role.builder() + .roleId(r.getRoleId()) + .roleName(r.getRoleName()) + .description(r.getDescription()) + .permissions(r.getPermissions().stream().map(p -> Permission.builder() + .functions(new HashSet<>(p.getFunctions())) + .resources(new HashSet<>(p.getResources())) + .description(p.getDescription()) + .permissionId(p.getPermissionId()) + .name(p.getName()) + .build()).collect(Collectors.toSet())) + .build()).collect(Collectors.toSet())); + return u; + }); + } + + /** + * 用户统计 + * + */ + public List statistics() { + List userStatistics = new ArrayList<>(); + + UserStatistics statistics = new UserStatistics(); + statistics.setUserTypeName("公安用户"); + statistics.setUserType("police"); + statistics.setUserTotal(userRepo.count(statisticsCondition(Constants.UserTypeCategory.POLICE))); + userStatistics.add(statistics); + + statistics = new UserStatistics(); + statistics.setUserTypeName("企业用户"); + statistics.setUserType("enterprise"); + statistics.setUserTotal(userRepo.count(statisticsCondition(Constants.UserTypeCategory.ENTERPRISE))); + userStatistics.add(statistics); + + statistics = new UserStatistics(); + statistics.setUserTypeName("门户用户"); + statistics.setUserType("portal"); + statistics.setUserTotal(userRepo.count(statisticsCondition(Constants.UserTypeCategory.PORTAL))); + userStatistics.add(statistics); + return userStatistics; + } + + private Specification statisticsCondition(String tag) { + return Specification.where((root, criteriaQuery, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + SetJoin userType = root.join(User_.userTypes); + if (Constants.UserTypeCategory.POLICE.equals(tag)) { + predicates.add(criteriaBuilder.like(userType.get(UserType_.typeCode), "%" + Constants.UserTypeCategory.POLICE)); + } else if (Constants.UserTypeCategory.ENTERPRISE.equals(tag)) { + predicates.add(criteriaBuilder.like(userType.get(UserType_.typeCode), "%" + Constants.UserTypeCategory.ENTERPRISE)); + } else if (Constants.UserTypeCategory.PORTAL.equals(tag)) { + predicates.add(criteriaBuilder.like(userType.get(UserType_.typeCode), "%" + Constants.UserTypeCategory.PORTAL)); + predicates.add(criteriaBuilder.notLike(userType.get(UserType_.typeCode), "%" + Constants.UserTypeCategory.ENTERPRISE)); + } + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }); + } + + /** + * 重置密码 + * + */ + @Transactional + public void resetPassword(@NotNull UserDTO userDTO) { + if (StringUtils.isEmpty(userDTO.getUserId())) { + throw new BusinessError("用户ID为空"); + } + userRepo.findById(userDTO.getUserId()).map(u -> { + if (StringUtils.isNotEmpty(userDTO.getNewpassword())) + u.setPassword(userDTO.getNewpassword()); + if (StringUtils.isNotEmpty(userDTO.getRegisterMobilePhone())) + u.setRegisterMobilePhone(userDTO.getRegisterMobilePhone()); + return userRepo.save(u); + }).orElseThrow(() -> new BusinessError("该用户不存在")); + } + + + /** + * 获取单个用户 + * + * @param userId 用户id + * @return 可能存在的用户 + */ + public Boolean findUserIsDefaultPassword(String userId) { + return Boolean.valueOf(userRepo.findById(userId).filter(u -> { + return Optional.ofNullable(u.getIdNum()).filter(n -> n.length()==18 && + Md5Utils.getMd5Str("hycs@" + n.substring(12)).equals(u.getPassword())).isPresent(); + }).isPresent()); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/service/UserTypeService.java b/server/src/main/java/com/aisino/iles/core/service/UserTypeService.java new file mode 100644 index 0000000..934d760 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/service/UserTypeService.java @@ -0,0 +1,141 @@ +package com.aisino.iles.core.service; + +import com.aisino.iles.common.util.BeanUtils; +import com.aisino.iles.common.util.StringUtils; +import com.aisino.iles.core.model.UserType; +import com.aisino.iles.core.model.UserType_; +import com.aisino.iles.core.repository.UserTypeRepo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.persistence.criteria.Predicate; +import jakarta.validation.constraints.NotNull; +import java.util.*; + +@Service +@Transactional(readOnly = true) +public class UserTypeService { + private final UserTypeRepo userTypeRepo; + + public UserTypeService(UserTypeRepo userTypeRepo) { + this.userTypeRepo = userTypeRepo; + } + + /** + * 用户类别列表查询 + * + * @param typeCode 类别代码 + * @param typeName 类别名称 + * @param level 用户级别 + * @return 用户类别列表 + */ + public List list(String typeCode, String typeName, String level) { + return userTypeRepo.findAll(buildQueryCondition(typeCode, typeName, level)); + } + + /** + * 用户类别列表查询分页 + * + * @param page 当前页数 + * @param limit 每页显示数 + * @param sort 拍序列 + * @param dir 排序方式 + * @param typeCode 类别代码 + * @param typeName 类别名称 + * @param level 用户级别 + * @return 用户类别列表分页 + */ + public Page page(Integer page, Integer limit, String sort, String dir, String typeCode, String typeName, String level) { + Integer _psize = Optional.ofNullable(limit).filter(f -> f > 0).orElse(20); + Integer _page = Optional.ofNullable(page).filter(f -> f > 0).map(f -> f - 1).orElse(0); + String _sort = Optional.ofNullable(sort).filter(StringUtils::isNotEmpty).orElse(UserType_.USER_TYPE_ID); + String _dir = Optional.ofNullable(dir).filter(d -> Sort.Direction.fromOptionalString(d).isPresent()).orElse("desc"); + + return userTypeRepo.findAll(buildQueryCondition(typeCode, typeName, level), PageRequest.of(_page, _psize, Sort.by(Sort.Direction.fromString(_dir), _sort))); + } + + /** + * 动态查询条件构建 + * + * @param typeCode 类别代码 + * @param typeName 类别名称 + * @param level 用户级别 + * @return 条件 + */ + private Specification buildQueryCondition(String typeCode, String typeName, String level) { + return (root, criteriaQuery, criteriaBuilder) -> { + Set predicates = new LinkedHashSet<>(); + Optional.ofNullable(typeCode).filter(StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(UserType_.typeCode), f)).ifPresent(predicates::add); + Optional.ofNullable(typeName).filter(StringUtils::isNotEmpty).map(f -> criteriaBuilder.like(root.get(UserType_.typeName), "%" + f + "%")).ifPresent(predicates::add); + Optional.ofNullable(level).filter(StringUtils::isNotEmpty).map(f -> criteriaBuilder.equal(root.get(UserType_.level), f)).ifPresent(predicates::add); + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + } + + /** + * 新增用户类别 + * + * @param userType 用户类别数据 + * @return 新增的用户类别 + */ + @Transactional + public UserType save(@Validated(UserType.Add.class) @NotNull UserType userType) { + return userTypeRepo.save(userType); + } + + /** + * 用户类别修改fame + * + * @param userType 用户类别数据 + */ + @Transactional + public void modify(@Validated(UserType.Modify.class) @NotNull UserType userType) { + userTypeRepo.findById(userType.getUserTypeId()).ifPresent(ut -> { + BeanUtils.copyProperties(userType, ut, UserType_.MENUS, UserType_.ROLES); + ut.getMenus().addAll(userType.getMenus()); + ut.getMenus().removeIf(m -> !userType.getMenus().contains(m)); + ut.getRoles().addAll(userType.getRoles()); + ut.getRoles().removeIf(m -> !userType.getRoles().contains(m)); + }); + } + + /** + * 删除用户类别 + * @param userTypeId 用户类别ID + */ + @Transactional + public void remove(@Validated @NotNull String userTypeId) { + userTypeRepo.deleteById(userTypeId); + } + + /** + * 用户类别单个(详细,带所属菜单) + * @param userTypeId 用户类别ID + * @return 用户类别 + */ + public Optional findOne(@Validated @NotNull String userTypeId) { + return userTypeRepo.findById(userTypeId).map(ut-> UserType.builder() + .userTypeId(ut.getUserTypeId()) + .typeCode(ut.getTypeCode()) + .typeName(ut.getTypeName()) + .hylbdm(ut.getHylbdm()) + .hylb(ut.getHylb()) + .level(ut.getLevel()) + .menus(new HashSet<>(ut.getMenus())) + .roles(new HashSet<>(ut.getRoles())) + .build()); + } + + /** + * 获取全部用户类别 + * @return 全部用户类别 + */ + public List all() { + return userTypeRepo.findAll(); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/util/PermissionUtils.java b/server/src/main/java/com/aisino/iles/core/util/PermissionUtils.java new file mode 100644 index 0000000..53503b6 --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/util/PermissionUtils.java @@ -0,0 +1,204 @@ +package com.aisino.iles.core.util; + +import com.aisino.iles.core.exception.ArgError; +import com.aisino.iles.core.exception.TokenError; +import com.aisino.iles.core.json.mixins.FunctionMixin; +import com.aisino.iles.core.json.mixins.MenuMixin; +import com.aisino.iles.core.json.mixins.ResourceMixin; +import com.aisino.iles.core.model.*; +import com.aisino.iles.core.repository.ResourceRepo; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * 权限相关的工具. + */ +public class PermissionUtils { + private static final Log log = LogFactory.getLog(PermissionUtils.class); + private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); + private static final ObjectMapper objectMapper; + + static { + objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false) + .configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, false) + .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false); + objectMapper.addMixIn(Menu.class, MenuMixin.class) + .addMixIn(Resource.class, ResourceMixin.class) + .addMixIn(Function.class, FunctionMixin.class); + } + /** + * 验证是否有资源操作权限 + * + * @param token 令牌 + * @param resourcePath 资源路径 + * @param reqMethod 资源请求方法 + * @param resourceRepo 资源数据服务 + * @return 是否有操作权限 + */ + public static boolean checkHasResourcePermission(String token, String resourcePath, String reqMethod, ResourceRepo resourceRepo) { + boolean ret = false; + assertTokenValid(token); + if (StringUtils.isEmpty(resourcePath)) + throw new ArgError("资源路径不能为空"); + if (StringUtils.isEmpty(reqMethod)) + throw new ArgError("请求类型方法不能为空"); + Token tk = TokenUtil.parseToken(token); + JsonNode jsonProfile; + try { + jsonProfile = objectMapper.readTree(Objects.requireNonNull(tk).getJsonProfile()); + if (!checkJsonPermission(jsonProfile)) + throw new TokenError("令牌不包含权限信息"); + List resources = objectMapper.readValue(jsonProfile.get("permission").get("resources").toString(), new TypeReference>() { + }); + // 单批查询数量 + int batchSize = 30; + IntStream.range(0, resources.size()).forEach(i -> resources.get(i).setGroup(i)); + ret = resources + .stream() + .collect(Collectors.groupingBy(r -> r.getGroup() / batchSize, Collectors.mapping(Resource::getResourceId, Collectors.toList()))) + .entrySet().parallelStream() + .flatMap(es -> resourceRepo.findByResourceIdIn(es.getValue()).stream()) + .anyMatch(r -> { + boolean result = r.getAction().toString().equals(reqMethod.toLowerCase()); + result = result && PATH_MATCHER.match(r.getResourcePath(), resourcePath); + return result; + }); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + + return ret; + + } + + /** + * 验证是否有功能的操作权限 + * + * @param token 令牌 + * @param funcCode 功能代码 + * @return 是否有操作权限 + */ + public static boolean checkHasFunctionPermission(String token, String funcCode) { + assertTokenValid(token); + + if (StringUtils.isEmpty(funcCode)) + throw new ArgError("功能代码不能为空"); + + Token tk = TokenUtil.parseToken(token); + JsonNode jsonProfile; + try { + jsonProfile = objectMapper.readTree(Objects.requireNonNull(tk).getJsonProfile()); + if (!checkJsonPermission(jsonProfile)) + throw new TokenError("令牌不包含权限信息"); + return objectMapper.readValue(jsonProfile.get("permission").get("functions").toString(), new TypeReference>() { + }).stream().anyMatch(f -> funcCode.equals(f.getFuncCode())); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + + return false; + } + + /** + * 验证是否有指定菜单的操作权限 + * + * @param token 令牌 + * @param menuCode 菜单代码 + * @return 是否有操作权限 + */ + public static boolean checkHasMenuPermission(String token, String menuCode) { + assertTokenValid(token); + if (StringUtils.isEmpty(menuCode)) + throw new ArgError("菜单代码不能为空"); + Token tk = TokenUtil.parseToken(token); + JsonNode jsonProfile; + try { + jsonProfile = objectMapper.readTree(Objects.requireNonNull(tk).getJsonProfile()); + if (!checkJsonPermission(jsonProfile)) + throw new TokenError("令牌不包含权限信息"); + return objectMapper.readValue(jsonProfile.get("permission").get("menus").toString(), new TypeReference>() { + }) + .stream().anyMatch(m -> m.getMenuCode().equals(menuCode)); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return false; + } + + /** + * 验证用户是否具有指定的功能权限 + * + * @param user 用户信息 + * @param functionCode 功能代码 + * @return 是否有操作权限 + */ + public static boolean checkHasFunctionPermission(User user, String functionCode) { + assert user != null; + return Stream.of(user.getPermissions().stream(), user.getRoles().stream().flatMap(r -> r.getPermissions().stream())) + .flatMap(p -> p.flatMap(pp -> pp.getFunctions().stream())) + .map(Function::getFuncCode) + .anyMatch(funcCode -> funcCode.equals(functionCode)); + + } + + /** + * 验证json属性的profile对象是否包含权限信息 + * + * @param jsonProfile profile对象 + * @return 是否包含权限信息 + */ + private static boolean checkJsonPermission(JsonNode jsonProfile) { + return jsonProfile.has("permission") && jsonProfile.get("permission").size() > 0; + } + + + /** + * 令牌验证 + * + * @param token 令牌 + */ + private static void assertTokenValid(String token) { + if (StringUtils.isEmpty(token)) + throw new ArgError("令牌不能为空"); + if (!TokenUtil.validateToken(token)) + throw new TokenError("令牌验证失败,无效的令牌"); + } + + /** + * 验证用户类别是否属于某个用户类别分类 + * + * @param userTypeCode 用户类别代码 + * @param userTypeCategoryCodes 用户类别分类代码集合 + * @return 是否属于指定的分类 + */ + public static boolean checkUserTypeCategoryIs(@NonNull String userTypeCode, @NonNull String... userTypeCategoryCodes) { + return Arrays.stream(userTypeCategoryCodes).allMatch(userTypeCode::endsWith); + } + + /** + * 验证用户类别是否不属于指定用户类别分类 + * + * @param userTypeCode 用户类别代码 + * @param userTypeCategoryCodes 用户类别分类代码集合 + * @return 是否不属于指定的分类 + */ + public static boolean checkUserTypeCategoryIsNot(String userTypeCode, String... userTypeCategoryCodes) { + return Arrays.stream(userTypeCategoryCodes).noneMatch(userTypeCode::endsWith); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/util/RedisUtil.java b/server/src/main/java/com/aisino/iles/core/util/RedisUtil.java new file mode 100644 index 0000000..feaa7bd --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/util/RedisUtil.java @@ -0,0 +1,195 @@ +package com.aisino.iles.core.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * redisTemplate 工具类 + *

+ * 使用 StringRedisTemplate 并通过 Jackson 进行序列化/反序列化,以支持对象存储。 + * + * @author hx + * @since 2023-06-12 + * @since 2025-07-01 + */ +@Component +public class RedisUtil { + private final StringRedisTemplate rt; + private final ObjectMapper om; + + private static StringRedisTemplate redisTemplate; + private static ObjectMapper objectMapper; + + public RedisUtil(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) { + this.rt = redisTemplate; + this.om = objectMapper; + } + + @PostConstruct + public void staticInit() { + redisTemplate = rt; + objectMapper = om; + } + + /** + * 将 Object 类型的 key 转换为 String + */ + private static String toKey(Object key) { + return String.valueOf(key); + } + + /** + * 将 Object 类型的 value 序列化为 JSON 字符串 + */ + private static String toJson(Object value) { + if (value == null) { + return null; + } + // 如果值本身就是字符串,则直接返回 + if (value instanceof String) { + return (String) value; + } + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + // 根据项目的异常处理策略进行调整 + throw new RuntimeException("Failed to serialize object to JSON", e); + } + } + + /** + * 将 JSON 字符串反序列化为指定类型的对象 + */ + private static T fromJson(String json, Class returnType) { + if (json == null) { + return null; + } + // 如果返回类型是 String,则直接转换返回 + if (returnType == String.class) { + return returnType.cast(json); + } + try { + return objectMapper.readValue(json, returnType); + } catch (JsonProcessingException e) { + // 根据项目的异常处理策略进行调整 + throw new RuntimeException("Failed to deserialize JSON to object", e); + } + } + + public static boolean hasKey(Object key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(toKey(key))); + } + + public static void lpush(Object key, Object value) { + redisTemplate.opsForList().leftPush(toKey(key), toJson(value)); + } + + public static void rpush(Object key, Object value) { + redisTemplate.opsForList().rightPush(toKey(key), toJson(value)); + } + + public static List lrange(Object key, long start, long end, Class returnType) { + return Optional.ofNullable(redisTemplate.opsForList().range(toKey(key), start, end)) + .map(ol -> ol.stream().map(json -> fromJson(json, returnType)) + .collect(Collectors.toList())) + .orElse(new ArrayList<>()); + } + + public static void set(Object key, Object value, Duration expire) { + redisTemplate.opsForValue().set(toKey(key), toJson(value), expire); + } + + public static void set(Object key, Object value) { + redisTemplate.opsForValue().set(toKey(key), toJson(value)); + } + + public static boolean setnx(Object key, Object value) { + return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(toKey(key), toJson(value))); + } + + public static boolean setnx(Object key, Object value, Duration expire) { + return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(toKey(key), toJson(value), expire)); + } + + public static T get(Object key, Class returnType) { + return Optional.ofNullable(redisTemplate.opsForValue().get(toKey(key))) + .map(json -> fromJson(json, returnType)) + .orElse(null); + } + + /** + * 设置key的过期 + * + * @param key 缓存key + * @param expire 过期时间 + */ + public static void expire(Object key, Duration expire) { + redisTemplate.expire(toKey(key), expire); + } + + /** + * 向哈希表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param hashKey 项 + * @param value 值 + */ + public static void hset(Object key, Object hashKey, Object value) { + redisTemplate.opsForHash().put(toKey(key), String.valueOf(hashKey), toJson(value)); + } + + /** + * 获取哈希表中的值 + * + * @param key 键 + * @param hashKey 项 + * @param returnType 返回值类型 + * @return 值 + */ + public static T hget(Object key, Object hashKey, Class returnType) { + return Optional.ofNullable(redisTemplate.opsForHash().get(toKey(key), String.valueOf(hashKey))) + .map(json -> fromJson(String.valueOf(json), returnType)) + .orElse(null); + } + + /** + * 判断哈希表中是否有该项的值 + * + * @param key 键 + * @param hashKey 项 + * @return true 存在 false不存在 + */ + public static boolean hhasKey(Object key, Object hashKey) { + return redisTemplate.opsForHash().hasKey(toKey(key), String.valueOf(hashKey)); + } + + /** + * 哈希表新增,如果项存在则不修改 + * + * @param key 键 + * @param hashKey 项 + * @param value 值 + * @return true 成功 false失败 + */ + public static boolean hsetnx(Object key, Object hashKey, Object value) { + return redisTemplate.opsForHash().putIfAbsent(toKey(key), String.valueOf(hashKey), toJson(value)); + } + + /** + * 删除缓存 + * + * @param key 缓存key + */ + public static void del(Object key) { + redisTemplate.delete(toKey(key)); + } +} diff --git a/server/src/main/java/com/aisino/iles/core/util/TokenUtil.java b/server/src/main/java/com/aisino/iles/core/util/TokenUtil.java new file mode 100644 index 0000000..d304a4f --- /dev/null +++ b/server/src/main/java/com/aisino/iles/core/util/TokenUtil.java @@ -0,0 +1,125 @@ +package com.aisino.iles.core.util; + +import cn.hutool.core.util.StrUtil; +import com.aisino.iles.core.exception.TokenError; +import com.aisino.iles.core.model.Token; +import com.aisino.iles.core.model.dto.LoginUserDto; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.digest.DigestUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Optional; + +@Component +public class TokenUtil { + private static final String magicWords = "!@#$%^%$#$#"; + private static final String sep = ",;,;"; + private static ObjectMapper objectMapper; + + public TokenUtil(ObjectMapper objectMapper) { + TokenUtil.objectMapper = objectMapper; + } + + public static Boolean validateToken(String token) { + Token token1 = parseToken(token); + if (token1 == null) { + return false; + } else if (generateSign(token1.getUid(), token1.getJsonProfile(), token1.getExpireTime()).equals(token.split("\\.")[1])) { + return LocalDateTime.now().isBefore(token1.getExpireTime().atZone(ZoneId.systemDefault()).toLocalDateTime()); + } else { + return false; + } + } + + public static String generateToken(String uid, String profileJson, Instant expireTime) { + return generateTokenProfile(uid, profileJson, expireTime) + "." + generateSign(uid, profileJson, expireTime); + } + + public static Token parseToken(String token) { + if (!StringUtils.hasLength(token)) { + throw new RuntimeException("令牌为空"); + } else { + String[] ts = token.split("\\."); + if (ts.length == 2) { + String[] tss = (new String(Base64.decodeBase64(ts[0]))).split(sep); + if (tss.length == 3) { + Token token1 = new Token(); + token1.setUid(tss[0]); + token1.setJsonProfile(tss[1]); + token1.setExpireTime(Instant.ofEpochMilli(Long.parseLong(tss[2]))); + return token1; + } + } + + return null; + } + } + + public static String generateTokenProfile(String userId, String profileJson, Instant expireTime) { + if (!StringUtils.hasLength(userId)) { + throw new RuntimeException("用户主键为空"); + } else if (expireTime == null) { + throw new RuntimeException("过期时间为空"); + } else { + if (profileJson == null) { + profileJson = ""; + } + return Base64.encodeBase64String((userId + sep + profileJson + sep + expireTime.toEpochMilli()).getBytes()); + } + } + + public static String generateSign(String userId, String profileJson, Instant expireTime) { + if (!StringUtils.hasLength(userId)) { + throw new RuntimeException("用户主键为空"); + } else if (expireTime == null) { + throw new RuntimeException("过期时间为空"); + } else { + if (profileJson == null) { + profileJson = ""; + } + return DigestUtils.md2Hex(userId + magicWords + profileJson + magicWords + expireTime.toEpochMilli()); + } + } + + public static Optional ipFromToken(String token) { + String all = new String(Base64.decodeBase64(token.split("\\.")[0])).split(sep)[1]; + try { + JsonNode jsonNode = JsonMapper.builder().build().readTree(all); + LoginUserDto u = objectMapper.readValue(jsonNode.findPath("user").toString(), LoginUserDto.class); + return Optional.of(u); + } catch (Exception e) { + e.printStackTrace(); + } + return Optional.empty(); + } + + public static String signFrom(String token) { + return Optional.ofNullable(token) + .map(StrUtil::trimToNull) + .map(t -> t.split("\\.")) + .filter(arr -> arr.length >= 2) + .map(arr -> arr[1]) + .orElseThrow(() -> new TokenError("格式可能不对")); + + } + + public static String signFromKey(String key) { + // aisino-tokens:Auth-Token:-- + return Optional.ofNullable(key) + .map(StrUtil::trimToNull) + .map(s -> s.split(":")) + .filter(arr -> arr.length >= 3) + .map(arr -> arr[2]) + .map(s -> s.split("-")) + .filter(arr -> arr.length > 2) + .map(arr -> arr[1]) + .orElseThrow(() -> new TokenError("格式可能不对")); + } +}